trptk/components/TabsClient.tsx
2026-02-24 17:14:07 +01:00

103 lines
3 KiB
TypeScript

"use client";
import * as React from "react";
export type TabDef = {
id: string;
label: string;
content: React.ReactNode;
};
type Props = {
defaultTabId: string;
tabs: TabDef[];
};
function normalizeHash(hash: string) {
return hash.replace(/^#/, "").trim();
}
export function TabsClient({ defaultTabId, tabs }: Props) {
const tabIds = React.useMemo(() => new Set(tabs.map((t) => t.id)), [tabs]);
const [activeId, setActiveId] = React.useState(defaultTabId);
React.useEffect(() => {
const fromHash = normalizeHash(window.location.hash);
if (fromHash && tabIds.has(fromHash)) {
setActiveId(fromHash);
return;
}
const base = `${window.location.pathname}${window.location.search}`;
window.history.replaceState(null, "", `${base}#${defaultTabId}`);
}, [defaultTabId, tabIds]);
React.useEffect(() => {
const onHashChange = () => {
const next = normalizeHash(window.location.hash);
if (next && tabIds.has(next)) setActiveId(next);
};
window.addEventListener("hashchange", onHashChange);
return () => window.removeEventListener("hashchange", onHashChange);
}, [tabIds]);
const selectTab = React.useCallback((id: string) => {
setActiveId(id);
const base = `${window.location.pathname}${window.location.search}`;
window.history.replaceState(null, "", `${base}#${id}`);
}, []);
return (
<section className="w-full">
<div
role="tablist"
aria-label="Artist details"
className="relative z-10 my-10 flex w-full overflow-hidden rounded-2xl bg-lightbg text-lighttext shadow-lg ring-1 ring-lightline transition-all duration-300 ease-in-out hover:ring-lightline-hover md:my-12 lg:my-20 dark:bg-darkbg dark:text-darktext dark:ring-darkline dark:hover:ring-darkline-hover"
>
{tabs.map((t) => {
const active = t.id === activeId;
return (
<button
key={t.id}
role="tab"
type="button"
aria-selected={active}
aria-controls={`panel-${t.id}`}
id={`tab-${t.id}`}
onClick={() => selectTab(t.id)}
className={[
"relative flex-1 px-5 py-3 text-center text-sm transition-all duration-300 ease-in-out first:rounded-l-2xl last:rounded-r-2xl hover:bg-lightline sm:text-base dark:hover:bg-darkline",
active
? "bg-lightline font-medium text-current dark:bg-darkline"
: "text-lightsec dark:text-darksec",
].join(" ")}
>
{t.label}
</button>
);
})}
</div>
<div className="pt-4 sm:pt-0">
{tabs.map((t) => {
const active = t.id === activeId;
return (
<div
key={t.id}
role="tabpanel"
id={`panel-${t.id}`}
aria-labelledby={`tab-${t.id}`}
hidden={!active}
>
{t.content}
</div>
);
})}
</div>
</section>
);
}