This commit is contained in:
freedakgmail
2025-11-22 19:52:55 +08:00
commit 43c8389705
28950 changed files with 3640981 additions and 0 deletions
+215
View File
@@ -0,0 +1,215 @@
"use client"
import Link from "next/link"
import { useParams, useRouter } from "next/navigation"
import { useFamily } from "@/context/family-context"
import { SiteHeader } from "@/components/site-header"
import { MemberForm } from "@/components/members/member-form"
import { Button } from "@/components/ui/button"
import { ArrowLeft, Edit, Trash, Share2, Printer } from "lucide-react"
import { useState } from "react"
import type { FamilyMember } from "@/types/family"
export default function MemberProfilePage() {
const { id } = useParams()
const { getMember, updateMember, deleteMember } = useFamily()
const router = useRouter()
const [isEditing, setIsEditing] = useState(false)
const member = getMember(id as string)
if (!member) {
return (
<div className="min-h-screen flex flex-col bg-background">
<SiteHeader />
<div className="flex-1 flex flex-col items-center justify-center gap-4">
<p className="text-muted-foreground">Member not found</p>
<Button onClick={() => router.push("/members")}>Back to List</Button>
</div>
</div>
)
}
const handleSave = (updatedMember: FamilyMember) => {
updateMember(updatedMember.id, updatedMember)
setIsEditing(false)
}
const handleDelete = () => {
if (confirm("确定要删除这位成员吗?这将同时删除其关联关系。")) {
deleteMember(member.id)
router.push("/members")
}
}
if (isEditing) {
return (
<div className="min-h-screen flex flex-col bg-background">
<SiteHeader />
<div className="container py-8">
<div className="mb-6 flex items-center gap-2">
<Button variant="ghost" onClick={() => setIsEditing(false)}>
<ArrowLeft className="mr-2 h-4 w-4" />
</Button>
<h1 className="text-2xl font-bold font-serif">: {member.fullName}</h1>
</div>
<MemberForm initialData={member} onSubmit={handleSave} onCancel={() => setIsEditing(false)} />
</div>
</div>
)
}
return (
<div className="min-h-screen flex flex-col bg-background">
<SiteHeader />
<main className="container py-8 px-4 md:px-6">
{/* Header Section */}
<div className="flex flex-col md:flex-row justify-between items-start gap-6 mb-8 border-b border-border pb-8">
<div className="flex items-start gap-6">
<div className="h-32 w-32 rounded-lg bg-muted flex items-center justify-center border-2 border-primary/20 overflow-hidden">
{member.avatarUrl ? (
<img
src={member.avatarUrl || "/placeholder.svg"}
alt={member.fullName}
className="h-full w-full object-cover"
/>
) : (
<span className="text-4xl font-serif font-bold text-muted-foreground/50">{member.surname}</span>
)}
</div>
<div>
<div className="flex items-center gap-3 mb-2">
<h1 className="text-4xl font-bold font-serif text-foreground">{member.fullName}</h1>
<span className="px-2 py-1 bg-primary/10 text-primary text-sm font-medium rounded-full border border-primary/20">
{member.generation}
</span>
</div>
<div className="space-y-1 text-muted-foreground">
{member.courtesyName && (
<p>
<span className="text-foreground">{member.courtesyName}</span>
</p>
)}
{member.artName && (
<p>
<span className="text-foreground">{member.artName}</span>
</p>
)}
{member.ancestralHome && <p>{member.ancestralHome}</p>}
</div>
</div>
</div>
<div className="flex gap-2">
<Button variant="outline" size="icon" onClick={() => window.print()}>
<Printer className="h-4 w-4" />
</Button>
<Button variant="outline" size="icon">
<Share2 className="h-4 w-4" />
</Button>
<Button variant="outline" onClick={() => setIsEditing(true)} className="gap-2">
<Edit className="h-4 w-4" />
</Button>
<Button variant="destructive" size="icon" onClick={handleDelete}>
<Trash className="h-4 w-4" />
</Button>
</div>
</div>
{/* Content Grid */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Main Biography */}
<div className="lg:col-span-2 space-y-8">
<section>
<h2 className="text-2xl font-serif font-bold mb-4 flex items-center gap-2">
<span className="w-1 h-6 bg-primary rounded-full"></span>
</h2>
<div className="prose prose-stone dark:prose-invert max-w-none bg-card p-6 rounded-lg border border-border shadow-sm">
{member.bio ? (
<p className="whitespace-pre-line leading-relaxed">{member.bio}</p>
) : (
<p className="text-muted-foreground italic">...</p>
)}
</div>
</section>
<section>
<h2 className="text-2xl font-serif font-bold mb-4 flex items-center gap-2">
<span className="w-1 h-6 bg-secondary rounded-full"></span>
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-card p-4 rounded-lg border border-border">
<h3 className="font-medium text-muted-foreground mb-2 text-sm uppercase tracking-wider">
(Father)
</h3>
{member.fatherId ? (
<Link
href={`/members/${member.fatherId}`}
className="text-lg font-serif font-bold hover:underline text-primary"
>
</Link>
) : (
<span className="text-muted-foreground">-</span>
)}
</div>
<div className="bg-card p-4 rounded-lg border border-border">
<h3 className="font-medium text-muted-foreground mb-2 text-sm uppercase tracking-wider">
(Children)
</h3>
<div className="flex flex-wrap gap-2">
{member.childrenIds?.length > 0 ? (
member.childrenIds.map((childId) => (
<Link
href={`/members/${childId}`}
key={childId}
className="px-3 py-1 bg-muted rounded hover:bg-muted/80 text-sm font-medium"
>
{childId} {/* In real app, fetch name */}
</Link>
))
) : (
<span className="text-muted-foreground"></span>
)}
</div>
</div>
</div>
</section>
</div>
{/* Sidebar Info */}
<div className="space-y-6">
<div className="bg-card rounded-lg border border-border p-6 shadow-sm">
<h3 className="font-serif font-bold text-lg mb-4 border-b border-border pb-2"></h3>
<dl className="space-y-4 text-sm">
<div>
<dt className="text-muted-foreground"></dt>
<dd className="font-medium">{member.birthDate || "-"}</dd>
</div>
<div>
<dt className="text-muted-foreground"></dt>
<dd className="font-medium">{member.deathDate || "-"}</dd>
</div>
<div>
<dt className="text-muted-foreground"></dt>
<dd className="font-medium">{member.generationName || "-"}</dd>
</div>
<div>
<dt className="text-muted-foreground"></dt>
<dd className="font-medium">{member.posthumousName || "-"}</dd>
</div>
<div>
<dt className="text-muted-foreground"></dt>
<dd className="font-medium">{member.burialPlace || "-"}</dd>
</div>
</dl>
</div>
</div>
</div>
</main>
</div>
)
}
+3
View File
@@ -0,0 +1,3 @@
export default function Loading() {
return null
}
+40
View File
@@ -0,0 +1,40 @@
"use client"
import { useRouter } from "next/navigation"
import { useFamily } from "@/context/family-context"
import { SiteHeader } from "@/components/site-header"
import { MemberForm } from "@/components/members/member-form"
import { Button } from "@/components/ui/button"
import { ArrowLeft } from "lucide-react"
import type { FamilyMember } from "@/types/family"
export default function NewMemberPage() {
const { addMember, treeData } = useFamily()
const router = useRouter()
const existingMembers = Object.values(treeData.members)
const handleSave = (newMember: FamilyMember) => {
addMember({
...newMember,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
})
router.push(`/members/${newMember.id}`)
}
return (
<div className="min-h-screen flex flex-col bg-background">
<SiteHeader />
<div className="container py-8">
<div className="mb-6 flex items-center gap-2">
<Button variant="ghost" onClick={() => router.back()}>
<ArrowLeft className="mr-2 h-4 w-4" />
</Button>
<h1 className="text-2xl font-bold font-serif"></h1>
</div>
<MemberForm existingMembers={existingMembers} onSubmit={handleSave} onCancel={() => router.back()} />
</div>
</div>
)
}
+106
View File
@@ -0,0 +1,106 @@
"use client"
import { useState } from "react"
import { SiteHeader } from "@/components/site-header"
import { useFamily } from "@/context/family-context"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { Search, Plus, Filter } from "lucide-react"
import Link from "next/link"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
export default function MembersPage() {
const { treeData, searchResults, searchMembers } = useFamily()
const [query, setQuery] = useState("")
const displayMembers = query
? searchResults
: Object.values(treeData.members).sort(
(a, b) => a.generation - b.generation || (a.birthDate || "").localeCompare(b.birthDate || ""),
)
return (
<div className="min-h-screen bg-background flex flex-col font-sans">
<SiteHeader />
<main className="container py-8 px-4 md:px-6 flex-1">
<div className="flex flex-col md:flex-row justify-between items-center mb-8 gap-4">
<h1 className="text-3xl font-serif font-bold"></h1>
<div className="flex items-center gap-2 w-full md:w-auto">
<div className="relative flex-1 md:w-80">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="search"
placeholder="搜索姓名、字号..."
className="pl-9"
value={query}
onChange={(e) => {
setQuery(e.target.value)
searchMembers(e.target.value)
}}
/>
</div>
<Button variant="outline" size="icon">
<Filter className="h-4 w-4" />
</Button>
<Link href="/members/new">
<Button className="gap-2 bg-primary text-primary-foreground">
<Plus className="h-4 w-4" />
</Button>
</Link>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{displayMembers.map((member) => (
<Link href={`/members/${member.id}`} key={member.id}>
<Card className="hover:border-primary transition-colors cursor-pointer h-full">
<CardContent className="p-6 flex items-start gap-4">
<Avatar className="h-16 w-16 border-2 border-border">
<AvatarImage src={member.avatarUrl || "/placeholder.svg"} />
<AvatarFallback className="text-lg font-serif bg-muted">{member.surname}</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<div className="flex items-baseline justify-between">
<h3 className="text-xl font-bold font-serif truncate">
{member.fullName}
{member.courtesyName && (
<span className="text-sm font-normal text-muted-foreground ml-2">
{member.courtesyName}
</span>
)}
</h3>
<span className="text-xs font-mono bg-muted px-1.5 py-0.5 rounded text-muted-foreground whitespace-nowrap">
{member.generation}
</span>
</div>
<p className="text-sm text-muted-foreground mt-1 truncate">
{member.generationName ? `字辈: ${member.generationName}` : "未录入字辈"}
</p>
<p className="text-sm text-muted-foreground truncate">{member.ancestralHome || "籍贯未知"}</p>
<div className="mt-3 flex flex-wrap gap-1">
{member.lifeStatus === "deceased" && (
<span className="text-[10px] bg-neutral-100 text-neutral-600 px-1.5 py-0.5 rounded border border-neutral-200">
</span>
)}
{member.tags?.map((tag) => (
<span
key={tag}
className="text-[10px] bg-primary/10 text-primary px-1.5 py-0.5 rounded border border-primary/20"
>
{tag}
</span>
))}
</div>
</div>
</CardContent>
</Card>
</Link>
))}
</div>
</main>
</div>
)
}