feat: mobile-optimized schedule with compact agenda list view
Mobile shows all days as a vertical timeline with compact class rows (time + trainer + type on each line). Desktop keeps the card grid. Filters are horizontally scrollable on mobile. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -48,3 +48,14 @@ body {
|
|||||||
:focus-visible {
|
:focus-visible {
|
||||||
@apply outline-2 outline-offset-2 outline-[#c9a96e];
|
@apply outline-2 outline-offset-2 outline-[#c9a96e];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== Scrollbar hide utility ===== */
|
||||||
|
|
||||||
|
.scrollbar-hide {
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar-hide::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { MapPin, Clock, User, X, ChevronDown } from "lucide-react";
|
|||||||
import { siteContent } from "@/data/content";
|
import { siteContent } from "@/data/content";
|
||||||
import { SectionHeading } from "@/components/ui/SectionHeading";
|
import { SectionHeading } from "@/components/ui/SectionHeading";
|
||||||
import { Reveal } from "@/components/ui/Reveal";
|
import { Reveal } from "@/components/ui/Reveal";
|
||||||
|
import type { ScheduleDay } from "@/types/content";
|
||||||
|
|
||||||
const TYPE_DOT: Record<string, string> = {
|
const TYPE_DOT: Record<string, string> = {
|
||||||
"Exotic Pole Dance": "bg-[#c9a96e]",
|
"Exotic Pole Dance": "bg-[#c9a96e]",
|
||||||
@@ -15,6 +16,63 @@ const TYPE_DOT: Record<string, string> = {
|
|||||||
|
|
||||||
type StatusFilter = "all" | "hasSlots" | "recruiting";
|
type StatusFilter = "all" | "hasSlots" | "recruiting";
|
||||||
|
|
||||||
|
function DayCard({ day }: { day: ScheduleDay }) {
|
||||||
|
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-[#c9a96e]/10 text-sm font-bold text-[#a08050] dark:bg-[#c9a96e]/10 dark:text-[#d4b87a]">
|
||||||
|
{day.dayShort}
|
||||||
|
</span>
|
||||||
|
<span className="text-base font-semibold text-neutral-900 dark:text-white/90">
|
||||||
|
{day.day}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Classes */}
|
||||||
|
<div className="divide-y divide-neutral-100 dark:divide-white/[0.04]">
|
||||||
|
{day.classes.map((cls, i) => (
|
||||||
|
<div key={i} 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>
|
||||||
|
<div className="mt-1.5 flex items-center gap-2 text-sm font-medium text-neutral-800 dark:text-white/80">
|
||||||
|
<User size={13} className="shrink-0 text-neutral-400 dark:text-white/30" />
|
||||||
|
{cls.trainer}
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 flex items-center gap-2 flex-wrap">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className={`h-2 w-2 shrink-0 rounded-full ${TYPE_DOT[cls.type] ?? "bg-white/30"}`} />
|
||||||
|
<span className="text-xs text-neutral-500 dark:text-white/40">{cls.type}</span>
|
||||||
|
</div>
|
||||||
|
{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>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function Schedule() {
|
export function Schedule() {
|
||||||
const { schedule } = siteContent;
|
const { schedule } = siteContent;
|
||||||
const [locationIndex, setLocationIndex] = useState(0);
|
const [locationIndex, setLocationIndex] = useState(0);
|
||||||
@@ -71,7 +129,7 @@ export function Schedule() {
|
|||||||
setFilterStatus("all");
|
setFilterStatus("all");
|
||||||
}
|
}
|
||||||
|
|
||||||
const pillBase = "inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-[11px] font-medium transition-all duration-200 cursor-pointer";
|
const pillBase = "inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-[11px] font-medium transition-all duration-200 cursor-pointer whitespace-nowrap";
|
||||||
const pillActive = "bg-[#c9a96e]/20 text-[#a08050] border border-[#c9a96e]/40 dark:text-[#d4b87a] dark:border-[#c9a96e]/30";
|
const pillActive = "bg-[#c9a96e]/20 text-[#a08050] border border-[#c9a96e]/40 dark:text-[#d4b87a] dark:border-[#c9a96e]/30";
|
||||||
const pillInactive = "border border-neutral-200 text-neutral-500 hover:border-neutral-300 dark:border-white/[0.08] dark:text-white/35 dark:hover:border-white/15";
|
const pillInactive = "border border-neutral-200 text-neutral-500 hover:border-neutral-300 dark:border-white/[0.08] dark:text-white/35 dark:hover:border-white/15";
|
||||||
|
|
||||||
@@ -111,9 +169,9 @@ export function Schedule() {
|
|||||||
</div>
|
</div>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
|
|
||||||
{/* Compact filters */}
|
{/* Compact filters — scrollable on mobile */}
|
||||||
<Reveal>
|
<Reveal>
|
||||||
<div className="mt-5 flex flex-wrap items-center justify-center gap-1.5">
|
<div className="mt-5 flex items-center justify-center gap-1.5 overflow-x-auto pb-1 scrollbar-hide sm:flex-wrap sm:overflow-visible sm:pb-0">
|
||||||
{/* Class types */}
|
{/* Class types */}
|
||||||
{types.map((type) => (
|
{types.map((type) => (
|
||||||
<button
|
<button
|
||||||
@@ -121,13 +179,13 @@ export function Schedule() {
|
|||||||
onClick={() => setFilterType(filterType === type ? null : type)}
|
onClick={() => setFilterType(filterType === type ? null : type)}
|
||||||
className={`${pillBase} ${filterType === type ? pillActive : pillInactive}`}
|
className={`${pillBase} ${filterType === type ? pillActive : pillInactive}`}
|
||||||
>
|
>
|
||||||
<span className={`h-1.5 w-1.5 rounded-full ${TYPE_DOT[type] ?? "bg-white/30"}`} />
|
<span className={`h-1.5 w-1.5 shrink-0 rounded-full ${TYPE_DOT[type] ?? "bg-white/30"}`} />
|
||||||
{type}
|
{type}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Divider */}
|
{/* Divider */}
|
||||||
<span className="mx-1 h-4 w-px bg-neutral-200 dark:bg-white/10" />
|
<span className="mx-1 h-4 w-px shrink-0 bg-neutral-200 dark:bg-white/10" />
|
||||||
|
|
||||||
{/* Status filters */}
|
{/* Status filters */}
|
||||||
{hasAnySlots && (
|
{hasAnySlots && (
|
||||||
@@ -135,7 +193,7 @@ export function Schedule() {
|
|||||||
onClick={() => setFilterStatus(filterStatus === "hasSlots" ? "all" : "hasSlots")}
|
onClick={() => setFilterStatus(filterStatus === "hasSlots" ? "all" : "hasSlots")}
|
||||||
className={`${pillBase} ${filterStatus === "hasSlots" ? "bg-emerald-500/20 text-emerald-700 border border-emerald-500/40 dark:text-emerald-400 dark:border-emerald-500/30" : pillInactive}`}
|
className={`${pillBase} ${filterStatus === "hasSlots" ? "bg-emerald-500/20 text-emerald-700 border border-emerald-500/40 dark:text-emerald-400 dark:border-emerald-500/30" : pillInactive}`}
|
||||||
>
|
>
|
||||||
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500" />
|
<span className="h-1.5 w-1.5 shrink-0 rounded-full bg-emerald-500" />
|
||||||
Есть места
|
Есть места
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@@ -144,13 +202,13 @@ export function Schedule() {
|
|||||||
onClick={() => setFilterStatus(filterStatus === "recruiting" ? "all" : "recruiting")}
|
onClick={() => setFilterStatus(filterStatus === "recruiting" ? "all" : "recruiting")}
|
||||||
className={`${pillBase} ${filterStatus === "recruiting" ? "bg-sky-500/20 text-sky-700 border border-sky-500/40 dark:text-sky-400 dark:border-sky-500/30" : pillInactive}`}
|
className={`${pillBase} ${filterStatus === "recruiting" ? "bg-sky-500/20 text-sky-700 border border-sky-500/40 dark:text-sky-400 dark:border-sky-500/30" : pillInactive}`}
|
||||||
>
|
>
|
||||||
<span className="h-1.5 w-1.5 rounded-full bg-sky-500" />
|
<span className="h-1.5 w-1.5 shrink-0 rounded-full bg-sky-500" />
|
||||||
Набор
|
Набор
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Divider */}
|
{/* Divider */}
|
||||||
<span className="mx-1 h-4 w-px bg-neutral-200 dark:bg-white/10" />
|
<span className="mx-1 h-4 w-px shrink-0 bg-neutral-200 dark:bg-white/10" />
|
||||||
|
|
||||||
{/* Trainer dropdown toggle */}
|
{/* Trainer dropdown toggle */}
|
||||||
<button
|
<button
|
||||||
@@ -166,7 +224,7 @@ export function Schedule() {
|
|||||||
{hasActiveFilter && (
|
{hasActiveFilter && (
|
||||||
<button
|
<button
|
||||||
onClick={clearFilters}
|
onClick={clearFilters}
|
||||||
className="inline-flex items-center gap-1 rounded-full px-2.5 py-1 text-[11px] text-neutral-400 hover:text-neutral-600 dark:text-white/25 dark:hover:text-white/50 transition-colors cursor-pointer"
|
className="inline-flex shrink-0 items-center gap-1 rounded-full px-2.5 py-1 text-[11px] text-neutral-400 hover:text-neutral-600 dark:text-white/25 dark:hover:text-white/50 transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
<X size={11} />
|
<X size={11} />
|
||||||
</button>
|
</button>
|
||||||
@@ -193,70 +251,91 @@ export function Schedule() {
|
|||||||
</Reveal>
|
</Reveal>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Day columns — full width */}
|
{/* Mobile: compact agenda list */}
|
||||||
<Reveal>
|
<Reveal>
|
||||||
<div
|
<div className="mt-6 px-4 sm:hidden">
|
||||||
key={`${locationIndex}-${filterTrainer}-${filterType}-${filterStatus}`}
|
{filteredDays.length > 0 ? (
|
||||||
className={`mt-8 grid grid-cols-1 gap-3 px-4 sm:grid-cols-2 sm:px-6 lg:grid-cols-3 lg:px-8 xl:px-6 ${filteredDays.length >= 7 ? "xl:grid-cols-7" : filteredDays.length >= 4 ? "xl:grid-cols-" + filteredDays.length : "xl:grid-cols-4"}`}
|
<div className="space-y-1">
|
||||||
style={filteredDays.length < 4 && filteredDays.length > 0 ? { maxWidth: filteredDays.length * 320 + (filteredDays.length - 1) * 12, marginInline: "auto" } : undefined}
|
|
||||||
>
|
|
||||||
{filteredDays.map((day) => (
|
{filteredDays.map((day) => (
|
||||||
<div
|
<div key={day.day}>
|
||||||
key={day.day}
|
|
||||||
className="rounded-2xl border border-neutral-200 bg-white dark:border-white/[0.06] dark:bg-[#0a0a0a] overflow-hidden"
|
|
||||||
>
|
|
||||||
{/* Day header */}
|
{/* 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-2.5 py-2.5">
|
||||||
<div className="flex items-center gap-3">
|
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-[#c9a96e]/10 text-xs font-bold text-[#a08050] dark:bg-[#c9a96e]/10 dark:text-[#d4b87a]">
|
||||||
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-[#c9a96e]/10 text-sm font-bold text-[#a08050] dark:bg-[#c9a96e]/10 dark:text-[#d4b87a]">
|
|
||||||
{day.dayShort}
|
{day.dayShort}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-base font-semibold text-neutral-900 dark:text-white/90">
|
<span className="text-sm font-semibold text-neutral-900 dark:text-white/90">
|
||||||
{day.day}
|
{day.day}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Classes */}
|
{/* Class rows */}
|
||||||
<div className="divide-y divide-neutral-100 dark:divide-white/[0.04]">
|
<div className="ml-1 border-l-2 border-neutral-200 dark:border-white/[0.08]">
|
||||||
{day.classes.map((cls, i) => (
|
{day.classes.map((cls, i) => (
|
||||||
<div key={i} className={`px-5 py-3.5 ${cls.hasSlots ? "bg-emerald-500/5" : cls.recruiting ? "bg-sky-500/5" : ""}`}>
|
<div
|
||||||
<div className="flex items-center justify-between gap-2">
|
key={i}
|
||||||
<div className="flex items-center gap-2 text-sm text-neutral-500 dark:text-white/40">
|
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" : ""}`}
|
||||||
<Clock size={13} />
|
>
|
||||||
<span className="font-semibold">{cls.time}</span>
|
{/* Time */}
|
||||||
</div>
|
<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 */}
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span className="truncate text-sm font-medium text-neutral-800 dark:text-white/80">
|
||||||
|
{cls.trainer}
|
||||||
|
</span>
|
||||||
{cls.hasSlots && (
|
{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 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>
|
</span>
|
||||||
)}
|
)}
|
||||||
{cls.recruiting && (
|
{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 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>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
<div className="mt-1.5 flex items-center gap-2 text-sm font-medium text-neutral-800 dark:text-white/80">
|
|
||||||
<User size={13} className="shrink-0 text-neutral-400 dark:text-white/30" />
|
|
||||||
{cls.trainer}
|
|
||||||
</div>
|
|
||||||
<div className="mt-2 flex items-center gap-2 flex-wrap">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className={`h-2 w-2 shrink-0 rounded-full ${TYPE_DOT[cls.type] ?? "bg-white/30"}`} />
|
|
||||||
<span className="text-xs text-neutral-500 dark:text-white/40">{cls.type}</span>
|
|
||||||
</div>
|
|
||||||
{cls.level && (
|
{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">
|
<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}
|
{cls.level}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mt-0.5 flex items-center gap-1.5">
|
||||||
|
<span className={`h-1.5 w-1.5 shrink-0 rounded-full ${TYPE_DOT[cls.type] ?? "bg-white/30"}`} />
|
||||||
|
<span className="text-[11px] text-neutral-400 dark:text-white/30">{cls.type}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="py-12 text-center text-sm text-neutral-400 dark:text-white/30">
|
||||||
|
Нет занятий по выбранным фильтрам
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
|
||||||
|
{/* Desktop: grid layout */}
|
||||||
|
<Reveal>
|
||||||
|
<div
|
||||||
|
key={`${locationIndex}-${filterTrainer}-${filterType}-${filterStatus}`}
|
||||||
|
className={`mt-8 hidden sm:grid grid-cols-1 gap-3 px-4 sm:px-6 lg:px-8 xl:px-6 ${filteredDays.length >= 7 ? "sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-7" : filteredDays.length >= 4 ? "sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-" + filteredDays.length : filteredDays.length === 3 ? "sm:grid-cols-2 lg:grid-cols-3" : filteredDays.length === 2 ? "sm:grid-cols-2" : "justify-items-center"}`}
|
||||||
|
style={filteredDays.length === 1 ? undefined : filteredDays.length <= 3 && filteredDays.length > 0 ? { maxWidth: filteredDays.length * 340 + (filteredDays.length - 1) * 12, marginInline: "auto" } : undefined}
|
||||||
|
>
|
||||||
|
{filteredDays.map((day) => (
|
||||||
|
<div
|
||||||
|
key={day.day}
|
||||||
|
className={filteredDays.length === 1 ? "w-full max-w-[340px]" : ""}
|
||||||
|
>
|
||||||
|
<DayCard day={day} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
{filteredDays.length === 0 && (
|
{filteredDays.length === 0 && (
|
||||||
<div className="col-span-full py-12 text-center text-sm text-neutral-400 dark:text-white/30">
|
<div className="col-span-full py-12 text-center text-sm text-neutral-400 dark:text-white/30">
|
||||||
|
|||||||
Reference in New Issue
Block a user