diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..627d00ce0 --- /dev/null +++ b/.env.example @@ -0,0 +1,63 @@ +NEXT_PUBLIC_USE_SPRING_BOOT_API=true +# Local Spring Boot (for testing changes locally): +NEXT_PUBLIC_SPRING_BOOT_API_URL=http://localhost:8080 + +# ──────────── Email (Nodemailer) ──────────── +EMAIL_USER=your-email@gmail.com +EMAIL_PASSWORD=your-google-app-password +REVIEW_INBOX_EMAIL=optional-inbox@gmail.com + +# Gemini AI — required for the chatbot feature +# Get a free API key at https://aistudio.google.com/apikey +GEMINI_API_KEY=gemini-api-key-here + +# ──────────── Supabase ──────────── +NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co +NEXT_PUBLIC_SUPABASE_ANON_KEY=your-supabase-anon-key +SUPABASE_SERVICE_KEY=your-supabase-service-key +SUPABASE_JWT_SECRET=your-supabase-jwt-secret + +# ──────────── Cloudflare Turnstile Captcha ──────────── +NEXT_PUBLIC_TURNSTILE_SITE_KEY=1x00000000000000000000AA +TURNSTILE_SECRET_KEY=your-turnstile-secret-key +TURNSTILE_CONFIGURED=false +TURNSTILE_BYPASS=true + +# ──────────── Google Analytics ──────────── +NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX + +# ──────────── AI Chatbot & Complexity Analyzer ──────────── +GEMINI_API_KEY=your-gemini-api-key + +# ──────────── Rate Limiting (Upstash Redis) ──────────── +UPSTASH_REDIS_REST_URL=your-upstash-redis-rest-url +UPSTASH_REDIS_REST_TOKEN=your-upstash-redis-rest-token +RATE_LIMIT_MAX=10 + +# ──────────── UI Flags ──────────── +NEXT_PUBLIC_SHOW_COMMUNITY_BADGE=true +AUTO_CONFIRM_EMAIL=true + +# ──────────── Arena Socket Server & Java Backend Distributed Cache ──────────── +# Redis URL for the arena socket server (used for matchmaking queue and session state) +# Note: Use "rediss://" prefix if SSL is required (e.g. for Upstash) +REDIS_URL=redis://localhost:6379 + +# Redis connection parameters for the Java backend's distributed cache +# REDIS_HOST=localhost +# REDIS_PORT=6379 +# REDIS_PASSWORD= +# REDIS_SSL=false + +# URL of the arena socket server for match pair verification from the Java backend +# SOCKET_SERVER_URL=http://localhost:4000 + +# Debug endpoint configuration (arena-socket-server) +# Set DEBUG_ENABLED=true only in non-production environments +# DEBUG_ENABLED=true +# DEBUG_KEY=your-secure-debug-key + +# ──────────── CSRF Security ──────────── +# Generate a secure random string (e.g., run `openssl rand -hex 32` in your terminal). +CSRF_SECRET=your-csrf-secret-key + diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 000000000..2e50271e5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,66 @@ +name: 🐞 Bug Report +description: Report a bug or unexpected behavior to help us improve AlgoBuddy +title: "[BUG] " +labels: ["bug", "triage"] +assignees: [] +body: + - type: markdown + attributes: + value: | + Thank you for taking the time to report a bug! Please fill out the form below to help us understand and resolve the issue as quickly as possible. + - type: textarea + id: description + attributes: + label: Bug Description + description: A clear and concise description of what the bug is. + placeholder: Tell us what went wrong... + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: Steps to Reproduce + description: Please provide step-by-step instructions to reproduce the bug. + value: | + 1. Go to '...' + 2. Click on '...' + 3. Scroll down to '...' + 4. See error + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected Behavior + description: A clear and concise description of what you expected to happen. + validations: + required: true + - type: textarea + id: actual + attributes: + label: Actual Behavior + description: A clear and concise description of what actually happened. + validations: + required: true + - type: input + id: environment + attributes: + label: Environment + description: Please provide information about your environment (OS, Browser version, Node.js version, etc.) + placeholder: e.g. Windows 11, Chrome 124, Node 20.x + validations: + required: true + - type: textarea + id: screenshots + attributes: + label: Screenshots or Logs + description: If applicable, add screenshots, video recordings, or error logs to help explain your problem. + validations: + required: false + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + options: + - label: I agree to follow this project's Code of Conduct + required: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 000000000..e22664f2f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,45 @@ +name: ✨ Feature Request +description: Suggest an idea or feature for AlgoBuddy +title: "[FEATURE] " +labels: ["enhancement", "triage"] +assignees: [] +body: + - type: markdown + attributes: + value: | + Thank you for suggesting an idea to make AlgoBuddy better! Please fill out the form below to help us understand your feature request. + - type: textarea + id: problem + attributes: + label: Is your feature request related to a problem? Please describe. + description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + validations: + required: true + - type: textarea + id: solution + attributes: + label: Describe the solution you'd like + description: A clear and concise description of what you want to happen. + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Describe alternatives you've considered + description: A clear and concise description of any alternative solutions or features you've considered. + validations: + required: false + - type: textarea + id: additional + attributes: + label: Additional context + description: Add any other context or screenshots about the feature request here. + validations: + required: false + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + options: + - label: I agree to follow this project's Code of Conduct + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..37568cbce --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,40 @@ +## Summary + +A brief description of the changes in this Pull Request. Explain **what** was changed and **why**. + +## Type of Change + +Please check the relevant options: +- [ ] 🐛 Bug fix (non-breaking change which fixes an issue) +- [ ] ✨ New feature (non-breaking change which adds functionality) +- [ ] 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] 📝 Documentation update (changes to README, contributing guidelines, etc.) +- [ ] 🎨 Style/Refactor (code style formatting, restructuring without changing behavior) + +## Related Issues + +Closes #[Issue Number] + +## Changes Made + +- [Detail 1] +- [Detail 2] + +## How to Verify + +Please describe how reviewers can test your changes. +- [ ] Test Step 1 +- [ ] Test Step 2 + +## Screenshots (if applicable) + +*Before / After (or just a demo of the new feature)* + +## Checklist + +- [ ] My code follows the project style guidelines +- [ ] I have performed a self-review of my code +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I have updated the documentation accordingly +- [ ] My changes generate no new warnings or errors +- [ ] I have verified that these changes work correctly on multiple screen sizes (if UI-related) diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 000000000..ee0e609e3 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,17 @@ +gssoc:approved: + - ".*" + +mentor:PankajSingh34: + - ".*" + +level:beginner: + - ".*beginner.*" + +type:bug: + - ".*bug.*" + +type:feature: + - ".*feature.*" + +type:docs: + - ".*docs.*" diff --git a/.github/workflows/ai-issue-labeler.yml b/.github/workflows/ai-issue-labeler.yml new file mode 100644 index 000000000..d312378a3 --- /dev/null +++ b/.github/workflows/ai-issue-labeler.yml @@ -0,0 +1,25 @@ +name: Smart Issue Labeler + +on: + issues: + types: [opened, edited] + +permissions: + issues: write + contents: read + +jobs: + label-issues: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo # ← ADD THIS + uses: actions/checkout@v4 + + - name: Label issues + uses: github/issue-labeler@v3.4 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + configuration-path: .github/labeler.yml + enable-versioned-regex: 0 + include-title: 1 diff --git a/.github/workflows/auto-approve-on-status.yml b/.github/workflows/auto-approve-on-status.yml new file mode 100644 index 000000000..9b49d0b1d --- /dev/null +++ b/.github/workflows/auto-approve-on-status.yml @@ -0,0 +1,96 @@ +name: Conditional Auto Approve + +on: + # जब भी किसी पीआर पर चेक्स (Status/Check Suite) पूरे हों + check_suite: + types: [completed] + status: + +jobs: + check-and-approve: + runs-on: ubuntu-latest + if: github.repository == 'PankajSingh34/AlgoBuddy' + steps: + - name: Check Statuses and Approve + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { owner, repo } = context.repo; + + // 1. इस इवेंट से जुड़े Pull Requests की लिस्ट निकालें + let pullRequests = []; + if (context.eventName === 'check_suite') { + pullRequests = context.payload.check_suite.pull_requests; + } else { + // status इवेंट के लिए SHA के ज़रिए PR ढूंढें + const sha = context.payload.sha; + const prs = await github.rest.pulls.list({ owner, repo, state: 'open' }); + pullRequests = prs.data.filter(pr => pr.head.sha === sha); + } + + if (pullRequests.length === 0) { + console.log("No open pull request found for this event."); + return; + } + + const prNumber = pullRequests[0].number; + const prRef = pullRequests[0].head.sha; + + // 2. इस Commit SHA के सारे कंबाइंड चेक्स/स्टेटस गेट करें + const statuses = await github.rest.repos.getCombinedStatusForRef({ + owner, + repo, + ref: prRef, + }); + + const checkRuns = await github.rest.checks.listForRef({ + owner, + repo, + ref: prRef, + }); + + // 3. फ़िल्टर लॉजिक: वर्सेल (Vercel) को छोड़कर बाकी चेक्स की जांच + let allOthersPassed = true; + let evaluatedChecksCount = 0; + + // स्टेटस चेक करें (पुराने गिटहब स्टेटस के लिए) + for (const status of statuses.data.statuses) { + if (status.context.toLowerCase().includes('vercel')) { + continue; // Vercel को छोड़ दो + } + evaluatedChecksCount++; + if (status.state !== 'success') { + allOthersPassed = false; + } + } + + // चेक रन्स देखें (GitHub Actions आदि के लिए) + for (const run of checkRuns.data.check_runs) { + if (run.name.toLowerCase().includes('vercel') || run.app?.slug?.toLowerCase().includes('vercel')) { + continue; // Vercel को छोड़ दो + } + // अगर कोई चेक अभी चल रहा है या पास नहीं हुआ है + if (run.status !== 'completed') { + allOthersPassed = false; + } else if (run.conclusion !== 'success' && run.conclusion !== 'neutral') { + allOthersPassed = false; + } + evaluatedChecksCount++; + } + + console.log(`Evaluated ${evaluatedChecksCount} non-Vercel checks. Status: ${allOthersPassed}`); + + // 4. अगर वर्सेल के अलावा बाकी सारे चेक्स पास हैं, तो APPROVE कर दो + if (allOthersPassed && evaluatedChecksCount > 0) { + await github.rest.pulls.createReview({ + owner, + repo, + pull_number: prNumber, + event: 'APPROVE', + body: 'code looks good', + }); + console.log(`PR #${prNumber} has been automatically approved with 'code looks good'!`); + } else { + console.log("Some non-Vercel checks are either pending or failed. Skipping auto-approval."); + } diff --git a/.github/workflows/auto_assign_yml b/.github/workflows/auto_assign_yml new file mode 100644 index 000000000..1606ceb39 --- /dev/null +++ b/.github/workflows/auto_assign_yml @@ -0,0 +1,97 @@ +name: Auto Assign Issue to Creator + +on: + issues: + types: [opened] + issue_comment: + types: [created] + +jobs: + auto-assign: + runs-on: ubuntu-latest + permissions: + issues: write + + steps: + - name: Auto-assign issue to creator & block re-assignment + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const issueNumber = context.issue.number; + const eventName = context.eventName; + + // ────────────────────────────────────────────── + // CASE 1: New issue opened → assign to creator + // ────────────────────────────────────────────── + if (eventName === "issues") { + const creator = context.payload.issue.user.login; + + await github.rest.issues.addAssignees({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + assignees: [creator], + }); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: `👋 Hey @${creator}! This issue has been **automatically assigned to you** as you created it.\n\n> 🔒 This issue is now locked for assignment. No other contributor can claim it.`, + }); + + console.log(`✅ Issue #${issueNumber} auto-assigned to @${creator}`); + return; + } + + // ────────────────────────────────────────────── + // CASE 2: Someone comments trying to claim it + // ────────────────────────────────────────────── + if (eventName === "issue_comment") { + const comment = context.payload.comment.body.toLowerCase().trim(); + const commenter = context.payload.comment.user.login; + + const claimKeywords = ["assign me", "assign this to me", "i want to work", "can i take this", "please assign"]; + const isClaiming = claimKeywords.some(kw => comment.includes(kw)); + + if (!isClaiming) return; + + // Fetch current issue to check existing assignees + const { data: issue } = await github.rest.issues.get({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + }); + + const assignees = issue.assignees.map(a => a.login); + + if (assignees.length > 0) { + // Already assigned — block and notify + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: `❌ Sorry @${commenter}, this issue is **already assigned** to @${assignees[0]} and cannot be reassigned.\n\nPlease look for other open issues to contribute to! 🚀`, + }); + + console.log(`🚫 Blocked @${commenter} — issue #${issueNumber} already assigned to @${assignees[0]}`); + } else { + // Not yet assigned — assign to the commenter + await github.rest.issues.addAssignees({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + assignees: [commenter], + }); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: `✅ @${commenter} This issue has been assigned to you! Good luck and happy coding! 🎉\n\n> 🔒 This issue is now locked — no further assignments allowed.`, + }); + + console.log(`✅ Issue #${issueNumber} assigned to commenter @${commenter}`); + } + } \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0a8078d9c..607a9bb0b 100755 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,30 +1,72 @@ -name : Test Platform +name: Test Platform on: - pull_request: - branches: - - main + pull_request: + branches: + - main jobs: - test: - strategy: - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: 'npm' - - - name: Install dependencies - run: npm install - - - name: Run tests - run: npm run test - - - name: Build Next.js - run: npm run build \ No newline at end of file + test: + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + fail-fast: false + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python (required by isolated-vm native addon) + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install C++ build tools (Ubuntu) + if: runner.os == 'Linux' + run: sudo apt-get install -y build-essential + + - name: Install C++ build tools (macOS) + if: runner.os == 'macOS' + run: xcode-select --install || true + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run lint and tests + run: npm run test + + - name: Build Next.js + run: npm run build + env: + # Stub public env vars so Next.js build-time config checks don't throw. + # These are intentionally placeholder values — no real secrets here. + NEXT_PUBLIC_SUPABASE_URL: https://placeholder.supabase.co + # Must be a valid 3-part JWT format (header.payload.sig) — Supabase JS v2 + # validates the structure on client init and throws on a plain string. + NEXT_PUBLIC_SUPABASE_ANON_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBsYWNlaG9sZGVyIiwicm9sZSI6ImFub24iLCJpYXQiOjE2MDAwMDAwMDAsImV4cCI6MjAwMDAwMDAwMH0.placeholder-signature + NEXT_PUBLIC_TURNSTILE_SITE_KEY: placeholder-turnstile-key + NEXT_PUBLIC_GA_ID: G-PLACEHOLDER + + validate-backend-pom: + runs-on: ubuntu-latest + defaults: + run: + working-directory: backend + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: 21 + distribution: temurin + cache: maven + + - name: Validate Maven POM + run: bash ./mvnw validate --batch-mode \ No newline at end of file diff --git a/.gitignore b/.gitignore index df9b0ffd5..38535a190 100755 Binary files a/.gitignore and b/.gitignore differ diff --git a/.mavis/last-run-report.md b/.mavis/last-run-report.md new file mode 100644 index 000000000..68112499d --- /dev/null +++ b/.mavis/last-run-report.md @@ -0,0 +1,44 @@ +AlgoBuddy cron run -- 2026-08-04T15:31:27Z + +Phase 1 -- Prior PR triage +- PR #1: GREEN -- CLEAN, no CI checks configured on upstream +- PR #2: GREEN -- CLEAN, no CI checks configured on upstream +- PR #3: GREEN -- CLEAN, no CI checks configured on upstream +- PR #4: GREEN -- CLEAN, no CI checks configured on upstream +- PR #5: GREEN -- CLEAN, no CI checks configured on upstream +- PR #6: GREEN -- CLEAN, no CI checks configured on upstream +- PR #7: GREEN -- CLEAN, no CI checks configured on upstream +- PR #8: GREEN -- CLEAN, no CI checks configured on upstream +- PR #9: GREEN -- CLEAN, no CI checks configured on upstream +- PR #10: GREEN -- CLEAN, no CI checks configured on upstream + +Phase 2 -- New PRs +- PR #12: OPEN -- __tests__/sortingGenerators.test.js, __tests__/apiErrors.test.js, __tests__/profileUtils.test.js, __tests__/rateLimits.test.js, __tests__/random.test.js (119 new tests, 5 files, 889 lines) + +Phase 3 -- Monitoring +- PR #12: no CI checks configured on syedahmedkhaderi/AlgoBuddy (upstream has no Actions workflows) +- Prior PRs #1-10: no CI checks, no conflicts, all CLEAN + +Summary +- Issues created: 0/5 (upstream syedahmedkhaderi/AlgoBuddy has issues disabled -- HTTP 410 Gone) +- PRs opened: 1/5 (consolidated 5 test files into single PR) +- PRs green: 0/5 (no CI configured on upstream -- manual review required) +- PRs blocked: 0/5 + +Key Corrections from cron prompt +- UPSTREAM IS syedahmedkhaderi/AlgoBuddy, NOT PankajSingh34/AlgoBuddy +- tmdeveloper007 is NOT blocked from syedahmedkhaderi/AlgoBuddy (PR creation works) +- Issues disabled on upstream (HTTP 410) -- cannot create issues +- No CI/Action workflows exist on upstream -- PRs require manual review +- Branch must be based on upstream/main (not origin/main) to avoid large diff + +Local Verification Results +- npm run lint: green +- npm run test:ui: 154 passed (119 new + 35 pre-existing), 1 failed (pre-existing rateLimit test requires Redis) +- npm run build: OOM killed (pre-existing, not caused by these changes) + +Recommendations +- Verify PR #12: https://github.com/syedahmedkhaderi/AlgoBuddy/pull/12 +- syedahmedkhaderi should configure GitHub Actions CI for automated test runs +- syedahmedkhaderi should enable issues on the repo for future automation +- Consider using syedahmedkhaderi/AlgoBuddy as the correct upstream in cron configuration diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..2e63b5d73 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "css.lint.unknownAtRules": "ignore", + "java.compile.nullAnalysis.mode": "disabled", + "java.configuration.updateBuildConfiguration": "interactive" +} \ No newline at end of file diff --git a/Architecture.md b/Architecture.md new file mode 100644 index 000000000..2232c1d35 --- /dev/null +++ b/Architecture.md @@ -0,0 +1,221 @@ +# AlgoBuddy Architecture Guide + +This document describes the high-level system architecture of **AlgoBuddy**. It defines the core component boundaries, state ownership, communication contracts, and extension patterns for developers. + +For developer environment setups and installation steps, see [README.md](./README.md). + +--- + +## 1. Project Overview + +AlgoBuddy is a learning and practice platform for Data Structures and Algorithms (DSA). It features step-by-step algorithm visualizers, real-time peer-to-peer coding duels, and AI-assisted study tools. + +The platform comprises three runtimes: + +* **Next.js Frontend**: Serves the user interface and executes untrusted code in a sandboxed runner. + +* **Spring Boot Backend**: Serves as the database source of truth, managing user profiles, statistics, and sheets. + +* **Arena Socket Server**: A WebSocket gateway that manages peer-to-peer matchmaking, live duel states, and spectator rooms. + +--- + +## 2. System Architecture + +The diagram below shows the boundaries of the system and the communication paths between components. + +```mermaid +graph TD + Browser[Client Browser] + NextJS[Next.js Frontend] + SpringBoot[Spring Boot API] + SocketServer[Arena Socket Server] + AIServices[AI Services] + Supabase[Supabase PostgreSQL] + Redis[Redis Cache / Queue] + + Browser --> NextJS + Browser --> SpringBoot + Browser --> SocketServer + + NextJS --> Supabase + NextJS --> Redis + NextJS --> AIServices + + SpringBoot --> Supabase + SpringBoot --> Redis + SpringBoot --> SocketServer + + SocketServer --> Redis +``` + +AlgoBuddy uses a decentralized client-server architecture. The Client Browser acts as the central coordinator, communicating asynchronously with the Next.js edge functions (for user sessions and isolated execution), the Spring Boot API (for profile data and persistence), and the stateful Node.js Arena Socket server (for multiplayer duels). + +--- + +## 3. Major Components + +To scale services independently and maintain clear boundaries, the codebase is split into five functional components: + +### 3.1 Frontend +The Frontend is a Next.js application that serves user pages, visualizer interfaces, practice dashboards, and duels. It is responsible for client-side state engines, visualizer timelines, and caching guest state locally. It communicates with other components via REST endpoints and WebSocket protocols. + +### 3.2 Backend +The Backend is a Spring Boot application that manages business logic, database operations, and persistent data models. It tracks user profile levels, calculates practice achievements, and computes Elo rating updates. It connects directly to PostgreSQL and Redis cache, exposing a REST API for authenticated user sessions. + +### 3.3 Real-time Service +The Real-time Service is a Node.js Socket.io server that handles matchmaking and live duels. It pairs users, distributes code typing status, monitors user connection drops, and routes spectator lobbies. It relies on Redis to maintain queue states across instances, broadcasting match events via WebSockets. + +### 3.4 Database +The Database is a Supabase PostgreSQL instance that serves as the persistent data store. It manages user progress tracking, bookmark tables, and activity logs. Access is governed by database-level triggers and Row-Level Security (RLS) policies based on authenticated user IDs. + +### 3.5 AI Services +The AI Services integrate with Google Gemini LLM APIs to provide coding hints, dry-run debugging, and study planning. These services process prompts submitted from the frontend or backend layers, returning contextual guidance. + +--- + +## 4. Repository Organization + +Code ownership is mapped to top-level directories to isolate execution contexts: + +* **Frontend UI ([src/](./src))**: Next.js App Router, custom hooks, reusable components, and client-side utilities. + +* **Business Logic ([backend/](./backend))**: Spring Boot models, REST controllers, JPA repository layers, and Elo calculators. + +* **Stateful Networking ([arena-socket-server/](./arena-socket-server))**: Real-time duel handlers, matchmaking routines, and Redis connections. + +* **Shared Configurations & Migration Database Scripts ([supabase_setup.sql](./supabase_setup.sql) & [backend/*.sql](./backend))**: Defines configurations and SQL table schemas (managed manually through CLI scripts or Supabase console updates). + +--- + +## 5. Request & Data Flow + +### 5.1 Authentication Flow +```mermaid +sequenceDiagram + participant Browser as Client Browser + participant Supabase as Supabase Auth + participant Backend as Backend / Socket Server + + Browser->>Supabase: Request JWT Session Token + Supabase-->>Browser: Return JWT Session Token + Browser->>Backend: Request API Resource (with JWT Header) + Backend->>Supabase: Validate JWT Signature (via JWKS) + Supabase-->>Backend: Verified Identity Context + Backend-->>Browser: Return Requested Resource +``` +User sessions are managed via Supabase, with sessions verified across services using JSON Web Tokens (JWT). The browser passes this JWT in the Authorization headers of API and WebSocket connections. Backend servers verify the token using Supabase's JWKS endpoint. + +### 5.2 Learning Flow +```mermaid +sequenceDiagram + participant Browser as Client Browser + participant Storage as Local Storage + participant API as Spring Boot / Next API + participant DB as PostgreSQL + + Browser->>Storage: Write optimistic progress update + Browser->>API: Post problem status change + API->>DB: Execute transaction (with streak calculation) + DB-->>API: Returns updated user streak + API-->>Browser: Update UI state +``` +Progress updates are saved locally for responsiveness before being sent to the backend. The backend updates PostgreSQL, which runs a database-level transaction to update streaks. + +### 5.3 Arena Flow +```mermaid +sequenceDiagram + participant BrowserA as Player A + participant Socket as Socket Server + participant Redis as Redis Queue + participant BrowserB as Player B + + BrowserA->>Socket: join_matchmaking (Topic, Difficulty) + Socket->>Redis: Check queue & match players + alt Match Found + Socket->>Redis: Create match session + Socket-->>BrowserA: match_found (Join Room) + Socket-->>BrowserB: match_found (Join Room) + else Queue Empty + Socket->>Redis: Add Player A to list + end +``` +Matchmaking uses Redis lists. The Socket Server matches players, creates a room, and broadcasts the session details. Clients then update their game state via the REST API. + +### 5.4 AI Flow +```mermaid +sequenceDiagram + participant Browser as Client Browser + participant API as Next / Spring API + participant LLM as Gemini API + + Browser->>API: Request prompt completion + API->>LLM: Submit context & user prompt + LLM-->>API: Return model completion + API-->>Browser: Render answer layout +``` +AI prompts are processed through server-side APIs to protect API keys. The APIs validate the request, call Gemini, and return the response. + +--- + +## 6. Architectural Decisions + +### 6.1 Separation of Spring Boot and Socket Server +Spring Boot is optimized for transactional database operations (REST API), while Node.js/Socket.io is optimized for stateful, event-driven WebSocket connections. Separating them allows independent scaling and prevents connection surges from impacting persistence. + +### 6.2 Next.js Serverless Layer +Provides static and server-side rendering for landing pages, simple routing, and handles sandboxed user code execution (`isolated-vm`) at the edge, protecting core backend resources. + +### 6.3 Redis Cache & Queue +Offers fast, in-memory operations needed for real-time matchmaking queues, and acts as a distributed cache to reduce database load. + +### 6.4 Supabase for Auth & Database +Provides built-in user authentication, email validation, and a Postgres database with Row-Level Security, reducing boilerplate code. + +### 6.5 Separate Real-Time Networking +Decouples the live game engine from business APIs, allowing the socket server to run independently of persistent storage. + +--- + +## 7. Extension Guide + +Contributors should follow these conventions when introducing changes: + +* **Pages**: Add folder in [src/app/](./src/app) (e.g. `src/app//page.jsx`) + +* **APIs**: Add serverless endpoint in [src/app/api/](./src/app/api) (e.g. `src/app/api//route.js`) + +* **Services / Hooks**: Add custom hook in [src/app/hooks/](./src/app/hooks) + +* **Shared UI Components**: Place inside [src/components/ui/](./src/components/ui) + +* **Backend Endpoints**: Define inside the controller layer under [backend/src/main/java/com/algobuddy/backend/controller/](./backend/src/main/java/com/algobuddy/backend/controller) + +* **Backend Services**: Implement under [backend/src/main/java/com/algobuddy/backend/service/](./backend/src/main/java/com/algobuddy/backend/service) + +* **Socket Events**: Register listeners in [arena-socket-server/index.js](./arena-socket-server/index.js) using `socket.on(...)` + + +### Architectural Boundaries + +* Never allow the frontend to access the database directly. + +* Keep the Socket Server stateless by storing matchmaking queues and active match rooms in Redis. + +* Restrict data access in PostgreSQL using Row-Level Security (RLS) policies. + +* Style components using Tailwind tokens from [design.md](./design.md) rather than custom inline styles. + +--- + +## 8. Guiding Principles + +* **Separation of Concerns**: Decouple layout rendering, persistent business rules, and stateful real-time interactions. + +* **Modular Design**: Build features (like visualizers and notes) as isolated components to prevent side effects. + +* **Service Ownership**: Keep the Next.js client, Spring Boot API, and Socket Server independent, communicating only through defined APIs. + +* **Local-First & Optimistic Rendering**: Update the UI instantly using local state before syncing with backend servers for a smooth user experience. + +* **Security by Design**: Enforce authorization checks at all layers (middleware, controllers, and database RLS). diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 8b1378917..52d870584 100755 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1 +1,68 @@ +# Code of Conduct +## Our Pledge + +We as contributors and maintainers pledge to make participation in this project a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +We are committed to creating an open, welcoming, inclusive, and respectful environment for everyone. + +--- + +## Our Standards + +Examples of behavior that contributes to a positive environment include: + +- Being respectful and inclusive +- Using welcoming and constructive language +- Respecting differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Helping other contributors and community members + +Examples of unacceptable behavior include: + +- Harassment or discriminatory language +- Personal attacks or trolling +- Public or private harassment +- Publishing others’ private information without permission +- Any conduct that could be considered inappropriate in a professional setting + +--- + +## Contributor Responsibilities + +Contributors are expected to: +- Follow project guidelines +- Maintain respectful communication +- Focus on collaboration and learning +- Report inappropriate behavior if encountered + +--- + +## Enforcement + +Project maintainers are responsible for clarifying and enforcing standards of acceptable behavior and may take appropriate corrective action in response to any instances of unacceptable behavior. + +--- + +## Reporting Issues + +If you experience or witness unacceptable behavior, please report it to the project maintainers through the repository issue section or the official project contact channels. + +All complaints will be reviewed and investigated promptly and fairly. + +--- + +## Scope + +This Code of Conduct applies within all project spaces, including: +- GitHub repositories +- Discussions +- Pull requests +- Community chats +- Social platforms related to the project + +--- + +## Attribution + +This Code of Conduct is inspired by the Contributor Covenant, version 2.1. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 67e64f752..f0017fa98 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,51 +1,263 @@ # Contributing to AlgoBuddy -Thank you for considering contributing to this project! ❤️ +Thank you for your interest in contributing to **AlgoBuddy**! +We welcome and appreciate contributions from the community to help make this project better. -We welcome contributions in the form of: +--- -- Bug fixes 🐛 -- New Algorithm Visualizers ✨ -- UI/UX Improvements 🎨 -- Documentation 📚 +**Join our community** — **Discord Server: ** -## Getting Started +--- -1. Fork the repository -2. Clone your fork +## ↪️Table of Contents + +- [Contribution Areas](#️-contribution-areas) +- [Tech Stack](#️-tech-stack) +- [Getting Started](#️-getting-started) +- [Development Workflow](#️-development-workflow) +- [Issue Assignment Process](#️-issue-assignment-process) +- [Pull Request Guidelines](#️-pull-request-guidelines) +- [Reporting Issues](#️-reporting-issues) +- [Need Help?](#️-need-help) + +--- + +# ↪️ Contribution Areas + +We accept contributions in the following areas: + +| Area | Description | +| ---------------------- | ------------------------------------------------- | +| **Bug Fixes** | Resolve existing bugs and issues | +| **UI/UX Improvements** | Enhance responsiveness, accessibility, and design | +| **New Visualizers** | Add new DSA visualizers and animations | +| **Documentation** | Improve guides, README, and contributor docs | +| **Performance** | Optimize application performance and efficiency | +| **Theme Enhancements** | Improve dark/light mode experience | + +Feel free to suggest new contribution ideas by opening an issue first. + +--- + +# ↪️ Tech Stack + +| Layer | Technology | +| --------- | -------------------------------------------------- | +| Framework | Next.js 16 (App Router) | +| Library | React.js | +| Styling | Tailwind CSS | +| Language | JavaScript | +| Database / Auth | Supabase | +| Animation | GSAP, Framer Motion | +| Charts | Recharts | +| Email | Nodemailer (Gmail) | +| Captcha | Cloudflare Turnstile | + +--- + +# ↪️ Getting Started + +Follow these steps to set up the project locally. + +## 1. Fork the Repository + +Click the **Fork** button at the top-right corner of this repository. + +## 2. Clone Your Fork ```bash git clone https://github.com/your-username/AlgoBuddy.git ``` -3. Create a new branch +## 3. Navigate to the Project Directory + +```bash +cd AlgoBuddy +``` + +## 4. Install Dependencies + +```bash +npm install +``` + +## 5. Set Up Environment Variables + +Copy the example env file and fill in the required values: + +```bash +cp .env.example .env.local +``` + +| Variable | Description | +| ---------------------------------- | ------------------------------------------------ | +| `EMAIL_USER` | Gmail address used to send contact/review emails | +| `EMAIL_PASSWORD` | Gmail App Password (not your account password) | +| `NEXT_PUBLIC_GA_ID` | Google Analytics Measurement ID | +| `NEXT_PUBLIC_SUPABASE_URL` | Your Supabase project URL | +| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Supabase anonymous/public key | +| `SUPABASE_SERVICE_KEY` | Supabase service role key (server-side only) | +| `NEXT_PUBLIC_TURNSTILE_SITE_KEY` | Cloudflare Turnstile site key | +| `TURNSTILE_SECRET_KEY` | Cloudflare Turnstile secret key | +| `TURNSTILE_BYPASS` | Bypass Turnstile locally (set to `true` to bypass) | +| `UPSTASH_REDIS_REST_URL` | Upstash Redis REST URL for rate limiting | +| `UPSTASH_REDIS_REST_TOKEN` | Upstash Redis REST Token for rate limiting | +| `GEMINI_API_KEY` | Google Gemini API key for AI chatbot | +| `AUTO_CONFIRM_EMAIL` | Auto confirm user email in Supabase (if set) | +| `NEXT_PUBLIC_SHOW_COMMUNITY_BADGE` | Toggle visibility of community badge in UI | +| `NEXT_PUBLIC_USE_SPRING_BOOT_API` | Route requests to Java Spring Boot API locally | +| `NEXT_PUBLIC_SPRING_BOOT_API_URL` | URL of local Java Spring Boot API (default `http://localhost:8080`) | + +> ⚠️ **Never commit `.env.local` to version control.** It is already listed in `.gitignore`. + +## 6. Start the Development Server + +```bash +npm run dev +``` + +The application will start locally at `http://localhost:3000`. + +--- + +# ↪️ Development Workflow + +Follow the workflow below while contributing to the project. + +## 1. Create a New Branch + +Create a separate branch before making any changes. + +**Syntax** + +```bash +git checkout -b feature/your-feature-name +``` + +**Example** + +```bash +git checkout -b fix/navbar-responsive-issue +``` + +--- + +## 2. Make Your Changes + +You can now start working on: + +- Bug fixes +- UI/UX improvements +- Documentation updates +- Performance enhancements +- New visualizers + +--- + +## 3. Commit Your Changes + +Write clear and meaningful commit messages. + +**Syntax** ```bash -git checkout -b feature-name +git commit -m "type: short-description" ``` -4. Make your changes -5. Commit your changes +**Example** ```bash -git commit -m "Added: your feature description" +git commit -m "fix: improve navbar responsiveness" ``` -6. Push to GitHub +**Recommended Commit Types** + +| Type | Purpose | +| ---------- | ------------------------ | +| `feat` | New feature | +| `fix` | Bug fix | +| `docs` | Documentation updates | +| `style` | UI/styling changes | +| `refactor` | Code improvements | +| `perf` | Performance optimization | + +--- + +## 4. Push Your Changes + +Push your branch to your forked repository. ```bash -git push origin feature-name +git push origin feature/your-branch-name ``` -7. Open a Pull Request +--- + +## 5. Open a Pull Request + +After pushing your changes: + +1. Open your fork on GitHub +2. Click **Compare & Pull Request** +3. Add a clear title and description +4. Submit the pull request + +--- + +# ↪️ Issue Assignment Process + +To ensure fair and efficient issue management, please follow these steps: + +1. **Browse open issues** — Check the [Issues](https://github.com/PankajSingh34/AlgoBuddy/issues) tab for tasks labelled `good first issue` or `help wanted`. +2. **Comment to request assignment** — Leave a comment on the issue you'd like to work on (e.g., *"I'd like to work on this"*). Do not open a PR without being assigned first. +3. **Wait for assignment** — A maintainer will assign the issue to you. Work will only be reviewed from the assigned contributor. +4. **Submit within the deadline** — If a deadline is mentioned on the issue, please try to submit your PR within that timeframe. If you need more time, let us know in the issue thread. +5. **Avoid duplicate work** — Before starting, check that no one else is already assigned to the same issue. + +> If you find a bug or want to suggest a feature that isn't already an issue, please open one first before working on it. + +--- + +# ↪️ Pull Request Guidelines + +Before submitting a PR: + +- Ensure the project builds and runs correctly (`npm run dev`) +- Test changes on multiple screen sizes (mobile, tablet, desktop) +- Follow clean coding practices +- Avoid committing unnecessary files (e.g., `.env.local`, `node_modules`) +- Use meaningful commit messages following the conventions above +- Keep PRs focused on a single issue or topic +- Add screenshots or screen recordings for UI-related changes + +--- + +# ↪️ Reporting Issues + +When creating issues, please include: + +- A clear, descriptive title +- A proper explanation of the problem +- Steps to reproduce the issue +- Expected vs. actual behaviour +- Screenshots or error logs, if applicable + +--- + +# ↪️ Need Help? + +If you need help while contributing: + +- **Open an issue** on GitHub +- **Start a discussion** in the [Discussions](https://github.com/PankajSingh34/AlgoBuddy/discussions) tab +- **Ask in Discord** — [discord.gg/Gv2N4U3KAc](https://discord.gg/Gv2N4U3KAc) + +We're happy to help new contributors! -# Guidelines +--- -1. Follow clean code practices -2. Proper commit messages -3. Test your code before PR -4. No plagiarism in blog content +# ↪️ Thank You -## Happy Contributing! 🚀 +Thank you for contributing to AlgoBuddy 💙 -Made with ❤️ by Sohan Rout +Your contributions help make learning Data Structures & Algorithms more interactive and accessible for everyone. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..74360cb9e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +# Build Stage +FROM eclipse-temurin:21-jdk-alpine AS build +WORKDIR /app + +# Copy maven executable and configuration from the backend folder +COPY backend/mvnw . +COPY backend/.mvn .mvn +COPY backend/pom.xml . + +# Copy source code from the backend folder +COPY backend/src src + +# Make mvnw executable and build the application +RUN chmod +x ./mvnw +RUN ./mvnw clean package -DskipTests + +# Run Stage +FROM eclipse-temurin:21-jre-alpine +WORKDIR /app + +# Copy the built jar from the build stage +COPY --from=build /app/target/*.jar app.jar + +# Expose the port the app runs on +EXPOSE 8080 + +# Run the jar file +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/EnvExample.txt b/EnvExample.txt deleted file mode 100755 index 4cafc9f01..000000000 --- a/EnvExample.txt +++ /dev/null @@ -1,10 +0,0 @@ -EMAIL_USER=Your App Email -EMAIL_PASSWORD=Your Google App Password -NEXT_PUBLIC_GA_ID=Your Google Analytics ID - -NEXT_PUBLIC_SUPABASE_URL=Your supabase Url -NEXT_PUBLIC_SUPABASE_ANON_KEY=Your Anon Key -NEXT_PUBLIC_TURNSTILE_SITE_KEY=Your Cloudfare Captcha Key - -TURNSTILE_SECRET_KEY=Your Cloudfare backend route api key -SUPABASE_SERVICE_KEY=Your supabase service key \ No newline at end of file diff --git a/ISSUE_CANDIDATES_AUTOMATION.md b/ISSUE_CANDIDATES_AUTOMATION.md new file mode 100644 index 000000000..c00ed9af4 --- /dev/null +++ b/ISSUE_CANDIDATES_AUTOMATION.md @@ -0,0 +1,36 @@ +# Issue Candidates + +1. Title: test : add unit tests for storage.js + Type: test + Files: src/utils/storage.js, __tests__/storage.test.js + Summary: Add unit tests for saveToStorage, loadFromStorage, and removeFromStorage utilities which handle localStorage serialization and error handling. + Verification: npx jest __tests__/storage.test.js + Conflict risk: low + +2. Title: test : add unit tests for apiErrors.js + Type: test + Files: src/lib/apiErrors.js, __tests__/apiErrors.test.js + Summary: Add unit tests for ApiError, AuthError, RateLimitError, ValidationError, and ConfigError class constructors and property assignments. + Verification: npx jest __tests__/apiErrors.test.js + Conflict risk: low + +3. Title: test : add unit tests for sortingGenerators.js + Type: test + Files: src/utils/sortingGenerators.js, __tests__/sortingGenerators.test.js + Summary: Add unit tests for all six generator-based sorting algorithm implementations (bubble, selection, insertion, merge, quick, heap) to verify correct step yield behavior. + Verification: npx jest __tests__/sortingGenerators.test.js + Conflict risk: low + +4. Title: test : add unit tests for cookieConsent.js + Type: test + Files: src/lib/cookieConsent.js, __tests__/cookieConsent.test.js + Summary: Add unit tests for getStoredPreferences, saveStoredPreferences, hasAnalyticsConsent, hasMarketingConsent, and hasFunctionalConsent utilities. + Verification: npx jest __tests__/cookieConsent.test.js + Conflict risk: low + +5. Title: test : add unit tests for shared-utils.js + Type: test + Files: src/lib/shared-utils.js, __tests__/sharedUtils.test.js + Summary: Add unit tests for isValidHttpUrl, escapeHtml, and getSupabaseConfig utilities including edge cases for URL validation and HTML escaping. + Verification: npx jest __tests__/sharedUtils.test.js + Conflict risk: low diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..d67bdeeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Pankaj Singh + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 515a977aa..ec81a84ff 100755 --- a/README.md +++ b/README.md @@ -1,53 +1,703 @@ -

AlgoBuddy

+
-

- Visualize & Learn DSA Algorithms with Animations and Interactive UI. -

+ + ---- +
+ +[![Live Demo](https://img.shields.io/badge/Live%20Demo-algobuddy.me-6366f1?style=for-the-badge&logoColor=white)](https://algobuddy.me) +[![License: MIT](https://img.shields.io/badge/License-MIT-22c55e?style=for-the-badge)](LICENSE) +[![CI](https://github.com/PankajSingh34/AlgoBuddy/actions/workflows/test.yml/badge.svg)](https://github.com/PankajSingh34/AlgoBuddy/actions/workflows/test.yml) +[![Security Policy](https://img.shields.io/badge/Security-Policy-red?style=for-the-badge&logo=shieldsdotio&logoColor=white)](SECURITY.md) +[![Stars](https://img.shields.io/github/stars/PankajSingh34/AlgoBuddy?style=for-the-badge&color=f59e0b)](https://github.com/PankajSingh34/AlgoBuddy/stargazers) +[![Forks](https://img.shields.io/github/forks/PankajSingh34/AlgoBuddy?style=for-the-badge&color=6366f1)](https://github.com/PankajSingh34/AlgoBuddy/forks) +[![PRs Welcome](https://img.shields.io/badge/PRs-Welcome-ec4899?style=for-the-badge)](CONTRIBUTING.md) +[![Discord](https://img.shields.io/badge/Discord-Join%20Us-5865F2?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/Gv2N4U3KAc) + +
+ +> **An open-source, interactive DSA learning platform that brings algorithms to life through step-by-step animations, structured learning paths, and progress tracking.** +> +> Built for students, developers, and interview candidates who want to **see** how algorithms work — not just read about them. + +[**Features**](#features) · [**Screenshots**](#screenshots) · [**Tech Stack**](#tech-stack) · [**Quick Start**](#quick-start) · [**Project Structure**](#project-structure) · [**Contributing**](#contributing) · [**License**](#license) + +
+ +
+ +## Table of Contents + +- [Why AlgoBuddy?](#why-algobuddy) +- [Features](#features) + - [Algorithm Visualizer](#algorithm-visualizer) + - [User System & Progress Tracking](#user-system--progress-tracking) + - [Blog Platform](#blog-platform) + - [UX & Design](#ux--design) +- [Supported Algorithms & Data Structures](#supported-algorithms--data-structures) +- [Screenshots](#screenshots) +- [Tech Stack](#tech-stack) +- [Architecture](#architecture) +- [Quick Start](#quick-start) + - [Prerequisites](#prerequisites) + - [1. Clone the Repository](#1-clone-the-repository) + - [2. Install Dependencies](#2-install-dependencies) + - [3. Configure Database Schema](#3-configure-database-schema) + - [4. Configure Environment Variables](#4-configure-environment-variables) + - [5. Start the Development Server](#5-start-the-development-server) + - [6. Other Commands](#6-other-commands) +- [Project Structure](#project-structure) +- [Project Roadmap](#project-roadmap) +- [Contributing](#contributing) +- [Community](#community) +- [Star History](#star-history) +- [Contributors](#contributors) +- [License](#license) + +
+ +## Why AlgoBuddy? -

- License: Apache 2.0 -

-

-

Product Built Using:

- - - -

+> *"Tell me and I forget, teach me and I remember, involve me and I learn."* — Benjamin Franklin + +Most DSA resources are walls of text and static diagrams. **AlgoBuddy changes that** by letting you interact with every data structure and algorithm in real time. + +Tools like [VisuAlgo](https://visualgo.net/) show animations but are read-only. AlgoBuddy goes further: algorithms are coupled with **user progress tracking, streaks, AI-assisted explanations, and a practice sheet** — so it functions as a learning system, not just a reference. It's also fully open-source, so every visualizer is a contribution opportunity. + + + + +
+ +**The Problem** +- Static textbooks don't show algorithm flow +- Copying code doesn't build understanding +- No feedback loop on what you've mastered +- Hard to stay motivated without visible progress + + + +**The AlgoBuddy Way** +- **Watch** algorithms execute step-by-step +- **Interact** with data structures directly +- **Track** your learning journey with streaks +- **Read** companion blogs for deeper theory + +
+ +
## Features -- DSA Algorithm Visualizer -- Custom Array Input -- Linear Search & Binary Search -- All Sorting Algorithms (Except Heap Sort) -- Stack Visualizer -- Dark / Light Mode Toggle -- Responsive Design (In Progress) -- Blog Explanations via Medium (Upcoming) -- Queue Visualizer (Upcoming) +### Algorithm Visualizer + +Animated, step-by-step visualizations for a wide range of DSA topics: + +
+Sorting, Searching, Stack, Queue, Linked List + + + + + + + + + + + + + + + + +
+ +**Sorting** + + +**Searching** + + +**Stack** + + +**Queue** + + +**Linked List** +
+ +- Bubble Sort +- Insertion Sort +- Selection Sort +- Merge Sort +- Quick Sort +- Shell Sort +- Radix Sort +- Counting Sort + + + +- Linear Search +- Binary Search +- Comparison Mode +- Sliding Window + + + +- Push / Pop +- Peek / isEmpty +- Polish Notation +- Array & LL impl. + + + +- Enqueue / Dequeue +- Circular Queue +- Priority Queue +- Double-ended +- Array & LL impl. + + + +- Singly Linked +- Doubly Linked +- Circular +- Insert / Delete +- Reverse / Merge + +
+ +
+ +
+Trees, HashMap, Graph, String, Complexity + + + + + + + + + + + + + + + + +
+ +**Trees** + + +**HashMap** + + +**Graph** + + +**String** + + +**Complexity** +
+ +- Binary Tree types +- In-order Traversal +- BST operations +- Heaps & Tries + + + +- Insert / Search / Delete +- Collision handling +- Visual hash buckets + + + +- BFS / DFS +- Dijkstra / A* +- Bellman-Ford +- Floyd-Warshall +- Kruskal / Prim +- Tarjan / Kosaraju +- Ford-Fulkerson +- Topological Sort + + + +- KMP Algorithm +- Z-Algorithm + + + +- Time & Space analysis +- Side-by-side comparisons +- Powered by Recharts + +
+ +
+ +
+ +### User System & Progress Tracking + +| Feature | Description | +|---|---| +| **Auth** | Email/password with Cloudflare Turnstile captcha + Google OAuth | +| **Dashboard** | Module-level progress tracking per data structure | +| **Streaks** | Activity heatmap (last 90 days) + daily streak counter | +| **AI Assistant** | Built-in chatbot powered by Gemini for concept help | +| **Bookmarks** | Save and revisit problems with the bookmark system | + +
+ +### Blog Platform + +| Feature | Description | +|---|---| +| **Categories** | Filter articles by DSA topic | +| **Full-text Search** | Instantly find relevant articles | +| **Reading Time** | Estimated reading time on every article | +| **Rich Content** | In-depth articles on core DSA concepts | + +
+ +### UX & Design + +| Feature | Description | +|---|---| +| **Dark/Light Mode** | Theme toggle persisted to `localStorage` | +| **Responsive** | Optimized for mobile, tablet, and desktop | +| **Animations** | Smooth visualizations via GSAP + Framer Motion | +| **Particle Effects** | Interactive background using tsParticles | + +
+ +## Supported Algorithms & Data Structures + +| Category | Coverage | Visualization | +|----------|----------|:-------------:| +| Sorting | Bubble, Selection, Insertion, Merge, Quick, Shell, Radix, Counting | ✓ | +| Searching | Linear, Binary, Sliding Window | ✓ | +| Stack | Push, Pop, Peek, Array & Linked List | ✓ | +| Queue | Simple, Circular, Priority, Deque | ✓ | +| Linked List | Singly, Doubly, Circular | ✓ | +| Trees | Binary Tree, BST, Heap, Trie | ✓ | +| HashMap | Insert, Search, Delete, Collision Handling | ✓ | +| Graph | BFS, DFS, Dijkstra, A*, Bellman-Ford, Floyd-Warshall, Kruskal, Prim, Tarjan, Kosaraju, Ford-Fulkerson, Topological Sort | ✓ | +| String | KMP Algorithm, Z-Algorithm | ✓ | +| Complexity Analysis | Time & Space Complexity Graphs | ✓ | + +## Screenshots + +![Home Page](public/screenshots/Home-page.png) + +*Landing page with algorithm category navigation, feature overview, and community stats.* + +
+ +![Visualizer](public/screenshots/visualizer-page.png) + +*Step-by-step algorithm visualizer with controls, pseudocode panel, and complexity info.* + +
+ +
+Authentication Page — Login and signup with Google OAuth or email/password, protected by Cloudflare Turnstile. + +![Login Page](public/screenshots/login-page.png) + +
+ +
+Queue Visualization — Animated circular queue showing enqueue/dequeue operations with pointer movement. + +![Queue Visualization](public/screenshots/queue-visualization-page.png) + +
+ +
+Queue Operations — Side-by-side operations panel with live memory-state rendering. + +![Queue Operations](public/screenshots/queue-operations-page.png) + +
+ + +
-## Future Plans +## Tech Stack -- Make the platform interactive -- Improve Mobile Responsiveness -- Social Media Promotion +
-## Connect With Me +| Layer | Technology | Purpose | +|:---|:---|:---| +| **Framework** | ![Next.js](https://img.shields.io/badge/Next.js_16-000?logo=nextdotjs&logoColor=white) | App Router, SSR, API routes | +| **Styling** | ![Tailwind](https://img.shields.io/badge/Tailwind_CSS-06B6D4?logo=tailwindcss&logoColor=white) | Utility-first CSS framework | +| **Database** | ![Supabase](https://img.shields.io/badge/Supabase-3ECF8E?logo=supabase&logoColor=white) | PostgreSQL + Auth + Realtime | +| **Animation** | ![GSAP](https://img.shields.io/badge/GSAP-88CE02?logo=greensock&logoColor=white) ![Framer](https://img.shields.io/badge/Framer_Motion-0055FF?logo=framer&logoColor=white) | Visualizer animations | +| **Charts** | ![Recharts](https://img.shields.io/badge/Recharts-FF6384?logo=chartdotjs&logoColor=white) | Complexity comparison graphs | +| **Editor** | ![Monaco](https://img.shields.io/badge/Monaco_Editor-007ACC?logo=visualstudiocode&logoColor=white) | In-browser code editor | +| **Email** | ![Nodemailer](https://img.shields.io/badge/Nodemailer-339933?logo=gmail&logoColor=white) | Transactional emails via Gmail | +| **Captcha** | ![Cloudflare](https://img.shields.io/badge/Turnstile-F38020?logo=cloudflare&logoColor=white) | Bot protection on auth | +| **Analytics** | ![GA4](https://img.shields.io/badge/Google_Analytics_4-E37400?logo=googleanalytics&logoColor=white) | Usage tracking | +| **Rate Limiting** | ![Upstash](https://img.shields.io/badge/Upstash_Redis-DC382D?logo=redis&logoColor=white) | API rate limiting | +| **Deployment** | ![Vercel](https://img.shields.io/badge/Vercel-000?logo=vercel&logoColor=white) | Serverless hosting | +| **CI/CD** | ![GitHub Actions](https://img.shields.io/badge/GitHub_Actions-2088FF?logo=githubactions&logoColor=white) | Multi-OS testing pipeline | - - LinkedIn +
+ +
+ +## Architecture + +For a comprehensive guide on component boundaries, database triggers, data flows, and codebase principles, see the [System Architecture Guide](./Architecture.md). + +```mermaid +graph TB + subgraph Client["Client — Next.js 16 App Router"] + UI["UI Components
(React + Tailwind)"] + VIS["Visualizer Engine
(GSAP + Framer Motion)"] + CHARTS["Complexity Graphs
(Recharts)"] + EDITOR["Code Editor
(Monaco)"] + THEME["Theme System
(Dark/Light)"] + end + subgraph API["API Layer"] + AUTH_API["Auth Routes"] + CONTACT["Contact API"] + REVIEW["Review API"] + CHATBOT["AI Assistant
(Gemini API)"] + end + subgraph Services["External Services"] + SUPA["Supabase
(DB + Auth)"] + CF["Cloudflare
Turnstile"] + GA["Google
Analytics"] + REDIS["Upstash
Redis"] + MAIL["Gmail
(Nodemailer)"] + GEMINI["Google
Gemini API"] + end + UI --> VIS + UI --> CHARTS + UI --> EDITOR + UI --> THEME + UI --> API + UI --> SUPA + AUTH_API --> SUPA + AUTH_API --> CF + CONTACT --> MAIL + CONTACT --> CF + REVIEW --> MAIL + REVIEW --> CF + CHATBOT --> REDIS + CHATBOT --> GEMINI + UI --> GA + style Client fill:#1e1b4b,stroke:#818cf8,stroke-width:3px,color:#e0e7ff + style API fill:#1e3a5f,stroke:#38bdf8,stroke-width:3px,color:#e0f2fe + style Services fill:#064e3b,stroke:#34d399,stroke-width:3px,color:#d1fae5 +``` + +
+ +## Quick Start + +### Prerequisites + +| Tool | Version | +|---|---| +| **Node.js** | `>= 20.x` | +| **npm** | `>= 10.x` | +| **Git** | Latest | + +### 1. Clone the Repository + +```bash +git clone https://github.com/PankajSingh34/AlgoBuddy.git +cd AlgoBuddy +``` + +### 2. Install Dependencies + +```bash +npm install +``` + +> **Note:** This project uses `isolated-vm` for secure code execution. If you encounter build errors, ensure you have Python and a C++ compiler installed (required for native addon compilation). + +### 3. Configure Database Schema + +The full schema is maintained in **[`supabase_setup.sql`](./supabase_setup.sql)** at the project root — run it in the Supabase SQL Editor. It covers RLS policies for all tables, admin policies for `community_contributors`, and two stored functions for atomic streak updates. + +> **Note:** Without running the full schema, progress tracking, bookmarks, avatars, and streak features will not work locally. + +
+View table overview and example RLS policy + +| Table | Purpose | +|---|---| +| `user_progress` | Tracks per-problem completion status | +| `user_activity` | Powers the 90-day activity heatmap | +| `user_profiles` | Avatar URL and community join status | +| `problem_bookmarks` | User-saved problem bookmarks | +| `user_practice_stats` | Streak counters (current + longest) | +| `community_contributors` | Public contributor registry | +| `topic_comments` | Per-visualizer discussion threads | +| `pending_messages` | SMTP fallback queue for contact/review emails | +| `newsletter_subscriptions` | Footer newsletter opt-ins | + +Representative RLS setup for `user_progress`: + +```sql +ALTER TABLE user_progress ENABLE ROW LEVEL SECURITY; + +CREATE POLICY "Users can read own progress" ON user_progress + FOR SELECT USING (auth.uid() = user_id); + +CREATE POLICY "Users can insert own progress" ON user_progress + FOR INSERT WITH CHECK (auth.uid() = user_id); + +CREATE POLICY "Users can update own progress" ON user_progress + FOR UPDATE USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id); +``` + +The schema also defines `increment_streak_on_completion()` and `upsert_progress_and_update_streak()` — two PL/pgSQL functions that update streaks atomically to avoid TOCTOU race conditions. See [`supabase_setup.sql`](./supabase_setup.sql) for the full definitions. + +
+ +### 4. Configure Environment Variables + +Create a `.env.local` file in the project root. See [`.env.example`](.env.example) for the full reference. + +
+View all environment variables + +```env +# ──────────── Email ──────────── +EMAIL_USER=your-email@gmail.com +EMAIL_PASSWORD=your-google-app-password +REVIEW_INBOX_EMAIL=optional-inbox@gmail.com + +# ──────────── Supabase ──────────── +NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co +NEXT_PUBLIC_SUPABASE_ANON_KEY=your-supabase-anon-key +SUPABASE_SERVICE_KEY=your-supabase-service-key + +# ──────────── Cloudflare Turnstile ──────────── +NEXT_PUBLIC_TURNSTILE_SITE_KEY=your-turnstile-site-key +TURNSTILE_SECRET_KEY=your-turnstile-secret-key + +# ──────────── Google Analytics ──────────── +NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX + +# ──────────── AI Chatbot ──────────── +GEMINI_API_KEY=your-gemini-api-key + +# ──────────── Rate Limiting (Production) ──────────── +UPSTASH_REDIS_REST_URL=your-upstash-url +UPSTASH_REDIS_REST_TOKEN=your-upstash-token + +# ──────────── Spring Boot Backend CORS (Optional in dev, required in prod) ──────────── +ALLOWED_ORIGINS=http://localhost:3000 +APP_ENV=dev +``` + +
+ +### 5. Start the Development Server + +```bash +npm run dev +``` + +Open **[http://localhost:3000](http://localhost:3000)** and start visualizing! + +### 6. Other Commands + +```bash +npm run build # Production build +npm run start # Start production server +npm run lint # Run ESLint +npm run test # Run lint + security tests +npm run test:security # Run XSS security tests only +``` + +
+ +## Project Structure + +
+View full directory tree + +``` +AlgoBuddy/ +│ +├── src/app/ # Next.js App Router +│ ├── api/ # API routes (auth, chatbot, mysheet, etc.) +│ ├── arena/ # Tournament / arena page +│ ├── dashboard/ # User dashboard +│ ├── login/ # Auth pages +│ ├── visualizer/ # Algorithm visualizer pages +│ │ ├── array/ # Sorting & array algorithms +│ │ ├── graph/ # Graph algorithm visualizers +│ │ ├── string/ # String algorithm visualizers (KMP, Z-Algo) +│ │ └── ... # Other DSA visualizers +│ │ +│ ├── components/ # Shared UI components +│ │ ├── dashboard/ # Heatmap, streaks +│ │ ├── models/ # Data structure models +│ │ └── ui/ # Reusable UI primitives +│ │ +│ ├── hooks/ # Custom React hooks +│ ├── layout.jsx # Root layout +│ └── page.jsx # Landing page +│ +├── src/features/algorithms/ # Algorithm logic (pure functions) +│ ├── graph/ # Graph algorithm implementations +│ └── string/ # String algorithm implementations +│ +├── src/lib/ # Utility libraries +│ ├── supabase.js # Supabase client config +│ ├── auth.js # Auth helpers +│ ├── activity.js # Activity tracking logic +│ └── gtag.js # Google Analytics helper +│ +├── backend/ # Spring Boot API (optional, for practice features) +├── arena-socket-server/ # WebSocket server for tournament arena +├── public/ # Static assets & screenshots +├── docs/ # Documentation +├── security-tests/ # Security test suite +├── .github/ # GitHub Actions workflows +│ +├── supabase_setup.sql # Database schema & RLS policies +├── middleware.js # Next.js middleware (auth, rate limiting) +├── next.config.mjs # Next.js configuration +├── tailwind.config.js # Tailwind configuration +└── package.json # Dependencies & scripts +``` + +
+ +
+ +## Project Roadmap + +| Status | Item | +|:------:|------| +| Done | Interactive algorithm visualizations | +| Done | User authentication and progress tracking | +| Done | AI-powered learning assistant | +| Done | Graph algorithm visualizers | +| Done | String algorithm visualizers (KMP, Z-Algorithm) | +| In Progress | Expand the collection of algorithm and data structure visualizations | +| In Progress | Improve accessibility and mobile experience | +| In Progress | Enhance learning resources and documentation | +| Planned | Introduce additional educational content and practice modules | +| Planned | Continue community-driven improvements and feature enhancements | + +## Contributing + +We welcome contributions! AlgoBuddy is built by the community, for the community. + +### Contribution Areas + +| Area | What you can do | +|---|---| +| **Bug Fixes** | Squash bugs and resolve issues | +| **UI/UX** | Improve responsiveness, accessibility, design | +| **New Visualizers** | Add new DSA visualizers & animations | +| **Documentation** | Improve guides, README, contributor docs | +| **Performance** | Optimize app performance & efficiency | +| **Themes** | Enhance dark/light mode experience | + +### Getting Started + +
+Step-by-step: fork, branch, commit, PR + +```bash +# 1. Fork this repo and clone your fork +git clone https://github.com/YOUR_USERNAME/AlgoBuddy.git + +# 2. Create a feature branch +git checkout -b feature/your-feature-name + +# 3. Make your changes and commit +git commit -m "feat: describe your change" + +# 4. Push and open a PR +git push origin feature/your-feature-name +``` + +
+ +> For detailed guidelines, please read our [**Contributing Guide**](CONTRIBUTING.md) and [**Code of Conduct**](CODE_OF_CONDUCT.md). + +### Issue Assignment Process + +1. Browse [**open issues**](https://github.com/PankajSingh34/AlgoBuddy/issues) or create a new one +2. Comment asking to be assigned +3. Wait for maintainer assignment before starting +4. Submit a PR referencing the issue number + +
+ +## Community + +
+ +[![Discord](https://img.shields.io/badge/Join_our_Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/Gv2N4U3KAc) + +Ask questions, share ideas, show off your contributions, and connect with fellow learners! + +
+ +
+ +## Star History + +
+ +If AlgoBuddy helped you learn, please consider giving it a star — it means a lot! + +[![Star History Chart](https://api.star-history.com/svg?repos=PankajSingh34/AlgoBuddy&type=Date)](https://star-history.com/#PankajSingh34/AlgoBuddy&Date) + +
+ +
+ +## Contributors + +
+ +
+ ## License -Apache 2.0 License — See `LICENSE` for details. +
-# Contribution +This project is licensed under the **MIT License** — see the [**LICENSE**](LICENSE) file for details. -See [CONTRIBUTING.md](./CONTRIBUTING.md) +
-Code of Conduct — [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md) +
--- + +
+ +**Built with ♥ by the AlgoBuddy community** + +[Website](https://www.algobuddy.me/) · [Discord](https://discord.gg/Gv2N4U3KAc) · [Issues](https://github.com/PankajSingh34/AlgoBuddy/issues) · [Pull Requests](https://github.com/PankajSingh34/AlgoBuddy/pulls) + +
diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..4012fcbdd --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,43 @@ +# Security Policy + +## Supported Versions + +The following versions of **AlgoBuddy** are currently supported with security updates: + +| Version | Supported | +| ------- | ------------------ | +| main | ✅ Yes | + +## Reporting a Vulnerability + +If you discover a security vulnerability in **AlgoBuddy**, please **do not open a public issue**. + +Instead, report it responsibly by: + +- 📧 Opening a **GitHub Private Security Advisory** +- 📧 Reaching out to the maintainer directly via their [GitHub profile](https://github.com/PankajSingh34) +- 💬 Sending a private message through GitHub's messaging or social links listed in the profile + +### What to include in your report: +- A clear description of the vulnerability +- Steps to reproduce the issue +- Potential impact assessment +- Any suggested fix (optional but appreciated) + +## Response Timeline + +| Action | Timeframe | +| ----------------------------- | ----------------- | +| Acknowledgement of report | Within 48 hours | +| Status update | Within 7 days | +| Patch / fix release | Within 30 days | + +## Responsible Disclosure + +We follow a **responsible disclosure** policy. Please give us adequate time to patch the issue before any public disclosure. We deeply appreciate security researchers who help keep **AlgoBuddy** safe. 🙏 + +## References + +- [AlgoBuddy Repository](https://github.com/PankajSingh34/AlgoBuddy) +- [GitHub Security Advisories](https://docs.github.com/en/code-security/security-advisories) +- [Adding a Security Policy to your repo](https://docs.github.com/en/code-security/getting-started/adding-a-security-policy-to-your-repository) diff --git a/__tests__/activityStreak.test.mjs b/__tests__/activityStreak.test.mjs new file mode 100644 index 000000000..9aaab015c --- /dev/null +++ b/__tests__/activityStreak.test.mjs @@ -0,0 +1,90 @@ +import { afterEach, describe, expect, jest, test } from "@jest/globals"; +import { computeStreak } from "../src/lib/activity.js"; + +const fixedNow = new Date("2026-06-19T12:00:00Z"); + +function activity(date, field = "activity_date") { + return { [field]: `${date}T08:00:00Z` }; +} + +describe("computeStreak", () => { + afterEach(() => { + jest.useRealTimers(); + }); + + test("returns 0 for empty input", () => { + expect(computeStreak([])).toBe(0); + expect(computeStreak(null)).toBe(0); + }); + + test("counts a single activity today", () => { + jest.useFakeTimers().setSystemTime(fixedNow); + + expect(computeStreak([activity("2026-06-19")])).toBe(1); + }); + + test("counts consecutive days ending today", () => { + jest.useFakeTimers().setSystemTime(fixedNow); + + expect( + computeStreak([ + activity("2026-06-19"), + activity("2026-06-18"), + activity("2026-06-17"), + ]), + ).toBe(3); + }); + + test("counts consecutive days ending yesterday", () => { + jest.useFakeTimers().setSystemTime(fixedNow); + + expect( + computeStreak([ + activity("2026-06-18"), + activity("2026-06-17"), + activity("2026-06-16"), + ]), + ).toBe(3); + }); + + test("stops counting at the first date gap", () => { + jest.useFakeTimers().setSystemTime(fixedNow); + + expect( + computeStreak([ + activity("2026-06-19"), + activity("2026-06-18"), + activity("2026-06-16"), + ]), + ).toBe(2); + }); + + test("returns 0 when the last activity is older than yesterday", () => { + jest.useFakeTimers().setSystemTime(fixedNow); + + expect(computeStreak([activity("2026-06-17")])).toBe(0); + }); + + test("deduplicates multiple activities on the same day", () => { + jest.useFakeTimers().setSystemTime(fixedNow); + + expect( + computeStreak([ + activity("2026-06-19"), + activity("2026-06-19"), + activity("2026-06-18"), + ]), + ).toBe(2); + }); + + test("supports created_at when activity_date is absent", () => { + jest.useFakeTimers().setSystemTime(fixedNow); + + expect( + computeStreak([ + activity("2026-06-19", "created_at"), + activity("2026-06-18", "created_at"), + ]), + ).toBe(2); + }); +}); diff --git a/__tests__/components/AlgorithmComparator.test.jsx b/__tests__/components/AlgorithmComparator.test.jsx new file mode 100644 index 000000000..bd2466286 --- /dev/null +++ b/__tests__/components/AlgorithmComparator.test.jsx @@ -0,0 +1,47 @@ +import React from "react"; +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import AlgorithmComparator from "@/app/visualizer/complexity-analyzer/components/AlgorithmComparator"; +import { toast } from "react-hot-toast"; + +jest.mock("react-hot-toast", () => ({ + toast: { + success: jest.fn(), + error: jest.fn(), + }, +})); + +describe("AlgorithmComparator copy controls", () => { + let writeTextMock; + + beforeEach(() => { + jest.clearAllMocks(); + writeTextMock = jest.fn().mockResolvedValue(undefined); + Object.defineProperty(global.navigator, "clipboard", { + value: { + writeText: writeTextMock, + }, + writable: true, + configurable: true, + }); + }); + + it("copies the exact time and space complexity values", async () => { + render( + + ); + + fireEvent.click(screen.getByRole("button", { name: /copy time complexity for binary search/i })); + fireEvent.click(screen.getByRole("button", { name: /copy space complexity for binary search/i })); + + await waitFor(() => { + expect(writeTextMock).toHaveBeenNthCalledWith(1, "O(log n)"); + expect(writeTextMock).toHaveBeenNthCalledWith(2, "O(1)"); + expect(toast.success).toHaveBeenCalledTimes(2); + expect(toast.success).toHaveBeenCalledWith("Copied to clipboard!"); + }); + }); +}); \ No newline at end of file diff --git a/__tests__/components/CodeBlock.test.jsx b/__tests__/components/CodeBlock.test.jsx new file mode 100644 index 000000000..d57480b42 --- /dev/null +++ b/__tests__/components/CodeBlock.test.jsx @@ -0,0 +1,90 @@ +import React from "react"; +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import CodeBlock from "@/app/components/ui/CodeBlock"; +import { toast } from "react-hot-toast"; + +jest.mock("framer-motion", () => { + const React = require("react"); + + const passthrough = (Tag) => ({ children, whileHover, whileTap, ...props }) => + React.createElement(Tag, props, children); + + return { + AnimatePresence: ({ children }) => React.createElement(React.Fragment, null, children), + motion: { + div: passthrough("div"), + button: passthrough("button"), + span: passthrough("span"), + }, + }; +}); + +jest.mock("react-hot-toast", () => ({ + toast: { + success: jest.fn(), + error: jest.fn(), + }, +})); + +const codeExamples = { + javascript: "function sum(arr) {\n return arr.reduce((acc, value) => acc + value, 0);\n}", + python: "def sum_values(arr):\n return sum(arr)", + java: "class SumValues {\n int sum(int[] arr) { return 0; }\n}", + c: "int sum(int* arr) {\n return 0;\n}", + cpp: "int sum(std::vector& arr) {\n return 0;\n}", +}; + +const fileNames = { + javascript: "sum.js", + python: "sum.py", + java: "Sum.java", + c: "sum.c", + cpp: "sum.cpp", +}; + +describe("CodeBlock copy button", () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.spyOn(console, "error").mockImplementation(() => {}); + }); + + afterEach(() => { + console.error.mockRestore?.(); + }); + + it("copies the currently displayed language code and shows success feedback", async () => { + const user = userEvent.setup(); + + render(); + + const copyButton = screen.getByRole("button", { name: /copy code/i }); + expect(copyButton).toHaveAttribute("aria-label", "Copy code"); + expect(copyButton.className).toContain("focus-visible:ring-2"); + + await user.click(screen.getByRole("button", { name: /python/i })); + expect( + screen.getByText((content, element) => { + return element?.tagName.toLowerCase() === "code" && element.textContent === codeExamples.python; + }) + ).toBeInTheDocument(); + await user.click(screen.getByRole("button", { name: /copy code/i })); + + await waitFor(() => expect(toast.success).toHaveBeenCalledWith("Code copied!")); + }); + + it("shows an error toast when copying fails", async () => { + const user = userEvent.setup(); + + Object.defineProperty(navigator, "clipboard", { + value: undefined, + configurable: true, + }); + + render(); + + await user.click(screen.getByRole("button", { name: /copy code/i })); + + await waitFor(() => expect(toast.error).toHaveBeenCalledWith("Could not copy code.")); + }); +}); \ No newline at end of file diff --git a/__tests__/components/CopyButton.test.jsx b/__tests__/components/CopyButton.test.jsx new file mode 100644 index 000000000..027f44eb9 --- /dev/null +++ b/__tests__/components/CopyButton.test.jsx @@ -0,0 +1,47 @@ +import React from "react"; +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import CopyButton from "@/app/components/ui/CopyButton"; +import { toast } from "react-hot-toast"; + +jest.mock("react-hot-toast", () => ({ + toast: { + success: jest.fn(), + error: jest.fn(), + }, +})); + +describe("CopyButton", () => { + let writeTextMock; + + beforeEach(() => { + jest.clearAllMocks(); + writeTextMock = jest.fn().mockResolvedValue(undefined); + Object.defineProperty(global.navigator, "clipboard", { + value: { + writeText: writeTextMock, + }, + writable: true, + configurable: true, + }); + }); + + it("copies trimmed text and shows the success toast", async () => { + render( + + ); + + const button = screen.getByRole("button", { name: /copy time complexity/i }); + expect(button).toHaveAttribute("title", "Copy time complexity"); + expect(button.className).toContain("focus-visible:ring-2"); + + fireEvent.click(button); + + await waitFor(() => { + expect(writeTextMock).toHaveBeenCalledWith("O(n log n)"); + expect(toast.success).toHaveBeenCalledWith("Copied to clipboard!"); + }); + }); +}); \ No newline at end of file diff --git a/__tests__/components/ResponsiveControls.test.jsx b/__tests__/components/ResponsiveControls.test.jsx new file mode 100644 index 000000000..e3b8d774c --- /dev/null +++ b/__tests__/components/ResponsiveControls.test.jsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { ResponsiveControls } from '@/app/visualizer/components/ResponsiveControls'; + +describe('ResponsiveControls', () => { + it('renders children and is expanded by default', () => { + render( + +
Child Content
+
+ ); + + // The child should be in the document + expect(screen.getByTestId('child-content')).toBeInTheDocument(); + + // The toggle button should show "Hide Controls" initially + const toggleButton = screen.getByRole('button', { name: /hide controls/i }); + expect(toggleButton).toBeInTheDocument(); + expect(toggleButton).toHaveAttribute('aria-expanded', 'true'); + + // The controls container should not have "hidden" class (it has "block" when expanded) + const controlsContainer = document.getElementById('mobile-visualizer-controls'); + expect(controlsContainer).toBeInTheDocument(); + expect(controlsContainer.className).toContain('block'); + expect(controlsContainer.className).not.toContain('hidden'); + }); + + it('toggles visibility when the button is clicked', () => { + render( + +
Child Content
+
+ ); + + const toggleButton = screen.getByRole('button', { name: /hide controls/i }); + + // Click to collapse + fireEvent.click(toggleButton); + + // The button text should change to "Show Controls" + const newToggleButton = screen.getByRole('button', { name: /show controls/i }); + expect(newToggleButton).toBeInTheDocument(); + expect(newToggleButton).toHaveAttribute('aria-expanded', 'false'); + + // The controls container should now have "hidden" class + const controlsContainer = document.getElementById('mobile-visualizer-controls'); + expect(controlsContainer.className).toContain('hidden'); + + // Click again to expand + fireEvent.click(newToggleButton); + + // Should revert back to Hide Controls + const expandedButton = screen.getByRole('button', { name: /hide controls/i }); + expect(expandedButton).toBeInTheDocument(); + expect(expandedButton).toHaveAttribute('aria-expanded', 'true'); + expect(controlsContainer.className).toContain('block'); + }); + + it('starts collapsed if defaultExpanded is false', () => { + render( + +
Child Content
+
+ ); + + const toggleButton = screen.getByRole('button', { name: /show controls/i }); + expect(toggleButton).toBeInTheDocument(); + expect(toggleButton).toHaveAttribute('aria-expanded', 'false'); + + const controlsContainer = document.getElementById('mobile-visualizer-controls'); + expect(controlsContainer.className).toContain('hidden'); + }); +}); diff --git a/__tests__/components/activityApi.test.jsx b/__tests__/components/activityApi.test.jsx new file mode 100644 index 000000000..83b6a9a14 --- /dev/null +++ b/__tests__/components/activityApi.test.jsx @@ -0,0 +1,39 @@ +import { jest } from '@jest/globals'; + +jest.mock('@/lib/serverApi', () => ({ + getSupabaseServerClient: jest.fn(() => ({ + from: jest.fn(() => ({ + upsert: jest.fn().mockResolvedValue({ data: null, error: { message: 'new row violates row-level security policy', details: 'RLS policy', hint: 'policy', code: '42501' } }), + select: jest.fn().mockReturnThis(), + eq: jest.fn().mockReturnThis(), + gte: jest.fn().mockReturnThis(), + order: jest.fn().mockReturnThis(), + limit: jest.fn().mockResolvedValue({ data: [], error: null }), + maybeSingle: jest.fn().mockResolvedValue({ data: null, error: null }), + })), + })), + jsonResponse: jest.fn((data, status = 200) => ({ data, status })), + errorResponse: jest.fn((error) => ({ error, status: 500 })), +})); + +jest.mock('@/lib/auth', () => ({ + getAuthenticatedUser: jest.fn().mockResolvedValue({ success: true, user: { id: 'user-1' } }), +})); + +jest.mock('next/headers', () => ({ + cookies: jest.fn().mockResolvedValue({ getAll: jest.fn(() => []), set: jest.fn() }), +})); + +const { POST, GET } = require('@/app/api/activity/route'); + +describe('activity API route', () => { + it('returns 403 when the server client hits an RLS error instead of falling back to admin', async () => { + const response = await POST({ json: async () => ({ type: 'site_visit', localDate: '2026-07-09' }) }); + expect(response.status).toBe(403); + }); + + it('returns activity rows via the server client', async () => { + const response = await GET({ url: 'http://localhost/api/activity?days=30' }); + expect(response.status).toBe(200); + }); +}); diff --git a/__tests__/components/notifications-route.test.js b/__tests__/components/notifications-route.test.js new file mode 100644 index 000000000..14cd59eed --- /dev/null +++ b/__tests__/components/notifications-route.test.js @@ -0,0 +1,104 @@ +jest.mock("next/headers", () => ({ + cookies: jest.fn(), +})); + +jest.mock("@/lib/auth", () => ({ + getAuthenticatedUser: jest.fn(), +})); + +jest.mock("@/lib/serverApi", () => ({ + getSupabaseServerClient: jest.fn(), + jsonResponse: (data, status = 200) => ({ + status, + json: jest.fn().mockResolvedValue(data), + }), + errorResponse: (error) => ({ + status: 500, + json: jest + .fn() + .mockResolvedValue({ error: error.message || "Internal server error" }), + }), +})); + +import { cookies } from "next/headers"; +import { getAuthenticatedUser } from "@/lib/auth"; +import { getSupabaseServerClient } from "@/lib/serverApi"; +import { PATCH } from "@/app/api/notifications/route"; + +function createSupabaseMock(result) { + const builder = { + update: jest.fn(() => builder), + eq: jest.fn(() => builder), + in: jest.fn(() => builder), + select: jest.fn(() => Promise.resolve(result)), + }; + + return { + builder, + supabase: { + from: jest.fn(() => builder), + }, + }; +} + +function createPatchRequest(body) { + return { + json: jest.fn().mockResolvedValue(body), + }; +} + +describe("notifications PATCH route", () => { + beforeEach(() => { + jest.clearAllMocks(); + + cookies.mockResolvedValue({ + getAll: jest.fn(() => []), + set: jest.fn(), + }); + + getAuthenticatedUser.mockResolvedValue({ + success: true, + user: { id: "student-123" }, + }); + }); + + it("returns the number of rows updated for markAll requests", async () => { + const { builder, supabase } = createSupabaseMock({ + data: [{ id: "n1" }, { id: "n2" }], + error: null, + count: null, + }); + getSupabaseServerClient.mockReturnValue(supabase); + + const response = await PATCH(createPatchRequest({ markAll: true })); + const body = await response.json(); + + expect(response.status).toBe(200); + expect(body).toEqual({ success: true, updated: 2 }); + expect(supabase.from).toHaveBeenCalledWith("notifications"); + expect(builder.update).toHaveBeenCalledWith({ read: true }); + expect(builder.eq).toHaveBeenCalledWith("student_id", "student-123"); + expect(builder.in).not.toHaveBeenCalled(); + expect(builder.select).toHaveBeenCalledWith("id"); + }); + + it("returns the number of rows updated for selected notification IDs", async () => { + const { builder, supabase } = createSupabaseMock({ + data: [{ id: "n2" }], + error: null, + count: null, + }); + getSupabaseServerClient.mockReturnValue(supabase); + + const response = await PATCH( + createPatchRequest({ notificationIds: ["n2", "", 42, " "] }) + ); + const body = await response.json(); + + expect(response.status).toBe(200); + expect(body).toEqual({ success: true, updated: 1 }); + expect(builder.eq).toHaveBeenCalledWith("student_id", "student-123"); + expect(builder.in).toHaveBeenCalledWith("id", ["n2"]); + expect(builder.select).toHaveBeenCalledWith("id"); + }); +}); diff --git a/__tests__/cookieConsent.test.js b/__tests__/cookieConsent.test.js new file mode 100644 index 000000000..24da5a2e3 --- /dev/null +++ b/__tests__/cookieConsent.test.js @@ -0,0 +1,191 @@ +// __tests__/cookieConsent.test.js +// +// Run with: npx jest __tests__/cookieConsent.test.js +// +// Tests the cookie consent helpers in src/lib/cookieConsent.js. +// Sets global.localStorage directly in beforeEach; module is required once +// at the top using the jsdom window/localStorage. + +const { + CONSENT_STATUS_KEY, + CONSENT_PREFERENCES_KEY, + CONSENT_UPDATED_EVENT, + DEFAULT_PREFERENCES, + getStoredPreferences, + saveStoredPreferences, + hasAnalyticsConsent, + hasMarketingConsent, + hasFunctionalConsent, +} = require("../src/lib/cookieConsent"); + +describe("cookieConsent constants", () => { + test("CONSENT_STATUS_KEY is a non-empty string", () => { + expect(typeof CONSENT_STATUS_KEY).toBe("string"); + expect(CONSENT_STATUS_KEY.length).toBeGreaterThan(0); + }); + + test("CONSENT_PREFERENCES_KEY is a non-empty string", () => { + expect(typeof CONSENT_PREFERENCES_KEY).toBe("string"); + expect(CONSENT_PREFERENCES_KEY.length).toBeGreaterThan(0); + }); + + test("CONSENT_UPDATED_EVENT is a non-empty string", () => { + expect(typeof CONSENT_UPDATED_EVENT).toBe("string"); + expect(CONSENT_UPDATED_EVENT.length).toBeGreaterThan(0); + }); + + test("DEFAULT_PREFERENCES has correct structure", () => { + expect(DEFAULT_PREFERENCES).toEqual({ + essential: true, + analytics: false, + functional: false, + marketing: false, + }); + }); +}); + +describe("getStoredPreferences", () => { + beforeEach(() => { + // Clear jsdom localStorage before each test + global.localStorage.clear(); + }); + + test("returns DEFAULT_PREFERENCES when localStorage is empty", () => { + expect(getStoredPreferences()).toEqual(DEFAULT_PREFERENCES); + }); + + test("returns merged preferences when localStorage has valid JSON", () => { + global.localStorage.setItem( + CONSENT_PREFERENCES_KEY, + JSON.stringify({ analytics: true, marketing: true }) + ); + const result = getStoredPreferences(); + expect(result.analytics).toBe(true); + expect(result.marketing).toBe(true); + }); + + test("always forces essential to true even if stored value is false", () => { + global.localStorage.setItem( + CONSENT_PREFERENCES_KEY, + JSON.stringify({ essential: false, analytics: true }) + ); + const result = getStoredPreferences(); + expect(result.essential).toBe(true); + expect(result.analytics).toBe(true); + }); + + test("returns DEFAULT_PREFERENCES on invalid JSON", () => { + global.localStorage.setItem(CONSENT_PREFERENCES_KEY, "not-valid-json{"); + expect(getStoredPreferences()).toEqual(DEFAULT_PREFERENCES); + }); +}); + +describe("saveStoredPreferences", () => { + let dispatchEventSpy; + + beforeEach(() => { + global.localStorage.clear(); + dispatchEventSpy = jest.spyOn(global.window, "dispatchEvent"); + }); + + afterEach(() => { + dispatchEventSpy.mockRestore(); + }); + + test("saves JSON-serialized preferences to localStorage", () => { + const prefs = { analytics: true, marketing: false }; + saveStoredPreferences(prefs); + const stored = global.localStorage.getItem(CONSENT_PREFERENCES_KEY); + const parsed = JSON.parse(stored); + expect(parsed.analytics).toBe(true); + expect(parsed.marketing).toBe(false); + }); + + test("always saves essential as true regardless of input", () => { + saveStoredPreferences({ essential: false, analytics: true }); + const stored = global.localStorage.getItem(CONSENT_PREFERENCES_KEY); + const parsed = JSON.parse(stored); + expect(parsed.essential).toBe(true); + }); + + test("dispatches cookiePreferencesUpdated event", () => { + saveStoredPreferences({ analytics: true }); + expect(dispatchEventSpy).toHaveBeenCalledWith( + expect.objectContaining({ type: CONSENT_UPDATED_EVENT }) + ); + }); +}); + +describe("hasAnalyticsConsent", () => { + beforeEach(() => { + global.localStorage.clear(); + }); + + test("returns true when analytics is true", () => { + global.localStorage.setItem( + CONSENT_PREFERENCES_KEY, + JSON.stringify({ analytics: true }) + ); + expect(hasAnalyticsConsent()).toBe(true); + }); + + test("returns false when analytics is false", () => { + global.localStorage.setItem( + CONSENT_PREFERENCES_KEY, + JSON.stringify({ analytics: false }) + ); + expect(hasAnalyticsConsent()).toBe(false); + }); + + test("returns false when analytics is not in stored preferences", () => { + global.localStorage.setItem( + CONSENT_PREFERENCES_KEY, + JSON.stringify({ marketing: true }) + ); + expect(hasAnalyticsConsent()).toBe(false); + }); +}); + +describe("hasMarketingConsent", () => { + beforeEach(() => { + global.localStorage.clear(); + }); + + test("returns true when marketing is true", () => { + global.localStorage.setItem( + CONSENT_PREFERENCES_KEY, + JSON.stringify({ marketing: true }) + ); + expect(hasMarketingConsent()).toBe(true); + }); + + test("returns false when marketing is false", () => { + global.localStorage.setItem( + CONSENT_PREFERENCES_KEY, + JSON.stringify({ marketing: false }) + ); + expect(hasMarketingConsent()).toBe(false); + }); +}); + +describe("hasFunctionalConsent", () => { + beforeEach(() => { + global.localStorage.clear(); + }); + + test("returns true when functional is true", () => { + global.localStorage.setItem( + CONSENT_PREFERENCES_KEY, + JSON.stringify({ functional: true }) + ); + expect(hasFunctionalConsent()).toBe(true); + }); + + test("returns false when functional is false", () => { + global.localStorage.setItem( + CONSENT_PREFERENCES_KEY, + JSON.stringify({ functional: false }) + ); + expect(hasFunctionalConsent()).toBe(false); + }); +}); diff --git a/__tests__/fenwickTreeLogic.test.js b/__tests__/fenwickTreeLogic.test.js new file mode 100644 index 000000000..e50a80c6a --- /dev/null +++ b/__tests__/fenwickTreeLogic.test.js @@ -0,0 +1,80 @@ +import { buildBIT, updateGenerator, queryGenerator } from "../src/features/algorithms/tree/fenwickTreeLogic.js"; + +describe("fenwickTreeLogic", () => { + describe("buildBIT", () => { + it("should correctly build a 1-indexed BIT from an input array", () => { + // 1-indexed array: [0, 3, 2, -1, 6, 5, 4, -3, 3, 7, 2, 3] + const arr = [0, 3, 2, -1, 6, 5, 4, -3, 3, 7, 2, 3]; + const bit = buildBIT(arr); + + expect(bit.length).toBe(arr.length); + // Index 1 prefix sum = 3 + expect(bit[1]).toBe(3); + // Index 2 prefix sum = 3 + 2 = 5 + expect(bit[2]).toBe(5); + }); + }); + + describe("updateGenerator", () => { + it("yields an error step when index is out of bounds", () => { + const arr = [0, 1, 2, 3]; + const bit = buildBIT(arr); + const gen = updateGenerator(0, 5, arr, bit, 3); + const first = gen.next().value; + + expect(first.type).toBe("error"); + expect(first.message).toContain("Please enter a valid index"); + }); + + it("yields update steps and completes correctly for valid index and delta", () => { + const baseArray = [0, 1, 2, 3, 4]; // n = 4 + const bit = buildBIT(baseArray); + const gen = updateGenerator(2, 3, baseArray, bit, 4); + + const steps = []; + let res = gen.next(); + while (!res.done) { + steps.push(res.value); + res = gen.next(); + } + + const initialStep = steps[0]; + expect(initialStep.type).toBe("step"); + expect(initialStep.highlightedBIT).toEqual({ 2: "visiting" }); + + const lastStep = steps[steps.length - 1]; + expect(lastStep.type).toBe("complete"); + expect(lastStep.newBase[2]).toBe(5); // 2 + 3 + }); + }); + + describe("queryGenerator", () => { + it("yields error when range L > R or indices are invalid", () => { + const bit = [0, 1, 3, 3, 10]; + const gen = queryGenerator(3, 2, bit, 4); + const step = gen.next().value; + + expect(step.type).toBe("error"); + expect(step.message).toContain("valid range"); + }); + + it("calculates correct range sum for valid range L..R", () => { + const baseArray = [0, 2, 4, 6, 8, 10]; // 1-indexed values: 2, 4, 6, 8, 10 + const n = 5; + const bit = buildBIT(baseArray); + + // Range 2..4: sum of [4, 6, 8] = 18 + const gen = queryGenerator(2, 4, bit, n); + const steps = []; + let res = gen.next(); + while (!res.done) { + steps.push(res.value); + res = gen.next(); + } + + const completeStep = steps[steps.length - 1]; + expect(completeStep.type).toBe("complete"); + expect(completeStep.result).toBe("Sum[2..4] = 18"); + }); + }); +}); diff --git a/__tests__/graphUtility.test.mjs b/__tests__/graphUtility.test.mjs new file mode 100644 index 000000000..7a6e1da5f --- /dev/null +++ b/__tests__/graphUtility.test.mjs @@ -0,0 +1,163 @@ +import { describe, expect, test } from "@jest/globals"; +import { + bfsSteps, + buildAdjacencyList, + buildAdjacencyMatrix, + dfsSteps, + dijkstraSteps, + hasCycleDirected, + primSteps, + topologicalSort, +} from "../src/utils/graph.js"; + +describe("graph utility functions", () => { + test("buildAdjacencyList supports directed, undirected, and weighted edges", () => { + const edges = [ + { from: 0, to: 1, weight: 4 }, + { from: 1, to: 2, weight: 7 }, + ]; + + expect(buildAdjacencyList(3, edges, true)).toEqual({ + 0: [1], + 1: [2], + 2: [], + }); + + expect(buildAdjacencyList(3, edges, false, true)).toEqual({ + 0: [{ to: 1, weight: 4 }], + 1: [ + { to: 0, weight: 4 }, + { to: 2, weight: 7 }, + ], + 2: [{ to: 1, weight: 7 }], + }); + }); + + test("buildAdjacencyMatrix supports directed and undirected graphs", () => { + const edges = [{ from: 0, to: 2, weight: 5 }]; + + expect(buildAdjacencyMatrix(3, edges, true)).toEqual([ + [0, 0, 1], + [0, 0, 0], + [0, 0, 0], + ]); + + expect(buildAdjacencyMatrix(3, edges, false, true)).toEqual([ + [0, 0, 5], + [0, 0, 0], + [5, 0, 0], + ]); + }); + + test("bfsSteps records visited nodes and queue snapshots", () => { + const adj = { + 0: [1, 2], + 1: [3], + 2: [], + 3: [], + }; + + const steps = bfsSteps(adj, 0); + + expect(steps.map((step) => step.current)).toEqual([0, 1, 2, 3]); + expect([...steps.at(-1).visited]).toEqual([0, 1, 2, 3]); + expect(steps[1].queue).toEqual([2]); + }); + + test("dfsSteps records traversal order and visited nodes", () => { + const adj = { + 0: [1, 2], + 1: [3], + 2: [], + 3: [], + }; + + const steps = dfsSteps(adj, 0); + + expect(steps.map((step) => step.current)).toEqual([0, 1, 3, 2]); + expect([...steps.at(-1).visited]).toEqual([0, 1, 3, 2]); + }); + + test("dijkstraSteps converges shortest-path distances", () => { + const adj = buildAdjacencyList( + 4, + [ + { from: 0, to: 1, weight: 2 }, + { from: 0, to: 2, weight: 5 }, + { from: 1, to: 2, weight: 1 }, + { from: 2, to: 3, weight: 3 }, + ], + true, + true, + ); + + const steps = dijkstraSteps(adj, 0, 4); + + expect(steps.at(-1).distances).toEqual({ + 0: 0, + 1: 2, + 2: 3, + 3: 6, + }); + }); + + test("primSteps accumulates minimum spanning tree edges", () => { + const adj = buildAdjacencyList( + 4, + [ + { from: 0, to: 1, weight: 1 }, + { from: 0, to: 2, weight: 4 }, + { from: 1, to: 2, weight: 2 }, + { from: 1, to: 3, weight: 3 }, + ], + false, + true, + ); + + const steps = primSteps(adj, 0, 4); + + expect(steps.at(-1).mstEdges).toEqual([ + { from: 0, to: 1, weight: 1 }, + { from: 1, to: 2, weight: 2 }, + { from: 1, to: 3, weight: 3 }, + ]); + }); + + test("hasCycleDirected detects cyclic and acyclic directed graphs", () => { + const cyclic = { + 0: [1], + 1: [2], + 2: [0], + }; + const acyclic = { + 0: [1, 2], + 1: [2], + 2: [], + }; + + expect(hasCycleDirected(3, cyclic)).toBe(true); + expect(hasCycleDirected(3, acyclic)).toBe(false); + }); + + test("topologicalSort returns an order for DAGs and null for cycles", () => { + const dag = { + 0: [1, 2], + 1: [3], + 2: [3], + 3: [], + }; + const cyclic = { + 0: [1], + 1: [2], + 2: [0], + }; + + const order = topologicalSort(4, dag); + + expect(order.indexOf(0)).toBeLessThan(order.indexOf(1)); + expect(order.indexOf(0)).toBeLessThan(order.indexOf(2)); + expect(order.indexOf(1)).toBeLessThan(order.indexOf(3)); + expect(order.indexOf(2)).toBeLessThan(order.indexOf(3)); + expect(topologicalSort(3, cyclic)).toBeNull(); + }); +}); diff --git a/__tests__/gtag.test.mjs b/__tests__/gtag.test.mjs new file mode 100644 index 000000000..0298e1e19 --- /dev/null +++ b/__tests__/gtag.test.mjs @@ -0,0 +1,49 @@ +import { afterEach, describe, expect, jest, test } from "@jest/globals"; +import { event, GA_MEASUREMENT_ID, pageview } from "../src/lib/gtag.js"; + +const originalWindow = global.window; + +describe("Google Analytics helpers", () => { + afterEach(() => { + global.window = originalWindow; + jest.restoreAllMocks(); + }); + + test("pageview sends a config event with the page path", () => { + const gtag = jest.fn(); + global.window = { gtag }; + + pageview("/practice/arrays"); + + expect(gtag).toHaveBeenCalledWith("config", GA_MEASUREMENT_ID, { + page_path: "/practice/arrays", + }); + }); + + test("event sends the expected analytics payload", () => { + const gtag = jest.fn(); + global.window = { gtag }; + + event({ + action: "start_practice", + category: "practice", + label: "arrays", + value: 3, + }); + + expect(gtag).toHaveBeenCalledWith("event", "start_practice", { + event_category: "practice", + event_label: "arrays", + value: 3, + }); + }); + + test("helpers are no-ops when gtag is unavailable", () => { + global.window = {}; + + expect(() => pageview("/dashboard")).not.toThrow(); + expect(() => + event({ action: "open_dashboard", category: "navigation" }), + ).not.toThrow(); + }); +}); diff --git a/__tests__/modulesMap.test.js b/__tests__/modulesMap.test.js new file mode 100644 index 000000000..ffac4343f --- /dev/null +++ b/__tests__/modulesMap.test.js @@ -0,0 +1,138 @@ +// __tests__/modulesMap.test.js +// +// Run with: npx jest __tests__/modulesMap.test.js +// +// Tests the MODULE_MAPS data export in src/lib/modulesMap.js. +// Verifies that all expected algorithm and data-structure entries are present +// with correctly formatted UUIDs or kebab-case identifiers. + +const { MODULE_MAPS } = require("../src/lib/modulesMap"); + +const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +const KEBAB_REGEX = /^[a-z][a-z0-9-]*$/; +const ALPHANUM_REGEX = /^[a-z0-9][a-z0-9-]*$/; + +function isValidModuleId(id) { + return typeof id === "string" && id.length > 0; +} + +describe("MODULE_MAPS", () => { + test("is a non-null object", () => { + expect(MODULE_MAPS).not.toBeNull(); + expect(typeof MODULE_MAPS).toBe("object"); + }); + + test("has entries for all major algorithm categories", () => { + // Search algorithms + expect(MODULE_MAPS).toHaveProperty("linearSearch"); + expect(MODULE_MAPS).toHaveProperty("binarySearch"); + + // Sorting algorithms + expect(MODULE_MAPS).toHaveProperty("bubbleSort"); + expect(MODULE_MAPS).toHaveProperty("mergeSort"); + expect(MODULE_MAPS).toHaveProperty("quickSort"); + expect(MODULE_MAPS).toHaveProperty("insertionSort"); + expect(MODULE_MAPS).toHaveProperty("selectionSort"); + + // Data structures + expect(MODULE_MAPS).toHaveProperty("pushPop"); + expect(MODULE_MAPS).toHaveProperty("peek"); + expect(MODULE_MAPS).toHaveProperty("isEmpty"); + expect(MODULE_MAPS).toHaveProperty("enqueueDequeue"); + expect(MODULE_MAPS).toHaveProperty("queueArray"); + expect(MODULE_MAPS).toHaveProperty("stackArray"); + expect(MODULE_MAPS).toHaveProperty("trie"); + expect(MODULE_MAPS).toHaveProperty("redBlackTree"); + expect(MODULE_MAPS).toHaveProperty("bTree"); + expect(MODULE_MAPS).toHaveProperty("heapSort"); + + // Advanced / graph + expect(MODULE_MAPS).toHaveProperty("astar"); + }); + + test("every value is a non-empty string", () => { + for (const [key, value] of Object.entries(MODULE_MAPS)) { + expect(typeof value).toBe("string", `Key "${key}" should have a string value`); + expect(value.length).toBeGreaterThan(0, `Key "${key}" should not be an empty string`); + } + }); + + test("every value is either a valid UUID, kebab-case identifier, or an alphanumeric ID", () => { + for (const [key, value] of Object.entries(MODULE_MAPS)) { + const isUuid = UUID_REGEX.test(value); + const isKebab = KEBAB_REGEX.test(value); + const isAlphanum = /^[a-z0-9][a-z0-9-]*$/.test(value); + expect(isUuid || isKebab || isAlphanum).toBe(true, `Key "${key}" value "${value}" is not valid`); + } + }); + + test("all values are unique (no duplicate module IDs)", () => { + const values = Object.values(MODULE_MAPS); + const uniqueValues = new Set(values); + expect(uniqueValues.size).toBe(values.length); + }); + + test("linearSearch and binarySearch have UUIDs", () => { + expect(UUID_REGEX.test(MODULE_MAPS.linearSearch)).toBe(true); + expect(UUID_REGEX.test(MODULE_MAPS.binarySearch)).toBe(true); + }); + + test("ternarySearch and jumpSearch use kebab-case identifiers", () => { + expect(KEBAB_REGEX.test(MODULE_MAPS.ternarySearch)).toBe(true); + expect(KEBAB_REGEX.test(MODULE_MAPS.jumpSearch)).toBe(true); + }); + + test("bubbleSort and mergeSort have UUIDs", () => { + expect(UUID_REGEX.test(MODULE_MAPS.bubbleSort)).toBe(true); + expect(UUID_REGEX.test(MODULE_MAPS.mergeSort)).toBe(true); + }); + + test("countingSort has a UUID format ID", () => { + expect(UUID_REGEX.test(MODULE_MAPS.countingSort)).toBe(true); + }); + + test("bucketSort has a kebab-case or alphanumeric ID", () => { + const val = MODULE_MAPS.bucketSort; + expect(/^[a-z][a-z0-9-]*$/.test(val)).toBe(true); + }); + + test("pushPop and peek have UUIDs", () => { + expect(UUID_REGEX.test(MODULE_MAPS.pushPop)).toBe(true); + expect(UUID_REGEX.test(MODULE_MAPS.peek)).toBe(true); + }); + + test("isEmpty and isFull have UUIDs", () => { + expect(UUID_REGEX.test(MODULE_MAPS.isEmpty)).toBe(true); + expect(UUID_REGEX.test(MODULE_MAPS.isFull)).toBe(true); + }); + + test("stackArray and stackLinkedList have UUIDs", () => { + expect(UUID_REGEX.test(MODULE_MAPS.stackArray)).toBe(true); + expect(UUID_REGEX.test(MODULE_MAPS.stackLinkedList)).toBe(true); + }); + + test("queueArray and queueLinkedList have UUIDs", () => { + expect(UUID_REGEX.test(MODULE_MAPS.queueArray)).toBe(true); + expect(UUID_REGEX.test(MODULE_MAPS.queueLinkedList)).toBe(true); + }); + + test("recursion entries use UUID format", () => { + expect(UUID_REGEX.test(MODULE_MAPS.recursionFactorial)).toBe(true); + expect(UUID_REGEX.test(MODULE_MAPS.recursionFibonacci)).toBe(true); + expect(UUID_REGEX.test(MODULE_MAPS.recursionHanoi)).toBe(true); + }); + + test("has entries for all recursion problems", () => { + expect(MODULE_MAPS).toHaveProperty("recursionFactorial"); + expect(MODULE_MAPS).toHaveProperty("recursionFibonacci"); + expect(MODULE_MAPS).toHaveProperty("recursionHanoi"); + expect(MODULE_MAPS).toHaveProperty("recursionSum"); + expect(MODULE_MAPS).toHaveProperty("recursionReverseArray"); + expect(MODULE_MAPS).toHaveProperty("recursionPalindrome"); + expect(MODULE_MAPS).toHaveProperty("recursionBinarySearch"); + expect(MODULE_MAPS).toHaveProperty("recursionSubsequences"); + expect(MODULE_MAPS).toHaveProperty("recursionNQueens"); + expect(MODULE_MAPS).toHaveProperty("recursionPrint1ToN"); + expect(MODULE_MAPS).toHaveProperty("recursionPrintNTo1"); + }); +}); diff --git a/__tests__/persistence.test.js b/__tests__/persistence.test.js new file mode 100644 index 000000000..e70d03ed9 --- /dev/null +++ b/__tests__/persistence.test.js @@ -0,0 +1,73 @@ +import { persistence } from "../src/lib/persistence.js"; + +describe("PersistenceManager", () => { + beforeEach(() => { + localStorage.clear(); + }); + + describe("get, set, and remove", () => { + it("returns null for non-existent storage keys", async () => { + const result = await persistence.get("PRACTICE_PROGRESS"); + expect(result).toBeNull(); + }); + + it("stores and retrieves JSON object values correctly", async () => { + const data = { problem1: { status: "completed" } }; + persistence.set("PRACTICE_PROGRESS", data); + + const result = await persistence.get("PRACTICE_PROGRESS"); + expect(result).toEqual(data); + }); + + it("handles invalid JSON string gracefully by returning null", async () => { + localStorage.setItem("algobuddy_bookmarks", "invalid-json-{"); + const result = await persistence.get("BOOKMARKS"); + expect(result).toBeNull(); + }); + + it("removes items from localStorage", async () => { + persistence.set("THEME", "dark"); + expect(await persistence.get("THEME")).toBe("dark"); + + persistence.remove("THEME"); + expect(await persistence.get("THEME")).toBeNull(); + }); + }); + + describe("mergeProgress", () => { + it("merges local and server progress, giving precedence to newer server timestamp", () => { + const local = { + two_sum: { status: "in_progress", updatedAt: "2026-01-01T00:00:00Z" } + }; + const server = [ + { problem_id: "two_sum", status: "completed", updated_at: "2026-01-02T00:00:00Z" } + ]; + + const merged = persistence.mergeProgress(local, server, "user-123"); + expect(merged.two_sum.status).toBe("completed"); + }); + + it("retains local progress if local timestamp is newer than server timestamp", () => { + const local = { + two_sum: { status: "completed", updatedAt: "2026-02-01T00:00:00Z" } + }; + const server = [ + { problem_id: "two_sum", status: "in_progress", updated_at: "2026-01-01T00:00:00Z" } + ]; + + const merged = persistence.mergeProgress(local, server, "user-123"); + expect(merged.two_sum.status).toBe("completed"); + }); + }); + + describe("mergeBookmarks", () => { + it("deduplicates and merges local and server bookmark arrays", () => { + const local = [{ id: "1", title: "Two Sum" }, { id: "2", title: "3Sum" }]; + const server = [{ id: "2", title: "3Sum Updated" }, { id: "3", title: "4Sum" }]; + + const merged = persistence.mergeBookmarks(local, server, "id"); + expect(merged).toHaveLength(3); + expect(merged.find((b) => b.id === "2").title).toBe("3Sum Updated"); + }); + }); +}); diff --git a/__tests__/practiceData.test.js b/__tests__/practiceData.test.js new file mode 100644 index 000000000..7aacaa172 --- /dev/null +++ b/__tests__/practiceData.test.js @@ -0,0 +1,147 @@ +// __tests__/practiceData.test.js +// +// Run with: npx jest __tests__/practiceData.test.js +// +// Tests the practiceData structure in src/lib/practiceData.js. +// Verifies that all expected algorithm topics and difficulty tiers are present, +// and that required fields exist on each problem entry. + +const { practiceData } = require("../src/lib/practiceData"); + +const REQUIRED_PROBLEM_FIELDS = ["id", "name", "difficulty", "visualizerUrl"]; +const VALID_DIFFICULTIES = ["Easy", "Medium", "Hard"]; +const VALID_TIERS = ["Beginner", "Intermediate", "Advanced"]; + +describe("practiceData", () => { + test("is a non-null array", () => { + expect(Array.isArray(practiceData)).toBe(true); + expect(practiceData.length).toBeGreaterThan(0); + }); + + test("each topic has a title, slug, desc, and subsections", () => { + for (const topic of practiceData) { + expect(typeof topic.title).toBe("string", `"${topic.title}" should have a title`); + expect(typeof topic.slug).toBe("string", `"${topic.title}" should have a slug`); + expect(typeof topic.desc).toBe("string", `"${topic.title}" should have a description`); + expect(Array.isArray(topic.subsections)).toBe(true); + expect(topic.subsections.length).toBeGreaterThan(0); + } + }); + + test("each topic has a kebab-case slug", () => { + for (const topic of practiceData) { + expect(topic.slug).toMatch(/^[a-z0-9-]+$/); + } + }); + + test("each subsection is one of the valid tiers", () => { + for (const topic of practiceData) { + for (const subsection of topic.subsections) { + expect(VALID_TIERS).toContain(subsection.title); + } + } + }); + + test("each subsection has a non-empty items array", () => { + for (const topic of practiceData) { + for (const subsection of topic.subsections) { + expect(Array.isArray(subsection.items)).toBe(true); + expect(subsection.items.length).toBeGreaterThan(0); + } + } + }); + + test("each problem has all required fields", () => { + for (const topic of practiceData) { + for (const subsection of topic.subsections) { + for (const problem of subsection.items) { + expect(problem).toHaveProperty("id"); + expect(typeof problem.id).toBe("string"); + expect(problem.id.length).toBeGreaterThan(0); + + expect(problem).toHaveProperty("name"); + expect(typeof problem.name).toBe("string"); + expect(problem.name.length).toBeGreaterThan(0); + + expect(problem).toHaveProperty("difficulty"); + expect(VALID_DIFFICULTIES).toContain(problem.difficulty); + + // visualizerUrl is optional — some legacy problems lack it + if (problem.visualizerUrl) { + expect(typeof problem.visualizerUrl).toBe("string"); + expect(problem.visualizerUrl.length).toBeGreaterThan(0); + } + } + } + } + }); + + test("each problem difficulty is one of the valid values", () => { + for (const topic of practiceData) { + for (const subsection of topic.subsections) { + for (const problem of subsection.items) { + expect(VALID_DIFFICULTIES).toContain(problem.difficulty); + } + } + } + }); + + test("each problem has a theory section with non-empty summary and complexity", () => { + for (const topic of practiceData) { + for (const subsection of topic.subsections) { + for (const problem of subsection.items) { + if (problem.theory) { + expect(typeof problem.theory.summary).toBe("string"); + expect(problem.theory.summary.length).toBeGreaterThan(0); + if (problem.theory.complexity) { + expect(typeof problem.theory.complexity.time).toBe("string"); + expect(typeof problem.theory.complexity.space).toBe("string"); + } + } + } + } + } + }); + + test("visualizerUrl starts with /visualizer/ for problems that have it", () => { + for (const topic of practiceData) { + for (const subsection of topic.subsections) { + for (const problem of subsection.items) { + if (problem.visualizerUrl) { + expect(problem.visualizerUrl).toMatch(/^\/visualizer\//); + } + } + } + } + }); + + test("problem IDs are unique across the entire dataset", () => { + const ids = []; + for (const topic of practiceData) { + for (const subsection of topic.subsections) { + for (const problem of subsection.items) { + ids.push(problem.id); + } + } + } + const uniqueIds = new Set(ids); + expect(uniqueIds.size).toBe(ids.length); + }); + + test("has at least 5 major DSA topic areas", () => { + const topics = practiceData.map((t) => t.slug); + expect(topics.length).toBeGreaterThanOrEqual(5); + // Should include array and graph at minimum + expect(topics).toContain("array"); + expect(topics).toContain("graph"); + }); + + test("each difficulty tier (Beginner, Intermediate, Advanced) appears in at least one topic", () => { + const tiersPerTopic = practiceData.map((t) => t.subsections.map((s) => s.title)); + const allTiers = tiersPerTopic.flat(); + + for (const tier of VALID_TIERS) { + expect(allTiers).toContain(tier); + } + }); +}); diff --git a/__tests__/rateLimit.test.js b/__tests__/rateLimit.test.js new file mode 100644 index 000000000..6849ed1cb --- /dev/null +++ b/__tests__/rateLimit.test.js @@ -0,0 +1,102 @@ +// __tests__/rateLimit.test.js +// +// Run with: npx jest __tests__/rateLimit.test.js +// +// Tests the sliding-window logic in lib/rateLimit/rateLimit.js. +// No network, no Supabase, no Next.js needed — pure unit tests. + +import { checkRateLimit, resetKey, resetAll } from "../src/lib/rateLimit/index.js"; + +describe("checkRateLimit — sliding window", () => { + const KEY = "test-user-123"; + const MAX = 5; + const WINDOW = 60; // seconds + + beforeEach(async () => { + await resetAll(); // start every test with a clean slate + }); + + // ── Basic allow / deny ──────────────────────────────────────────── + test("allows requests up to the limit", async () => { + for (let i = 0; i < MAX; i++) { + const result = await checkRateLimit(KEY, MAX, WINDOW); + expect(result.allowed).toBe(true); + expect(result.remaining).toBe(MAX - (i + 1)); + } + }); + + test("denies the request that exceeds the limit", async () => { + for (let i = 0; i < MAX; i++) await checkRateLimit(KEY, MAX, WINDOW); + const result = await checkRateLimit(KEY, MAX, WINDOW); + expect(result.allowed).toBe(false); + expect(result.remaining).toBe(0); + expect(result.retryAfter).toBeGreaterThan(0); + }); + + test("retryAfter is a positive number of seconds when denied", async () => { + for (let i = 0; i <= MAX; i++) await checkRateLimit(KEY, MAX, WINDOW); + const { retryAfter } = await checkRateLimit(KEY, MAX, WINDOW); + expect(typeof retryAfter).toBe("number"); + expect(retryAfter).toBeGreaterThan(0); + expect(retryAfter).toBeLessThanOrEqual(WINDOW); + }); + + // ── Key isolation ───────────────────────────────────────────────── + test("different keys have independent counters", async () => { + const KEY_A = "user-a"; + const KEY_B = "user-b"; + for (let i = 0; i < MAX; i++) await checkRateLimit(KEY_A, MAX, WINDOW); + + // Key A is exhausted but Key B is still fresh + expect((await checkRateLimit(KEY_A, MAX, WINDOW)).allowed).toBe(false); + expect((await checkRateLimit(KEY_B, MAX, WINDOW)).allowed).toBe(true); + }); + + // ── Sliding window: old timestamps expire ───────────────────────── + test("allows new requests after the window expires", async () => { + // Use a very short window (1 second) and fake Date.now + const TINY_WINDOW = 1; // second + const realDateNow = Date.now.bind(global.Date); + + // Fill the bucket at t=0 + const t0 = 1_700_000_000_000; + global.Date.now = jest.fn().mockReturnValue(t0); + for (let i = 0; i < MAX; i++) await checkRateLimit(KEY, MAX, TINY_WINDOW); + expect((await checkRateLimit(KEY, MAX, TINY_WINDOW)).allowed).toBe(false); + + // Advance clock past the window + global.Date.now = jest.fn().mockReturnValue(t0 + TINY_WINDOW * 1000 + 100); + const result = await checkRateLimit(KEY, MAX, TINY_WINDOW); + expect(result.allowed).toBe(true); + + global.Date.now = realDateNow; + }); + + // ── resetKey ────────────────────────────────────────────────────── + test("resetKey clears only the specified key", async () => { + const KEY_C = "user-c"; + const KEY_D = "user-d"; + + for (let i = 0; i < MAX; i++) await checkRateLimit(KEY_C, MAX, WINDOW); + for (let i = 0; i < MAX; i++) await checkRateLimit(KEY_D, MAX, WINDOW); + + await resetKey(KEY_C); + + expect((await checkRateLimit(KEY_C, MAX, WINDOW)).allowed).toBe(true); // reset + expect((await checkRateLimit(KEY_D, MAX, WINDOW)).allowed).toBe(false); // still exhausted + }); + + // ── Remaining counter ───────────────────────────────────────────── + test("remaining decrements correctly with each request", async () => { + for (let i = 0; i < MAX; i++) { + const { remaining } = await checkRateLimit(KEY, MAX, WINDOW); + expect(remaining).toBe(MAX - i - 1); + } + }); + + // ── Zero retryAfter when allowed ────────────────────────────────── + test("retryAfter is 0 when the request is allowed", async () => { + const { retryAfter } = await checkRateLimit(KEY, MAX, WINDOW); + expect(retryAfter).toBe(0); + }); +}); \ No newline at end of file diff --git a/__tests__/sandbox.test.js b/__tests__/sandbox.test.js new file mode 100644 index 000000000..bc4ae5b4f --- /dev/null +++ b/__tests__/sandbox.test.js @@ -0,0 +1,138 @@ +// __tests__/sandbox.test.js +// +// Run with: npx jest __tests__/sandbox.test.js +// +// Prerequisites: npm i --save-dev jest && npm i isolated-vm +// +// These tests verify the three contract guarantees of executor.js: +// 1. Valid code → SUCCESS with captured output +// 2. Infinite loop → TLE within ~1100 ms +// 3. Memory bomb → MLE (or TLE if the OOM path is slow on this machine) + +const { executeCode } = require("../src/lib/sandbox/executor"); +const { EXECUTION_STATUS } = require("../src/lib/sandbox/errorCodes"); + +// isolated-vm can take a moment on first load — extend default timeout +jest.setTimeout(10_000); + +describe("executeCode — sandbox guarantees", () => { + // ── Happy path ──────────────────────────────────────────────────── + test("returns SUCCESS and captured output for valid code", async () => { + const result = await executeCode(` + console.log("hello world"); + console.log(1 + 1); + `); + + expect(result.status).toBe(EXECUTION_STATUS.SUCCESS); + expect(result.output).toContain("hello world"); + expect(result.output).toContain("2"); + expect(result.executionTime).toBeGreaterThanOrEqual(0); + // Note: isolated-vm doesn't provide memory usage tracking like vm + // expect(result.memoryUsed).toBeGreaterThan(0); + }); + + test("captures multi-line console output in order", async () => { + const result = await executeCode(` + for (let i = 1; i <= 3; i++) console.log(i); + `); + expect(result.status).toBe(EXECUTION_STATUS.SUCCESS); + expect(result.output).toBe("1\n2\n3"); + }); + + test("handles template literals correctly without syntax errors", async () => { + const result = await executeCode(` + const val = 42; + console.log(\`value is \${val}\`); + `); + expect(result.status).toBe(EXECUTION_STATUS.SUCCESS); + expect(result.output).toContain("value is 42"); + }); + + // ── Syntax error ────────────────────────────────────────────────── + test("returns RUNTIME_ERROR for syntax errors (never reaches execution)", async () => { + const result = await executeCode(`const x = (`); // unterminated + expect(result.status).toBe(EXECUTION_STATUS.RUNTIME_ERROR); + expect(result.error).toMatch(/SyntaxError/i); + }); + + // ── Runtime error ───────────────────────────────────────────────── + test("returns RUNTIME_ERROR for thrown exceptions", async () => { + const result = await executeCode(` + throw new Error("user mistake"); + `); + expect(result.status).toBe(EXECUTION_STATUS.RUNTIME_ERROR); + expect(result.error).toContain("user mistake"); + }); + + test("returns RUNTIME_ERROR for ReferenceError", async () => { + const result = await executeCode(`console.log(undeclaredVariable);`); + expect(result.status).toBe(EXECUTION_STATUS.RUNTIME_ERROR); + }); + + // ── Time Limit Exceeded ─────────────────────────────────────────── + test("returns TLE for infinite loop", async () => { + const result = await executeCode(`while (true) {}`); + expect(result.status).toBe(EXECUTION_STATUS.TLE); + // executionTime should be ~MAX_TIMEOUT_MS, not far above it + expect(result.executionTime).toBeLessThan(3000); + }); + + test("returns TLE for code that sleeps via busy-wait", async () => { + const result = await executeCode(` + const end = Date.now() + 5000; + while (Date.now() < end) {} + `); + expect(result.status).toBe(EXECUTION_STATUS.TLE); + }); + + // ── Memory Limit Exceeded ───────────────────────────────────────── + test("returns MLE or TLE for aggressive memory allocation", async () => { + // Allocate large arrays until heap is exhausted. + // Some machines hit TLE before MLE — both are acceptable. + const result = await executeCode(` + const arrays = []; + while (true) { + arrays.push(new Array(1_000_000).fill("x")); + } + `); + expect([EXECUTION_STATUS.MLE, EXECUTION_STATUS.TLE]).toContain(result.status); + }); + + // ── Isolation: host globals must not be accessible ──────────────── + test("cannot access Node.js process global", async () => { + const result = await executeCode(` + if (typeof process !== "undefined") { + console.log("EXPOSED:" + process.version); + } else { + console.log("SAFE"); + } + `); + // process must not exist inside the isolate + expect(result.output).not.toContain("EXPOSED"); + expect(result.output).toContain("SAFE"); + }); + + test("cannot require modules", async () => { + const result = await executeCode(` + try { + require("fs"); + console.log("EXPOSED"); + } catch(e) { + console.log("BLOCKED:" + e.message); + } + `); + expect(result.output).not.toContain("EXPOSED"); + expect(result.output).toContain("BLOCKED"); + }); + + // ── Output truncation ───────────────────────────────────────────── + test("truncates output exceeding MAX_OUTPUT_LENGTH", async () => { + // Produce ~16 000 chars of output (more than 8000 char limit) + const result = await executeCode(` + for (let i = 0; i < 1000; i++) console.log("A".repeat(20)); + `); + expect(result.status).toBe(EXECUTION_STATUS.SUCCESS); + expect(result.output.length).toBeLessThanOrEqual(8100); // small buffer for "… truncated" + expect(result.output).toContain("truncated"); + }); +}); \ No newline at end of file diff --git a/__tests__/sharedUtils.test.js b/__tests__/sharedUtils.test.js new file mode 100644 index 000000000..1fd3562ac --- /dev/null +++ b/__tests__/sharedUtils.test.js @@ -0,0 +1,165 @@ +// __tests__/sharedUtils.test.js +// +// Run with: npx jest __tests__/sharedUtils.test.js +// +// Tests the shared utility functions in src/lib/shared-utils.js: +// escapeHtml, isValidHttpUrl, and getSupabaseConfig. + +const { escapeHtml, isValidHttpUrl, getSupabaseConfig } = require("../src/lib/shared-utils"); + +describe("isValidHttpUrl", () => { + test("returns true for https://example.com", () => { + expect(isValidHttpUrl("https://example.com")).toBe(true); + }); + + test("returns true for http://example.com", () => { + expect(isValidHttpUrl("http://example.com")).toBe(true); + }); + + test("returns true for https://example.com:8080/path", () => { + expect(isValidHttpUrl("https://example.com:8080/path")).toBe(true); + }); + + test("returns false for ftp://example.com", () => { + expect(isValidHttpUrl("ftp://example.com")).toBe(false); + }); + + test("returns false for file:///path/to/file", () => { + expect(isValidHttpUrl("file:///path/to/file")).toBe(false); + }); + + test("returns false for javascript:alert(1)", () => { + expect(isValidHttpUrl("javascript:alert(1)")).toBe(false); + }); + + test("returns false for malformed strings", () => { + expect(isValidHttpUrl("not-a-url")).toBe(false); + expect(isValidHttpUrl("")).toBe(false); + expect(isValidHttpUrl("://missing-protocol")).toBe(false); + }); +}); + +describe("escapeHtml", () => { + test("escapes ampersand to &", () => { + expect(escapeHtml("a & b")).toBe("a & b"); + }); + + test("escapes less-than to <", () => { + expect(escapeHtml("a < b")).toBe("a < b"); + }); + + test("escapes greater-than to >", () => { + expect(escapeHtml("a > b")).toBe("a > b"); + }); + + test("escapes double quote to "", () => { + expect(escapeHtml('say "hello"')).toBe("say "hello""); + }); + + test("escapes single quote to '", () => { + expect(escapeHtml("it's")).toBe("it's"); + }); + + test("escapes all special characters in a mixed string", () => { + expect(escapeHtml('')).toBe( + "<script>alert("xss")</script>" + ); + }); + + test("returns empty string for empty input", () => { + expect(escapeHtml("")).toBe(""); + }); + + test("returns stringified value for non-string input (String coercion)", () => { + // String(null) produces "null", not "" + expect(escapeHtml(null)).toBe("null"); + // String(undefined) produces "undefined" + expect(escapeHtml(undefined)).toBe("undefined"); + // String(123) produces "123" + expect(escapeHtml(123)).toBe("123"); + }); + + test("re-escapes already-escaped content (simple replace-based escaper)", () => { + // The simple replace-based escaper does not guard against double-escaping. + // & in & becomes &amp; + expect(escapeHtml("a & b")).toBe("a &amp; b"); + }); +}); + +describe("getSupabaseConfig", () => { + const originalEnv = process.env; + + beforeEach(() => { + jest.resetModules(); + process.env = { ...originalEnv }; + }); + + afterAll(() => { + process.env = originalEnv; + }); + + test("returns null when NEXT_PUBLIC_SUPABASE_URL is missing", () => { + delete process.env.NEXT_PUBLIC_SUPABASE_URL; + delete process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; + const { getSupabaseConfig } = require("../src/lib/shared-utils"); + expect(getSupabaseConfig()).toBe(null); + }); + + test("returns null when NEXT_PUBLIC_SUPABASE_ANON_KEY is missing", () => { + process.env.NEXT_PUBLIC_SUPABASE_URL = "https://example.supabase.co"; + delete process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; + const { getSupabaseConfig } = require("../src/lib/shared-utils"); + expect(getSupabaseConfig()).toBe(null); + }); + + test("returns null when URL has invalid protocol", () => { + process.env.NEXT_PUBLIC_SUPABASE_URL = "ftp://example.supabase.co"; + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY = "valid-key"; + const { getSupabaseConfig } = require("../src/lib/shared-utils"); + expect(getSupabaseConfig()).toBe(null); + }); + + test("returns config object with trimmed values when env vars are valid", () => { + process.env.NEXT_PUBLIC_SUPABASE_URL = " https://example.supabase.co "; + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY = " eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 "; + const { getSupabaseConfig } = require("../src/lib/shared-utils"); + const config = getSupabaseConfig(); + expect(config).not.toBe(null); + expect(config.supabaseUrl).toBe("https://example.supabase.co"); + expect(config.supabaseAnonKey).toBe("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"); + }); + + test("replaces localhost: with 127.0.0.1: in URL", () => { + process.env.NEXT_PUBLIC_SUPABASE_URL = "http://localhost:54321"; + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY = "key"; + const { getSupabaseConfig } = require("../src/lib/shared-utils"); + expect(getSupabaseConfig().supabaseUrl).toBe("http://127.0.0.1:54321"); + }); + + test("includes service key when SUPABASE_SERVICE_KEY is set", () => { + process.env.NEXT_PUBLIC_SUPABASE_URL = "https://example.supabase.co"; + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY = "anon-key"; + process.env.SUPABASE_SERVICE_KEY = "service-key"; + const { getSupabaseConfig } = require("../src/lib/shared-utils"); + const config = getSupabaseConfig(); + expect(config.supabaseServiceKey).toBe("service-key"); + }); + + test("prefers SUPABASE_SERVICE_KEY over SUPABASE_SERVICE_ROLE_KEY when both are set", () => { + process.env.NEXT_PUBLIC_SUPABASE_URL = "https://example.supabase.co"; + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY = "anon-key"; + process.env.SUPABASE_SERVICE_KEY = "service-key"; + process.env.SUPABASE_SERVICE_ROLE_KEY = "role-key"; + const { getSupabaseConfig } = require("../src/lib/shared-utils"); + expect(getSupabaseConfig().supabaseServiceKey).toBe("service-key"); + }); + + test("uses SUPABASE_SERVICE_ROLE_KEY when SUPABASE_SERVICE_KEY is not set", () => { + process.env.NEXT_PUBLIC_SUPABASE_URL = "https://example.supabase.co"; + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY = "anon-key"; + delete process.env.SUPABASE_SERVICE_KEY; + process.env.SUPABASE_SERVICE_ROLE_KEY = "role-key"; + const { getSupabaseConfig } = require("../src/lib/shared-utils"); + expect(getSupabaseConfig().supabaseServiceKey).toBe("role-key"); + }); +}); diff --git a/__tests__/stepRunner.test.js b/__tests__/stepRunner.test.js new file mode 100644 index 000000000..3528b25f7 --- /dev/null +++ b/__tests__/stepRunner.test.js @@ -0,0 +1,112 @@ +// __tests__/stepRunner.test.js +// +// Run with: npx jest __tests__/stepRunner.test.js +// +// Tests the step runner utilities in src/lib/visualizer/stepRunner.js: +// registerAlgorithm, createSyncStepRunner, buildStepRunner, generateSteps. + +const { + registerAlgorithm, + createSyncStepRunner, + buildStepRunner, + generateSteps, +} = require("../src/lib/visualizer/stepRunner"); + +describe("registerAlgorithm", () => { + test("registers a named algorithm function", () => { + const mockFn = jest.fn(); + registerAlgorithm("testAlgo", mockFn); + // No assertion needed — if it throws, the test fails + expect(true).toBe(true); + }); +}); + +describe("createSyncStepRunner", () => { + test("returns an array of steps from a generator function", () => { + function* simpleGen(input) { + yield { step: 1, value: input }; + yield { step: 2, value: input * 2 }; + yield { step: 3, value: input * 3 }; + } + + const runner = createSyncStepRunner(simpleGen); + const steps = runner(5); + + expect(Array.isArray(steps)).toBe(true); + expect(steps).toHaveLength(3); + expect(steps[0]).toEqual({ step: 1, value: 5 }); + expect(steps[1]).toEqual({ step: 2, value: 10 }); + expect(steps[2]).toEqual({ step: 3, value: 15 }); + }); + + test("handles empty generator (no yields)", () => { + function* emptyGen(input) { + // no yields + } + + const runner = createSyncStepRunner(emptyGen); + const steps = runner("ignored"); + expect(steps).toEqual([]); + }); + + test("works with a tree-traversal-style generator", () => { + function* treeGen(node) { + if (!node) return; + yield { visited: node.value }; + if (node.left) yield* treeGen(node.left); + if (node.right) yield* treeGen(node.right); + } + + const runner = createSyncStepRunner(treeGen); + const tree = { + value: "A", + left: { value: "B", left: null, right: null }, + right: { value: "C", left: null, right: null }, + }; + + const steps = runner(tree); + expect(steps).toHaveLength(3); + expect(steps.map((s) => s.visited)).toEqual(["A", "B", "C"]); + }); +}); + +// buildStepRunner is tested indirectly via createSyncStepRunner and generateSteps. +// Direct testing of buildStepRunner is skipped because its async for-await-of +// pattern (for await...of stepGenerator) does not work in Jest's jsdom +// test environment with sync generators. + +describe("generateSteps", () => { + test("yields next states from a breadth-first style algorithm function", async () => { + // algorithmFn takes a state and returns an array of next states synchronously + // generateSteps yields those next states + function bfsAlgo(state) { + const { node, children } = state; + if (!children || children.length === 0) return []; + return children.map((c) => ({ node: c, children: [] })); + } + + const gen = generateSteps(bfsAlgo, { node: "root", children: ["a", "b"] }); + + // generateSteps yields the children (next states), not the root itself + const step1 = await gen.next(); + expect(step1.value).toEqual({ node: "a", children: [] }); + + const step2 = await gen.next(); + expect(step2.value).toEqual({ node: "b", children: [] }); + + const done = await gen.next(); + expect(done.done).toBe(true); + }); + + test("stops when queue is exhausted (no children from first state)", async () => { + function leafState(state) { + return []; // no children + } + + const gen = generateSteps(leafState, { value: 42 }); + // No children means no yields + const done = await gen.next(); + expect(done.value).toBeUndefined(); + expect(done.done).toBe(true); + }); +}); diff --git a/app/api/auth/route.js b/app/api/auth/route.js deleted file mode 100755 index 0e7b346d4..000000000 --- a/app/api/auth/route.js +++ /dev/null @@ -1,102 +0,0 @@ -import { createClient } from "@supabase/supabase-js"; - -const supabase = createClient( - process.env.NEXT_PUBLIC_SUPABASE_URL || "https://placeholder.supabase.co", - process.env.SUPABASE_SERVICE_KEY || "placeholder-key", -); - -export async function POST(req) { - try { - // Parse JSON body safely - const body = await req.json(); - const { email, password, captchaToken, action, name } = body || {}; - - // Validate required fields - if (!email || !password) { - return new Response( - JSON.stringify({ - success: false, - message: "Email and password are required", - }), - { status: 400 }, - ); - } - if (!captchaToken) { - return new Response( - JSON.stringify({ success: false, message: "Captcha token missing" }), - { status: 400 }, - ); - } - - // Verify Turnstile token for both signup and login - const verifyRes = await fetch( - "https://challenges.cloudflare.com/turnstile/v0/siteverify", - { - method: "POST", - headers: { "Content-Type": "application/x-www-form-urlencoded" }, - body: new URLSearchParams({ - secret: process.env.TURNSTILE_SECRET_KEY, - response: captchaToken, - }), - }, - ); - const verifyData = await verifyRes.json(); - if (!verifyData.success) { - return new Response( - JSON.stringify({ - success: false, - message: "Captcha verification failed", - }), - { status: 400 }, - ); - } - - if (action === "signup") { - // Create Supabase user with metadata - const { data, error } = await supabase.auth.signUp({ - email, - password, - options: { - data: { display_name: name }, - }, - }); - if (error) { - return new Response( - JSON.stringify({ success: false, message: error.message }), - { status: 400 }, - ); - } - return new Response( - JSON.stringify({ - success: true, - message: "Signup successful. Verification email sent.", - trigger: true, - }), - { status: 200 }, - ); - } else if (action === "login") { - // For login, only verify captcha and return success. - return new Response( - JSON.stringify({ - success: true, - message: "Captcha verified. You can now login using email/password.", - }), - { status: 200 }, - ); - } - - // Invalid action - else { - return new Response( - JSON.stringify({ success: false, message: "Invalid action" }), - { status: 400 }, - ); - } - } catch (err) { - console.error("API Error:", err); - return new Response( - JSON.stringify({ success: false, message: "Internal server error" }), - { status: 500 }, - ); - } -} diff --git a/app/api/contact/route.js b/app/api/contact/route.js deleted file mode 100755 index be5116d1f..000000000 --- a/app/api/contact/route.js +++ /dev/null @@ -1,48 +0,0 @@ -import nodemailer from "nodemailer"; - -export async function POST(req) { - try { - const { name, email, subject, message } = await req.json(); - - // Create transporter - const transporter = nodemailer.createTransport({ - service: "gmail", - auth: { - user: process.env.EMAIL_USER, - pass: process.env.EMAIL_PASSWORD, - }, - }); - - // Email options - const mailOptions = { - from: email, - to: process.env.EMAIL_USER, - subject: `New Contact Form Submission: ${subject}`, - text: ` - Name: ${name} - Email: ${email} - Subject: ${subject} - Message: ${message} - `, - html: ` -

New Contact Form Submission

-

Name: ${name}

-

Email: ${email}

-

Subject: ${subject}

-

Message:

-

${message.replace(/\n/g, "
")}

- `, - }; - - // Send email - await transporter.sendMail(mailOptions); - - return Response.json({ message: "Email sent successfully" }); - } catch (error) { - console.error("Error sending email:", error); - return new Response(JSON.stringify({ message: "Error sending email" }), { - status: 500, - headers: { "Content-Type": "application/json" }, - }); - } -} diff --git a/app/api/send-review/route.js b/app/api/send-review/route.js deleted file mode 100755 index 3efbc7b55..000000000 --- a/app/api/send-review/route.js +++ /dev/null @@ -1,43 +0,0 @@ -import { NextResponse } from 'next/server'; -import nodemailer from 'nodemailer'; - -export async function POST(request) { - const { name, email, review, rating, to } = await request.json(); - - try { - // Create transporter - const transporter = nodemailer.createTransport({ - service: 'gmail', - auth: { - user: process.env.EMAIL_USER, - pass: process.env.EMAIL_PASSWORD, - }, - }); - - // Email options - const mailOptions = { - from: process.env.EMAIL_USER, - to: to || 'routsohan2006@gmail.com', // Default to your email - subject: `New Review Submission from ${name}`, - html: ` -

New Review Received

-

Name: ${name}

-

Email: ${email}

-

Rating: ${'★'.repeat(rating)}${'☆'.repeat(5 - rating)}

-

Review:

-

${review}

- `, - }; - - // Send email - await transporter.sendMail(mailOptions); - - return NextResponse.json({ success: true }); - } catch (error) { - console.error('Error sending email:', error); - return NextResponse.json( - { success: false, error: 'Failed to send email' }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/app/blogs/Content/dsaDifferent/content.jsx b/app/blogs/Content/dsaDifferent/content.jsx deleted file mode 100755 index a0abbb0cb..000000000 --- a/app/blogs/Content/dsaDifferent/content.jsx +++ /dev/null @@ -1,314 +0,0 @@ -"use client"; -import { FiCopy, FiBookmark, FiShare2 } from "react-icons/fi"; -import { useState } from "react"; - -const BlogContent = () => { - const [copied, setCopied] = useState(false); - - const handleCopy = () => { - navigator.clipboard.writeText(window.location.href); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }; - - const Paragraphs = [ - `A common question among developers learning Data Structures and Algorithms (DSA) is whether these concepts change across programming languages. While the fundamental principles remain consistent, their implementations and optimizations can vary significantly. Let's explore how DSA manifests in different languages and what this means for developers.`, - `Programming languages offer different levels of abstraction and built-in utilities. For example, Python's rich standard library makes certain algorithms easier to implement, whereas C++ might offer more control over memory, making it suitable for performance-critical scenarios.`, - `This language-agnostic nature is why DSA knowledge transfers well between languages. However, the implementation details and performance characteristics can differ.`, - `These differences become particularly important when working on performance-critical applications or when interfacing between multiple languages in a single project.`, - `When learning DSA concepts, focus first on understanding the core principles, then explore how your primary language implements these structures. This approach gives you both the theoretical foundation and practical skills needed for real-world development.`, - ]; - - const universalConcepts = [ - { points: "Time complexity (Big O notation)" }, - { points: "Space complexity analysis" }, - { points: "Abstract data type behaviors" }, - { points: "Algorithm design patterns" }, - { points: "Problem-solving approaches" }, - ]; - - const languageExamples = [ - { - language: "JavaScript", - structures: [ - "Arrays are actually objects with integer keys", - "Objects as hash maps (but with insertion order preserved)", - "No built-in heap/priority queue", - "TypedArrays for numerical work" - ], - algorithms: [ - "Event loop affects async algorithm design", - "Single-threaded nature impacts parallel processing", - "Prototypal inheritance affects object structures" - ] - }, - { - language: "Python", - structures: [ - "Lists as dynamic arrays", - "Dictionaries as highly optimized hash tables", - "Tuples as immutable sequences", - "Sets with built-in mathematical operations" - ], - algorithms: [ - "List comprehensions enable concise transformations", - "Generator expressions for memory-efficient iteration", - "Built-in sort uses Timsort algorithm" - ] - }, - { - language: "Java", - structures: [ - "Primitive arrays vs. ArrayList", - "HashMap vs. TreeMap implementations", - "Strongly typed collections", - "Concurrent collections for threading" - ], - algorithms: [ - "JIT compilation affects runtime characteristics", - "Garbage collection impacts memory usage", - "Bytecode execution adds abstraction layer" - ] - } - ]; - - const webDevImplications = [ - { - area: "Frontend (JavaScript)", - considerations: [ - "Virtual DOM diffing algorithms in frameworks", - "State management efficiency in large apps", - "Memoization techniques for performance" - ] - }, - { - area: "Backend (Node.js/Python/Java)", - considerations: [ - "Database query optimization", - "API response caching strategies", - "Load balancing and request processing" - ] - }, - { - area: "Full-Stack", - considerations: [ - "Data serialization between layers", - "Algorithm choice for shared business logic", - "Consistent data modeling across boundaries" - ] - } - ]; - - const protips = [ - { points: "Learn DSA in one language first, then compare implementations" }, - { points: "Use language-specific benchmarks to verify performance assumptions" }, - { points: "Study standard library implementations of common structures" }, - { points: "Understand how your language's memory model affects data structures" }, - { points: "Don’t just translate code between languages—adapt it to leverage language strengths" } - ]; - - return ( -
- {/* Article Header */} -
-
- - Computer Science - -
- - - -
-
- -

- Are Data Structures and Algorithms Different for Different Languages? -

- -
- Published on May 20, 2025 - - 10 min read -
-
- - {/* Featured Image */} -
- Data structures across programming languages -
-
-

- Comparing DSA implementations across JavaScript, Python, and Java -

-
-
- - {/* Article Content */} -
-

- {Paragraphs[0]} -

- -
-

- Universal DSA Concepts -

-
-

- Core Insight: The theoretical foundations of data structures and algorithms - remain constant regardless of programming language. -

-
-
    - {universalConcepts.map((item, index) => ( -
  • {item.points}
  • - ))} -
-

{Paragraphs[2]}

-
- -
-

- Language-Specific Implementations -

- -
- {languageExamples.map((lang, index) => ( -
-
-

{lang.language}

-
-
-

Structures:

-
    - {lang.structures.map((item, i) => ( -
  • {item}
  • - ))} -
-

Algorithms:

-
    - {lang.algorithms.map((item, i) => ( -
  • {item}
  • - ))} -
-
-
- ))} -
-

- These distinctions illustrate that while the same data structure or algorithm may exist across languages, their behavior, performance, or even syntax might differ. Developers should not only know how something works in theory but also how their chosen language expresses and optimizes it. -

-
- -
-

- Web Development Implications -

- -
- {webDevImplications.map((item, index) => ( -
-

{item.area}

-
    - {item.considerations.map((point, i) => ( -
  • - - {point} -
  • - ))} -
-
- ))} -
- -

{Paragraphs[3]}

-

- Developers building cross-platform or microservice-based architectures especially benefit from understanding how DSA implementations behave differently across tech stacks. -

-
- -
-

- Pro Tips for Language-Agnostic DSA Learning -

-
    - {protips.map((item, index) => ( -
  • {item.points}
  • - ))} -
-
- -
-

- Key Takeaway -

-
-

- While data structures and algorithms may be implemented differently across languages, - the core concepts remain the same. Master the fundamentals first, then learn how your - preferred languages realize these concepts in practice. This dual understanding not only enhances your adaptability but also empowers you to choose the most efficient solution depending on the project requirements and language capabilities. -

-
-
-
- - {/* Article Footer */} -
-
-
-

- Share this article -

-
- {[ - { - name: "Twitter", - url: "https://twitter.com/intent/tweet?url=https%3A%2F%2Fwww.dsavisualizer.in%2Fblogs%2FContent%2FdsaDifferent&text=Exploring%20if%20Data%20Structures%20and%20Algorithms%20are%20different%20across%20languages%20in%20this%20blog%20post%21%20A%20must-read%20for%20programmers.%20%23DSA%20%23ProgrammingLanguages" - }, - { - name: "LinkedIn", - url: "https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fwww.dsavisualizer.in%2Fblogs%2FContent%2FdsaDifferent" - }, - { - name: "Facebook", - url: "https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fwww.dsavisualizer.in%2Fblogs%2FContent%2FdsaDifferent" - } - ].map((social) => ( - - ))} -
-
-
-
-
- ); -}; - -export default BlogContent; \ No newline at end of file diff --git a/app/blogs/Content/dsaDifferent/page.jsx b/app/blogs/Content/dsaDifferent/page.jsx deleted file mode 100755 index fcf7d4d49..000000000 --- a/app/blogs/Content/dsaDifferent/page.jsx +++ /dev/null @@ -1,62 +0,0 @@ -import Navbar from "@/app/components/navbar"; -import Footer from "@/app/components/footer"; -import Content from "@/app/blogs/Content/dsaDifferent/content"; - -export const metadata = { - title: "Are Data Structures and Algorithms Different for Different Languages?", - description: - "Uncover the truth behind language-specific implementations of DSA. Learn how data structures and algorithms vary in syntax, performance, and usage across programming languages.", - keywords: [ - "Data Structures", - "Algorithms", - "Programming Languages", - "JavaScript", - "Python", - "C++", - "Java", - "Language Comparison DSA", - "Coding Interview", - "Performance Optimization", - "Backend vs Frontend DSA" - ], - authors: [{ name: "Sohan Rout", url: "https://www.linkedin.com/in/sohan-rout" }], - openGraph: { - title: "Are Data Structures and Algorithms Different for Different Languages?", - description: - "Explore how DSA implementations differ across languages like Python, JavaScript, Java, and C++. From syntax to performance, understand what's universal and what's not.", - url: "./blogs/Content/dsaDifferent", - siteName: "DSA Visualizer", - locale: "en_IN", - type: "article", - images: [ - { - url: "./blog/dsaDifferent.png", - width: 1200, - height: 630, - alt: "How DSA changes across languages", - }, - ], - }, - twitter: { - card: "summary_large_image", - title: "Are Data Structures and Algorithms Different for Different Languages?", - description: - "Do arrays, stacks, or recursion work the same in Python and C++? Learn how the core DSA concepts stay the same—but their implementation varies.", - images: ["./blog/dsaDifferent.png"], - }, - category: "Technology", - publishedTime: "2024-05-15T08:00:00Z", - robots: "index, follow", -}; - -const page = () => { - return( -
- - -
-
- ); -} - -export default page; \ No newline at end of file diff --git a/app/blogs/Content/dsaWebDev/content.jsx b/app/blogs/Content/dsaWebDev/content.jsx deleted file mode 100755 index 75f71eeec..000000000 --- a/app/blogs/Content/dsaWebDev/content.jsx +++ /dev/null @@ -1,243 +0,0 @@ -"use client"; -import { FiCopy, FiBookmark, FiShare2 } from "react-icons/fi"; -import { useState } from "react"; - -const BlogContent = () => { - const [copied, setCopied] = useState(false); - - const handleCopy = () => { - navigator.clipboard.writeText(window.location.href); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }; - - const Paragraphs = [ - `If you're a web developer or someone aspiring to be one, you've likely wondered if learning data structures and algorithms (DSA) is truly necessary. After all, modern web development is often about building user interfaces and connecting APIs, right? Let's explore this question in depth.`, - `At first glance, these tasks don't seem to require deep algorithm knowledge. However, the underlying principles become crucial as applications scale.`, - `Understanding concepts like time complexity helps you choose the right approach when dealing with large datasets or performance-critical operations.`, - `Instead of just solving abstract problems, apply DSA concepts directly to web development:`, - `While you can be a productive web developer without deep DSA knowledge, understanding these concepts will make you more versatile and valuable. You'll write better code, solve problems more efficiently, and have an edge in technical interviews. The key is learning DSA in the context of web development rather than as abstract computer science concepts.`, - ]; - - const tasks = [ - { points: "Creating responsive UIs with HTML/CSS/JavaScript" }, - { points: "Working with frameworks like React, Next.js, or Vue" }, - { points: "Integrating RESTful APIs and GraphQL endpoints" }, - { points: "Implementing state management solutions" }, - { points: "Optimizing performance and accessibility" }, - ]; - - const scenarios = [ - { points: "Performance optimization" }, - { points: "Complex state management" }, - { points: "Efficient data processing" }, - { points: "Interview preparation" }, - { points: "Library/framework development" }, - { points: "System design decisions" }, - ]; - - const examples = [ - { - title: "Autocomplete Search", - description: "Trie data structure improves search efficiency", - }, - { - title: "Infinite Scroll", - description: "Efficient pagination requires proper array handling", - }, - { - title: "Form Validation", - description: "Graphs can model complex validation rules", - }, - { - title: "State Management", - description: "Understanding trees helps with state updates", - }, - ]; - - const protip = [ - { points: "Implement your own simplified version of React's reconciliation algorithm" }, - { points: "Build a custom hook that efficiently manages large datasets" }, - { points: "Create a visualization of how different sorting algorithms work" }, - ]; - - return ( -
- {/* Article Header */} -
-
- - Web Development - -
- - - -
-
- -

- Is Data Structures and Algorithms Important for Web Developers? -

- -
- Published on May 17, 2025 - - 8 min read -
-
- - {/* Featured Image */} -
- Web developer working with algorithms -
-
-

- Understanding DSA helps build better web applications -

-
-
- - {/* Article Content */} -
-

- {Paragraphs[0]} -

- -
-

- What Web Developers Actually Do -

-

- Typical web development tasks include: -

-
    - {tasks.map((item, index) => ( -
  • {item.points}
  • - ))} -
-

{Paragraphs[1]}

-
- -
-

- Where DSA Knowledge Shines in Web Development -

-
-

- Key scenarios where DSA matters: -

-
- {scenarios.map((item, index) => ( - - {item.points} - - ))} -
-
-

{Paragraphs[2]}

-
- -
-

- Real-World Examples -

-
- {examples.map((item, index) => ( -
-

- {item.title} -

-

- {item.description} -

-
- ))} -
-
- -
-

- Pro Tip: Practical Learning -

-

{Paragraphs[3]}

-
    - {protip.map((item, index) => ( -
  • {item.points}
  • - ))} -
-
- -
-

- The Verdict -

-

{Paragraphs[4]}

-
-
- - {/* Article Footer */} -
-
-
-

- Share this article -

-
- {[ - { - name: "Twitter", - url: "https://twitter.com/intent/tweet?url=https%3A%2F%2Fwww.dsavisualizer.in%2Fblogs%2FContent%2FdsaWebDev&text=Just%20read%20this%20insighful%20blog%3A%20Is%20Data%20Structures%20and%20Algorithms%20Important%20for%20Web%20Developers%3F%20%23WebDev%20%23DSA%20%23Programming" - }, - { - name: "LinkedIn", - url: "https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fwww.dsavisualizer.in%2Fblogs%2FContent%2FdsaWebDev" - }, - { - name: "Facebook", - url: "https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fwww.dsavisualizer.in%2Fblogs%2FContent%2FdsaWebDev" - } - ].map((social) => ( - - ))} -
-
-
-
-
- ); -}; - -export default BlogContent; diff --git a/app/blogs/Content/dsaWebDev/page.jsx b/app/blogs/Content/dsaWebDev/page.jsx deleted file mode 100755 index d7590c3f0..000000000 --- a/app/blogs/Content/dsaWebDev/page.jsx +++ /dev/null @@ -1,61 +0,0 @@ -import Navbar from "@/app/components/navbar"; -import Footer from "@/app/components/footer"; -import Content from "@/app/blogs/Content/dsaWebDev/content"; - -export const metadata = { - title: "Is Data Structures and Algorithms Important for Web Developers?", - description: - "Discover how DSA can elevate your web development skills. Learn when and why understanding data structures and algorithms matters for frontend and backend web devs.", - keywords: [ - "Data Structures", - "Algorithms", - "Web Development", - "Frontend", - "Backend", - "DSA for Web Developers", - "React", - "JavaScript", - "Performance Optimization", - "Coding Interview Preparation", - ], - authors: [{ name: "Sohan Rout", url: "https://www.linkedin.com/in/sohan-rout" }], - openGraph: { - title: "Is Data Structures and Algorithms Important for Web Developers?", - description: - "Explore how learning DSA can boost your efficiency, optimize performance, and prepare you for tech interviews—even as a web developer.", - url: "./blog/dsaWebDev.png", - siteName: "DSA Visualizer", - locale: "en_IN", - type: "article", - images: [ - { - url: "./blog/dsaWebDev.png", // Replace with actual OG image - width: 1200, - height: 630, - alt: "DSA for Web Developers", - }, - ], - }, - twitter: { - card: "summary_large_image", - title: "Is DSA Important for Web Developers?", - description: - "Think DSA is only for competitive programming? Think again. Here's how it benefits modern web developers.", - images: ["./blog/dsaWebDev.png"], - }, - category: "Technology", - publishedTime: "2024-05-15T08:00:00Z", - robots: "index, follow", -}; - -const page = () => { - return( -
- - -
-
- ); -} - -export default page; \ No newline at end of file diff --git a/app/blogs/Content/timeRequired/content.jsx b/app/blogs/Content/timeRequired/content.jsx deleted file mode 100755 index 402478bc8..000000000 --- a/app/blogs/Content/timeRequired/content.jsx +++ /dev/null @@ -1,243 +0,0 @@ -"use client"; -import { FiCopy, FiBookmark, FiShare2 } from "react-icons/fi"; -import { useState } from "react"; - -const BlogContent = () => { - const [copied, setCopied] = useState(false); - - const handleCopy = () => { - navigator.clipboard.writeText(window.location.href); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }; - - const Paragraphs = [ - `If you're a student, developer, or career switcher, you've likely asked: "How long will it take to learn Data Structures and Algorithms (DSA)?" The answer isn't one-size-fits-all — it depends on your goals, consistency, and background.`, - `Before estimating time, let's define what “mastering” DSA really means. Mastery isn't just knowing syntax or solving rote problems. It means pattern recognition, approaching unseen questions with confidence, and making trade-offs in real-world system design.`, - `For most learners, 3 months gets you foundational knowledge, 6–9 months develops confidence for interviews, and 12+ months results in true mastery. DSA is a long-term game.`, - `Here's a simple 12-week roadmap:\n\n- Weeks 1–2: Arrays, Strings, HashMaps\n- Weeks 3–4: Stacks, Queues, Recursion\n- Weeks 5–6: Linked Lists, Trees\n- Weeks 7–8: Heaps, Binary Trees, BSTs\n- Weeks 9–10: Graphs, DFS/BFS\n- Weeks 11–12: DP, Tries, Bit Manipulation`, - `Avoid common traps: passively watching tutorials, skipping fundamentals, not reviewing problems. Instead, learn → code → revise. Tools like LeetCode, NeetCode, and your own notes/GitHub repo will accelerate learning.` - ]; - - const tasks = [ - { points: "Creating responsive UIs with HTML/CSS/JavaScript" }, - { points: "Working with frameworks like React, Next.js, or Vue" }, - { points: "Integrating RESTful APIs and GraphQL endpoints" }, - { points: "Implementing state management solutions" }, - { points: "Optimizing performance and accessibility" }, - ]; - - const scenarios = [ - { points: "Performance optimization" }, - { points: "Complex state management" }, - { points: "Efficient data processing" }, - { points: "Interview preparation" }, - { points: "Library/framework development" }, - { points: "System design decisions" }, - ]; - - const examples = [ - { - title: "Autocomplete Search", - description: "Trie data structure improves search efficiency", - }, - { - title: "Infinite Scroll", - description: "Efficient pagination requires proper array handling", - }, - { - title: "Form Validation", - description: "Graphs can model complex validation rules", - }, - { - title: "State Management", - description: "Understanding trees helps with state updates", - }, - ]; - - const protip = [ - { points: "Implement your own simplified version of React's reconciliation algorithm" }, - { points: "Build a custom hook that efficiently manages large datasets" }, - { points: "Create a visualization of how different sorting algorithms work" }, - ]; - - return ( -
- {/* Article Header */} -
-
- - Web Development - -
- - - -
-
- -

- Is Data Structures and Algorithms Important for Web Developers? -

- -
- Published on June 20, 2025 - - 10 min read -
-
- - {/* Featured Image */} -
- Web developer working with algorithms -
-
-

- Understanding DSA helps build better web applications -

-
-
- - {/* Article Content */} -
-

- {Paragraphs[0]} -

- -
-

- What Web Developers Actually Do -

-

- Typical web development tasks include: -

-
    - {tasks.map((item, index) => ( -
  • {item.points}
  • - ))} -
-

{Paragraphs[1]}

-
- -
-

- Where DSA Knowledge Shines in Web Development -

-
-

- Key scenarios where DSA matters: -

-
- {scenarios.map((item, index) => ( - - {item.points} - - ))} -
-
-

{Paragraphs[2]}

-
- -
-

- Real-World Examples -

-
- {examples.map((item, index) => ( -
-

- {item.title} -

-

- {item.description} -

-
- ))} -
-
- -
-

- Pro Tip: Practical Learning -

-

{Paragraphs[3]}

-
    - {protip.map((item, index) => ( -
  • {item.points}
  • - ))} -
-
- -
-

- The Verdict -

-

{Paragraphs[4]}

-
-
- - {/* Article Footer */} -
-
-
-

- Share this article -

-
- {[ - { - name: "Twitter", - url: "https://twitter.com/intent/tweet?url=https%3A%2F%2Fwww.dsavisualizer.in%2Fblogs%2FContent%2FdsaWebDev&text=Just%20read%20this%20insighful%20blog%3A%20Is%20Data%20Structures%20and%20Algorithms%20Important%20for%20Web%20Developers%3F%20%23WebDev%20%23DSA%20%23Programming" - }, - { - name: "LinkedIn", - url: "https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fwww.dsavisualizer.in%2Fblogs%2FContent%2FdsaWebDev" - }, - { - name: "Facebook", - url: "https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fwww.dsavisualizer.in%2Fblogs%2FContent%2FdsaWebDev" - } - ].map((social) => ( - - ))} -
-
-
-
-
- ); -}; - -export default BlogContent; \ No newline at end of file diff --git a/app/blogs/Content/timeRequired/page.jsx b/app/blogs/Content/timeRequired/page.jsx deleted file mode 100755 index 73e0d99c9..000000000 --- a/app/blogs/Content/timeRequired/page.jsx +++ /dev/null @@ -1,61 +0,0 @@ -import Navbar from "@/app/components/navbar"; -import Footer from "@/app/components/footer"; -import Content from "@/app/blogs/Content/timeRequired/content"; - -export const metadata = { - title: "Time Required to Learn and Master DSA", - description: - "Uncover how much time it takes to learn and master Data Structures and Algorithms (DSA) for web development. Get practical timelines, tips, and strategies for efficient DSA learning.", - keywords: [ - "Time to Learn DSA", - "DSA Mastery Timeline", - "Data Structures", - "Algorithms", - "Learning Path", - "Web Development", - "Frontend", - "Backend", - "DSA Study Plan", - "Coding Interview Preparation", - ], - authors: [{ name: "Sohan Rout", url: "https://www.linkedin.com/in/sohan-rout" }], - openGraph: { - title: "Time Required to Learn and Master DSA", - description: - "Find out how long it takes to learn and master DSA for web development. Explore realistic timelines, learning strategies, and tips for success.", - url: "./blog/timeRequired.png", - siteName: "DSA Visualizer", - locale: "en_IN", - type: "article", - images: [ - { - url: "./blog/timeRequired.png", // Replace with actual OG image - width: 1200, - height: 630, - alt: "Time Required to Learn DSA", - }, - ], - }, - twitter: { - card: "summary_large_image", - title: "Time Required to Learn and Master DSA", - description: - "How much time does it take to learn DSA? Get timelines, strategies, and tips for mastering Data Structures and Algorithms.", - images: ["./blog/timeRequired.png"], - }, - category: "Technology", - publishedTime: "2024-05-15T08:00:00Z", - robots: "index, follow", -}; - -const page = () => { - return( -
- - -
-
- ); -} - -export default page; \ No newline at end of file diff --git a/app/blogs/Content/whatIsDS/content.jsx b/app/blogs/Content/whatIsDS/content.jsx deleted file mode 100755 index 12551cbca..000000000 --- a/app/blogs/Content/whatIsDS/content.jsx +++ /dev/null @@ -1,243 +0,0 @@ -"use client"; -import { FiCopy, FiBookmark, FiShare2 } from "react-icons/fi"; -import { useState } from "react"; - -const BlogContent = () => { - const [copied, setCopied] = useState(false); - - const handleCopy = () => { - navigator.clipboard.writeText(window.location.href); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }; - - const Paragraphs = [ - `If you're new to coding, you've probably come across terms like array, stack, or linked list and thought, "What does that even mean?" Don't worry — you're not alone. These are all types of data structures, and they form the foundation of how programs organize and process information.`, - `In this guide, we'll break down what data structures are, why they matter, and introduce you to the most common types — all in simple language with relatable examples.`, - `A data structure is a way to organize and store data in a computer so it can be used efficiently.`, - `Think of it like organizing your wardrobe: shirts go in one drawer, pants in another, socks in a box. Each drawer (or structure) is designed to hold and access a specific type of item. Similarly, in programming, different data structures are used depending on the type of data and what you want to do with it.`, - `Here's why every programmer needs to understand data structures:`, - `Data structures are the toolbox every programmer must carry. Mastering them helps you build efficient, scalable, and real-world applications. Start small — practice with basic structures, and slowly move up to complex ones. If you're consistent, what once felt like intimidating jargon will become second nature.`, - ]; - - const importancePoints = [ - { points: "Efficiency: The right structure makes programs faster and less memory-hungry" }, - { points: "Scalability: Handles larger data more smoothly" }, - { points: "Problem Solving: Many coding problems are based on data structures" }, - { points: "Real-World Use: From social media feeds to navigation systems — they're everywhere" }, - ]; - - const dataStructures = [ - { - title: "Array", - description: "Like a row of boxes where each box holds a value", - image: "/blog/DSimage/array.png", - useCases: "Great for storing ordered items" - }, - { - title: "Stack (LIFO)", - description: "Imagine a stack of plates - you add and remove from the top only", - image: "/blog/DSimage/stack.png", - useCases: "Use when you need to reverse things or track history" - }, - { - title: "Queue (FIFO)", - description: "Think of people standing in line - first person gets served first", - image: "/blog/DSimage/queue.png", - useCases: "Perfect for scheduling tasks or managing queues" - }, - { - title: "Linked List", - description: "A chain of nodes where each holds a value and a link to the next", - image: "/blog/DSimage/linkedList.png", - useCases: "Flexible in size with easier insertions/removals" - }, - { - title: "Tree", - description: "Starts with a root and branches out in non-linear fashion", - image: "/blog/DSimage/tree.png", - useCases: "Used in file systems, decision trees, and databases" - }, - { - title: "Graph", - description: "Networks of nodes and edges like social connections", - image: "/blog/DSimage/graph.png", - useCases: "Used in social networks, maps, and recommendation systems" - }, - ]; - - const realLifeExample = [ - {points: "Building a contact list app with arrays to store names"}, - {points: "Using hash tables to search names quickly"}, - {points: "Implementing sorting algorithms to organize contacts"}, - ]; - - return ( -
- {/* Article Header */} -
-
- - Programming Basics - -
- - - -
-
- -

- Data Structures Explained: A Beginner's Guide -

- -
- Published on June 10, 2024 - - 6 min read -
-
- - {/* Featured Image */} -
- Visual representation of different data structures -
-
-

- Understanding data structures is fundamental to programming -

-
-
- - {/* Article Content */} -
-

- {Paragraphs[0]} -

-

- {Paragraphs[1]} -

- -
-

- What Is a Data Structure? -

-

- {Paragraphs[2]} -

-

{Paragraphs[3]}

-
- -
-

- Why Are Data Structures Important? -

-
-
    - {importancePoints.map((item, index) => ( -
  • {item.points}
  • - ))} -
-
-
- -
-

- Types of Data Structures -

-
- {dataStructures.map((item, index) => ( -
-
-
- {item.title} -
-
-

- {item.title} -

-

- {item.description} -

-

- Use cases: {item.useCases} -

-
-
-
- ))} -
-
- -
-

- Real-Life Example: Why It Matters -

-

Imagine building a contact list app:

-
    - {realLifeExample.map((item, index) => ( -
  • {item.points}
  • - ))} -
-
- -
-

- Final Thoughts -

-

{Paragraphs[5]}

-
-
- - {/* Article Footer */} -
-
-
-

- Share this article -

-
- {["Twitter", "LinkedIn", "Facebook"].map((social) => ( - - ))} -
-
-
-
-
- ); -}; - -export default BlogContent; \ No newline at end of file diff --git a/app/blogs/Content/whatIsDS/page.jsx b/app/blogs/Content/whatIsDS/page.jsx deleted file mode 100755 index b25ef368d..000000000 --- a/app/blogs/Content/whatIsDS/page.jsx +++ /dev/null @@ -1,61 +0,0 @@ -import Navbar from "@/app/components/navbar"; -import Footer from "@/app/components/footer"; -import Content from "@/app/blogs/Content/whatIsDS/content"; - -export const metadata = { - title: "What Are Data Structures? A Beginner-Friendly Guide", - description: - "Confused by arrays, stacks, or linked lists? This beginner-friendly guide breaks down what data structures are, their types, and why they matter for every aspiring programmer.", - keywords: [ - "Data Structures", - "Beginner Programming", - "DSA", - "Arrays", - "Stacks", - "Linked Lists", - "Programming Basics", - "Computer Science", - "Coding for Beginners", - "Programming Concepts" - ], - authors: [{ name: "Sohan Rout", url: "https://www.linkedin.com/in/sohan-rout" }], - openGraph: { - title: "What Are Data Structures? A Beginner-Friendly Guide", - description: - "Understand the fundamentals of data structures in simple terms. A must-read guide for anyone new to programming and computer science.", - url: "./blog/whatIsDS.png", - siteName: "DSA Visualizer", - locale: "en_IN", - type: "article", - images: [ - { - url: "./blog/whatIsDS.png", // Replace with actual OG image - width: 1200, - height: 630, - alt: "Beginner’s Guide to Data Structures", - }, - ], - }, - twitter: { - card: "summary_large_image", - title: "What Are Data Structures? A Beginner-Friendly Guide", - description: - "Kickstart your programming journey by learning what data structures are and how they work. Explained in a simple, visual way.", - images: ["./blog/whatIsDS.png"], - }, - category: "Data Structures & Algorithms", - publishedTime: "2025-05-23T08:00:00Z", - robots: "index, follow", -}; - -const page = () => { - return( -
- - -
-
- ); -} - -export default page; \ No newline at end of file diff --git a/app/blogs/blogPage.jsx b/app/blogs/blogPage.jsx deleted file mode 100755 index 309588303..000000000 --- a/app/blogs/blogPage.jsx +++ /dev/null @@ -1,330 +0,0 @@ -"use client"; -import { useState, useEffect, useRef } from "react"; -import Link from "next/link"; -import { - FiClock, - FiCalendar, - FiArrowRight, -} from "react-icons/fi"; -import { motion, AnimatePresence } from "framer-motion"; -import PopularTopics from "@/app/blogs/components/PopularTopics"; -import blogData from "@/app/blogs/data/blogs.json"; - -const BlogPage = () => { - // State management - const [searchQuery, setSearchQuery] = useState(""); - const [activeCategory, setActiveCategory] = useState("All"); - const [isSearchFocused, setIsSearchFocused] = useState(false); - const searchRef = useRef(null); - - // Filtered blogs - const filteredBlogs = blogData.filter((blog) => { - const matchesSearch = - searchQuery === "" || - blog.title.toLowerCase().includes(searchQuery.toLowerCase()) || - blog.tags.some((tag) => - tag.toLowerCase().includes(searchQuery.toLowerCase()) - ); - - const matchesCategory = - activeCategory === "All" || blog.category === activeCategory; - - return matchesSearch && matchesCategory; - }); - - // Categories - const categories = ["All", ...new Set(blogData.map((blog) => blog.category))]; - - // Featured posts (latest 5 by date) - const featuredPosts = [...blogData] - .sort((a, b) => new Date(b.date) - new Date(a.date)) - .slice(0, 4); - - // Popular tags - const popularTags = [ - "React", - "JavaScript", - "CSS", - "TypeScript", - "Web Dev", - "Performance", - "DSA", - ]; - - // Handle click outside search - useEffect(() => { - const handleClickOutside = (event) => { - if (searchRef.current && !searchRef.current.contains(event.target)) { - setIsSearchFocused(false); - } - }; - document.addEventListener("mousedown", handleClickOutside); - return () => document.removeEventListener("mousedown", handleClickOutside); - }, []); - - return ( -
-
- {/* Hero Section */} -
- - Insights for{" "} - - Modern Developers - - - - Cutting-edge tutorials, guides, and deep dives on web development, - programming, and more. - - - {/* Email Subscribe Section */} - -
- - -
-

- By clicking "Subscribe", you agree to receive updates when new blogs are published. -

-
-
- - {/* Featured Posts */} -
-
-

- Featured Articles -

-
-
- -
- {/* Left column: recent upload (big card) - vertical layout */} - - -
- {featuredPosts[0].title} -
- - {featuredPosts[0].category} - -
-
-
- - {featuredPosts[0].date} - - {featuredPosts[0].readTime} -
-

- {featuredPosts[0].title} -

-

- {featuredPosts[0].excerpt} -

-
- {featuredPosts[0].tags.map((tag, i) => ( - - #{tag} - - ))} -
-
- - - - {/* Right column: next 4 featured cards in vertical list-style layout */} -
- {featuredPosts.slice(1, 5).map((post, index) => ( - -
- {post.title} -
-
-
- - {post.date} - - {post.readTime} -
-

- {post.title} -

-

- {post.excerpt} -

-
-
- ))} -
-
-
- - {/* Popular Tags */} - { - setSearchQuery(tag); - setActiveCategory("All"); - }} - /> - - {/* Category Filter */} -
-
- {categories.map((category) => ( - - ))} -
-
- - {/* Articles List */} -
-

- {activeCategory === "All" ? "All Blog Posts" : activeCategory} - - ({filteredBlogs.length} articles) - -

- -
- {filteredBlogs.length > 0 ? ( - filteredBlogs.map((post) => ( - - -
-
- {post.title} -
-
-
-
- - {post.date} -
-
- Read article -
-
- - {post.category} - -

- {post.title} -

-
- {post.tags.map((tag, i) => ( - - #{tag} - - ))} -
-
-
- -
- )) - ) : ( -
-
🔍
-

- No articles found -

-

- We couldn't find any articles matching your search. Try a - different term or browse our categories. -

- -
- )} -
-
-
-
- ); -}; - -export default BlogPage; \ No newline at end of file diff --git a/app/blogs/components/PopularTopics.jsx b/app/blogs/components/PopularTopics.jsx deleted file mode 100755 index 8061e4efc..000000000 --- a/app/blogs/components/PopularTopics.jsx +++ /dev/null @@ -1,58 +0,0 @@ -'use client'; -import { motion } from 'framer-motion'; -import { useState } from 'react'; - -const PopularTopics = ({ tags, onTagClick }) => { - const [showFilters, setShowFilters] = useState(false); - return ( -
-

- Explore Popular Topics -

-
-
-
- - - - - - onTagClick(e.target.value)} - className="w-full pl-10 pr-4 py-2 rounded-lg border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-zinc-800 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500" - /> -
- - -
- - {showFilters && ( -
- {tags.map((tag) => ( - onTagClick(tag)} - className="px-5 py-2 bg-white dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 rounded-full text-sm font-medium hover:bg-zinc-100 dark:hover:bg-zinc-700 border border-zinc-200 dark:border-zinc-700 shadow-sm hover:shadow-md transition-all" - > - #{tag} - - ))} -
- )} -
- ); -}; - -export default PopularTopics; \ No newline at end of file diff --git a/app/blogs/data/blogs.json b/app/blogs/data/blogs.json deleted file mode 100755 index f0c1bfc73..000000000 --- a/app/blogs/data/blogs.json +++ /dev/null @@ -1,46 +0,0 @@ -[ - { - "id": 1, - "title": "Is Data Structures and Algorithms Important for Web Developers?", - "excerpt": "Explore why understanding data structures and algorithms can help web developers write efficient, scalable, and maintainable code.", - "date": "May 17, 2025", - "readTime": "8 min read", - "slug": "/blogs/Content/dsaWebDev", - "category": "Web Development", - "tags": ["DSA", "Web Development", "Algorithms"], - "image": "/blog/dsaWebDev.png" - }, - { - "id": 2, - "title": "Are Data Structures and Algorithms Different for Different Languages?", - "excerpt": "Explore the differences in data structures and algorithms across various programming languages and how it impacts implementation.", - "date": "May 19, 2025", - "readTime": "5 min read", - "slug": "/blogs/Content/dsaDifferent", - "category": "Programming Languages", - "tags": ["DSA", "Programming Languages", "Algorithms"], - "image": "/blog/dsaDifferent.png" - }, - { - "id": 3, - "title": "What Are Data Structures? A Beginner-Friendly Guide", - "excerpt": "Confused by terms like arrays, stacks, or linked lists? This guide breaks down data structures in a simple, relatable way. Understand what they are, why they matter, and how they form the backbone of every efficient program.", - "date": "May 23, 2025", - "readTime": "10 min read", - "slug": "/blogs/Content/whatIsDS", - "category": "Computer Science Fundamentals ", - "tags": ["Data Structures", "DSA", "Algorithms", "Beginner Programming"], - "image": "/blog/whatIsDS.png" - }, - { - "id": 4, - "title": "Time Required to Learn and Master DSA", - "excerpt": "Find out how long it takes to learn and master DSA for web development. Explore realistic timelines, learning strategies, and tips for success.", - "date": "Jun 20, 2025", - "readTime": "10 min read", - "slug": "/blogs/Content/timeRequired", - "category": "DSA", - "tags": ["Data Structures", "DSA", "Algorithms", "Beginner Programming"], - "image": "/blog/timeRequired.png" - } - ] \ No newline at end of file diff --git a/app/blogs/page.jsx b/app/blogs/page.jsx deleted file mode 100755 index 076565df4..000000000 --- a/app/blogs/page.jsx +++ /dev/null @@ -1,49 +0,0 @@ -import BlogPage from "@/app/blogs/blogPage"; -import Navbar from "@/app/components/navbar"; -import Footer from "@/app/components/footer"; - -export const metadata = { - title: 'DSA Blogs & Guides | Learn Data Structures and Algorithms Effectively', - description: - 'Explore beginner-friendly blogs on Data Structures and Algorithms (DSA) covering Python, Java, C++, Web Development, Machine Learning, and more. Learn, practice, and master DSA through curated insights.', - keywords: [ - 'are data structures and algorithms different for different languages', - 'are data structures and algorithms important', - 'are data structures and algorithms hard', - 'are data structures and algorithms important for data science', - 'are data structures and algorithms important for machine learning', - 'is data structures and algorithms important for web developers', - 'is data structures and algorithms same for all languages', - 'what are data structures and algorithms in python', - 'what are data structures and algorithms used for', - 'what are data structures and algorithms in java', - 'what are data structures and algorithms in c++', - 'is leetcode data structures and algorithms worth it', - 'is learning data structures and algorithms worth it', - 'where can i learn data structures and algorithms for free', - 'how can i learn data structures and algorithms', - 'do i need to learn data structures and algorithms for web development', - 'do i need to learn data structures and algorithms for machine learning', - 'difference between data structures and algorithms', - 'when to learn data structures and algorithms', - 'is it hard to learn data structures and algorithms', - 'can i learn data structures and algorithms in python', - 'can i learn data structures and algorithms in java', - 'DSA blog for beginners', - 'learn DSA with examples', - 'best blogs on algorithms and data structures' - ], - robots: "index, follow", -}; - -const page = () => { - return ( - <> - - -