Compare commits
34 Commits
bb-web-app
...
47d5d17d29
| Author | SHA1 | Date | |
|---|---|---|---|
| 47d5d17d29 | |||
| c686da0c03 | |||
| 680d2b21a1 | |||
| c86d49f317 | |||
| b8a0fd1963 | |||
| 00613ebc9f | |||
| 05a4ad2720 | |||
| eb1dfef7b7 | |||
| 10148acaa9 | |||
| 019e1892de | |||
| 668398e66a | |||
| f12e2ffee7 | |||
| 3a345b6c26 | |||
| 754a378782 | |||
| 37921469d5 | |||
| 5526920ff8 | |||
| fb52521495 | |||
| f8265063d5 | |||
| 140b83bd65 | |||
| 775ee0f691 | |||
| 4989bf6de4 | |||
| 4267c34399 | |||
| 6d1697e672 | |||
| 45270f9b18 | |||
| a13687635b | |||
| c2600b6a68 | |||
| b45493f5bd | |||
| cb46430ce4 | |||
| 77c31ae4f9 | |||
| fb2effca47 | |||
| b09ccc542e | |||
| 3aee0e6755 | |||
| e49dc96522 | |||
| 50ef395a38 |
@@ -7,7 +7,3 @@ NEXT_PUBLIC_LAUNCH_ONLY_ROOT=false
|
||||
|
||||
NEXT_PUBLIC_SHORTLINK_BASE_URL=http://localhost:8080
|
||||
|
||||
# Brevo API key for beta sign up collection
|
||||
BREVO_API_KEY=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
|
||||
BREVO_LIST_ID=9
|
||||
|
||||
|
||||
+2
-4
@@ -2,10 +2,8 @@
|
||||
NEXT_PUBLIC_API_BASE_URL=http://battlbuilder-api:8080
|
||||
|
||||
# Middleware to limited site to only root page
|
||||
NEXT_PUBLIC_LAUNCH_ONLY_ROOT=true
|
||||
LAUNCH_ONLY_ROOT=false
|
||||
|
||||
NEXT_PUBLIC_SHORTLINK_BASE_URL=https://battl.build
|
||||
|
||||
# Brevo API key for beta sign up collection
|
||||
BREVO_API_KEY=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
|
||||
BREVO_LIST_ID=9
|
||||
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
test:
|
||||
deploy:
|
||||
runs-on: ubuntu
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: echo "Hello from Gitea Actions"
|
||||
|
||||
- name: Pull latest images
|
||||
run: |
|
||||
docker pull gitea.gofwd.group/forward_group/ballistic-builder-spring/spring-api:latest
|
||||
docker pull gitea.gofwd.group/sean/shadow-gunbuilder-ai-proto/webui:latest
|
||||
|
||||
- name: Recreate containers with new images
|
||||
run: docker compose up -d
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="db-tree-configuration">
|
||||
<option name="data" value="" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,68 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
npm run dev # Start dev server at localhost:3000
|
||||
npm run build # Production build (TS errors and ESLint are suppressed — see next.config.mjs)
|
||||
npm run lint # ESLint check
|
||||
```
|
||||
|
||||
No test framework is configured.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
`.env.local` for development:
|
||||
```
|
||||
NEXT_PUBLIC_API_BASE_URL=http://localhost:8080
|
||||
BACKEND_BASE_URL=http://localhost:8080
|
||||
LAUNCH_ONLY_ROOT=false # Set to "true" to restrict unauthenticated users to root page only
|
||||
NEXT_PUBLIC_SHORTLINK_BASE_URL=http://localhost:8080
|
||||
```
|
||||
|
||||
In Docker, the Spring Boot service is resolved as `http://battlbuilder-api:8080` (the default in `lib/springProxy.ts` when `SPRING_BASE_URL` is unset).
|
||||
|
||||
## Architecture
|
||||
|
||||
### Two backends
|
||||
1. **Spring Boot** — primary data backend. All product catalog, builds, auth, merchant/admin data lives here.
|
||||
2. **Sanity CMS** — content only (guides/blog posts). Project `zal102rv`, dataset `production`. Queries in `lib/sanity.ts`.
|
||||
|
||||
### API proxy pattern
|
||||
Next.js API routes (`app/api/`) act as a thin proxy to Spring Boot. Client code never calls Spring Boot directly. The helper `lib/springProxy.ts` forwards cookies and auth headers to Spring Boot and handles response serialization.
|
||||
|
||||
### Authentication
|
||||
- **Session cookie**: `session_token` HTTP-only cookie set by Spring Boot, forwarded by the proxy.
|
||||
- **Client state**: `AuthContext` (`context/AuthContext.tsx`) hydrates from `localStorage` on mount, then verifies against `/api/auth/me`.
|
||||
- **Middleware**: `middleware.ts` enforces route protection. Public paths are whitelisted; `/admin/*` requires login; `/builder`, `/builds`, `/vault` are gated only when `LAUNCH_ONLY_ROOT=true`.
|
||||
|
||||
### Route groups
|
||||
- `app/(app)/` — main consumer app layout (TopNav + Footer)
|
||||
- `app/(app)/(builder)/` — builder + parts browser (BuilderNav layout)
|
||||
- `app/(account)/` — account settings pages (AccountChrome sidebar layout)
|
||||
- `app/admin/` — internal admin tools (AdminLeftNavigation layout)
|
||||
- `app/api/` — 50 server-side route handlers proxying to Spring Boot
|
||||
|
||||
### Builder domain model
|
||||
The builder is AR-platform-centric. Key concepts:
|
||||
|
||||
- **`BuilderSlotKey`** (`types/builderSlots.ts`) — the canonical kebab-case category ID (e.g., `"complete-upper"`, `"barrel"`). This is the source of truth for localStorage keys, URL state, and backend part role mapping.
|
||||
- **`CATEGORY_TO_PART_ROLES` / `PART_ROLE_TO_CATEGORY`** (`lib/catalogMappings.ts`) — bidirectional mapping between `BuilderSlotKey` and backend `partRole` strings. Update here when the backend adds new part roles.
|
||||
- **`BUILDER_SLOTS`** (`data/builderSlots.ts`) — defines slot groups (LOWER, UPPER, SYSTEM, ACCESSORIES) and satisfaction patterns (e.g., a complete lower satisfies the lower assembly slot).
|
||||
- **`BuildState`** — `Partial<Record<BuilderSlotKey, string>>` stored in `localStorage`. Product IDs are the values.
|
||||
- **Overlap detection** (`lib/buildOverlaps.ts`) — warns users when they select both a complete assembly and individual sub-parts.
|
||||
|
||||
### Legacy key aliases
|
||||
`types/builderSlots.ts` exports `normalizeSlotKey()` which maps camelCase legacy keys (e.g., `completeUpper`) to current kebab-case keys. This is needed for backwards-compatible URL/localStorage deserialization.
|
||||
|
||||
### API client pattern
|
||||
- Client components use `useApi()` hook (`lib/api.ts`) for fetch calls with `credentials: 'include'`.
|
||||
- Admin-specific API calls live in `lib/api/` (e.g., `adminCategoryMappings.ts`, `adminMerchantMappings.ts`).
|
||||
- Product catalog helpers are in `lib/catalog.ts`.
|
||||
|
||||
### Sanity content
|
||||
- `lib/sanity.ts` — Sanity client + GROQ queries for posts.
|
||||
- `lib/sanityFetch.ts` — low-level HTTPS fetch to Sanity IP (bypasses CDN DNS issues in server/Docker environments).
|
||||
- Guides at `/guides/[slug]` are the only public Sanity-driven pages.
|
||||
@@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useSearchParams, useRouter } from "next/navigation";
|
||||
import { Suspense } from "react";
|
||||
|
||||
const STORAGE_KEY = "gunbuilder-build-state";
|
||||
|
||||
function BuildLoader() {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const encoded = searchParams.get("build");
|
||||
|
||||
if (encoded) {
|
||||
try {
|
||||
const decoded = JSON.parse(atob(encoded));
|
||||
if (decoded && typeof decoded === "object") {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(decoded));
|
||||
}
|
||||
} catch {
|
||||
// Bad payload — just go to builder anyway, it'll load fresh
|
||||
}
|
||||
}
|
||||
|
||||
router.replace("/builder");
|
||||
}, [searchParams, router]);
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-black text-zinc-50 flex items-center justify-center">
|
||||
<p className="text-sm text-zinc-400">Loading build…</p>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default function BuildPage() {
|
||||
return (
|
||||
<Suspense>
|
||||
<BuildLoader />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import { X, Link2, Square, CheckSquare } from "lucide-react";
|
||||
|
||||
import { useApi } from "@/lib/api";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import { trackBuildStarted, trackPartSelected, trackAffiliateLinkClicked, trackBuildShared, trackBuildSaved } from "@/lib/analytics";
|
||||
|
||||
import { CATEGORIES } from "@/data/gunbuilderParts";
|
||||
import { BUILDER_SLOTS, isSlotSatisfied } from "@/data/builderSlots";
|
||||
@@ -74,8 +75,7 @@ const isUuid = (v: string) =>
|
||||
v
|
||||
);
|
||||
|
||||
const API_BASE_URL =
|
||||
process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
|
||||
// API routes now handled by Next.js /api routes (server-side)
|
||||
|
||||
const STORAGE_KEY = "gunbuilder-build-state";
|
||||
|
||||
@@ -296,7 +296,7 @@ function buildAssemblyRows(params: {
|
||||
rows.push({
|
||||
key: `${groupLabel}-${paths.complete}`,
|
||||
kind: "category",
|
||||
label: "Complete Assembly (Fastest)",
|
||||
label: `Completed ${groupLabel.split(" ")[0]} (Fastest)`,
|
||||
categoryId: paths.complete,
|
||||
indent: 0,
|
||||
pill: mode === "complete" ? "Selected" : undefined,
|
||||
@@ -337,8 +337,8 @@ function GunbuilderPageContent() {
|
||||
|
||||
const api = useApi();
|
||||
|
||||
// ✅ Auth: we need the token ready before calling /builds/me/*
|
||||
const { token, loading: authLoading, getAuthHeaders } = useAuth();
|
||||
// ✅ Auth: check if user is logged in
|
||||
const { user, loading: authLoading } = useAuth();
|
||||
|
||||
const [platform, setPlatform] = useState<(typeof PLATFORMS)[number]>("AR15");
|
||||
|
||||
@@ -356,6 +356,8 @@ function GunbuilderPageContent() {
|
||||
// Parts data state
|
||||
// -----------------------------
|
||||
const [parts, setParts] = useState<Part[]>([]);
|
||||
const previousPartIdsRef = useRef<Set<string>>(new Set());
|
||||
const isFirstPartsHydrationRef = useRef(true);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
@@ -370,20 +372,33 @@ function GunbuilderPageContent() {
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
// Hydrate persisted build state only. URL actions (platform/select/remove)
|
||||
// are handled by the dedicated useSearchParams effect later in this file.
|
||||
// Intentional: Object.keys(...).length > 0 tightens the original condition.
|
||||
// Previously, an empty {} from localStorage would call setBuild(normalizeBuildState({})),
|
||||
// which is a no-op. The new check skips that call, which is safe and correct.
|
||||
let hasBuild = false;
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored) {
|
||||
try {
|
||||
const parsed = JSON.parse(stored);
|
||||
if (parsed && typeof parsed === "object") {
|
||||
if (parsed && typeof parsed === "object" && Object.keys(parsed).length > 0) {
|
||||
setBuild(normalizeBuildState(parsed));
|
||||
hasBuild = true;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
// Fire build_started only for genuinely new builds:
|
||||
// - No existing parts in localStorage
|
||||
// - No ?select= param (share-link arrivals have empty localStorage but are not starting fresh)
|
||||
// Use window.location.search directly: this effect runs at mount before any router
|
||||
// processing, making window.location.search the authoritative source of the landing URL.
|
||||
const hasSelectParam = new URLSearchParams(window.location.search).has("select");
|
||||
if (!hasBuild && !hasSelectParam) {
|
||||
trackBuildStarted();
|
||||
}
|
||||
|
||||
// Signal that hydration is complete - this allows URL param processing to run
|
||||
setIsHydrated(true);
|
||||
}, []);
|
||||
@@ -603,6 +618,7 @@ function GunbuilderPageContent() {
|
||||
if (ids.length === 0) {
|
||||
lastHydrateKeyRef.current = "";
|
||||
setParts([]);
|
||||
previousPartIdsRef.current = new Set(); // keep in sync so re-selecting fires the event
|
||||
setError(null);
|
||||
setLoading(false);
|
||||
return;
|
||||
@@ -616,7 +632,7 @@ function GunbuilderPageContent() {
|
||||
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/catalog/products/by-ids`,
|
||||
`/api/catalog/products/by-ids`,
|
||||
{
|
||||
method: "POST",
|
||||
signal: controller.signal,
|
||||
@@ -655,6 +671,21 @@ function GunbuilderPageContent() {
|
||||
|
||||
setParts(hydrated);
|
||||
|
||||
// Track newly selected parts after API fetch — fires with real name, not a numeric ID.
|
||||
// Skip the first hydration: this covers both localStorage build restoration and ?select=
|
||||
// share-link arrivals. Neither represents a live user interaction in the current session.
|
||||
if (isFirstPartsHydrationRef.current) {
|
||||
isFirstPartsHydrationRef.current = false;
|
||||
} else {
|
||||
const prevIds = previousPartIdsRef.current;
|
||||
for (const p of hydrated) {
|
||||
if (!prevIds.has(String(p.id))) {
|
||||
trackPartSelected(p.categoryId as BuilderSlotKey, p.name, String(p.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
previousPartIdsRef.current = new Set(hydrated.map((p) => String(p.id)));
|
||||
|
||||
// ✅ Mark key as hydrated ONLY after success
|
||||
lastHydrateKeyRef.current = key;
|
||||
} catch (err: any) {
|
||||
@@ -728,7 +759,7 @@ function GunbuilderPageContent() {
|
||||
if (authLoading) return;
|
||||
|
||||
// If not logged in, don't spam the backend; show a helpful message instead.
|
||||
if (!token) {
|
||||
if (!user) {
|
||||
setShareStatus("Please log in to load builds from your Vault.");
|
||||
window.setTimeout(() => setShareStatus(null), 4500);
|
||||
return;
|
||||
@@ -738,14 +769,12 @@ function GunbuilderPageContent() {
|
||||
try {
|
||||
setShareStatus("Loading build…");
|
||||
|
||||
// NOTE: using fetch here avoids any “stale api instance” issues and lets us
|
||||
// explicitly attach JWT headers.
|
||||
// NOTE: using fetch here goes through our Next.js API routes with HTTP-only cookies
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/builds/me/${encodeURIComponent(uuidToLoad)}`,
|
||||
`/api/builds/${encodeURIComponent(uuidToLoad)}`,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...getAuthHeaders(),
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -787,9 +816,7 @@ function GunbuilderPageContent() {
|
||||
searchParams,
|
||||
router,
|
||||
authLoading,
|
||||
token,
|
||||
getAuthHeaders,
|
||||
// API_BASE_URL is a module constant; no need to include
|
||||
user,
|
||||
]);
|
||||
|
||||
// -----------------------------
|
||||
@@ -847,6 +874,7 @@ function GunbuilderPageContent() {
|
||||
type: "success",
|
||||
message: "Saved to your Vault ✅ ",
|
||||
});
|
||||
trackBuildSaved();
|
||||
|
||||
// Close modal + reset inputs for next variant
|
||||
setSaveAsOpen(false);
|
||||
@@ -1060,6 +1088,7 @@ function GunbuilderPageContent() {
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(shareUrl);
|
||||
trackBuildShared();
|
||||
setShareStatus("Shareable link copied to clipboard.");
|
||||
} catch {
|
||||
setShareStatus("Could not copy link to clipboard.");
|
||||
@@ -1079,6 +1108,7 @@ function GunbuilderPageContent() {
|
||||
text: "Check out my Battl Build.",
|
||||
url: shareUrl,
|
||||
});
|
||||
trackBuildShared(); // fires only on successful share, not on cancel
|
||||
setShareStatus("Share dialog opened.");
|
||||
} catch {
|
||||
setShareStatus(
|
||||
@@ -1200,53 +1230,64 @@ function GunbuilderPageContent() {
|
||||
<div className="mx-auto max-w-6xl px-4 py-6 lg:py-10">
|
||||
{/* Header */}
|
||||
<header className="mb-6">
|
||||
<div>
|
||||
<p className="text-xs font-semibold tracking-[0.2em] uppercase text-zinc-500">
|
||||
BATTL BUILDERS
|
||||
</p>
|
||||
<h1 className="mt-1 text-2xl md:text-3xl font-semibold tracking-tight">
|
||||
Builder <span className="text-amber-300">Early Access</span>
|
||||
</h1>
|
||||
<p className="mt-2 text-sm text-zinc-400 max-w-xl">
|
||||
Explore components from trusted brands, choose one part per
|
||||
category, track live prices, and watch your total build cost
|
||||
update as you go. This early-access builder keeps your setup saved
|
||||
locally so you can come back and refine it anytime.
|
||||
</p>
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-xs font-semibold tracking-[0.2em] uppercase text-zinc-500">
|
||||
BATTL BUILDERS
|
||||
</p>
|
||||
<h1 className="mt-1 text-2xl md:text-3xl font-semibold tracking-tight">
|
||||
Builder <span className="text-amber-300">Early Access</span>
|
||||
</h1>
|
||||
<p className="mt-2 text-sm text-zinc-400 max-w-xl">
|
||||
Explore components from trusted brands, choose one part per
|
||||
category, track live prices, and watch your total build cost
|
||||
update as you go. This early-access builder keeps your setup saved
|
||||
locally so you can come back and refine it anytime.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-wrap items-center gap-3">
|
||||
<label className="text-xs font-medium text-zinc-400 flex items-center gap-2">
|
||||
Platform
|
||||
<select
|
||||
value={platform}
|
||||
onChange={(e) => {
|
||||
const next = e.target.value as (typeof PLATFORMS)[number];
|
||||
setPlatform(next);
|
||||
{user?.role === "ADMIN" ? (
|
||||
<Link
|
||||
href="/admin"
|
||||
className="inline-flex items-center gap-2 rounded-md border border-amber-400/40 bg-amber-400/10 px-3 py-2 text-xs font-semibold text-amber-200 hover:border-amber-300 hover:text-amber-100"
|
||||
>
|
||||
Admin Toolbar →
|
||||
</Link>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
// Keep URL in sync, and clear transient action params
|
||||
const qp = new URLSearchParams(searchParams.toString());
|
||||
qp.set("platform", normalizePlatformKey(next));
|
||||
qp.delete("select");
|
||||
qp.delete("remove");
|
||||
<div className="mt-4 flex flex-wrap items-center gap-3">
|
||||
<label className="text-xs font-medium text-zinc-400 flex items-center gap-2">
|
||||
Platform
|
||||
<select
|
||||
value={platform}
|
||||
onChange={(e) => {
|
||||
const next = e.target.value as (typeof PLATFORMS)[number];
|
||||
setPlatform(next);
|
||||
|
||||
router.replace(`/builder?${qp.toString()}`, {
|
||||
scroll: false,
|
||||
});
|
||||
}}
|
||||
className="rounded-md border border-zinc-700 bg-zinc-900 px-2 py-1 text-xs text-zinc-100 focus:outline-none focus:ring-1 focus:ring-amber-400"
|
||||
>
|
||||
{PLATFORMS.map((p) => (
|
||||
<option key={p} value={p}>
|
||||
{PLATFORM_LABEL[p]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<p className="text-[0.7rem] text-zinc-500">
|
||||
// Keep URL in sync, and clear transient action params
|
||||
const qp = new URLSearchParams(searchParams.toString());
|
||||
qp.set("platform", normalizePlatformKey(next));
|
||||
qp.delete("select");
|
||||
qp.delete("remove");
|
||||
|
||||
router.replace(`/builder?${qp.toString()}`, {
|
||||
scroll: false,
|
||||
});
|
||||
}}
|
||||
className="rounded-md border border-zinc-700 bg-zinc-900 px-2 py-1 text-xs text-zinc-100 focus:outline-none focus:ring-1 focus:ring-amber-400"
|
||||
>
|
||||
{PLATFORMS.map((p) => (
|
||||
<option key={p} value={p}>
|
||||
{PLATFORM_LABEL[p]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<p className="text-[0.7rem] text-zinc-500">
|
||||
Parts list updates automatically when you change platforms.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Build summary panel */}
|
||||
@@ -1475,6 +1516,11 @@ function GunbuilderPageContent() {
|
||||
|
||||
if (groupCategories.length === 0) return null;
|
||||
|
||||
// Check if there are locked/conflicting parts in this group
|
||||
const hasConflicts = tableRows.some(
|
||||
(row) => row.kind === "category" && row.locked
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
id={`group-${group.id}`}
|
||||
@@ -1494,26 +1540,43 @@ function GunbuilderPageContent() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasConflicts && (
|
||||
<div className="rounded-md border border-amber-500/30 bg-amber-500/10 p-3">
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-amber-400 text-sm">⚠</span>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-amber-300">
|
||||
Part Conflict Detected
|
||||
</p>
|
||||
<p className="text-xs text-zinc-300 mt-1">
|
||||
Some parts below are disabled because you've selected a complete assembly that already includes them. To customize individual parts, remove the complete assembly first.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="overflow-x-auto rounded-md border border-zinc-800 bg-zinc-950/80">
|
||||
<table className="min-w-full text-xs md:text-sm">
|
||||
<table className="min-w-full text-[11px] sm:text-xs md:text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-zinc-800 bg-zinc-900/60">
|
||||
<th className="px-3 py-2 text-left font-semibold text-zinc-400">
|
||||
<th className="px-2 sm:px-3 py-2 text-left font-semibold text-zinc-400">
|
||||
Component / Part Type
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left font-semibold text-zinc-400">
|
||||
<th className="hidden sm:table-cell px-2 sm:px-3 py-2 text-left font-semibold text-zinc-400">
|
||||
Brand
|
||||
</th>
|
||||
<th className="px-3 py-2 text-right font-semibold text-zinc-400">
|
||||
<th className="px-2 sm:px-3 py-2 text-right font-semibold text-zinc-400">
|
||||
Price
|
||||
</th>
|
||||
<th className="px-3 py-2 text-right font-semibold text-zinc-400">
|
||||
<th className="hidden md:table-cell px-2 sm:px-3 py-2 text-right font-semibold text-zinc-400">
|
||||
Sale Price
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left font-semibold text-zinc-400">
|
||||
<th className="hidden md:table-cell px-2 sm:px-3 py-2 text-left font-semibold text-zinc-400">
|
||||
Caliber
|
||||
</th>
|
||||
<th className="px-3 py-2 text-right font-semibold text-zinc-400">
|
||||
<th className="px-2 sm:px-3 py-2 text-right font-semibold text-zinc-400">
|
||||
Buy / Choose
|
||||
</th>
|
||||
</tr>
|
||||
@@ -1529,7 +1592,7 @@ function GunbuilderPageContent() {
|
||||
>
|
||||
<td
|
||||
colSpan={6}
|
||||
className="px-3 py-2 text-[0.7rem] text-zinc-500 bg-zinc-950/70"
|
||||
className="px-2 sm:px-3 py-2 text-[0.7rem] text-zinc-500 bg-zinc-950/70"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="font-semibold text-zinc-400">
|
||||
@@ -1578,7 +1641,7 @@ function GunbuilderPageContent() {
|
||||
row.tone === "muted" ? "opacity-70" : ""
|
||||
}`}
|
||||
>
|
||||
<td className="px-3 py-2 align-top">
|
||||
<td className="px-2 sm:px-3 py-2 align-top">
|
||||
<div className="flex items-start gap-2">
|
||||
{indent > 0 && (
|
||||
<div
|
||||
@@ -1608,9 +1671,15 @@ function GunbuilderPageContent() {
|
||||
</div>
|
||||
|
||||
{selectedPart ? (
|
||||
<div className="text-[0.7rem] text-zinc-500 line-clamp-1">
|
||||
<Link
|
||||
href={`/parts/p/${canonicalPlatform}/${category.id}/${selectedPart.id}-${selectedPart.name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/(^-|-$)/g, "")}`}
|
||||
className="text-[0.7rem] text-zinc-500 hover:text-amber-300 hover:underline line-clamp-1 transition-colors"
|
||||
>
|
||||
{selectedPart.name}
|
||||
</div>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="text-[0.7rem] text-zinc-600">
|
||||
No part selected
|
||||
@@ -1620,28 +1689,47 @@ function GunbuilderPageContent() {
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td className="px-3 py-2 align-top text-zinc-300">
|
||||
<td className="hidden sm:table-cell px-2 sm:px-3 py-2 align-top text-zinc-300">
|
||||
{selectedPart ? selectedPart.brand : "—"}
|
||||
</td>
|
||||
|
||||
<td className="px-3 py-2 align-top text-right text-amber-300">
|
||||
<td className="px-2 sm:px-3 py-2 align-top text-right text-amber-300">
|
||||
{selectedPart
|
||||
? `$${selectedPart.price.toFixed(2)}`
|
||||
: "—"}
|
||||
</td>
|
||||
|
||||
<td className="px-3 py-2 align-top text-right text-zinc-400">
|
||||
<td className="hidden md:table-cell px-2 sm:px-3 py-2 align-top text-right text-zinc-400">
|
||||
—
|
||||
</td>
|
||||
<td className="px-3 py-2 align-top text-zinc-400">
|
||||
<td className="hidden md:table-cell px-2 sm:px-3 py-2 align-top text-zinc-400">
|
||||
—
|
||||
</td>
|
||||
|
||||
<td className="px-3 py-2 align-top">
|
||||
<td className="px-2 sm:px-3 py-2 align-top">
|
||||
<div className="flex justify-end gap-2">
|
||||
{isLocked ? (
|
||||
<div className="text-[0.7rem] text-zinc-500">
|
||||
—
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[0.7rem] text-amber-400/70">
|
||||
⚠ Disabled
|
||||
</span>
|
||||
<div className="group relative">
|
||||
<button
|
||||
type="button"
|
||||
className="text-zinc-500 hover:text-zinc-300 text-xs"
|
||||
title="Why is this disabled?"
|
||||
>
|
||||
ⓘ
|
||||
</button>
|
||||
<div className="invisible group-hover:visible absolute right-0 top-6 z-10 w-64 rounded-md border border-zinc-800 bg-zinc-950 p-3 text-xs text-zinc-300 shadow-xl">
|
||||
<p className="font-semibold text-amber-300 mb-1">
|
||||
Part Conflict
|
||||
</p>
|
||||
<p>
|
||||
You've selected a complete assembly which already includes this part. Remove the complete assembly to select individual parts.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : selectedPart ? (
|
||||
<>
|
||||
@@ -1651,6 +1739,14 @@ function GunbuilderPageContent() {
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1.5 whitespace-nowrap rounded-md border border-amber-600 bg-amber-500 px-3 py-1.5 text-xs font-semibold text-black hover:bg-amber-400 transition-colors"
|
||||
onClick={() => {
|
||||
try {
|
||||
const retailer = new URL(selectedPart.affiliateUrl).hostname.replace(/^www\./, "");
|
||||
trackAffiliateLinkClicked(selectedPart.name, retailer);
|
||||
} catch {
|
||||
// ignore malformed URLs — if affiliateUrl is malformed, the link href is also broken
|
||||
}
|
||||
}}
|
||||
>
|
||||
Buy
|
||||
</a>
|
||||
|
||||
@@ -6,6 +6,8 @@ import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
import type { BuilderSlotKey } from "@/types/builderSlots";
|
||||
import { PART_ROLE_TO_CATEGORY, normalizePartRole } from "@/lib/catalogMappings";
|
||||
import { trackAffiliateLinkClicked } from "@/lib/analytics";
|
||||
import { ProductImage } from "@/components/parts/ProductImage";
|
||||
|
||||
/**
|
||||
* API Shapes
|
||||
@@ -18,6 +20,7 @@ type OfferFromApi = {
|
||||
price?: number | null;
|
||||
originalPrice?: number | null;
|
||||
buyUrl?: string | null;
|
||||
buyShortUrl?: string | null;
|
||||
inStock?: boolean | null;
|
||||
lastUpdated?: string | null;
|
||||
};
|
||||
@@ -29,6 +32,7 @@ type GunbuilderProductFromApi = {
|
||||
platform: string;
|
||||
partRole: string;
|
||||
price: number | null;
|
||||
caliber?: string | null;
|
||||
|
||||
// Optional (legacy fallback label if product.offers is missing)
|
||||
merchantName?: string | null;
|
||||
@@ -37,6 +41,7 @@ type GunbuilderProductFromApi = {
|
||||
mainImageUrl?: string | null;
|
||||
|
||||
buyUrl?: string | null;
|
||||
buyShortUrl?: string | null;
|
||||
inStock?: boolean | null;
|
||||
|
||||
shortDescription?: string | null;
|
||||
@@ -238,14 +243,23 @@ export default function ProductDetailsPage() {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const url = `${API_BASE_URL}/api/v1/products/${numericId}`;
|
||||
const res = await fetch(url, { signal: controller.signal });
|
||||
const url = `/api/catalog/products/${numericId}`;
|
||||
const res = await fetch(url, {
|
||||
signal: controller.signal,
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to load product (${res.status})`);
|
||||
}
|
||||
|
||||
const data: GunbuilderProductFromApi = await res.json();
|
||||
console.log('[DEBUG] Product from API:', {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
buyUrl: data.buyUrl,
|
||||
buyShortUrl: data.buyShortUrl
|
||||
});
|
||||
setProduct(data);
|
||||
} catch (err: any) {
|
||||
if (err?.name === "AbortError") return;
|
||||
@@ -272,14 +286,18 @@ export default function ProductDetailsPage() {
|
||||
try {
|
||||
setOffersLoading(true);
|
||||
|
||||
const url = `${API_BASE_URL}/api/v1/products/${numericId}/offers`;
|
||||
const res = await fetch(url, { signal: controller.signal });
|
||||
const url = `/api/catalog/products/${numericId}/offers`;
|
||||
const res = await fetch(url, {
|
||||
signal: controller.signal,
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to load offers (${res.status})`);
|
||||
}
|
||||
|
||||
const data: OfferFromApi[] = await res.json();
|
||||
console.log('[DEBUG] Offers from API:', data);
|
||||
setOffersFromApi(Array.isArray(data) ? data : []);
|
||||
} catch (err: any) {
|
||||
if (err?.name === "AbortError") return;
|
||||
@@ -330,14 +348,23 @@ export default function ProductDetailsPage() {
|
||||
* - Fallback to legacy single-offer derived from product if needed
|
||||
*/
|
||||
const offers = useMemo(() => {
|
||||
if (offersFromApi.length > 0) return sortOffers(offersFromApi);
|
||||
if (offersFromApi.length > 0) {
|
||||
console.log('[DEBUG] Using offers from API, first offer:', offersFromApi[0]);
|
||||
const fallbackShortUrl = product?.buyShortUrl ?? null;
|
||||
const normalized = offersFromApi.map((offer) => ({
|
||||
...offer,
|
||||
buyShortUrl: offer.buyShortUrl || fallbackShortUrl,
|
||||
}));
|
||||
return sortOffers(normalized);
|
||||
}
|
||||
|
||||
if (product?.buyUrl) {
|
||||
if (product?.buyUrl || product?.buyShortUrl) {
|
||||
console.log('[DEBUG] Using fallback from product, buyShortUrl:', product.buyShortUrl, 'buyUrl:', product.buyUrl);
|
||||
return sortOffers([
|
||||
{
|
||||
merchantName: product.merchantName ?? "Retailer",
|
||||
price: product.price,
|
||||
buyUrl: product.buyUrl,
|
||||
buyUrl: product.buyShortUrl || product.buyUrl,
|
||||
inStock: product.inStock ?? true,
|
||||
},
|
||||
]);
|
||||
@@ -467,12 +494,10 @@ export default function ProductDetailsPage() {
|
||||
className="h-full w-full"
|
||||
title="Click to enlarge"
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
<ProductImage
|
||||
src={imageUrl}
|
||||
alt={product.name}
|
||||
className="h-full w-full object-contain p-2"
|
||||
loading="lazy"
|
||||
/>
|
||||
</button>
|
||||
) : (
|
||||
@@ -559,6 +584,14 @@ export default function ProductDetailsPage() {
|
||||
{product.platform || platform}
|
||||
</span>
|
||||
</span>
|
||||
{product.caliber && (
|
||||
<span className="rounded border border-zinc-800 bg-zinc-900/60 px-2 py-1 text-zinc-300">
|
||||
Caliber:{" "}
|
||||
<span className="font-semibold text-zinc-100">
|
||||
{product.caliber}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
<span className="rounded border border-zinc-800 bg-zinc-900/60 px-2 py-1 text-zinc-300">
|
||||
Role:{" "}
|
||||
<span className="font-semibold text-zinc-100">
|
||||
@@ -660,12 +693,13 @@ export default function ProductDetailsPage() {
|
||||
{o.price != null ? `$${o.price.toFixed(2)}` : "—"}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right">
|
||||
{o.buyUrl ? (
|
||||
{(o.buyShortUrl || o.buyUrl) ? (
|
||||
<a
|
||||
href={o.buyUrl}
|
||||
href={o.buyShortUrl || o.buyUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex items-center justify-center rounded-md bg-amber-400 px-3 py-1.5 text-xs font-semibold text-black hover:bg-amber-300"
|
||||
onClick={() => trackAffiliateLinkClicked(product?.name ?? "", merchantLabel(o))}
|
||||
>
|
||||
Buy
|
||||
</a>
|
||||
@@ -686,13 +720,14 @@ export default function ProductDetailsPage() {
|
||||
)}
|
||||
|
||||
{/* Best Offer CTA */}
|
||||
{bestOffer?.buyUrl ? (
|
||||
{(bestOffer?.buyShortUrl || bestOffer?.buyUrl) ? (
|
||||
<div className="mt-3 flex justify-end">
|
||||
<a
|
||||
href={bestOffer.buyUrl}
|
||||
href={bestOffer.buyShortUrl || bestOffer.buyUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="rounded-md border border-zinc-700 bg-zinc-900/70 px-4 py-2 text-sm font-semibold text-zinc-200 hover:bg-zinc-800"
|
||||
onClick={() => trackAffiliateLinkClicked(product?.name ?? "", merchantLabel(bestOffer))}
|
||||
>
|
||||
Buy from {merchantLabel(bestOffer)} →
|
||||
</a>
|
||||
@@ -735,8 +770,7 @@ export default function ProductDetailsPage() {
|
||||
Close
|
||||
</button>
|
||||
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
<ProductImage
|
||||
src={imageUrl}
|
||||
alt={product?.name ?? "Product image"}
|
||||
className="max-h-[90vh] max-w-[90vw] rounded-lg border border-zinc-800 bg-black object-contain"
|
||||
@@ -747,4 +781,4 @@ export default function ProductDetailsPage() {
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
import { postBySlugQuery, postSlugsQuery, urlFor } from '@/lib/sanity'
|
||||
import { sanityFetch } from '@/lib/sanityFetch'
|
||||
import { PortableTextRenderer } from '@/components/PortableTextRenderer'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { notFound } from 'next/navigation'
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
export const revalidate = 60
|
||||
|
||||
type Params = { slug: string }
|
||||
|
||||
type Post = {
|
||||
_id: string
|
||||
title: string
|
||||
slug: { current: string }
|
||||
publishedAt: string
|
||||
excerpt: string | null
|
||||
mainImage: { asset: object; alt?: string } | null
|
||||
body: unknown[]
|
||||
seoTitle: string | null
|
||||
seoDescription: string | null
|
||||
author: { name: string; image?: object; bio?: string } | null
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
try {
|
||||
const slugs: { slug: string }[] = await sanityFetch<{ slug: string }[]>(postSlugsQuery)
|
||||
return slugs.map((s) => ({ slug: s.slug }))
|
||||
} catch (err) {
|
||||
console.error('[Guides] generateStaticParams fetch failed:', err)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<Params> }): Promise<Metadata> {
|
||||
const { slug } = await params
|
||||
let post: Post | null = null
|
||||
try {
|
||||
post = await sanityFetch<Post | null>(postBySlugQuery, { slug })
|
||||
} catch (err) {
|
||||
console.error('[Guides] generateMetadata fetch failed:', err)
|
||||
}
|
||||
if (!post) return {}
|
||||
|
||||
return {
|
||||
title: post.seoTitle ?? post.title,
|
||||
description: post.seoDescription ?? post.excerpt ?? undefined,
|
||||
openGraph: {
|
||||
title: post.seoTitle ?? post.title,
|
||||
description: post.seoDescription ?? post.excerpt ?? undefined,
|
||||
images: post.mainImage?.asset
|
||||
? [{ url: urlFor(post.mainImage).width(1200).height(630).url() }]
|
||||
: [],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(iso: string) {
|
||||
return new Date(iso).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})
|
||||
}
|
||||
|
||||
export default async function PostPage({ params }: { params: Promise<Params> }) {
|
||||
const { slug } = await params
|
||||
let post: Post | null = null
|
||||
try {
|
||||
post = await sanityFetch<Post | null>(postBySlugQuery, { slug })
|
||||
} catch (err) {
|
||||
console.error('[Guides] PostPage fetch failed:', err)
|
||||
}
|
||||
|
||||
if (!post) notFound()
|
||||
|
||||
return (
|
||||
<main className="mx-auto max-w-3xl px-4 py-10">
|
||||
{/* Breadcrumb */}
|
||||
<nav className="mb-6 text-xs text-zinc-500">
|
||||
<Link href="/guides" className="hover:text-zinc-300">
|
||||
← Guides
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
{/* Header */}
|
||||
<header className="mb-8">
|
||||
<div className="mb-3 flex items-center gap-2 text-[0.7rem] text-zinc-500">
|
||||
{post.author?.name && <span>{post.author.name}</span>}
|
||||
{post.author?.name && <span>·</span>}
|
||||
<time dateTime={post.publishedAt}>{formatDate(post.publishedAt)}</time>
|
||||
</div>
|
||||
|
||||
<h1 className="text-3xl font-semibold leading-tight tracking-tight text-zinc-100 md:text-4xl">
|
||||
{post.title}
|
||||
</h1>
|
||||
|
||||
{post.excerpt && (
|
||||
<p className="mt-4 text-base text-zinc-400 leading-relaxed">
|
||||
{post.excerpt}
|
||||
</p>
|
||||
)}
|
||||
</header>
|
||||
|
||||
{/* Hero image */}
|
||||
{post.mainImage?.asset && (
|
||||
<div className="relative mb-10 h-64 w-full overflow-hidden rounded-xl md:h-80">
|
||||
<Image
|
||||
src={urlFor(post.mainImage).width(1200).height(630).url()}
|
||||
alt={post.mainImage.alt ?? post.title}
|
||||
fill
|
||||
priority
|
||||
className="object-cover"
|
||||
sizes="(max-width: 768px) 100vw, 768px"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Body */}
|
||||
{post.body && (
|
||||
<article className="border-b border-zinc-800 pb-10">
|
||||
<PortableTextRenderer value={post.body} />
|
||||
</article>
|
||||
)}
|
||||
|
||||
{/* CTA footer */}
|
||||
<div className="mt-10 rounded-xl border border-zinc-800 bg-zinc-950/60 p-6 text-center">
|
||||
<p className="text-sm font-medium text-zinc-200">
|
||||
Ready to put this into practice?
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-zinc-500">
|
||||
Use the Battl Builder to price out your parts and track your total.
|
||||
</p>
|
||||
<Link
|
||||
href="/builder"
|
||||
className="mt-4 inline-block rounded-md bg-amber-400 px-5 py-2.5 text-sm font-semibold text-black hover:bg-amber-300 transition-colors"
|
||||
>
|
||||
Start Your Build →
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Back link */}
|
||||
<div className="mt-8 text-center">
|
||||
<Link href="/guides" className="text-xs text-zinc-500 hover:text-zinc-300">
|
||||
← Back to all guides
|
||||
</Link>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { postsQuery, urlFor } from '@/lib/sanity'
|
||||
import { sanityFetch } from '@/lib/sanityFetch'
|
||||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Build Guides',
|
||||
description: 'AR-15 build guides, part breakdowns, and buying advice from Battl Builders.',
|
||||
}
|
||||
|
||||
export const revalidate = 60 // ISR — regenerate every 60s
|
||||
|
||||
type Post = {
|
||||
_id: string
|
||||
title: string
|
||||
slug: { current: string }
|
||||
publishedAt: string
|
||||
excerpt: string | null
|
||||
mainImage: { asset: object; alt?: string } | null
|
||||
author: { name: string; image?: object } | null
|
||||
}
|
||||
|
||||
function formatDate(iso: string) {
|
||||
return new Date(iso).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})
|
||||
}
|
||||
|
||||
export default async function GuidesPage() {
|
||||
let posts: Post[] = []
|
||||
try {
|
||||
posts = await sanityFetch<Post[]>(postsQuery)
|
||||
} catch (err) {
|
||||
console.error('[Guides] Sanity fetch failed:', err)
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="mx-auto max-w-4xl px-4 py-10">
|
||||
<header className="mb-10">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-zinc-500">
|
||||
Battl Builders
|
||||
</p>
|
||||
<h1 className="mt-2 text-3xl font-semibold tracking-tight">
|
||||
Build Guides
|
||||
</h1>
|
||||
<p className="mt-3 text-sm text-zinc-400 max-w-xl">
|
||||
Part breakdowns, buying advice, and build walkthroughs to help you
|
||||
make smarter decisions before you spend a dollar.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{posts.length === 0 ? (
|
||||
<p className="text-sm text-zinc-500">No guides published yet — check back soon.</p>
|
||||
) : (
|
||||
<div className="space-y-8">
|
||||
{posts.map((post) => (
|
||||
<article
|
||||
key={post._id}
|
||||
className="group flex flex-col gap-4 rounded-xl border border-zinc-800 bg-zinc-950/60 p-5 transition-colors hover:border-zinc-700 sm:flex-row"
|
||||
>
|
||||
{/* Thumbnail */}
|
||||
{post.mainImage?.asset && (
|
||||
<div className="relative h-44 w-full shrink-0 overflow-hidden rounded-lg sm:h-32 sm:w-48">
|
||||
<Image
|
||||
src={urlFor(post.mainImage).width(400).height(256).url()}
|
||||
alt={post.mainImage.alt ?? post.title}
|
||||
fill
|
||||
className="object-cover transition-transform duration-300 group-hover:scale-105"
|
||||
sizes="(max-width: 640px) 100vw, 192px"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Text */}
|
||||
<div className="flex flex-col justify-center gap-2">
|
||||
<div className="flex items-center gap-2 text-[0.7rem] text-zinc-500">
|
||||
{post.author?.name && <span>{post.author.name}</span>}
|
||||
{post.author?.name && <span>·</span>}
|
||||
<time dateTime={post.publishedAt}>{formatDate(post.publishedAt)}</time>
|
||||
</div>
|
||||
|
||||
<h2 className="text-lg font-semibold leading-snug text-zinc-100 group-hover:text-amber-300 transition-colors">
|
||||
<Link href={`/guides/${post.slug.current}`} className="stretched-link">
|
||||
{post.title}
|
||||
</Link>
|
||||
</h2>
|
||||
|
||||
{post.excerpt && (
|
||||
<p className="text-sm text-zinc-400 leading-relaxed line-clamp-2">
|
||||
{post.excerpt}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<Link
|
||||
href={`/guides/${post.slug.current}`}
|
||||
className="mt-1 self-start text-xs font-medium text-amber-400 hover:text-amber-300"
|
||||
>
|
||||
Read guide →
|
||||
</Link>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-12 rounded-lg border border-zinc-800 bg-zinc-950/60 p-5 text-center">
|
||||
<p className="text-sm text-zinc-400">
|
||||
Ready to start building?{' '}
|
||||
<Link href="/builder" className="font-semibold text-amber-400 hover:text-amber-300">
|
||||
Open the Builder →
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { Suspense } from "react";
|
||||
|
||||
import { BuilderNav } from "@/components/BuilderNav";
|
||||
import { BuilderNavConditional } from "@/components/BuilderNavConditional";
|
||||
import { TopNav } from "@/components/TopNav";
|
||||
import { Footer } from "@/components/Footer";
|
||||
|
||||
@@ -11,7 +11,7 @@ export default function BuilderLayout({ children }: { children: ReactNode }) {
|
||||
<div className="min-h-screen bg-white text-zinc-900 dark:bg-neutral-950 dark:text-zinc-50">
|
||||
<TopNav />
|
||||
<Suspense fallback={<div className="h-[52px]" />}>
|
||||
<BuilderNav />
|
||||
<BuilderNavConditional />
|
||||
</Suspense>
|
||||
<main className="min-h-screen">{children}</main>
|
||||
<Footer />
|
||||
|
||||
@@ -417,9 +417,9 @@ export default function PrivacyPolicy() {
|
||||
<br />
|
||||
Battl Builder, LLC.
|
||||
<br />
|
||||
[Your Address]
|
||||
{/* [Your Address]
|
||||
<br />
|
||||
[City, State, ZIP]
|
||||
[City, State, ZIP] */}
|
||||
</p>
|
||||
</div>
|
||||
<p className="mt-4 text-sm">
|
||||
|
||||
@@ -18,8 +18,7 @@ type BuildDto = {
|
||||
updatedAt?: string | null;
|
||||
};
|
||||
|
||||
const API_BASE_URL =
|
||||
process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
|
||||
// API routes now handled by Next.js /api routes (server-side)
|
||||
|
||||
// UUID validator (defensive)
|
||||
const isUuid = (v: string) =>
|
||||
@@ -40,14 +39,14 @@ export default function VaultPage() {
|
||||
|
||||
// NOTE:
|
||||
// - `loading` here is AuthContext hydration, not network.
|
||||
// - `token` is the real gate for /me endpoints.
|
||||
const { user, token, loading: authLoading } = useAuth();
|
||||
// - Auth is handled by HTTP-only cookies automatically.
|
||||
const { user, loading: authLoading } = useAuth();
|
||||
|
||||
const [builds, setBuilds] = useState<BuildDto[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const authed = !!user && !!token;
|
||||
const authed = !!user;
|
||||
|
||||
// Redirect if not logged in (after auth hydrates)
|
||||
useEffect(() => {
|
||||
@@ -56,12 +55,12 @@ export default function VaultPage() {
|
||||
}
|
||||
}, [authLoading, authed, router]);
|
||||
|
||||
// When auth identity changes, clear stale list to avoid “ghost builds”
|
||||
// When auth identity changes, clear stale list to avoid "ghost builds"
|
||||
useEffect(() => {
|
||||
if (authLoading) return;
|
||||
setBuilds([]);
|
||||
setError(null);
|
||||
}, [authLoading, user?.uuid, token]);
|
||||
}, [authLoading, user?.uuid]);
|
||||
|
||||
// Fetch user builds
|
||||
useEffect(() => {
|
||||
@@ -75,8 +74,8 @@ export default function VaultPage() {
|
||||
|
||||
try {
|
||||
// IMPORTANT:
|
||||
// Use api wrapper so Authorization: Bearer <token> is consistently applied.
|
||||
const res = await api.get("/api/v1/builds/me");
|
||||
// Use api wrapper which includes credentials (cookies) automatically.
|
||||
const res = await api.get("/api/builds");
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => "");
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button, Field, Input } from "@/components/ui/form";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
|
||||
const API_BASE_URL =
|
||||
process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
|
||||
|
||||
type AdminBetaRequestDto = {
|
||||
id: number;
|
||||
@@ -40,17 +36,12 @@ type AdminInviteResponse = {
|
||||
token?: string;
|
||||
};
|
||||
|
||||
async function fetchJson(
|
||||
path: string,
|
||||
init: RequestInit = {},
|
||||
authHeaders: HeadersInit = {}
|
||||
) {
|
||||
const res = await fetch(`${API_BASE_URL}${path}`, {
|
||||
async function fetchJson(path: string, init: RequestInit = {}) {
|
||||
const res = await fetch(path, {
|
||||
...init,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(init.headers ?? {}),
|
||||
...authHeaders,
|
||||
},
|
||||
cache: "no-store",
|
||||
});
|
||||
@@ -101,9 +92,6 @@ function Badge({
|
||||
}
|
||||
|
||||
export default function AdminBetaInvitesPage() {
|
||||
const { getAuthHeaders, user } = useAuth();
|
||||
const authHeaders = useMemo(() => getAuthHeaders(), [getAuthHeaders]);
|
||||
|
||||
// ------------------------------
|
||||
// Batch invites
|
||||
// ------------------------------
|
||||
@@ -126,11 +114,9 @@ export default function AdminBetaInvitesPage() {
|
||||
tokenMinutes: String(tokenMinutes || "30"),
|
||||
});
|
||||
|
||||
const data = await fetchJson(
|
||||
`/api/v1/admin/beta/invites/send?${qs.toString()}`,
|
||||
{ method: "POST" },
|
||||
authHeaders
|
||||
);
|
||||
const data = await fetchJson(`/api/admin/beta-invites?${qs.toString()}`, {
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
setBatchResult(data);
|
||||
await loadRequests();
|
||||
@@ -166,9 +152,8 @@ export default function AdminBetaInvitesPage() {
|
||||
|
||||
try {
|
||||
const data = (await fetchJson(
|
||||
`/api/v1/admin/beta/requests?page=${page}&size=${size}`,
|
||||
{ method: "GET" },
|
||||
authHeaders
|
||||
`/api/admin/beta/requests?page=${page}&size=${size}`,
|
||||
{ method: "GET" }
|
||||
)) as PageResponse<AdminBetaRequestDto>;
|
||||
|
||||
setRequests(data);
|
||||
@@ -181,11 +166,9 @@ export default function AdminBetaInvitesPage() {
|
||||
|
||||
async function inviteSingle(userId: number): Promise<AdminInviteResponse> {
|
||||
// calls backend: POST /api/v1/admin/beta/requests/{userId}/invite
|
||||
return (await fetchJson(
|
||||
`/api/v1/admin/beta/requests/${userId}/invite`,
|
||||
{ method: "POST" },
|
||||
authHeaders
|
||||
)) as AdminInviteResponse;
|
||||
return (await fetchJson(`/api/admin/beta/requests/${userId}/invite`, {
|
||||
method: "POST",
|
||||
})) as AdminInviteResponse;
|
||||
}
|
||||
|
||||
async function onSendInviteEmail(userId: number) {
|
||||
@@ -241,19 +224,6 @@ export default function AdminBetaInvitesPage() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [page]);
|
||||
|
||||
const isAdmin = (user?.role ?? "").toUpperCase() === "ADMIN";
|
||||
|
||||
if (!isAdmin) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-xl font-semibold">Admin</h1>
|
||||
<p className="text-sm text-zinc-400">
|
||||
You don’t have access to this page.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import {
|
||||
fetchEmailRequests,
|
||||
createEmailRequest,
|
||||
@@ -17,7 +16,6 @@ import { formatDate } from "@/lib/dateFormattingUtility";
|
||||
type EmailStatusTab = "ALL" | "PENDING" | "SENT" | "FAILED" | "OTHER";
|
||||
|
||||
export default function AdminEmailPage() {
|
||||
const { token } = useAuth();
|
||||
const [emails, setEmails] = useState<EmailRequest[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -33,11 +31,9 @@ export default function AdminEmailPage() {
|
||||
const [statusTab, setStatusTab] = useState<EmailStatusTab>("ALL");
|
||||
|
||||
const loadEmails = useCallback(async () => {
|
||||
if (!token) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await fetchEmailRequests(token);
|
||||
const data = await fetchEmailRequests();
|
||||
setEmails(data);
|
||||
setError(null);
|
||||
} catch (e: any) {
|
||||
@@ -46,7 +42,7 @@ export default function AdminEmailPage() {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [token]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadEmails();
|
||||
@@ -108,11 +104,10 @@ export default function AdminEmailPage() {
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
if (!token) return;
|
||||
if (!confirm("Are you sure you want to delete this email request?")) return;
|
||||
|
||||
try {
|
||||
await deleteEmailRequest(token, id);
|
||||
await deleteEmailRequest(id);
|
||||
await loadEmails();
|
||||
} catch (e: any) {
|
||||
alert(e?.message ?? "Failed to delete email request");
|
||||
@@ -121,17 +116,15 @@ export default function AdminEmailPage() {
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!token) return;
|
||||
|
||||
try {
|
||||
setSubmitting(true);
|
||||
if (modalMode === "create") {
|
||||
await createEmailRequest(token, formData);
|
||||
await createEmailRequest(formData);
|
||||
} else if (selectedEmail) {
|
||||
const updateData: UpdateEmailRequestDto = {};
|
||||
if (formData.email !== selectedEmail.email) updateData.email = formData.email;
|
||||
if (formData.status !== selectedEmail.status) updateData.status = formData.status;
|
||||
await updateEmailRequest(token, selectedEmail.id, updateData);
|
||||
await updateEmailRequest(selectedEmail.id, updateData);
|
||||
}
|
||||
setShowModal(false);
|
||||
await loadEmails();
|
||||
@@ -142,14 +135,6 @@ export default function AdminEmailPage() {
|
||||
}
|
||||
};
|
||||
|
||||
if (!token) {
|
||||
return (
|
||||
<div className="p-6 text-sm text-red-500">
|
||||
You must be logged in to view this page.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-4">
|
||||
{/* Header */}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import {
|
||||
fetchEmailRequests,
|
||||
createEmailRequest,
|
||||
@@ -14,7 +13,6 @@ import {
|
||||
import { Pencil, Trash2, Plus, X } from "lucide-react";
|
||||
import { formatDate } from "@/lib/dateFormattingUtility";
|
||||
export default function AdminEmailPage() {
|
||||
const { token } = useAuth();
|
||||
const [emails, setEmails] = useState<EmailRequest[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -28,11 +26,9 @@ export default function AdminEmailPage() {
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const loadEmails = useCallback(async () => {
|
||||
if (!token) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await fetchEmailRequests(token);
|
||||
const data = await fetchEmailRequests();
|
||||
setEmails(data);
|
||||
setError(null);
|
||||
} catch (e: any) {
|
||||
@@ -41,7 +37,7 @@ export default function AdminEmailPage() {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [token]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadEmails();
|
||||
@@ -62,11 +58,10 @@ export default function AdminEmailPage() {
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
if (!token) return;
|
||||
if (!confirm("Are you sure you want to delete this email request?")) return;
|
||||
|
||||
try {
|
||||
await deleteEmailRequest(token, id);
|
||||
await deleteEmailRequest(id);
|
||||
await loadEmails();
|
||||
} catch (e: any) {
|
||||
alert(e?.message ?? "Failed to delete email request");
|
||||
@@ -75,17 +70,15 @@ export default function AdminEmailPage() {
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!token) return;
|
||||
|
||||
try {
|
||||
setSubmitting(true);
|
||||
if (modalMode === "create") {
|
||||
await createEmailRequest(token, formData);
|
||||
await createEmailRequest(formData);
|
||||
} else if (selectedEmail) {
|
||||
const updateData: UpdateEmailRequestDto = {};
|
||||
if (formData.email !== selectedEmail.email) updateData.email = formData.email;
|
||||
if (formData.status !== selectedEmail.status) updateData.status = formData.status;
|
||||
await updateEmailRequest(token, selectedEmail.id, updateData);
|
||||
await updateEmailRequest(selectedEmail.id, updateData);
|
||||
}
|
||||
setShowModal(false);
|
||||
await loadEmails();
|
||||
@@ -96,14 +89,6 @@ export default function AdminEmailPage() {
|
||||
}
|
||||
};
|
||||
|
||||
if (!token) {
|
||||
return (
|
||||
<div className="p-6 text-sm text-red-500">
|
||||
You must be logged in to view this page.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-4">
|
||||
{/* Header */}
|
||||
|
||||
@@ -46,8 +46,8 @@ export default function ImportStatusPage() {
|
||||
setError(null);
|
||||
|
||||
const [summaryRes, byMerchantRes] = await Promise.all([
|
||||
fetch(`${API_BASE_URL}/api/admin/import-status/summary`),
|
||||
fetch(`${API_BASE_URL}/api/admin/import-status/by-merchant`),
|
||||
fetch('/api/admin/import-status/summary', { credentials: 'include' }),
|
||||
fetch('/api/admin/import-status/by-merchant', { credentials: 'include' }),
|
||||
]);
|
||||
|
||||
if (!summaryRes.ok || !byMerchantRes.ok) {
|
||||
@@ -108,9 +108,10 @@ export default function ImportStatusPage() {
|
||||
try {
|
||||
setImportingId(merchantId);
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/admin/merchants/${merchantId}/import`,
|
||||
`/api/admin/merchants/${merchantId}/import`,
|
||||
{
|
||||
method: "POST",
|
||||
credentials: 'include',
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
+4
-13
@@ -149,7 +149,7 @@ export default function AdminLayout({
|
||||
|
||||
return (
|
||||
// ✅ prevent browser-level horizontal scroll from any wide children
|
||||
<div className="min-h-screen bg-black text-zinc-50 overflow-x-hidden pt-[52px]">
|
||||
<div className="h-screen bg-black text-zinc-50 overflow-hidden">
|
||||
<AdminLeftNavigation
|
||||
collapsed={collapsed}
|
||||
onToggleCollapsed={() => setCollapsed((v) => !v)}
|
||||
@@ -158,7 +158,7 @@ export default function AdminLayout({
|
||||
|
||||
{/* ✅ min-w-0 is the critical fix: lets the main column shrink */}
|
||||
<div
|
||||
className="flex min-h-screen flex-1 min-w-0 flex-col transition-all duration-300"
|
||||
className="flex h-full flex-1 min-w-0 flex-col transition-all duration-300"
|
||||
style={{ marginLeft: collapsed ? '68px' : '280px' }}
|
||||
>
|
||||
<header className="flex items-center justify-between border-b border-zinc-900 bg-zinc-950/70 px-4 py-3 sticky top-0 z-30 backdrop-blur-sm">
|
||||
@@ -168,22 +168,13 @@ export default function AdminLayout({
|
||||
</p>
|
||||
<p className="text-sm text-zinc-200">Battl Control Panel</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="rounded-full border border-amber-500/30 bg-amber-500/10 px-3 py-1 text-[11px] font-medium text-amber-300">
|
||||
Internal • v0.1
|
||||
</span>
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-zinc-900 text-[11px] text-zinc-300">
|
||||
ADMIN
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* ✅ min-w-0 here prevents table min-width from widening the page */}
|
||||
<main className="flex w-full flex-1 min-w-0 flex-col px-4 py-6">
|
||||
<main className="flex w-full flex-1 min-w-0 flex-col overflow-y-auto px-4 py-6">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,8 +198,9 @@ export default function MappingAdminPage() {
|
||||
|
||||
// EXPECTED backend endpoint (new):
|
||||
// GET { merchants: [{id,name}], canonicalCategories: [{id,name,slug}] }
|
||||
const res = await fetch(`${API_BASE_URL}/api/admin/mapping/options`, {
|
||||
const res = await fetch('/api/admin/mapping/options', {
|
||||
headers: { Accept: "application/json" },
|
||||
credentials: 'include',
|
||||
cache: "no-store",
|
||||
});
|
||||
|
||||
@@ -243,8 +244,9 @@ export default function MappingAdminPage() {
|
||||
if (tab === "roles") {
|
||||
// Existing endpoint you already have:
|
||||
// GET /api/admin/mapping/pending-buckets
|
||||
const res = await fetch(`${API_BASE_URL}/api/admin/mapping/pending-buckets`, {
|
||||
const res = await fetch('/api/admin/mapping/pending-buckets', {
|
||||
headers: { Accept: "application/json" },
|
||||
credentials: 'include',
|
||||
cache: "no-store",
|
||||
});
|
||||
|
||||
@@ -277,8 +279,8 @@ export default function MappingAdminPage() {
|
||||
if (q?.trim()) params.set("q", q.trim());
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/admin/mapping/raw-categories?${params.toString()}`,
|
||||
{ headers: { Accept: "application/json" }, cache: "no-store" }
|
||||
`/api/admin/mapping/raw-categories?${params.toString()}`,
|
||||
{ headers: { Accept: "application/json" }, credentials: 'include', cache: "no-store" }
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
@@ -336,9 +338,10 @@ export default function MappingAdminPage() {
|
||||
|
||||
// Existing endpoint you already have:
|
||||
// POST /api/admin/mapping/apply
|
||||
const res = await fetch(`${API_BASE_URL}/api/admin/mapping/apply`, {
|
||||
const res = await fetch('/api/admin/mapping/apply', {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
merchantId: row.merchantId,
|
||||
rawCategoryKey: row.rawCategoryKey,
|
||||
@@ -390,9 +393,10 @@ export default function MappingAdminPage() {
|
||||
|
||||
// EXPECTED new endpoint:
|
||||
// POST /api/admin/mapping/upsert
|
||||
const res = await fetch(`${API_BASE_URL}/api/admin/mapping/upsert`, {
|
||||
const res = await fetch('/api/admin/mapping/upsert', {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
merchantId: row.merchantId,
|
||||
platform: row.platform ?? platform ?? null,
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const API_BASE_URL =
|
||||
process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
|
||||
// API routes now handled by Next.js /api routes (server-side)
|
||||
|
||||
type MerchantAdminDto = {
|
||||
id: number;
|
||||
@@ -91,7 +90,7 @@ export default function MerchantsAdminPage() {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const url = `${API_BASE_URL}/api/admin/merchants`;
|
||||
const url = `/api/admin/merchants`;
|
||||
console.log("Loading merchants from:", url);
|
||||
|
||||
const res = await fetch(url, {
|
||||
@@ -150,7 +149,7 @@ export default function MerchantsAdminPage() {
|
||||
setError(null);
|
||||
setBanner(null);
|
||||
|
||||
const res = await fetch(`${API_BASE_URL}/api/admin/merchants/${id}`, {
|
||||
const res = await fetch(`/api/admin/merchants/${id}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
@@ -189,7 +188,7 @@ export default function MerchantsAdminPage() {
|
||||
setError(null);
|
||||
setBanner(null);
|
||||
|
||||
const res = await fetch(`${API_BASE_URL}/api/admin/imports/${id}`, {
|
||||
const res = await fetch(`/api/admin/merchants/${id}/import`, {
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
@@ -218,7 +217,7 @@ export default function MerchantsAdminPage() {
|
||||
setBanner(null);
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/admin/imports/${id}/offers-only`,
|
||||
`/api/admin/merchants/${id}/offer-sync`,
|
||||
{ method: "POST" }
|
||||
);
|
||||
|
||||
@@ -247,7 +246,7 @@ export default function MerchantsAdminPage() {
|
||||
setError(null);
|
||||
setBanner(null);
|
||||
|
||||
const res = await fetch(`${API_BASE_URL}/api/v1/admin/merchants`, {
|
||||
const res = await fetch(`/api/admin/merchants`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
|
||||
+3
-1
@@ -24,7 +24,9 @@ export default function AdminLandingPage() {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const res = await fetch(`${API_BASE_URL}/api/admin/dashboard/overview`);
|
||||
const res = await fetch('/api/admin/dashboard/overview', {
|
||||
credentials: 'include',
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to load dashboard (${res.status})`);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Plus, Pencil, Trash2, X } from "lucide-react";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import {
|
||||
createPlatform,
|
||||
deletePlatform,
|
||||
@@ -15,7 +14,6 @@ import {
|
||||
} from "@/lib/api/platforms";
|
||||
|
||||
export default function AdminPlatformsPage() {
|
||||
const { getAuthHeaders } = useAuth();
|
||||
|
||||
const [platforms, setPlatforms] = useState<Platform[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -36,7 +34,7 @@ export default function AdminPlatformsPage() {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const data = await fetchPlatforms(getAuthHeaders());
|
||||
const data = await fetchPlatforms({});
|
||||
setPlatforms(data);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
@@ -77,7 +75,7 @@ export default function AdminPlatformsPage() {
|
||||
try {
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
await deletePlatform(getAuthHeaders(), p.id);
|
||||
await deletePlatform({}, p.id);
|
||||
await load();
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
@@ -103,7 +101,7 @@ export default function AdminPlatformsPage() {
|
||||
setError(null);
|
||||
|
||||
if (modalMode === "create") {
|
||||
await createPlatform(getAuthHeaders(), { ...form, key, label });
|
||||
await createPlatform({}, { ...form, key, label });
|
||||
} else if (selected) {
|
||||
const update: UpdatePlatformDto = {};
|
||||
if (key !== selected.key) update.key = key;
|
||||
@@ -111,7 +109,7 @@ export default function AdminPlatformsPage() {
|
||||
if ((form.isActive ?? true) !== selected.isActive)
|
||||
update.isActive = form.isActive;
|
||||
|
||||
await updatePlatform(getAuthHeaders(), selected.id, update);
|
||||
await updatePlatform({}, selected.id, update);
|
||||
}
|
||||
|
||||
setShowModal(false);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { API_BASE, getToken, qs } from "./auth";
|
||||
import { API_BASE, qs } from "./auth";
|
||||
import type {
|
||||
AdminProductRow,
|
||||
PageResponse,
|
||||
@@ -9,25 +9,14 @@ import type {
|
||||
|
||||
import type { CaliberDto } from "./types";
|
||||
|
||||
|
||||
async function authedFetch(url: string, init?: RequestInit) {
|
||||
const token = getToken();
|
||||
const res = await fetch(url, {
|
||||
credentials: "include",
|
||||
cache: "no-store",
|
||||
...(init ?? {}),
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
...(init?.headers ?? {}),
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
} as any,
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function fetchAdminRoles(params: Record<string, any>) {
|
||||
const res = await authedFetch(
|
||||
`${API_BASE}/api/v1/admin/products/roles?${qs(params)}`
|
||||
const queryString = qs(params);
|
||||
const res = await fetch(
|
||||
`/api/admin/products/roles${queryString ? `?${queryString}` : ''}`,
|
||||
{
|
||||
credentials: "include",
|
||||
cache: "no-store",
|
||||
}
|
||||
);
|
||||
if (!res.ok) throw new Error(`Failed to load roles (${res.status}): ${await res.text()}`);
|
||||
return (await res.json()) as string[];
|
||||
@@ -35,8 +24,13 @@ export async function fetchAdminRoles(params: Record<string, any>) {
|
||||
|
||||
// Facet/filtering can still use distinct values seen on products (optional)
|
||||
export async function fetchProductCaliberFacet(params: Record<string, any>) {
|
||||
const res = await authedFetch(
|
||||
`${API_BASE}/api/v1/admin/products/calibers?${qs(params)}`
|
||||
const queryString = qs(params);
|
||||
const res = await fetch(
|
||||
`/api/admin/products/calibers${queryString ? `?${queryString}` : ''}`,
|
||||
{
|
||||
credentials: "include",
|
||||
cache: "no-store",
|
||||
}
|
||||
);
|
||||
if (!res.ok)
|
||||
throw new Error(
|
||||
@@ -46,8 +40,13 @@ export async function fetchProductCaliberFacet(params: Record<string, any>) {
|
||||
}
|
||||
|
||||
export async function fetchAdminProducts(params: Record<string, any>) {
|
||||
const res = await authedFetch(
|
||||
`${API_BASE}/api/v1/admin/products?${qs(params)}`
|
||||
const queryString = qs(params);
|
||||
const res = await fetch(
|
||||
`/api/admin/products${queryString ? `?${queryString}` : ''}`,
|
||||
{
|
||||
credentials: "include",
|
||||
cache: "no-store",
|
||||
}
|
||||
);
|
||||
if (!res.ok) throw new Error(`Failed to load admin products (${res.status}): ${await res.text()}`);
|
||||
return (await res.json()) as PageResponse<AdminProductRow>;
|
||||
@@ -84,14 +83,11 @@ export async function bulkUpdate(
|
||||
classificationReason: "Admin bulk override";
|
||||
}>
|
||||
) {
|
||||
const token = getToken();
|
||||
|
||||
const res = await fetch(`${API_BASE}/api/v1/admin/products/bulk`, {
|
||||
const res = await fetch(`/api/admin/products/bulk`, {
|
||||
method: "PATCH",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
body: JSON.stringify({ productIds, ...set }),
|
||||
});
|
||||
@@ -103,8 +99,13 @@ export async function bulkUpdate(
|
||||
|
||||
|
||||
export async function fetchAdminCalibers(params: Record<string, any>) {
|
||||
const res = await authedFetch(
|
||||
`${API_BASE}/api/v1/admin/calibers?${qs(params)}`
|
||||
const queryString = qs(params);
|
||||
const res = await fetch(
|
||||
`/api/admin/calibers${queryString ? `?${queryString}` : ''}`,
|
||||
{
|
||||
credentials: "include",
|
||||
cache: "no-store",
|
||||
}
|
||||
);
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to load calibers (${res.status}): ${await res.text()}`);
|
||||
|
||||
@@ -17,11 +17,12 @@ export default function AdminUsersPage() {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
async function fetchUsers() {
|
||||
console.log("in the users fetch");
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const res = await fetch(`${API_BASE_URL}/admin/users`, {
|
||||
const res = await fetch(`/api/admin/users`, {
|
||||
headers: {
|
||||
...getAuthHeaders(),
|
||||
},
|
||||
|
||||
@@ -6,7 +6,9 @@ const API_BASE_URL =
|
||||
|
||||
// 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;
|
||||
const cookieStore = await cookies();
|
||||
|
||||
const token = cookieStore.get("session_token")?.value;
|
||||
|
||||
if (!token) {
|
||||
return NextResponse.json({ error: "Missing admin token" }, { status: 401 });
|
||||
@@ -32,4 +34,4 @@ export async function POST(req: Request) {
|
||||
status: res.status,
|
||||
headers: { "Content-Type": res.headers.get("content-type") ?? "application/json" },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ?? "";
|
||||
|
||||
// POST /api/admin/beta-invites?dryRun=true&limit=10&tokenMinutes=30
|
||||
export async function POST(req: Request) {
|
||||
const token = cookies().get("session_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" },
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
const API_BASE_URL =
|
||||
process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
async function getAuthHeader() {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get("session_token")?.value;
|
||||
if (!token) throw new Error("Unauthorized");
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
_req: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const resolvedParams = await params;
|
||||
const headers = await getAuthHeader();
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/admin/beta/requests/${resolvedParams.id}/invite`,
|
||||
{
|
||||
method: "POST",
|
||||
headers,
|
||||
cache: "no-store",
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
const API_BASE_URL =
|
||||
process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
async function getAuthHeader() {
|
||||
const token = cookies().get("session_token")?.value;
|
||||
if (!token) throw new Error("Unauthorized");
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
_req: Request,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/admin/beta/requests/${params.id}/invite`,
|
||||
{
|
||||
method: "POST",
|
||||
headers,
|
||||
cache: "no-store",
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
const API_BASE_URL =
|
||||
process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
async function getAuthHeader() {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get("session_token")?.value;
|
||||
if (!token) throw new Error("Unauthorized");
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
export async function GET(req: Request) {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
const url = new URL(req.url);
|
||||
const search = url.searchParams.toString();
|
||||
|
||||
const upstream = `${API_BASE_URL}/api/v1/admin/beta/requests${
|
||||
search ? `?${search}` : ""
|
||||
}`;
|
||||
|
||||
const res = await fetch(upstream, {
|
||||
headers,
|
||||
cache: "no-store",
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
const API_BASE_URL =
|
||||
process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
async function getAuthHeader() {
|
||||
const token = cookies().get("session_token")?.value;
|
||||
if (!token) throw new Error("Unauthorized");
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
export async function GET(req: Request) {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
const url = new URL(req.url);
|
||||
const search = url.searchParams.toString();
|
||||
|
||||
const upstream = `${API_BASE_URL}/api/v1/admin/beta/requests${
|
||||
search ? `?${search}` : ""
|
||||
}`;
|
||||
|
||||
const res = await fetch(upstream, {
|
||||
headers,
|
||||
cache: "no-store",
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
|
||||
const token = cookieStore.get('session_token')?.value;
|
||||
if (!token) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/admin/calibers${queryString ? `?${queryString}` : ''}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const token = cookies().get('session_token')?.value;
|
||||
if (!token) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/admin/calibers${queryString ? `?${queryString}` : ''}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
async function getAuthHeader() {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('session_token')?.value;
|
||||
if (!token) throw new Error('Unauthorized');
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
|
||||
const res = await fetch(`${API_BASE_URL}/api/admin/dashboard/overview`, {
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
async function getAuthHeader() {
|
||||
const token = cookies().get('session_token')?.value;
|
||||
if (!token) throw new Error('Unauthorized');
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
|
||||
const res = await fetch(`${API_BASE_URL}/api/admin/dashboard/overview`, {
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
const API_BASE_URL =
|
||||
process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
async function getAuthHeader() {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get("session_token")?.value;
|
||||
if (!token) throw new Error("Unauthorized");
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const resolvedParams = await params;
|
||||
const headers = await getAuthHeader();
|
||||
const body = await request.json();
|
||||
|
||||
const res = await fetch(`${API_BASE_URL}/api/email/${resolvedParams.id}`, {
|
||||
method: "PUT",
|
||||
headers: { ...headers, "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const resolvedParams = await params;
|
||||
const headers = await getAuthHeader();
|
||||
|
||||
const res = await fetch(`${API_BASE_URL}/api/email/${resolvedParams.id}`, {
|
||||
method: "DELETE",
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
const API_BASE_URL =
|
||||
process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
async function getAuthHeader() {
|
||||
const token = cookies().get("session_token")?.value;
|
||||
if (!token) throw new Error("Unauthorized");
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
const body = await request.json();
|
||||
|
||||
const res = await fetch(`${API_BASE_URL}/api/email/${params.id}`, {
|
||||
method: "PUT",
|
||||
headers: { ...headers, "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
|
||||
const res = await fetch(`${API_BASE_URL}/api/email/${params.id}`, {
|
||||
method: "DELETE",
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
const API_BASE_URL =
|
||||
process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
async function getAuthHeader() {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get("session_token")?.value;
|
||||
if (!token) throw new Error("Unauthorized");
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
const res = await fetch(`${API_BASE_URL}/api/email`, {
|
||||
headers,
|
||||
cache: "no-store",
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
const body = await request.json();
|
||||
|
||||
const res = await fetch(`${API_BASE_URL}/api/email`, {
|
||||
method: "POST",
|
||||
headers: { ...headers, "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
const API_BASE_URL =
|
||||
process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
async function getAuthHeader() {
|
||||
const token = cookies().get("session_token")?.value;
|
||||
if (!token) throw new Error("Unauthorized");
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
const res = await fetch(`${API_BASE_URL}/api/email`, {
|
||||
headers,
|
||||
cache: "no-store",
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
const body = await request.json();
|
||||
|
||||
const res = await fetch(`${API_BASE_URL}/api/email`, {
|
||||
method: "POST",
|
||||
headers: { ...headers, "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string; action: string }> }
|
||||
) {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
|
||||
const token = cookieStore.get('session_token')?.value;
|
||||
if (!token) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const resolvedParams = await params;
|
||||
|
||||
const { id, action } = resolvedParams;
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/admin/enrichment/${id}/${action}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
const contentType = res.headers.get('content-type');
|
||||
if (contentType?.includes('application/json')) {
|
||||
return NextResponse.json(await res.json());
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string; action: string } }
|
||||
) {
|
||||
try {
|
||||
const token = cookies().get('session_token')?.value;
|
||||
if (!token) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id, action } = params;
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/admin/enrichment/${id}/${action}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
const contentType = res.headers.get('content-type');
|
||||
if (contentType?.includes('application/json')) {
|
||||
return NextResponse.json(await res.json());
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
|
||||
const token = cookieStore.get('session_token')?.value;
|
||||
if (!token) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/admin/enrichment/ai/run${queryString ? `?${queryString}` : ''}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
const contentType = res.headers.get('content-type');
|
||||
if (contentType?.includes('application/json')) {
|
||||
return NextResponse.json(await res.json());
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const token = cookies().get('session_token')?.value;
|
||||
if (!token) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/admin/enrichment/ai/run${queryString ? `?${queryString}` : ''}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
const contentType = res.headers.get('content-type');
|
||||
if (contentType?.includes('application/json')) {
|
||||
return NextResponse.json(await res.json());
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { fixImageUrls } from '@/lib/server/image-utils';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
|
||||
const token = cookieStore.get('session_token')?.value;
|
||||
if (!token) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/admin/enrichment/queue2${queryString ? `?${queryString}` : ''}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
// Fix mixed content warnings: upgrade HTTP image URLs to HTTPS
|
||||
return NextResponse.json(fixImageUrls(data));
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { fixImageUrls } from '@/lib/server/image-utils';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const token = cookies().get('session_token')?.value;
|
||||
if (!token) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/admin/enrichment/queue2${queryString ? `?${queryString}` : ''}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
// Fix mixed content warnings: upgrade HTTP image URLs to HTTPS
|
||||
return NextResponse.json(fixImageUrls(data));
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
|
||||
const token = cookieStore.get('session_token')?.value;
|
||||
if (!token) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/admin/enrichment/run${queryString ? `?${queryString}` : ''}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
const contentType = res.headers.get('content-type');
|
||||
if (contentType?.includes('application/json')) {
|
||||
return NextResponse.json(await res.json());
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const token = cookies().get('session_token')?.value;
|
||||
if (!token) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/admin/enrichment/run${queryString ? `?${queryString}` : ''}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
const contentType = res.headers.get('content-type');
|
||||
if (contentType?.includes('application/json')) {
|
||||
return NextResponse.json(await res.json());
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
async function getAuthHeader() {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('session_token')?.value;
|
||||
if (!token) throw new Error('Unauthorized');
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
|
||||
const res = await fetch(`${API_BASE_URL}/api/admin/import-status/by-merchant`, {
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
async function getAuthHeader() {
|
||||
const token = cookies().get('session_token')?.value;
|
||||
if (!token) throw new Error('Unauthorized');
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
|
||||
const res = await fetch(`${API_BASE_URL}/api/admin/import-status/by-merchant`, {
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
async function getAuthHeader() {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('session_token')?.value;
|
||||
if (!token) throw new Error('Unauthorized');
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
|
||||
const res = await fetch(`${API_BASE_URL}/api/admin/import-status/summary`, {
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
async function getAuthHeader() {
|
||||
const token = cookies().get('session_token')?.value;
|
||||
if (!token) throw new Error('Unauthorized');
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
|
||||
const res = await fetch(`${API_BASE_URL}/api/admin/import-status/summary`, {
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
async function getAuthHeader() {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('session_token')?.value;
|
||||
if (!token) throw new Error('Unauthorized');
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
const body = await request.json();
|
||||
|
||||
const res = await fetch(`${API_BASE_URL}/api/admin/mapping/apply`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...headers,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
async function getAuthHeader() {
|
||||
const token = cookies().get('session_token')?.value;
|
||||
if (!token) throw new Error('Unauthorized');
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
const body = await request.json();
|
||||
|
||||
const res = await fetch(`${API_BASE_URL}/api/admin/mapping/apply`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...headers,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
async function getAuthHeader() {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('session_token')?.value;
|
||||
if (!token) throw new Error('Unauthorized');
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
|
||||
const res = await fetch(`${API_BASE_URL}/api/admin/mapping/options`, {
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
async function getAuthHeader() {
|
||||
const token = cookies().get('session_token')?.value;
|
||||
if (!token) throw new Error('Unauthorized');
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
|
||||
const res = await fetch(`${API_BASE_URL}/api/admin/mapping/options`, {
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
async function getAuthHeader() {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('session_token')?.value;
|
||||
if (!token) throw new Error('Unauthorized');
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
|
||||
const res = await fetch(`${API_BASE_URL}/api/admin/mapping/pending-buckets`, {
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
async function getAuthHeader() {
|
||||
const token = cookies().get('session_token')?.value;
|
||||
if (!token) throw new Error('Unauthorized');
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
|
||||
const res = await fetch(`${API_BASE_URL}/api/admin/mapping/pending-buckets`, {
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
async function getAuthHeader() {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('session_token')?.value;
|
||||
if (!token) throw new Error('Unauthorized');
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
const { searchParams } = new URL(request.url);
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/admin/mapping/raw-categories?${searchParams.toString()}`,
|
||||
{
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
async function getAuthHeader() {
|
||||
const token = cookies().get('session_token')?.value;
|
||||
if (!token) throw new Error('Unauthorized');
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
const { searchParams } = new URL(request.url);
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/admin/mapping/raw-categories?${searchParams.toString()}`,
|
||||
{
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
async function getAuthHeader() {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('session_token')?.value;
|
||||
if (!token) throw new Error('Unauthorized');
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
const body = await request.json();
|
||||
|
||||
const res = await fetch(`${API_BASE_URL}/api/admin/mapping/upsert`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...headers,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
async function getAuthHeader() {
|
||||
const token = cookies().get('session_token')?.value;
|
||||
if (!token) throw new Error('Unauthorized');
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
const body = await request.json();
|
||||
|
||||
const res = await fetch(`${API_BASE_URL}/api/admin/mapping/upsert`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...headers,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
async function getAuthHeader() {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('session_token')?.value;
|
||||
if (!token) throw new Error('Unauthorized');
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const resolvedParams = await params;
|
||||
const headers = await getAuthHeader();
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/admin/merchants/${resolvedParams.id}/import`,
|
||||
{ method: 'POST', headers }
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
async function getAuthHeader() {
|
||||
const token = cookies().get('session_token')?.value;
|
||||
if (!token) throw new Error('Unauthorized');
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/admin/merchants/${params.id}/import`,
|
||||
{ method: 'POST', headers }
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
async function getAuthHeader() {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('session_token')?.value;
|
||||
if (!token) throw new Error('Unauthorized');
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const resolvedParams = await params;
|
||||
const headers = await getAuthHeader();
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/admin/merchants/${resolvedParams.id}/offer-sync`,
|
||||
{ method: 'POST', headers }
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
async function getAuthHeader() {
|
||||
const token = cookies().get('session_token')?.value;
|
||||
if (!token) throw new Error('Unauthorized');
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/admin/merchants/${params.id}/offer-sync`,
|
||||
{ method: 'POST', headers }
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
async function getAuthHeader() {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('session_token')?.value;
|
||||
if (!token) throw new Error('Unauthorized');
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const resolvedParams = await params;
|
||||
const headers = await getAuthHeader();
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/admin/merchants/${resolvedParams.id}`,
|
||||
{ headers }
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const resolvedParams = await params;
|
||||
const headers = await getAuthHeader();
|
||||
const body = await request.json();
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/admin/merchants/${resolvedParams.id}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: { ...headers, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const resolvedParams = await params;
|
||||
const headers = await getAuthHeader();
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/admin/merchants/${resolvedParams.id}`,
|
||||
{ method: 'DELETE', headers }
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
async function getAuthHeader() {
|
||||
const token = cookies().get('session_token')?.value;
|
||||
if (!token) throw new Error('Unauthorized');
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/admin/merchants/${params.id}`,
|
||||
{ headers }
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
const body = await request.json();
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/admin/merchants/${params.id}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: { ...headers, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/admin/merchants/${params.id}`,
|
||||
{ method: 'DELETE', headers }
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
async function getAuthHeader() {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('session_token')?.value;
|
||||
if (!token) throw new Error('Unauthorized');
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
const res = await fetch(`${API_BASE_URL}/api/v1/admin/merchants`, {
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
const body = await request.json();
|
||||
|
||||
const res = await fetch(`${API_BASE_URL}/api/v1/admin/merchants`, {
|
||||
method: 'POST',
|
||||
headers: { ...headers, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
async function getAuthHeader() {
|
||||
const token = cookies().get('session_token')?.value;
|
||||
if (!token) throw new Error('Unauthorized');
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
const res = await fetch(`${API_BASE_URL}/api/v1/admin/merchants`, {
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
const body = await request.json();
|
||||
|
||||
const res = await fetch(`${API_BASE_URL}/api/v1/admin/merchants`, {
|
||||
method: 'POST',
|
||||
headers: { ...headers, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
async function getAuthHeader() {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('session_token')?.value;
|
||||
if (!token) throw new Error('Unauthorized');
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const resolvedParams = await params;
|
||||
const headers = await getAuthHeader();
|
||||
const body = await request.json();
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/admin/platforms/${resolvedParams.id}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
...headers,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const resolvedParams = await params;
|
||||
const headers = await getAuthHeader();
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/admin/platforms/${resolvedParams.id}`,
|
||||
{ method: 'DELETE', headers }
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
async function getAuthHeader() {
|
||||
const token = cookies().get('session_token')?.value;
|
||||
if (!token) throw new Error('Unauthorized');
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
const body = await request.json();
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/admin/platforms/${params.id}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
...headers,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/admin/platforms/${params.id}`,
|
||||
{ method: 'DELETE', headers }
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
async function getAuthHeader() {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('session_token')?.value;
|
||||
if (!token) throw new Error('Unauthorized');
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/admin/platforms${queryString ? `?${queryString}` : ''}`,
|
||||
{
|
||||
headers: {
|
||||
...headers,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
const body = await request.json();
|
||||
|
||||
const res = await fetch(`${API_BASE_URL}/api/v1/admin/platforms`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...headers,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
async function getAuthHeader() {
|
||||
const token = cookies().get('session_token')?.value;
|
||||
if (!token) throw new Error('Unauthorized');
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/admin/platforms${queryString ? `?${queryString}` : ''}`,
|
||||
{
|
||||
headers: {
|
||||
...headers,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
const body = await request.json();
|
||||
|
||||
const res = await fetch(`${API_BASE_URL}/api/v1/admin/platforms`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...headers,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
export async function PATCH(request: NextRequest) {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
|
||||
const token = cookieStore.get('session_token')?.value;
|
||||
if (!token) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/admin/products/bulk`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
export async function PATCH(request: NextRequest) {
|
||||
try {
|
||||
const token = cookies().get('session_token')?.value;
|
||||
if (!token) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/admin/products/bulk`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
|
||||
const token = cookieStore.get('session_token')?.value;
|
||||
if (!token) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/admin/products/calibers${queryString ? `?${queryString}` : ''}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const token = cookies().get('session_token')?.value;
|
||||
if (!token) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/admin/products/calibers${queryString ? `?${queryString}` : ''}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
|
||||
const token = cookieStore.get('session_token')?.value;
|
||||
if (!token) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/admin/products/roles${queryString ? `?${queryString}` : ''}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const token = cookies().get('session_token')?.value;
|
||||
if (!token) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/admin/products/roles${queryString ? `?${queryString}` : ''}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { fixImageUrls } from '@/lib/server/image-utils';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
|
||||
const token = cookieStore.get('session_token')?.value;
|
||||
if (!token) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/admin/products${queryString ? `?${queryString}` : ''}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
return NextResponse.json(fixImageUrls(data));
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { fixImageUrls } from '@/lib/server/image-utils';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const token = cookies().get('session_token')?.value;
|
||||
if (!token) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/admin/products${queryString ? `?${queryString}` : ''}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
return NextResponse.json(fixImageUrls(data));
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
async function getAuthHeader() {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('session_token')?.value;
|
||||
if (!token) throw new Error('Unauthorized');
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const resolvedParams = await params;
|
||||
const headers = await getAuthHeader();
|
||||
const body = await request.json();
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/admin/platforms/${resolvedParams.id}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
...headers,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const resolvedParams = await params;
|
||||
const headers = await getAuthHeader();
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/admin/platforms/${resolvedParams.id}`,
|
||||
{ method: 'DELETE', headers }
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
async function getAuthHeader() {
|
||||
const token = cookies().get('session_token')?.value;
|
||||
if (!token) throw new Error('Unauthorized');
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
const body = await request.json();
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/admin/platforms/${params.id}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
...headers,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/admin/platforms/${params.id}`,
|
||||
{ method: 'DELETE', headers }
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
async function getAuthHeader() {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('session_token')?.value;
|
||||
if (!token) throw new Error('Unauthorized');
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/admin/users${queryString ? `?${queryString}` : ''}`,
|
||||
{
|
||||
headers: {
|
||||
...headers,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
const data = await res.json();
|
||||
return NextResponse.json(data);
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
const body = await request.json();
|
||||
|
||||
const res = await fetch(`${API_BASE_URL}/api/v1/admin/users`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...headers,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
async function getAuthHeader() {
|
||||
const token = cookies().get('session_token')?.value;
|
||||
if (!token) throw new Error('Unauthorized');
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/admin/users${queryString ? `?${queryString}` : ''}`,
|
||||
{
|
||||
headers: {
|
||||
...headers,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
const data = await res.json();
|
||||
return NextResponse.json(data);
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
const body = await request.json();
|
||||
|
||||
const res = await fetch(`${API_BASE_URL}/api/v1/admin/users`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...headers,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
const res = await fetch(`${API_BASE_URL}/api/auth/beta/signup`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
// Forward to backend API
|
||||
const res = await fetch(`${API_BASE_URL}/api/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => '');
|
||||
return NextResponse.json(
|
||||
{ error: text || 'Login failed' },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
// Set HTTP-only cookie instead of returning token
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.set('session_token', data.token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 60 * 60 * 24 * 7, // 7 days
|
||||
path: '/',
|
||||
});
|
||||
|
||||
// Return user data without token
|
||||
return NextResponse.json({
|
||||
user: data.user,
|
||||
});
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Login failed' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
export async function POST() {
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.delete('session_token');
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
const res = await fetch(`${API_BASE_URL}/api/auth/magic/exchange`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('session_token')?.value;
|
||||
|
||||
if (!token) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE_URL}/api/users/me`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
cookieStore.delete('session_token');
|
||||
return NextResponse.json({ error: 'Session expired' }, { status: 401 });
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Failed to fetch user' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
// Forward to backend API
|
||||
const res = await fetch(`${API_BASE_URL}/api/auth/register`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => '');
|
||||
return NextResponse.json(
|
||||
{ error: text || 'Registration failed' },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
// Set HTTP-only cookie on successful registration
|
||||
if (data.token) {
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.set('session_token', data.token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 60 * 60 * 24 * 7, // 7 days
|
||||
path: '/',
|
||||
});
|
||||
}
|
||||
|
||||
// Return user data without token
|
||||
return NextResponse.json({
|
||||
user: data.user,
|
||||
});
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Registration failed' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
async function getAuthHeader() {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('session_token')?.value;
|
||||
if (!token) throw new Error('Unauthorized');
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const headers = await getAuthHeader();
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/gunbuilder/builds/${id}`,
|
||||
{ headers }
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const headers = await getAuthHeader();
|
||||
const body = await request.json();
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/gunbuilder/builds/${id}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: { ...headers, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const headers = await getAuthHeader();
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/gunbuilder/builds/${id}`,
|
||||
{ method: 'DELETE', headers }
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const { id } = params;
|
||||
|
||||
// Public build detail - no auth required
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/builds/${id}`,
|
||||
{
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Failed to fetch build' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
|
||||
// Public builds endpoint - no auth required
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/builds?${searchParams}`,
|
||||
{
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Failed to fetch builds' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
async function getAuthHeader() {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('session_token')?.value;
|
||||
if (!token) throw new Error('Unauthorized');
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
const { searchParams } = new URL(request.url);
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/gunbuilder/builds?${searchParams}`,
|
||||
{ headers }
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const headers = await getAuthHeader();
|
||||
const body = await request.json();
|
||||
|
||||
const res = await fetch(`${API_BASE_URL}/api/v1/gunbuilder/builds`, {
|
||||
method: 'POST',
|
||||
headers: { ...headers, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json(), { status: res.status });
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
|
||||
// No auth required for public catalog
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/catalog/options?${searchParams}`
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Failed to fetch catalog' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const { id } = params;
|
||||
|
||||
// No auth required for public catalog
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/products/${id}/offers`,
|
||||
{
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Failed to fetch product offers' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const { id } = params;
|
||||
|
||||
// No auth required for public catalog
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/products/${id}`,
|
||||
{
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Failed to fetch product' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
// No auth required for public catalog
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/catalog/products/by-ids`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Failed to fetch products' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
|
||||
// No auth required for public catalog
|
||||
const res = await fetch(
|
||||
`${API_BASE_URL}/api/v1/products?${searchParams}`,
|
||||
{
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
cache: 'no-store',
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: await res.text() },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(await res.json());
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message || 'Failed to fetch products' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
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 }
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user