From d29b1d9772f440e17992ddaa79bc01f6591a8fe0 Mon Sep 17 00:00:00 2001 From: Kai Chappell Date: Tue, 29 Apr 2025 19:57:31 +0100 Subject: [PATCH] types and api client --- frontend/src/lib/api.ts | 79 ++++++++++++++++++++++++ frontend/src/lib/utils.ts | 20 +++++++ frontend/src/types/index.ts | 116 ++++++++++++++++++++++++++++++++++++ 3 files changed, 215 insertions(+) create mode 100644 frontend/src/lib/api.ts create mode 100644 frontend/src/lib/utils.ts create mode 100644 frontend/src/types/index.ts diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts new file mode 100644 index 0000000..528ac63 --- /dev/null +++ b/frontend/src/lib/api.ts @@ -0,0 +1,79 @@ +import type { + CategoryListResponse, + Pattern, + PatternListResponse, + QuestionDetail, + QuestionListResponse, + Stats, +} from "@/types"; + +const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"; + +async function fetchApi( + endpoint: string, + options?: RequestInit +): Promise { + const url = `${API_BASE_URL}${endpoint}`; + const response = await fetch(url, { + ...options, + headers: { + "Content-Type": "application/json", + ...options?.headers, + }, + }); + + if (!response.ok) { + throw new Error(`API error: ${response.status} ${response.statusText}`); + } + + 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 { + 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(endpoint); +} + +export async function getQuestion(slug: string): Promise { + return fetchApi(`/api/questions/${slug}`); +} + +export async function getCategories(): Promise { + return fetchApi("/api/categories"); +} + +export async function getPatterns(): Promise { + return fetchApi("/api/patterns"); +} + +export async function getPattern(slug: string): Promise { + return fetchApi(`/api/patterns/${slug}`); +} + +export async function getStats(): Promise { + return fetchApi("/api/stats"); +} diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts new file mode 100644 index 0000000..302e46c --- /dev/null +++ b/frontend/src/lib/utils.ts @@ -0,0 +1,20 @@ +import type { Difficulty } from "@/types"; + +export function cn(...classes: (string | undefined | false)[]): string { + return classes.filter(Boolean).join(" "); +} + +export function getDifficultyColor(difficulty: Difficulty): string { + switch (difficulty) { + case "easy": + return "text-green-600 bg-green-100 dark:text-green-400 dark:bg-green-900/30"; + case "medium": + return "text-yellow-600 bg-yellow-100 dark:text-yellow-400 dark:bg-yellow-900/30"; + case "hard": + return "text-red-600 bg-red-100 dark:text-red-400 dark:bg-red-900/30"; + } +} + +export function capitalize(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1); +} diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts new file mode 100644 index 0000000..444bb90 --- /dev/null +++ b/frontend/src/types/index.ts @@ -0,0 +1,116 @@ +export type Difficulty = "easy" | "medium" | "hard"; + +export interface CategoryBrief { + id: string; + name: string; + slug: string; +} + +export interface PatternBrief { + id: string; + name: string; + slug: string; +} + +export interface Category extends CategoryBrief { + description: string | null; + question_count: number; +} + +export interface Pattern extends PatternBrief { + description: string | null; + when_to_use: string | null; + question_count: number; +} + +export interface QuestionListItem { + id: string; + title: string; + slug: string; + difficulty: Difficulty; + leetcode_id: number | null; + leetcode_url: string | null; + categories: CategoryBrief[]; + patterns: PatternBrief[]; + created_at: string; +} + +export interface CommonPitfall { + title: string; + description: string; + wrong_approach?: string; + correct_approach?: string; +} + +export interface Explanation { + approach: string; + intuition: string; + common_pitfalls: CommonPitfall[] | null; + key_takeaways: string[] | null; + time_complexity: string; + space_complexity: string; + complexity_explanation: string | null; +} + +export interface Solution { + id: string; + approach_name: string; + code: string; + language: string; + is_optimal: boolean; + explanation: string | null; +} + +export interface QuestionDetail extends QuestionListItem { + description: string; + constraints: string | null; + examples: Array<{ + input: string; + output: string; + explanation?: string; + }> | null; + explanation: Explanation | null; + solutions: Solution[]; + updated_at: string; +} + +export interface QuestionListResponse { + items: QuestionListItem[]; + total: number; + page: number; + limit: number; + pages: number; +} + +export interface CategoryListResponse { + items: Category[]; +} + +export interface PatternListResponse { + items: Pattern[]; +} + +export interface DifficultyCount { + easy: number; + medium: number; + hard: number; +} + +export interface CategoryCount { + name: string; + slug: string; + count: number; +} + +export interface PatternCount { + name: string; + slug: string; + count: number; +} + +export interface Stats { + total_questions: number; + by_difficulty: DifficultyCount; + by_category: CategoryCount[]; + by_pattern: PatternCount[]; +}