From 46ad10e8a01a3aa878953ec80340c8fb897772e7 Mon Sep 17 00:00:00 2001 From: "diana.dolgolyova" Date: Thu, 12 Mar 2026 21:25:11 +0300 Subject: [PATCH] feat: upgrade schedule with cross-location views, day/time filters, and clickable trainers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/app/admin/classes/page.tsx | 144 +++++++-- src/components/sections/Classes.tsx | 23 +- src/components/sections/Schedule.tsx | 282 ++++++++++++++---- src/components/sections/schedule/DayCard.tsx | 157 +++++++--- .../sections/schedule/GroupView.tsx | 186 ++++++++++++ .../sections/schedule/MobileSchedule.tsx | 204 +++++++++---- .../sections/schedule/ScheduleFilters.tsx | 79 +++-- src/components/sections/schedule/constants.ts | 38 +++ 8 files changed, 891 insertions(+), 222 deletions(-) create mode 100644 src/components/sections/schedule/GroupView.tsx diff --git a/src/app/admin/classes/page.tsx b/src/app/admin/classes/page.tsx index 326bbec..f32c29f 100644 --- a/src/app/admin/classes/page.tsx +++ b/src/app/admin/classes/page.tsx @@ -1,13 +1,127 @@ "use client"; +import { useState, useRef, useEffect, useMemo } from "react"; import { SectionEditor } from "../_components/SectionEditor"; import { InputField, TextareaField } from "../_components/FormField"; import { ArrayEditor } from "../_components/ArrayEditor"; +import { icons, type LucideIcon } from "lucide-react"; -const ICON_OPTIONS = [ - "sparkles", "flame", "wind", "zap", "star", "monitor", - "heart", "music", "dumbbell", "trophy", -]; +// PascalCase "HeartPulse" → kebab "heart-pulse" +function toKebab(name: string) { + return name.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(); +} + +// All icons as { key: kebab-name, Icon: component, label: PascalCase } +const ALL_ICONS = Object.entries(icons).map(([name, Icon]) => ({ + key: toKebab(name), + Icon: Icon as LucideIcon, + label: name, +})); + +const ICON_BY_KEY = Object.fromEntries(ALL_ICONS.map((i) => [i.key, i])); + +function IconPicker({ + value, + onChange, +}: { + value: string; + onChange: (v: string) => void; +}) { + const [open, setOpen] = useState(false); + const [search, setSearch] = useState(""); + const ref = useRef(null); + const inputRef = useRef(null); + const selected = ICON_BY_KEY[value]; + + useEffect(() => { + if (!open) return; + function handle(e: MouseEvent) { + if (ref.current && !ref.current.contains(e.target as Node)) { + setOpen(false); + setSearch(""); + } + } + document.addEventListener("mousedown", handle); + return () => document.removeEventListener("mousedown", handle); + }, [open]); + + const filtered = useMemo(() => { + if (!search) return ALL_ICONS.slice(0, 60); + const q = search.toLowerCase(); + return ALL_ICONS.filter((i) => i.label.toLowerCase().includes(q)).slice(0, 60); + }, [search]); + + const SelectedIcon = selected?.Icon; + + return ( +
+ + + + {open && ( +
+
+ setSearch(e.target.value)} + placeholder="Поиск иконки... (flame, heart, star...)" + className="w-full rounded-md border border-white/10 bg-neutral-900 px-3 py-1.5 text-sm text-white outline-none focus:border-gold/50 placeholder:text-neutral-600" + /> +
+
+ {filtered.length === 0 ? ( +
Ничего не найдено
+ ) : ( +
+ {filtered.map(({ key, Icon, label }) => ( + + ))} +
+ )} +
+
+ )} +
+ ); +} const COLOR_SWATCHES: { value: string; bg: string }[] = [ { value: "rose", bg: "bg-rose-500" }, @@ -63,24 +177,10 @@ export default function ClassesEditorPage() { value={item.name} onChange={(v) => updateItem({ ...item, name: v })} /> -
- - -
+ updateItem({ ...item, icon: v })} + />