MED-151: add profile view and working smoking dashboard card (#71)
* MED-151: add profile view and working smoking dashboard card * update zod * move some components to shared * move some components to shared * remove console.logs * remove unused password form components * only check null for variant * use pathsconfig
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
'use client';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
import { AccountWithParams } from '@kit/accounts/api';
|
||||
import { useRevalidatePersonalAccountDataQuery } from '@kit/accounts/hooks/use-personal-account-data';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Card, CardDescription, CardTitle } from '@kit/ui/card';
|
||||
import { Form } from '@kit/ui/form';
|
||||
import { toast } from '@kit/ui/sonner';
|
||||
import { Switch } from '@kit/ui/switch';
|
||||
|
||||
import {
|
||||
AccountPreferences,
|
||||
accountPreferencesSchema,
|
||||
} from '../_lib/account-preferences.schema';
|
||||
import { updatePersonalAccountPreferencesAction } from '../_lib/server/actions';
|
||||
import { LanguageSelector } from '@kit/ui/language-selector';
|
||||
|
||||
export default function AccountPreferencesForm({
|
||||
account,
|
||||
}: {
|
||||
account: AccountWithParams | null;
|
||||
}) {
|
||||
const revalidateUserDataQuery = useRevalidatePersonalAccountDataQuery();
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(accountPreferencesSchema),
|
||||
defaultValues: {
|
||||
preferredLanguage: account?.preferred_locale,
|
||||
isConsentToAnonymizedCompanyStatistics:
|
||||
!!account?.has_consent_anonymized_company_statistics,
|
||||
},
|
||||
});
|
||||
|
||||
const { register, handleSubmit, watch, setValue } = form;
|
||||
|
||||
const onSubmit = async (data: AccountPreferences) => {
|
||||
if (!account?.id) {
|
||||
return toast.error(<Trans i18nKey="account:updateAccountError" />);
|
||||
}
|
||||
const result = await updatePersonalAccountPreferencesAction({
|
||||
accountId: account.id,
|
||||
data,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
revalidateUserDataQuery(account.primary_owner_user_id);
|
||||
return toast.success(
|
||||
<Trans i18nKey="account:updateAccountPreferencesSuccess" />,
|
||||
);
|
||||
}
|
||||
return toast.error(
|
||||
<Trans i18nKey="account:updateAccountPreferencesError" />,
|
||||
);
|
||||
};
|
||||
|
||||
const watchedConsent = watch('isConsentToAnonymizedCompanyStatistics');
|
||||
|
||||
if (!account) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<CardTitle className="text-base">
|
||||
<Trans i18nKey={'account:language'} />
|
||||
</CardTitle>
|
||||
<LanguageSelector />
|
||||
</div>
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="flex flex-col gap-6 text-left"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xl font-semibold text-gray-900">
|
||||
<Trans i18nKey="account:consents" />
|
||||
</h2>
|
||||
|
||||
<Card>
|
||||
<div className="flex items-center justify-between p-3">
|
||||
<div>
|
||||
<CardTitle className="text-base">
|
||||
<Trans i18nKey="account:consentToAnonymizedCompanyData.label" />
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
<Trans i18nKey="account:consentToAnonymizedCompanyData.description" />
|
||||
</CardDescription>
|
||||
</div>
|
||||
|
||||
<Switch
|
||||
checked={!!watchedConsent}
|
||||
onCheckedChange={(checked) =>
|
||||
setValue('isConsentToAnonymizedCompanyStatistics', checked)
|
||||
}
|
||||
{...register('isConsentToAnonymizedCompanyStatistics')}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="w-36">
|
||||
<Trans i18nKey="account:updateProfileSubmitLabel" />
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
259
app/home/(user)/settings/_components/account-settings-form.tsx
Normal file
259
app/home/(user)/settings/_components/account-settings-form.tsx
Normal file
@@ -0,0 +1,259 @@
|
||||
'use client';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
import { AccountWithParams } from '@kit/accounts/api';
|
||||
import { useRevalidatePersonalAccountDataQuery } from '@kit/accounts/hooks/use-personal-account-data';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Card, CardTitle } from '@kit/ui/card';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@kit/ui/form';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@kit/ui/select';
|
||||
import { toast } from '@kit/ui/sonner';
|
||||
import { Switch } from '@kit/ui/switch';
|
||||
|
||||
import {
|
||||
AccountSettings,
|
||||
accountSettingsSchema,
|
||||
} from '../_lib/account-settings.schema';
|
||||
import { updatePersonalAccountAction } from '../_lib/server/actions';
|
||||
|
||||
export default function AccountSettingsForm({
|
||||
account,
|
||||
}: {
|
||||
account: AccountWithParams | null;
|
||||
}) {
|
||||
const revalidateUserDataQuery = useRevalidatePersonalAccountDataQuery();
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(accountSettingsSchema),
|
||||
defaultValues: {
|
||||
firstName: account?.name,
|
||||
lastName: account?.last_name ?? '',
|
||||
email: account?.email,
|
||||
phone: account?.phone ?? '',
|
||||
accountParams: {
|
||||
height: account?.accountParams?.height,
|
||||
weight: account?.accountParams?.weight,
|
||||
isSmoker: !!account?.accountParams?.isSmoker,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { handleSubmit } = form;
|
||||
|
||||
const onSubmit = async (data: AccountSettings) => {
|
||||
if (!account?.id) {
|
||||
return toast.error(<Trans i18nKey="account:updateAccountError" />);
|
||||
}
|
||||
|
||||
const result = await updatePersonalAccountAction({
|
||||
accountId: account.id,
|
||||
data,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
revalidateUserDataQuery(account.primary_owner_user_id);
|
||||
return toast.success(<Trans i18nKey="account:updateAccountSuccess" />);
|
||||
}
|
||||
return toast.error(<Trans i18nKey="account:updateAccountError" />);
|
||||
};
|
||||
|
||||
if (!account) return null;
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="flex flex-col gap-6 text-left"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<FormField
|
||||
name={'firstName'}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'common:formField.firstName'} />
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
name={'lastName'}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'common:formField.lastName'} />
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
name={'accountParams.height'}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'common:formField.height'} />
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
name={'accountParams.weight'}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'common:formField.weight'} />
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
name={'phone'}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'common:formField.phone'} />
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FormField
|
||||
name={'email'}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'common:formField.email'} />
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xl font-semibold text-gray-900">
|
||||
<Trans i18nKey="account:myHabits" />
|
||||
</h2>
|
||||
|
||||
<FormField
|
||||
name="accountParams.isSmoker"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'common:formField.smoking'} />
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Select
|
||||
value={
|
||||
field.value === true
|
||||
? 'yes'
|
||||
: field.value === false
|
||||
? 'no'
|
||||
: 'preferNotToAnswer'
|
||||
}
|
||||
onValueChange={(value) => {
|
||||
if (value === 'yes') {
|
||||
field.onChange(true);
|
||||
} else if (value === 'no') {
|
||||
field.onChange(false);
|
||||
} else {
|
||||
field.onChange(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="yes">
|
||||
<Trans i18nKey="common:yes" />
|
||||
</SelectItem>
|
||||
<SelectItem value="no">
|
||||
<Trans i18nKey="common:no" />
|
||||
</SelectItem>
|
||||
<SelectItem value="preferNotToAnswer">
|
||||
<Trans i18nKey="common:preferNotToAnswer" />
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="w-36">
|
||||
<Trans i18nKey="account:updateProfileSubmitLabel" />
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
152
app/home/(user)/settings/_components/settings-navigation.tsx
Normal file
152
app/home/(user)/settings/_components/settings-navigation.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import { StoreCart } from '@medusajs/types';
|
||||
import { Cross, Menu, Shield, ShoppingCart } from 'lucide-react';
|
||||
|
||||
import { usePersonalAccountData } from '@kit/accounts/hooks/use-personal-account-data';
|
||||
import { ApplicationRoleEnum } from '@kit/accounts/types/accounts';
|
||||
import { pathsConfig } from '@kit/shared/config';
|
||||
import { useSignOut } from '@kit/supabase/hooks/use-sign-out';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@kit/ui/dropdown-menu';
|
||||
import { If } from '@kit/ui/if';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import SignOutDropdownItem from '@kit/shared/components/sign-out-dropdown-item';
|
||||
import DropdownLink from '@kit/shared/components/ui/dropdown-link';
|
||||
import { UserWorkspace } from '../../_lib/server/load-user-workspace';
|
||||
import { routes } from './settings-sidebar';
|
||||
|
||||
export function SettingsMobileNavigation(props: {
|
||||
workspace: UserWorkspace;
|
||||
cart: StoreCart | null;
|
||||
}) {
|
||||
const user = props.workspace.user;
|
||||
|
||||
const signOut = useSignOut();
|
||||
const { data: personalAccountData } = usePersonalAccountData(user.id);
|
||||
|
||||
const Links = [
|
||||
{
|
||||
children: [{ path: pathsConfig.app.home, label: 'common:routes.home' }],
|
||||
},
|
||||
]
|
||||
.concat(routes)
|
||||
.map((item, index) => {
|
||||
if ('children' in item) {
|
||||
return item.children.map((child) => {
|
||||
return (
|
||||
<DropdownLink
|
||||
key={child.path}
|
||||
path={child.path}
|
||||
label={child.label}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if ('divider' in item) {
|
||||
return <DropdownMenuSeparator key={index} />;
|
||||
}
|
||||
});
|
||||
|
||||
const hasTotpFactor = useMemo(() => {
|
||||
const factors = user?.factors ?? [];
|
||||
return factors.some(
|
||||
(factor) => factor.factor_type === 'totp' && factor.status === 'verified',
|
||||
);
|
||||
}, [user?.factors]);
|
||||
|
||||
const isSuperAdmin = useMemo(() => {
|
||||
const hasAdminRole =
|
||||
personalAccountData?.application_role === ApplicationRoleEnum.SuperAdmin;
|
||||
|
||||
return hasAdminRole && hasTotpFactor;
|
||||
}, [user, personalAccountData, hasTotpFactor]);
|
||||
|
||||
const isDoctor = useMemo(() => {
|
||||
const hasDoctorRole =
|
||||
personalAccountData?.application_role === ApplicationRoleEnum.Doctor;
|
||||
|
||||
return hasDoctorRole && hasTotpFactor;
|
||||
}, [user, personalAccountData, hasTotpFactor]);
|
||||
|
||||
const cartQuantityTotal =
|
||||
props.cart?.items?.reduce((acc, item) => acc + item.quantity, 0) ?? 0;
|
||||
const hasCartItems = cartQuantityTotal > 0;
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Menu className={'h-9'} />
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent sideOffset={10} className={'w-screen rounded-none'}>
|
||||
<If condition={props.cart && hasCartItems}>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownLink
|
||||
path={pathsConfig.app.cart}
|
||||
label="common:shoppingCartCount"
|
||||
Icon={<ShoppingCart className="stroke-[1.5px]" />}
|
||||
labelOptions={{ count: cartQuantityTotal }}
|
||||
/>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
</If>
|
||||
|
||||
<DropdownMenuGroup>{Links}</DropdownMenuGroup>
|
||||
|
||||
<If condition={isSuperAdmin}>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
className={
|
||||
's-full flex cursor-pointer items-center space-x-2 text-yellow-700 dark:text-yellow-500'
|
||||
}
|
||||
href={pathsConfig.app.admin}
|
||||
>
|
||||
<Shield className={'h-5'} />
|
||||
|
||||
<span>Super Admin</span>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</If>
|
||||
|
||||
<If condition={isDoctor}>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
className={
|
||||
'flex h-full cursor-pointer items-center space-x-2 text-yellow-700 dark:text-yellow-500'
|
||||
}
|
||||
href={pathsConfig.app.doctor}
|
||||
>
|
||||
<Cross className={'h-5'} />
|
||||
|
||||
<span>
|
||||
<Trans i18nKey="common:doctor" />
|
||||
</span>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</If>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<SignOutDropdownItem onSignOut={() => signOut.mutateAsync()} />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Separator } from "@kit/ui/separator";
|
||||
import { Trans } from "@kit/ui/trans";
|
||||
|
||||
export default function SettingsSectionHeader({
|
||||
titleKey,
|
||||
descriptionKey,
|
||||
}: {
|
||||
titleKey: string;
|
||||
descriptionKey: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-2xl font-semibold text-gray-900">
|
||||
<Trans i18nKey={titleKey} />
|
||||
</h1>
|
||||
<p className="text-gray-600">
|
||||
<Trans i18nKey={descriptionKey} />
|
||||
</p>
|
||||
<Separator />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
55
app/home/(user)/settings/_components/settings-sidebar.tsx
Normal file
55
app/home/(user)/settings/_components/settings-sidebar.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import z from 'zod';
|
||||
|
||||
import { pathsConfig } from '@kit/shared/config';
|
||||
import { NavigationConfigSchema } from '@kit/ui/navigation-schema';
|
||||
import { PageHeader } from '@kit/ui/page';
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarHeader,
|
||||
SidebarNavigation,
|
||||
} from '@kit/ui/shadcn-sidebar';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
export const routes = [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
label: 'common:routes.profile',
|
||||
path: pathsConfig.app.personalAccountSettings,
|
||||
end: true,
|
||||
},
|
||||
|
||||
{
|
||||
label: 'common:routes.preferences',
|
||||
path: pathsConfig.app.personalAccountPreferences,
|
||||
end: true,
|
||||
},
|
||||
|
||||
{
|
||||
label: 'common:routes.security',
|
||||
path: pathsConfig.app.personalAccountSecurity,
|
||||
end: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
] satisfies z.infer<typeof NavigationConfigSchema>['routes'];
|
||||
|
||||
export function SettingsSidebar() {
|
||||
return (
|
||||
<Sidebar>
|
||||
<SidebarHeader className="mt-16 h-24 w-[95vw] max-w-screen justify-center border-b bg-white pt-2">
|
||||
<PageHeader
|
||||
title={<Trans i18nKey="account:accountTabLabel" />}
|
||||
description={<Trans i18nKey={'account:accountTabDescription'} />}
|
||||
/>
|
||||
</SidebarHeader>
|
||||
|
||||
<SidebarContent className="w-auto">
|
||||
<SidebarNavigation
|
||||
config={{ style: 'custom', sidebarCollapsedStyle: 'none', routes }}
|
||||
/>
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const accountPreferencesSchema = z.object({
|
||||
preferredLanguage: z.enum(['et', 'en', 'ru']).optional().nullable(),
|
||||
isConsentToAnonymizedCompanyStatistics: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type AccountPreferences = z.infer<typeof accountPreferencesSchema>;
|
||||
21
app/home/(user)/settings/_lib/account-settings.schema.ts
Normal file
21
app/home/(user)/settings/_lib/account-settings.schema.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const accountSettingsSchema = z.object({
|
||||
firstName: z
|
||||
.string()
|
||||
.min(1, { error: 'error:tooShort' })
|
||||
.max(200, { error: 'error:tooLong' }),
|
||||
lastName: z
|
||||
.string()
|
||||
.min(1, { error: 'error:tooShort' })
|
||||
.max(200, { error: 'error:tooLong' }),
|
||||
email: z.email({ error: 'error:invalidEmail' }).nullable(),
|
||||
phone: z.e164({ error: 'error:invalidPhone' }),
|
||||
accountParams: z.object({
|
||||
height: z.coerce.number({ error: 'error:invalidNumber' }),
|
||||
weight: z.coerce.number({ error: 'error:invalidNumber' }),
|
||||
isSmoker: z.boolean().optional().nullable(),
|
||||
}),
|
||||
});
|
||||
|
||||
export type AccountSettings = z.infer<typeof accountSettingsSchema>;
|
||||
66
app/home/(user)/settings/_lib/server/actions.ts
Normal file
66
app/home/(user)/settings/_lib/server/actions.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
'use server';
|
||||
|
||||
import z from 'zod';
|
||||
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
|
||||
import {
|
||||
updatePersonalAccount,
|
||||
updatePersonalAccountPreferences,
|
||||
} from '~/lib/services/account.service';
|
||||
|
||||
import {
|
||||
AccountPreferences,
|
||||
accountPreferencesSchema,
|
||||
} from '../account-preferences.schema';
|
||||
import {
|
||||
AccountSettings,
|
||||
accountSettingsSchema,
|
||||
} from '../account-settings.schema';
|
||||
|
||||
export const updatePersonalAccountAction = enhanceAction(
|
||||
async ({ accountId, data }: { accountId: string; data: AccountSettings }) => {
|
||||
const logger = await getLogger();
|
||||
|
||||
try {
|
||||
logger.info({ accountId }, 'Updating account');
|
||||
await updatePersonalAccount(accountId, data);
|
||||
logger.info({ accountId }, 'Successfully updated account');
|
||||
return { success: true };
|
||||
} catch (e) {
|
||||
logger.error('Failed to update account', JSON.stringify(e));
|
||||
|
||||
return { success: false };
|
||||
}
|
||||
},
|
||||
{
|
||||
schema: z.object({ accountId: z.uuid(), data: accountSettingsSchema }),
|
||||
},
|
||||
);
|
||||
|
||||
export const updatePersonalAccountPreferencesAction = enhanceAction(
|
||||
async ({
|
||||
accountId,
|
||||
data,
|
||||
}: {
|
||||
accountId: string;
|
||||
data: AccountPreferences;
|
||||
}) => {
|
||||
const logger = await getLogger();
|
||||
|
||||
try {
|
||||
logger.info({ accountId }, 'Updating account preferences');
|
||||
await updatePersonalAccountPreferences(accountId, data);
|
||||
logger.info({ accountId }, 'Successfully updated account preferences');
|
||||
return { success: true };
|
||||
} catch (e) {
|
||||
logger.error('Failed to update account preferences', JSON.stringify(e));
|
||||
|
||||
return { success: false };
|
||||
}
|
||||
},
|
||||
{
|
||||
schema: z.object({ accountId: z.uuid(), data: accountPreferencesSchema }),
|
||||
},
|
||||
);
|
||||
@@ -1,23 +1,69 @@
|
||||
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import { use } from 'react';
|
||||
|
||||
import { retrieveCart } from '@lib/data/cart';
|
||||
import { StoreCart } from '@medusajs/types';
|
||||
|
||||
import { UserWorkspaceContextProvider } from '@kit/accounts/components';
|
||||
import { AppLogo } from '@kit/shared/components/app-logo';
|
||||
import { pathsConfig } from '@kit/shared/config';
|
||||
import { Page, PageMobileNavigation, PageNavigation } from '@kit/ui/page';
|
||||
import { SidebarProvider } from '@kit/ui/shadcn-sidebar';
|
||||
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
import { SettingsSidebar } from './_components/settings-sidebar';
|
||||
// home imports
|
||||
import { HomeMenuNavigation } from '../_components/home-menu-navigation';
|
||||
import { loadUserWorkspace } from '../_lib/server/load-user-workspace';
|
||||
import { SettingsMobileNavigation } from './_components/settings-navigation';
|
||||
|
||||
// local imports
|
||||
import { HomeLayoutPageHeader } from '../_components/home-page-header';
|
||||
|
||||
function UserSettingsLayout(props: React.PropsWithChildren) {
|
||||
return (
|
||||
<>
|
||||
<HomeLayoutPageHeader
|
||||
title={<Trans i18nKey={'account:routes.settings'} />}
|
||||
description={<AppBreadcrumbs />}
|
||||
/>
|
||||
|
||||
{props.children}
|
||||
</>
|
||||
);
|
||||
function UserSettingsLayout({ children }: React.PropsWithChildren) {
|
||||
return <HeaderLayout>{children}</HeaderLayout>;
|
||||
}
|
||||
|
||||
export default withI18n(UserSettingsLayout);
|
||||
|
||||
function HeaderLayout({ children }: React.PropsWithChildren) {
|
||||
const workspace = use(loadUserWorkspace());
|
||||
const cart = use(retrieveCart());
|
||||
|
||||
|
||||
return (
|
||||
<UserWorkspaceContextProvider value={workspace}>
|
||||
<Page style={'header'}>
|
||||
<PageNavigation>
|
||||
<HomeMenuNavigation workspace={workspace} cart={cart} />
|
||||
</PageNavigation>
|
||||
|
||||
<PageMobileNavigation className={'flex items-center justify-between'}>
|
||||
<MobileNavigation workspace={workspace} cart={cart} />
|
||||
</PageMobileNavigation>
|
||||
|
||||
<SidebarProvider defaultOpen>
|
||||
<Page style={'sidebar'}>
|
||||
<PageNavigation>
|
||||
<SettingsSidebar />
|
||||
</PageNavigation>
|
||||
<div className="md:mt-28 min-w-full min-h-full">{children}</div>
|
||||
</Page>
|
||||
</SidebarProvider>
|
||||
</Page>
|
||||
</UserWorkspaceContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileNavigation({
|
||||
workspace,
|
||||
cart,
|
||||
}: {
|
||||
workspace: Awaited<ReturnType<typeof loadUserWorkspace>>;
|
||||
cart: StoreCart | null;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<AppLogo href={pathsConfig.app.home} />
|
||||
|
||||
<SettingsMobileNavigation workspace={workspace} cart={cart} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,28 +1,11 @@
|
||||
import { use } from 'react';
|
||||
|
||||
import { PersonalAccountSettingsContainer } from '@kit/accounts/personal-account-settings';
|
||||
import {
|
||||
authConfig,
|
||||
featureFlagsConfig,
|
||||
pathsConfig,
|
||||
} from '@kit/shared/config';
|
||||
import { PageBody } from '@kit/ui/page';
|
||||
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { requireUserInServerComponent } from '~/lib/server/require-user-in-server-component';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
const features = {
|
||||
enableAccountDeletion: featureFlagsConfig.enableAccountDeletion,
|
||||
enablePasswordUpdate: authConfig.providers.password,
|
||||
};
|
||||
|
||||
const callbackPath = pathsConfig.auth.callback;
|
||||
const accountHomePath = pathsConfig.app.accountHome;
|
||||
|
||||
const paths = {
|
||||
callback: callbackPath + `?next=${accountHomePath}`,
|
||||
};
|
||||
import { loadCurrentUserAccount } from '../_lib/server/load-user-account';
|
||||
import AccountSettingsForm from './_components/account-settings-form';
|
||||
import SettingsSectionHeader from './_components/settings-section-header';
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
const i18n = await createI18nServerInstance();
|
||||
@@ -33,17 +16,18 @@ export const generateMetadata = async () => {
|
||||
};
|
||||
};
|
||||
|
||||
function PersonalAccountSettingsPage() {
|
||||
const user = use(requireUserInServerComponent());
|
||||
|
||||
async function PersonalAccountSettingsPage() {
|
||||
const account = await loadCurrentUserAccount();
|
||||
return (
|
||||
<PageBody>
|
||||
<div className={'flex w-full flex-1 flex-col lg:max-w-2xl'}>
|
||||
<PersonalAccountSettingsContainer
|
||||
userId={user.id}
|
||||
features={features}
|
||||
paths={paths}
|
||||
/>
|
||||
<div className="mx-auto w-full bg-white p-6">
|
||||
<div className="space-y-6">
|
||||
<SettingsSectionHeader
|
||||
titleKey="account:accountTabLabel"
|
||||
descriptionKey="account:accountTabDescription"
|
||||
/>
|
||||
<AccountSettingsForm account={account} />
|
||||
</div>
|
||||
</div>
|
||||
</PageBody>
|
||||
);
|
||||
|
||||
24
app/home/(user)/settings/preferences/page.tsx
Normal file
24
app/home/(user)/settings/preferences/page.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { CardTitle } from '@kit/ui/card';
|
||||
import { LanguageSelector } from '@kit/ui/language-selector';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { loadCurrentUserAccount } from '../../_lib/server/load-user-account';
|
||||
import AccountPreferencesForm from '../_components/account-preferences-form';
|
||||
import SettingsSectionHeader from '../_components/settings-section-header';
|
||||
|
||||
export default async function PreferencesPage() {
|
||||
const account = await loadCurrentUserAccount();
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full bg-white p-6">
|
||||
<div className="space-y-6">
|
||||
<SettingsSectionHeader
|
||||
titleKey="account:preferencesTabLabel"
|
||||
descriptionKey="account:preferencesTabDescription"
|
||||
/>
|
||||
|
||||
<AccountPreferencesForm account={account} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
17
app/home/(user)/settings/security/page.tsx
Normal file
17
app/home/(user)/settings/security/page.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { MultiFactorAuthFactorsList } from '@kit/accounts/components';
|
||||
|
||||
import SettingsSectionHeader from '../_components/settings-section-header';
|
||||
|
||||
export default function SecuritySettingsPage() {
|
||||
return (
|
||||
<div className="mx-auto w-full bg-white p-6">
|
||||
<div className="space-y-6">
|
||||
<SettingsSectionHeader
|
||||
titleKey="account:securityTabLabel"
|
||||
descriptionKey="account:securityTabDescription"
|
||||
/>
|
||||
<MultiFactorAuthFactorsList />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user