draupnir4all-web

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

commit 6642ad66811b17217a2cea79b06f2b5319c42f06
parent ae190fad46e1af5baa05ef955e8c3a1e4b116a15
Author: MTRNord <mtrnord1@gmail.com>
Date:   Sat, 26 Apr 2025 15:49:56 +0200

Make dashboard tabs actual pages

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

Diffstat:
Asrc/app/dashboard/analytics/page.tsx | 370+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/app/dashboard/bans/page.tsx | 154+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/app/dashboard/components/analytics-dashboard.tsx | 356-------------------------------------------------------------------------------
Dsrc/app/dashboard/components/bans-pane.tsx | 136-------------------------------------------------------------------------------
Msrc/app/dashboard/components/dashboard-header.tsx | 202++++++++++++++++++++++++++++---------------------------------------------------
Msrc/app/dashboard/components/modals/add-ban.tsx | 2+-
Asrc/app/dashboard/components/modals/create-bot.tsx | 45+++++++++++++++++++++++++++++++++++++++++++++
Asrc/app/dashboard/components/modals/kick-user.tsx | 29+++++++++++++++++++++++++++++
Dsrc/app/dashboard/components/overview-tab.tsx | 124-------------------------------------------------------------------------------
Msrc/app/dashboard/components/protected-rooms-list.tsx | 2+-
Dsrc/app/dashboard/components/reports-tab.tsx | 262-------------------------------------------------------------------------------
Asrc/app/dashboard/components/tab-navigation.tsx | 36++++++++++++++++++++++++++++++++++++
Msrc/app/dashboard/layout.tsx | 79++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Asrc/app/dashboard/mockData.ts | 312+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/app/dashboard/overview/loading.tsx | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/app/dashboard/overview/page.tsx | 143+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/app/dashboard/page.tsx | 312+++----------------------------------------------------------------------------
Asrc/app/dashboard/reports/page.tsx | 277+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/app/dashboard/settings/page.tsx | 246+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/app/dashboard/team-management.tsx | 357-------------------------------------------------------------------------------
Msrc/components/analytics/bar-chart.tsx | 1+
21 files changed, 1862 insertions(+), 1673 deletions(-)

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