From 3ddb48fe5806dd21dc8ce6535f9e57705c583e97 Mon Sep 17 00:00:00 2001 From: Don Strawsburg Date: Fri, 12 Dec 2025 23:19:33 -0500 Subject: [PATCH] put adminleftnavigation in a component, toi unclutter the page --- app/admin/layout.tsx | 122 ++++-------- app/admin/platforms/page.tsx | 301 +++++++++++++++++++++++++++++ app/admin/settings/page.tsx | 34 ++++ components/AdminLeftNavigation.tsx | 136 +++++++++++++ lib/api/platforms.ts | 103 ++++++++++ 5 files changed, 611 insertions(+), 85 deletions(-) create mode 100644 app/admin/platforms/page.tsx create mode 100644 app/admin/settings/page.tsx create mode 100644 components/AdminLeftNavigation.tsx create mode 100644 lib/api/platforms.ts diff --git a/app/admin/layout.tsx b/app/admin/layout.tsx index 0ca302b..331ab2b 100644 --- a/app/admin/layout.tsx +++ b/app/admin/layout.tsx @@ -1,6 +1,7 @@ "use client"; import type React from "react"; import { useState } from "react"; +import AdminLeftNavigation from "@/components/AdminLeftNavigation"; import { LayoutDashboard, Download, @@ -8,10 +9,10 @@ import { Boxes, Store, Users, - Settings, LucideMail, + Settings, + LucideMail, } from "lucide-react"; - const navItems = [ { label: "Dashboard", @@ -38,13 +39,18 @@ const navItems = [ href: "/admin/merchants", icon: , }, + { + label: "Platforms", + href: "/admin/platforms", + icon: , + }, { label: "Users", href: "/admin/users", icon: , }, { - label: "Settings (TO DO)", + label: "Settings", href: "/admin/settings", icon: , }, @@ -60,6 +66,7 @@ const navItems = [ }, ]; +// ... existing code ... // ADMIN CHECK FOR LOGIN // const { user, loading } = useAuth(); @@ -68,101 +75,46 @@ const navItems = [ // } export default function AdminLayout({ - children, -}: { + children, + }: { children: React.ReactNode; }) { const [collapsed, setCollapsed] = useState(false); + return ( -
- {/* Sidebar */} - - - {/* Main column */} -
- {/* Top bar */} -
-
-

- Admin -

-

- Battl Builders Control Panel -

-
-
+
Internal • v0.1 -
- ADMIN +
+ ADMIN +
-
-
+ - {/* Content */} -
- {children} -
+ {/* Content */} +
+ {children} +
+
- ); } \ No newline at end of file diff --git a/app/admin/platforms/page.tsx b/app/admin/platforms/page.tsx new file mode 100644 index 0000000..874ad6d --- /dev/null +++ b/app/admin/platforms/page.tsx @@ -0,0 +1,301 @@ +// app/admin/platforms/page.tsx +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import { Plus, Pencil, Trash2, X } from "lucide-react"; +import { useAuth } from "@/context/AuthContext"; +import { + createPlatform, + deletePlatform, + fetchPlatforms, + updatePlatform, + type Platform, + type CreatePlatformDto, + type UpdatePlatformDto, +} from "@/lib/api/platforms"; + +export default function AdminPlatformsPage() { + const { getAuthHeaders } = useAuth(); + + const [platforms, setPlatforms] = useState([]); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + + const [showModal, setShowModal] = useState(false); + const [modalMode, setModalMode] = useState<"create" | "edit">("create"); + const [selected, setSelected] = useState(null); + + const [form, setForm] = useState({ + key: "", + label: "", + isActive: true, + }); + + async function load() { + try { + setLoading(true); + setError(null); + const data = await fetchPlatforms(getAuthHeaders()); + setPlatforms(data); + } catch (e: any) { + console.error(e); + setError(e?.message ?? "Failed to load platforms"); + setPlatforms([]); + } finally { + setLoading(false); + } + } + + useEffect(() => { + void load(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const sorted = useMemo(() => { + return [...platforms].sort((a, b) => a.key.localeCompare(b.key)); + }, [platforms]); + + function openCreate() { + setModalMode("create"); + setSelected(null); + setForm({ key: "", label: "", isActive: true }); + setShowModal(true); + } + + function openEdit(p: Platform) { + setModalMode("edit"); + setSelected(p); + setForm({ key: p.key, label: p.label, isActive: p.isActive }); + setShowModal(true); + } + + async function onDelete(p: Platform) { + if (!window.confirm(`Delete platform "${p.key}"? This cannot be undone.`)) return; + + try { + setSaving(true); + setError(null); + await deletePlatform(getAuthHeaders(), p.id); + await load(); + } catch (e: any) { + console.error(e); + setError(e?.message ?? "Failed to delete platform"); + } finally { + setSaving(false); + } + } + + async function onSubmit(e: React.FormEvent) { + e.preventDefault(); + + const key = (form.key ?? "").trim(); + const label = (form.label ?? "").trim(); + + if (!key || !label) { + setError("Key and Label are required."); + return; + } + + try { + setSaving(true); + setError(null); + + if (modalMode === "create") { + await createPlatform(getAuthHeaders(), { ...form, key, label }); + } else if (selected) { + const update: UpdatePlatformDto = {}; + if (key !== selected.key) update.key = key; + if (label !== selected.label) update.label = label; + if ((form.isActive ?? true) !== selected.isActive) update.isActive = form.isActive; + + await updatePlatform(getAuthHeaders(), selected.id, update); + } + + setShowModal(false); + await load(); + } catch (e: any) { + console.error(e); + setError(e?.message ?? "Failed to save platform"); + } finally { + setSaving(false); + } + } + + return ( +
+
+
+

Platforms

+

+ Manage canonical platforms used across products and filtering. +

+
+ + +
+ + {error && ( +
+ {error} +
+ )} + + {loading ? ( +
Loading…
+ ) : sorted.length === 0 ? ( +
+ No platforms found. +
+ ) : ( +
+ + + + + + + + + + + + {sorted.map((p, idx) => ( + + + + + + + + ))} + +
IDKeyLabelActiveActions
+ {p.id} + + {p.key} + + {p.label} + + + {p.isActive ? "ACTIVE" : "INACTIVE"} + + +
+ + +
+
+
+ )} + + {showModal && ( +
+
+
+

+ {modalMode === "create" ? "Create Platform" : "Edit Platform"} +

+ +
+ +
+
+ + setForm((s) => ({ ...s, key: e.target.value }))} + className="w-full rounded-md border border-zinc-800 bg-zinc-900 px-3 py-2 text-sm text-zinc-100 placeholder-zinc-500 focus:border-amber-400/80 focus:outline-none focus:ring-1 focus:ring-amber-400" + placeholder="AR15" + required + /> +
+ +
+ + setForm((s) => ({ ...s, label: e.target.value }))} + className="w-full rounded-md border border-zinc-800 bg-zinc-900 px-3 py-2 text-sm text-zinc-100 placeholder-zinc-500 focus:border-amber-400/80 focus:outline-none focus:ring-1 focus:ring-amber-400" + placeholder="AR-15" + required + /> +
+ + + +
+ + +
+
+
+
+ )} +
+ ); +} diff --git a/app/admin/settings/page.tsx b/app/admin/settings/page.tsx new file mode 100644 index 0000000..97a3c2b --- /dev/null +++ b/app/admin/settings/page.tsx @@ -0,0 +1,34 @@ +// app/admin/settings/page.tsx +"use client"; + +import { Settings } from "lucide-react"; + +export default function Page() { + return ( +
+
+
+

Settings

+

+ Admin configuration and environment controls. +

+
+
+ +
+
+
+ +
+ +
+

Coming soon

+

+ This section is under construction. Check back after the next deploy. +

+
+
+
+
+ ); +} diff --git a/components/AdminLeftNavigation.tsx b/components/AdminLeftNavigation.tsx new file mode 100644 index 0000000..c229cd7 --- /dev/null +++ b/components/AdminLeftNavigation.tsx @@ -0,0 +1,136 @@ +"use client"; + +import type React from "react"; +import { + LayoutDashboard, + Download, + Layers, + Boxes, + Store, + Users, + Settings, + LucideMail, +} from "lucide-react"; + +type AdminLeftNavigationProps = { + collapsed: boolean; + onToggleCollapsed: () => void; +}; + +const navItems = [ + { + label: "Dashboard", + href: "/admin", + icon: , + }, + { + label: "Imports", + href: "/admin/import-status", + icon: , + }, + { + label: "Mappings", + href: "/admin/mapping", + icon: , + }, + { + label: "Products", + href: "/admin/products", + icon: , + }, + { + label: "Merchants", + href: "/admin/merchants", + icon: , + }, + { + label: "Platforms", + href: "/admin/platforms", + icon: , + }, + { + label: "Users", + href: "/admin/users", + icon: , + }, + { + label: "Settings", + href: "/admin/settings", + icon: , + }, + { + label: "List Emails", + href: "/admin/email", + icon: , + }, + { + label: "Send a Email", + href: "/admin/email/send", + icon: , + }, +]; + +export default function AdminLeftNavigation({ + collapsed, + onToggleCollapsed, +}: AdminLeftNavigationProps) { + return ( + + ); +} diff --git a/lib/api/platforms.ts b/lib/api/platforms.ts new file mode 100644 index 0000000..1d3b365 --- /dev/null +++ b/lib/api/platforms.ts @@ -0,0 +1,103 @@ +// lib/api/platforms.ts +export type Platform = { + id: number; + key: string; + label: string; + isActive: boolean; +}; + +export type CreatePlatformDto = { + key: string; + label: string; + isActive?: boolean; +}; + +export type UpdatePlatformDto = Partial; + +const API_BASE = + process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080"; + +function normalizePlatform(raw: any): Platform { + const id = Number(raw?.id); + const key = String(raw?.key ?? "").trim(); + const label = String(raw?.label ?? "").trim(); + + const activeRaw = raw?.isActive ?? raw?.is_active ?? true; + const isActive = + typeof activeRaw === "boolean" + ? activeRaw + : typeof activeRaw === "number" + ? activeRaw === 1 + : String(activeRaw).toLowerCase() === "true"; + + return { id, key, label, isActive }; +} + +export async function fetchPlatforms(tokenHeaders: HeadersInit): Promise { + const res = await fetch(`${API_BASE}/api/platforms`, { + headers: { ...tokenHeaders }, + }); + + if (!res.ok) { + throw new Error(`Failed to load platforms (${res.status})`); + } + + const data = (await res.json()) as unknown; + const items = Array.isArray(data) ? data : []; + return items.map(normalizePlatform).filter((p) => Number.isFinite(p.id) && p.key && p.label); +} + +export async function createPlatform( + tokenHeaders: HeadersInit, + dto: CreatePlatformDto +): Promise { + const res = await fetch(`${API_BASE}/api/platforms`, { + method: "POST", + headers: { + "Content-Type": "application/json", + ...tokenHeaders, + }, + body: JSON.stringify(dto), + }); + + if (!res.ok) { + const text = await res.text().catch(() => ""); + throw new Error(text || `Failed to create platform (${res.status})`); + } + + return normalizePlatform(await res.json()); +} + +export async function updatePlatform( + tokenHeaders: HeadersInit, + id: number, + dto: UpdatePlatformDto +): Promise { + const res = await fetch(`${API_BASE}/api/platforms/${id}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + ...tokenHeaders, + }, + body: JSON.stringify(dto), + }); + + if (!res.ok) { + const text = await res.text().catch(() => ""); + throw new Error(text || `Failed to update platform (${res.status})`); + } + + return normalizePlatform(await res.json()); +} + +export async function deletePlatform(tokenHeaders: HeadersInit, id: number): Promise { + const res = await fetch(`${API_BASE}/api/platforms/${id}`, { + method: "DELETE", + headers: { ...tokenHeaders }, + }); + + if (!res.ok) { + const text = await res.text().catch(() => ""); + throw new Error(text || `Failed to delete platform (${res.status})`); + } +}