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:
@@ -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" && (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user