-
Community and events
-
- Welcome to Community and Events section of the admin dashboard! Here, you can manage community interactions, events, and engagement activities. This space is designed to help you foster a vibrant community around your platform, ensuring that users have a place to connect, share, and participate in events.
-
+
+
+
+
+
+
+
+
+
+
+
+ {COMMUNITIES.map((c) => (
+
+ ))}
+
>
diff --git a/src/app/dashboard/admin/community-and-events/utills/data.ts b/src/app/dashboard/admin/community-and-events/utills/data.ts
new file mode 100644
index 0000000..ed7cf2d
--- /dev/null
+++ b/src/app/dashboard/admin/community-and-events/utills/data.ts
@@ -0,0 +1,50 @@
+export type Community = {
+ id: number;
+ name: string;
+ visibility: "Public" | "Private";
+ membersLabel: string;
+ sessionsLabel: string;
+ liveNow?: boolean;
+ cover?: string;
+ description: string;
+};
+
+export const COMMUNITIES: Community[] = [
+ {
+ id: 101,
+ name: "Fantasy Enthusiasts",
+ visibility: "Public",
+ membersLabel: "1.5k+ Members",
+ sessionsLabel: "2+ Session a Month",
+ liveNow: true,
+ description:
+ "Vibrant book club dedicated to exploring the vast and magical realms of fantasy literature. From epic sagas and dark fantasy to urban magic and whimsical tales, we delve into all corners of the genre.",
+ },
+ {
+ id: 102,
+ name: "Rustaceans Lab",
+ visibility: "Public",
+ membersLabel: "3.2k+ Members",
+ sessionsLabel: "4 Sessions a Month",
+ description:
+ "Community for Rust learners and professionals focusing on systems programming, performance and safety.",
+ },
+ {
+ id: 103,
+ name: "Creators Hub",
+ visibility: "Private",
+ membersLabel: "820 Members",
+ sessionsLabel: "Weekly Sessions",
+ description:
+ "A private space for content creators to share practices, tools and collaborate on projects.",
+ },
+ {
+ id: 104,
+ name: "AI Readers Club",
+ visibility: "Public",
+ membersLabel: "2.1k+ Members",
+ sessionsLabel: "Bi-weekly",
+ description:
+ "We read and discuss approachable AI/ML books and articles. No PhD required—curiosity welcome.",
+ },
+];
diff --git a/src/app/dashboard/admin/components/admin-sidenavbar.tsx b/src/app/dashboard/admin/components/admin-sidenavbar.tsx
index cb180bf..4e0fe41 100644
--- a/src/app/dashboard/admin/components/admin-sidenavbar.tsx
+++ b/src/app/dashboard/admin/components/admin-sidenavbar.tsx
@@ -1,6 +1,15 @@
"use client";
-import { LayoutDashboard, Users2, FileText, BarChart3, CalendarDays, Bell, MessagesSquare } from 'lucide-react';
+import {
+ LayoutDashboard,
+ Users2,
+ FileText,
+ BarChart3,
+ CalendarDays,
+ Bell,
+ MessagesSquare,
+ User,
+} from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
@@ -17,7 +26,7 @@ export function Sidebar() {
{
icon: Users2,
label: "User Management",
- href: "/dashboard/admin/user-management",
+ href: "/dashboard/admin/user-management",
},
{
icon: FileText,
@@ -45,11 +54,15 @@ export function Sidebar() {
label: "Community and Events",
href: "/dashboard/admin/community-and-events",
},
-
+ {
+ icon: User,
+ label: "Profile",
+ href: "/dashboard/admin/profile",
+ },
];
return (
-
+
C
@@ -63,7 +76,7 @@ export function Sidebar() {
))}
-
diff --git a/src/app/dashboard/admin/layout.tsx b/src/app/dashboard/admin/layout.tsx
index bf55f54..b3c701e 100644
--- a/src/app/dashboard/admin/layout.tsx
+++ b/src/app/dashboard/admin/layout.tsx
@@ -1,11 +1,8 @@
-
-
-
+import { Suspense } from "react";
import type React from "react";
import { Sidebar } from "./components/admin-sidenavbar";
import "@/app/globals.css";
-
export default function RootLayout({
children,
}: {
@@ -13,14 +10,12 @@ export default function RootLayout({
}) {
return (
<>
-
-
+
+
+
>
-
);
-}
\ No newline at end of file
+}
diff --git a/src/app/dashboard/admin/notifications/components/AnnouncementModal.tsx b/src/app/dashboard/admin/notifications/components/AnnouncementModal.tsx
new file mode 100644
index 0000000..f8d3c51
--- /dev/null
+++ b/src/app/dashboard/admin/notifications/components/AnnouncementModal.tsx
@@ -0,0 +1,112 @@
+"use client";
+import React from "react";
+import { useRouter, useSearchParams } from "next/navigation";
+import {
+ ArrowLeft,
+ Bold,
+ Italic,
+ Underline,
+ Image as ImageIcon,
+ Link as LinkIcon,
+ Undo2,
+} from "lucide-react";
+
+export default function AnnouncementModal() {
+ const router = useRouter();
+ const sp = useSearchParams();
+ const open = sp.get("announce") === "1";
+
+ const [title, setTitle] = React.useState("");
+ const [body, setBody] = React.useState("");
+
+ const close = () => {
+ const params = new URLSearchParams(sp.toString());
+ params.delete("announce");
+ router.push(`?${params.toString()}`);
+ };
+
+ // lock scroll while open
+ React.useEffect(() => {
+ if (!open) return;
+ const prev = document.body.style.overflow;
+ document.body.style.overflow = "hidden";
+ return () => {
+ document.body.style.overflow = prev;
+ };
+ }, [open]);
+
+ if (!open) return null;
+
+ return (
+
+
+
+
+
+
+
+
+
+
setTitle(e.target.value)}
+ placeholder="Title"
+ className="w-full border-[1px] border-[#D1D1D1] rounded-lg px-4 py-3 text-xl sm:text-2xl font-semibold outline-none placeholder:text-[#B7BCC2]"
+ />
+
+
+
+
+
+
+
+
+
+
+
+ Cancel
+
+
+ Send
+
+
+
+
+ );
+}
diff --git a/src/app/dashboard/admin/notifications/components/NotificationDetailsModal.tsx b/src/app/dashboard/admin/notifications/components/NotificationDetailsModal.tsx
new file mode 100644
index 0000000..5ca9c23
--- /dev/null
+++ b/src/app/dashboard/admin/notifications/components/NotificationDetailsModal.tsx
@@ -0,0 +1,114 @@
+"use client";
+import React from "react";
+import { useRouter, useSearchParams } from "next/navigation";
+
+import { ChevronLeft } from "lucide-react";
+import { NOTIFICATIONS } from "../utils/dummy_data";
+
+export default function NotificationDetailsModal() {
+ const router = useRouter();
+ const sp = useSearchParams();
+ const id = sp.get("details");
+ const item = NOTIFICATIONS.find((n) => n.id === id);
+
+ const close = () => {
+ const params = new URLSearchParams(sp.toString());
+ params.delete("details");
+ router.push(`?${params.toString()}`);
+ };
+
+ React.useEffect(() => {
+ if (!id) return;
+ const onKey = (e: KeyboardEvent) => {
+ if (e.key === "Escape") close();
+ };
+ window.addEventListener("keydown", onKey);
+ const prev = document.body.style.overflow;
+ document.body.style.overflow = "hidden";
+ return () => {
+ window.removeEventListener("keydown", onKey);
+ document.body.style.overflow = prev;
+ };
+ }, [id]);
+
+ if (!id || !item) return null;
+
+ const date = new Date(item.date);
+ const dateText = date.toLocaleDateString("en-GB", {
+ day: "2-digit",
+ month: "long",
+ year: "numeric",
+ });
+ const timeText = date.toLocaleTimeString([], {
+ hour: "2-digit",
+ minute: "2-digit",
+ });
+
+ const statusStyles =
+ item.status === "Sent"
+ ? "border-[#34A853] bg-[#34A8531A] text-[#34A853]"
+ : item.status === "Pending"
+ ? "border-[#F9A825] bg-[#F9A8251A] text-[#F9A825]"
+ : "border-[#EB5757] bg-[#EB57571A] text-[#EB5757]";
+
+ return (
+
+
+
+
+
+
+
+
+
+ Sent by: {item.email.replace(/(.{2}).*(?=@)/, "$1**")}
+
+
+
+
+ {item.title}
+
+
+ {item.image && (
+
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+
+
+ )}
+
+ {item.body && (
+
{item.body}
+ )}
+
+
+
+
Receiver
+
{item.recipients}
+
+
+
+
+
Status
+
+ {item.status}
+
+
+
+
+
+ );
+}
diff --git a/src/app/dashboard/admin/notifications/components/NotificationFilter.tsx b/src/app/dashboard/admin/notifications/components/NotificationFilter.tsx
new file mode 100644
index 0000000..aaeae89
--- /dev/null
+++ b/src/app/dashboard/admin/notifications/components/NotificationFilter.tsx
@@ -0,0 +1,109 @@
+"use client";
+import { ListFilter } from "lucide-react";
+import React from "react";
+import { useRouter, useSearchParams } from "next/navigation";
+
+const FILTER_OPTIONS = [
+ { key: "week", label: "This Week" },
+ { key: "month", label: "This Month" },
+ { key: "year", label: "This Year" },
+ { key: "all", label: "All Time" },
+];
+
+export default function NotificationFilter() {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+
+ const activeFilter = searchParams.get("filter") || "week";
+ const start = searchParams.get("start") || "";
+ const end = searchParams.get("end") || "";
+
+ const pushWith = (next: Record
) => {
+ const params = new URLSearchParams(searchParams.toString());
+ Object.entries(next).forEach(([k, v]) => {
+ if (v === undefined || v === null || v === "") params.delete(k);
+ else params.set(k, String(v));
+ });
+ params.delete("page");
+ router.push(`?${params.toString()}`);
+ };
+
+ const onQuick = (key: string) => {
+ pushWith({ filter: key, start: undefined, end: undefined });
+ };
+
+ const onApplyDates = () => {
+ pushWith({ start, end, filter: undefined });
+ };
+
+ const onClearDates = () => {
+ pushWith({ start: undefined, end: undefined });
+ };
+
+ return (
+
+
+
+ {FILTER_OPTIONS.map((opt) => (
+ onQuick(opt.key)}
+ className={`px-2 py-[6px] rounded-[4px] ${
+ activeFilter === opt.key && !start && !end
+ ? "bg-[#F6F6F6] text-[#454545]"
+ : "bg-transparent text-[#888888]"
+ }`}
+ >
+ {opt.label}
+
+ ))}
+
+
+
+
+
+ {/* Opens the modal via URL param */}
+
pushWith({ showFilters: "1" })}
+ className="flex items-center gap-x-2 text-[#444] hover:opacity-80"
+ aria-haspopup="dialog"
+ aria-controls="notif-filter-modal"
+ aria-expanded={searchParams.get("showFilters") === "1"}
+ >
+ Filter
+
+
+ );
+}
diff --git a/src/app/dashboard/admin/notifications/components/NotificationFilterModal.tsx b/src/app/dashboard/admin/notifications/components/NotificationFilterModal.tsx
new file mode 100644
index 0000000..f7802ae
--- /dev/null
+++ b/src/app/dashboard/admin/notifications/components/NotificationFilterModal.tsx
@@ -0,0 +1,121 @@
+"use client";
+import React from "react";
+import { useRouter, useSearchParams } from "next/navigation";
+
+const RECIPIENTS = [
+ "All",
+ "Private",
+ "Write",
+ "General",
+ "Writer",
+ "Reader",
+] as const;
+const STATUS = ["All", "Sent", "Pending", "Failed"] as const;
+
+export default function NotificationFilterModal() {
+ const router = useRouter();
+ const sp = useSearchParams();
+ const open = sp.get("showFilters") === "1";
+
+ const sentBy = sp.get("sentBy") ?? "All";
+ const receiver = sp.get("receiver") ?? "All";
+ const status = sp.get("status") ?? "All";
+
+ const pushWith = (next: Record) => {
+ const params = new URLSearchParams(sp.toString());
+ Object.entries(next).forEach(([k, v]) => {
+ if (!v || v === "All") params.delete(k);
+ else params.set(k, String(v));
+ });
+ params.delete("page");
+ router.push(`?${params.toString()}`);
+ };
+
+ const close = () => {
+ const params = new URLSearchParams(sp.toString());
+ params.delete("showFilters");
+ router.push(`?${params.toString()}`);
+ };
+
+ React.useEffect(() => {
+ if (!open) return;
+ const prev = document.body.style.overflow;
+ document.body.style.overflow = "hidden";
+ return () => {
+ document.body.style.overflow = prev;
+ };
+ }, [open]);
+
+ if (!open) return null;
+
+ return (
+
+
+
+
Filter by
+
+
+ {/* Sent by */}
+
+ Sent by
+ pushWith({ sentBy: e.target.value })}
+ >
+ {RECIPIENTS.map((v) => (
+
+ {v}
+
+ ))}
+
+
+
+ {/* Receiver */}
+
+ Receiver
+ pushWith({ receiver: e.target.value })}
+ >
+ {RECIPIENTS.map((v) => (
+
+ {v}
+
+ ))}
+
+
+
+ {/* Status */}
+
+ Status
+ pushWith({ status: e.target.value })}
+ >
+ {STATUS.map((v) => (
+
+ {v}
+
+ ))}
+
+
+
+
+
+ Apply
+
+
+
+ );
+}
diff --git a/src/app/dashboard/admin/notifications/components/NotificationTable.tsx b/src/app/dashboard/admin/notifications/components/NotificationTable.tsx
new file mode 100644
index 0000000..942c8b5
--- /dev/null
+++ b/src/app/dashboard/admin/notifications/components/NotificationTable.tsx
@@ -0,0 +1,52 @@
+"use client";
+import React, { useMemo } from "react";
+import { useSearchParams } from "next/navigation";
+import NotificationTableHeader from "./NotificationTableHeader";
+import NotificationTableRow from "./NotificationTableRow";
+import NotificationTablePagination from "./NotificationTablePagination";
+import NotificationFilterModal from "./NotificationDetailsModal";
+import { NotificationItem, NOTIFICATIONS } from "../utils/dummy_data";
+
+export default function NotificationTable() {
+ const sp = useSearchParams();
+
+ const pageSize = Number(sp.get("pageSize") || 5);
+ const page = Number(sp.get("page") || 1);
+
+ const items = useMemo(
+ () =>
+ [...NOTIFICATIONS].sort((a, b) => +new Date(b.date) - +new Date(a.date)),
+ []
+ );
+
+ const filtered: NotificationItem[] = items;
+
+ const total = filtered.length;
+ const startIdx = (page - 1) * pageSize;
+ const pageItems = filtered.slice(startIdx, startIdx + pageSize);
+
+ return (
+
+
+
+ All Notifications
+
+
+
+
+
+
+ {pageItems.map((it) => (
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/src/app/dashboard/admin/notifications/components/NotificationTableHeader.tsx b/src/app/dashboard/admin/notifications/components/NotificationTableHeader.tsx
new file mode 100644
index 0000000..cd03d02
--- /dev/null
+++ b/src/app/dashboard/admin/notifications/components/NotificationTableHeader.tsx
@@ -0,0 +1,16 @@
+import React from "react";
+
+function NotificationTableHeader() {
+ return (
+
+
Title
+
Sender
+
Receiver
+
Status
+
Date
+
+
+ );
+}
+
+export default NotificationTableHeader;
diff --git a/src/app/dashboard/admin/notifications/components/NotificationTablePagination.tsx b/src/app/dashboard/admin/notifications/components/NotificationTablePagination.tsx
new file mode 100644
index 0000000..e8de55b
--- /dev/null
+++ b/src/app/dashboard/admin/notifications/components/NotificationTablePagination.tsx
@@ -0,0 +1,58 @@
+"use client";
+import { ChevronLeft, ChevronRight } from "lucide-react";
+import { useRouter, useSearchParams } from "next/navigation";
+import React from "react";
+
+type Props = {
+ total: number;
+ page: number;
+ pageSize: number;
+};
+
+export default function NotificationTablePagination({
+ total,
+ page,
+ pageSize,
+}: Props) {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+
+ const totalPages = Math.max(1, Math.ceil(total / pageSize));
+ const clampedPage = Math.min(Math.max(1, page), totalPages);
+ const from = total === 0 ? 0 : (clampedPage - 1) * pageSize + 1;
+ const to = Math.min(clampedPage * pageSize, total);
+
+ const pushWith = (next: Record) => {
+ const params = new URLSearchParams(searchParams.toString());
+ Object.entries(next).forEach(([k, v]) => {
+ if (v === undefined || v === null || v === "") params.delete(k);
+ else params.set(k, String(v));
+ });
+ router.push(`?${params.toString()}`);
+ };
+
+ return (
+
+
+ {total === 0 ? "No results" : `Showing ${from} to ${to} of ${total}`}
+
+
+
+ pushWith({ page: clampedPage - 1 })}
+ className="flex items-center gap-x-1 border border-[#E7E7E7] py-[6px] px-3 rounded-full disabled:opacity-50"
+ >
+ Prev
+
+ = totalPages}
+ onClick={() => pushWith({ page: clampedPage + 1 })}
+ className="flex items-center gap-x-1 border border-[#E7E7E7] py-[6px] px-3 rounded-full disabled:opacity-50"
+ >
+ Next
+
+
+
+ );
+}
diff --git a/src/app/dashboard/admin/notifications/components/NotificationTableRow.tsx b/src/app/dashboard/admin/notifications/components/NotificationTableRow.tsx
new file mode 100644
index 0000000..b93729d
--- /dev/null
+++ b/src/app/dashboard/admin/notifications/components/NotificationTableRow.tsx
@@ -0,0 +1,57 @@
+"use client";
+import React from "react";
+import { useRouter, useSearchParams } from "next/navigation";
+import { NotificationItem } from "../utils/dummy_data";
+
+export default function NotificationTableRow({
+ item,
+}: {
+ item: NotificationItem;
+}) {
+ const router = useRouter();
+ const sp = useSearchParams();
+
+ const statusStyles =
+ item.status === "Sent"
+ ? "border-[#34A853] bg-[#34A8531A] text-[#34A853]"
+ : item.status === "Pending"
+ ? "border-[#F9A825] bg-[#F9A8251A] text-[#F9A825]"
+ : "border-[#EB5757] bg-[#EB57571A] text-[#EB5757]";
+
+ const formatted = new Date(item.date).toLocaleDateString("en-GB", {
+ day: "2-digit",
+ month: "long",
+ year: "numeric",
+ });
+
+ const openDetails = () => {
+ const params = new URLSearchParams(sp.toString());
+ params.set("details", item.id);
+ router.push(`?${params.toString()}`);
+ };
+
+ return (
+
+
{item.title}
+
+
{item.recipients}
+
+
{formatted}
+
+
+ View Details
+
+
+
+ );
+}
diff --git a/src/app/dashboard/admin/notifications/page.tsx b/src/app/dashboard/admin/notifications/page.tsx
index 39bd566..c45bfd8 100644
--- a/src/app/dashboard/admin/notifications/page.tsx
+++ b/src/app/dashboard/admin/notifications/page.tsx
@@ -1,20 +1,44 @@
-"use-client";
+"use client";
import { Header } from "@/components/dashboard/header";
-
+import NotificationFilter from "./components/NotificationFilter";
+import NotificationTable from "./components/NotificationTable";
+import { useRouter, useSearchParams } from "next/navigation";
+import AnnouncementModal from "./components/AnnouncementModal";
export default function Notifications() {
+ const router = useRouter();
+ const sp = useSearchParams();
+
+ const openAnnouncement = () => {
+ const params = new URLSearchParams(sp.toString());
+ params.set("announce", "1");
+ router.push(`?${params.toString()}`);
+ };
return (
<>
-
-
-
Notification
-
- Welcome to the Notification section of the admin dashboard! Here, you can manage all aspects of notifications on your platform, including user alerts, system messages, and other important communications. This space is designed to help you keep users informed and engaged with timely updates and announcements.
-
+
+
+
+ Make Announcement
+
+
+
+
+
+
+
+
>
);
}
diff --git a/src/app/dashboard/admin/notifications/utils/dummy_data.ts b/src/app/dashboard/admin/notifications/utils/dummy_data.ts
new file mode 100644
index 0000000..f8507c7
--- /dev/null
+++ b/src/app/dashboard/admin/notifications/utils/dummy_data.ts
@@ -0,0 +1,41 @@
+export type Recipients = "Private" | "Write" | "General" | "Writer" | "Reader";
+export type NotificationItem = {
+ id: string;
+ title: string;
+ email: string;
+ recipients: Recipients;
+ status: "Sent" | "Pending" | "Failed";
+ date: string;
+ sentBy: string;
+ receiver: string;
+ image?: string;
+ body?: string;
+};
+
+const RECIPS: Recipients[] = [
+ "Private",
+ "Write",
+ "General",
+ "Writer",
+ "Reader",
+];
+const STATUS: NotificationItem["status"][] = ["Sent", "Pending", "Failed"];
+
+export const NOTIFICATIONS: NotificationItem[] = Array.from({ length: 40 }).map(
+ (_, i) => {
+ const d = new Date();
+ d.setDate(d.getDate() - i);
+ return {
+ id: String(i + 1),
+ title: i % 2 ? "Policy Violation" : "Announcement: Coming Soon",
+ email: `user${i + 1}@example.com`,
+ recipients: RECIPS[i % RECIPS.length],
+ status: STATUS[i % STATUS.length],
+ date: d.toISOString(),
+ sentBy: i % 2 ? "Admin Team" : "Moderator",
+ receiver: i % 3 ? "Writers" : "Readers",
+ image: "/coming-soon.jpg",
+ body: "We are excited to announce a series of improvements and new features in our subscription plans. Starting this month, subscribers will enjoy enhanced benefits tailored to provide greater value and a more seamless experience.",
+ };
+ }
+);
diff --git a/src/app/dashboard/admin/page.tsx b/src/app/dashboard/admin/page.tsx
index 36f9382..c52636a 100644
--- a/src/app/dashboard/admin/page.tsx
+++ b/src/app/dashboard/admin/page.tsx
@@ -41,229 +41,223 @@ const statCard = (label: string, value: string) => (
export default function DashboardHome() {
return (
<>
-
-
-
-
-
- {[
- {
- label: "Number of Regular",
- value: "257",
- color: "text-blue-600",
- icon:
,
- },
- {
- label: "NFT Books",
- value: "83",
- color: "text-purple-600",
- icon:
,
- },
- {
- label: "Readers",
- value: "1093",
- color: "text-green-600",
- icon:
,
- },
- {
- label: "Writers",
- value: "204",
- color: "text-orange-500",
- icon:
,
- },
- ].map((card, idx) => (
-
-
-
-
{card.label}
- {card.icon}
-
-
- {card.value}
-
-
+
+
+
+ {[
+ {
+ label: "Number of Regular",
+ value: "257",
+ color: "text-blue-600",
+ icon:
,
+ },
+ {
+ label: "NFT Books",
+ value: "83",
+ color: "text-purple-600",
+ icon:
,
+ },
+ {
+ label: "Readers",
+ value: "1093",
+ color: "text-green-600",
+ icon:
,
+ },
+ {
+ label: "Writers",
+ value: "204",
+ color: "text-orange-500",
+ icon:
,
+ },
+ ].map((card, idx) => (
+
+
+
+
{card.label}
+ {card.icon}
- ))}
-
-
-
-
-
- Transactions
-
+
+ {card.value}
+
+
+ ))}
+
-
-
-
Total Transactions
-
-
- 20% ▲
-
-
+
+
+
+ Transactions
+
+
-
- {statCard("Commission Eared", "21,070.93")}
- {statCard("Payout Sent", "51,070.93")}
- {statCard("Pending Payout", "12,070.93")}
- {statCard("Payout Sent", "21,070.93")}
-
+
+
+
Total Transactions
+
+
+ 20% ▲
+
-
-
-
New Payout Requests
-
- Filter
-
-
-
-
-
-
- Author
- Amount
- Wallet Address
- Request Date
- Status
- Actions
-
-
-
- {Array(5)
- .fill(0)
- .map((_, i) => (
-
-
-
-
Olu Ademola
-
- olusx_dgmail.com
-
-
-
-
-
-
- 500.67 STR
-
-
- 0xABC...789
- 27 May,2025
-
-
- Pending
-
-
-
-
- Approve
-
-
- Decline
-
-
-
- ))}
-
-
-
- Showing 1 to 5 of 12
-
-
+
+ {statCard("Commission Eared", "21,070.93")}
+ {statCard("Payout Sent", "51,070.93")}
+ {statCard("Pending Payout", "12,070.93")}
+ {statCard("Payout Sent", "21,070.93")}
+
+
-
-
Trending Books
-
- {books.map((book, idx) => (
-
- {book.imgSrc}
-
-
{book.title}
-
- By {book.author}{" "}
-
-
+
+
+
New Payout Requests
+
+ Filter
+
+
+
+
+
+
+ Author
+ Amount
+ Wallet Address
+ Request Date
+ Status
+ Actions
+
+
+
+ {Array(5)
+ .fill(0)
+ .map((_, i) => (
+
+
+
+
Olu Ademola
+
+ olusx_dgmail.com
+
+
+
+
- {" "}
- {book.price}
+
+ 500.67 STR
-
-
-
-
- {book.rating}
+
+ 0xABC...789
+ 27 May,2025
+
+
+ Pending
-
+
+
+
+ Approve
+
+
+ Decline
+
+
+
+ ))}
+
+
+
+ Showing 1 to 5 of 12
+
+
+
+
+
+
Trending Books
+
+ {books.map((book, idx) => (
+
+ {book.imgSrc}
+
+
{book.title}
+
+ By {book.author}{" "}
+
+
- ))}
+
+
+
+ {book.rating}
+
+
+
-
+ ))}
+
+
-
-
-
Top Authors
-
- View All →
-
-
+
+
+
Top Authors
+
+ View All →
+
+
-
- {[
- {
- name: "Elizabeth Joe",
- img: "https://randomuser.me/api/portraits/women/65.jpg",
- },
- {
- name: "Alex Paul",
- img: "https://randomuser.me/api/portraits/men/32.jpg",
- },
- {
- name: "Samson Tersoor",
- img: "https://randomuser.me/api/portraits/men/77.jpg",
- },
- {
- name: "Vamika Maya",
- img: "https://randomuser.me/api/portraits/women/49.jpg",
- },
- {
- name: "Samson Tersoor",
- img: "https://randomuser.me/api/portraits/women/44.jpg",
- },
- ].map((author, i) => (
-
- ))}
+
+ {[
+ {
+ name: "Elizabeth Joe",
+ img: "https://randomuser.me/api/portraits/women/65.jpg",
+ },
+ {
+ name: "Alex Paul",
+ img: "https://randomuser.me/api/portraits/men/32.jpg",
+ },
+ {
+ name: "Samson Tersoor",
+ img: "https://randomuser.me/api/portraits/men/77.jpg",
+ },
+ {
+ name: "Vamika Maya",
+ img: "https://randomuser.me/api/portraits/women/49.jpg",
+ },
+ {
+ name: "Samson Tersoor",
+ img: "https://randomuser.me/api/portraits/women/44.jpg",
+ },
+ ].map((author, i) => (
+
-
+ ))}
-
-
+
+
>
);
}
diff --git a/src/app/dashboard/admin/profile/components/AdminEditModal.tsx b/src/app/dashboard/admin/profile/components/AdminEditModal.tsx
new file mode 100644
index 0000000..b30e446
--- /dev/null
+++ b/src/app/dashboard/admin/profile/components/AdminEditModal.tsx
@@ -0,0 +1,116 @@
+"use client";
+import React from "react";
+import { useRouter, useSearchParams } from "next/navigation";
+import { ADMINS, Admin } from "../utils/data";
+import { Wallet } from "lucide-react";
+
+const ROLES = ["Super Admin", "Admin", "Content Admin", "Moderator"] as const;
+
+type Role = (typeof ROLES)[number];
+
+export default function AdminEditModal() {
+ const router = useRouter();
+ const sp = useSearchParams();
+ const editId = sp.get("edit");
+ const admin: Admin | undefined = ADMINS.find((a) => String(a.id) === editId);
+ const [email, setEmail] = React.useState(admin?.email);
+ const [role, setRole] = React.useState
(
+ (admin?.role as Role) ?? "Admin"
+ );
+ const close = () => {
+ const params = new URLSearchParams(sp.toString());
+ params.delete("edit");
+ router.push(`?${params.toString()}`);
+ };
+
+ React.useEffect(() => {
+ if (!editId) return;
+ const prev = document.body.style.overflow;
+ document.body.style.overflow = "hidden";
+ const onEsc = (e: KeyboardEvent) => e.key === "Escape" && close();
+ window.addEventListener("keydown", onEsc);
+ return () => {
+ document.body.style.overflow = prev;
+ window.removeEventListener("keydown", onEsc);
+ };
+ }, [editId]);
+
+ if (!editId || !admin) return null;
+
+ const wallet = "0xA3B4...29ABn";
+ const createdAt = "16 May, 2025";
+
+ return (
+
+
+
+
+
+
{admin.name}
+
Created {createdAt}
+
+ {role}
+
+
+
+
+
+ Email
+ setEmail(e.target.value)}
+ className="w-full border border-[#E5E7EB] rounded-xl px-4 py-3 outline-none"
+ placeholder="email@example.com"
+ />
+
+
+
+
Authorized wallet address
+
+
+
+
+
+
+
+ Role
+ setRole(e.target.value as Role)}
+ className="w-full border border-[#E5E7EB] rounded-xl px-4 py-3 outline-none"
+ >
+ {ROLES.map((r) => (
+
+ {r}
+
+ ))}
+
+
+
+
+
+
+ Cancel
+
+
+ Save Change
+
+
+
+
+ );
+}
diff --git a/src/app/dashboard/admin/profile/components/AdminRevokeModal.tsx b/src/app/dashboard/admin/profile/components/AdminRevokeModal.tsx
new file mode 100644
index 0000000..45b5e72
--- /dev/null
+++ b/src/app/dashboard/admin/profile/components/AdminRevokeModal.tsx
@@ -0,0 +1,79 @@
+"use client";
+import React from "react";
+import { useRouter, useSearchParams } from "next/navigation";
+import { ADMINS, Admin } from "../utils/data";
+import { AlertTriangle } from "lucide-react";
+
+export default function AdminRevokeModal() {
+ const router = useRouter();
+ const sp = useSearchParams();
+ const revokeId = sp.get("revoke");
+
+ const admin: Admin | undefined = ADMINS.find(
+ (a) => String(a.id) === revokeId
+ );
+
+ const close = () => {
+ const params = new URLSearchParams(sp.toString());
+ params.delete("revoke");
+ router.push(`?${params.toString()}`);
+ };
+
+ React.useEffect(() => {
+ if (!revokeId) return;
+ const prev = document.body.style.overflow;
+ document.body.style.overflow = "hidden";
+ const onEsc = (e: KeyboardEvent) => e.key === "Escape" && close();
+ window.addEventListener("keydown", onEsc);
+ return () => {
+ document.body.style.overflow = prev;
+ window.removeEventListener("keydown", onEsc);
+ };
+ }, [revokeId]);
+
+ if (!revokeId || !admin) return null;
+
+ const atName = `@${admin.name.replace(/\s+/g, " ").trim()}`;
+
+ return (
+
+
+
+
+
+
+
+ Are you sure you want to revoke{" "}
+ {atName} access?
+
+
+
+ This action will immediately disable his login and remove their
+ permissions. You can restore access later if needed.
+
+
+
+
+ Confirm
+
+
+
+ Cancel
+
+
+
+
+ );
+}
diff --git a/src/app/dashboard/admin/profile/components/AdminTable.tsx b/src/app/dashboard/admin/profile/components/AdminTable.tsx
new file mode 100644
index 0000000..51bbaed
--- /dev/null
+++ b/src/app/dashboard/admin/profile/components/AdminTable.tsx
@@ -0,0 +1,132 @@
+"use client";
+import React, { useMemo } from "react";
+import { useRouter, useSearchParams } from "next/navigation";
+import AdminTableHeader from "./AdminTableHeader";
+import AdminTablePagination from "./AdminTablePagination";
+import AdminTableRow from "./AdminTableRow";
+import { ADMINS } from "../utils/data";
+import AdminEditModal from "./AdminEditModal";
+import AdminRevokeModal from "./AdminRevokeModal";
+
+type StatusTab = "all" | "active" | "revoked";
+
+export default function AdminTable() {
+ const sp = useSearchParams();
+ const router = useRouter();
+
+ const status = (sp.get("status") as StatusTab) || "all";
+ const page = Number(sp.get("page") || 1);
+ const pageSize = Number(sp.get("pageSize") || 5);
+
+ const pushWith = (next: Record) => {
+ const params = new URLSearchParams(sp.toString());
+ Object.entries(next).forEach(([k, v]) => {
+ if (v === undefined || v === null || v === "") params.delete(k);
+ else params.set(k, String(v));
+ });
+ router.push(`?${params.toString()}`);
+ };
+
+ const setStatus = (next: StatusTab) => {
+ const params: Record = {
+ page: 1,
+ };
+ if (next !== "all") params.status = next;
+ else params.status = undefined;
+
+ pushWith(params);
+ };
+
+ const filtered = useMemo(() => {
+ if (status === "active") return ADMINS.filter((a) => a.status === "active");
+ if (status === "revoked")
+ return ADMINS.filter((a) => a.status === "revoked");
+ return ADMINS;
+ }, [status]);
+
+ const total = filtered.length;
+ const totalPages = Math.max(1, Math.ceil(total / pageSize));
+ const safePage = Math.min(Math.max(1, page), totalPages);
+ const sliceStart = (safePage - 1) * pageSize;
+ const pageItems = filtered.slice(sliceStart, sliceStart + pageSize);
+
+ const isActive = (t: StatusTab) => status === t;
+
+ return (
+
+
+ Create Admin
+
+
+
+
+
+ setStatus("all")}
+ className={`py-2 px-4 rounded-[8px] ${
+ isActive("all") ? "text-white" : "text-[#5D5D5D]"
+ }`}
+ style={{
+ background: isActive("all")
+ ? "linear-gradient(180deg, #096CFF 40.7%, #054199 180.61%)"
+ : "transparent",
+ }}
+ >
+ All
+
+ setStatus("active")}
+ className={`py-2 px-4 rounded-[8px] ${
+ isActive("active") ? "text-white" : "text-[#5D5D5D]"
+ }`}
+ style={{
+ background: isActive("active")
+ ? "linear-gradient(180deg, #096CFF 40.7%, #054199 180.61%)"
+ : "transparent",
+ }}
+ >
+ Active
+
+ setStatus("revoked")}
+ className={`py-2 px-4 rounded-[8px] ${
+ isActive("revoked") ? "text-white" : "text-[#5D5D5D]"
+ }`}
+ style={{
+ background: isActive("revoked")
+ ? "linear-gradient(180deg, #096CFF 40.7%, #054199 180.61%)"
+ : "transparent",
+ }}
+ >
+ Revoked
+
+
+
+
+
+
+
+
+ {pageItems.length === 0 ? (
+
No admins found.
+ ) : (
+ pageItems.map((item) =>
)
+ )}
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/dashboard/admin/profile/components/AdminTableHeader.tsx b/src/app/dashboard/admin/profile/components/AdminTableHeader.tsx
new file mode 100644
index 0000000..8e503ad
--- /dev/null
+++ b/src/app/dashboard/admin/profile/components/AdminTableHeader.tsx
@@ -0,0 +1,15 @@
+import React from "react";
+
+function AdminTableHeader() {
+ return (
+
+
Name
+
Email
+
Last Login
+
Status
+
+
+ );
+}
+
+export default AdminTableHeader;
diff --git a/src/app/dashboard/admin/profile/components/AdminTablePagination.tsx b/src/app/dashboard/admin/profile/components/AdminTablePagination.tsx
new file mode 100644
index 0000000..5137f24
--- /dev/null
+++ b/src/app/dashboard/admin/profile/components/AdminTablePagination.tsx
@@ -0,0 +1,54 @@
+"use client";
+import { ChevronLeft, ChevronRight } from "lucide-react";
+import { useRouter, useSearchParams } from "next/navigation";
+import React from "react";
+
+type Props = {
+ total: number;
+ page: number;
+ pageSize: number;
+};
+
+export default function AdminTablePagination({ total, page, pageSize }: Props) {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+
+ const totalPages = Math.max(1, Math.ceil(total / pageSize));
+ const clampedPage = Math.min(Math.max(1, page), totalPages);
+ const from = total === 0 ? 0 : (clampedPage - 1) * pageSize + 1;
+ const to = Math.min(clampedPage * pageSize, total);
+
+ const pushWith = (next: Record) => {
+ const params = new URLSearchParams(searchParams.toString());
+ Object.entries(next).forEach(([k, v]) => {
+ if (v === undefined || v === null || v === "") params.delete(k);
+ else params.set(k, String(v));
+ });
+ router.push(`?${params.toString()}`);
+ };
+
+ return (
+
+
+ {total === 0 ? "No results" : `Showing ${from} to ${to} of ${total}`}
+
+
+
+ pushWith({ page: clampedPage - 1 })}
+ className="flex items-center gap-x-1 border border-[#E7E7E7] py-[6px] px-3 rounded-full disabled:opacity-50"
+ >
+ Prev
+
+ = totalPages}
+ onClick={() => pushWith({ page: clampedPage + 1 })}
+ className="flex items-center gap-x-1 border border-[#E7E7E7] py-[6px] px-3 rounded-full disabled:opacity-50"
+ >
+ Next
+
+
+
+ );
+}
diff --git a/src/app/dashboard/admin/profile/components/AdminTableRow.tsx b/src/app/dashboard/admin/profile/components/AdminTableRow.tsx
new file mode 100644
index 0000000..8f93db9
--- /dev/null
+++ b/src/app/dashboard/admin/profile/components/AdminTableRow.tsx
@@ -0,0 +1,47 @@
+"use client";
+import React from "react";
+import { useRouter, useSearchParams } from "next/navigation";
+import { Admin, timeAgo } from "../utils/data";
+
+export default function AdminTableRow({ item }: { item: Admin }) {
+ const router = useRouter();
+ const sp = useSearchParams();
+
+ const openRevoke = () => {
+ const params = new URLSearchParams(sp.toString());
+ params.set("revoke", String(item.id));
+ router.push(`?${params.toString()}`);
+ };
+
+ const statusStyles =
+ item.status === "active"
+ ? "border-[#34A853] bg-[#34A8531A] text-[#34A853]"
+ : "border-[#FF5C5C] bg-[#FF5C5C1A] text-[#FF5C5C]";
+
+ return (
+
+
{item.name}
+
{item.email}
+
+ {timeAgo(item.lastLoginAt)}
+
+
+
+ Edit
+ {item.status === "active" ? (
+
+ Revoke
+
+ ) : (
+ Restore
+ )}
+
+
+ );
+}
diff --git a/src/app/dashboard/admin/profile/page.tsx b/src/app/dashboard/admin/profile/page.tsx
new file mode 100644
index 0000000..7cb280c
--- /dev/null
+++ b/src/app/dashboard/admin/profile/page.tsx
@@ -0,0 +1,40 @@
+import { Header } from "@/components/dashboard/header";
+import { Pencil } from "lucide-react";
+import AdminTable from "./components/AdminTable";
+function page() {
+ return (
+ <>
+
+
+
+
+
+
+
+ Anna Loop
+
+
annaloop@gmail.com
+
+ Super Admin
+
+
+
+
+
+
+ Authorized wallet address
+
+
+
+ 0xA3B4…29ABn
+
+
+
+
+
+
+ >
+ );
+}
+
+export default page;
diff --git a/src/app/dashboard/admin/profile/utils/data.ts b/src/app/dashboard/admin/profile/utils/data.ts
new file mode 100644
index 0000000..fc32a09
--- /dev/null
+++ b/src/app/dashboard/admin/profile/utils/data.ts
@@ -0,0 +1,179 @@
+export type AdminStatus = "active" | "revoked";
+export type AdminRole = "Super Admin" | "Admin" | "Moderator";
+
+export interface Admin {
+ id: number;
+ firstName: string;
+ lastName: string;
+ name: string;
+ email: string;
+ role: AdminRole;
+ status: AdminStatus;
+ lastLoginAt: string;
+}
+
+export function timeAgo(iso: string): string {
+ const now = Date.now();
+ const diffMs = now - new Date(iso).getTime();
+ const mins = Math.max(1, Math.round(diffMs / 60000));
+ if (mins < 60) return `${mins} Minute${mins === 1 ? "" : "s"} Ago`;
+ const hrs = Math.round(mins / 60);
+ if (hrs < 24) return `${hrs} Hour${hrs === 1 ? "" : "s"} Ago`;
+ const days = Math.round(hrs / 24);
+ return `${days} Day${days === 1 ? "" : "s"} Ago`;
+}
+
+const hoursAgo = (h: number) =>
+ new Date(Date.now() - h * 3600_000).toISOString();
+const daysAgo = (d: number) =>
+ new Date(Date.now() - d * 24 * 3600_000).toISOString();
+
+/** Dummy admins */
+export const ADMINS: Admin[] = [
+ {
+ id: 1,
+ firstName: "Anna",
+ lastName: "Loop",
+ name: "Anna Loop",
+ email: "annaloop@gmail.com",
+ role: "Super Admin",
+ status: "active",
+ lastLoginAt: hoursAgo(2),
+ },
+ {
+ id: 2,
+ firstName: "Habib",
+ lastName: "Musa",
+ name: "Habib Musa",
+ email: "habibmusa@gmail.com",
+ role: "Admin",
+ status: "active",
+ lastLoginAt: hoursAgo(2),
+ },
+ {
+ id: 3,
+ firstName: "Darrin",
+ lastName: "Collins",
+ name: "Darrin Collins",
+ email: "darrin.collins@example.com",
+ role: "Moderator",
+ status: "active",
+ lastLoginAt: hoursAgo(5),
+ },
+ {
+ id: 4,
+ firstName: "Ola",
+ lastName: "Peters",
+ name: "Ola Peters",
+ email: "ola.peters@example.com",
+ role: "Admin",
+ status: "revoked",
+ lastLoginAt: daysAgo(10),
+ },
+ {
+ id: 5,
+ firstName: "Grace",
+ lastName: "Kim",
+ name: "Grace Kim",
+ email: "grace.kim@example.com",
+ role: "Admin",
+ status: "active",
+ lastLoginAt: hoursAgo(1),
+ },
+ {
+ id: 6,
+ firstName: "Ahmed",
+ lastName: "Saleh",
+ name: "Ahmed Saleh",
+ email: "ahmed.saleh@example.com",
+ role: "Moderator",
+ status: "active",
+ lastLoginAt: daysAgo(1),
+ },
+ {
+ id: 7,
+ firstName: "Maya",
+ lastName: "Singh",
+ name: "Maya Singh",
+ email: "maya.singh@example.com",
+ role: "Admin",
+ status: "revoked",
+ lastLoginAt: daysAgo(30),
+ },
+ {
+ id: 8,
+ firstName: "Leo",
+ lastName: "Garcia",
+ name: "Leo Garcia",
+ email: "leo.garcia@example.com",
+ role: "Admin",
+ status: "active",
+ lastLoginAt: hoursAgo(12),
+ },
+ {
+ id: 9,
+ firstName: "Zara",
+ lastName: "Ali",
+ name: "Zara Ali",
+ email: "zara.ali@example.com",
+ role: "Moderator",
+ status: "active",
+ lastLoginAt: daysAgo(3),
+ },
+ {
+ id: 10,
+ firstName: "Victor",
+ lastName: "Ng",
+ name: "Victor Ng",
+ email: "victor.ng@example.com",
+ role: "Admin",
+ status: "active",
+ lastLoginAt: hoursAgo(4),
+ },
+ {
+ id: 11,
+ firstName: "Chioma",
+ lastName: "Okeke",
+ name: "Chioma Okeke",
+ email: "chioma.okeke@example.com",
+ role: "Admin",
+ status: "revoked",
+ lastLoginAt: daysAgo(14),
+ },
+ {
+ id: 12,
+ firstName: "Tomiwa",
+ lastName: "Adeyemi",
+ name: "Tomiwa Adeyemi",
+ email: "tomiwa.adeyemi@example.com",
+ role: "Admin",
+ status: "active",
+ lastLoginAt: hoursAgo(7),
+ },
+];
+
+export const getActiveAdmins = () =>
+ ADMINS.filter((a) => a.status === "active");
+export const getRevokedAdmins = () =>
+ ADMINS.filter((a) => a.status === "revoked");
+
+export function searchAdmins(q: string, list: Admin[] = ADMINS): Admin[] {
+ const s = q.trim().toLowerCase();
+ if (!s) return list;
+ return list.filter(
+ (a) =>
+ a.name.toLowerCase().includes(s) ||
+ a.email.toLowerCase().includes(s) ||
+ a.role.toLowerCase().includes(s)
+ );
+}
+
+export function paginate(items: T[], page = 1, pageSize = 10) {
+ const start = (page - 1) * pageSize;
+ return {
+ total: items.length,
+ page,
+ pageSize,
+ data: items.slice(start, start + pageSize),
+ };
+}
diff --git a/src/app/globals.css b/src/app/globals.css
index ea3adcb..c4e266f 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -213,10 +213,6 @@
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
-
- * {
- /* outline: 1px solid black; */
- }
}
.dark {
@@ -253,19 +249,6 @@
--sidebar-ring: oklch(0.556 0 0);
}
-@layer base {
- * {
- @apply border-border outline-ring/50;
- }
- body {
- @apply bg-background text-foreground;
- }
-}
-
-* {
- @apply border-border outline-ring/50;
-}
-
body {
@apply bg-background text-foreground;
}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 5899eee..02b4adb 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,4 +1,3 @@
-
import { Metadata } from "next";
import { WalletProvider } from "../components/blockchain/WalletProvider";
import "@/app/globals.css";
@@ -105,9 +104,7 @@ export default function RootLayout({
-
- {children}
-
+ {children}