103 lines
3 KiB
TypeScript
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>
|
|
);
|
|
}
|