Compare commits
28 Commits
| 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 |
@@ -7,7 +7,3 @@ NEXT_PUBLIC_LAUNCH_ONLY_ROOT=false
|
|||||||
|
|
||||||
NEXT_PUBLIC_SHORTLINK_BASE_URL=http://localhost:8080
|
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
|
NEXT_PUBLIC_API_BASE_URL=http://battlbuilder-api:8080
|
||||||
|
|
||||||
# Middleware to limited site to only root page
|
# 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
|
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
|
name: CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
pull_request:
|
branches:
|
||||||
|
- develop
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
deploy:
|
||||||
runs-on: ubuntu
|
runs-on: ubuntu
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- 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 { useApi } from "@/lib/api";
|
||||||
import { useAuth } from "@/context/AuthContext";
|
import { useAuth } from "@/context/AuthContext";
|
||||||
|
import { trackBuildStarted, trackPartSelected, trackAffiliateLinkClicked, trackBuildShared, trackBuildSaved } from "@/lib/analytics";
|
||||||
|
|
||||||
import { CATEGORIES } from "@/data/gunbuilderParts";
|
import { CATEGORIES } from "@/data/gunbuilderParts";
|
||||||
import { BUILDER_SLOTS, isSlotSatisfied } from "@/data/builderSlots";
|
import { BUILDER_SLOTS, isSlotSatisfied } from "@/data/builderSlots";
|
||||||
@@ -295,7 +296,7 @@ function buildAssemblyRows(params: {
|
|||||||
rows.push({
|
rows.push({
|
||||||
key: `${groupLabel}-${paths.complete}`,
|
key: `${groupLabel}-${paths.complete}`,
|
||||||
kind: "category",
|
kind: "category",
|
||||||
label: "Completed Lower (Fastest)",
|
label: `Completed ${groupLabel.split(" ")[0]} (Fastest)`,
|
||||||
categoryId: paths.complete,
|
categoryId: paths.complete,
|
||||||
indent: 0,
|
indent: 0,
|
||||||
pill: mode === "complete" ? "Selected" : undefined,
|
pill: mode === "complete" ? "Selected" : undefined,
|
||||||
@@ -355,6 +356,8 @@ function GunbuilderPageContent() {
|
|||||||
// Parts data state
|
// Parts data state
|
||||||
// -----------------------------
|
// -----------------------------
|
||||||
const [parts, setParts] = useState<Part[]>([]);
|
const [parts, setParts] = useState<Part[]>([]);
|
||||||
|
const previousPartIdsRef = useRef<Set<string>>(new Set());
|
||||||
|
const isFirstPartsHydrationRef = useRef(true);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
@@ -369,20 +372,33 @@ function GunbuilderPageContent() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window === "undefined") return;
|
if (typeof window === "undefined") return;
|
||||||
|
|
||||||
// Hydrate persisted build state only. URL actions (platform/select/remove)
|
// Intentional: Object.keys(...).length > 0 tightens the original condition.
|
||||||
// are handled by the dedicated useSearchParams effect later in this file.
|
// 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);
|
const stored = localStorage.getItem(STORAGE_KEY);
|
||||||
if (stored) {
|
if (stored) {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(stored);
|
const parsed = JSON.parse(stored);
|
||||||
if (parsed && typeof parsed === "object") {
|
if (parsed && typeof parsed === "object" && Object.keys(parsed).length > 0) {
|
||||||
setBuild(normalizeBuildState(parsed));
|
setBuild(normalizeBuildState(parsed));
|
||||||
|
hasBuild = true;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// 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
|
// Signal that hydration is complete - this allows URL param processing to run
|
||||||
setIsHydrated(true);
|
setIsHydrated(true);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -602,6 +618,7 @@ function GunbuilderPageContent() {
|
|||||||
if (ids.length === 0) {
|
if (ids.length === 0) {
|
||||||
lastHydrateKeyRef.current = "";
|
lastHydrateKeyRef.current = "";
|
||||||
setParts([]);
|
setParts([]);
|
||||||
|
previousPartIdsRef.current = new Set(); // keep in sync so re-selecting fires the event
|
||||||
setError(null);
|
setError(null);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
@@ -654,6 +671,21 @@ function GunbuilderPageContent() {
|
|||||||
|
|
||||||
setParts(hydrated);
|
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
|
// ✅ Mark key as hydrated ONLY after success
|
||||||
lastHydrateKeyRef.current = key;
|
lastHydrateKeyRef.current = key;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -842,6 +874,7 @@ function GunbuilderPageContent() {
|
|||||||
type: "success",
|
type: "success",
|
||||||
message: "Saved to your Vault ✅ ",
|
message: "Saved to your Vault ✅ ",
|
||||||
});
|
});
|
||||||
|
trackBuildSaved();
|
||||||
|
|
||||||
// Close modal + reset inputs for next variant
|
// Close modal + reset inputs for next variant
|
||||||
setSaveAsOpen(false);
|
setSaveAsOpen(false);
|
||||||
@@ -1055,6 +1088,7 @@ function GunbuilderPageContent() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(shareUrl);
|
await navigator.clipboard.writeText(shareUrl);
|
||||||
|
trackBuildShared();
|
||||||
setShareStatus("Shareable link copied to clipboard.");
|
setShareStatus("Shareable link copied to clipboard.");
|
||||||
} catch {
|
} catch {
|
||||||
setShareStatus("Could not copy link to clipboard.");
|
setShareStatus("Could not copy link to clipboard.");
|
||||||
@@ -1074,6 +1108,7 @@ function GunbuilderPageContent() {
|
|||||||
text: "Check out my Battl Build.",
|
text: "Check out my Battl Build.",
|
||||||
url: shareUrl,
|
url: shareUrl,
|
||||||
});
|
});
|
||||||
|
trackBuildShared(); // fires only on successful share, not on cancel
|
||||||
setShareStatus("Share dialog opened.");
|
setShareStatus("Share dialog opened.");
|
||||||
} catch {
|
} catch {
|
||||||
setShareStatus(
|
setShareStatus(
|
||||||
@@ -1523,25 +1558,25 @@ function GunbuilderPageContent() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="overflow-x-auto rounded-md border border-zinc-800 bg-zinc-950/80">
|
<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>
|
<thead>
|
||||||
<tr className="border-b border-zinc-800 bg-zinc-900/60">
|
<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
|
Component / Part Type
|
||||||
</th>
|
</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
|
Brand
|
||||||
</th>
|
</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
|
Price
|
||||||
</th>
|
</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
|
Sale Price
|
||||||
</th>
|
</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
|
Caliber
|
||||||
</th>
|
</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
|
Buy / Choose
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -1557,7 +1592,7 @@ function GunbuilderPageContent() {
|
|||||||
>
|
>
|
||||||
<td
|
<td
|
||||||
colSpan={6}
|
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="flex items-center justify-between gap-2">
|
||||||
<div className="font-semibold text-zinc-400">
|
<div className="font-semibold text-zinc-400">
|
||||||
@@ -1606,7 +1641,7 @@ function GunbuilderPageContent() {
|
|||||||
row.tone === "muted" ? "opacity-70" : ""
|
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">
|
<div className="flex items-start gap-2">
|
||||||
{indent > 0 && (
|
{indent > 0 && (
|
||||||
<div
|
<div
|
||||||
@@ -1654,24 +1689,24 @@ function GunbuilderPageContent() {
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</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 : "—"}
|
{selectedPart ? selectedPart.brand : "—"}
|
||||||
</td>
|
</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
|
||||||
? `$${selectedPart.price.toFixed(2)}`
|
? `$${selectedPart.price.toFixed(2)}`
|
||||||
: "—"}
|
: "—"}
|
||||||
</td>
|
</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>
|
||||||
<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>
|
||||||
|
|
||||||
<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">
|
<div className="flex justify-end gap-2">
|
||||||
{isLocked ? (
|
{isLocked ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -1704,6 +1739,14 @@ function GunbuilderPageContent() {
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
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"
|
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
|
Buy
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { useParams, useRouter, useSearchParams } from "next/navigation";
|
|||||||
|
|
||||||
import type { BuilderSlotKey } from "@/types/builderSlots";
|
import type { BuilderSlotKey } from "@/types/builderSlots";
|
||||||
import { PART_ROLE_TO_CATEGORY, normalizePartRole } from "@/lib/catalogMappings";
|
import { PART_ROLE_TO_CATEGORY, normalizePartRole } from "@/lib/catalogMappings";
|
||||||
|
import { trackAffiliateLinkClicked } from "@/lib/analytics";
|
||||||
|
import { ProductImage } from "@/components/parts/ProductImage";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API Shapes
|
* API Shapes
|
||||||
@@ -492,12 +494,10 @@ export default function ProductDetailsPage() {
|
|||||||
className="h-full w-full"
|
className="h-full w-full"
|
||||||
title="Click to enlarge"
|
title="Click to enlarge"
|
||||||
>
|
>
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
<ProductImage
|
||||||
<img
|
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
alt={product.name}
|
alt={product.name}
|
||||||
className="h-full w-full object-contain p-2"
|
className="h-full w-full object-contain p-2"
|
||||||
loading="lazy"
|
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
@@ -699,6 +699,7 @@ export default function ProductDetailsPage() {
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
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"
|
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
|
Buy
|
||||||
</a>
|
</a>
|
||||||
@@ -726,6 +727,7 @@ export default function ProductDetailsPage() {
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
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"
|
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)} →
|
Buy from {merchantLabel(bestOffer)} →
|
||||||
</a>
|
</a>
|
||||||
@@ -768,8 +770,7 @@ export default function ProductDetailsPage() {
|
|||||||
Close
|
Close
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
<ProductImage
|
||||||
<img
|
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
alt={product?.name ?? "Product image"}
|
alt={product?.name ?? "Product image"}
|
||||||
className="max-h-[90vh] max-w-[90vw] rounded-lg border border-zinc-800 bg-black object-contain"
|
className="max-h-[90vh] max-w-[90vw] rounded-lg border border-zinc-800 bg-black object-contain"
|
||||||
|
|||||||
@@ -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 type { ReactNode } from "react";
|
||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
|
|
||||||
import { BuilderNav } from "@/components/BuilderNav";
|
import { BuilderNavConditional } from "@/components/BuilderNavConditional";
|
||||||
import { TopNav } from "@/components/TopNav";
|
import { TopNav } from "@/components/TopNav";
|
||||||
import { Footer } from "@/components/Footer";
|
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">
|
<div className="min-h-screen bg-white text-zinc-900 dark:bg-neutral-950 dark:text-zinc-50">
|
||||||
<TopNav />
|
<TopNav />
|
||||||
<Suspense fallback={<div className="h-[52px]" />}>
|
<Suspense fallback={<div className="h-[52px]" />}>
|
||||||
<BuilderNav />
|
<BuilderNavConditional />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<main className="min-h-screen">{children}</main>
|
<main className="min-h-screen">{children}</main>
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|||||||
@@ -417,9 +417,9 @@ export default function PrivacyPolicy() {
|
|||||||
<br />
|
<br />
|
||||||
Battl Builder, LLC.
|
Battl Builder, LLC.
|
||||||
<br />
|
<br />
|
||||||
[Your Address]
|
{/* [Your Address]
|
||||||
<br />
|
<br />
|
||||||
[City, State, ZIP]
|
[City, State, ZIP] */}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-4 text-sm">
|
<p className="mt-4 text-sm">
|
||||||
|
|||||||
@@ -168,12 +168,6 @@ export default function AdminLayout({
|
|||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-zinc-200">Battl Control Panel</p>
|
<p className="text-sm text-zinc-200">Battl Control Panel</p>
|
||||||
</div>
|
</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>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* ✅ min-w-0 here prevents table min-width from widening the page */}
|
{/* ✅ min-w-0 here prevents table min-width from widening the page */}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { Plus, Pencil, Trash2, X } from "lucide-react";
|
import { Plus, Pencil, Trash2, X } from "lucide-react";
|
||||||
import { useAuth } from "@/context/AuthContext";
|
|
||||||
import {
|
import {
|
||||||
createPlatform,
|
createPlatform,
|
||||||
deletePlatform,
|
deletePlatform,
|
||||||
@@ -15,7 +14,6 @@ import {
|
|||||||
} from "@/lib/api/platforms";
|
} from "@/lib/api/platforms";
|
||||||
|
|
||||||
export default function AdminPlatformsPage() {
|
export default function AdminPlatformsPage() {
|
||||||
const { getAuthHeaders } = useAuth();
|
|
||||||
|
|
||||||
const [platforms, setPlatforms] = useState<Platform[]>([]);
|
const [platforms, setPlatforms] = useState<Platform[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -36,7 +34,7 @@ export default function AdminPlatformsPage() {
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
const data = await fetchPlatforms(getAuthHeaders());
|
const data = await fetchPlatforms({});
|
||||||
setPlatforms(data);
|
setPlatforms(data);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@@ -77,7 +75,7 @@ export default function AdminPlatformsPage() {
|
|||||||
try {
|
try {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
await deletePlatform(getAuthHeaders(), p.id);
|
await deletePlatform({}, p.id);
|
||||||
await load();
|
await load();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@@ -103,7 +101,7 @@ export default function AdminPlatformsPage() {
|
|||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
if (modalMode === "create") {
|
if (modalMode === "create") {
|
||||||
await createPlatform(getAuthHeaders(), { ...form, key, label });
|
await createPlatform({}, { ...form, key, label });
|
||||||
} else if (selected) {
|
} else if (selected) {
|
||||||
const update: UpdatePlatformDto = {};
|
const update: UpdatePlatformDto = {};
|
||||||
if (key !== selected.key) update.key = key;
|
if (key !== selected.key) update.key = key;
|
||||||
@@ -111,7 +109,7 @@ export default function AdminPlatformsPage() {
|
|||||||
if ((form.isActive ?? true) !== selected.isActive)
|
if ((form.isActive ?? true) !== selected.isActive)
|
||||||
update.isActive = form.isActive;
|
update.isActive = form.isActive;
|
||||||
|
|
||||||
await updatePlatform(getAuthHeaders(), selected.id, update);
|
await updatePlatform({}, selected.id, update);
|
||||||
}
|
}
|
||||||
|
|
||||||
setShowModal(false);
|
setShowModal(false);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { API_BASE, getToken, qs } from "./auth";
|
import { API_BASE, qs } from "./auth";
|
||||||
import type {
|
import type {
|
||||||
AdminProductRow,
|
AdminProductRow,
|
||||||
PageResponse,
|
PageResponse,
|
||||||
@@ -9,25 +9,14 @@ import type {
|
|||||||
|
|
||||||
import type { CaliberDto } from "./types";
|
import type { CaliberDto } from "./types";
|
||||||
|
|
||||||
|
export async function fetchAdminRoles(params: Record<string, any>) {
|
||||||
async function authedFetch(url: string, init?: RequestInit) {
|
const queryString = qs(params);
|
||||||
const token = getToken();
|
const res = await fetch(
|
||||||
const res = await fetch(url, {
|
`/api/admin/products/roles${queryString ? `?${queryString}` : ''}`,
|
||||||
|
{
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
cache: "no-store",
|
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)}`
|
|
||||||
);
|
);
|
||||||
if (!res.ok) throw new Error(`Failed to load roles (${res.status}): ${await res.text()}`);
|
if (!res.ok) throw new Error(`Failed to load roles (${res.status}): ${await res.text()}`);
|
||||||
return (await res.json()) as string[];
|
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)
|
// Facet/filtering can still use distinct values seen on products (optional)
|
||||||
export async function fetchProductCaliberFacet(params: Record<string, any>) {
|
export async function fetchProductCaliberFacet(params: Record<string, any>) {
|
||||||
const res = await authedFetch(
|
const queryString = qs(params);
|
||||||
`${API_BASE}/api/v1/admin/products/calibers?${qs(params)}`
|
const res = await fetch(
|
||||||
|
`/api/admin/products/calibers${queryString ? `?${queryString}` : ''}`,
|
||||||
|
{
|
||||||
|
credentials: "include",
|
||||||
|
cache: "no-store",
|
||||||
|
}
|
||||||
);
|
);
|
||||||
if (!res.ok)
|
if (!res.ok)
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -46,8 +40,13 @@ export async function fetchProductCaliberFacet(params: Record<string, any>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchAdminProducts(params: Record<string, any>) {
|
export async function fetchAdminProducts(params: Record<string, any>) {
|
||||||
const res = await authedFetch(
|
const queryString = qs(params);
|
||||||
`${API_BASE}/api/v1/admin/products?${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()}`);
|
if (!res.ok) throw new Error(`Failed to load admin products (${res.status}): ${await res.text()}`);
|
||||||
return (await res.json()) as PageResponse<AdminProductRow>;
|
return (await res.json()) as PageResponse<AdminProductRow>;
|
||||||
@@ -84,14 +83,11 @@ export async function bulkUpdate(
|
|||||||
classificationReason: "Admin bulk override";
|
classificationReason: "Admin bulk override";
|
||||||
}>
|
}>
|
||||||
) {
|
) {
|
||||||
const token = getToken();
|
const res = await fetch(`/api/admin/products/bulk`, {
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE}/api/v1/admin/products/bulk`, {
|
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ productIds, ...set }),
|
body: JSON.stringify({ productIds, ...set }),
|
||||||
});
|
});
|
||||||
@@ -103,8 +99,13 @@ export async function bulkUpdate(
|
|||||||
|
|
||||||
|
|
||||||
export async function fetchAdminCalibers(params: Record<string, any>) {
|
export async function fetchAdminCalibers(params: Record<string, any>) {
|
||||||
const res = await authedFetch(
|
const queryString = qs(params);
|
||||||
`${API_BASE}/api/v1/admin/calibers?${qs(params)}`
|
const res = await fetch(
|
||||||
|
`/api/admin/calibers${queryString ? `?${queryString}` : ''}`,
|
||||||
|
{
|
||||||
|
credentials: "include",
|
||||||
|
cache: "no-store",
|
||||||
|
}
|
||||||
);
|
);
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error(`Failed to load calibers (${res.status}): ${await res.text()}`);
|
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);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
async function fetchUsers() {
|
async function fetchUsers() {
|
||||||
|
console.log("in the users fetch");
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE_URL}/admin/users`, {
|
const res = await fetch(`/api/admin/users`, {
|
||||||
headers: {
|
headers: {
|
||||||
...getAuthHeaders(),
|
...getAuthHeaders(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ const API_BASE_URL =
|
|||||||
|
|
||||||
// POST /api/admin/beta-invites?dryRun=true&limit=10&tokenMinutes=30
|
// POST /api/admin/beta-invites?dryRun=true&limit=10&tokenMinutes=30
|
||||||
export async function POST(req: Request) {
|
export async function POST(req: Request) {
|
||||||
const token = cookies().get("session_token")?.value;
|
const cookieStore = await cookies();
|
||||||
|
|
||||||
|
const token = cookieStore.get("session_token")?.value;
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return NextResponse.json({ error: "Missing admin token" }, { status: 401 });
|
return NextResponse.json({ error: "Missing admin token" }, { status: 401 });
|
||||||
|
|||||||
@@ -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" },
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -5,19 +5,21 @@ const API_BASE_URL =
|
|||||||
process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||||
|
|
||||||
async function getAuthHeader() {
|
async function getAuthHeader() {
|
||||||
const token = cookies().get("session_token")?.value;
|
const cookieStore = await cookies();
|
||||||
|
const token = cookieStore.get("session_token")?.value;
|
||||||
if (!token) throw new Error("Unauthorized");
|
if (!token) throw new Error("Unauthorized");
|
||||||
return { Authorization: `Bearer ${token}` };
|
return { Authorization: `Bearer ${token}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(
|
export async function POST(
|
||||||
_req: Request,
|
_req: Request,
|
||||||
{ params }: { params: { id: string } }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
const resolvedParams = await params;
|
||||||
const headers = await getAuthHeader();
|
const headers = await getAuthHeader();
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${API_BASE_URL}/api/v1/admin/beta/requests/${params.id}/invite`,
|
`${API_BASE_URL}/api/v1/admin/beta/requests/${resolvedParams.id}/invite`,
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers,
|
headers,
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,8 @@ const API_BASE_URL =
|
|||||||
process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||||
|
|
||||||
async function getAuthHeader() {
|
async function getAuthHeader() {
|
||||||
const token = cookies().get("session_token")?.value;
|
const cookieStore = await cookies();
|
||||||
|
const token = cookieStore.get("session_token")?.value;
|
||||||
if (!token) throw new Error("Unauthorized");
|
if (!token) throw new Error("Unauthorized");
|
||||||
return { Authorization: `Bearer ${token}` };
|
return { Authorization: `Bearer ${token}` };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,8 @@ import { cookies } from 'next/headers';
|
|||||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||||
|
|
||||||
async function getAuthHeader() {
|
async function getAuthHeader() {
|
||||||
const token = cookies().get('session_token')?.value;
|
const cookieStore = await cookies();
|
||||||
|
const token = cookieStore.get('session_token')?.value;
|
||||||
if (!token) throw new Error('Unauthorized');
|
if (!token) throw new Error('Unauthorized');
|
||||||
return { Authorization: `Bearer ${token}` };
|
return { Authorization: `Bearer ${token}` };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,20 +5,22 @@ const API_BASE_URL =
|
|||||||
process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||||
|
|
||||||
async function getAuthHeader() {
|
async function getAuthHeader() {
|
||||||
const token = cookies().get("session_token")?.value;
|
const cookieStore = await cookies();
|
||||||
|
const token = cookieStore.get("session_token")?.value;
|
||||||
if (!token) throw new Error("Unauthorized");
|
if (!token) throw new Error("Unauthorized");
|
||||||
return { Authorization: `Bearer ${token}` };
|
return { Authorization: `Bearer ${token}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function PUT(
|
export async function PUT(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
{ params }: { params: { id: string } }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
const resolvedParams = await params;
|
||||||
const headers = await getAuthHeader();
|
const headers = await getAuthHeader();
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE_URL}/api/email/${params.id}`, {
|
const res = await fetch(`${API_BASE_URL}/api/email/${resolvedParams.id}`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: { ...headers, "Content-Type": "application/json" },
|
headers: { ...headers, "Content-Type": "application/json" },
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
@@ -39,12 +41,13 @@ export async function PUT(
|
|||||||
|
|
||||||
export async function DELETE(
|
export async function DELETE(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
{ params }: { params: { id: string } }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
const resolvedParams = await params;
|
||||||
const headers = await getAuthHeader();
|
const headers = await getAuthHeader();
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE_URL}/api/email/${params.id}`, {
|
const res = await fetch(`${API_BASE_URL}/api/email/${resolvedParams.id}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers,
|
headers,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,8 @@ const API_BASE_URL =
|
|||||||
process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||||
|
|
||||||
async function getAuthHeader() {
|
async function getAuthHeader() {
|
||||||
const token = cookies().get("session_token")?.value;
|
const cookieStore = await cookies();
|
||||||
|
const token = cookieStore.get("session_token")?.value;
|
||||||
if (!token) throw new Error("Unauthorized");
|
if (!token) throw new Error("Unauthorized");
|
||||||
return { Authorization: `Bearer ${token}` };
|
return { Authorization: `Bearer ${token}` };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,8 @@ import { cookies } from 'next/headers';
|
|||||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||||
|
|
||||||
async function getAuthHeader() {
|
async function getAuthHeader() {
|
||||||
const token = cookies().get('session_token')?.value;
|
const cookieStore = await cookies();
|
||||||
|
const token = cookieStore.get('session_token')?.value;
|
||||||
if (!token) throw new Error('Unauthorized');
|
if (!token) throw new Error('Unauthorized');
|
||||||
return { Authorization: `Bearer ${token}` };
|
return { Authorization: `Bearer ${token}` };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,8 @@ import { cookies } from 'next/headers';
|
|||||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||||
|
|
||||||
async function getAuthHeader() {
|
async function getAuthHeader() {
|
||||||
const token = cookies().get('session_token')?.value;
|
const cookieStore = await cookies();
|
||||||
|
const token = cookieStore.get('session_token')?.value;
|
||||||
if (!token) throw new Error('Unauthorized');
|
if (!token) throw new Error('Unauthorized');
|
||||||
return { Authorization: `Bearer ${token}` };
|
return { Authorization: `Bearer ${token}` };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,8 @@ import { cookies } from 'next/headers';
|
|||||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||||
|
|
||||||
async function getAuthHeader() {
|
async function getAuthHeader() {
|
||||||
const token = cookies().get('session_token')?.value;
|
const cookieStore = await cookies();
|
||||||
|
const token = cookieStore.get('session_token')?.value;
|
||||||
if (!token) throw new Error('Unauthorized');
|
if (!token) throw new Error('Unauthorized');
|
||||||
return { Authorization: `Bearer ${token}` };
|
return { Authorization: `Bearer ${token}` };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,8 @@ import { cookies } from 'next/headers';
|
|||||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||||
|
|
||||||
async function getAuthHeader() {
|
async function getAuthHeader() {
|
||||||
const token = cookies().get('session_token')?.value;
|
const cookieStore = await cookies();
|
||||||
|
const token = cookieStore.get('session_token')?.value;
|
||||||
if (!token) throw new Error('Unauthorized');
|
if (!token) throw new Error('Unauthorized');
|
||||||
return { Authorization: `Bearer ${token}` };
|
return { Authorization: `Bearer ${token}` };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,8 @@ import { cookies } from 'next/headers';
|
|||||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||||
|
|
||||||
async function getAuthHeader() {
|
async function getAuthHeader() {
|
||||||
const token = cookies().get('session_token')?.value;
|
const cookieStore = await cookies();
|
||||||
|
const token = cookieStore.get('session_token')?.value;
|
||||||
if (!token) throw new Error('Unauthorized');
|
if (!token) throw new Error('Unauthorized');
|
||||||
return { Authorization: `Bearer ${token}` };
|
return { Authorization: `Bearer ${token}` };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,8 @@ import { cookies } from 'next/headers';
|
|||||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||||
|
|
||||||
async function getAuthHeader() {
|
async function getAuthHeader() {
|
||||||
const token = cookies().get('session_token')?.value;
|
const cookieStore = await cookies();
|
||||||
|
const token = cookieStore.get('session_token')?.value;
|
||||||
if (!token) throw new Error('Unauthorized');
|
if (!token) throw new Error('Unauthorized');
|
||||||
return { Authorization: `Bearer ${token}` };
|
return { Authorization: `Bearer ${token}` };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,8 @@ import { cookies } from 'next/headers';
|
|||||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||||
|
|
||||||
async function getAuthHeader() {
|
async function getAuthHeader() {
|
||||||
const token = cookies().get('session_token')?.value;
|
const cookieStore = await cookies();
|
||||||
|
const token = cookieStore.get('session_token')?.value;
|
||||||
if (!token) throw new Error('Unauthorized');
|
if (!token) throw new Error('Unauthorized');
|
||||||
return { Authorization: `Bearer ${token}` };
|
return { Authorization: `Bearer ${token}` };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,20 +4,22 @@ import { cookies } from 'next/headers';
|
|||||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||||
|
|
||||||
async function getAuthHeader() {
|
async function getAuthHeader() {
|
||||||
const token = cookies().get('session_token')?.value;
|
const cookieStore = await cookies();
|
||||||
|
const token = cookieStore.get('session_token')?.value;
|
||||||
if (!token) throw new Error('Unauthorized');
|
if (!token) throw new Error('Unauthorized');
|
||||||
return { Authorization: `Bearer ${token}` };
|
return { Authorization: `Bearer ${token}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(
|
export async function POST(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
{ params }: { params: { id: string } }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
const resolvedParams = await params;
|
||||||
const headers = await getAuthHeader();
|
const headers = await getAuthHeader();
|
||||||
|
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${API_BASE_URL}/api/v1/admin/merchants/${params.id}/import`,
|
`${API_BASE_URL}/api/v1/admin/merchants/${resolvedParams.id}/import`,
|
||||||
{ method: 'POST', headers }
|
{ method: 'POST', headers }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,20 +4,22 @@ import { cookies } from 'next/headers';
|
|||||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||||
|
|
||||||
async function getAuthHeader() {
|
async function getAuthHeader() {
|
||||||
const token = cookies().get('session_token')?.value;
|
const cookieStore = await cookies();
|
||||||
|
const token = cookieStore.get('session_token')?.value;
|
||||||
if (!token) throw new Error('Unauthorized');
|
if (!token) throw new Error('Unauthorized');
|
||||||
return { Authorization: `Bearer ${token}` };
|
return { Authorization: `Bearer ${token}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(
|
export async function POST(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
{ params }: { params: { id: string } }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
const resolvedParams = await params;
|
||||||
const headers = await getAuthHeader();
|
const headers = await getAuthHeader();
|
||||||
|
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${API_BASE_URL}/api/v1/admin/merchants/${params.id}/offer-sync`,
|
`${API_BASE_URL}/api/v1/admin/merchants/${resolvedParams.id}/offer-sync`,
|
||||||
{ method: 'POST', headers }
|
{ method: 'POST', headers }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,20 +4,22 @@ import { cookies } from 'next/headers';
|
|||||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||||
|
|
||||||
async function getAuthHeader() {
|
async function getAuthHeader() {
|
||||||
const token = cookies().get('session_token')?.value;
|
const cookieStore = await cookies();
|
||||||
|
const token = cookieStore.get('session_token')?.value;
|
||||||
if (!token) throw new Error('Unauthorized');
|
if (!token) throw new Error('Unauthorized');
|
||||||
return { Authorization: `Bearer ${token}` };
|
return { Authorization: `Bearer ${token}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
{ params }: { params: { id: string } }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
const resolvedParams = await params;
|
||||||
const headers = await getAuthHeader();
|
const headers = await getAuthHeader();
|
||||||
|
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${API_BASE_URL}/api/v1/admin/merchants/${params.id}`,
|
`${API_BASE_URL}/api/v1/admin/merchants/${resolvedParams.id}`,
|
||||||
{ headers }
|
{ headers }
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -36,14 +38,15 @@ export async function GET(
|
|||||||
|
|
||||||
export async function PUT(
|
export async function PUT(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
{ params }: { params: { id: string } }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
const resolvedParams = await params;
|
||||||
const headers = await getAuthHeader();
|
const headers = await getAuthHeader();
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
|
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${API_BASE_URL}/api/v1/admin/merchants/${params.id}`,
|
`${API_BASE_URL}/api/v1/admin/merchants/${resolvedParams.id}`,
|
||||||
{
|
{
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { ...headers, 'Content-Type': 'application/json' },
|
headers: { ...headers, 'Content-Type': 'application/json' },
|
||||||
@@ -66,13 +69,14 @@ export async function PUT(
|
|||||||
|
|
||||||
export async function DELETE(
|
export async function DELETE(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
{ params }: { params: { id: string } }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
const resolvedParams = await params;
|
||||||
const headers = await getAuthHeader();
|
const headers = await getAuthHeader();
|
||||||
|
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${API_BASE_URL}/api/v1/admin/merchants/${params.id}`,
|
`${API_BASE_URL}/api/v1/admin/merchants/${resolvedParams.id}`,
|
||||||
{ method: 'DELETE', headers }
|
{ method: 'DELETE', headers }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,8 @@ import { cookies } from 'next/headers';
|
|||||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||||
|
|
||||||
async function getAuthHeader() {
|
async function getAuthHeader() {
|
||||||
const token = cookies().get('session_token')?.value;
|
const cookieStore = await cookies();
|
||||||
|
const token = cookieStore.get('session_token')?.value;
|
||||||
if (!token) throw new Error('Unauthorized');
|
if (!token) throw new Error('Unauthorized');
|
||||||
return { Authorization: `Bearer ${token}` };
|
return { Authorization: `Bearer ${token}` };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,7 +25,8 @@ export async function POST(request: NextRequest) {
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
// Set HTTP-only cookie instead of returning token
|
// Set HTTP-only cookie instead of returning token
|
||||||
cookies().set('session_token', data.token, {
|
const cookieStore = await cookies();
|
||||||
|
cookieStore.set('session_token', data.token, {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: process.env.NODE_ENV === 'production',
|
secure: process.env.NODE_ENV === 'production',
|
||||||
sameSite: 'lax',
|
sameSite: 'lax',
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server';
|
|||||||
import { cookies } from 'next/headers';
|
import { cookies } from 'next/headers';
|
||||||
|
|
||||||
export async function POST() {
|
export async function POST() {
|
||||||
cookies().delete('session_token');
|
const cookieStore = await cookies();
|
||||||
|
cookieStore.delete('session_token');
|
||||||
return NextResponse.json({ success: true });
|
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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,8 @@ import { cookies } from 'next/headers';
|
|||||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
const token = cookies().get('session_token')?.value;
|
const cookieStore = await cookies();
|
||||||
|
const token = cookieStore.get('session_token')?.value;
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
@@ -16,7 +17,7 @@ export async function GET(request: NextRequest) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
cookies().delete('session_token');
|
cookieStore.delete('session_token');
|
||||||
return NextResponse.json({ error: 'Session expired' }, { status: 401 });
|
return NextResponse.json({ error: 'Session expired' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
// Set HTTP-only cookie on successful registration
|
// Set HTTP-only cookie on successful registration
|
||||||
if (data.token) {
|
if (data.token) {
|
||||||
cookies().set('session_token', data.token, {
|
const cookieStore = await cookies();
|
||||||
|
cookieStore.set('session_token', data.token, {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: process.env.NODE_ENV === 'production',
|
secure: process.env.NODE_ENV === 'production',
|
||||||
sameSite: 'lax',
|
sameSite: 'lax',
|
||||||
|
|||||||
@@ -4,20 +4,22 @@ import { cookies } from 'next/headers';
|
|||||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||||
|
|
||||||
async function getAuthHeader() {
|
async function getAuthHeader() {
|
||||||
const token = cookies().get('session_token')?.value;
|
const cookieStore = await cookies();
|
||||||
|
const token = cookieStore.get('session_token')?.value;
|
||||||
if (!token) throw new Error('Unauthorized');
|
if (!token) throw new Error('Unauthorized');
|
||||||
return { Authorization: `Bearer ${token}` };
|
return { Authorization: `Bearer ${token}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
{ params }: { params: { id: string } }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
const { id } = await params;
|
||||||
const headers = await getAuthHeader();
|
const headers = await getAuthHeader();
|
||||||
|
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${API_BASE_URL}/api/v1/gunbuilder/builds/${params.id}`,
|
`${API_BASE_URL}/api/v1/gunbuilder/builds/${id}`,
|
||||||
{ headers }
|
{ headers }
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -36,14 +38,15 @@ export async function GET(
|
|||||||
|
|
||||||
export async function PUT(
|
export async function PUT(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
{ params }: { params: { id: string } }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
const { id } = await params;
|
||||||
const headers = await getAuthHeader();
|
const headers = await getAuthHeader();
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
|
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${API_BASE_URL}/api/v1/gunbuilder/builds/${params.id}`,
|
`${API_BASE_URL}/api/v1/gunbuilder/builds/${id}`,
|
||||||
{
|
{
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { ...headers, 'Content-Type': 'application/json' },
|
headers: { ...headers, 'Content-Type': 'application/json' },
|
||||||
@@ -66,13 +69,14 @@ export async function PUT(
|
|||||||
|
|
||||||
export async function DELETE(
|
export async function DELETE(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
{ params }: { params: { id: string } }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
const { id } = await params;
|
||||||
const headers = await getAuthHeader();
|
const headers = await getAuthHeader();
|
||||||
|
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${API_BASE_URL}/api/v1/gunbuilder/builds/${params.id}`,
|
`${API_BASE_URL}/api/v1/gunbuilder/builds/${id}`,
|
||||||
{ method: 'DELETE', headers }
|
{ method: 'DELETE', headers }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import { cookies } from 'next/headers';
|
|||||||
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
const API_BASE_URL = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL;
|
||||||
|
|
||||||
async function getAuthHeader() {
|
async function getAuthHeader() {
|
||||||
const token = cookies().get('session_token')?.value;
|
const cookieStore = await cookies();
|
||||||
|
const token = cookieStore.get('session_token')?.value;
|
||||||
if (!token) throw new Error('Unauthorized');
|
if (!token) throw new Error('Unauthorized');
|
||||||
return { Authorization: `Bearer ${token}` };
|
return { Authorization: `Bearer ${token}` };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// app/builds/[buildId]/page.tsx
|
// app/builds/[buildId]/page.tsx
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
|
import { ProductImage } from "@/components/parts/ProductImage";
|
||||||
|
|
||||||
const API_BASE_URL =
|
const API_BASE_URL =
|
||||||
process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
|
process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
|
||||||
@@ -29,11 +30,13 @@ function timeAgo(iso?: string | null) {
|
|||||||
export default async function BuildBreakdownPage({
|
export default async function BuildBreakdownPage({
|
||||||
params,
|
params,
|
||||||
}: {
|
}: {
|
||||||
params: { buildId: string };
|
params: Promise<{ buildId: string }>;
|
||||||
}) {
|
}) {
|
||||||
|
const { buildId } = await params;
|
||||||
|
|
||||||
// Public detail endpoint - use Next.js API route
|
// Public detail endpoint - use Next.js API route
|
||||||
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000';
|
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000';
|
||||||
const res = await fetch(`${baseUrl}/api/builds/public/${params.buildId}`, {
|
const res = await fetch(`${baseUrl}/api/builds/public/${buildId}`, {
|
||||||
cache: "no-store",
|
cache: "no-store",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -167,18 +170,11 @@ export default async function BuildBreakdownPage({
|
|||||||
>
|
>
|
||||||
{/* Thumb */}
|
{/* Thumb */}
|
||||||
<div className="h-12 w-12 flex-none overflow-hidden rounded-md border border-zinc-800 bg-zinc-950">
|
<div className="h-12 w-12 flex-none overflow-hidden rounded-md border border-zinc-800 bg-zinc-950">
|
||||||
{it.productImageUrl ? (
|
<ProductImage
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
|
||||||
<img
|
|
||||||
src={it.productImageUrl}
|
src={it.productImageUrl}
|
||||||
alt={it.productName ?? "Part image"}
|
alt={it.productName ?? "Part image"}
|
||||||
className="h-full w-full object-cover"
|
className="h-full w-full object-cover"
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
<div className="flex h-full w-full items-center justify-center text-[10px] text-zinc-600">
|
|
||||||
IMG
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Info */}
|
{/* Info */}
|
||||||
@@ -359,7 +355,7 @@ export default async function BuildBreakdownPage({
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-zinc-500">Build ID</span>
|
<span className="text-zinc-500">Build ID</span>
|
||||||
<span className="text-zinc-300">
|
<span className="text-zinc-300">
|
||||||
{(build.uuid ?? params.buildId).slice(0, 8)}…
|
{(build.uuid ?? buildId).slice(0, 8)}…
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|||||||
@@ -2,20 +2,17 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
|
|
||||||
import { AuthProvider } from "@/context/AuthContext";
|
|
||||||
import { BuilderNav } from "@/components/BuilderNav";
|
import { BuilderNav } from "@/components/BuilderNav";
|
||||||
import { TopNav } from "@/components/TopNav";
|
import { TopNav } from "@/components/TopNav";
|
||||||
|
|
||||||
export default function BuilderLayout({ children }: { children: ReactNode }) {
|
export default function BuilderLayout({ children }: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-neutral-950 text-zinc-50">
|
<div className="min-h-screen bg-neutral-950 text-zinc-50">
|
||||||
<AuthProvider>
|
|
||||||
<TopNav />
|
<TopNav />
|
||||||
<Suspense fallback={<div className="h-[52px]" />}>
|
<Suspense fallback={<div className="h-[52px]" />}>
|
||||||
<BuilderNav />
|
<BuilderNav />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<main className="min-h-screen">{children}</main>
|
<main className="min-h-screen">{children}</main>
|
||||||
</AuthProvider>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+4
-17
@@ -20,27 +20,14 @@ export const metadata = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const themeScript = `
|
|
||||||
(function () {
|
|
||||||
try {
|
|
||||||
const storedTheme = localStorage.getItem("theme");
|
|
||||||
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
||||||
const theme = storedTheme || (prefersDark ? "dark" : "light");
|
|
||||||
document.documentElement.classList.toggle("dark", theme === "dark");
|
|
||||||
} catch (_) {}
|
|
||||||
})();
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default function RootLayout({ children }: { children: ReactNode }) {
|
export default function RootLayout({ children }: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning className="dark">
|
<html lang="en" className="dark">
|
||||||
<head>
|
<head />
|
||||||
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body className="bg-dark text-zinc-950 dark:bg-black dark:text-zinc-50">
|
<body className="bg-black text-zinc-50">
|
||||||
{" "}
|
{" "}
|
||||||
<Script src="https://umami.ash.gofwd.group/script.js" data-website-id="ad310b4a-aa5e-471a-938b-47a6c0f4108d"
|
<Script src="https://umami.ash.gofwd.group/script.js" data-website-id="15c6b9ad-4119-4981-829c-26db69c07a15"
|
||||||
strategy="lazyOnload"
|
strategy="lazyOnload"
|
||||||
/>
|
/>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
|
|||||||
+2
-1
@@ -13,7 +13,7 @@ const API_BASE_URL =
|
|||||||
function LoginPageContent() {
|
function LoginPageContent() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const { login, loading } = useAuth();
|
const { login, refreshUser, loading } = useAuth();
|
||||||
|
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
@@ -36,6 +36,7 @@ function LoginPageContent() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await login({ email, password });
|
await login({ email, password });
|
||||||
|
await refreshUser();
|
||||||
router.push(next);
|
router.push(next);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err?.message ?? "Failed to log in");
|
setError(err?.message ?? "Failed to log in");
|
||||||
|
|||||||
+1
-4
@@ -13,7 +13,7 @@ export default function NotFound() {
|
|||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p className="mt-3 text-sm text-zinc-400">
|
<p className="mt-3 text-sm text-zinc-400">
|
||||||
This area of the site isn’t publicly accessible yet.
|
The page you're looking for doesn't exist or has been moved.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="mt-6 flex justify-center gap-3">
|
<div className="mt-6 flex justify-center gap-3">
|
||||||
@@ -32,9 +32,6 @@ export default function NotFound() {
|
|||||||
</a> */}
|
</a> */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="mt-6 text-[11px] text-zinc-600">
|
|
||||||
Early access features are launching soon.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
+4
-4
@@ -172,12 +172,12 @@ export default function HomePage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav className="flex items-center gap-2 sm:gap-3">
|
<nav className="flex items-center gap-2 sm:gap-3">
|
||||||
<a
|
<Link
|
||||||
href="#request-access"
|
href="/builder"
|
||||||
className="rounded-md border border-zinc-800 bg-zinc-950/60 px-3 py-2 text-xs font-medium text-zinc-200 hover:bg-zinc-900 hover:text-white"
|
className="rounded-md border border-zinc-800 bg-zinc-950/60 px-3 py-2 text-xs font-medium text-zinc-200 hover:bg-zinc-900 hover:text-white"
|
||||||
>
|
>
|
||||||
Request Access
|
Go To Builder
|
||||||
</a>
|
</Link>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href="/login"
|
href="/login"
|
||||||
|
|||||||
@@ -119,24 +119,15 @@ export function BuilderNav({ activeCategoryId }: BuilderNavProps) {
|
|||||||
{/* LEFT SIDE: primary nav */}
|
{/* LEFT SIDE: primary nav */}
|
||||||
{/* ------------------------------------------------------------------ */}
|
{/* ------------------------------------------------------------------ */}
|
||||||
|
|
||||||
<Link
|
{/* Section label — makes the catalog bar purpose unmistakable */}
|
||||||
href="/builder"
|
<span className="text-[9px] font-bold tracking-[0.2em] uppercase text-zinc-600 select-none pr-3 border-r border-zinc-800">
|
||||||
className="font-semibold text-neutral-100 hover:text-white tracking-[0.25em] uppercase text-[11px]"
|
Parts
|
||||||
>
|
</span>
|
||||||
Builder
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
{renderDropdown("Lower Parts", lower)}
|
{renderDropdown("Lower Parts", lower)}
|
||||||
{renderDropdown("Upper Parts", upper)}
|
{renderDropdown("Upper Parts", upper)}
|
||||||
{renderDropdown("Accessories", accessories)}
|
{renderDropdown("Accessories", accessories)}
|
||||||
|
|
||||||
<Link
|
|
||||||
href="/builds"
|
|
||||||
className="font-medium text-neutral-300 hover:text-white tracking-[0.25em] uppercase text-[11px]"
|
|
||||||
>
|
|
||||||
Builds
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
{/* ------------------------------------------------------------------ */}
|
{/* ------------------------------------------------------------------ */}
|
||||||
{/* RIGHT SIDE: actions + state */}
|
{/* RIGHT SIDE: actions + state */}
|
||||||
{/* IMPORTANT: this is the ONLY `ml-auto` in the nav to avoid conflicts */}
|
{/* IMPORTANT: this is the ONLY `ml-auto` in the nav to avoid conflicts */}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
import { BuilderNav } from "./BuilderNav";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps BuilderNav with pathname-aware visibility.
|
||||||
|
* The catalog bar is irrelevant on guide pages — hide it there.
|
||||||
|
*/
|
||||||
|
export function BuilderNavConditional() {
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
// Hide catalog bar on guide pages — part categories don't apply when reading an article
|
||||||
|
if (pathname.startsWith("/guides")) return null;
|
||||||
|
|
||||||
|
return <BuilderNav />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BuilderNavConditional;
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { PortableText, type PortableTextComponents } from 'next-sanity'
|
||||||
|
import Image from 'next/image'
|
||||||
|
import { urlFor } from '@/lib/sanity'
|
||||||
|
|
||||||
|
const components: PortableTextComponents = {
|
||||||
|
block: {
|
||||||
|
normal: ({ children }) => (
|
||||||
|
<p className="mb-5 leading-relaxed text-zinc-300">{children}</p>
|
||||||
|
),
|
||||||
|
h1: ({ children }) => (
|
||||||
|
<h1 className="mb-4 mt-10 text-3xl font-semibold text-zinc-100">{children}</h1>
|
||||||
|
),
|
||||||
|
h2: ({ children }) => (
|
||||||
|
<h2 className="mb-3 mt-8 text-2xl font-semibold text-zinc-100">{children}</h2>
|
||||||
|
),
|
||||||
|
h3: ({ children }) => (
|
||||||
|
<h3 className="mb-2 mt-6 text-xl font-semibold text-zinc-100">{children}</h3>
|
||||||
|
),
|
||||||
|
blockquote: ({ children }) => (
|
||||||
|
<blockquote className="my-6 border-l-2 border-amber-400/60 pl-4 text-zinc-400 italic">
|
||||||
|
{children}
|
||||||
|
</blockquote>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
marks: {
|
||||||
|
strong: ({ children }) => (
|
||||||
|
<strong className="font-semibold text-zinc-100">{children}</strong>
|
||||||
|
),
|
||||||
|
em: ({ children }) => <em className="italic text-zinc-300">{children}</em>,
|
||||||
|
code: ({ children }) => (
|
||||||
|
<code className="rounded bg-zinc-800 px-1.5 py-0.5 font-mono text-sm text-amber-300">
|
||||||
|
{children}
|
||||||
|
</code>
|
||||||
|
),
|
||||||
|
link: ({ value, children }) => (
|
||||||
|
<a
|
||||||
|
href={value?.href}
|
||||||
|
target={value?.href?.startsWith('http') ? '_blank' : undefined}
|
||||||
|
rel={value?.href?.startsWith('http') ? 'noopener noreferrer' : undefined}
|
||||||
|
className="text-amber-400 underline underline-offset-2 hover:text-amber-300"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
list: {
|
||||||
|
bullet: ({ children }) => (
|
||||||
|
<ul className="mb-5 ml-6 list-disc space-y-1.5 text-zinc-300">{children}</ul>
|
||||||
|
),
|
||||||
|
number: ({ children }) => (
|
||||||
|
<ol className="mb-5 ml-6 list-decimal space-y-1.5 text-zinc-300">{children}</ol>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
listItem: {
|
||||||
|
bullet: ({ children }) => <li className="leading-relaxed">{children}</li>,
|
||||||
|
number: ({ children }) => <li className="leading-relaxed">{children}</li>,
|
||||||
|
},
|
||||||
|
types: {
|
||||||
|
image: ({ value }) => {
|
||||||
|
if (!value?.asset) return null
|
||||||
|
return (
|
||||||
|
<figure className="my-8">
|
||||||
|
<div className="relative overflow-hidden rounded-lg">
|
||||||
|
<Image
|
||||||
|
src={urlFor(value).width(800).url()}
|
||||||
|
alt={value.alt ?? ''}
|
||||||
|
width={800}
|
||||||
|
height={450}
|
||||||
|
className="w-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{value.caption && (
|
||||||
|
<figcaption className="mt-2 text-center text-xs text-zinc-500">
|
||||||
|
{value.caption}
|
||||||
|
</figcaption>
|
||||||
|
)}
|
||||||
|
</figure>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PortableTextRenderer({ value }: { value: unknown[] }) {
|
||||||
|
return (
|
||||||
|
<div className="prose-battl">
|
||||||
|
<PortableText value={value as any} components={components} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+15
-9
@@ -6,7 +6,6 @@ import Image from "next/image";
|
|||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
|
|
||||||
import { useAuth } from "@/context/AuthContext";
|
import { useAuth } from "@/context/AuthContext";
|
||||||
import ThemeToggle from "@/components/ThemeToggle";
|
|
||||||
|
|
||||||
function NavLink({
|
function NavLink({
|
||||||
href,
|
href,
|
||||||
@@ -18,15 +17,17 @@ function NavLink({
|
|||||||
title?: string;
|
title?: string;
|
||||||
}) {
|
}) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const active = pathname === href;
|
const active = pathname === href || pathname.startsWith(href + '/');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
href={href}
|
href={href}
|
||||||
title={title}
|
title={title}
|
||||||
className={[
|
className={[
|
||||||
"text-xs font-medium transition-colors",
|
"relative text-xs font-medium transition-colors pb-0.5",
|
||||||
active ? "text-zinc-100" : "text-zinc-400 hover:text-zinc-100",
|
active
|
||||||
|
? "text-zinc-100 after:absolute after:bottom-0 after:left-0 after:right-0 after:h-px after:bg-amber-400"
|
||||||
|
: "text-zinc-400 hover:text-zinc-100",
|
||||||
].join(" ")}
|
].join(" ")}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -55,10 +56,15 @@ export function TopNav() {
|
|||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Right side actions */}
|
{/* Centre: primary nav links */}
|
||||||
<div className="flex items-center gap-4">
|
<nav className="hidden sm:flex items-center gap-5">
|
||||||
<ThemeToggle />
|
<NavLink href="/builder">Builder</NavLink>
|
||||||
|
<NavLink href="/guides">Guides</NavLink>
|
||||||
|
<NavLink href="/builds">Community</NavLink>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Right side actions */}
|
||||||
|
<div className="flex items-center justify-end gap-3 sm:gap-4">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<span className="text-xs text-zinc-500">Checking session…</span>
|
<span className="text-xs text-zinc-500">Checking session…</span>
|
||||||
) : isAuthenticated ? (
|
) : isAuthenticated ? (
|
||||||
@@ -83,7 +89,7 @@ export function TopNav() {
|
|||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<div className="flex flex-col items-end gap-2 sm:flex-row sm:items-center sm:gap-4">
|
||||||
<NavLink href="/login">Log In</NavLink>
|
<NavLink href="/login">Log In</NavLink>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
@@ -92,7 +98,7 @@ export function TopNav() {
|
|||||||
>
|
>
|
||||||
Join Beta
|
Join Beta
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Plus } from "lucide-react";
|
import { Plus } from "lucide-react";
|
||||||
import type { UiPart } from "@/types/uiPart";
|
import type { UiPart } from "@/types/uiPart";
|
||||||
|
import { ProductImage } from './ProductImage';
|
||||||
|
|
||||||
export default function PartsGrid(props: {
|
export default function PartsGrid(props: {
|
||||||
viewMode: "card" | "list";
|
viewMode: "card" | "list";
|
||||||
@@ -130,17 +131,12 @@ export default function PartsGrid(props: {
|
|||||||
>
|
>
|
||||||
{/* Image */}
|
{/* Image */}
|
||||||
<div className="hidden md:block justify-self-start">
|
<div className="hidden md:block justify-self-start">
|
||||||
{part.imageUrl ? (
|
<ProductImage
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
|
||||||
<img
|
|
||||||
src={part.imageUrl}
|
src={part.imageUrl}
|
||||||
alt=""
|
alt=""
|
||||||
className="h-10 w-10 rounded object-cover border border-zinc-800 bg-zinc-950"
|
className="h-10 w-10 rounded object-cover border border-zinc-800 bg-zinc-950"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
<div className="h-10 w-10 rounded border border-zinc-800 bg-zinc-950" />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Part */}
|
{/* Part */}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { ProductImage } from './ProductImage';
|
||||||
import PlatformSwitcher from "./PlatformSwitcher";
|
import PlatformSwitcher from "./PlatformSwitcher";
|
||||||
|
|
||||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
|
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
|
||||||
@@ -149,14 +150,11 @@ export default function ProductDetailPageClient(props: {
|
|||||||
<div className="flex flex-col gap-6 md:flex-row">
|
<div className="flex flex-col gap-6 md:flex-row">
|
||||||
<div className="w-full md:w-72">
|
<div className="w-full md:w-72">
|
||||||
<div className="aspect-square overflow-hidden rounded-lg border border-zinc-800 bg-black/30">
|
<div className="aspect-square overflow-hidden rounded-lg border border-zinc-800 bg-black/30">
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
<ProductImage
|
||||||
{p.imageUrl ? (
|
src={p.imageUrl}
|
||||||
<img src={p.imageUrl} alt={p.name} className="h-full w-full object-cover" />
|
alt={p.name}
|
||||||
) : (
|
className="h-full w-full object-cover"
|
||||||
<div className="h-full w-full grid place-items-center text-sm text-zinc-600">
|
/>
|
||||||
No image
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
const PLACEHOLDER = '/images/product-placeholder.svg';
|
||||||
|
|
||||||
|
interface ProductImageProps {
|
||||||
|
src?: string | null;
|
||||||
|
alt: string;
|
||||||
|
className?: string;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
loading?: 'lazy' | 'eager';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProductImage({ src, alt, className, width, height, loading }: ProductImageProps) {
|
||||||
|
const hasErrored = useRef(false);
|
||||||
|
|
||||||
|
// Reset the loop guard whenever src changes (e.g. navigating between products)
|
||||||
|
useEffect(() => {
|
||||||
|
hasErrored.current = false;
|
||||||
|
}, [src]);
|
||||||
|
|
||||||
|
function handleError(e: React.SyntheticEvent<HTMLImageElement>) {
|
||||||
|
if (hasErrored.current) return;
|
||||||
|
hasErrored.current = true;
|
||||||
|
e.currentTarget.src = PLACEHOLDER;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
|
<img
|
||||||
|
src={src ?? PLACEHOLDER}
|
||||||
|
alt={alt}
|
||||||
|
className={className}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
loading={loading}
|
||||||
|
onError={handleError}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,9 +3,9 @@ import { useMemo } from "react";
|
|||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
|
||||||
import "react-quill/dist/quill.snow.css";
|
// import "react-quill/dist/quill.snow.css";
|
||||||
|
|
||||||
const ReactQuill = dynamic(() => import("react-quill"), { ssr: false });
|
const ReactQuill = dynamic(() => import("react-quill-new"), { ssr: false });
|
||||||
|
|
||||||
type RichTextEditorProps = {
|
type RichTextEditorProps = {
|
||||||
value: string;
|
value: string;
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import type { BuilderSlotKey } from "@/types/builderSlots";
|
||||||
|
|
||||||
|
export function trackBuildStarted() {
|
||||||
|
window.umami?.track("build_started");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function trackPartSelected(slot: BuilderSlotKey, partName: string, partId: string) {
|
||||||
|
window.umami?.track("part_selected", { slot, part_name: partName, part_id: partId });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function trackAffiliateLinkClicked(partName: string, retailer: string) {
|
||||||
|
window.umami?.track("affiliate_link_clicked", { part_name: partName, retailer });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function trackBuildShared() {
|
||||||
|
window.umami?.track("build_shared");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function trackBuildSaved() {
|
||||||
|
window.umami?.track("build_saved");
|
||||||
|
}
|
||||||
+13
-16
@@ -14,9 +14,6 @@ export type CreatePlatformDto = {
|
|||||||
|
|
||||||
export type UpdatePlatformDto = Partial<CreatePlatformDto>;
|
export type UpdatePlatformDto = Partial<CreatePlatformDto>;
|
||||||
|
|
||||||
const API_BASE =
|
|
||||||
process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
|
|
||||||
|
|
||||||
function normalizePlatform(raw: any): Platform {
|
function normalizePlatform(raw: any): Platform {
|
||||||
const id = Number(raw?.id);
|
const id = Number(raw?.id);
|
||||||
const key = String(raw?.key ?? "").trim();
|
const key = String(raw?.key ?? "").trim();
|
||||||
@@ -33,9 +30,9 @@ function normalizePlatform(raw: any): Platform {
|
|||||||
return { id, key, label, isActive };
|
return { id, key, label, isActive };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchPlatforms(tokenHeaders: HeadersInit): Promise<Platform[]> {
|
export async function fetchPlatforms(_tokenHeaders: HeadersInit): Promise<Platform[]> {
|
||||||
const res = await fetch(`${API_BASE}/api/platforms`, {
|
const res = await fetch('/api/admin/platforms', {
|
||||||
headers: { ...tokenHeaders },
|
credentials: 'include',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
@@ -48,15 +45,15 @@ export async function fetchPlatforms(tokenHeaders: HeadersInit): Promise<Platfor
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function createPlatform(
|
export async function createPlatform(
|
||||||
tokenHeaders: HeadersInit,
|
_tokenHeaders: HeadersInit,
|
||||||
dto: CreatePlatformDto
|
dto: CreatePlatformDto
|
||||||
): Promise<Platform> {
|
): Promise<Platform> {
|
||||||
const res = await fetch(`${API_BASE}/api/platforms`, {
|
const res = await fetch('/api/admin/platforms', {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...tokenHeaders,
|
|
||||||
},
|
},
|
||||||
|
credentials: 'include',
|
||||||
body: JSON.stringify(dto),
|
body: JSON.stringify(dto),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -69,17 +66,17 @@ export async function createPlatform(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function updatePlatform(
|
export async function updatePlatform(
|
||||||
tokenHeaders: HeadersInit,
|
_tokenHeaders: HeadersInit,
|
||||||
id: number,
|
id: number,
|
||||||
dto: UpdatePlatformDto
|
dto: UpdatePlatformDto
|
||||||
): Promise<Platform> {
|
): Promise<Platform> {
|
||||||
const res = await fetch(`${API_BASE}/api/platforms/${id}`, {
|
const res = await fetch(`/api/admin/platforms/${id}`, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...tokenHeaders,
|
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ isActive: dto.isActive }),
|
credentials: 'include',
|
||||||
|
body: JSON.stringify(dto),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
@@ -90,10 +87,10 @@ export async function updatePlatform(
|
|||||||
return normalizePlatform(await res.json());
|
return normalizePlatform(await res.json());
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deletePlatform(tokenHeaders: HeadersInit, id: number): Promise<void> {
|
export async function deletePlatform(_tokenHeaders: HeadersInit, id: number): Promise<void> {
|
||||||
const res = await fetch(`${API_BASE}/api/platforms/${id}`, {
|
const res = await fetch(`/api/admin/platforms/${id}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: { ...tokenHeaders },
|
credentials: 'include',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user