Merge branch 'main' into B2B-30

This commit is contained in:
devmc-ee
2025-07-01 23:27:59 +03:00
95 changed files with 2343 additions and 2297 deletions

View File

@@ -1,10 +1,15 @@
# MedReport README
## Prerequisites
"node": ">=20.0.0",
"pnpm": ">=9.0.0"
```json
"node": ">=20.0.0",
"pnpm": ">=9.0.0"
```
## Project structure
```
```text
/ app - pages
/ components - custom components, helper components that not provided by any package. Place to extend an redefine components from packages
/ config - bunch of configs, that are provided by starter kit.
@@ -25,12 +30,10 @@
/ supabase - primary supabase
/ tooling - a workspace package, used for generation packages in node_modules and provides global links for its data. The most important is typescript config
/ utils
```
## Migration from old structure
```bash
pnpm clean
pnpm i
@@ -43,4 +46,27 @@ pnpm add <pacakge-name> -w
```
## Supabase
TODO
Start supabase in docker
```bash
npm run supabase:start
```
Link your local supabase with a supabase project
```bash
npm run supabase:deploy
```
After editing supabase tables/functions etc update migration files
```bash
npm run supabase:db:diff
```
To update database types run:
```bash
npm run supabase:typegen:app
```

View File

@@ -1,6 +1,6 @@
import Link from 'next/link';
import { MedReportLogo } from '@/components/med-report-title';
import { MedReportLogo } from '@/components/med-report-logo';
import { ArrowRightIcon } from 'lucide-react';
import { CtaButton, Hero } from '@kit/ui/marketing';
@@ -49,7 +49,7 @@ function MainCallToActionButton() {
</CtaButton>
<CtaButton variant={'link'}>
<Link href={'/register-company'}>
<Link href={'/company-offer'}>
<Trans i18nKey={'account:createCompanyAccount'} />
</Link>
</CtaButton>

View File

@@ -0,0 +1,102 @@
'use client';
import React from 'react';
import { useRouter } from 'next/navigation';
import { MedReportLogo } from '@/components/med-report-logo';
import { SubmitButton } from '@/components/ui/submit-button';
import { sendCompanyOfferEmail } from '@/lib/services/mailer.service';
import { CompanySubmitData } from '@/lib/types/company';
import { companyOfferSchema } from '@/lib/validations/company-offer.schema';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { FormItem } from '@kit/ui/form';
import { Input } from '@kit/ui/input';
import { Label } from '@kit/ui/label';
import { Trans } from '@kit/ui/trans';
export default function CompanyOffer() {
const router = useRouter();
const {
register,
handleSubmit,
formState: { isValid, isSubmitting },
} = useForm({
resolver: zodResolver(companyOfferSchema),
mode: 'onChange',
});
const language = useTranslation().i18n.language;
const onSubmit = async (data: CompanySubmitData) => {
const formData = new FormData();
Object.entries(data).forEach(([key, value]) => {
if (value !== undefined) formData.append(key, value);
});
try {
sendCompanyOfferEmail(data, language)
.then(() => router.push('/company-offer/success'))
.catch((error) => alert('error: ' + error));
} catch (err: unknown) {
if (err instanceof Error) {
console.warn('Server validation error: ' + err.message);
}
console.warn('Server validation error: ', err);
}
};
return (
<div className="border-border flex max-w-5xl flex-row overflow-hidden rounded-3xl border">
<div className="flex w-1/2 flex-col px-12 py-14 text-center">
<MedReportLogo />
<h1 className="pt-8">
<Trans i18nKey={'account:requestCompanyAccount:title'} />
</h1>
<p className="text-muted-foreground pt-2 text-sm">
<Trans i18nKey={'account:requestCompanyAccount:description'} />
</p>
<form
onSubmit={handleSubmit(onSubmit)}
noValidate
className="flex flex-col gap-6 px-6 pt-8 text-left"
>
<FormItem>
<Label>
<Trans i18nKey={'common:formField:companyName'} />
</Label>
<Input {...register('companyName')} />
</FormItem>
<FormItem>
<Label>
<Trans i18nKey={'common:formField:contactPerson'} />
</Label>
<Input {...register('contactPerson')} />
</FormItem>
<FormItem>
<Label>
<Trans i18nKey={'common:formField:email'} />
</Label>
<Input type="email" {...register('email')}></Input>
</FormItem>
<FormItem>
<Label>
<Trans i18nKey={'common:formField:phone'} />
</Label>
<Input type="tel" {...register('phone')} />
</FormItem>
<SubmitButton
disabled={!isValid || isSubmitting}
pendingText="Saatmine..."
type="submit"
>
<Trans i18nKey={'account:requestCompanyAccount:button'} />
</SubmitButton>
</form>
</div>
<div className="w-1/2 min-w-[460px] bg-[url(/assets/med-report-logo-big.png)] bg-cover bg-center bg-no-repeat"></div>
</div>
);
}

View File

@@ -0,0 +1,16 @@
'use client';
import { SuccessNotification } from '@/packages/features/notifications/src/components';
export default function CompanyRegistrationSuccess() {
return (
<SuccessNotification
titleKey="account:requestCompanyAccount:successTitle"
descriptionKey="account:requestCompanyAccount:successDescription"
buttonProps={{
buttonTitleKey: 'account:requestCompanyAccount:successButton',
href: '/',
}}
/>
);
}

View File

@@ -1,89 +0,0 @@
"use client";
import { MedReportLogo } from "@/components/med-report-title";
import React from "react";
import { yupResolver } from "@hookform/resolvers/yup";
import { useForm } from "react-hook-form";
import { companySchema } from "@/lib/validations/companySchema";
import { CompanySubmitData } from "@/lib/types/company";
import { submitCompanyRegistration } from "@/lib/services/register-company.service";
import { useRouter } from "next/navigation";
import { Label } from "@kit/ui/label";
import { Input } from "@kit/ui/input";
import { SubmitButton } from "@/components/ui/submit-button";
import { FormItem } from "@kit/ui/form";
import { Trans } from "@kit/ui/trans";
export default function RegisterCompany() {
const router = useRouter();
const {
register,
handleSubmit,
formState: { errors, isValid, isSubmitting },
} = useForm({
resolver: yupResolver(companySchema),
mode: "onChange",
});
async function onSubmit(data: CompanySubmitData) {
const formData = new FormData();
Object.entries(data).forEach(([key, value]) => {
if (value !== undefined) formData.append(key, value);
});
try {
await submitCompanyRegistration(formData);
router.push("/register-company/success");
} catch (err: unknown) {
if (err instanceof Error) {
alert("Server validation error: " + err.message);
}
alert("Server validation error");
}
}
return (
<div className="flex flex-row border rounded-3xl border-border max-w-5xl overflow-hidden">
<div className="flex flex-col text-center py-14 px-12 w-1/2">
<MedReportLogo />
<h1 className="pt-8">Ettevõtte andmed</h1>
<p className="pt-2 text-muted-foreground text-sm">
Pakkumise saamiseks palun sisesta ettevõtte andmed millega MedReport
kasutada kavatsed.
</p>
<form
onSubmit={handleSubmit(onSubmit)}
noValidate
className="flex gap-6 flex-col text-left pt-8 px-6"
>
<FormItem>
<Label>Ettevõtte nimi</Label>
<Input {...register("companyName")} />
</FormItem>
<FormItem>
<Label>Kontaktisik</Label>
<Input {...register("contactPerson")} />
</FormItem>
<FormItem>
<Label>E-mail</Label>
<Input type="email" {...register("email")}></Input>
</FormItem>
<FormItem>
<Label>Telefon</Label>
<Input type="tel" {...register("phone")} />
</FormItem>
<SubmitButton
disabled={!isValid || isSubmitting}
pendingText="Saatmine..."
type="submit"
formAction={submitCompanyRegistration}
>
<Trans i18nKey={'account:requestCompanyAccount'} />
</SubmitButton>
</form>
</div>
<div className="w-1/2 min-w-[460px] bg-[url(/assets/med-report-logo-big.png)] bg-cover bg-center bg-no-repeat">
</div>
</div>
);
}

View File

@@ -1,26 +0,0 @@
import { MedReportLogo } from "@/components/med-report-title";
import { Button } from "@kit/ui/button";
import Image from "next/image";
import Link from "next/link";
export default function CompanyRegistrationSuccess() {
return (
<div className="pt-2 px-16 pb-12 border rounded-3xl border-border">
<MedReportLogo />
<div className="flex flex-col items-center px-4">
<Image
src="/assets/success.png"
alt="Success"
className="pt-6 pb-8"
width={326}
height={195}
/>
<h1 className="pb-2">Päring edukalt saadetud!</h1>
<p className=" text-muted-foreground text-sm">Saadame teile esimesel võimalusel vastuse</p>
</div>
<Button className="w-full mt-8">
<Link href="/">Tagasi kodulehele</Link>
</Button>
</div>
);
}

View File

@@ -1,6 +1,8 @@
import React from 'react';
import Link from 'next/link';
import { Button } from '@kit/ui/button';
import Link from "next/link";
import React from "react";
export default async function SignIn() {
return (
@@ -15,7 +17,7 @@ export default async function SignIn() {
<Link href="/">ID-Kaart</Link>
</Button>
<Button variant="outline">
<Link href="/register-company">Loo ettevõtte konto</Link>
<Link href="/company-offer">Loo ettevõtte konto</Link>
</Button>
</div>
);

View File

@@ -0,0 +1,9 @@
import { AuthLayoutShell } from '@kit/auth/shared';
import { AppLogo } from '~/components/app-logo';
function AuthLayout({ children }: React.PropsWithChildren) {
return <AuthLayoutShell Logo={AppLogo}>{children}</AuthLayoutShell>;
}
export default AuthLayout;

View File

@@ -0,0 +1,9 @@
import { AuthLayoutShell } from '@kit/auth/shared';
import { AppLogo } from '~/components/app-logo';
function AuthLayout({ children }: React.PropsWithChildren) {
return <AuthLayoutShell Logo={AppLogo}>{children}</AuthLayoutShell>;
}
export default AuthLayout;

View File

@@ -0,0 +1,9 @@
import { AuthLayoutShell } from '@kit/auth/shared';
import { AppLogo } from '~/components/app-logo';
function AuthLayout({ children }: React.PropsWithChildren) {
return <AuthLayoutShell Logo={AppLogo}>{children}</AuthLayoutShell>;
}
export default AuthLayout;

View File

@@ -1,5 +1,7 @@
import Link from 'next/link';
import { register } from 'module';
import { SignInMethodsContainer } from '@kit/auth/sign-in';
import { Button } from '@kit/ui/button';
import { Heading } from '@kit/ui/heading';
@@ -26,7 +28,8 @@ export const generateMetadata = async () => {
};
async function SignInPage({ searchParams }: SignInPageProps) {
const { invite_token: inviteToken, next = '' } = await searchParams;
const { invite_token: inviteToken, next = pathsConfig.app.home } =
await searchParams;
const signUpPath =
pathsConfig.auth.signUp +
@@ -36,6 +39,7 @@ async function SignInPage({ searchParams }: SignInPageProps) {
callback: pathsConfig.auth.callback,
returnPath: next ?? pathsConfig.app.home,
joinTeam: pathsConfig.app.joinTeam,
updateAccount: pathsConfig.auth.updateAccount,
};
return (

View File

@@ -0,0 +1,9 @@
import { AuthLayoutShell } from '@kit/auth/shared';
import { AppLogo } from '~/components/app-logo';
function AuthLayout({ children }: React.PropsWithChildren) {
return <AuthLayoutShell Logo={AppLogo}>{children}</AuthLayoutShell>;
}
export default AuthLayout;

View File

@@ -27,6 +27,7 @@ interface Props {
const paths = {
callback: pathsConfig.auth.callback,
appHome: pathsConfig.app.home,
updateAccount: pathsConfig.auth.updateAccount,
};
async function SignUpPage({ searchParams }: Props) {

View File

@@ -0,0 +1,11 @@
import { withI18n } from '~/lib/i18n/with-i18n';
async function SiteLayout(props: React.PropsWithChildren) {
return (
<div className={'flex min-h-[100vh] flex-col items-center justify-center'}>
{props.children}
</div>
);
}
export default withI18n(SiteLayout);

View File

@@ -0,0 +1,43 @@
import { redirect } from 'next/navigation';
import { BackButton } from '@/components/back-button';
import { MedReportLogo } from '@/components/med-report-logo';
import pathsConfig from '@/config/paths.config';
import { signOutAction } from '@/lib/actions/sign-out';
import { UpdateAccountForm } from '@/packages/features/auth/src/components/update-account-form';
import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client';
import { Trans } from '@kit/ui/trans';
import { withI18n } from '~/lib/i18n/with-i18n';
async function UpdateAccount() {
const client = getSupabaseServerClient();
const {
data: { user },
} = await client.auth.getUser();
if (!user) {
redirect(pathsConfig.auth.signIn);
}
return (
<div className="border-border flex max-w-5xl flex-row overflow-hidden rounded-3xl border">
<div className="relative flex w-1/2 min-w-md flex-col px-12 pt-7 pb-22 text-center">
<BackButton onBack={signOutAction} />
<MedReportLogo />
<h1 className="pt-8">
<Trans i18nKey={'account:updateAccount:title'} />
</h1>
<p className="text-muted-foreground pt-1 text-sm">
<Trans i18nKey={'account:updateAccount:description'} />
</p>
<UpdateAccountForm user={user} />
</div>
<div className="w-1/2 min-w-[460px] bg-[url(/assets/med-report-logo-big.png)] bg-cover bg-center bg-no-repeat"></div>
</div>
);
}
export default withI18n(UpdateAccount);

View File

@@ -0,0 +1,17 @@
import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client';
import { UpdateAccountSuccessNotification } from '@kit/notifications/components';
import { withI18n } from '~/lib/i18n/with-i18n';
async function UpdateAccountSuccess() {
const client = getSupabaseServerClient();
const {
data: { user },
} = await client.auth.getUser();
return <UpdateAccountSuccessNotification userId={user?.id} />;
}
export default withI18n(UpdateAccountSuccess);

View File

@@ -0,0 +1,9 @@
import { AuthLayoutShell } from '@kit/auth/shared';
import { AppLogo } from '~/components/app-logo';
function AuthLayout({ children }: React.PropsWithChildren) {
return <AuthLayoutShell Logo={AppLogo}>{children}</AuthLayoutShell>;
}
export default AuthLayout;

View 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>
</>
);
}

View File

@@ -1,7 +1,12 @@
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 { Trans } from '@kit/ui/trans';
import { Search } from '~/components/ui/search';
import { SIDEBAR_WIDTH } from '../../../../packages/ui/src/shadcn/constants';
// home imports
import { UserNotifications } from '../_components/user-notifications';
import { type UserWorkspace } from '../_lib/server/load-user-workspace';
@@ -10,31 +15,33 @@ import { ShoppingCart } from 'lucide-react';
export function HomeMenuNavigation(props: { workspace: UserWorkspace }) {
const { workspace, user, accounts } = 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 />
</div>
{/* searbar */}
<div className={'flex justify-end space-x-2.5 gap-2 items-center'}>
{/* TODO: implement account budget */}
<Button className='relative px-4 py-2 h-10 border-1 mr-0 cursor-pointer' variant={'ghost'}>
<span className='flex items-center text-nowrap'> 231,89</span>
</Button>
{/* TODO: implement cart */}
<Button className='relative px-4 py-2 h-10 border-1 mr-0 cursor-pointer gap-2' variant={'ghost'}>
<ShoppingCart size={16} />
<Search
className="flex grow"
startElement={<Trans i18nKey="common:search" values={{ end: '...' }} />}
/>
<Trans i18nKey="billing:cart.label" values={{ items: 0 }}/>
<div className="flex items-center justify-end gap-3">
<Button variant="ghost">
<ShoppingCart className="stroke-[1.5px]" />
<Trans i18nKey="common:shoppingCart" /> (0)
</Button>
<UserNotifications userId={user.id} />
<ProfileAccountDropdownContainer
user={user}
account={workspace}
accounts={accounts}
showProfileName={true}
/>
<div>
<ProfileAccountDropdownContainer
user={user}
account={workspace}
showProfileName
accounts={accounts}
/>
</div>
</div>
</div>
);

View File

@@ -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>
);
}

View File

@@ -2,6 +2,7 @@ import 'server-only';
import { SupabaseClient } from '@supabase/supabase-js';
import { Database } from '@/packages/supabase/src/database.types';
import { z } from 'zod';
import { createAccountsApi } from '@kit/accounts/api';
@@ -13,7 +14,6 @@ import { requireUser } from '@kit/supabase/require-user';
import appConfig from '~/config/app.config';
import billingConfig from '~/config/billing.config';
import pathsConfig from '~/config/paths.config';
import { Database } from '~/lib/database.types';
import { PersonalAccountCheckoutSchema } from '../schema/personal-account-checkout.schema';

View File

@@ -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>
);

View File

@@ -3,10 +3,12 @@ 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';
import { use } from 'react';
import { loadUserWorkspace } from './_lib/server/load-user-workspace';
import { PageBody } from '@kit/ui/page';
export const generateMetadata = async () => {
const i18n = await createI18nServerInstance();
@@ -23,16 +25,12 @@ function UserHomePage() {
<>
<HomeLayoutPageHeader
title={<Trans i18nKey={'common:routes.home'} />}
description={<Trans i18nKey={'common:homeTabDescription'} />}
description={<></>}
/>
{tempVisibleAccounts.length && (
<>
Member of companies:
<pre>{JSON.stringify(tempVisibleAccounts, null, 2)}</pre>
</>
)}
<PageBody>
<Dashboard />
</PageBody>
</>
);
}

View File

@@ -2,6 +2,7 @@ import 'server-only';
import { SupabaseClient } from '@supabase/supabase-js';
import { Database } from '@/packages/supabase/src/database.types';
import { z } from 'zod';
import { LineItemSchema } from '@kit/billing';
@@ -14,7 +15,6 @@ import { createTeamAccountsApi } from '@kit/team-accounts/api';
import appConfig from '~/config/app.config';
import billingConfig from '~/config/billing.config';
import pathsConfig from '~/config/paths.config';
import { Database } from '~/lib/database.types';
import { TeamCheckoutSchema } from '../schema/team-billing.schema';

View File

@@ -2,8 +2,9 @@ import 'server-only';
import { SupabaseClient } from '@supabase/supabase-js';
import { Database } from '@/packages/supabase/src/database.types';
import { loadTeamWorkspace } from '~/home/[account]/_lib/server/team-account-workspace.loader';
import { Database } from '~/lib/database.types';
/**
* Load data for the members page

View File

@@ -1,6 +1,6 @@
import Link from 'next/link';
import { MedReportLogo } from './med-report-title';
import { MedReportLogo } from './med-report-logo';
function LogoImage({
className,

View File

@@ -0,0 +1,31 @@
'use client';
import { useRouter } from 'next/navigation';
import { ArrowLeft } from '@/public/assets/arrow-left';
import { Trans } from '@kit/ui/trans';
export function BackButton({ onBack }: { onBack?: () => void }) {
const router = useRouter();
return (
<form
action={() => {
if (onBack) {
onBack();
} else {
router.back();
}
}}
>
<button className="absolute top-4 left-4 flex cursor-pointer flex-row items-center gap-3">
<div className="flex items-center justify-center rounded-sm border p-3">
<ArrowLeft />
</div>
<span className="text-sm">
<Trans i18nKey="common:goBack" />
</span>
</button>
</form>
);
}

View File

@@ -1,9 +1,11 @@
import { signOutAction } from "@/lib/actions/sign-out";
import { hasEnvVars } from "@/utils/supabase/check-env-vars";
import Link from "next/link";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { createClient } from "@/utils/supabase/server";
import Link from 'next/link';
import { signOutAction } from '@/lib/actions/sign-out';
import { hasEnvVars } from '@/utils/supabase/check-env-vars';
import { createClient } from '@/utils/supabase/server';
import { Badge } from '@kit/ui/badge';
import { Button } from '@kit/ui/button';
export default async function AuthButton() {
const supabase = await createClient();
@@ -15,11 +17,11 @@ export default async function AuthButton() {
if (!hasEnvVars) {
return (
<>
<div className="flex gap-4 items-center">
<div className="flex items-center gap-4">
<div>
<Badge
variant={"default"}
className="font-normal pointer-events-none"
variant={'default'}
className="pointer-events-none font-normal"
>
Please update .env.local file with anon key and url
</Badge>
@@ -28,18 +30,18 @@ export default async function AuthButton() {
<Button
asChild
size="sm"
variant={"outline"}
variant={'outline'}
disabled
className="opacity-75 cursor-none pointer-events-none"
className="pointer-events-none cursor-none opacity-75"
>
<Link href="/sign-in">Sign in</Link>
</Button>
<Button
asChild
size="sm"
variant={"default"}
variant={'default'}
disabled
className="opacity-75 cursor-none pointer-events-none"
className="pointer-events-none cursor-none opacity-75"
>
<Link href="example/sign-up">Sign up</Link>
</Button>
@@ -52,14 +54,14 @@ export default async function AuthButton() {
<div className="flex items-center gap-4">
Hey, {user.email}!
<form action={signOutAction}>
<Button type="submit" variant={"outline"}>
<Button type="submit" variant={'outline'}>
Sign out
</Button>
</form>
</div>
) : (
<div className="flex gap-2">
<Button asChild size="sm" variant={"outline"}>
<Button asChild size="sm" variant={'outline'}>
<Link href="/sign-in">Sign in</Link>
</Button>
</div>

View 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
View 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 };

View File

@@ -8,9 +8,17 @@ const PathsSchema = z.object({
callback: z.string().min(1),
passwordReset: z.string().min(1),
passwordUpdate: z.string().min(1),
updateAccount: z.string().min(1),
updateAccountSuccess: z.string().min(1),
}),
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),
@@ -31,6 +39,8 @@ const pathsConfig = PathsSchema.parse({
callback: '/auth/callback',
passwordReset: '/auth/password-reset',
passwordUpdate: '/update-password',
updateAccount: '/auth/update-account',
updateAccountSuccess: '/auth/update-account/success',
},
app: {
home: '/home',
@@ -43,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>);

View File

@@ -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',
});

View File

@@ -1,10 +1,11 @@
"use server";
'use server';
import { createClient } from "@/utils/supabase/server";
import { redirect } from "next/navigation";
import { redirect } from 'next/navigation';
import { createClient } from '@/utils/supabase/server';
export const signOutAction = async () => {
const supabase = await createClient();
await supabase.auth.signOut();
return redirect("/sign-in");
return redirect('/');
};

View File

@@ -32,6 +32,7 @@ export const defaultI18nNamespaces = [
'teams',
'billing',
'marketing',
'dashboard',
];
/**

View File

@@ -0,0 +1,42 @@
'use server';
import { getMailer } from '@kit/mailers';
import { enhanceAction } from '@kit/next/actions';
import { CompanySubmitData } from '../types/company';
import { emailSchema } from '../validations/email.schema';
export const sendCompanyOfferEmail = async (
data: CompanySubmitData,
language: string,
) => {
const { renderCompanyOfferEmail } = await import('@kit/email-templates');
const { html, subject, to } = await renderCompanyOfferEmail({
language,
companyData: data,
});
await sendEmail({
subject,
html,
to,
});
};
export const sendEmail = enhanceAction(
async ({ subject, html, to }) => {
const mailer = await getMailer();
await mailer.sendEmail({
to,
subject,
html,
});
return {};
},
{
schema: emailSchema,
auth: false,
},
);

View File

@@ -1,31 +0,0 @@
"use server";
import * as yup from "yup";
import { companySchema } from "@/lib/validations/companySchema";
export async function submitCompanyRegistration(formData: FormData) {
const data = {
companyName: formData.get("companyName")?.toString() || "",
contactPerson: formData.get("contactPerson")?.toString() || "",
email: formData.get("email")?.toString() || "",
phone: formData.get("phone")?.toString() || "",
};
try {
await companySchema.validate(data, { abortEarly: false });
console.log("Valid data:", data);
} catch (validationError) {
if (validationError instanceof yup.ValidationError) {
const errors = validationError.inner.map((err) => ({
path: err.path,
message: err.message,
}));
throw new Error(
"Validation failed: " +
errors.map((e) => `${e.path}: ${e.message}`).join(", ")
);
}
throw validationError;
}
}

View File

@@ -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(),
);
}

View File

@@ -0,0 +1,16 @@
import { z } from 'zod';
export const companyOfferSchema = z.object({
companyName: z.string({
required_error: 'Company name is required',
}),
contactPerson: z.string({
required_error: 'Contact person is required',
}),
email: z
.string({
required_error: 'Email is required',
})
.email('Invalid email'),
phone: z.string().optional(),
});

View File

@@ -1,8 +0,0 @@
import * as yup from "yup";
export const companySchema = yup.object({
companyName: yup.string().required("Company name is required"),
contactPerson: yup.string().required("Contact person is required"),
email: yup.string().email("Invalid email").required("Email is required"),
phone: yup.string().optional(),
});

View File

@@ -0,0 +1,7 @@
import { z } from 'zod';
export const emailSchema = z.object({
to: z.string().email(),
subject: z.string().min(1).max(200),
html: z.string().min(1).max(5000),
});

View File

@@ -139,7 +139,7 @@ function getPatterns() {
handler: adminMiddleware,
},
{
pattern: new URLPattern({ pathname: '/auth/*?' }),
pattern: new URLPattern({ pathname: '/auth/update-account' }),
handler: async (req: NextRequest, res: NextResponse) => {
const {
data: { user },
@@ -147,21 +147,27 @@ function getPatterns() {
// the user is logged out, so we don't need to do anything
if (!user) {
return;
return NextResponse.redirect(new URL('/', req.nextUrl.origin).href);
}
// check if we need to verify MFA (user is authenticated but needs to verify MFA)
const isVerifyMfa = req.nextUrl.pathname === pathsConfig.auth.verifyMfa;
const client = createMiddlewareClient(req, res);
const userIsSuperAdmin = await isSuperAdmin(client);
// If user is logged in and does not need to verify MFA,
// redirect to home page.
if (!isVerifyMfa) {
const nextPath =
req.nextUrl.searchParams.get('next') ?? pathsConfig.app.home;
if (userIsSuperAdmin) {
// check if we need to verify MFA (user is authenticated but needs to verify MFA)
const isVerifyMfa =
req.nextUrl.pathname === pathsConfig.auth.verifyMfa;
return NextResponse.redirect(
new URL(nextPath, req.nextUrl.origin).href,
);
// If user is logged in and does not need to verify MFA,
// redirect to home page.
if (!isVerifyMfa) {
const nextPath =
req.nextUrl.searchParams.get('next') ?? pathsConfig.app.home;
return NextResponse.redirect(
new URL(nextPath, req.nextUrl.origin).href,
);
}
}
},
},

View File

@@ -25,9 +25,7 @@
"supabase:db:lint": "supabase db lint",
"supabase:db:diff": "supabase db diff",
"supabase:deploy": "supabase link --project-ref $SUPABASE_PROJECT_REF && supabase db push",
"supabase:typegen": "pnpm run supabase:typegen:packages && pnpm run supabase:typegen:app",
"supabase:typegen:packages": "supabase gen types typescript --local > ../../packages/supabase/src/database.types.ts",
"supabase:typegen:app": "supabase gen types typescript --local > ./supabase/database.types.ts",
"supabase:typegen": "supabase gen types typescript --local > ./packages/supabase/src/database.types.ts",
"supabase:db:dump:local": "supabase db dump --local --data-only",
"sync-analysis-groups:dev": "NODE_ENV=local ts-node jobs/sync-analysis-groups.ts",
"sync-connected-online:dev": "NODE_ENV=local ts-node jobs/sync-connected-online.ts"

View File

@@ -0,0 +1,90 @@
import {
Body,
Head,
Html,
Preview,
Tailwind,
Text,
render,
} from '@react-email/components';
import { BodyStyle } from '../components/body-style';
import { EmailContent } from '../components/content';
import { EmailHeader } from '../components/header';
import { EmailHeading } from '../components/heading';
import { EmailWrapper } from '../components/wrapper';
import { initializeEmailI18n } from '../lib/i18n';
export async function renderCompanyOfferEmail({
language,
companyData,
}: {
language?: string;
companyData: {
companyName: string;
contactPerson: string;
email: string;
phone?: string;
};
}) {
const namespace = 'company-offer-email';
const { t } = await initializeEmailI18n({
language,
namespace,
});
const to = process.env.CONTACT_EMAIL || '';
const previewText = t(`${namespace}:previewText`, {
companyName: companyData.companyName,
});
const subject = t(`${namespace}:subject`, {
companyName: companyData.companyName,
});
const html = await render(
<Html>
<Head>
<BodyStyle />
</Head>
<Preview>{previewText}</Preview>
<Tailwind>
<Body>
<EmailWrapper>
<EmailHeader>
<EmailHeading>{previewText}</EmailHeading>
</EmailHeader>
<EmailContent>
<Text className="text-[16px] leading-[24px] text-[#242424]">
{t(`${namespace}:companyName`)} {companyData.companyName}
</Text>
<Text className="text-[16px] leading-[24px] text-[#242424]">
{t(`${namespace}:contactPerson`)} {companyData.contactPerson}
</Text>
<Text className="text-[16px] leading-[24px] text-[#242424]">
{t(`${namespace}:email`)} {companyData.email}
</Text>
<Text className="text-[16px] leading-[24px] text-[#242424]">
{t(`${namespace}:phone`)} {companyData.phone || 'N/A'}
</Text>
</EmailContent>
</EmailWrapper>
</Body>
</Tailwind>
</Html>,
);
return {
html,
subject,
to,
};
}

View File

@@ -1,3 +1,4 @@
export * from './emails/invite.email';
export * from './emails/account-delete.email';
export * from './emails/otp.email';
export * from './emails/company-offer.email';

View File

@@ -0,0 +1,8 @@
{
"subject": "Uus ettevõtte liitumispäring",
"previewText": "Ettevõte {{companyName}} soovib pakkumist",
"companyName": "Ettevõtte nimi:",
"contactPerson": "Kontaktisik:",
"email": "E-mail:",
"phone": "Telefon:"
}

View File

@@ -26,7 +26,8 @@ export function usePersonalAccountData(
`
id,
name,
picture_url
picture_url,
last_name
`,
)
.eq('primary_owner_user_id', userId)

View File

@@ -215,7 +215,6 @@ async function TeamAccountPage(props: {
<div>
<div className={'flex flex-col gap-y-8'}>
<div className={'flex flex-col gap-y-2.5'}>
<Heading level={6}>Company Members</Heading>

View File

@@ -4,6 +4,8 @@ import { useRouter } from 'next/navigation';
import type { Provider } from '@supabase/supabase-js';
import { useSupabase } from '@/packages/supabase/src/hooks/use-supabase';
import { isBrowser } from '@kit/shared/utils';
import { If } from '@kit/ui/if';
import { Separator } from '@kit/ui/separator';
@@ -20,6 +22,7 @@ export function SignInMethodsContainer(props: {
callback: string;
joinTeam: string;
returnPath: string;
updateAccount: string;
};
providers: {
@@ -28,13 +31,14 @@ export function SignInMethodsContainer(props: {
oAuth: Provider[];
};
}) {
const client = useSupabase();
const router = useRouter();
const redirectUrl = isBrowser()
? new URL(props.paths.callback, window?.location.origin).toString()
: '';
const onSignIn = () => {
const onSignIn = async (userId?: string) => {
// if the user has an invite token, we should join the team
if (props.inviteToken) {
const searchParams = new URLSearchParams({
@@ -45,8 +49,28 @@ export function SignInMethodsContainer(props: {
router.replace(joinTeamPath);
} else {
// otherwise, we should redirect to the return path
router.replace(props.paths.returnPath);
if (!userId) {
router.replace(props.paths.callback);
return;
}
try {
const { data: hasPersonalCode } = await client.rpc(
'has_personal_code',
{
account_id: userId,
},
);
if (hasPersonalCode) {
router.replace(props.paths.returnPath);
} else {
router.replace(props.paths.updateAccount);
}
} catch {
router.replace(props.paths.callback);
return;
}
}
};

View File

@@ -17,6 +17,7 @@ export function SignUpMethodsContainer(props: {
paths: {
callback: string;
appHome: string;
updateAccount: string;
};
providers: {

View File

@@ -0,0 +1,225 @@
'use client';
import Link from 'next/link';
import { User } from '@supabase/supabase-js';
import { ExternalLink } from '@/public/assets/external-link';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { Button } from '@kit/ui/button';
import { Checkbox } from '@kit/ui/checkbox';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@kit/ui/form';
import { Input } from '@kit/ui/input';
import { Trans } from '@kit/ui/trans';
import { UpdateAccountSchema } from '../schemas/update-account.schema';
import { onUpdateAccount } from '../server/actions/update-account-actions';
export function UpdateAccountForm({ user }: { user: User }) {
const form = useForm({
resolver: zodResolver(UpdateAccountSchema),
mode: 'onChange',
defaultValues: {
firstName: '',
lastName: '',
personalCode: '',
email: user.email,
phone: '',
city: '',
weight: 0,
height: 0,
userConsent: false,
},
});
return (
<Form {...form}>
<form
className="flex flex-col gap-6 px-6 pt-10 text-left"
onSubmit={form.handleSubmit(onUpdateAccount)}
>
<FormField
name="firstName"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans i18nKey={'common:formField:firstName'} />
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="lastName"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans i18nKey={'common:formField:lastName'} />
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="personalCode"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans i18nKey={'common:formField:personalCode'} />
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans i18nKey={'common:formField:email'} />
</FormLabel>
<FormControl>
<Input {...field} disabled />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="phone"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans i18nKey={'common:formField:phone'} />
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="city"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans i18nKey={'common:formField:city'} />
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex flex-row justify-between gap-4">
<FormField
name="weight"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans i18nKey={'common:formField:weight'} />
</FormLabel>
<FormControl>
<Input
type="number"
placeholder="kg"
{...field}
value={field.value ?? ''}
onChange={(e) =>
field.onChange(
e.target.value === '' ? null : Number(e.target.value),
)
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="height"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans i18nKey={'common:formField:height'} />
</FormLabel>
<FormControl>
<Input
placeholder="cm"
type="number"
{...field}
value={field.value ?? ''}
onChange={(e) =>
field.onChange(
e.target.value === '' ? null : Number(e.target.value),
)
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
name="userConsent"
render={({ field }) => (
<FormItem>
<div className="flex flex-row items-center gap-2 pb-1">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel>
<Trans i18nKey={'account:updateAccount:userConsentLabel'} />
</FormLabel>
</div>
<Link
href={''}
className="flex flex-row items-center gap-2 text-sm hover:underline"
target="_blank"
>
<ExternalLink />
<Trans i18nKey={'account:updateAccount:userConsentUrlTitle'} />
</Link>
</FormItem>
)}
/>
<Button disabled={form.formState.isSubmitting} type="submit">
<Trans i18nKey={'account:updateAccount:button'} />
</Button>
</form>
</Form>
);
}

View File

@@ -0,0 +1,44 @@
import { z } from 'zod';
export const UpdateAccountSchema = z.object({
firstName: z
.string({
required_error: 'First name is required',
})
.nonempty(),
lastName: z
.string({
required_error: 'Last name is required',
})
.nonempty(),
personalCode: z
.string({
required_error: 'Personal code is required',
})
.nonempty(),
email: z.string().email({
message: 'Email is required',
}),
phone: z
.string({
required_error: 'Phone number is required',
})
.nonempty(),
city: z.string().optional(),
weight: z
.number({
required_error: 'Weight is required',
invalid_type_error: 'Weight must be a number',
})
.gt(0, { message: 'Weight must be greater than 0' }),
height: z
.number({
required_error: 'Height is required',
invalid_type_error: 'Height must be a number',
})
.gt(0, { message: 'Height must be greater than 0' }),
userConsent: z.boolean().refine((val) => val === true, {
message: 'Must be true',
}),
});

View File

@@ -0,0 +1,44 @@
'use server';
import { redirect } from 'next/navigation';
import { enhanceAction } from '@kit/next/actions';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import pathsConfig from '~/config/paths.config';
import { UpdateAccountSchema } from '../../schemas/update-account.schema';
import { createAuthApi } from '../api';
export interface AccountSubmitData {
firstName: string;
lastName: string;
personalCode: string;
email: string;
phone?: string;
city?: string;
weight: number | null;
height: number | null;
userConsent: boolean;
}
export const onUpdateAccount = enhanceAction(
async (params) => {
const client = getSupabaseServerClient();
const api = createAuthApi(client);
try {
await api.updateAccount(params);
console.log('SUCCESS', pathsConfig.auth.updateAccountSuccess);
} catch (err: unknown) {
if (err instanceof Error) {
console.warn('On update account error: ' + err.message);
}
console.warn('On update account error: ', err);
}
redirect(pathsConfig.auth.updateAccountSuccess);
},
{
schema: UpdateAccountSchema,
},
);

View File

@@ -0,0 +1,93 @@
import { SupabaseClient } from '@supabase/supabase-js';
import { Database } from '@kit/supabase/database';
import { AccountSubmitData } from './actions/update-account-actions';
/**
* Class representing an API for interacting with user accounts.
* @constructor
* @param {SupabaseClient<Database>} client - The Supabase client instance.
*/
class AuthApi {
constructor(private readonly client: SupabaseClient<Database>) {}
/**
* @name hasPersonalCode
* @description Check if given account ID has added personal code.
* @param id
*/
async hasPersonalCode(id: string) {
const { data, error } = await this.client.rpc('has_personal_code', {
account_id: id,
});
if (error) {
throw error;
}
return data;
}
/**
* @name updateAccount
* @description Update required fields for the account.
* @param data
*/
async updateAccount(data: AccountSubmitData) {
const {
data: { user },
} = await this.client.auth.getUser();
if (!user) {
throw new Error('User not authenticated');
}
const { error } = await this.client.rpc('update_account', {
p_name: data.firstName,
p_last_name: data.lastName,
p_personal_code: data.personalCode,
p_phone: data.phone || '',
p_city: data.city || '',
p_has_consent_personal_data: data.userConsent,
p_uid: user.id,
});
if (error) {
throw error;
}
if (data.height || data.weight) {
await this.updateAccountParams(data);
}
}
/**
* @name updateAccountParams
* @description Update account parameters.
* @param data
*/
async updateAccountParams(data: AccountSubmitData) {
const {
data: { user },
} = await this.client.auth.getUser();
if (!user) {
throw new Error('User not authenticated');
}
console.log('test', user, data);
const response = await this.client.from('account_params').insert({
account_id: user.id,
height: data.height,
weight: data.weight,
});
if (response.error) {
throw response.error;
}
}
}
export function createAuthApi(client: SupabaseClient<Database>) {
return new AuthApi(client);
}

View File

@@ -1,2 +1,3 @@
export * from './components/sign-up-methods-container';
export * from './schemas/password-sign-up.schema';
export * from './components/update-account-form';

View File

@@ -1,5 +1,5 @@
{
"extends": "@kit/tsconfig/base.json",
"extends": "../../../tsconfig.json",
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},

View File

@@ -1 +1,3 @@
export * from './notifications-popover';
export * from './success-notification';
export * from './update-account-success-notification';

View File

@@ -126,7 +126,7 @@ export function NotificationsPopover(params: {
<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) {

View File

@@ -0,0 +1,50 @@
import Image from 'next/image';
import Link from 'next/link';
import { MedReportLogo } from '@/components/med-report-logo';
import { Button } from '@kit/ui/button';
import { Trans } from '@kit/ui/trans';
export const SuccessNotification = ({
showLogo = true,
title,
titleKey,
descriptionKey,
buttonProps,
}: {
showLogo?: boolean;
title?: string;
titleKey?: string;
descriptionKey?: string;
buttonProps?: {
buttonTitleKey: string;
href: string;
};
}) => {
return (
<div className="border-border rounded-3xl border px-16 pt-4 pb-12">
{showLogo && <MedReportLogo />}
<div className="flex flex-col items-center px-4">
<Image
src="/assets/success.png"
alt="Success"
className="pt-6 pb-8"
width={326}
height={195}
/>
<h1 className="pb-2">{title || <Trans i18nKey={titleKey} />}</h1>
<p className="text-muted-foreground text-sm">
<Trans i18nKey={descriptionKey} />
</p>
</div>
{buttonProps && (
<Button className="mt-8 w-full">
<Link href={buttonProps.href}>
<Trans i18nKey={buttonProps.buttonTitleKey} />
</Link>
</Button>
)}
</div>
);
};

View File

@@ -0,0 +1,39 @@
'use client';
import { redirect } from 'next/navigation';
import pathsConfig from '@/config/paths.config';
import { useTranslation } from 'react-i18next';
import { usePersonalAccountData } from '@kit/accounts/hooks/use-personal-account-data';
import { SuccessNotification } from './success-notification';
export const UpdateAccountSuccessNotification = ({
userId,
}: {
userId?: string;
}) => {
const { t } = useTranslation('account');
if (!userId) {
redirect(pathsConfig.app.home);
}
const { data: accountData } = usePersonalAccountData(userId);
return (
<SuccessNotification
showLogo={false}
title={t('account:updateAccount:successTitle', {
firstName: accountData?.name,
lastName: accountData?.last_name,
})}
descriptionKey="account:updateAccount:successDescription"
buttonProps={{
buttonTitleKey: 'account:updateAccount:successButton',
href: pathsConfig.app.home,
}}
/>
);
};

View File

@@ -1,5 +1,5 @@
{
"extends": "@kit/tsconfig/base.json",
"extends": "../../../tsconfig.json",
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},

View File

@@ -5,7 +5,7 @@ export const MailerSchema = z
to: z.string().email(),
// this is not necessarily formatted
// as an email so we type it loosely
from: z.string().min(1),
from: z.string().min(1).optional(),
subject: z.string(),
})
.and(

View File

@@ -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: {
@@ -116,15 +159,43 @@ export type Database = {
}
public: {
Tables: {
account_params: {
Row: {
account_id: string
height: number | null
id: string
recorded_at: string
weight: number | null
}
Insert: {
account_id?: string
height?: number | null
id?: string
recorded_at?: string
weight?: number | null
}
Update: {
account_id?: string
height?: number | null
id?: string
recorded_at?: string
weight?: number | null
}
Relationships: []
}
accounts: {
Row: {
city: string | null
created_at: string | null
created_by: string | null
email: string | null
has_consent_personal_data: boolean | null
id: string
is_personal_account: boolean
last_name: string | null
name: string
personal_code: string | null
phone: string | null
picture_url: string | null
primary_owner_user_id: string
public_data: Json
@@ -133,13 +204,17 @@ export type Database = {
updated_by: string | null
}
Insert: {
city?: string | null
created_at?: string | null
created_by?: string | null
email?: string | null
has_consent_personal_data?: boolean | null
id?: string
is_personal_account?: boolean
last_name?: string | null
name: string
personal_code?: string | null
phone?: string | null
picture_url?: string | null
primary_owner_user_id?: string
public_data?: Json
@@ -148,13 +223,17 @@ export type Database = {
updated_by?: string | null
}
Update: {
city?: string | null
created_at?: string | null
created_by?: string | null
email?: string | null
has_consent_personal_data?: boolean | null
id?: string
is_personal_account?: boolean
last_name?: string | null
name?: string
personal_code?: string | null
phone?: string | null
picture_url?: string | null
primary_owner_user_id?: string
public_data?: Json
@@ -200,20 +279,6 @@ export type Database = {
referencedRelation: "accounts"
referencedColumns: ["id"]
},
{
foreignKeyName: "accounts_memberships_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "invitations_with_accounts"
referencedColumns: ["account_id"]
},
{
foreignKeyName: "accounts_memberships_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "invitations_with_personal_accounts"
referencedColumns: ["account_id"]
},
{
foreignKeyName: "accounts_memberships_account_id_fkey"
columns: ["account_id"]
@@ -515,20 +580,6 @@ export type Database = {
referencedRelation: "accounts"
referencedColumns: ["id"]
},
{
foreignKeyName: "billing_customers_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "invitations_with_accounts"
referencedColumns: ["account_id"]
},
{
foreignKeyName: "billing_customers_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "invitations_with_personal_accounts"
referencedColumns: ["account_id"]
},
{
foreignKeyName: "billing_customers_account_id_fkey"
columns: ["account_id"]
@@ -627,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
@@ -672,20 +875,6 @@ export type Database = {
referencedRelation: "accounts"
referencedColumns: ["id"]
},
{
foreignKeyName: "invitations_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "invitations_with_accounts"
referencedColumns: ["account_id"]
},
{
foreignKeyName: "invitations_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "invitations_with_personal_accounts"
referencedColumns: ["account_id"]
},
{
foreignKeyName: "invitations_account_id_fkey"
columns: ["account_id"]
@@ -709,6 +898,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
@@ -808,20 +1120,6 @@ export type Database = {
referencedRelation: "accounts"
referencedColumns: ["id"]
},
{
foreignKeyName: "notifications_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "invitations_with_accounts"
referencedColumns: ["account_id"]
},
{
foreignKeyName: "notifications_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "invitations_with_personal_accounts"
referencedColumns: ["account_id"]
},
{
foreignKeyName: "notifications_account_id_fkey"
columns: ["account_id"]
@@ -921,20 +1219,6 @@ export type Database = {
referencedRelation: "accounts"
referencedColumns: ["id"]
},
{
foreignKeyName: "orders_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "invitations_with_accounts"
referencedColumns: ["account_id"]
},
{
foreignKeyName: "orders_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "invitations_with_personal_accounts"
referencedColumns: ["account_id"]
},
{
foreignKeyName: "orders_account_id_fkey"
columns: ["account_id"]
@@ -1106,20 +1390,6 @@ export type Database = {
referencedRelation: "accounts"
referencedColumns: ["id"]
},
{
foreignKeyName: "subscriptions_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "invitations_with_accounts"
referencedColumns: ["account_id"]
},
{
foreignKeyName: "subscriptions_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "invitations_with_personal_accounts"
referencedColumns: ["account_id"]
},
{
foreignKeyName: "subscriptions_account_id_fkey"
columns: ["account_id"]
@@ -1145,23 +1415,6 @@ export type Database = {
}
}
Views: {
invitations_with_accounts: {
Row: {
account_id: string | null
invite_token: string | null
personal_code: string | null
}
Relationships: []
}
invitations_with_personal_accounts: {
Row: {
account_id: string | null
account_slug: string | null
invite_token: string | null
personal_code: string | null
}
Relationships: []
}
user_account_workspace: {
Row: {
id: string | null
@@ -1241,13 +1494,17 @@ export type Database = {
create_team_account: {
Args: { account_name: string }
Returns: {
city: string | null
created_at: string | null
created_by: string | null
email: string | null
has_consent_personal_data: boolean | null
id: string
is_personal_account: boolean
last_name: string | null
name: string
personal_code: string | null
phone: string | null
picture_url: string | null
primary_owner_user_id: string
public_data: Json
@@ -1329,6 +1586,10 @@ export type Database = {
}
Returns: boolean
}
has_personal_code: {
Args: { account_id: string }
Returns: boolean
}
has_role_on_account: {
Args: { account_id: string; account_role?: string }
Returns: boolean
@@ -1395,6 +1656,18 @@ export type Database = {
Args: { target_account_id: string; new_owner_id: string }
Returns: undefined
}
update_account: {
Args: {
p_name: string
p_last_name: string
p_personal_code: string
p_phone: string
p_city: string
p_has_consent_personal_data: boolean
p_uid: string
}
Returns: undefined
}
upsert_order: {
Args: {
target_account_id: string
@@ -1611,6 +1884,7 @@ export type CompositeTypes<
export const Constants = {
audit: {
Enums: {
request_status: ["SUCCESS", "FAIL"],
sync_status: ["SUCCESS", "FAIL"],
},
},

View File

@@ -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])),
});

View File

@@ -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-bg-background border-1 light:border-border dark:border-border dark:shadow-primary/10 flex h-15 items-center justify-between px-4 py-1 lg:justify-start lg:shadow-xs',
'bg-bg-background border-1 light:border-border dark:border-border dark:shadow-primary/10 flex h-15 items-center justify-between px-4 py-1 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;

View File

@@ -70,7 +70,7 @@ export function VersionUpdater(props: { intervalTimeInSecond?: number }) {
setDismissed(true);
}}
>
<Trans i18nKey="common:back" />
<Trans i18nKey="common:goBack" />
</Button>
<Button onClick={() => window.location.reload()}>

View File

@@ -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: {

View File

@@ -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';

View File

@@ -12,7 +12,7 @@ const Checkbox: React.FC<
> = ({ className, ...props }) => (
<CheckboxPrimitive.Root
className={cn(
'peer border-primary focus-visible:ring-ring data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground h-4 w-4 shrink-0 rounded-xs border shadow-xs focus-visible:ring-1 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
'peer border-primary focus-visible:ring-ring data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground h-4 w-4 shrink-0 rounded-sm border shadow-xs focus-visible:ring-1 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
{...props}

View File

@@ -0,0 +1,3 @@
export const SIDEBAR_WIDTH = '16rem';
export const SIDEBAR_WIDTH_MOBILE = '18rem';
export const SIDEBAR_WIDTH_ICON = '4rem';

View File

@@ -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,
},

View File

@@ -1,5 +1,5 @@
{
"extends": "@kit/tsconfig/base.json",
"extends": "../../tsconfig.json",
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json",
"paths": {

View File

@@ -0,0 +1,28 @@
export const ArrowLeft = () => (
<svg
width="16"
height="17"
viewBox="0 0 16 17"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<mask id="path-1-inside-1_583_1979" fill="white">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M9.40588 4.96054C9.59333 5.14799 9.59333 5.45191 9.40588 5.63937L6.54529 8.49995L9.40588 11.3605C9.59333 11.548 9.59333 11.8519 9.40588 12.0394C9.21843 12.2268 8.91451 12.2268 8.72706 12.0394L5.52706 8.83937C5.43705 8.74934 5.38647 8.62725 5.38647 8.49995C5.38647 8.37265 5.43705 8.25055 5.52706 8.16054L8.72706 4.96054C8.91451 4.77308 9.21843 4.77308 9.40588 4.96054Z"
/>
</mask>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M9.40588 4.96054C9.59333 5.14799 9.59333 5.45191 9.40588 5.63937L6.54529 8.49995L9.40588 11.3605C9.59333 11.548 9.59333 11.8519 9.40588 12.0394C9.21843 12.2268 8.91451 12.2268 8.72706 12.0394L5.52706 8.83937C5.43705 8.74934 5.38647 8.62725 5.38647 8.49995C5.38647 8.37265 5.43705 8.25055 5.52706 8.16054L8.72706 4.96054C8.91451 4.77308 9.21843 4.77308 9.40588 4.96054Z"
fill="#09090B"
/>
<path
d="M9.40588 4.96054L8.69875 5.66762L8.69877 5.66765L9.40588 4.96054ZM9.40588 5.63937L10.113 6.34647V6.34647L9.40588 5.63937ZM6.54529 8.49995L5.83818 7.79285L5.13108 8.49995L5.83818 9.20706L6.54529 8.49995ZM9.40588 11.3605L8.69877 12.0677L8.69885 12.0677L9.40588 11.3605ZM9.40588 12.0394L10.1129 12.7465L10.1131 12.7464L9.40588 12.0394ZM8.72706 12.0394L8.01995 12.7465L8.02001 12.7465L8.72706 12.0394ZM5.52706 8.83937L4.81991 9.54643L4.81995 9.54647L5.52706 8.83937ZM5.52706 8.16054L6.23417 8.86765H6.23417L5.52706 8.16054ZM8.72706 4.96054L9.43417 5.66765L9.43419 5.66762L8.72706 4.96054ZM9.40588 4.96054L8.69877 5.66765C8.4957 5.46458 8.4957 5.13533 8.69877 4.93226L9.40588 5.63937L10.113 6.34647C10.691 5.76849 10.691 4.83141 10.113 4.25343L9.40588 4.96054ZM9.40588 5.63937L8.69877 4.93226L5.83818 7.79285L6.54529 8.49995L7.2524 9.20706L10.113 6.34647L9.40588 5.63937ZM6.54529 8.49995L5.83818 9.20706L8.69877 12.0677L9.40588 11.3605L10.113 10.6534L7.2524 7.79285L6.54529 8.49995ZM9.40588 11.3605L8.69885 12.0677C8.49556 11.8645 8.49585 11.5353 8.69865 11.3324L9.40588 12.0394L10.1131 12.7464C10.6908 12.1685 10.6911 11.2314 10.1129 10.6534L9.40588 11.3605ZM9.40588 12.0394L8.69883 11.3322C8.9019 11.1292 9.23104 11.1292 9.43411 11.3322L8.72706 12.0394L8.02001 12.7465C8.59797 13.3244 9.53497 13.3244 10.1129 12.7465L9.40588 12.0394ZM8.72706 12.0394L9.43417 11.3323L6.23417 8.13226L5.52706 8.83937L4.81995 9.54647L8.01995 12.7465L8.72706 12.0394ZM5.52706 8.83937L6.23421 8.1323C6.33168 8.22978 6.38647 8.36202 6.38647 8.49995H5.38647H4.38647C4.38647 8.89248 4.54241 9.26889 4.81991 9.54643L5.52706 8.83937ZM5.38647 8.49995H6.38647C6.38647 8.63785 6.3317 8.77012 6.23417 8.86765L5.52706 8.16054L4.81995 7.45343C4.5424 7.73099 4.38647 8.10744 4.38647 8.49995H5.38647ZM5.52706 8.16054L6.23417 8.86765L9.43417 5.66765L8.72706 4.96054L8.01995 4.25343L4.81995 7.45343L5.52706 8.16054ZM8.72706 4.96054L9.43419 5.66762C9.2311 5.87072 8.90183 5.87072 8.69875 5.66762L9.40588 4.96054L10.113 4.25345C9.53503 3.67544 8.59791 3.67544 8.01993 4.25345L8.72706 4.96054Z"
fill="#18181B"
mask="url(#path-1-inside-1_583_1979)"
/>
</svg>
);

View File

@@ -0,0 +1,16 @@
export const ExternalLink = () => (
<svg
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 7.66667V11.6667C11 12.0203 10.8595 12.3594 10.6095 12.6095C10.3594 12.8595 10.0203 13 9.66667 13H2.33333C1.97971 13 1.64057 12.8595 1.39052 12.6095C1.14048 12.3594 1 12.0203 1 11.6667V4.33333C1 3.97971 1.14048 3.64057 1.39052 3.39052C1.64057 3.14048 1.97971 3 2.33333 3H6.33333M9 1H13M13 1V5M13 1L5.66667 8.33333"
stroke="#09090B"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);

View File

@@ -113,5 +113,7 @@
"createTeam": "Create a team to get started.",
"createTeamButtonLabel": "Create a Team",
"createCompanyAccount": "Create Company Account",
"requestCompanyAccount": "Request Company Account"
"requestCompanyAccount": {
"title": "Company details"
}
}

View File

@@ -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": "Members",
"billing": "Billing",

View 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"
}

View File

@@ -113,5 +113,22 @@
"createTeam": "Create a team to get started.",
"createTeamButtonLabel": "Create a Team",
"createCompanyAccount": "Create Company Account",
"requestCompanyAccount": "Request Company Account"
"requestCompanyAccount": {
"title": "Ettevõtte andmed",
"description": "Pakkumise saamiseks palun sisesta ettevõtte andmed millega MedReport kasutada kavatsed.",
"button": "Küsi pakkumist",
"successTitle": "Päring edukalt saadetud!",
"successDescription": "Saadame teile esimesel võimalusel vastuse",
"successButton": "Tagasi kodulehele"
},
"updateAccount": {
"title": "Isikuandmed",
"description": "Jätkamiseks palun sisestage enda isikuandmed",
"button": "Jätka",
"userConsentLabel": "Nõustun isikuandmete kasutamisega platvormil",
"userConsentUrlTitle": "Vaata isikuandmete töötlemise põhimõtteid",
"successTitle": "Tere, {{firstName}} {{lastName}}",
"successDescription": "Teie tervisekonto on aktiveeritud ja kasutamiseks valmis!",
"successButton": "Jätka"
}
}

View File

@@ -19,7 +19,7 @@
"clear": "Clear",
"notFound": "Not Found",
"backToHomePage": "Back to Home Page",
"goBack": "Go Back",
"goBack": "Tagasi",
"genericServerError": "Sorry, something went wrong.",
"genericServerErrorHeading": "Sorry, something went wrong while processing your request. Please contact us if the issue persists.",
"pageNotFound": "Sorry, this page does not exist.",
@@ -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": "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": "Members",
"billing": "Billing",
@@ -92,5 +103,17 @@
"description": "This website uses cookies to ensure you get the best experience on our website.",
"reject": "Reject",
"accept": "Accept"
},
"formField": {
"companyName": "Ettevõtte nimi",
"contactPerson": "Kontaktisik",
"email": "E-mail",
"phone": "Telefon",
"firstName": "Eesnimi",
"lastName": "Perenimi",
"personalCode": "Isikukood",
"city": "Linn",
"weight": "Kaal",
"height": "Pikkus"
}
}

View 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"
}

View File

@@ -112,6 +112,5 @@
"noTeamsYet": "You don't have any teams yet.",
"createTeam": "Create a team to get started.",
"createTeamButtonLabel": "Create a Team",
"createCompanyAccount": "Create Company Account",
"requestCompanyAccount": "Request Company Account"
"createCompanyAccount": "Create Company Account"
}

View File

@@ -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": "Members",
"billing": "Billing",

View 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"
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,119 @@
alter table "public"."accounts" add column "last_name" text;
alter table "public"."accounts" add column "personal_code" text;
alter table "public"."accounts" add column "city" text;
alter table "public"."accounts" add column "has_consent_personal_data" boolean;
alter table "public"."accounts" add column "phone" text;
set check_function_bodies = off;
CREATE OR REPLACE FUNCTION public.update_account(p_name character varying, p_last_name text, p_personal_code text, p_phone text, p_city text, p_has_consent_personal_data boolean, p_uid uuid)
RETURNS void
LANGUAGE plpgsql
AS $function$begin
update public.accounts
set name = coalesce(p_name, name),
last_name = coalesce(p_last_name, last_name),
personal_code = coalesce(p_personal_code, personal_code),
phone = coalesce(p_phone, phone),
city = coalesce(p_city, city),
has_consent_personal_data = coalesce(p_has_consent_personal_data,
has_consent_personal_data)
where id = p_uid;
end;$function$
;
grant
execute on function public.update_account(p_name character varying, p_last_name text, p_personal_code text, p_phone text, p_city text, p_has_consent_personal_data boolean, p_uid uuid) to authenticated,
service_role;
CREATE OR REPLACE FUNCTION public.has_personal_code(account_id uuid)
RETURNS boolean
LANGUAGE plpgsql
AS $function$BEGIN
RETURN EXISTS (
SELECT 1
FROM public.accounts
WHERE id = account_id
AND personal_code IS NOT NULL
AND personal_code <> ''
);
END;$function$
;
grant
execute on function public.has_personal_code(account_id uuid) to authenticated,
service_role;
create table "public"."account_params" (
"recorded_at" timestamp with time zone not null default now(),
"id" uuid not null default gen_random_uuid(),
"account_id" uuid not null default auth.uid(),
"weight" integer,
"height" integer
);
alter table "public"."account_params" enable row level security;
CREATE UNIQUE INDEX account_params_pkey ON public.account_params USING btree (id);
alter table "public"."account_params" add constraint "account_params_pkey" PRIMARY KEY using index "account_params_pkey";
grant delete on table "public"."account_params" to "anon";
grant insert on table "public"."account_params" to "anon";
grant references on table "public"."account_params" to "anon";
grant select on table "public"."account_params" to "anon";
grant trigger on table "public"."account_params" to "anon";
grant truncate on table "public"."account_params" to "anon";
grant update on table "public"."account_params" to "anon";
grant delete on table "public"."account_params" to "authenticated";
grant insert on table "public"."account_params" to "authenticated";
grant references on table "public"."account_params" to "authenticated";
grant select on table "public"."account_params" to "authenticated";
grant trigger on table "public"."account_params" to "authenticated";
grant truncate on table "public"."account_params" to "authenticated";
grant update on table "public"."account_params" to "authenticated";
grant delete on table "public"."account_params" to "service_role";
grant insert on table "public"."account_params" to "service_role";
grant references on table "public"."account_params" to "service_role";
grant select on table "public"."account_params" to "service_role";
grant trigger on table "public"."account_params" to "service_role";
grant truncate on table "public"."account_params" to "service_role";
grant update on table "public"."account_params" to "service_role";
-- Everyone can read their own rows
create policy "users can read their params"
on public.account_params
for select
to authenticated
using ( account_id = auth.uid() );
-- Everyone can insert rows for themselves
create policy "users can insert their params"
on public.account_params
for insert
to authenticated
with check ( account_id = auth.uid() );

View File

@@ -3,7 +3,7 @@
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*":["./*"],
"@/*": ["./*"],
"~/*": ["./app/*"],
"~/config/*": ["./config/*"],
"~/components/*": ["./components/*"],