echos-ocr/src/components/layout/sidebar.tsx

204 lines
5.3 KiB
TypeScript
Raw Normal View History

"use client";
import * as React from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import {
LayoutDashboard,
CreditCard,
CalendarDays,
Users,
BarChart3,
Settings,
ChevronsLeft,
ChevronsRight,
} from "lucide-react";
import { cn } from "@/lib/utils";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
import { ScrollArea } from "@/components/ui/scroll-area";
import { OrgSwitcher } from "@/components/layout/org-switcher";
const STORAGE_KEY = "echo-sidebar-collapsed";
const navItems = [
{ href: "/", label: "Dashboard", icon: LayoutDashboard },
{ href: "/cards", label: "Response Cards", icon: CreditCard },
{ href: "/events", label: "Collection Days", icon: CalendarDays },
{ href: "/people", label: "People", icon: Users },
{ href: "/reports", label: "Reports", icon: BarChart3 },
];
const bottomItems = [
{ href: "/settings", label: "Settings", icon: Settings },
];
function isActive(pathname: string, href: string) {
if (href === "/") return pathname === "/";
return pathname === href || pathname.startsWith(href + "/");
}
type SidebarContextValue = {
collapsed: boolean;
setCollapsed: (v: boolean) => void;
toggle: () => void;
};
const SidebarContext = React.createContext<SidebarContextValue>({
collapsed: false,
setCollapsed: () => {},
toggle: () => {},
});
export function useSidebar() {
return React.useContext(SidebarContext);
}
export function SidebarProvider({ children }: { children: React.ReactNode }) {
const [collapsed, setCollapsedState] = React.useState(false);
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => {
setMounted(true);
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored === "true") setCollapsedState(true);
} catch {}
}, []);
const setCollapsed = React.useCallback((v: boolean) => {
setCollapsedState(v);
try {
localStorage.setItem(STORAGE_KEY, String(v));
} catch {}
}, []);
const toggle = React.useCallback(() => {
setCollapsed(!collapsed);
}, [collapsed, setCollapsed]);
const value = React.useMemo(
() => ({ collapsed: mounted ? collapsed : false, setCollapsed, toggle }),
[mounted, collapsed, setCollapsed, toggle]
);
return (
<SidebarContext.Provider value={value}>{children}</SidebarContext.Provider>
);
}
function NavItem({
href,
label,
icon: Icon,
active,
collapsed,
}: {
href: string;
label: string;
icon: React.ComponentType<{ className?: string }>;
active: boolean;
collapsed: boolean;
}) {
const content = (
<Link
href={href}
Add dynamic fields, people directory, analytics, security hardening, and UX polish Phase 1 - Security & Bug Fixes: - Add requireApiAuth helper and protect all 25 unprotected API routes - Add org-tenant scoping to all card, job, stats, and notification queries - Fix SSRF in ai-test, mask secrets in settings API, fix middleware bypass - Fix cards pagination routing, stat filter sync, drag-drop file passing - Add PUT /api/auth/me for profile persistence, stuck job recovery - Fix email watcher MIME type detection Phase 2 - Dynamic Fields & Digital Survey: - Add FormTemplate, FormField, Person, PasswordResetToken models to schema - Add fieldData, formTemplateId, firstName, lastName, personId to ResponseCard - Build FormTemplate CRUD API with field management and org scoping - Build Form Builder UI with field ordering, type config, and section management - Refactor card detail page to render fields dynamically from templates - Add dynamic OCR prompt/schema generation from template fields - Build public survey page at /s/[orgSlug]/[formSlug] with branding - Add QR code generation API and share section component Phase 3 - People & Analytics: - Build People CRUD API with merge and batch auto-link endpoints - Build People list and detail pages with search, merge dialog - Add auto-link logic in OCR completion to match/create Person records - Add /api/stats/trends endpoint with time series and team activity - Build Reports page with Recharts (area charts, bar charts, pipeline) - Upgrade dashboard with sparklines and People stat card Phase 4 - UX Polish: - Replace silent error handling with toast notifications across all pages - Add loading skeletons, differentiated empty states - Add ARIA labels, skip-to-content link, accessible column toggle - Add forgot password flow, Cmd+K command palette, Collection Days pages - Unify Echo branding and theme toggle consistency Made-with: Cursor
2026-04-17 00:29:26 -04:00
aria-current={active ? "page" : undefined}
className={cn(
"flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors",
active
? "bg-sidebar-accent text-sidebar-accent-foreground"
: "text-sidebar-foreground/70 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground",
collapsed && "justify-center px-0"
)}
>
<Icon className="size-4 shrink-0" />
{!collapsed && <span className="truncate">{label}</span>}
</Link>
);
if (collapsed) {
return (
<Tooltip>
<TooltipTrigger render={<div />}>{content}</TooltipTrigger>
<TooltipContent side="right" sideOffset={8}>
{label}
</TooltipContent>
</Tooltip>
);
}
return content;
}
export function Sidebar() {
const pathname = usePathname();
const { collapsed, toggle } = useSidebar();
return (
<aside
Add dynamic fields, people directory, analytics, security hardening, and UX polish Phase 1 - Security & Bug Fixes: - Add requireApiAuth helper and protect all 25 unprotected API routes - Add org-tenant scoping to all card, job, stats, and notification queries - Fix SSRF in ai-test, mask secrets in settings API, fix middleware bypass - Fix cards pagination routing, stat filter sync, drag-drop file passing - Add PUT /api/auth/me for profile persistence, stuck job recovery - Fix email watcher MIME type detection Phase 2 - Dynamic Fields & Digital Survey: - Add FormTemplate, FormField, Person, PasswordResetToken models to schema - Add fieldData, formTemplateId, firstName, lastName, personId to ResponseCard - Build FormTemplate CRUD API with field management and org scoping - Build Form Builder UI with field ordering, type config, and section management - Refactor card detail page to render fields dynamically from templates - Add dynamic OCR prompt/schema generation from template fields - Build public survey page at /s/[orgSlug]/[formSlug] with branding - Add QR code generation API and share section component Phase 3 - People & Analytics: - Build People CRUD API with merge and batch auto-link endpoints - Build People list and detail pages with search, merge dialog - Add auto-link logic in OCR completion to match/create Person records - Add /api/stats/trends endpoint with time series and team activity - Build Reports page with Recharts (area charts, bar charts, pipeline) - Upgrade dashboard with sparklines and People stat card Phase 4 - UX Polish: - Replace silent error handling with toast notifications across all pages - Add loading skeletons, differentiated empty states - Add ARIA labels, skip-to-content link, accessible column toggle - Add forgot password flow, Cmd+K command palette, Collection Days pages - Unify Echo branding and theme toggle consistency Made-with: Cursor
2026-04-17 00:29:26 -04:00
aria-label="Main navigation"
className={cn(
"fixed left-0 top-16 z-30 flex h-[calc(100vh-4rem)] flex-col border-r border-sidebar-border bg-sidebar transition-[width] duration-200 ease-in-out",
collapsed ? "w-16" : "w-60"
)}
>
<div
className={cn(
"border-b border-sidebar-border p-3",
collapsed && "px-2"
)}
>
<OrgSwitcher collapsed={collapsed} />
</div>
<ScrollArea className="flex-1">
<nav className={cn("flex flex-col gap-1 p-3", collapsed && "px-2")}>
{navItems.map((item) => (
<NavItem
key={item.href}
{...item}
active={isActive(pathname, item.href)}
collapsed={collapsed}
/>
))}
</nav>
</ScrollArea>
<div
className={cn(
"flex flex-col gap-1 border-t border-sidebar-border p-3",
collapsed && "px-2"
)}
>
{bottomItems.map((item) => (
<NavItem
key={item.href}
{...item}
active={isActive(pathname, item.href)}
collapsed={collapsed}
/>
))}
<button
onClick={toggle}
Add dynamic fields, people directory, analytics, security hardening, and UX polish Phase 1 - Security & Bug Fixes: - Add requireApiAuth helper and protect all 25 unprotected API routes - Add org-tenant scoping to all card, job, stats, and notification queries - Fix SSRF in ai-test, mask secrets in settings API, fix middleware bypass - Fix cards pagination routing, stat filter sync, drag-drop file passing - Add PUT /api/auth/me for profile persistence, stuck job recovery - Fix email watcher MIME type detection Phase 2 - Dynamic Fields & Digital Survey: - Add FormTemplate, FormField, Person, PasswordResetToken models to schema - Add fieldData, formTemplateId, firstName, lastName, personId to ResponseCard - Build FormTemplate CRUD API with field management and org scoping - Build Form Builder UI with field ordering, type config, and section management - Refactor card detail page to render fields dynamically from templates - Add dynamic OCR prompt/schema generation from template fields - Build public survey page at /s/[orgSlug]/[formSlug] with branding - Add QR code generation API and share section component Phase 3 - People & Analytics: - Build People CRUD API with merge and batch auto-link endpoints - Build People list and detail pages with search, merge dialog - Add auto-link logic in OCR completion to match/create Person records - Add /api/stats/trends endpoint with time series and team activity - Build Reports page with Recharts (area charts, bar charts, pipeline) - Upgrade dashboard with sparklines and People stat card Phase 4 - UX Polish: - Replace silent error handling with toast notifications across all pages - Add loading skeletons, differentiated empty states - Add ARIA labels, skip-to-content link, accessible column toggle - Add forgot password flow, Cmd+K command palette, Collection Days pages - Unify Echo branding and theme toggle consistency Made-with: Cursor
2026-04-17 00:29:26 -04:00
aria-label={collapsed ? "Expand sidebar" : "Collapse sidebar"}
className={cn(
"flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium text-sidebar-foreground/50 transition-colors hover:bg-sidebar-accent/50 hover:text-sidebar-foreground",
collapsed && "justify-center px-0"
)}
>
{collapsed ? (
<ChevronsRight className="size-4 shrink-0" />
) : (
<ChevronsLeft className="size-4 shrink-0" />
)}
{!collapsed && <span>Collapse</span>}
</button>
</div>
</aside>
);
}