diff --git a/src/app/admin/_components/ArrayEditor.tsx b/src/app/admin/_components/ArrayEditor.tsx index 996f705..102731d 100644 --- a/src/app/admin/_components/ArrayEditor.tsx +++ b/src/app/admin/_components/ArrayEditor.tsx @@ -2,7 +2,7 @@ import { useState, useRef, useCallback, useEffect } from "react"; import { createPortal } from "react-dom"; -import { Plus, Trash2, GripVertical } from "lucide-react"; +import { Plus, Trash2, GripVertical, ChevronDown } from "lucide-react"; interface ArrayEditorProps { items: T[]; @@ -11,6 +11,8 @@ interface ArrayEditorProps { createItem: () => T; label?: string; addLabel?: string; + collapsible?: boolean; + getItemTitle?: (item: T, index: number) => string; } export function ArrayEditor({ @@ -20,6 +22,8 @@ export function ArrayEditor({ createItem, label, addLabel = "Добавить", + collapsible = false, + getItemTitle, }: ArrayEditorProps) { const [dragIndex, setDragIndex] = useState(null); const [insertAt, setInsertAt] = useState(null); @@ -29,6 +33,16 @@ export function ArrayEditor({ const itemRefs = useRef<(HTMLDivElement | null)[]>([]); const [mounted, setMounted] = useState(false); const [newItemIndex, setNewItemIndex] = useState(null); + const [collapsed, setCollapsed] = useState>(() => collapsible ? new Set(items.map((_, i) => i)) : new Set()); + + function toggleCollapse(index: number) { + setCollapsed(prev => { + const next = new Set(prev); + if (next.has(index)) next.delete(index); + else next.add(index); + return next; + }); + } useEffect(() => { setMounted(true); }, []); @@ -130,32 +144,63 @@ export function ArrayEditor({ function renderList() { if (dragIndex === null || insertAt === null) { - return items.map((item, i) => ( -
{ itemRefs.current[i] = el; }} - className={`rounded-lg border bg-neutral-900/50 p-4 mb-3 hover:border-white/25 hover:bg-neutral-800/50 transition-all ${ - newItemIndex === i ? "border-gold/40 ring-1 ring-gold/20" : "border-white/10" - }`} - > -
-
handleGripMouseDown(e, i)} - > - + return items.map((item, i) => { + const isCollapsed = collapsible && collapsed.has(i) && newItemIndex !== i; + const title = getItemTitle?.(item, i) || `#${i + 1}`; + return ( +
{ itemRefs.current[i] = el; }} + className={`rounded-lg border bg-neutral-900/50 mb-3 hover:border-white/25 hover:bg-neutral-800/50 transition-all ${ + newItemIndex === i ? "border-gold/40 ring-1 ring-gold/20" : "border-white/10" + }`} + > +
+
+
handleGripMouseDown(e, i)} + > + +
+ {collapsible && ( + + )} +
+
- + {collapsible ? ( +
+
+
+ {renderItem(item, i, (updated) => updateItem(i, updated))} +
+
+
+ ) : ( +
+ {renderItem(item, i, (updated) => updateItem(i, updated))} +
+ )}
- {renderItem(item, i, (updated) => updateItem(i, updated))} -
- )); + ); + }); } const elements: React.ReactNode[] = []; diff --git a/src/app/admin/classes/page.tsx b/src/app/admin/classes/page.tsx index f32c29f..0e0db8f 100644 --- a/src/app/admin/classes/page.tsx +++ b/src/app/admin/classes/page.tsx @@ -4,21 +4,63 @@ 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"; +import { + icons, type LucideIcon, + Flame, Heart, HeartPulse, Star, Sparkles, Music, Zap, Crown, + Dumbbell, Wind, Moon, Sun, Ribbon, Gem, Feather, CircleDot, + Activity, Drama, PersonStanding, Footprints, PartyPopper, Flower2, + Waves, Eye, Orbit, Brush, Palette, HandMetal, Theater, +} from "lucide-react"; + +// Curated icons for dance school +const CURATED_ICONS: { key: string; Icon: LucideIcon; label: string }[] = [ + { key: "flame", Icon: Flame, label: "Flame" }, + { key: "heart", Icon: Heart, label: "Heart" }, + { key: "heart-pulse", Icon: HeartPulse, label: "HeartPulse" }, + { key: "star", Icon: Star, label: "Star" }, + { key: "sparkles", Icon: Sparkles, label: "Sparkles" }, + { key: "music", Icon: Music, label: "Music" }, + { key: "zap", Icon: Zap, label: "Zap" }, + { key: "crown", Icon: Crown, label: "Crown" }, + { key: "dumbbell", Icon: Dumbbell, label: "Dumbbell" }, + { key: "wind", Icon: Wind, label: "Wind" }, + { key: "moon", Icon: Moon, label: "Moon" }, + { key: "sun", Icon: Sun, label: "Sun" }, + { key: "ribbon", Icon: Ribbon, label: "Ribbon" }, + { key: "gem", Icon: Gem, label: "Gem" }, + { key: "feather", Icon: Feather, label: "Feather" }, + { key: "circle-dot", Icon: CircleDot, label: "CircleDot" }, + { key: "activity", Icon: Activity, label: "Activity" }, + { key: "drama", Icon: Drama, label: "Drama" }, + { key: "person-standing", Icon: PersonStanding, label: "PersonStanding" }, + { key: "footprints", Icon: Footprints, label: "Footprints" }, + { key: "party-popper", Icon: PartyPopper, label: "PartyPopper" }, + { key: "flower-2", Icon: Flower2, label: "Flower" }, + { key: "waves", Icon: Waves, label: "Waves" }, + { key: "eye", Icon: Eye, label: "Eye" }, + { key: "orbit", Icon: Orbit, label: "Orbit" }, + { key: "brush", Icon: Brush, label: "Brush" }, + { key: "palette", Icon: Palette, label: "Palette" }, + { key: "hand-metal", Icon: HandMetal, label: "HandMetal" }, + { key: "theater", Icon: Theater, label: "Theater" }, +]; // 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 } +// Full icon list for search fallback 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])); +const ICON_BY_KEY = Object.fromEntries([ + ...CURATED_ICONS.map((i) => [i.key, i]), + ...ALL_ICONS.map((i) => [i.key, i]), +]); function IconPicker({ value, @@ -46,9 +88,12 @@ function IconPicker({ }, [open]); const filtered = useMemo(() => { - if (!search) return ALL_ICONS.slice(0, 60); + if (!search) return CURATED_ICONS; const q = search.toLowerCase(); - return ALL_ICONS.filter((i) => i.label.toLowerCase().includes(q)).slice(0, 60); + // Search curated first, then all icons + const curated = CURATED_ICONS.filter((i) => i.label.toLowerCase().includes(q)); + const rest = ALL_ICONS.filter((i) => i.label.toLowerCase().includes(q) && !curated.some((c) => c.key === i.key)); + return [...curated, ...rest].slice(0, 40); }, [search]); const SelectedIcon = selected?.Icon; @@ -85,7 +130,7 @@ function IconPicker({ type="text" value={search} onChange={(e) => setSearch(e.target.value)} - placeholder="Поиск иконки... (flame, heart, star...)" + placeholder="Поиск..." 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" />
@@ -188,18 +233,21 @@ export default function ClassesEditorPage() {
{COLOR_SWATCHES.map((c) => { - const isUsed = data.items.some( + const isSelected = item.color === c.value; + const isUsed = !isSelected && data.items.some( (other) => other !== item && other.color === c.value ); - if (isUsed) return null; return (