Files
blackheart-website/src/components/sections/schedule/DayCard.tsx
diana.dolgolyova 46ad10e8a0 feat: upgrade schedule with cross-location views, day/time filters, and clickable trainers
- Add "Все студии" tab merging all locations by weekday with location sub-headers
- Location tabs show hall name + address subtitle for clarity
- Add day multi-select and time-of-day preset filters (Утро/День/Вечер) behind collapsible "Когда" button
- Make trainer and type names clickable in day cards for inline filtering
- Add group view clustering classes by trainer+type+location
- Remove trainer dropdown from filter bar — filter by clicking names in schedule
- Add searchable icon picker and lucide-react icon rendering for classes admin/section

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 21:25:11 +03:00

143 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Clock, User, MapPin } from "lucide-react";
import { shortAddress } from "./constants";
import type { ScheduleDayMerged, ScheduleClassWithLocation } from "./constants";
interface DayCardProps {
day: ScheduleDayMerged;
typeDots: Record<string, string>;
showLocation?: boolean;
filterTrainer: string | null;
setFilterTrainer: (trainer: string | null) => void;
filterType: string | null;
setFilterType: (type: string | null) => void;
}
function ClassRow({
cls,
typeDots,
filterTrainer,
setFilterTrainer,
filterType,
setFilterType,
}: {
cls: ScheduleClassWithLocation;
typeDots: Record<string, string>;
filterTrainer: string | null;
setFilterTrainer: (trainer: string | null) => void;
filterType: string | null;
setFilterType: (type: string | null) => void;
}) {
return (
<div className={`px-5 py-3.5 ${cls.hasSlots ? "bg-emerald-500/5" : cls.recruiting ? "bg-sky-500/5" : ""}`}>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2 text-sm text-neutral-500 dark:text-white/40">
<Clock size={13} />
<span className="font-semibold">{cls.time}</span>
</div>
{cls.hasSlots && (
<span className="shrink-0 rounded-full bg-emerald-500/15 border border-emerald-500/25 px-2 py-0.5 text-[10px] font-semibold text-emerald-600 dark:text-emerald-400">
есть места
</span>
)}
{cls.recruiting && (
<span className="shrink-0 rounded-full bg-sky-500/15 border border-sky-500/25 px-2 py-0.5 text-[10px] font-semibold text-sky-600 dark:text-sky-400">
набор
</span>
)}
</div>
<button
onClick={() => setFilterTrainer(filterTrainer === cls.trainer ? null : cls.trainer)}
className={`mt-1.5 flex items-center gap-2 text-sm font-medium cursor-pointer active:opacity-60 ${
filterTrainer === cls.trainer
? "text-gold underline underline-offset-2"
: "text-neutral-800 dark:text-white/80"
}`}
>
<User size={13} className="shrink-0 text-neutral-400 dark:text-white/30" />
{cls.trainer}
</button>
<div className="mt-2 flex items-center gap-2 flex-wrap">
<button
onClick={() => setFilterType(filterType === cls.type ? null : cls.type)}
className="flex items-center gap-2 cursor-pointer active:opacity-60"
>
<span className={`h-2 w-2 shrink-0 rounded-full ${typeDots[cls.type] ?? "bg-white/30"}`} />
<span className={`text-xs ${
filterType === cls.type
? "text-gold underline underline-offset-2"
: "text-neutral-500 dark:text-white/40"
}`}>{cls.type}</span>
</button>
{cls.level && (
<span className="rounded-full bg-rose-500/15 border border-rose-500/25 px-2 py-0.5 text-[10px] font-semibold text-rose-600 dark:text-rose-400">
{cls.level}
</span>
)}
</div>
</div>
);
}
export function DayCard({ day, typeDots, showLocation, filterTrainer, setFilterTrainer, filterType, setFilterType }: DayCardProps) {
// Group classes by location when showLocation is true
const locationGroups = showLocation
? Array.from(
day.classes.reduce((map, cls) => {
const loc = cls.locationName ?? "";
if (!map.has(loc)) {
map.set(loc, { address: cls.locationAddress, classes: [] });
}
map.get(loc)!.classes.push(cls);
return map;
}, new Map<string, { address?: string; classes: ScheduleClassWithLocation[] }>())
)
: null;
return (
<div className="rounded-2xl border border-neutral-200 bg-white dark:border-white/[0.06] dark:bg-[#0a0a0a] overflow-hidden">
{/* Day header */}
<div className="border-b border-neutral-100 bg-neutral-50 px-5 py-4 dark:border-white/[0.04] dark:bg-white/[0.02]">
<div className="flex items-center gap-3">
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-gold/10 text-sm font-bold text-gold-dark dark:bg-gold/10 dark:text-gold-light">
{day.dayShort}
</span>
<span className="text-base font-semibold text-neutral-900 dark:text-white/90">
{day.day}
</span>
</div>
</div>
{/* Classes */}
{locationGroups ? (
// Split by location
<div>
{locationGroups.map(([locName, { address, classes }], gi) => (
<div key={locName}>
{/* Location sub-header */}
<div className={`flex items-center gap-1.5 px-5 py-2 bg-neutral-100/60 dark:bg-white/[0.03] ${gi > 0 ? "border-t border-neutral-200 dark:border-white/[0.06]" : ""}`}>
<MapPin size={11} className="shrink-0 text-neutral-400 dark:text-white/25" />
<span className="text-[11px] font-medium text-neutral-400 dark:text-white/30">
{locName}
{address && <span className="text-neutral-300 dark:text-white/15"> · {shortAddress(address)}</span>}
</span>
</div>
<div className="divide-y divide-neutral-100 dark:divide-white/[0.04]">
{classes.map((cls, i) => (
<ClassRow key={i} cls={cls} typeDots={typeDots} filterTrainer={filterTrainer} setFilterTrainer={setFilterTrainer} filterType={filterType} setFilterType={setFilterType} />
))}
</div>
</div>
))}
</div>
) : (
// Single location — no sub-headers
<div className="divide-y divide-neutral-100 dark:divide-white/[0.04]">
{day.classes.map((cls, i) => (
<ClassRow key={i} cls={cls} typeDots={typeDots} filterTrainer={filterTrainer} setFilterTrainer={setFilterTrainer} filterType={filterType} setFilterType={setFilterType} />
))}
</div>
)}
</div>
);
}