diff --git a/app/admin/beta-invites/page.tsx b/app/admin/beta-invites/page.tsx new file mode 100644 index 0000000..8797672 --- /dev/null +++ b/app/admin/beta-invites/page.tsx @@ -0,0 +1,92 @@ +"use client"; + +import { useState } from "react"; +import { Button, Field, Input } from "@/components/ui/form"; + +export default function AdminBetaInvitesPage() { + const [dryRun, setDryRun] = useState(true); + const [limit, setLimit] = useState("1"); + const [tokenMinutes, setTokenMinutes] = useState("30"); + const [loading, setLoading] = useState(false); + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + + async function runInvites() { + setLoading(true); + setError(null); + setResult(null); + + try { + const qs = new URLSearchParams({ + dryRun: String(dryRun), + limit: String(limit || "0"), + tokenMinutes: String(tokenMinutes || "30"), + }); + + const res = await fetch(`/api/admin/beta-invites?${qs.toString()}`, { + method: "POST", + }); + + const data = await res.json().catch(() => ({})); + if (!res.ok) throw new Error(data?.message || data?.error || "Request failed"); + + setResult(data); + } catch (e: any) { + setError(e?.message ?? "Failed"); + } finally { + setLoading(false); + } + } + + return ( +
+
+

Send Beta Invites

+

+ Runs the backend batch invite sender (uses your email templates + tracking). +

+
+ +
+ + +
+ + setLimit(e.target.value)} /> + + + + setTokenMinutes(e.target.value)} + /> + +
+ + + + {error && ( +
+ {error} +
+ )} + + {result && ( +
+            {JSON.stringify(result, null, 2)}
+          
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/app/admin/layout.tsx b/app/admin/layout.tsx index f7db1ea..2b615f9 100644 --- a/app/admin/layout.tsx +++ b/app/admin/layout.tsx @@ -29,6 +29,8 @@ const navItems = [ { label: "Settings", href: "/admin/settings", icon: }, { label: "List Emails", href: "/admin/email", icon: }, { label: "Send an Email", href: "/admin/email/send", icon: }, + { label: "Beta Invites", href: "/admin/beta-invites", icon: }, + ]; export default function AdminLayout({ diff --git a/app/api/admin/beta-invites/route.ts b/app/api/admin/beta-invites/route.ts new file mode 100644 index 0000000..596d2ce --- /dev/null +++ b/app/api/admin/beta-invites/route.ts @@ -0,0 +1,35 @@ +import { NextResponse } from "next/server"; +import { cookies } from "next/headers"; + +const API_BASE_URL = + process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080"; + +// POST /api/admin/beta-invites?dryRun=true&limit=10&tokenMinutes=30 +export async function POST(req: Request) { + const token = cookies().get("bb_access_token")?.value; + + if (!token) { + return NextResponse.json({ error: "Missing admin token" }, { status: 401 }); + } + + const url = new URL(req.url); + const dryRun = url.searchParams.get("dryRun") ?? "true"; + const limit = url.searchParams.get("limit") ?? "0"; + const tokenMinutes = url.searchParams.get("tokenMinutes") ?? "30"; + + const upstream = `${API_BASE_URL}/api/v1/admin/beta/invites/send?dryRun=${dryRun}&limit=${limit}&tokenMinutes=${tokenMinutes}`; + + const res = await fetch(upstream, { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + }, + cache: "no-store", + }); + + const text = await res.text(); + return new NextResponse(text, { + status: res.status, + headers: { "Content-Type": res.headers.get("content-type") ?? "application/json" }, + }); +} \ No newline at end of file diff --git a/app/builds/[buildId]/page.tsx b/app/builds/[buildId]/page.tsx new file mode 100644 index 0000000..b98e234 --- /dev/null +++ b/app/builds/[buildId]/page.tsx @@ -0,0 +1,66 @@ +import Link from "next/link"; +import { notFound } from "next/navigation"; + +const API_BASE_URL = + process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080"; + +export default async function BuildBreakdownPage({ + params, +}: { + params: { buildId: string }; +}) { + // You may need to add/confirm this endpoint in backend: + // GET /api/v1/builds/{uuid} + const res = await fetch(`${API_BASE_URL}/api/v1/builds/${params.buildId}`, { + cache: "no-store", + }); + + if (res.status === 404) return notFound(); + + const build = await res.json().catch(() => null); + if (!build) return notFound(); + + return ( +
+ + ← Back to Community Builds + + +
+

+ {build.title ?? "Untitled build"} +

+ + {build.description && ( +

{build.description}

+ )} + +
+

Parts

+ + {/* Adjust keys to match your API payload */} +
+ {(build.items ?? build.buildItems ?? []).map((it: any) => ( +
+
+
+ {it.product?.name ?? it.name ?? "Part"} +
+
+ {it.slot ?? it.partRole ?? "—"} +
+
+
+ x{it.quantity ?? 1} +
+
+ ))} +
+
+
+
+ ); +} \ No newline at end of file diff --git a/components/AdminLeftNavigation.tsx b/components/AdminLeftNavigation.tsx index 27457b9..6952d79 100644 --- a/components/AdminLeftNavigation.tsx +++ b/components/AdminLeftNavigation.tsx @@ -38,6 +38,8 @@ const defaultNavItems: AdminNavItem[] = [ { label: "Settings", href: "/admin/settings", icon: }, { label: "Manage Emails", href: "/admin/email/manage", icon: }, { label: "Send an Email", href: "/admin/email/send", icon: }, + { label: "Beta Invites", href: "/admin/beta-invites", icon: , + }, ]; export default function AdminLeftNavigation({ diff --git a/middleware.ts b/middleware.ts index a78fdc1..9034512 100644 --- a/middleware.ts +++ b/middleware.ts @@ -18,6 +18,9 @@ const PUBLIC_FILE = export function middleware(req: NextRequest) { const { pathname } = req.nextUrl; + console.log("LAUNCH_ONLY_ROOT:", process.env.NEXT_PUBLIC_LAUNCH_ONLY_ROOT); + + // ========================= // 1) Admin gate (always on) // ========================= @@ -55,10 +58,10 @@ if (pathname.startsWith("/admin")) { if (ALWAYS_ALLOW.has(pathname)) return NextResponse.next(); + return NextResponse.rewrite(new URL("/404", req.url)); } - ` 1` export const config = { matcher: [