diff --git a/__tests__/admin/AdminSpacesPage.test.jsx b/__tests__/admin/AdminSpacesPage.test.jsx
new file mode 100644
index 00000000..76384977
--- /dev/null
+++ b/__tests__/admin/AdminSpacesPage.test.jsx
@@ -0,0 +1,98 @@
+/**
+ * Admin Spaces Oversight page — loading / data / filter tests (#270).
+ * -------------------------------------------------------------------
+ * The page renders a table of community spaces with title, host, type,
+ * scheduled time, status, participant count, and flags. Live rooms are
+ * sorted to the top with a pulsing status dot. Filters by host and
+ * status are available. Em-dash is rendered for unknown participant
+ * counts instead of zeros.
+ */
+import { describe, it, expect, vi, beforeEach } from "vitest";
+import { render, screen, within } from "@testing-library/react";
+
+vi.mock("@/lib/config/font.config", () => ({
+ poppins_400: { className: "" },
+ poppins_500: { className: "" },
+ poppins_600: { className: "" },
+}));
+
+vi.mock("@/components/ui/empty-state", () => ({
+ EmptyState: ({ title, description }) => (
+
+
{title}
+
{description}
+
+ ),
+}));
+
+let AdminSpacesPage;
+beforeEach(async () => {
+ if (!AdminSpacesPage) {
+ const mod = await import("@/app/[locale]/admin/spaces/page");
+ AdminSpacesPage = mod.default;
+ }
+});
+
+describe("AdminSpacesPage — states", () => {
+ it("renders skeleton placeholders while loading", () => {
+ const { container } = render();
+ expect(screen.getByText("Spaces Oversight")).toBeInTheDocument();
+ expect(
+ container.querySelector('[data-slot="skeleton"]')
+ ).not.toBeNull();
+ });
+
+ it("renders summary stat cards after data loads", async () => {
+ render();
+
+ await screen.findByText("Live Now");
+ expect(screen.getByText("Scheduled")).toBeInTheDocument();
+ expect(screen.getByText("Ended")).toBeInTheDocument();
+ });
+
+ it("renders the spaces table with data after loading", async () => {
+ render();
+
+ await screen.findByText("Quran Study Circle");
+ expect(screen.getByText("Youth Halaqah")).toBeInTheDocument();
+ expect(screen.getByText("Islamic History Discussion")).toBeInTheDocument();
+ });
+
+ it("renders pulsing status dot for live rooms", async () => {
+ render();
+ await screen.findByText("Quran Study Circle");
+
+ expect(screen.getAllByLabelText("Live").length).toBeGreaterThanOrEqual(1);
+ });
+
+ it("renders em-dash for unknown participant counts", async () => {
+ render();
+ await screen.findByText("Quran Study Circle");
+
+ const dashes = screen.getAllByText("—");
+ expect(dashes.length).toBeGreaterThanOrEqual(1);
+ });
+
+ it("renders participant count for live rooms", async () => {
+ render();
+ await screen.findByText("Quran Study Circle");
+
+ expect(screen.getByText("24")).toBeInTheDocument();
+ });
+
+ it("renders Jitsi badge for room type", async () => {
+ render();
+ await screen.findByText("Quran Study Circle");
+
+ const jitsiBadges = screen.getAllByText("Jitsi");
+ expect(jitsiBadges.length).toBeGreaterThanOrEqual(1);
+ });
+
+ it("renders filter controls", async () => {
+ render();
+ await screen.findByText("Quran Study Circle");
+
+ expect(screen.getByLabelText("Status")).toBeInTheDocument();
+ expect(screen.getByLabelText("Host")).toBeInTheDocument();
+ });
+});
diff --git a/app/[locale]/admin/spaces/page.jsx b/app/[locale]/admin/spaces/page.jsx
new file mode 100644
index 00000000..8eba992d
--- /dev/null
+++ b/app/[locale]/admin/spaces/page.jsx
@@ -0,0 +1,576 @@
+"use client";
+
+/**
+ * Admin Spaces Oversight (#270)
+ *
+ * Comprehensive overview of all live and scheduled community spaces.
+ * - Table with title, host, type, scheduled time, status, participant count, flags
+ * - Live rooms sorted to top with pulsing status dot
+ * - Filters by host and status
+ * - Em-dash for unknown participant counts
+ * - Jitsi room type display
+ */
+
+import { useState, useMemo, useCallback, useEffect } from "react";
+import { PageShell } from "@/components/ui/page-shell";
+import { PageHeader } from "@/components/ui/page-header";
+import {
+ Card,
+ CardHeader,
+ CardTitle,
+ CardDescription,
+ CardContent,
+} 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 {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+import { Skeleton } from "@/components/ui/skeleton";
+import { EmptyState } from "@/components/ui/empty-state";
+import {
+ Video,
+ Users,
+ Calendar,
+ Radio,
+ Clock,
+ AlertTriangle,
+ RefreshCw,
+ Search,
+ Flag,
+ AudioWaveform,
+} from "lucide-react";
+import { cn } from "@/lib/utils";
+import { poppins_400, poppins_500, poppins_600 } from "@/lib/config/font.config";
+import { format, formatDistanceToNow } from "date-fns";
+
+// ─── Mock data ──────────────────────────────────────────────────────────────
+
+const MOCK_SPACES = [
+ {
+ id: "sp_001",
+ title: "Quran Study Circle",
+ host: { name: "Sheikh Ahmad", avatar: null },
+ type: "jitsi",
+ scheduledTime: new Date().toISOString(), // live now
+ status: "live",
+ participantCount: 24,
+ flags: [],
+ },
+ {
+ id: "sp_002",
+ title: "Youth Halaqah",
+ host: { name: "Ustadh Ibrahim", avatar: null },
+ type: "jitsi",
+ scheduledTime: new Date(Date.now() + 3600000).toISOString(), // 1 hour from now
+ status: "scheduled",
+ participantCount: null,
+ flags: [],
+ },
+ {
+ id: "sp_003",
+ title: "Islamic History Discussion",
+ host: { name: "Dr. Fatima", avatar: null },
+ type: "jitsi",
+ scheduledTime: new Date(Date.now() - 7200000).toISOString(), // 2 hours ago
+ status: "ended",
+ participantCount: null,
+ flags: [],
+ },
+ {
+ id: "sp_004",
+ title: "Arabic Language Workshop",
+ host: { name: "Sheikh Ahmad", avatar: null },
+ type: "jitsi",
+ scheduledTime: new Date(Date.now() + 86400000).toISOString(), // tomorrow
+ status: "scheduled",
+ participantCount: null,
+ flags: ["flagged"],
+ },
+ {
+ id: "sp_005",
+ title: "Tafsir Session",
+ host: { name: "Dr. Yusuf", avatar: null },
+ type: "jitsi",
+ scheduledTime: new Date().toISOString(), // live now
+ status: "live",
+ participantCount: 42,
+ flags: [],
+ },
+ {
+ id: "sp_006",
+ title: "Seerah Study Group",
+ host: { name: "Sister Maryam", avatar: null },
+ type: "jitsi",
+ scheduledTime: new Date(Date.now() + 172800000).toISOString(), // 2 days from now
+ status: "scheduled",
+ participantCount: null,
+ flags: [],
+ },
+ {
+ id: "sp_007",
+ title: "Fiqh Q&A",
+ host: { name: "Sheikh Omar", avatar: null },
+ type: "jitsi",
+ scheduledTime: new Date(Date.now() - 3600000).toISOString(), // 1 hour ago
+ status: "ended",
+ participantCount: null,
+ flags: [],
+ },
+ {
+ id: "sp_008",
+ title: "Hadith Sciences",
+ host: { name: "Dr. Fatima", avatar: null },
+ type: "jitsi",
+ scheduledTime: new Date().toISOString(), // live now
+ status: "live",
+ participantCount: 15,
+ flags: [],
+ },
+ {
+ id: "sp_009",
+ title: "Quran Recitation Circle",
+ host: { name: "Ustadh Ibrahim", avatar: null },
+ type: "jitsi",
+ scheduledTime: new Date(Date.now() + 43200000).toISOString(), // 12 hours from now
+ status: "scheduled",
+ participantCount: null,
+ flags: [],
+ },
+ {
+ id: "sp_010",
+ title: "Community Town Hall",
+ host: { name: "Admin Team", avatar: null },
+ type: "jitsi",
+ scheduledTime: new Date(Date.now() - 1800000).toISOString(), // 30 min ago
+ status: "ended",
+ participantCount: null,
+ flags: ["flagged"],
+ },
+];
+
+// ─── Helpers ────────────────────────────────────────────────────────────────
+
+const STATUS_CONFIG = {
+ live: {
+ label: "Live",
+ color: "text-green-600",
+ bg: "bg-green-100 dark:bg-green-900/30",
+ badge: "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400",
+ },
+ scheduled: {
+ label: "Scheduled",
+ color: "text-blue-600",
+ bg: "bg-blue-100 dark:bg-blue-900/30",
+ badge: "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400",
+ },
+ ended: {
+ label: "Ended",
+ color: "text-muted-foreground",
+ bg: "bg-muted",
+ badge: "bg-muted text-muted-foreground",
+ },
+};
+
+function StatusDot({ status }) {
+ if (status !== "live") return null;
+ return (
+
+
+
+
+ );
+}
+
+function formatScheduledTime(isoString) {
+ const date = new Date(isoString);
+ return format(date, "MMM d, yyyy · h:mm a");
+}
+
+function formatRelativeTime(isoString) {
+ return formatDistanceToNow(new Date(isoString), { addSuffix: true });
+}
+
+function sortSpacesByStatus(spaces) {
+ const order = { live: 0, scheduled: 1, ended: 2 };
+ return [...spaces].sort((a, b) => order[a.status] - order[b.status]);
+}
+
+// ─── Page Component ─────────────────────────────────────────────────────────
+
+export default function AdminSpacesPage() {
+ const [spaces, setSpaces] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [hostFilter, setHostFilter] = useState("all");
+ const [statusFilter, setStatusFilter] = useState("all");
+
+ const fetchSpaces = useCallback(async () => {
+ setLoading(true);
+ await new Promise((r) => setTimeout(r, 400));
+ setSpaces(MOCK_SPACES);
+ setLoading(false);
+ }, []);
+
+ useEffect(() => {
+ fetchSpaces();
+ }, [fetchSpaces]);
+
+ // Unique hosts for filter
+ const hosts = useMemo(() => {
+ const seen = new Set();
+ return spaces
+ .map((s) => s.host.name)
+ .filter((name) => {
+ if (seen.has(name)) return false;
+ seen.add(name);
+ return true;
+ })
+ .sort();
+ }, [spaces]);
+
+ // Filtered + sorted spaces
+ const filteredSpaces = useMemo(() => {
+ let result = [...spaces];
+
+ if (hostFilter !== "all") {
+ result = result.filter((s) => s.host.name === hostFilter);
+ }
+ if (statusFilter !== "all") {
+ result = result.filter((s) => s.status === statusFilter);
+ }
+
+ return sortSpacesByStatus(result);
+ }, [spaces, hostFilter, statusFilter]);
+
+ // Summary counts
+ const counts = useMemo(() => {
+ const c = { live: 0, scheduled: 0, ended: 0 };
+ spaces.forEach((s) => {
+ if (c[s.status] !== undefined) c[s.status]++;
+ });
+ return c;
+ }, [spaces]);
+
+ return (
+
+
+
+ Refresh
+
+ }
+ />
+
+ {/* Summary Stats */}
+
+
+
+
+
+
+ Live Now
+
+
+ {loading ? "—" : counts.live}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Scheduled
+
+
+ {loading ? "—" : counts.scheduled}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Ended
+
+
+ {loading ? "—" : counts.ended}
+
+
+
+
+
+
+
+
+
+
+ {/* Filters */}
+
+
+
+
+
+
+ Filters:
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {(hostFilter !== "all" || statusFilter !== "all") && (
+
+ )}
+
+
+
+
+ {/* Spaces Table */}
+
+
+
+
+ Community Spaces
+
+
+ {filteredSpaces.length} space
+ {filteredSpaces.length !== 1 && "s"} · Live rooms shown first
+
+
+
+ {loading ? (
+
+ {Array.from({ length: 5 }).map((_, i) => (
+
+ ))}
+
+ ) : filteredSpaces.length === 0 ? (
+
+ ) : (
+
+
+
+
+ Title
+ Host
+ Type
+ Scheduled Time
+ Status
+
+ Participants
+
+ Flags
+
+
+
+ {filteredSpaces.map((space) => {
+ const statusConfig = STATUS_CONFIG[space.status];
+ return (
+
+ {/* Title */}
+
+
+
+
+
+ {space.title}
+
+ {space.status === "scheduled" && (
+
+ {formatRelativeTime(space.scheduledTime)}
+
+ )}
+
+
+
+
+ {/* Host */}
+
+ {space.host.name}
+
+
+ {/* Type */}
+
+
+
+ Jitsi
+
+
+
+ {/* Scheduled Time */}
+
+
+
+ {formatScheduledTime(space.scheduledTime)}
+
+
+
+ {/* Status */}
+
+
+ {statusConfig.label}
+
+
+
+ {/* Participant Count */}
+
+ {space.participantCount !== null ? (
+
+
+
+ {space.participantCount}
+
+
+ ) : (
+ —
+ )}
+
+
+ {/* Flags */}
+
+ {space.flags.includes("flagged") ? (
+
+
+
+ ) : (
+ —
+ )}
+
+
+ );
+ })}
+
+
+
+ )}
+
+
+
+ );
+}