commit af2d92012b646c6f1bb1ab1d3f4fd04b7451af5d
parent 4ccff93c728265300094ce27cdf62df9c1bbc6df
Author: MTRNord <mtrnord1@gmail.com>
Date: Tue, 22 Apr 2025 15:32:02 +0200
Add statistics page and set up some more auth related things like session management
Signed-off-by: MTRNord <mtrnord1@gmail.com>
Diffstat:
34 files changed, 2418 insertions(+), 1913 deletions(-)
diff --git a/package.json b/package.json
@@ -22,6 +22,7 @@
"@radix-ui/react-tabs": "^1.1.8",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "jose": "^6.0.10",
"lucide-react": "^0.503.0",
"next": "15.3.1-canary.14",
"next-themes": "^0.4.6",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
@@ -47,6 +47,9 @@ importers:
clsx:
specifier: ^2.1.1
version: 2.1.1
+ jose:
+ specifier: ^6.0.10
+ version: 6.0.10
lucide-react:
specifier: ^0.503.0
version: 0.503.0(react@19.1.0)
@@ -84,9 +87,6 @@ importers:
'@types/react-dom':
specifier: ^19
version: 19.1.2(@types/react@19.1.2)
- autoprefixer:
- specifier: ^10.4.21
- version: 10.4.21(postcss@8.5.3)
eslint:
specifier: ^9
version: 9.25.0(jiti@2.4.2)
@@ -1082,13 +1082,6 @@ packages:
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
engines: {node: '>= 0.4'}
- autoprefixer@10.4.21:
- resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==}
- engines: {node: ^10 || ^12 || >=14}
- hasBin: true
- peerDependencies:
- postcss: ^8.1.0
-
available-typed-arrays@1.0.7:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'}
@@ -1118,11 +1111,6 @@ packages:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
- browserslist@4.24.4:
- resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==}
- engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
- hasBin: true
-
busboy@1.6.0:
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
engines: {node: '>=10.16.0'}
@@ -1278,9 +1266,6 @@ packages:
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
- electron-to-chromium@1.5.139:
- resolution: {integrity: sha512-GGnRYOTdN5LYpwbIr0rwP/ZHOQSvAF6TG0LSzp28uCBb9JiXHJGmaaKw29qjNJc5bGnnp6kXJqRnGMQoELwi5w==}
-
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@@ -1319,10 +1304,6 @@ packages:
resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==}
engines: {node: '>= 0.4'}
- escalade@3.2.0:
- resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
- engines: {node: '>=6'}
-
escape-string-regexp@4.0.0:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'}
@@ -1498,9 +1479,6 @@ packages:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'}
- fraction.js@4.3.7:
- resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
-
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -1747,6 +1725,9 @@ packages:
resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
hasBin: true
+ jose@6.0.10:
+ resolution: {integrity: sha512-skIAxZqcMkOrSwjJvplIPYrlXGpxTPnro2/QWTDCxAdWQrSTV5/KqspMWmi5WAx5+ULswASJiZ0a+1B/Lxt9cw==}
+
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -1887,17 +1868,10 @@ packages:
sass:
optional: true
- node-releases@2.0.19:
- resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
-
normalize-path@3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
- normalize-range@0.1.2:
- resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
- engines: {node: '>=0.10.0'}
-
object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
@@ -2379,12 +2353,6 @@ packages:
unrs-resolver@1.6.1:
resolution: {integrity: sha512-PLDI7BRVaI1C0x8mXr8leLPIOPPF1wCRFyKIswJAPJG3LdMxWNiAVvlTvmff5DSezapWFLagk18NF2cCNhe8Fg==}
- update-browserslist-db@1.1.3:
- resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==}
- hasBin: true
- peerDependencies:
- browserslist: '>= 4.21.0'
-
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
@@ -3429,16 +3397,6 @@ snapshots:
async-function@1.0.0: {}
- autoprefixer@10.4.21(postcss@8.5.3):
- dependencies:
- browserslist: 4.24.4
- caniuse-lite: 1.0.30001715
- fraction.js: 4.3.7
- normalize-range: 0.1.2
- picocolors: 1.1.1
- postcss: 8.5.3
- postcss-value-parser: 4.2.0
-
available-typed-arrays@1.0.7:
dependencies:
possible-typed-array-names: 1.1.0
@@ -3464,13 +3422,6 @@ snapshots:
dependencies:
fill-range: 7.1.1
- browserslist@4.24.4:
- dependencies:
- caniuse-lite: 1.0.30001715
- electron-to-chromium: 1.5.139
- node-releases: 2.0.19
- update-browserslist-db: 1.1.3(browserslist@4.24.4)
-
busboy@1.6.0:
dependencies:
streamsearch: 1.1.0
@@ -3624,8 +3575,6 @@ snapshots:
eastasianwidth@0.2.0: {}
- electron-to-chromium@1.5.139: {}
-
emoji-regex@8.0.0: {}
emoji-regex@9.2.2: {}
@@ -3728,8 +3677,6 @@ snapshots:
is-date-object: 1.1.0
is-symbol: 1.1.1
- escalade@3.2.0: {}
-
escape-string-regexp@4.0.0: {}
eslint-config-next@15.3.1-canary.14(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3):
@@ -3988,8 +3935,6 @@ snapshots:
cross-spawn: 7.0.6
signal-exit: 4.1.0
- fraction.js@4.3.7: {}
-
fsevents@2.3.3:
optional: true
@@ -4251,6 +4196,8 @@ snapshots:
jiti@2.4.2:
optional: true
+ jose@6.0.10: {}
+
js-tokens@4.0.0: {}
js-yaml@4.1.0:
@@ -4376,12 +4323,8 @@ snapshots:
- '@babel/core'
- babel-plugin-macros
- node-releases@2.0.19: {}
-
normalize-path@3.0.0: {}
- normalize-range@0.1.2: {}
-
object-assign@4.1.1: {}
object-hash@3.0.0: {}
@@ -4980,12 +4923,6 @@ snapshots:
'@unrs/resolver-binding-win32-ia32-msvc': 1.6.1
'@unrs/resolver-binding-win32-x64-msvc': 1.6.1
- update-browserslist-db@1.1.3(browserslist@4.24.4):
- dependencies:
- browserslist: 4.24.4
- escalade: 3.2.0
- picocolors: 1.1.1
-
uri-js@4.4.1:
dependencies:
punycode: 2.3.1
diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts
@@ -0,0 +1,35 @@
+import { type NextRequest, NextResponse } from "next/server"
+import { createSession, setSessionCookie } from "@/lib/auth"
+
+export async function POST(request: NextRequest) {
+ try {
+ const { matrixId } = await request.json()
+
+ if (!matrixId) {
+ return NextResponse.json({ error: "Missing required fields" }, { status: 400 })
+ }
+
+ // In a real app, you would verify the user's credentials with Matrix
+ // For demo purposes, we'll create a mock user
+ const user = {
+ id: "user_" + Math.random().toString(36).substring(2, 9),
+ matrixId,
+ displayName: matrixId.split(":")[0].substring(1),
+ isAdmin: matrixId.includes("admin"),
+ }
+
+ // Create a session token
+ const token = await createSession(user)
+
+ // Create a response
+ const response = NextResponse.json({ success: true, user })
+
+ // Set the session cookie
+ await setSessionCookie(token, response)
+
+ return response
+ } catch (error) {
+ console.error("Login failed:", error)
+ return NextResponse.json({ error: "Authentication failed" }, { status: 500 })
+ }
+}
diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts
@@ -0,0 +1,17 @@
+import { NextResponse } from "next/server"
+import { clearSession } from "@/lib/auth"
+
+export async function POST() {
+ try {
+ // Create a response
+ const response = NextResponse.json({ success: true })
+
+ // Clear the session cookie
+ await clearSession(response)
+
+ return response
+ } catch (error) {
+ console.error("Logout failed:", error)
+ return NextResponse.json({ error: "Logout failed" }, { status: 500 })
+ }
+}
diff --git a/src/app/api/auth/register/route.ts b/src/app/api/auth/register/route.ts
@@ -0,0 +1,35 @@
+import { type NextRequest, NextResponse } from "next/server"
+import { createSession, setSessionCookie } from "@/lib/auth"
+
+export async function POST(request: NextRequest) {
+ try {
+ const { matrixId } = await request.json()
+
+ if (!matrixId) {
+ return NextResponse.json({ error: "Missing required fields" }, { status: 400 })
+ }
+
+ // In a real app, you would register the user with Matrix
+ // For demo purposes, we'll create a mock user
+ const user = {
+ id: "user_" + Math.random().toString(36).substring(2, 9),
+ matrixId,
+ displayName: matrixId.split(":")[0].substring(1),
+ isAdmin: false,
+ }
+
+ // Create a session token
+ const token = await createSession(user)
+
+ // Create a response
+ const response = NextResponse.json({ success: true, user })
+
+ // Set the session cookie
+ await setSessionCookie(token, response)
+
+ return response
+ } catch (error) {
+ console.error("Registration failed:", error)
+ return NextResponse.json({ error: "Registration failed" }, { status: 500 })
+ }
+}
diff --git a/src/app/api/auth/session/route.ts b/src/app/api/auth/session/route.ts
@@ -0,0 +1,17 @@
+import { type NextRequest, NextResponse } from "next/server"
+import { getSessionUser } from "@/lib/auth"
+
+export async function GET(request: NextRequest) {
+ try {
+ const user = await getSessionUser(request)
+
+ if (!user) {
+ return NextResponse.json({ authenticated: false }, { status: 401 })
+ }
+
+ return NextResponse.json({ authenticated: true, user })
+ } catch (error) {
+ console.error("Session check failed:", error)
+ return NextResponse.json({ authenticated: false }, { status: 500 })
+ }
+}
diff --git a/src/app/dashboard/analytics-dashboard.tsx b/src/app/dashboard/analytics-dashboard.tsx
@@ -0,0 +1,394 @@
+"use client"
+
+import { useState } from "react"
+import { Filter, TrendingUp, Users } from "lucide-react"
+import { useRouter } from "next/navigation"
+
+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 { Button } from "@/components/ui/button"
+
+import { PieChart } from "@/components/analytics/pie-chart"
+import { Heatmap } from "@/components/analytics/heatmap"
+import { BarChart } from "@/components/analytics/bar-chart"
+import { HorizontalBarChart } from "@/components/analytics/horizontal-bar-chart"
+
+// 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 = monthlyActivityData.map((item) => ({
+ label: `${item.month} ${item.year}`,
+ 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 router = useRouter()
+
+ const handleViewAllBannedServers = () => {
+ // Navigate to the bans tab
+ router.push("/dashboard?tab=bans")
+ }
+
+ 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>
+ <Button variant="outline" size="sm" className="h-8 border-gray-800 text-gray-400 hover:text-white">
+ <Filter className="h-3.5 w-3.5 mr-1" />
+ Filter
+ </Button>
+ </div>
+ </div>
+
+ <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">Total Reports</CardTitle>
+ </CardHeader>
+ <CardContent>
+ <div className="text-2xl font-bold">247</div>
+ <div className="flex items-center text-xs text-green-400">
+ <TrendingUp className="h-3.5 w-3.5 mr-1" />
+ <span>+12% from previous period</span>
+ </div>
+ </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">98</div>
+ <div className="flex items-center text-xs text-red-400">
+ <TrendingUp className="h-3.5 w-3.5 mr-1" />
+ <span>+5% from previous period</span>
+ </div>
+ </CardContent>
+ </Card>
+ <Card className="border-gray-800 bg-gray-950">
+ <CardHeader className="pb-2">
+ <CardTitle className="text-sm font-medium">Unique Reporters</CardTitle>
+ </CardHeader>
+ <CardContent>
+ <div className="text-2xl font-bold">42</div>
+ <div className="flex items-center text-xs text-green-400">
+ <TrendingUp className="h-3.5 w-3.5 mr-1" />
+ <span>+8% from previous period</span>
+ </div>
+ </CardContent>
+ </Card>
+ <Card className="border-gray-800 bg-gray-950">
+ <CardHeader className="pb-2">
+ <CardTitle className="text-sm font-medium">Avg. Response Time</CardTitle>
+ </CardHeader>
+ <CardContent>
+ <div className="text-2xl font-bold">1.4h</div>
+ <div className="flex items-center text-xs text-green-400">
+ <TrendingUp className="h-3.5 w-3.5 mr-1 rotate-180" />
+ <span>-15% from previous period</span>
+ </div>
+ </CardContent>
+ </Card>
+ </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} className="w-[240px]">
+ <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>
+ <div className="h-[250px] w-full">
+ <BarChart data={monthlyBarChartData} height={200} />
+ </div>
+ </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" />
+ <div className="mt-4 flex items-center justify-center">
+ <Button
+ variant="outline"
+ size="sm"
+ className="border-gray-800 text-gray-400 hover:text-white"
+ onClick={handleViewAllBannedServers}
+ >
+ <Users className="mr-2 h-4 w-4" />
+ View All Banned Servers
+ </Button>
+ </div>
+ </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-2">
+ <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-2">
+ <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-2">
+ <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
@@ -0,0 +1,212 @@
+"use client";
+import { Checkbox } from "@/components/ui/checkbox"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+import { ScrollArea } from "@/components/ui/scroll-area"
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
+import { Ban, Badge } 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 { Textarea } from "@/components/ui/textarea";
+import { Label } from "@/components/ui/label"
+import { useState } from "react";
+import { PolicyList } from "../page";
+
+interface BansProps {
+ policyLists: PolicyList[];
+}
+
+export default function Bans({ policyLists }: BansProps) {
+ const [selectedPolicyList, setSelectedPolicyList] = useState("global")
+ const selectedList = policyLists.find((list) => list.id === selectedPolicyList) || policyLists[0] || { entries: [] }
+
+ return (
+ <Card className="border-gray-800 bg-gray-950">
+ <CardHeader className="pb-2">
+ <div className="flex items-center justify-between">
+ <CardTitle>Policy Lists</CardTitle>
+ <Dialog>
+ <DialogTrigger asChild>
+ <Button size="sm" className="bg-purple-600 text-white hover:bg-purple-700">
+ <Ban className="mr-2 h-4 w-4" />
+ Add Ban
+ </Button>
+ </DialogTrigger>
+ <DialogContent className="bg-gray-950 border-gray-800">
+ <DialogHeader>
+ <DialogTitle>Add New Ban</DialogTitle>
+ <DialogDescription className="text-gray-400">
+ Ban a user or server from your Matrix rooms
+ </DialogDescription>
+ </DialogHeader>
+ <div className="space-y-4 py-4">
+ <div className="space-y-2">
+ <Label htmlFor="ban-type">Ban Type</Label>
+ <Select defaultValue="user">
+ <SelectTrigger className="bg-gray-900 border-gray-800">
+ <SelectValue placeholder="Select ban type" />
+ </SelectTrigger>
+ <SelectContent className="bg-gray-900 border-gray-800">
+ <SelectItem value="user">User</SelectItem>
+ <SelectItem value="server">Server</SelectItem>
+ <SelectItem value="room">Room</SelectItem>
+ </SelectContent>
+ </Select>
+ </div>
+ <div className="space-y-2">
+ <Label htmlFor="ban-target">Target</Label>
+ <Input
+ id="ban-target"
+ placeholder="@user:matrix.org"
+ className="bg-gray-900 border-gray-800"
+ />
+ </div>
+ <div className="space-y-2">
+ <Label htmlFor="ban-reason">Reason</Label>
+ <Textarea
+ id="ban-reason"
+ placeholder="Reason for the ban"
+ className="bg-gray-900 border-gray-800"
+ />
+ </div>
+ <div className="space-y-2">
+ <Label>Policy Lists</Label>
+ <div className="space-y-2 mt-1">
+ {policyLists
+ .filter((list) => !list.readOnly)
+ .map((list) => (
+ <div key={list.id} className="flex items-center space-x-2">
+ <Checkbox id={`list-${list.id}`} />
+ <Label htmlFor={`list-${list.id}`} className="font-normal">
+ {list.name}
+ </Label>
+ </div>
+ ))}
+ </div>
+ </div>
+ <div className="space-y-2">
+ <p className="text-xs text-gray-400">
+ Bans are applied to all protected rooms monitored by this bot.
+ </p>
+ </div>
+ </div>
+ <DialogFooter>
+ <Button
+ variant="outline"
+ className="border-gray-700 text-gray-400 hover:bg-gray-900 hover:text-gray-300"
+ >
+ Cancel
+ </Button>
+ <Button className="bg-red-600 text-white hover:bg-red-700">Add Ban</Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ </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 ? (
+ <Badge className="bg-gray-800 text-gray-400">Read Only</Badge>
+ ) : (
+ <Badge className="bg-green-900/50 text-green-300">Editable</Badge>
+ )}
+ </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" />
+ {selectedList && !selectedList.readOnly && (
+ <Dialog>
+ <DialogTrigger asChild>
+ <Button size="sm" className="bg-purple-600 text-white hover:bg-purple-700">
+ Add Entry
+ </Button>
+ </DialogTrigger>
+ <DialogContent className="bg-gray-950 border-gray-800">
+ {/* Add entry dialog content */}
+ </DialogContent>
+ </Dialog>
+ )}
+ </div>
+ </div>
+
+ <ScrollArea className="h-[400px] rounded-md border border-gray-800">
+ <div className="p-4 space-y-3">
+ {selectedList && selectedList.entries && selectedList.entries.length > 0 ? (
+ selectedList.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
@@ -0,0 +1,479 @@
+"use client"
+
+import { useState } from "react"
+import Link from "next/link"
+import { Shield, Bell, Menu, Ban, UserX, 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,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu"
+import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
+import { ScrollArea } from "@/components/ui/scroll-area"
+import { useSession } from "@/contexts/session-context"
+
+interface Notification {
+ id: string
+ type: string
+ title: string
+ message: string
+ timestamp: string
+ read: boolean
+ teamId?: string
+}
+
+interface Team {
+ id: string
+ name: string
+ description: string
+}
+
+interface DashboardHeaderProps {
+ selectedTeam: Team
+ onTeamChange: (teamId: string) => void
+ activeTab: string
+ setActiveTab: (tab: string) => void
+ teams: Team[]
+}
+
+export function DashboardHeader({ selectedTeam, onTeamChange, activeTab, setActiveTab, teams }: DashboardHeaderProps) {
+ const { logout } = useSession()
+ const [notifications, setNotifications] = useState<Notification[]>([
+ {
+ id: "notif1",
+ type: "report",
+ title: "New report",
+ message: "A new message has been reported in #general",
+ timestamp: "2025-04-21T15:30:00Z",
+ read: false,
+ teamId: "team1",
+ },
+ {
+ id: "notif2",
+ type: "ban",
+ title: "User banned",
+ message: "@spammer:matrix.org has been banned by @mod1:matrix.org",
+ timestamp: "2025-04-21T14:45:00Z",
+ read: false,
+ teamId: "team1",
+ },
+ {
+ id: "notif3",
+ type: "invite",
+ title: "Team invitation",
+ message: "You've been invited to join the Support Team Bot",
+ timestamp: "2025-04-21T12:15:00Z",
+ read: true,
+ teamId: "team2",
+ },
+ ])
+
+ const teamNotifications = notifications.filter((n) => n.teamId === selectedTeam.id || !n.teamId)
+ const unreadCount = teamNotifications.filter((n) => !n.read).length
+
+ const markNotificationAsRead = (id: string) => {
+ setNotifications(notifications.map((n) => (n.id === id ? { ...n, read: true } : n)))
+ }
+
+ const clearAllNotifications = () => {
+ setNotifications(notifications.map((n) => ({ ...n, read: true })))
+ }
+
+ const deleteNotification = (id: string) => {
+ setNotifications(notifications.filter((n) => n.id !== id))
+ }
+
+ const handleLogout = async () => {
+ await logout()
+ }
+
+ return (
+ <header className="sticky top-0 z-10 border-b border-gray-800 bg-black/80 backdrop-blur-sm">
+ <div className="container flex h-16 items-center justify-between px-4 md:px-6">
+ <div className="flex items-center gap-4">
+ <Link href="/" className="flex items-center gap-2">
+ <Shield className="h-6 w-6 text-purple-400" />
+ <span className="text-xl font-bold tracking-tighter">Draupnir4All</span>
+ </Link>
+
+ {/* Global Team Selector */}
+ <DropdownMenu>
+ <DropdownMenuTrigger asChild>
+ <Button variant="outline" className="border-purple-500 text-purple-400 hover:bg-purple-950">
+ <Crown className="mr-2 h-4 w-4" />
+ {selectedTeam.name}
+ <ChevronDown className="ml-2 h-4 w-4" />
+ </Button>
+ </DropdownMenuTrigger>
+ <DropdownMenuContent className="w-56 bg-gray-900 border-gray-800">
+ <DropdownMenuLabel>Your Bots</DropdownMenuLabel>
+ <DropdownMenuSeparator className="bg-gray-800" />
+ {teams.map((team) => (
+ <DropdownMenuItem
+ key={team.id}
+ className={selectedTeam.id === team.id ? "bg-gray-800" : ""}
+ onClick={() => onTeamChange(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>
+ </DropdownMenuContent>
+ </DropdownMenu>
+ </div>
+
+ <div className="flex items-center gap-4">
+ {/* Notifications */}
+ <Popover>
+ <PopoverTrigger asChild>
+ <Button variant="ghost" size="icon" className="relative text-gray-400 hover:text-white">
+ <Bell className="h-5 w-5" />
+ {unreadCount > 0 && (
+ <span className="absolute -top-1 -right-1 flex h-4 w-4 items-center justify-center rounded-full bg-red-500 text-[10px] font-medium text-white">
+ {unreadCount}
+ </span>
+ )}
+ </Button>
+ </PopoverTrigger>
+ <PopoverContent className="w-80 p-0 bg-gray-900 border-gray-800">
+ <div className="flex items-center justify-between border-b border-gray-800 p-3">
+ <h3 className="font-medium">Notifications</h3>
+ <div className="flex items-center gap-1">
+ <Button
+ variant="ghost"
+ size="sm"
+ className="h-7 px-2 text-xs text-gray-400 hover:text-white"
+ onClick={clearAllNotifications}
+ >
+ Clear all
+ </Button>
+ </div>
+ </div>
+ <ScrollArea className="h-[300px]">
+ {teamNotifications.length > 0 ? (
+ <div className="space-y-1 p-1">
+ {teamNotifications.map((notification) => (
+ <div
+ key={notification.id}
+ className={`flex items-start p-3 rounded-md ${notification.read ? "bg-gray-900" : "bg-gray-800"}`}
+ onClick={() => markNotificationAsRead(notification.id)}
+ >
+ <div
+ className={`rounded-full p-1.5 mr-3 ${notification.type === "report"
+ ? "bg-yellow-500/20"
+ : notification.type === "ban"
+ ? "bg-red-500/20"
+ : "bg-blue-500/20"
+ }`}
+ >
+ {notification.type === "report" ? (
+ <Bell className="h-3.5 w-3.5 text-yellow-500" />
+ ) : notification.type === "ban" ? (
+ <Ban className="h-3.5 w-3.5 text-red-500" />
+ ) : (
+ <Plus className="h-3.5 w-3.5 text-blue-500" />
+ )}
+ </div>
+ <div className="flex-1 min-w-0">
+ <div className="flex items-center justify-between">
+ <p className="text-sm font-medium">{notification.title}</p>
+ <Button
+ variant="ghost"
+ size="sm"
+ className="h-6 w-6 p-0 text-gray-500 hover:text-gray-300"
+ onClick={(e) => {
+ e.stopPropagation()
+ deleteNotification(notification.id)
+ }}
+ >
+ <X className="h-3 w-3" />
+ <span className="sr-only">Dismiss</span>
+ </Button>
+ </div>
+ <p className="text-xs text-gray-400 line-clamp-2">{notification.message}</p>
+ <p className="text-xs text-gray-500 mt-1">
+ {new Date(notification.timestamp).toLocaleString()}
+ </p>
+ </div>
+ </div>
+ ))}
+ </div>
+ ) : (
+ <div className="flex flex-col items-center justify-center h-full p-6 text-center">
+ <Bell className="h-8 w-8 text-gray-600 mb-2" />
+ <p className="text-gray-400">No notifications</p>
+ </div>
+ )}
+ </ScrollArea>
+ </PopoverContent>
+ </Popover>
+
+ {/* Quick Action Buttons */}
+ <Dialog>
+ <DialogTrigger asChild>
+ <Button
+ variant="outline"
+ size="sm"
+ className="hidden md:flex border-red-500 text-red-400 hover:bg-red-950 hover:text-red-300"
+ >
+ <Ban className="mr-2 h-4 w-4" />
+ Ban User
+ </Button>
+ </DialogTrigger>
+ <DialogContent className="bg-gray-950 border-gray-800">
+ <DialogHeader>
+ <DialogTitle>Ban User</DialogTitle>
+ <DialogDescription className="text-gray-400">Ban a user from your Matrix rooms</DialogDescription>
+ </DialogHeader>
+ <div className="space-y-4 py-4">
+ <div className="space-y-2">
+ <Label htmlFor="user-id">Matrix User ID</Label>
+ <Input id="user-id" placeholder="@user:matrix.org" className="bg-gray-900 border-gray-800" />
+ </div>
+ <div className="space-y-2">
+ <Label htmlFor="ban-reason">Reason</Label>
+ <Input id="ban-reason" placeholder="Reason for the ban" 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-red-600 text-white hover:bg-red-700">Ban User</Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+
+ <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>
+
+ <Button
+ variant="outline"
+ className="hidden md:flex border-purple-500 text-purple-400 hover:bg-purple-950 hover:text-purple-300"
+ onClick={handleLogout}
+ >
+ <LogOut className="mr-2 h-4 w-4" />
+ Log Out
+ </Button>
+ <Sheet>
+ <SheetTrigger asChild>
+ <Button variant="ghost" size="icon" className="md:hidden">
+ <Menu className="h-5 w-5" />
+ </Button>
+ </SheetTrigger>
+ <SheetContent side="right" className="bg-gray-950 border-gray-800 p-0">
+ <div className="flex flex-col h-full">
+ <div className="border-b border-gray-800 p-4">
+ <div className="flex items-center gap-2">
+ <Shield className="h-5 w-5 text-purple-400" />
+ <span className="font-medium">Draupnir4All</span>
+ </div>
+ </div>
+ <div className="p-4">
+ <DropdownMenu>
+ <DropdownMenuTrigger asChild>
+ <Button
+ variant="outline"
+ className="w-full border-purple-500 text-purple-400 hover:bg-purple-950"
+ >
+ <Crown className="mr-2 h-4 w-4" />
+ {selectedTeam.name}
+ <ChevronDown className="ml-2 h-4 w-4" />
+ </Button>
+ </DropdownMenuTrigger>
+ <DropdownMenuContent className="w-56 bg-gray-900 border-gray-800">
+ <DropdownMenuLabel>Your Bots</DropdownMenuLabel>
+ <DropdownMenuSeparator className="bg-gray-800" />
+ {teams.map((team) => (
+ <DropdownMenuItem
+ key={team.id}
+ className={selectedTeam.id === team.id ? "bg-gray-800" : ""}
+ onClick={() => onTeamChange(team.id)}
+ >
+ {team.name}
+ </DropdownMenuItem>
+ ))}
+ </DropdownMenuContent>
+ </DropdownMenu>
+ </div>
+ <nav className="flex flex-col p-4 gap-2">
+ <Button
+ variant={activeTab === "overview" ? "secondary" : "ghost"}
+ className="justify-start"
+ onClick={() => setActiveTab("overview")}
+ >
+ Overview
+ </Button>
+ <Button
+ variant={activeTab === "reports" ? "secondary" : "ghost"}
+ className="justify-start"
+ onClick={() => setActiveTab("reports")}
+ >
+ Reports
+ </Button>
+ <Button
+ variant={activeTab === "bans" ? "secondary" : "ghost"}
+ className="justify-start"
+ onClick={() => setActiveTab("bans")}
+ >
+ Bans
+ </Button>
+ <Button
+ variant={activeTab === "analytics" ? "secondary" : "ghost"}
+ className="justify-start"
+ onClick={() => setActiveTab("analytics")}
+ >
+ Analytics
+ </Button>
+ <Button
+ variant={activeTab === "settings" ? "secondary" : "ghost"}
+ className="justify-start"
+ onClick={() => setActiveTab("settings")}
+ >
+ Settings
+ </Button>
+
+ <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>
+
+ <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>
+ </nav>
+ <div className="mt-auto border-t border-gray-800 p-4">
+ <Button
+ variant="outline"
+ className="w-full border-purple-500 text-purple-400 hover:bg-purple-950 hover:text-purple-300"
+ onClick={handleLogout}
+ >
+ <LogOut className="mr-2 h-4 w-4" />
+ Log Out
+ </Button>
+ </div>
+ </div>
+ </SheetContent>
+ </Sheet>
+ </div>
+ </div>
+ </header>
+ )
+}
diff --git a/src/components/dashboard/modals/add-protected-room.tsx b/src/app/dashboard/components/modals/add-protected-room.tsx
diff --git a/src/app/dashboard/components/overview-tab.tsx b/src/app/dashboard/components/overview-tab.tsx
@@ -0,0 +1,210 @@
+"use client"
+import { Plus, X, AlertTriangle, Ban, CheckCircle } from "lucide-react"
+
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
+import { Button } from "@/components/ui/button"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog"
+import { Label } from "@/components/ui/label"
+import { Input } from "@/components/ui/input"
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
+import { PolicyList, Report } from "../page"
+
+interface Team {
+ id: string
+ name: string
+ rooms: string[]
+}
+
+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>
+ <Dialog>
+ <DialogTrigger asChild>
+ <Button variant="ghost" size="sm" className="h-7 px-2 text-xs text-purple-400">
+ <Plus className="h-3 w-3 mr-1" />
+ Add Room
+ </Button>
+ </DialogTrigger>
+ <DialogContent className="bg-gray-950 border-gray-800">
+ <DialogHeader>
+ <DialogTitle>Add Protected Room</DialogTitle>
+ <DialogDescription className="text-gray-400">
+ Add a Matrix room to be monitored by this bot
+ </DialogDescription>
+ </DialogHeader>
+ <div className="space-y-4 py-4">
+ <div className="space-y-2">
+ <Label htmlFor="room-id">Room ID or Alias</Label>
+ <Input id="room-id" placeholder="#roomname:matrix.org" className="bg-gray-900 border-gray-800" />
+ <p className="text-xs text-gray-400">
+ Enter a room ID or alias. The bot must be invited to this room.
+ </p>
+ </div>
+ <div className="space-y-2">
+ <Label>Protection Level</Label>
+ <Select defaultValue="standard">
+ <SelectTrigger className="bg-gray-900 border-gray-800">
+ <SelectValue placeholder="Select protection level" />
+ </SelectTrigger>
+ <SelectContent className="bg-gray-900 border-gray-800">
+ <SelectItem value="minimal">Minimal - Report only</SelectItem>
+ <SelectItem value="standard">Standard - Report and auto-moderate</SelectItem>
+ <SelectItem value="strict">Strict - Aggressive moderation</SelectItem>
+ </SelectContent>
+ </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">Add Room</Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ </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>
+ <Dialog>
+ <DialogTrigger asChild>
+ <Button size="sm" className="bg-purple-600 text-white hover:bg-purple-700">
+ <Plus className="mr-2 h-4 w-4" />
+ Add Room
+ </Button>
+ </DialogTrigger>
+ <DialogContent className="bg-gray-950 border-gray-800">
+ {/* Same content as the other add room dialog */}
+ </DialogContent>
+ </Dialog>
+ </div>
+ </CardHeader>
+ <CardContent>
+ <div className="space-y-4">
+ {selectedTeam.rooms.map((room, index) => (
+ <div key={index} className="flex items-center justify-between">
+ <div className="flex items-center gap-2">
+ <div className="h-2 w-2 rounded-full bg-green-500"></div>
+ <p className="text-sm">{room}</p>
+ </div>
+ <Button variant="ghost" size="sm" className="h-7 w-7 p-0 text-gray-400 hover:text-red-400">
+ <X className="h-4 w-4" />
+ <span className="sr-only">Remove</span>
+ </Button>
+ </div>
+ ))}
+ </div>
+ </CardContent>
+ </Card>
+ </div>
+ </div>
+ )
+}
diff --git a/src/app/dashboard/components/reports-tab.tsx b/src/app/dashboard/components/reports-tab.tsx
@@ -0,0 +1,262 @@
+"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/layout.tsx b/src/app/dashboard/layout.tsx
@@ -0,0 +1,24 @@
+import type React from "react"
+import { Suspense } from "react"
+import { redirect } from "next/navigation"
+import { getSessionUser } from "@/lib/auth"
+
+export default async function DashboardLayout({
+ children,
+}: {
+ children: React.ReactNode
+}) {
+ // Check if the user is authenticated on the server
+ const user = await getSessionUser()
+
+ // If not authenticated, redirect to login
+ if (!user) {
+ redirect("/login")
+ }
+
+ return (
+ <Suspense fallback={<div className="flex h-screen w-full items-center justify-center">Loading...</div>}>
+ {children}
+ </Suspense>
+ )
+}
diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx
@@ -1,103 +1,65 @@
-'use client';
+"use client"
import { useState } from "react"
-import Link from "next/link"
-import {
- Shield,
- AlertTriangle,
- CheckCircle,
- Ban,
- LogOut,
- Bell,
- Menu,
- UserX,
- MessageSquare,
- UserPlus,
- Filter,
- Crown,
- X,
- Plus,
- Home,
- ChevronDown,
-} from "lucide-react"
-
-import { Button } from "@/components/ui/button"
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
-import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog"
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
-import { Input } from "@/components/ui/input"
-import { Label } from "@/components/ui/label"
-import { Textarea } from "@/components/ui/textarea"
-import { BlurredText } from "@/components/blurred-text"
-import { BlurredImage } from "@/components/blurred-image"
-import { Badge } from "@/components/ui/badge"
-import { Separator } from "@/components/ui/separator"
-import { ScrollArea } from "@/components/ui/scroll-area"
-import { Checkbox } from "@/components/ui/checkbox"
-import { mockTeams, TeamManagement } from "./team-management"
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuLabel,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu"
-import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
-import Overview from "@/components/dashboard/overview-pane";
-import Reports from "@/components/dashboard/reports-pane";
-import Bans from "@/components/dashboard/bans-pane";
-// Mock notifications
-const mockNotifications = [
- {
- id: "notif1",
- type: "report",
- title: "New report",
- message: "A new message has been reported in #general",
- timestamp: "2025-04-21T15:30:00Z",
- read: false,
- teamId: "team1",
- },
- {
- id: "notif2",
- type: "ban",
- title: "User banned",
- message: "@spammer:matrix.org has been banned by @mod1:matrix.org",
- timestamp: "2025-04-21T14:45:00Z",
- read: false,
- teamId: "team1",
- },
- {
- id: "notif3",
- type: "invite",
- title: "Team invitation",
- message: "You've been invited to join the Support Team Bot",
- timestamp: "2025-04-21T12:15:00Z",
- read: true,
- teamId: "team2",
- },
-]
+import { mockTeams, Team, TeamManagement } from "./team-management"
+import Bans from "./components/bans-pane"
+import { AnalyticsDashboard } from "./analytics-dashboard"
+import { ReportsTab } from "./components/reports-tab"
+import { DashboardHeader } from "./components/dashboard-header"
+import { OverviewTab } from "./components/overview-tab"
+
+export interface PolicyList {
+ id: string;
+ name: string;
+ description: string;
+ readOnly: boolean;
+ teamId: string;
+ entries: {
+ id: string,
+ target: string,
+ reason: string,
+ timestamp: string,
+ }[];
+};
+
+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 [selectedReport, setSelectedReport] = useState<string | null>(null)
- const [selectedPolicyList, setSelectedPolicyList] = useState("global")
- const [selectedTeam, setSelectedTeam] = useState(mockTeams[0])
- const [notifications, setNotifications] = useState(mockNotifications)
+ const [selectedTeam, setSelectedTeam] = useState<Team>(mockTeams[0])
- // Mock data for reports - now filtered by team
- const reports = [
+ // Mock data for reports - filtered by team
+ const reports: Report[] = [
{
id: "rep_1",
type: "message",
@@ -187,8 +149,8 @@ export default function DashboardPage() {
},
].filter((report) => report.teamId === selectedTeam.id)
- // Mock data for policy lists - now filtered by team
- const policyLists = [
+ // Mock data for policy lists - filtered by team
+ const policyLists: PolicyList[] = [
{
id: "global",
name: "Global Ban List",
@@ -259,433 +221,23 @@ export default function DashboardPage() {
},
].filter((list) => list.teamId === selectedTeam.id)
- const selectedList = policyLists.find((list) => list.id === selectedPolicyList) || policyLists[0] || { entries: [] }
-
- const teamNotifications = notifications.filter((n) => n.teamId === selectedTeam.id || !n.teamId)
- const unreadCount = teamNotifications.filter((n) => !n.read).length
const handleTeamChange = (teamId: string) => {
const team = mockTeams.find((t) => t.id === teamId)
if (team) {
setSelectedTeam(team)
- // Reset selected report and policy list when changing teams
- setSelectedReport(null)
- if (policyLists.length > 0) {
- setSelectedPolicyList(policyLists[0].id)
- }
}
}
- const markNotificationAsRead = (id: string) => {
- setNotifications(notifications.map((n) => (n.id === id ? { ...n, read: true } : n)))
- }
-
- const clearAllNotifications = () => {
- setNotifications(notifications.map((n) => ({ ...n, read: true })))
- }
-
- const deleteNotification = (id: string) => {
- setNotifications(notifications.filter((n) => n.id !== id))
- }
-
return (
<div className="flex min-h-screen flex-col bg-black text-white">
- <header className="sticky top-0 z-10 border-b border-gray-800 bg-black/80 backdrop-blur-sm">
- <div className="container flex h-16 items-center justify-between px-4 md:px-6">
- <div className="flex items-center gap-4">
- <Link href="/" className="flex items-center gap-2">
- <Shield className="h-6 w-6 text-purple-400" />
- <span className="text-xl font-bold tracking-tighter">Draupnir4All</span>
- </Link>
-
- {/* Global Team Selector */}
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <Button variant="outline" className="border-purple-500 text-purple-400 hover:bg-purple-950">
- <Crown className="mr-2 h-4 w-4" />
- {selectedTeam.name}
- <ChevronDown className="ml-2 h-4 w-4" />
- </Button>
- </DropdownMenuTrigger>
- <DropdownMenuContent className="w-56 bg-gray-900 border-gray-800">
- <DropdownMenuLabel>Your Bots</DropdownMenuLabel>
- <DropdownMenuSeparator className="bg-gray-800" />
- {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>
- <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>
- </DropdownMenuContent>
- </DropdownMenu>
- </div>
-
- <div className="flex items-center gap-4">
- {/* Notifications */}
- <Popover>
- <PopoverTrigger asChild>
- <Button variant="ghost" size="icon" className="relative text-gray-400 hover:text-white">
- <Bell className="h-5 w-5" />
- {unreadCount > 0 && (
- <span className="absolute -top-1 -right-1 flex h-4 w-4 items-center justify-center rounded-full bg-red-500 text-[10px] font-medium text-white">
- {unreadCount}
- </span>
- )}
- </Button>
- </PopoverTrigger>
- <PopoverContent className="w-80 p-0 bg-gray-900 border-gray-800">
- <div className="flex items-center justify-between border-b border-gray-800 p-3">
- <h3 className="font-medium">Notifications</h3>
- <div className="flex items-center gap-1">
- <Button
- variant="ghost"
- size="sm"
- className="h-7 px-2 text-xs text-gray-400 hover:text-white"
- onClick={clearAllNotifications}
- >
- Clear all
- </Button>
- </div>
- </div>
- <ScrollArea className="h-[300px]">
- {teamNotifications.length > 0 ? (
- <div className="space-y-1 p-1">
- {teamNotifications.map((notification) => (
- <div
- key={notification.id}
- className={`flex items-start p-3 rounded-md ${notification.read ? "bg-gray-900" : "bg-gray-800"}`}
- onClick={() => markNotificationAsRead(notification.id)}
- >
- <div
- className={`rounded-full p-1.5 mr-3 ${notification.type === "report"
- ? "bg-yellow-500/20"
- : notification.type === "ban"
- ? "bg-red-500/20"
- : "bg-blue-500/20"
- }`}
- >
- {notification.type === "report" ? (
- <AlertTriangle className="h-3.5 w-3.5 text-yellow-500" />
- ) : notification.type === "ban" ? (
- <Ban className="h-3.5 w-3.5 text-red-500" />
- ) : (
- <UserPlus className="h-3.5 w-3.5 text-blue-500" />
- )}
- </div>
- <div className="flex-1 min-w-0">
- <div className="flex items-center justify-between">
- <p className="text-sm font-medium">{notification.title}</p>
- <Button
- variant="ghost"
- size="sm"
- className="h-6 w-6 p-0 text-gray-500 hover:text-gray-300"
- onClick={(e) => {
- e.stopPropagation()
- deleteNotification(notification.id)
- }}
- >
- <X className="h-3 w-3" />
- <span className="sr-only">Dismiss</span>
- </Button>
- </div>
- <p className="text-xs text-gray-400 line-clamp-2">{notification.message}</p>
- <p className="text-xs text-gray-500 mt-1">
- {new Date(notification.timestamp).toLocaleString()}
- </p>
- </div>
- </div>
- ))}
- </div>
- ) : (
- <div className="flex flex-col items-center justify-center h-full p-6 text-center">
- <Bell className="h-8 w-8 text-gray-600 mb-2" />
- <p className="text-gray-400">No notifications</p>
- </div>
- )}
- </ScrollArea>
- </PopoverContent>
- </Popover>
-
- {/* Quick Action Buttons */}
- <Dialog>
- <DialogTrigger asChild>
- <Button
- variant="outline"
- size="sm"
- className="hidden md:flex border-red-500 text-red-400 hover:bg-red-950 hover:text-red-300"
- >
- <Ban className="mr-2 h-4 w-4" />
- Ban User
- </Button>
- </DialogTrigger>
- <DialogContent className="bg-gray-950 border-gray-800">
- <DialogHeader>
- <DialogTitle>Ban User</DialogTitle>
- <DialogDescription className="text-gray-400">Ban a user from your Matrix rooms</DialogDescription>
- </DialogHeader>
- <div className="space-y-4 py-4">
- <div className="space-y-2">
- <Label htmlFor="user-id">Matrix User ID</Label>
- <Input id="user-id" placeholder="@user:matrix.org" className="bg-gray-900 border-gray-800" />
- </div>
- <div className="space-y-2">
- <Label htmlFor="ban-reason">Reason</Label>
- <Textarea
- id="ban-reason"
- placeholder="Reason for the ban"
- className="bg-gray-900 border-gray-800"
- />
- </div>
- <div className="space-y-2">
- <Label>Policy Lists</Label>
- <div className="space-y-2 mt-1">
- {policyLists
- .filter((list) => !list.readOnly)
- .map((list) => (
- <div key={list.id} className="flex items-center space-x-2">
- <Checkbox id={`list-${list.id}`} />
- <Label htmlFor={`list-${list.id}`} className="font-normal">
- {list.name}
- </Label>
- </div>
- ))}
- </div>
- </div>
- <div className="space-y-2">
- <p className="text-xs text-gray-400">
- Bans are applied to all protected rooms monitored by this bot.
- </p>
- </div>
- </div>
- <DialogFooter>
- <Button
- variant="outline"
- className="border-gray-700 text-gray-400 hover:bg-gray-900 hover:text-gray-300"
- >
- Cancel
- </Button>
- <Button className="bg-red-600 text-white hover:bg-red-700">Ban User</Button>
- </DialogFooter>
- </DialogContent>
- </Dialog>
-
- <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 className="space-y-2">
- <Label htmlFor="room-id">Room</Label>
- <Select defaultValue="general">
- <SelectTrigger className="bg-gray-900 border-gray-800">
- <SelectValue placeholder="Select a room" />
- </SelectTrigger>
- <SelectContent className="bg-gray-900 border-gray-800">
- {selectedTeam.rooms.map((room) => (
- <SelectItem key={room} value={room.replace("#", "").split(":")[0]}>
- {room}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- </div>
- <div className="space-y-2">
- <Label htmlFor="kick-reason">Reason</Label>
- <Textarea
- id="kick-reason"
- placeholder="Reason for the kick"
- 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>
-
- <Link href="/">
- <Button
- variant="outline"
- className="hidden md:flex border-purple-500 text-purple-400 hover:bg-purple-950 hover:text-purple-300"
- >
- <LogOut className="mr-2 h-4 w-4" />
- Log Out
- </Button>
- </Link>
- <Sheet>
- <SheetTrigger asChild>
- <Button variant="ghost" size="icon" className="md:hidden">
- <Menu className="h-5 w-5" />
- </Button>
- </SheetTrigger>
- <SheetContent side="right" className="bg-gray-950 border-gray-800 p-0">
- <div className="flex flex-col h-full">
- <div className="border-b border-gray-800 p-4">
- <div className="flex items-center gap-2">
- <Shield className="h-5 w-5 text-purple-400" />
- <span className="font-medium">Draupnir4All</span>
- </div>
- </div>
- <div className="p-4">
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <Button
- variant="outline"
- className="w-full border-purple-500 text-purple-400 hover:bg-purple-950"
- >
- <Crown className="mr-2 h-4 w-4" />
- {selectedTeam.name}
- <ChevronDown className="ml-2 h-4 w-4" />
- </Button>
- </DropdownMenuTrigger>
- <DropdownMenuContent className="w-56 bg-gray-900 border-gray-800">
- <DropdownMenuLabel>Your Bots</DropdownMenuLabel>
- <DropdownMenuSeparator className="bg-gray-800" />
- {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" />
- <DropdownMenuItem>
- <Plus className="mr-2 h-4 w-4" />
- Create New Bot
- </DropdownMenuItem>
- </DropdownMenuContent>
- </DropdownMenu>
- </div>
- <nav className="flex flex-col p-4 gap-2">
- <Link
- className={`flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium transition-colors ${activeTab === "overview" ? "bg-gray-900 text-white" : "text-gray-400 hover:text-white"}`}
- href="#"
- onClick={() => setActiveTab("overview")}
- >
- <Home className="h-4 w-4" />
- Dashboard
- </Link>
-
- <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>
-
- <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>
- </nav>
- <div className="mt-auto border-t border-gray-800 p-4">
- <Link href="/">
- <Button
- variant="outline"
- className="w-full border-purple-500 text-purple-400 hover:bg-purple-950 hover:text-purple-300"
- >
- <LogOut className="mr-2 h-4 w-4" />
- Log Out
- </Button>
- </Link>
- </div>
- </div>
- </SheetContent>
- </Sheet>
- </div>
- </div>
- </header>
+ <DashboardHeader
+ selectedTeam={selectedTeam}
+ onTeamChange={handleTeamChange}
+ activeTab={activeTab}
+ setActiveTab={setActiveTab}
+ teams={mockTeams}
+ />
<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">
@@ -694,19 +246,23 @@ export default function DashboardPage() {
<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" className="space-y-8">
- <Overview selectedTeam={selectedTeam} reports={reports} policyLists={policyLists} />
+ <TabsContent value="overview">
+ <OverviewTab selectedTeam={selectedTeam} policyLists={policyLists} reports={reports} />
+ </TabsContent>
+ <TabsContent value="reports">
+ <ReportsTab reports={reports} />
</TabsContent>
- <TabsContent value="reports" className="space-y-4">
- <Reports selectedReport={selectedReport} reports={reports} setSelectedReport={setSelectedReport} />
+ <TabsContent value="bans">
+ <Bans policyLists={policyLists} />
</TabsContent>
- <TabsContent value="bans" className="space-y-4">
- <Bans selectedList={selectedList} selectedPolicyList={selectedPolicyList} policyLists={policyLists} setSelectedPolicyList={setSelectedPolicyList} />
+ <TabsContent value="analytics">
+ <AnalyticsDashboard selectedTeam={selectedTeam} />
</TabsContent>
- <TabsContent value="settings" className="space-y-4">
+ <TabsContent value="settings">
<TeamManagement selectedTeam={selectedTeam} onTeamChange={handleTeamChange} />
</TabsContent>
</Tabs>
diff --git a/src/app/dashboard/team-management.tsx b/src/app/dashboard/team-management.tsx
@@ -28,7 +28,7 @@ import {
} 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/dashboard/modals/add-protected-room";
+import AddProtectedRoom from "./components/modals/add-protected-room";
export interface Team {
id: string;
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
@@ -1,7 +1,8 @@
import type React from "react"
-import "./globals.css"
+import "@/app/globals.css"
import { Inter } from "next/font/google"
import { ThemeProvider } from "@/components/theme-provider"
+import { SessionProvider } from "@/contexts/session-context"
const inter = Inter({ subsets: ["latin"] })
@@ -19,9 +20,9 @@ export default function RootLayout({
<html lang="en" className="dark" style={{ colorScheme: "dark" }}>
<body className={inter.className}>
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem disableTransitionOnChange>
- {children}
+ <SessionProvider>{children}</SessionProvider>
</ThemeProvider>
</body>
- </html >
+ </html>
)
}
diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx
@@ -1,7 +1,6 @@
"use client"
import { useState } from "react"
-import { useRouter } from "next/navigation"
import Link from "next/link"
import { Shield, ArrowRight, Loader2 } from "lucide-react"
@@ -10,11 +9,12 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { MatrixLoginForm } from "@/components/matrix-login-form"
+import { useSession } from "@/contexts/session-context"
export default function LoginPage() {
- const router = useRouter()
+ const { login, isLoading } = useSession()
const [matrixId, setMatrixId] = useState("")
- const [step, setStep] = useState<"initial" | "login" | "verifying" | "error">("initial")
+ const [step, setStep] = useState<"initial" | "login" | "error">("initial")
const [errorMessage, setErrorMessage] = useState("")
const startMatrixAuth = () => {
@@ -23,26 +23,12 @@ export default function LoginPage() {
}
const handleLoginSuccess = async (token: string) => {
- setStep("verifying")
-
try {
- // In a real implementation, send the token to your backend for verification
- // const response = await fetch('/api/verify-matrix-token', {
- // method: 'POST',
- // headers: { 'Content-Type': 'application/json' },
- // body: JSON.stringify({ matrixId, token })
- // })
- // const data = await response.json()
- // if (!data.success) throw new Error(data.error || 'Verification failed')
-
- // For demo purposes, simulate a successful verification
- await new Promise((resolve) => setTimeout(resolve, 1500))
-
- // Redirect to dashboard after successful login
- router.push("/dashboard")
+ // Use the login function from session context
+ await login(matrixId)
} catch (error) {
- console.error("Token verification failed", error)
- setErrorMessage(error instanceof Error ? error.message : "Verification failed")
+ console.error("Login failed", error)
+ setErrorMessage(error instanceof Error ? error.message : "Authentication failed")
setStep("error")
}
}
@@ -84,7 +70,7 @@ export default function LoginPage() {
<MatrixLoginForm matrixId={matrixId} onSuccess={handleLoginSuccess} onCancel={() => setStep("initial")} />
)}
- {step === "verifying" && (
+ {isLoading && (
<div className="flex flex-col items-center justify-center py-6 space-y-4">
<Loader2 className="h-8 w-8 animate-spin text-purple-400" />
<p className="text-gray-300">Logging in...</p>
@@ -126,7 +112,7 @@ export default function LoginPage() {
</>
)}
- {step === "verifying" && (
+ {isLoading && (
<Button className="w-full" disabled>
Please wait...
</Button>
diff --git a/src/app/register/page.tsx b/src/app/register/page.tsx
@@ -1,7 +1,6 @@
"use client"
import { useState } from "react"
-import { useRouter } from "next/navigation"
import Link from "next/link"
import { Shield, ArrowRight, Loader2 } from "lucide-react"
@@ -10,11 +9,12 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { MatrixLoginForm } from "@/components/matrix-login-form"
+import { useSession } from "@/contexts/session-context"
export default function RegisterPage() {
- const router = useRouter()
+ const { register, isLoading } = useSession()
const [matrixId, setMatrixId] = useState("")
- const [step, setStep] = useState<"initial" | "login" | "verifying" | "error">("initial")
+ const [step, setStep] = useState<"initial" | "login" | "error">("initial")
const [errorMessage, setErrorMessage] = useState("")
const startMatrixAuth = () => {
@@ -23,26 +23,12 @@ export default function RegisterPage() {
}
const handleLoginSuccess = async (token: string) => {
- setStep("verifying")
-
try {
- // In a real implementation, send the token to your backend for verification
- // const response = await fetch('/api/verify-matrix-token', {
- // method: 'POST',
- // headers: { 'Content-Type': 'application/json' },
- // body: JSON.stringify({ matrixId, token })
- // })
- // const data = await response.json()
- // if (!data.success) throw new Error(data.error || 'Verification failed')
-
- // For demo purposes, simulate a successful verification
- await new Promise((resolve) => setTimeout(resolve, 1500))
-
- // Redirect to dashboard after successful registration
- router.push("/dashboard")
+ // Use the register function from session context
+ await register(matrixId)
} catch (error) {
- console.error("Token verification failed", error)
- setErrorMessage(error instanceof Error ? error.message : "Verification failed")
+ console.error("Registration failed", error)
+ setErrorMessage(error instanceof Error ? error.message : "Registration failed")
setStep("error")
}
}
@@ -86,7 +72,7 @@ export default function RegisterPage() {
<MatrixLoginForm matrixId={matrixId} onSuccess={handleLoginSuccess} onCancel={() => setStep("initial")} />
)}
- {step === "verifying" && (
+ {isLoading && (
<div className="flex flex-col items-center justify-center py-6 space-y-4">
<Loader2 className="h-8 w-8 animate-spin text-purple-400" />
<p className="text-gray-300">Registering your account...</p>
@@ -120,7 +106,7 @@ export default function RegisterPage() {
</Button>
)}
- {step === "verifying" && (
+ {isLoading && (
<Button className="w-full" disabled>
Please wait...
</Button>
diff --git a/src/components/analytics/bar-chart.tsx b/src/components/analytics/bar-chart.tsx
@@ -0,0 +1,66 @@
+"use client"
+
+interface BarChartItem {
+ label: string
+ values: {
+ value: number
+ color: string
+ label: string
+ }[]
+}
+
+interface BarChartProps {
+ data: BarChartItem[]
+ height?: number
+ className?: string
+}
+
+export function BarChart({ data, height = 200, className = "" }: BarChartProps) {
+ // Find the maximum value across all data points for scaling
+ const maxValue = Math.max(...data.flatMap((item) => item.values.map((v) => v.value)))
+
+ return (
+ <div className={`w-full ${className}`}>
+ <div className={`h-[${height}px] w-full`}>
+ <div className="flex h-full items-end gap-2">
+ {data.map((item, index) => (
+ <div key={index} className="group relative flex w-full flex-col items-center">
+ <div className="relative flex h-full w-full flex-col justify-end">
+ {item.values.map((value, valueIndex) => {
+ const barHeight = (value.value / maxValue) * 100
+ return (
+ <div
+ key={valueIndex}
+ className={`w-full rounded-t transition-all group-hover:opacity-90 ${valueIndex > 0 ? "mt-0.5" : ""}`}
+ style={{
+ height: `${barHeight}%`,
+ backgroundColor: value.color,
+ }}
+ ></div>
+ )
+ })}
+ </div>
+ <div className="absolute bottom-0 left-0 right-0 h-0 overflow-hidden bg-gray-800 opacity-0 transition-all group-hover:h-auto group-hover:opacity-100 z-10 rounded-md">
+ <div className="p-2 text-center text-xs">
+ <div className="font-medium">{item.label}</div>
+ {item.values.map((value, valueIndex) => (
+ <div key={valueIndex} style={{ color: value.color }}>
+ {value.label}: {value.value}
+ </div>
+ ))}
+ </div>
+ </div>
+ </div>
+ ))}
+ </div>
+ </div>
+ <div className="mt-2 flex w-full justify-between">
+ {data.map((item, index) => (
+ <div key={index} className="text-center text-xs text-gray-400">
+ {item.label}
+ </div>
+ ))}
+ </div>
+ </div>
+ )
+}
diff --git a/src/components/analytics/heatmap.tsx b/src/components/analytics/heatmap.tsx
@@ -0,0 +1,100 @@
+"use client"
+
+import { Calendar } from "lucide-react"
+
+interface HeatmapDay {
+ date: string
+ count: number
+}
+
+interface HeatmapProps {
+ data: HeatmapDay[]
+ title: string
+ colorIntensityLabel?: string
+ className?: string
+}
+
+export function Heatmap({ data, title, colorIntensityLabel = "Activity", className = "" }: HeatmapProps) {
+ // Get intensity color based on count
+ const getColor = (count: number) => {
+ if (count === 0) return "bg-gray-900"
+ if (count < 3) return "bg-purple-950"
+ if (count < 6) return "bg-purple-900"
+ if (count < 9) return "bg-purple-800"
+ return "bg-purple-600"
+ }
+
+ // Group data by week
+ const weeks: HeatmapDay[][] = []
+ let currentWeek: HeatmapDay[] = []
+
+ // Process data to create weeks (7 days per week)
+ data.forEach((day, index) => {
+ currentWeek.push(day)
+ if (currentWeek.length === 7 || index === data.length - 1) {
+ weeks.push([...currentWeek])
+ currentWeek = []
+ }
+ })
+
+ return (
+ <div className={`space-y-2 ${className}`}>
+ <div className="flex items-center justify-between">
+ <div className="flex items-center gap-2">
+ <Calendar className="h-4 w-4 text-gray-400" />
+ <h3 className="text-sm font-medium">{title}</h3>
+ </div>
+ <div className="flex items-center gap-1 text-xs text-gray-400">
+ <span>Less</span>
+ <div className="flex gap-1">
+ <div className="h-3 w-3 rounded-sm bg-gray-900"></div>
+ <div className="h-3 w-3 rounded-sm bg-purple-950"></div>
+ <div className="h-3 w-3 rounded-sm bg-purple-900"></div>
+ <div className="h-3 w-3 rounded-sm bg-purple-800"></div>
+ <div className="h-3 w-3 rounded-sm bg-purple-600"></div>
+ </div>
+ <span>More</span>
+ </div>
+ </div>
+
+ <div className="flex gap-2">
+ {/* Days of week labels */}
+ <div className="flex flex-col pt-6 pr-2 text-xs text-gray-500">
+ <div className="h-[18px] mb-[2px]">Mon</div>
+ <div className="h-[18px] mb-[2px]">Tue</div>
+ <div className="h-[18px] mb-[2px]">Wed</div>
+ <div className="h-[18px] mb-[2px]">Thu</div>
+ <div className="h-[18px] mb-[2px]">Fri</div>
+ <div className="h-[18px] mb-[2px]">Sat</div>
+ <div className="h-[18px]">Sun</div>
+ </div>
+
+ {/* Heatmap grid */}
+ <div className="overflow-x-auto">
+ <div className="flex gap-1 min-w-[650px]">
+ {weeks.map((week, weekIndex) => (
+ <div key={weekIndex} className="flex flex-col gap-[2px]">
+ {week.map((day, dayIndex) => (
+ <div
+ key={`${weekIndex}-${dayIndex}`}
+ className={`h-[18px] w-[18px] rounded-sm ${getColor(day.count)}`}
+ title={`${day.date}: ${day.count} ${colorIntensityLabel}`}
+ ></div>
+ ))}
+ </div>
+ ))}
+ </div>
+
+ {/* Month labels */}
+ <div className="flex text-xs text-gray-500 mt-1 min-w-[650px]">
+ {["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"].map((month, i) => (
+ <div key={i} className="flex-1">
+ {month}
+ </div>
+ ))}
+ </div>
+ </div>
+ </div>
+ </div>
+ )
+}
diff --git a/src/components/analytics/horizontal-bar-chart.tsx b/src/components/analytics/horizontal-bar-chart.tsx
@@ -0,0 +1,46 @@
+"use client"
+
+interface HorizontalBarItem {
+ label: string
+ value: number
+ color?: string
+ secondaryLabel?: string
+}
+
+interface HorizontalBarChartProps {
+ data: HorizontalBarItem[]
+ maxValue?: number
+ color?: string
+ className?: string
+}
+
+export function HorizontalBarChart({ data, maxValue, color = "#ef4444", className = "" }: HorizontalBarChartProps) {
+ // If maxValue is not provided, calculate it from the data
+ const calculatedMax = maxValue || Math.max(...data.map((item) => item.value))
+
+ return (
+ <div className={`space-y-4 ${className}`}>
+ {data.map((item, index) => {
+ const percentage = (item.value / calculatedMax) * 100
+
+ return (
+ <div key={index} className="space-y-1">
+ <div className="flex items-center justify-between">
+ <span className="text-sm font-medium">{item.label}</span>
+ <span className="text-xs text-gray-400">{item.secondaryLabel || `${item.value}`}</span>
+ </div>
+ <div className="h-2 w-full overflow-hidden rounded-full bg-gray-900">
+ <div
+ className="h-full rounded-full transition-all duration-300 hover:opacity-80"
+ style={{
+ width: `${percentage}%`,
+ backgroundColor: item.color || color,
+ }}
+ ></div>
+ </div>
+ </div>
+ )
+ })}
+ </div>
+ )
+}
diff --git a/src/components/analytics/pie-chart.tsx b/src/components/analytics/pie-chart.tsx
@@ -0,0 +1,147 @@
+"use client"
+
+import { useState } from "react"
+import { PieChartIcon } from "lucide-react"
+
+interface PieChartItem {
+ id: string | number
+ label: string
+ value: number
+ color: string
+}
+
+interface PieChartProps {
+ data: PieChartItem[]
+ centerLabel?: string
+ className?: string
+}
+
+interface PieSlice {
+ id: string | number;
+ d: string;
+ fill: string;
+ percentage: number;
+ midAngle: number;
+ hoverDx: number;
+ hoverDy: number;
+ label: string;
+ value: number;
+ index: number;
+}
+
+export function PieChart({
+ data,
+ centerLabel = "Distribution",
+ className = "",
+}: PieChartProps) {
+ const [hoveredSlice, setHoveredSlice] = useState<number | null>(null)
+
+ // Calculate total for percentages
+ const total = data.reduce((sum, item) => sum + item.value, 0)
+
+ // Generate pie slices
+ const { slices } = data.reduce(
+ (acc, item, i) => {
+ const startAngle = acc.offset
+ const angle = (item.value / total) * 360
+ const endAngle = startAngle + angle
+ const largeArcFlag = angle > 180 ? 1 : 0
+
+ // Calculate coordinates
+ const startX = 50 + 45 * Math.cos((startAngle * Math.PI) / 180)
+ const startY = 50 + 45 * Math.sin((startAngle * Math.PI) / 180)
+ const endX = 50 + 45 * Math.cos((endAngle * Math.PI) / 180)
+ const endY = 50 + 45 * Math.sin((endAngle * Math.PI) / 180)
+
+ // Calculate midpoint angle for hover direction
+ const midAngle = startAngle + angle / 2
+
+ // Calculate hover translation direction
+ const hoverDx = 5 * Math.cos((midAngle * Math.PI) / 180)
+ const hoverDy = 5 * Math.sin((midAngle * Math.PI) / 180)
+
+ const d = [`M 50 50`, `L ${startX} ${startY}`, `A 45 45 0 ${largeArcFlag} 1 ${endX} ${endY}`, `Z`].join(" ")
+
+ const percentage = Math.round((item.value / total) * 100)
+
+ acc.slices.push({
+ id: item.id,
+ d,
+ fill: item.color,
+ percentage,
+ midAngle,
+ hoverDx,
+ hoverDy,
+ label: item.label,
+ value: item.value,
+ index: i,
+ } as PieSlice)
+
+ acc.offset = endAngle
+ return acc
+ },
+ { slices: [] as PieSlice[], offset: 0 },
+ )
+
+ return (
+ <div className={`flex flex-col ${className}`}>
+ <div className="flex h-[220px] items-center justify-center">
+ <div className="relative h-[180px] w-[180px]">
+ {/* Pie chart visualization */}
+ <div className="relative">
+ <svg viewBox="0 0 100 100" className="h-full w-full -rotate-90">
+ {slices.map((slice, i) => (
+ <path
+ key={slice.id}
+ d={slice.d}
+ fill={slice.fill}
+ stroke="#111"
+ strokeWidth="0.5"
+ className="transition-transform duration-200 cursor-pointer hover:opacity-90"
+ style={{
+ transform: hoveredSlice === i ? `translate(${slice.hoverDx}px, ${slice.hoverDy}px)` : "none",
+ }}
+ onMouseEnter={() => setHoveredSlice(i)}
+ onMouseLeave={() => setHoveredSlice(null)}
+ />
+ ))}
+ </svg>
+
+ {/* Center circle */}
+ <div className="absolute inset-0 flex items-center justify-center pointer-events-none">
+ <div className="flex h-[100px] w-[100px] flex-col items-center justify-center rounded-full bg-gray-950">
+ <PieChartIcon className="h-5 w-5 text-gray-400" />
+ <span className="text-xs text-gray-400">{centerLabel}</span>
+ </div>
+ </div>
+ </div>
+
+ {/* Tooltip container - positioned outside the SVG */}
+ {hoveredSlice !== null && (
+ <div className="absolute top-0 left-0 w-full">
+ <div className="absolute top-[-40px] left-1/2 transform -translate-x-1/2 bg-gray-800 text-white text-xs rounded px-2 py-1 z-50 whitespace-nowrap">
+ {slices[hoveredSlice].label}: {slices[hoveredSlice].value} ({slices[hoveredSlice].percentage}%)
+ </div>
+ </div>
+ )}
+ </div>
+ </div>
+ <div className="mt-4 grid grid-cols-2 gap-2">
+ {slices.map((slice, index) => (
+ <div
+ key={slice.id}
+ className="flex items-center gap-2 p-1 rounded hover:bg-gray-900 cursor-pointer transition-colors"
+ onMouseEnter={() => setHoveredSlice(index)}
+ onMouseLeave={() => setHoveredSlice(null)}
+ >
+ <div className="h-3 w-3 rounded-full" style={{ backgroundColor: slice.fill }}></div>
+ <div className="flex flex-1 items-center justify-between">
+ <span className="text-xs">{slice.label}</span>
+ <span className="text-xs text-gray-400">{slice.value}</span>
+ </div>
+ </div>
+ ))}
+ </div>
+ </div>
+ )
+}
diff --git a/src/components/dashboard/bans-pane.tsx b/src/components/dashboard/bans-pane.tsx
@@ -1,209 +0,0 @@
-import { Checkbox } from "../ui/checkbox"
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "../ui/dialog";
-import { ScrollArea } from "../ui/scroll-area"
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"
-import { Ban, Badge } from "lucide-react";
-import { Button } from "../ui/button";
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card";
-import { Input } from "../ui/input";
-import { Textarea } from "../ui/textarea";
-import { Label } from "../ui/label"
-
-interface BansProps {
- policyLists: any[];
- selectedPolicyList: string | null;
- setSelectedPolicyList: (id: string) => void;
- selectedList: any;
-}
-
-export default function Bans({ policyLists, selectedPolicyList, setSelectedPolicyList, selectedList }: BansProps) {
- return (
- <Card className="border-gray-800 bg-gray-950">
- <CardHeader className="pb-2">
- <div className="flex items-center justify-between">
- <CardTitle>Policy Lists</CardTitle>
- <Dialog>
- <DialogTrigger asChild>
- <Button size="sm" className="bg-purple-600 text-white hover:bg-purple-700">
- <Ban className="mr-2 h-4 w-4" />
- Add Ban
- </Button>
- </DialogTrigger>
- <DialogContent className="bg-gray-950 border-gray-800">
- <DialogHeader>
- <DialogTitle>Add New Ban</DialogTitle>
- <DialogDescription className="text-gray-400">
- Ban a user or server from your Matrix rooms
- </DialogDescription>
- </DialogHeader>
- <div className="space-y-4 py-4">
- <div className="space-y-2">
- <Label htmlFor="ban-type">Ban Type</Label>
- <Select defaultValue="user">
- <SelectTrigger className="bg-gray-900 border-gray-800">
- <SelectValue placeholder="Select ban type" />
- </SelectTrigger>
- <SelectContent className="bg-gray-900 border-gray-800">
- <SelectItem value="user">User</SelectItem>
- <SelectItem value="server">Server</SelectItem>
- <SelectItem value="room">Room</SelectItem>
- </SelectContent>
- </Select>
- </div>
- <div className="space-y-2">
- <Label htmlFor="ban-target">Target</Label>
- <Input
- id="ban-target"
- placeholder="@user:matrix.org"
- className="bg-gray-900 border-gray-800"
- />
- </div>
- <div className="space-y-2">
- <Label htmlFor="ban-reason">Reason</Label>
- <Textarea
- id="ban-reason"
- placeholder="Reason for the ban"
- className="bg-gray-900 border-gray-800"
- />
- </div>
- <div className="space-y-2">
- <Label>Policy Lists</Label>
- <div className="space-y-2 mt-1">
- {policyLists
- .filter((list) => !list.readOnly)
- .map((list) => (
- <div key={list.id} className="flex items-center space-x-2">
- <Checkbox id={`list-${list.id}`} />
- <Label htmlFor={`list-${list.id}`} className="font-normal">
- {list.name}
- </Label>
- </div>
- ))}
- </div>
- </div>
- <div className="space-y-2">
- <p className="text-xs text-gray-400">
- Bans are applied to all protected rooms monitored by this bot.
- </p>
- </div>
- </div>
- <DialogFooter>
- <Button
- variant="outline"
- className="border-gray-700 text-gray-400 hover:bg-gray-900 hover:text-gray-300"
- >
- Cancel
- </Button>
- <Button className="bg-red-600 text-white hover:bg-red-700">Add Ban</Button>
- </DialogFooter>
- </DialogContent>
- </Dialog>
- </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 ? (
- <Badge className="bg-gray-800 text-gray-400">Read Only</Badge>
- ) : (
- <Badge className="bg-green-900/50 text-green-300">Editable</Badge>
- )}
- </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" />
- {selectedList && !selectedList.readOnly && (
- <Dialog>
- <DialogTrigger asChild>
- <Button size="sm" className="bg-purple-600 text-white hover:bg-purple-700">
- Add Entry
- </Button>
- </DialogTrigger>
- <DialogContent className="bg-gray-950 border-gray-800">
- {/* Add entry dialog content */}
- </DialogContent>
- </Dialog>
- )}
- </div>
- </div>
-
- <ScrollArea className="h-[400px] rounded-md border border-gray-800">
- <div className="p-4 space-y-3">
- {selectedList && selectedList.entries && selectedList.entries.length > 0 ? (
- selectedList.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/components/dashboard/overview-pane.tsx b/src/components/dashboard/overview-pane.tsx
@@ -1,151 +0,0 @@
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "../ui/dialog";
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"
-import { Label } from "../ui/label"
-import { Plus, Ban, AlertTriangle, CheckCircle, X } from "lucide-react";
-import { Button } from "../ui/button";
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card";
-import { Input } from "../ui/input";
-import { Team } from "@/app/dashboard/team-management";
-import AddProtectedRoom from "./modals/add-protected-room";
-
-interface OverviewProps {
- selectedTeam: Team;
- reports: any[];
- policyLists: any[];
-}
-
-export default function Overview({ selectedTeam, reports, policyLists }: OverviewProps) {
-
- return (
- <>
- <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>
- <div className="space-y-4">
- {selectedTeam.rooms.map((room, index) => (
- <div key={index} className="flex items-center justify-between">
- <div className="flex items-center gap-2">
- <div className="h-2 w-2 rounded-full bg-green-500"></div>
- <p className="text-sm">{room}</p>
- </div>
- <Button variant="ghost" size="sm" className="h-7 w-7 p-0 text-gray-400 hover:text-red-400">
- <X className="h-4 w-4" />
- <span className="sr-only">Remove</span>
- </Button>
- </div>
- ))}
- </div>
- </CardContent>
- </Card>
- </div>
- </>
- );
-
-}
-\ No newline at end of file
diff --git a/src/components/dashboard/reports-pane.tsx b/src/components/dashboard/reports-pane.tsx
@@ -1,251 +0,0 @@
-import {
- Dialog,
- DialogContent,
- DialogDescription,
-
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "../ui/dialog"
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"
-import { MessageSquare, UserPlus, Badge, CheckCircle, Filter, Ban, AlertTriangle } from "lucide-react"
-import { BlurredImage } from "../blurred-image"
-import { BlurredText } from "../blurred-text"
-import { Button } from "../ui/button"
-import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "../ui/card"
-
-interface ReportsProps {
- selectedReport: string | null;
- reports: any[];
- setSelectedReport: (report: string) => void;
-}
-
-export default function Reports({ selectedReport, reports, setSelectedReport }: ReportsProps) {
- 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">
- <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>
- {reports.length > 0 ? (
- reports.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>
- )
-}
-\ No newline at end of file
diff --git a/src/components/ui/calendar.tsx b/src/components/ui/calendar.tsx
@@ -1,66 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { ChevronLeft, ChevronRight } from "lucide-react"
-import { DayPicker } from "react-day-picker"
-
-import { cn } from "@/lib/utils"
-import { buttonVariants } from "@/components/ui/button"
-
-export type CalendarProps = React.ComponentProps<typeof DayPicker>
-
-function Calendar({
- className,
- classNames,
- showOutsideDays = true,
- ...props
-}: CalendarProps) {
- return (
- <DayPicker
- showOutsideDays={showOutsideDays}
- className={cn("p-3", className)}
- classNames={{
- months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
- month: "space-y-4",
- caption: "flex justify-center pt-1 relative items-center",
- caption_label: "text-sm font-medium",
- nav: "space-x-1 flex items-center",
- nav_button: cn(
- buttonVariants({ variant: "outline" }),
- "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
- ),
- nav_button_previous: "absolute left-1",
- nav_button_next: "absolute right-1",
- table: "w-full border-collapse space-y-1",
- head_row: "flex",
- head_cell:
- "text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
- row: "flex w-full mt-2",
- cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
- day: cn(
- buttonVariants({ variant: "ghost" }),
- "h-9 w-9 p-0 font-normal aria-selected:opacity-100"
- ),
- day_range_end: "day-range-end",
- day_selected:
- "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
- day_today: "bg-accent text-accent-foreground",
- day_outside:
- "day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
- day_disabled: "text-muted-foreground opacity-50",
- day_range_middle:
- "aria-selected:bg-accent aria-selected:text-accent-foreground",
- day_hidden: "invisible",
- ...classNames,
- }}
- components={{
- IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />,
- IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />,
- }}
- {...props}
- />
- )
-}
-Calendar.displayName = "Calendar"
-
-export { Calendar }
diff --git a/src/components/ui/chart.tsx b/src/components/ui/chart.tsx
@@ -1,365 +0,0 @@
-"use client"
-
-import * as React from "react"
-import * as RechartsPrimitive from "recharts"
-
-import { cn } from "@/lib/utils"
-
-// Format: { THEME_NAME: CSS_SELECTOR }
-const THEMES = { light: "", dark: ".dark" } as const
-
-export type ChartConfig = {
- [k in string]: {
- label?: React.ReactNode
- icon?: React.ComponentType
- } & (
- | { color?: string; theme?: never }
- | { color?: never; theme: Record<keyof typeof THEMES, string> }
- )
-}
-
-type ChartContextProps = {
- config: ChartConfig
-}
-
-const ChartContext = React.createContext<ChartContextProps | null>(null)
-
-function useChart() {
- const context = React.useContext(ChartContext)
-
- if (!context) {
- throw new Error("useChart must be used within a <ChartContainer />")
- }
-
- return context
-}
-
-const ChartContainer = React.forwardRef<
- HTMLDivElement,
- React.ComponentProps<"div"> & {
- config: ChartConfig
- children: React.ComponentProps<
- typeof RechartsPrimitive.ResponsiveContainer
- >["children"]
- }
->(({ id, className, children, config, ...props }, ref) => {
- const uniqueId = React.useId()
- const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
-
- return (
- <ChartContext.Provider value={{ config }}>
- <div
- data-chart={chartId}
- ref={ref}
- className={cn(
- "flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
- className
- )}
- {...props}
- >
- <ChartStyle id={chartId} config={config} />
- <RechartsPrimitive.ResponsiveContainer>
- {children}
- </RechartsPrimitive.ResponsiveContainer>
- </div>
- </ChartContext.Provider>
- )
-})
-ChartContainer.displayName = "Chart"
-
-const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
- const colorConfig = Object.entries(config).filter(
- ([_, config]) => config.theme || config.color
- )
-
- if (!colorConfig.length) {
- return null
- }
-
- return (
- <style
- dangerouslySetInnerHTML={{
- __html: Object.entries(THEMES)
- .map(
- ([theme, prefix]) => `
-${prefix} [data-chart=${id}] {
-${colorConfig
- .map(([key, itemConfig]) => {
- const color =
- itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
- itemConfig.color
- return color ? ` --color-${key}: ${color};` : null
- })
- .join("\n")}
-}
-`
- )
- .join("\n"),
- }}
- />
- )
-}
-
-const ChartTooltip = RechartsPrimitive.Tooltip
-
-const ChartTooltipContent = React.forwardRef<
- HTMLDivElement,
- React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
- React.ComponentProps<"div"> & {
- hideLabel?: boolean
- hideIndicator?: boolean
- indicator?: "line" | "dot" | "dashed"
- nameKey?: string
- labelKey?: string
- }
->(
- (
- {
- active,
- payload,
- className,
- indicator = "dot",
- hideLabel = false,
- hideIndicator = false,
- label,
- labelFormatter,
- labelClassName,
- formatter,
- color,
- nameKey,
- labelKey,
- },
- ref
- ) => {
- const { config } = useChart()
-
- const tooltipLabel = React.useMemo(() => {
- if (hideLabel || !payload?.length) {
- return null
- }
-
- const [item] = payload
- const key = `${labelKey || item.dataKey || item.name || "value"}`
- const itemConfig = getPayloadConfigFromPayload(config, item, key)
- const value =
- !labelKey && typeof label === "string"
- ? config[label as keyof typeof config]?.label || label
- : itemConfig?.label
-
- if (labelFormatter) {
- return (
- <div className={cn("font-medium", labelClassName)}>
- {labelFormatter(value, payload)}
- </div>
- )
- }
-
- if (!value) {
- return null
- }
-
- return <div className={cn("font-medium", labelClassName)}>{value}</div>
- }, [
- label,
- labelFormatter,
- payload,
- hideLabel,
- labelClassName,
- config,
- labelKey,
- ])
-
- if (!active || !payload?.length) {
- return null
- }
-
- const nestLabel = payload.length === 1 && indicator !== "dot"
-
- return (
- <div
- ref={ref}
- className={cn(
- "grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
- className
- )}
- >
- {!nestLabel ? tooltipLabel : null}
- <div className="grid gap-1.5">
- {payload.map((item, index) => {
- const key = `${nameKey || item.name || item.dataKey || "value"}`
- const itemConfig = getPayloadConfigFromPayload(config, item, key)
- const indicatorColor = color || item.payload.fill || item.color
-
- return (
- <div
- key={item.dataKey}
- className={cn(
- "flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
- indicator === "dot" && "items-center"
- )}
- >
- {formatter && item?.value !== undefined && item.name ? (
- formatter(item.value, item.name, item, index, item.payload)
- ) : (
- <>
- {itemConfig?.icon ? (
- <itemConfig.icon />
- ) : (
- !hideIndicator && (
- <div
- className={cn(
- "shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
- {
- "h-2.5 w-2.5": indicator === "dot",
- "w-1": indicator === "line",
- "w-0 border-[1.5px] border-dashed bg-transparent":
- indicator === "dashed",
- "my-0.5": nestLabel && indicator === "dashed",
- }
- )}
- style={
- {
- "--color-bg": indicatorColor,
- "--color-border": indicatorColor,
- } as React.CSSProperties
- }
- />
- )
- )}
- <div
- className={cn(
- "flex flex-1 justify-between leading-none",
- nestLabel ? "items-end" : "items-center"
- )}
- >
- <div className="grid gap-1.5">
- {nestLabel ? tooltipLabel : null}
- <span className="text-muted-foreground">
- {itemConfig?.label || item.name}
- </span>
- </div>
- {item.value && (
- <span className="font-mono font-medium tabular-nums text-foreground">
- {item.value.toLocaleString()}
- </span>
- )}
- </div>
- </>
- )}
- </div>
- )
- })}
- </div>
- </div>
- )
- }
-)
-ChartTooltipContent.displayName = "ChartTooltip"
-
-const ChartLegend = RechartsPrimitive.Legend
-
-const ChartLegendContent = React.forwardRef<
- HTMLDivElement,
- React.ComponentProps<"div"> &
- Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
- hideIcon?: boolean
- nameKey?: string
- }
->(
- (
- { className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
- ref
- ) => {
- const { config } = useChart()
-
- if (!payload?.length) {
- return null
- }
-
- return (
- <div
- ref={ref}
- className={cn(
- "flex items-center justify-center gap-4",
- verticalAlign === "top" ? "pb-3" : "pt-3",
- className
- )}
- >
- {payload.map((item) => {
- const key = `${nameKey || item.dataKey || "value"}`
- const itemConfig = getPayloadConfigFromPayload(config, item, key)
-
- return (
- <div
- key={item.value}
- className={cn(
- "flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
- )}
- >
- {itemConfig?.icon && !hideIcon ? (
- <itemConfig.icon />
- ) : (
- <div
- className="h-2 w-2 shrink-0 rounded-[2px]"
- style={{
- backgroundColor: item.color,
- }}
- />
- )}
- {itemConfig?.label}
- </div>
- )
- })}
- </div>
- )
- }
-)
-ChartLegendContent.displayName = "ChartLegend"
-
-// Helper to extract item config from a payload.
-function getPayloadConfigFromPayload(
- config: ChartConfig,
- payload: unknown,
- key: string
-) {
- if (typeof payload !== "object" || payload === null) {
- return undefined
- }
-
- const payloadPayload =
- "payload" in payload &&
- typeof payload.payload === "object" &&
- payload.payload !== null
- ? payload.payload
- : undefined
-
- let configLabelKey: string = key
-
- if (
- key in payload &&
- typeof payload[key as keyof typeof payload] === "string"
- ) {
- configLabelKey = payload[key as keyof typeof payload] as string
- } else if (
- payloadPayload &&
- key in payloadPayload &&
- typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
- ) {
- configLabelKey = payloadPayload[
- key as keyof typeof payloadPayload
- ] as string
- }
-
- return configLabelKey in config
- ? config[configLabelKey]
- : config[key as keyof typeof config]
-}
-
-export {
- ChartContainer,
- ChartTooltip,
- ChartTooltipContent,
- ChartLegend,
- ChartLegendContent,
- ChartStyle,
-}
diff --git a/src/components/ui/use-toast.ts b/src/components/ui/use-toast.ts
@@ -18,6 +18,7 @@ type ToasterToast = ToastProps & {
action?: ToastActionElement
}
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
@@ -36,21 +37,21 @@ type ActionType = typeof actionTypes
type Action =
| {
- type: ActionType["ADD_TOAST"]
- toast: ToasterToast
- }
+ type: ActionType["ADD_TOAST"]
+ toast: ToasterToast
+ }
| {
- type: ActionType["UPDATE_TOAST"]
- toast: Partial<ToasterToast>
- }
+ type: ActionType["UPDATE_TOAST"]
+ toast: Partial<ToasterToast>
+ }
| {
- type: ActionType["DISMISS_TOAST"]
- toastId?: ToasterToast["id"]
- }
+ type: ActionType["DISMISS_TOAST"]
+ toastId?: ToasterToast["id"]
+ }
| {
- type: ActionType["REMOVE_TOAST"]
- toastId?: ToasterToast["id"]
- }
+ type: ActionType["REMOVE_TOAST"]
+ toastId?: ToasterToast["id"]
+ }
interface State {
toasts: ToasterToast[]
@@ -108,9 +109,9 @@ export const reducer = (state: State, action: Action): State => {
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
- ...t,
- open: false,
- }
+ ...t,
+ open: false,
+ }
: t
),
}
@@ -158,7 +159,7 @@ function toast({ ...props }: Toast) {
...props,
id,
open: true,
- onOpenChange: (open) => {
+ onOpenChange: (open: boolean) => {
if (!open) dismiss()
},
},
diff --git a/src/contexts/session-context.tsx b/src/contexts/session-context.tsx
@@ -0,0 +1,130 @@
+"use client"
+
+import { createContext, useContext, useEffect, useState, type ReactNode } from "react"
+import { useRouter } from "next/navigation"
+
+type User = {
+ id: string
+ matrixId: string
+ displayName: string
+ avatarUrl?: string
+ isAdmin: boolean
+}
+
+type SessionContextType = {
+ user: User | null
+ isLoading: boolean
+ isAuthenticated: boolean
+ login: (matrixId: string) => Promise<void>
+ register: (matrixId: string) => Promise<void>
+ logout: () => Promise<void>
+}
+
+const SessionContext = createContext<SessionContextType | undefined>(undefined)
+
+export function SessionProvider({ children }: { children: ReactNode }) {
+ const [user, setUser] = useState<User | null>(null)
+ const [isLoading, setIsLoading] = useState(true)
+ const router = useRouter()
+
+ // Check for existing session on mount
+ useEffect(() => {
+ const checkSession = async () => {
+ try {
+ const response = await fetch("/api/auth/session")
+ const data = await response.json()
+
+ if (data.authenticated && data.user) {
+ setUser(data.user)
+ }
+ } catch (error) {
+ console.error("Failed to check session:", error)
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ checkSession()
+ }, [])
+
+ const login = async (matrixId: string) => {
+ setIsLoading(true)
+ try {
+ const response = await fetch("/api/auth/login", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ matrixId }),
+ })
+
+ if (!response.ok) {
+ throw new Error("Authentication failed")
+ }
+
+ const data = await response.json()
+ setUser(data.user)
+ router.push("/dashboard")
+ } catch (error) {
+ console.error("Login failed:", error)
+ throw error
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ const register = async (matrixId: string) => {
+ setIsLoading(true)
+ try {
+ const response = await fetch("/api/auth/register", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ matrixId }),
+ })
+
+ if (!response.ok) {
+ throw new Error("Registration failed")
+ }
+
+ const data = await response.json()
+ setUser(data.user)
+ router.push("/dashboard")
+ } catch (error) {
+ console.error("Registration failed:", error)
+ throw error
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ const logout = async () => {
+ try {
+ await fetch("/api/auth/logout", { method: "POST" })
+ setUser(null)
+ router.push("/")
+ } catch (error) {
+ console.error("Logout failed:", error)
+ }
+ }
+
+ return (
+ <SessionContext.Provider
+ value={{
+ user,
+ isLoading,
+ isAuthenticated: !!user,
+ login,
+ register,
+ logout,
+ }}
+ >
+ {children}
+ </SessionContext.Provider>
+ )
+}
+
+export function useSession() {
+ const context = useContext(SessionContext)
+ if (context === undefined) {
+ throw new Error("useSession must be used within a SessionProvider")
+ }
+ return context
+}
diff --git a/src/hooks/use-mobile.tsx b/src/hooks/use-mobile.tsx
@@ -1,19 +0,0 @@
-import * as React from "react"
-
-const MOBILE_BREAKPOINT = 768
-
-export function useIsMobile() {
- const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
-
- React.useEffect(() => {
- const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
- const onChange = () => {
- setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
- }
- mql.addEventListener("change", onChange)
- setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
- return () => mql.removeEventListener("change", onChange)
- }, [])
-
- return !!isMobile
-}
diff --git a/src/hooks/use-toast.ts b/src/hooks/use-toast.ts
@@ -1,194 +0,0 @@
-"use client"
-
-// Inspired by react-hot-toast library
-import * as React from "react"
-
-import type {
- ToastActionElement,
- ToastProps,
-} from "@/components/ui/toast"
-
-const TOAST_LIMIT = 1
-const TOAST_REMOVE_DELAY = 1000000
-
-type ToasterToast = ToastProps & {
- id: string
- title?: React.ReactNode
- description?: React.ReactNode
- action?: ToastActionElement
-}
-
-const actionTypes = {
- ADD_TOAST: "ADD_TOAST",
- UPDATE_TOAST: "UPDATE_TOAST",
- DISMISS_TOAST: "DISMISS_TOAST",
- REMOVE_TOAST: "REMOVE_TOAST",
-} as const
-
-let count = 0
-
-function genId() {
- count = (count + 1) % Number.MAX_SAFE_INTEGER
- return count.toString()
-}
-
-type ActionType = typeof actionTypes
-
-type Action =
- | {
- type: ActionType["ADD_TOAST"]
- toast: ToasterToast
- }
- | {
- type: ActionType["UPDATE_TOAST"]
- toast: Partial<ToasterToast>
- }
- | {
- type: ActionType["DISMISS_TOAST"]
- toastId?: ToasterToast["id"]
- }
- | {
- type: ActionType["REMOVE_TOAST"]
- toastId?: ToasterToast["id"]
- }
-
-interface State {
- toasts: ToasterToast[]
-}
-
-const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
-
-const addToRemoveQueue = (toastId: string) => {
- if (toastTimeouts.has(toastId)) {
- return
- }
-
- const timeout = setTimeout(() => {
- toastTimeouts.delete(toastId)
- dispatch({
- type: "REMOVE_TOAST",
- toastId: toastId,
- })
- }, TOAST_REMOVE_DELAY)
-
- toastTimeouts.set(toastId, timeout)
-}
-
-export const reducer = (state: State, action: Action): State => {
- switch (action.type) {
- case "ADD_TOAST":
- return {
- ...state,
- toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
- }
-
- case "UPDATE_TOAST":
- return {
- ...state,
- toasts: state.toasts.map((t) =>
- t.id === action.toast.id ? { ...t, ...action.toast } : t
- ),
- }
-
- case "DISMISS_TOAST": {
- const { toastId } = action
-
- // ! Side effects ! - This could be extracted into a dismissToast() action,
- // but I'll keep it here for simplicity
- if (toastId) {
- addToRemoveQueue(toastId)
- } else {
- state.toasts.forEach((toast) => {
- addToRemoveQueue(toast.id)
- })
- }
-
- return {
- ...state,
- toasts: state.toasts.map((t) =>
- t.id === toastId || toastId === undefined
- ? {
- ...t,
- open: false,
- }
- : t
- ),
- }
- }
- case "REMOVE_TOAST":
- if (action.toastId === undefined) {
- return {
- ...state,
- toasts: [],
- }
- }
- return {
- ...state,
- toasts: state.toasts.filter((t) => t.id !== action.toastId),
- }
- }
-}
-
-const listeners: Array<(state: State) => void> = []
-
-let memoryState: State = { toasts: [] }
-
-function dispatch(action: Action) {
- memoryState = reducer(memoryState, action)
- listeners.forEach((listener) => {
- listener(memoryState)
- })
-}
-
-type Toast = Omit<ToasterToast, "id">
-
-function toast({ ...props }: Toast) {
- const id = genId()
-
- const update = (props: ToasterToast) =>
- dispatch({
- type: "UPDATE_TOAST",
- toast: { ...props, id },
- })
- const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
-
- dispatch({
- type: "ADD_TOAST",
- toast: {
- ...props,
- id,
- open: true,
- onOpenChange: (open) => {
- if (!open) dismiss()
- },
- },
- })
-
- return {
- id: id,
- dismiss,
- update,
- }
-}
-
-function useToast() {
- const [state, setState] = React.useState<State>(memoryState)
-
- React.useEffect(() => {
- listeners.push(setState)
- return () => {
- const index = listeners.indexOf(setState)
- if (index > -1) {
- listeners.splice(index, 1)
- }
- }
- }, [state])
-
- return {
- ...state,
- toast,
- dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
- }
-}
-
-export { useToast, toast }
diff --git a/src/lib/auth.ts b/src/lib/auth.ts
@@ -0,0 +1,82 @@
+import { cookies } from "next/headers"
+import type { NextRequest, NextResponse } from "next/server"
+import { jwtVerify, SignJWT } from "jose"
+
+// Secret key for JWT signing - in production, use a proper secret management system
+const JWT_SECRET = new TextEncoder().encode(
+ process.env.JWT_SECRET || "this_is_a_secret_key_that_should_be_in_env_variables",
+)
+
+export type User = {
+ id: string
+ matrixId: string
+ displayName: string
+ avatarUrl?: string
+ isAdmin: boolean
+}
+
+export async function createSession(user: User): Promise<string> {
+ // Create a JWT token with user data
+ const token = await new SignJWT({ user })
+ .setProtectedHeader({ alg: "HS256" })
+ .setIssuedAt()
+ .setExpirationTime("7d") // Token expires in 7 days
+ .sign(JWT_SECRET)
+
+ return token
+}
+
+export async function setSessionCookie(token: string, response: NextResponse): Promise<void> {
+ // Set the session cookie with secure attributes
+ response.cookies.set({
+ name: "matrix_session",
+ value: token,
+ httpOnly: true,
+ secure: process.env.NODE_ENV === "production",
+ sameSite: "lax",
+ maxAge: 60 * 60 * 24 * 7, // 7 days
+ path: "/",
+ })
+}
+
+export async function getSessionUser(request?: NextRequest): Promise<User | null> {
+ try {
+ // Get the session cookie
+ const cookieStore = request ? request.cookies : await cookies()
+ const sessionCookie = cookieStore.get("matrix_session")
+
+ if (!sessionCookie?.value) {
+ return null
+ }
+
+ // Verify the JWT token
+ try {
+ const { payload } = await jwtVerify(sessionCookie.value, JWT_SECRET)
+
+ if (!payload.user) {
+ return null
+ }
+
+ return payload.user as User
+ } catch (jwtError) {
+ console.error("JWT verification failed:", jwtError)
+ return null
+ }
+ } catch (error) {
+ console.error("Failed to verify session:", error)
+ return null
+ }
+}
+
+export async function clearSession(response: NextResponse): Promise<void> {
+ // Clear the session cookie
+ response.cookies.set({
+ name: "matrix_session",
+ value: "",
+ httpOnly: true,
+ secure: process.env.NODE_ENV === "production",
+ sameSite: "lax",
+ maxAge: 0,
+ path: "/",
+ })
+}
diff --git a/src/middleware.ts b/src/middleware.ts
@@ -0,0 +1,34 @@
+import { NextResponse } from "next/server"
+import type { NextRequest } from "next/server"
+import { getSessionUser } from "./lib/auth"
+
+export async function middleware(request: NextRequest) {
+ // Get the pathname of the request
+ const path = request.nextUrl.pathname
+
+ // Define public paths that don't require authentication
+ const isPublicPath = path === "/" || path === "/login" || path === "/register"
+
+ // Check if the user is authenticated
+ const user = await getSessionUser(request)
+ const isAuthenticated = !!user
+
+ // If the path is dashboard and the user is not authenticated, redirect to login
+ if (path.startsWith("/dashboard") && !isAuthenticated) {
+ const url = new URL("/login", request.url)
+ return NextResponse.redirect(url)
+ }
+
+ // If the user is authenticated and trying to access login/register, redirect to dashboard
+ if (isAuthenticated && isPublicPath) {
+ const url = new URL("/dashboard", request.url)
+ return NextResponse.redirect(url)
+ }
+
+ return NextResponse.next()
+}
+
+// Configure the middleware to run only on specific paths
+export const config = {
+ matcher: ["/", "/login", "/register", "/dashboard/:path*"],
+}
diff --git a/tailwind.config.ts b/tailwind.config.ts
@@ -19,6 +19,10 @@ const config = {
},
},
extend: {
+ fontFamily: {
+ sans: ['var(--font-inter)'],
+ mono: ['var(--font-roboto-mono)'],
+ },
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",