the cleanup

This commit is contained in:
2026-01-23 21:40:16 -05:00
parent 2ea1f19863
commit c4f69c0811
44 changed files with 183 additions and 42 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
NEXT_PUBLIC_API_BASE_URL=http://localhost:8080
NEXT_PUBLIC_API_BASE_URL=http://battlbuilder-api:8080
# Middleware to limited site to only root page
NEXT_PUBLIC_LAUNCH_ONLY_ROOT=false
+1 -1
View File
@@ -75,7 +75,7 @@ const isUuid = (v: string) =>
);
const API_BASE_URL =
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
const STORAGE_KEY = "gunbuilder-build-state";
@@ -49,7 +49,7 @@ type GunbuilderProductFromApi = {
type BuildState = Partial<Record<BuilderSlotKey, string>>;
const API_BASE_URL =
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
const STORAGE_KEY = "gunbuilder-build-state";
+1 -1
View File
@@ -19,7 +19,7 @@ type BuildDto = {
};
const API_BASE_URL =
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
// UUID validator (defensive)
const isUuid = (v: string) =>
+1 -1
View File
@@ -5,7 +5,7 @@ import { Button, Field, Input } from "@/components/ui/form";
import { useAuth } from "@/context/AuthContext";
const API_BASE_URL =
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
type AdminBetaRequestDto = {
id: number;
+2 -2
View File
@@ -38,7 +38,7 @@ const DEFAULT_PLATFORM = "AR-15";
// Update these if your admin mapping UI lives elsewhere.
const MAPPING_UI_PATH = "/admin/mapping"; // <- change if needed
const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
const url = `${API_BASE}/admin/classification/reconcile?dryRun=true&limit=500&platform=AR-15&merchantId=4`;
@@ -80,7 +80,7 @@ export default function AdminClassificationReconcilePage() {
if (merchantId?.trim()) params.set("merchantId", merchantId.trim());
if (includeLocked) params.set("includeLocked", "true");
const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
const url = `${API_BASE}/admin/classification/reconcile?${params.toString()}`;
const res = await fetch(url, {
@@ -29,7 +29,7 @@ type QueueItem = {
};
const API_BASE =
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
function getAuthHeaders(): HeadersInit {
if (typeof window === "undefined") return {};
+1 -1
View File
@@ -4,7 +4,7 @@ import { useEffect, useState } from "react";
import Link from "next/link";
const API_BASE_URL =
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
const formatCount = (value: number | null | undefined) =>
(typeof value === "number" ? value : 0).toLocaleString();
+1 -1
View File
@@ -4,7 +4,7 @@ import { useEffect, useMemo, useState } from "react";
import { useSearchParams, useRouter } from "next/navigation";
const API_BASE_URL =
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
/**
* Tabs:
+1 -1
View File
@@ -3,7 +3,7 @@
import { useEffect, useState } from "react";
const API_BASE_URL =
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
type MerchantAdminDto = {
id: number;
+1 -1
View File
@@ -4,7 +4,7 @@ import { useEffect, useState } from "react";
import Link from "next/link";
const API_BASE_URL =
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
type AdminOverview = {
totalProducts: number;
+1 -1
View File
@@ -1,5 +1,5 @@
export const API_BASE =
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
export function getCookie(name: string): string | null {
if (typeof document === "undefined") return null;
+1 -1
View File
@@ -7,7 +7,7 @@ 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";
process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
export default function AdminUsersPage() {
const { getAuthHeaders } = useAuth();
+1 -1
View File
@@ -2,7 +2,7 @@ import { NextResponse } from "next/server";
import { cookies } from "next/headers";
const API_BASE_URL =
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
// POST /api/admin/beta-invites?dryRun=true&limit=10&tokenMinutes=30
export async function POST(req: Request) {
+1 -1
View File
@@ -2,7 +2,7 @@
import { NextResponse } from "next/server";
const API_BASE_URL =
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
const TOS_VERSION = "2025-12-27";
+1 -1
View File
@@ -1,7 +1,7 @@
import { NextResponse } from "next/server";
const API_BASE_URL =
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
export async function POST(req: Request) {
const body = await req.text().catch(() => "");
+1 -1
View File
@@ -1,7 +1,7 @@
import { NextResponse } from "next/server";
const API_BASE_URL =
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
function passthroughHeaders(req: Request) {
const h = new Headers();
+1 -1
View File
@@ -1,7 +1,7 @@
import { NextResponse } from "next/server";
const API_BASE_URL =
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
export async function GET(req: Request) {
const url = new URL(req.url);
+18
View File
@@ -0,0 +1,18 @@
import { springProxy } from "@/lib/springProxy";
import type {
MerchantAdminResponse,
MerchantAdminUpdateRequest,
} from "@/types/merchant-admin";
export async function PUT(
req: Request,
{ params }: { params: { id: string } }
) {
const body = (await req.json()) as MerchantAdminUpdateRequest;
return springProxy<MerchantAdminResponse, MerchantAdminUpdateRequest>(
req,
`/api/v1/admin/merchants/${params.id}`,
{ method: "PUT", body }
);
}
+19
View File
@@ -0,0 +1,19 @@
import { springProxy } from "@/lib/springProxy";
import type {
MerchantAdminCreateRequest,
MerchantAdminResponse,
} from "@/types/merchant-admin";
export async function GET(req: Request) {
return springProxy<MerchantAdminResponse[]>(req, "/api/v1/admin/merchants");
}
export async function POST(req: Request) {
const body = (await req.json()) as MerchantAdminCreateRequest;
return springProxy<MerchantAdminResponse, MerchantAdminCreateRequest>(
req,
"/api/v1/admin/merchants",
{ method: "POST", body }
);
}
+1 -1
View File
@@ -6,7 +6,7 @@ import { useRouter, useSearchParams } from "next/navigation";
import { useAuth } from "@/context/AuthContext";
const API_BASE_URL =
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
function BetaConfirmPageContent() {
const params = useSearchParams();
+1 -1
View File
@@ -27,7 +27,7 @@ function BetaMagicPageContent() {
(async () => {
try {
const res = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080"}/api/auth/magic/exchange`, {
const res = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://battlbuilder-api:8080"}/api/auth/magic/exchange`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token }),
+1 -1
View File
@@ -3,7 +3,7 @@ import Link from "next/link";
import { notFound } from "next/navigation";
const API_BASE_URL =
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
function formatMoney(n?: number | null) {
if (n == null) return "—";
+1 -1
View File
@@ -57,7 +57,7 @@ type BuildFeedCardDto = {
};
const API_BASE_URL =
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
// --- UI Filter Sets (keep; well generate options from data too) ---
const CLASS_FILTERS: (BuildClass | "all")[] = ["all", "Rifle", "Pistol", "NFA"];
+1 -1
View File
@@ -8,7 +8,7 @@ import { useAuth } from "@/context/AuthContext";
import { Button, Field, Input } from "@/components/ui/form";
const API_BASE_URL =
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
function LoginPageContent() {
const router = useRouter();
+1 -1
View File
@@ -59,7 +59,7 @@ type GunbuilderProductFromApi = {
};
const API_BASE_URL =
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
const PAGE_SIZE = 24;
+1 -1
View File
@@ -5,7 +5,7 @@ import Link from "next/link";
import PlatformSwitcher from "./PlatformSwitcher";
import Image from "next/image";
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
type ProductListDto = {
id: string; // your API returns strings sometimes; keep it flexible
+1 -1
View File
@@ -4,7 +4,7 @@ import { useEffect, useMemo, useState } from "react";
import Link from "next/link";
import PlatformSwitcher from "./PlatformSwitcher";
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
type ProductDetailDto = {
id: string;
+1 -1
View File
@@ -10,7 +10,7 @@ import {
} from "react";
const API_BASE_URL =
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
type AuthUser = {
uuid: string;
+3 -1
View File
@@ -8,11 +8,13 @@ WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
ENV NODE_ENV=production
ENV NEXT_PUBLIC_API_BASE_URL=http://battlbuilder-api:8080
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
+1 -1
View File
@@ -4,7 +4,7 @@ import { useMemo } from "react";
import { useAuth } from "@/context/AuthContext";
const API_BASE_URL =
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
type JsonValue = any;
+1 -1
View File
@@ -1,7 +1,7 @@
// lib/api/adminCategoryMappings.ts
const API_BASE =
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
export interface AdminCategory {
id: number;
+1 -1
View File
@@ -1,5 +1,5 @@
const API_BASE =
process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8080";
process.env.NEXT_PUBLIC_API_URL ?? "";
export interface AdminMerchantCategoryMapping {
id: number;
+1 -1
View File
@@ -17,7 +17,7 @@ export interface UpdateEmailRequestDto {
status?: string;
}
const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8080';
const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL || '';
export async function fetchEmailRequests(token: string): Promise<EmailRequest[]> {
const response = await fetch(`${API_BASE}/api/email`, {
+1 -1
View File
@@ -15,7 +15,7 @@ export type CreatePlatformDto = {
export type UpdatePlatformDto = Partial<CreatePlatformDto>;
const API_BASE =
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
function normalizePlatform(raw: any): Platform {
const id = Number(raw?.id);
+1 -1
View File
@@ -1,7 +1,7 @@
// lib/auth-client.ts
const API_BASE_URL =
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
export type AuthUser = {
id: number;
+1 -1
View File
@@ -1,6 +1,6 @@
// app/lib/catalog.ts
export const API_BASE_URL =
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
export type ProductListItem = {
id: string;
+1 -1
View File
@@ -1,7 +1,7 @@
// Central place for app-wide constant values.
// Branding / product
export const APP_NAME = "Ballistic Builder";
export const APP_NAME = "Battl Builder";
export const APP_TAGLINE = "A smarter way to explore and assemble builds.";
// URLs
+68
View File
@@ -0,0 +1,68 @@
import { NextResponse } from "next/server";
const SPRING_BASE =
process.env.SPRING_BASE_URL ?? "http://battlbuilder-api:8080";
function forwardHeaders(req: Request) {
const headers = new Headers();
const auth = req.headers.get("authorization");
const cookie = req.headers.get("cookie");
if (auth) headers.set("authorization", auth);
if (cookie) headers.set("cookie", cookie);
headers.set("accept", "application/json");
return headers;
}
type SpringProxyOptions<TBody> = {
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
body?: TBody;
cache?: RequestCache;
};
export async function springProxy<TResponse, TBody = never>(
req: Request,
path: string,
options?: SpringProxyOptions<TBody>
) {
const url = `${SPRING_BASE}${path}`;
const headers = forwardHeaders(req);
const init: RequestInit = {
method: options?.method ?? "GET",
headers,
cache: options?.cache ?? "no-store",
};
if (options && "body" in options && options.body !== undefined) {
headers.set("content-type", "application/json");
init.body = JSON.stringify(options.body);
}
const res = await fetch(url, init);
// If Spring returns 204 No Content, don't try to parse JSON
if (res.status === 204) {
return new NextResponse(null, { status: 204 });
}
const contentType = res.headers.get("content-type") ?? "";
let payload: unknown;
if (contentType.includes("application/json")) {
payload = await res.json().catch(() => null);
} else {
payload = await res.text().catch(() => null);
}
// If Spring returned JSON, we respond JSON; otherwise text
if (typeof payload === "string") {
return new NextResponse(payload, {
status: res.status,
headers: { "content-type": contentType || "text/plain" },
});
}
return NextResponse.json(payload as TResponse, { status: res.status });
}
+1 -1
View File
@@ -4,7 +4,7 @@ import { useAuth } from "@/context/AuthContext";
import { useCallback, useMemo } from "react";
const DEFAULT_API_BASE =
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
export function useApi(baseUrl: string = DEFAULT_API_BASE) {
const { token } = useAuth();
+3 -3
View File
@@ -10,14 +10,14 @@ const nextConfig = {
ignoreDuringBuilds: true,
},
async rewrites() {
/* async rewrites() {
return [
{
source: "/api/:path*",
destination: "http://localhost:8080/api/:path*",
destination: "http://battlbuilder-api:8080/api/:path*",
},
];
},
},*/
};
export default nextConfig;
+1 -1
View File
@@ -10,7 +10,7 @@ TAG=$(git rev-parse --short HEAD)
FULL_IMAGE="$REGISTRY/$OWNER/$IMAGE"
echo "Building $FULL_IMAGE:$TAG"
docker build -f frontend/Dockerfile -t $FULL_IMAGE:$TAG .
docker build -f frontend/Dockerfile --no-cache -t $FULL_IMAGE:$TAG .
echo "Tagging latest"
docker tag $FULL_IMAGE:$TAG $FULL_IMAGE:latest
+24
View File
@@ -0,0 +1,24 @@
export type MerchantAdminResponse = {
id: number;
name: string;
avantlinkMid: string | null;
feedUrl: string | null;
offerFeedUrl: string | null;
isActive: boolean;
lastFullImportAt: string | null;
lastOfferSyncAt: string | null;
};
export type MerchantAdminCreateRequest = {
name: string;
avantlinkMid?: string | null;
feedUrl?: string | null;
offerFeedUrl?: string | null;
isActive?: boolean | null;
};
export type MerchantAdminUpdateRequest = {
feedUrl?: string | null;
offerFeedUrl?: string | null;
isActive?: boolean | null;
};
+10
View File
@@ -0,0 +1,10 @@
export type MerchantAdminDto = {
id?: number; // Optional for create payloads
name: string;
avantlinkMid?: string | null;
feedUrl?: string | null;
offerFeedUrl?: string | null;
isActive?: boolean | null;
lastFullImportAt?: string | null; // Spring Instant/LocalDateTime serialized as string
lastOfferSyncAt?: string | null;
};