feat: horizontal drag-scroll for all bio sections, fix tab resize

- Replace wrapping grid with horizontal ScrollRow (drag to scroll)
- Apply to victories, education, and experience sections
- Grid overlay for victory tabs so height stays stable across tabs
- Fixed-width cards (w-44/w-48) with items-stretch for uniform height
- Remove scrollbar, add grab cursor for drag interaction

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-15 16:19:12 +03:00
parent d4751975d2
commit ce033074cd

View File

@@ -1,6 +1,6 @@
import { useState, useEffect } 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 {
@@ -132,17 +132,14 @@ export function TeamProfile({ member, onBack }: TeamProfileProps) {
</button> </button>
))} ))}
</div> </div>
<div className="grid mt-5 pt-1" style={{ gridTemplateColumns: "1fr", gridTemplateRows: "1fr" }}> <div className="grid mt-4" style={{ gridTemplateColumns: "1fr", gridTemplateRows: "1fr" }}>
{victoryTabs.map(tab => ( {victoryTabs.map(tab => (
<div <div key={tab.key} className={`col-start-1 row-start-1 ${activeTab === tab.key ? "" : "invisible"}`}>
key={tab.key} <ScrollRow>
className={`flex flex-col sm:flex-row sm:flex-wrap gap-4 col-start-1 row-start-1 ${ {tab.items.map((item, i) => (
activeTab === tab.key ? "" : "invisible" <VictoryCard key={i} victory={item} />
}`} ))}
> </ScrollRow>
{tab.items.map((item, i) => (
<VictoryCard key={i} victory={item} />
))}
</div> </div>
))} ))}
</div> </div>
@@ -156,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>
)} )}
@@ -171,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>
)} )}
@@ -230,11 +223,53 @@ 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:flex-1 sm:min-w-[180px] 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 left-1/2 -translate-x-1/2 z-10"> <div className="absolute -top-3 left-1/2 -translate-x-1/2 z-10">
@@ -270,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>
@@ -302,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 && (