From 2cd871b5298d03f2f3beb8616229ab03fd9b8ad4 Mon Sep 17 00:00:00 2001 From: Sean Date: Mon, 8 Dec 2025 07:08:54 -0500 Subject: [PATCH] lots of admin stuff. admin layout, user mangement page, new landing page, etc --- .../builder/[categoryId]/[partId]/data.ts | 0 .../builder/[categoryId]/[partId]/page.tsx | 0 .../builder/[categoryId]/page.tsx | 0 app/(builder)/builder/layout.tsx | 58 ++++ app/{ => (builder)}/builder/page.tsx | 0 app/{ => (builder)}/builder/summary/page.tsx | 0 app/(builder)/layout.tsx | 11 + app/admin/layout.tsx | 157 +++++++++++ app/admin/users/page.tsx | 227 ++++++++++++++++ app/builds/layout.tsx | 16 ++ app/layout.tsx | 23 +- app/page.tsx | 254 ++++++++++++++++-- components/Banner.tsx | 27 ++ components/PricingHistoryGraph.tsx | 2 +- components/RetailersList.tsx | 2 +- components/TopNav.tsx | 38 +-- lib/api/beta-signup/route.ts | 38 +++ types/user.ts | 34 +++ 18 files changed, 807 insertions(+), 80 deletions(-) rename app/{ => (builder)}/builder/[categoryId]/[partId]/data.ts (100%) rename app/{ => (builder)}/builder/[categoryId]/[partId]/page.tsx (100%) rename app/{ => (builder)}/builder/[categoryId]/page.tsx (100%) create mode 100644 app/(builder)/builder/layout.tsx rename app/{ => (builder)}/builder/page.tsx (100%) rename app/{ => (builder)}/builder/summary/page.tsx (100%) create mode 100644 app/(builder)/layout.tsx create mode 100644 app/admin/layout.tsx create mode 100644 app/admin/users/page.tsx create mode 100644 app/builds/layout.tsx create mode 100644 components/Banner.tsx create mode 100644 lib/api/beta-signup/route.ts create mode 100644 types/user.ts diff --git a/app/builder/[categoryId]/[partId]/data.ts b/app/(builder)/builder/[categoryId]/[partId]/data.ts similarity index 100% rename from app/builder/[categoryId]/[partId]/data.ts rename to app/(builder)/builder/[categoryId]/[partId]/data.ts diff --git a/app/builder/[categoryId]/[partId]/page.tsx b/app/(builder)/builder/[categoryId]/[partId]/page.tsx similarity index 100% rename from app/builder/[categoryId]/[partId]/page.tsx rename to app/(builder)/builder/[categoryId]/[partId]/page.tsx diff --git a/app/builder/[categoryId]/page.tsx b/app/(builder)/builder/[categoryId]/page.tsx similarity index 100% rename from app/builder/[categoryId]/page.tsx rename to app/(builder)/builder/[categoryId]/page.tsx diff --git a/app/(builder)/builder/layout.tsx b/app/(builder)/builder/layout.tsx new file mode 100644 index 0000000..b04be4c --- /dev/null +++ b/app/(builder)/builder/layout.tsx @@ -0,0 +1,58 @@ +// app/(builder)/layout.tsx +import type { ReactNode } from "react"; + +import { AuthProvider } from "@/context/AuthContext"; +import { BuilderNav } from "@/components/BuilderNav"; + +export const metadata = { + title: { + default: "Battl Builder", + template: "%s | Battl Builder", + }, + description: + "Battl Builder — the smarter, faster, data‑driven way to build your next firearm.", +}; + +export default function BuilderLayout({ children }: { children: ReactNode }) { + return ( +
+ + +
{children}
+
+
+ ); +} + + +// +// Original homepage. will delete after testing +// + +// import "./globals.css"; +// import type { ReactNode } from "react"; +// import { AuthProvider } from "@/context/AuthContext"; +// import { TopNav } from "@/components/TopNav"; +// import { BuilderNav } from "@/components/BuilderNav"; + +// export const metadata = { +// title: { +// default: "Battl Builder", +// template: "%s | Battl Builder", +// }, +// description: "Battl Builder — the smarter, faster, data‑driven way to build your next firearm.", +// }; + +// export default function RootLayout({ children }: { children: ReactNode }) { +// return ( +// +// +// +// +// +//
{children}
+//
+// +// +// ); +// } diff --git a/app/builder/page.tsx b/app/(builder)/builder/page.tsx similarity index 100% rename from app/builder/page.tsx rename to app/(builder)/builder/page.tsx diff --git a/app/builder/summary/page.tsx b/app/(builder)/builder/summary/page.tsx similarity index 100% rename from app/builder/summary/page.tsx rename to app/(builder)/builder/summary/page.tsx diff --git a/app/(builder)/layout.tsx b/app/(builder)/layout.tsx new file mode 100644 index 0000000..57be1b0 --- /dev/null +++ b/app/(builder)/layout.tsx @@ -0,0 +1,11 @@ +import { TopNav } from "@/components/TopNav"; +import type { ReactNode } from "react"; + +export default function BuilderLayout({ children }: { children: ReactNode }) { + return ( +
+ + {children} +
+ ); +} \ No newline at end of file diff --git a/app/admin/layout.tsx b/app/admin/layout.tsx new file mode 100644 index 0000000..c7a9c39 --- /dev/null +++ b/app/admin/layout.tsx @@ -0,0 +1,157 @@ +"use client"; +import type React from "react"; +import { useState } from "react"; +import { + LayoutDashboard, + Download, + Layers, + Boxes, + Store, + Users, + Settings, +} from "lucide-react"; + +const navItems = [ + { + label: "Dashboard", + href: "/admin", + icon: , + }, + { + label: "Imports", + href: "/admin/import-status", + icon: , + }, + { + label: "Mappings", + href: "/admin/mapping", + icon: , + }, + { + label: "Products (TO DO)", + href: "/admin/products", + icon: , + }, + { + label: "Merchants", + href: "/admin/merchants", + icon: , + }, + { + label: "Users (TO DO)", + href: "/admin/users", + icon: , + }, + { + label: "Settings (TO DO)", + href: "/admin/settings", + icon: , + }, +]; + +// ADMIN CHECK FOR LOGIN +// const { user, loading } = useAuth(); + +// if (!loading && user?.role !== "ADMIN") { +// redirect("/"); // or /login +// } + +export default function AdminLayout({ + children, +}: { + children: React.ReactNode; +}) { + const [collapsed, setCollapsed] = useState(false); + return ( +
+ {/* Sidebar */} + + + {/* Main column */} +
+ {/* Top bar */} +
+
+

+ Admin +

+

+ Battl Builders Control Panel +

+
+
+ + Internal • v0.1 + +
+ ADMIN +
+
+
+ + {/* Content */} +
+ {children} +
+
+
+ ); +} \ No newline at end of file diff --git a/app/admin/users/page.tsx b/app/admin/users/page.tsx new file mode 100644 index 0000000..11c3fc5 --- /dev/null +++ b/app/admin/users/page.tsx @@ -0,0 +1,227 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useAuth } from "@/context/AuthContext"; // adjust path if needed +import { Loader2 } from "lucide-react"; + +import type { AdminUser, UserRole } from "@/types/user"; + +const API_BASE_URL = + process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080"; + +export default function AdminUsersPage() { + const { getAuthHeaders } = useAuth(); + const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(true); + const [savingId, setSavingId] = useState(null); + const [error, setError] = useState(null); + + async function fetchUsers() { + try { + setLoading(true); + setError(null); + + const res = await fetch(`${API_BASE_URL}/admin/users`, { + headers: { + ...getAuthHeaders(), + }, + }); + + if (!res.ok) { + throw new Error(`Failed to load users (${res.status})`); + } + + const data: AdminUser[] = await res.json(); + setUsers(data); + } catch (err: any) { + console.error(err); + setError(err.message ?? "Failed to load users"); + } finally { + setLoading(false); + } + } + + useEffect(() => { + fetchUsers(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + async function handleRoleChange(userId: string, newRole: UserRole) { + setSavingId(userId); + setError(null); + + const prev = users; + setUsers((cur) => + cur.map((u) => (u.id === userId ? { ...u, role: newRole } : u)) + ); + + try { + const res = await fetch(`${API_BASE_URL}/admin/users/${userId}/role`, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + ...getAuthHeaders(), + }, + body: JSON.stringify({ role: newRole }), + }); + + if (!res.ok) { + throw new Error(`Failed to update role (${res.status})`); + } + } catch (err: any) { + console.error(err); + setError(err.message ?? "Failed to update role"); + // rollback + setUsers(prev); + } finally { + setSavingId(null); + } + } + + function handleRoleSelectChange(user: AdminUser, newRole: UserRole) { + if (newRole === user.role) { + return; + } + + // Confirm promotions to ADMIN + if ( + newRole === "ADMIN" && + !window.confirm( + `Promote ${user.email} to ADMIN? This will give them access to the admin console.` + ) + ) { + return; + } + + // Confirm demotions from ADMIN + if ( + user.role === "ADMIN" && + newRole === "USER" && + !window.confirm( + `Remove ADMIN access from ${user.email}? They will lose access to the admin console.` + ) + ) { + return; + } + + void handleRoleChange(user.id, newRole); + } + + return ( +
+
+
+

+ Users & Roles +

+

+ Promote trusted accounts to admins. All new signups start as{" "} + USER. +

+
+ +
+ + {error && ( +
+ {error} +
+ )} + + {loading ? ( +
+ + Loading users… +
+ ) : users.length === 0 ? ( +
+ No users found yet. Once people start signing up, they'll show up + here. +
+ ) : ( +
+ + + + + + + + + + + + {users.map((u, idx) => ( + + + + + + + + ))} + +
Email + Display Name + RoleCreated + Last Login +
+ {u.email} + + {u.displayName || "—"} + +
+ + {u.role} + + +
+
+ {u.createdAt + ? new Date(u.createdAt).toLocaleDateString() + : "—"} + + {u.lastLoginAt + ? new Date(u.lastLoginAt).toLocaleDateString() + : "—"} +
+
+ )} + +

+ Note: Role changes take effect on the user's next request. All new + accounts are created as USER by + default. +

+
+ ); +} diff --git a/app/builds/layout.tsx b/app/builds/layout.tsx new file mode 100644 index 0000000..a14e64f --- /dev/null +++ b/app/builds/layout.tsx @@ -0,0 +1,16 @@ +export const metadata = { + title: 'Next.js', + description: 'Generated by Next.js', +} + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/app/layout.tsx b/app/layout.tsx index a1046f0..2b9ba81 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,27 +1,22 @@ - +// app/layout.tsx import "./globals.css"; import type { ReactNode } from "react"; import { AuthProvider } from "@/context/AuthContext"; -import { TopNav } from "@/components/TopNav"; -import { BuilderNav } from "@/components/BuilderNav"; -export const metadata = { - title: { - default: "Battl Builder", - template: "%s | Battl Builder", - }, - description: "Battl Builder — the smarter, faster, data‑driven way to build your next firearm.", -}; +import { Banner } from "@/components/Banner"; +import { TopNav } from "@/components/TopNav"; + export default function RootLayout({ children }: { children: ReactNode }) { return ( - + + - - -
{children}
+ + {children}
+ ); diff --git a/app/page.tsx b/app/page.tsx index 7a10837..782f6fb 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,34 +1,234 @@ -import Link from "next/link"; +"use client"; + +import { FormEvent, useState } from "react"; +import Image from "next/image"; export default function HomePage() { + const [email, setEmail] = useState(""); + const [useCase, setUseCase] = useState(""); + const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">( + "idle" + ); + const [message, setMessage] = useState(null); + + async function handleSubmit(e: FormEvent) { + e.preventDefault(); + if (!email) { + setMessage("Drop an email in first, operator."); + setStatus("error"); + return; + } + + try { + setStatus("loading"); + setMessage(null); + + const res = await fetch("/api/beta-signup", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, useCase }), + }); + + if (!res.ok) { + throw new Error("Failed to save your signup."); + } + + setStatus("success"); + setMessage("You’re locked in. Watch your inbox."); + setEmail(""); + setUseCase(""); + } catch (err) { + console.error(err); + setStatus("error"); + setMessage("Something broke. Try again in a minute."); + } + } + return ( -
-
-

- For Those Who Build With Intent. -

-

+ {/* Abstract Brand Background */} +
+ - Battl Builder™ -

-

- Battl Builder™ is your command center for rifle builds. We surface live - parts and hard-use brands, giving you a mission-ready workbench to - design smarter, track cost, swap components, and save setups. No - fluff. Just clean, trustworthy data. -

-
- - Launch The Builder - -
+ + + + + + + + + + + + + +
+
+ {/* Top Bar / Logo */} +
+
+ {/* Logo slot – replace src with your actual logo file */} +
+ Battl Builders logo +
+
+

+ Battl Builders +

+ +
+
+ + + Private Beta • Coming Soon + +
+ + {/* Hero */} +
+
+

+ Stop Guessing. + + Start Building + +

+

+ From part comparisons to full-build matchups, Battl Builders helps you find the right components, catch compatibility issues, and score the best deals — all without juggling tabs or spreadsheets. + +

+ +
    +
  • • Side-by-side part & build comparisons
  • +
  • • Smart compatibility checks
  • +
  • • Live pricing from vetted merchants
  • +
  • • Save & share builds with your crew
  • +
  • • Vote on the cleanest, meanest setups
  • + +
+
+ + {/* Beta Form */} +
+

+ Join the Beta List +

+

+ Drop your email and we'll send early access when the Builder + goes hot. No spam, just builds. +

+ +
+
+ + setEmail(e.target.value)} + placeholder="you@gearjunkie.com" + className="mt-1 w-full rounded-md border border-zinc-800 bg-zinc-950 px-3 py-2 text-sm text-zinc-50 outline-none ring-amber-500/30 placeholder:text-zinc-600 focus:border-amber-400/80 focus:ring-2" + required + /> +
+ +
+ +