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:
@@ -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
|
||||
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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user