Files
chinese-family-tree-2/components/members/member-form.tsx
T
freedakgmail b83382b604 0.0.0.3
2025-11-22 20:18:26 +08:00

406 lines
15 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 type React from "react"
import { useState } from "react"
import type { FamilyMember } from "@/types/family"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { CalendarIcon, User, Scroll, Users } from "lucide-react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { ImageUpload } from "@/components/ui/image-upload"
interface MemberFormProps {
initialData?: Partial<FamilyMember>
existingMembers?: FamilyMember[]
onSubmit: (data: FamilyMember) => void
onCancel: () => void
}
export function MemberForm({ initialData, existingMembers = [], onSubmit, onCancel }: MemberFormProps) {
// Initialize state with default values or initialData
const [formData, setFormData] = useState<Partial<FamilyMember>>({
id: initialData?.id || crypto.randomUUID(),
surname: initialData?.surname || "李",
givenName: initialData?.givenName || "",
fullName: initialData?.fullName || "",
gender: initialData?.gender || "male",
generation: initialData?.generation || 1,
spouseIds: initialData?.spouseIds || [],
childrenIds: initialData?.childrenIds || [],
fatherId: initialData?.fatherId,
motherId: initialData?.motherId,
...initialData,
})
const [errors, setErrors] = useState<string[]>([])
const handleChange = (field: keyof FamilyMember, value: any) => {
setFormData((prev) => {
const newData = { ...prev, [field]: value }
// Auto-update full name if surname or given name changes
if (field === "surname" || field === "givenName") {
newData.fullName = (newData.surname || "") + (newData.givenName || "")
}
return newData
})
// Clear errors when user makes changes
if (errors.length > 0) setErrors([])
}
const handleSpouseChange = (spouseId: string) => {
// If selecting "none" (empty string), clear spouses
if (!spouseId) {
setFormData((prev) => ({ ...prev, spouseIds: [] }))
return
}
// Otherwise set as single spouse (logic can be expanded for multiple later)
setFormData((prev) => ({ ...prev, spouseIds: [spouseId] }))
}
const validate = (): boolean => {
const newErrors: string[] = []
// 1. Date Validation
if (formData.birthDate && formData.deathDate) {
if (new Date(formData.birthDate) > new Date(formData.deathDate)) {
newErrors.push("出生日期不能晚于逝世日期")
}
}
// 2. Parent Age Validation
const birthYear = formData.birthDate ? new Date(formData.birthDate).getFullYear() : null
if (birthYear) {
if (formData.fatherId) {
const father = existingMembers.find((m) => m.id === formData.fatherId)
if (father?.birthDate) {
const fatherBirthYear = new Date(father.birthDate).getFullYear()
if (birthYear - fatherBirthYear < 15) {
newErrors.push(`与父亲的年龄差过小 (${birthYear - fatherBirthYear}岁),请确认`)
}
if (birthYear < fatherBirthYear) {
newErrors.push("出生日期不能早于父亲")
}
}
}
if (formData.motherId) {
const mother = existingMembers.find((m) => m.id === formData.motherId)
if (mother?.birthDate) {
const motherBirthYear = new Date(mother.birthDate).getFullYear()
if (birthYear - motherBirthYear < 15) {
newErrors.push(`与母亲的年龄差过小 (${birthYear - motherBirthYear}岁),请确认`)
}
if (birthYear < motherBirthYear) {
newErrors.push("出生日期不能早于母亲")
}
}
}
}
// 3. Generation Validation
if (formData.fatherId) {
const father = existingMembers.find((m) => m.id === formData.fatherId)
if (father && formData.generation !== father.generation + 1) {
newErrors.push(`世系错误:父亲是第 ${father.generation} 世,子女应为第 ${father.generation + 1}`)
}
}
setErrors(newErrors)
return newErrors.length === 0
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (validate()) {
onSubmit(formData as FamilyMember)
}
}
const potentialRelatives = existingMembers.filter((m) => m.id !== formData.id)
return (
<form onSubmit={handleSubmit} className="space-y-8 max-w-4xl mx-auto p-6">
{errors.length > 0 && (
<div className="bg-destructive/15 text-destructive px-4 py-3 rounded-md border border-destructive/20">
<p className="font-bold mb-1"></p>
<ul className="list-disc list-inside text-sm">
{errors.map((err, index) => (
<li key={index}>{err}</li>
))}
</ul>
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Basic Identity */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg font-serif">
<User className="h-5 w-5" />
(Identity)
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex justify-center mb-4">
<ImageUpload
value={formData.avatarImageId}
onChange={(imageId) => handleChange("avatarImageId", imageId)}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="surname"> (Surname)</Label>
<Input
id="surname"
value={formData.surname}
onChange={(e) => handleChange("surname", e.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="givenName"> (Given Name)</Label>
<Input
id="givenName"
value={formData.givenName}
onChange={(e) => handleChange("givenName", e.target.value)}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="gender"> (Gender)</Label>
<Select value={formData.gender} onValueChange={(val) => handleChange("gender", val)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="male"> (Male)</SelectItem>
<SelectItem value="female"> (Female)</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="generation"> (Generation)</Label>
<Input
id="generation"
type="number"
value={formData.generation}
onChange={(e) => handleChange("generation", Number.parseInt(e.target.value))}
/>
</div>
</div>
</CardContent>
</Card>
{/* Traditional Names */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg font-serif">
<Scroll className="h-5 w-5" />
(Traditional Names)
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="courtesyName"> (Courtesy Name)</Label>
<Input
id="courtesyName"
value={formData.courtesyName || ""}
onChange={(e) => handleChange("courtesyName", e.target.value)}
placeholder="e.g. 伯虎"
/>
</div>
<div className="space-y-2">
<Label htmlFor="artName"> (Art Name)</Label>
<Input
id="artName"
value={formData.artName || ""}
onChange={(e) => handleChange("artName", e.target.value)}
placeholder="e.g. 六如居士"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="generationName"> (Gen. Name)</Label>
<Input
id="generationName"
value={formData.generationName || ""}
onChange={(e) => handleChange("generationName", e.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="posthumousName"> (Posthumous)</Label>
<Input
id="posthumousName"
value={formData.posthumousName || ""}
onChange={(e) => handleChange("posthumousName", e.target.value)}
/>
</div>
</div>
</CardContent>
</Card>
{/* Dates & Places */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg font-serif">
<CalendarIcon className="h-5 w-5" />
(Life & Places)
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="birthDate"> (Birth)</Label>
<Input
id="birthDate"
type="date"
value={formData.birthDate || ""}
onChange={(e) => handleChange("birthDate", e.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="deathDate"> (Death)</Label>
<Input
id="deathDate"
type="date"
value={formData.deathDate || ""}
onChange={(e) => handleChange("deathDate", e.target.value)}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="ancestralHome"> (Ancestral Home)</Label>
<Input
id="ancestralHome"
value={formData.ancestralHome || ""}
onChange={(e) => handleChange("ancestralHome", e.target.value)}
placeholder="e.g. 福建省泉州市"
/>
</div>
<div className="space-y-2">
<Label htmlFor="burialPlace"> (Burial Place)</Label>
<Input
id="burialPlace"
value={formData.burialPlace || ""}
onChange={(e) => handleChange("burialPlace", e.target.value)}
/>
</div>
</CardContent>
</Card>
{/* Biography */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg font-serif">
<Scroll className="h-5 w-5" />
(Biography)
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<Textarea
className="min-h-[200px]"
placeholder="记录生平事迹..."
value={formData.bio || ""}
onChange={(e) => handleChange("bio", e.target.value)}
/>
</CardContent>
</Card>
{/* Relationships */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg font-serif">
<Users className="h-5 w-5" />
(Relationships)
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 gap-4">
<div className="space-y-2">
<Label htmlFor="fatherId"> (Father)</Label>
<Select
value={formData.fatherId || "none"}
onValueChange={(val) => handleChange("fatherId", val === "none" ? undefined : val)}
>
<SelectTrigger>
<SelectValue placeholder="选择父亲 Select Father" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none"> (None)</SelectItem>
{potentialRelatives
.filter((m) => m.gender === "male")
.map((m) => (
<SelectItem key={m.id} value={m.id}>
{m.fullName} ({m.generation})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="motherId"> (Mother)</Label>
<Select
value={formData.motherId || "none"}
onValueChange={(val) => handleChange("motherId", val === "none" ? undefined : val)}
>
<SelectTrigger>
<SelectValue placeholder="选择母亲 Select Mother" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none"> (None)</SelectItem>
{potentialRelatives
.filter((m) => m.gender === "female")
.map((m) => (
<SelectItem key={m.id} value={m.id}>
{m.fullName} ({m.generation})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="spouseId"> (Spouse)</Label>
<Select value={formData.spouseIds?.[0] || "none"} onValueChange={handleSpouseChange}>
<SelectTrigger>
<SelectValue placeholder="选择配偶 Select Spouse" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none"> (None)</SelectItem>
{potentialRelatives
.filter((m) => m.gender !== formData.gender) // Suggest opposite gender
.map((m) => (
<SelectItem key={m.id} value={m.id}>
{m.fullName} ({m.generation})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</CardContent>
</Card>
</div>
<div className="flex justify-end gap-4 sticky bottom-4 bg-background/90 p-4 border-t border-border backdrop-blur rounded-lg">
<Button type="button" variant="outline" onClick={onCancel}>
(Cancel)
</Button>
<Button type="submit" className="bg-primary text-primary-foreground hover:bg-primary/90">
(Save Member)
</Button>
</div>
</form>
)
}