Compare commits
2 Commits
1b391cdde6
...
ce033074cd
| Author | SHA1 | Date | |
|---|---|---|---|
| ce033074cd | |||
| d4751975d2 |
@@ -49,6 +49,7 @@ export function TeamCarousel({ members, activeIndex, onActiveChange }: TeamCarou
|
|||||||
const total = members.length;
|
const total = members.length;
|
||||||
const [dragOffset, setDragOffset] = useState(0);
|
const [dragOffset, setDragOffset] = useState(0);
|
||||||
const isDraggingRef = useRef(false);
|
const isDraggingRef = useRef(false);
|
||||||
|
const wasDragRef = useRef(false);
|
||||||
const pausedUntilRef = useRef(0);
|
const pausedUntilRef = useRef(0);
|
||||||
const dragStartRef = useRef<{ x: number; startIndex: number } | null>(null);
|
const dragStartRef = useRef<{ x: number; startIndex: number } | null>(null);
|
||||||
|
|
||||||
@@ -76,6 +77,7 @@ export function TeamCarousel({ members, activeIndex, onActiveChange }: TeamCarou
|
|||||||
(e: React.PointerEvent) => {
|
(e: React.PointerEvent) => {
|
||||||
(e.target as HTMLElement).setPointerCapture(e.pointerId);
|
(e.target as HTMLElement).setPointerCapture(e.pointerId);
|
||||||
isDraggingRef.current = true;
|
isDraggingRef.current = true;
|
||||||
|
wasDragRef.current = false;
|
||||||
dragStartRef.current = { x: e.clientX, startIndex: activeIndex };
|
dragStartRef.current = { x: e.clientX, startIndex: activeIndex };
|
||||||
setDragOffset(0);
|
setDragOffset(0);
|
||||||
},
|
},
|
||||||
@@ -86,6 +88,7 @@ export function TeamCarousel({ members, activeIndex, onActiveChange }: TeamCarou
|
|||||||
(e: React.PointerEvent) => {
|
(e: React.PointerEvent) => {
|
||||||
if (!dragStartRef.current) return;
|
if (!dragStartRef.current) return;
|
||||||
const dx = e.clientX - dragStartRef.current.x;
|
const dx = e.clientX - dragStartRef.current.x;
|
||||||
|
if (Math.abs(dx) > 10) wasDragRef.current = true;
|
||||||
setDragOffset(dx);
|
setDragOffset(dx);
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
@@ -194,7 +197,13 @@ export function TeamCarousel({ members, activeIndex, onActiveChange }: TeamCarou
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={m.name}
|
key={m.name}
|
||||||
className={`absolute bottom-0 overflow-hidden rounded-2xl border pointer-events-none ${style.isCenter ? "team-card-glitter" : ""}`}
|
onClick={() => {
|
||||||
|
if (!style.isCenter && !wasDragRef.current) {
|
||||||
|
onActiveChange(i);
|
||||||
|
pausedUntilRef.current = Date.now() + PAUSE_MS;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={`absolute bottom-0 overflow-hidden rounded-2xl border ${style.isCenter ? "team-card-glitter" : "cursor-pointer"} pointer-events-auto`}
|
||||||
style={{
|
style={{
|
||||||
width: style.width,
|
width: style.width,
|
||||||
height: style.height,
|
height: style.height,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState, useEffect, useRef, useCallback } from "react";
|
||||||
import Image from "next/image";
|
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";
|
import type { TeamMember, RichListItem, VictoryItem } from "@/types/content";
|
||||||
|
|
||||||
interface TeamProfileProps {
|
interface TeamProfileProps {
|
||||||
@@ -10,6 +10,17 @@ interface TeamProfileProps {
|
|||||||
|
|
||||||
export function TeamProfile({ member, onBack }: TeamProfileProps) {
|
export function TeamProfile({ member, onBack }: TeamProfileProps) {
|
||||||
const [lightbox, setLightbox] = useState<string | null>(null);
|
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 places = member.victories?.filter(v => !v.type || v.type === 'place') ?? [];
|
||||||
const nominations = member.victories?.filter(v => v.type === 'nomination') ?? [];
|
const nominations = member.victories?.filter(v => v.type === 'nomination') ?? [];
|
||||||
const judging = member.victories?.filter(v => v.type === 'judge') ?? [];
|
const judging = member.victories?.filter(v => v.type === 'judge') ?? [];
|
||||||
@@ -29,19 +40,17 @@ export function TeamProfile({ member, onBack }: TeamProfileProps) {
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
style={{ animation: "team-info-in 0.6s cubic-bezier(0.16, 1, 0.3, 1)" }}
|
style={{ animation: "team-info-in 0.6s cubic-bezier(0.16, 1, 0.3, 1)" }}
|
||||||
>
|
>
|
||||||
{/* Back button */}
|
|
||||||
<button
|
|
||||||
onClick={onBack}
|
|
||||||
className="mb-6 inline-flex items-center gap-1.5 text-sm text-white/40 transition-colors hover:text-gold-light cursor-pointer"
|
|
||||||
>
|
|
||||||
<ArrowLeft size={16} />
|
|
||||||
Назад
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Magazine editorial layout */}
|
{/* Magazine editorial layout */}
|
||||||
<div className="relative mx-auto max-w-4xl flex flex-col sm:flex-row sm:items-start">
|
<div className="relative mx-auto max-w-5xl flex flex-col sm:flex-row sm:items-start">
|
||||||
{/* Photo — left column, sticky */}
|
{/* 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 shrink-0 w-full sm:w-[380px] lg:w-[420px] sm:sticky sm:top-8">
|
||||||
|
<button
|
||||||
|
onClick={onBack}
|
||||||
|
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/50 transition-colors hover:text-white hover:bg-white/[0.1] cursor-pointer"
|
||||||
|
>
|
||||||
|
<ArrowLeft size={14} />
|
||||||
|
Назад
|
||||||
|
</button>
|
||||||
<div className="relative aspect-[3/4] overflow-hidden rounded-2xl border border-white/[0.06]">
|
<div className="relative aspect-[3/4] overflow-hidden rounded-2xl border border-white/[0.06]">
|
||||||
<Image
|
<Image
|
||||||
src={member.image}
|
src={member.image}
|
||||||
@@ -87,7 +96,20 @@ export function TeamProfile({ member, onBack }: TeamProfileProps) {
|
|||||||
|
|
||||||
{/* Bio panel — overlaps photo edge on desktop */}
|
{/* 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="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 */}
|
{/* Victory tabs */}
|
||||||
{hasVictories && (
|
{hasVictories && (
|
||||||
<div>
|
<div>
|
||||||
@@ -110,15 +132,17 @@ export function TeamProfile({ member, onBack }: TeamProfileProps) {
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{victoryTabs.map(tab => (
|
<div className="grid mt-4" style={{ gridTemplateColumns: "1fr", gridTemplateRows: "1fr" }}>
|
||||||
activeTab === tab.key && (
|
{victoryTabs.map(tab => (
|
||||||
<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"}`}>
|
||||||
{tab.items.map((item, i) => (
|
<ScrollRow>
|
||||||
<VictoryCard key={i} victory={item} />
|
{tab.items.map((item, i) => (
|
||||||
))}
|
<VictoryCard key={i} victory={item} />
|
||||||
|
))}
|
||||||
|
</ScrollRow>
|
||||||
</div>
|
</div>
|
||||||
)
|
))}
|
||||||
))}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -129,11 +153,11 @@ export function TeamProfile({ member, onBack }: TeamProfileProps) {
|
|||||||
<GraduationCap size={14} />
|
<GraduationCap size={14} />
|
||||||
Образование
|
Образование
|
||||||
</span>
|
</span>
|
||||||
<div className="mt-4 space-y-2">
|
<ScrollRow>
|
||||||
{member.education!.map((item, i) => (
|
{member.education!.map((item, i) => (
|
||||||
<RichCard key={i} item={item} onImageClick={setLightbox} />
|
<RichCard key={i} item={item} onImageClick={setLightbox} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</ScrollRow>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -144,17 +168,13 @@ export function TeamProfile({ member, onBack }: TeamProfileProps) {
|
|||||||
<Trophy size={15} />
|
<Trophy size={15} />
|
||||||
Опыт
|
Опыт
|
||||||
</span>
|
</span>
|
||||||
<ul className="mt-4 space-y-2.5">
|
<ScrollRow>
|
||||||
{member.experience!.map((item, i) => (
|
{member.experience!.map((item, i) => (
|
||||||
<li
|
<div key={i} className="w-48 shrink-0 rounded-xl border border-white/[0.08] bg-white/[0.03] p-3">
|
||||||
key={i}
|
<p className="text-sm text-white/60">{item}</p>
|
||||||
className="flex items-start gap-2.5 text-sm text-white/60"
|
</div>
|
||||||
>
|
|
||||||
<span className="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-gold/40" />
|
|
||||||
{item}
|
|
||||||
</li>
|
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ScrollRow>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -171,6 +191,7 @@ export function TeamProfile({ member, onBack }: TeamProfileProps) {
|
|||||||
Информация скоро появится
|
Информация скоро появится
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -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 }) {
|
function VictoryCard({ victory }: { victory: VictoryItem }) {
|
||||||
const hasLink = !!victory.link;
|
const hasLink = !!victory.link;
|
||||||
|
|
||||||
return (
|
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" />
|
<div className="absolute top-0 left-0 w-1 h-full bg-gold/40 rounded-l-xl" />
|
||||||
{victory.place && (
|
{victory.place && (
|
||||||
<div className="absolute -top-3.5 left-1/2 -translate-x-1/2 z-10">
|
<div className="absolute -top-3 left-1/2 -translate-x-1/2 z-10">
|
||||||
<span
|
<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">
|
||||||
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}
|
{victory.place}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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 && (
|
{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-xs text-white/50">{victory.competition}</p>
|
<p className="text-sm text-white/50">{victory.competition}</p>
|
||||||
{hasLink && (
|
{hasLink && (
|
||||||
<a
|
<a
|
||||||
href={victory.link}
|
href={victory.link}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
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} />
|
<ExternalLink size={10} />
|
||||||
Подробнее
|
Подробнее
|
||||||
@@ -249,16 +305,16 @@ function RichCard({ item, onImageClick }: { item: RichListItem; onImageClick: (s
|
|||||||
|
|
||||||
if (hasImage) {
|
if (hasImage) {
|
||||||
return (
|
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
|
<button
|
||||||
onClick={() => onImageClick(item.image!)}
|
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
|
<Image
|
||||||
src={item.image!}
|
src={item.image!}
|
||||||
alt={item.text}
|
alt={item.text}
|
||||||
fill
|
fill
|
||||||
sizes="64px"
|
sizes="56px"
|
||||||
className="object-cover transition-transform group-hover:scale-105"
|
className="object-cover transition-transform group-hover:scale-105"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
@@ -281,7 +337,7 @@ function RichCard({ item, onImageClick }: { item: RichListItem; onImageClick: (s
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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">
|
<div className="p-3">
|
||||||
<p className="text-sm text-white/60">{item.text}</p>
|
<p className="text-sm text-white/60">{item.text}</p>
|
||||||
{hasLink && (
|
{hasLink && (
|
||||||
|
|||||||
Reference in New Issue
Block a user