213 lines
8.1 KiB
TypeScript
213 lines
8.1 KiB
TypeScript
"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>
|
||
);
|
||
}
|