fuck if i know
This commit is contained in:
+26
@@ -0,0 +1,26 @@
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
|
||||
export function useApi() {
|
||||
const { token } = useAuth();
|
||||
|
||||
async function get(path: string) {
|
||||
return fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}${path}`, {
|
||||
headers: {
|
||||
Authorization: token ? `Bearer ${token}` : "",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function post(path: string, body: any) {
|
||||
return fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}${path}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: token ? `Bearer ${token}` : "",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
|
||||
return { get, post };
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
// lib/api/adminCategoryMappings.ts
|
||||
|
||||
const API_BASE =
|
||||
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
|
||||
|
||||
export interface AdminCategory {
|
||||
id: number;
|
||||
slug: string;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
groupName?: string | null;
|
||||
sortOrder?: number | null;
|
||||
}
|
||||
|
||||
export interface AdminPartRoleMapping {
|
||||
id: number;
|
||||
platform: string;
|
||||
partRole: string;
|
||||
categorySlug: string;
|
||||
groupName?: string | null;
|
||||
notes?: string | null;
|
||||
}
|
||||
|
||||
export interface CreatePartRoleMappingRequest {
|
||||
platform: string;
|
||||
partRole: string;
|
||||
categorySlug: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface UpdatePartRoleMappingRequest {
|
||||
platform?: string;
|
||||
partRole?: string;
|
||||
categorySlug?: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
async function apiFetch<T>(
|
||||
path: string,
|
||||
options: {
|
||||
token?: string;
|
||||
method?: string;
|
||||
body?: any;
|
||||
} = {}
|
||||
): Promise<T> {
|
||||
const { token, method = "GET", body } = options;
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
if (token) {
|
||||
headers["Authorization"] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const res = await fetch(`${API_BASE}${path}`, {
|
||||
method,
|
||||
headers,
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => "");
|
||||
throw new Error(
|
||||
`API error ${res.status}: ${res.statusText}${text ? ` – ${text}` : ""}`
|
||||
);
|
||||
}
|
||||
|
||||
return (await res.json()) as T;
|
||||
}
|
||||
|
||||
export async function fetchAdminCategories(token: string) {
|
||||
return apiFetch<AdminCategory[]>("/api/admin/categories", { token });
|
||||
}
|
||||
|
||||
// ---------------- Part-role mappings ----------------
|
||||
|
||||
const PART_ROLE_BASE = "/api/admin/part-role-mappings";
|
||||
|
||||
/**
|
||||
* Fetch part-role/category mappings scoped by platform.
|
||||
* Backend should expose: GET /api/admin/part-role-mappings?platform=AR-15
|
||||
*/
|
||||
export async function fetchPartRoleMappings(
|
||||
token: string,
|
||||
platform = "AR-15"
|
||||
) {
|
||||
const params = new URLSearchParams({ platform });
|
||||
return apiFetch<AdminPartRoleMapping[]>(
|
||||
`${PART_ROLE_BASE}?${params.toString()}`,
|
||||
{ token }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new part-role → category mapping
|
||||
* POST /api/admin/part-role-mappings
|
||||
*/
|
||||
export async function createPartRoleMapping(
|
||||
token: string,
|
||||
payload: CreatePartRoleMappingRequest
|
||||
) {
|
||||
return apiFetch<AdminPartRoleMapping>(PART_ROLE_BASE, {
|
||||
token,
|
||||
method: "POST",
|
||||
body: payload,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing part-role mapping
|
||||
* PUT /api/admin/part-role-mappings/{id}
|
||||
*/
|
||||
export async function updatePartRoleMapping(
|
||||
token: string,
|
||||
id: number,
|
||||
payload: UpdatePartRoleMappingRequest
|
||||
) {
|
||||
return apiFetch<AdminPartRoleMapping>(`${PART_ROLE_BASE}/${id}`, {
|
||||
token,
|
||||
method: "PUT",
|
||||
body: payload,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a part-role mapping
|
||||
* DELETE /api/admin/part-role-mappings/{id}
|
||||
*/
|
||||
export async function deletePartRoleMapping(token: string, id: number) {
|
||||
await apiFetch<void>(`${PART_ROLE_BASE}/${id}`, {
|
||||
token,
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
const API_BASE =
|
||||
process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8080";
|
||||
|
||||
export interface AdminMerchantCategoryMapping {
|
||||
id: number;
|
||||
merchantId: number;
|
||||
merchantName: string;
|
||||
rawCategoryPath: string;
|
||||
partCategorySlug: string | null;
|
||||
}
|
||||
|
||||
export async function fetchMerchantCategoryMappings(
|
||||
token: string,
|
||||
filters?: { merchantId?: number }
|
||||
): Promise<AdminMerchantCategoryMapping[]> {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (filters?.merchantId) {
|
||||
params.append("merchantId", String(filters.merchantId));
|
||||
}
|
||||
|
||||
const res = await fetch(
|
||||
`${API_BASE}/api/admin/category-mappings?${params.toString()}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
cache: "no-store",
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(`API error ${res.status}: ${text}`);
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function updateMerchantCategoryMapping(
|
||||
token: string,
|
||||
id: number,
|
||||
payload: { partCategorySlug: string | null }
|
||||
): Promise<AdminMerchantCategoryMapping> {
|
||||
const res = await fetch(`${API_BASE}/api/admin/category-mappings/${id}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(`API error ${res.status}: ${text}`);
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
// lib/auth-client.ts
|
||||
|
||||
const API_BASE_URL =
|
||||
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
|
||||
|
||||
export type AuthUser = {
|
||||
id: number;
|
||||
email: string;
|
||||
displayName?: string | null;
|
||||
role: string;
|
||||
};
|
||||
|
||||
export type AuthPayload = {
|
||||
accessToken: string;
|
||||
refreshToken?: string;
|
||||
userId: number;
|
||||
email: string;
|
||||
displayName?: string | null;
|
||||
role: string;
|
||||
};
|
||||
|
||||
export type RegisterInput = {
|
||||
email: string;
|
||||
password: string;
|
||||
displayName?: string;
|
||||
};
|
||||
|
||||
export type LoginInput = {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
const AUTH_STORAGE_KEY = "armory-auth";
|
||||
|
||||
export function storeAuth(payload: AuthPayload) {
|
||||
if (typeof window === "undefined") return;
|
||||
const user: AuthUser = {
|
||||
id: payload.userId,
|
||||
email: payload.email,
|
||||
displayName: payload.displayName,
|
||||
role: payload.role,
|
||||
};
|
||||
|
||||
const toStore = {
|
||||
user,
|
||||
accessToken: payload.accessToken,
|
||||
refreshToken: payload.refreshToken ?? null,
|
||||
};
|
||||
|
||||
window.localStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify(toStore));
|
||||
}
|
||||
|
||||
export function loadStoredAuth():
|
||||
| { user: AuthUser; accessToken: string; refreshToken: string | null }
|
||||
| null {
|
||||
if (typeof window === "undefined") return null;
|
||||
try {
|
||||
const raw = window.localStorage.getItem(AUTH_STORAGE_KEY);
|
||||
if (!raw) return null;
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!parsed.user || !parsed.accessToken) return null;
|
||||
return {
|
||||
user: parsed.user as AuthUser,
|
||||
accessToken: parsed.accessToken as string,
|
||||
refreshToken: parsed.refreshToken ?? null,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function clearStoredAuth() {
|
||||
if (typeof window === "undefined") return;
|
||||
window.localStorage.removeItem(AUTH_STORAGE_KEY);
|
||||
}
|
||||
|
||||
async function handleAuthResponse(res: Response): Promise<AuthPayload> {
|
||||
if (!res.ok) {
|
||||
let message = `Request failed (${res.status})`;
|
||||
try {
|
||||
const body = await res.json();
|
||||
if (body?.message) message = body.message;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
return (await res.json()) as AuthPayload;
|
||||
}
|
||||
|
||||
export async function apiRegister(input: RegisterInput): Promise<AuthPayload> {
|
||||
const res = await fetch(`${API_BASE_URL}/api/auth/register`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
|
||||
return handleAuthResponse(res);
|
||||
}
|
||||
|
||||
export async function apiLogin(input: LoginInput): Promise<AuthPayload> {
|
||||
const res = await fetch(`${API_BASE_URL}/api/auth/login`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
|
||||
return handleAuthResponse(res);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
"use client";
|
||||
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import { useCallback, useMemo } from "react";
|
||||
|
||||
const DEFAULT_API_BASE =
|
||||
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
|
||||
|
||||
export function useApi(baseUrl: string = DEFAULT_API_BASE) {
|
||||
const { token } = useAuth();
|
||||
|
||||
const apiFetch = useCallback(
|
||||
async <T = any>(path: string, options: RequestInit = {}): Promise<T> => {
|
||||
// Allow either full URLs or just `/api/...`
|
||||
const url = path.startsWith("http")
|
||||
? path
|
||||
: `${baseUrl.replace(/\/$/, "")}${path}`;
|
||||
|
||||
const headers: HeadersInit = {
|
||||
"Content-Type": "application/json",
|
||||
...(options.headers || {}),
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
};
|
||||
|
||||
const res = await fetch(url, {
|
||||
...options,
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
let bodyText = "";
|
||||
try {
|
||||
bodyText = await res.text();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
throw new Error(
|
||||
`API error ${res.status}: ${
|
||||
bodyText || res.statusText || "Unknown error"
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
// handle 204 / empty body safely
|
||||
try {
|
||||
return (await res.json()) as T;
|
||||
} catch {
|
||||
return {} as T;
|
||||
}
|
||||
},
|
||||
[baseUrl, token] // 👈 only changes when baseUrl or token change
|
||||
);
|
||||
|
||||
const get = useCallback(
|
||||
<T = any>(path: string) => apiFetch<T>(path, { method: "GET" }),
|
||||
[apiFetch]
|
||||
);
|
||||
|
||||
const post = useCallback(
|
||||
<T = any>(path: string, body?: unknown) =>
|
||||
apiFetch<T>(path, {
|
||||
method: "POST",
|
||||
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||||
}),
|
||||
[apiFetch]
|
||||
);
|
||||
|
||||
const put = useCallback(
|
||||
<T = any>(path: string, body?: unknown) =>
|
||||
apiFetch<T>(path, {
|
||||
method: "PUT",
|
||||
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||||
}),
|
||||
[apiFetch]
|
||||
);
|
||||
|
||||
const del = useCallback(
|
||||
<T = any>(path: string) => apiFetch<T>(path, { method: "DELETE" }),
|
||||
[apiFetch]
|
||||
);
|
||||
|
||||
// Optional: memoize the returned object so destructuring doesn't change refs
|
||||
return useMemo(
|
||||
() => ({ apiFetch, get, post, put, del }),
|
||||
[apiFetch, get, post, put, del]
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user