From 33584dc4f14aa73e70d79ca5eb27f54c7dfa073e Mon Sep 17 00:00:00 2001 From: Birajit Saikia Date: Thu, 5 Mar 2026 23:43:02 +0530 Subject: [PATCH 1/5] feat: deliver issue #7 frontend completion pass --- .github/workflows/ci.yml | 9 + README.md | 101 +++++-- e2e/auth-flow.spec.ts | 6 +- e2e/navigation-pages.spec.ts | 29 +- middleware.ts | 12 +- src/app/competitions/page.tsx | 131 ++++----- src/app/components/MainLayout.tsx | 55 +--- src/app/components/Navbar.tsx | 131 ++++++--- src/app/globals.css | 19 +- src/app/learn/page.tsx | 135 ++++----- src/app/login/page.test.tsx | 6 +- src/app/login/page.tsx | 218 ++++++++------- src/app/problems/page.tsx | 447 +++++++++++++++++++++--------- src/app/profile/page.tsx | 288 +++++++++++-------- src/lib/api.ts | 59 +++- src/test/setup.ts | 35 +++ 16 files changed, 1068 insertions(+), 613 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d088dc6..15e7a3f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,5 +26,14 @@ jobs: - name: Run linter run: npm run lint + - name: Run unit tests + run: npm run test:unit + - name: Run build run: npm run build + + - name: Install Playwright browser + run: npx playwright install --with-deps chromium + + - name: Run E2E tests + run: npm run test:e2e diff --git a/README.md b/README.md index e215bc4..23bd3a1 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,97 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# MLBoost Frontend -## Getting Started +Production-grade frontend for the MLBoost coding practice platform. -First, run the development server: +## Highlights + +- LeetCode-inspired UI shell and problem workflow +- Auth-gated app routes via middleware +- Problemset filters, search, company insights, and status tracking +- Coding arena with Monaco editor, run/submit console, history replay, and editorial unlock +- Profile analytics, contest dashboard, explore/learning experiences +- Mock-first API layer with live-backend switch support +- Sentry + web-vitals instrumentation hooks + +## Tech Stack + +- Next.js 16 (App Router) +- React 19 + TypeScript +- Tailwind CSS 4 +- Vitest + Testing Library +- Playwright + +## Quick Start ```bash +npm ci npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +Open [http://localhost:3000/login](http://localhost:3000/login) + +## Scripts + +- `npm run dev` - start dev server +- `npm run lint` - run ESLint +- `npm run test:unit` - run Vitest tests +- `npm run test:e2e` - run Playwright tests +- `npm run build` - production build +- `npm run start` - start built app + +## Environment Variables + +Create `.env.local` as needed: -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +```bash +# API +NEXT_PUBLIC_API_MODE=mock # mock | live | auto +NEXT_PUBLIC_API_URL=http://localhost:5001/api +NEXT_PUBLIC_API_RETRY_COUNT=2 +NEXT_PUBLIC_API_TIMEOUT_MS=8000 +NEXT_PUBLIC_API_FALLBACK_TO_MOCK=true + +# Analytics +NEXT_PUBLIC_ANALYTICS_ENDPOINT= + +# App environment +NEXT_PUBLIC_APP_ENV=development + +# Sentry (browser) +NEXT_PUBLIC_SENTRY_DSN= +NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE=0.1 -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. +# Sentry (server/edge) +SENTRY_DSN= +SENTRY_TRACES_SAMPLE_RATE=0.1 +``` + +## Live API Contract (expected paths) -## Learn More +- `POST /auth/login` +- `POST /auth/signup` +- `GET /problems` +- `GET /problems/:slug` +- `POST /submissions/run` +- `POST /submissions` +- `GET /tracks` +- `GET /profile/me` -To learn more about Next.js, take a look at the following resources: +## Testing Notes -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +- Unit tests use JSDOM + local storage mocks from `src/test/setup.ts`. +- E2E tests require Playwright browsers: -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! +```bash +npx playwright install chromium +``` -## Deploy on Vercel +## CI -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. +GitHub Actions pipeline runs: -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +1. `npm ci` +2. `npm run lint` +3. `npm run test:unit` +4. `npm run build` +5. `npx playwright install --with-deps chromium` +6. `npm run test:e2e` diff --git a/e2e/auth-flow.spec.ts b/e2e/auth-flow.spec.ts index 488ed74..e60e5c8 100644 --- a/e2e/auth-flow.spec.ts +++ b/e2e/auth-flow.spec.ts @@ -3,12 +3,12 @@ import { expect, test } from "@playwright/test"; test("user can login and open problems page", async ({ page }) => { await page.goto("/login"); - await expect(page.getByRole("heading", { name: /sign in to continue/i })).toBeVisible(); + await expect(page.getByRole("heading", { name: /mlboost/i })).toBeVisible(); await page.getByLabel("Email").fill("playwright@example.com"); await page.getByLabel("Password").fill("password123"); - await page.getByRole("button", { name: "Sign in" }).click(); + await page.getByTestId("auth-submit").click(); await expect(page).toHaveURL(/\/problems/); - await expect(page.getByRole("heading", { name: /^problems$/i })).toBeVisible(); + await expect(page.getByRole("heading", { name: /^problemset$/i })).toBeVisible(); }); diff --git a/e2e/navigation-pages.spec.ts b/e2e/navigation-pages.spec.ts index b5d2f0b..f653c7b 100644 --- a/e2e/navigation-pages.spec.ts +++ b/e2e/navigation-pages.spec.ts @@ -5,22 +5,21 @@ test("all primary pages load and are not blank", async ({ page }) => { await page.getByLabel("Email").fill("navigation@example.com"); await page.getByLabel("Password").fill("password123"); - await page.getByRole("button", { name: "Sign in" }).click(); + await page.getByTestId("auth-submit").click(); const checks = [ - { link: "Dashboard", pathSuffix: "/", heading: /^dashboard$/i }, - { link: "Problems", pathSuffix: "/problems", heading: /^problems$/i }, + { link: "Problems", pathSuffix: "/problems", heading: /^problemset$/i }, { - link: "Competitions", + link: "Contest", pathSuffix: "/competitions", - heading: /^competitions$/i, + heading: /^contest$/i, }, - { link: "Learn", pathSuffix: "/learn", heading: /^learn$/i }, - { link: "Progress", pathSuffix: "/progress", heading: /^progress$/i }, + { link: "Explore", pathSuffix: "/learn", heading: /^explore$/i }, + { link: "Discuss", pathSuffix: "/progress", heading: /^progress$/i }, ]; for (const item of checks) { - await page.getByRole("link", { name: item.link }).click(); + await page.getByRole("link", { name: new RegExp(`^${item.link}$`, "i") }).click(); await expect(page).toHaveURL(new RegExp(`${item.pathSuffix}$`)); await expect(page.getByRole("heading", { name: item.heading })).toBeVisible(); } @@ -31,9 +30,9 @@ test("problem arena route opens from problems list", async ({ page }) => { await page.getByLabel("Email").fill("arena@example.com"); await page.getByLabel("Password").fill("password123"); - await page.getByRole("button", { name: "Sign in" }).click(); + await page.getByTestId("auth-submit").click(); - await page.getByRole("link", { name: "Problems" }).click(); + await page.getByRole("link", { name: /^problems$/i }).click(); await page.getByText("KNN Classifier on Iris", { exact: false }).first().click(); await expect(page).toHaveURL(/\/problems\/knn-classifier-iris/); @@ -42,16 +41,16 @@ test("problem arena route opens from problems list", async ({ page }) => { ).toBeVisible(); }); -test("topics in sidebar navigate to filtered problems", async ({ page }) => { +test("category pills filter problems in problemset view", async ({ page }) => { await page.goto("/login"); await page.getByLabel("Email").fill("topics@example.com"); await page.getByLabel("Password").fill("password123"); - await page.getByRole("button", { name: "Sign in" }).click(); + await page.getByTestId("auth-submit").click(); - await page.getByRole("link", { name: "Learn" }).click(); + await page.getByRole("link", { name: /^problems$/i }).click(); await page.getByRole("button", { name: "Data Preprocessing" }).click(); - await expect(page).toHaveURL(/\/problems\?category=Data\+Preprocessing$/); - await expect(page.getByRole("heading", { name: /^problems$/i })).toBeVisible(); + await expect(page.getByRole("heading", { name: /^problemset$/i })).toBeVisible(); + await expect(page.getByText("Data Preprocessing", { exact: false })).toBeVisible(); }); diff --git a/middleware.ts b/middleware.ts index 513c1ff..cd7f139 100644 --- a/middleware.ts +++ b/middleware.ts @@ -2,7 +2,17 @@ import { NextRequest, NextResponse } from "next/server"; import { AUTH_COOKIE_NAME } from "./src/lib/auth"; function isAuthenticated(request: NextRequest): boolean { - return request.cookies.has(AUTH_COOKIE_NAME); + const cookieValue = request.cookies.get(AUTH_COOKIE_NAME)?.value; + if (!cookieValue) { + return false; + } + + const expiresAtMs = Number(cookieValue); + if (!Number.isFinite(expiresAtMs)) { + return true; + } + + return expiresAtMs > Date.now(); } export function middleware(request: NextRequest) { diff --git a/src/app/competitions/page.tsx b/src/app/competitions/page.tsx index 5aaf66c..d9e7576 100644 --- a/src/app/competitions/page.tsx +++ b/src/app/competitions/page.tsx @@ -1,97 +1,78 @@ "use client"; import MainLayout from "../components/MainLayout"; +import { AlarmClockCheck, Shuffle, Trophy } from "lucide-react"; const UPCOMING = [ { - id: "c1", - title: "Regression Sprint #12", - startsIn: "2h 15m", - participants: 1240, - duration: "90 min", - difficulty: "Medium", + id: "weekly", + title: "Weekly Contest 492", + startsAt: "Sun, Mar 8, 08:00 GMT+05:30", + countdown: "2d 08:44:53", + gradient: "from-amber-300/90 via-orange-400/90 to-amber-700/70", }, { - id: "c2", - title: "Feature Engineering Arena", - startsIn: "Tomorrow", - participants: 980, - duration: "120 min", - difficulty: "Hard", - }, - { - id: "c3", - title: "Pandas Speed Round", - startsIn: "3 days", - participants: 2100, - duration: "60 min", - difficulty: "Easy", + id: "biweekly", + title: "Biweekly Contest 178", + startsAt: "Sat, Mar 14, 20:00 GMT+05:30", + countdown: "8d 20:44:53", + gradient: "from-indigo-500/90 via-violet-500/90 to-indigo-900/70", }, ]; -const LEADERBOARD = [ - { rank: 1, name: "Aarav", score: 1980 }, - { rank: 2, name: "Emily", score: 1945 }, - { rank: 3, name: "Noah", score: 1910 }, - { rank: 4, name: "Saanvi", score: 1890 }, -]; - export default function CompetitionsPage() { return ( - -
-

Weekly Event

-

Model Metrics Championship

-

- Solve 5 evaluation-heavy challenges under 75 minutes. -

-
- -
-
-

- Upcoming Contests -

-
- {UPCOMING.map((contest) => ( -
-
-

{contest.title}

-

- Starts in {contest.startsIn} -

-
-

{contest.participants} participants

-

{contest.duration}

-

{contest.difficulty}

-
- ))} + +
+
+
+
+
+

+ MLBoost Contest +

+

+ Weekly and biweekly battles for machine learning interview mastery. +

-
-

- Leaderboard Snapshot -

-
- {LEADERBOARD.map((entry) => ( -
- #{entry.rank} {entry.name} - {entry.score} +
+ {UPCOMING.map((contest) => ( +
+
+

+ + {contest.countdown} +

+
- ))} -
+
+

{contest.title}

+

{contest.startsAt}

+
+ + ))}
+ +
+ + + + +
); } diff --git a/src/app/components/MainLayout.tsx b/src/app/components/MainLayout.tsx index 1076aca..5f0c3ad 100644 --- a/src/app/components/MainLayout.tsx +++ b/src/app/components/MainLayout.tsx @@ -1,9 +1,6 @@ "use client"; -import { useState } from "react"; import { useRouter } from "next/navigation"; -import { Menu } from "lucide-react"; -import Sidebar from "./Sidebar"; import Navbar from "./Navbar"; import { useAuth } from "@/context/AuthContext"; @@ -21,12 +18,13 @@ export default function MainLayout({ subtitle, children, headerSlot, - selectedCategory, - onCategoryChange, + selectedCategory: _selectedCategory, + onCategoryChange: _onCategoryChange, }: MainLayoutProps) { + void _selectedCategory; + void _onCategoryChange; const router = useRouter(); const { logout } = useAuth(); - const [isSidebarOpen, setIsSidebarOpen] = useState(true); const handleLogout = async () => { await logout(); @@ -34,43 +32,14 @@ export default function MainLayout({ }; return ( -
- setIsSidebarOpen(false)} - /> - - {!isSidebarOpen && ( - - )} - -
- - -
-
- {headerSlot} - {children} -
-
-
+
+ +
+
+ {headerSlot} + {children} +
+
); } diff --git a/src/app/components/Navbar.tsx b/src/app/components/Navbar.tsx index e0f8990..56b8528 100644 --- a/src/app/components/Navbar.tsx +++ b/src/app/components/Navbar.tsx @@ -1,28 +1,40 @@ "use client"; import { useMemo } from "react"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { Bell, ChevronDown, Flame, Search } from "lucide-react"; import { useAuth } from "@/context/AuthContext"; -import ThemeSwitcher from "./ThemeSwitcher"; interface NavbarProps { title?: string; subtitle?: string; onLogout?: () => void; - isSidebarOpen?: boolean; } export default function Navbar({ title, subtitle, onLogout, - isSidebarOpen = true, }: NavbarProps) { + const pathname = usePathname(); const { user, logout } = useAuth(); const avatarInitial = useMemo( () => user?.name?.trim().charAt(0).toUpperCase() || "U", [user] ); + const navItems = useMemo( + () => [ + { label: "Explore", href: "/learn", activeWhen: /^\/learn$/ }, + { label: "Problems", href: "/problems", activeWhen: /^\/problems/ }, + { label: "Contest", href: "/competitions", activeWhen: /^\/competitions/ }, + { label: "Discuss", href: "/progress", activeWhen: /^\/progress/ }, + { label: "Interview", href: "/tracks", activeWhen: /^\/tracks/ }, + ], + [] + ); + const handleLogout = async () => { if (onLogout) { onLogout(); @@ -32,38 +44,89 @@ export default function Navbar({ }; return ( - +
+
+ + +
+ + +
+ {avatarInitial} +
+ + +
+ + {(title || subtitle) && ( +
+
+ {title ? ( +

+ {title} +

+ ) : null} + {subtitle ? ( +

{subtitle}

+ ) : null} +
+
+ )} + ); } diff --git a/src/app/globals.css b/src/app/globals.css index 1d9e790..7d50351 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2,7 +2,13 @@ @custom-variant dark (&:where(.dark, .dark *)); :root { - color-scheme: light; + color-scheme: dark; + --app-bg: #0f1117; + --app-surface: #1a1d24; + --app-surface-2: #232832; + --app-text: #e4e7ee; + --app-muted: #97a0af; + --app-accent: #f59e0b; } .dark { @@ -21,11 +27,12 @@ body { body { margin: 0; - background-color: #f5f6f8; - color: #111827; + background-color: var(--app-bg); + color: var(--app-text); + text-rendering: optimizeLegibility; } -.dark body { - background-color: #0a0a0a; - color: #f3f4f6; +::selection { + background: #f59e0b; + color: #111827; } diff --git a/src/app/learn/page.tsx b/src/app/learn/page.tsx index 24c1049..a6d8e97 100644 --- a/src/app/learn/page.tsx +++ b/src/app/learn/page.tsx @@ -1,85 +1,90 @@ "use client"; +import { Clock3, PlayCircle, Star } from "lucide-react"; import MainLayout from "../components/MainLayout"; -const TRACKS = [ +const FEATURED = [ { - id: "t1", - title: "ML Foundations", - description: "Linear models, overfitting, feature scaling, and evaluation.", - lessons: 18, - progress: 72, + title: "Data Structures and Algorithms", + chapters: 13, + items: 149, + gradient: "from-indigo-500 to-fuchsia-500", }, { - id: "t2", - title: "Pandas for Interviews", - description: "Joins, groupby, window operations, and common data transforms.", - lessons: 14, - progress: 38, + title: "System Design for Interviews", + chapters: 10, + items: 104, + gradient: "from-emerald-600 to-teal-700", }, { - id: "t3", - title: "Model Selection", - description: "Cross-validation, hyperparameter tuning, and proper split strategy.", - lessons: 11, - progress: 12, + title: "ML Beginner's Guide", + chapters: 9, + items: 72, + gradient: "from-amber-400 to-rose-400", + }, + { + title: "Top Interview Questions", + chapters: 11, + items: 130, + gradient: "from-cyan-700 to-emerald-800", }, -]; - -const MODULES = [ - "Bias-Variance Tradeoff", - "Confusion Matrix and F1", - "Regularization in Practice", - "Feature Drift Detection", ]; export default function LearnPage() { return ( - -
-
-

- Learning Tracks -

-
- {TRACKS.map((track) => ( -
-
-
-

{track.title}

-

{track.description}

-
- {track.lessons} lessons -
-
-
-
-
- ))} + +
+

Welcome to

+
+

MLBoost Explore

+
+ +
- +
+
+
+

Continue Previous

+

Data Structures and Algorithms

+
+
+
+

13

+

Chapters

+
+
+

149

+

Items

+
+
+ +

0%

+
+
+
+ +
+

Featured

+
+ {FEATURED.map((course) => ( +
+

Interview Crash Course

+

{course.title}

+

+ {course.chapters} chapters · {course.items} lessons +

+
+ ))} +
+
+
); diff --git a/src/app/login/page.test.tsx b/src/app/login/page.test.tsx index 600d5b3..e182e36 100644 --- a/src/app/login/page.test.tsx +++ b/src/app/login/page.test.tsx @@ -38,7 +38,7 @@ describe("LoginPage", () => { target: { value: "password123" }, }); - fireEvent.click(screen.getByRole("button", { name: "Sign in" })); + fireEvent.click(screen.getByTestId("auth-submit")); await waitFor(() => { expect(loginMock).toHaveBeenCalledWith({ @@ -53,7 +53,7 @@ describe("LoginPage", () => { it("submits signup payload", async () => { render(); - fireEvent.click(screen.getByRole("button", { name: "Sign up" })); + fireEvent.click(screen.getByTestId("auth-tab-signup")); fireEvent.change(screen.getByLabelText(/name/i), { target: { value: "Ada" }, @@ -65,7 +65,7 @@ describe("LoginPage", () => { target: { value: "password123" }, }); - fireEvent.click(screen.getByRole("button", { name: "Create account" })); + fireEvent.click(screen.getByTestId("auth-submit")); await waitFor(() => { expect(signupMock).toHaveBeenCalledWith({ diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 0e9e628..ebb54ca 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,10 +1,10 @@ "use client"; import { FormEvent, Suspense, useMemo, useState } from "react"; +import Link from "next/link"; import { useRouter, useSearchParams } from "next/navigation"; import { Loader2 } from "lucide-react"; import { useAuth } from "@/context/AuthContext"; -import ThemeSwitcher from "@/app/components/ThemeSwitcher"; type AuthMode = "login" | "signup"; @@ -22,7 +22,6 @@ function LoginForm() { const router = useRouter(); const searchParams = useSearchParams(); const { login, signup, isLoading } = useAuth(); - const [mode, setMode] = useState("login"); const [name, setName] = useState(""); const [email, setEmail] = useState(""); @@ -46,6 +45,7 @@ function LoginForm() { setErrorMessage("Email and password are required."); return; } + if (mode === "signup" && !trimmedName) { setErrorMessage("Name is required for signup."); return; @@ -70,110 +70,128 @@ function LoginForm() { }; return ( -
-
- -
-
-
-

- MLBoost -

-

- {mode === "login" ? "Sign in to continue" : "Create your account"} -

-

- Practice ML coding with instant feedback. -

-
- -
- - + + + + + +
+
- -
- {mode === "signup" && ( + + +
+
+
+
+ ⌂ +
+

MLBoost

+
+ +
+ + +
+ + + {mode === "signup" ? ( + + ) : null} - )} - - - - - - {errorMessage && ( -

- {errorMessage} -

- )} - - - -
- + + + {errorMessage ? ( +

+ {errorMessage} +

+ ) : null} + + + + +
+ + Forgot Password? + + +
+
+ +
); } @@ -181,7 +199,7 @@ export default function LoginPage() { return ( +
Loading authentication...
} diff --git a/src/app/problems/page.tsx b/src/app/problems/page.tsx index e75563b..bff9b5b 100644 --- a/src/app/problems/page.tsx +++ b/src/app/problems/page.tsx @@ -2,18 +2,57 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { useRouter, useSearchParams } from "next/navigation"; +import { + Bookmark, + BriefcaseBusiness, + CheckCircle2, + Circle, + Dot, + Filter, + Flame, + NotebookTabs, + Search, + SlidersHorizontal, + Star, + Trophy, +} from "lucide-react"; import MainLayout from "../components/MainLayout"; -import ProblemsTable from "../components/ProblemsTable"; -import SearchBar from "../components/SearchBar"; import { FilterState, Problem } from "@/types"; import { fetchProblems } from "@/lib/api"; +const LEFT_ITEMS = [ + { label: "Library", icon: NotebookTabs }, + { label: "Quest", icon: Trophy, badge: "New" }, + { label: "Study Plan", icon: Flame }, +]; + +const LIST_ITEMS = ["Favorites", "Top 150", "Interview Prep"]; + +function statusIcon(status: Problem["status"]) { + if (status === "solved") { + return ; + } + if (status === "attempted") { + return ; + } + return ; +} + +function difficultyClass(difficulty: Problem["difficulty"]) { + if (difficulty === "Easy") { + return "text-emerald-400"; + } + if (difficulty === "Medium") { + return "text-amber-300"; + } + return "text-rose-400"; +} + export default function ProblemsPage() { const router = useRouter(); const searchParams = useSearchParams(); const [problems, setProblems] = useState([]); const [isLoading, setIsLoading] = useState(true); - const [selectedCategory, setSelectedCategory] = useState(""); const [searchQuery, setSearchQuery] = useState(""); const searchInputRef = useRef(null); const [filters, setFilters] = useState({ @@ -32,20 +71,18 @@ export default function ProblemsPage() { setIsLoading(false); } }; - void load(); }, []); useEffect(() => { const categoryParam = searchParams.get("category"); - const category = categoryParam?.trim() || "All Categories"; - setSelectedCategory(category); - setFilters((current) => { - if (current.category === category) { - return current; - } - return { ...current, category }; - }); + if (!categoryParam) { + return; + } + setFilters((current) => ({ + ...current, + category: categoryParam, + })); }, [searchParams]); useEffect(() => { @@ -55,19 +92,35 @@ export default function ProblemsPage() { target?.tagName === "INPUT" || target?.tagName === "TEXTAREA" || target?.isContentEditable; - if (event.key === "/" && !isTextInput) { event.preventDefault(); searchInputRef.current?.focus(); } }; - window.addEventListener("keydown", handleShortcut); - return () => { - window.removeEventListener("keydown", handleShortcut); - }; + return () => window.removeEventListener("keydown", handleShortcut); }, []); + const categories = useMemo(() => { + const counts = new Map(); + for (const problem of problems) { + counts.set(problem.category, (counts.get(problem.category) || 0) + 1); + } + return Array.from(counts.entries()); + }, [problems]); + + const companyCounts = useMemo(() => { + const counts = new Map(); + for (const problem of problems) { + for (const company of problem.companies || []) { + counts.set(company, (counts.get(company) || 0) + 1); + } + } + return Array.from(counts.entries()) + .sort((a, b) => b[1] - a[1]) + .slice(0, 10); + }, [problems]); + const filteredProblems = useMemo(() => { let list = [...problems]; @@ -88,8 +141,8 @@ export default function ProblemsPage() { list = list.filter( (problem) => problem.title.toLowerCase().includes(query) || - problem.tags.some((tag) => tag.toLowerCase().includes(query)) || - problem.category.toLowerCase().includes(query) + problem.category.toLowerCase().includes(query) || + problem.tags.some((tag) => tag.toLowerCase().includes(query)) ); } @@ -97,125 +150,255 @@ export default function ProblemsPage() { }, [problems, filters, searchQuery]); const solvedCount = problems.filter((problem) => problem.status === "solved").length; - const mediumCount = problems.filter( - (problem) => problem.difficulty === "Medium" - ).length; - - const headerSlot = ( -
-
-

- Total Problems -

-

- {problems.length} -

-
-
-

- Solved -

-

- {solvedCount} -

-
-
-

- Medium Challenges -

-

- {mediumCount} -

-
-
- ); return ( { - setSelectedCategory(category); - setFilters((current) => ({ ...current, category })); - }} + title="Problemset" + subtitle="Train on curated ML, data science, and interview coding questions." > -
-
- - - - - - - -
-

- Shortcut: press / to focus search. -

-
+
+ + +
+
+
+

Offer Track

+

ML Offer Campaign

+

60-day guided sprint.

+
+
+

Daily Focus

+

JavaScript Day 30

+

Short challenge for momentum.

+
+
+

Interview Prep

+

Top Questions

+

Frequently asked rounds.

+
+
+ +
+ {categories.map(([category, count]) => ( + + ))} +
+ +
+
+ + + + +
+ +
+ + + + + +
+ +
+ {solvedCount}/{problems.length} solved + Shortcut: press / to focus search +
+ +
+ + + + + + + + + + + + {isLoading ? ( + + + + ) : filteredProblems.length === 0 ? ( + + + + ) : ( + filteredProblems.map((problem) => ( + problem.slug && router.push(`/problems/${problem.slug}`)} + className="cursor-pointer border-t border-zinc-800/80 bg-zinc-950/70 text-sm text-zinc-200 transition hover:bg-zinc-900" + > + + + + + + + )) + )} + +
StatusTitleAcceptanceDifficultyStar
+ Loading problemset... +
+ No problems matched your filters. +
{statusIcon(problem.status)}{problem.title}{problem.acceptanceRate}% + {problem.difficulty} + + +
+
+
+
+ +
); diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx index 20d31fe..757ee03 100644 --- a/src/app/profile/page.tsx +++ b/src/app/profile/page.tsx @@ -1,6 +1,16 @@ "use client"; import { useEffect, useMemo, useState } from "react"; +import { + ExternalLink, + Eye, + Github, + GraduationCap, + HeartHandshake, + Linkedin, + MapPin, + Trophy, +} from "lucide-react"; import MainLayout from "../components/MainLayout"; import { fetchUserProfile } from "@/lib/api"; import { UserProfile } from "@/types"; @@ -13,9 +23,9 @@ function heatLevelClass(count: number): string { return "bg-emerald-400"; } if (count >= 1) { - return "bg-emerald-300"; + return "bg-emerald-300/80"; } - return "bg-zinc-200 dark:bg-zinc-800"; + return "bg-zinc-800"; } export default function ProfilePage() { @@ -32,7 +42,6 @@ export default function ProfilePage() { setIsLoading(false); } }; - void load(); }, []); @@ -40,142 +49,201 @@ export default function ProfilePage() { if (!profile) { return [] as UserProfile["heatmap"][]; } - const cells = [...profile.heatmap]; const weeks: UserProfile["heatmap"][] = []; - while (cells.length > 0) { weeks.push(cells.splice(0, 7)); } - - return weeks.slice(-18); + return weeks.slice(-22); }, [profile]); return ( - + {isLoading || !profile ? ( -
+
Loading profile...
) : ( - <> -
-
-

Solved

-

{profile.totalSolved}

-
-
-

Acceptance

-

{profile.acceptanceRate}%

+
+
+ -
-

- Submission Heatmap -

-
-
- {heatmapWeeks.map((week, index) => ( -
- {week.map((cell) => ( +
+
+
+
+
+

+ Contest Rating +

+

+ {1380 + profile.totalSolved} +

+
+
+

Global Rank

+

+ {Math.max(100, 740000 - profile.totalSolved * 9).toLocaleString()} +

+
+
+

Attended

+

5

+
+
+

Top

+

87.29%

+
+
+
+
+ {[58, 44, 67, 51, 39].map((value) => (
))}
- ))} -
-
-
+
+
+ +
+

Solved

+

+ {profile.totalSolved}/3860 +

+
+
+ Easy + 238/929 +
+
+ Medium + 103/2019 +
+
+ Hard + 9/912 +
+
+
+
-
-
-

- Solved by Topic -

-
- {profile.topicProgress.map((topic) => { - const pct = Math.round((topic.solved / topic.total) * 100); - return ( -
-
- {topic.topic} - {topic.solved}/{topic.total} -
-
-
-
+
+
+

+ {profile.heatmap.filter((cell) => cell.count > 0).length} submissions in past year +

+

Current streak: {profile.streakDays}

+
+
+
+ {heatmapWeeks.map((week, index) => ( +
+ {week.map((cell) => ( +
+ ))}
- ); - })} + ))} +
-
-

- Acceptance Trend -

-
- {profile.acceptanceTrend.map((point) => ( -
-
-
-
-

{point.label.slice(5)}

-

{point.acceptance}%

+
+
+ {["Recent AC", "List", "Solutions", "Discuss"].map((item) => ( + + ))} +
+
+ {[ + "Validate Binary Search Tree", + "Maximum 69 Number", + "Median of Two Sorted Arrays", + "KNN Classifier on Iris", + ].map((title, index) => ( +
+

{title}

+

{index + 1} months ago

))}
-
- -
-

- Recent Contest Ranks -

-
- - - - - - - - - - - - {profile.recentContestRanks.map((row) => ( - - - - - - - - ))} - -
ContestRankParticipantsScoreDate
{row.contest}#{row.rank}{row.participants}{row.score}{row.date}
-
-
- +
+
)}
); diff --git a/src/lib/api.ts b/src/lib/api.ts index e5abe8b..379af64 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -468,36 +468,80 @@ export function getBackendMode(): ApiMode { return getNormalizedApiMode(); } +function shouldUseSecureCookie(): boolean { + return isBrowser() && window.location.protocol === "https:"; +} + +function buildCookieAttributes(maxAgeSeconds: number): string { + const attrs = ["path=/", `max-age=${maxAgeSeconds}`, "samesite=lax"]; + if (shouldUseSecureCookie()) { + attrs.push("secure"); + } + return attrs.join("; "); +} + +function getAccessToken(): string | null { + if (!isBrowser()) { + return null; + } + const token = window.localStorage.getItem(ACCESS_TOKEN_KEY); + if (!token) { + return null; + } + const trimmed = token.trim(); + return trimmed || null; +} + function persistSession(session: AuthSession): void { if (!isBrowser()) { return; } + + const expiresAtMs = new Date(session.expiresAt).getTime(); + const cookieValue = Number.isFinite(expiresAtMs) + ? String(expiresAtMs) + : String(Date.now() + AUTH_COOKIE_TTL_SECONDS * 1000); + window.localStorage.setItem(MOCK_SESSION_KEY, JSON.stringify(session)); window.localStorage.setItem(ACCESS_TOKEN_KEY, session.accessToken); - window.document.cookie = `${AUTH_COOKIE_NAME}=1; path=/; max-age=${AUTH_COOKIE_TTL_SECONDS}; samesite=lax`; + window.document.cookie = `${AUTH_COOKIE_NAME}=${cookieValue}; ${buildCookieAttributes( + AUTH_COOKIE_TTL_SECONDS + )}`; } function readStoredSession(): AuthSession | null { if (!isBrowser()) { return null; } - const hasAuthCookie = window.document.cookie + const authCookie = window.document.cookie .split(";") - .some((part) => part.trim().startsWith(`${AUTH_COOKIE_NAME}=`)); + .find((part) => part.trim().startsWith(`${AUTH_COOKIE_NAME}=`)); - if (!hasAuthCookie) { + if (!authCookie) { window.localStorage.removeItem(MOCK_SESSION_KEY); window.localStorage.removeItem(ACCESS_TOKEN_KEY); return null; } + const cookieValue = authCookie.split("=")[1]; + const cookieExpiryMs = Number(cookieValue); + if (Number.isFinite(cookieExpiryMs) && cookieExpiryMs < Date.now()) { + clearStoredSession(); + return null; + } + const raw = window.localStorage.getItem(MOCK_SESSION_KEY); if (!raw) { return null; } try { - return JSON.parse(raw) as AuthSession; + const parsed = JSON.parse(raw) as AuthSession; + if (new Date(parsed.expiresAt).getTime() <= Date.now()) { + clearStoredSession(); + return null; + } + return parsed; } catch { window.localStorage.removeItem(MOCK_SESSION_KEY); return null; @@ -510,7 +554,7 @@ function clearStoredSession(): void { } window.localStorage.removeItem(MOCK_SESSION_KEY); window.localStorage.removeItem(ACCESS_TOKEN_KEY); - window.document.cookie = `${AUTH_COOKIE_NAME}=; path=/; max-age=0; samesite=lax`; + window.document.cookie = `${AUTH_COOKIE_NAME}=; ${buildCookieAttributes(0)}`; } function createMockSession(name: string, email: string): AuthSession { @@ -844,11 +888,14 @@ async function fetchWithRetry( const timeoutId = setTimeout(() => controller.abort(), API_TIMEOUT_MS); try { + const token = getAccessToken(); const response = await fetch(`${API_BASE_URL}${path}`, { ...init, signal: controller.signal, + credentials: "include", headers: { "Content-Type": "application/json", + ...(token ? { Authorization: `Bearer ${token}` } : {}), ...(init?.headers || {}), }, }); diff --git a/src/test/setup.ts b/src/test/setup.ts index fcef802..a3b7bb5 100644 --- a/src/test/setup.ts +++ b/src/test/setup.ts @@ -6,6 +6,41 @@ afterEach(() => { cleanup(); }); +function createStorageMock(): Storage { + let store: Record = {}; + + return { + get length() { + return Object.keys(store).length; + }, + clear() { + store = {}; + }, + getItem(key: string) { + return Object.prototype.hasOwnProperty.call(store, key) ? store[key] : null; + }, + key(index: number) { + return Object.keys(store)[index] ?? null; + }, + removeItem(key: string) { + delete store[key]; + }, + setItem(key: string, value: string) { + store[key] = String(value); + }, + }; +} + +Object.defineProperty(window, "localStorage", { + writable: true, + value: createStorageMock(), +}); + +Object.defineProperty(window, "sessionStorage", { + writable: true, + value: createStorageMock(), +}); + Object.defineProperty(window, "matchMedia", { writable: true, value: vi.fn().mockImplementation((query: string) => ({ From 3b0422c40a58d5de482b6b8c4c0176c7cdf52168 Mon Sep 17 00:00:00 2001 From: Birajit Saikia Date: Thu, 5 Mar 2026 23:54:20 +0530 Subject: [PATCH 2/5] chore: retrigger vercel preview From 8aed663cce28e066ae5a585d679ee93c70e43e58 Mon Sep 17 00:00:00 2001 From: Birajit Saikia Date: Fri, 6 Mar 2026 00:08:22 +0530 Subject: [PATCH 3/5] fix: upgrade next to patched version for vercel security gate --- package-lock.json | 230 ++++++++++++++++++++-------------------------- package.json | 4 +- 2 files changed, 100 insertions(+), 134 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0e317d1..55bb178 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.576.0", - "next": "16.0.1", + "next": "^16.1.6", "next-themes": "^0.4.6", "react": "19.2.0", "react-dom": "19.2.0", @@ -30,7 +30,7 @@ "@types/react-dom": "^19", "babel-plugin-react-compiler": "1.0.0", "eslint": "^9", - "eslint-config-next": "16.0.1", + "eslint-config-next": "^16.1.6", "jsdom": "^28.1.0", "tailwindcss": "^4", "typescript": "^5", @@ -150,6 +150,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -457,6 +458,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" }, @@ -497,6 +499,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" } @@ -1742,7 +1745,6 @@ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -1801,15 +1803,15 @@ } }, "node_modules/@next/env": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.1.tgz", - "integrity": "sha512-LFvlK0TG2L3fEOX77OC35KowL8D7DlFF45C0OvKMC4hy8c/md1RC4UMNDlUGJqfCoCS2VWrZ4dSE6OjaX5+8mw==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.6.tgz", + "integrity": "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.0.1.tgz", - "integrity": "sha512-g4Cqmv/gyFEXNeVB2HkqDlYKfy+YrlM2k8AVIO/YQVEPfhVruH1VA99uT1zELLnPLIeOnx8IZ6Ddso0asfTIdw==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.1.6.tgz", + "integrity": "sha512-/Qq3PTagA6+nYVfryAtQ7/9FEr/6YVyvOtl6rZnGsbReGLf0jZU6gkpr1FuChAQpvV46a78p4cmHOVP8mbfSMQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1817,9 +1819,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.1.tgz", - "integrity": "sha512-R0YxRp6/4W7yG1nKbfu41bp3d96a0EalonQXiMe+1H9GTHfKxGNCGFNWUho18avRBPsO8T3RmdWuzmfurlQPbg==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.6.tgz", + "integrity": "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==", "cpu": [ "arm64" ], @@ -1833,9 +1835,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.1.tgz", - "integrity": "sha512-kETZBocRux3xITiZtOtVoVvXyQLB7VBxN7L6EPqgI5paZiUlnsgYv4q8diTNYeHmF9EiehydOBo20lTttCbHAg==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.6.tgz", + "integrity": "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==", "cpu": [ "x64" ], @@ -1849,9 +1851,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.1.tgz", - "integrity": "sha512-hWg3BtsxQuSKhfe0LunJoqxjO4NEpBmKkE+P2Sroos7yB//OOX3jD5ISP2wv8QdUwtRehMdwYz6VB50mY6hqAg==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.6.tgz", + "integrity": "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==", "cpu": [ "arm64" ], @@ -1865,9 +1867,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.1.tgz", - "integrity": "sha512-UPnOvYg+fjAhP3b1iQStcYPWeBFRLrugEyK/lDKGk7kLNua8t5/DvDbAEFotfV1YfcOY6bru76qN9qnjLoyHCQ==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.6.tgz", + "integrity": "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==", "cpu": [ "arm64" ], @@ -1881,9 +1883,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.1.tgz", - "integrity": "sha512-Et81SdWkcRqAJziIgFtsFyJizHoWne4fzJkvjd6V4wEkWTB4MX6J0uByUb0peiJQ4WeAt6GGmMszE5KrXK6WKg==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.6.tgz", + "integrity": "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==", "cpu": [ "x64" ], @@ -1897,9 +1899,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.1.tgz", - "integrity": "sha512-qBbgYEBRrC1egcG03FZaVfVxrJm8wBl7vr8UFKplnxNRprctdP26xEv9nJ07Ggq4y1adwa0nz2mz83CELY7N6Q==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz", + "integrity": "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==", "cpu": [ "x64" ], @@ -1913,9 +1915,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.1.tgz", - "integrity": "sha512-cPuBjYP6I699/RdbHJonb3BiRNEDm5CKEBuJ6SD8k3oLam2fDRMKAvmrli4QMDgT2ixyRJ0+DTkiODbIQhRkeQ==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.6.tgz", + "integrity": "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==", "cpu": [ "arm64" ], @@ -1929,9 +1931,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.1.tgz", - "integrity": "sha512-XeEUJsE4JYtfrXe/LaJn3z1pD19fK0Q6Er8Qoufi+HqvdO4LEPyCxLUt4rxA+4RfYo6S9gMlmzCMU2F+AatFqQ==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.6.tgz", + "integrity": "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==", "cpu": [ "x64" ], @@ -1997,6 +1999,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -2018,6 +2021,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.5.1.tgz", "integrity": "sha512-MHbu8XxCHcBn6RwvCt2Vpn1WnLMNECfNKYB14LI5XypcgH4IE0/DiVifVR9tAkwPMyLXN8dOoPJfya3IryLQVw==", "license": "Apache-2.0", + "peer": true, "engines": { "node": "^18.19.0 || >=20.6.0" }, @@ -2030,6 +2034,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.1.tgz", "integrity": "sha512-Dwlc+3HAZqpgTYq0MUyZABjFkcrKTePwuiFVLjahGD8cx3enqihmpAmdgNFO1R4m/sIe5afjJrA25Prqy4NXlA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, @@ -2453,6 +2458,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.1.tgz", "integrity": "sha512-BViBCdE/GuXRlp9k7nS1w6wJvY5fnFX5XvuEtWsTAOQFIO89Eru7lGW3WbfbxtCuZ/GbrJfAziXG0w0dpxL7eQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "2.5.1", "@opentelemetry/semantic-conventions": "^1.29.0" @@ -2469,6 +2475,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.5.1.tgz", "integrity": "sha512-iZH3Gw8cxQn0gjpOjJMmKLd9GIaNh/E3v3ST67vyzLSxHBs14HsG4dy7jMYyC5WXGdBVEcM7U/XTF5hCQxjDMw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "2.5.1", "@opentelemetry/resources": "2.5.1", @@ -2486,6 +2493,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz", "integrity": "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=14" } @@ -2511,6 +2519,7 @@ "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "playwright": "1.58.2" }, @@ -2616,6 +2625,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -3785,7 +3795,6 @@ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "dequal": "^2.0.3" } @@ -3875,8 +3884,7 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/chai": { "version": "5.2.3", @@ -3910,7 +3918,6 @@ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -3921,7 +3928,6 @@ "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", "license": "MIT", - "peer": true, "dependencies": { "@types/eslint": "*", "@types/estree": "*" @@ -3990,6 +3996,7 @@ "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -4000,6 +4007,7 @@ "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -4018,8 +4026,7 @@ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.46.2", @@ -4067,6 +4074,7 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -4707,7 +4715,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" @@ -4717,29 +4724,25 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/helper-buffer": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", @@ -4750,15 +4753,13 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -4771,7 +4772,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "license": "MIT", - "peer": true, "dependencies": { "@xtuc/ieee754": "^1.2.0" } @@ -4781,7 +4781,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@xtuc/long": "4.2.2" } @@ -4790,15 +4789,13 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/wasm-edit": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -4815,7 +4812,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", @@ -4829,7 +4825,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -4842,7 +4837,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", @@ -4857,7 +4851,6 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "license": "MIT", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" @@ -4867,21 +4860,20 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4903,7 +4895,6 @@ "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=10.13.0" }, @@ -4953,7 +4944,6 @@ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "license": "MIT", - "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -4971,7 +4961,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -4987,8 +4976,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/ansi-regex": { "version": "5.0.1", @@ -4996,7 +4984,6 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -5263,6 +5250,7 @@ "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/types": "^7.26.0" } @@ -5339,6 +5327,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -5357,8 +5346,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/call-bind": { "version": "1.0.8", @@ -5472,7 +5460,6 @@ "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=6.0" } @@ -5534,8 +5521,7 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/commondir": { "version": "1.0.1", @@ -5773,7 +5759,6 @@ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6" } @@ -5806,15 +5791,13 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/dompurify": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", "license": "(MPL-2.0 OR Apache-2.0)", - "peer": true, "optionalDependencies": { "@types/trusted-types": "^2.0.7" } @@ -6139,6 +6122,7 @@ "integrity": "sha512-iy2GE3MHrYTL5lrCtMZ0X1KLEKKUjmK0kzwcnefhR66txcEmXZD2YWgR5GNdcEwkNx3a0siYkSvl0vIC+Svjmg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6194,13 +6178,13 @@ } }, "node_modules/eslint-config-next": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.0.1.tgz", - "integrity": "sha512-wNuHw5gNOxwLUvpg0cu6IL0crrVC9hAwdS/7UwleNkwyaMiWIOAwf8yzXVqBBzL3c9A7jVRngJxjoSpPP1aEhg==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.1.6.tgz", + "integrity": "sha512-vKq40io2B0XtkkNDYyleATwblNt8xuh3FWp8SpSz3pt7P01OkBFlKsJZ2mWt5WsCySlDQLckb1zMY9yE9Qy0LA==", "dev": true, "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "16.0.1", + "@next/eslint-plugin-next": "16.1.6", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", @@ -6570,7 +6554,6 @@ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.8.x" } @@ -6649,8 +6632,7 @@ "url": "https://opencollective.com/fastify" } ], - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/fastq": { "version": "1.19.1", @@ -6925,8 +6907,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "license": "BSD-2-Clause", - "peer": true + "license": "BSD-2-Clause" }, "node_modules/glob/node_modules/balanced-match": { "version": "4.0.4", @@ -7712,7 +7693,6 @@ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -7727,7 +7707,6 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "license": "MIT", - "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -7773,6 +7752,7 @@ "integrity": "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@acemir/cssom": "^0.9.31", "@asamuzakjp/dom-selector": "^6.8.1", @@ -7831,8 +7811,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", @@ -8186,7 +8165,6 @@ "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=6.11.5" }, @@ -8254,7 +8232,6 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -8273,7 +8250,6 @@ "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", "license": "MIT", - "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -8302,8 +8278,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/merge2": { "version": "1.4.1", @@ -8334,7 +8309,6 @@ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -8344,7 +8318,6 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "license": "MIT", - "peer": true, "dependencies": { "mime-db": "1.52.0" }, @@ -8462,17 +8435,18 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/next": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/next/-/next-16.0.1.tgz", - "integrity": "sha512-e9RLSssZwd35p7/vOa+hoDFggUZIUbZhIUSLZuETCwrCVvxOs87NamoUzT+vbcNAL8Ld9GobBnWOA6SbV/arOw==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz", + "integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==", "license": "MIT", + "peer": true, "dependencies": { - "@next/env": "16.0.1", + "@next/env": "16.1.6", "@swc/helpers": "0.5.15", + "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" @@ -8484,14 +8458,14 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.0.1", - "@next/swc-darwin-x64": "16.0.1", - "@next/swc-linux-arm64-gnu": "16.0.1", - "@next/swc-linux-arm64-musl": "16.0.1", - "@next/swc-linux-x64-gnu": "16.0.1", - "@next/swc-linux-x64-musl": "16.0.1", - "@next/swc-win32-arm64-msvc": "16.0.1", - "@next/swc-win32-x64-msvc": "16.0.1", + "@next/swc-darwin-arm64": "16.1.6", + "@next/swc-darwin-x64": "16.1.6", + "@next/swc-linux-arm64-gnu": "16.1.6", + "@next/swc-linux-arm64-musl": "16.1.6", + "@next/swc-linux-x64-gnu": "16.1.6", + "@next/swc-linux-x64-musl": "16.1.6", + "@next/swc-win32-arm64-msvc": "16.1.6", + "@next/swc-win32-x64-msvc": "16.1.6", "sharp": "^0.34.4" }, "peerDependencies": { @@ -9063,7 +9037,6 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -9079,7 +9052,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -9092,8 +9064,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/progress": { "version": "2.0.3", @@ -9158,6 +9129,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -9167,6 +9139,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -9328,6 +9301,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -9470,7 +9444,6 @@ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "license": "MIT", - "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -9507,7 +9480,6 @@ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -9519,8 +9491,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/semver": { "version": "6.3.1", @@ -9747,7 +9718,6 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "license": "BSD-3-Clause", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -9766,7 +9736,6 @@ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "license": "MIT", - "peer": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -10055,7 +10024,6 @@ "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -10074,7 +10042,6 @@ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.17.tgz", "integrity": "sha512-YR7PtUp6GMU91BgSJmlaX/rS2lGDbAF7D+Wtq7hRO+MiljNmodYvqslzCFiYVAgW+Qoaaia/QUIP4lGXufjdZw==", "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", @@ -10161,6 +10128,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -10388,6 +10356,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10549,6 +10518,7 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -10657,6 +10627,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -10773,7 +10744,6 @@ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", "license": "MIT", - "peer": true, "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -10797,7 +10767,6 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.4.tgz", "integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==", "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -10846,7 +10815,6 @@ "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=10.13.0" } @@ -10855,15 +10823,13 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/webpack/node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "license": "BSD-2-Clause", - "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -10877,7 +10843,6 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=4.0" } @@ -11088,6 +11053,7 @@ "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 4dbf436..21bc3f8 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.576.0", - "next": "16.0.1", + "next": "^16.1.6", "next-themes": "^0.4.6", "react": "19.2.0", "react-dom": "19.2.0", @@ -35,7 +35,7 @@ "@types/react-dom": "^19", "babel-plugin-react-compiler": "1.0.0", "eslint": "^9", - "eslint-config-next": "16.0.1", + "eslint-config-next": "^16.1.6", "jsdom": "^28.1.0", "tailwindcss": "^4", "typescript": "^5", From 3cc5c78ff4b11a33fb9f83c3497a43de2d0cd677 Mon Sep 17 00:00:00 2001 From: Birajit Saikia Date: Fri, 6 Mar 2026 01:18:08 +0530 Subject: [PATCH 4/5] feat: harden auth flow and polish leetcode-style app UX --- src/app/competitions/page.tsx | 64 ++++++++++-- src/app/components/Navbar.tsx | 97 ++++++++++++++++-- src/app/learn/page.tsx | 61 +++++++++-- src/app/login/page.tsx | 124 +++++++++++++++------- src/app/problems/page.tsx | 182 ++++++++++++++++++++++----------- src/app/progress/page.tsx | 187 ++++++++++++++++++++++------------ src/app/tracks/page.tsx | 107 ++++++++++++------- src/proxy.ts | 47 +++++++++ 8 files changed, 647 insertions(+), 222 deletions(-) create mode 100644 src/proxy.ts diff --git a/src/app/competitions/page.tsx b/src/app/competitions/page.tsx index d9e7576..ce8bd89 100644 --- a/src/app/competitions/page.tsx +++ b/src/app/competitions/page.tsx @@ -1,5 +1,6 @@ "use client"; +import { useEffect, useMemo, useState } from "react"; import MainLayout from "../components/MainLayout"; import { AlarmClockCheck, Shuffle, Trophy } from "lucide-react"; @@ -7,20 +8,65 @@ const UPCOMING = [ { id: "weekly", title: "Weekly Contest 492", - startsAt: "Sun, Mar 8, 08:00 GMT+05:30", - countdown: "2d 08:44:53", + startsAt: "2026-03-08T08:00:00+05:30", gradient: "from-amber-300/90 via-orange-400/90 to-amber-700/70", }, { id: "biweekly", title: "Biweekly Contest 178", - startsAt: "Sat, Mar 14, 20:00 GMT+05:30", - countdown: "8d 20:44:53", + startsAt: "2026-03-14T20:00:00+05:30", gradient: "from-indigo-500/90 via-violet-500/90 to-indigo-900/70", }, ]; +function formatCountdown(targetMs: number, nowMs: number): string { + const remaining = targetMs - nowMs; + if (remaining <= 0) { + return "Live now"; + } + + const totalSeconds = Math.floor(remaining / 1000); + const days = Math.floor(totalSeconds / (24 * 60 * 60)); + const hours = Math.floor((totalSeconds % (24 * 60 * 60)) / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; + + const padded = `${String(hours).padStart(2, "0")}:${String(minutes).padStart( + 2, + "0" + )}:${String(seconds).padStart(2, "0")}`; + return `${days}d ${padded}`; +} + export default function CompetitionsPage() { + const [nowMs, setNowMs] = useState(() => Date.now()); + + useEffect(() => { + const timer = window.setInterval(() => setNowMs(Date.now()), 1000); + return () => window.clearInterval(timer); + }, []); + + const contestCards = useMemo( + () => + UPCOMING.map((contest) => { + const startsAtMs = new Date(contest.startsAt).getTime(); + return { + ...contest, + startsAtLabel: new Date(contest.startsAt).toLocaleString(undefined, { + weekday: "short", + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + timeZoneName: "short", + }), + countdownLabel: formatCountdown(startsAtMs, nowMs), + isLive: startsAtMs <= nowMs, + }; + }), + [nowMs] + ); + return (
@@ -41,7 +87,7 @@ export default function CompetitionsPage() {
- {UPCOMING.map((contest) => ( + {contestCards.map((contest) => (

- {contest.countdown} + {contest.countdownLabel}

- +

{contest.title}

-

{contest.startsAt}

+

{contest.startsAtLabel}

))} diff --git a/src/app/components/Navbar.tsx b/src/app/components/Navbar.tsx index 56b8528..333e09a 100644 --- a/src/app/components/Navbar.tsx +++ b/src/app/components/Navbar.tsx @@ -1,9 +1,9 @@ "use client"; -import { useMemo } from "react"; +import { FormEvent, useMemo, useState } from "react"; import Link from "next/link"; -import { usePathname } from "next/navigation"; -import { Bell, ChevronDown, Flame, Search } from "lucide-react"; +import { usePathname, useRouter } from "next/navigation"; +import { Bell, ChevronDown, Flame, Menu, Search, X } from "lucide-react"; import { useAuth } from "@/context/AuthContext"; interface NavbarProps { @@ -18,6 +18,9 @@ export default function Navbar({ onLogout, }: NavbarProps) { const pathname = usePathname(); + const router = useRouter(); + const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); + const [globalSearch, setGlobalSearch] = useState(""); const { user, logout } = useAuth(); const avatarInitial = useMemo( () => user?.name?.trim().charAt(0).toUpperCase() || "U", @@ -36,6 +39,7 @@ export default function Navbar({ ); const handleLogout = async () => { + setIsMobileMenuOpen(false); if (onLogout) { onLogout(); return; @@ -43,28 +47,49 @@ export default function Navbar({ await logout(); }; + const handleSearchSubmit = (event: FormEvent) => { + event.preventDefault(); + setIsMobileMenuOpen(false); + const query = globalSearch.trim(); + if (!query) { + router.push("/problems"); + return; + } + router.push(`/problems?query=${encodeURIComponent(query)}`); + }; + return (
+ {isMobileMenuOpen ? ( +
+
+ + setGlobalSearch(event.target.value)} + placeholder="Search" + className="w-full bg-transparent text-sm text-zinc-200 outline-none placeholder:text-zinc-500" + /> + +
+ {navItems.map((item) => ( + setIsMobileMenuOpen(false)} + className={`rounded-lg border px-3 py-2 text-sm ${ + item.activeWhen.test(pathname) + ? "border-amber-400/40 bg-amber-500/10 text-amber-200" + : "border-zinc-700 text-zinc-300" + }`} + aria-current={item.activeWhen.test(pathname) ? "page" : undefined} + > + {item.label} + + ))} +
+
+ + +
+
+ ) : null} {(title || subtitle) && (
diff --git a/src/app/learn/page.tsx b/src/app/learn/page.tsx index a6d8e97..565f8ce 100644 --- a/src/app/learn/page.tsx +++ b/src/app/learn/page.tsx @@ -1,5 +1,7 @@ "use client"; +import { useState } from "react"; +import { useRouter } from "next/navigation"; import { Clock3, PlayCircle, Star } from "lucide-react"; import MainLayout from "../components/MainLayout"; @@ -9,34 +11,50 @@ const FEATURED = [ chapters: 13, items: 149, gradient: "from-indigo-500 to-fuchsia-500", + category: "Supervised Learning", }, { title: "System Design for Interviews", chapters: 10, items: 104, gradient: "from-emerald-600 to-teal-700", + category: "Model Evaluation", }, { title: "ML Beginner's Guide", chapters: 9, items: 72, gradient: "from-amber-400 to-rose-400", + category: "Data Preprocessing", }, { title: "Top Interview Questions", chapters: 11, items: 130, gradient: "from-cyan-700 to-emerald-800", + category: "Supervised Learning", }, ]; export default function LearnPage() { + const router = useRouter(); + const [activeCategory, setActiveCategory] = useState("All"); + + const categories = ["All", "Supervised Learning", "Data Preprocessing", "Model Evaluation"]; + + const visible = + activeCategory === "All" + ? FEATURED + : FEATURED.filter((course) => course.category === activeCategory); + return (

Welcome to

-

MLBoost Explore

+

+ MLBoost Explore +

@@ -47,7 +65,7 @@ export default function LearnPage() {

Continue Previous

-

Data Structures and Algorithms

+

Data Structures and Algorithms

@@ -59,7 +77,10 @@ export default function LearnPage() {

Items

-

0%

@@ -68,18 +89,44 @@ export default function LearnPage() {
-

Featured

+
+

Featured

+
+ {categories.map((category) => ( + + ))} +
+
- {FEATURED.map((course) => ( + {visible.map((course) => (

Interview Crash Course

-

{course.title}

-

+

{course.title}

+

{course.category}

+

{course.chapters} chapters · {course.items} lessons

+
))}
diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index ebb54ca..08b7247 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,9 +1,9 @@ "use client"; -import { FormEvent, Suspense, useMemo, useState } from "react"; +import { FormEvent, Suspense, useEffect, useMemo, useState } from "react"; import Link from "next/link"; import { useRouter, useSearchParams } from "next/navigation"; -import { Loader2 } from "lucide-react"; +import { Github, Loader2, Search } from "lucide-react"; import { useAuth } from "@/context/AuthContext"; type AuthMode = "login" | "signup"; @@ -21,7 +21,7 @@ function sanitizeRedirect(path: string | null): string { function LoginForm() { const router = useRouter(); const searchParams = useSearchParams(); - const { login, signup, isLoading } = useAuth(); + const { login, signup, isLoading, isAuthenticated } = useAuth(); const [mode, setMode] = useState("login"); const [name, setName] = useState(""); const [email, setEmail] = useState(""); @@ -33,6 +33,12 @@ function LoginForm() { [searchParams] ); + useEffect(() => { + if (isAuthenticated && !isLoading) { + router.replace(redirectTo); + } + }, [isAuthenticated, isLoading, redirectTo, router]); + const handleSubmit = async (event: FormEvent) => { event.preventDefault(); setErrorMessage(""); @@ -45,7 +51,6 @@ function LoginForm() { setErrorMessage("Email and password are required."); return; } - if (mode === "signup" && !trimmedName) { setErrorMessage("Name is required for signup."); return; @@ -70,54 +75,72 @@ function LoginForm() { }; return ( -
-
-
-
- - - - - - - +
+
+
+
+ +
-
-
-
+
+
-
- ⌂ +
+ △
-

MLBoost

+

+ MLBoost +

-
+
+ +
+

or continue with

+
+ + + +
+
+ +
+ Copyright © 2026 MLBoost · Help · Jobs · Terms · Privacy Policy +
); } diff --git a/src/app/problems/page.tsx b/src/app/problems/page.tsx index bff9b5b..fb26eca 100644 --- a/src/app/problems/page.tsx +++ b/src/app/problems/page.tsx @@ -26,7 +26,34 @@ const LEFT_ITEMS = [ { label: "Study Plan", icon: Flame }, ]; -const LIST_ITEMS = ["Favorites", "Top 150", "Interview Prep"]; +const LIST_ITEMS = ["Favorites", "Top Interview 150", "SQL 50"]; +const TOPIC_FILTERS = [ + { key: "all", label: "All Topics" }, + { key: "algorithms", label: "Algorithms" }, + { key: "database", label: "Database" }, + { key: "shell", label: "Shell" }, +] as const; +const DATABASE_HINTS = ["database", "sql", "pandas", "joins", "groupby", "preprocessing"]; +const SHELL_HINTS = ["shell", "bash", "terminal", "script"]; + +type TopicFilterKey = (typeof TOPIC_FILTERS)[number]["key"]; + +function getTopicBucket(problem: Problem): Exclude { + const tags = problem.tags.map((tag) => tag.toLowerCase()); + const isDatabase = tags.some((tag) => + DATABASE_HINTS.some((needle) => tag.includes(needle)) + ); + if (isDatabase) { + return "database"; + } + + const isShell = tags.some((tag) => SHELL_HINTS.some((needle) => tag.includes(needle))); + if (isShell) { + return "shell"; + } + + return "algorithms"; +} function statusIcon(status: Problem["status"]) { if (status === "solved") { @@ -54,6 +81,8 @@ export default function ProblemsPage() { const [problems, setProblems] = useState([]); const [isLoading, setIsLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(""); + const [topicFilter, setTopicFilter] = useState("all"); + const [starredProblems, setStarredProblems] = useState>(new Set()); const searchInputRef = useRef(null); const [filters, setFilters] = useState({ level: "All Levels", @@ -76,13 +105,12 @@ export default function ProblemsPage() { useEffect(() => { const categoryParam = searchParams.get("category"); - if (!categoryParam) { - return; - } + const queryParam = searchParams.get("query") || ""; setFilters((current) => ({ ...current, - category: categoryParam, + category: categoryParam || "All Categories", })); + setSearchQuery(queryParam); }, [searchParams]); useEffect(() => { @@ -121,9 +149,21 @@ export default function ProblemsPage() { .slice(0, 10); }, [problems]); + const topicCounts = useMemo(() => { + return TOPIC_FILTERS.reduce>( + (acc, filter) => { + acc[filter.key] = + filter.key === "all" + ? problems.length + : problems.filter((problem) => getTopicBucket(problem) === filter.key).length; + return acc; + }, + { all: 0, algorithms: 0, database: 0, shell: 0 } + ); + }, [problems]); + const filteredProblems = useMemo(() => { let list = [...problems]; - if (filters.level !== "All Levels") { list = list.filter((problem) => problem.difficulty === filters.level); } @@ -146,18 +186,22 @@ export default function ProblemsPage() { ); } + if (topicFilter !== "all") { + list = list.filter((problem) => getTopicBucket(problem) === topicFilter); + } + return list; - }, [problems, filters, searchQuery]); + }, [problems, filters, searchQuery, topicFilter]); const solvedCount = problems.filter((problem) => problem.status === "solved").length; return ( -
-