Add Oudelaa dashboard API integration
فشلت بعض الفحوصات
Deploy To Ghaymah / deploy (push) Has been cancelled

هذا الالتزام موجود في:
boutmoun123
2026-05-25 20:36:52 +03:00
الأصل 367fce6557
التزام 8863f61d00
90 ملفات معدلة مع 16694 إضافات و1 حذوفات

عرض الملف

@@ -0,0 +1,76 @@
"use client";
import {
Area,
AreaChart,
Bar,
BarChart,
CartesianGrid,
Cell,
Legend,
Pie,
PieChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import type { ChannelPoint, Insight, RevenuePoint } from "@/types";
const pieColors = ["hsl(var(--chart-1))", "hsl(var(--chart-2))", "hsl(var(--chart-3))", "hsl(var(--chart-4))"];
export function RevenueAreaChart({ data }: { data: RevenuePoint[] }) {
return (
<div className="h-72 w-full">
<ResponsiveContainer>
<AreaChart data={data}>
<CartesianGrid stroke="rgba(180,150,110,0.14)" vertical={false} />
<XAxis dataKey="month" tick={{ fill: "#c6b698", fontSize: 12 }} axisLine={false} tickLine={false} />
<YAxis tick={{ fill: "#a79577", fontSize: 12 }} axisLine={false} tickLine={false} />
<Tooltip
cursor={{ stroke: "rgba(214,170,100,0.35)" }}
contentStyle={{ background: "#20160f", border: "1px solid #5e4732", borderRadius: "12px" }}
/>
<Legend />
<Area type="monotone" dataKey="revenue" name="الإيراد" stroke="hsl(var(--chart-1))" fill="hsl(var(--chart-1))" fillOpacity={0.2} strokeWidth={2.5} />
<Area type="monotone" dataKey="orders" name="الطلبات" stroke="hsl(var(--chart-2))" fill="hsl(var(--chart-2))" fillOpacity={0.16} strokeWidth={2} />
</AreaChart>
</ResponsiveContainer>
</div>
);
}
export function ChannelPieChart({ data }: { data: ChannelPoint[] }) {
return (
<div className="h-72 w-full">
<ResponsiveContainer>
<PieChart>
<Tooltip contentStyle={{ background: "#20160f", border: "1px solid #5e4732", borderRadius: "12px" }} />
<Pie data={data} dataKey="value" nameKey="name" innerRadius={58} outerRadius={94} paddingAngle={4}>
{data.map((entry, index) => (
<Cell key={entry.name} fill={pieColors[index % pieColors.length]} />
))}
</Pie>
<Legend />
</PieChart>
</ResponsiveContainer>
</div>
);
}
export function InsightBarChart({ data }: { data: Insight[] }) {
return (
<div className="h-72 w-full">
<ResponsiveContainer>
<BarChart data={data}>
<CartesianGrid stroke="rgba(180,150,110,0.14)" vertical={false} />
<XAxis dataKey="label" tick={{ fill: "#c6b698", fontSize: 12 }} axisLine={false} tickLine={false} />
<YAxis tick={{ fill: "#a79577", fontSize: 12 }} axisLine={false} tickLine={false} />
<Tooltip contentStyle={{ background: "#20160f", border: "1px solid #5e4732", borderRadius: "12px" }} />
<Bar dataKey="value" radius={[8, 8, 0, 0]} fill="hsl(var(--chart-1))" />
</BarChart>
</ResponsiveContainer>
</div>
);
}

عرض الملف

@@ -0,0 +1,42 @@
"use client";
import Link from "next/link";
import { useMemo } from "react";
import { usePathname } from "next/navigation";
import { useSuperAdminSession } from "@/components/auth/session-context";
import { dashboardNav } from "@/lib/navigation";
import { matchesPermissions } from "@/lib/permissions";
import { cn } from "@/lib/utils";
export function MobileNav() {
const pathname = usePathname();
const { permissions } = useSuperAdminSession();
const accessibleNav = useMemo(
() =>
dashboardNav.filter((item) =>
matchesPermissions(permissions, item.requiredPermissions, item.permissionMode),
),
[permissions],
);
return (
<div className="mb-4 flex gap-2 overflow-x-auto pb-1 lg:hidden">
{accessibleNav.map((item) => {
const active = pathname === item.href;
return (
<Link
key={item.href}
href={item.href}
className={cn(
"whitespace-nowrap rounded-full border px-3 py-1.5 text-xs",
active ? "border-primary/40 bg-primary/20 text-primary" : "border-border bg-card text-muted-foreground",
)}
>
{item.label}
</Link>
);
})}
</div>
);
}

عرض الملف

@@ -0,0 +1,22 @@
import type { ReactNode } from "react";
import { cn } from "@/lib/utils";
type PageHeaderProps = {
title: string;
subtitle: string;
actions?: ReactNode;
className?: string;
};
export function PageHeader({ title, subtitle, actions, className }: PageHeaderProps) {
return (
<header className={cn("page-enter flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between", className)}>
<div>
<h1 className="font-heading text-2xl font-bold tracking-tight text-foreground sm:text-3xl">{title}</h1>
<p className="mt-1 text-sm text-muted-foreground">{subtitle}</p>
</div>
{actions ? <div className="flex items-center gap-2">{actions}</div> : null}
</header>
);
}

عرض الملف

@@ -0,0 +1,49 @@
"use client";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { Button } from "@/components/ui/button";
import type { PaginationMeta } from "@/types/api";
type PaginationControlsProps = {
pagination?: PaginationMeta | null;
loading?: boolean;
onPageChange: (page: number) => void;
};
export function PaginationControls({ pagination, loading, onPageChange }: PaginationControlsProps) {
if (!pagination) return null;
const currentPage = pagination.page ?? 1;
const totalPages = pagination.totalPages ?? 1;
const previousPage = pagination.previousPage ?? (currentPage > 1 ? currentPage - 1 : null);
const nextPage = pagination.nextPage ?? (currentPage < totalPages ? currentPage + 1 : null);
return (
<div className="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-border/70 bg-card/60 p-3">
<div className="text-sm text-muted-foreground">
Page {currentPage} of {totalPages} · Total {pagination.total}
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
disabled={loading || !previousPage}
onClick={() => previousPage && onPageChange(previousPage)}
>
<ChevronRight className="h-4 w-4" />
Previous
</Button>
<Button
variant="outline"
size="sm"
disabled={loading || !nextPage}
onClick={() => nextPage && onPageChange(nextPage)}
>
Next
<ChevronLeft className="h-4 w-4" />
</Button>
</div>
</div>
);
}

عرض الملف

@@ -0,0 +1,157 @@
"use client";
import Image from "next/image";
import { useEffect, useState } from "react";
import { AudioLines, Images, PlayCircle } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent } from "@/components/ui/card";
import { formatDateTime } from "@/lib/format";
import { getPostAuthor, getPostPreviewMedia, getUserLabel } from "@/lib/post-utils";
import type { ApiPost } from "@/types/api";
function formatDuration(durationSeconds?: number | null) {
if (typeof durationSeconds !== "number" || !Number.isFinite(durationSeconds) || durationSeconds <= 0) {
return null;
}
const minutes = Math.floor(durationSeconds / 60);
const seconds = Math.floor(durationSeconds % 60);
return `${minutes}:${seconds.toString().padStart(2, "0")}`;
}
export function PostPreviewCard({ post }: { post: ApiPost }) {
const author = getPostAuthor(post);
const media = getPostPreviewMedia(post);
const likes = post.engagement?.likesCount ?? post.likesCount ?? 0;
const comments = post.engagement?.commentsCount ?? post.commentsCount ?? 0;
const shares = post.engagement?.shareCount ?? post.shareCount ?? 0;
const waveformPeaks = Array.isArray(post.waveformPeaks) && post.waveformPeaks.length ? post.waveformPeaks : [18, 32, 24, 44, 28, 40, 22, 36, 26, 30];
const durationLabel = formatDuration(post.durationSeconds);
const [imageFailed, setImageFailed] = useState(false);
const [sourceFailed, setSourceFailed] = useState(false);
useEffect(() => {
setImageFailed(false);
setSourceFailed(false);
}, [media.url, media.sourceUrl]);
const showAudioPreview = media.kind === "audio" && !!media.sourceUrl && !sourceFailed;
const showVideoPreview = media.kind === "video" && !!media.sourceUrl && !sourceFailed && (!media.url || imageFailed);
const showImagePreview = !!media.url && !imageFailed;
const showMediaShell = media.kind !== "text";
const showUnavailable = !showImagePreview && !showVideoPreview && !showAudioPreview;
return (
<Card className="overflow-hidden border-border/70">
<CardContent className="p-0">
{showMediaShell ? (
<div className="relative aspect-[16/9] border-b border-border/70 bg-secondary/30">
{showImagePreview ? (
<Image
src={media.url}
alt={post.content || post.postType || "post"}
fill
unoptimized
loading="lazy"
onError={() => setImageFailed(true)}
className="object-cover"
/>
) : null}
{showVideoPreview ? (
<video
src={media.sourceUrl}
preload="metadata"
muted
playsInline
onError={() => setSourceFailed(true)}
className="h-full w-full object-cover"
/>
) : null}
{showAudioPreview ? (
<div className="absolute inset-0 flex flex-col justify-between bg-gradient-to-br from-secondary/70 via-background/60 to-secondary/80 p-5">
<div className="flex items-center justify-between gap-3 text-sm text-foreground">
<div className="inline-flex items-center gap-2 rounded-full border border-border/60 bg-background/70 px-3 py-1">
<AudioLines className="h-4 w-4" />
<span>Audio preview</span>
</div>
{durationLabel ? <span className="text-xs text-muted-foreground">{durationLabel}</span> : null}
</div>
<div className="flex h-24 items-end justify-center gap-1.5 px-2">
{waveformPeaks.slice(0, 48).map((peak, index) => {
const safePeak = Number.isFinite(peak) ? Math.max(8, Math.min(100, peak)) : 20;
return (
<span
key={`${post._id}-peak-${index}`}
className="w-1.5 rounded-full bg-primary/80"
style={{ height: `${safePeak}%` }}
/>
);
})}
</div>
<audio
src={media.sourceUrl}
preload="metadata"
controls
onError={() => setSourceFailed(true)}
className="w-full"
/>
</div>
) : null}
{showUnavailable ? (
<div className="absolute inset-0 flex items-center justify-center px-4 text-center text-sm text-muted-foreground">
Media preview unavailable
</div>
) : null}
<div className="absolute left-3 top-3 flex items-center gap-2">
<Badge variant="warning">{post.postType ?? "post"}</Badge>
{media.kind === "image" && media.count > 1 ? (
<Badge variant="muted">
<Images className="mr-1 h-3 w-3" />
{media.count}
</Badge>
) : null}
</div>
<div className="absolute bottom-3 right-3 rounded-full border border-border/60 bg-background/85 p-2 text-foreground">
{media.kind === "audio" ? <AudioLines className="h-4 w-4" /> : <PlayCircle className="h-4 w-4" />}
</div>
</div>
) : null}
<div className="space-y-3 p-4">
<div className="flex flex-wrap items-center gap-2">
<Badge variant="muted">{post.visibility ?? "public"}</Badge>
<Badge
variant={
post.moderationStatus === "hidden"
? "danger"
: post.moderationStatus === "flagged"
? "warning"
: "success"
}
>
{post.moderationStatus ?? "active"}
</Badge>
</div>
<div>
<div className="text-sm font-semibold text-foreground">{getUserLabel(author)}</div>
<div className="mt-1 text-xs text-muted-foreground">{formatDateTime(post.createdAt)}</div>
</div>
<p className="line-clamp-4 text-sm leading-6 text-foreground">
{post.content?.trim() || "Media post without caption."}
</p>
<div className="flex flex-wrap gap-2 text-xs text-muted-foreground">
<span>{likes} likes</span>
<span>{comments} comments</span>
<span>{shares} shares</span>
</div>
</div>
</CardContent>
</Card>
);
}

عرض الملف

@@ -0,0 +1,5 @@
import type { ReactNode } from "react";
export function SectionGrid({ children }: { children: ReactNode }) {
return <section className="grid gap-4 xl:grid-cols-12">{children}</section>;
}

عرض الملف

@@ -0,0 +1,84 @@
"use client";
import Link from "next/link";
import { useMemo, useState } from "react";
import { usePathname } from "next/navigation";
import { Search, ShieldCheck } from "lucide-react";
import { useSuperAdminSession } from "@/components/auth/session-context";
import { Input } from "@/components/ui/input";
import { dashboardNav } from "@/lib/navigation";
import { matchesPermissions } from "@/lib/permissions";
import { cn } from "@/lib/utils";
export function DashboardSidebar() {
const pathname = usePathname();
const [search, setSearch] = useState("");
const { permissions } = useSuperAdminSession();
const accessibleNav = useMemo(
() =>
dashboardNav.filter((item) =>
matchesPermissions(permissions, item.requiredPermissions, item.permissionMode),
),
[permissions],
);
const filteredNav = useMemo(() => {
const needle = search.trim().toLowerCase();
if (!needle) return accessibleNav;
return accessibleNav.filter((item) => item.label.toLowerCase().includes(needle));
}, [accessibleNav, search]);
return (
<aside className="frame-panel ornament-grid sticky top-4 hidden h-[calc(100vh-2rem)] w-72 shrink-0 flex-col border-border/70 p-4 lg:flex">
<div className="mb-5 rounded-xl border border-border/60 bg-background/70 p-4">
<p className="font-heading text-xl font-bold tracking-wide text-primary">Oudelaa</p>
<p className="text-xs text-muted-foreground">SuperAdmin Command Console</p>
</div>
<div className="relative mb-4">
<Search className="pointer-events-none absolute right-3 top-3 h-4 w-4 text-muted-foreground" />
<Input
className="pr-9"
placeholder="Search sections..."
value={search}
onChange={(event) => setSearch(event.target.value)}
/>
</div>
<nav className="space-y-1">
{filteredNav.map((item) => {
const isActive = pathname === item.href;
return (
<Link
key={item.href}
href={item.href}
className={cn(
"group flex items-center gap-3 rounded-lg border px-3 py-2 text-sm transition",
isActive
? "border-primary/40 bg-primary/15 text-primary"
: "border-transparent text-muted-foreground hover:border-border hover:bg-secondary/70 hover:text-foreground",
)}
>
<item.icon className="h-4 w-4" />
<span>{item.label}</span>
</Link>
);
})}
</nav>
<div className="mt-auto space-y-3 rounded-xl border border-border/70 bg-secondary/40 p-4">
<div className="flex items-center justify-between text-xs text-muted-foreground">
<span>Moderation mode</span>
<ShieldCheck className="h-4 w-4" />
</div>
<p className="text-sm text-foreground">
This console manages users, marketplace items, content, reports, and SuperAdmin sessions
through dedicated admin API routes.
</p>
<p className="text-xs text-muted-foreground">Data is loaded from the live API, not mock data.</p>
</div>
</aside>
);
}

عرض الملف

@@ -0,0 +1,31 @@
import { ArrowDownLeft, ArrowUpRight, Minus } from "lucide-react";
import type { StatMetric } from "@/types";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { cn } from "@/lib/utils";
const trendStyles = {
up: "text-emerald-300",
down: "text-rose-300",
neutral: "text-amber-200",
} as const;
export function StatCard({ metric }: { metric: StatMetric }) {
const Icon = metric.trend === "up" ? ArrowUpRight : metric.trend === "down" ? ArrowDownLeft : Minus;
return (
<Card className="ornament-grid">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-semibold text-muted-foreground">{metric.label}</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-foreground">{metric.value}</div>
<p className={cn("mt-1 flex items-center gap-1 text-xs", trendStyles[metric.trend])}>
<Icon className="h-3.5 w-3.5" />
{metric.delta}
</p>
{metric.note ? <p className="mt-2 text-xs text-muted-foreground">{metric.note}</p> : null}
</CardContent>
</Card>
);
}

عرض الملف

@@ -0,0 +1,276 @@
"use client";
import Link from "next/link";
import { useCallback, useEffect, useMemo, useState } from "react";
import { Bell, MoonStar, RefreshCcw, ShieldCheck, SunMedium } from "lucide-react";
import { useSuperAdminSession } from "@/components/auth/session-context";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Drawer } from "@/components/ui/drawer";
import { useToast } from "@/components/ui/toast";
import { useTheme } from "@/components/theme/theme-provider";
import { listSuperAdminSessions } from "@/lib/api/auth";
import { listPlatformNotifications } from "@/lib/api/notifications";
import { getSuperAdminOverview, getSuperAdminRecentActivity } from "@/lib/api/superadmin";
import { formatDateTime } from "@/lib/format";
import { SUPERADMIN_PERMISSIONS, hasPermission } from "@/lib/permissions";
import type {
NotificationItem,
NotificationsResponse,
SessionItem,
SessionsResponse,
SuperAdminOverviewResponse,
SuperAdminRecentActivityItem,
SuperAdminRecentActivityResponse,
} from "@/types/api";
const EMPTY_NOTIFICATIONS: NotificationsResponse = { items: [], data: [], unreadCount: 0 };
const EMPTY_SESSIONS: SessionsResponse = { items: [] };
const EMPTY_ACTIVITY: SuperAdminRecentActivityResponse = { items: [] };
export function DashboardTopbar() {
const { toast } = useToast();
const { theme, toggle } = useTheme();
const { permissions } = useSuperAdminSession();
const [drawerOpen, setDrawerOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [overview, setOverview] = useState<SuperAdminOverviewResponse | null>(null);
const [recentActivity, setRecentActivity] = useState<SuperAdminRecentActivityItem[]>([]);
const [notifications, setNotifications] = useState<NotificationItem[]>([]);
const [sessions, setSessions] = useState<SessionItem[]>([]);
const canReadOverview = hasPermission(permissions, SUPERADMIN_PERMISSIONS.OVERVIEW_READ);
const canReadAnalytics = hasPermission(permissions, SUPERADMIN_PERMISSIONS.ANALYTICS_READ);
const canReadNotifications = hasPermission(permissions, SUPERADMIN_PERMISSIONS.NOTIFICATIONS_READ);
const canManageSessions = hasPermission(permissions, SUPERADMIN_PERMISSIONS.SESSIONS_MANAGE);
const todayLabel = useMemo(() => {
try {
return new Intl.DateTimeFormat("ar-SA", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
}).format(new Date());
} catch {
return "Today";
}
}, []);
const unreadCount =
overview?.metrics.unreadNotificationsCount ??
notifications.filter((item) => item.read === false).length;
const moderationAttentionCount =
(overview?.metrics.flaggedPostsCount ?? 0) + (overview?.metrics.flaggedCommentsCount ?? 0);
const summaryCards = [
canManageSessions
? {
key: "sessions",
label: "SuperAdmin sessions",
value: String(sessions.length),
}
: null,
canReadNotifications
? {
key: "notifications",
label: "Unread notifications",
value: String(unreadCount),
}
: null,
canReadOverview
? {
key: "moderation",
label: "Needs review",
value: String(moderationAttentionCount),
}
: null,
].filter(Boolean) as Array<{ key: string; label: string; value: string }>;
const summaryGridClass =
summaryCards.length >= 3
? "grid gap-3 sm:grid-cols-3"
: summaryCards.length === 2
? "grid gap-3 sm:grid-cols-2"
: "grid gap-3";
const loadOverview = useCallback(async () => {
setLoading(true);
try {
const [overviewResponse, activityResponse, notificationsResponse, sessionsResponse] =
await Promise.all([
canReadOverview ? getSuperAdminOverview() : Promise.resolve(null),
canReadAnalytics
? getSuperAdminRecentActivity({ limit: 5 })
: Promise.resolve(EMPTY_ACTIVITY),
canReadNotifications
? listPlatformNotifications({ limit: 5, sortOrder: "desc" })
: Promise.resolve(EMPTY_NOTIFICATIONS),
canManageSessions ? listSuperAdminSessions() : Promise.resolve(EMPTY_SESSIONS),
]);
setOverview(overviewResponse);
setRecentActivity(activityResponse.items ?? []);
setNotifications(notificationsResponse.items ?? notificationsResponse.data ?? []);
setSessions(sessionsResponse.items ?? []);
} catch (error) {
toast({ title: "Failed to load command center", description: String(error), variant: "danger" });
} finally {
setLoading(false);
}
}, [canManageSessions, canReadAnalytics, canReadNotifications, canReadOverview, toast]);
useEffect(() => {
void loadOverview();
}, [loadOverview]);
return (
<>
<div className="page-enter mb-5 flex flex-wrap items-center justify-between gap-3 rounded-xl border border-border/70 bg-card/80 p-3 backdrop-blur">
<div className="flex items-center gap-2">
<Badge variant="warning">SuperAdmin</Badge>
<p className="text-sm text-muted-foreground">{todayLabel}</p>
</div>
<div className="flex items-center gap-2">
<Button variant="ghost" size="icon" onClick={() => void loadOverview()} disabled={loading}>
<RefreshCcw className={`h-4 w-4 ${loading ? "animate-spin" : ""}`} />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => {
toggle();
toast({
title: "Theme updated",
description: theme === "dark" ? "Switched to light mode." : "Switched to dark mode.",
});
}}
>
{theme === "dark" ? <SunMedium className="h-4 w-4" /> : <MoonStar className="h-4 w-4" />}
</Button>
<Button variant="ghost" size="icon" onClick={() => setDrawerOpen(true)}>
<Bell className="h-4 w-4" />
</Button>
</div>
</div>
<Drawer
open={drawerOpen}
onClose={() => setDrawerOpen(false)}
title="Operations Center"
description="A quick view of the latest notifications, activity, and active sessions."
side="right"
>
<div className="space-y-4">
{summaryCards.length ? (
<div className={summaryGridClass}>
{summaryCards.map((card) => (
<div key={card.key} className="rounded-xl border border-border bg-secondary/40 p-4">
<div className="text-xs text-muted-foreground">{card.label}</div>
<div className="mt-2 text-2xl font-bold text-foreground">{card.value}</div>
</div>
))}
</div>
) : null}
{canReadNotifications ? (
<div className="rounded-xl border border-border bg-secondary/40 p-4">
<div className="mb-3 flex items-center justify-between">
<p className="text-sm font-semibold text-foreground">Latest notifications</p>
<Link href="/notifications" className="text-xs font-semibold text-primary">
Open page
</Link>
</div>
<div className="space-y-2">
{notifications.length ? (
notifications.map((item) => (
<div key={item._id} className="rounded-lg border border-border/60 bg-background/40 p-3">
<div className="flex items-center justify-between gap-2">
<p className="text-sm font-medium text-foreground">{item.title ?? item.type}</p>
<Badge variant={item.read ? "muted" : "warning"}>
{item.read ? "Read" : "New"}
</Badge>
</div>
<p className="mt-1 text-xs text-muted-foreground">
{item.previewText ?? item.deepLink ?? "-"}
</p>
</div>
))
) : (
<p className="text-sm text-muted-foreground">No recent notifications.</p>
)}
</div>
</div>
) : null}
{canReadAnalytics ? (
<div className="rounded-xl border border-border bg-secondary/40 p-4">
<div className="mb-3 flex items-center justify-between">
<p className="text-sm font-semibold text-foreground">Recent activity</p>
<Link href="/dashboard" className="text-xs font-semibold text-primary">
Dashboard
</Link>
</div>
<div className="space-y-2">
{recentActivity.length ? (
recentActivity.map((item, index) => (
<div
key={`${item.type}-${item.createdAt ?? index}`}
className="rounded-lg border border-border/60 bg-background/40 p-3"
>
<div className="flex items-center justify-between gap-2">
<p className="text-sm font-medium text-foreground">{item.title}</p>
<Badge
variant={
item.status === "flagged"
? "warning"
: item.status === "hidden"
? "danger"
: "muted"
}
>
{item.status}
</Badge>
</div>
<p className="mt-1 text-xs text-muted-foreground">{item.subtitle}</p>
<p className="mt-2 text-xs text-muted-foreground">{formatDateTime(item.createdAt)}</p>
</div>
))
) : (
<p className="text-sm text-muted-foreground">No recent activity.</p>
)}
</div>
</div>
) : null}
{canManageSessions ? (
<div className="rounded-xl border border-border bg-secondary/40 p-4">
<div className="mb-3 flex items-center gap-2">
<ShieldCheck className="h-4 w-4 text-primary" />
<p className="text-sm font-semibold text-foreground">Active sessions</p>
</div>
<div className="space-y-2">
{sessions.length ? (
sessions.map((item) => (
<div
key={item.id ?? item.jti}
className="rounded-lg border border-border/60 bg-background/40 p-3"
>
<div className="text-sm font-medium text-foreground">
{item.id ?? item.jti ?? "session"}
</div>
<div className="mt-1 text-xs text-muted-foreground">
{formatDateTime(item.createdAt)} - {formatDateTime(item.expiresAt)}
</div>
</div>
))
) : (
<p className="text-sm text-muted-foreground">No active sessions to display.</p>
)}
</div>
</div>
) : null}
</div>
</Drawer>
</>
);
}