types and api client
This commit is contained in:
79
frontend/src/lib/api.ts
Normal file
79
frontend/src/lib/api.ts
Normal file
@@ -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<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) {
|
||||||
|
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<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 getStats(): Promise<Stats> {
|
||||||
|
return fetchApi<Stats>("/api/stats");
|
||||||
|
}
|
||||||
20
frontend/src/lib/utils.ts
Normal file
20
frontend/src/lib/utils.ts
Normal file
@@ -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);
|
||||||
|
}
|
||||||
116
frontend/src/types/index.ts
Normal file
116
frontend/src/types/index.ts
Normal file
@@ -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[];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user