components/we-inc/settings-accordion-with-nested-sections.tsx
import { useState } from "react";
type Row = { id: string; label: string; hint: string; on: boolean };
type Group = { id: string; title: string; summary: string; rows: Row[] };
const groups: Group[] = [
{ id: "notify", title: "Notifications", summary: "Email, push and the weekly digest", rows: [
{ id: "n1", label: "New booking", hint: "Email me the moment a client books a slot", on: true },
{ id: "n2", label: "Cancellations", hint: "Push alert when a booking is cancelled", on: true },
{ id: "n3", label: "Weekly digest", hint: "A Monday summary of last week's sessions", on: false },
] },
{ id: "privacy", title: "Privacy", summary: "Who can see your profile and calendar", rows: [
{ id: "p1", label: "Public profile", hint: "Show your page in the studio directory", on: true },
{ id: "p2", label: "Show full calendar", hint: "Let clients see busy times, not just free ones", on: false },
] },
{ id: "billing", title: "Billing", summary: "Invoices, receipts and payment reminders", rows: [
{ id: "b1", label: "Send receipts", hint: "Email clients a receipt after each payment", on: true },
{ id: "b2", label: "Overdue reminders", hint: "Nudge clients three days after an unpaid invoice", on: false },
] },
];
function Chevron({ open }: { open: boolean }) {
return (
<svg viewBox="0 0 20 20" className={`h-5 w-5 shrink-0 text-slate-500 transition-transform duration-200 motion-reduce:transition-none ${open ? "rotate-90" : ""}`} fill="none" stroke="currentColor" strokeWidth="1.8" aria-hidden="true">
<path d="M7.5 5l5 5-5 5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
export default function SettingsAccordionWithNestedSections() {
const [openGroups, setOpenGroups] = useState<string[]>(["notify"]);
const [values, setValues] = useState<Record<string, boolean>>(() =>
Object.fromEntries(groups.flatMap((g) => g.rows.map((r) => [r.id, r.on]))),
);
const flip = (id: string) => setOpenGroups((g) => (g.includes(id) ? g.filter((x) => x !== id) : [...g, id]));
return (
<section className="bg-violet-50 px-5 py-14 sm:py-20">
<div className="mx-auto max-w-xl rounded-2xl border border-violet-100 bg-white p-2 shadow-sm">
<div className="px-4 pb-3 pt-4">
<h2 className="text-lg font-semibold text-slate-900">Settings</h2>
<p className="text-sm text-slate-600">Juniper Pilates, instructor account</p>
</div>
{groups.map((g) => {
const open = openGroups.includes(g.id);
const count = g.rows.filter((r) => values[r.id]).length;
return (
<div key={g.id} className="border-t border-slate-100">
<h3>
<button
type="button"
id={`nsa-${g.id}`}
aria-expanded={open}
aria-controls={`nsa-${g.id}-rows`}
onClick={() => flip(g.id)}
className="flex min-h-[64px] w-full items-center gap-3 rounded-xl px-4 py-3 text-left hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet-600"
>
<Chevron open={open} />
<span className="flex-1">
<span className="block text-[15px] font-semibold text-slate-900">{g.title}</span>
<span className="block text-sm text-slate-600">{g.summary}</span>
</span>
<span className="rounded-full bg-violet-100 px-2.5 py-0.5 text-xs font-medium text-violet-800">{count} on</span>
</button>
</h3>
{open && (
<ul id={`nsa-${g.id}-rows`} role="region" aria-labelledby={`nsa-${g.id}`} className="mb-2 ml-11 mr-2 border-l border-violet-200">
{g.rows.map((r) => (
<li key={r.id} className="flex items-center gap-4 py-3 pl-4 pr-2">
<span className="flex-1">
<span id={`nsa-l-${r.id}`} className="block text-sm font-medium text-slate-900">{r.label}</span>
<span className="block text-[13px] text-slate-600">{r.hint}</span>
</span>
<button
type="button"
role="switch"
aria-checked={values[r.id]}
aria-labelledby={`nsa-l-${r.id}`}
onClick={() => setValues((v) => ({ ...v, [r.id]: !v[r.id] }))}
className={`relative h-6 w-11 shrink-0 rounded-full transition-colors motion-reduce:transition-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet-600 focus-visible:ring-offset-2 ${values[r.id] ? "bg-violet-600" : "bg-slate-300"}`}
>
<span className={`absolute left-0.5 top-0.5 h-5 w-5 rounded-full bg-white shadow transition-transform motion-reduce:transition-none ${values[r.id] ? "translate-x-5" : ""}`} />
</button>
</li>
))}
</ul>
)}
</div>
);
})}
</div>
</section>
);
}