Next.js 15 frontend for YeetCode — a competitive LeetCode platform with duels, XP, streaks, and group leaderboards.
- Next.js 15 — App Router, React Server Components
- TypeScript — Full type safety
- Tailwind CSS — Neobrutalist design system
- Resend — Transactional email (via API backend)
- Node.js 18+
- The YeetCode API running locally at
http://localhost:6969
git clone https://github.com/yeetcode-xyz/yeetcode-website.git
cd yeetcode-website
npm install
cp .env.example .env.local
# Fill in .env.local (see Environment Variables below)npm run devApp is at http://localhost:3000.
| Variable | Required | Description |
|---|---|---|
YEETCODE_API_URL |
Yes | URL of the YeetCode API (e.g. http://localhost:6969) |
YEETCODE_API_KEY |
Yes | API key matching YETCODE_API_KEY on the backend |
yeetcode-website/
│
├── app/
│ ├── layout.tsx # Root layout (fonts, metadata)
│ ├── page.tsx # Landing page
│ │
│ ├── (app)/ # Auth-required pages
│ │ ├── layout.tsx # Wraps with AppProvider + ToastProvider
│ │ ├── dashboard/page.tsx # Main dashboard
│ │ └── group/page.tsx # Group management page
│ │
│ ├── (auth)/ # Auth pages (no session required)
│ │ ├── login/page.tsx # Email input → OTP send
│ │ ├── verify/page.tsx # OTP verification
│ │ └── onboarding/page.tsx # Username + university setup (new users)
│ │
│ ├── duel-invite/[token]/page.tsx # Public duel invite landing page (no auth)
│ │
│ ├── components/
│ │ ├── ui/
│ │ │ ├── toast.tsx # Toast + sendBrowserNotification
│ │ │ └── searchable-dropdown.tsx
│ │ └── dashboard/
│ │ ├── DuelsSection.tsx # Full duel UI (challenge, accept, solve, history)
│ │ ├── TodaysChallenge.tsx # Daily problem card
│ │ ├── UserStats.tsx # XP, streak, solve counts
│ │ ├── FriendsLeaderboard.tsx
│ │ ├── ActiveBounties.tsx
│ │ ├── QuickActions.tsx
│ │ └── LeaderboardHeader.tsx
│ │
│ └── api/ # Next.js API routes (thin proxies to backend)
│ ├── auth/
│ │ ├── send-otp/ # POST → /store-verification-code
│ │ ├── login/ # POST → /verify-code (sets session cookie)
│ │ ├── me/ # GET → /user (reads session cookie)
│ │ └── logout/ # POST → clears session cookie
│ ├── user/
│ │ ├── create/ # POST → /create-user
│ │ ├── [username]/ # GET → /user/{username}
│ │ └── display-name/ # POST → /update-display-name
│ ├── duels/
│ │ ├── [username]/ # GET → /duels/{username}
│ │ ├── create/ # POST → /create-duel
│ │ ├── accept/ # POST → /accept-duel
│ │ ├── reject/ # POST → /reject-duel
│ │ ├── start/ # POST → /start-duel
│ │ ├── verify/ # POST → /verify-duel-solve
│ │ ├── submit/ # POST → /record-duel-submission
│ │ ├── detail/[duelId]/ # GET → /duel/{duelId}
│ │ ├── create-open/ # POST → /create-open-challenge
│ │ ├── accept-open/ # POST → /accept-open-challenge
│ │ ├── open-challenges/[username]/ # GET → /open-challenges/{username}
│ │ ├── create-invite/ # POST → /create-duel-invite
│ │ ├── invite/[token]/ # GET → /duel-invite/{token}
│ │ └── accept-invite/ # POST → /accept-duel-invite
│ ├── group/
│ │ ├── create/ # POST → /create-group
│ │ ├── join/ # POST → /join-group
│ │ ├── leave/ # POST → /leave-group
│ │ └── [groupId]/stats/ # GET → /group-stats/{groupId}
│ ├── leaderboard/ # GET → /leaderboard
│ ├── daily/
│ │ ├── [username]/ # GET → /daily-problem/{username}
│ │ └── complete/ # POST → /complete-daily-problem
│ └── bounties/[username]/ # GET → /bounties/{username}
│
└── lib/
├── api.ts # Typed fetch helpers + shared interfaces (Duel, User, etc.)
└── contexts/
└── app-context.tsx # Global state: user, duels, daily, groups
All global state lives in AppProvider (lib/contexts/app-context.tsx):
user— current user (XP, streak, group, university, etc.)duels— user's duel list (PENDING, ACTIVE, COMPLETED)dailyData— today's challenge + completion statusisDuelsLoading— controls skeleton vs. content in DuelsSectionrefreshDuels(silent?)— re-fetches duels; passsilent=trueto skip the loading skeleton (used by the 3s background poll in DuelsSection)
- User enters email →
/api/auth/send-otp→ backend sends OTP via Resend - User enters OTP →
/api/auth/login→ backend verifies, returnsusername - Next.js route sets an HTTP-only
sessioncookie with the username /api/auth/mereads that cookie and returns the current user- New users (no username yet) are redirected to
/onboardingto pick a username and university
DuelsSection manages the full duel lifecycle across tabs:
Challenge tab → create-duel / create-open-challenge / create-duel-invite
↓
Pending tab → accept-duel / reject-duel (shows expiry countdown)
↓
Active tab → start-duel → verify-duel-solve (user clicks) or auto-poll every 3s
↓
History tab → completed duels with Rematch button
Notifications: A browser notification fires when a new PENDING challenge arrives. A toast + browser notification fires when a duel completes.
Invite link: "Invite someone new" generates a shareable yeetcode.xyz/duel-invite/{token} link and optionally emails it. The recipient sees the invite landing page and can accept after logging in or creating an account.
All app/api/ routes are thin proxies. They read the session cookie for username, forward the request to the backend with X-API-Key, and return the JSON response.
// Example: app/api/duels/accept/route.ts
export async function POST(req: Request) {
const session = (await cookies()).get("session")?.value
const body = await req.json()
const res = await fetch(`${process.env.YEETCODE_API_URL}/accept-duel`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": process.env.YEETCODE_API_KEY!,
},
body: JSON.stringify({ ...body, username: session }),
})
return NextResponse.json(await res.json())
}| Rank | XP Range |
|---|---|
| Script Kiddie | 0–499 |
| Debugger | 500–1,499 |
| Stack Overflower | 1,500–3,499 |
| Algorithm Apprentice | 3,500–6,499 |
| Loop Guru | 6,500–11,999 |
| Recursion Wizard | 12,000–19,999 |
| Regex Sorcerer | 20,000–34,999 |
| Master Yeeter | 35,000–49,999 |
| 0xDEADBEEF | 50,000+ |
Each rank has three subdivisions (I, II, III).
- Point Coolify at this repo (
mainbranch) - Set build command:
npm run build - Set start command:
npm start - Set env vars:
YEETCODE_API_URL,YEETCODE_API_KEY - Deploy — no persistent storage needed (all data lives in the API)
Create a file under app/api/ following the proxy pattern above. Keep routes thin — no business logic, just forward to the backend.
- Create the component in
app/components/dashboard/ - Import and render it in
app/(app)/dashboard/page.tsx - Use
useAppContext()for global state (user, duels, etc.)
- Fork → create a branch (
git checkout -b feature/your-feature) - Run locally:
npm run dev - Lint before pushing:
npm run lint - PR against
main