2025-12-17 10:13:47 +00:00
|
|
|
'use client';
|
|
|
|
|
import { useEffect, useState, useCallback } from 'react';
|
|
|
|
|
import { z } from 'zod';
|
|
|
|
|
import dayjs from 'dayjs';
|
2025-12-09 09:13:46 +00:00
|
|
|
import {
|
|
|
|
|
Form,
|
|
|
|
|
FormControl,
|
|
|
|
|
FormField,
|
|
|
|
|
FormItem,
|
|
|
|
|
FormLabel,
|
|
|
|
|
FormMessage,
|
2025-12-17 10:13:47 +00:00
|
|
|
} from '@/components/ui/form';
|
|
|
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
|
|
|
import { useForm } from 'react-hook-form';
|
|
|
|
|
import { Gender } from '@/types/user';
|
|
|
|
|
import { Input } from '@/components/ui/input';
|
2025-12-09 09:13:46 +00:00
|
|
|
import {
|
|
|
|
|
Select,
|
|
|
|
|
SelectContent,
|
|
|
|
|
SelectItem,
|
|
|
|
|
SelectTrigger,
|
|
|
|
|
SelectValue,
|
2025-12-17 10:13:47 +00:00
|
|
|
} from '@/components/ui/select';
|
|
|
|
|
import { Label } from '@/components/ui/label';
|
|
|
|
|
import { calculateAge, getDaysInMonth } from '@/lib/utils';
|
|
|
|
|
import { Textarea } from '@/components/ui/textarea';
|
|
|
|
|
import { Button } from '@/components/ui/button';
|
2025-12-09 09:13:46 +00:00
|
|
|
import {
|
|
|
|
|
AlertDialog,
|
|
|
|
|
AlertDialogAction,
|
|
|
|
|
AlertDialogCancel,
|
|
|
|
|
AlertDialogContent,
|
|
|
|
|
AlertDialogDescription,
|
|
|
|
|
AlertDialogFooter,
|
|
|
|
|
AlertDialogHeader,
|
|
|
|
|
AlertDialogTitle,
|
2025-12-17 10:13:47 +00:00
|
|
|
} from '@/components/ui/alert-dialog';
|
2025-12-09 09:13:46 +00:00
|
|
|
|
2025-12-17 10:13:47 +00:00
|
|
|
const currentYear = dayjs().year();
|
|
|
|
|
const years = Array.from({ length: currentYear - 1950 + 1 }, (_, i) => `${1950 + i}`);
|
|
|
|
|
const months = Array.from({ length: 12 }, (_, i) => `${i + 1}`.padStart(2, '0'));
|
|
|
|
|
const monthTexts = Array.from({ length: 12 }, (_, i) => dayjs().month(i).format('MMM'));
|
2025-12-09 09:13:46 +00:00
|
|
|
|
|
|
|
|
const characterFormSchema = z
|
|
|
|
|
.object({
|
|
|
|
|
nickname: z
|
|
|
|
|
.string()
|
|
|
|
|
.trim()
|
|
|
|
|
.min(1, 'Please Enter nickname')
|
|
|
|
|
.min(2, 'Nickname must be between 2 and 20 characters')
|
|
|
|
|
.max(20, 'Nickname must be less than 20 characters'),
|
|
|
|
|
sex: z.enum(Gender, { message: 'Please select gender' }),
|
|
|
|
|
year: z.string().min(1, 'Please select year'),
|
|
|
|
|
month: z.string().min(1, 'Please select month'),
|
|
|
|
|
day: z.string().min(1, 'Please select day'),
|
|
|
|
|
profile: z.string().trim().optional(),
|
|
|
|
|
})
|
|
|
|
|
.refine(
|
|
|
|
|
(data) => {
|
2025-12-17 10:13:47 +00:00
|
|
|
const age = calculateAge(data.year, data.month, data.day);
|
|
|
|
|
return age >= 18;
|
2025-12-09 09:13:46 +00:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
message: 'Character age must be at least 18 years old',
|
|
|
|
|
path: ['year'],
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
.refine(
|
|
|
|
|
(data) => {
|
|
|
|
|
if (data.profile) {
|
|
|
|
|
if (data.profile.trim().length > 300) {
|
2025-12-17 10:13:47 +00:00
|
|
|
return false;
|
2025-12-09 09:13:46 +00:00
|
|
|
}
|
2025-12-17 10:13:47 +00:00
|
|
|
return data.profile.trim().length >= 10;
|
2025-12-09 09:13:46 +00:00
|
|
|
}
|
2025-12-17 10:13:47 +00:00
|
|
|
return true;
|
2025-12-09 09:13:46 +00:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
message: 'At least 10 characters',
|
|
|
|
|
path: ['profile'],
|
|
|
|
|
}
|
2025-12-17 10:13:47 +00:00
|
|
|
);
|
2025-12-09 09:13:46 +00:00
|
|
|
|
|
|
|
|
export default function Personal() {
|
|
|
|
|
// 静态数据,模拟从接口获取的数据
|
|
|
|
|
const chatSettingData = {
|
|
|
|
|
nickname: 'John',
|
|
|
|
|
sex: Gender.MALE,
|
|
|
|
|
birthday: dayjs('1995-06-15').valueOf(),
|
|
|
|
|
whoAmI: 'A creative and passionate developer',
|
2025-12-17 10:13:47 +00:00
|
|
|
};
|
2025-12-09 09:13:46 +00:00
|
|
|
|
2025-12-17 10:13:47 +00:00
|
|
|
const birthday = chatSettingData?.birthday ? dayjs(chatSettingData.birthday) : undefined;
|
2025-12-09 09:13:46 +00:00
|
|
|
|
|
|
|
|
const form = useForm<z.infer<typeof characterFormSchema>>({
|
|
|
|
|
resolver: zodResolver(characterFormSchema),
|
|
|
|
|
defaultValues: {
|
|
|
|
|
nickname: chatSettingData?.nickname || '',
|
|
|
|
|
sex: chatSettingData?.sex,
|
|
|
|
|
year: birthday?.year().toString() || undefined,
|
|
|
|
|
month:
|
|
|
|
|
birthday?.month() !== undefined
|
|
|
|
|
? (birthday.month() + 1).toString().padStart(2, '0')
|
|
|
|
|
: undefined,
|
|
|
|
|
day: birthday?.date().toString().padStart(2, '0') || undefined,
|
|
|
|
|
profile: chatSettingData?.whoAmI || '',
|
|
|
|
|
},
|
2025-12-17 10:13:47 +00:00
|
|
|
});
|
2025-12-09 09:13:46 +00:00
|
|
|
|
2025-12-17 10:13:47 +00:00
|
|
|
const selectedYear = form.watch('year');
|
|
|
|
|
const selectedMonth = form.watch('month');
|
|
|
|
|
const days = selectedYear && selectedMonth ? getDaysInMonth(selectedYear, selectedMonth) : [];
|
2025-12-09 09:13:46 +00:00
|
|
|
|
|
|
|
|
const genderTexts = [
|
|
|
|
|
{
|
|
|
|
|
value: Gender.MALE,
|
|
|
|
|
label: 'Male',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
value: Gender.FEMALE,
|
|
|
|
|
label: 'Female',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
value: Gender.OTHER,
|
|
|
|
|
label: 'Other',
|
|
|
|
|
},
|
2025-12-17 10:13:47 +00:00
|
|
|
];
|
2025-12-09 09:13:46 +00:00
|
|
|
|
2025-12-17 10:13:47 +00:00
|
|
|
const gender = form.watch('sex');
|
|
|
|
|
const genderText = genderTexts.find((text) => text.value === gender)?.label;
|
2025-12-09 09:13:46 +00:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<div className="flex flex-col gap-6">
|
|
|
|
|
<Form {...form}>
|
|
|
|
|
<form className="space-y-8">
|
|
|
|
|
<FormField
|
|
|
|
|
control={form.control}
|
|
|
|
|
name="nickname"
|
|
|
|
|
render={({ field }) => (
|
|
|
|
|
<FormItem>
|
|
|
|
|
<FormLabel>Nickname</FormLabel>
|
|
|
|
|
<FormControl>
|
|
|
|
|
<Input
|
|
|
|
|
placeholder="Enter nickname"
|
|
|
|
|
maxLength={20}
|
|
|
|
|
error={!!form.formState.errors.nickname}
|
|
|
|
|
{...field}
|
|
|
|
|
/>
|
|
|
|
|
</FormControl>
|
|
|
|
|
<FormMessage />
|
|
|
|
|
</FormItem>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<div className="">
|
|
|
|
|
<div className="txt-label-m text-txt-secondary-normal">Gender</div>
|
|
|
|
|
|
|
|
|
|
<div className="mt-3">
|
|
|
|
|
<div className="bg-surface-element-normal rounded-m txt-body-l text-txt-secondary-disabled flex h-12 items-center px-4 py-3">
|
|
|
|
|
{genderText}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="txt-body-s text-txt-secondary-disabled mt-1">
|
|
|
|
|
Please note: gender cannot be changed after setting
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<Label className="txt-label-m mb-3 block">Birthday</Label>
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<FormField
|
|
|
|
|
control={form.control}
|
|
|
|
|
name="year"
|
|
|
|
|
render={({ field }) => (
|
|
|
|
|
<FormItem className="flex-1">
|
|
|
|
|
<FormControl>
|
|
|
|
|
<Select onValueChange={field.onChange} value={field.value}>
|
|
|
|
|
<SelectTrigger className="w-full">
|
|
|
|
|
<SelectValue placeholder="Year" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
{years.map((year) => (
|
|
|
|
|
<SelectItem key={year} value={year}>
|
|
|
|
|
{year}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</FormControl>
|
|
|
|
|
</FormItem>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
<FormField
|
|
|
|
|
control={form.control}
|
|
|
|
|
name="month"
|
|
|
|
|
render={({ field }) => (
|
|
|
|
|
<FormItem className="flex-1">
|
|
|
|
|
<FormControl>
|
|
|
|
|
<Select onValueChange={field.onChange} value={field.value}>
|
|
|
|
|
<SelectTrigger className="w-full">
|
|
|
|
|
<SelectValue placeholder="Month" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
{months.map((m, index) => (
|
|
|
|
|
<SelectItem key={m} value={m}>
|
|
|
|
|
{monthTexts[index]}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</FormControl>
|
|
|
|
|
</FormItem>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
<FormField
|
|
|
|
|
control={form.control}
|
|
|
|
|
name="day"
|
|
|
|
|
render={({ field }) => (
|
|
|
|
|
<FormItem className="flex-1">
|
|
|
|
|
<FormControl>
|
|
|
|
|
<Select onValueChange={field.onChange} value={field.value}>
|
|
|
|
|
<SelectTrigger className="w-full">
|
|
|
|
|
<SelectValue placeholder="Day" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
{days.map((d) => (
|
|
|
|
|
<SelectItem key={d} value={d}>
|
|
|
|
|
{d}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</FormControl>
|
|
|
|
|
</FormItem>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<FormMessage>
|
|
|
|
|
{form.formState.errors.year?.message ||
|
|
|
|
|
form.formState.errors.month?.message ||
|
|
|
|
|
form.formState.errors.day?.message}
|
|
|
|
|
</FormMessage>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<FormField
|
|
|
|
|
control={form.control}
|
|
|
|
|
name="profile"
|
|
|
|
|
render={({ field }) => (
|
|
|
|
|
<FormItem>
|
|
|
|
|
<FormLabel>
|
|
|
|
|
My Persona
|
|
|
|
|
<span className="txt-label-m text-txt-secondary-normal">(Optional)</span>
|
|
|
|
|
</FormLabel>
|
|
|
|
|
<FormControl>
|
|
|
|
|
<Textarea
|
|
|
|
|
{...field}
|
|
|
|
|
maxLength={300}
|
|
|
|
|
error={!!form.formState.errors.profile}
|
|
|
|
|
placeholder="Set your own persona in CrushLevel"
|
|
|
|
|
/>
|
|
|
|
|
</FormControl>
|
|
|
|
|
<FormMessage />
|
|
|
|
|
</FormItem>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
</form>
|
|
|
|
|
</Form>
|
|
|
|
|
|
2025-12-17 10:13:47 +00:00
|
|
|
{/* <div className="flex gap-3">
|
2025-12-09 09:13:46 +00:00
|
|
|
<Button variant="tertiary" size="large" className="flex-1" onClick={handleGoBack}>
|
|
|
|
|
Cancel
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
type="submit"
|
|
|
|
|
size="large"
|
|
|
|
|
className="flex-1"
|
|
|
|
|
onClick={form.handleSubmit(onSubmit)}
|
|
|
|
|
loading={loading}
|
|
|
|
|
>
|
|
|
|
|
Save
|
|
|
|
|
</Button>
|
2025-12-17 10:13:47 +00:00
|
|
|
</div> */}
|
2025-12-09 09:13:46 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 确认放弃修改的对话框 */}
|
2025-12-18 10:14:12 +00:00
|
|
|
{/* <AlertDialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
|
2025-12-09 09:13:46 +00:00
|
|
|
<AlertDialogContent>
|
|
|
|
|
<AlertDialogHeader>
|
|
|
|
|
<AlertDialogTitle>Unsaved Edits</AlertDialogTitle>
|
|
|
|
|
<AlertDialogDescription>
|
|
|
|
|
The edited content will not be saved after exiting. Please confirm whether to continue
|
|
|
|
|
exiting?
|
|
|
|
|
</AlertDialogDescription>
|
|
|
|
|
</AlertDialogHeader>
|
|
|
|
|
<AlertDialogFooter>
|
|
|
|
|
<AlertDialogCancel onClick={() => setShowConfirmDialog(false)}>
|
|
|
|
|
Cancel
|
|
|
|
|
</AlertDialogCancel>
|
|
|
|
|
<AlertDialogAction variant="destructive" onClick={handleConfirmDiscard}>
|
|
|
|
|
Exit
|
|
|
|
|
</AlertDialogAction>
|
|
|
|
|
</AlertDialogFooter>
|
|
|
|
|
</AlertDialogContent>
|
2025-12-18 10:14:12 +00:00
|
|
|
</AlertDialog> */}
|
2025-12-09 09:13:46 +00:00
|
|
|
</>
|
2025-12-17 10:13:47 +00:00
|
|
|
);
|
2025-12-09 09:13:46 +00:00
|
|
|
}
|