Next.js Supabase V3 (#463)
Version 3 of the kit: - Radix UI replaced with Base UI (using the Shadcn UI patterns) - next-intl replaces react-i18next - enhanceAction deprecated; usage moved to next-safe-action - main layout now wrapped with [locale] path segment - Teams only mode - Layout updates - Zod v4 - Next.js 16.2 - Typescript 6 - All other dependencies updated - Removed deprecated Edge CSRF - Dynamic Github Action runner
This commit is contained in:
committed by
GitHub
parent
4912e402a3
commit
7ebff31475
@@ -1,10 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { CaretSortIcon, PersonIcon } from '@radix-ui/react-icons';
|
||||
import { CheckCircle, Plus } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ChevronsUpDown, Plus, User } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@kit/ui/avatar';
|
||||
import { Button } from '@kit/ui/button';
|
||||
@@ -40,7 +39,7 @@ interface AccountSelectorProps {
|
||||
selectedAccount?: string;
|
||||
collapsed?: boolean;
|
||||
className?: string;
|
||||
collisionPadding?: number;
|
||||
showPersonalAccount?: boolean;
|
||||
|
||||
onAccountChange: (value: string | undefined) => void;
|
||||
}
|
||||
@@ -57,16 +56,14 @@ export function AccountSelector({
|
||||
enableTeamCreation: true,
|
||||
},
|
||||
collapsed = false,
|
||||
collisionPadding = 20,
|
||||
showPersonalAccount = true,
|
||||
}: React.PropsWithChildren<AccountSelectorProps>) {
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const [isCreatingAccount, setIsCreatingAccount] = useState<boolean>(false);
|
||||
const { t } = useTranslation('teams');
|
||||
const t = useTranslations('teams');
|
||||
const personalData = usePersonalAccountData(userId);
|
||||
|
||||
const value = useMemo(() => {
|
||||
return selectedAccount ?? PERSONAL_ACCOUNT_SLUG;
|
||||
}, [selectedAccount]);
|
||||
const value = selectedAccount ?? PERSONAL_ACCOUNT_SLUG;
|
||||
|
||||
const selected = accounts.find((account) => account.value === value);
|
||||
const pictureUrl = personalData.data?.picture_url;
|
||||
@@ -74,128 +71,134 @@ export function AccountSelector({
|
||||
return (
|
||||
<>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
data-test={'account-selector-trigger'}
|
||||
size={collapsed ? 'icon' : 'default'}
|
||||
variant="ghost"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className={cn(
|
||||
'dark:shadow-primary/10 group mr-1 w-full min-w-0 px-2 lg:w-auto lg:max-w-fit',
|
||||
{
|
||||
'justify-start': !collapsed,
|
||||
'm-auto justify-center px-2 lg:w-full': collapsed,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<If
|
||||
condition={selected}
|
||||
fallback={
|
||||
<span
|
||||
className={cn('flex max-w-full items-center', {
|
||||
'justify-center gap-x-0': collapsed,
|
||||
'gap-x-2': !collapsed,
|
||||
})}
|
||||
>
|
||||
<PersonalAccountAvatar pictureUrl={pictureUrl} />
|
||||
|
||||
<span
|
||||
className={cn('truncate', {
|
||||
hidden: collapsed,
|
||||
})}
|
||||
>
|
||||
<Trans i18nKey={'teams:personalAccount'} />
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{(account) => (
|
||||
<span
|
||||
className={cn('flex max-w-full items-center', {
|
||||
'justify-center gap-x-0': collapsed,
|
||||
'gap-x-2': !collapsed,
|
||||
})}
|
||||
>
|
||||
<Avatar className={'h-6 w-6 rounded-xs'}>
|
||||
<AvatarImage src={account.image ?? undefined} />
|
||||
|
||||
<AvatarFallback
|
||||
className={'group-hover:bg-background rounded-xs'}
|
||||
>
|
||||
{account.label ? account.label[0] : ''}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
<span
|
||||
className={cn('truncate', {
|
||||
hidden: collapsed,
|
||||
})}
|
||||
>
|
||||
{account.label}
|
||||
</span>
|
||||
</span>
|
||||
<PopoverTrigger
|
||||
render={
|
||||
<Button
|
||||
data-test={'account-selector-trigger'}
|
||||
size={collapsed ? 'icon' : 'default'}
|
||||
variant="ghost"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className={cn(
|
||||
'dark:shadow-primary/10 group w-full min-w-0 px-1 lg:w-auto',
|
||||
{
|
||||
'justify-start': !collapsed,
|
||||
'm-auto justify-center lg:w-full': collapsed,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
</If>
|
||||
|
||||
<CaretSortIcon
|
||||
className={cn('ml-1 h-4 w-4 shrink-0 opacity-50', {
|
||||
hidden: collapsed,
|
||||
})}
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<If
|
||||
condition={selected}
|
||||
fallback={
|
||||
<span
|
||||
className={cn('flex max-w-full items-center', {
|
||||
'justify-center gap-x-0': collapsed,
|
||||
'gap-x-2': !collapsed,
|
||||
})}
|
||||
>
|
||||
<PersonalAccountAvatar pictureUrl={pictureUrl} />
|
||||
|
||||
<span
|
||||
className={cn('truncate', {
|
||||
hidden: collapsed,
|
||||
})}
|
||||
>
|
||||
<Trans i18nKey={'teams.personalAccount'} />
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{(account) => (
|
||||
<span
|
||||
className={cn('flex max-w-full items-center', {
|
||||
'justify-center gap-x-0': collapsed,
|
||||
'gap-x-2': !collapsed,
|
||||
})}
|
||||
>
|
||||
<Avatar className={'h-6 w-6'}>
|
||||
<AvatarImage src={account.image ?? undefined} />
|
||||
|
||||
<AvatarFallback>
|
||||
{account.label ? account.label[0] : ''}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
<span
|
||||
className={cn('truncate lg:max-w-[130px]', {
|
||||
hidden: collapsed,
|
||||
})}
|
||||
>
|
||||
{account.label}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</If>
|
||||
|
||||
<ChevronsUpDown
|
||||
className={cn('h-4 w-4 shrink-0 opacity-50', {
|
||||
hidden: collapsed,
|
||||
})}
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent
|
||||
data-test={'account-selector-content'}
|
||||
className="w-full p-0"
|
||||
collisionPadding={collisionPadding}
|
||||
className="w-full gap-0 p-0"
|
||||
>
|
||||
<Command>
|
||||
<Command value={value}>
|
||||
<CommandInput placeholder={t('searchAccount')} className="h-9" />
|
||||
|
||||
<CommandList>
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
className="shadow-none"
|
||||
onSelect={() => onAccountChange(undefined)}
|
||||
value={PERSONAL_ACCOUNT_SLUG}
|
||||
>
|
||||
<PersonalAccountAvatar />
|
||||
{showPersonalAccount && (
|
||||
<>
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
tabIndex={0}
|
||||
value={PERSONAL_ACCOUNT_SLUG}
|
||||
onSelect={() => onAccountChange(undefined)}
|
||||
className={cn('', {
|
||||
'bg-muted': value === PERSONAL_ACCOUNT_SLUG,
|
||||
'data-selected:hover:bg-muted/50 data-selected:bg-transparent':
|
||||
value !== PERSONAL_ACCOUNT_SLUG,
|
||||
})}
|
||||
>
|
||||
<PersonalAccountAvatar />
|
||||
|
||||
<span className={'ml-2'}>
|
||||
<Trans i18nKey={'teams:personalAccount'} />
|
||||
</span>
|
||||
<span className={'ml-2'}>
|
||||
<Trans i18nKey={'teams.personalAccount'} />
|
||||
</span>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
|
||||
<Icon selected={value === PERSONAL_ACCOUNT_SLUG} />
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
|
||||
<CommandSeparator />
|
||||
<CommandSeparator />
|
||||
</>
|
||||
)}
|
||||
|
||||
<If condition={accounts.length > 0}>
|
||||
<CommandGroup
|
||||
heading={
|
||||
<Trans
|
||||
i18nKey={'teams:yourTeams'}
|
||||
i18nKey={'teams.yourTeams'}
|
||||
values={{ teamsCount: accounts.length }}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{(accounts ?? []).map((account) => (
|
||||
<CommandItem
|
||||
className={cn('', {
|
||||
'bg-muted': value === account.value,
|
||||
'data-selected:hover:bg-muted/50 data-selected:bg-transparent':
|
||||
value !== account.value,
|
||||
})}
|
||||
tabIndex={0}
|
||||
data-test={'account-selector-team'}
|
||||
data-name={account.label}
|
||||
data-slug={account.value}
|
||||
className={cn(
|
||||
'group my-1 flex justify-between shadow-none transition-colors',
|
||||
{
|
||||
['bg-muted']: value === account.value,
|
||||
},
|
||||
)}
|
||||
key={account.value}
|
||||
value={account.value ?? ''}
|
||||
value={account.value ?? undefined}
|
||||
onSelect={(currentValue) => {
|
||||
setOpen(false);
|
||||
|
||||
@@ -204,13 +207,12 @@ export function AccountSelector({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className={'flex items-center'}>
|
||||
<Avatar className={'mr-2 h-6 w-6 rounded-xs'}>
|
||||
<div className={'flex w-full items-center'}>
|
||||
<Avatar className={'mr-2 h-6 w-6'}>
|
||||
<AvatarImage src={account.image ?? undefined} />
|
||||
|
||||
<AvatarFallback
|
||||
className={cn('rounded-xs', {
|
||||
['bg-background']: value === account.value,
|
||||
className={cn({
|
||||
['group-hover:bg-background']:
|
||||
value !== account.value,
|
||||
})}
|
||||
@@ -219,12 +221,10 @@ export function AccountSelector({
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
<span className={'mr-2 max-w-[165px] truncate'}>
|
||||
<span className={'max-w-[165px] truncate'}>
|
||||
{account.label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Icon selected={(account.value ?? '') === value} />
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
@@ -232,26 +232,27 @@ export function AccountSelector({
|
||||
</CommandList>
|
||||
</Command>
|
||||
|
||||
<Separator />
|
||||
|
||||
<If condition={features.enableTeamCreation}>
|
||||
<div className={'p-1'}>
|
||||
<Button
|
||||
data-test={'create-team-account-trigger'}
|
||||
variant="ghost"
|
||||
size={'sm'}
|
||||
className="w-full justify-start text-sm font-normal"
|
||||
onClick={() => {
|
||||
setIsCreatingAccount(true);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<Plus className="mr-3 h-4 w-4" />
|
||||
<div className="px-1">
|
||||
<Separator />
|
||||
|
||||
<span>
|
||||
<Trans i18nKey={'teams:createTeam'} />
|
||||
</span>
|
||||
</Button>
|
||||
<div className="py-1">
|
||||
<Button
|
||||
data-test={'create-team-account-trigger'}
|
||||
variant="ghost"
|
||||
className="w-full justify-start text-sm font-normal"
|
||||
onClick={() => {
|
||||
setIsCreatingAccount(true);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<Plus className="mr-3 h-4 w-4" />
|
||||
|
||||
<span>
|
||||
<Trans i18nKey={'teams.createTeam'} />
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</If>
|
||||
</PopoverContent>
|
||||
@@ -275,18 +276,10 @@ function UserAvatar(props: { pictureUrl?: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
function Icon({ selected }: { selected: boolean }) {
|
||||
return (
|
||||
<CheckCircle
|
||||
className={cn('ml-auto h-4 w-4', selected ? 'opacity-100' : 'opacity-0')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function PersonalAccountAvatar({ pictureUrl }: { pictureUrl?: string | null }) {
|
||||
return pictureUrl ? (
|
||||
<UserAvatar pictureUrl={pictureUrl} />
|
||||
) : (
|
||||
<PersonIcon className="h-5 w-5" />
|
||||
<User className="h-5 w-5" />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -87,20 +87,19 @@ export function PersonalAccountDropdown({
|
||||
aria-label="Open your profile menu"
|
||||
data-test={'account-dropdown-trigger'}
|
||||
className={cn(
|
||||
'group/trigger fade-in focus:outline-primary flex cursor-pointer items-center group-data-[minimized=true]/sidebar:px-0',
|
||||
'group/trigger fade-in focus:outline-primary flex cursor-pointer items-center group-data-[collapsible=icon]:px-0',
|
||||
className ?? '',
|
||||
{
|
||||
['active:bg-secondary/50 items-center gap-4 rounded-md' +
|
||||
' hover:bg-secondary border border-dashed p-2 transition-colors']:
|
||||
['active:bg-secondary/50 group-data-[collapsible=none]:hover:bg-secondary items-center gap-4 rounded-md border-dashed p-2 transition-colors group-data-[collapsible=none]:border']:
|
||||
showProfileName,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<ProfileAvatar
|
||||
className={
|
||||
'group-hover/trigger:border-background/50 rounded-md border border-transparent transition-colors'
|
||||
'group-hover/trigger:border-background/50 border border-transparent transition-colors'
|
||||
}
|
||||
fallbackClassName={'rounded-md border'}
|
||||
fallbackClassName={'border'}
|
||||
displayName={displayName ?? user?.email ?? ''}
|
||||
pictureUrl={personalAccountData?.picture_url}
|
||||
/>
|
||||
@@ -108,7 +107,7 @@ export function PersonalAccountDropdown({
|
||||
<If condition={showProfileName}>
|
||||
<div
|
||||
className={
|
||||
'fade-in flex w-full flex-col truncate text-left group-data-[minimized=true]/sidebar:hidden'
|
||||
'fade-in flex w-full flex-col truncate text-left group-data-[collapsible=icon]:hidden'
|
||||
}
|
||||
>
|
||||
<span
|
||||
@@ -128,19 +127,25 @@ export function PersonalAccountDropdown({
|
||||
|
||||
<ChevronsUpDown
|
||||
className={
|
||||
'text-muted-foreground mr-1 h-8 group-data-[minimized=true]/sidebar:hidden'
|
||||
'text-muted-foreground mr-1 h-8 group-data-[collapsible=icon]:hidden'
|
||||
}
|
||||
/>
|
||||
</If>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent className={'xl:min-w-[15rem]!'}>
|
||||
<DropdownMenuItem className={'h-10! rounded-none'}>
|
||||
<DropdownMenuItem
|
||||
className={'group/item h-10! data-[highlighted]:bg-transparent'}
|
||||
>
|
||||
<div
|
||||
className={'flex flex-col justify-start truncate text-left text-xs'}
|
||||
>
|
||||
<div className={'text-muted-foreground'}>
|
||||
<Trans i18nKey={'common:signedInAs'} />
|
||||
<div
|
||||
className={
|
||||
'text-muted-foreground group-hover/item:text-muted-foreground!'
|
||||
}
|
||||
>
|
||||
<Trans i18nKey={'common.signedInAs'} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -151,48 +156,48 @@ export function PersonalAccountDropdown({
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
className={'s-full flex cursor-pointer items-center space-x-2'}
|
||||
href={paths.home}
|
||||
>
|
||||
<Home className={'h-5'} />
|
||||
<DropdownMenuItem
|
||||
render={
|
||||
<Link className={'flex items-center gap-x-2'} href={paths.home} />
|
||||
}
|
||||
>
|
||||
<Home className={'h-4 w-4'} />
|
||||
|
||||
<span>
|
||||
<Trans i18nKey={'common:routes.home'} />
|
||||
</span>
|
||||
</Link>
|
||||
<span>
|
||||
<Trans i18nKey={'common.routes.home'} />
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
className={'s-full flex cursor-pointer items-center space-x-2'}
|
||||
href={'/docs'}
|
||||
>
|
||||
<MessageCircleQuestion className={'h-5'} />
|
||||
<DropdownMenuItem
|
||||
render={
|
||||
<Link className={'flex items-center gap-x-2'} href={'/docs'} />
|
||||
}
|
||||
>
|
||||
<MessageCircleQuestion className={'h-4 w-4'} />
|
||||
|
||||
<span>
|
||||
<Trans i18nKey={'common:documentation'} />
|
||||
</span>
|
||||
</Link>
|
||||
<span>
|
||||
<Trans i18nKey={'common.documentation'} />
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<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={'/admin'}
|
||||
>
|
||||
<Shield className={'h-5'} />
|
||||
<DropdownMenuItem
|
||||
render={
|
||||
<Link
|
||||
className={
|
||||
'flex items-center gap-x-2 text-yellow-700 dark:text-yellow-500'
|
||||
}
|
||||
href={'/admin'}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Shield className={'h-4 w-4'} />
|
||||
|
||||
<span>Super Admin</span>
|
||||
</Link>
|
||||
<span>Super Admin</span>
|
||||
</DropdownMenuItem>
|
||||
</If>
|
||||
|
||||
@@ -210,11 +215,11 @@ export function PersonalAccountDropdown({
|
||||
className={'cursor-pointer'}
|
||||
onClick={signOutRequested}
|
||||
>
|
||||
<span className={'flex w-full items-center space-x-2'}>
|
||||
<LogOut className={'h-5'} />
|
||||
<span className={'flex w-full items-center gap-x-2'}>
|
||||
<LogOut className={'h-4 w-4'} />
|
||||
|
||||
<span>
|
||||
<Trans i18nKey={'auth:signOut'} />
|
||||
<Trans i18nKey={'auth.signOut'} />
|
||||
</span>
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useFormStatus } from 'react-dom';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
|
||||
import { TriangleAlert } from 'lucide-react';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
|
||||
import { ErrorBoundary } from '@kit/monitoring/components';
|
||||
@@ -31,11 +30,11 @@ export function AccountDangerZone() {
|
||||
<div className={'flex flex-col space-y-4'}>
|
||||
<div className={'flex flex-col space-y-1'}>
|
||||
<span className={'text-sm font-medium'}>
|
||||
<Trans i18nKey={'account:deleteAccount'} />
|
||||
<Trans i18nKey={'account.deleteAccount'} />
|
||||
</span>
|
||||
|
||||
<p className={'text-muted-foreground text-sm'}>
|
||||
<Trans i18nKey={'account:deleteAccountDescription'} />
|
||||
<Trans i18nKey={'account.deleteAccountDescription'} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -55,16 +54,18 @@ function DeleteAccountModal() {
|
||||
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button data-test={'delete-account-button'} variant={'destructive'}>
|
||||
<Trans i18nKey={'account:deleteAccount'} />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogTrigger
|
||||
render={
|
||||
<Button data-test={'delete-account-button'} variant={'destructive'}>
|
||||
<Trans i18nKey={'account.deleteAccount'} />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<AlertDialogContent onEscapeKeyDown={(e) => e.preventDefault()}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
<Trans i18nKey={'account:deleteAccount'} />
|
||||
<Trans i18nKey={'account.deleteAccount'} />
|
||||
</AlertDialogTitle>
|
||||
</AlertDialogHeader>
|
||||
|
||||
@@ -77,6 +78,8 @@ function DeleteAccountModal() {
|
||||
}
|
||||
|
||||
function DeleteAccountForm(props: { email: string }) {
|
||||
const { execute, isPending } = useAction(deletePersonalAccountAction);
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(DeletePersonalAccountSchema),
|
||||
defaultValues: {
|
||||
@@ -94,7 +97,7 @@ function DeleteAccountForm(props: { email: string }) {
|
||||
onSuccess={(otp) => form.setValue('otp', otp, { shouldValidate: true })}
|
||||
CancelButton={
|
||||
<AlertDialogCancel>
|
||||
<Trans i18nKey={'common:cancel'} />
|
||||
<Trans i18nKey={'common.cancel'} />
|
||||
</AlertDialogCancel>
|
||||
}
|
||||
/>
|
||||
@@ -105,11 +108,12 @@ function DeleteAccountForm(props: { email: string }) {
|
||||
<Form {...form}>
|
||||
<form
|
||||
data-test={'delete-account-form'}
|
||||
action={deletePersonalAccountAction}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
execute({ otp });
|
||||
}}
|
||||
className={'flex flex-col space-y-4'}
|
||||
>
|
||||
<input type="hidden" name="otp" value={otp} />
|
||||
|
||||
<div className={'flex flex-col space-y-6'}>
|
||||
<div
|
||||
className={
|
||||
@@ -118,11 +122,11 @@ function DeleteAccountForm(props: { email: string }) {
|
||||
>
|
||||
<div className={'flex flex-col space-y-2'}>
|
||||
<div>
|
||||
<Trans i18nKey={'account:deleteAccountDescription'} />
|
||||
<Trans i18nKey={'account.deleteAccountDescription'} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Trans i18nKey={'common:modalConfirmationQuestion'} />
|
||||
<Trans i18nKey={'common.modalConfirmationQuestion'} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -130,36 +134,28 @@ function DeleteAccountForm(props: { email: string }) {
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
<Trans i18nKey={'common:cancel'} />
|
||||
<Trans i18nKey={'common.cancel'} />
|
||||
</AlertDialogCancel>
|
||||
|
||||
<DeleteAccountSubmitButton disabled={!form.formState.isValid} />
|
||||
<Button
|
||||
data-test={'confirm-delete-account-button'}
|
||||
type={'submit'}
|
||||
disabled={isPending || !form.formState.isValid}
|
||||
name={'action'}
|
||||
variant={'destructive'}
|
||||
>
|
||||
{isPending ? (
|
||||
<Trans i18nKey={'account.deletingAccount'} />
|
||||
) : (
|
||||
<Trans i18nKey={'account.deleteAccount'} />
|
||||
)}
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
function DeleteAccountSubmitButton(props: { disabled: boolean }) {
|
||||
const { pending } = useFormStatus();
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-test={'confirm-delete-account-button'}
|
||||
type={'submit'}
|
||||
disabled={pending || props.disabled}
|
||||
name={'action'}
|
||||
variant={'destructive'}
|
||||
>
|
||||
{pending ? (
|
||||
<Trans i18nKey={'account:deletingAccount'} />
|
||||
) : (
|
||||
<Trans i18nKey={'account:deleteAccount'} />
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function DeleteAccountErrorContainer() {
|
||||
return (
|
||||
<div className="flex flex-col gap-y-4">
|
||||
@@ -167,7 +163,7 @@ function DeleteAccountErrorContainer() {
|
||||
|
||||
<div>
|
||||
<AlertDialogCancel>
|
||||
<Trans i18nKey={'common:cancel'} />
|
||||
<Trans i18nKey={'common.cancel'} />
|
||||
</AlertDialogCancel>
|
||||
</div>
|
||||
</div>
|
||||
@@ -177,14 +173,14 @@ function DeleteAccountErrorContainer() {
|
||||
function DeleteAccountErrorAlert() {
|
||||
return (
|
||||
<Alert variant={'destructive'}>
|
||||
<ExclamationTriangleIcon className={'h-4'} />
|
||||
<TriangleAlert className={'h-4'} />
|
||||
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'account:deleteAccountErrorHeading'} />
|
||||
<Trans i18nKey={'account.deleteAccountErrorHeading'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans i18nKey={'common:genericError'} />
|
||||
<Trans i18nKey={'common.genericError'} />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
import type { Provider } from '@supabase/supabase-js';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { routing } from '@kit/i18n';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -55,11 +54,11 @@ export function PersonalAccountSettingsContainer(
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Trans i18nKey={'account:accountImage'} />
|
||||
<Trans i18nKey={'account.accountImage'} />
|
||||
</CardTitle>
|
||||
|
||||
<CardDescription>
|
||||
<Trans i18nKey={'account:accountImageDescription'} />
|
||||
<Trans i18nKey={'account.accountImageDescription'} />
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
@@ -76,11 +75,11 @@ export function PersonalAccountSettingsContainer(
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Trans i18nKey={'account:name'} />
|
||||
<Trans i18nKey={'account.name'} />
|
||||
</CardTitle>
|
||||
|
||||
<CardDescription>
|
||||
<Trans i18nKey={'account:nameDescription'} />
|
||||
<Trans i18nKey={'account.nameDescription'} />
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
@@ -93,16 +92,16 @@ export function PersonalAccountSettingsContainer(
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Trans i18nKey={'account:language'} />
|
||||
<Trans i18nKey={'account.language'} />
|
||||
</CardTitle>
|
||||
|
||||
<CardDescription>
|
||||
<Trans i18nKey={'account:languageDescription'} />
|
||||
<Trans i18nKey={'account.languageDescription'} />
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<LanguageSelector />
|
||||
<LanguageSelector locales={routing.locales} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</If>
|
||||
@@ -110,11 +109,11 @@ export function PersonalAccountSettingsContainer(
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Trans i18nKey={'account:updateEmailCardTitle'} />
|
||||
<Trans i18nKey={'account.updateEmailCardTitle'} />
|
||||
</CardTitle>
|
||||
|
||||
<CardDescription>
|
||||
<Trans i18nKey={'account:updateEmailCardDescription'} />
|
||||
<Trans i18nKey={'account.updateEmailCardDescription'} />
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
@@ -127,11 +126,11 @@ export function PersonalAccountSettingsContainer(
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Trans i18nKey={'account:updatePasswordCardTitle'} />
|
||||
<Trans i18nKey={'account.updatePasswordCardTitle'} />
|
||||
</CardTitle>
|
||||
|
||||
<CardDescription>
|
||||
<Trans i18nKey={'account:updatePasswordCardDescription'} />
|
||||
<Trans i18nKey={'account.updatePasswordCardDescription'} />
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
@@ -144,11 +143,11 @@ export function PersonalAccountSettingsContainer(
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Trans i18nKey={'account:multiFactorAuth'} />
|
||||
<Trans i18nKey={'account.multiFactorAuth'} />
|
||||
</CardTitle>
|
||||
|
||||
<CardDescription>
|
||||
<Trans i18nKey={'account:multiFactorAuthDescription'} />
|
||||
<Trans i18nKey={'account.multiFactorAuthDescription'} />
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
@@ -160,11 +159,11 @@ export function PersonalAccountSettingsContainer(
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Trans i18nKey={'account:linkedAccounts'} />
|
||||
<Trans i18nKey={'account.linkedAccounts'} />
|
||||
</CardTitle>
|
||||
|
||||
<CardDescription>
|
||||
<Trans i18nKey={'account:linkedAccountsDescription'} />
|
||||
<Trans i18nKey={'account.linkedAccountsDescription'} />
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
@@ -183,11 +182,11 @@ export function PersonalAccountSettingsContainer(
|
||||
<Card className={'border-destructive'}>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Trans i18nKey={'account:dangerZone'} />
|
||||
<Trans i18nKey={'account.dangerZone'} />
|
||||
</CardTitle>
|
||||
|
||||
<CardDescription>
|
||||
<Trans i18nKey={'account:dangerZoneDescription'} />
|
||||
<Trans i18nKey={'account.dangerZoneDescription'} />
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
@@ -201,10 +200,7 @@ export function PersonalAccountSettingsContainer(
|
||||
}
|
||||
|
||||
function useSupportMultiLanguage() {
|
||||
const { i18n } = useTranslation();
|
||||
const langs = (i18n?.options?.supportedLngs as string[]) ?? [];
|
||||
const { locales } = routing;
|
||||
|
||||
const supportedLangs = langs.filter((lang) => lang !== 'cimode');
|
||||
|
||||
return supportedLangs.length > 1;
|
||||
return locales.length > 1;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { CheckIcon } from '@radix-ui/react-icons';
|
||||
import { Mail } from 'lucide-react';
|
||||
import { Check, Mail } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useUpdateUser } from '@kit/supabase/hooks/use-update-user-mutation';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
@@ -62,7 +61,7 @@ export function UpdateEmailForm({
|
||||
callbackPath: string;
|
||||
onSuccess?: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation('account');
|
||||
const t = useTranslations('account');
|
||||
const updateUserMutation = useUpdateUser();
|
||||
const isSettingEmail = !email;
|
||||
|
||||
@@ -108,14 +107,14 @@ export function UpdateEmailForm({
|
||||
>
|
||||
<If condition={updateUserMutation.data}>
|
||||
<Alert variant={'success'}>
|
||||
<CheckIcon className={'h-4'} />
|
||||
<Check className={'h-4'} />
|
||||
|
||||
<AlertTitle>
|
||||
<Trans
|
||||
i18nKey={
|
||||
isSettingEmail
|
||||
? 'account:setEmailSuccess'
|
||||
: 'account:updateEmailSuccess'
|
||||
? 'account.setEmailSuccess'
|
||||
: 'account.updateEmailSuccess'
|
||||
}
|
||||
/>
|
||||
</AlertTitle>
|
||||
@@ -124,8 +123,8 @@ export function UpdateEmailForm({
|
||||
<Trans
|
||||
i18nKey={
|
||||
isSettingEmail
|
||||
? 'account:setEmailSuccessMessage'
|
||||
: 'account:updateEmailSuccessMessage'
|
||||
? 'account.setEmailSuccessMessage'
|
||||
: 'account.updateEmailSuccessMessage'
|
||||
}
|
||||
/>
|
||||
</AlertDescription>
|
||||
@@ -148,9 +147,7 @@ export function UpdateEmailForm({
|
||||
required
|
||||
type={'email'}
|
||||
placeholder={t(
|
||||
isSettingEmail
|
||||
? 'account:emailAddress'
|
||||
: 'account:newEmail',
|
||||
isSettingEmail ? 'emailAddress' : 'newEmail',
|
||||
)}
|
||||
{...field}
|
||||
/>
|
||||
@@ -177,7 +174,7 @@ export function UpdateEmailForm({
|
||||
data-test={'account-email-form-repeat-email-input'}
|
||||
required
|
||||
type={'email'}
|
||||
placeholder={t('account:repeatEmail')}
|
||||
placeholder={t('repeatEmail')}
|
||||
/>
|
||||
</InputGroup>
|
||||
</FormControl>
|
||||
@@ -190,12 +187,12 @@ export function UpdateEmailForm({
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button disabled={updateUserMutation.isPending}>
|
||||
<Button type="submit" disabled={updateUserMutation.isPending}>
|
||||
<Trans
|
||||
i18nKey={
|
||||
isSettingEmail
|
||||
? 'account:setEmailAddress'
|
||||
: 'account:updateEmailSubmitLabel'
|
||||
? 'account.setEmailAddress'
|
||||
: 'account.updateEmailSubmitLabel'
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
|
||||
@@ -112,9 +112,9 @@ export function LinkAccountsList(props: LinkAccountsListProps) {
|
||||
const promise = unlinkMutation.mutateAsync(identity);
|
||||
|
||||
toast.promise(promise, {
|
||||
loading: <Trans i18nKey={'account:unlinkingAccount'} />,
|
||||
success: <Trans i18nKey={'account:accountUnlinked'} />,
|
||||
error: <Trans i18nKey={'account:unlinkAccountError'} />,
|
||||
loading: <Trans i18nKey={'account.unlinkingAccount'} />,
|
||||
success: <Trans i18nKey={'account.accountUnlinked'} />,
|
||||
error: <Trans i18nKey={'account.unlinkAccountError'} />,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -129,9 +129,9 @@ export function LinkAccountsList(props: LinkAccountsListProps) {
|
||||
});
|
||||
|
||||
toast.promise(promise, {
|
||||
loading: <Trans i18nKey={'account:linkingAccount'} />,
|
||||
success: <Trans i18nKey={'account:accountLinked'} />,
|
||||
error: <Trans i18nKey={'account:linkAccountError'} />,
|
||||
loading: <Trans i18nKey={'account.linkingAccount'} />,
|
||||
success: <Trans i18nKey={'account.accountLinked'} />,
|
||||
error: <Trans i18nKey={'account.linkAccountError'} />,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -149,11 +149,11 @@ export function LinkAccountsList(props: LinkAccountsListProps) {
|
||||
<div className="space-y-2.5">
|
||||
<div>
|
||||
<h3 className="text-foreground text-sm font-medium">
|
||||
<Trans i18nKey={'account:linkedMethods'} />
|
||||
<Trans i18nKey={'account.linkedMethods'} />
|
||||
</h3>
|
||||
|
||||
<p className="text-muted-foreground text-xs">
|
||||
<Trans i18nKey={'account:alreadyLinkedMethodsDescription'} />
|
||||
<Trans i18nKey={'account.alreadyLinkedMethodsDescription'} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -185,28 +185,30 @@ export function LinkAccountsList(props: LinkAccountsListProps) {
|
||||
<ItemActions>
|
||||
<If condition={hasMultipleIdentities}>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={unlinkMutation.isPending}
|
||||
>
|
||||
<If condition={unlinkMutation.isPending}>
|
||||
<Spinner className="mr-2 h-3 w-3" />
|
||||
</If>
|
||||
<Trans i18nKey={'account:unlinkAccount'} />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogTrigger
|
||||
render={
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={unlinkMutation.isPending}
|
||||
>
|
||||
<If condition={unlinkMutation.isPending}>
|
||||
<Spinner className="mr-2 h-3 w-3" />
|
||||
</If>
|
||||
<Trans i18nKey={'account.unlinkAccount'} />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
<Trans i18nKey={'account:confirmUnlinkAccount'} />
|
||||
<Trans i18nKey={'account.confirmUnlinkAccount'} />
|
||||
</AlertDialogTitle>
|
||||
|
||||
<AlertDialogDescription>
|
||||
<Trans
|
||||
i18nKey={'account:unlinkAccountConfirmation'}
|
||||
i18nKey={'account.unlinkAccountConfirmation'}
|
||||
values={{ provider: identity.provider }}
|
||||
/>
|
||||
</AlertDialogDescription>
|
||||
@@ -214,14 +216,14 @@ export function LinkAccountsList(props: LinkAccountsListProps) {
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
<Trans i18nKey={'common:cancel'} />
|
||||
<Trans i18nKey={'common.cancel'} />
|
||||
</AlertDialogCancel>
|
||||
|
||||
<AlertDialogAction
|
||||
onClick={() => handleUnlinkAccount(identity)}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
<Trans i18nKey={'account:unlinkAccount'} />
|
||||
<Trans i18nKey={'account.unlinkAccount'} />
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
@@ -243,11 +245,11 @@ export function LinkAccountsList(props: LinkAccountsListProps) {
|
||||
<div className="space-y-2.5">
|
||||
<div>
|
||||
<h3 className="text-foreground text-sm font-medium">
|
||||
<Trans i18nKey={'account:availableMethods'} />
|
||||
<Trans i18nKey={'account.availableMethods'} />
|
||||
</h3>
|
||||
|
||||
<p className="text-muted-foreground text-xs">
|
||||
<Trans i18nKey={'account:availableMethodsDescription'} />
|
||||
<Trans i18nKey={'account.availableMethodsDescription'} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -281,7 +283,7 @@ export function LinkAccountsList(props: LinkAccountsListProps) {
|
||||
|
||||
<ItemDescription>
|
||||
<Trans
|
||||
i18nKey={'account:linkAccountDescription'}
|
||||
i18nKey={'account.linkAccountDescription'}
|
||||
values={{ provider }}
|
||||
/>
|
||||
</ItemDescription>
|
||||
@@ -299,7 +301,7 @@ function NoAccountsAvailable() {
|
||||
return (
|
||||
<div>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
<Trans i18nKey={'account:noAccountsAvailable'} />
|
||||
<Trans i18nKey={'account.noAccountsAvailable'} />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
@@ -310,38 +312,41 @@ function UpdateEmailDialog(props: { redirectTo: string }) {
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Item variant="outline" role="button" className="hover:bg-muted/50">
|
||||
<ItemMedia>
|
||||
<div className="text-muted-foreground flex h-5 w-5 items-center justify-center">
|
||||
<OauthProviderLogoImage providerId={'email'} />
|
||||
</div>
|
||||
</ItemMedia>
|
||||
|
||||
<ItemContent>
|
||||
<ItemHeader>
|
||||
<div className="flex flex-col">
|
||||
<ItemTitle className="text-sm font-medium">
|
||||
<Trans i18nKey={'account:setEmailAddress'} />
|
||||
</ItemTitle>
|
||||
|
||||
<ItemDescription>
|
||||
<Trans i18nKey={'account:setEmailDescription'} />
|
||||
</ItemDescription>
|
||||
<DialogTrigger
|
||||
nativeButton={false}
|
||||
render={
|
||||
<Item variant="outline" role="button" className="hover:bg-muted/50">
|
||||
<ItemMedia>
|
||||
<div className="text-muted-foreground flex h-5 w-5 items-center justify-center">
|
||||
<OauthProviderLogoImage providerId={'email'} />
|
||||
</div>
|
||||
</ItemHeader>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
</DialogTrigger>
|
||||
</ItemMedia>
|
||||
|
||||
<ItemContent>
|
||||
<ItemHeader>
|
||||
<div className="flex flex-col">
|
||||
<ItemTitle className="text-sm font-medium">
|
||||
<Trans i18nKey={'account.setEmailAddress'} />
|
||||
</ItemTitle>
|
||||
|
||||
<ItemDescription>
|
||||
<Trans i18nKey={'account.setEmailDescription'} />
|
||||
</ItemDescription>
|
||||
</div>
|
||||
</ItemHeader>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
}
|
||||
/>
|
||||
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans i18nKey={'account:setEmailAddress'} />
|
||||
<Trans i18nKey={'account.setEmailAddress'} />
|
||||
</DialogTitle>
|
||||
|
||||
<DialogDescription>
|
||||
<Trans i18nKey={'account:setEmailDescription'} />
|
||||
<Trans i18nKey={'account.setEmailDescription'} />
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -373,34 +378,38 @@ function UpdatePasswordDialog(props: {
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild data-test="open-password-dialog-trigger">
|
||||
<Item variant="outline" role="button" className="hover:bg-muted/50">
|
||||
<ItemMedia>
|
||||
<div className="text-muted-foreground flex h-5 w-5 items-center justify-center">
|
||||
<OauthProviderLogoImage providerId={'password'} />
|
||||
</div>
|
||||
</ItemMedia>
|
||||
|
||||
<ItemContent>
|
||||
<ItemHeader>
|
||||
<div className="flex flex-col">
|
||||
<ItemTitle className="text-sm font-medium">
|
||||
<Trans i18nKey={'account:linkEmailPassword'} />
|
||||
</ItemTitle>
|
||||
|
||||
<ItemDescription>
|
||||
<Trans i18nKey={'account:updatePasswordDescription'} />
|
||||
</ItemDescription>
|
||||
<DialogTrigger
|
||||
nativeButton={false}
|
||||
data-test="open-password-dialog-trigger"
|
||||
render={
|
||||
<Item variant="outline" role="button" className="hover:bg-muted/50">
|
||||
<ItemMedia>
|
||||
<div className="text-muted-foreground flex h-5 w-5 items-center justify-center">
|
||||
<OauthProviderLogoImage providerId={'password'} />
|
||||
</div>
|
||||
</ItemHeader>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
</DialogTrigger>
|
||||
</ItemMedia>
|
||||
|
||||
<ItemContent>
|
||||
<ItemHeader>
|
||||
<div className="flex flex-col">
|
||||
<ItemTitle className="text-sm font-medium">
|
||||
<Trans i18nKey={'account.linkEmailPassword'} />
|
||||
</ItemTitle>
|
||||
|
||||
<ItemDescription>
|
||||
<Trans i18nKey={'account.updatePasswordDescription'} />
|
||||
</ItemDescription>
|
||||
</div>
|
||||
</ItemHeader>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
}
|
||||
/>
|
||||
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans i18nKey={'account:linkEmailPassword'} />
|
||||
<Trans i18nKey={'account.linkEmailPassword'} />
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
|
||||
@@ -4,10 +4,9 @@ import { useCallback, useState } from 'react';
|
||||
|
||||
import type { Factor } from '@supabase/supabase-js';
|
||||
|
||||
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { ShieldCheck, X } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ShieldCheck, TriangleAlert, X } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
import { useFetchAuthFactors } from '@kit/supabase/hooks/use-fetch-mfa-factors';
|
||||
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
|
||||
@@ -78,7 +77,7 @@ function FactorsTableContainer(props: { userId: string }) {
|
||||
<Spinner />
|
||||
|
||||
<div>
|
||||
<Trans i18nKey={'account:loadingFactors'} />
|
||||
<Trans i18nKey={'account.loadingFactors'} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -88,14 +87,14 @@ function FactorsTableContainer(props: { userId: string }) {
|
||||
return (
|
||||
<div>
|
||||
<Alert variant={'destructive'}>
|
||||
<ExclamationTriangleIcon className={'h-4'} />
|
||||
<TriangleAlert className={'h-4'} />
|
||||
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'account:factorsListError'} />
|
||||
<Trans i18nKey={'account.factorsListError'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans i18nKey={'account:factorsListErrorDescription'} />
|
||||
<Trans i18nKey={'account.factorsListErrorDescription'} />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
@@ -114,11 +113,11 @@ function FactorsTableContainer(props: { userId: string }) {
|
||||
|
||||
<ItemContent>
|
||||
<ItemTitle>
|
||||
<Trans i18nKey={'account:multiFactorAuthHeading'} />
|
||||
<Trans i18nKey={'account.multiFactorAuthHeading'} />
|
||||
</ItemTitle>
|
||||
|
||||
<ItemDescription>
|
||||
<Trans i18nKey={'account:multiFactorAuthDescription'} />
|
||||
<Trans i18nKey={'account.multiFactorAuthDescription'} />
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
@@ -136,7 +135,7 @@ function ConfirmUnenrollFactorModal(
|
||||
setIsModalOpen: (isOpen: boolean) => void;
|
||||
}>,
|
||||
) {
|
||||
const { t } = useTranslation();
|
||||
const t = useTranslations();
|
||||
const unEnroll = useUnenrollFactor(props.userId);
|
||||
|
||||
const onUnenrollRequested = useCallback(
|
||||
@@ -149,15 +148,18 @@ function ConfirmUnenrollFactorModal(
|
||||
if (!response.success) {
|
||||
const errorCode = response.data;
|
||||
|
||||
throw t(`auth:errors.${errorCode}`, {
|
||||
defaultValue: t(`account:unenrollFactorError`),
|
||||
});
|
||||
throw t(
|
||||
`auth.errors.${errorCode}` as never,
|
||||
{
|
||||
defaultValue: t(`account.unenrollFactorError` as never),
|
||||
} as never,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
toast.promise(promise, {
|
||||
loading: t(`account:unenrollingFactor`),
|
||||
success: t(`account:unenrollFactorSuccess`),
|
||||
loading: t(`account.unenrollingFactor` as never),
|
||||
success: t(`account.unenrollFactorSuccess` as never),
|
||||
error: (error: string) => {
|
||||
return error;
|
||||
},
|
||||
@@ -171,17 +173,17 @@ function ConfirmUnenrollFactorModal(
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
<Trans i18nKey={'account:unenrollFactorModalHeading'} />
|
||||
<Trans i18nKey={'account.unenrollFactorModalHeading'} />
|
||||
</AlertDialogTitle>
|
||||
|
||||
<AlertDialogDescription>
|
||||
<Trans i18nKey={'account:unenrollFactorModalDescription'} />
|
||||
<Trans i18nKey={'account.unenrollFactorModalDescription'} />
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
<Trans i18nKey={'common:cancel'} />
|
||||
<Trans i18nKey={'common.cancel'} />
|
||||
</AlertDialogCancel>
|
||||
|
||||
<AlertDialogAction
|
||||
@@ -189,7 +191,7 @@ function ConfirmUnenrollFactorModal(
|
||||
disabled={unEnroll.isPending}
|
||||
onClick={() => onUnenrollRequested(props.factorId)}
|
||||
>
|
||||
<Trans i18nKey={'account:unenrollFactorModalButtonLabel'} />
|
||||
<Trans i18nKey={'account.unenrollFactorModalButtonLabel'} />
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
@@ -212,13 +214,13 @@ function FactorsTable({
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>
|
||||
<Trans i18nKey={'account:factorName'} />
|
||||
<Trans i18nKey={'account.factorName'} />
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
<Trans i18nKey={'account:factorType'} />
|
||||
<Trans i18nKey={'account.factorType'} />
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
<Trans i18nKey={'account:factorStatus'} />
|
||||
<Trans i18nKey={'account.factorStatus'} />
|
||||
</TableHead>
|
||||
|
||||
<TableHead />
|
||||
@@ -250,18 +252,20 @@ function FactorsTable({
|
||||
<td className={'flex justify-end'}>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant={'ghost'}
|
||||
size={'icon'}
|
||||
onClick={() => setUnenrolling(factor.id)}
|
||||
>
|
||||
<X className={'h-4'} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<Button
|
||||
variant={'ghost'}
|
||||
size={'icon'}
|
||||
onClick={() => setUnenrolling(factor.id)}
|
||||
>
|
||||
<X className={'h-4'} />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<TooltipContent>
|
||||
<Trans i18nKey={'account:unenrollTooltip'} />
|
||||
<Trans i18nKey={'account.unenrollTooltip'} />
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
@@ -3,12 +3,11 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { ArrowLeftIcon } from 'lucide-react';
|
||||
import { ArrowLeftIcon, TriangleAlert } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { z } from 'zod';
|
||||
import * as z from 'zod';
|
||||
|
||||
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
|
||||
import { useFactorsMutationKey } from '@kit/supabase/hooks/use-user-factors-mutation-key';
|
||||
@@ -31,6 +30,7 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@kit/ui/form';
|
||||
import { useAsyncDialog } from '@kit/ui/hooks/use-async-dialog';
|
||||
import { If } from '@kit/ui/if';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import {
|
||||
@@ -45,41 +45,43 @@ import { Trans } from '@kit/ui/trans';
|
||||
import { refreshAuthSession } from '../../../server/personal-accounts-server-actions';
|
||||
|
||||
export function MultiFactorAuthSetupDialog(props: { userId: string }) {
|
||||
const { t } = useTranslation();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const t = useTranslations();
|
||||
const { dialogProps, isPending, setIsPending, setOpen } = useAsyncDialog();
|
||||
|
||||
const onEnrollSuccess = useCallback(() => {
|
||||
setIsOpen(false);
|
||||
setIsPending(false);
|
||||
setOpen(false);
|
||||
|
||||
return toast.success(t(`account:multiFactorSetupSuccess`));
|
||||
}, [t]);
|
||||
return toast.success(t(`account.multiFactorSetupSuccess` as never));
|
||||
}, [t, setIsPending, setOpen]);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Trans i18nKey={'account:setupMfaButtonLabel'} />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<Dialog {...dialogProps}>
|
||||
<DialogTrigger
|
||||
render={
|
||||
<Button>
|
||||
<Trans i18nKey={'account.setupMfaButtonLabel'} />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<DialogContent
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
onEscapeKeyDown={(e) => e.preventDefault()}
|
||||
>
|
||||
<DialogContent showCloseButton={!isPending}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans i18nKey={'account:setupMfaButtonLabel'} />
|
||||
<Trans i18nKey={'account.setupMfaButtonLabel'} />
|
||||
</DialogTitle>
|
||||
|
||||
<DialogDescription>
|
||||
<Trans i18nKey={'account:multiFactorAuthDescription'} />
|
||||
<Trans i18nKey={'account.multiFactorAuthDescription'} />
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div>
|
||||
<MultiFactorAuthSetupForm
|
||||
userId={props.userId}
|
||||
onCancel={() => setIsOpen(false)}
|
||||
isPending={isPending}
|
||||
setIsPending={setIsPending}
|
||||
onCancel={() => setOpen(false)}
|
||||
onEnrolled={onEnrollSuccess}
|
||||
/>
|
||||
</div>
|
||||
@@ -92,10 +94,14 @@ function MultiFactorAuthSetupForm({
|
||||
onEnrolled,
|
||||
onCancel,
|
||||
userId,
|
||||
isPending,
|
||||
setIsPending,
|
||||
}: React.PropsWithChildren<{
|
||||
userId: string;
|
||||
onCancel: () => void;
|
||||
onEnrolled: () => void;
|
||||
isPending: boolean;
|
||||
setIsPending: (pending: boolean) => void;
|
||||
}>) {
|
||||
const verifyCodeMutation = useVerifyCodeMutation(userId);
|
||||
|
||||
@@ -112,10 +118,7 @@ function MultiFactorAuthSetupForm({
|
||||
},
|
||||
});
|
||||
|
||||
const [state, setState] = useState({
|
||||
loading: false,
|
||||
error: '',
|
||||
});
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const factorId = useWatch({
|
||||
name: 'factorId',
|
||||
@@ -130,10 +133,8 @@ function MultiFactorAuthSetupForm({
|
||||
verificationCode: string;
|
||||
factorId: string;
|
||||
}) => {
|
||||
setState({
|
||||
loading: true,
|
||||
error: '',
|
||||
});
|
||||
setIsPending(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
await verifyCodeMutation.mutateAsync({
|
||||
@@ -143,25 +144,18 @@ function MultiFactorAuthSetupForm({
|
||||
|
||||
await refreshAuthSession();
|
||||
|
||||
setState({
|
||||
loading: false,
|
||||
error: '',
|
||||
});
|
||||
|
||||
onEnrolled();
|
||||
} catch (error) {
|
||||
const message = (error as Error).message || `Unknown error`;
|
||||
|
||||
setState({
|
||||
loading: false,
|
||||
error: message,
|
||||
});
|
||||
setIsPending(false);
|
||||
setError(message);
|
||||
}
|
||||
},
|
||||
[onEnrolled, verifyCodeMutation],
|
||||
[onEnrolled, verifyCodeMutation, setIsPending],
|
||||
);
|
||||
|
||||
if (state.error) {
|
||||
if (error) {
|
||||
return <ErrorAlert />;
|
||||
}
|
||||
|
||||
@@ -170,6 +164,7 @@ function MultiFactorAuthSetupForm({
|
||||
<div className={'flex justify-center'}>
|
||||
<FactorQrCode
|
||||
userId={userId}
|
||||
isPending={isPending}
|
||||
onCancel={onCancel}
|
||||
onSetFactorId={(factorId) =>
|
||||
verificationCodeForm.setValue('factorId', factorId)
|
||||
@@ -210,7 +205,7 @@ function MultiFactorAuthSetupForm({
|
||||
|
||||
<FormDescription>
|
||||
<Trans
|
||||
i18nKey={'account:verifyActivationCodeDescription'}
|
||||
i18nKey={'account.verifyActivationCodeDescription'}
|
||||
/>
|
||||
</FormDescription>
|
||||
|
||||
@@ -222,20 +217,25 @@ function MultiFactorAuthSetupForm({
|
||||
/>
|
||||
|
||||
<div className={'flex justify-end space-x-2'}>
|
||||
<Button type={'button'} variant={'ghost'} onClick={onCancel}>
|
||||
<Trans i18nKey={'common:cancel'} />
|
||||
<Button
|
||||
type={'button'}
|
||||
variant={'ghost'}
|
||||
disabled={isPending}
|
||||
onClick={onCancel}
|
||||
>
|
||||
<Trans i18nKey={'common.cancel'} />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
disabled={
|
||||
!verificationCodeForm.formState.isValid || state.loading
|
||||
!verificationCodeForm.formState.isValid || isPending
|
||||
}
|
||||
type={'submit'}
|
||||
>
|
||||
{state.loading ? (
|
||||
<Trans i18nKey={'account:verifyingCode'} />
|
||||
{isPending ? (
|
||||
<Trans i18nKey={'account.verifyingCode'} />
|
||||
) : (
|
||||
<Trans i18nKey={'account:enableMfaFactor'} />
|
||||
<Trans i18nKey={'account.enableMfaFactor'} />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -251,13 +251,15 @@ function FactorQrCode({
|
||||
onSetFactorId,
|
||||
onCancel,
|
||||
userId,
|
||||
isPending,
|
||||
}: React.PropsWithChildren<{
|
||||
userId: string;
|
||||
isPending: boolean;
|
||||
onCancel: () => void;
|
||||
onSetFactorId: (factorId: string) => void;
|
||||
}>) {
|
||||
const enrollFactorMutation = useEnrollFactor(userId);
|
||||
const { t } = useTranslation();
|
||||
const t = useTranslations();
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
const form = useForm({
|
||||
@@ -279,16 +281,16 @@ function FactorQrCode({
|
||||
return (
|
||||
<div className={'flex w-full flex-col space-y-2'}>
|
||||
<Alert variant={'destructive'}>
|
||||
<ExclamationTriangleIcon className={'h-4'} />
|
||||
<TriangleAlert className={'h-4'} />
|
||||
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'account:qrCodeErrorHeading'} />
|
||||
<Trans i18nKey={'account.qrCodeErrorHeading'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans
|
||||
i18nKey={`auth:errors.${error}`}
|
||||
defaults={t('account:qrCodeErrorDescription')}
|
||||
i18nKey={`auth.errors.${error}`}
|
||||
defaults={t('account.qrCodeErrorDescription')}
|
||||
/>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
@@ -296,7 +298,7 @@ function FactorQrCode({
|
||||
<div>
|
||||
<Button variant={'outline'} onClick={onCancel}>
|
||||
<ArrowLeftIcon className={'h-4'} />
|
||||
<Trans i18nKey={`common:retry`} />
|
||||
<Trans i18nKey={`common.retry`} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -306,6 +308,7 @@ function FactorQrCode({
|
||||
if (!factorName) {
|
||||
return (
|
||||
<FactorNameForm
|
||||
isPending={isPending}
|
||||
onCancel={onCancel}
|
||||
onSetFactorName={async (name) => {
|
||||
const response = await enrollFactorMutation.mutateAsync(name);
|
||||
@@ -336,7 +339,7 @@ function FactorQrCode({
|
||||
>
|
||||
<p>
|
||||
<span className={'text-muted-foreground text-sm'}>
|
||||
<Trans i18nKey={'account:multiFactorModalHeading'} />
|
||||
<Trans i18nKey={'account.multiFactorModalHeading'} />
|
||||
</span>
|
||||
</p>
|
||||
|
||||
@@ -349,6 +352,7 @@ function FactorQrCode({
|
||||
|
||||
function FactorNameForm(
|
||||
props: React.PropsWithChildren<{
|
||||
isPending: boolean;
|
||||
onSetFactorName: (name: string) => void;
|
||||
onCancel: () => void;
|
||||
}>,
|
||||
@@ -379,7 +383,7 @@ function FactorNameForm(
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'account:factorNameLabel'} />
|
||||
<Trans i18nKey={'account.factorNameLabel'} />
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
@@ -387,7 +391,7 @@ function FactorNameForm(
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
<Trans i18nKey={'account:factorNameHint'} />
|
||||
<Trans i18nKey={'account.factorNameHint'} />
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
@@ -397,12 +401,17 @@ function FactorNameForm(
|
||||
/>
|
||||
|
||||
<div className={'flex justify-end space-x-2'}>
|
||||
<Button type={'button'} variant={'ghost'} onClick={props.onCancel}>
|
||||
<Trans i18nKey={'common:cancel'} />
|
||||
<Button
|
||||
type={'button'}
|
||||
variant={'ghost'}
|
||||
disabled={props.isPending}
|
||||
onClick={props.onCancel}
|
||||
>
|
||||
<Trans i18nKey={'common.cancel'} />
|
||||
</Button>
|
||||
|
||||
<Button type={'submit'}>
|
||||
<Trans i18nKey={'account:factorNameSubmitLabel'} />
|
||||
<Button type={'submit'} disabled={props.isPending}>
|
||||
<Trans i18nKey={'account.factorNameSubmitLabel'} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -501,14 +510,14 @@ function useVerifyCodeMutation(userId: string) {
|
||||
function ErrorAlert() {
|
||||
return (
|
||||
<Alert variant={'destructive'}>
|
||||
<ExclamationTriangleIcon className={'h-4'} />
|
||||
<TriangleAlert className={'h-4'} />
|
||||
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'account:multiFactorSetupErrorHeading'} />
|
||||
<Trans i18nKey={'account.multiFactorSetupErrorHeading'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans i18nKey={'account:multiFactorSetupErrorDescription'} />
|
||||
<Trans i18nKey={'account.multiFactorSetupErrorDescription'} />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
|
||||
@@ -5,10 +5,9 @@ import { useState } from 'react';
|
||||
import type { PostgrestError } from '@supabase/supabase-js';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
|
||||
import { Check, Lock, XIcon } from 'lucide-react';
|
||||
import { Check, Lock, TriangleAlert, XIcon } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useUpdateUser } from '@kit/supabase/hooks/use-update-user-mutation';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
@@ -41,7 +40,7 @@ export const UpdatePasswordForm = ({
|
||||
callbackPath: string;
|
||||
onSuccess?: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation('account');
|
||||
const t = useTranslations('account');
|
||||
const updateUserMutation = useUpdateUser();
|
||||
const [needsReauthentication, setNeedsReauthentication] = useState(false);
|
||||
|
||||
@@ -131,7 +130,7 @@ export const UpdatePasswordForm = ({
|
||||
autoComplete={'new-password'}
|
||||
required
|
||||
type={'password'}
|
||||
placeholder={t('account:newPassword')}
|
||||
placeholder={t('newPassword')}
|
||||
{...field}
|
||||
/>
|
||||
</InputGroup>
|
||||
@@ -160,14 +159,14 @@ export const UpdatePasswordForm = ({
|
||||
}
|
||||
required
|
||||
type={'password'}
|
||||
placeholder={t('account:repeatPassword')}
|
||||
placeholder={t('repeatPassword')}
|
||||
{...field}
|
||||
/>
|
||||
</InputGroup>
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
<Trans i18nKey={'account:repeatPasswordDescription'} />
|
||||
<Trans i18nKey={'account.repeatPasswordDescription'} />
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
@@ -179,10 +178,11 @@ export const UpdatePasswordForm = ({
|
||||
|
||||
<div>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={updateUserMutation.isPending}
|
||||
data-test="identity-form-submit"
|
||||
>
|
||||
<Trans i18nKey={'account:updatePasswordSubmitLabel'} />
|
||||
<Trans i18nKey={'account.updatePasswordSubmitLabel'} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -192,20 +192,20 @@ export const UpdatePasswordForm = ({
|
||||
};
|
||||
|
||||
function ErrorAlert({ error }: { error: { code: string } }) {
|
||||
const { t } = useTranslation();
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
<Alert variant={'destructive'}>
|
||||
<XIcon className={'h-4'} />
|
||||
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'account:updatePasswordError'} />
|
||||
<Trans i18nKey={'account.updatePasswordError'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans
|
||||
i18nKey={`auth:errors.${error.code}`}
|
||||
defaults={t('auth:resetPasswordError')}
|
||||
i18nKey={`auth.errors.${error.code}`}
|
||||
defaults={t('auth.resetPasswordError')}
|
||||
/>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
@@ -218,11 +218,11 @@ function SuccessAlert() {
|
||||
<Check className={'h-4'} />
|
||||
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'account:updatePasswordSuccess'} />
|
||||
<Trans i18nKey={'account.updatePasswordSuccess'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans i18nKey={'account:updatePasswordSuccessMessage'} />
|
||||
<Trans i18nKey={'account.updatePasswordSuccessMessage'} />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
@@ -231,14 +231,14 @@ function SuccessAlert() {
|
||||
function NeedsReauthenticationAlert() {
|
||||
return (
|
||||
<Alert variant={'warning'}>
|
||||
<ExclamationTriangleIcon className={'h-4'} />
|
||||
<TriangleAlert className={'h-4'} />
|
||||
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'account:needsReauthentication'} />
|
||||
<Trans i18nKey={'account.needsReauthentication'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans i18nKey={'account:needsReauthenticationDescription'} />
|
||||
<Trans i18nKey={'account.needsReauthenticationDescription'} />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { User } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Database } from '@kit/supabase/database';
|
||||
import { Button } from '@kit/ui/button';
|
||||
@@ -35,7 +35,7 @@ export function UpdateAccountDetailsForm({
|
||||
onUpdate: (user: Partial<UpdateUserDataParams>) => void;
|
||||
}) {
|
||||
const updateAccountMutation = useUpdateAccountData(userId);
|
||||
const { t } = useTranslation('account');
|
||||
const t = useTranslations('account');
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(AccountDetailsSchema),
|
||||
@@ -79,7 +79,7 @@ export function UpdateAccountDetailsForm({
|
||||
<InputGroupInput
|
||||
data-test={'account-display-name'}
|
||||
minLength={2}
|
||||
placeholder={t('account:name')}
|
||||
placeholder={t('name')}
|
||||
maxLength={100}
|
||||
{...field}
|
||||
/>
|
||||
@@ -92,8 +92,8 @@ export function UpdateAccountDetailsForm({
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Button disabled={updateAccountMutation.isPending}>
|
||||
<Trans i18nKey={'account:updateProfileSubmitLabel'} />
|
||||
<Button type="submit" disabled={updateAccountMutation.isPending}>
|
||||
<Trans i18nKey={'account.updateProfileSubmitLabel'} />
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useCallback } from 'react';
|
||||
|
||||
import type { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
import { Database } from '@kit/supabase/database';
|
||||
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
|
||||
@@ -41,7 +41,7 @@ function UploadProfileAvatarForm(props: {
|
||||
onAvatarUpdated: () => void;
|
||||
}) {
|
||||
const client = useSupabase();
|
||||
const { t } = useTranslation('account');
|
||||
const t = useTranslations('account');
|
||||
|
||||
const createToaster = useCallback(
|
||||
(promise: () => Promise<unknown>) => {
|
||||
@@ -111,11 +111,11 @@ function UploadProfileAvatarForm(props: {
|
||||
<ImageUploader value={props.pictureUrl} onValueChange={onValueChange}>
|
||||
<div className={'flex flex-col space-y-1'}>
|
||||
<span className={'text-sm'}>
|
||||
<Trans i18nKey={'account:profilePictureHeading'} />
|
||||
<Trans i18nKey={'account.profilePictureHeading'} />
|
||||
</span>
|
||||
|
||||
<span className={'text-xs'}>
|
||||
<Trans i18nKey={'account:profilePictureSubheading'} />
|
||||
<Trans i18nKey={'account.profilePictureSubheading'} />
|
||||
</span>
|
||||
</div>
|
||||
</ImageUploader>
|
||||
|
||||
Reference in New Issue
Block a user