feat: show trainer's groups on profile from schedule data

- Extract classes from schedule matching trainer name
- Group by type+time+location, combine days (e.g. ПН, СР)
- Display as horizontal scroll cards with time, location, level
- Show recruiting badge and address (without city prefix)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-15 16:35:17 +03:00
parent ce033074cd
commit 4f92057411
3 changed files with 66 additions and 9 deletions

View File

@@ -27,7 +27,7 @@ export default function HomePage() {
locations: content.schedule.locations.length,
}}
/>
<Team data={content.team} />
<Team data={content.team} schedule={content.schedule.locations} />
<Classes data={content.classes} />
<Schedule data={content.schedule} classItems={content.classes.items} />
<Pricing data={content.pricing} />

View File

@@ -6,13 +6,14 @@ import { Reveal } from "@/components/ui/Reveal";
import { TeamCarousel } from "@/components/sections/team/TeamCarousel";
import { TeamMemberInfo } from "@/components/sections/team/TeamMemberInfo";
import { TeamProfile } from "@/components/sections/team/TeamProfile";
import type { SiteContent } from "@/types/content";
import type { SiteContent, ScheduleLocation } from "@/types/content";
interface TeamProps {
data: SiteContent["team"];
schedule?: ScheduleLocation[];
}
export function Team({ data: team }: TeamProps) {
export function Team({ data: team, schedule }: TeamProps) {
const [activeIndex, setActiveIndex] = useState(0);
const [showProfile, setShowProfile] = useState(false);
@@ -61,6 +62,7 @@ export function Team({ data: team }: TeamProps) {
<TeamProfile
member={team.members[activeIndex]}
onBack={() => setShowProfile(false)}
schedule={schedule}
/>
)}
</div>

View File

@@ -1,14 +1,15 @@
import { useState, useEffect, useRef, useCallback } from "react";
import Image from "next/image";
import { ArrowLeft, Instagram, Trophy, GraduationCap, ExternalLink, X, Award, Scale } from "lucide-react";
import type { TeamMember, RichListItem, VictoryItem } from "@/types/content";
import { ArrowLeft, Instagram, Trophy, GraduationCap, ExternalLink, X, Award, Scale, Clock, MapPin } from "lucide-react";
import type { TeamMember, RichListItem, VictoryItem, ScheduleLocation } from "@/types/content";
interface TeamProfileProps {
member: TeamMember;
onBack: () => void;
schedule?: ScheduleLocation[];
}
export function TeamProfile({ member, onBack }: TeamProfileProps) {
export function TeamProfile({ member, onBack, schedule }: TeamProfileProps) {
const [lightbox, setLightbox] = useState<string | null>(null);
useEffect(() => {
@@ -33,7 +34,28 @@ export function TeamProfile({ member, onBack }: TeamProfileProps) {
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;
// Extract trainer's groups from schedule, grouped by type + time + location
const groupMap = new Map<string, { type: string; time: string; location: string; address: string; days: string[]; level?: string; recruiting?: boolean }>();
schedule?.forEach(location => {
location.days.forEach(day => {
day.classes
.filter(c => c.trainer === member.name)
.forEach(c => {
const key = `${c.type}|${c.time}|${location.name}`;
const existing = groupMap.get(key);
if (existing) {
if (!existing.days.includes(day.dayShort)) existing.days.push(day.dayShort);
} else {
groupMap.set(key, { type: c.type, time: c.time, location: location.name, address: location.address, days: [day.dayShort], level: c.level, recruiting: c.recruiting });
}
});
});
});
const uniqueGroups = Array.from(groupMap.values());
const hasGroups = uniqueGroups.length > 0;
const hasBio = hasVictories || hasExperience || hasEducation || hasGroups;
return (
<div
@@ -146,9 +168,42 @@ export function TeamProfile({ member, onBack }: TeamProfileProps) {
</div>
)}
{/* Groups */}
{hasGroups && (
<div className={hasVictories ? "mt-8" : ""}>
<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">
<Clock size={14} />
Группы
</span>
<ScrollRow>
{uniqueGroups.map((g, i) => (
<div key={i} className="w-44 shrink-0 rounded-xl border border-white/[0.08] bg-white/[0.03] p-3 space-y-1.5">
<p className="text-xs font-semibold uppercase tracking-wider text-white/80">{g.type}</p>
<div className="flex items-center gap-1.5 text-xs text-white/50">
<Clock size={11} />
{g.days.join(", ")} · {g.time}
</div>
<div className="flex items-start gap-1.5 text-xs text-white/40">
<MapPin size={11} className="mt-0.5 shrink-0" />
<span>{g.location} · {g.address.replace(/^г\.\s*\S+,\s*/, "")}</span>
</div>
{g.level && (
<p className="text-[10px] text-gold/60">{g.level}</p>
)}
{g.recruiting && (
<span className="inline-block rounded-full bg-green-500/15 border border-green-500/30 px-2 py-0.5 text-[10px] text-green-400">
Набор открыт
</span>
)}
</div>
))}
</ScrollRow>
</div>
)}
{/* Education */}
{hasEducation && (
<div className={hasVictories ? "mt-8" : ""}>
<div className={hasVictories || hasGroups ? "mt-8" : ""}>
<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">
<GraduationCap size={14} />
Образование
@@ -163,7 +218,7 @@ export function TeamProfile({ member, onBack }: TeamProfileProps) {
{/* Experience */}
{hasExperience && (
<div className={hasVictories || hasEducation ? "mt-8" : ""}>
<div className={hasVictories || hasGroups || hasEducation ? "mt-8" : ""}>
<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={15} />
Опыт