feat: Implement company offer submission page and success notification
- Added CompanyOffer component for submitting company offers with validation. - Integrated email sending functionality upon form submission. - Created a success page for company registration confirmation. - Introduced a reusable SuccessNotification component for displaying success messages. - Updated account update functionality with new fields and validation. - Enhanced user experience with back button and logo components. - Added necessary database migrations for account updates.
This commit is contained in:
36
README.md
36
README.md
@@ -1,10 +1,15 @@
|
||||
# MedReport README
|
||||
|
||||
## 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.
|
||||
@@ -25,12 +30,10 @@
|
||||
/ supabase - primary supabase
|
||||
/ tooling - a workspace package, used for generation packages in node_modules and provides global links for its data. The most important is typescript config
|
||||
/ utils
|
||||
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Migration from old structure
|
||||
|
||||
```bash
|
||||
pnpm clean
|
||||
pnpm i
|
||||
@@ -43,4 +46,27 @@ pnpm add <pacakge-name> -w
|
||||
```
|
||||
|
||||
## Supabase
|
||||
TODO
|
||||
|
||||
Start supabase in docker
|
||||
|
||||
```bash
|
||||
npm run supabase:start
|
||||
```
|
||||
|
||||
Link your local supabase with a supabase project
|
||||
|
||||
```bash
|
||||
npm run supabase:deploy
|
||||
```
|
||||
|
||||
After editing supabase tables/functions etc update migration files
|
||||
|
||||
```bash
|
||||
npm run supabase:db:diff
|
||||
```
|
||||
|
||||
To update database types run:
|
||||
|
||||
```bash
|
||||
npm run supabase:typegen:app
|
||||
```
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -4,12 +4,11 @@ import React from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { MedReportLogo } from '@/components/med-report-title';
|
||||
import { MedReportLogo } from '@/components/med-report-logo';
|
||||
import { SubmitButton } from '@/components/ui/submit-button';
|
||||
import { sendCompanyOfferEmail } from '@/lib/services/mailer.service';
|
||||
import { submitCompanyRegistration } from '@/lib/services/register-company.service';
|
||||
import { CompanySubmitData } from '@/lib/types/company';
|
||||
import { companySchema } from '@/lib/validations/companySchema';
|
||||
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';
|
||||
@@ -19,36 +18,35 @@ import { Input } from '@kit/ui/input';
|
||||
import { Label } from '@kit/ui/label';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
export default function RegisterCompany() {
|
||||
export default function CompanyOffer() {
|
||||
const router = useRouter();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors, isValid, isSubmitting },
|
||||
formState: { isValid, isSubmitting },
|
||||
} = useForm({
|
||||
resolver: zodResolver(companySchema),
|
||||
resolver: zodResolver(companyOfferSchema),
|
||||
mode: 'onChange',
|
||||
});
|
||||
const language = useTranslation().i18n.language;
|
||||
|
||||
async function onSubmit(data: CompanySubmitData) {
|
||||
const onSubmit = async (data: CompanySubmitData) => {
|
||||
const formData = new FormData();
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
if (value !== undefined) formData.append(key, value);
|
||||
});
|
||||
|
||||
try {
|
||||
await submitCompanyRegistration(formData);
|
||||
sendCompanyOfferEmail(data, language)
|
||||
.then(() => router.push('/register-company/success'))
|
||||
.then(() => router.push('/company-offer/success'))
|
||||
.catch((error) => alert('error: ' + error));
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error) {
|
||||
alert('Server validation error: ' + err.message);
|
||||
}
|
||||
alert('Server validation 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">
|
||||
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,37 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { MedReportLogo } from '@/components/med-report-title';
|
||||
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
export default function CompanyRegistrationSuccess() {
|
||||
return (
|
||||
<div className="border-border rounded-3xl border px-16 pt-4 pb-12">
|
||||
<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">
|
||||
<Trans i18nKey="account:requestCompanyAccount:successTitle" />
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
<Trans i18nKey="account:requestCompanyAccount:successDescription" />
|
||||
</p>
|
||||
</div>
|
||||
<Button className="mt-8 w-full">
|
||||
<Link href="/">
|
||||
<Trans i18nKey="account:requestCompanyAccount:successButton" />
|
||||
</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;
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -8,6 +8,8 @@ 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),
|
||||
@@ -31,6 +33,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',
|
||||
|
||||
@@ -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('/');
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,26 +0,0 @@
|
||||
'use server';
|
||||
|
||||
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() || '',
|
||||
};
|
||||
|
||||
const result = companySchema.safeParse(data);
|
||||
|
||||
if (!result.success) {
|
||||
const errors = result.error.errors.map((err) => ({
|
||||
path: err.path.join('.'),
|
||||
message: err.message,
|
||||
}));
|
||||
|
||||
throw new Error(
|
||||
'Validation failed: ' +
|
||||
errors.map((e) => `${e.path}: ${e.message}`).join(', '),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const companySchema = z.object({
|
||||
export const companyOfferSchema = z.object({
|
||||
companyName: z.string({
|
||||
required_error: 'Company name is required',
|
||||
}),
|
||||
@@ -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,22 +147,23 @@ 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;
|
||||
// TODO: this mfa should only be applied to SUPER_ADMIN
|
||||
// const isVerifyMfa = req.nextUrl.pathname === pathsConfig.auth.verifyMfa;
|
||||
|
||||
// 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 (!isVerifyMfa) {
|
||||
// const nextPath =
|
||||
// req.nextUrl.searchParams.get('next') ?? pathsConfig.app.home;
|
||||
|
||||
return NextResponse.redirect(
|
||||
new URL(nextPath, req.nextUrl.origin).href,
|
||||
);
|
||||
}
|
||||
// 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 > ./lib/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-data:dev": "NODE_ENV=local ts-node jobs/sync-analysis-groups.ts"
|
||||
},
|
||||
|
||||
@@ -26,7 +26,8 @@ export function usePersonalAccountData(
|
||||
`
|
||||
id,
|
||||
name,
|
||||
picture_url
|
||||
picture_url,
|
||||
last_name
|
||||
`,
|
||||
)
|
||||
.eq('primary_owner_user_id', userId)
|
||||
|
||||
@@ -211,7 +211,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 Employees</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
|
||||
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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -16,6 +16,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';
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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()}>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": "@kit/tsconfig/base.json",
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json",
|
||||
"paths": {
|
||||
|
||||
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>
|
||||
);
|
||||
@@ -120,5 +120,15 @@
|
||||
"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.",
|
||||
@@ -54,7 +54,6 @@
|
||||
"newVersionAvailable": "New version available",
|
||||
"newVersionAvailableDescription": "A new version of the app is available. It is recommended to refresh the page to get the latest updates and avoid any issues.",
|
||||
"newVersionSubmitButton": "Reload and Update",
|
||||
"back": "Back",
|
||||
"routes": {
|
||||
"home": "Home",
|
||||
"account": "Account",
|
||||
@@ -97,6 +96,12 @@
|
||||
"companyName": "Ettevõtte nimi",
|
||||
"contactPerson": "Kontaktisik",
|
||||
"email": "E-mail",
|
||||
"phone": "Telefon"
|
||||
"phone": "Telefon",
|
||||
"firstName": "Eesnimi",
|
||||
"lastName": "Perenimi",
|
||||
"personalCode": "Isikukood",
|
||||
"city": "Linn",
|
||||
"weight": "Kaal",
|
||||
"height": "Pikkus"
|
||||
}
|
||||
}
|
||||
|
||||
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() );
|
||||
Reference in New Issue
Block a user