Files
lidify/frontend/components/layout/ActivityPanel.tsx
2025-12-25 18:58:06 -06:00

274 lines
11 KiB
TypeScript

"use client";
import { useState } from "react";
import { useNotifications } from "@/hooks/useNotifications";
import { useActiveDownloads } from "@/hooks/useNotifications";
import { NotificationsTab } from "@/components/activity/NotificationsTab";
import { ActiveDownloadsTab } from "@/components/activity/ActiveDownloadsTab";
import { HistoryTab } from "@/components/activity/HistoryTab";
import {
Bell,
Download,
History,
ChevronLeft,
ChevronRight,
X,
} from "lucide-react";
import { cn } from "@/utils/cn";
import { useIsMobile, useIsTablet } from "@/hooks/useMediaQuery";
type ActivityTab = "notifications" | "active" | "history";
const TABS: { id: ActivityTab; label: string; icon: React.ElementType }[] = [
{ id: "notifications", label: "Notifications", icon: Bell },
{ id: "active", label: "Active", icon: Download },
{ id: "history", label: "History", icon: History },
];
interface ActivityPanelProps {
isOpen: boolean;
onToggle: () => void;
activeTab?: ActivityTab;
onTabChange?: (tab: ActivityTab) => void;
}
export function ActivityPanel({
isOpen,
onToggle,
activeTab,
onTabChange,
}: ActivityPanelProps) {
const [internalActiveTab, setInternalActiveTab] =
useState<ActivityTab>("notifications");
const resolvedActiveTab = activeTab ?? internalActiveTab;
const setResolvedActiveTab = onTabChange ?? setInternalActiveTab;
const { unreadCount } = useNotifications();
const { downloads: activeDownloads } = useActiveDownloads();
const isMobile = useIsMobile();
const isTablet = useIsTablet();
const isMobileOrTablet = isMobile || isTablet;
// Badge counts
const notificationBadge = unreadCount > 0 ? unreadCount : null;
const activeBadge =
activeDownloads.length > 0 ? activeDownloads.length : null;
const hasActivity = unreadCount > 0 || activeDownloads.length > 0;
// Mobile/Tablet: Full-screen overlay
if (isMobileOrTablet) {
if (!isOpen) return null;
return (
<>
{/* Backdrop */}
<div
className="fixed inset-0 bg-black/60 z-[100]"
onClick={onToggle}
/>
{/* Panel - slides in from right */}
<div
className="fixed inset-y-0 right-0 w-full max-w-md bg-[#0a0a0a] z-[101] flex flex-col"
style={{ paddingTop: "env(safe-area-inset-top)" }}
>
{/* Header */}
<div className="flex items-center justify-between px-4 py-4 border-b border-white/10">
<h2 className="text-lg font-semibold text-white">
Activity
</h2>
<button
onClick={onToggle}
className="p-2 hover:bg-white/10 rounded-full transition-colors"
title="Close"
>
<X className="w-5 h-5 text-white/60" />
</button>
</div>
{/* Tabs */}
<div className="flex border-b border-white/10">
{TABS.map((tab) => {
const Icon = tab.icon;
const badge =
tab.id === "notifications"
? notificationBadge
: tab.id === "active"
? activeBadge
: null;
return (
<button
key={tab.id}
onClick={() => setResolvedActiveTab(tab.id)}
className={cn(
"flex-1 flex items-center justify-center gap-2 py-3 text-sm font-medium transition-colors relative",
resolvedActiveTab === tab.id
? "text-white border-b-2 border-[#f5c518]"
: "text-white/50 hover:text-white/70"
)}
>
<Icon className="w-4 h-4" />
<span>{tab.label}</span>
{badge && (
<span
className={cn(
"min-w-[18px] h-[18px] px-1 rounded-full text-xs font-bold flex items-center justify-center ml-1",
tab.id === "active"
? "bg-blue-500 text-white"
: "bg-[#f5c518] text-black"
)}
>
{badge > 99 ? "99+" : badge}
</span>
)}
</button>
);
})}
</div>
{/* Tab Content */}
<div className="flex-1 overflow-hidden">
{resolvedActiveTab === "notifications" && (
<NotificationsTab />
)}
{resolvedActiveTab === "active" && (
<ActiveDownloadsTab />
)}
{resolvedActiveTab === "history" && <HistoryTab />}
</div>
</div>
</>
);
}
// Desktop: Side panel
return (
<div
className={cn(
"shrink-0 h-full bg-[#0d0d0d] rounded-tl-lg rounded-bl-lg border-l border-white/5 flex flex-col z-10 transition-all duration-300 ease-out overflow-hidden relative",
isOpen ? "w-[400px]" : "w-12"
)}
>
{/* Collapsed state overlay */}
{!isOpen && (
<div
onClick={onToggle}
className="absolute inset-0 flex items-center justify-center cursor-pointer hover:bg-[#141414] transition-colors"
title="Open activity panel"
>
<ChevronLeft className="w-5 h-5 text-white/40" />
{/* Activity badge */}
{hasActivity && (
<span className="absolute top-4 right-3 w-2.5 h-2.5 rounded-full bg-[#ecb200]" />
)}
</div>
)}
{/* Expanded content - only visible when open */}
<div
className={cn(
"flex flex-col h-full transition-opacity duration-200",
isOpen ? "opacity-100" : "opacity-0 pointer-events-none"
)}
>
{/* Header */}
<div className="flex items-center justify-between px-5 py-4 border-b border-white/10">
<h2 className="text-base font-semibold text-white whitespace-nowrap">
Activity
</h2>
<button
onClick={onToggle}
className="p-1.5 hover:bg-white/10 rounded transition-colors"
title="Close panel"
>
<ChevronRight className="w-5 h-5 text-white/60" />
</button>
</div>
{/* Tabs */}
<div className="flex border-b border-white/10">
{TABS.map((tab) => {
const Icon = tab.icon;
const badge =
tab.id === "notifications"
? notificationBadge
: tab.id === "active"
? activeBadge
: null;
return (
<button
key={tab.id}
onClick={() => setResolvedActiveTab(tab.id)}
className={cn(
"flex-1 flex items-center justify-center gap-2 py-3 text-sm font-medium transition-colors relative whitespace-nowrap",
resolvedActiveTab === tab.id
? "text-white border-b-2 border-[#ecb200]"
: "text-white/50 hover:text-white/70"
)}
>
<Icon className="w-4 h-4" />
<span>{tab.label}</span>
{badge && (
<span
className={cn(
"absolute -top-0.5 right-1/4 min-w-[18px] h-[18px] px-1 rounded-full text-xs font-bold flex items-center justify-center",
tab.id === "active"
? "bg-blue-500 text-white"
: "bg-[#ecb200] text-black"
)}
>
{badge > 99 ? "99+" : badge}
</span>
)}
</button>
);
})}
</div>
{/* Tab Content */}
<div className="flex-1 overflow-hidden">
{resolvedActiveTab === "notifications" && (
<NotificationsTab />
)}
{resolvedActiveTab === "active" && <ActiveDownloadsTab />}
{resolvedActiveTab === "history" && <HistoryTab />}
</div>
</div>
</div>
);
}
// Toggle button for TopBar
export function ActivityPanelToggle() {
const { unreadCount } = useNotifications();
const { downloads: activeDownloads } = useActiveDownloads();
const isMobile = useIsMobile();
const isTablet = useIsTablet();
if (isMobile || isTablet) {
return null;
}
const hasActivity = unreadCount > 0 || activeDownloads.length > 0;
return (
<button
onClick={() =>
window.dispatchEvent(new CustomEvent("toggle-activity-panel"))
}
className={cn(
"relative p-2 rounded-full transition-all",
"text-white/60 hover:text-white"
)}
title="Toggle activity panel"
>
<Bell className="w-5 h-5" />
{hasActivity && (
<span className="absolute top-1.5 right-2 w-1 h-1 rounded-full bg-[#ecb200]" />
)}
</button>
);
}