Files
famapp/src/components/sidebar.tsx
T
ginnoir a312d4ce39
CI / checks (push) Successful in 12m55s
CI / build (push) Successful in 15m16s
fix: tighten sharing and shadcn composition
2026-06-13 05:20:01 -05:00

151 lines
4.7 KiB
TypeScript

import Link from "next/link";
import { eq } from "drizzle-orm";
import { cn } from "@/lib/utils";
import { auth } from "@/lib/auth";
import { db } from "@/lib/db";
import { getRegistry } from "@/modules/_core/registry";
import { householdMembers, households, users } from "@/modules/_core/schema";
import { BrandMark } from "@/components/brand-mark";
import { NavIcon } from "@/components/nav-icon";
import { NavLink } from "@/components/nav-link";
import { AvatarFallbackWithName } from "@/components/avatar-fallback";
type Variant = "side" | "top";
const PRIMARY_NAV: Array<{ href: string; label: string; icon: string }> = [
{ href: "/", label: "Dashboard", icon: "home" },
];
const SECONDARY_NAV: Array<{ href: string; label: string; icon: string }> = [
{ href: "/settings#sharing", label: "Share links", icon: "link" },
];
async function getHouseholdInfo(userId: string) {
const [row] = await db
.select({ household: households })
.from(householdMembers)
.innerJoin(households, eq(householdMembers.householdId, households.id))
.where(eq(householdMembers.userId, userId))
.limit(1);
if (!row) return null;
const members = await db
.select({ id: users.id, name: users.name, image: users.image, email: users.email })
.from(householdMembers)
.innerJoin(users, eq(householdMembers.userId, users.id))
.where(eq(householdMembers.householdId, row.household.id))
.limit(6);
return { household: row.household, members };
}
export async function Sidebar({ variant = "side" }: { variant?: Variant }) {
const { modules } = getRegistry();
const moduleNav = modules.flatMap((m) => (m.nav ? [m.nav] : []));
const navItems = [
...PRIMARY_NAV,
...moduleNav.map((n) => ({ href: n.href, label: n.label, icon: n.icon ?? "circle" })),
{ href: "/settings", label: "Settings", icon: "settings" },
];
const session = await auth();
const householdInfo = session?.user?.id ? await getHouseholdInfo(session.user.id) : null;
if (variant === "top") {
return (
<header className="sidebar sidebar-top">
<Link href="/" className="brand">
<BrandMark />
<span className="brand-name">famapp</span>
</Link>
{navItems.map((n) => (
<NavLink
key={n.href}
href={n.href}
label={n.label}
icon={n.icon}
className="!w-auto !h-9"
/>
))}
<div className="ml-auto" />
{householdInfo && (
<span className="badge">
<NavIcon name="people" className="size-3" />
{householdInfo.household.name}
</span>
)}
</header>
);
}
return (
<aside className="sidebar">
<Link href="/" className="brand">
<BrandMark />
<span className="brand-name">famapp</span>
</Link>
{navItems.map((n) => (
<NavLink key={n.href} href={n.href} label={n.label} icon={n.icon} />
))}
<div className="nav-divider" />
{SECONDARY_NAV.map((n) => (
<NavLink key={n.href} href={n.href} label={n.label} icon={n.icon} />
))}
{householdInfo && <HouseholdPill info={householdInfo} />}
</aside>
);
}
function HouseholdPill({
info,
}: {
info: NonNullable<Awaited<ReturnType<typeof getHouseholdInfo>>>;
}) {
const visible = info.members.slice(0, 3);
return (
<div className="household-pill" title={info.household.name}>
<div style={{ display: "flex" }}>
{visible.map((m, i) => (
<AvatarFallbackWithName
key={m.id}
name={m.name ?? m.email ?? undefined}
image={m.image}
className={cn(i > 0 && "-ml-1.5", "shadow-[0_0_0_1.5px_var(--card)]")}
fallbackStyle={{ background: avatarColor(m.id) }}
title={m.name ?? m.email ?? undefined}
/>
))}
</div>
<div className="household-text" style={{ minWidth: 0 }}>
<div
style={{
fontWeight: 500,
color: "var(--ink)",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{info.household.name}
</div>
<div className="muted" style={{ fontSize: 11 }}>
{info.members.length} {info.members.length === 1 ? "member" : "members"}
</div>
</div>
</div>
);
}
// Stable color from id — uses the 5 functional accents in rotation.
const ACCENT_HUES = [
"var(--c-household)",
"var(--c-private)",
"var(--c-kids)",
"var(--c-work)",
"var(--c-bills)",
];
function avatarColor(id: string): string {
let hash = 0;
for (let i = 0; i < id.length; i++) hash = (hash * 31 + id.charCodeAt(i)) & 0xffff;
return ACCENT_HUES[hash % ACCENT_HUES.length] ?? "var(--c-household)";
}