Files
sendou.ink/app/components/SideNav.tsx
Kalle e3bfd23dc2
Some checks failed
E2E Tests / e2e (push) Has been cancelled
Tests and checks on push / run-checks-and-tests (push) Has been cancelled
Updates translation progress / update-translation-progress-issue (push) Has been cancelled
Set up prefetch intent
2026-09-09 20:21:49 +03:00

269 lines
5.7 KiB
TypeScript

import clsx from "clsx";
import { X } from "lucide-react";
import type * as React from "react";
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,
...rest
}: {
children: React.ReactNode;
className?: string;
footer?: React.ReactNode;
top?: React.ReactNode;
topCentered?: boolean;
collapsed?: boolean;
} & Omit<React.ComponentPropsWithRef<"nav">, "children" | "className">) {
return (
<nav
className={clsx(styles.sideNav, className, {
[styles.sideNavCollapsed]: collapsed,
})}
{...rest}
>
<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"> & {
customAvatarUrl?: string | null;
};
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,
isActive,
imageUrl,
overlayIconUrl,
user,
subtitle,
badge,
badgeVariant,
}: {
children: React.ReactNode;
to: string;
isActive?: boolean;
imageUrl?: string;
overlayIconUrl?: string;
user?: Pick<Tables["User"], "discordId" | "discordAvatar"> & {
customAvatarUrl?: string | null;
};
subtitle?: React.ReactNode;
badge?: React.ReactNode;
badgeVariant?: "default" | "warning";
}) {
return (
<Link
to={to}
prefetch="intent"
className={styles.listLink}
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,
...rest
}: {
children: React.ReactNode;
user?: Pick<Tables["User"], "discordId" | "discordAvatar"> & {
customAvatarUrl?: string | null;
};
subtitle?: string | null;
badge?: string | null;
badgeVariant?: "default" | "warning";
} & Omit<React.ComponentPropsWithoutRef<"button">, "children">) {
return (
<button type="button" className={styles.listButton} {...rest}>
<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>;
}
export function NavIconContainer({ children }: { children: React.ReactNode }) {
return <div className={styles.iconContainer}>{children}</div>;
}
export function NavListButton({
children,
className,
onClick,
}: {
children: React.ReactNode;
className?: string;
onClick: () => void;
}) {
return (
<button
type="button"
className={clsx(styles.listButton, className)}
onClick={onClick}
>
{children}
</button>
);
}
export function NavListImage({ src }: { src: string }) {
return <img src={src} alt="" className={styles.listLinkImage} />;
}
export function NavListTexts({ children }: { children: React.ReactNode }) {
return <div className={styles.listLinkContent}>{children}</div>;
}
export function NavListTitle({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) {
return (
<span className={clsx(styles.listLinkTitle, className)}>{children}</span>
);
}
export function NavListSubtitle({ children }: { children: React.ReactNode }) {
return <span className={styles.listLinkSubtitle}>{children}</span>;
}