feat: update security settings UI and intl messages
- Renamed password management keys in translation files for clarity. - Added new translation keys for saving and cancel actions in both English and Chinese. - Refactored the DeleteAccountCard component to utilize a new PasswordCardWrapper for better modularity. - Updated UpdatePasswordCard to enhance user feedback and maintain consistency in translation usage. - Removed unnecessary client directive from billing page for improved performance. - Renamed SettingsAccountPage to SettingsProfilePage for better clarity in profile settings.
This commit is contained in:
parent
639347806b
commit
63bf471b63
@ -473,7 +473,7 @@
|
||||
"security": {
|
||||
"title": "Security",
|
||||
"description": "Manage your security settings",
|
||||
"password": {
|
||||
"updatePassword": {
|
||||
"title": "Change Password",
|
||||
"description": "Enter your current password and a new password",
|
||||
"currentPassword": "Current Password",
|
||||
@ -484,7 +484,9 @@
|
||||
"showPassword": "Show password",
|
||||
"hidePassword": "Hide password",
|
||||
"success": "Password updated successfully",
|
||||
"fail": "Failed to update password"
|
||||
"fail": "Failed to update password",
|
||||
"saving": "Saving...",
|
||||
"save": "Save"
|
||||
},
|
||||
"setupPassword": {
|
||||
"title": "Set Up Password",
|
||||
@ -500,6 +502,7 @@
|
||||
"confirmTitle": "Delete Account",
|
||||
"confirmDescription": "Are you sure you want to delete your account? This action cannot be undone.",
|
||||
"confirm": "Delete",
|
||||
"cancel": "Cancel",
|
||||
"deleting": "Deleting...",
|
||||
"success": "Account deleted successfully",
|
||||
"fail": "Failed to delete account"
|
||||
|
@ -470,7 +470,7 @@
|
||||
"security": {
|
||||
"title": "安全",
|
||||
"description": "管理您的安全设置",
|
||||
"password": {
|
||||
"updatePassword": {
|
||||
"title": "修改密码",
|
||||
"description": "输入您的当前密码和新密码",
|
||||
"currentPassword": "当前密码",
|
||||
@ -481,7 +481,9 @@
|
||||
"showPassword": "显示密码",
|
||||
"hidePassword": "隐藏密码",
|
||||
"success": "密码更新成功",
|
||||
"fail": "更新密码失败"
|
||||
"fail": "更新密码失败",
|
||||
"saving": "保存中...",
|
||||
"save": "保存"
|
||||
},
|
||||
"setupPassword": {
|
||||
"title": "设置密码",
|
||||
@ -497,6 +499,7 @@
|
||||
"confirmTitle": "删除账号",
|
||||
"confirmDescription": "您确定要删除您的账号吗?此操作无法撤销",
|
||||
"confirm": "删除",
|
||||
"cancel": "取消",
|
||||
"deleting": "删除中...",
|
||||
"success": "账号删除成功",
|
||||
"fail": "删除账号失败"
|
||||
|
@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { DashboardHeader } from '@/components/dashboard/dashboard-header';
|
||||
import BillingCard from '@/components/settings/billing/billing-card';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
@ -3,7 +3,7 @@ import { UpdateAvatarCard } from '@/components/settings/account/update-avatar-ca
|
||||
import { UpdateNameCard } from '@/components/settings/account/update-name-card';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export default function SettingsAccountPage() {
|
||||
export default function SettingsProfilePage() {
|
||||
const t = useTranslations();
|
||||
|
||||
const breadcrumbs = [
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { DashboardHeader } from '@/components/dashboard/dashboard-header';
|
||||
import { ConditionalUpdatePasswordCard } from '@/components/settings/security/conditional-update-password-card';
|
||||
import { PasswordCardWrapper } from '@/components/settings/security/password-card-wrapper';
|
||||
import { DeleteAccountCard } from '@/components/settings/security/delete-account-card';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
@ -33,7 +33,7 @@ export default function SettingsSecurityPage() {
|
||||
</div>
|
||||
|
||||
<div className="grid gap-8 md:grid-cols-2">
|
||||
<ConditionalUpdatePasswordCard />
|
||||
<PasswordCardWrapper />
|
||||
<DeleteAccountCard />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,6 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { FormError } from '@/components/shared/form-error';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Card,
|
||||
@ -10,14 +18,6 @@ import {
|
||||
CardHeader,
|
||||
CardTitle
|
||||
} from '@/components/ui/card';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { useLocaleRouter } from '@/i18n/navigation';
|
||||
import { authClient } from '@/lib/auth-client';
|
||||
import { cn } from '@/lib/utils';
|
||||
@ -25,19 +25,14 @@ import { useTranslations } from 'next-intl';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface DeleteAccountCardProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete user account
|
||||
*
|
||||
* This component allows users to permanently delete their account.
|
||||
* It includes a confirmation dialog to prevent accidental deletions.
|
||||
*/
|
||||
export function DeleteAccountCard({ className }: DeleteAccountCardProps) {
|
||||
const ct = useTranslations('Common');
|
||||
const t = useTranslations('Dashboard.sidebar.settings.items.security');
|
||||
export function DeleteAccountCard() {
|
||||
const t = useTranslations('Dashboard.sidebar.settings.items.security.deleteAccount');
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [showConfirmation, setShowConfirmation] = useState(false);
|
||||
const [error, setError] = useState<string | undefined>('');
|
||||
@ -64,7 +59,7 @@ export function DeleteAccountCard({ className }: DeleteAccountCardProps) {
|
||||
setShowConfirmation(false);
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success(t('deleteAccount.success'));
|
||||
toast.success(t('success'));
|
||||
refetch();
|
||||
router.replace('/');
|
||||
},
|
||||
@ -75,25 +70,25 @@ export function DeleteAccountCard({ className }: DeleteAccountCardProps) {
|
||||
// "status": 400, "statusText": "BAD_REQUEST" }
|
||||
// set freshAge to 0 to disable session refreshness check for user deletion
|
||||
setError(`${ctx.error.status}: ${ctx.error.message}`);
|
||||
toast.error(t('deleteAccount.fail'));
|
||||
toast.error(t('fail'));
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className={cn("w-full max-w-lg md:max-w-xl border-destructive/50 overflow-hidden pt-6 pb-0 flex flex-col", className)}>
|
||||
<Card className={cn("w-full max-w-lg md:max-w-xl border-destructive/50 overflow-hidden pt-6 pb-0 flex flex-col")}>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg font-bold text-destructive">
|
||||
{t('deleteAccount.title')}
|
||||
{t('title')}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{t('deleteAccount.description')}
|
||||
{t('description')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('deleteAccount.warning')}
|
||||
{t('warning')}
|
||||
</p>
|
||||
|
||||
{error && (
|
||||
@ -108,7 +103,7 @@ export function DeleteAccountCard({ className }: DeleteAccountCardProps) {
|
||||
onClick={() => setShowConfirmation(true)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
{t('deleteAccount.button')}
|
||||
{t('button')}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
|
||||
@ -117,10 +112,10 @@ export function DeleteAccountCard({ className }: DeleteAccountCardProps) {
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="text-destructive">
|
||||
{t('deleteAccount.confirmTitle')}
|
||||
{t('confirmTitle')}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t('deleteAccount.confirmDescription')}
|
||||
{t('confirmDescription')}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter className="flex justify-end gap-3">
|
||||
@ -129,7 +124,7 @@ export function DeleteAccountCard({ className }: DeleteAccountCardProps) {
|
||||
onClick={() => setShowConfirmation(false)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
{ct('cancel')}
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
@ -137,7 +132,7 @@ export function DeleteAccountCard({ className }: DeleteAccountCardProps) {
|
||||
disabled={isDeleting}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
{isDeleting ? t('deleteAccount.deleting') : t('deleteAccount.confirm')}
|
||||
{isDeleting ? t('deleting') : t('confirm')}
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
|
@ -9,18 +9,14 @@ import { useTranslations } from 'next-intl';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface ConditionalUpdatePasswordCardProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Conditionally renders either:
|
||||
* PasswordCardWrapper renders either:
|
||||
* - UpdatePasswordCard: if the user has a credential provider (email/password login)
|
||||
* - ResetPasswordCard: if the user only has social login providers and has an email
|
||||
* - PasswordSkeletonCard: when this component is still loading
|
||||
* - Nothing: if the user has no credential provider and no email
|
||||
*/
|
||||
export function ConditionalUpdatePasswordCard({ className }: ConditionalUpdatePasswordCardProps) {
|
||||
export function PasswordCardWrapper() {
|
||||
const { data: session } = authClient.useSession();
|
||||
const [hasCredentialProvider, setHasCredentialProvider] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
@ -57,28 +53,28 @@ export function ConditionalUpdatePasswordCard({ className }: ConditionalUpdatePa
|
||||
|
||||
// Don't render anything while loading
|
||||
if (isLoading) {
|
||||
return <PasswordSkeletonCard className={className} />;
|
||||
return <PasswordSkeletonCard />;
|
||||
}
|
||||
|
||||
// If user has credential provider, show UpdatePasswordCard
|
||||
if (hasCredentialProvider) {
|
||||
return <UpdatePasswordCard className={className} />;
|
||||
return <UpdatePasswordCard />;
|
||||
}
|
||||
|
||||
// If user doesn't have credential provider but has an email, show ResetPasswordCard
|
||||
// The forgot password flow requires an email address
|
||||
if (session?.user?.email) {
|
||||
return <ResetPasswordCard className={className} />;
|
||||
return <ResetPasswordCard />;
|
||||
}
|
||||
|
||||
// If user has no credential provider and no email, don't show anything
|
||||
return null;
|
||||
}
|
||||
|
||||
function PasswordSkeletonCard({ className }: { className?: string }) {
|
||||
function PasswordSkeletonCard() {
|
||||
const t = useTranslations('Dashboard.sidebar.settings.items.security');
|
||||
return (
|
||||
<Card className={cn("w-full max-w-lg md:max-w-xl overflow-hidden pt-6 pb-6 flex flex-col", className)}>
|
||||
<Card className={cn("w-full max-w-lg md:max-w-xl overflow-hidden pt-6 pb-6 flex flex-col")}>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg font-semibold">
|
||||
{t('password.title')}
|
@ -48,8 +48,7 @@ interface UpdatePasswordCardProps {
|
||||
*/
|
||||
export function UpdatePasswordCard({ className }: UpdatePasswordCardProps) {
|
||||
const router = useLocaleRouter();
|
||||
const ct = useTranslations('Common');
|
||||
const t = useTranslations('Dashboard.sidebar.settings.items.security');
|
||||
const t = useTranslations('Dashboard.sidebar.settings.items.security.updatePassword');
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [showCurrentPassword, setShowCurrentPassword] = useState(false);
|
||||
const [showNewPassword, setShowNewPassword] = useState(false);
|
||||
@ -60,10 +59,10 @@ export function UpdatePasswordCard({ className }: UpdatePasswordCardProps) {
|
||||
const formSchema = z.object({
|
||||
currentPassword: z
|
||||
.string()
|
||||
.min(1, { message: t('password.currentRequired') }),
|
||||
.min(1, { message: t('currentRequired') }),
|
||||
newPassword: z
|
||||
.string()
|
||||
.min(8, { message: t('password.newMinLength') }),
|
||||
.min(8, { message: t('newMinLength') }),
|
||||
});
|
||||
|
||||
// Initialize the form
|
||||
@ -102,7 +101,7 @@ export function UpdatePasswordCard({ className }: UpdatePasswordCardProps) {
|
||||
onSuccess: (ctx) => {
|
||||
// update password success, user information stored in ctx.data
|
||||
// console.log("update password, success:", ctx.data);
|
||||
toast.success(t('password.success'));
|
||||
toast.success(t('success'));
|
||||
router.refresh();
|
||||
form.reset();
|
||||
},
|
||||
@ -111,7 +110,7 @@ export function UpdatePasswordCard({ className }: UpdatePasswordCardProps) {
|
||||
// { "message": "Invalid password", "code": "INVALID_PASSWORD", "status": 400, "statusText": "BAD_REQUEST" }
|
||||
console.error('update password, error:', ctx.error);
|
||||
setError(`${ctx.error.status}: ${ctx.error.message}`);
|
||||
toast.error(t('password.fail'));
|
||||
toast.error(t('fail'));
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -120,10 +119,10 @@ export function UpdatePasswordCard({ className }: UpdatePasswordCardProps) {
|
||||
<Card className={cn("w-full max-w-lg md:max-w-xl overflow-hidden pt-6 pb-0 flex flex-col", className)}>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg font-semibold">
|
||||
{t('password.title')}
|
||||
{t('title')}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{t('password.description')}
|
||||
{t('description')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<Form {...form}>
|
||||
@ -135,13 +134,13 @@ export function UpdatePasswordCard({ className }: UpdatePasswordCardProps) {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('password.currentPassword')}
|
||||
{t('currentPassword')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type={showCurrentPassword ? 'text' : 'password'}
|
||||
placeholder={t('password.currentPassword')}
|
||||
placeholder={t('currentPassword')}
|
||||
{...field}
|
||||
/>
|
||||
<Button
|
||||
@ -157,7 +156,7 @@ export function UpdatePasswordCard({ className }: UpdatePasswordCardProps) {
|
||||
<EyeIcon className="h-4 w-4" />
|
||||
)}
|
||||
<span className="sr-only">
|
||||
{showCurrentPassword ? t('password.hidePassword') : t('password.showPassword')}
|
||||
{showCurrentPassword ? t('hidePassword') : t('showPassword')}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
@ -171,12 +170,12 @@ export function UpdatePasswordCard({ className }: UpdatePasswordCardProps) {
|
||||
name="newPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('password.newPassword')}</FormLabel>
|
||||
<FormLabel>{t('newPassword')}</FormLabel>
|
||||
<FormControl>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type={showNewPassword ? 'text' : 'password'}
|
||||
placeholder={t('password.newPassword')}
|
||||
placeholder={t('newPassword')}
|
||||
{...field}
|
||||
/>
|
||||
<Button
|
||||
@ -192,7 +191,7 @@ export function UpdatePasswordCard({ className }: UpdatePasswordCardProps) {
|
||||
<EyeIcon className="size-4" />
|
||||
)}
|
||||
<span className="sr-only">
|
||||
{showNewPassword ? t('password.hidePassword') : t('password.showPassword')}
|
||||
{showNewPassword ? t('hidePassword') : t('showPassword')}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
@ -205,11 +204,11 @@ export function UpdatePasswordCard({ className }: UpdatePasswordCardProps) {
|
||||
</CardContent>
|
||||
<CardFooter className="mt-6 px-6 py-4 flex justify-between items-center bg-muted rounded-none">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('password.hint')}
|
||||
{t('hint')}
|
||||
</p>
|
||||
|
||||
<Button type="submit" disabled={isSaving} className="cursor-pointer">
|
||||
{isSaving ? ct('saving') : ct('save')}
|
||||
{isSaving ? t('saving') : t('save')}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</form>
|
||||
|
Loading…
Reference in New Issue
Block a user