Skip to main content
CodePlanet Docs

Frontend Stack

Next.js, React, and UI components

CodePlanet's frontend is built with modern web technologies designed for performance, developer experience, and maintainability.

Technology Overview

TechnologyVersionPurpose
Next.js14+React framework with App Router
React19UI library
TypeScript5.xType safety
Tailwind CSS3.xUtility-first styling
shadcn/uiLatestComponent library
Framer Motion11.xAnimations
Lucide IconsLatestIcon library

Next.js App Router

We use the Next.js 14+ App Router for routing and layouts:

src/app/
├── (auth)/              # Auth routes (no layout)
│   ├── login/
│   └── signup/
├── (marketing)/         # Public pages
│   └── page.tsx         # Landing page
├── dashboard/           # Protected dashboard
│   ├── layout.tsx       # Dashboard layout with sidebar
│   ├── page.tsx         # Home
│   └── problems/
├── docs/                # Documentation
│   ├── layout.tsx       # Docs layout with nav
│   └── [slug]/
└── api/                 # API routes
    └── v1/

Route Groups

Route groups (name) organize routes without affecting URLs:

  • (auth) — Login/signup pages without main layout
  • (marketing) — Public pages with marketing layout
  • (dashboard) — Protected pages with app layout

Server vs Client Components

By default, components are Server Components:

// Server Component (default)
async function ProblemsPage() {
  const problems = await fetchProblems(); // Runs on server
  return <ProblemList problems={problems} />;
}

Add "use client" for interactivity:

"use client";
 
import { useState } from "react";
 
function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

Component Architecture

UI Primitives (/components/ui/)

Base components from shadcn/ui:

import { Button } from "@/components/ui/button";
import { Card, CardHeader, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";

Feature Components

Organized by feature/domain:

components/
├── ui/                  # Base primitives (shadcn/ui)
├── dashboard/           # Dashboard-specific
│   ├── WeakTopicsWidget.tsx
│   ├── StatsCard.tsx
│   └── ActivityCalendar.tsx
├── docs/                # Documentation
│   ├── ProfessionalDocsLayout.tsx
│   └── ProfessionalDocContent.tsx
├── payments/            # Payment flow
│   └── CheckoutFlow.tsx
└── problems/            # Problem-related
    ├── ProblemCard.tsx
    └── CodeEditor.tsx

Component Patterns

Compound Components:

<Card>
  <CardHeader>
    <CardTitle>Title</CardTitle>
  </CardHeader>
  <CardContent>Content</CardContent>
</Card>

Render Props:

<DataTable
  data={problems}
  renderRow={(problem) => <ProblemRow {...problem} />}
/>

Composition:

<Dialog>
  <DialogTrigger asChild>
    <Button>Open</Button>
  </DialogTrigger>
  <DialogContent>...</DialogContent>
</Dialog>

Styling with Tailwind

We use Tailwind CSS with custom configuration:

Theme Colors

// tailwind.config.ts
const config = {
  theme: {
    extend: {
      colors: {
        background: "hsl(var(--background))",
        foreground: "hsl(var(--foreground))",
        primary: "hsl(var(--primary))",
        // ... CSS variables for theming
      },
    },
  },
};

Common Patterns

// Flex centering
<div className="flex items-center justify-center">
 
// Responsive layout
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
 
// Hover states
<button className="hover:bg-neutral-800 transition-colors">
 
// Dark theme (default)
<div className="bg-neutral-900 text-white">

CSS Variables

:root {
  --background: 0 0% 4%;      /* #0a0a0a */
  --foreground: 0 0% 100%;    /* #ffffff */
  --primary: 0 0% 100%;
  --muted: 0 0% 40%;
  --border: 0 0% 15%;
}

Animations with Framer Motion

Basic Animation

import { motion } from "framer-motion";
 
<motion.div
  initial={{ opacity: 0, y: 20 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ duration: 0.3 }}
>
  Content
</motion.div>

Spring Animations

<motion.div
  animate={{ width: isExpanded ? 240 : 64 }}
  transition={{
    type: "spring",
    stiffness: 300,
    damping: 30,
  }}
>
  Sidebar
</motion.div>

AnimatePresence

<AnimatePresence mode="wait">
  {isVisible && (
    <motion.div
      key="modal"
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      exit={{ opacity: 0 }}
    >
      Modal Content
    </motion.div>
  )}
</AnimatePresence>

Reduced Motion

Always respect user preferences:

const prefersReducedMotion = window.matchMedia(
  "(prefers-reduced-motion: reduce)"
).matches;
 
<motion.div
  transition={prefersReducedMotion ? { duration: 0 } : { duration: 0.3 }}
>

State Management

Local State

For component-level state:

const [isOpen, setIsOpen] = useState(false);

React Context

For shared state across components:

// DocsContext for documentation
const DocsContext = createContext<DocsContextType>(null);
 
function DocsProvider({ children }) {
  const [searchOpen, setSearchOpen] = useState(false);
  return (
    <DocsContext.Provider value={{ searchOpen, setSearchOpen }}>
      {children}
    </DocsContext.Provider>
  );
}

URL State

For persistent, shareable state:

import { useSearchParams } from "next/navigation";
 
function ProblemFilters() {
  const searchParams = useSearchParams();
  const difficulty = searchParams.get("difficulty");
  // ...
}

Data Fetching

Server Components (Preferred)

async function ProblemsPage() {
  const problems = await fetch("/api/v1/problems").then(r => r.json());
  return <ProblemList problems={problems} />;
}

Client-Side

"use client";
 
function ProblemDetails({ slug }) {
  const [problem, setProblem] = useState(null);
  
  useEffect(() => {
    fetch(`/api/v1/problems/${slug}`)
      .then(r => r.json())
      .then(setProblem);
  }, [slug]);
  
  return problem ? <Problem {...problem} /> : <Loading />;
}

Performance Optimizations

Code Splitting

Dynamic imports for heavy components:

const CodeEditor = dynamic(() => import("@/components/CodeEditor"), {
  loading: () => <EditorSkeleton />,
});

Image Optimization

import Image from "next/image";
 
<Image
  src="/logo.png"
  alt="CodePlanet"
  width={120}
  height={40}
  priority // For above-fold images
/>

Memoization

const memoizedValue = useMemo(() => expensiveComputation(data), [data]);
const memoizedCallback = useCallback(() => doSomething(), []);

Next Steps