draupnir4all-web

git clone git://archive.git.mtrnord.blog/MTRNord/draupnir4all-web.git
Log | Files | Refs | README | LICENSE

commit bbac29273aaf595c3ce0285cc213406a594ae01d
parent 6642ad66811b17217a2cea79b06f2b5319c42f06
Author: MTRNord <mtrnord1@gmail.com>
Date:   Sat, 26 Apr 2025 16:02:50 +0200

Move dashboard components to global components

Signed-off-by: MTRNord <mtrnord1@gmail.com>

Diffstat:
Msrc/app/dashboard/analytics/page.tsx | 2+-
Msrc/app/dashboard/bans/page.tsx | 4++--
Dsrc/app/dashboard/components/analytics-dashboard.tsx | 0
Dsrc/app/dashboard/components/dashboard-header.tsx | 387-------------------------------------------------------------------------------
Dsrc/app/dashboard/components/modals/add-ban.tsx | 116-------------------------------------------------------------------------------
Dsrc/app/dashboard/components/protected-rooms-list.tsx | 26--------------------------
Dsrc/app/dashboard/components/tab-navigation.tsx | 36------------------------------------
Msrc/app/dashboard/layout.tsx | 2+-
Msrc/app/dashboard/overview/page.tsx | 6+++---
Msrc/app/dashboard/reports/page.tsx | 2+-
Msrc/app/dashboard/settings/page.tsx | 8++++----
Asrc/components/dashboard/dashboard-header.tsx | 387+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/dashboard/protected-rooms-list.tsx | 26++++++++++++++++++++++++++
Asrc/components/dashboard/tab-navigation.tsx | 36++++++++++++++++++++++++++++++++++++
Asrc/components/modals/add-ban.tsx | 116+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rsrc/app/dashboard/components/modals/add-protected-room.tsx -> src/components/modals/add-protected-room.tsx | 0
Rsrc/app/dashboard/components/modals/create-bot.tsx -> src/components/modals/create-bot.tsx | 0
Rsrc/app/dashboard/components/modals/kick-user.tsx -> src/components/modals/kick-user.tsx | 0
18 files changed, 577 insertions(+), 577 deletions(-)

diff --git a/src/app/dashboard/analytics/page.tsx b/src/app/dashboard/analytics/page.tsx @@ -13,7 +13,7 @@ import { BarChart, BarChartItem } from "@/components/analytics/bar-chart" import { HorizontalBarChart } from "@/components/analytics/horizontal-bar-chart" import { useSearchParams } from "next/navigation" import { mockTeams } from "../mockData" -import TabNavigation from "../components/tab-navigation"; +import TabNavigation from "../../../components/dashboard/tab-navigation"; interface InfoCardWithTrendProps { title: string; diff --git a/src/app/dashboard/bans/page.tsx b/src/app/dashboard/bans/page.tsx @@ -7,9 +7,9 @@ import { Input } from "@/components/ui/input"; import { useEffect, useState } from "react"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { EBanTypes, mockPolicyLists, mockTeams, } from "../mockData"; -import AddBan from "../components/modals/add-ban"; +import AddBan from "../../../components/modals/add-ban"; import { useSearchParams } from "next/navigation"; -import TabNavigation from "../components/tab-navigation"; +import TabNavigation from "../../../components/dashboard/tab-navigation"; export default function Bans() { diff --git a/src/app/dashboard/components/analytics-dashboard.tsx b/src/app/dashboard/components/analytics-dashboard.tsx diff --git a/src/app/dashboard/components/dashboard-header.tsx b/src/app/dashboard/components/dashboard-header.tsx @@ -1,387 +0,0 @@ -"use client" - -import { useState, useEffect } from "react" -import Link from "next/link" -import { usePathname, useRouter, useSearchParams } from "next/navigation" -import { Shield, Bell, Menu, Ban, LogOut, Crown, ChevronDown, Plus, X } from "lucide-react" - -import { Button } from "@/components/ui/button" -import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet" -import { Separator } from "@/components/ui/separator" -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" -import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" -import { ScrollArea } from "@/components/ui/scroll-area" -import { useSession } from "@/contexts/session-context" -import AddBan from "./modals/add-ban" -import KickUserModal from "./modals/kick-user" -import CreateBotModal from "./modals/create-bot" -import { mockPolicyLists } from "../mockData" - -interface Notification { - id: string - type: string - title: string - message: string - timestamp: string - read: boolean - teamId?: string -} - -interface Team { - id: string - name: string - description: string -} - -interface DashboardHeaderProps { - teams: Team[] -} - -export function DashboardHeader({ teams }: DashboardHeaderProps) { - const { logout } = useSession() - const pathname = usePathname() - const router = useRouter() - const searchParams = useSearchParams() - - // Get team ID from query params or use default - const teamIdParam = searchParams.get("team") - const [selectedTeam, setSelectedTeam] = useState<Team>(teams.find((t) => t.id === teamIdParam) || teams[0]) - - const policyLists = mockPolicyLists.filter((list) => list.teamId === selectedTeam.id) - - // Determine active tab from the URL - const getActiveTabFromPath = (path: string) => { - if (path.includes("/dashboard/reports")) return "reports" - if (path.includes("/dashboard/bans")) return "bans" - if (path.includes("/dashboard/analytics")) return "analytics" - if (path.includes("/dashboard/settings")) return "settings" - return "overview" - } - - const activeTab = getActiveTabFromPath(pathname) - - // Update selected team when URL param changes - useEffect(() => { - const team = teams.find((t) => t.id === teamIdParam) - if (team) { - setSelectedTeam(team) - } - }, [teamIdParam, teams]) - - const [notifications, setNotifications] = useState<Notification[]>([ - { - id: "notif1", - type: "report", - title: "New report", - message: "A new message has been reported in #general", - timestamp: "2025-04-21T15:30:00Z", - read: false, - teamId: "team1", - }, - { - id: "notif2", - type: "ban", - title: "User banned", - message: "@spammer:matrix.org has been banned by @mod1:matrix.org", - timestamp: "2025-04-21T14:45:00Z", - read: false, - teamId: "team1", - }, - { - id: "notif3", - type: "invite", - title: "Team invitation", - message: "You've been invited to join the Support Team Bot", - timestamp: "2025-04-21T12:15:00Z", - read: true, - teamId: "team2", - }, - ]) - - const teamNotifications = notifications.filter((n) => n.teamId === selectedTeam.id || !n.teamId) - const unreadCount = teamNotifications.filter((n) => !n.read).length - - const markNotificationAsRead = (id: string) => { - setNotifications(notifications.map((n) => (n.id === id ? { ...n, read: true } : n))) - } - - const clearAllNotifications = () => { - setNotifications(notifications.map((n) => ({ ...n, read: true }))) - } - - const deleteNotification = (id: string) => { - setNotifications(notifications.filter((n) => n.id !== id)) - } - - const handleLogout = async () => { - await logout() - } - - const handleTeamChange = (teamId: string) => { - const team = teams.find((t) => t.id === teamId) - if (team) { - setSelectedTeam(team) - - // Update URL with team parameter - const params = new URLSearchParams(searchParams) - params.set("team", teamId) - router.push(`${pathname}?${params.toString()}`) - } - } - - const navigateToTab = (tab: string) => { - const baseUrl = tab === "overview" ? "/dashboard" : `/dashboard/${tab}` - - // Preserve team parameter - const params = new URLSearchParams() - params.set("team", selectedTeam.id) - router.push(`${baseUrl}?${params.toString()}`) - } - - return ( - <header className="sticky top-0 z-10 border-b border-gray-800 bg-black/80 backdrop-blur-sm"> - <div className="container flex h-16 items-center justify-between px-4 md:px-6"> - <div className="flex items-center gap-4"> - <Link href="/" className="flex items-center gap-2"> - <Shield className="h-6 w-6 text-purple-400" /> - <span className="text-xl font-bold tracking-tighter">Draupnir4All</span> - </Link> - - {/* Global Team Selector */} - <DropdownMenu> - <DropdownMenuTrigger asChild> - <Button variant="outline" className="border-purple-500 text-purple-400 hover:bg-purple-950"> - <Crown className="mr-2 h-4 w-4" /> - {selectedTeam.name} - <ChevronDown className="ml-2 h-4 w-4" /> - </Button> - </DropdownMenuTrigger> - <DropdownMenuContent className="w-56 bg-gray-900 border-gray-800"> - <DropdownMenuLabel>Your Bots</DropdownMenuLabel> - <DropdownMenuSeparator className="bg-gray-800" /> - {teams.map((team) => ( - <DropdownMenuItem - key={team.id} - className={selectedTeam.id === team.id ? "bg-gray-800" : ""} - onClick={() => handleTeamChange(team.id)} - > - {team.name} - </DropdownMenuItem> - ))} - <DropdownMenuSeparator className="bg-gray-800" /> - <CreateBotModal /> - </DropdownMenuContent> - </DropdownMenu> - </div> - - <div className="flex items-center gap-4"> - {/* Notifications */} - <Popover> - <PopoverTrigger asChild> - <Button variant="ghost" size="icon" className="relative text-gray-400 hover:text-white"> - <Bell className="h-5 w-5" /> - {unreadCount > 0 && ( - <span className="absolute -top-1 -right-1 flex h-4 w-4 items-center justify-center rounded-full bg-red-500 text-[10px] font-medium text-white"> - {unreadCount} - </span> - )} - </Button> - </PopoverTrigger> - <PopoverContent className="w-80 p-0 bg-gray-900 border-gray-800"> - <div className="flex items-center justify-between border-b border-gray-800 p-3"> - <h3 className="font-medium">Notifications</h3> - <div className="flex items-center gap-1"> - <Button - variant="ghost" - size="sm" - className="h-7 px-2 text-xs text-gray-400 hover:text-white" - onClick={clearAllNotifications} - > - Clear all - </Button> - </div> - </div> - <ScrollArea className="h-[300px]"> - {teamNotifications.length > 0 ? ( - <div className="space-y-1 p-1"> - {teamNotifications.map((notification) => ( - <div - key={notification.id} - className={`flex items-start p-3 rounded-md ${notification.read ? "bg-gray-900" : "bg-gray-800"}`} - onClick={() => markNotificationAsRead(notification.id)} - > - <div - className={`rounded-full p-1.5 mr-3 ${notification.type === "report" - ? "bg-yellow-500/20" - : notification.type === "ban" - ? "bg-red-500/20" - : "bg-blue-500/20" - }`} - > - {notification.type === "report" ? ( - <Bell className="h-3.5 w-3.5 text-yellow-500" /> - ) : notification.type === "ban" ? ( - <Ban className="h-3.5 w-3.5 text-red-500" /> - ) : ( - <Plus className="h-3.5 w-3.5 text-blue-500" /> - )} - </div> - <div className="flex-1 min-w-0"> - <div className="flex items-center justify-between"> - <p className="text-sm font-medium">{notification.title}</p> - <Button - variant="ghost" - size="sm" - className="h-6 w-6 p-0 text-gray-500 hover:text-gray-300" - onClick={(e) => { - e.stopPropagation() - deleteNotification(notification.id) - }} - > - <X className="h-3 w-3" /> - <span className="sr-only">Dismiss</span> - </Button> - </div> - <p className="text-xs text-gray-400 line-clamp-2">{notification.message}</p> - <p className="text-xs text-gray-500 mt-1"> - {new Date(notification.timestamp).toLocaleString()} - </p> - </div> - </div> - ))} - </div> - ) : ( - <div className="flex flex-col items-center justify-center h-full p-6 text-center"> - <Bell className="h-8 w-8 text-gray-600 mb-2" /> - <p className="text-gray-400">No notifications</p> - </div> - )} - </ScrollArea> - </PopoverContent> - </Popover> - - {/* Quick Action Buttons */} - <AddBan header policyLists={policyLists} /> - - <KickUserModal /> - - <Button - variant="outline" - size="sm" - className="hidden md:flex border-purple-500 text-purple-400 hover:bg-purple-950 hover:text-purple-300" - onClick={handleLogout} - > - <LogOut className="mr-2 h-4 w-4" /> - Log Out - </Button> - <Sheet> - <SheetTrigger asChild> - <Button variant="ghost" size="icon" className="md:hidden"> - <Menu className="h-5 w-5" /> - </Button> - </SheetTrigger> - <SheetContent side="right" className="bg-gray-950 border-gray-800 p-0"> - <div className="flex flex-col h-full"> - <div className="border-b border-gray-800 p-4"> - <div className="flex items-center gap-2"> - <Shield className="h-5 w-5 text-purple-400" /> - <span className="font-medium">Draupnir4All</span> - </div> - </div> - <div className="p-4"> - <DropdownMenu> - <DropdownMenuTrigger asChild> - <Button - variant="outline" - className="w-full border-purple-500 text-purple-400 hover:bg-purple-950" - > - <Crown className="mr-2 h-4 w-4" /> - {selectedTeam.name} - <ChevronDown className="ml-2 h-4 w-4" /> - </Button> - </DropdownMenuTrigger> - <DropdownMenuContent className="w-56 bg-gray-900 border-gray-800"> - <DropdownMenuLabel>Your Bots</DropdownMenuLabel> - <DropdownMenuSeparator className="bg-gray-800" /> - {teams.map((team) => ( - <DropdownMenuItem - key={team.id} - className={selectedTeam.id === team.id ? "bg-gray-800" : ""} - onClick={() => handleTeamChange(team.id)} - > - {team.name} - </DropdownMenuItem> - ))} - </DropdownMenuContent> - </DropdownMenu> - </div> - <nav className="flex flex-col p-4 gap-2"> - <Button - variant={activeTab === "overview" ? "secondary" : "ghost"} - className="justify-start" - onClick={() => navigateToTab("overview")} - > - Overview - </Button> - <Button - variant={activeTab === "reports" ? "secondary" : "ghost"} - className="justify-start" - onClick={() => navigateToTab("reports")} - > - Reports - </Button> - <Button - variant={activeTab === "bans" ? "secondary" : "ghost"} - className="justify-start" - onClick={() => navigateToTab("bans")} - > - Bans - </Button> - <Button - variant={activeTab === "analytics" ? "secondary" : "ghost"} - className="justify-start" - onClick={() => navigateToTab("analytics")} - > - Analytics - </Button> - <Button - variant={activeTab === "settings" ? "secondary" : "ghost"} - className="justify-start" - onClick={() => navigateToTab("settings")} - > - Settings - </Button> - - <Separator className="my-2" /> - - {/* Mobile Quick Actions */} - <AddBan header policyLists={policyLists} /> - - <KickUserModal /> - </nav> - <div className="mt-auto border-t border-gray-800 p-4"> - <Button - variant="outline" - size="sm" - className="w-full border-purple-500 text-purple-400 hover:bg-purple-950 hover:text-purple-300" - onClick={handleLogout} - > - <LogOut className="mr-2 h-4 w-4" /> - Log Out - </Button> - </div> - </div> - </SheetContent> - </Sheet> - </div> - </div> - </header> - ) -} diff --git a/src/app/dashboard/components/modals/add-ban.tsx b/src/app/dashboard/components/modals/add-ban.tsx @@ -1,115 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { Checkbox } from "@/components/ui/checkbox"; -import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { Textarea } from "@/components/ui/textarea"; -import { Ban, Plus } from "lucide-react"; -import { PolicyList } from "../../mockData"; - -interface AddBanProps { - policyLists: PolicyList[]; - filled?: boolean; - header?: boolean; -} - -export default function AddBan({ filled, policyLists, header }: AddBanProps) { - return ( - <Dialog> - <DialogTrigger asChild> - {header ? ( - <Button - variant="outline" - size="sm" - className="hidden md:flex border-red-500 text-red-400 hover:bg-red-950 hover:text-red-300" - > - <Ban className="mr-2 h-4 w-4" /> - Add Ban - </Button> - ) : ( - filled ? ( - <Button size="sm" className="bg-purple-600 text-white hover:bg-purple-700"> - <Ban className="mr-2 h-4 w-4" /> - Add Ban - </Button> - ) : ( - <Button variant="ghost" size="sm" className="h-7 px-2 text-xs text-purple-400"> - <Plus className="h-3 w-3 mr-1" /> - Add Ban - </Button> - ) - )} - - </DialogTrigger> - <DialogContent className="bg-gray-950 border-gray-800"> - <DialogHeader> - <DialogTitle>Add New Ban</DialogTitle> - <DialogDescription className="text-gray-400"> - Ban a user or server from your Matrix rooms - </DialogDescription> - </DialogHeader> - <div className="space-y-4 py-4"> - <div className="space-y-2"> - <Label htmlFor="ban-type">Ban Type</Label> - <Select defaultValue="user"> - <SelectTrigger className="bg-gray-900 border-gray-800"> - <SelectValue placeholder="Select ban type" /> - </SelectTrigger> - <SelectContent className="bg-gray-900 border-gray-800"> - <SelectItem value="user">User</SelectItem> - <SelectItem value="server">Server</SelectItem> - <SelectItem value="room">Room</SelectItem> - </SelectContent> - </Select> - </div> - <div className="space-y-2"> - <Label htmlFor="ban-target">Target</Label> - <Input - id="ban-target" - placeholder="@user:matrix.org" - className="bg-gray-900 border-gray-800" - /> - </div> - <div className="space-y-2"> - <Label htmlFor="ban-reason">Reason</Label> - <Textarea - id="ban-reason" - placeholder="Reason for the ban" - className="bg-gray-900 border-gray-800" - /> - </div> - <div className="space-y-2"> - <Label>Policy Lists</Label> - <div className="space-y-2 mt-1"> - {policyLists - .filter((list) => !list.readOnly) - .map((list) => ( - <div key={list.id} className="flex items-center space-x-2"> - <Checkbox id={`list-${list.id}`} /> - <Label htmlFor={`list-${list.id}`} className="font-normal"> - {list.name} - </Label> - </div> - ))} - </div> - </div> - <div className="space-y-2"> - <p className="text-xs text-gray-400"> - Bans are applied to all protected rooms monitored by this bot. - </p> - </div> - </div> - <DialogFooter> - <Button - variant="outline" - className="border-gray-700 text-gray-400 hover:bg-gray-900 hover:text-gray-300" - > - Cancel - </Button> - <Button className="bg-red-600 text-white hover:bg-red-700">Add Ban</Button> - </DialogFooter> - </DialogContent> - </Dialog > - ) -} -\ No newline at end of file diff --git a/src/app/dashboard/components/protected-rooms-list.tsx b/src/app/dashboard/components/protected-rooms-list.tsx @@ -1,25 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { Trash2 } from "lucide-react"; -import { Team } from "../mockData"; - -interface ProtectedRomsListProps { - team: Team; -} - -export default function ProtectedRomsList({ team }: ProtectedRomsListProps) { - return ( - <div className="p-4 space-y-3"> - {team.rooms.map((room, index) => ( - <div key={index} className="flex items-center justify-between p-3 px-4 rounded-md bg-gray-900"> - <div> - <p>{room}</p> - </div> - <Button variant="ghost" size="sm" className="h-8 w-8 p-0 text-gray-400 hover:text-red-400"> - <Trash2 className="h-4 w-4" /> - <span className="sr-only">Remove</span> - </Button> - </div> - ))} - </div> - ) -} -\ No newline at end of file diff --git a/src/app/dashboard/components/tab-navigation.tsx b/src/app/dashboard/components/tab-navigation.tsx @@ -1,35 +0,0 @@ -import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import Link from "next/link"; -import { Team } from "../mockData"; - -interface TabNavigationProps { - selectedTeam: Team; - currentTab: string; -} - -export default function TabNavigation({ selectedTeam, currentTab }: TabNavigationProps) { - return ( - <div className="flex items-center justify-between mb-8"> - <h1 className="text-3xl font-bold tracking-tight">{selectedTeam.name} Dashboard</h1> - <Tabs defaultValue={currentTab} className="hidden md:block"> - <TabsList className="bg-gray-900"> - <TabsTrigger value="overview" asChild> - <Link href={`/dashboard/overview?team=${selectedTeam.id}`}>Overview</Link> - </TabsTrigger> - <TabsTrigger value="reports" asChild> - <Link href={`/dashboard/reports?team=${selectedTeam.id}`}>Reports</Link> - </TabsTrigger> - <TabsTrigger value="bans" asChild> - <Link href={`/dashboard/bans?team=${selectedTeam.id}`}>Bans</Link> - </TabsTrigger> - <TabsTrigger value="analytics" asChild> - <Link href={`/dashboard/analytics?team=${selectedTeam.id}`}>Analytics</Link> - </TabsTrigger> - <TabsTrigger value="settings" asChild> - <Link href={`/dashboard/settings?team=${selectedTeam.id}`}>Settings</Link> - </TabsTrigger> - </TabsList> - </Tabs> - </div> - ) -} -\ No newline at end of file diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx @@ -2,7 +2,7 @@ import type React from "react" import { Suspense } from "react" import { redirect } from "next/navigation" import { getSessionUser } from "@/lib/auth" -import { DashboardHeader } from "./components/dashboard-header" +import { DashboardHeader } from "../../components/dashboard/dashboard-header" // Mock data for teams/bots const mockTeams = [ diff --git a/src/app/dashboard/overview/page.tsx b/src/app/dashboard/overview/page.tsx @@ -5,10 +5,10 @@ import { AlertTriangle, Ban, CheckCircle } from "lucide-react" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { useSearchParams } from "next/navigation" import { useEffect, useState } from "react" -import AddProtectedRoom from "../components/modals/add-protected-room" -import ProtectedRomsList from "../components/protected-rooms-list" +import AddProtectedRoom from "../../../components/modals/add-protected-room" +import ProtectedRomsList from "../../../components/dashboard/protected-rooms-list" import { mockPolicyLists, mockReports, mockTeams } from "../mockData" -import TabNavigation from "../components/tab-navigation"; +import TabNavigation from "../../../components/dashboard/tab-navigation"; export default function OverviewPage() { const searchParams = useSearchParams() diff --git a/src/app/dashboard/reports/page.tsx b/src/app/dashboard/reports/page.tsx @@ -19,7 +19,7 @@ import { } from "@/components/ui/dialog" import { mockReports, mockTeams } from "../mockData"; import { useSearchParams } from "next/navigation" -import TabNavigation from "../components/tab-navigation" +import TabNavigation from "../../../components/dashboard/tab-navigation" export default function ReportsPage() { const searchParams = useSearchParams() diff --git a/src/app/dashboard/settings/page.tsx b/src/app/dashboard/settings/page.tsx @@ -30,10 +30,10 @@ import { ScrollArea } from "@/components/ui/scroll-area" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { useSearchParams } from "next/navigation" import { mockTeams } from "../mockData" -import AddProtectedRoom from "../components/modals/add-protected-room" -import ProtectedRomsList from "../components/protected-rooms-list" -import TabNavigation from "../components/tab-navigation" -import CreateBotModal from "../components/modals/create-bot"; +import AddProtectedRoom from "../../../components/modals/add-protected-room" +import ProtectedRomsList from "../../../components/dashboard/protected-rooms-list" +import TabNavigation from "../../../components/dashboard/tab-navigation" +import CreateBotModal from "../../../components/modals/create-bot"; export default function TeamManagement() { diff --git a/src/components/dashboard/dashboard-header.tsx b/src/components/dashboard/dashboard-header.tsx @@ -0,0 +1,387 @@ +"use client" + +import { useState, useEffect } from "react" +import Link from "next/link" +import { usePathname, useRouter, useSearchParams } from "next/navigation" +import { Shield, Bell, Menu, Ban, LogOut, Crown, ChevronDown, Plus, X } from "lucide-react" + +import { Button } from "@/components/ui/button" +import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet" +import { Separator } from "@/components/ui/separator" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" +import { ScrollArea } from "@/components/ui/scroll-area" +import { useSession } from "@/contexts/session-context" +import AddBan from "../modals/add-ban" +import KickUserModal from "../modals/kick-user" +import CreateBotModal from "../modals/create-bot" +import { mockPolicyLists } from "../../app/dashboard/mockData" + +interface Notification { + id: string + type: string + title: string + message: string + timestamp: string + read: boolean + teamId?: string +} + +interface Team { + id: string + name: string + description: string +} + +interface DashboardHeaderProps { + teams: Team[] +} + +export function DashboardHeader({ teams }: DashboardHeaderProps) { + const { logout } = useSession() + const pathname = usePathname() + const router = useRouter() + const searchParams = useSearchParams() + + // Get team ID from query params or use default + const teamIdParam = searchParams.get("team") + const [selectedTeam, setSelectedTeam] = useState<Team>(teams.find((t) => t.id === teamIdParam) || teams[0]) + + const policyLists = mockPolicyLists.filter((list) => list.teamId === selectedTeam.id) + + // Determine active tab from the URL + const getActiveTabFromPath = (path: string) => { + if (path.includes("/dashboard/reports")) return "reports" + if (path.includes("/dashboard/bans")) return "bans" + if (path.includes("/dashboard/analytics")) return "analytics" + if (path.includes("/dashboard/settings")) return "settings" + return "overview" + } + + const activeTab = getActiveTabFromPath(pathname) + + // Update selected team when URL param changes + useEffect(() => { + const team = teams.find((t) => t.id === teamIdParam) + if (team) { + setSelectedTeam(team) + } + }, [teamIdParam, teams]) + + const [notifications, setNotifications] = useState<Notification[]>([ + { + id: "notif1", + type: "report", + title: "New report", + message: "A new message has been reported in #general", + timestamp: "2025-04-21T15:30:00Z", + read: false, + teamId: "team1", + }, + { + id: "notif2", + type: "ban", + title: "User banned", + message: "@spammer:matrix.org has been banned by @mod1:matrix.org", + timestamp: "2025-04-21T14:45:00Z", + read: false, + teamId: "team1", + }, + { + id: "notif3", + type: "invite", + title: "Team invitation", + message: "You've been invited to join the Support Team Bot", + timestamp: "2025-04-21T12:15:00Z", + read: true, + teamId: "team2", + }, + ]) + + const teamNotifications = notifications.filter((n) => n.teamId === selectedTeam.id || !n.teamId) + const unreadCount = teamNotifications.filter((n) => !n.read).length + + const markNotificationAsRead = (id: string) => { + setNotifications(notifications.map((n) => (n.id === id ? { ...n, read: true } : n))) + } + + const clearAllNotifications = () => { + setNotifications(notifications.map((n) => ({ ...n, read: true }))) + } + + const deleteNotification = (id: string) => { + setNotifications(notifications.filter((n) => n.id !== id)) + } + + const handleLogout = async () => { + await logout() + } + + const handleTeamChange = (teamId: string) => { + const team = teams.find((t) => t.id === teamId) + if (team) { + setSelectedTeam(team) + + // Update URL with team parameter + const params = new URLSearchParams(searchParams) + params.set("team", teamId) + router.push(`${pathname}?${params.toString()}`) + } + } + + const navigateToTab = (tab: string) => { + const baseUrl = tab === "overview" ? "/dashboard" : `/dashboard/${tab}` + + // Preserve team parameter + const params = new URLSearchParams() + params.set("team", selectedTeam.id) + router.push(`${baseUrl}?${params.toString()}`) + } + + return ( + <header className="sticky top-0 z-10 border-b border-gray-800 bg-black/80 backdrop-blur-sm"> + <div className="container flex h-16 items-center justify-between px-4 md:px-6"> + <div className="flex items-center gap-4"> + <Link href="/" className="flex items-center gap-2"> + <Shield className="h-6 w-6 text-purple-400" /> + <span className="text-xl font-bold tracking-tighter">Draupnir4All</span> + </Link> + + {/* Global Team Selector */} + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="outline" className="border-purple-500 text-purple-400 hover:bg-purple-950"> + <Crown className="mr-2 h-4 w-4" /> + {selectedTeam.name} + <ChevronDown className="ml-2 h-4 w-4" /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent className="w-56 bg-gray-900 border-gray-800"> + <DropdownMenuLabel>Your Bots</DropdownMenuLabel> + <DropdownMenuSeparator className="bg-gray-800" /> + {teams.map((team) => ( + <DropdownMenuItem + key={team.id} + className={selectedTeam.id === team.id ? "bg-gray-800" : ""} + onClick={() => handleTeamChange(team.id)} + > + {team.name} + </DropdownMenuItem> + ))} + <DropdownMenuSeparator className="bg-gray-800" /> + <CreateBotModal /> + </DropdownMenuContent> + </DropdownMenu> + </div> + + <div className="flex items-center gap-4"> + {/* Notifications */} + <Popover> + <PopoverTrigger asChild> + <Button variant="ghost" size="icon" className="relative text-gray-400 hover:text-white"> + <Bell className="h-5 w-5" /> + {unreadCount > 0 && ( + <span className="absolute -top-1 -right-1 flex h-4 w-4 items-center justify-center rounded-full bg-red-500 text-[10px] font-medium text-white"> + {unreadCount} + </span> + )} + </Button> + </PopoverTrigger> + <PopoverContent className="w-80 p-0 bg-gray-900 border-gray-800"> + <div className="flex items-center justify-between border-b border-gray-800 p-3"> + <h3 className="font-medium">Notifications</h3> + <div className="flex items-center gap-1"> + <Button + variant="ghost" + size="sm" + className="h-7 px-2 text-xs text-gray-400 hover:text-white" + onClick={clearAllNotifications} + > + Clear all + </Button> + </div> + </div> + <ScrollArea className="h-[300px]"> + {teamNotifications.length > 0 ? ( + <div className="space-y-1 p-1"> + {teamNotifications.map((notification) => ( + <div + key={notification.id} + className={`flex items-start p-3 rounded-md ${notification.read ? "bg-gray-900" : "bg-gray-800"}`} + onClick={() => markNotificationAsRead(notification.id)} + > + <div + className={`rounded-full p-1.5 mr-3 ${notification.type === "report" + ? "bg-yellow-500/20" + : notification.type === "ban" + ? "bg-red-500/20" + : "bg-blue-500/20" + }`} + > + {notification.type === "report" ? ( + <Bell className="h-3.5 w-3.5 text-yellow-500" /> + ) : notification.type === "ban" ? ( + <Ban className="h-3.5 w-3.5 text-red-500" /> + ) : ( + <Plus className="h-3.5 w-3.5 text-blue-500" /> + )} + </div> + <div className="flex-1 min-w-0"> + <div className="flex items-center justify-between"> + <p className="text-sm font-medium">{notification.title}</p> + <Button + variant="ghost" + size="sm" + className="h-6 w-6 p-0 text-gray-500 hover:text-gray-300" + onClick={(e) => { + e.stopPropagation() + deleteNotification(notification.id) + }} + > + <X className="h-3 w-3" /> + <span className="sr-only">Dismiss</span> + </Button> + </div> + <p className="text-xs text-gray-400 line-clamp-2">{notification.message}</p> + <p className="text-xs text-gray-500 mt-1"> + {new Date(notification.timestamp).toLocaleString()} + </p> + </div> + </div> + ))} + </div> + ) : ( + <div className="flex flex-col items-center justify-center h-full p-6 text-center"> + <Bell className="h-8 w-8 text-gray-600 mb-2" /> + <p className="text-gray-400">No notifications</p> + </div> + )} + </ScrollArea> + </PopoverContent> + </Popover> + + {/* Quick Action Buttons */} + <AddBan header policyLists={policyLists} /> + + <KickUserModal /> + + <Button + variant="outline" + size="sm" + className="hidden md:flex border-purple-500 text-purple-400 hover:bg-purple-950 hover:text-purple-300" + onClick={handleLogout} + > + <LogOut className="mr-2 h-4 w-4" /> + Log Out + </Button> + <Sheet> + <SheetTrigger asChild> + <Button variant="ghost" size="icon" className="md:hidden"> + <Menu className="h-5 w-5" /> + </Button> + </SheetTrigger> + <SheetContent side="right" className="bg-gray-950 border-gray-800 p-0"> + <div className="flex flex-col h-full"> + <div className="border-b border-gray-800 p-4"> + <div className="flex items-center gap-2"> + <Shield className="h-5 w-5 text-purple-400" /> + <span className="font-medium">Draupnir4All</span> + </div> + </div> + <div className="p-4"> + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button + variant="outline" + className="w-full border-purple-500 text-purple-400 hover:bg-purple-950" + > + <Crown className="mr-2 h-4 w-4" /> + {selectedTeam.name} + <ChevronDown className="ml-2 h-4 w-4" /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent className="w-56 bg-gray-900 border-gray-800"> + <DropdownMenuLabel>Your Bots</DropdownMenuLabel> + <DropdownMenuSeparator className="bg-gray-800" /> + {teams.map((team) => ( + <DropdownMenuItem + key={team.id} + className={selectedTeam.id === team.id ? "bg-gray-800" : ""} + onClick={() => handleTeamChange(team.id)} + > + {team.name} + </DropdownMenuItem> + ))} + </DropdownMenuContent> + </DropdownMenu> + </div> + <nav className="flex flex-col p-4 gap-2"> + <Button + variant={activeTab === "overview" ? "secondary" : "ghost"} + className="justify-start" + onClick={() => navigateToTab("overview")} + > + Overview + </Button> + <Button + variant={activeTab === "reports" ? "secondary" : "ghost"} + className="justify-start" + onClick={() => navigateToTab("reports")} + > + Reports + </Button> + <Button + variant={activeTab === "bans" ? "secondary" : "ghost"} + className="justify-start" + onClick={() => navigateToTab("bans")} + > + Bans + </Button> + <Button + variant={activeTab === "analytics" ? "secondary" : "ghost"} + className="justify-start" + onClick={() => navigateToTab("analytics")} + > + Analytics + </Button> + <Button + variant={activeTab === "settings" ? "secondary" : "ghost"} + className="justify-start" + onClick={() => navigateToTab("settings")} + > + Settings + </Button> + + <Separator className="my-2" /> + + {/* Mobile Quick Actions */} + <AddBan header policyLists={policyLists} /> + + <KickUserModal /> + </nav> + <div className="mt-auto border-t border-gray-800 p-4"> + <Button + variant="outline" + size="sm" + className="w-full border-purple-500 text-purple-400 hover:bg-purple-950 hover:text-purple-300" + onClick={handleLogout} + > + <LogOut className="mr-2 h-4 w-4" /> + Log Out + </Button> + </div> + </div> + </SheetContent> + </Sheet> + </div> + </div> + </header> + ) +} diff --git a/src/components/dashboard/protected-rooms-list.tsx b/src/components/dashboard/protected-rooms-list.tsx @@ -0,0 +1,25 @@ +import { Button } from "@/components/ui/button"; +import { Trash2 } from "lucide-react"; +import { Team } from "../../app/dashboard/mockData"; + +interface ProtectedRomsListProps { + team: Team; +} + +export default function ProtectedRomsList({ team }: ProtectedRomsListProps) { + return ( + <div className="p-4 space-y-3"> + {team.rooms.map((room, index) => ( + <div key={index} className="flex items-center justify-between p-3 px-4 rounded-md bg-gray-900"> + <div> + <p>{room}</p> + </div> + <Button variant="ghost" size="sm" className="h-8 w-8 p-0 text-gray-400 hover:text-red-400"> + <Trash2 className="h-4 w-4" /> + <span className="sr-only">Remove</span> + </Button> + </div> + ))} + </div> + ) +} +\ No newline at end of file diff --git a/src/components/dashboard/tab-navigation.tsx b/src/components/dashboard/tab-navigation.tsx @@ -0,0 +1,35 @@ +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import Link from "next/link"; +import { Team } from "../../app/dashboard/mockData"; + +interface TabNavigationProps { + selectedTeam: Team; + currentTab: string; +} + +export default function TabNavigation({ selectedTeam, currentTab }: TabNavigationProps) { + return ( + <div className="flex items-center justify-between mb-8"> + <h1 className="text-3xl font-bold tracking-tight">{selectedTeam.name} Dashboard</h1> + <Tabs defaultValue={currentTab} className="hidden md:block"> + <TabsList className="bg-gray-900"> + <TabsTrigger value="overview" asChild> + <Link href={`/dashboard/overview?team=${selectedTeam.id}`}>Overview</Link> + </TabsTrigger> + <TabsTrigger value="reports" asChild> + <Link href={`/dashboard/reports?team=${selectedTeam.id}`}>Reports</Link> + </TabsTrigger> + <TabsTrigger value="bans" asChild> + <Link href={`/dashboard/bans?team=${selectedTeam.id}`}>Bans</Link> + </TabsTrigger> + <TabsTrigger value="analytics" asChild> + <Link href={`/dashboard/analytics?team=${selectedTeam.id}`}>Analytics</Link> + </TabsTrigger> + <TabsTrigger value="settings" asChild> + <Link href={`/dashboard/settings?team=${selectedTeam.id}`}>Settings</Link> + </TabsTrigger> + </TabsList> + </Tabs> + </div> + ) +} +\ No newline at end of file diff --git a/src/components/modals/add-ban.tsx b/src/components/modals/add-ban.tsx @@ -0,0 +1,115 @@ +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; +import { Ban, Plus } from "lucide-react"; +import { PolicyList } from "../../app/dashboard/mockData"; + +interface AddBanProps { + policyLists: PolicyList[]; + filled?: boolean; + header?: boolean; +} + +export default function AddBan({ filled, policyLists, header }: AddBanProps) { + return ( + <Dialog> + <DialogTrigger asChild> + {header ? ( + <Button + variant="outline" + size="sm" + className="hidden md:flex border-red-500 text-red-400 hover:bg-red-950 hover:text-red-300" + > + <Ban className="mr-2 h-4 w-4" /> + Add Ban + </Button> + ) : ( + filled ? ( + <Button size="sm" className="bg-purple-600 text-white hover:bg-purple-700"> + <Ban className="mr-2 h-4 w-4" /> + Add Ban + </Button> + ) : ( + <Button variant="ghost" size="sm" className="h-7 px-2 text-xs text-purple-400"> + <Plus className="h-3 w-3 mr-1" /> + Add Ban + </Button> + ) + )} + + </DialogTrigger> + <DialogContent className="bg-gray-950 border-gray-800"> + <DialogHeader> + <DialogTitle>Add New Ban</DialogTitle> + <DialogDescription className="text-gray-400"> + Ban a user or server from your Matrix rooms + </DialogDescription> + </DialogHeader> + <div className="space-y-4 py-4"> + <div className="space-y-2"> + <Label htmlFor="ban-type">Ban Type</Label> + <Select defaultValue="user"> + <SelectTrigger className="bg-gray-900 border-gray-800"> + <SelectValue placeholder="Select ban type" /> + </SelectTrigger> + <SelectContent className="bg-gray-900 border-gray-800"> + <SelectItem value="user">User</SelectItem> + <SelectItem value="server">Server</SelectItem> + <SelectItem value="room">Room</SelectItem> + </SelectContent> + </Select> + </div> + <div className="space-y-2"> + <Label htmlFor="ban-target">Target</Label> + <Input + id="ban-target" + placeholder="@user:matrix.org" + className="bg-gray-900 border-gray-800" + /> + </div> + <div className="space-y-2"> + <Label htmlFor="ban-reason">Reason</Label> + <Textarea + id="ban-reason" + placeholder="Reason for the ban" + className="bg-gray-900 border-gray-800" + /> + </div> + <div className="space-y-2"> + <Label>Policy Lists</Label> + <div className="space-y-2 mt-1"> + {policyLists + .filter((list) => !list.readOnly) + .map((list) => ( + <div key={list.id} className="flex items-center space-x-2"> + <Checkbox id={`list-${list.id}`} /> + <Label htmlFor={`list-${list.id}`} className="font-normal"> + {list.name} + </Label> + </div> + ))} + </div> + </div> + <div className="space-y-2"> + <p className="text-xs text-gray-400"> + Bans are applied to all protected rooms monitored by this bot. + </p> + </div> + </div> + <DialogFooter> + <Button + variant="outline" + className="border-gray-700 text-gray-400 hover:bg-gray-900 hover:text-gray-300" + > + Cancel + </Button> + <Button className="bg-red-600 text-white hover:bg-red-700">Add Ban</Button> + </DialogFooter> + </DialogContent> + </Dialog > + ) +} +\ No newline at end of file diff --git a/src/app/dashboard/components/modals/add-protected-room.tsx b/src/components/modals/add-protected-room.tsx diff --git a/src/app/dashboard/components/modals/create-bot.tsx b/src/components/modals/create-bot.tsx diff --git a/src/app/dashboard/components/modals/kick-user.tsx b/src/components/modals/kick-user.tsx