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:
javayhu 2025-04-02 16:15:50 +08:00
parent 639347806b
commit 63bf471b63
8 changed files with 56 additions and 62 deletions

View File

@ -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"

View File

@ -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": "删除账号失败"

View File

@ -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';

View File

@ -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 = [

View File

@ -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>

View File

@ -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>

View File

@ -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')}

View File

@ -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>