feat: classes admin collapsible cards, icon curation, color fix + user-side polish
Admin classes: - Collapsible cards in ArrayEditor (start collapsed, expand on click) - Curated 29 dance-relevant icons shown by default, full search as fallback - Color swatches: used colors dimmed instead of hidden (no layout shift) User side: - Classes: icon + name side by side on photo overlay - ShowcaseLayout: fix image flash during transition (2-frame swap while hidden) - Team bio: section headings gold, admin cards focus-within highlight
This commit is contained in:
@@ -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<T> {
|
||||
items: T[];
|
||||
@@ -11,6 +11,8 @@ interface ArrayEditorProps<T> {
|
||||
createItem: () => T;
|
||||
label?: string;
|
||||
addLabel?: string;
|
||||
collapsible?: boolean;
|
||||
getItemTitle?: (item: T, index: number) => string;
|
||||
}
|
||||
|
||||
export function ArrayEditor<T>({
|
||||
@@ -20,6 +22,8 @@ export function ArrayEditor<T>({
|
||||
createItem,
|
||||
label,
|
||||
addLabel = "Добавить",
|
||||
collapsible = false,
|
||||
getItemTitle,
|
||||
}: ArrayEditorProps<T>) {
|
||||
const [dragIndex, setDragIndex] = useState<number | null>(null);
|
||||
const [insertAt, setInsertAt] = useState<number | null>(null);
|
||||
@@ -29,6 +33,16 @@ export function ArrayEditor<T>({
|
||||
const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [newItemIndex, setNewItemIndex] = useState<number | null>(null);
|
||||
const [collapsed, setCollapsed] = useState<Set<number>>(() => 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<T>({
|
||||
|
||||
function renderList() {
|
||||
if (dragIndex === null || insertAt === null) {
|
||||
return items.map((item, i) => (
|
||||
<div
|
||||
key={i}
|
||||
ref={(el) => { 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"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2 mb-3">
|
||||
<div
|
||||
className="cursor-grab active:cursor-grabbing rounded p-1 text-neutral-500 hover:text-white transition-colors select-none"
|
||||
onMouseDown={(e) => handleGripMouseDown(e, i)}
|
||||
>
|
||||
<GripVertical size={16} />
|
||||
return items.map((item, i) => {
|
||||
const isCollapsed = collapsible && collapsed.has(i) && newItemIndex !== i;
|
||||
const title = getItemTitle?.(item, i) || `#${i + 1}`;
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
ref={(el) => { 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"
|
||||
}`}
|
||||
>
|
||||
<div className={`flex items-center justify-between gap-2 p-4 ${isCollapsed ? "" : "pb-0 mb-3"}`}>
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<div
|
||||
className="cursor-grab active:cursor-grabbing rounded p-1 text-neutral-500 hover:text-white transition-colors select-none"
|
||||
onMouseDown={(e) => handleGripMouseDown(e, i)}
|
||||
>
|
||||
<GripVertical size={16} />
|
||||
</div>
|
||||
{collapsible && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleCollapse(i)}
|
||||
className="flex items-center gap-2 flex-1 min-w-0 text-left cursor-pointer group"
|
||||
>
|
||||
<span className="text-sm font-medium text-neutral-300 truncate group-hover:text-white transition-colors">{title}</span>
|
||||
<ChevronDown size={14} className={`text-neutral-500 transition-transform duration-200 shrink-0 ${isCollapsed ? "" : "rotate-180"}`} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeItem(i)}
|
||||
className="rounded p-1 text-neutral-500 hover:text-red-400 transition-colors shrink-0"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeItem(i)}
|
||||
className="rounded p-1 text-neutral-500 hover:text-red-400 transition-colors"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
{collapsible ? (
|
||||
<div
|
||||
className="grid transition-[grid-template-rows] duration-300 ease-out"
|
||||
style={{ gridTemplateRows: isCollapsed ? "0fr" : "1fr" }}
|
||||
>
|
||||
<div className="overflow-hidden">
|
||||
<div className="px-4 pb-4">
|
||||
{renderItem(item, i, (updated) => updateItem(i, updated))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="px-4 pb-4">
|
||||
{renderItem(item, i, (updated) => updateItem(i, updated))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{renderItem(item, i, (updated) => updateItem(i, updated))}
|
||||
</div>
|
||||
));
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const elements: React.ReactNode[] = [];
|
||||
|
||||
Reference in New Issue
Block a user