119 lines
3.1 KiB
TypeScript
119 lines
3.1 KiB
TypeScript
import type {
|
|
CategoryListResponse,
|
|
Pattern,
|
|
PatternListResponse,
|
|
PatternTutorial,
|
|
QuestionDetail,
|
|
QuestionListResponse,
|
|
Stats,
|
|
SubmissionRequest,
|
|
SubmissionResponse,
|
|
} from "@/types";
|
|
|
|
// Server-side uses internal Docker network, client-side uses public URL
|
|
const API_BASE_URL =
|
|
typeof window === "undefined"
|
|
? process.env.API_URL || "http://localhost:8000"
|
|
: process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
|
|
|
|
export class ApiError extends Error {
|
|
constructor(
|
|
public status: number,
|
|
public statusText: string,
|
|
public detail?: string
|
|
) {
|
|
super(detail || `API error: ${status} ${statusText}`);
|
|
this.name = "ApiError";
|
|
}
|
|
}
|
|
|
|
async function fetchApi<T>(
|
|
endpoint: string,
|
|
options?: RequestInit
|
|
): Promise<T> {
|
|
const url = `${API_BASE_URL}${endpoint}`;
|
|
const response = await fetch(url, {
|
|
...options,
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
...options?.headers,
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
let detail: string | undefined;
|
|
try {
|
|
const errorBody = await response.json();
|
|
detail = errorBody.detail;
|
|
} catch {
|
|
// Ignore JSON parse errors
|
|
}
|
|
throw new ApiError(response.status, response.statusText, detail);
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
export interface QuestionFilters {
|
|
page?: number;
|
|
limit?: number;
|
|
difficulty?: string;
|
|
category?: string;
|
|
pattern?: string;
|
|
search?: string;
|
|
}
|
|
|
|
export async function getQuestions(
|
|
filters: QuestionFilters = {}
|
|
): Promise<QuestionListResponse> {
|
|
const params = new URLSearchParams();
|
|
|
|
if (filters.page) params.set("page", filters.page.toString());
|
|
if (filters.limit) params.set("limit", filters.limit.toString());
|
|
if (filters.difficulty) params.set("difficulty", filters.difficulty);
|
|
if (filters.category) params.set("category", filters.category);
|
|
if (filters.pattern) params.set("pattern", filters.pattern);
|
|
if (filters.search) params.set("search", filters.search);
|
|
|
|
const queryString = params.toString();
|
|
const endpoint = queryString
|
|
? `/api/questions?${queryString}`
|
|
: "/api/questions";
|
|
|
|
return fetchApi<QuestionListResponse>(endpoint);
|
|
}
|
|
|
|
export async function getQuestion(slug: string): Promise<QuestionDetail> {
|
|
return fetchApi<QuestionDetail>(`/api/questions/${slug}`);
|
|
}
|
|
|
|
export async function getCategories(): Promise<CategoryListResponse> {
|
|
return fetchApi<CategoryListResponse>("/api/categories");
|
|
}
|
|
|
|
export async function getPatterns(): Promise<PatternListResponse> {
|
|
return fetchApi<PatternListResponse>("/api/patterns");
|
|
}
|
|
|
|
export async function getPattern(slug: string): Promise<Pattern> {
|
|
return fetchApi<Pattern>(`/api/patterns/${slug}`);
|
|
}
|
|
|
|
export async function getPatternTutorial(slug: string): Promise<PatternTutorial> {
|
|
return fetchApi<PatternTutorial>(`/api/patterns/${slug}/tutorial`);
|
|
}
|
|
|
|
export async function getStats(): Promise<Stats> {
|
|
return fetchApi<Stats>("/api/stats");
|
|
}
|
|
|
|
export async function submitSolution(
|
|
slug: string,
|
|
submission: SubmissionRequest
|
|
): Promise<SubmissionResponse> {
|
|
return fetchApi<SubmissionResponse>(`/api/questions/${slug}/submit`, {
|
|
method: "POST",
|
|
body: JSON.stringify(submission),
|
|
});
|
|
}
|