From 1b391cdde6cfb889b3114f7058020ce526a32fa2 Mon Sep 17 00:00:00 2001 From: "diana.dolgolyova" Date: Fri, 13 Mar 2026 16:31:00 +0300 Subject: [PATCH] feat: split victories into tabbed sections (place/nomination/judge) Add type field to VictoryItem, tabbed UI in trainer profile, and type dropdown in admin form. Co-Authored-By: Claude Opus 4.6 --- src/app/admin/_components/FormField.tsx | 92 +++----------- src/components/sections/team/TeamProfile.tsx | 125 +++++++++---------- src/types/content.ts | 1 + 3 files changed, 76 insertions(+), 142 deletions(-) diff --git a/src/app/admin/_components/FormField.tsx b/src/app/admin/_components/FormField.tsx index 41efee2..7f54b21 100644 --- a/src/app/admin/_components/FormField.tsx +++ b/src/app/admin/_components/FormField.tsx @@ -657,80 +657,6 @@ export function ValidatedLinkField({ value, onChange, onValidate, validationKey, ); } -// --- Place/Nomination Select --- -const PLACE_OPTIONS = [ - { value: "1 место", label: "1 место", icon: "🥇" }, - { value: "2 место", label: "2 место", icon: "🥈" }, - { value: "3 место", label: "3 место", icon: "🥉" }, - { value: "4 место", label: "4 место" }, - { value: "5 место", label: "5 место" }, - { value: "6 место", label: "6 место" }, - { value: "Финалист", label: "Финалист", icon: "🏅" }, - { value: "Полуфиналист", label: "Полуфиналист" }, - { value: "Лауреат", label: "Лауреат", icon: "🏆" }, - { value: "Номинант", label: "Номинант" }, - { value: "Участник", label: "Участник" }, - { value: "Победитель", label: "Победитель", icon: "🏆" }, - { value: "Гран-при", label: "Гран-при", icon: "👑" }, - { value: "Best Show", label: "Best Show", icon: "⭐" }, - { value: "Vice Champion", label: "Vice Champion" }, - { value: "Champion", label: "Champion", icon: "🏆" }, -]; - -interface PlaceSelectProps { - value: string; - onChange: (value: string) => void; -} - -function PlaceSelect({ value, onChange }: PlaceSelectProps) { - const [open, setOpen] = useState(false); - const containerRef = useRef(null); - - useEffect(() => { - if (!open) return; - function handle(e: MouseEvent) { - if (containerRef.current && !containerRef.current.contains(e.target as Node)) setOpen(false); - } - document.addEventListener("mousedown", handle); - return () => document.removeEventListener("mousedown", handle); - }, [open]); - - const selected = PLACE_OPTIONS.find((o) => o.value === value); - - return ( -
- - {open && ( -
-
- {PLACE_OPTIONS.map((opt) => ( - - ))} -
-
- )} -
- ); -} - interface VictoryItemListFieldProps { label: string; items: VictoryItem[]; @@ -744,7 +670,7 @@ interface VictoryItemListFieldProps { export function VictoryItemListField({ label, items, onChange, cityErrors, citySuggestions, onCitySearch, onCitySelect, onLinkValidate }: VictoryItemListFieldProps) { function add() { - onChange([...items, { place: "", category: "", competition: "" }]); + onChange([...items, { type: "place", place: "", category: "", competition: "" }]); } function remove(index: number) { @@ -762,9 +688,21 @@ export function VictoryItemListField({ label, items, onChange, cityErrors, cityS {items.map((item, i) => (
- update(i, "type", e.target.value)} + className="w-32 shrink-0 rounded-md border border-white/10 bg-neutral-800 px-2 py-1.5 text-sm text-white outline-none focus:border-gold transition-colors" + > + + + + + update(i, "place", v)} + onChange={(e) => update(i, "place", e.target.value)} + placeholder="1 место, финалист..." + className="w-28 shrink-0 rounded-md border border-white/10 bg-neutral-800 px-2.5 py-1.5 text-sm text-white placeholder-neutral-600 outline-none focus:border-gold transition-colors" /> (null); - const hasVictories = member.victories && member.victories.length > 0; + const places = member.victories?.filter(v => !v.type || v.type === 'place') ?? []; + const nominations = member.victories?.filter(v => v.type === 'nomination') ?? []; + const judging = member.victories?.filter(v => v.type === 'judge') ?? []; + const victoryTabs = [ + ...(places.length > 0 ? [{ key: 'place' as const, label: 'Достижения', icon: Trophy, items: places }] : []), + ...(nominations.length > 0 ? [{ key: 'nomination' as const, label: 'Номинации', icon: Award, items: nominations }] : []), + ...(judging.length > 0 ? [{ key: 'judge' as const, label: 'Судейство', icon: Scale, items: judging }] : []), + ]; + const hasVictories = victoryTabs.length > 0; + const [activeTab, setActiveTab] = useState(victoryTabs[0]?.key ?? 'place'); const hasExperience = member.experience && member.experience.length > 0; const hasEducation = member.education && member.education.length > 0; const hasBio = hasVictories || hasExperience || hasEducation; @@ -79,18 +88,37 @@ export function TeamProfile({ member, onBack }: TeamProfileProps) { {/* Bio panel — overlaps photo edge on desktop */}
- {/* Victories */} + {/* Victory tabs */} {hasVictories && (
- - - Достижения - -
- {member.victories!.map((item, i) => ( - +
+ {victoryTabs.map(tab => ( + ))}
+ {victoryTabs.map(tab => ( + activeTab === tab.key && ( +
+ {tab.items.map((item, i) => ( + + ))} +
+ ) + ))}
)} @@ -174,72 +202,39 @@ export function TeamProfile({ member, onBack }: TeamProfileProps) { ); } -function VictoryCard({ victory, onImageClick }: { victory: VictoryItem; onImageClick: (src: string) => void }) { - const hasImage = !!victory.image; +function VictoryCard({ victory }: { victory: VictoryItem }) { const hasLink = !!victory.link; - if (hasImage) { - return ( -
- -
- ); - } - return ( -
-
- {victory.place && ( -

{victory.place}

- )} +
+
+ {victory.place && ( +
+ + {victory.place} + +
+ )} +
{victory.category && ( -

{victory.category}

- )} -

{victory.competition}

- {(victory.location || victory.date) && ( -
- {victory.location && ( - - - {victory.location} - - )} - {victory.date && ( - - - {victory.date} - - )} -
+

{victory.category}

)} +

{victory.competition}

{hasLink && ( - + Подробнее )} diff --git a/src/types/content.ts b/src/types/content.ts index 607cb1f..ed7a411 100644 --- a/src/types/content.ts +++ b/src/types/content.ts @@ -14,6 +14,7 @@ export interface RichListItem { } export interface VictoryItem { + type?: 'place' | 'nomination' | 'judge'; place: string; category: string; competition: string;