Files
chinese-family-tree-2/components/members/member-form.tsx
T
freedakgmail c033e46782 0.0.0.4
2025-11-22 23:49:24 +08:00

530 lines
20 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, Images, BookOpen } from "lucide-react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { ImageUpload } from "@/components/ui/image-upload"
import { PhotoGallery } from "./photo-gallery"
import { StoryManager } from "./story-manager"
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}`)
}
}
// 4. Circular Reference Detection (循环引用检测)
if (formData.fatherId || formData.motherId) {
const visited = new Set<string>()
const checkCircular = (memberId: string): boolean => {
if (memberId === formData.id) return true
if (visited.has(memberId)) return false
visited.add(memberId)
const member = existingMembers.find((m) => m.id === memberId)
if (!member) return false
if (member.fatherId && checkCircular(member.fatherId)) return true
if (member.motherId && checkCircular(member.motherId)) return true
return false
}
if (formData.fatherId && checkCircular(formData.fatherId)) {
newErrors.push("检测到循环引用:不能将自己的后代设置为父亲")
}
if (formData.motherId && checkCircular(formData.motherId)) {
newErrors.push("检测到循环引用:不能将自己的后代设置为母亲")
}
}
// 5. Self-Reference Detection (自我引用检测)
if (formData.fatherId === formData.id) {
newErrors.push("不能将自己设置为自己的父亲")
}
if (formData.motherId === formData.id) {
newErrors.push("不能将自己设置为自己的母亲")
}
if (formData.spouseIds?.includes(formData.id || "")) {
newErrors.push("不能将自己设置为自己的配偶")
}
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>
{/* Contact Information */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg font-serif">
<User className="h-5 w-5" />
(Contact Information)
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label htmlFor="phone"> (Mobile Phone)</Label>
<Input
id="phone"
type="tel"
value={formData.phone || ""}
onChange={(e) => handleChange("phone", e.target.value)}
placeholder="e.g. 13800138000"
/>
</div>
<div className="space-y-2">
<Label htmlFor="telephone"> (Telephone)</Label>
<Input
id="telephone"
type="tel"
value={formData.telephone || ""}
onChange={(e) => handleChange("telephone", e.target.value)}
placeholder="e.g. 0592-1234567"
/>
</div>
<div className="space-y-2">
<Label htmlFor="email"> (Email)</Label>
<Input
id="email"
type="email"
value={formData.email || ""}
onChange={(e) => handleChange("email", e.target.value)}
placeholder="e.g. example@email.com"
/>
</div>
<div className="space-y-2 md:col-span-2">
<Label htmlFor="address"> (Address)</Label>
<Input
id="address"
value={formData.address || ""}
onChange={(e) => handleChange("address", e.target.value)}
placeholder="e.g. 福建省厦门市思明区XX路XX号"
/>
</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>
{/* Photo Gallery */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg font-serif">
<Images className="h-5 w-5" />
(Photo Gallery)
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<PhotoGallery
photoIds={formData.photoIds || []}
onChange={(photoIds) => handleChange("photoIds", photoIds)}
/>
</CardContent>
</Card>
{/* Family Stories */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg font-serif">
<BookOpen className="h-5 w-5" />
(Family Stories)
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<StoryManager
stories={formData.stories || []}
onChange={(stories) => handleChange("stories", stories)}
/>
</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>
)
}