Adds a PetAvatar wrapper (hash(name) mod 360 -> HSL background) and migrates the four most-visible avatar call sites (sidebar, mobile nav, post card, post detail incl. comments) to use it instead of duplicated inline initials logic. Desktop sidebar can now collapse to an icon-only 76px rail, persisted in localStorage (same client-init pattern as ActivePetContext). All nav rows, the new-post button, and the owner row adapt; secondary sections (language switcher, legal footer) hide when collapsed. Gitea #12, #14
80 lines
2.7 KiB
TypeScript
80 lines
2.7 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { Bell } from "lucide-react";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { useTRPC } from "@/trpc/client";
|
|
import { useActivePet } from "@/context/ActivePetContext";
|
|
import { useTranslations } from "next-intl";
|
|
|
|
interface NotificationBellProps {
|
|
/** "sidebar" = full row with label; "mobile" = icon + label stacked */
|
|
variant: "sidebar" | "mobile";
|
|
isActive?: boolean;
|
|
/** Sidebar variant only: icon-only row when the sidebar is collapsed */
|
|
collapsed?: boolean;
|
|
}
|
|
|
|
export function NotificationBell({ variant, isActive, collapsed }: NotificationBellProps) {
|
|
const trpc = useTRPC();
|
|
const { activePetId } = useActivePet();
|
|
const t = useTranslations("Nav");
|
|
|
|
const { data } = useQuery({
|
|
...trpc.notifications.unreadCount.queryOptions({ petId: activePetId ?? "" }),
|
|
enabled: !!activePetId,
|
|
refetchInterval: 10_000,
|
|
refetchOnWindowFocus: true,
|
|
});
|
|
|
|
const count = data?.count ?? 0;
|
|
const label = count > 99 ? "99+" : count > 0 ? String(count) : null;
|
|
|
|
if (variant === "sidebar") {
|
|
return (
|
|
<Link
|
|
href="/notifications"
|
|
title={collapsed ? t("notifications") : undefined}
|
|
className={`flex items-center gap-3 px-3 py-2 rounded-md text-sm transition-colors ${
|
|
collapsed ? "justify-center px-0" : ""
|
|
} ${
|
|
isActive
|
|
? collapsed
|
|
? "text-foreground font-medium bg-muted"
|
|
: "border-l-2 border-orange-500 text-foreground font-medium bg-muted pl-[10px]"
|
|
: "text-muted-foreground hover:text-foreground hover:bg-muted"
|
|
}`}
|
|
>
|
|
<span className="relative shrink-0">
|
|
<Bell className="h-4 w-4" aria-hidden="true" />
|
|
{label && (
|
|
<span className="absolute -top-1.5 -right-1.5 min-w-[16px] h-4 px-0.5 rounded-full bg-orange-500 text-[10px] text-white font-semibold flex items-center justify-center leading-none">
|
|
{label}
|
|
</span>
|
|
)}
|
|
</span>
|
|
{!collapsed && <span>{t("notifications")}</span>}
|
|
</Link>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Link
|
|
href="/notifications"
|
|
className={`flex flex-col items-center gap-0.5 px-3 py-2 text-xs ${
|
|
isActive ? "text-orange-500" : "text-muted-foreground"
|
|
}`}
|
|
>
|
|
<span className="relative">
|
|
<Bell className="h-5 w-5" aria-hidden="true" />
|
|
{label && (
|
|
<span className="absolute -top-1.5 -right-1.5 min-w-[16px] h-4 px-0.5 rounded-full bg-orange-500 text-[10px] text-white font-semibold flex items-center justify-center leading-none">
|
|
{label}
|
|
</span>
|
|
)}
|
|
</span>
|
|
<span>{t("notifications")}</span>
|
|
</Link>
|
|
);
|
|
}
|