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 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 16:31:00 +03:00
parent d3bb43af80
commit 1b391cdde6
3 changed files with 76 additions and 142 deletions

View File

@@ -1,6 +1,6 @@
import { useState } from "react";
import Image from "next/image";
import { ArrowLeft, Instagram, Trophy, GraduationCap, ExternalLink, X, MapPin, Calendar } from "lucide-react";
import { ArrowLeft, Instagram, Trophy, GraduationCap, ExternalLink, X, MapPin, Calendar, Award, Scale } from "lucide-react";
import type { TeamMember, RichListItem, VictoryItem } from "@/types/content";
interface TeamProfileProps {
@@ -10,7 +10,16 @@ interface TeamProfileProps {
export function TeamProfile({ member, onBack }: TeamProfileProps) {
const [lightbox, setLightbox] = useState<string | null>(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 */}
<div className="relative sm:-ml-12 sm:mt-8 mt-0 flex-1 min-w-0 z-10">
<div className="rounded-2xl border border-white/[0.08] bg-black/60 backdrop-blur-xl p-5 sm:p-6 shadow-2xl shadow-black/40">
{/* Victories */}
{/* Victory tabs */}
{hasVictories && (
<div>
<span className="inline-flex items-center gap-1.5 rounded-full border border-gold/20 bg-gold/5 px-4 py-1.5 text-sm font-medium text-gold">
<Trophy size={14} />
Достижения
</span>
<div className="mt-4 flex flex-col sm:flex-row sm:flex-wrap gap-2.5">
{member.victories!.map((item, i) => (
<VictoryCard key={i} victory={item} onImageClick={setLightbox} />
<div className="flex flex-wrap gap-2">
{victoryTabs.map(tab => (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key)}
className={`inline-flex items-center gap-1.5 rounded-full border px-4 py-1.5 text-sm font-medium transition-colors cursor-pointer ${
activeTab === tab.key
? "border-gold/30 bg-gold/10 text-gold"
: "border-white/[0.08] bg-white/[0.03] text-white/40 hover:text-white/60"
}`}
>
<tab.icon size={14} />
{tab.label}
<span className={`ml-0.5 text-xs ${activeTab === tab.key ? "text-gold/60" : "text-white/20"}`}>
{tab.items.length}
</span>
</button>
))}
</div>
{victoryTabs.map(tab => (
activeTab === tab.key && (
<div key={tab.key} className="mt-5 pt-1 flex flex-col sm:flex-row sm:flex-wrap gap-4">
{tab.items.map((item, i) => (
<VictoryCard key={i} victory={item} />
))}
</div>
)
))}
</div>
)}
@@ -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 (
<div className="group w-full sm:w-44 shrink-0 rounded-xl border border-white/[0.08] overflow-hidden">
<button
onClick={() => onImageClick(victory.image!)}
className="relative w-full aspect-square overflow-hidden cursor-pointer"
>
<Image
src={victory.image!}
alt={victory.competition}
fill
sizes="176px"
className="object-cover transition-transform group-hover:scale-105"
/>
<div className="absolute bottom-0 left-0 right-0 bg-black/80 backdrop-blur-sm p-2.5 space-y-0.5">
{victory.place && (
<p className="text-sm font-bold text-gold">{victory.place}</p>
)}
{victory.category && (
<p className="text-[10px] font-semibold uppercase tracking-wider text-white">{victory.category}</p>
)}
<p className="text-xs text-white/80">{victory.competition}</p>
</div>
</button>
</div>
);
}
return (
<div className="group w-full sm:w-56 shrink-0 rounded-xl border border-white/[0.08] overflow-hidden bg-white/[0.03]">
<div className="p-2.5 space-y-0.5">
{victory.place && (
<p className="text-lg font-bold text-gold">{victory.place}</p>
)}
<div className="group w-full sm:w-44 shrink-0 rounded-xl border border-white/[0.08] overflow-visible bg-white/[0.03] relative">
<div className="absolute top-0 left-0 w-1 h-full bg-gold/40 rounded-l-xl" />
{victory.place && (
<div className="absolute -top-3.5 left-1/2 -translate-x-1/2 z-10">
<span
className="inline-block px-4 py-1 text-[11px] font-semibold uppercase tracking-[0.2em] text-gold whitespace-nowrap"
style={{
background: "linear-gradient(135deg, rgba(201,169,110,0.12), rgba(201,169,110,0.25), rgba(201,169,110,0.12))",
border: "1px solid rgba(201,169,110,0.35)",
clipPath: "polygon(8px 0%, calc(100% - 8px) 0%, 100% 50%, calc(100% - 8px) 100%, 8px 100%, 0% 50%)",
}}
>
{victory.place}
</span>
</div>
)}
<div className={`pl-3.5 pr-2.5 pb-2 space-y-0.5 ${victory.place ? "pt-6" : "py-2"}`}>
{victory.category && (
<p className="text-xs font-medium uppercase tracking-wider text-white/80">{victory.category}</p>
)}
<p className="text-sm text-white/50">{victory.competition}</p>
{(victory.location || victory.date) && (
<div className="flex flex-wrap items-center gap-x-3 gap-y-0.5 pt-0.5">
{victory.location && (
<span className="inline-flex items-center gap-1 text-xs text-white/30">
<MapPin size={10} />
{victory.location}
</span>
)}
{victory.date && (
<span className="inline-flex items-center gap-1 text-xs text-white/30">
<Calendar size={10} />
{victory.date}
</span>
)}
</div>
<p className="text-[10px] font-semibold uppercase tracking-wider text-white/80">{victory.category}</p>
)}
<p className="text-xs text-white/50">{victory.competition}</p>
{hasLink && (
<a
href={victory.link}
target="_blank"
rel="noopener noreferrer"
className="mt-1 inline-flex items-center gap-1 text-xs text-gold/70 hover:text-gold transition-colors"
className="mt-0.5 inline-flex items-center gap-1 text-[10px] text-gold/70 hover:text-gold transition-colors"
>
<ExternalLink size={11} />
<ExternalLink size={10} />
Подробнее
</a>
)}