@@ -1,6 +1,6 @@
import { useState } from "react" ;
import { useState , useEffect , useRef , useCallback } from "react" ;
import Image from "next/image" ;
import { ArrowLeft , Instagram , Trophy , GraduationCap , ExternalLink , X , MapPin , Calendar , Award , Scale } from "lucide-react" ;
import { ArrowLeft , Instagram , Trophy , GraduationCap , ExternalLink , X , Award , Scale } from "lucide-react" ;
import type { TeamMember , RichListItem , VictoryItem } from "@/types/content" ;
interface TeamProfileProps {
@@ -10,6 +10,17 @@ interface TeamProfileProps {
export function TeamProfile ( { member , onBack } : TeamProfileProps ) {
const [ lightbox , setLightbox ] = useState < string | null > ( null ) ;
useEffect ( ( ) = > {
function handleKeyDown ( e : KeyboardEvent ) {
if ( e . key === "Escape" ) {
if ( lightbox ) setLightbox ( null ) ;
else onBack ( ) ;
}
}
window . addEventListener ( "keydown" , handleKeyDown ) ;
return ( ) = > window . removeEventListener ( "keydown" , handleKeyDown ) ;
} , [ lightbox , onBack ] ) ;
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' ) ? ? [ ] ;
@@ -29,19 +40,17 @@ export function TeamProfile({ member, onBack }: TeamProfileProps) {
className = "w-full"
style = { { animation : "team-info-in 0.6s cubic-bezier(0.16, 1, 0.3, 1)" } }
>
{ /* Back button */ }
{ /* Magazine editorial layout */ }
< div className = "relative mx-auto max-w-5xl flex flex-col sm:flex-row sm:items-start" >
{ /* Photo — left column, sticky */ }
< div className = "relative shrink-0 w-full sm:w-[380px] lg:w-[420px] sm:sticky sm:top-8" >
< button
onClick = { onBack }
className = "mb-6 inline-flex items-center gap-1.5 text-sm text-white/4 0 transition-colors hover:text-gold-light cursor-pointer"
className = "mb-3 inline-flex items-center gap-1.5 rounded-full bg-white/[0.06] px-3 py-1.5 text-sm text-white/5 0 transition-colors hover:text-white hover:bg-white/[0.1] cursor-pointer"
>
< ArrowLeft size = { 16 } / >
< ArrowLeft size = { 14 } / >
Н а з а д
< / button >
{ /* Magazine editorial layout */ }
< div className = "relative mx-auto max-w-4xl flex flex-col sm:flex-row sm:items-start" >
{ /* Photo — left column, sticky */ }
< div className = "relative shrink-0 w-full sm:w-[340px] lg:w-[380px] sm:sticky sm:top-8" >
< div className = "relative aspect-[3/4] overflow-hidden rounded-2xl border border-white/[0.06]" >
< Image
src = { member . image }
@@ -87,7 +96,20 @@ 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" >
< div className = "relative rounded-2xl border border-white/[0.08] overflow-hidden shadow-2xl shadow-black/40" >
{ /* Ambient photo background */ }
< div className = "absolute inset-0" >
< Image
src = { member . image }
alt = ""
fill
sizes = "600px"
className = "object-cover scale-150 blur-sm grayscale opacity-70 brightness-[0.6] contrast-[1.3]"
/ >
< div className = "absolute inset-0 bg-black/20 mix-blend-multiply" / >
< div className = "absolute inset-0 bg-gold/10 mix-blend-color" / >
< / div >
< div className = "relative p-5 sm:p-6" >
{ /* Victory tabs */ }
{ hasVictories && (
< div >
@@ -110,16 +132,18 @@ export function TeamProfile({ member, onBack }: TeamProfileProps) {
< / button >
) ) }
< / div >
< div className = "grid mt-4" style = { { gridTemplateColumns : "1fr" , gridTemplateRows : "1fr" } } >
{ 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" >
< div key = { tab . key } className = { ` col-start-1 row-start-1 ${ activeTab === tab . key ? "" : "invisible" } ` } >
< ScrollRow >
{ tab . items . map ( ( item , i ) = > (
< VictoryCard key = { i } victory = { item } / >
) ) }
< / ScrollRow >
< / div >
)
) ) }
< / div >
< / div >
) }
{ /* Education */ }
@@ -129,11 +153,11 @@ export function TeamProfile({ member, onBack }: TeamProfileProps) {
< GraduationCap size = { 14 } / >
О б р а з о в а н и е
< / span >
< div className = "mt-4 space-y-2" >
< ScrollRow >
{ member . education ! . map ( ( item , i ) = > (
< RichCard key = { i } item = { item } onImageClick = { setLightbox } / >
) ) }
< / div >
< / ScrollRow >
< / div >
) }
@@ -144,17 +168,13 @@ export function TeamProfile({ member, onBack }: TeamProfileProps) {
< Trophy size = { 15 } / >
О п ы т
< / span >
< ul className = "mt-4 space-y-2.5" >
< ScrollRow >
{ member . experience ! . map ( ( item , i ) = > (
< li
key = { i }
className = "flex items-start gap-2.5 text-sm text-white/60"
>
< span className = "mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-gold/40" / >
{ item }
< / li >
< div key = { i } className = "w-48 shrink-0 rounded-xl border border-white/[0.08] bg-white/[0.03] p-3" >
< p className = "text-sm text-white/60" > { item } < / p >
< / div >
) ) }
< / ul >
< / ScrollRow >
< / div >
) }
@@ -174,6 +194,7 @@ export function TeamProfile({ member, onBack }: TeamProfileProps) {
< / div >
< / div >
< / div >
< / div >
{ /* Image lightbox */ }
{ lightbox && (
@@ -202,37 +223,72 @@ export function TeamProfile({ member, onBack }: TeamProfileProps) {
) ;
}
function ScrollRow ( { children } : { children : React.ReactNode } ) {
const scrollRef = useRef < HTMLDivElement > ( null ) ;
const dragState = useRef < { startX : number ; scrollLeft : number } | null > ( null ) ;
const wasDragged = useRef ( false ) ;
const onPointerDown = useCallback ( ( e : React.PointerEvent ) = > {
const el = scrollRef . current ;
if ( ! el ) return ;
( e . target as HTMLElement ) . setPointerCapture ( e . pointerId ) ;
dragState . current = { startX : e.clientX , scrollLeft : el.scrollLeft } ;
wasDragged . current = false ;
} , [ ] ) ;
const onPointerMove = useCallback ( ( e : React.PointerEvent ) = > {
if ( ! dragState . current || ! scrollRef . current ) return ;
const dx = e . clientX - dragState . current . startX ;
if ( Math . abs ( dx ) > 4 ) wasDragged . current = true ;
scrollRef . current . scrollLeft = dragState . current . scrollLeft - dx ;
} , [ ] ) ;
const onPointerUp = useCallback ( ( ) = > {
dragState . current = null ;
} , [ ] ) ;
return (
< div className = "relative mt-4" >
< div
ref = { scrollRef }
className = "flex items-stretch gap-3 overflow-x-auto pb-2 pt-4 cursor-grab active:cursor-grabbing select-none"
style = { { scrollbarWidth : "none" , msOverflowStyle : "none" , WebkitOverflowScrolling : "touch" } }
onPointerDown = { onPointerDown }
onPointerMove = { onPointerMove }
onPointerUp = { onPointerUp }
onPointerCancel = { onPointerUp }
onLostPointerCapture = { onPointerUp }
>
{ children }
< / div >
< / div >
) ;
}
function VictoryCard ( { victory } : { victory : VictoryItem } ) {
const hasLink = ! ! victory . link ;
return (
< 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 = "group 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%)" ,
} }
>
< div className = "absolute -top-3 left-1/2 -translate-x-1/2 z-10" >
< span className = "inline-block rounded-full border border-gold/40 bg-gold/20 px-3 py-0.5 text-xs font-bold uppercase tracking-wider text-gold whitespace-nowrap backdrop-blur-sm" >
{ victory . place }
< / span >
< / div >
) }
< div className = { ` pl-3.5 pr-2.5 pb-2 space-y-0.5 ${ victory . place ? "pt-6" : "py-2 " } ` } >
< div className = { ` pl-4 pr-3 pb-3 space-y-1 ${ victory . place ? "pt-6" : "py-3 " } ` } >
{ victory . category && (
< p className = "text-[10px] font-semibold uppercase tracking-wider text-white/80" > { victory . category } < / p >
< p className = "text-xs font-semibold uppercase tracking-wider text-white/80" > { victory . category } < / p >
) }
< p className = "text-x s text-white/50" > { victory . competition } < / p >
< p className = "text-sm text-white/50" > { victory . competition } < / p >
{ hasLink && (
< a
href = { victory . link }
target = "_blank"
rel = "noopener noreferrer"
className = "mt-0.5 inline-flex items-center gap-1 text-[10px] text-gold/70 hover:text-gold transition-colors"
className = "mt-1 inline-flex items-center gap-1 text-xs text-gold/70 hover:text-gold transition-colors"
>
< ExternalLink size = { 10 } / >
П о д р о б н е е
@@ -249,16 +305,16 @@ function RichCard({ item, onImageClick }: { item: RichListItem; onImageClick: (s
if ( hasImage ) {
return (
< div className = "group flex rounded-xl border border-white/[0.08] overflow-hidden bg-white/[0.03]" >
< div className = "group w-48 shrink-0 flex rounded-xl border border-white/[0.08] overflow-hidden bg-white/[0.03]" >
< button
onClick = { ( ) = > onImageClick ( item . image ! ) }
className = "relative w-16 shrink-0 overflow-hidden cursor-pointer"
className = "relative w-14 shrink-0 overflow-hidden cursor-pointer"
>
< Image
src = { item . image ! }
alt = { item . text }
fill
sizes = "64 px"
sizes = "5 6px"
className = "object-cover transition-transform group-hover:scale-105"
/ >
< / button >
@@ -281,7 +337,7 @@ function RichCard({ item, onImageClick }: { item: RichListItem; onImageClick: (s
}
return (
< div className = "group rounded-xl border border-white/[0.08] overflow-hidden bg-white/[0.03]" >
< div className = "group w-48 shrink-0 rounded-xl border border-white/[0.08] overflow-hidden bg-white/[0.03]" >
< div className = "p-3" >
< p className = "text-sm text-white/60" > { item . text } < / p >
{ hasLink && (