- "frontend/src/pages/PostEditor.tsx" - "frontend/src/pages/PostEditor.module.css" - "frontend/src/api/posts.ts" - "frontend/src/api/client.ts" - "frontend/src/App.tsx" - "frontend/src/pages/CreatorDashboard.tsx" GSD-Task: S01/T03
102 lines
2.6 KiB
TypeScript
102 lines
2.6 KiB
TypeScript
/**
|
|
* Shared API client infrastructure: request helper, error class, auth token management.
|
|
*/
|
|
|
|
export const BASE = "/api/v1";
|
|
export const AUTH_TOKEN_KEY = "chrysopedia_token";
|
|
|
|
export class ApiError extends Error {
|
|
constructor(
|
|
public status: number,
|
|
public detail: string,
|
|
) {
|
|
super(`API ${status}: ${detail}`);
|
|
this.name = "ApiError";
|
|
}
|
|
}
|
|
|
|
function getStoredToken(): string | null {
|
|
try {
|
|
return localStorage.getItem(AUTH_TOKEN_KEY);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function request<T>(url: string, init?: RequestInit): Promise<T> {
|
|
const token = getStoredToken();
|
|
const headers: Record<string, string> = {
|
|
"Content-Type": "application/json",
|
|
...(init?.headers as Record<string, string>),
|
|
};
|
|
if (token && !headers["Authorization"]) {
|
|
headers["Authorization"] = `Bearer ${token}`;
|
|
}
|
|
|
|
const res = await fetch(url, {
|
|
...init,
|
|
headers,
|
|
});
|
|
|
|
if (!res.ok) {
|
|
let detail = res.statusText;
|
|
try {
|
|
const body: unknown = await res.json();
|
|
if (typeof body === "object" && body !== null && "detail" in body) {
|
|
const d = (body as { detail: unknown }).detail;
|
|
detail = typeof d === "string" ? d : Array.isArray(d) ? d.map((e: any) => e.msg || JSON.stringify(e)).join("; ") : JSON.stringify(d);
|
|
}
|
|
} catch {
|
|
// body not JSON — keep statusText
|
|
}
|
|
throw new ApiError(res.status, detail);
|
|
}
|
|
|
|
return res.json() as Promise<T>;
|
|
}
|
|
|
|
/**
|
|
* Multipart form-data request — does NOT set Content-Type (browser adds
|
|
* multipart boundary automatically). Still attaches auth token.
|
|
*/
|
|
export async function requestMultipart<T>(
|
|
url: string,
|
|
formData: FormData,
|
|
init?: RequestInit,
|
|
): Promise<T> {
|
|
const token = getStoredToken();
|
|
const headers: Record<string, string> = {
|
|
...(init?.headers as Record<string, string>),
|
|
};
|
|
if (token && !headers["Authorization"]) {
|
|
headers["Authorization"] = `Bearer ${token}`;
|
|
}
|
|
|
|
const res = await fetch(url, {
|
|
method: "POST",
|
|
...init,
|
|
headers,
|
|
body: formData,
|
|
});
|
|
|
|
if (!res.ok) {
|
|
let detail = res.statusText;
|
|
try {
|
|
const body: unknown = await res.json();
|
|
if (typeof body === "object" && body !== null && "detail" in body) {
|
|
const d = (body as { detail: unknown }).detail;
|
|
detail =
|
|
typeof d === "string"
|
|
? d
|
|
: Array.isArray(d)
|
|
? d.map((e: any) => e.msg || JSON.stringify(e)).join("; ")
|
|
: JSON.stringify(d);
|
|
}
|
|
} catch {
|
|
// body not JSON — keep statusText
|
|
}
|
|
throw new ApiError(res.status, detail);
|
|
}
|
|
|
|
return res.json() as Promise<T>;
|
|
}
|