crush-level-web/src/app/(main)/profile/edit/page.tsx

337 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { useForm } from 'react-hook-form';
import { IconButton, Button } from '@/components/ui/button';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import GenderInput from '@/components/features/genderInput';
import { Gender } from '@/types/user';
import { useRouter } from 'next/navigation';
import { useCheckNickname, useCurrentUser, useUpdateUser } from '@/hooks/auth';
import { useEffect, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
import { calculateAge } from '@/lib/utils';
import dayjs from 'dayjs';
import ProfileLayout from '@/layout/ProfileLayout';
const schema = z
.object({
nickname: z
.string()
.trim()
.min(1, 'Nickname is required')
.min(2, 'Nickname must be between 2 and 20 characters'),
gender: z.enum(Gender, { message: 'Please select a gender' }),
year: z.string().min(1, 'Please select birth year'),
month: z.string().min(1, 'Please select birth month'),
day: z.string().min(1, 'Please select birth day'),
})
.refine(
(data) => {
const age = calculateAge(data.year, data.month, data.day);
return age >= 18;
},
{
message: 'Character age must be at least 18 years old',
path: ['year'],
}
);
type EditProfileFormData = z.infer<typeof schema>;
const EditPage = () => {
const router = useRouter();
const { data: user, isLoading } = useCurrentUser();
const { mutateAsync: updateUser } = useUpdateUser();
const { mutateAsync: checkNickname } = useCheckNickname({
onError: (error) => {
form.setError('nickname', {
message: error.errorMsg,
});
},
});
const [loading, setLoading] = useState(false);
// 获取用户生日如果是时间戳则转换为Date对象
const getUserBirthday = () => {
if (!user?.birthday) return null;
return new Date(user.birthday);
};
const userBirthday = getUserBirthday();
const form = useForm<EditProfileFormData>({
resolver: zodResolver(schema),
defaultValues: {
nickname: user?.nickname || '',
gender: user?.sex,
year: userBirthday ? userBirthday.getFullYear().toString() : '',
month: userBirthday ? (userBirthday.getMonth() + 1).toString().padStart(2, '0') : '',
day: userBirthday ? userBirthday.getDate().toString().padStart(2, '0') : '',
},
mode: 'onChange',
});
const {
formState: { isValid, isDirty },
} = form;
// 当用户数据加载完成后,更新表单值
useEffect(() => {
if (user && !isLoading) {
const userBirthday = getUserBirthday();
form.reset({
nickname: user.nickname || '',
gender: user.sex,
year: userBirthday ? userBirthday.getFullYear().toString() : '',
month: userBirthday ? (userBirthday.getMonth() + 1).toString().padStart(2, '0') : '',
day: userBirthday ? userBirthday.getDate().toString().padStart(2, '0') : '',
});
}
}, [user, isLoading, form]);
const onSubmit = async (data: EditProfileFormData) => {
if (!user?.userId) {
return;
}
setLoading(true);
try {
const isExist = await checkNickname({
nickname: data.nickname.trim(),
exUserId: user.userId,
});
if (isExist) {
form.setError('nickname', {
message: 'This nickname is already taken',
});
return;
}
// 将日期字符串转换为时间戳
const birthdayDate = new Date(`${data.year}-${data.month}-${data.day}`);
const birthdayTimestamp = birthdayDate.getTime();
await updateUser({
nickname: data.nickname,
birthday: birthdayTimestamp,
userId: user.userId,
});
router.push('/profile');
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
};
const handleBack = () => {
router.back();
};
// 生成年份选项 (当前年份往前100年)
const currentYear = new Date().getFullYear();
const years = Array.from({ length: 100 }, (_, i) => currentYear - i).map((year) => ({
value: year.toString(),
label: year.toString(),
}));
const monthTexts = Array.from({ length: 12 }, (_, i) => dayjs().month(i).format('MMM'));
// 生成月份选项
const months = Array.from({ length: 12 }, (_, i) => ({
value: (i + 1).toString().padStart(2, '0'),
label: monthTexts[i],
}));
// 根据年份和月份计算该月的天数
const getDaysInMonth = (year: string, month: string): number => {
if (!year || !month) return 31; // 默认返回31天
const yearNum = parseInt(year);
const monthNum = parseInt(month);
// 使用 Date 对象计算该月的实际天数
// 传入下个月的第0天会返回上个月的最后一天
return new Date(yearNum, monthNum, 0).getDate();
};
// 监听年份和月份变化,动态生成日期选项
const selectedYear = form.watch('year');
const selectedMonth = form.watch('month');
const selectedDay = form.watch('day');
const daysInMonth = getDaysInMonth(selectedYear, selectedMonth);
// 生成日期选项,根据选中的年月动态调整
const days = Array.from({ length: daysInMonth }, (_, i) => ({
value: (i + 1).toString().padStart(2, '0'),
label: (i + 1).toString(),
}));
// 当年份或月份变化时,检查并调整日期
useEffect(() => {
if (selectedYear && selectedMonth && selectedDay) {
const currentDay = parseInt(selectedDay);
const maxDay = getDaysInMonth(selectedYear, selectedMonth);
// 如果当前选中的日期超出了该月的最大天数,自动调整为该月最后一天
if (currentDay > maxDay) {
form.setValue('day', maxDay.toString().padStart(2, '0'), { shouldValidate: true });
}
}
}, [selectedYear, selectedMonth, selectedDay, form]);
return (
<ProfileLayout title="Edit Profile">
{/* 表单容器 */}
<div className="bg-surface-base-normal rounded-2xl p-6">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
{/* 昵称字段 */}
<FormField
control={form.control}
name="nickname"
render={({ field }) => (
<FormItem>
<FormLabel className="txt-label-m text-txt-primary-normal">Nickname</FormLabel>
<FormControl>
<Input
placeholder="Enter nickname"
maxLength={20}
showCount
error={!!form.formState.errors.nickname}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* 性别字段 */}
<FormField
control={form.control}
name="gender"
disabled={true}
render={({ field }) => (
<FormItem>
<FormLabel className="txt-label-m text-txt-primary-normal">Gender</FormLabel>
<FormControl>
<GenderInput
value={field.value as Gender}
onChange={field.onChange}
disabled={true}
/>
</FormControl>
<p className="txt-body-s text-txt-secondary-normal">
Please note: gender cannot be changed after setting
</p>
<FormMessage />
</FormItem>
)}
/>
{/* 年龄字段 */}
<FormItem>
<FormLabel className="txt-label-m text-txt-primary-normal">Age</FormLabel>
<div className="grid grid-cols-3 gap-2">
<FormField
control={form.control}
name="year"
render={({ field }) => (
<FormControl>
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger block error={!!form.formState.errors.year}>
<SelectValue placeholder="Year" />
</SelectTrigger>
<SelectContent>
{years.map((year) => (
<SelectItem key={`${year.value}`} value={`${year.value}`}>
{year.label}
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
)}
/>
<FormField
control={form.control}
name="month"
render={({ field }) => (
<FormControl>
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger block error={!!form.formState.errors.year}>
<SelectValue placeholder="Month" />
</SelectTrigger>
<SelectContent>
{months.map((month) => (
<SelectItem key={month.value} value={month.value}>
{month.label}
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
)}
/>
<FormField
control={form.control}
name="day"
render={({ field }) => (
<FormControl>
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger block error={!!form.formState.errors.year}>
<SelectValue placeholder="Day" />
</SelectTrigger>
<SelectContent>
{days.map((day) => (
<SelectItem key={day.value} value={day.value}>
{day.label}
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
)}
/>
</div>
<FormMessage>
{form.formState.errors.year?.message ||
form.formState.errors.month?.message ||
form.formState.errors.day?.message}
</FormMessage>
</FormItem>
{/* 保存按钮 */}
<div className="flex justify-end">
<Button type="submit" size="large" disabled={!isValid || !isDirty} loading={loading}>
Save
</Button>
</div>
</form>
</Form>
</div>
</ProfileLayout>
);
};
export default EditPage;