feat: collapse/expand all — toggle icon in ArrayEditor + pricing sections

- ArrayEditor shows ChevronsUpDown toggle when collapsible with 2+ items
- Toggle appears even without label prop (fixes pricing missing icon)
- Pricing: centralized section state with toggle-all button for all 3 sections
This commit is contained in:
2026-03-26 11:31:31 +03:00
parent 4c8c6eb0d2
commit 09b2f40090
2 changed files with 74 additions and 33 deletions

View File

@@ -2,7 +2,7 @@
import { useState, useRef, useCallback, useEffect } from "react";
import { createPortal } from "react-dom";
import { Plus, Trash2, GripVertical, ChevronDown } from "lucide-react";
import { Plus, Trash2, GripVertical, ChevronDown, ChevronsUpDown } from "lucide-react";
interface ArrayEditorProps<T> {
items: T[];
@@ -293,8 +293,23 @@ export function ArrayEditor<T>({
return (
<div>
{label && (
<h3 className="text-sm font-medium text-neutral-300 mb-3">{label}</h3>
{(label || (collapsible && items.length > 1)) && (
<div className="flex items-center justify-between mb-3">
{label ? <h3 className="text-sm font-medium text-neutral-300">{label}</h3> : <div />}
{collapsible && items.length > 1 && (() => {
const allCollapsed = collapsed.size >= items.length;
return (
<button
type="button"
onClick={() => allCollapsed ? setCollapsed(new Set()) : setCollapsed(new Set(items.map((_, i) => i)))}
className="rounded p-1 text-neutral-500 hover:text-white transition-colors"
title={allCollapsed ? "Развернуть все" : "Свернуть все"}
>
<ChevronsUpDown size={16} className={`transition-transform duration-200 ${allCollapsed ? "" : "rotate-90"}`} />
</button>
);
})()}
</div>
)}
{addPosition === "top" && (

View File

@@ -1,7 +1,7 @@
"use client";
import { useState } from "react";
import { ChevronDown } from "lucide-react";
import { ChevronDown, ChevronsUpDown } from "lucide-react";
import { SectionEditor } from "../_components/SectionEditor";
import { InputField, SelectField } from "../_components/FormField";
import { ArrayEditor } from "../_components/ArrayEditor";
@@ -52,21 +52,21 @@ function PriceField({ label, value, onChange }: { label: string; value: string;
function CollapsibleSection({
title,
count,
defaultOpen = true,
isOpen,
onToggle,
children,
}: {
title: string;
count?: number;
defaultOpen?: boolean;
isOpen: boolean;
onToggle: () => void;
children: React.ReactNode;
}) {
const [open, setOpen] = useState(defaultOpen);
return (
<div className="rounded-xl border border-white/10 bg-neutral-900/30 overflow-hidden">
<button
type="button"
onClick={() => setOpen(!open)}
onClick={onToggle}
className="flex items-center justify-between w-full px-5 py-3.5 text-left cursor-pointer group hover:bg-white/[0.02] transition-colors"
>
<div className="flex items-center gap-2">
@@ -77,12 +77,12 @@ function CollapsibleSection({
</div>
<ChevronDown
size={16}
className={`text-neutral-500 transition-transform duration-200 ${open ? "rotate-180" : ""}`}
className={`text-neutral-500 transition-transform duration-200 ${isOpen ? "rotate-180" : ""}`}
/>
</button>
<div
className="grid transition-[grid-template-rows] duration-300 ease-out"
style={{ gridTemplateRows: open ? "1fr" : "0fr" }}
style={{ gridTemplateRows: isOpen ? "1fr" : "0fr" }}
>
<div className="overflow-hidden">
<div className="px-5 pb-5 space-y-4">
@@ -94,12 +94,23 @@ function CollapsibleSection({
);
}
export default function PricingEditorPage() {
function PricingContent({ data, update }: { data: PricingData; update: (d: PricingData) => void }) {
const [sections, setSections] = useState({ subscriptions: true, rental: true, rules: false });
const allOpen = sections.subscriptions && sections.rental && sections.rules;
function toggleAll() {
const target = !allOpen;
setSections({ subscriptions: target, rental: target, rules: target });
}
function toggleSection(key: keyof typeof sections) {
setSections(prev => ({ ...prev, [key]: !prev[key] }));
}
return (
<SectionEditor<PricingData> sectionKey="pricing" title="Цены">
{(data, update) => (
<div className="space-y-4">
<div className="grid gap-3 sm:grid-cols-2">
<div className="flex items-center justify-between">
<div className="grid gap-3 sm:grid-cols-2 flex-1">
<InputField
label="Заголовок секции"
value={data.title}
@@ -111,9 +122,18 @@ export default function PricingEditorPage() {
onChange={(v) => update({ ...data, subtitle: v })}
/>
</div>
<button
type="button"
onClick={toggleAll}
className="rounded p-1 text-neutral-500 hover:text-white transition-colors ml-3 mt-4"
title={allOpen ? "Свернуть все секции" : "Развернуть все секции"}
>
<ChevronsUpDown size={16} className={`transition-transform duration-200 ${allOpen ? "rotate-90" : ""}`} />
</button>
</div>
{/* Абонементы */}
<CollapsibleSection title="Абонементы" count={data.items.length}>
<CollapsibleSection title="Абонементы" count={data.items.length} isOpen={sections.subscriptions} onToggle={() => toggleSection("subscriptions")}>
{(() => {
const itemOptions = data.items
.map((it, idx) => ({ value: String(idx), label: it.name }))
@@ -196,7 +216,7 @@ export default function PricingEditorPage() {
</CollapsibleSection>
{/* Аренда */}
<CollapsibleSection title="Аренда" count={data.rentalItems.length}>
<CollapsibleSection title="Аренда" count={data.rentalItems.length} isOpen={sections.rental} onToggle={() => toggleSection("rental")}>
<InputField
label="Заголовок"
value={data.rentalTitle}
@@ -236,7 +256,7 @@ export default function PricingEditorPage() {
</CollapsibleSection>
{/* Правила */}
<CollapsibleSection title="Правила" count={data.rules.length} defaultOpen={false}>
<CollapsibleSection title="Правила" count={data.rules.length} isOpen={sections.rules} onToggle={() => toggleSection("rules")}>
<ArrayEditor
items={data.rules}
onChange={(rules) => update({ ...data, rules })}
@@ -248,7 +268,13 @@ export default function PricingEditorPage() {
/>
</CollapsibleSection>
</div>
)}
);
}
export default function PricingEditorPage() {
return (
<SectionEditor<PricingData> sectionKey="pricing" title="Цены">
{(data, update) => <PricingContent data={data} update={update} />}
</SectionEditor>
);
}