diff --git a/src/app/admin/_components/FormField.tsx b/src/app/admin/_components/FormField.tsx index 57367c8..a98242f 100644 --- a/src/app/admin/_components/FormField.tsx +++ b/src/app/admin/_components/FormField.tsx @@ -1,4 +1,5 @@ import { useRef, useEffect, useState } from "react"; +import { Plus, X } from "lucide-react"; interface InputFieldProps { label: string; @@ -269,3 +270,72 @@ export function ToggleField({ label, checked, onChange }: ToggleFieldProps) { ); } + +interface ListFieldProps { + label: string; + items: string[]; + onChange: (items: string[]) => void; + placeholder?: string; +} + +export function ListField({ label, items, onChange, placeholder }: ListFieldProps) { + const [draft, setDraft] = useState(""); + + function add() { + const val = draft.trim(); + if (!val) return; + onChange([...items, val]); + setDraft(""); + } + + function remove(index: number) { + onChange(items.filter((_, i) => i !== index)); + } + + function update(index: number, value: string) { + onChange(items.map((item, i) => (i === index ? value : item))); + } + + return ( +
+ +
+ {items.map((item, i) => ( +
+ update(i, e.target.value)} + className="flex-1 rounded-lg border border-white/10 bg-neutral-800 px-4 py-2 text-sm text-white outline-none focus:border-gold transition-colors" + /> + +
+ ))} +
+ setDraft(e.target.value)} + onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); add(); } }} + placeholder={placeholder || "Добавить..."} + className="flex-1 rounded-lg border border-dashed border-white/10 bg-neutral-800/50 px-4 py-2 text-sm text-white placeholder-neutral-600 outline-none focus:border-gold/50 transition-colors" + /> + +
+
+
+ ); +} diff --git a/src/app/admin/team/[id]/page.tsx b/src/app/admin/team/[id]/page.tsx index 5ae3a95..11f4ed2 100644 --- a/src/app/admin/team/[id]/page.tsx +++ b/src/app/admin/team/[id]/page.tsx @@ -4,7 +4,7 @@ import { useState, useEffect } from "react"; import { useRouter, useParams } from "next/navigation"; import Image from "next/image"; import { Save, Loader2, Check, ArrowLeft, Upload } from "lucide-react"; -import { InputField, TextareaField } from "../../_components/FormField"; +import { InputField, TextareaField, ListField } from "../../_components/FormField"; interface MemberForm { name: string; @@ -12,6 +12,9 @@ interface MemberForm { image: string; instagram: string; description: string; + experience: string[]; + victories: string[]; + education: string[]; } export default function TeamMemberEditorPage() { @@ -25,6 +28,9 @@ export default function TeamMemberEditorPage() { image: "/images/team/placeholder.webp", instagram: "", description: "", + experience: [], + victories: [], + education: [], }); const [loading, setLoading] = useState(!isNew); const [saving, setSaving] = useState(false); @@ -42,6 +48,9 @@ export default function TeamMemberEditorPage() { image: member.image, instagram: member.instagram || "", description: member.description || "", + experience: member.experience || [], + victories: member.victories || [], + education: member.education || [], }) ) .finally(() => setLoading(false)); @@ -192,6 +201,30 @@ export default function TeamMemberEditorPage() { onChange={(v) => setData({ ...data, description: v })} rows={6} /> + +
+

Биография

+
+ setData({ ...data, experience: items })} + placeholder="Например: 10 лет в танцах" + /> + setData({ ...data, victories: items })} + placeholder="Например: 1 место — чемпионат..." + /> + setData({ ...data, education: items })} + placeholder="Например: Сертификат IPSF" + /> +
+
diff --git a/src/app/api/admin/team/route.ts b/src/app/api/admin/team/route.ts index 77cf367..c4713d5 100644 --- a/src/app/api/admin/team/route.ts +++ b/src/app/api/admin/team/route.ts @@ -14,6 +14,9 @@ export async function POST(request: NextRequest) { image: string; instagram?: string; description?: string; + experience?: string[]; + victories?: string[]; + education?: string[]; }; if (!data.name || !data.role || !data.image) { diff --git a/src/components/sections/Team.tsx b/src/components/sections/Team.tsx index 9c3e7b7..9789516 100644 --- a/src/components/sections/Team.tsx +++ b/src/components/sections/Team.tsx @@ -5,6 +5,7 @@ import { SectionHeading } from "@/components/ui/SectionHeading"; import { Reveal } from "@/components/ui/Reveal"; import { TeamCarousel } from "@/components/sections/team/TeamCarousel"; import { TeamMemberInfo } from "@/components/sections/team/TeamMemberInfo"; +import { TeamProfile } from "@/components/sections/team/TeamProfile"; import type { SiteContent } from "@/types/content"; interface TeamProps { @@ -13,6 +14,7 @@ interface TeamProps { export function Team({ data: team }: TeamProps) { const [activeIndex, setActiveIndex] = useState(0); + const [showProfile, setShowProfile] = useState(false); return (
- + {!showProfile ? ( + <> + - + setShowProfile(true)} + /> + + ) : ( + setShowProfile(false)} + /> + )}
diff --git a/src/components/sections/team/TeamMemberInfo.tsx b/src/components/sections/team/TeamMemberInfo.tsx index d7e7ca3..49d0da7 100644 --- a/src/components/sections/team/TeamMemberInfo.tsx +++ b/src/components/sections/team/TeamMemberInfo.tsx @@ -5,9 +5,10 @@ interface TeamMemberInfoProps { members: TeamMember[]; activeIndex: number; onSelect: (index: number) => void; + onOpenBio: () => void; } -export function TeamMemberInfo({ members, activeIndex, onSelect }: TeamMemberInfoProps) { +export function TeamMemberInfo({ members, activeIndex, onSelect, onOpenBio }: TeamMemberInfoProps) { const member = members[activeIndex]; return ( @@ -36,6 +37,13 @@ export function TeamMemberInfo({ members, activeIndex, onSelect }: TeamMemberInf

)} + + {/* Progress dots */}
{members.map((_, i) => ( diff --git a/src/components/sections/team/TeamProfile.tsx b/src/components/sections/team/TeamProfile.tsx new file mode 100644 index 0000000..93560ec --- /dev/null +++ b/src/components/sections/team/TeamProfile.tsx @@ -0,0 +1,109 @@ +import Image from "next/image"; +import { ArrowLeft, Instagram, Trophy, Award, GraduationCap } from "lucide-react"; +import type { TeamMember } from "@/types/content"; + +interface TeamProfileProps { + member: TeamMember; + onBack: () => void; +} + +const BIO_SECTIONS = [ + { key: "experience" as const, label: "Опыт", icon: Trophy }, + { key: "victories" as const, label: "Достижения", icon: Award }, + { key: "education" as const, label: "Образование", icon: GraduationCap }, +]; + +export function TeamProfile({ member, onBack }: TeamProfileProps) { + const hasBio = BIO_SECTIONS.some( + (s) => member[s.key] && member[s.key]!.length > 0 + ); + + return ( +
+ {/* Back button */} + + + {/* Main: photo + info */} +
+ {/* Photo */} +
+ {member.name} +
+ + {/* Info */} +
+

{member.name}

+

+ {member.role} +

+ + {member.instagram && ( + + + {member.instagram.split("/").filter(Boolean).pop()} + + )} + + {member.description && ( +

+ {member.description} +

+ )} +
+
+ + {/* Bio sections */} + {hasBio && ( +
+ {BIO_SECTIONS.map((section) => { + const items = member[section.key]; + if (!items || items.length === 0) return null; + const Icon = section.icon; + + return ( +
+
+ + + {section.label} + +
+
    + {items.map((item, i) => ( +
  • + + {item} +
  • + ))} +
+
+ ); + })} +
+ )} +
+ ); +} diff --git a/src/lib/db.ts b/src/lib/db.ts index 442eaec..d27b990 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -33,11 +33,21 @@ function initTables(db: Database.Database) { image TEXT NOT NULL, instagram TEXT, description TEXT, + experience TEXT, + victories TEXT, + education TEXT, sort_order INTEGER NOT NULL DEFAULT 0, created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')) ); `); + + // Migrate: add bio columns if missing + const cols = db.prepare("PRAGMA table_info(team_members)").all() as { name: string }[]; + const colNames = new Set(cols.map((c) => c.name)); + if (!colNames.has("experience")) db.exec("ALTER TABLE team_members ADD COLUMN experience TEXT"); + if (!colNames.has("victories")) db.exec("ALTER TABLE team_members ADD COLUMN victories TEXT"); + if (!colNames.has("education")) db.exec("ALTER TABLE team_members ADD COLUMN education TEXT"); } // --- Sections --- @@ -67,9 +77,17 @@ interface TeamMemberRow { image: string; instagram: string | null; description: string | null; + experience: string | null; + victories: string | null; + education: string | null; sort_order: number; } +function parseJsonArray(val: string | null): string[] | undefined { + if (!val) return undefined; + try { const arr = JSON.parse(val); return Array.isArray(arr) && arr.length > 0 ? arr : undefined; } catch { return undefined; } +} + export function getTeamMembers(): (TeamMember & { id: number })[] { const db = getDb(); const rows = db @@ -82,6 +100,9 @@ export function getTeamMembers(): (TeamMember & { id: number })[] { image: r.image, instagram: r.instagram ?? undefined, description: r.description ?? undefined, + experience: parseJsonArray(r.experience), + victories: parseJsonArray(r.victories), + education: parseJsonArray(r.education), })); } @@ -100,6 +121,9 @@ export function getTeamMember( image: r.image, instagram: r.instagram ?? undefined, description: r.description ?? undefined, + experience: parseJsonArray(r.experience), + victories: parseJsonArray(r.victories), + education: parseJsonArray(r.education), }; } @@ -112,8 +136,8 @@ export function createTeamMember( .get() as { max: number }; const result = db .prepare( - `INSERT INTO team_members (name, role, image, instagram, description, sort_order) - VALUES (?, ?, ?, ?, ?, ?)` + `INSERT INTO team_members (name, role, image, instagram, description, experience, victories, education, sort_order) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)` ) .run( data.name, @@ -121,6 +145,9 @@ export function createTeamMember( data.image, data.instagram ?? null, data.description ?? null, + data.experience?.length ? JSON.stringify(data.experience) : null, + data.victories?.length ? JSON.stringify(data.victories) : null, + data.education?.length ? JSON.stringify(data.education) : null, maxOrder.max + 1 ); return result.lastInsertRowid as number; @@ -139,6 +166,9 @@ export function updateTeamMember( if (data.image !== undefined) { fields.push("image = ?"); values.push(data.image); } if (data.instagram !== undefined) { fields.push("instagram = ?"); values.push(data.instagram || null); } if (data.description !== undefined) { fields.push("description = ?"); values.push(data.description || null); } + if (data.experience !== undefined) { fields.push("experience = ?"); values.push(data.experience?.length ? JSON.stringify(data.experience) : null); } + if (data.victories !== undefined) { fields.push("victories = ?"); values.push(data.victories?.length ? JSON.stringify(data.victories) : null); } + if (data.education !== undefined) { fields.push("education = ?"); values.push(data.education?.length ? JSON.stringify(data.education) : null); } if (fields.length === 0) return; fields.push("updated_at = datetime('now')"); diff --git a/src/types/content.ts b/src/types/content.ts index 81c5164..db997f2 100644 --- a/src/types/content.ts +++ b/src/types/content.ts @@ -13,6 +13,9 @@ export interface TeamMember { image: string; instagram?: string; description?: string; + experience?: string[]; + victories?: string[]; + education?: string[]; } export interface FAQItem {