From 1aafd08af09953056de181c7d321bfd0649d67b6 Mon Sep 17 00:00:00 2001 From: Sean Date: Tue, 31 Mar 2026 14:15:16 -0400 Subject: [PATCH] bug fixes. remove builds page for now --- app/api/feedback/route.ts | 26 +- app/builds/[buildId]/page.tsx | 406 +--------------------------- app/builds/page.tsx | 481 +--------------------------------- components/Footer.tsx | 6 +- components/TopNav.tsx | 1 - 5 files changed, 13 insertions(+), 907 deletions(-) diff --git a/app/api/feedback/route.ts b/app/api/feedback/route.ts index f2298e2..53a806e 100644 --- a/app/api/feedback/route.ts +++ b/app/api/feedback/route.ts @@ -1,6 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' - -const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL +import { springProxy } from '@/lib/springProxy' export async function POST(req: NextRequest) { let body: unknown @@ -16,23 +15,8 @@ export async function POST(req: NextRequest) { 'unknown' const userAgent = req.headers.get('user-agent') ?? 'unknown' - try { - const res = await fetch(`${API_BASE_URL}/api/v1/feedback`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ ...(body as object), ipAddress: ip, userAgent }), - cache: 'no-store', - }) - - if (!res.ok) { - const text = await res.text().catch(() => '') - console.error('[/api/feedback] Spring Boot error:', res.status, text) - return NextResponse.json({ error: 'Failed to submit' }, { status: res.status }) - } - - return NextResponse.json({ ok: true }) - } catch (e) { - console.error('[/api/feedback] proxy error:', e) - return NextResponse.json({ error: 'Service unavailable' }, { status: 500 }) - } + return springProxy(req, '/api/v1/feedback', { + method: 'POST', + body: { ...(body as object), ipAddress: ip, userAgent }, + }) } diff --git a/app/builds/[buildId]/page.tsx b/app/builds/[buildId]/page.tsx index 64d06ad..90d359d 100644 --- a/app/builds/[buildId]/page.tsx +++ b/app/builds/[buildId]/page.tsx @@ -1,404 +1,6 @@ -// app/builds/[buildId]/page.tsx -import Link from "next/link"; -import { notFound } from "next/navigation"; -import { ProductImage } from "@/components/parts/ProductImage"; +import { redirect } from "next/navigation"; -const API_BASE_URL = - process.env.NEXT_PUBLIC_API_BASE_URL ?? ""; - -function formatMoney(n?: number | null) { - if (n == null) return "—"; - return new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - maximumFractionDigits: 0, - }).format(n); -} - -function timeAgo(iso?: string | null) { - if (!iso) return ""; - const d = new Date(iso); - const diff = Math.max(0, Date.now() - d.getTime()); - const mins = Math.floor(diff / 60000); - if (mins < 60) return `${mins}m ago`; - const hrs = Math.floor(mins / 60); - if (hrs < 24) return `${hrs}h ago`; - const days = Math.floor(hrs / 24); - return `${days}d ago`; -} - -export default async function BuildBreakdownPage({ - params, -}: { - params: Promise<{ buildId: string }>; -}) { - const { buildId } = await params; - - // Public detail endpoint - use Next.js API route - const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'; - const res = await fetch(`${baseUrl}/api/builds/public/${buildId}`, { - cache: "no-store", - }); - - if (res.status === 404) return notFound(); - - const build = await res.json().catch(() => null); - if (!build) return notFound(); - - const items: any[] = build.items ?? []; - - const total = items.reduce((sum, it) => { - const qty = Number(it.quantity ?? 1); - const price = it.bestPrice == null ? 0 : Number(it.bestPrice); - return sum + qty * price; - }, 0); - - const images: any[] = build.images ?? build.photos ?? build.media ?? []; - - return ( -
- {/* Header crumb */} -
- - ← Back to Community Builds - - -
- - Public Build - - {build.updatedAt && ( - - updated {timeAgo(build.updatedAt)} - - )} -
-
- - {/* Layout */} -
- {/* Post column */} -
- {/* “Reddit-ish” Post Card */} -
-
- {/* Vote rail */} -
- -
- -
- - {/* Post body */} -
- {/* Meta */} -
- - r/BattlBuilders - - - posted by - u/anonymous - {build.createdAt && ( - <> - - {timeAgo(build.createdAt)} - - )} -
- - {/* Title */} -

- {build.title ?? "Untitled Build"} -

- - {/* Description */} - {build.description ? ( -

- {build.description} -

- ) : ( -

- {/* placeholder copy */} - Field notes coming soon. This build breakdown will include - purpose, tradeoffs, and why each part made the cut. -

- )} - - {/* Action bar */} -
- - - - -
- Est. Total - - {formatMoney(total)} - -
-
-
-
-
- - {/* Parts card */} -
-
-

Parts

-
{items.length} items
-
- -
- {items.map((it) => ( -
- {/* Thumb */} -
- -
- - {/* Info */} -
-
- {it.productName ?? "Part"} -
-
- - {it.slot ?? "slot"} - - {it.productBrand && {it.productBrand}} - - x{it.quantity ?? 1} -
-
- - {/* Price */} -
-
- {formatMoney(it.bestPrice)} -
- -
-
- ))} -
-
- - {/* Gallery */} -
-
-

- Gallery -

- user submitted -
- {images.length === 0 ? ( -
-

- No photos yet. Be the first to add a build pic. -

-

- (Upload UI coming soon — we’ll store and moderate images - per build.) -

- - {/* Mock thumbnails */} -
- {Array.from({ length: 8 }).map((_, i) => ( -
- IMG -
- ))} -
- - -
- ) : ( -
- {/* “Hero” preview */} -
- {/* eslint-disable-next-line @next/next/no-img-element */} - Build photo -
- - {/* Thumbs */} -
- {images.slice(0, 12).map((img, idx) => ( -
- {/* eslint-disable-next-line @next/next/no-img-element */} - {`Build -
- ))} -
- -
- - - - {images.length} photos - -
-
- )} -
- - {/* Comments (placeholder) */} -
-
-

Comments

- mocked -
- -
- {[ - { - user: "u/gearhead", - body: "Solid parts list. Any reason you went with that upper over a BCM?", - }, - { - user: "u/OP", - body: "Mostly availability + price. I’ll probably swap once I track deals for a week.", - }, - { - user: "u/boltcarrier", - body: "Would love to see this with a pinned/weld option for 14.5 builds.", - }, - ].map((c, idx) => ( -
-
- {c.user === "u/OP" ? "OP" : "u"} -
-
-
- {c.user} • 1h ago -
-
- {c.body} -
-
- - - -
-
-
- ))} - -
- Commenting is coming soon. This will become the “breakdown” - thread for the build (notes, tradeoffs, deal alerts, revisions). -
- -
-
-
- - {/* Sidebar */} - -
-
- ); +// Community Builds is temporarily disabled — redirect to home until ready. +export default function BuildDetailPage() { + redirect("/"); } diff --git a/app/builds/page.tsx b/app/builds/page.tsx index 60488c9..ac68d62 100644 --- a/app/builds/page.tsx +++ b/app/builds/page.tsx @@ -1,481 +1,6 @@ -"use client"; - -/** - * app/builds/page.tsx - * Community Builds feed (public) - * - * Dev Notes: - * - We are moving to Option B: - * Build (core) + BuildProfile (meta) + Votes/Comments/Media as separate tables. - * - This page expects a lightweight "feed card" DTO from the backend, NOT full build items. - * - Keep UI/filters here client-side for MVP; move to server-side query params later if needed. - * - * Backend (target contract): - * GET /api/v1/builds?limit=50 -> BuildFeedCardDto[] - * (Optional later) POST /api/v1/builds/{uuid}/vote { delta: 1 | -1 } - */ - -import { useEffect, useMemo, useState, useCallback } from "react"; -import Link from "next/link"; - -type BuildClass = "Rifle" | "Pistol" | "NFA"; - -/** - * Keep this loose. Backend will control the allowed calibers via BuildProfile. - * We can tighten once the controlled vocab is finalized. - */ -type Caliber = string; - -/** - * This matches what the UI needs (card format). - * It maps cleanly to: builds + build_profiles + aggregated votes (+ optional price). - */ -type BuildCard = { - id: string; // we use uuid here (public identifier) - title: string; - slug: string; // can be uuid for now; keep property so UI does not change - creator: string; // placeholder until user profiles are real - caliber: Caliber; - buildClass: BuildClass; - price: number; // optional server-side later; for now allow 0 fallback - votes: number; - tags: string[]; - coverImageUrl?: string | null; -}; - -type BuildFeedCardDto = { - uuid: string; - title?: string | null; - slug?: string | null; // optional (we can generate from title later) - creator?: string | null; // optional - caliber?: string | null; - buildClass?: BuildClass | null; - price?: number | null; - votes?: number | null; - tags?: string[] | null; - coverImageUrl?: string | null; -}; - -const API_BASE_URL = - process.env.NEXT_PUBLIC_API_BASE_URL ?? ""; - -// --- UI Filter Sets (keep; we’ll generate options from data too) --- -const CLASS_FILTERS: (BuildClass | "all")[] = ["all", "Rifle", "Pistol", "NFA"]; - -const PRICE_FILTERS = [ - { id: "all", label: "All", min: 0, max: Infinity }, - { id: "sub1k", label: "< $1k", min: 0, max: 1000_00 }, - { id: "1to2k", label: "$1k–$2k", min: 1000_00, max: 2000_00 }, - { id: "2to3k", label: "$2k–$3k", min: 2000_00, max: 3000_00 }, - { id: "3kplus", label: "$3k+", min: 3000_00, max: Infinity }, -]; - -function safeArray(v: unknown): string[] { - return Array.isArray(v) ? v.filter(Boolean).map(String) : []; -} - -function fallbackSlug(dto: BuildFeedCardDto) { - // MVP: stable route id is uuid. Keep a "slug" field for UI continuity. - return dto.slug?.trim() || dto.uuid; -} - -function normalizeCard(dto: BuildFeedCardDto): BuildCard { - const title = (dto.title ?? "Untitled Build").trim() || "Untitled Build"; - - return { - id: dto.uuid, - title, - slug: fallbackSlug(dto), - - // Until we wire real users: keep a placeholder creator string. - creator: (dto.creator ?? "anonymous").trim() || "anonymous", - - caliber: (dto.caliber ?? "—").trim() || "—", - - // Default to Rifle for display if missing; backend should set this via profile. - buildClass: (dto.buildClass ?? "Rifle") as BuildClass, - - // Price is optional (we can compute later from BuildItems + offers). - price: typeof dto.price === "number" ? dto.price : 0, - - votes: typeof dto.votes === "number" ? dto.votes : 0, - - tags: safeArray(dto.tags), - - coverImageUrl: dto.coverImageUrl ?? null, - }; -} +import { redirect } from "next/navigation"; +// Community Builds is temporarily disabled — redirect to home until ready. export default function BuildsPage() { - // --- Remote data state --- - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [builds, setBuilds] = useState([]); - - // --- UI state (filters/votes) --- - const [caliberFilter, setCaliberFilter] = useState("all"); - const [classFilter, setClassFilter] = useState("all"); - const [priceFilterId, setPriceFilterId] = useState("all"); - - // Local vote state for optimistic UI - const [votes, setVotes] = useState>({}); - - const activePriceFilter = - PRICE_FILTERS.find((p) => p.id === priceFilterId) ?? PRICE_FILTERS[0]; - - // --- Fetch public builds feed --- - useEffect(() => { - let cancelled = false; - - async function run() { - try { - setLoading(true); - setError(null); - - const res = await fetch('/api/builds/public?limit=50', { - method: "GET", - credentials: "include", - headers: { "Content-Type": "application/json" }, - }); - - if (!res.ok) { - const txt = await res.text().catch(() => ""); - throw new Error(txt || `Failed to load builds (${res.status})`); - } - - const data = (await res.json()) as BuildFeedCardDto[]; - const cards = (Array.isArray(data) ? data : []).map(normalizeCard); - - if (cancelled) return; - - setBuilds(cards); - - // Initialize vote UI from payload totals - setVotes(Object.fromEntries(cards.map((b) => [b.id, b.votes]))); - } catch (e: any) { - if (!cancelled) setError(e?.message || "Failed to load builds feed"); - } finally { - if (!cancelled) setLoading(false); - } - } - - run(); - return () => { - cancelled = true; - }; - }, []); - - // Build the caliber filters dynamically from data, but keep "all" first. - const CALIBER_FILTERS = useMemo<(Caliber | "all")[]>(() => { - const uniq = new Set(); - for (const b of builds) { - const c = (b.caliber ?? "").trim(); - if (c && c !== "—") uniq.add(c); - } - return ["all", ...Array.from(uniq).sort()]; - }, [builds]); - - const filteredBuilds = useMemo(() => { - return builds.filter((build) => { - const matchesCaliber = - caliberFilter === "all" || build.caliber === caliberFilter; - - const matchesClass = - classFilter === "all" || build.buildClass === classFilter; - - const matchesPrice = - build.price >= activePriceFilter.min && - build.price < activePriceFilter.max; - - return matchesCaliber && matchesClass && matchesPrice; - }); - }, [builds, caliberFilter, classFilter, activePriceFilter]); - - const handleVote = useCallback((id: string, delta: 1 | -1) => { - // MVP: optimistic local vote only. - // Later: POST /api/v1/builds/{uuid}/vote { delta } and reconcile. - setVotes((prev) => ({ - ...prev, - [id]: (prev[id] ?? 0) + delta, - })); - }, []); - - return ( -
-
- {/* Header */} -
-
-

- Battl Builder · Build Book -

-

- Community Builds -

-

- Browse community builds, vote them up or down, and steal good - ideas without shame. This feed is backed by public builds - (isPublic=true). -

-
- -
- - Open Builder - - - {/* Later: route to /vault + choose build to submit, or direct /builder submit flow */} - - + Submit Build - -
-
- - {/* Loading / Error */} - {loading && ( -
- Loading community builds… -
- )} - - {error && !loading && ( -
- {error} -
- Dev tip: confirm backend route{" "} - GET /api/v1/builds is - implemented + CORS is configured. -
-
- )} - - {/* Filters */} -
-
-
-

- Filter Builds -

-

- Filter by caliber, class, and price range. (Client-side for - MVP.) -

-
- -
- Showing{" "} - - {filteredBuilds.length} - {" "} - of{" "} - {builds.length}{" "} - builds -
-
- -
- {/* Caliber filter */} -
-
- Caliber -
-
- {CALIBER_FILTERS.map((caliber) => ( - - ))} -
-
- - {/* Class filter */} -
-
- Class -
-
- {CLASS_FILTERS.map((cls) => ( - - ))} -
-
- - {/* Price filter */} -
-
- Price Range -
-
- {PRICE_FILTERS.map((range) => ( - - ))} -
-
-
-
- - {/* Build list */} -
-
- {filteredBuilds.map((build) => { - const dollars = Math.floor((build.price ?? 0) / 100); - - return ( -
- {/* Votes column */} -
- -
- {votes[build.id] ?? build.votes} -
- -
- - {/* Thumbnail */} -
-
- {/* eslint-disable-next-line @next/next/no-img-element */} - {`${build.title} -
-
- - {/* Main content */} -
-
-
-
- {build.buildClass} · {build.caliber} -
-

- - {build.title} - -

-

- Posted by{" "} - - b/{build.creator} - {" "} - · Build detail page will show the parts list + - comments. -

-
- -
-
- Est. Build Cost -
-
- {build.price > 0 ? ( - - ${Math.floor(build.price / 100).toLocaleString()} - - ) : ( - "—" - )} -
-
-
- -
- {build.tags.map((tag) => ( - - {tag} - - ))} - - - Comments + save coming soon. - -
-
-
- ); - })} - - {!loading && !error && filteredBuilds.length === 0 && ( -
- No builds match those filters yet. -
- )} -
-
-
-
- ); + redirect("/"); } diff --git a/components/Footer.tsx b/components/Footer.tsx index f30b8b5..61737d8 100644 --- a/components/Footer.tsx +++ b/components/Footer.tsx @@ -37,11 +37,7 @@ export function Footer() { Builder -
  • - - Community Builds - -
  • +
  • My Builds diff --git a/components/TopNav.tsx b/components/TopNav.tsx index ab46241..4f3724a 100644 --- a/components/TopNav.tsx +++ b/components/TopNav.tsx @@ -64,7 +64,6 @@ export function TopNav() { {/* Right side actions */}