Files
blackheart-website/src/components/sections/schedule/MobileSchedule.tsx

213 lines
8.1 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.
"use client";
import { User, X, MapPin } from "lucide-react";
import { shortAddress } from "./constants";
import type { ScheduleDayMerged, ScheduleClassWithLocation } from "./constants";
interface MobileScheduleProps {
typeDots: Record<string, string>;
filteredDays: ScheduleDayMerged[];
filterTypes: Set<string>;
toggleFilterType: (type: string) => void;
filterTrainer: string | null;
setFilterTrainer: (trainer: string | null) => void;
hasActiveFilter: boolean;
clearFilters: () => void;
showLocation?: boolean;
}
function ClassRow({
cls,
typeDots,
filterTypes,
toggleFilterType,
filterTrainer,
setFilterTrainer,
showLocation,
}: {
cls: ScheduleClassWithLocation;
typeDots: Record<string, string>;
filterTypes: Set<string>;
toggleFilterType: (type: string) => void;
filterTrainer: string | null;
setFilterTrainer: (trainer: string | null) => void;
showLocation?: boolean;
}) {
return (
<div
className={`ml-3 flex items-start gap-3 rounded-lg px-3 py-2 ${cls.hasSlots ? "bg-emerald-500/5" : cls.recruiting ? "bg-sky-500/5" : ""}`}
>
{/* Time */}
<span className="shrink-0 w-[72px] text-xs font-semibold tabular-nums text-neutral-500 dark:text-white/40 pt-0.5">
{cls.time}
</span>
{/* Info — tappable trainer & type */}
<div className="min-w-0 flex-1">
<div className="flex items-center gap-1.5">
<button
onClick={() => setFilterTrainer(filterTrainer === cls.trainer ? null : cls.trainer)}
className="truncate text-sm font-medium text-left active:opacity-60 text-neutral-800 dark:text-white/80"
>
{cls.trainer}
</button>
{cls.hasSlots && (
<span className="shrink-0 rounded-full bg-emerald-500/15 border border-emerald-500/25 px-1.5 py-px text-[9px] 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-1.5 py-px text-[9px] font-semibold text-sky-600 dark:text-sky-400">
набор
</span>
)}
{cls.level && (
<span className="shrink-0 rounded-full bg-rose-500/15 border border-rose-500/25 px-1.5 py-px text-[9px] font-semibold text-rose-600 dark:text-rose-400">
{cls.level}
</span>
)}
</div>
<div className="mt-0.5 flex items-center gap-2">
<button
onClick={() => toggleFilterType(cls.type)}
className={`flex items-center gap-1.5 active:opacity-60 ${filterTypes.has(cls.type) ? "opacity-100" : ""}`}
>
<span className={`h-1.5 w-1.5 shrink-0 rounded-full ${typeDots[cls.type] ?? "bg-white/30"}`} />
<span className="text-[11px] text-neutral-400 dark:text-white/30">{cls.type}</span>
</button>
{showLocation && cls.locationName && (
<span className="flex items-center gap-0.5 text-[10px] text-neutral-400 dark:text-white/20">
<MapPin size={8} className="shrink-0" />
{cls.locationName}
</span>
)}
</div>
</div>
</div>
);
}
export function MobileSchedule({
typeDots,
filteredDays,
filterTypes,
toggleFilterType,
filterTrainer,
setFilterTrainer,
hasActiveFilter,
clearFilters,
showLocation,
}: MobileScheduleProps) {
return (
<div className="mt-6 px-4 sm:hidden">
{/* Active filter indicator */}
{hasActiveFilter && (
<div className="mb-3 flex items-center justify-between rounded-xl bg-gold/10 px-4 py-2.5 dark:bg-gold/5">
<div className="flex items-center gap-2 text-xs font-medium text-gold-dark dark:text-gold-light">
{filterTrainer && (
<span className="flex items-center gap-1">
<User size={11} />
{filterTrainer}
</span>
)}
{filterTypes.size > 0 && Array.from(filterTypes).map((type) => (
<span key={type} className="flex items-center gap-1">
<span className={`h-1.5 w-1.5 rounded-full ${typeDots[type] ?? "bg-white/30"}`} />
{type}
</span>
))}
</div>
<button
onClick={clearFilters}
className="flex items-center gap-1 rounded-full px-2 py-1 text-[11px] text-gold-dark dark:text-gold-light active:bg-gold/20"
>
<X size={12} />
Сбросить
</button>
</div>
)}
{filteredDays.length > 0 ? (
<div className="space-y-1">
{filteredDays.map((day) => {
// 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 key={day.day}>
{/* Day header */}
<div className="flex items-center gap-2.5 py-2.5">
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-gold/10 text-xs font-bold text-gold-dark dark:bg-gold/10 dark:text-gold-light">
{day.dayShort}
</span>
<span className="text-sm font-semibold text-neutral-900 dark:text-white/90">
{day.day}
</span>
</div>
{/* Class rows */}
<div className="ml-1 border-l-2 border-neutral-200 dark:border-white/[0.08]">
{locationGroups ? (
// Split by location
locationGroups.map(([locName, { address, classes }]) => (
<div key={locName}>
{/* Location sub-header */}
<div className="ml-3 flex items-center gap-1 px-3 py-1.5">
<MapPin size={9} className="shrink-0 text-neutral-400 dark:text-white/20" />
<span className="text-[10px] font-medium text-neutral-400 dark:text-white/25">
{locName}
{address && <span className="text-neutral-300 dark:text-white/15"> · {shortAddress(address)}</span>}
</span>
</div>
{classes.map((cls, i) => (
<ClassRow
key={i}
cls={cls}
typeDots={typeDots}
filterTypes={filterTypes}
toggleFilterType={toggleFilterType}
filterTrainer={filterTrainer}
setFilterTrainer={setFilterTrainer}
/>
))}
</div>
))
) : (
// Single location — no sub-headers
day.classes.map((cls, i) => (
<ClassRow
key={i}
cls={cls}
typeDots={typeDots}
filterTypes={filterTypes}
toggleFilterType={toggleFilterType}
filterTrainer={filterTrainer}
setFilterTrainer={setFilterTrainer}
/>
))
)}
</div>
</div>
);
})}
</div>
) : (
<div className="py-12 text-center text-sm text-neutral-400 dark:text-white/30">
Нет занятий по выбранным фильтрам
</div>
)}
</div>
);
}