sendou.ink/app/components/SideNav.tsx
Kalle fef1ffc955
Design refresh + a bunch of stuff (#2864)
Co-authored-by: hfcRed <hfcred@gmx.net>
2026-03-19 17:51:42 +02:00

215 lines
4.5 KiB
TypeScript

import clsx from "clsx";
import { X } from "lucide-react";
import type * as React from "react";
import { Button } from "react-aria-components";
import { Link } from "react-router";
import { SendouButton } from "~/components/elements/Button";
import type { Tables } from "~/db/tables";
import { Avatar } from "./Avatar";
import styles from "./SideNav.module.css";
export function SideNav({
children,
className,
footer,
top,
topCentered,
collapsed,
}: {
children: React.ReactNode;
className?: string;
footer?: React.ReactNode;
top?: React.ReactNode;
topCentered?: boolean;
collapsed?: boolean;
}) {
return (
<nav
className={clsx(styles.sideNav, className, {
[styles.sideNavCollapsed]: collapsed,
})}
>
<div
className={clsx(styles.sideNavTop, {
[styles.sideNavTopCentered]: topCentered,
})}
>
{top}
</div>
<div className={clsx(styles.sideNavInner, "scrollbar")}>{children}</div>
{footer}
</nav>
);
}
export function SideNavHeader({
children,
icon,
showClose,
action,
}: {
children: React.ReactNode;
icon?: React.ReactNode;
showClose?: boolean;
action?: React.ReactNode;
}) {
return (
<header className={styles.sideNavHeader}>
{icon ? <div className={styles.iconContainer}>{icon}</div> : null}
<h2>{children}</h2>
{action ? (
<span className={styles.sideNavHeaderAction}>{action}</span>
) : null}
{showClose ? (
<SendouButton
icon={<X />}
variant="minimal"
slot="close"
className={styles.sideNavHeaderClose}
/>
) : null}
</header>
);
}
function ListItemContent({
children,
user,
imageUrl,
overlayIconUrl,
subtitle,
badge,
badgeVariant,
suppressSubtitleHydrationWarning,
}: {
children: React.ReactNode;
user?: Pick<Tables["User"], "discordId" | "discordAvatar">;
imageUrl?: string;
overlayIconUrl?: string;
subtitle?: React.ReactNode;
badge?: React.ReactNode;
badgeVariant?: "default" | "warning";
suppressSubtitleHydrationWarning?: boolean;
}) {
return (
<>
{user ? (
<Avatar user={user} size="xxsm" />
) : imageUrl ? (
<div className={styles.listLinkImageContainer}>
<img src={imageUrl} alt="" className={styles.listLinkImage} />
{overlayIconUrl ? (
<img
src={overlayIconUrl}
alt=""
className={styles.listLinkOverlayIcon}
/>
) : null}
</div>
) : null}
<div className={styles.listLinkContent}>
<span className={styles.listLinkTitle}>{children}</span>
{subtitle || badge ? (
<div className={styles.listLinkSubtitleRow}>
{subtitle ? (
<span
className={styles.listLinkSubtitle}
suppressHydrationWarning={suppressSubtitleHydrationWarning}
>
{subtitle}
</span>
) : null}
{typeof badge === "string" ? (
<span
className={clsx(styles.listLinkBadge, {
[styles.listLinkBadgeWarning]: badgeVariant === "warning",
})}
>
{badge}
</span>
) : (
badge
)}
</div>
) : null}
</div>
</>
);
}
export function ListLink({
children,
to,
onClick,
isActive,
imageUrl,
overlayIconUrl,
user,
subtitle,
badge,
badgeVariant,
}: {
children: React.ReactNode;
to: string;
onClick?: (event: React.MouseEvent<HTMLAnchorElement>) => void;
isActive?: boolean;
imageUrl?: string;
overlayIconUrl?: string;
user?: Pick<Tables["User"], "discordId" | "discordAvatar">;
subtitle?: React.ReactNode;
badge?: React.ReactNode;
badgeVariant?: "default" | "warning";
}) {
return (
<Link
to={to}
className={styles.listLink}
onClick={onClick}
aria-current={isActive ? "page" : undefined}
>
<ListItemContent
user={user}
imageUrl={imageUrl}
overlayIconUrl={overlayIconUrl}
subtitle={subtitle}
badge={badge}
badgeVariant={badgeVariant}
suppressSubtitleHydrationWarning
>
{children}
</ListItemContent>
</Link>
);
}
export function ListButton({
children,
user,
subtitle,
badge,
badgeVariant,
}: {
children: React.ReactNode;
user?: Pick<Tables["User"], "discordId" | "discordAvatar">;
subtitle?: string | null;
badge?: string | null;
badgeVariant?: "default" | "warning";
}) {
return (
<Button className={styles.listButton}>
<ListItemContent
user={user}
subtitle={subtitle}
badge={badge}
badgeVariant={badgeVariant}
>
{children}
</ListItemContent>
</Button>
);
}
export function SideNavFooter({ children }: { children: React.ReactNode }) {
return <div className={styles.sideNavFooter}>{children}</div>;
}