B2B-36: add personal dashboard (#20)
* B2B-36: add dashboard cards * B2B-36: add dashboard cards * card variants, some improvements, gen db types * add menus to home page * update db types * remove unnecessary card variant --------- Co-authored-by: Helena <helena@Helenas-MacBook-Pro.local>
This commit is contained in:
237
app/home/(user)/_components/dashboard.tsx
Normal file
237
app/home/(user)/_components/dashboard.tsx
Normal file
@@ -0,0 +1,237 @@
|
||||
'use client';
|
||||
|
||||
import { InfoTooltip } from '@/components/ui/info-tooltip';
|
||||
import { toTitleCase } from '@/lib/utils';
|
||||
import { BlendingModeIcon, RulerHorizontalIcon } from '@radix-ui/react-icons';
|
||||
import {
|
||||
Activity,
|
||||
ChevronRight,
|
||||
Clock9,
|
||||
Droplets,
|
||||
LineChart,
|
||||
Pill,
|
||||
Scale,
|
||||
TrendingUp,
|
||||
User,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { usePersonalAccountData } from '@kit/accounts/hooks/use-personal-account-data';
|
||||
import { useUserWorkspace } from '@kit/accounts/hooks/use-user-workspace';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardProps,
|
||||
} from '@kit/ui/card';
|
||||
import { PageDescription } from '@kit/ui/page';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import { cn } from '@kit/ui/utils';
|
||||
|
||||
const dummyCards = [
|
||||
{
|
||||
title: 'dashboard:gender',
|
||||
description: 'dashboard:male',
|
||||
icon: <User />,
|
||||
iconBg: 'bg-success',
|
||||
},
|
||||
{
|
||||
title: 'dashboard:age',
|
||||
description: '43',
|
||||
icon: <Clock9 />,
|
||||
iconBg: 'bg-success',
|
||||
},
|
||||
{
|
||||
title: 'dashboard:height',
|
||||
description: '183',
|
||||
icon: <RulerHorizontalIcon className="size-4" />,
|
||||
iconBg: 'bg-success',
|
||||
},
|
||||
{
|
||||
title: 'dashboard:weight',
|
||||
description: '92kg',
|
||||
icon: <Scale />,
|
||||
iconBg: 'bg-warning',
|
||||
},
|
||||
{
|
||||
title: 'dashboard:bmi',
|
||||
description: '27.5',
|
||||
icon: <TrendingUp />,
|
||||
iconBg: 'bg-warning',
|
||||
},
|
||||
{
|
||||
title: 'dashboard:bloodPressure',
|
||||
description: '160/98',
|
||||
icon: <Activity />,
|
||||
iconBg: 'bg-warning',
|
||||
},
|
||||
{
|
||||
title: 'dashboard:cholesterol',
|
||||
description: '5',
|
||||
icon: <BlendingModeIcon className="size-4" />,
|
||||
iconBg: 'bg-destructive',
|
||||
},
|
||||
{
|
||||
title: 'dashboard:ldlCholesterol',
|
||||
description: '3,6',
|
||||
icon: <Pill />,
|
||||
iconBg: 'bg-warning',
|
||||
},
|
||||
{
|
||||
title: 'Score 2',
|
||||
description: 'Normis',
|
||||
icon: <LineChart />,
|
||||
iconBg: 'bg-success',
|
||||
},
|
||||
{
|
||||
title: 'dashboard:smoking',
|
||||
description: 'dashboard:respondToQuestion',
|
||||
descriptionColor: 'text-primary',
|
||||
icon: (
|
||||
<Button size="icon" variant="outline" className="px-2 text-black">
|
||||
<ChevronRight className="size-4 stroke-2" />
|
||||
</Button>
|
||||
),
|
||||
cardVariant: 'gradient-success' as CardProps['variant'],
|
||||
},
|
||||
];
|
||||
|
||||
const dummyRecommendations = [
|
||||
{
|
||||
icon: <BlendingModeIcon className="size-4" />,
|
||||
color: 'bg-cyan/10 text-cyan',
|
||||
title: 'Kolesterooli kontroll',
|
||||
description: 'HDL-kolestrool',
|
||||
tooltipContent: 'Selgitus',
|
||||
price: '20,00 €',
|
||||
buttonText: 'Telli',
|
||||
},
|
||||
{
|
||||
icon: <BlendingModeIcon className="size-4" />,
|
||||
color: 'bg-primary/10 text-primary',
|
||||
title: 'Kolesterooli kontroll',
|
||||
tooltipContent: 'Selgitus',
|
||||
description: 'LDL-Kolesterool',
|
||||
buttonText: 'Broneeri',
|
||||
},
|
||||
{
|
||||
icon: <Droplets />,
|
||||
color: 'bg-destructive/10 text-destructive',
|
||||
title: 'Vererõhu kontroll',
|
||||
tooltipContent: 'Selgitus',
|
||||
description: 'Score-Risk 2',
|
||||
price: '20,00 €',
|
||||
buttonText: 'Telli',
|
||||
},
|
||||
];
|
||||
|
||||
export default function Dashboard() {
|
||||
const userWorkspace = useUserWorkspace();
|
||||
const account = usePersonalAccountData(userWorkspace.user.id);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<h4>
|
||||
<Trans i18nKey={'common:welcome'} />
|
||||
{account?.data?.name ? `, ${toTitleCase(account.data.name)}` : ''}
|
||||
</h4>
|
||||
<PageDescription>
|
||||
<Trans i18nKey={'dashboard:recentlyCheckedDescription'} />:
|
||||
</PageDescription>
|
||||
</div>
|
||||
<div className="grid auto-rows-fr grid-cols-5 gap-3">
|
||||
{dummyCards.map(
|
||||
({
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
iconBg,
|
||||
cardVariant,
|
||||
descriptionColor,
|
||||
}) => (
|
||||
<Card
|
||||
key={title}
|
||||
variant={cardVariant}
|
||||
className="flex flex-col justify-between"
|
||||
>
|
||||
<CardHeader className="items-end-safe">
|
||||
<div
|
||||
className={cn(
|
||||
'flex size-8 items-center-safe justify-center-safe rounded-full text-white',
|
||||
iconBg,
|
||||
)}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardFooter className="flex flex-col items-start">
|
||||
<h5>
|
||||
<Trans i18nKey={title} />
|
||||
</h5>
|
||||
<CardDescription className={descriptionColor}>
|
||||
<Trans i18nKey={description} />
|
||||
</CardDescription>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader className="items-start">
|
||||
<h4>
|
||||
<Trans i18nKey="dashboard:recommendedForYou" />
|
||||
</h4>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{dummyRecommendations.map(
|
||||
(
|
||||
{
|
||||
icon,
|
||||
color,
|
||||
title,
|
||||
description,
|
||||
tooltipContent,
|
||||
price,
|
||||
buttonText,
|
||||
},
|
||||
index,
|
||||
) => {
|
||||
return (
|
||||
<div className="flex justify-between" key={index}>
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
<div
|
||||
className={cn(
|
||||
'flex size-8 items-center-safe justify-center-safe rounded-full text-white',
|
||||
color,
|
||||
)}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
<div>
|
||||
<div className="inline-flex items-center gap-1 align-baseline text-sm font-medium">
|
||||
{title}
|
||||
<InfoTooltip content={tooltipContent} />
|
||||
</div>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid w-36 auto-rows-fr grid-cols-2 items-center gap-4">
|
||||
<p className="text-sm font-medium"> {price}</p>
|
||||
<Button size="sm" variant="secondary">
|
||||
{buttonText}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,65 +1,43 @@
|
||||
import {
|
||||
BorderedNavigationMenu,
|
||||
BorderedNavigationMenuItem,
|
||||
} from '@kit/ui/bordered-navigation-menu';
|
||||
import { If } from '@kit/ui/if';
|
||||
import { ShoppingCart } from 'lucide-react';
|
||||
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import { cn } from '@kit/ui/utils';
|
||||
|
||||
import { AppLogo } from '~/components/app-logo';
|
||||
import { ProfileAccountDropdownContainer } from '~/components/personal-account-dropdown-container';
|
||||
import featuresFlagConfig from '~/config/feature-flags.config';
|
||||
import { personalAccountNavigationConfig } from '~/config/personal-account-navigation.config';
|
||||
import { Search } from '~/components/ui/search';
|
||||
|
||||
import { SIDEBAR_WIDTH } from '../../../../packages/ui/src/shadcn/constants';
|
||||
// home imports
|
||||
import { HomeAccountSelector } from '../_components/home-account-selector';
|
||||
import { UserNotifications } from '../_components/user-notifications';
|
||||
import { type UserWorkspace } from '../_lib/server/load-user-workspace';
|
||||
|
||||
export function HomeMenuNavigation(props: { workspace: UserWorkspace }) {
|
||||
const { workspace, user, accounts } = props.workspace;
|
||||
|
||||
const routes = personalAccountNavigationConfig.routes.reduce<
|
||||
Array<{
|
||||
path: string;
|
||||
label: string;
|
||||
Icon?: React.ReactNode;
|
||||
end?: boolean | ((path: string) => boolean);
|
||||
}>
|
||||
>((acc, item) => {
|
||||
if ('children' in item) {
|
||||
return [...acc, ...item.children];
|
||||
}
|
||||
|
||||
if ('divider' in item) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
return [...acc, item];
|
||||
}, []);
|
||||
const { workspace, user } = props.workspace;
|
||||
|
||||
return (
|
||||
<div className={'flex w-full flex-1 justify-between'}>
|
||||
<div className={'flex items-center space-x-8'}>
|
||||
<div className={'flex w-full flex-1 items-center justify-between gap-3'}>
|
||||
<div className={cn('flex items-center', `w-[${SIDEBAR_WIDTH}]`)}>
|
||||
<AppLogo />
|
||||
|
||||
<BorderedNavigationMenu>
|
||||
{routes.map((route) => (
|
||||
<BorderedNavigationMenuItem {...route} key={route.path} />
|
||||
))}
|
||||
</BorderedNavigationMenu>
|
||||
</div>
|
||||
<Search
|
||||
className="flex grow"
|
||||
startElement={<Trans i18nKey="common:search" values={{ end: '...' }} />}
|
||||
/>
|
||||
|
||||
<div className={'flex justify-end space-x-2.5'}>
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
<Button variant="outline">
|
||||
<ShoppingCart className="stroke-[1.5px]" />
|
||||
<Trans i18nKey="common:shoppingCart" /> (0)
|
||||
</Button>
|
||||
<UserNotifications userId={user.id} />
|
||||
|
||||
<If condition={featuresFlagConfig.enableTeamAccounts}>
|
||||
<HomeAccountSelector userId={user.id} accounts={accounts} />
|
||||
</If>
|
||||
|
||||
<div>
|
||||
<ProfileAccountDropdownContainer
|
||||
user={user}
|
||||
account={workspace}
|
||||
showProfileName={false}
|
||||
showProfileName
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,61 +1,29 @@
|
||||
import { If } from '@kit/ui/if';
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarHeader,
|
||||
SidebarNavigation,
|
||||
} from '@kit/ui/shadcn-sidebar';
|
||||
import { cn } from '@kit/ui/utils';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { AppLogo } from '~/components/app-logo';
|
||||
import { ProfileAccountDropdownContainer } from '~/components/personal-account-dropdown-container';
|
||||
import featuresFlagConfig from '~/config/feature-flags.config';
|
||||
import { personalAccountNavigationConfig } from '~/config/personal-account-navigation.config';
|
||||
import { UserNotifications } from '~/home/(user)/_components/user-notifications';
|
||||
|
||||
// home imports
|
||||
import type { UserWorkspace } from '../_lib/server/load-user-workspace';
|
||||
import { HomeAccountSelector } from './home-account-selector';
|
||||
|
||||
interface HomeSidebarProps {
|
||||
workspace: UserWorkspace;
|
||||
}
|
||||
|
||||
export function HomeSidebar(props: HomeSidebarProps) {
|
||||
const { workspace, user, accounts } = props.workspace;
|
||||
export function HomeSidebar() {
|
||||
const collapsible = personalAccountNavigationConfig.sidebarCollapsedStyle;
|
||||
|
||||
return (
|
||||
<Sidebar collapsible={collapsible}>
|
||||
<SidebarHeader className={'h-16 justify-center'}>
|
||||
<div className={'flex items-center justify-between gap-x-3'}>
|
||||
<If
|
||||
condition={featuresFlagConfig.enableTeamAccounts}
|
||||
fallback={
|
||||
<AppLogo
|
||||
className={cn(
|
||||
'p-2 group-data-[minimized=true]:max-w-full group-data-[minimized=true]:py-0',
|
||||
)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<HomeAccountSelector userId={user.id} accounts={accounts} />
|
||||
</If>
|
||||
|
||||
<div className={'group-data-[minimized=true]:hidden'}>
|
||||
<UserNotifications userId={user.id} />
|
||||
</div>
|
||||
<SidebarHeader className="h-24 justify-center">
|
||||
<div className="mt-24 flex items-center">
|
||||
<h5>
|
||||
<Trans i18nKey="common:myActions" />
|
||||
</h5>
|
||||
</div>
|
||||
</SidebarHeader>
|
||||
|
||||
<SidebarContent>
|
||||
<SidebarNavigation config={personalAccountNavigationConfig} />
|
||||
</SidebarContent>
|
||||
|
||||
<SidebarFooter>
|
||||
<ProfileAccountDropdownContainer user={user} account={workspace} />
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ function SidebarLayout({ children }: React.PropsWithChildren) {
|
||||
<SidebarProvider defaultOpen={state.open}>
|
||||
<Page style={'sidebar'}>
|
||||
<PageNavigation>
|
||||
<HomeSidebar workspace={workspace} />
|
||||
<HomeSidebar />
|
||||
</PageNavigation>
|
||||
|
||||
<PageMobileNavigation className={'flex items-center justify-between'}>
|
||||
@@ -58,8 +58,8 @@ function HeaderLayout({ children }: React.PropsWithChildren) {
|
||||
|
||||
return (
|
||||
<UserWorkspaceContextProvider value={workspace}>
|
||||
<Page style={'header'}>
|
||||
<PageNavigation>
|
||||
<Page style={'header'} >
|
||||
<PageNavigation >
|
||||
<HomeMenuNavigation workspace={workspace} />
|
||||
</PageNavigation>
|
||||
|
||||
@@ -67,7 +67,14 @@ function HeaderLayout({ children }: React.PropsWithChildren) {
|
||||
<MobileNavigation workspace={workspace} />
|
||||
</PageMobileNavigation>
|
||||
|
||||
{children}
|
||||
<SidebarProvider defaultOpen>
|
||||
<Page style={'sidebar'}>
|
||||
<PageNavigation>
|
||||
<HomeSidebar />
|
||||
</PageNavigation>
|
||||
{children}
|
||||
</Page>
|
||||
</SidebarProvider>
|
||||
</Page>
|
||||
</UserWorkspaceContextProvider>
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Trans } from '@kit/ui/trans';
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
import Dashboard from './_components/dashboard';
|
||||
// local imports
|
||||
import { HomeLayoutPageHeader } from './_components/home-page-header';
|
||||
|
||||
@@ -21,10 +22,12 @@ function UserHomePage() {
|
||||
<>
|
||||
<HomeLayoutPageHeader
|
||||
title={<Trans i18nKey={'common:routes.home'} />}
|
||||
description={<Trans i18nKey={'common:homeTabDescription'} />}
|
||||
description={<></>}
|
||||
/>
|
||||
|
||||
<PageBody></PageBody>
|
||||
<PageBody>
|
||||
<Dashboard />
|
||||
</PageBody>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
16
components/ui/info-tooltip.tsx
Normal file
16
components/ui/info-tooltip.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@kit/ui/tooltip";
|
||||
import { Info } from "lucide-react";
|
||||
|
||||
export function InfoTooltip({ content }: { content?: string }) {
|
||||
if (!content) return null;
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Info className="size-4 cursor-pointer" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{content}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
33
components/ui/search.tsx
Normal file
33
components/ui/search.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import React, { JSX, ReactNode } from 'react';
|
||||
|
||||
import { cn } from '@kit/ui/utils';
|
||||
|
||||
export type SearchProps = React.InputHTMLAttributes<HTMLInputElement> & {
|
||||
startElement?: string | JSX.Element;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const Search = React.forwardRef<HTMLInputElement, SearchProps>(
|
||||
({ className, startElement, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'border-input ring-offset-background focus-within:ring-ring flex h-10 items-center rounded-md border bg-white pl-3 text-sm focus-within:ring-1 focus-within:ring-offset-2',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{!!startElement && startElement}
|
||||
<input
|
||||
{...props}
|
||||
type="search"
|
||||
ref={ref}
|
||||
className="placeholder:text-muted-foreground w-full p-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Search.displayName = 'Search';
|
||||
|
||||
export { Search };
|
||||
@@ -13,6 +13,12 @@ const PathsSchema = z.object({
|
||||
}),
|
||||
app: z.object({
|
||||
home: z.string().min(1),
|
||||
booking: z.string().min(1),
|
||||
myOrders: z.string().min(1),
|
||||
analysisResults: z.string().min(1),
|
||||
orderAnalysisPackage: z.string().min(1),
|
||||
orderAnalysis: z.string().min(1),
|
||||
orderHealthAnalysis: z.string().min(1),
|
||||
personalAccountSettings: z.string().min(1),
|
||||
personalAccountBilling: z.string().min(1),
|
||||
personalAccountBillingReturn: z.string().min(1),
|
||||
@@ -47,6 +53,13 @@ const pathsConfig = PathsSchema.parse({
|
||||
accountMembers: `/home/[account]/members`,
|
||||
accountBillingReturn: `/home/[account]/billing/return`,
|
||||
joinTeam: '/join',
|
||||
// these routes are added as placeholders and can be changed when the pages are added
|
||||
booking: '/booking',
|
||||
myOrders: '/my-orders',
|
||||
analysisResults: '/analysis-results',
|
||||
orderAnalysisPackage: '/order-analysis-package',
|
||||
orderAnalysis: '/order-analysis',
|
||||
orderHealthAnalysis: '/order-health-analysis'
|
||||
},
|
||||
} satisfies z.infer<typeof PathsSchema>);
|
||||
|
||||
|
||||
@@ -1,47 +1,72 @@
|
||||
import { CreditCard, Home, User } from 'lucide-react';
|
||||
import {
|
||||
FileLineChart,
|
||||
HeartPulse,
|
||||
LineChart,
|
||||
MousePointerClick,
|
||||
ShoppingCart,
|
||||
Stethoscope,
|
||||
TestTube2,
|
||||
} from 'lucide-react';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { NavigationConfigSchema } from '@kit/ui/navigation-schema';
|
||||
|
||||
import featureFlagsConfig from '~/config/feature-flags.config';
|
||||
import pathsConfig from '~/config/paths.config';
|
||||
|
||||
const iconClasses = 'w-4';
|
||||
const iconClasses = 'w-4 stroke-[1.5px]';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
label: 'common:routes.application',
|
||||
children: [
|
||||
{
|
||||
label: 'common:routes.home',
|
||||
label: 'common:routes.overview',
|
||||
path: pathsConfig.app.home,
|
||||
Icon: <Home className={iconClasses} />,
|
||||
Icon: <LineChart className={iconClasses} />,
|
||||
end: true,
|
||||
},
|
||||
{
|
||||
label: 'common:routes.booking',
|
||||
path: pathsConfig.app.booking,
|
||||
Icon: <MousePointerClick className={iconClasses} />,
|
||||
end: true,
|
||||
},
|
||||
{
|
||||
label: 'common:routes.myOrders',
|
||||
path: pathsConfig.app.myOrders,
|
||||
Icon: <ShoppingCart className={iconClasses} />,
|
||||
end: true,
|
||||
},
|
||||
{
|
||||
label: 'common:routes.analysisResults',
|
||||
path: pathsConfig.app.analysisResults,
|
||||
Icon: <TestTube2 className={iconClasses} />,
|
||||
end: true,
|
||||
},
|
||||
{
|
||||
label: 'common:routes.orderAnalysisPackage',
|
||||
path: pathsConfig.app.orderAnalysisPackage,
|
||||
Icon: <HeartPulse className={iconClasses} />,
|
||||
end: true,
|
||||
},
|
||||
{
|
||||
label: 'common:routes.orderAnalysis',
|
||||
path: pathsConfig.app.orderAnalysis,
|
||||
Icon: <FileLineChart className={iconClasses} />,
|
||||
end: true,
|
||||
},
|
||||
{
|
||||
label: 'common:routes.orderHealthAnalysis',
|
||||
path: pathsConfig.app.orderHealthAnalysis,
|
||||
Icon: <Stethoscope className={iconClasses} />,
|
||||
end: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'common:routes.settings',
|
||||
children: [
|
||||
{
|
||||
label: 'common:routes.profile',
|
||||
path: pathsConfig.app.personalAccountSettings,
|
||||
Icon: <User className={iconClasses} />,
|
||||
},
|
||||
featureFlagsConfig.enablePersonalAccountBilling
|
||||
? {
|
||||
label: 'common:routes.billing',
|
||||
path: pathsConfig.app.personalAccountBilling,
|
||||
Icon: <CreditCard className={iconClasses} />,
|
||||
}
|
||||
: undefined,
|
||||
].filter((route) => !!route),
|
||||
},
|
||||
] satisfies z.infer<typeof NavigationConfigSchema>['routes'];
|
||||
|
||||
export const personalAccountNavigationConfig = NavigationConfigSchema.parse({
|
||||
routes,
|
||||
style: process.env.NEXT_PUBLIC_USER_NAVIGATION_STYLE,
|
||||
sidebarCollapsed: process.env.NEXT_PUBLIC_HOME_SIDEBAR_COLLAPSED,
|
||||
sidebarCollapsedStyle: process.env.NEXT_PUBLIC_SIDEBAR_COLLAPSED_STYLE,
|
||||
style: 'custom',
|
||||
sidebarCollapsed: false,
|
||||
sidebarCollapsedStyle: 'icon',
|
||||
});
|
||||
|
||||
@@ -32,6 +32,7 @@ export const defaultI18nNamespaces = [
|
||||
'teams',
|
||||
'billing',
|
||||
'marketing',
|
||||
'dashboard',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
13
lib/utils.ts
13
lib/utils.ts
@@ -1,5 +1,5 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { type ClassValue, clsx } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
@@ -9,3 +9,12 @@ export function toArray<T>(input?: T | T[] | null): T[] {
|
||||
if (!input) return [];
|
||||
return Array.isArray(input) ? input : [input];
|
||||
}
|
||||
|
||||
export function toTitleCase(str?: string) {
|
||||
if (!str) return '';
|
||||
return str.replace(
|
||||
/\w\S*/g,
|
||||
(text: string) =>
|
||||
text.charAt(0).toUpperCase() + text.substring(1).toLowerCase(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -121,12 +121,12 @@ export function NotificationsPopover(params: {
|
||||
return (
|
||||
<Popover modal open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button className={'relative h-9 w-9'} variant={'ghost'}>
|
||||
<Bell className={'min-h-4 min-w-4'} />
|
||||
<Button className="relative w-5" variant={'outline'}>
|
||||
<Bell className={'min-h-4 min-w-4 stroke-[1.5px]'} />
|
||||
|
||||
<span
|
||||
className={cn(
|
||||
`fade-in animate-in zoom-in absolute right-1 top-1 mt-0 flex h-3.5 w-3.5 items-center justify-center rounded-full bg-red-500 text-[0.65rem] text-white`,
|
||||
`fade-in animate-in zoom-in absolute top-1 right-1 mt-0 flex h-3.5 w-3.5 items-center justify-center rounded-full bg-red-500 text-[0.65rem] text-white`,
|
||||
{
|
||||
hidden: !notifications.length,
|
||||
},
|
||||
@@ -186,7 +186,7 @@ export function NotificationsPopover(params: {
|
||||
<div
|
||||
key={notification.id.toString()}
|
||||
className={cn(
|
||||
'min-h-18 flex flex-col items-start justify-center gap-y-1 px-3 py-2',
|
||||
'flex min-h-18 flex-col items-start justify-center gap-y-1 px-3 py-2',
|
||||
)}
|
||||
onClick={() => {
|
||||
if (params.onClick) {
|
||||
|
||||
@@ -48,6 +48,48 @@ export type Database = {
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
request_entries: {
|
||||
Row: {
|
||||
comment: string | null
|
||||
created_at: string
|
||||
id: number
|
||||
personal_code: number | null
|
||||
request_api: string
|
||||
request_api_method: string
|
||||
requested_end_date: string | null
|
||||
requested_start_date: string | null
|
||||
service_id: number | null
|
||||
service_provider_id: number | null
|
||||
status: Database["audit"]["Enums"]["request_status"]
|
||||
}
|
||||
Insert: {
|
||||
comment?: string | null
|
||||
created_at?: string
|
||||
id?: number
|
||||
personal_code?: number | null
|
||||
request_api: string
|
||||
request_api_method: string
|
||||
requested_end_date?: string | null
|
||||
requested_start_date?: string | null
|
||||
service_id?: number | null
|
||||
service_provider_id?: number | null
|
||||
status: Database["audit"]["Enums"]["request_status"]
|
||||
}
|
||||
Update: {
|
||||
comment?: string | null
|
||||
created_at?: string
|
||||
id?: number
|
||||
personal_code?: number | null
|
||||
request_api?: string
|
||||
request_api_method?: string
|
||||
requested_end_date?: string | null
|
||||
requested_start_date?: string | null
|
||||
service_id?: number | null
|
||||
service_provider_id?: number | null
|
||||
status?: Database["audit"]["Enums"]["request_status"]
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
sync_entries: {
|
||||
Row: {
|
||||
changed_by_role: string
|
||||
@@ -83,6 +125,7 @@ export type Database = {
|
||||
[_ in never]: never
|
||||
}
|
||||
Enums: {
|
||||
request_status: "SUCCESS" | "FAIL"
|
||||
sync_status: "SUCCESS" | "FAIL"
|
||||
}
|
||||
CompositeTypes: {
|
||||
@@ -635,6 +678,158 @@ export type Database = {
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
connected_online_providers: {
|
||||
Row: {
|
||||
can_select_worker: boolean
|
||||
created_at: string
|
||||
email: string | null
|
||||
id: number
|
||||
name: string
|
||||
personal_code_required: boolean
|
||||
phone_number: string | null
|
||||
updated_at: string | null
|
||||
}
|
||||
Insert: {
|
||||
can_select_worker: boolean
|
||||
created_at?: string
|
||||
email?: string | null
|
||||
id: number
|
||||
name: string
|
||||
personal_code_required: boolean
|
||||
phone_number?: string | null
|
||||
updated_at?: string | null
|
||||
}
|
||||
Update: {
|
||||
can_select_worker?: boolean
|
||||
created_at?: string
|
||||
email?: string | null
|
||||
id?: number
|
||||
name?: string
|
||||
personal_code_required?: boolean
|
||||
phone_number?: string | null
|
||||
updated_at?: string | null
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
connected_online_reservation: {
|
||||
Row: {
|
||||
booking_code: string
|
||||
clinic_id: number
|
||||
comments: string | null
|
||||
created_at: string
|
||||
discount_code: string | null
|
||||
id: number
|
||||
lang: string
|
||||
requires_payment: boolean
|
||||
service_id: number
|
||||
service_user_id: number | null
|
||||
start_time: string
|
||||
sync_user_id: number
|
||||
updated_at: string | null
|
||||
user_id: string
|
||||
}
|
||||
Insert: {
|
||||
booking_code: string
|
||||
clinic_id: number
|
||||
comments?: string | null
|
||||
created_at?: string
|
||||
discount_code?: string | null
|
||||
id?: number
|
||||
lang: string
|
||||
requires_payment: boolean
|
||||
service_id: number
|
||||
service_user_id?: number | null
|
||||
start_time: string
|
||||
sync_user_id: number
|
||||
updated_at?: string | null
|
||||
user_id: string
|
||||
}
|
||||
Update: {
|
||||
booking_code?: string
|
||||
clinic_id?: number
|
||||
comments?: string | null
|
||||
created_at?: string
|
||||
discount_code?: string | null
|
||||
id?: number
|
||||
lang?: string
|
||||
requires_payment?: boolean
|
||||
service_id?: number
|
||||
service_user_id?: number | null
|
||||
start_time?: string
|
||||
sync_user_id?: number
|
||||
updated_at?: string | null
|
||||
user_id?: string
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
connected_online_services: {
|
||||
Row: {
|
||||
clinic_id: number
|
||||
code: string
|
||||
created_at: string
|
||||
description: string | null
|
||||
display: string | null
|
||||
duration: number
|
||||
has_free_codes: boolean
|
||||
id: number
|
||||
name: string
|
||||
neto_duration: number | null
|
||||
online_hide_duration: number | null
|
||||
online_hide_price: number | null
|
||||
price: number
|
||||
price_periods: string | null
|
||||
requires_payment: boolean
|
||||
sync_id: number
|
||||
updated_at: string | null
|
||||
}
|
||||
Insert: {
|
||||
clinic_id: number
|
||||
code: string
|
||||
created_at?: string
|
||||
description?: string | null
|
||||
display?: string | null
|
||||
duration: number
|
||||
has_free_codes: boolean
|
||||
id: number
|
||||
name: string
|
||||
neto_duration?: number | null
|
||||
online_hide_duration?: number | null
|
||||
online_hide_price?: number | null
|
||||
price: number
|
||||
price_periods?: string | null
|
||||
requires_payment: boolean
|
||||
sync_id: number
|
||||
updated_at?: string | null
|
||||
}
|
||||
Update: {
|
||||
clinic_id?: number
|
||||
code?: string
|
||||
created_at?: string
|
||||
description?: string | null
|
||||
display?: string | null
|
||||
duration?: number
|
||||
has_free_codes?: boolean
|
||||
id?: number
|
||||
name?: string
|
||||
neto_duration?: number | null
|
||||
online_hide_duration?: number | null
|
||||
online_hide_price?: number | null
|
||||
price?: number
|
||||
price_periods?: string | null
|
||||
requires_payment?: boolean
|
||||
sync_id?: number
|
||||
updated_at?: string | null
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "connected_online_services_clinic_id_fkey"
|
||||
columns: ["clinic_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "connected_online_providers"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
invitations: {
|
||||
Row: {
|
||||
account_id: string
|
||||
@@ -700,6 +895,129 @@ export type Database = {
|
||||
},
|
||||
]
|
||||
}
|
||||
medreport_product_groups: {
|
||||
Row: {
|
||||
created_at: string
|
||||
id: number
|
||||
name: string
|
||||
updated_at: string | null
|
||||
}
|
||||
Insert: {
|
||||
created_at?: string
|
||||
id?: number
|
||||
name: string
|
||||
updated_at?: string | null
|
||||
}
|
||||
Update: {
|
||||
created_at?: string
|
||||
id?: number
|
||||
name?: string
|
||||
updated_at?: string | null
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
medreport_products: {
|
||||
Row: {
|
||||
created_at: string
|
||||
id: number
|
||||
name: string
|
||||
product_group_id: number | null
|
||||
updated_at: string | null
|
||||
}
|
||||
Insert: {
|
||||
created_at?: string
|
||||
id?: number
|
||||
name: string
|
||||
product_group_id?: number | null
|
||||
updated_at?: string | null
|
||||
}
|
||||
Update: {
|
||||
created_at?: string
|
||||
id?: number
|
||||
name?: string
|
||||
product_group_id?: number | null
|
||||
updated_at?: string | null
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "medreport_products_product_groups_id_fkey"
|
||||
columns: ["product_group_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "medreport_product_groups"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
medreport_products_analyses_relations: {
|
||||
Row: {
|
||||
analysis_element_id: number | null
|
||||
analysis_id: number | null
|
||||
product_id: number
|
||||
}
|
||||
Insert: {
|
||||
analysis_element_id?: number | null
|
||||
analysis_id?: number | null
|
||||
product_id: number
|
||||
}
|
||||
Update: {
|
||||
analysis_element_id?: number | null
|
||||
analysis_id?: number | null
|
||||
product_id?: number
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "medreport_products_analyses_analysis_element_id_fkey"
|
||||
columns: ["analysis_element_id"]
|
||||
isOneToOne: true
|
||||
referencedRelation: "analysis_elements"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
{
|
||||
foreignKeyName: "medreport_products_analyses_analysis_id_fkey"
|
||||
columns: ["analysis_id"]
|
||||
isOneToOne: true
|
||||
referencedRelation: "analyses"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
{
|
||||
foreignKeyName: "medreport_products_analyses_product_id_fkey"
|
||||
columns: ["product_id"]
|
||||
isOneToOne: true
|
||||
referencedRelation: "medreport_products"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
medreport_products_external_services_relations: {
|
||||
Row: {
|
||||
connected_online_service_id: number
|
||||
product_id: number
|
||||
}
|
||||
Insert: {
|
||||
connected_online_service_id: number
|
||||
product_id: number
|
||||
}
|
||||
Update: {
|
||||
connected_online_service_id?: number
|
||||
product_id?: number
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "medreport_products_connected_online_services_id_fkey"
|
||||
columns: ["connected_online_service_id"]
|
||||
isOneToOne: true
|
||||
referencedRelation: "connected_online_services"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
{
|
||||
foreignKeyName: "medreport_products_connected_online_services_product_id_fkey"
|
||||
columns: ["product_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "medreport_products"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
nonces: {
|
||||
Row: {
|
||||
client_token: string
|
||||
@@ -1543,6 +1861,7 @@ export type CompositeTypes<
|
||||
export const Constants = {
|
||||
audit: {
|
||||
Enums: {
|
||||
request_status: ["SUCCESS", "FAIL"],
|
||||
sync_status: ["SUCCESS", "FAIL"],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -29,7 +29,7 @@ const RouteChild = z.object({
|
||||
});
|
||||
|
||||
const RouteGroup = z.object({
|
||||
label: z.string(),
|
||||
label: z.string().optional(),
|
||||
collapsible: z.boolean().optional(),
|
||||
collapsed: z.boolean().optional(),
|
||||
children: z.array(RouteChild),
|
||||
@@ -37,12 +37,8 @@ const RouteGroup = z.object({
|
||||
});
|
||||
|
||||
export const NavigationConfigSchema = z.object({
|
||||
style: z.enum(['custom', 'sidebar', 'header']).default('sidebar'),
|
||||
sidebarCollapsed: z
|
||||
.enum(['false', 'true'])
|
||||
.default('true')
|
||||
.optional()
|
||||
.transform((value) => value === `true`),
|
||||
style: z.enum(['custom', 'sidebar', 'header']).default('custom'),
|
||||
sidebarCollapsed: z.boolean().optional(),
|
||||
sidebarCollapsedStyle: z.enum(['offcanvas', 'icon', 'none']).default('icon'),
|
||||
routes: z.array(z.union([RouteGroup, Divider])),
|
||||
});
|
||||
|
||||
@@ -14,10 +14,6 @@ type PageProps = React.PropsWithChildren<{
|
||||
sticky?: boolean;
|
||||
}>;
|
||||
|
||||
const ENABLE_SIDEBAR_TRIGGER = process.env.NEXT_PUBLIC_ENABLE_SIDEBAR_TRIGGER
|
||||
? process.env.NEXT_PUBLIC_ENABLE_SIDEBAR_TRIGGER === 'true'
|
||||
: true;
|
||||
|
||||
export function Page(props: PageProps) {
|
||||
switch (props.style) {
|
||||
case 'header':
|
||||
@@ -79,7 +75,7 @@ function PageWithHeader(props: PageProps) {
|
||||
const { Navigation, Children, MobileNavigation } = getSlotsFromPage(props);
|
||||
|
||||
return (
|
||||
<div className={cn('flex h-screen flex-1 flex-col', props.className)}>
|
||||
<div className={cn('flex h-screen flex-1 flex-col z-1000', props.className)}>
|
||||
<div
|
||||
className={
|
||||
props.contentContainerClassName ?? 'flex flex-1 flex-col space-y-4'
|
||||
@@ -87,9 +83,9 @@ function PageWithHeader(props: PageProps) {
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'bg-muted/40 dark:border-border dark:shadow-primary/10 flex h-14 items-center justify-between px-4 lg:justify-start lg:shadow-xs',
|
||||
'bg-muted/40 dark:border-border dark:shadow-primary/10 flex h-14 items-center justify-between px-4 lg:justify-start lg:shadow-xs border-b',
|
||||
{
|
||||
'sticky top-0 z-10 backdrop-blur-md': props.sticky ?? true,
|
||||
'sticky top-0 z-1000 backdrop-blur-md': props.sticky ?? true,
|
||||
},
|
||||
)}
|
||||
>
|
||||
@@ -113,7 +109,10 @@ export function PageBody(
|
||||
className?: string;
|
||||
}>,
|
||||
) {
|
||||
const className = cn('flex w-full flex-1 flex-col lg:px-4', props.className);
|
||||
const className = cn(
|
||||
'flex w-full flex-1 flex-col space-y-6 lg:px-4',
|
||||
props.className,
|
||||
);
|
||||
|
||||
return <div className={className}>{props.children}</div>;
|
||||
}
|
||||
@@ -125,7 +124,7 @@ export function PageNavigation(props: React.PropsWithChildren) {
|
||||
export function PageDescription(props: React.PropsWithChildren) {
|
||||
return (
|
||||
<div className={'flex h-6 items-center'}>
|
||||
<div className={'text-muted-foreground text-xs leading-none font-normal'}>
|
||||
<div className={'text-muted-foreground text-sm leading-none font-normal'}>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
@@ -153,7 +152,7 @@ export function PageHeader({
|
||||
title,
|
||||
description,
|
||||
className,
|
||||
displaySidebarTrigger = ENABLE_SIDEBAR_TRIGGER,
|
||||
displaySidebarTrigger = false,
|
||||
}: React.PropsWithChildren<{
|
||||
className?: string;
|
||||
title?: string | React.ReactNode;
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { VariantProps } from 'class-variance-authority';
|
||||
import { cn } from '../lib/utils';
|
||||
|
||||
const buttonVariants = cva(
|
||||
'focus-visible:ring-ring inline-flex items-center justify-center rounded-md text-sm font-medium whitespace-nowrap transition-colors focus-visible:ring-1 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50',
|
||||
'focus-visible:ring-ring gap-1 inline-flex items-center justify-center rounded-md text-sm font-medium whitespace-nowrap transition-colors focus-visible:ring-1 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
||||
@@ -1,15 +1,31 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '../lib/utils';
|
||||
import { VariantProps, cva } from 'class-variance-authority';
|
||||
import { cn } from '.';
|
||||
|
||||
const Card: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<div
|
||||
className={cn('bg-card text-card-foreground rounded-xl border', className)}
|
||||
{...props}
|
||||
/>
|
||||
const cardVariants = cva('text-card-foreground rounded-xl border', {
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-card',
|
||||
'gradient-warning':
|
||||
'from-warning/30 via-warning/10 to-background bg-gradient-to-t',
|
||||
'gradient-destructive':
|
||||
'from-destructive/30 via-destructive/10 to-background bg-gradient-to-t',
|
||||
'gradient-success':
|
||||
'from-success/30 via-success/10 to-background bg-gradient-to-t',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
});
|
||||
|
||||
export interface CardProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof cardVariants> {}
|
||||
|
||||
const Card: React.FC<CardProps> = ({ className, variant, ...props }) => (
|
||||
<div className={cn(cardVariants({ variant, className }))} {...props} />
|
||||
);
|
||||
Card.displayName = 'Card';
|
||||
|
||||
|
||||
3
packages/ui/src/shadcn/constants.ts
Normal file
3
packages/ui/src/shadcn/constants.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const SIDEBAR_WIDTH = '16rem';
|
||||
export const SIDEBAR_WIDTH_MOBILE = '18rem';
|
||||
export const SIDEBAR_WIDTH_ICON = '4rem';
|
||||
@@ -21,6 +21,11 @@ import {
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from './collapsible';
|
||||
import {
|
||||
SIDEBAR_WIDTH,
|
||||
SIDEBAR_WIDTH_ICON,
|
||||
SIDEBAR_WIDTH_MOBILE,
|
||||
} from './constants';
|
||||
import { Input } from './input';
|
||||
import { Separator } from './separator';
|
||||
import { Sheet, SheetContent } from './sheet';
|
||||
@@ -34,9 +39,6 @@ import {
|
||||
|
||||
const SIDEBAR_COOKIE_NAME = 'sidebar:state';
|
||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
||||
const SIDEBAR_WIDTH = '16rem';
|
||||
const SIDEBAR_WIDTH_MOBILE = '18rem';
|
||||
const SIDEBAR_WIDTH_ICON = '4rem';
|
||||
const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
|
||||
const SIDEBAR_MINIMIZED_WIDTH = SIDEBAR_WIDTH_ICON;
|
||||
|
||||
@@ -276,7 +278,7 @@ const Sidebar: React.FC<
|
||||
<div
|
||||
data-sidebar="sidebar"
|
||||
className={cn(
|
||||
'bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm',
|
||||
'bg-sidebar group-data-[variant=floating]:border-sidebar-border ml-3 flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm',
|
||||
{
|
||||
'bg-transparent': variant === 'ghost',
|
||||
},
|
||||
@@ -908,7 +910,7 @@ export function SidebarNavigation({
|
||||
tooltip={child.label}
|
||||
>
|
||||
<Link
|
||||
className={cn('flex items-center', {
|
||||
className={cn('flex items-center font-medium', {
|
||||
'mx-auto w-full gap-0! [&>svg]:flex-1': !open,
|
||||
})}
|
||||
href={path}
|
||||
@@ -916,7 +918,7 @@ export function SidebarNavigation({
|
||||
{child.Icon}
|
||||
<span
|
||||
className={cn(
|
||||
'w-auto transition-opacity duration-300',
|
||||
'text-md w-auto font-medium transition-opacity duration-300',
|
||||
{
|
||||
'w-0 opacity-0': !open,
|
||||
},
|
||||
|
||||
@@ -55,8 +55,19 @@
|
||||
"newVersionAvailableDescription": "A new version of the app is available. It is recommended to refresh the page to get the latest updates and avoid any issues.",
|
||||
"newVersionSubmitButton": "Reload and Update",
|
||||
"back": "Back",
|
||||
"welcome": "Welcome",
|
||||
"shoppingCart": "Shopping cart",
|
||||
"search": "Search{{end}}",
|
||||
"myActions": "My actions",
|
||||
"routes": {
|
||||
"home": "Home",
|
||||
"overview": "Overview",
|
||||
"booking": "Booking",
|
||||
"myOrders": "My orders",
|
||||
"analysisResults": "Analysis results",
|
||||
"orderAnalysisPackage": "Telli analüüside pakett",
|
||||
"orderAnalysis": "Order analysis",
|
||||
"orderHealthAnalysis": "Telli terviseuuring",
|
||||
"account": "Account",
|
||||
"members": "Employees",
|
||||
"billing": "Billing",
|
||||
|
||||
16
public/locales/en/dashboard.json
Normal file
16
public/locales/en/dashboard.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"recentlyCheckedDescription": "Super, oled käinud tervist kontrollimas. Siin on sinule olulised näitajad",
|
||||
"respondToQuestion": "Respond",
|
||||
"gender": "Gender",
|
||||
"male": "Male",
|
||||
"female": "Female",
|
||||
"age": "Age",
|
||||
"height": "Height",
|
||||
"weight": "Weight",
|
||||
"bmi": "BMI",
|
||||
"bloodPressure": "Blood pressure",
|
||||
"cholesterol": "Cholesterol",
|
||||
"ldlCholesterol": "LDL Cholesterol",
|
||||
"smoking": "Smoking",
|
||||
"recommendedForYou": "Recommended for you"
|
||||
}
|
||||
@@ -54,8 +54,20 @@
|
||||
"newVersionAvailable": "New version available",
|
||||
"newVersionAvailableDescription": "A new version of the app is available. It is recommended to refresh the page to get the latest updates and avoid any issues.",
|
||||
"newVersionSubmitButton": "Reload and Update",
|
||||
"back": "Back",
|
||||
"welcome": "Tere tulemast",
|
||||
"shoppingCart": "Ostukorv",
|
||||
"search": "Otsi{{end}}",
|
||||
"myActions": "Minu toimingud",
|
||||
"routes": {
|
||||
"home": "Home",
|
||||
"overview": "Ülevaade",
|
||||
"booking": "Broneeri aeg",
|
||||
"myOrders": "Minu tellimused",
|
||||
"analysisResults": "Analüüside vastused",
|
||||
"orderAnalysisPackage": "Telli analüüside pakett",
|
||||
"orderAnalysis": "Telli analüüs",
|
||||
"orderHealthAnalysis": "Telli terviseuuring",
|
||||
"account": "Account",
|
||||
"members": "Employees",
|
||||
"billing": "Billing",
|
||||
|
||||
16
public/locales/et/dashboard.json
Normal file
16
public/locales/et/dashboard.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"recentlyCheckedDescription": "Super, oled käinud tervist kontrollimas. Siin on sinule olulised näitajad",
|
||||
"respondToQuestion": "Vasta küsimusele",
|
||||
"gender": "Sugu",
|
||||
"male": "Mees",
|
||||
"female": "Naine",
|
||||
"age": "Vanus",
|
||||
"height": "Pikkus",
|
||||
"weight": "Kaal",
|
||||
"bmi": "KMI",
|
||||
"bloodPressure": "Vererõhk",
|
||||
"cholesterol": "Kolesterool",
|
||||
"ldlCholesterol": "LDL kolesterool",
|
||||
"smoking": "Suitsetamine",
|
||||
"recommendedForYou": "Soovitused sulle"
|
||||
}
|
||||
@@ -55,8 +55,18 @@
|
||||
"newVersionAvailableDescription": "A new version of the app is available. It is recommended to refresh the page to get the latest updates and avoid any issues.",
|
||||
"newVersionSubmitButton": "Reload and Update",
|
||||
"back": "Back",
|
||||
"welcome": "Welcome",
|
||||
"shoppingCart": "Shopping cart",
|
||||
"search": "Search{{end}}",
|
||||
"myActions": "My actions",
|
||||
"routes": {
|
||||
"home": "Home",
|
||||
"overview": "Overview",
|
||||
"booking": "Booking",
|
||||
"myOrders": "My orders",
|
||||
"orderAnalysis": "Order analysis",
|
||||
"orderAnalysisPackage": "Order analysis package",
|
||||
"orderHealthAnalysis": "Order health analysis",
|
||||
"account": "Account",
|
||||
"members": "Employees",
|
||||
"billing": "Billing",
|
||||
|
||||
16
public/locales/ru/dashboard.json
Normal file
16
public/locales/ru/dashboard.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"recentlyCheckedDescription": "Super, oled käinud tervist kontrollimas. Siin on sinule olulised näitajad",
|
||||
"respondToQuestion": "Respond",
|
||||
"gender": "Gender",
|
||||
"male": "Male",
|
||||
"female": "Female",
|
||||
"age": "Age",
|
||||
"height": "Height",
|
||||
"weight": "Weight",
|
||||
"bmi": "BMI",
|
||||
"bloodPressure": "Blood pressure",
|
||||
"cholesterol": "Cholesterol",
|
||||
"ldlCholesterol": "LDL Cholesterol",
|
||||
"smoking": "Smoking",
|
||||
"recommendedForYou": "Recommended for you"
|
||||
}
|
||||
@@ -31,7 +31,9 @@
|
||||
@layer base {
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-feature-settings: "rlig" 1, "calt" 1;
|
||||
font-feature-settings:
|
||||
'rlig' 1,
|
||||
'calt' 1;
|
||||
}
|
||||
|
||||
*,
|
||||
@@ -47,9 +49,44 @@
|
||||
color: theme(--color-muted-foreground);
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
@apply font-heading text-foreground font-semibold tracking-tight;
|
||||
}
|
||||
|
||||
h1,h2,h3,h4,h5,h6 {
|
||||
@apply font-heading text-foreground text-2xl font-semibold tracking-tight
|
||||
}
|
||||
h1 {
|
||||
@apply text-5xl;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply text-4xl;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@apply text-3xl;
|
||||
}
|
||||
|
||||
h4 {
|
||||
@apply text-2xl;
|
||||
}
|
||||
|
||||
h5 {
|
||||
@apply text-xl;
|
||||
}
|
||||
|
||||
h6 {
|
||||
@apply text-lg;
|
||||
}
|
||||
|
||||
.lucide {
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.lucide {
|
||||
@apply size-4;
|
||||
}
|
||||
}
|
||||
@@ -7,127 +7,130 @@
|
||||
*/
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--font-sans: var(--font-sans) -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
--font-heading: var(--font-heading);
|
||||
:root {
|
||||
--font-sans:
|
||||
var(--font-sans) -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||
Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
||||
'Segoe UI Symbol';
|
||||
--font-heading: var(--font-heading);
|
||||
|
||||
--background: hsla(0, 0%, 100%, 1);
|
||||
--foreground: hsla(240, 10%, 4%, 1);
|
||||
--foreground-50: hsla(240, 10%, 4%, 0.5);
|
||||
--background: hsla(0, 0%, 100%, 1);
|
||||
--foreground: hsla(240, 10%, 4%, 1);
|
||||
--foreground-50: hsla(240, 10%, 4%, 0.5);
|
||||
|
||||
--background-90: hsla(0, 0%, 100%, 0.9);
|
||||
--background-80: hsla(0, 0%, 100%, 0.8);
|
||||
--background-90: hsla(0, 0%, 100%, 0.9);
|
||||
--background-80: hsla(0, 0%, 100%, 0.8);
|
||||
|
||||
--card: var(--color-white);
|
||||
--card-foreground: var(--color-neutral-950);
|
||||
--card: var(--color-white);
|
||||
--card-foreground: var(--color-neutral-950);
|
||||
|
||||
--popover: hsla(0, 0%, 100%, 1);
|
||||
--popover-foreground: hsla(240, 10%, 4%, 1);
|
||||
--popover: hsla(0, 0%, 100%, 1);
|
||||
--popover-foreground: hsla(240, 10%, 4%, 1);
|
||||
|
||||
--primary: hsla(145, 78%, 18%, 1);
|
||||
--primary-foreground: hsla(356, 100%, 97%, 1);
|
||||
--primary: hsla(145, 78%, 18%, 1);
|
||||
--primary-foreground: hsla(356, 100%, 97%, 1);
|
||||
|
||||
--primary-90: hsla(145, 78%, 18%, 0.9);
|
||||
--primary-80: hsla(145, 78%, 18%, 0.8);
|
||||
--primary-50: hsla(145, 78%, 18%, 0.5);
|
||||
--primary-20: hsla(145, 78%, 18%, 0.2);
|
||||
--primary-10: hsla(145, 78%, 18%, 0.1);
|
||||
--primary-90: hsla(145, 78%, 18%, 0.9);
|
||||
--primary-80: hsla(145, 78%, 18%, 0.8);
|
||||
--primary-50: hsla(145, 78%, 18%, 0.5);
|
||||
--primary-20: hsla(145, 78%, 18%, 0.2);
|
||||
--primary-10: hsla(145, 78%, 18%, 0.1);
|
||||
|
||||
--secondary: hsla(240, 5%, 96%, 1);
|
||||
--secondary-foreground: hsla(240, 6%, 10%, 1);
|
||||
--secondary: hsla(240, 5%, 96%, 1);
|
||||
--secondary-foreground: hsla(240, 6%, 10%, 1);
|
||||
|
||||
--secondary-90: hsla(240, 5%, 96%, 0.9);
|
||||
--secondary-80: hsla(240, 5%, 96%, 0.8);
|
||||
--secondary-90: hsla(240, 5%, 96%, 0.9);
|
||||
--secondary-80: hsla(240, 5%, 96%, 0.8);
|
||||
|
||||
--muted: hsla(240, 5%, 96%, 1);
|
||||
--muted-foreground: hsla(240, 4%, 41%, 1);
|
||||
|
||||
--muted: hsla(240, 5%, 96%, 1);
|
||||
--muted-foreground: hsla(240, 4%, 41%, 1);
|
||||
--muted-90: hsla(240, 5%, 96%, 0.9);
|
||||
--muted-80: hsla(240, 5%, 96%, 0.8);
|
||||
--muted-50: hsla(240, 5%, 96%, 0.5);
|
||||
--muted-40: hsla(240, 5%, 96%, 0.4);
|
||||
|
||||
--muted-90: hsla(240, 5%, 96%, 0.9);
|
||||
--muted-80: hsla(240, 5%, 96%, 0.8);
|
||||
--muted-50: hsla(240, 5%, 96%, 0.5);
|
||||
--muted-40: hsla(240, 5%, 96%, 0.4);
|
||||
--accent: hsla(240, 5%, 96%, 1);
|
||||
--accent-foreground: hsla(240, 6%, 10%, 1);
|
||||
|
||||
--accent: hsla(240, 5%, 96%, 1);
|
||||
--accent-foreground: hsla(240, 6%, 10%, 1);
|
||||
--accent-90: hsla(240, 5%, 96%, 0.9);
|
||||
--accent-80: hsla(240, 5%, 96%, 0.8);
|
||||
--accent-50: hsla(240, 5%, 96%, 0.5);
|
||||
|
||||
--accent-90: hsla(240, 5%, 96%, 0.9);
|
||||
--accent-80: hsla(240, 5%, 96%, 0.8);
|
||||
--accent-50: hsla(240, 5%, 96%, 0.5);
|
||||
--success: hsla(142, 76%, 36%, 1);
|
||||
|
||||
--destructive: hsla(0, 84%, 60%, 1);
|
||||
--destructive-foreground: hsla(0, 0%, 98%, 1);
|
||||
--destructive: hsla(0, 84%, 60%, 1);
|
||||
--destructive-foreground: hsla(0, 0%, 98%, 1);
|
||||
|
||||
--destructiv-90: hsla(0, 84%, 60%, 0.9);
|
||||
--destructiv-80: hsla(0, 84%, 60%, 0.8);
|
||||
--destructiv-50: hsla(0, 84%, 60%, 0.5);
|
||||
--destructiv-90: hsla(0, 84%, 60%, 0.9);
|
||||
--destructiv-80: hsla(0, 84%, 60%, 0.8);
|
||||
--destructiv-50: hsla(0, 84%, 60%, 0.5);
|
||||
|
||||
--border: hsla(240, 6%, 90%, 1);
|
||||
--input: hsla(240, 6%, 90%, 1);
|
||||
--ring: var(--color-neutral-800);
|
||||
|
||||
--border: hsla(240, 6%, 90%, 1);
|
||||
--input: hsla(240, 6%, 90%, 1);
|
||||
--ring: var(--color-neutral-800);
|
||||
--radius: calc(1rem);
|
||||
--spacing: 0.25rem;
|
||||
|
||||
--radius: calc(1rem);
|
||||
--spacing: 0.25rem;
|
||||
--chart-1: var(--color-orange-400);
|
||||
--chart-2: var(--color-teal-600);
|
||||
--chart-3: var(--color-green-800);
|
||||
--chart-4: var(--color-yellow-200);
|
||||
--chart-5: var(--color-orange-200);
|
||||
|
||||
--chart-1: var(--color-orange-400);
|
||||
--chart-2: var(--color-teal-600);
|
||||
--chart-3: var(--color-green-800);
|
||||
--chart-4: var(--color-yellow-200);
|
||||
--chart-5: var(--color-orange-200);
|
||||
--sidebar-background: var(--background);
|
||||
--sidebar-foreground: var(--foreground);
|
||||
--sidebar-primary: var(--primary);
|
||||
--sidebar-primary-foreground: var(--color-white);
|
||||
--sidebar-accent: var(--secondary);
|
||||
--sidebar-accent-foreground: var(--secondary-foreground);
|
||||
--sidebar-border: var(--border);
|
||||
--sidebar-ring: var(--ring);
|
||||
}
|
||||
|
||||
--sidebar-background: var(--background);
|
||||
--sidebar-foreground: var(--foreground);
|
||||
--sidebar-primary: var(--primary);
|
||||
--sidebar-primary-foreground: var(--color-white);
|
||||
--sidebar-accent: var(--secondary);
|
||||
--sidebar-accent-foreground: var(--secondary-foreground);
|
||||
--sidebar-border: var(--border);
|
||||
--sidebar-ring: var(--ring);
|
||||
}
|
||||
.dark {
|
||||
--background: var(--color-neutral-900);
|
||||
--foreground: var(--color-white);
|
||||
|
||||
.dark {
|
||||
--background: var(--color-neutral-900);
|
||||
--foreground: var(--color-white);
|
||||
--card: var(--color-neutral-900);
|
||||
--card-foreground: var(--color-white);
|
||||
|
||||
--card: var(--color-neutral-900);
|
||||
--card-foreground: var(--color-white);
|
||||
--popover: var(--color-neutral-900);
|
||||
--popover-foreground: var(--color-white);
|
||||
|
||||
--popover: var(--color-neutral-900);
|
||||
--popover-foreground: var(--color-white);
|
||||
--primary: var(--color-white);
|
||||
--primary-foreground: var(--color-neutral-900);
|
||||
|
||||
--primary: var(--color-white);
|
||||
--primary-foreground: var(--color-neutral-900);
|
||||
--secondary: var(--color-neutral-800);
|
||||
--secondary-foreground: oklch(98.43% 0.0017 247.84);
|
||||
|
||||
--secondary: var(--color-neutral-800);
|
||||
--secondary-foreground: oklch(98.43% 0.0017 247.84);
|
||||
--muted: var(--color-neutral-800);
|
||||
--muted-foreground: hsla(240, 4%, 41%, 1);
|
||||
|
||||
--muted: var(--color-neutral-800);
|
||||
--muted-foreground: hsla(240, 4%, 41%, 1);
|
||||
--accent: var(--color-neutral-800);
|
||||
--accent-foreground: oklch(98.48% 0 0);
|
||||
|
||||
--accent: var(--color-neutral-800);
|
||||
--accent-foreground: oklch(98.48% 0 0);
|
||||
--destructive: var(--color-red-700);
|
||||
--destructive-foreground: var(--color-white);
|
||||
|
||||
--destructive: var(--color-red-700);
|
||||
--destructive-foreground: var(--color-white);
|
||||
--border: var(--color-neutral-800);
|
||||
--input: var(--color-neutral-700);
|
||||
--ring: oklch(87.09% 0.0055 286.29);
|
||||
|
||||
--border: var(--color-neutral-800);
|
||||
--input: var(--color-neutral-700);
|
||||
--ring: oklch(87.09% 0.0055 286.29);
|
||||
--chart-1: var(--color-blue-600);
|
||||
--chart-2: var(--color-emerald-400);
|
||||
--chart-3: var(--color-orange-400);
|
||||
--chart-4: var(--color-purple-500);
|
||||
--chart-5: var(--color-pink-500);
|
||||
|
||||
--chart-1: var(--color-blue-600);
|
||||
--chart-2: var(--color-emerald-400);
|
||||
--chart-3: var(--color-orange-400);
|
||||
--chart-4: var(--color-purple-500);
|
||||
--chart-5: var(--color-pink-500);
|
||||
|
||||
--sidebar-background: var(--color-neutral-900);
|
||||
--sidebar-foreground: var(--color-white);
|
||||
--sidebar-primary: var(--color-blue-500);
|
||||
--sidebar-primary-foreground: var(--color-white);
|
||||
--sidebar-accent: var(--color-neutral-800);
|
||||
--sidebar-accent-foreground: var(--color-white);
|
||||
--sidebar-border: var(--border);
|
||||
--sidebar-ring: var(--color-blue-500);
|
||||
}
|
||||
--sidebar-background: var(--color-neutral-900);
|
||||
--sidebar-foreground: var(--color-white);
|
||||
--sidebar-primary: var(--color-blue-500);
|
||||
--sidebar-primary-foreground: var(--color-white);
|
||||
--sidebar-accent: var(--color-neutral-800);
|
||||
--sidebar-accent-foreground: var(--color-white);
|
||||
--sidebar-border: var(--border);
|
||||
--sidebar-ring: var(--color-blue-500);
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,15 @@
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
|
||||
--success: hsla(142, 76%, 36%, 1);
|
||||
--color-success: var(--success);
|
||||
|
||||
--warning: hsla(25, 95%, 53%, 1);
|
||||
--color-warning: var(--warning);
|
||||
|
||||
--cyan: hsla(189, 94%, 43%, 1);
|
||||
--color-cyan: var(--cyan);
|
||||
|
||||
/* text colors */
|
||||
--color-text-foreground: var(--foreground);
|
||||
--color-text-primary: var(--primary);
|
||||
|
||||
Reference in New Issue
Block a user