commit 6642ad66811b17217a2cea79b06f2b5319c42f06
parent ae190fad46e1af5baa05ef955e8c3a1e4b116a15
Author: MTRNord <mtrnord1@gmail.com>
Date: Sat, 26 Apr 2025 15:49:56 +0200
Make dashboard tabs actual pages
Signed-off-by: MTRNord <mtrnord1@gmail.com>
Diffstat:
21 files changed, 1862 insertions(+), 1673 deletions(-)
diff --git a/src/app/dashboard/analytics/page.tsx b/src/app/dashboard/analytics/page.tsx
@@ -0,0 +1,370 @@
+"use client";
+
+import { useEffect, useState } from "react"
+import { TrendingUp } from "lucide-react"
+
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
+import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
+
+import { PieChart } from "@/components/analytics/pie-chart"
+import { Heatmap } from "@/components/analytics/heatmap"
+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";
+
+interface InfoCardWithTrendProps {
+ title: string;
+ changePercent: number;
+ value: string | number;
+ trendColorOverride?: string;
+}
+
+function InfoCardWithTrend({ title, changePercent, value, trendColorOverride }: InfoCardWithTrendProps) {
+ const trendColor = changePercent > 0 ? "text-green-400" : "text-red-400"
+ const trendIcon = changePercent > 0 ? <TrendingUp className="h-3.5 w-3.5 mr-1" /> : <TrendingUp className="h-3.5 w-3.5 mr-1 rotate-180" />
+ const trendText = changePercent > 0 ? `+${changePercent}% from previous period` : `${changePercent}% from previous period`
+ const trendClass = trendColorOverride || trendColor
+
+ return (
+ <Card className="border-gray-800 bg-gray-950">
+ <CardHeader className="pb-2">
+ <CardTitle className="text-sm font-medium">{title}</CardTitle>
+ </CardHeader>
+ <CardContent>
+ <div className="text-2xl font-bold">{value}</div>
+ <div className={`flex items-center text-xs ${trendClass}`}>
+ {trendIcon}
+ <span>{trendText}</span>
+ </div>
+ </CardContent>
+ </Card>
+ )
+}
+
+// Mock data for heatmaps
+const generateHeatmapData = (intensity = 1, seed = 42) => {
+ const today = new Date()
+ const data = []
+
+ // Generate data for the past year (52 weeks)
+ for (let i = 0; i < 364; i++) {
+ const date = new Date(today)
+ date.setDate(today.getDate() - i)
+
+ // Use a simple algorithm to generate semi-random but consistent data
+ const dayOfYear = Math.floor((date.getTime() - new Date(date.getFullYear(), 0, 0).getTime()) / 86400000)
+ const value = Math.floor(Math.sin(dayOfYear * 0.1 + seed) * Math.cos(date.getMonth() * 0.3 + seed) * 10 * intensity)
+
+ data.push({
+ date: date.toISOString().split("T")[0],
+ count: Math.max(0, value),
+ })
+ }
+
+ // Sort by date (oldest first)
+ return data.reverse()
+}
+
+// Generate mock data
+const reportsHeatmapData = generateHeatmapData(1.5, 42)
+const bansHeatmapData = generateHeatmapData(1, 123)
+
+// Mock data for charts
+const roomActivityData = [
+ { name: "#general:matrix.org", reports: 42, bans: 15 },
+ { name: "#support:matrix.org", reports: 28, bans: 9 },
+ { name: "#community:matrix.org", reports: 17, bans: 5 },
+ { name: "#development:matrix.org", reports: 8, bans: 2 },
+ { name: "#random:matrix.org", reports: 5, bans: 1 },
+]
+
+const reportTypesData = [
+ { id: 1, label: "Spam", value: 45, color: "#f97316" },
+ { id: 2, label: "Harassment", value: 32, color: "#ef4444" },
+ { id: 3, label: "Inappropriate Content", value: 28, color: "#ec4899" },
+ { id: 4, label: "Scam", value: 15, color: "#f59e0b" },
+ { id: 5, label: "Other", value: 10, color: "#6366f1" },
+]
+
+const bannedServersData = [
+ { label: "spam.org", value: 28, secondaryLabel: "28 users" },
+ { label: "scam.net", value: 17, secondaryLabel: "17 users" },
+ { label: "badactor.io", value: 12, secondaryLabel: "12 users" },
+ { label: "malicious.com", value: 9, secondaryLabel: "9 users" },
+ { label: "troll.xyz", value: 7, secondaryLabel: "7 users" },
+]
+
+// Enhanced monthly activity data with more realistic patterns
+const monthlyActivityData = [
+ { month: "Jan", reports: 24, bans: 8, year: 2025 },
+ { month: "Feb", reports: 32, bans: 12, year: 2025 },
+ { month: "Mar", reports: 45, bans: 18, year: 2025 },
+ { month: "Apr", reports: 38, bans: 15, year: 2025 },
+ { month: "May", reports: 29, bans: 11, year: 2025 },
+ { month: "Jun", reports: 35, bans: 14, year: 2025 },
+ { month: "Jul", reports: 42, bans: 17, year: 2025 },
+ { month: "Aug", reports: 50, bans: 22, year: 2025 },
+ { month: "Sep", reports: 43, bans: 19, year: 2025 },
+ { month: "Oct", reports: 37, bans: 16, year: 2025 },
+ { month: "Nov", reports: 31, bans: 13, year: 2025 },
+ { month: "Dec", reports: 28, bans: 10, year: 2025 },
+]
+
+// Transform monthly data for the BarChart component
+const monthlyBarChartData: BarChartItem[] = monthlyActivityData.map((item) => ({
+ label: `${item.month}`,
+ values: [
+ { value: item.reports, color: "#9333ea", label: "Reports" },
+ { value: item.bans, color: "#dc2626", label: "Bans" },
+ ],
+}))
+
+export default function AnalyticsDashboard() {
+ const searchParams = useSearchParams()
+ const teamIdParam = searchParams.get("team")
+ const [selectedTeam, setSelectedTeam] = useState(mockTeams.find((t) => t.id === teamIdParam) || mockTeams[0])
+
+
+ // Update selected team when URL param changes
+ useEffect(() => {
+ const team = mockTeams.find((t) => t.id === teamIdParam)
+ if (team) {
+ setSelectedTeam(team)
+ }
+ }, [teamIdParam])
+
+ const [timeRange, setTimeRange] = useState("year")
+ const [heatmapType, setHeatmapType] = useState("reports")
+
+
+ const reportChangePercent = 12
+
+ return (
+ <>
+ <TabNavigation selectedTeam={selectedTeam} currentTab="analytics" />
+ <div className="space-y-4">
+ <div className="flex items-center justify-between">
+ <h2 className="text-2xl font-bold">{selectedTeam.name} Analytics</h2>
+ <div className="flex items-center gap-2">
+ <Select value={timeRange} onValueChange={setTimeRange}>
+ <SelectTrigger className="w-[120px] h-8 bg-gray-900 border-gray-800">
+ <SelectValue placeholder="Time Range" />
+ </SelectTrigger>
+ <SelectContent className="bg-gray-900 border-gray-800">
+ <SelectItem value="month">Past Month</SelectItem>
+ <SelectItem value="quarter">Past Quarter</SelectItem>
+ <SelectItem value="year">Past Year</SelectItem>
+ <SelectItem value="all">All Time</SelectItem>
+ </SelectContent>
+ </Select>
+
+ </div>
+ </div>
+
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
+ <InfoCardWithTrend title="Total Reports" changePercent={12} value={247} trendColorOverride={reportChangePercent > 0 ? "text-red-400" : "text-green-400"} />
+ <InfoCardWithTrend title="Bans Issued" changePercent={5} value={98} />
+ <InfoCardWithTrend title="Unique Reporters" changePercent={8} value={42} />
+ <InfoCardWithTrend title="Avg. Response Time" changePercent={-15} value="1.4h" />
+ </div>
+
+ <Card className="border-gray-800 bg-gray-950">
+ <CardHeader>
+ <div className="flex items-center justify-between">
+ <div>
+ <CardTitle>Activity Heatmap</CardTitle>
+ <CardDescription className="text-gray-400">
+ Visualization of moderation activity over time
+ </CardDescription>
+ </div>
+ <Tabs value={heatmapType} onValueChange={setHeatmapType}>
+ <TabsList className="bg-gray-900">
+ <TabsTrigger value="reports" className="text-xs">
+ Reports
+ </TabsTrigger>
+ <TabsTrigger value="bans" className="text-xs">
+ Bans
+ </TabsTrigger>
+ </TabsList>
+ </Tabs>
+ </div>
+ </CardHeader>
+ <CardContent className="overflow-hidden">
+ <Heatmap
+ data={heatmapType === "reports" ? reportsHeatmapData : bansHeatmapData}
+ title={`${heatmapType === "reports" ? "Reports Activity" : "Ban Actions"} (Past Year)`}
+ colorIntensityLabel={heatmapType === "reports" ? "reports" : "bans"}
+ />
+ </CardContent>
+ </Card>
+
+ <div className="grid gap-4 md:grid-cols-2">
+ <Card className="border-gray-800 bg-gray-950">
+ <CardHeader>
+ <CardTitle>Room Activity</CardTitle>
+ <CardDescription className="text-gray-400">Reports and bans by room</CardDescription>
+ </CardHeader>
+ <CardContent>
+ <div className="space-y-4">
+ {roomActivityData.map((room, index) => (
+ <div key={index} className="space-y-2">
+ <div className="flex items-center justify-between">
+ <span className="text-sm font-medium">{room.name}</span>
+ <span className="text-xs text-gray-400">{room.reports + room.bans} actions</span>
+ </div>
+ <div className="flex h-2 overflow-hidden rounded-full bg-gray-900">
+ <div
+ className="bg-purple-600"
+ style={{ width: `${(room.reports / (room.reports + room.bans)) * 100}%` }}
+ title={`${room.reports} reports`}
+ ></div>
+ <div
+ className="bg-red-600"
+ style={{ width: `${(room.bans / (room.reports + room.bans)) * 100}%` }}
+ title={`${room.bans} bans`}
+ ></div>
+ </div>
+ <div className="flex items-center justify-between text-xs text-gray-400">
+ <div className="flex items-center gap-1">
+ <div className="h-2 w-2 rounded-full bg-purple-600"></div>
+ <span>{room.reports} reports</span>
+ </div>
+ <div className="flex items-center gap-1">
+ <div className="h-2 w-2 rounded-full bg-red-600"></div>
+ <span>{room.bans} bans</span>
+ </div>
+ </div>
+ </div>
+ ))}
+ </div>
+ </CardContent>
+ </Card>
+
+ <Card className="border-gray-800 bg-gray-950">
+ <CardHeader>
+ <CardTitle>Report Types</CardTitle>
+ <CardDescription className="text-gray-400">Distribution of report reasons</CardDescription>
+ </CardHeader>
+ <CardContent>
+ <PieChart data={reportTypesData} centerLabel="Report Types" />
+ </CardContent>
+ </Card>
+ </div>
+
+ <div className="grid gap-4 md:grid-cols-2">
+ <Card className="border-gray-800 bg-gray-950">
+ <CardHeader>
+ <CardTitle>Monthly Activity</CardTitle>
+ <CardDescription className="text-gray-400">Reports and bans over the past year</CardDescription>
+ </CardHeader>
+ <CardContent>
+ <BarChart data={monthlyBarChartData} height={200} />
+ </CardContent>
+ </Card>
+
+ <Card className="border-gray-800 bg-gray-950">
+ <CardHeader>
+ <CardTitle>Most Banned Servers</CardTitle>
+ <CardDescription className="text-gray-400">Servers with the most banned users</CardDescription>
+ </CardHeader>
+ <CardContent>
+ <HorizontalBarChart data={bannedServersData} color="#dc2626" />
+ </CardContent>
+ </Card>
+ </div>
+
+ <Card className="border-gray-800 bg-gray-950">
+ <CardHeader>
+ <CardTitle>Moderation Efficiency</CardTitle>
+ <CardDescription className="text-gray-400">
+ Key performance indicators for your moderation team
+ </CardDescription>
+ </CardHeader>
+ <CardContent>
+ <div className="grid gap-4 md:grid-cols-3">
+ <div className="space-y-2">
+ <h3 className="text-sm font-medium text-gray-400">Average Response Time</h3>
+ <div className="flex items-end gap-3">
+ <span className="text-2xl font-bold">1.4h</span>
+ <span className="text-xs text-green-400">-15% from last month</span>
+ </div>
+ <p className="text-xs text-gray-400">Time between report creation and moderator action</p>
+ </div>
+
+ <div className="space-y-2">
+ <h3 className="text-sm font-medium text-gray-400">Resolution Rate</h3>
+ <div className="flex items-end gap-3">
+ <span className="text-2xl font-bold">92%</span>
+ <span className="text-xs text-green-400">+3% from last month</span>
+ </div>
+ <p className="text-xs text-gray-400">Percentage of reports that were resolved</p>
+ </div>
+
+ <div className="space-y-2">
+ <h3 className="text-sm font-medium text-gray-400">Moderator Activity</h3>
+ <div className="flex items-end gap-3">
+ <span className="text-2xl font-bold">4.2</span>
+ <span className="text-xs text-gray-400">actions per day</span>
+ </div>
+ <p className="text-xs text-gray-400">Average number of moderation actions per day</p>
+ </div>
+ </div>
+
+ <div className="mt-6">
+ <h3 className="mb-2 text-sm font-medium text-gray-400">Moderator Performance</h3>
+ <div className="space-y-3">
+ <div className="flex items-center gap-3">
+ <div className="flex h-8 w-8 items-center justify-center rounded-full bg-purple-900 text-xs font-medium">
+ AM
+ </div>
+ <div className="flex-1">
+ <div className="flex items-center justify-between">
+ <span className="text-sm">@admin:matrix.org</span>
+ <span className="text-xs text-gray-400">42 actions</span>
+ </div>
+ <div className="mt-1 h-1.5 w-full overflow-hidden rounded-full bg-gray-900">
+ <div className="h-full rounded-full bg-purple-600" style={{ width: "85%" }}></div>
+ </div>
+ </div>
+ </div>
+
+ <div className="flex items-center gap-3">
+ <div className="flex h-8 w-8 items-center justify-center rounded-full bg-blue-900 text-xs font-medium">
+ M1
+ </div>
+ <div className="flex-1">
+ <div className="flex items-center justify-between">
+ <span className="text-sm">@mod1:matrix.org</span>
+ <span className="text-xs text-gray-400">36 actions</span>
+ </div>
+ <div className="mt-1 h-1.5 w-full overflow-hidden rounded-full bg-gray-900">
+ <div className="h-full rounded-full bg-blue-600" style={{ width: "72%" }}></div>
+ </div>
+ </div>
+ </div>
+
+ <div className="flex items-center gap-3">
+ <div className="flex h-8 w-8 items-center justify-center rounded-full bg-green-900 text-xs font-medium">
+ M2
+ </div>
+ <div className="flex-1">
+ <div className="flex items-center justify-between">
+ <span className="text-sm">@mod2:matrix.org</span>
+ <span className="text-xs text-gray-400">28 actions</span>
+ </div>
+ <div className="mt-1 h-1.5 w-full overflow-hidden rounded-full bg-gray-900">
+ <div className="h-full rounded-full bg-green-600" style={{ width: "56%" }}></div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </CardContent>
+ </Card>
+ </div>
+ </>
+ )
+}
diff --git a/src/app/dashboard/bans/page.tsx b/src/app/dashboard/bans/page.tsx
@@ -0,0 +1,153 @@
+"use client";
+import { ScrollArea } from "@/components/ui/scroll-area"
+import { Ban, PenOff, Pen } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+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 { useSearchParams } from "next/navigation";
+import TabNavigation from "../components/tab-navigation";
+
+
+export default function Bans() {
+ const searchParams = useSearchParams()
+ const teamIdParam = searchParams.get("team")
+ const [selectedTeam, setSelectedTeam] = useState(mockTeams.find((t) => t.id === teamIdParam) || mockTeams[0])
+
+ // Update selected team when URL param changes
+ useEffect(() => {
+ const team = mockTeams.find((t) => t.id === teamIdParam)
+ if (team) {
+ setSelectedTeam(team)
+ }
+ }, [teamIdParam])
+
+ // Filter policy lists based on selected team
+ const policyLists = mockPolicyLists.filter((list) => list.teamId === selectedTeam.id)
+
+ const [selectedPolicyList, setSelectedPolicyList] = useState("global")
+ const [filter, setFilter] = useState("all")
+ const selectedList = policyLists.find((list) => list.id === selectedPolicyList) || policyLists[0] || { entries: [] }
+ const entries = selectedList.entries.filter((entry) => {
+ if (filter === "all") return true;
+ if (filter === "users") return entry.type === EBanTypes.User;
+ if (filter === "servers") return entry.type === EBanTypes.Server;
+ if (filter === "rooms") return entry.type === EBanTypes.Room;
+ return false;
+ });
+
+ return (
+ <>
+ <TabNavigation selectedTeam={selectedTeam} currentTab="bans" />
+
+ <Card className="border-gray-800 bg-gray-950">
+ <CardHeader className="pb-2">
+ <div className="flex items-center justify-between">
+ <CardTitle>Policy Lists</CardTitle>
+ <AddBan policyLists={policyLists} filled />
+ </div>
+ <CardDescription className="text-gray-400">Manage ban lists across your Matrix rooms</CardDescription>
+ </CardHeader>
+ <CardContent>
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
+ <div className="space-y-4">
+ <h3 className="text-sm font-medium text-gray-400">Policy Lists</h3>
+ <div className="space-y-2">
+ {policyLists.length > 0 ? (
+ policyLists.map((list) => (
+ <div
+ key={list.id}
+ className={`p-3 rounded-md border cursor-pointer transition-colors ${selectedPolicyList === list.id
+ ? "bg-gray-800 border-purple-600"
+ : "bg-gray-900 border-gray-800 hover:border-gray-700"
+ }`}
+ onClick={() => setSelectedPolicyList(list.id)}
+ >
+ <div className="flex items-center justify-between mb-1">
+ <span className="font-medium">{list.name}</span>
+ {list.readOnly ? (
+ <PenOff className=" text-gray-400">Read Only</PenOff>
+ ) : (
+ <Pen className="text-green-300">Editable</Pen>
+ )}
+ </div>
+ <p className="text-xs text-gray-400">{list.description}</p>
+ <p className="text-xs text-gray-500 mt-1">{list.entries.length} entries</p>
+ </div>
+ ))
+ ) : (
+ <div className="flex flex-col items-center justify-center p-8 text-center">
+ <Ban className="h-8 w-8 text-gray-500 mb-2" />
+ <p className="text-gray-400">No policy lists available</p>
+ </div>
+ )}
+ </div>
+ </div>
+
+ <div className="md:col-span-2 space-y-4">
+ <div className="flex items-center justify-between">
+ <h3 className="text-sm font-medium text-gray-400">
+ {selectedList.name || "Policy List"} Entries
+ </h3>
+ <div className="flex items-center gap-2">
+ <Input placeholder="Search entries..." className="h-8 w-[200px] bg-gray-900 border-gray-800" />
+ <div className="flex items-center gap-2">
+ <Select defaultValue="all" onValueChange={setFilter}>
+ <SelectTrigger className="h-8 w-[120px] bg-gray-900 border-gray-800">
+ <SelectValue placeholder="Filter" />
+ </SelectTrigger>
+ <SelectContent className="bg-gray-900 border-gray-800">
+ <SelectItem value="all">All Bans</SelectItem>
+ <SelectItem value="users">Users</SelectItem>
+ <SelectItem value="servers">Servers</SelectItem>
+ <SelectItem value="rooms">Rooms</SelectItem>
+ </SelectContent>
+ </Select>
+ </div>
+ </div>
+ </div>
+
+ <ScrollArea className="h-[400px] rounded-md border border-gray-800">
+ <div className="p-4 space-y-3">
+ {selectedList && entries && entries.length > 0 ? (
+ entries.map((entry) => (
+ <div key={entry.id} className="p-3 rounded-md bg-gray-900 border border-gray-800">
+ <div className="flex items-start justify-between">
+ <div>
+ <p className="font-medium">{entry.target}</p>
+ <p className="text-sm text-gray-400 mt-1">{entry.reason}</p>
+ <p className="text-xs text-gray-500 mt-1">
+ {new Date(entry.timestamp).toLocaleString()}
+ </p>
+ </div>
+ {!selectedList.readOnly && (
+ <Button
+ variant="ghost"
+ size="sm"
+ className="h-8 w-8 p-0 text-gray-400 hover:text-red-400"
+ >
+ <Ban className="h-4 w-4" />
+ <span className="sr-only">Remove</span>
+ </Button>
+ )}
+ </div>
+ </div>
+ ))
+ ) : (
+ <div className="flex flex-col items-center justify-center h-full p-6 text-center">
+ <Ban className="h-8 w-8 text-gray-500 mb-2" />
+ <p className="text-gray-400">No ban entries found</p>
+ </div>
+ )}
+ </div>
+ </ScrollArea>
+ </div>
+ </div>
+ </CardContent>
+ </Card>
+ </>
+ );
+}
+\ No newline at end of file
diff --git a/src/app/dashboard/components/analytics-dashboard.tsx b/src/app/dashboard/components/analytics-dashboard.tsx
@@ -1,356 +0,0 @@
-import { useState } from "react"
-import { TrendingUp } from "lucide-react"
-
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
-import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
-
-import { PieChart } from "@/components/analytics/pie-chart"
-import { Heatmap } from "@/components/analytics/heatmap"
-import { BarChart, BarChartItem } from "@/components/analytics/bar-chart"
-import { HorizontalBarChart } from "@/components/analytics/horizontal-bar-chart"
-
-interface InfoCardWithTrendProps {
- title: string;
- changePercent: number;
- value: string | number;
- trendColorOverride?: string;
-}
-
-function InfoCardWithTrend({ title, changePercent, value, trendColorOverride }: InfoCardWithTrendProps) {
- const trendColor = changePercent > 0 ? "text-green-400" : "text-red-400"
- const trendIcon = changePercent > 0 ? <TrendingUp className="h-3.5 w-3.5 mr-1" /> : <TrendingUp className="h-3.5 w-3.5 mr-1 rotate-180" />
- const trendText = changePercent > 0 ? `+${changePercent}% from previous period` : `${changePercent}% from previous period`
- const trendClass = trendColorOverride || trendColor
-
- return (
- <Card className="border-gray-800 bg-gray-950">
- <CardHeader className="pb-2">
- <CardTitle className="text-sm font-medium">{title}</CardTitle>
- </CardHeader>
- <CardContent>
- <div className="text-2xl font-bold">{value}</div>
- <div className={`flex items-center text-xs ${trendClass}`}>
- {trendIcon}
- <span>{trendText}</span>
- </div>
- </CardContent>
- </Card>
- )
-}
-
-// Mock data for heatmaps
-const generateHeatmapData = (intensity = 1, seed = 42) => {
- const today = new Date()
- const data = []
-
- // Generate data for the past year (52 weeks)
- for (let i = 0; i < 364; i++) {
- const date = new Date(today)
- date.setDate(today.getDate() - i)
-
- // Use a simple algorithm to generate semi-random but consistent data
- const dayOfYear = Math.floor((date.getTime() - new Date(date.getFullYear(), 0, 0).getTime()) / 86400000)
- const value = Math.floor(Math.sin(dayOfYear * 0.1 + seed) * Math.cos(date.getMonth() * 0.3 + seed) * 10 * intensity)
-
- data.push({
- date: date.toISOString().split("T")[0],
- count: Math.max(0, value),
- })
- }
-
- // Sort by date (oldest first)
- return data.reverse()
-}
-
-// Generate mock data
-const reportsHeatmapData = generateHeatmapData(1.5, 42)
-const bansHeatmapData = generateHeatmapData(1, 123)
-
-// Mock data for charts
-const roomActivityData = [
- { name: "#general:matrix.org", reports: 42, bans: 15 },
- { name: "#support:matrix.org", reports: 28, bans: 9 },
- { name: "#community:matrix.org", reports: 17, bans: 5 },
- { name: "#development:matrix.org", reports: 8, bans: 2 },
- { name: "#random:matrix.org", reports: 5, bans: 1 },
-]
-
-const reportTypesData = [
- { id: 1, label: "Spam", value: 45, color: "#f97316" },
- { id: 2, label: "Harassment", value: 32, color: "#ef4444" },
- { id: 3, label: "Inappropriate Content", value: 28, color: "#ec4899" },
- { id: 4, label: "Scam", value: 15, color: "#f59e0b" },
- { id: 5, label: "Other", value: 10, color: "#6366f1" },
-]
-
-const bannedServersData = [
- { label: "spam.org", value: 28, secondaryLabel: "28 users" },
- { label: "scam.net", value: 17, secondaryLabel: "17 users" },
- { label: "badactor.io", value: 12, secondaryLabel: "12 users" },
- { label: "malicious.com", value: 9, secondaryLabel: "9 users" },
- { label: "troll.xyz", value: 7, secondaryLabel: "7 users" },
-]
-
-// Enhanced monthly activity data with more realistic patterns
-const monthlyActivityData = [
- { month: "Jan", reports: 24, bans: 8, year: 2025 },
- { month: "Feb", reports: 32, bans: 12, year: 2025 },
- { month: "Mar", reports: 45, bans: 18, year: 2025 },
- { month: "Apr", reports: 38, bans: 15, year: 2025 },
- { month: "May", reports: 29, bans: 11, year: 2025 },
- { month: "Jun", reports: 35, bans: 14, year: 2025 },
- { month: "Jul", reports: 42, bans: 17, year: 2025 },
- { month: "Aug", reports: 50, bans: 22, year: 2025 },
- { month: "Sep", reports: 43, bans: 19, year: 2025 },
- { month: "Oct", reports: 37, bans: 16, year: 2025 },
- { month: "Nov", reports: 31, bans: 13, year: 2025 },
- { month: "Dec", reports: 28, bans: 10, year: 2025 },
-]
-
-// Transform monthly data for the BarChart component
-const monthlyBarChartData: BarChartItem[] = monthlyActivityData.map((item) => ({
- label: `${item.month}`,
- values: [
- { value: item.reports, color: "#9333ea", label: "Reports" },
- { value: item.bans, color: "#dc2626", label: "Bans" },
- ],
-}))
-
-interface AnalyticsDashboardProps {
- selectedTeam: {
- id: string
- name: string
- }
-}
-
-export function AnalyticsDashboard({ selectedTeam }: AnalyticsDashboardProps) {
- const [timeRange, setTimeRange] = useState("year")
- const [heatmapType, setHeatmapType] = useState("reports")
-
-
- const reportChangePercent = 12
-
- return (
- <div className="space-y-4">
- <div className="flex items-center justify-between">
- <h2 className="text-2xl font-bold">{selectedTeam.name} Analytics</h2>
- <div className="flex items-center gap-2">
- <Select value={timeRange} onValueChange={setTimeRange}>
- <SelectTrigger className="w-[120px] h-8 bg-gray-900 border-gray-800">
- <SelectValue placeholder="Time Range" />
- </SelectTrigger>
- <SelectContent className="bg-gray-900 border-gray-800">
- <SelectItem value="month">Past Month</SelectItem>
- <SelectItem value="quarter">Past Quarter</SelectItem>
- <SelectItem value="year">Past Year</SelectItem>
- <SelectItem value="all">All Time</SelectItem>
- </SelectContent>
- </Select>
-
- </div>
- </div>
-
- <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
- <InfoCardWithTrend title="Total Reports" changePercent={12} value={247} trendColorOverride={reportChangePercent > 0 ? "text-red-400" : "text-green-400"} />
- <InfoCardWithTrend title="Bans Issued" changePercent={5} value={98} />
- <InfoCardWithTrend title="Unique Reporters" changePercent={8} value={42} />
- <InfoCardWithTrend title="Avg. Response Time" changePercent={-15} value="1.4h" />
- </div>
-
- <Card className="border-gray-800 bg-gray-950">
- <CardHeader>
- <div className="flex items-center justify-between">
- <div>
- <CardTitle>Activity Heatmap</CardTitle>
- <CardDescription className="text-gray-400">
- Visualization of moderation activity over time
- </CardDescription>
- </div>
- <Tabs value={heatmapType} onValueChange={setHeatmapType}>
- <TabsList className="bg-gray-900">
- <TabsTrigger value="reports" className="text-xs">
- Reports
- </TabsTrigger>
- <TabsTrigger value="bans" className="text-xs">
- Bans
- </TabsTrigger>
- </TabsList>
- </Tabs>
- </div>
- </CardHeader>
- <CardContent className="overflow-hidden">
- <Heatmap
- data={heatmapType === "reports" ? reportsHeatmapData : bansHeatmapData}
- title={`${heatmapType === "reports" ? "Reports Activity" : "Ban Actions"} (Past Year)`}
- colorIntensityLabel={heatmapType === "reports" ? "reports" : "bans"}
- />
- </CardContent>
- </Card>
-
- <div className="grid gap-4 md:grid-cols-2">
- <Card className="border-gray-800 bg-gray-950">
- <CardHeader>
- <CardTitle>Room Activity</CardTitle>
- <CardDescription className="text-gray-400">Reports and bans by room</CardDescription>
- </CardHeader>
- <CardContent>
- <div className="space-y-4">
- {roomActivityData.map((room, index) => (
- <div key={index} className="space-y-2">
- <div className="flex items-center justify-between">
- <span className="text-sm font-medium">{room.name}</span>
- <span className="text-xs text-gray-400">{room.reports + room.bans} actions</span>
- </div>
- <div className="flex h-2 overflow-hidden rounded-full bg-gray-900">
- <div
- className="bg-purple-600"
- style={{ width: `${(room.reports / (room.reports + room.bans)) * 100}%` }}
- title={`${room.reports} reports`}
- ></div>
- <div
- className="bg-red-600"
- style={{ width: `${(room.bans / (room.reports + room.bans)) * 100}%` }}
- title={`${room.bans} bans`}
- ></div>
- </div>
- <div className="flex items-center justify-between text-xs text-gray-400">
- <div className="flex items-center gap-1">
- <div className="h-2 w-2 rounded-full bg-purple-600"></div>
- <span>{room.reports} reports</span>
- </div>
- <div className="flex items-center gap-1">
- <div className="h-2 w-2 rounded-full bg-red-600"></div>
- <span>{room.bans} bans</span>
- </div>
- </div>
- </div>
- ))}
- </div>
- </CardContent>
- </Card>
-
- <Card className="border-gray-800 bg-gray-950">
- <CardHeader>
- <CardTitle>Report Types</CardTitle>
- <CardDescription className="text-gray-400">Distribution of report reasons</CardDescription>
- </CardHeader>
- <CardContent>
- <PieChart data={reportTypesData} centerLabel="Report Types" />
- </CardContent>
- </Card>
- </div>
-
- <div className="grid gap-4 md:grid-cols-2">
- <Card className="border-gray-800 bg-gray-950">
- <CardHeader>
- <CardTitle>Monthly Activity</CardTitle>
- <CardDescription className="text-gray-400">Reports and bans over the past year</CardDescription>
- </CardHeader>
- <CardContent>
- <BarChart data={monthlyBarChartData} height={200} />
- </CardContent>
- </Card>
-
- <Card className="border-gray-800 bg-gray-950">
- <CardHeader>
- <CardTitle>Most Banned Servers</CardTitle>
- <CardDescription className="text-gray-400">Servers with the most banned users</CardDescription>
- </CardHeader>
- <CardContent>
- <HorizontalBarChart data={bannedServersData} color="#dc2626" />
- </CardContent>
- </Card>
- </div>
-
- <Card className="border-gray-800 bg-gray-950">
- <CardHeader>
- <CardTitle>Moderation Efficiency</CardTitle>
- <CardDescription className="text-gray-400">
- Key performance indicators for your moderation team
- </CardDescription>
- </CardHeader>
- <CardContent>
- <div className="grid gap-4 md:grid-cols-3">
- <div className="space-y-2">
- <h3 className="text-sm font-medium text-gray-400">Average Response Time</h3>
- <div className="flex items-end gap-3">
- <span className="text-2xl font-bold">1.4h</span>
- <span className="text-xs text-green-400">-15% from last month</span>
- </div>
- <p className="text-xs text-gray-400">Time between report creation and moderator action</p>
- </div>
-
- <div className="space-y-2">
- <h3 className="text-sm font-medium text-gray-400">Resolution Rate</h3>
- <div className="flex items-end gap-3">
- <span className="text-2xl font-bold">92%</span>
- <span className="text-xs text-green-400">+3% from last month</span>
- </div>
- <p className="text-xs text-gray-400">Percentage of reports that were resolved</p>
- </div>
-
- <div className="space-y-2">
- <h3 className="text-sm font-medium text-gray-400">Moderator Activity</h3>
- <div className="flex items-end gap-3">
- <span className="text-2xl font-bold">4.2</span>
- <span className="text-xs text-gray-400">actions per day</span>
- </div>
- <p className="text-xs text-gray-400">Average number of moderation actions per day</p>
- </div>
- </div>
-
- <div className="mt-6">
- <h3 className="mb-2 text-sm font-medium text-gray-400">Moderator Performance</h3>
- <div className="space-y-3">
- <div className="flex items-center gap-3">
- <div className="flex h-8 w-8 items-center justify-center rounded-full bg-purple-900 text-xs font-medium">
- AM
- </div>
- <div className="flex-1">
- <div className="flex items-center justify-between">
- <span className="text-sm">@admin:matrix.org</span>
- <span className="text-xs text-gray-400">42 actions</span>
- </div>
- <div className="mt-1 h-1.5 w-full overflow-hidden rounded-full bg-gray-900">
- <div className="h-full rounded-full bg-purple-600" style={{ width: "85%" }}></div>
- </div>
- </div>
- </div>
-
- <div className="flex items-center gap-3">
- <div className="flex h-8 w-8 items-center justify-center rounded-full bg-blue-900 text-xs font-medium">
- M1
- </div>
- <div className="flex-1">
- <div className="flex items-center justify-between">
- <span className="text-sm">@mod1:matrix.org</span>
- <span className="text-xs text-gray-400">36 actions</span>
- </div>
- <div className="mt-1 h-1.5 w-full overflow-hidden rounded-full bg-gray-900">
- <div className="h-full rounded-full bg-blue-600" style={{ width: "72%" }}></div>
- </div>
- </div>
- </div>
-
- <div className="flex items-center gap-3">
- <div className="flex h-8 w-8 items-center justify-center rounded-full bg-green-900 text-xs font-medium">
- M2
- </div>
- <div className="flex-1">
- <div className="flex items-center justify-between">
- <span className="text-sm">@mod2:matrix.org</span>
- <span className="text-xs text-gray-400">28 actions</span>
- </div>
- <div className="mt-1 h-1.5 w-full overflow-hidden rounded-full bg-gray-900">
- <div className="h-full rounded-full bg-green-600" style={{ width: "56%" }}></div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </CardContent>
- </Card>
- </div>
- )
-}
diff --git a/src/app/dashboard/components/bans-pane.tsx b/src/app/dashboard/components/bans-pane.tsx
@@ -1,135 +0,0 @@
-"use client";
-import { ScrollArea } from "@/components/ui/scroll-area"
-import { Ban, PenOff, Pen } from "lucide-react";
-import { Button } from "@/components/ui/button";
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
-import { Input } from "@/components/ui/input";
-import { useState } from "react";
-import { EBanTypes, PolicyList } from "../page";
-import AddBan from "./modals/add-ban";
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
-
-interface BansProps {
- policyLists: PolicyList[];
-}
-
-export default function Bans({ policyLists }: BansProps) {
- const [selectedPolicyList, setSelectedPolicyList] = useState("global")
- const [filter, setFilter] = useState("all")
- const selectedList = policyLists.find((list) => list.id === selectedPolicyList) || policyLists[0] || { entries: [] }
- const entries = selectedList.entries.filter((entry) => {
- if (filter === "all") return true;
- if (filter === "users") return entry.type === EBanTypes.User;
- if (filter === "servers") return entry.type === EBanTypes.Server;
- if (filter === "rooms") return entry.type === EBanTypes.Room;
- return false;
- });
-
- return (
- <Card className="border-gray-800 bg-gray-950">
- <CardHeader className="pb-2">
- <div className="flex items-center justify-between">
- <CardTitle>Policy Lists</CardTitle>
- <AddBan policyLists={policyLists} filled />
- </div>
- <CardDescription className="text-gray-400">Manage ban lists across your Matrix rooms</CardDescription>
- </CardHeader>
- <CardContent>
- <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
- <div className="space-y-4">
- <h3 className="text-sm font-medium text-gray-400">Policy Lists</h3>
- <div className="space-y-2">
- {policyLists.length > 0 ? (
- policyLists.map((list) => (
- <div
- key={list.id}
- className={`p-3 rounded-md border cursor-pointer transition-colors ${selectedPolicyList === list.id
- ? "bg-gray-800 border-purple-600"
- : "bg-gray-900 border-gray-800 hover:border-gray-700"
- }`}
- onClick={() => setSelectedPolicyList(list.id)}
- >
- <div className="flex items-center justify-between mb-1">
- <span className="font-medium">{list.name}</span>
- {list.readOnly ? (
- <PenOff className=" text-gray-400">Read Only</PenOff>
- ) : (
- <Pen className="text-green-300">Editable</Pen>
- )}
- </div>
- <p className="text-xs text-gray-400">{list.description}</p>
- <p className="text-xs text-gray-500 mt-1">{list.entries.length} entries</p>
- </div>
- ))
- ) : (
- <div className="flex flex-col items-center justify-center p-8 text-center">
- <Ban className="h-8 w-8 text-gray-500 mb-2" />
- <p className="text-gray-400">No policy lists available</p>
- </div>
- )}
- </div>
- </div>
-
- <div className="md:col-span-2 space-y-4">
- <div className="flex items-center justify-between">
- <h3 className="text-sm font-medium text-gray-400">
- {selectedList.name || "Policy List"} Entries
- </h3>
- <div className="flex items-center gap-2">
- <Input placeholder="Search entries..." className="h-8 w-[200px] bg-gray-900 border-gray-800" />
- <div className="flex items-center gap-2">
- <Select defaultValue="all" onValueChange={setFilter}>
- <SelectTrigger className="h-8 w-[120px] bg-gray-900 border-gray-800">
- <SelectValue placeholder="Filter" />
- </SelectTrigger>
- <SelectContent className="bg-gray-900 border-gray-800">
- <SelectItem value="all">All Bans</SelectItem>
- <SelectItem value="users">Users</SelectItem>
- <SelectItem value="servers">Servers</SelectItem>
- <SelectItem value="rooms">Rooms</SelectItem>
- </SelectContent>
- </Select>
- </div>
- </div>
- </div>
-
- <ScrollArea className="h-[400px] rounded-md border border-gray-800">
- <div className="p-4 space-y-3">
- {selectedList && entries && entries.length > 0 ? (
- entries.map((entry) => (
- <div key={entry.id} className="p-3 rounded-md bg-gray-900 border border-gray-800">
- <div className="flex items-start justify-between">
- <div>
- <p className="font-medium">{entry.target}</p>
- <p className="text-sm text-gray-400 mt-1">{entry.reason}</p>
- <p className="text-xs text-gray-500 mt-1">
- {new Date(entry.timestamp).toLocaleString()}
- </p>
- </div>
- {!selectedList.readOnly && (
- <Button
- variant="ghost"
- size="sm"
- className="h-8 w-8 p-0 text-gray-400 hover:text-red-400"
- >
- <Ban className="h-4 w-4" />
- <span className="sr-only">Remove</span>
- </Button>
- )}
- </div>
- </div>
- ))
- ) : (
- <div className="flex flex-col items-center justify-center h-full p-6 text-center">
- <Ban className="h-8 w-8 text-gray-500 mb-2" />
- <p className="text-gray-400">No ban entries found</p>
- </div>
- )}
- </div>
- </ScrollArea>
- </div>
- </div>
- </CardContent>
- </Card>
- );
-}
-\ No newline at end of file
diff --git a/src/app/dashboard/components/dashboard-header.tsx b/src/app/dashboard/components/dashboard-header.tsx
@@ -1,22 +1,12 @@
"use client"
-import { useState } from "react"
+import { useState, useEffect } from "react"
import Link from "next/link"
-import { Shield, Bell, Menu, Ban, UserX, LogOut, Crown, ChevronDown, Plus, X } from "lucide-react"
+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 {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog"
-import { Input } from "@/components/ui/input"
-import { Label } from "@/components/ui/label"
import { Separator } from "@/components/ui/separator"
import {
DropdownMenu,
@@ -30,7 +20,9 @@ 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 { PolicyList } from "../page"
+import KickUserModal from "./modals/kick-user"
+import CreateBotModal from "./modals/create-bot"
+import { mockPolicyLists } from "../mockData"
interface Notification {
id: string
@@ -49,16 +41,40 @@ interface Team {
}
interface DashboardHeaderProps {
- selectedTeam: Team;
- onTeamChange: (teamId: string) => void;
- activeTab: string;
- setActiveTab: (tab: string) => void;
- teams: Team[];
- policyLists: PolicyList[];
+ teams: Team[]
}
-export function DashboardHeader({ selectedTeam, onTeamChange, activeTab, setActiveTab, teams, policyLists }: DashboardHeaderProps) {
+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",
@@ -108,6 +124,27 @@ export function DashboardHeader({ selectedTeam, onTeamChange, activeTab, setActi
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">
@@ -120,7 +157,7 @@ export function DashboardHeader({ selectedTeam, onTeamChange, activeTab, setActi
{/* Global Team Selector */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
- <Button size="sm" variant="outline" className="border-purple-500 text-purple-400 hover:bg-purple-950">
+ <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" />
@@ -133,51 +170,13 @@ export function DashboardHeader({ selectedTeam, onTeamChange, activeTab, setActi
<DropdownMenuItem
key={team.id}
className={selectedTeam.id === team.id ? "bg-gray-800" : ""}
- onClick={() => onTeamChange(team.id)}
+ onClick={() => handleTeamChange(team.id)}
>
{team.name}
</DropdownMenuItem>
))}
<DropdownMenuSeparator className="bg-gray-800" />
- <Dialog>
- <DialogTrigger asChild>
- <DropdownMenuItem>
- <Plus className="mr-2 h-4 w-4" />
- Create New Bot
- </DropdownMenuItem>
- </DialogTrigger>
- <DialogContent className="bg-gray-950 border-gray-800">
- <DialogHeader>
- <DialogTitle>Create New Bot</DialogTitle>
- <DialogDescription className="text-gray-400">
- Set up a new Draupnir bot for a different community
- </DialogDescription>
- </DialogHeader>
- <div className="space-y-4 py-4">
- <div className="space-y-2">
- <Label htmlFor="bot-name">Bot Name</Label>
- <Input id="bot-name" placeholder="My Community Bot" className="bg-gray-900 border-gray-800" />
- </div>
- <div className="space-y-2">
- <Label htmlFor="bot-description">Description</Label>
- <Input
- id="bot-description"
- placeholder="Moderation for my community"
- className="bg-gray-900 border-gray-800"
- />
- </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-purple-600 text-white hover:bg-purple-700">Create Bot</Button>
- </DialogFooter>
- </DialogContent>
- </Dialog>
+ <CreateBotModal />
</DropdownMenuContent>
</DropdownMenu>
</div>
@@ -271,39 +270,7 @@ export function DashboardHeader({ selectedTeam, onTeamChange, activeTab, setActi
{/* Quick Action Buttons */}
<AddBan header policyLists={policyLists} />
- <Dialog>
- <DialogTrigger asChild>
- <Button
- variant="outline"
- size="sm"
- className="hidden md:flex border-orange-500 text-orange-400 hover:bg-orange-950 hover:text-orange-300"
- >
- <UserX className="mr-2 h-4 w-4" />
- Kick User
- </Button>
- </DialogTrigger>
- <DialogContent className="bg-gray-950 border-gray-800">
- <DialogHeader>
- <DialogTitle>Kick User</DialogTitle>
- <DialogDescription className="text-gray-400">Kick a user from a Matrix room</DialogDescription>
- </DialogHeader>
- <div className="space-y-4 py-4">
- <div className="space-y-2">
- <Label htmlFor="kick-user-id">Matrix User ID</Label>
- <Input id="kick-user-id" placeholder="@user:matrix.org" className="bg-gray-900 border-gray-800" />
- </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-orange-600 text-white hover:bg-orange-700">Kick User</Button>
- </DialogFooter>
- </DialogContent>
- </Dialog>
+ <KickUserModal />
<Button
variant="outline"
@@ -347,7 +314,7 @@ export function DashboardHeader({ selectedTeam, onTeamChange, activeTab, setActi
<DropdownMenuItem
key={team.id}
className={selectedTeam.id === team.id ? "bg-gray-800" : ""}
- onClick={() => onTeamChange(team.id)}
+ onClick={() => handleTeamChange(team.id)}
>
{team.name}
</DropdownMenuItem>
@@ -359,35 +326,35 @@ export function DashboardHeader({ selectedTeam, onTeamChange, activeTab, setActi
<Button
variant={activeTab === "overview" ? "secondary" : "ghost"}
className="justify-start"
- onClick={() => setActiveTab("overview")}
+ onClick={() => navigateToTab("overview")}
>
Overview
</Button>
<Button
variant={activeTab === "reports" ? "secondary" : "ghost"}
className="justify-start"
- onClick={() => setActiveTab("reports")}
+ onClick={() => navigateToTab("reports")}
>
Reports
</Button>
<Button
variant={activeTab === "bans" ? "secondary" : "ghost"}
className="justify-start"
- onClick={() => setActiveTab("bans")}
+ onClick={() => navigateToTab("bans")}
>
Bans
</Button>
<Button
variant={activeTab === "analytics" ? "secondary" : "ghost"}
className="justify-start"
- onClick={() => setActiveTab("analytics")}
+ onClick={() => navigateToTab("analytics")}
>
Analytics
</Button>
<Button
variant={activeTab === "settings" ? "secondary" : "ghost"}
className="justify-start"
- onClick={() => setActiveTab("settings")}
+ onClick={() => navigateToTab("settings")}
>
Settings
</Button>
@@ -395,41 +362,14 @@ export function DashboardHeader({ selectedTeam, onTeamChange, activeTab, setActi
<Separator className="my-2" />
{/* Mobile Quick Actions */}
- <Dialog>
- <DialogTrigger asChild>
- <Button
- variant="outline"
- size="sm"
- className="w-full justify-start border-red-500 text-red-400 hover:bg-red-950 hover:text-red-300"
- >
- <Ban className="mr-2 h-4 w-4" />
- Ban User
- </Button>
- </DialogTrigger>
- <DialogContent className="bg-gray-950 border-gray-800">
- {/* Same content as desktop dialog */}
- </DialogContent>
- </Dialog>
+ <AddBan header policyLists={policyLists} />
- <Dialog>
- <DialogTrigger asChild>
- <Button
- variant="outline"
- size="sm"
- className="w-full justify-start border-orange-500 text-orange-400 hover:bg-orange-950 hover:text-orange-300"
- >
- <UserX className="mr-2 h-4 w-4" />
- Kick User
- </Button>
- </DialogTrigger>
- <DialogContent className="bg-gray-950 border-gray-800">
- {/* Same content as desktop dialog */}
- </DialogContent>
- </Dialog>
+ <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}
>
diff --git a/src/app/dashboard/components/modals/add-ban.tsx b/src/app/dashboard/components/modals/add-ban.tsx
@@ -6,7 +6,7 @@ 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 "../../page";
+import { PolicyList } from "../../mockData";
interface AddBanProps {
policyLists: PolicyList[];
diff --git a/src/app/dashboard/components/modals/create-bot.tsx b/src/app/dashboard/components/modals/create-bot.tsx
@@ -0,0 +1,44 @@
+import { Button } from "@/components/ui/button";
+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 { Settings } from "lucide-react";
+
+export default function CreateBotModal() {
+ return (
+ <Dialog>
+ <DialogTrigger asChild>
+ <Button
+ variant="link"
+ >
+ <Settings className="mr-2 h-4 w-4" />
+ Create New Bot
+ </Button>
+ </DialogTrigger>
+ <DialogContent className="bg-gray-950 border-gray-800">
+ <DialogHeader>
+ <DialogTitle>Create New Bot</DialogTitle>
+ <DialogDescription className="text-gray-400">
+ Set up a new Draupnir bot for a different community
+ </DialogDescription>
+ </DialogHeader>
+ <div className="space-y-4 py-4">
+ <div className="space-y-2">
+ <Label htmlFor="bot-name">Bot Name</Label>
+ <Input id="bot-name" placeholder="My Community Bot" className="bg-gray-900 border-gray-800" />
+ </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-purple-600 text-white hover:bg-purple-700">Create Bot</Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ );
+}
+\ No newline at end of file
diff --git a/src/app/dashboard/components/modals/kick-user.tsx b/src/app/dashboard/components/modals/kick-user.tsx
@@ -0,0 +1,28 @@
+import { Button } from "@/components/ui/button";
+import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
+import { UserX } from "lucide-react";
+
+export default function KickUserModal() {
+ return (
+ <Dialog>
+ <DialogTrigger asChild>
+ <Button
+ variant="outline"
+ size="sm"
+ className="w-full justify-start border-orange-500 text-orange-400 hover:bg-orange-950 hover:text-orange-300"
+ >
+ <UserX className="mr-2 h-4 w-4" />
+ Kick User
+ </Button>
+ </DialogTrigger>
+ <DialogContent className="bg-gray-950 border-gray-800">
+ <DialogHeader>
+ <DialogTitle>Kick a User</DialogTitle>
+ <DialogDescription className="text-gray-400">
+ Remove a user from your Matrix rooms using a kick instead of a ban. This will not prevent them from rejoining the room.
+ </DialogDescription>
+ </DialogHeader>
+ </DialogContent>
+ </Dialog>
+ );
+}
+\ No newline at end of file
diff --git a/src/app/dashboard/components/overview-tab.tsx b/src/app/dashboard/components/overview-tab.tsx
@@ -1,124 +0,0 @@
-import { AlertTriangle, Ban, CheckCircle } from "lucide-react"
-
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
-import { PolicyList, Report } from "../page"
-import ProtectedRomsList from "./protected-rooms-list"
-import { Team } from "../team-management"
-import AddProtectedRoom from "./modals/add-protected-room"
-
-interface OverviewTabProps {
- selectedTeam: Team;
- policyLists: PolicyList[];
- reports: Report[];
-}
-
-export function OverviewTab({ selectedTeam, policyLists, reports }: OverviewTabProps) {
- return (
- <div className="space-y-8">
- <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
- <Card className="border-gray-800 bg-gray-950">
- <CardHeader className="pb-2">
- <CardTitle className="text-sm font-medium">Protected Rooms</CardTitle>
- </CardHeader>
- <CardContent>
- <div className="text-2xl font-bold">{selectedTeam.rooms.length}</div>
- <div className="flex justify-between items-center">
- <p className="text-xs text-gray-400">Monitored by this bot</p>
- <AddProtectedRoom />
- </div>
- </CardContent>
- </Card>
- <Card className="border-gray-800 bg-gray-950">
- <CardHeader className="pb-2">
- <CardTitle className="text-sm font-medium">Active Reports</CardTitle>
- </CardHeader>
- <CardContent>
- <div className="text-2xl font-bold">{reports.length}</div>
- <p className="text-xs text-gray-400">{reports.filter((r) => r.priority === "high").length} high priority</p>
- </CardContent>
- </Card>
- <Card className="border-gray-800 bg-gray-950">
- <CardHeader className="pb-2">
- <CardTitle className="text-sm font-medium">Bans Issued</CardTitle>
- </CardHeader>
- <CardContent>
- <div className="text-2xl font-bold">
- {policyLists.reduce((total, list) => total + list.entries.length, 0)}
- </div>
- <p className="text-xs text-gray-400">Across all policy lists</p>
- </CardContent>
- </Card>
- <Card className="border-gray-800 bg-gray-950">
- <CardHeader className="pb-2">
- <CardTitle className="text-sm font-medium">Bot Status</CardTitle>
- </CardHeader>
- <CardContent>
- <div className="flex items-center gap-2">
- <div className="h-3 w-3 rounded-full bg-green-500"></div>
- <div className="text-sm font-medium">Online</div>
- </div>
- <p className="text-xs text-gray-400">Last restart: 7d ago</p>
- </CardContent>
- </Card>
- </div>
-
- <div className="grid gap-4 md:grid-cols-2">
- <Card className="border-gray-800 bg-gray-950">
- <CardHeader>
- <CardTitle>Recent Activity</CardTitle>
- <CardDescription className="text-gray-400">Latest moderation actions</CardDescription>
- </CardHeader>
- <CardContent>
- <div className="space-y-4">
- <div className="flex items-start gap-4">
- <div className="rounded-full bg-red-500/20 p-2">
- <Ban className="h-4 w-4 text-red-500" />
- </div>
- <div className="space-y-1">
- <p className="text-sm font-medium">User banned</p>
- <p className="text-xs text-gray-400">@spammer:matrix.org was banned from all rooms</p>
- <p className="text-xs text-gray-500">10 minutes ago</p>
- </div>
- </div>
- <div className="flex items-start gap-4">
- <div className="rounded-full bg-yellow-500/20 p-2">
- <AlertTriangle className="h-4 w-4 text-yellow-500" />
- </div>
- <div className="space-y-1">
- <p className="text-sm font-medium">Report created</p>
- <p className="text-xs text-gray-400">Spam detected in #support</p>
- <p className="text-xs text-gray-500">1 hour ago</p>
- </div>
- </div>
- <div className="flex items-start gap-4">
- <div className="rounded-full bg-green-500/20 p-2">
- <CheckCircle className="h-4 w-4 text-green-500" />
- </div>
- <div className="space-y-1">
- <p className="text-sm font-medium">Report resolved</p>
- <p className="text-xs text-gray-400">Harassment report in #community resolved</p>
- <p className="text-xs text-gray-500">3 hours ago</p>
- </div>
- </div>
- </div>
- </CardContent>
- </Card>
-
- <Card className="border-gray-800 bg-gray-950">
- <CardHeader>
- <div className="flex items-center justify-between">
- <div>
- <CardTitle>Protected Rooms</CardTitle>
- <CardDescription className="text-gray-400">Rooms monitored by this bot</CardDescription>
- </div>
- <AddProtectedRoom filled />
- </div>
- </CardHeader>
- <CardContent>
- <ProtectedRomsList team={selectedTeam} />
- </CardContent>
- </Card>
- </div>
- </div>
- )
-}
diff --git a/src/app/dashboard/components/protected-rooms-list.tsx b/src/app/dashboard/components/protected-rooms-list.tsx
@@ -1,6 +1,6 @@
import { Button } from "@/components/ui/button";
import { Trash2 } from "lucide-react";
-import { Team } from "../team-management";
+import { Team } from "../mockData";
interface ProtectedRomsListProps {
team: Team;
diff --git a/src/app/dashboard/components/reports-tab.tsx b/src/app/dashboard/components/reports-tab.tsx
@@ -1,262 +0,0 @@
-"use client"
-
-import { useState } from "react"
-import { AlertTriangle, CheckCircle, MessageSquare, UserPlus, Filter, Ban } from "lucide-react"
-
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
-import { Badge } from "@/components/ui/badge"
-import { Button } from "@/components/ui/button"
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
-import { BlurredText } from "@/components/blurred-text"
-import { BlurredImage } from "@/components/blurred-image"
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog"
-import { Report } from "../page";
-
-interface ReportsTabProps {
- reports: Report[]
-}
-
-export function ReportsTab({ reports }: ReportsTabProps) {
- const [selectedReport, setSelectedReport] = useState<string | null>(null)
- const [filter, setFilter] = useState("all")
-
- const filteredReports =
- filter === "all"
- ? reports
- : filter === "high"
- ? reports.filter((r) => r.priority === "high")
- : reports.filter((r) => r.type === filter)
-
- return (
- <Card className="border-gray-800 bg-gray-950">
- <CardHeader className="pb-2">
- <div className="flex items-center justify-between">
- <CardTitle>Active Reports</CardTitle>
- <div className="flex items-center gap-2">
- <Select defaultValue="all" onValueChange={setFilter}>
- <SelectTrigger className="h-8 w-[120px] bg-gray-900 border-gray-800">
- <SelectValue placeholder="Filter" />
- </SelectTrigger>
- <SelectContent className="bg-gray-900 border-gray-800">
- <SelectItem value="all">All Reports</SelectItem>
- <SelectItem value="message">Messages</SelectItem>
- <SelectItem value="invite">Invites</SelectItem>
- <SelectItem value="high">High Priority</SelectItem>
- </SelectContent>
- </Select>
- </div>
- </div>
- <CardDescription className="text-gray-400">Reports requiring moderation action</CardDescription>
- </CardHeader>
- <CardContent>
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
- <div className="space-y-4">
- <h3 className="text-sm font-medium text-gray-400">Report List</h3>
- {filteredReports.length > 0 ? (
- filteredReports.map((report) => (
- <div
- key={report.id}
- className={`p-3 rounded-md border cursor-pointer transition-colors ${selectedReport === report.id
- ? "bg-gray-800 border-purple-600"
- : "bg-gray-900 border-gray-800 hover:border-gray-700"
- }`}
- onClick={() => setSelectedReport(report.id)}
- >
- <div className="flex items-center justify-between mb-2">
- <div className="flex items-center gap-2">
- {report.type === "message" ? (
- <MessageSquare className="h-4 w-4 text-blue-400" />
- ) : (
- <UserPlus className="h-4 w-4 text-green-400" />
- )}
- <span className="text-sm font-medium">
- {report.type === "message" ? "Message Report" : "Invite Report"}
- </span>
- </div>
- <Badge
- className={
- report.priority === "high"
- ? "bg-red-900/50 text-red-300 hover:bg-red-900/70"
- : "bg-yellow-900/50 text-yellow-300 hover:bg-yellow-900/70"
- }
- >
- {report.priority}
- </Badge>
- </div>
- <p className="text-xs text-gray-400 mb-1">{report.room}</p>
- <p className="text-xs text-gray-400 mb-1">
- {report.type === "message"
- ? `From: ${report.subject.sender}`
- : `From: ${report.subject.sender} To: ${report.subject.target}`}
- </p>
- <p className="text-xs text-gray-500">{new Date(report.timestamp).toLocaleString()}</p>
- </div>
- ))
- ) : (
- <div className="flex flex-col items-center justify-center p-8 text-center">
- <CheckCircle className="h-8 w-8 text-gray-500 mb-2" />
- <p className="text-gray-400">No active reports</p>
- </div>
- )}
- </div>
-
- <div className="space-y-4">
- <h3 className="text-sm font-medium text-gray-400">Report Details</h3>
- {selectedReport ? (
- (() => {
- const report = reports.find((r) => r.id === selectedReport)
- if (!report) return <p>Select a report to view details</p>
-
- return (
- <div className="space-y-4">
- <div className="p-4 rounded-md bg-gray-900 border border-gray-800">
- <div className="flex items-center justify-between mb-3">
- <h4 className="font-medium">Report Information</h4>
- <Badge className="bg-purple-900/50 text-purple-300">{report.status}</Badge>
- </div>
-
- <div className="space-y-2 text-sm">
- <div className="flex justify-between">
- <span className="text-gray-400">Reported by:</span>
- <span>{report.reporter}</span>
- </div>
- <div className="flex justify-between">
- <span className="text-gray-400">Room:</span>
- <span>{report.room}</span>
- </div>
- <div className="flex justify-between">
- <span className="text-gray-400">Reason:</span>
- <span>{report.reason}</span>
- </div>
- <div className="flex justify-between">
- <span className="text-gray-400">Time:</span>
- <span>{new Date(report.timestamp).toLocaleString()}</span>
- </div>
- </div>
- </div>
-
- <div className="p-4 rounded-md bg-gray-900 border border-gray-800">
- <h4 className="font-medium mb-3">
- {report.type === "message" ? "Message Content" : "Invite Details"}
- </h4>
-
- {report.type === "message" && (
- <div className="space-y-3">
- <div className="flex justify-between text-sm">
- <span className="text-gray-400">Sender:</span>
- <span>{report.subject.sender}</span>
- </div>
- <div className="flex justify-between text-sm">
- <span className="text-gray-400">Message ID:</span>
- <span className="font-mono text-xs">{report.subject.id}</span>
- </div>
- <div className="flex justify-between text-sm">
- <span className="text-gray-400">Time:</span>
- <span>{new Date(report.subject.timestamp).toLocaleString()}</span>
- </div>
-
- <div className="mt-4">
- <div className="flex items-center justify-between mb-2">
- <span className="text-sm text-gray-400">Content:</span>
- {report.subject.content?.msgtype === "m.text" && (
- <Button variant="ghost" size="sm" className="h-6 px-2 text-gray-400">
- <Filter className="h-3 w-3 mr-1" />
- Filter
- </Button>
- )}
- </div>
-
- {report.subject.content?.msgtype === "m.text" ? (
- <BlurredText text={report.subject.content?.body} />
- ) : report.subject.content?.msgtype === "m.image" ? (
- <div className="mt-2">
- <BlurredImage
- src="/placeholder.svg?height=300&width=400"
- alt={report.subject.content?.body}
- width={400}
- height={300}
- />
- <p className="text-xs text-gray-500 mt-1">Image: {report.subject.content.body}</p>
- </div>
- ) : (
- <p className="text-sm">Unsupported content type</p>
- )}
- </div>
- </div>
- )}
-
- {report.type === "invite" && (
- <div className="space-y-3">
- <div className="flex justify-between text-sm">
- <span className="text-gray-400">Sender:</span>
- <span>{report.subject.sender}</span>
- </div>
- <div className="flex justify-between text-sm">
- <span className="text-gray-400">Target:</span>
- <span>{report.subject.target}</span>
- </div>
- <div className="flex justify-between text-sm">
- <span className="text-gray-400">Time:</span>
- <span>{new Date(report.subject.timestamp).toLocaleString()}</span>
- </div>
- </div>
- )}
- </div>
-
- <div className="flex gap-2 mt-4">
- <Button className="bg-green-600 text-white hover:bg-green-700">
- <CheckCircle className="mr-2 h-4 w-4" />
- Resolve
- </Button>
- <Dialog>
- <DialogTrigger asChild>
- <Button
- variant="outline"
- className="border-red-500 text-red-400 hover:bg-red-950 hover:text-red-300"
- >
- <Ban className="mr-2 h-4 w-4" />
- Ban User
- </Button>
- </DialogTrigger>
- <DialogContent className="bg-gray-950 border-gray-800">
- <DialogHeader>
- <DialogTitle>Ban User</DialogTitle>
- <DialogDescription className="text-gray-400">
- Ban {report.subject.sender} from your Matrix rooms
- </DialogDescription>
- </DialogHeader>
- {/* Ban dialog content */}
- </DialogContent>
- </Dialog>
- {report.type === "message" && (
- <Button
- variant="outline"
- className="border-gray-700 text-gray-400 hover:bg-gray-900 hover:text-gray-300"
- >
- <MessageSquare className="mr-2 h-4 w-4" />
- Delete Message
- </Button>
- )}
- </div>
- </div>
- )
- })()
- ) : (
- <div className="flex flex-col items-center justify-center p-8 text-center">
- <AlertTriangle className="h-8 w-8 text-gray-500 mb-2" />
- <p className="text-gray-400">Select a report to view details</p>
- </div>
- )}
- </div>
- </div>
- </CardContent>
- </Card>
- )
-}
diff --git a/src/app/dashboard/components/tab-navigation.tsx b/src/app/dashboard/components/tab-navigation.tsx
@@ -0,0 +1,35 @@
+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,6 +2,66 @@ 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"
+
+// Mock data for teams/bots
+const mockTeams = [
+ {
+ id: "team1",
+ name: "Main Community Bot",
+ description: "Moderation for main community rooms",
+ owner: "@admin:matrix.org",
+ created: "2025-01-15T10:00:00Z",
+ members: [
+ {
+ id: "user1",
+ name: "@admin:matrix.org",
+ role: "owner",
+ avatar: null,
+ joined: "2025-01-15T10:00:00Z",
+ },
+ {
+ id: "user2",
+ name: "@mod1:matrix.org",
+ role: "moderator",
+ avatar: null,
+ joined: "2025-01-16T14:30:00Z",
+ },
+ {
+ id: "user3",
+ name: "@mod2:matrix.org",
+ role: "moderator",
+ avatar: null,
+ joined: "2025-02-01T09:15:00Z",
+ },
+ ],
+ rooms: ["#general:matrix.org", "#support:matrix.org", "#community:matrix.org"],
+ },
+ {
+ id: "team2",
+ name: "Development Team Bot",
+ description: "Moderation for development-related rooms",
+ owner: "@admin:matrix.org",
+ created: "2025-02-10T11:30:00Z",
+ members: [
+ {
+ id: "user1",
+ name: "@admin:matrix.org",
+ role: "owner",
+ avatar: null,
+ joined: "2025-02-10T11:30:00Z",
+ },
+ {
+ id: "user4",
+ name: "@dev1:matrix.org",
+ role: "moderator",
+ avatar: null,
+ joined: "2025-02-11T13:45:00Z",
+ },
+ ],
+ rooms: ["#development:matrix.org", "#coding:matrix.org"],
+ },
+]
export default async function DashboardLayout({
children,
@@ -17,8 +77,21 @@ export default async function DashboardLayout({
}
return (
- <Suspense fallback={<div className="flex h-screen w-full items-center justify-center">Loading...</div>}>
- {children}
- </Suspense>
+ <div className="flex min-h-screen flex-col bg-black text-white">
+ <Suspense
+ fallback={
+ <div className="flex h-16 items-center justify-between border-b border-gray-800 bg-black/80 backdrop-blur-sm">
+ Loading...
+ </div>
+ }
+ >
+ <DashboardHeader teams={mockTeams} />
+ </Suspense>
+ <main className="flex-1 container px-4 py-8 md:px-6 md:py-12">
+ <Suspense fallback={<div className="flex h-screen w-full items-center justify-center">Loading...</div>}>
+ {children}
+ </Suspense>
+ </main>
+ </div>
)
}
diff --git a/src/app/dashboard/mockData.ts b/src/app/dashboard/mockData.ts
@@ -0,0 +1,312 @@
+export const EBanTypes = {
+ User: "user",
+ Server: "server",
+ Room: "room",
+} as const;
+type BanTypes = typeof EBanTypes[keyof typeof EBanTypes];
+
+export interface PolicyList {
+ id: string;
+ name: string;
+ description: string;
+ readOnly: boolean;
+ teamId: string;
+ entries: {
+ id: string,
+ target: string,
+ reason: string,
+ timestamp: string,
+ type: BanTypes;
+ }[];
+};
+
+export interface Report {
+ id: string;
+ type: string;
+ status: string;
+ priority: string;
+ timestamp: string;
+ reporter: string;
+ room: string;
+ teamId: string;
+ subject: {
+ type: string;
+ id?: string;
+ sender: string;
+ content?: {
+ msgtype: string;
+ body: string;
+ url?: string;
+ info?: {
+ mimetype: string;
+ w: number;
+ h: number;
+ size: number;
+ };
+ };
+ target?: string;
+ timestamp: string;
+ };
+ reason: string;
+};
+
+// Mock data for policy lists - filtered by team
+export const mockPolicyLists: PolicyList[] = [
+ {
+ id: "global",
+ name: "Global Ban List",
+ description: "Shared ban list for all Matrix communities",
+ readOnly: true,
+ teamId: "team1",
+ entries: [
+ {
+ id: "ban_1",
+ target: "@spammer:badserver.org",
+ reason: "Spam across multiple rooms",
+ timestamp: "2025-04-20T10:30:00Z",
+ type: EBanTypes.User,
+ },
+ {
+ id: "ban_2",
+ target: "@troll:badserver.org",
+ reason: "Harassment and inappropriate content",
+ timestamp: "2025-04-19T15:45:00Z",
+ type: EBanTypes.User,
+ },
+ {
+ id: "ban_3",
+ target: "@phisher:scam.org",
+ reason: "Phishing attempts",
+ timestamp: "2025-04-18T09:20:00Z",
+ type: EBanTypes.User,
+ },
+ ],
+ },
+ {
+ id: "community",
+ name: "Community Ban List",
+ description: "Ban list for our community rooms",
+ readOnly: false,
+ teamId: "team1",
+ entries: [
+ {
+ id: "ban_4",
+ target: "@disruptor:matrix.org",
+ reason: "Disruptive behavior in #general",
+ timestamp: "2025-04-21T08:15:00Z",
+ type: EBanTypes.User,
+ },
+ {
+ id: "ban_5",
+ target: "@bot123:unknown.org",
+ reason: "Automated spam",
+ timestamp: "2025-04-20T14:30:00Z",
+ type: EBanTypes.User,
+ },
+ // Server ban
+ {
+ id: "ban_6",
+ target: "badserver.org",
+ reason: "Known spam server",
+ timestamp: "2025-04-19T11:00:00Z",
+ type: EBanTypes.Server,
+ },
+ ],
+ },
+ {
+ id: "support",
+ name: "Support Rooms Ban List",
+ description: "Ban list specific to support rooms",
+ readOnly: false,
+ teamId: "team1",
+ entries: [
+ {
+ id: "ban_6",
+ target: "@angryuser:matrix.org",
+ reason: "Abusive language to support staff",
+ timestamp: "2025-04-21T11:45:00Z",
+ type: EBanTypes.User,
+ },
+ ],
+ },
+ {
+ id: "dev",
+ name: "Development Ban List",
+ description: "Ban list for development rooms",
+ readOnly: false,
+ teamId: "team2",
+ entries: [
+ {
+ id: "ban_7",
+ target: "@spambot:matrix.org",
+ reason: "Code spam in development channels",
+ timestamp: "2025-04-19T09:30:00Z",
+ type: EBanTypes.User,
+ },
+ ],
+ },
+]
+
+// Mock data for reports - filtered by team
+export const mockReports: Report[] = [
+ {
+ id: "rep_1",
+ type: "message",
+ status: "open",
+ priority: "high",
+ timestamp: "2025-04-21T14:32:00Z",
+ reporter: "@moderator:matrix.org",
+ room: "#general:matrix.org",
+ teamId: "team1",
+ subject: {
+ type: "message",
+ id: "$1234567890abcdefghij:matrix.org",
+ sender: "@spammer:badserver.org",
+ content: {
+ msgtype: "m.text",
+ body: "Hey everyone! Check out this amazing investment opportunity at scam-crypto-site.com - 1000% returns guaranteed!",
+ },
+ timestamp: "2025-04-21T14:30:00Z",
+ },
+ reason: "Spam/scam content",
+ },
+ {
+ id: "rep_2",
+ type: "invite",
+ status: "open",
+ priority: "medium",
+ timestamp: "2025-04-21T13:15:00Z",
+ reporter: "@user:matrix.org",
+ room: "#general:matrix.org",
+ teamId: "team1",
+ subject: {
+ type: "invite",
+ sender: "@spambot:badserver.org",
+ target: "@user:matrix.org",
+ timestamp: "2025-04-21T13:10:00Z",
+ },
+ reason: "Unsolicited invite from unknown user",
+ },
+ {
+ id: "rep_3",
+ type: "message",
+ status: "open",
+ priority: "high",
+ timestamp: "2025-04-21T12:05:00Z",
+ reporter: "@admin:matrix.org",
+ room: "#support:matrix.org",
+ teamId: "team1",
+ subject: {
+ type: "message",
+ id: "$abcdefghij1234567890:matrix.org",
+ sender: "@troll:badserver.org",
+ content: {
+ msgtype: "m.image",
+ body: "image.jpg",
+ url: "mxc://matrix.org/abcdefghijklmnopqrstuvwxyz",
+ info: {
+ mimetype: "image/jpeg",
+ w: 800,
+ h: 600,
+ size: 95431,
+ },
+ },
+ timestamp: "2025-04-21T12:00:00Z",
+ },
+ reason: "Inappropriate image",
+ },
+ {
+ id: "rep_4",
+ type: "message",
+ status: "open",
+ priority: "medium",
+ timestamp: "2025-04-21T11:45:00Z",
+ reporter: "@dev1:matrix.org",
+ room: "#development:matrix.org",
+ teamId: "team2",
+ subject: {
+ type: "message",
+ id: "$devmessage123456789:matrix.org",
+ sender: "@newuser:matrix.org",
+ content: {
+ msgtype: "m.text",
+ body: "Can someone help me with this code? It's not working: <script>alert('hack');</script>",
+ },
+ timestamp: "2025-04-21T11:40:00Z",
+ },
+ reason: "Potential code injection attempt",
+ },
+]
+
+
+export interface Team {
+ id: string;
+ name: string;
+ owner: string;
+ created: string;
+ members: {
+ id: string;
+ name: string;
+ role: "owner" | "moderator";
+ avatar?: string;
+ joined: string;
+ }[];
+ rooms: string[];
+}
+
+// Mock data for teams/bots
+export const mockTeams: Team[] = [
+ {
+ id: "team1",
+ name: "Main Community Bot",
+ owner: "@admin:matrix.org",
+ created: "2025-01-15T10:00:00Z",
+ members: [
+ {
+ id: "user1",
+ name: "@admin:matrix.org",
+ role: "owner",
+ avatar: undefined,
+ joined: "2025-01-15T10:00:00Z",
+ },
+ {
+ id: "user2",
+ name: "@mod1:matrix.org",
+ role: "moderator",
+ avatar: undefined,
+ joined: "2025-01-16T14:30:00Z",
+ },
+ {
+ id: "user3",
+ name: "@mod2:matrix.org",
+ role: "moderator",
+ avatar: undefined,
+ joined: "2025-02-01T09:15:00Z",
+ },
+ ],
+ rooms: ["#general:matrix.org", "#support:matrix.org", "#community:matrix.org"],
+ },
+ {
+ id: "team2",
+ name: "Development Team Bot",
+ owner: "@admin:matrix.org",
+ created: "2025-02-10T11:30:00Z",
+ members: [
+ {
+ id: "user1",
+ name: "@admin:matrix.org",
+ role: "owner",
+ avatar: undefined,
+ joined: "2025-02-10T11:30:00Z",
+ },
+ {
+ id: "user4",
+ name: "@dev1:matrix.org",
+ role: "moderator",
+ avatar: undefined,
+ joined: "2025-02-11T13:45:00Z",
+ },
+ ],
+ rooms: ["#development:matrix.org", "#coding:matrix.org"],
+ },
+]
diff --git a/src/app/dashboard/overview/loading.tsx b/src/app/dashboard/overview/loading.tsx
@@ -0,0 +1,90 @@
+import { Skeleton } from "@/components/ui/skeleton"
+import { Card, CardContent, CardHeader } from "@/components/ui/card"
+
+export default function Loading() {
+ return (
+ <div>
+ <div className="flex items-center justify-between mb-8">
+ <Skeleton className="h-9 w-64" />
+ <div className="hidden md:block">
+ <Skeleton className="h-10 w-[400px]" />
+ </div>
+ </div>
+
+ <div className="space-y-8">
+ {/* Stats Cards */}
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
+ {Array(4)
+ .fill(0)
+ .map((_, i) => (
+ <Card key={i} className="border-gray-800 bg-gray-950">
+ <CardHeader className="pb-2">
+ <Skeleton className="h-5 w-32" />
+ </CardHeader>
+ <CardContent>
+ <Skeleton className="h-8 w-16 mb-2" />
+ <div className="flex justify-between items-center">
+ <Skeleton className="h-4 w-32" />
+ <Skeleton className="h-6 w-24" />
+ </div>
+ </CardContent>
+ </Card>
+ ))}
+ </div>
+
+ {/* Two Column Cards */}
+ <div className="grid gap-4 md:grid-cols-2">
+ <Card className="border-gray-800 bg-gray-950">
+ <CardHeader>
+ <Skeleton className="h-6 w-40 mb-2" />
+ <Skeleton className="h-4 w-56" />
+ </CardHeader>
+ <CardContent>
+ <div className="space-y-4">
+ {Array(3)
+ .fill(0)
+ .map((_, i) => (
+ <div key={i} className="flex items-start gap-4">
+ <Skeleton className="h-8 w-8 rounded-full" />
+ <div className="space-y-1 flex-1">
+ <Skeleton className="h-5 w-32" />
+ <Skeleton className="h-4 w-full" />
+ <Skeleton className="h-4 w-24" />
+ </div>
+ </div>
+ ))}
+ </div>
+ </CardContent>
+ </Card>
+
+ <Card className="border-gray-800 bg-gray-950">
+ <CardHeader>
+ <div className="flex items-center justify-between">
+ <div>
+ <Skeleton className="h-6 w-40 mb-2" />
+ <Skeleton className="h-4 w-56" />
+ </div>
+ <Skeleton className="h-9 w-24" />
+ </div>
+ </CardHeader>
+ <CardContent>
+ <div className="space-y-4">
+ {Array(4)
+ .fill(0)
+ .map((_, i) => (
+ <div key={i} className="flex items-center justify-between">
+ <div className="flex items-center gap-2">
+ <Skeleton className="h-2 w-2 rounded-full" />
+ <Skeleton className="h-5 w-40" />
+ </div>
+ <Skeleton className="h-7 w-7 rounded-md" />
+ </div>
+ ))}
+ </div>
+ </CardContent>
+ </Card>
+ </div>
+ </div>
+ </div>
+ )
+}
diff --git a/src/app/dashboard/overview/page.tsx b/src/app/dashboard/overview/page.tsx
@@ -0,0 +1,143 @@
+'use client';
+
+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 { mockPolicyLists, mockReports, mockTeams } from "../mockData"
+import TabNavigation from "../components/tab-navigation";
+
+export default function OverviewPage() {
+ const searchParams = useSearchParams()
+ const teamIdParam = searchParams.get("team")
+ const [selectedTeam, setSelectedTeam] = useState(mockTeams.find((t) => t.id === teamIdParam) || mockTeams[0])
+
+
+ // Update selected team when URL param changes
+ useEffect(() => {
+ const team = mockTeams.find((t) => t.id === teamIdParam)
+ if (team) {
+ setSelectedTeam(team)
+ }
+ }, [teamIdParam])
+
+ // Filter data based on selected team
+ const reports = mockReports.filter((report) => report.teamId === selectedTeam.id)
+ const policyLists = mockPolicyLists.filter((list) => list.teamId === selectedTeam.id)
+
+ return (
+
+ <>
+ <TabNavigation selectedTeam={selectedTeam} currentTab="overview" />
+ <div className="space-y-8">
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
+ <Card className="border-gray-800 bg-gray-950">
+ <CardHeader className="pb-2">
+ <CardTitle className="text-sm font-medium">Protected Rooms</CardTitle>
+ </CardHeader>
+ <CardContent>
+ <div className="text-2xl font-bold">{selectedTeam.rooms.length}</div>
+ <div className="flex justify-between items-center">
+ <p className="text-xs text-gray-400">Monitored by this bot</p>
+ <AddProtectedRoom />
+ </div>
+ </CardContent>
+ </Card>
+ <Card className="border-gray-800 bg-gray-950">
+ <CardHeader className="pb-2">
+ <CardTitle className="text-sm font-medium">Active Reports</CardTitle>
+ </CardHeader>
+ <CardContent>
+ <div className="text-2xl font-bold">{reports.length}</div>
+ <p className="text-xs text-gray-400">{reports.filter((r) => r.priority === "high").length} high priority</p>
+ </CardContent>
+ </Card>
+ <Card className="border-gray-800 bg-gray-950">
+ <CardHeader className="pb-2">
+ <CardTitle className="text-sm font-medium">Bans Issued</CardTitle>
+ </CardHeader>
+ <CardContent>
+ <div className="text-2xl font-bold">
+ {policyLists.reduce((total, list) => total + list.entries.length, 0)}
+ </div>
+ <p className="text-xs text-gray-400">Across all policy lists</p>
+ </CardContent>
+ </Card>
+ <Card className="border-gray-800 bg-gray-950">
+ <CardHeader className="pb-2">
+ <CardTitle className="text-sm font-medium">Bot Status</CardTitle>
+ </CardHeader>
+ <CardContent>
+ <div className="flex items-center gap-2">
+ <div className="h-3 w-3 rounded-full bg-green-500"></div>
+ <div className="text-sm font-medium">Online</div>
+ </div>
+ <p className="text-xs text-gray-400">Last restart: 7d ago</p>
+ </CardContent>
+ </Card>
+ </div>
+
+ <div className="grid gap-4 md:grid-cols-2">
+ <Card className="border-gray-800 bg-gray-950">
+ <CardHeader>
+ <CardTitle>Recent Activity</CardTitle>
+ <CardDescription className="text-gray-400">Latest moderation actions</CardDescription>
+ </CardHeader>
+ <CardContent>
+ <div className="space-y-4">
+ <div className="flex items-start gap-4">
+ <div className="rounded-full bg-red-500/20 p-2">
+ <Ban className="h-4 w-4 text-red-500" />
+ </div>
+ <div className="space-y-1">
+ <p className="text-sm font-medium">User banned</p>
+ <p className="text-xs text-gray-400">@spammer:matrix.org was banned from all rooms</p>
+ <p className="text-xs text-gray-500">10 minutes ago</p>
+ </div>
+ </div>
+ <div className="flex items-start gap-4">
+ <div className="rounded-full bg-yellow-500/20 p-2">
+ <AlertTriangle className="h-4 w-4 text-yellow-500" />
+ </div>
+ <div className="space-y-1">
+ <p className="text-sm font-medium">Report created</p>
+ <p className="text-xs text-gray-400">Spam detected in #support</p>
+ <p className="text-xs text-gray-500">1 hour ago</p>
+ </div>
+ </div>
+ <div className="flex items-start gap-4">
+ <div className="rounded-full bg-green-500/20 p-2">
+ <CheckCircle className="h-4 w-4 text-green-500" />
+ </div>
+ <div className="space-y-1">
+ <p className="text-sm font-medium">Report resolved</p>
+ <p className="text-xs text-gray-400">Harassment report in #community resolved</p>
+ <p className="text-xs text-gray-500">3 hours ago</p>
+ </div>
+ </div>
+ </div>
+ </CardContent>
+ </Card>
+
+ <Card className="border-gray-800 bg-gray-950">
+ <CardHeader>
+ <div className="flex items-center justify-between">
+ <div>
+ <CardTitle>Protected Rooms</CardTitle>
+ <CardDescription className="text-gray-400">Rooms monitored by this bot</CardDescription>
+ </div>
+ <AddProtectedRoom filled />
+ </div>
+ </CardHeader>
+ <CardContent>
+ <ProtectedRomsList team={selectedTeam} />
+ </CardContent>
+ </Card>
+ </div>
+ </div>
+ </>
+ )
+}
diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx
@@ -1,306 +1,14 @@
-"use client"
+import { redirect } from "next/navigation"
-import { useState } from "react"
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
+export default async function DashboardPage({
+ searchParams,
+}: {
+ searchParams: Promise<{ [key: string]: string | string[] | undefined }>
+}) {
+ const teamId = (await searchParams).team as string | undefined
-import { mockTeams, Team, TeamManagement } from "./team-management"
-import Bans from "./components/bans-pane"
-import { AnalyticsDashboard } from "./components/analytics-dashboard"
-import { ReportsTab } from "./components/reports-tab"
-import { DashboardHeader } from "./components/dashboard-header"
-import { OverviewTab } from "./components/overview-tab"
+ // Redirect to overview with team parameter
+ const redirectUrl = teamId ? `/dashboard/overview?team=${teamId}` : "/dashboard/overview"
-export const EBanTypes = {
- User: "user",
- Server: "server",
- Room: "room",
-} as const;
-type BanTypes = typeof EBanTypes[keyof typeof EBanTypes];
-
-export interface PolicyList {
- id: string;
- name: string;
- description: string;
- readOnly: boolean;
- teamId: string;
- entries: {
- id: string,
- target: string,
- reason: string,
- timestamp: string,
- type: BanTypes;
- }[];
-};
-
-export interface Report {
- id: string;
- type: string;
- status: string;
- priority: string;
- timestamp: string;
- reporter: string;
- room: string;
- teamId: string;
- subject: {
- type: string;
- id?: string;
- sender: string;
- content?: {
- msgtype: string;
- body: string;
- url?: string;
- info?: {
- mimetype: string;
- w: number;
- h: number;
- size: number;
- };
- };
- target?: string;
- timestamp: string;
- };
- reason: string;
-};
-
-export default function DashboardPage() {
- const [activeTab, setActiveTab] = useState("overview")
- const [selectedTeam, setSelectedTeam] = useState<Team>(mockTeams[0])
-
- // Mock data for reports - filtered by team
- const reports: Report[] = [
- {
- id: "rep_1",
- type: "message",
- status: "open",
- priority: "high",
- timestamp: "2025-04-21T14:32:00Z",
- reporter: "@moderator:matrix.org",
- room: "#general:matrix.org",
- teamId: "team1",
- subject: {
- type: "message",
- id: "$1234567890abcdefghij:matrix.org",
- sender: "@spammer:badserver.org",
- content: {
- msgtype: "m.text",
- body: "Hey everyone! Check out this amazing investment opportunity at scam-crypto-site.com - 1000% returns guaranteed!",
- },
- timestamp: "2025-04-21T14:30:00Z",
- },
- reason: "Spam/scam content",
- },
- {
- id: "rep_2",
- type: "invite",
- status: "open",
- priority: "medium",
- timestamp: "2025-04-21T13:15:00Z",
- reporter: "@user:matrix.org",
- room: "#general:matrix.org",
- teamId: "team1",
- subject: {
- type: "invite",
- sender: "@spambot:badserver.org",
- target: "@user:matrix.org",
- timestamp: "2025-04-21T13:10:00Z",
- },
- reason: "Unsolicited invite from unknown user",
- },
- {
- id: "rep_3",
- type: "message",
- status: "open",
- priority: "high",
- timestamp: "2025-04-21T12:05:00Z",
- reporter: "@admin:matrix.org",
- room: "#support:matrix.org",
- teamId: "team1",
- subject: {
- type: "message",
- id: "$abcdefghij1234567890:matrix.org",
- sender: "@troll:badserver.org",
- content: {
- msgtype: "m.image",
- body: "image.jpg",
- url: "mxc://matrix.org/abcdefghijklmnopqrstuvwxyz",
- info: {
- mimetype: "image/jpeg",
- w: 800,
- h: 600,
- size: 95431,
- },
- },
- timestamp: "2025-04-21T12:00:00Z",
- },
- reason: "Inappropriate image",
- },
- {
- id: "rep_4",
- type: "message",
- status: "open",
- priority: "medium",
- timestamp: "2025-04-21T11:45:00Z",
- reporter: "@dev1:matrix.org",
- room: "#development:matrix.org",
- teamId: "team2",
- subject: {
- type: "message",
- id: "$devmessage123456789:matrix.org",
- sender: "@newuser:matrix.org",
- content: {
- msgtype: "m.text",
- body: "Can someone help me with this code? It's not working: <script>alert('hack');</script>",
- },
- timestamp: "2025-04-21T11:40:00Z",
- },
- reason: "Potential code injection attempt",
- },
- ].filter((report) => report.teamId === selectedTeam.id)
-
- // Mock data for policy lists - filtered by team
- const policyLists: PolicyList[] = [
- {
- id: "global",
- name: "Global Ban List",
- description: "Shared ban list for all Matrix communities",
- readOnly: true,
- teamId: "team1",
- entries: [
- {
- id: "ban_1",
- target: "@spammer:badserver.org",
- reason: "Spam across multiple rooms",
- timestamp: "2025-04-20T10:30:00Z",
- type: EBanTypes.User,
- },
- {
- id: "ban_2",
- target: "@troll:badserver.org",
- reason: "Harassment and inappropriate content",
- timestamp: "2025-04-19T15:45:00Z",
- type: EBanTypes.User,
- },
- {
- id: "ban_3",
- target: "@phisher:scam.org",
- reason: "Phishing attempts",
- timestamp: "2025-04-18T09:20:00Z",
- type: EBanTypes.User,
- },
- ],
- },
- {
- id: "community",
- name: "Community Ban List",
- description: "Ban list for our community rooms",
- readOnly: false,
- teamId: "team1",
- entries: [
- {
- id: "ban_4",
- target: "@disruptor:matrix.org",
- reason: "Disruptive behavior in #general",
- timestamp: "2025-04-21T08:15:00Z",
- type: EBanTypes.User,
- },
- {
- id: "ban_5",
- target: "@bot123:unknown.org",
- reason: "Automated spam",
- timestamp: "2025-04-20T14:30:00Z",
- type: EBanTypes.User,
- },
- // Server ban
- {
- id: "ban_6",
- target: "badserver.org",
- reason: "Known spam server",
- timestamp: "2025-04-19T11:00:00Z",
- type: EBanTypes.Server,
- },
- ],
- },
- {
- id: "support",
- name: "Support Rooms Ban List",
- description: "Ban list specific to support rooms",
- readOnly: false,
- teamId: "team1",
- entries: [
- {
- id: "ban_6",
- target: "@angryuser:matrix.org",
- reason: "Abusive language to support staff",
- timestamp: "2025-04-21T11:45:00Z",
- type: EBanTypes.User,
- },
- ],
- },
- {
- id: "dev",
- name: "Development Ban List",
- description: "Ban list for development rooms",
- readOnly: false,
- teamId: "team2",
- entries: [
- {
- id: "ban_7",
- target: "@spambot:matrix.org",
- reason: "Code spam in development channels",
- timestamp: "2025-04-19T09:30:00Z",
- type: EBanTypes.User,
- },
- ],
- },
- ].filter((list) => list.teamId === selectedTeam.id)
-
-
- const handleTeamChange = (teamId: string) => {
- const team = mockTeams.find((t) => t.id === teamId)
- if (team) {
- setSelectedTeam(team)
- }
- }
-
- return (
- <div className="flex min-h-screen flex-col bg-black text-white">
- <DashboardHeader
- selectedTeam={selectedTeam}
- onTeamChange={handleTeamChange}
- activeTab={activeTab}
- setActiveTab={setActiveTab}
- teams={mockTeams}
- policyLists={policyLists}
- />
- <main className="flex-1 container px-4 py-8 md:px-6 md:py-12">
- <Tabs defaultValue="overview" value={activeTab} onValueChange={setActiveTab} className="space-y-8">
- <div className="flex items-center justify-between">
- <h1 className="text-3xl font-bold tracking-tight">{selectedTeam.name} Dashboard</h1>
- <TabsList className="bg-gray-900">
- <TabsTrigger value="overview">Overview</TabsTrigger>
- <TabsTrigger value="reports">Reports</TabsTrigger>
- <TabsTrigger value="bans">Bans</TabsTrigger>
- <TabsTrigger value="analytics">Analytics</TabsTrigger>
- <TabsTrigger value="settings">Settings</TabsTrigger>
- </TabsList>
- </div>
- <TabsContent value="overview">
- <OverviewTab selectedTeam={selectedTeam} policyLists={policyLists} reports={reports} />
- </TabsContent>
- <TabsContent value="reports">
- <ReportsTab reports={reports} />
- </TabsContent>
- <TabsContent value="bans">
- <Bans policyLists={policyLists} />
- </TabsContent>
- <TabsContent value="analytics">
- <AnalyticsDashboard selectedTeam={selectedTeam} />
- </TabsContent>
- <TabsContent value="settings">
- <TeamManagement selectedTeam={selectedTeam} onTeamChange={handleTeamChange} />
- </TabsContent>
- </Tabs>
- </main>
- </div>
- )
+ redirect(redirectUrl)
}
diff --git a/src/app/dashboard/reports/page.tsx b/src/app/dashboard/reports/page.tsx
@@ -0,0 +1,277 @@
+"use client"
+
+import { useEffect, useState } from "react"
+import { AlertTriangle, CheckCircle, MessageSquare, UserPlus, Filter, Ban } from "lucide-react"
+
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
+import { Badge } from "@/components/ui/badge"
+import { Button } from "@/components/ui/button"
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
+import { BlurredText } from "@/components/blurred-text"
+import { BlurredImage } from "@/components/blurred-image"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog"
+import { mockReports, mockTeams } from "../mockData";
+import { useSearchParams } from "next/navigation"
+import TabNavigation from "../components/tab-navigation"
+
+export default function ReportsPage() {
+ const searchParams = useSearchParams()
+ const teamIdParam = searchParams.get("team")
+ const [selectedTeam, setSelectedTeam] = useState(mockTeams.find((t) => t.id === teamIdParam) || mockTeams[0])
+
+ // Update selected team when URL param changes
+ useEffect(() => {
+ const team = mockTeams.find((t) => t.id === teamIdParam)
+ if (team) {
+ setSelectedTeam(team)
+ }
+ }, [teamIdParam])
+
+ // Filter reports based on selected team
+ const reports = mockReports.filter((report) => report.teamId === selectedTeam.id)
+ const [selectedReport, setSelectedReport] = useState<string | null>(null)
+ const [filter, setFilter] = useState("all")
+
+ const filteredReports =
+ filter === "all"
+ ? reports
+ : filter === "high"
+ ? reports.filter((r) => r.priority === "high")
+ : reports.filter((r) => r.type === filter)
+
+ return (
+ <>
+ <TabNavigation selectedTeam={selectedTeam} currentTab="reports" />
+ <Card className="border-gray-800 bg-gray-950">
+ <CardHeader className="pb-2">
+ <div className="flex items-center justify-between">
+ <CardTitle>Active Reports</CardTitle>
+ <div className="flex items-center gap-2">
+ <Select defaultValue="all" onValueChange={setFilter}>
+ <SelectTrigger className="h-8 w-[120px] bg-gray-900 border-gray-800">
+ <SelectValue placeholder="Filter" />
+ </SelectTrigger>
+ <SelectContent className="bg-gray-900 border-gray-800">
+ <SelectItem value="all">All Reports</SelectItem>
+ <SelectItem value="message">Messages</SelectItem>
+ <SelectItem value="invite">Invites</SelectItem>
+ <SelectItem value="high">High Priority</SelectItem>
+ </SelectContent>
+ </Select>
+ </div>
+ </div>
+ <CardDescription className="text-gray-400">Reports requiring moderation action</CardDescription>
+ </CardHeader>
+ <CardContent>
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+ <div className="space-y-4">
+ <h3 className="text-sm font-medium text-gray-400">Report List</h3>
+ {filteredReports.length > 0 ? (
+ filteredReports.map((report) => (
+ <div
+ key={report.id}
+ className={`p-3 rounded-md border cursor-pointer transition-colors ${selectedReport === report.id
+ ? "bg-gray-800 border-purple-600"
+ : "bg-gray-900 border-gray-800 hover:border-gray-700"
+ }`}
+ onClick={() => setSelectedReport(report.id)}
+ >
+ <div className="flex items-center justify-between mb-2">
+ <div className="flex items-center gap-2">
+ {report.type === "message" ? (
+ <MessageSquare className="h-4 w-4 text-blue-400" />
+ ) : (
+ <UserPlus className="h-4 w-4 text-green-400" />
+ )}
+ <span className="text-sm font-medium">
+ {report.type === "message" ? "Message Report" : "Invite Report"}
+ </span>
+ </div>
+ <Badge
+ className={
+ report.priority === "high"
+ ? "bg-red-900/50 text-red-300 hover:bg-red-900/70"
+ : "bg-yellow-900/50 text-yellow-300 hover:bg-yellow-900/70"
+ }
+ >
+ {report.priority}
+ </Badge>
+ </div>
+ <p className="text-xs text-gray-400 mb-1">{report.room}</p>
+ <p className="text-xs text-gray-400 mb-1">
+ {report.type === "message"
+ ? `From: ${report.subject.sender}`
+ : `From: ${report.subject.sender} To: ${report.subject.target}`}
+ </p>
+ <p className="text-xs text-gray-500">{new Date(report.timestamp).toLocaleString()}</p>
+ </div>
+ ))
+ ) : (
+ <div className="flex flex-col items-center justify-center p-8 text-center">
+ <CheckCircle className="h-8 w-8 text-gray-500 mb-2" />
+ <p className="text-gray-400">No active reports</p>
+ </div>
+ )}
+ </div>
+
+ <div className="space-y-4">
+ <h3 className="text-sm font-medium text-gray-400">Report Details</h3>
+ {selectedReport ? (
+ (() => {
+ const report = reports.find((r) => r.id === selectedReport)
+ if (!report) return <p>Select a report to view details</p>
+
+ return (
+ <div className="space-y-4">
+ <div className="p-4 rounded-md bg-gray-900 border border-gray-800">
+ <div className="flex items-center justify-between mb-3">
+ <h4 className="font-medium">Report Information</h4>
+ <Badge className="bg-purple-900/50 text-purple-300">{report.status}</Badge>
+ </div>
+
+ <div className="space-y-2 text-sm">
+ <div className="flex justify-between">
+ <span className="text-gray-400">Reported by:</span>
+ <span>{report.reporter}</span>
+ </div>
+ <div className="flex justify-between">
+ <span className="text-gray-400">Room:</span>
+ <span>{report.room}</span>
+ </div>
+ <div className="flex justify-between">
+ <span className="text-gray-400">Reason:</span>
+ <span>{report.reason}</span>
+ </div>
+ <div className="flex justify-between">
+ <span className="text-gray-400">Time:</span>
+ <span>{new Date(report.timestamp).toLocaleString()}</span>
+ </div>
+ </div>
+ </div>
+
+ <div className="p-4 rounded-md bg-gray-900 border border-gray-800">
+ <h4 className="font-medium mb-3">
+ {report.type === "message" ? "Message Content" : "Invite Details"}
+ </h4>
+
+ {report.type === "message" && (
+ <div className="space-y-3">
+ <div className="flex justify-between text-sm">
+ <span className="text-gray-400">Sender:</span>
+ <span>{report.subject.sender}</span>
+ </div>
+ <div className="flex justify-between text-sm">
+ <span className="text-gray-400">Message ID:</span>
+ <span className="font-mono text-xs">{report.subject.id}</span>
+ </div>
+ <div className="flex justify-between text-sm">
+ <span className="text-gray-400">Time:</span>
+ <span>{new Date(report.subject.timestamp).toLocaleString()}</span>
+ </div>
+
+ <div className="mt-4">
+ <div className="flex items-center justify-between mb-2">
+ <span className="text-sm text-gray-400">Content:</span>
+ {report.subject.content?.msgtype === "m.text" && (
+ <Button variant="ghost" size="sm" className="h-6 px-2 text-gray-400">
+ <Filter className="h-3 w-3 mr-1" />
+ Filter
+ </Button>
+ )}
+ </div>
+
+ {report.subject.content?.msgtype === "m.text" ? (
+ <BlurredText text={report.subject.content?.body} />
+ ) : report.subject.content?.msgtype === "m.image" ? (
+ <div className="mt-2">
+ <BlurredImage
+ src="/placeholder.svg?height=300&width=400"
+ alt={report.subject.content?.body}
+ width={400}
+ height={300}
+ />
+ <p className="text-xs text-gray-500 mt-1">Image: {report.subject.content.body}</p>
+ </div>
+ ) : (
+ <p className="text-sm">Unsupported content type</p>
+ )}
+ </div>
+ </div>
+ )}
+
+ {report.type === "invite" && (
+ <div className="space-y-3">
+ <div className="flex justify-between text-sm">
+ <span className="text-gray-400">Sender:</span>
+ <span>{report.subject.sender}</span>
+ </div>
+ <div className="flex justify-between text-sm">
+ <span className="text-gray-400">Target:</span>
+ <span>{report.subject.target}</span>
+ </div>
+ <div className="flex justify-between text-sm">
+ <span className="text-gray-400">Time:</span>
+ <span>{new Date(report.subject.timestamp).toLocaleString()}</span>
+ </div>
+ </div>
+ )}
+ </div>
+
+ <div className="flex gap-2 mt-4">
+ <Button className="bg-green-600 text-white hover:bg-green-700">
+ <CheckCircle className="mr-2 h-4 w-4" />
+ Resolve
+ </Button>
+ <Dialog>
+ <DialogTrigger asChild>
+ <Button
+ variant="outline"
+ className="border-red-500 text-red-400 hover:bg-red-950 hover:text-red-300"
+ >
+ <Ban className="mr-2 h-4 w-4" />
+ Ban User
+ </Button>
+ </DialogTrigger>
+ <DialogContent className="bg-gray-950 border-gray-800">
+ <DialogHeader>
+ <DialogTitle>Ban User</DialogTitle>
+ <DialogDescription className="text-gray-400">
+ Ban {report.subject.sender} from your Matrix rooms
+ </DialogDescription>
+ </DialogHeader>
+ {/* Ban dialog content */}
+ </DialogContent>
+ </Dialog>
+ {report.type === "message" && (
+ <Button
+ variant="outline"
+ className="border-gray-700 text-gray-400 hover:bg-gray-900 hover:text-gray-300"
+ >
+ <MessageSquare className="mr-2 h-4 w-4" />
+ Delete Message
+ </Button>
+ )}
+ </div>
+ </div>
+ )
+ })()
+ ) : (
+ <div className="flex flex-col items-center justify-center p-8 text-center">
+ <AlertTriangle className="h-8 w-8 text-gray-500 mb-2" />
+ <p className="text-gray-400">Select a report to view details</p>
+ </div>
+ )}
+ </div>
+ </div>
+ </CardContent>
+ </Card>
+ </>
+ )
+}
diff --git a/src/app/dashboard/settings/page.tsx b/src/app/dashboard/settings/page.tsx
@@ -0,0 +1,246 @@
+"use client";
+
+import { useEffect, useState } from "react"
+import { UserPlus, Settings, Crown, Trash2, LogOut } from "lucide-react"
+
+import { Button } from "@/components/ui/button"
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { Badge } from "@/components/ui/badge"
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu"
+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";
+
+
+export default function TeamManagement() {
+ const searchParams = useSearchParams()
+ const teamIdParam = searchParams.get("team")
+ const [selectedTeam, setSelectedTeam] = useState(mockTeams.find((t) => t.id === teamIdParam) || mockTeams[0])
+ const [activeTab, setActiveTab] = useState("members")
+
+ // Update selected team when URL param changes
+ useEffect(() => {
+ const team = mockTeams.find((t) => t.id === teamIdParam)
+ if (team) {
+ setSelectedTeam(team)
+ }
+ }, [teamIdParam])
+
+ const handleTeamChange = (teamId: string) => {
+ const team = mockTeams.find((t) => t.id === teamId)
+ if (team) {
+ setSelectedTeam(team)
+
+ // Update URL with team parameter
+ const url = new URL(window.location.href)
+ url.searchParams.set("team", teamId)
+ window.history.pushState({}, "", url.toString())
+ }
+ }
+
+ return (
+ <>
+ <TabNavigation selectedTeam={selectedTeam} currentTab="settings" />
+ <Card className="border-gray-800 bg-gray-950">
+ <CardHeader>
+ <div className="flex items-center justify-between">
+ <div>
+ <CardTitle>Team Management</CardTitle>
+ <CardDescription className="text-gray-400">Manage your bots and team members</CardDescription>
+ </div>
+ <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}
+ </Button>
+ </DropdownMenuTrigger>
+ <DropdownMenuContent className="w-56 bg-gray-900 border-gray-800">
+ <DropdownMenuLabel>Your Bots</DropdownMenuLabel>
+ <DropdownMenuSeparator className="bg-gray-800" />
+ {mockTeams.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>
+ </CardHeader>
+ <CardContent>
+ <Tabs defaultValue="members" value={activeTab} onValueChange={setActiveTab} className="space-y-4">
+ <TabsList className="bg-gray-900">
+ <TabsTrigger value="members">Team Members</TabsTrigger>
+ <TabsTrigger value="rooms">Protected Rooms</TabsTrigger>
+ <TabsTrigger value="settings">Bot Settings</TabsTrigger>
+ </TabsList>
+
+ <TabsContent value="members" className="space-y-4">
+ <div className="flex items-center justify-between">
+ <h3 className="text-sm font-medium text-gray-400">Team Members</h3>
+ <Dialog>
+ <DialogTrigger asChild>
+ <Button size="sm" className="bg-purple-600 text-white hover:bg-purple-700">
+ <UserPlus className="mr-2 h-4 w-4" />
+ Invite Member
+ </Button>
+ </DialogTrigger>
+ <DialogContent className="bg-gray-950 border-gray-800">
+ <DialogHeader>
+ <DialogTitle>Invite Team Member</DialogTitle>
+ <DialogDescription className="text-gray-400">
+ Invite a Matrix user to join your moderation team
+ </DialogDescription>
+ </DialogHeader>
+ <div className="space-y-4 py-4">
+ <div className="space-y-2">
+ <Label htmlFor="invite-matrix-id">Matrix ID</Label>
+ <Input
+ id="invite-matrix-id"
+ placeholder="@user:matrix.org"
+ className="bg-gray-900 border-gray-800"
+ />
+ </div>
+ <div className="space-y-2">
+ <Label htmlFor="invite-role">Role</Label>
+ <select
+ id="invite-role"
+ className="w-full rounded-md border border-gray-800 bg-gray-900 px-3 py-2 text-sm"
+ >
+ <option value="moderator">Moderator</option>
+ <option value="viewer">Viewer (Read-only)</option>
+ </select>
+ </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-purple-600 text-white hover:bg-purple-700">
+ Send Invitation
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ </div>
+
+ <ScrollArea className="h-[300px] rounded-md border border-gray-800">
+ <div className="p-4 space-y-3">
+ {selectedTeam.members.map((member) => (
+ <div key={member.id} className="flex items-center justify-between p-3 rounded-md bg-gray-900">
+ <div className="flex items-center gap-3">
+ <Avatar className="h-10 w-10 border border-gray-800">
+ <AvatarImage src={member.avatar || ""} />
+ <AvatarFallback className="bg-gray-800 text-gray-400">
+ {member.name.substring(1, 3).toUpperCase()}
+ </AvatarFallback>
+ </Avatar>
+ <div>
+ <p className="font-medium">{member.name}</p>
+ <div className="flex items-center gap-2">
+ <Badge
+ className={
+ member.role === "owner"
+ ? "bg-purple-900/50 text-purple-300"
+ : "bg-blue-900/50 text-blue-300"
+ }
+ >
+ {member.role}
+ </Badge>
+ <span className="text-xs text-gray-500">
+ Joined {new Date(member.joined).toLocaleDateString()}
+ </span>
+ </div>
+ </div>
+ </div>
+ {member.role !== "owner" && (
+ <DropdownMenu>
+ <DropdownMenuTrigger asChild>
+ <Button variant="ghost" size="sm" className="h-8 w-8 p-0">
+ <span className="sr-only">Open menu</span>
+ <Settings className="h-4 w-4" />
+ </Button>
+ </DropdownMenuTrigger>
+ <DropdownMenuContent align="end" className="bg-gray-900 border-gray-800">
+ <DropdownMenuItem className="text-yellow-400 focus:text-yellow-400 focus:bg-yellow-900/20">
+ <Settings className="mr-2 h-4 w-4" />
+ Change Role
+ </DropdownMenuItem>
+ <DropdownMenuItem className="text-red-400 focus:text-red-400 focus:bg-red-900/20">
+ <LogOut className="mr-2 h-4 w-4" />
+ Remove from Team
+ </DropdownMenuItem>
+ </DropdownMenuContent>
+ </DropdownMenu>
+ )}
+ </div>
+ ))}
+ </div>
+ </ScrollArea>
+ </TabsContent>
+
+ <TabsContent value="rooms" className="space-y-4">
+ <div className="flex items-center justify-between">
+ <h3 className="text-sm font-medium text-gray-400">Protected Rooms</h3>
+ <AddProtectedRoom filled />
+ </div>
+
+ <ScrollArea className="h-[300px] rounded-md border border-gray-800">
+ <ProtectedRomsList team={selectedTeam} />
+ </ScrollArea>
+ </TabsContent>
+
+ <TabsContent value="settings" className="space-y-4">
+ <div className="space-y-4">
+ <div className="space-y-2">
+ <Label htmlFor="bot-name">Bot Name</Label>
+ <Input id="bot-name" defaultValue={selectedTeam.name} className="bg-gray-900 border-gray-800" />
+ </div>
+ <div className="pt-4 flex justify-between">
+ <Button variant="outline" className="border-red-500 text-red-400 hover:bg-red-950 hover:text-red-300">
+ <Trash2 className="mr-2 h-4 w-4" />
+ Delete Bot
+ </Button>
+ <Button className="bg-purple-600 text-white hover:bg-purple-700">Save Changes</Button>
+ </div>
+ </div>
+ </TabsContent>
+ </Tabs>
+ </CardContent>
+ </Card>
+ </>
+ )
+}
diff --git a/src/app/dashboard/team-management.tsx b/src/app/dashboard/team-management.tsx
@@ -1,357 +0,0 @@
-import { useState } from "react"
-import { UserPlus, Settings, Crown, Trash2, LogOut } from "lucide-react"
-
-import { Button } from "@/components/ui/button"
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
-import { Input } from "@/components/ui/input"
-import { Label } from "@/components/ui/label"
-import { Badge } from "@/components/ui/badge"
-import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog"
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuLabel,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu"
-import { ScrollArea } from "@/components/ui/scroll-area"
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
-import AddProtectedRoom from "./components/modals/add-protected-room";
-import ProtectedRomsList from "./components/protected-rooms-list";
-
-export interface Team {
- id: string;
- name: string;
- description: string;
- owner: string;
- created: string;
- members: {
- id: string;
- name: string;
- role: "owner" | "moderator";
- avatar?: string;
- joined: string;
- }[];
- rooms: string[];
-}
-
-// Mock data for teams/bots
-export const mockTeams: Team[] = [
- {
- id: "team1",
- name: "Main Community Bot",
- description: "Moderation for main community rooms",
- owner: "@admin:matrix.org",
- created: "2025-01-15T10:00:00Z",
- members: [
- {
- id: "user1",
- name: "@admin:matrix.org",
- role: "owner",
- avatar: undefined,
- joined: "2025-01-15T10:00:00Z",
- },
- {
- id: "user2",
- name: "@mod1:matrix.org",
- role: "moderator",
- avatar: undefined,
- joined: "2025-01-16T14:30:00Z",
- },
- {
- id: "user3",
- name: "@mod2:matrix.org",
- role: "moderator",
- avatar: undefined,
- joined: "2025-02-01T09:15:00Z",
- },
- ],
- rooms: ["#general:matrix.org", "#support:matrix.org", "#community:matrix.org"],
- },
- {
- id: "team2",
- name: "Development Team Bot",
- description: "Moderation for development-related rooms",
- owner: "@admin:matrix.org",
- created: "2025-02-10T11:30:00Z",
- members: [
- {
- id: "user1",
- name: "@admin:matrix.org",
- role: "owner",
- avatar: undefined,
- joined: "2025-02-10T11:30:00Z",
- },
- {
- id: "user4",
- name: "@dev1:matrix.org",
- role: "moderator",
- avatar: undefined,
- joined: "2025-02-11T13:45:00Z",
- },
- ],
- rooms: ["#development:matrix.org", "#coding:matrix.org"],
- },
-]
-
-interface TeamManagementProps {
- selectedTeam: Team;
- onTeamChange: (teamId: string) => void;
-}
-
-export function TeamManagement({ selectedTeam: selectedTeamProp, onTeamChange }: TeamManagementProps) {
- const [activeTab, setActiveTab] = useState("members")
- const [selectedTeam, setSelectedTeam] = useState<Team>(selectedTeamProp)
- const [inviteEmail, setInviteEmail] = useState("")
-
- const handleTeamChange = (teamId: string) => {
- const team = mockTeams.find((t) => t.id === teamId)
- if (team) {
- setSelectedTeam(team)
- onTeamChange(team.id);
- }
- }
-
- const handleInvite = () => {
- // In a real app, this would send an invitation
- console.log(`Inviting ${inviteEmail} to team ${selectedTeam.id}`)
- setInviteEmail("")
- }
-
- return (
- <Card className="border-gray-800 bg-gray-950">
- <CardHeader>
- <div className="flex items-center justify-between">
- <div>
- <CardTitle>Team Management</CardTitle>
- <CardDescription className="text-gray-400">Manage your bots and team members</CardDescription>
- </div>
- <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}
- </Button>
- </DropdownMenuTrigger>
- <DropdownMenuContent className="w-56 bg-gray-900 border-gray-800">
- <DropdownMenuLabel>Your Bots</DropdownMenuLabel>
- <DropdownMenuSeparator className="bg-gray-800" />
- {mockTeams.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" />
- <Dialog>
- <DialogTrigger asChild>
- <DropdownMenuItem>
- <Settings className="mr-2 h-4 w-4" />
- Create New Bot
- </DropdownMenuItem>
- </DialogTrigger>
- <DialogContent className="bg-gray-950 border-gray-800">
- <DialogHeader>
- <DialogTitle>Create New Bot</DialogTitle>
- <DialogDescription className="text-gray-400">
- Set up a new Draupnir bot for a different community
- </DialogDescription>
- </DialogHeader>
- <div className="space-y-4 py-4">
- <div className="space-y-2">
- <Label htmlFor="bot-name">Bot Name</Label>
- <Input id="bot-name" placeholder="My Community Bot" className="bg-gray-900 border-gray-800" />
- </div>
- <div className="space-y-2">
- <Label htmlFor="bot-description">Description</Label>
- <Input
- id="bot-description"
- placeholder="Moderation for my community"
- className="bg-gray-900 border-gray-800"
- />
- </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-purple-600 text-white hover:bg-purple-700">Create Bot</Button>
- </DialogFooter>
- </DialogContent>
- </Dialog>
- </DropdownMenuContent>
- </DropdownMenu>
- </div>
- </CardHeader>
- <CardContent>
- <Tabs defaultValue="members" value={activeTab} onValueChange={setActiveTab} className="space-y-4">
- <TabsList className="bg-gray-900">
- <TabsTrigger value="members">Team Members</TabsTrigger>
- <TabsTrigger value="rooms">Protected Rooms</TabsTrigger>
- <TabsTrigger value="settings">Bot Settings</TabsTrigger>
- </TabsList>
-
- <TabsContent value="members" className="space-y-4">
- <div className="flex items-center justify-between">
- <h3 className="text-sm font-medium text-gray-400">Team Members</h3>
- <Dialog>
- <DialogTrigger asChild>
- <Button size="sm" className="bg-purple-600 text-white hover:bg-purple-700">
- <UserPlus className="mr-2 h-4 w-4" />
- Invite Member
- </Button>
- </DialogTrigger>
- <DialogContent className="bg-gray-950 border-gray-800">
- <DialogHeader>
- <DialogTitle>Invite Team Member</DialogTitle>
- <DialogDescription className="text-gray-400">
- Invite a Matrix user to join your moderation team
- </DialogDescription>
- </DialogHeader>
- <div className="space-y-4 py-4">
- <div className="space-y-2">
- <Label htmlFor="invite-matrix-id">Matrix ID</Label>
- <Input
- id="invite-matrix-id"
- placeholder="@user:matrix.org"
- className="bg-gray-900 border-gray-800"
- value={inviteEmail}
- onChange={(e) => setInviteEmail(e.target.value)}
- />
- </div>
- <div className="space-y-2">
- <Label htmlFor="invite-role">Role</Label>
- <select
- id="invite-role"
- className="w-full rounded-md border border-gray-800 bg-gray-900 px-3 py-2 text-sm"
- >
- <option value="moderator">Moderator</option>
- <option value="viewer">Viewer (Read-only)</option>
- </select>
- </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-purple-600 text-white hover:bg-purple-700" onClick={handleInvite}>
- Send Invitation
- </Button>
- </DialogFooter>
- </DialogContent>
- </Dialog>
- </div>
-
- <ScrollArea className="h-[300px] rounded-md border border-gray-800">
- <div className="p-4 space-y-3">
- {selectedTeam.members.map((member) => (
- <div key={member.id} className="flex items-center justify-between p-3 rounded-md bg-gray-900">
- <div className="flex items-center gap-3">
- <Avatar className="h-10 w-10 border border-gray-800">
- <AvatarImage src={member.avatar || ""} />
- <AvatarFallback className="bg-gray-800 text-gray-400">
- {member.name.substring(1, 3).toUpperCase()}
- </AvatarFallback>
- </Avatar>
- <div>
- <p className="font-medium">{member.name}</p>
- <div className="flex items-center gap-2">
- <Badge
- className={
- member.role === "owner"
- ? "bg-purple-900/50 text-purple-300"
- : "bg-blue-900/50 text-blue-300"
- }
- >
- {member.role}
- </Badge>
- <span className="text-xs text-gray-500">
- Joined {new Date(member.joined).toLocaleDateString()}
- </span>
- </div>
- </div>
- </div>
- {member.role !== "owner" && (
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <Button variant="ghost" size="sm" className="h-8 w-8 p-0">
- <span className="sr-only">Open menu</span>
- <Settings className="h-4 w-4" />
- </Button>
- </DropdownMenuTrigger>
- <DropdownMenuContent align="end" className="bg-gray-900 border-gray-800">
- <DropdownMenuItem className="text-yellow-400 focus:text-yellow-400 focus:bg-yellow-900/20">
- <Settings className="mr-2 h-4 w-4" />
- Change Role
- </DropdownMenuItem>
- <DropdownMenuItem className="text-red-400 focus:text-red-400 focus:bg-red-900/20">
- <LogOut className="mr-2 h-4 w-4" />
- Remove from Team
- </DropdownMenuItem>
- </DropdownMenuContent>
- </DropdownMenu>
- )}
- </div>
- ))}
- </div>
- </ScrollArea>
- </TabsContent>
-
- <TabsContent value="rooms" className="space-y-4">
- <div className="flex items-center justify-between">
- <h3 className="text-sm font-medium text-gray-400">Protected Rooms</h3>
- <AddProtectedRoom filled />
- </div>
-
- <ScrollArea className="h-[300px] rounded-md border border-gray-800">
- <ProtectedRomsList team={selectedTeam} />
- </ScrollArea>
- </TabsContent>
-
- <TabsContent value="settings" className="space-y-4">
- <div className="space-y-4">
- <div className="space-y-2">
- <Label htmlFor="bot-name">Bot Name</Label>
- <Input id="bot-name" defaultValue={selectedTeam.name} className="bg-gray-900 border-gray-800" />
- </div>
- <div className="space-y-2">
- <Label htmlFor="bot-description">Description</Label>
- <Input
- id="bot-description"
- defaultValue={selectedTeam.description}
- className="bg-gray-900 border-gray-800"
- />
- </div>
- <div className="pt-4 flex justify-between">
- <Button variant="outline" className="border-red-500 text-red-400 hover:bg-red-950 hover:text-red-300">
- <Trash2 className="mr-2 h-4 w-4" />
- Delete Bot
- </Button>
- <Button className="bg-purple-600 text-white hover:bg-purple-700">Save Changes</Button>
- </div>
- </div>
- </TabsContent>
- </Tabs>
- </CardContent>
- </Card>
- )
-}
diff --git a/src/components/analytics/bar-chart.tsx b/src/components/analytics/bar-chart.tsx
@@ -1,3 +1,4 @@
+"use client";
import { useRef, useState } from "react"
export interface BarChartItem {