From 874e8b8958a70a51d46f667edcb5da391b9a6a57 Mon Sep 17 00:00:00 2001 From: Kai Chappell Date: Tue, 29 Apr 2025 21:06:13 +0100 Subject: [PATCH] wire up pages --- frontend/src/app/categories/page.tsx | 37 ++++ frontend/src/app/error.tsx | 23 +++ frontend/src/app/globals.css | 40 ++++ frontend/src/app/layout.tsx | 59 ++++++ frontend/src/app/not-found.tsx | 18 ++ frontend/src/app/page.tsx | 103 +++++++++ frontend/src/app/patterns/[slug]/page.tsx | 63 ++++++ frontend/src/app/patterns/page.tsx | 38 ++++ frontend/src/app/providers.tsx | 22 ++ frontend/src/app/questions/[slug]/page.tsx | 229 +++++++++++++++++++++ frontend/src/app/questions/page.tsx | 159 ++++++++++++++ 11 files changed, 791 insertions(+) create mode 100644 frontend/src/app/categories/page.tsx create mode 100644 frontend/src/app/error.tsx create mode 100644 frontend/src/app/globals.css create mode 100644 frontend/src/app/layout.tsx create mode 100644 frontend/src/app/not-found.tsx create mode 100644 frontend/src/app/page.tsx create mode 100644 frontend/src/app/patterns/[slug]/page.tsx create mode 100644 frontend/src/app/patterns/page.tsx create mode 100644 frontend/src/app/providers.tsx create mode 100644 frontend/src/app/questions/[slug]/page.tsx create mode 100644 frontend/src/app/questions/page.tsx diff --git a/frontend/src/app/categories/page.tsx b/frontend/src/app/categories/page.tsx new file mode 100644 index 0000000..f0ef69a --- /dev/null +++ b/frontend/src/app/categories/page.tsx @@ -0,0 +1,37 @@ +import { getCategories } from "@/lib/api"; +import Link from "next/link"; + +export default async function CategoriesPage() { + const { items: categories } = await getCategories(); + + return ( +
+

Categories

+

+ Browse questions by data structure or algorithm category. +

+ +
+ {categories.map((category) => ( + +
+

{category.name}

+ + {category.question_count} questions + +
+ {category.description && ( +

+ {category.description} +

+ )} + + ))} +
+
+ ); +} diff --git a/frontend/src/app/error.tsx b/frontend/src/app/error.tsx new file mode 100644 index 0000000..f8e9f52 --- /dev/null +++ b/frontend/src/app/error.tsx @@ -0,0 +1,23 @@ +"use client"; + +export default function Error({ + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + return ( +
+

Something went wrong

+

+ An error occurred while loading this page. +

+ +
+ ); +} diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css new file mode 100644 index 0000000..e688465 --- /dev/null +++ b/frontend/src/app/globals.css @@ -0,0 +1,40 @@ +@import "tailwindcss"; + +:root { + --background: #ffffff; + --foreground: #171717; + --card: #ffffff; + --card-foreground: #171717; + --primary: #3b82f6; + --primary-foreground: #ffffff; + --secondary: #f3f4f6; + --secondary-foreground: #171717; + --muted: #f3f4f6; + --muted-foreground: #6b7280; + --border: #e5e7eb; + --ring: #3b82f6; +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + --card: #171717; + --card-foreground: #ededed; + --primary: #3b82f6; + --primary-foreground: #ffffff; + --secondary: #262626; + --secondary-foreground: #ededed; + --muted: #262626; + --muted-foreground: #a3a3a3; + --border: #262626; + --ring: #3b82f6; + } +} + +body { + background: var(--background); + color: var(--foreground); + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, + "Helvetica Neue", Arial, sans-serif; +} diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx new file mode 100644 index 0000000..3c77fb9 --- /dev/null +++ b/frontend/src/app/layout.tsx @@ -0,0 +1,59 @@ +import type { Metadata } from "next"; +import Link from "next/link"; +import { Providers } from "./providers"; +import "./globals.css"; + +export const metadata: Metadata = { + title: "CodeTutor - Coding Interview Preparation", + description: + "Master coding interviews with curated questions, detailed explanations, and optimal solutions.", +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + +
+ +
+
{children}
+
+ CodeTutor - Coding Interview Preparation +
+
+ + + ); +} diff --git a/frontend/src/app/not-found.tsx b/frontend/src/app/not-found.tsx new file mode 100644 index 0000000..d32f5e8 --- /dev/null +++ b/frontend/src/app/not-found.tsx @@ -0,0 +1,18 @@ +import Link from "next/link"; + +export default function NotFound() { + return ( +
+

404

+

+ The page you're looking for doesn't exist. +

+ + Go Home + +
+ ); +} diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx new file mode 100644 index 0000000..f833b47 --- /dev/null +++ b/frontend/src/app/page.tsx @@ -0,0 +1,103 @@ +import Link from "next/link"; +import { getStats } from "@/lib/api"; + +export default async function HomePage() { + let stats; + try { + stats = await getStats(); + } catch { + stats = null; + } + + return ( +
+
+

Master Coding Interviews

+

+ Curated collection of coding interview questions with detailed + explanations, common pitfalls, and optimal solutions. +

+ + Browse Questions + +
+ + {stats && ( +
+
+
+ {stats.total_questions} +
+
+ Total Questions +
+
+
+
+ {stats.by_category.length} +
+
Categories
+
+
+
+ {stats.by_pattern.length} +
+
+ Algorithmic Patterns +
+
+
+ )} + + {stats && ( +
+
+
+ {stats.by_difficulty.easy} +
+
Easy
+
+
+
+ {stats.by_difficulty.medium} +
+
Medium
+
+
+
+ {stats.by_difficulty.hard} +
+
Hard
+
+
+ )} + +
+

Quick Links

+
+ +

Browse by Category

+

+ Arrays, Trees, Graphs, Dynamic Programming, and more +

+ + +

Browse by Pattern

+

+ Two Pointers, Sliding Window, BFS, DFS, Backtracking, and more +

+ +
+
+
+ ); +} diff --git a/frontend/src/app/patterns/[slug]/page.tsx b/frontend/src/app/patterns/[slug]/page.tsx new file mode 100644 index 0000000..8f51f15 --- /dev/null +++ b/frontend/src/app/patterns/[slug]/page.tsx @@ -0,0 +1,63 @@ +import { getPattern, getQuestions } from "@/lib/api"; +import { QuestionCard } from "@/components/questions/question-card"; +import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; +import { notFound } from "next/navigation"; + +export default async function PatternDetailPage({ + params, +}: { + params: Promise<{ slug: string }>; +}) { + const { slug } = await params; + + let pattern; + let questions; + try { + [pattern, questions] = await Promise.all([ + getPattern(slug), + getQuestions({ pattern: slug, limit: 50 }), + ]); + } catch { + notFound(); + } + + return ( +
+
+

{pattern.name}

+

+ {pattern.question_count} questions using this pattern +

+
+ + {pattern.description && ( + + + Description + + {pattern.description} + + )} + + {pattern.when_to_use && ( + + + When to Use + + + {pattern.when_to_use} + + + )} + +
+

Questions

+
+ {questions.items.map((question) => ( + + ))} +
+
+
+ ); +} diff --git a/frontend/src/app/patterns/page.tsx b/frontend/src/app/patterns/page.tsx new file mode 100644 index 0000000..6d9d5a0 --- /dev/null +++ b/frontend/src/app/patterns/page.tsx @@ -0,0 +1,38 @@ +import { getPatterns } from "@/lib/api"; +import Link from "next/link"; + +export default async function PatternsPage() { + const { items: patterns } = await getPatterns(); + + return ( +
+

Algorithmic Patterns

+

+ Master common problem-solving patterns to recognize and apply them in + interviews. +

+ +
+ {patterns.map((pattern) => ( + +
+

{pattern.name}

+ + {pattern.question_count} questions + +
+ {pattern.description && ( +

+ {pattern.description} +

+ )} + + ))} +
+
+ ); +} diff --git a/frontend/src/app/providers.tsx b/frontend/src/app/providers.tsx new file mode 100644 index 0000000..8761383 --- /dev/null +++ b/frontend/src/app/providers.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { useState, type ReactNode } from "react"; + +export function Providers({ children }: { children: ReactNode }) { + const [queryClient] = useState( + () => + new QueryClient({ + defaultOptions: { + queries: { + staleTime: 60 * 1000, + refetchOnWindowFocus: false, + }, + }, + }) + ); + + return ( + {children} + ); +} diff --git a/frontend/src/app/questions/[slug]/page.tsx b/frontend/src/app/questions/[slug]/page.tsx new file mode 100644 index 0000000..dc4d31c --- /dev/null +++ b/frontend/src/app/questions/[slug]/page.tsx @@ -0,0 +1,229 @@ +import { getQuestion } from "@/lib/api"; +import { Badge } from "@/components/ui/badge"; +import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; +import { CodeBlock } from "@/components/ui/code-block"; +import { getDifficultyColor, capitalize } from "@/lib/utils"; +import Link from "next/link"; +import { notFound } from "next/navigation"; + +export default async function QuestionDetailPage({ + params, +}: { + params: Promise<{ slug: string }>; +}) { + const { slug } = await params; + + let question; + try { + question = await getQuestion(slug); + } catch { + notFound(); + } + + return ( +
+
+
+

{question.title}

+ + {capitalize(question.difficulty)} + +
+ +
+ {question.categories.map((cat) => ( + + {cat.name} + + ))} + {question.patterns.map((pat) => ( + + {pat.name} + + ))} +
+ + {question.leetcode_url && ( + + View on LeetCode + + )} +
+ + + + Problem + + +
{question.description}
+
+
+ + {question.constraints && ( + + + Constraints + + +
+              {question.constraints}
+            
+
+
+ )} + + {question.examples && question.examples.length > 0 && ( + + + Examples + + + {question.examples.map((example, i) => ( +
+
+ Input: + {example.input} +
+
+ Output: + {example.output} +
+ {example.explanation && ( +
+ {example.explanation} +
+ )} +
+ ))} +
+
+ )} + + {question.explanation && ( + <> + + + Approach + + + {question.explanation.approach} + + + + + + Intuition + + + {question.explanation.intuition} + + + + + + Complexity Analysis + + +
+ Time: + {question.explanation.time_complexity} +
+
+ Space: + {question.explanation.space_complexity} +
+ {question.explanation.complexity_explanation && ( +
+ {question.explanation.complexity_explanation} +
+ )} +
+
+ + {question.explanation.common_pitfalls && + question.explanation.common_pitfalls.length > 0 && ( + + + Common Pitfalls + + + {question.explanation.common_pitfalls.map((pitfall, i) => ( +
+

{pitfall.title}

+

+ {pitfall.description} +

+ {pitfall.wrong_approach && ( +
+ Wrong: + {pitfall.wrong_approach} +
+ )} + {pitfall.correct_approach && ( +
+ Correct: + {pitfall.correct_approach} +
+ )} +
+ ))} +
+
+ )} + + {question.explanation.key_takeaways && + question.explanation.key_takeaways.length > 0 && ( + + + Key Takeaways + + +
    + {question.explanation.key_takeaways.map((takeaway, i) => ( +
  • {takeaway}
  • + ))} +
+
+
+ )} + + )} + + {question.solutions.length > 0 && ( + + + Solutions + + + {question.solutions.map((solution) => ( +
+
+

{solution.approach_name}

+ {solution.is_optimal && ( + + Optimal + + )} +
+ {solution.explanation && ( +

+ {solution.explanation} +

+ )} + +
+ ))} +
+
+ )} +
+ ); +} diff --git a/frontend/src/app/questions/page.tsx b/frontend/src/app/questions/page.tsx new file mode 100644 index 0000000..e614e3f --- /dev/null +++ b/frontend/src/app/questions/page.tsx @@ -0,0 +1,159 @@ +import { getQuestions, getCategories, getPatterns } from "@/lib/api"; +import { QuestionCard } from "@/components/questions/question-card"; +import Link from "next/link"; + +interface SearchParams { + difficulty?: string; + category?: string; + pattern?: string; + search?: string; + page?: string; +} + +export default async function QuestionsPage({ + searchParams, +}: { + searchParams: Promise; +}) { + const params = await searchParams; + const [questionsResponse, categoriesResponse, patternsResponse] = + await Promise.all([ + getQuestions({ + difficulty: params.difficulty, + category: params.category, + pattern: params.pattern, + search: params.search, + page: params.page ? parseInt(params.page) : 1, + }), + getCategories(), + getPatterns(), + ]); + + const difficulties = ["easy", "medium", "hard"]; + + return ( +
+

Questions

+ +
+
+ +
+ + All + + {difficulties.map((d) => ( + + {d} + + ))} +
+
+ +
+ + +
+ +
+ + +
+
+ +
+ Showing {questionsResponse.items.length} of {questionsResponse.total}{" "} + questions +
+ +
+ {questionsResponse.items.map((question) => ( + + ))} +
+ + {questionsResponse.pages > 1 && ( +
+ {Array.from({ length: questionsResponse.pages }, (_, i) => i + 1).map( + (page) => ( + + {page} + + ) + )} +
+ )} +
+ ); +}