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 { useState, useRef, useCallback, useEffect } from "react";
|
||||||
import { createPortal } from "react-dom";
|
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> {
|
interface ArrayEditorProps<T> {
|
||||||
items: T[];
|
items: T[];
|
||||||
@@ -293,8 +293,23 @@ export function ArrayEditor<T>({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{label && (
|
{(label || (collapsible && items.length > 1)) && (
|
||||||
<h3 className="text-sm font-medium text-neutral-300 mb-3">{label}</h3>
|
<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" && (
|
{addPosition === "top" && (
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { ChevronDown } from "lucide-react";
|
import { ChevronDown, ChevronsUpDown } from "lucide-react";
|
||||||
import { SectionEditor } from "../_components/SectionEditor";
|
import { SectionEditor } from "../_components/SectionEditor";
|
||||||
import { InputField, SelectField } from "../_components/FormField";
|
import { InputField, SelectField } from "../_components/FormField";
|
||||||
import { ArrayEditor } from "../_components/ArrayEditor";
|
import { ArrayEditor } from "../_components/ArrayEditor";
|
||||||
@@ -52,21 +52,21 @@ function PriceField({ label, value, onChange }: { label: string; value: string;
|
|||||||
function CollapsibleSection({
|
function CollapsibleSection({
|
||||||
title,
|
title,
|
||||||
count,
|
count,
|
||||||
defaultOpen = true,
|
isOpen,
|
||||||
|
onToggle,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
title: string;
|
title: string;
|
||||||
count?: number;
|
count?: number;
|
||||||
defaultOpen?: boolean;
|
isOpen: boolean;
|
||||||
|
onToggle: () => void;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const [open, setOpen] = useState(defaultOpen);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl border border-white/10 bg-neutral-900/30 overflow-hidden">
|
<div className="rounded-xl border border-white/10 bg-neutral-900/30 overflow-hidden">
|
||||||
<button
|
<button
|
||||||
type="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"
|
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">
|
<div className="flex items-center gap-2">
|
||||||
@@ -77,12 +77,12 @@ function CollapsibleSection({
|
|||||||
</div>
|
</div>
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
size={16}
|
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>
|
</button>
|
||||||
<div
|
<div
|
||||||
className="grid transition-[grid-template-rows] duration-300 ease-out"
|
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="overflow-hidden">
|
||||||
<div className="px-5 pb-5 space-y-4">
|
<div className="px-5 pb-5 space-y-4">
|
||||||
@@ -94,26 +94,46 @@ function CollapsibleSection({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PricingEditorPage() {
|
function PricingContent({ data, update }: { data: PricingData; update: (d: PricingData) => void }) {
|
||||||
return (
|
const [sections, setSections] = useState({ subscriptions: true, rental: true, rules: false });
|
||||||
<SectionEditor<PricingData> sectionKey="pricing" title="Цены">
|
const allOpen = sections.subscriptions && sections.rental && sections.rules;
|
||||||
{(data, update) => (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="grid gap-3 sm:grid-cols-2">
|
|
||||||
<InputField
|
|
||||||
label="Заголовок секции"
|
|
||||||
value={data.title}
|
|
||||||
onChange={(v) => update({ ...data, title: v })}
|
|
||||||
/>
|
|
||||||
<InputField
|
|
||||||
label="Подзаголовок"
|
|
||||||
value={data.subtitle}
|
|
||||||
onChange={(v) => update({ ...data, subtitle: v })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Абонементы */}
|
function toggleAll() {
|
||||||
<CollapsibleSection title="Абонементы" count={data.items.length}>
|
const target = !allOpen;
|
||||||
|
setSections({ subscriptions: target, rental: target, rules: target });
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSection(key: keyof typeof sections) {
|
||||||
|
setSections(prev => ({ ...prev, [key]: !prev[key] }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="grid gap-3 sm:grid-cols-2 flex-1">
|
||||||
|
<InputField
|
||||||
|
label="Заголовок секции"
|
||||||
|
value={data.title}
|
||||||
|
onChange={(v) => update({ ...data, title: v })}
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
label="Подзаголовок"
|
||||||
|
value={data.subtitle}
|
||||||
|
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} isOpen={sections.subscriptions} onToggle={() => toggleSection("subscriptions")}>
|
||||||
{(() => {
|
{(() => {
|
||||||
const itemOptions = data.items
|
const itemOptions = data.items
|
||||||
.map((it, idx) => ({ value: String(idx), label: it.name }))
|
.map((it, idx) => ({ value: String(idx), label: it.name }))
|
||||||
@@ -196,7 +216,7 @@ export default function PricingEditorPage() {
|
|||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
|
|
||||||
{/* Аренда */}
|
{/* Аренда */}
|
||||||
<CollapsibleSection title="Аренда" count={data.rentalItems.length}>
|
<CollapsibleSection title="Аренда" count={data.rentalItems.length} isOpen={sections.rental} onToggle={() => toggleSection("rental")}>
|
||||||
<InputField
|
<InputField
|
||||||
label="Заголовок"
|
label="Заголовок"
|
||||||
value={data.rentalTitle}
|
value={data.rentalTitle}
|
||||||
@@ -236,7 +256,7 @@ export default function PricingEditorPage() {
|
|||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
|
|
||||||
{/* Правила */}
|
{/* Правила */}
|
||||||
<CollapsibleSection title="Правила" count={data.rules.length} defaultOpen={false}>
|
<CollapsibleSection title="Правила" count={data.rules.length} isOpen={sections.rules} onToggle={() => toggleSection("rules")}>
|
||||||
<ArrayEditor
|
<ArrayEditor
|
||||||
items={data.rules}
|
items={data.rules}
|
||||||
onChange={(rules) => update({ ...data, rules })}
|
onChange={(rules) => update({ ...data, rules })}
|
||||||
@@ -248,7 +268,13 @@ export default function PricingEditorPage() {
|
|||||||
/>
|
/>
|
||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
</div>
|
</div>
|
||||||
)}
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PricingEditorPage() {
|
||||||
|
return (
|
||||||
|
<SectionEditor<PricingData> sectionKey="pricing" title="Цены">
|
||||||
|
{(data, update) => <PricingContent data={data} update={update} />}
|
||||||
</SectionEditor>
|
</SectionEditor>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user