Merge branch 'main' into B2B-30
This commit is contained in:
52
README.md
52
README.md
@@ -1,17 +1,22 @@
|
||||
# MedReport README
|
||||
|
||||
## Prerequisites
|
||||
"node": ">=20.0.0",
|
||||
"pnpm": ">=9.0.0"
|
||||
## Prerequisites
|
||||
|
||||
```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.
|
||||
/ config - bunch of configs, that are provided by starter kit.
|
||||
/ content - (temporary?) - to be removed when cleaned all dependencies
|
||||
/ fonts - (temporary) - contains fonts, should be relocated to another place (maybe public)
|
||||
/ lib - diffirent libs, services, utils
|
||||
- fonts.ts - project fonts setup, which becomes available as a global css variable
|
||||
- fonts.ts - project fonts setup, which becomes available as a global css variable
|
||||
/ i18n - translations/localization setup
|
||||
/ public - public assets
|
||||
/ locales - translations under a corresponding local - at a specific namespace
|
||||
@@ -21,26 +26,47 @@
|
||||
- theme.css - more specific variables, available as tailwindcss property-class
|
||||
- makerkit.css - Makerkit-specific global styles
|
||||
- markdoc.css - Styles for Markdoc Markdown files.
|
||||
-
|
||||
-
|
||||
/ 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
|
||||
|
||||
```bash
|
||||
pnpm clean
|
||||
pnpm i
|
||||
```
|
||||
|
||||
## Adding new dependency
|
||||
|
||||
```bash
|
||||
```bash
|
||||
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
|
||||
```
|
||||
|
||||
@@ -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>
|
||||
|
||||
102
app/(public)/company-offer/page.tsx
Normal file
102
app/(public)/company-offer/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
16
app/(public)/company-offer/success/page.tsx
Normal file
16
app/(public)/company-offer/success/page.tsx
Normal 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: '/',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
9
app/auth/confirm/layout.tsx
Normal file
9
app/auth/confirm/layout.tsx
Normal 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;
|
||||
9
app/auth/password-reset/layout.tsx
Normal file
9
app/auth/password-reset/layout.tsx
Normal 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;
|
||||
9
app/auth/sign-in/layout.tsx
Normal file
9
app/auth/sign-in/layout.tsx
Normal 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;
|
||||
@@ -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 (
|
||||
|
||||
9
app/auth/sign-up/layout.tsx
Normal file
9
app/auth/sign-up/layout.tsx
Normal 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;
|
||||
@@ -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) {
|
||||
|
||||
11
app/auth/update-account/layout.tsx
Normal file
11
app/auth/update-account/layout.tsx
Normal 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);
|
||||
43
app/auth/update-account/page.tsx
Normal file
43
app/auth/update-account/page.tsx
Normal 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);
|
||||
17
app/auth/update-account/success/page.tsx
Normal file
17
app/auth/update-account/success/page.tsx
Normal 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);
|
||||
9
app/auth/verify/layout.tsx
Normal file
9
app/auth/verify/layout.tsx
Normal 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;
|
||||
237
app/home/(user)/_components/dashboard.tsx
Normal file
237
app/home/(user)/_components/dashboard.tsx
Normal file
@@ -0,0 +1,237 @@
|
||||
'use client';
|
||||
|
||||
import { InfoTooltip } from '@/components/ui/info-tooltip';
|
||||
import { toTitleCase } from '@/lib/utils';
|
||||
import { BlendingModeIcon, RulerHorizontalIcon } from '@radix-ui/react-icons';
|
||||
import {
|
||||
Activity,
|
||||
ChevronRight,
|
||||
Clock9,
|
||||
Droplets,
|
||||
LineChart,
|
||||
Pill,
|
||||
Scale,
|
||||
TrendingUp,
|
||||
User,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { usePersonalAccountData } from '@kit/accounts/hooks/use-personal-account-data';
|
||||
import { useUserWorkspace } from '@kit/accounts/hooks/use-user-workspace';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardProps,
|
||||
} from '@kit/ui/card';
|
||||
import { PageDescription } from '@kit/ui/page';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import { cn } from '@kit/ui/utils';
|
||||
|
||||
const dummyCards = [
|
||||
{
|
||||
title: 'dashboard:gender',
|
||||
description: 'dashboard:male',
|
||||
icon: <User />,
|
||||
iconBg: 'bg-success',
|
||||
},
|
||||
{
|
||||
title: 'dashboard:age',
|
||||
description: '43',
|
||||
icon: <Clock9 />,
|
||||
iconBg: 'bg-success',
|
||||
},
|
||||
{
|
||||
title: 'dashboard:height',
|
||||
description: '183',
|
||||
icon: <RulerHorizontalIcon className="size-4" />,
|
||||
iconBg: 'bg-success',
|
||||
},
|
||||
{
|
||||
title: 'dashboard:weight',
|
||||
description: '92kg',
|
||||
icon: <Scale />,
|
||||
iconBg: 'bg-warning',
|
||||
},
|
||||
{
|
||||
title: 'dashboard:bmi',
|
||||
description: '27.5',
|
||||
icon: <TrendingUp />,
|
||||
iconBg: 'bg-warning',
|
||||
},
|
||||
{
|
||||
title: 'dashboard:bloodPressure',
|
||||
description: '160/98',
|
||||
icon: <Activity />,
|
||||
iconBg: 'bg-warning',
|
||||
},
|
||||
{
|
||||
title: 'dashboard:cholesterol',
|
||||
description: '5',
|
||||
icon: <BlendingModeIcon className="size-4" />,
|
||||
iconBg: 'bg-destructive',
|
||||
},
|
||||
{
|
||||
title: 'dashboard:ldlCholesterol',
|
||||
description: '3,6',
|
||||
icon: <Pill />,
|
||||
iconBg: 'bg-warning',
|
||||
},
|
||||
{
|
||||
title: 'Score 2',
|
||||
description: 'Normis',
|
||||
icon: <LineChart />,
|
||||
iconBg: 'bg-success',
|
||||
},
|
||||
{
|
||||
title: 'dashboard:smoking',
|
||||
description: 'dashboard:respondToQuestion',
|
||||
descriptionColor: 'text-primary',
|
||||
icon: (
|
||||
<Button size="icon" variant="outline" className="px-2 text-black">
|
||||
<ChevronRight className="size-4 stroke-2" />
|
||||
</Button>
|
||||
),
|
||||
cardVariant: 'gradient-success' as CardProps['variant'],
|
||||
},
|
||||
];
|
||||
|
||||
const dummyRecommendations = [
|
||||
{
|
||||
icon: <BlendingModeIcon className="size-4" />,
|
||||
color: 'bg-cyan/10 text-cyan',
|
||||
title: 'Kolesterooli kontroll',
|
||||
description: 'HDL-kolestrool',
|
||||
tooltipContent: 'Selgitus',
|
||||
price: '20,00 €',
|
||||
buttonText: 'Telli',
|
||||
},
|
||||
{
|
||||
icon: <BlendingModeIcon className="size-4" />,
|
||||
color: 'bg-primary/10 text-primary',
|
||||
title: 'Kolesterooli kontroll',
|
||||
tooltipContent: 'Selgitus',
|
||||
description: 'LDL-Kolesterool',
|
||||
buttonText: 'Broneeri',
|
||||
},
|
||||
{
|
||||
icon: <Droplets />,
|
||||
color: 'bg-destructive/10 text-destructive',
|
||||
title: 'Vererõhu kontroll',
|
||||
tooltipContent: 'Selgitus',
|
||||
description: 'Score-Risk 2',
|
||||
price: '20,00 €',
|
||||
buttonText: 'Telli',
|
||||
},
|
||||
];
|
||||
|
||||
export default function Dashboard() {
|
||||
const userWorkspace = useUserWorkspace();
|
||||
const account = usePersonalAccountData(userWorkspace.user.id);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<h4>
|
||||
<Trans i18nKey={'common:welcome'} />
|
||||
{account?.data?.name ? `, ${toTitleCase(account.data.name)}` : ''}
|
||||
</h4>
|
||||
<PageDescription>
|
||||
<Trans i18nKey={'dashboard:recentlyCheckedDescription'} />:
|
||||
</PageDescription>
|
||||
</div>
|
||||
<div className="grid auto-rows-fr grid-cols-5 gap-3">
|
||||
{dummyCards.map(
|
||||
({
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
iconBg,
|
||||
cardVariant,
|
||||
descriptionColor,
|
||||
}) => (
|
||||
<Card
|
||||
key={title}
|
||||
variant={cardVariant}
|
||||
className="flex flex-col justify-between"
|
||||
>
|
||||
<CardHeader className="items-end-safe">
|
||||
<div
|
||||
className={cn(
|
||||
'flex size-8 items-center-safe justify-center-safe rounded-full text-white',
|
||||
iconBg,
|
||||
)}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardFooter className="flex flex-col items-start">
|
||||
<h5>
|
||||
<Trans i18nKey={title} />
|
||||
</h5>
|
||||
<CardDescription className={descriptionColor}>
|
||||
<Trans i18nKey={description} />
|
||||
</CardDescription>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader className="items-start">
|
||||
<h4>
|
||||
<Trans i18nKey="dashboard:recommendedForYou" />
|
||||
</h4>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{dummyRecommendations.map(
|
||||
(
|
||||
{
|
||||
icon,
|
||||
color,
|
||||
title,
|
||||
description,
|
||||
tooltipContent,
|
||||
price,
|
||||
buttonText,
|
||||
},
|
||||
index,
|
||||
) => {
|
||||
return (
|
||||
<div className="flex justify-between" key={index}>
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
<div
|
||||
className={cn(
|
||||
'flex size-8 items-center-safe justify-center-safe rounded-full text-white',
|
||||
color,
|
||||
)}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
<div>
|
||||
<div className="inline-flex items-center gap-1 align-baseline text-sm font-medium">
|
||||
{title}
|
||||
<InfoTooltip content={tooltipContent} />
|
||||
</div>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid w-36 auto-rows-fr grid-cols-2 items-center gap-4">
|
||||
<p className="text-sm font-medium"> {price}</p>
|
||||
<Button size="sm" variant="secondary">
|
||||
{buttonText}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { MedReportLogo } from './med-report-title';
|
||||
import { MedReportLogo } from './med-report-logo';
|
||||
|
||||
function LogoImage({
|
||||
className,
|
||||
|
||||
31
components/back-button.tsx
Normal file
31
components/back-button.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
16
components/ui/info-tooltip.tsx
Normal file
16
components/ui/info-tooltip.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@kit/ui/tooltip";
|
||||
import { Info } from "lucide-react";
|
||||
|
||||
export function InfoTooltip({ content }: { content?: string }) {
|
||||
if (!content) return null;
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Info className="size-4 cursor-pointer" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{content}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
33
components/ui/search.tsx
Normal file
33
components/ui/search.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import React, { JSX, ReactNode } from 'react';
|
||||
|
||||
import { cn } from '@kit/ui/utils';
|
||||
|
||||
export type SearchProps = React.InputHTMLAttributes<HTMLInputElement> & {
|
||||
startElement?: string | JSX.Element;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const Search = React.forwardRef<HTMLInputElement, SearchProps>(
|
||||
({ className, startElement, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'border-input ring-offset-background focus-within:ring-ring flex h-10 items-center rounded-md border bg-white pl-3 text-sm focus-within:ring-1 focus-within:ring-offset-2',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{!!startElement && startElement}
|
||||
<input
|
||||
{...props}
|
||||
type="search"
|
||||
ref={ref}
|
||||
className="placeholder:text-muted-foreground w-full p-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Search.displayName = 'Search';
|
||||
|
||||
export { Search };
|
||||
@@ -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>);
|
||||
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
@@ -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('/');
|
||||
};
|
||||
|
||||
@@ -32,6 +32,7 @@ export const defaultI18nNamespaces = [
|
||||
'teams',
|
||||
'billing',
|
||||
'marketing',
|
||||
'dashboard',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
42
lib/services/mailer.service.ts
Normal file
42
lib/services/mailer.service.ts
Normal 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,
|
||||
},
|
||||
);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
13
lib/utils.ts
13
lib/utils.ts
@@ -1,5 +1,5 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { type ClassValue, clsx } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
@@ -9,3 +9,12 @@ export function toArray<T>(input?: T | T[] | null): T[] {
|
||||
if (!input) return [];
|
||||
return Array.isArray(input) ? input : [input];
|
||||
}
|
||||
|
||||
export function toTitleCase(str?: string) {
|
||||
if (!str) return '';
|
||||
return str.replace(
|
||||
/\w\S*/g,
|
||||
(text: string) =>
|
||||
text.charAt(0).toUpperCase() + text.substring(1).toLowerCase(),
|
||||
);
|
||||
}
|
||||
|
||||
16
lib/validations/company-offer.schema.ts
Normal file
16
lib/validations/company-offer.schema.ts
Normal 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(),
|
||||
});
|
||||
@@ -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(),
|
||||
});
|
||||
7
lib/validations/email.schema.ts
Normal file
7
lib/validations/email.schema.ts
Normal 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),
|
||||
});
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
@@ -114,4 +112,4 @@
|
||||
"node": ">=20.0.0",
|
||||
"pnpm": ">=9.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
This package is responsible for managing email templates using the react.email library.
|
||||
|
||||
Here you can define email templates using React components and export them as a function that returns the email content.
|
||||
Here you can define email templates using React components and export them as a function that returns the email content.
|
||||
|
||||
90
packages/email-templates/src/emails/company-offer.email.tsx
Normal file
90
packages/email-templates/src/emails/company-offer.email.tsx
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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:"
|
||||
}
|
||||
@@ -26,7 +26,8 @@ export function usePersonalAccountData(
|
||||
`
|
||||
id,
|
||||
name,
|
||||
picture_url
|
||||
picture_url,
|
||||
last_name
|
||||
`,
|
||||
)
|
||||
.eq('primary_owner_user_id', userId)
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ export function SignUpMethodsContainer(props: {
|
||||
paths: {
|
||||
callback: string;
|
||||
appHome: string;
|
||||
updateAccount: string;
|
||||
};
|
||||
|
||||
providers: {
|
||||
|
||||
225
packages/features/auth/src/components/update-account-form.tsx
Normal file
225
packages/features/auth/src/components/update-account-form.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
44
packages/features/auth/src/schemas/update-account.schema.ts
Normal file
44
packages/features/auth/src/schemas/update-account.schema.ts
Normal 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',
|
||||
}),
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
93
packages/features/auth/src/server/api.ts
Normal file
93
packages/features/auth/src/server/api.ts
Normal 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);
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './components/sign-up-methods-container';
|
||||
export * from './schemas/password-sign-up.schema';
|
||||
export * from './components/update-account-form';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": "@kit/tsconfig/base.json",
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
|
||||
},
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
export * from './notifications-popover';
|
||||
export * from './success-notification';
|
||||
export * from './update-account-success-notification';
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": "@kit/tsconfig/base.json",
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
|
||||
},
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -29,7 +29,7 @@ const RouteChild = z.object({
|
||||
});
|
||||
|
||||
const RouteGroup = z.object({
|
||||
label: z.string(),
|
||||
label: z.string().optional(),
|
||||
collapsible: z.boolean().optional(),
|
||||
collapsed: z.boolean().optional(),
|
||||
children: z.array(RouteChild),
|
||||
@@ -37,12 +37,8 @@ const RouteGroup = z.object({
|
||||
});
|
||||
|
||||
export const NavigationConfigSchema = z.object({
|
||||
style: z.enum(['custom', 'sidebar', 'header']).default('sidebar'),
|
||||
sidebarCollapsed: z
|
||||
.enum(['false', 'true'])
|
||||
.default('true')
|
||||
.optional()
|
||||
.transform((value) => value === `true`),
|
||||
style: z.enum(['custom', 'sidebar', 'header']).default('custom'),
|
||||
sidebarCollapsed: z.boolean().optional(),
|
||||
sidebarCollapsedStyle: z.enum(['offcanvas', 'icon', 'none']).default('icon'),
|
||||
routes: z.array(z.union([RouteGroup, Divider])),
|
||||
});
|
||||
|
||||
@@ -14,10 +14,6 @@ type PageProps = React.PropsWithChildren<{
|
||||
sticky?: boolean;
|
||||
}>;
|
||||
|
||||
const ENABLE_SIDEBAR_TRIGGER = process.env.NEXT_PUBLIC_ENABLE_SIDEBAR_TRIGGER
|
||||
? process.env.NEXT_PUBLIC_ENABLE_SIDEBAR_TRIGGER === 'true'
|
||||
: true;
|
||||
|
||||
export function Page(props: PageProps) {
|
||||
switch (props.style) {
|
||||
case 'header':
|
||||
@@ -79,7 +75,7 @@ function PageWithHeader(props: PageProps) {
|
||||
const { Navigation, Children, MobileNavigation } = getSlotsFromPage(props);
|
||||
|
||||
return (
|
||||
<div className={cn('flex h-screen flex-1 flex-col', props.className)}>
|
||||
<div className={cn('flex h-screen flex-1 flex-col z-1000', props.className)}>
|
||||
<div
|
||||
className={
|
||||
props.contentContainerClassName ?? 'flex flex-1 flex-col space-y-4'
|
||||
@@ -87,9 +83,9 @@ function PageWithHeader(props: PageProps) {
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'bg-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;
|
||||
|
||||
@@ -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()}>
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { VariantProps } from 'class-variance-authority';
|
||||
import { cn } from '../lib/utils';
|
||||
|
||||
const buttonVariants = cva(
|
||||
'focus-visible:ring-ring inline-flex items-center justify-center rounded-md text-sm font-medium whitespace-nowrap transition-colors focus-visible:ring-1 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50',
|
||||
'focus-visible:ring-ring gap-1 inline-flex items-center justify-center rounded-md text-sm font-medium whitespace-nowrap transition-colors focus-visible:ring-1 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
||||
@@ -1,15 +1,31 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '../lib/utils';
|
||||
import { VariantProps, cva } from 'class-variance-authority';
|
||||
import { cn } from '.';
|
||||
|
||||
const Card: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<div
|
||||
className={cn('bg-card text-card-foreground rounded-xl border', className)}
|
||||
{...props}
|
||||
/>
|
||||
const cardVariants = cva('text-card-foreground rounded-xl border', {
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-card',
|
||||
'gradient-warning':
|
||||
'from-warning/30 via-warning/10 to-background bg-gradient-to-t',
|
||||
'gradient-destructive':
|
||||
'from-destructive/30 via-destructive/10 to-background bg-gradient-to-t',
|
||||
'gradient-success':
|
||||
'from-success/30 via-success/10 to-background bg-gradient-to-t',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
});
|
||||
|
||||
export interface CardProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof cardVariants> {}
|
||||
|
||||
const Card: React.FC<CardProps> = ({ className, variant, ...props }) => (
|
||||
<div className={cn(cardVariants({ variant, className }))} {...props} />
|
||||
);
|
||||
Card.displayName = 'Card';
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
3
packages/ui/src/shadcn/constants.ts
Normal file
3
packages/ui/src/shadcn/constants.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const SIDEBAR_WIDTH = '16rem';
|
||||
export const SIDEBAR_WIDTH_MOBILE = '18rem';
|
||||
export const SIDEBAR_WIDTH_ICON = '4rem';
|
||||
@@ -21,6 +21,11 @@ import {
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from './collapsible';
|
||||
import {
|
||||
SIDEBAR_WIDTH,
|
||||
SIDEBAR_WIDTH_ICON,
|
||||
SIDEBAR_WIDTH_MOBILE,
|
||||
} from './constants';
|
||||
import { Input } from './input';
|
||||
import { Separator } from './separator';
|
||||
import { Sheet, SheetContent } from './sheet';
|
||||
@@ -34,9 +39,6 @@ import {
|
||||
|
||||
const SIDEBAR_COOKIE_NAME = 'sidebar:state';
|
||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
||||
const SIDEBAR_WIDTH = '16rem';
|
||||
const SIDEBAR_WIDTH_MOBILE = '18rem';
|
||||
const SIDEBAR_WIDTH_ICON = '4rem';
|
||||
const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
|
||||
const SIDEBAR_MINIMIZED_WIDTH = SIDEBAR_WIDTH_ICON;
|
||||
|
||||
@@ -276,7 +278,7 @@ const Sidebar: React.FC<
|
||||
<div
|
||||
data-sidebar="sidebar"
|
||||
className={cn(
|
||||
'bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm',
|
||||
'bg-sidebar group-data-[variant=floating]:border-sidebar-border ml-3 flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm',
|
||||
{
|
||||
'bg-transparent': variant === 'ghost',
|
||||
},
|
||||
@@ -908,7 +910,7 @@ export function SidebarNavigation({
|
||||
tooltip={child.label}
|
||||
>
|
||||
<Link
|
||||
className={cn('flex items-center', {
|
||||
className={cn('flex items-center font-medium', {
|
||||
'mx-auto w-full gap-0! [&>svg]:flex-1': !open,
|
||||
})}
|
||||
href={path}
|
||||
@@ -916,7 +918,7 @@ export function SidebarNavigation({
|
||||
{child.Icon}
|
||||
<span
|
||||
className={cn(
|
||||
'w-auto transition-opacity duration-300',
|
||||
'text-md w-auto font-medium transition-opacity duration-300',
|
||||
{
|
||||
'w-0 opacity-0': !open,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": "@kit/tsconfig/base.json",
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json",
|
||||
"paths": {
|
||||
@@ -13,4 +13,4 @@
|
||||
},
|
||||
"include": ["*.ts", "src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
}
|
||||
|
||||
28
public/assets/arrow-left.tsx
Normal file
28
public/assets/arrow-left.tsx
Normal 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>
|
||||
);
|
||||
16
public/assets/external-link.tsx
Normal file
16
public/assets/external-link.tsx
Normal 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>
|
||||
);
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -93,4 +104,4 @@
|
||||
"reject": "Reject",
|
||||
"accept": "Accept"
|
||||
}
|
||||
}
|
||||
}
|
||||
16
public/locales/en/dashboard.json
Normal file
16
public/locales/en/dashboard.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"recentlyCheckedDescription": "Super, oled käinud tervist kontrollimas. Siin on sinule olulised näitajad",
|
||||
"respondToQuestion": "Respond",
|
||||
"gender": "Gender",
|
||||
"male": "Male",
|
||||
"female": "Female",
|
||||
"age": "Age",
|
||||
"height": "Height",
|
||||
"weight": "Weight",
|
||||
"bmi": "BMI",
|
||||
"bloodPressure": "Blood pressure",
|
||||
"cholesterol": "Cholesterol",
|
||||
"ldlCholesterol": "LDL Cholesterol",
|
||||
"smoking": "Smoking",
|
||||
"recommendedForYou": "Recommended for you"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
16
public/locales/et/dashboard.json
Normal file
16
public/locales/et/dashboard.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"recentlyCheckedDescription": "Super, oled käinud tervist kontrollimas. Siin on sinule olulised näitajad",
|
||||
"respondToQuestion": "Vasta küsimusele",
|
||||
"gender": "Sugu",
|
||||
"male": "Mees",
|
||||
"female": "Naine",
|
||||
"age": "Vanus",
|
||||
"height": "Pikkus",
|
||||
"weight": "Kaal",
|
||||
"bmi": "KMI",
|
||||
"bloodPressure": "Vererõhk",
|
||||
"cholesterol": "Kolesterool",
|
||||
"ldlCholesterol": "LDL kolesterool",
|
||||
"smoking": "Suitsetamine",
|
||||
"recommendedForYou": "Soovitused sulle"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -93,4 +103,4 @@
|
||||
"reject": "Reject",
|
||||
"accept": "Accept"
|
||||
}
|
||||
}
|
||||
}
|
||||
16
public/locales/ru/dashboard.json
Normal file
16
public/locales/ru/dashboard.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"recentlyCheckedDescription": "Super, oled käinud tervist kontrollimas. Siin on sinule olulised näitajad",
|
||||
"respondToQuestion": "Respond",
|
||||
"gender": "Gender",
|
||||
"male": "Male",
|
||||
"female": "Female",
|
||||
"age": "Age",
|
||||
"height": "Height",
|
||||
"weight": "Weight",
|
||||
"bmi": "BMI",
|
||||
"bloodPressure": "Blood pressure",
|
||||
"cholesterol": "Cholesterol",
|
||||
"ldlCholesterol": "LDL Cholesterol",
|
||||
"smoking": "Smoking",
|
||||
"recommendedForYou": "Recommended for you"
|
||||
}
|
||||
@@ -31,7 +31,9 @@
|
||||
@layer base {
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-feature-settings: "rlig" 1, "calt" 1;
|
||||
font-feature-settings:
|
||||
'rlig' 1,
|
||||
'calt' 1;
|
||||
}
|
||||
|
||||
*,
|
||||
@@ -47,9 +49,44 @@
|
||||
color: theme(--color-muted-foreground);
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
@apply font-heading text-foreground font-semibold tracking-tight;
|
||||
}
|
||||
|
||||
h1,h2,h3,h4,h5,h6 {
|
||||
@apply font-heading text-foreground text-2xl font-semibold tracking-tight
|
||||
}
|
||||
h1 {
|
||||
@apply text-5xl;
|
||||
}
|
||||
|
||||
}
|
||||
h2 {
|
||||
@apply text-4xl;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@apply text-3xl;
|
||||
}
|
||||
|
||||
h4 {
|
||||
@apply text-2xl;
|
||||
}
|
||||
|
||||
h5 {
|
||||
@apply text-xl;
|
||||
}
|
||||
|
||||
h6 {
|
||||
@apply text-lg;
|
||||
}
|
||||
|
||||
.lucide {
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.lucide {
|
||||
@apply size-4;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,127 +7,130 @@
|
||||
*/
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--font-sans: var(--font-sans) -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
--font-heading: var(--font-heading);
|
||||
:root {
|
||||
--font-sans:
|
||||
var(--font-sans) -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||
Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
||||
'Segoe UI Symbol';
|
||||
--font-heading: var(--font-heading);
|
||||
|
||||
--background: hsla(0, 0%, 100%, 1);
|
||||
--foreground: hsla(240, 10%, 4%, 1);
|
||||
--foreground-50: hsla(240, 10%, 4%, 0.5);
|
||||
--background: hsla(0, 0%, 100%, 1);
|
||||
--foreground: hsla(240, 10%, 4%, 1);
|
||||
--foreground-50: hsla(240, 10%, 4%, 0.5);
|
||||
|
||||
--background-90: hsla(0, 0%, 100%, 0.9);
|
||||
--background-80: hsla(0, 0%, 100%, 0.8);
|
||||
--background-90: hsla(0, 0%, 100%, 0.9);
|
||||
--background-80: hsla(0, 0%, 100%, 0.8);
|
||||
|
||||
--card: var(--color-white);
|
||||
--card-foreground: var(--color-neutral-950);
|
||||
--card: var(--color-white);
|
||||
--card-foreground: var(--color-neutral-950);
|
||||
|
||||
--popover: hsla(0, 0%, 100%, 1);
|
||||
--popover-foreground: hsla(240, 10%, 4%, 1);
|
||||
--popover: hsla(0, 0%, 100%, 1);
|
||||
--popover-foreground: hsla(240, 10%, 4%, 1);
|
||||
|
||||
--primary: hsla(145, 78%, 18%, 1);
|
||||
--primary-foreground: hsla(356, 100%, 97%, 1);
|
||||
--primary: hsla(145, 78%, 18%, 1);
|
||||
--primary-foreground: hsla(356, 100%, 97%, 1);
|
||||
|
||||
--primary-90: hsla(145, 78%, 18%, 0.9);
|
||||
--primary-80: hsla(145, 78%, 18%, 0.8);
|
||||
--primary-50: hsla(145, 78%, 18%, 0.5);
|
||||
--primary-20: hsla(145, 78%, 18%, 0.2);
|
||||
--primary-10: hsla(145, 78%, 18%, 0.1);
|
||||
--primary-90: hsla(145, 78%, 18%, 0.9);
|
||||
--primary-80: hsla(145, 78%, 18%, 0.8);
|
||||
--primary-50: hsla(145, 78%, 18%, 0.5);
|
||||
--primary-20: hsla(145, 78%, 18%, 0.2);
|
||||
--primary-10: hsla(145, 78%, 18%, 0.1);
|
||||
|
||||
--secondary: hsla(240, 5%, 96%, 1);
|
||||
--secondary-foreground: hsla(240, 6%, 10%, 1);
|
||||
|
||||
--secondary-90: hsla(240, 5%, 96%, 0.9);
|
||||
--secondary-80: hsla(240, 5%, 96%, 0.8);
|
||||
--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);
|
||||
|
||||
--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);
|
||||
|
||||
--destructive: hsla(0, 84%, 60%, 1);
|
||||
--destructive-foreground: hsla(0, 0%, 98%, 1);
|
||||
--success: hsla(142, 76%, 36%, 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);
|
||||
--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);
|
||||
|
||||
--border: hsla(240, 6%, 90%, 1);
|
||||
--input: hsla(240, 6%, 90%, 1);
|
||||
--ring: var(--color-neutral-800);
|
||||
--border: hsla(240, 6%, 90%, 1);
|
||||
--input: hsla(240, 6%, 90%, 1);
|
||||
--ring: var(--color-neutral-800);
|
||||
|
||||
--radius: calc(1rem);
|
||||
--spacing: 0.25rem;
|
||||
--radius: calc(1rem);
|
||||
--spacing: 0.25rem;
|
||||
|
||||
--chart-1: var(--color-orange-400);
|
||||
--chart-2: var(--color-teal-600);
|
||||
--chart-3: var(--color-green-800);
|
||||
--chart-4: var(--color-yellow-200);
|
||||
--chart-5: var(--color-orange-200);
|
||||
--chart-1: var(--color-orange-400);
|
||||
--chart-2: var(--color-teal-600);
|
||||
--chart-3: var(--color-green-800);
|
||||
--chart-4: var(--color-yellow-200);
|
||||
--chart-5: var(--color-orange-200);
|
||||
|
||||
--sidebar-background: var(--background);
|
||||
--sidebar-foreground: var(--foreground);
|
||||
--sidebar-primary: var(--primary);
|
||||
--sidebar-primary-foreground: var(--color-white);
|
||||
--sidebar-accent: var(--secondary);
|
||||
--sidebar-accent-foreground: var(--secondary-foreground);
|
||||
--sidebar-border: var(--border);
|
||||
--sidebar-ring: var(--ring);
|
||||
}
|
||||
--sidebar-background: var(--background);
|
||||
--sidebar-foreground: var(--foreground);
|
||||
--sidebar-primary: var(--primary);
|
||||
--sidebar-primary-foreground: var(--color-white);
|
||||
--sidebar-accent: var(--secondary);
|
||||
--sidebar-accent-foreground: var(--secondary-foreground);
|
||||
--sidebar-border: var(--border);
|
||||
--sidebar-ring: var(--ring);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: var(--color-neutral-900);
|
||||
--foreground: var(--color-white);
|
||||
.dark {
|
||||
--background: var(--color-neutral-900);
|
||||
--foreground: var(--color-white);
|
||||
|
||||
--card: var(--color-neutral-900);
|
||||
--card-foreground: var(--color-white);
|
||||
--card: var(--color-neutral-900);
|
||||
--card-foreground: var(--color-white);
|
||||
|
||||
--popover: var(--color-neutral-900);
|
||||
--popover-foreground: var(--color-white);
|
||||
--popover: var(--color-neutral-900);
|
||||
--popover-foreground: var(--color-white);
|
||||
|
||||
--primary: var(--color-white);
|
||||
--primary-foreground: var(--color-neutral-900);
|
||||
--primary: var(--color-white);
|
||||
--primary-foreground: var(--color-neutral-900);
|
||||
|
||||
--secondary: var(--color-neutral-800);
|
||||
--secondary-foreground: oklch(98.43% 0.0017 247.84);
|
||||
--secondary: var(--color-neutral-800);
|
||||
--secondary-foreground: oklch(98.43% 0.0017 247.84);
|
||||
|
||||
--muted: var(--color-neutral-800);
|
||||
--muted-foreground: hsla(240, 4%, 41%, 1);
|
||||
--muted: var(--color-neutral-800);
|
||||
--muted-foreground: hsla(240, 4%, 41%, 1);
|
||||
|
||||
--accent: var(--color-neutral-800);
|
||||
--accent-foreground: oklch(98.48% 0 0);
|
||||
--accent: var(--color-neutral-800);
|
||||
--accent-foreground: oklch(98.48% 0 0);
|
||||
|
||||
--destructive: var(--color-red-700);
|
||||
--destructive-foreground: var(--color-white);
|
||||
--destructive: var(--color-red-700);
|
||||
--destructive-foreground: var(--color-white);
|
||||
|
||||
--border: var(--color-neutral-800);
|
||||
--input: var(--color-neutral-700);
|
||||
--ring: oklch(87.09% 0.0055 286.29);
|
||||
--border: var(--color-neutral-800);
|
||||
--input: var(--color-neutral-700);
|
||||
--ring: oklch(87.09% 0.0055 286.29);
|
||||
|
||||
--chart-1: var(--color-blue-600);
|
||||
--chart-2: var(--color-emerald-400);
|
||||
--chart-3: var(--color-orange-400);
|
||||
--chart-4: var(--color-purple-500);
|
||||
--chart-5: var(--color-pink-500);
|
||||
--chart-1: var(--color-blue-600);
|
||||
--chart-2: var(--color-emerald-400);
|
||||
--chart-3: var(--color-orange-400);
|
||||
--chart-4: var(--color-purple-500);
|
||||
--chart-5: var(--color-pink-500);
|
||||
|
||||
--sidebar-background: var(--color-neutral-900);
|
||||
--sidebar-foreground: var(--color-white);
|
||||
--sidebar-primary: var(--color-blue-500);
|
||||
--sidebar-primary-foreground: var(--color-white);
|
||||
--sidebar-accent: var(--color-neutral-800);
|
||||
--sidebar-accent-foreground: var(--color-white);
|
||||
--sidebar-border: var(--border);
|
||||
--sidebar-ring: var(--color-blue-500);
|
||||
}
|
||||
}
|
||||
--sidebar-background: var(--color-neutral-900);
|
||||
--sidebar-foreground: var(--color-white);
|
||||
--sidebar-primary: var(--color-blue-500);
|
||||
--sidebar-primary-foreground: var(--color-white);
|
||||
--sidebar-accent: var(--color-neutral-800);
|
||||
--sidebar-accent-foreground: var(--color-white);
|
||||
--sidebar-border: var(--border);
|
||||
--sidebar-ring: var(--color-blue-500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,15 @@
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
|
||||
--success: hsla(142, 76%, 36%, 1);
|
||||
--color-success: var(--success);
|
||||
|
||||
--warning: hsla(25, 95%, 53%, 1);
|
||||
--color-warning: var(--warning);
|
||||
|
||||
--cyan: hsla(189, 94%, 43%, 1);
|
||||
--color-cyan: var(--cyan);
|
||||
|
||||
/* text colors */
|
||||
--color-text-foreground: var(--foreground);
|
||||
--color-text-primary: var(--primary);
|
||||
@@ -94,7 +103,7 @@
|
||||
--color-border-primary: var(--primary);
|
||||
--color-border-primary-50: var(--primary-50);
|
||||
--color-border-primary-foreground: var(--primary-foreground);
|
||||
|
||||
|
||||
--color-border-destructive: var(--destructive-50);
|
||||
--color-border-toast-destructive: var(--muted-40);
|
||||
--color-border-muted: var(--muted);
|
||||
@@ -123,13 +132,13 @@
|
||||
--animate-accordion-down: accordion-down 0.2s ease-out;
|
||||
--animate-accordion-up: accordion-up 0.2s ease-out;
|
||||
|
||||
--breakpoint-xs: 30rem;
|
||||
--breakpoint-xs: 30rem;
|
||||
--breakpoint-sm: 48rem;
|
||||
--breakpoint-md: 70rem;
|
||||
--breakpoint-lg: 80rem;
|
||||
--breakpoint-xl: 96rem;
|
||||
--breakpoint-2xl: 100rem;
|
||||
--breakpoint-3xl: 120rem;
|
||||
--breakpoint-lg: 80rem;
|
||||
--breakpoint-xl: 96rem;
|
||||
--breakpoint-2xl: 100rem;
|
||||
--breakpoint-3xl: 120rem;
|
||||
|
||||
--container-max-width: 80rem;
|
||||
|
||||
@@ -180,4 +189,4 @@
|
||||
transform: translateY(0px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
119
supabase/migrations/20250620124230_update_account.sql
Normal file
119
supabase/migrations/20250620124230_update_account.sql
Normal 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() );
|
||||
@@ -3,7 +3,7 @@
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*":["./*"],
|
||||
"@/*": ["./*"],
|
||||
"~/*": ["./app/*"],
|
||||
"~/config/*": ["./config/*"],
|
||||
"~/components/*": ["./components/*"],
|
||||
|
||||
Reference in New Issue
Block a user