{title}
-{body}
-diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 8144802c..883c04d4 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -4,7 +4,6 @@ about: Create a report to help us improve DevPath India title: '[BUG] ' labels: bug assignees: '' - --- **Describe the bug** @@ -12,6 +11,7 @@ A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: + 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' @@ -24,14 +24,16 @@ A clear and concise description of what you expected to happen. If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - - OS: [e.g. Windows, Mac, Linux] - - Browser: [e.g. Chrome, Safari, Firefox] - - Version: [e.g. 22] + +- OS: [e.g. Windows, Mac, Linux] +- Browser: [e.g. Chrome, Safari, Firefox] +- Version: [e.g. 22] **Smartphone (please complete the following information):** - - Device: [e.g. iPhone 13, Pixel 6] - - OS: [e.g. iOS 15, Android 13] - - Browser: [e.g. Safari, Chrome] + +- Device: [e.g. iPhone 13, Pixel 6] +- OS: [e.g. iOS 15, Android 13] +- Browser: [e.g. Safari, Chrome] **Additional context** Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 8a408bb5..422ff629 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -4,7 +4,6 @@ about: Suggest an idea for DevPath India title: '[FEATURE] ' labels: enhancement assignees: '' - --- **Is your feature request related to a problem? Please describe.** diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index b27e2a8b..3ad1dd43 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,21 +1,27 @@ ## Description + Fixes # (issue) ## Type of change + + - [ ] 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) - [ ] This change requires a documentation update ## How Has This Been Tested? + + - [ ] Local testing - [ ] Vercel Preview Deployment ## Checklist: + - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas diff --git a/.github/workflows/gitleaks.yml b/.github/workflows/gitleaks.yml index d50aaa7b..89654e89 100644 --- a/.github/workflows/gitleaks.yml +++ b/.github/workflows/gitleaks.yml @@ -1,9 +1,9 @@ name: Gitleaks on: push: - branches: [ "master", "main" ] + branches: ['master', 'main'] pull_request: - branches: [ "master", "main" ] + branches: ['master', 'main'] jobs: scan: diff --git a/.github/workflows/label-migrator.yml b/.github/workflows/label-migrator.yml index 2ed12738..45717652 100644 --- a/.github/workflows/label-migrator.yml +++ b/.github/workflows/label-migrator.yml @@ -20,7 +20,7 @@ jobs: console.log("No PR body found"); return; } - + // Regex to find linked issues like "Fixes #123", "Resolves #123", "Closes #123" const issueRegex = /(?:fix(?:e[sd])?|resolve[sd]?|close[sd]?)\s+#(\d+)/gi; let match; @@ -28,16 +28,16 @@ jobs: while ((match = issueRegex.exec(prBody)) !== null) { issueNumbers.push(parseInt(match[1])); } - + if (issueNumbers.length === 0) { console.log("No linked issues found in PR body"); return; } - + console.log(`Found linked issues: ${issueNumbers.join(", ")}`); - + const labelsToAdd = new Set(); - + for (const issueNumber of issueNumbers) { try { const issue = await github.rest.issues.get({ @@ -53,7 +53,7 @@ jobs: console.log(`Could not fetch issue #${issueNumber}: ${error.message}`); } } - + if (labelsToAdd.size > 0) { console.log(`Adding labels: ${Array.from(labelsToAdd).join(", ")}`); await github.rest.issues.addLabels({ diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 0ccc38ef..203260b2 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -17,23 +17,23 @@ diverse, inclusive, and healthy community. Examples of behavior that contributes to a positive environment for our community include: -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience -* Focusing on what is best not just for us as individuals, but for the +- Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: -* The use of sexualized language or imagery, and sexual attention or +- The use of sexualized language or imagery, and sexual attention or advances of any kind -* Trolling, insulting or derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or email +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, without their explicit permission -* Other conduct which could reasonably be considered inappropriate in a +- Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0d18eb4d..b6878895 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -307,4 +307,4 @@ If you are contributing as part of GSSoC, please follow these guidelines: ## Final Notes -Thank you for contributing to DevPath Web. Your time, effort, and ideas help make this project better for the whole community. \ No newline at end of file +Thank you for contributing to DevPath Web. Your time, effort, and ideas help make this project better for the whole community. diff --git a/GIT_GUIDE.md b/GIT_GUIDE.md index b2b2584a..3ea23880 100644 --- a/GIT_GUIDE.md +++ b/GIT_GUIDE.md @@ -11,6 +11,7 @@ If you're making your first contribution, follow the steps carefully. Forking creates a copy of the repository under your GitHub account. ### Steps + 1. Open the original repository. 2. Click the **Fork** button on the top-right corner. 3. GitHub will create a copy in your account. @@ -106,6 +107,7 @@ git checkout -b bug/login-error-fix # 6. Make Your Changes Now you can: + - Add new files - Modify existing files - Fix bugs @@ -257,9 +259,10 @@ Replace `3` with the number of commits you want to combine. # 15. Need Help? If you get stuck: + - Read the project documentation - Ask maintainers politely - Search GitHub Discussions or Issues - Learn gradually — everyone starts somewhere 🚀 -Happy Contributing! 🎉 \ No newline at end of file +Happy Contributing! 🎉 diff --git a/README.md b/README.md index 5abeb28b..162846e8 100644 --- a/README.md +++ b/README.md @@ -28,12 +28,12 @@ | Section | Description | | ----------------------------------------------------------------- | -------------------------------------------------------------------------- | | [🚀 Features](#-features) | Explore the platform's core functionality and community-focused features. | -| [🛠️ Tech Stack](#️-tech-stack) | Discover the technologies, frameworks, and services powering the platform. | +| [🛠️ Tech Stack](#️-tech-stack) | Discover the technologies, frameworks, and services powering the platform. | | [📂 Project Structure](#-project-structure) | Understand the organization and architecture of the codebase. | | [📸 Screenshots](#-screenshots) | Preview key pages and user experiences. | | [🏁 Getting Started](#-getting-started) | Set up and run the project locally. | | [📋 Prerequisites](#-prerequisites) | Review required tools and dependencies. | -| [⚡ Installation](#-installation) | Follow the project installation steps. | +| [⚡ Installation](#-installation) | Follow the project installation steps. | | [🔥 Local Firebase Configuration](#-local-firebase-configuration) | Configure Firebase for local development. | | [📜 Available Scripts](#-available-scripts) | Reference commonly used development commands. | | [🤝 Contributing](#-contributing) | Learn how to contribute to the project. | @@ -45,13 +45,13 @@ ## 🚀 Features -* 🤝 **Community Hub** — Connect with developers, mentors, and contributors. -* 📅 **Event Management** — Discover hackathons, workshops, and community events. -* 📚 **Resource Library** — Access curated roadmaps, tutorials, and learning resources. -* 📖 **Wiki & Knowledge Base** — Explore guides, documentation, and community articles. -* 👤 **User Profiles** — Showcase your skills, achievements, and contributions. -* 🌟 **Open Source Collaboration** — Contribute to projects and grow through real-world experience. -* 📱 **Responsive Design** — Seamlessly accessible across desktop, tablet, and mobile devices. +- 🤝 **Community Hub** — Connect with developers, mentors, and contributors. +- 📅 **Event Management** — Discover hackathons, workshops, and community events. +- 📚 **Resource Library** — Access curated roadmaps, tutorials, and learning resources. +- 📖 **Wiki & Knowledge Base** — Explore guides, documentation, and community articles. +- 👤 **User Profiles** — Showcase your skills, achievements, and contributions. +- 🌟 **Open Source Collaboration** — Contribute to projects and grow through real-world experience. +- 📱 **Responsive Design** — Seamlessly accessible across desktop, tablet, and mobile devices. --- @@ -63,22 +63,21 @@ DevPath India is built using a modern, scalable, and developer-friendly technolo
|
|
---
@@ -181,10 +179,10 @@ Get DevPath India running locally in just a few steps.
Make sure you have:
-* Node.js (Latest LTS recommended)
-* npm, yarn, or pnpm
-* Git
-* A Firebase project
+- Node.js (Latest LTS recommended)
+- npm, yarn, or pnpm
+- Git
+- A Firebase project
---
@@ -217,9 +215,9 @@ Add your Firebase credentials to `.env.local`.
1. Create a project in the Firebase Console.
2. Enable:
+ - Authentication
+ - Firestore Database
- * Authentication
- * Firestore Database
3. Register a Web App and copy the Firebase configuration.
4. Paste the values into `.env.local`.
@@ -301,9 +299,9 @@ Contributions of all sizes are welcome! Whether you're fixing bugs, improving do
Before contributing, please review our:
-* 📖 [Contributing Guidelines](CONTRIBUTING.md)
-* 🐛 [Open Issues](https://github.com/devpathindcommunity-india/DevPath-Web/issues)
-* 🌱 [Good First Issues](https://github.com/devpathindcommunity-india/DevPath-Web/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22)
+- 📖 [Contributing Guidelines](CONTRIBUTING.md)
+- 🐛 [Open Issues](https://github.com/devpathindcommunity-india/DevPath-Web/issues)
+- 🌱 [Good First Issues](https://github.com/devpathindcommunity-india/DevPath-Web/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22)
We appreciate every contribution, from first-time contributors to experienced maintainers.
@@ -315,7 +313,7 @@ DevPath India is committed to fostering a welcoming, inclusive, and respectful c
By participating in this project, you agree to follow our:
-* 📜 [Code of Conduct](CODE_OF_CONDUCT.md)
+- 📜 [Code of Conduct](CODE_OF_CONDUCT.md)
Let's build a positive and collaborative environment together.
@@ -327,15 +325,15 @@ This project is distributed under the **DevPath India Source-Available License**
### What You Can Do
-* ✅ Clone and run the project locally
-* ✅ Learn from and modify the source code
-* ✅ Submit pull requests and community contributions
+- ✅ Clone and run the project locally
+- ✅ Learn from and modify the source code
+- ✅ Submit pull requests and community contributions
### Restrictions
-* ❌ Commercial use is prohibited
-* ❌ Hosting public clones or competing services is not permitted
-* ❌ Redistribution under the DevPath India brand is not allowed
+- ❌ Commercial use is prohibited
+- ❌ Hosting public clones or competing services is not permitted
+- ❌ Redistribution under the DevPath India brand is not allowed
> [!WARNING]
> The **DevPath India** name, logo, branding assets, and visual identity are protected and may not be used without explicit permission.
@@ -344,10 +342,10 @@ For complete terms, please refer to the [LICENSE](LICENSE) file.
---
-
## 🌟 Major Contributors
- **Aditya948351** - Core Maintainer & Lead Developer
+
---
diff --git a/THEME_COLORS_DOCUMENTATION.md b/THEME_COLORS_DOCUMENTATION.md index a974d8c8..19ca6b65 100644 --- a/THEME_COLORS_DOCUMENTATION.md +++ b/THEME_COLORS_DOCUMENTATION.md @@ -11,21 +11,23 @@ This document explains the theme-aware text color system that has been implement The following CSS variables have been added to support theme-aware text colors: #### Light Mode (Default) + ```css ---text-primary: #0f172a; /* Dark slate for main text */ ---text-secondary: #64748b; /* Medium slate for secondary text */ ---text-muted: #94a3b8; /* Light slate for muted text */ ---text-light: #f1f5f9; /* Light color for text on dark backgrounds */ ---text-dark: #0f172a; /* Dark color for text on light backgrounds */ +--text-primary: #0f172a; /* Dark slate for main text */ +--text-secondary: #64748b; /* Medium slate for secondary text */ +--text-muted: #94a3b8; /* Light slate for muted text */ +--text-light: #f1f5f9; /* Light color for text on dark backgrounds */ +--text-dark: #0f172a; /* Dark color for text on light backgrounds */ ``` #### Dark Mode (.dark class) + ```css ---text-primary: #ffffff; /* White for main text */ ---text-secondary: #94a3b8; /* Slate for secondary text */ ---text-muted: #64748b; /* Darker slate for muted text */ ---text-light: #e2e8f0; /* Light color for text */ ---text-dark: #ffffff; /* White for dark theme */ +--text-primary: #ffffff; /* White for main text */ +--text-secondary: #94a3b8; /* Slate for secondary text */ +--text-muted: #64748b; /* Darker slate for muted text */ +--text-light: #e2e8f0; /* Light color for text */ +--text-dark: #ffffff; /* White for dark theme */ ``` ### 2. Utility Classes in `globals.css` @@ -67,15 +69,15 @@ Use CSS variables for text colors: ```css .myComponent { - color: var(--text-primary); /* Main text */ + color: var(--text-primary); /* Main text */ } .myComponent p { - color: var(--text-secondary); /* Secondary text */ + color: var(--text-secondary); /* Secondary text */ } .myComponent .helper { - color: var(--text-muted); /* Muted/disabled text */ + color: var(--text-muted); /* Muted/disabled text */ } ``` @@ -103,7 +105,7 @@ import { useThemeColors } from '@/lib/theme'; function MyComponent() { const { textPrimary, textSecondary } = useThemeColors(); - + return ( <>
Main text
@@ -118,17 +120,20 @@ function MyComponent() { ### ✅ Do's 1. **Use CSS variables for static colors:** + ```css color: var(--text-primary); ``` 2. **Use Tailwind classes when possible:** + ```jsx - className="text-foreground" - className="text-muted-foreground" + className = 'text-foreground'; + className = 'text-muted-foreground'; ``` 3. **Add smooth transitions:** + ```css color: var(--text-primary); transition: color var(--transition-fast); @@ -142,6 +147,7 @@ function MyComponent() { ### ❌ Don'ts 1. **Avoid hardcoded color values:** + ```css /* ❌ BAD */ color: #000000; @@ -150,19 +156,21 @@ function MyComponent() { ``` 2. **Don't mix theme approaches:** + ```css /* ❌ BAD */ .myClass { - color: white; /* Will be invisible in light mode */ + color: white; /* Will be invisible in light mode */ } ``` 3. **Don't forget transitions:** + ```css /* ❌ Jarring color change */ color: var(--text-primary); /* without transition */ - + /* ✅ Smooth transition */ color: var(--text-primary); transition: color var(--transition-fast); @@ -172,13 +180,13 @@ function MyComponent() { ### Text Colors -| Variable | Light Mode | Dark Mode | Use Case | -|----------|-----------|----------|----------| -| `--text-primary` | #0f172a | #ffffff | Main headings and body text | -| `--text-secondary` | #64748b | #94a3b8 | Secondary information, captions | -| `--text-muted` | #94a3b8 | #64748b | Disabled, placeholder, helper text | -| `--text-light` | #f1f5f9 | #e2e8f0 | Text on dark backgrounds | -| `--text-dark` | #0f172a | #ffffff | Text on light backgrounds | +| Variable | Light Mode | Dark Mode | Use Case | +| ------------------ | ---------- | --------- | ---------------------------------- | +| `--text-primary` | #0f172a | #ffffff | Main headings and body text | +| `--text-secondary` | #64748b | #94a3b8 | Secondary information, captions | +| `--text-muted` | #94a3b8 | #64748b | Disabled, placeholder, helper text | +| `--text-light` | #f1f5f9 | #e2e8f0 | Text on dark backgrounds | +| `--text-dark` | #0f172a | #ffffff | Text on light backgrounds | ### Accent Colors (Fixed across themes) @@ -193,15 +201,17 @@ function MyComponent() { If you're updating existing components: 1. **Find hardcoded text colors:** + ```bash grep -r "color: #" src/components --include="*.module.css" ``` 2. **Replace with variables:** + ```css /* Before */ color: #475569; - + /* After */ color: var(--text-secondary); ``` diff --git a/backend/server.js b/backend/server.js index c89e7154..c53b9d42 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,7 +1,7 @@ -require("dotenv").config(); +require('dotenv').config(); -const app = require("./src/app"); -const logger = require("./src/utils/logger"); +const app = require('./src/app'); +const logger = require('./src/utils/logger'); const PORT = process.env.PORT || 4000; diff --git a/backend/src/app.js b/backend/src/app.js index 56df2cae..70aacd28 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -1,34 +1,37 @@ -const express = require("express"); -const cors = require("cors"); -const morgan = require("morgan"); +const express = require('express'); +const cors = require('cors'); +const morgan = require('morgan'); -const assistantRoutes = require("./routes/assistant"); -const { notFoundHandler, errorHandler } = require("./middlewares/errorMiddleware"); -const { requestLoggerStream } = require("./utils/logger"); +const assistantRoutes = require('./routes/assistant'); +const { + notFoundHandler, + errorHandler, +} = require('./middlewares/errorMiddleware'); +const { requestLoggerStream } = require('./utils/logger'); const app = express(); // Global middleware stack: CORS -> JSON parser -> request logs app.use( cors({ - origin: process.env.CORS_ORIGIN ? process.env.CORS_ORIGIN.split(",") : true, + origin: process.env.CORS_ORIGIN ? process.env.CORS_ORIGIN.split(',') : true, credentials: true, }) ); -app.use(express.json({ limit: "1mb" })); -app.use(morgan("combined", { stream: requestLoggerStream })); +app.use(express.json({ limit: '1mb' })); +app.use(morgan('combined', { stream: requestLoggerStream })); // Lightweight uptime probe for deployments/health checks. -app.get("/health", async (req, res) => { +app.get('/health', async (req, res) => { return res.status(200).json({ success: true, - message: "Assistant backend is healthy", + message: 'Assistant backend is healthy', timestamp: new Date().toISOString(), }); }); -app.use("/api/assistant", assistantRoutes); +app.use('/api/assistant', assistantRoutes); // Terminal middleware pair for unknown routes and runtime failures. app.use(notFoundHandler); diff --git a/backend/src/config/firebaseAdmin.js b/backend/src/config/firebaseAdmin.js index 6b69c2df..058fd6ab 100644 --- a/backend/src/config/firebaseAdmin.js +++ b/backend/src/config/firebaseAdmin.js @@ -1,7 +1,7 @@ -const admin = require("firebase-admin"); -const path = require("path"); -const fs = require("fs"); -const logger = require("../utils/logger"); +const admin = require('firebase-admin'); +const path = require('path'); +const fs = require('fs'); +const logger = require('../utils/logger'); let initialized = false; @@ -12,8 +12,8 @@ const resolveServiceAccountPath = (rawPath) => { candidatePaths.push(rawPath); } else { candidatePaths.push(path.resolve(process.cwd(), rawPath)); - candidatePaths.push(path.resolve(__dirname, "../../../", rawPath)); - candidatePaths.push(path.resolve(__dirname, "../../../../", rawPath)); + candidatePaths.push(path.resolve(__dirname, '../../../', rawPath)); + candidatePaths.push(path.resolve(__dirname, '../../../../', rawPath)); } for (const candidatePath of candidatePaths) { @@ -34,18 +34,19 @@ const initFirebaseAdmin = () => { const serviceAccountPath = process.env.FIREBASE_SERVICE_ACCOUNT_PATH || - path.resolve(process.cwd(), "../service-account-key.json"); - const resolvedServiceAccountPath = resolveServiceAccountPath(serviceAccountPath); + path.resolve(process.cwd(), '../service-account-key.json'); + const resolvedServiceAccountPath = + resolveServiceAccountPath(serviceAccountPath); if (!admin.apps.length) { try { admin.initializeApp({ credential: admin.credential.cert(require(resolvedServiceAccountPath)), }); - logger.info("Firebase Admin SDK initialized successfully"); + logger.info('Firebase Admin SDK initialized successfully'); } catch (error) { logger.error({ - message: "Firebase Admin SDK initialization failed", + message: 'Firebase Admin SDK initialization failed', error: error.message, serviceAccountPath: resolvedServiceAccountPath, stack: error.stack, @@ -98,4 +99,4 @@ module.exports = { getFirestore, getAuth, withRetry, -}; \ No newline at end of file +}; diff --git a/backend/src/controllers/assistantController.js b/backend/src/controllers/assistantController.js index a632bc73..1b83f2e2 100644 --- a/backend/src/controllers/assistantController.js +++ b/backend/src/controllers/assistantController.js @@ -1,13 +1,13 @@ -const AppError = require("../utils/AppError"); -const asyncHandler = require("../utils/asyncHandler"); +const AppError = require('../utils/AppError'); +const asyncHandler = require('../utils/asyncHandler'); const { createChatCompletion, createStreamingChatCompletionConfig, -} = require("../services/openRouterService"); +} = require('../services/openRouterService'); const { appendMessages, getConversationHistory, -} = require("../services/conversationService"); +} = require('../services/conversationService'); const chatCompletion = asyncHandler(async (req, res) => { const { conversationId, message, history = [] } = req.body; @@ -38,21 +38,24 @@ const chatCompletionStreamConfig = asyncHandler(async (req, res) => { const { message, history = [] } = req.body; if (!message) { - throw new AppError("message is required", 400, "VALIDATION_ERROR"); + throw new AppError('message is required', 400, 'VALIDATION_ERROR'); } - const streamConfig = createStreamingChatCompletionConfig({ message, history }); + const streamConfig = createStreamingChatCompletionConfig({ + message, + history, + }); return res.status(200).json({ success: true, stream: { url: streamConfig.url, - method: "POST", + method: 'POST', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, body: streamConfig.body, - note: "Use server-side proxy/stream endpoint in production to avoid exposing API key.", + note: 'Use server-side proxy/stream endpoint in production to avoid exposing API key.', }, }); }); diff --git a/backend/src/middlewares/errorMiddleware.js b/backend/src/middlewares/errorMiddleware.js index 527037aa..fbd462e8 100644 --- a/backend/src/middlewares/errorMiddleware.js +++ b/backend/src/middlewares/errorMiddleware.js @@ -1,10 +1,10 @@ -const logger = require("../utils/logger"); +const logger = require('../utils/logger'); const notFoundHandler = (req, res) => { return res.status(404).json({ success: false, error: { - code: "NOT_FOUND", + code: 'NOT_FOUND', message: `Route not found: ${req.method} ${req.originalUrl}`, }, }); @@ -12,7 +12,7 @@ const notFoundHandler = (req, res) => { const errorHandler = (err, req, res, next) => { const statusCode = err.statusCode || 500; - const code = err.code || "INTERNAL_ERROR"; + const code = err.code || 'INTERNAL_ERROR'; logger.error({ message: err.message, @@ -27,7 +27,7 @@ const errorHandler = (err, req, res, next) => { success: false, error: { code, - message: err.message || "Unexpected server error", + message: err.message || 'Unexpected server error', }, }); }; diff --git a/backend/src/middlewares/rateLimitMiddleware.js b/backend/src/middlewares/rateLimitMiddleware.js index 8a2c2c73..5252e6b4 100644 --- a/backend/src/middlewares/rateLimitMiddleware.js +++ b/backend/src/middlewares/rateLimitMiddleware.js @@ -1,4 +1,4 @@ -const rateLimit = require("express-rate-limit"); +const rateLimit = require('express-rate-limit'); const assistantRateLimiter = rateLimit({ windowMs: 60 * 1000, @@ -8,8 +8,8 @@ const assistantRateLimiter = rateLimit({ message: { success: false, error: { - code: "RATE_LIMIT_EXCEEDED", - message: "Too many assistant requests. Please try again shortly.", + code: 'RATE_LIMIT_EXCEEDED', + message: 'Too many assistant requests. Please try again shortly.', }, }, }); diff --git a/backend/src/middlewares/validateRequest.js b/backend/src/middlewares/validateRequest.js index 65cab19b..4ff49aeb 100644 --- a/backend/src/middlewares/validateRequest.js +++ b/backend/src/middlewares/validateRequest.js @@ -1,5 +1,5 @@ -const { validationResult } = require("express-validator"); -const AppError = require("../utils/AppError"); +const { validationResult } = require('express-validator'); +const AppError = require('../utils/AppError'); const validateRequest = (req, res, next) => { const errors = validationResult(req); @@ -8,7 +8,7 @@ const validateRequest = (req, res, next) => { } const firstError = errors.array()[0]; - return next(new AppError(firstError.msg, 400, "VALIDATION_ERROR")); + return next(new AppError(firstError.msg, 400, 'VALIDATION_ERROR')); }; module.exports = validateRequest; diff --git a/backend/src/routes/assistant.js b/backend/src/routes/assistant.js index b3392fac..5c594c50 100644 --- a/backend/src/routes/assistant.js +++ b/backend/src/routes/assistant.js @@ -1,19 +1,25 @@ -const express = require("express"); +const express = require('express'); -const { assistantRateLimiter } = require("../middlewares/rateLimitMiddleware"); -const validateRequest = require("../middlewares/validateRequest"); -const { chatValidationRules } = require("../validators/assistantValidators"); +const { assistantRateLimiter } = require('../middlewares/rateLimitMiddleware'); +const validateRequest = require('../middlewares/validateRequest'); +const { chatValidationRules } = require('../validators/assistantValidators'); const { chatCompletion, chatCompletionStreamConfig, -} = require("../controllers/assistantController"); +} = require('../controllers/assistantController'); const router = express.Router(); -router.post("/chat", assistantRateLimiter, chatValidationRules, validateRequest, chatCompletion); +router.post( + '/chat', + assistantRateLimiter, + chatValidationRules, + validateRequest, + chatCompletion +); router.post( - "/chat/stream-config", + '/chat/stream-config', assistantRateLimiter, chatValidationRules, validateRequest, diff --git a/backend/src/services/conversationService.js b/backend/src/services/conversationService.js index 30373537..94e18394 100644 --- a/backend/src/services/conversationService.js +++ b/backend/src/services/conversationService.js @@ -1,7 +1,7 @@ -const crypto = require("crypto"); -const { getFirestore, withRetry } = require("../config/firebaseAdmin"); +const crypto = require('crypto'); +const { getFirestore, withRetry } = require('../config/firebaseAdmin'); -const COLLECTION = "assistant_conversations"; +const COLLECTION = 'assistant_conversations'; const buildConversationId = () => crypto.randomUUID(); @@ -24,26 +24,29 @@ const createOrGetConversationRef = async (conversationId) => { return { ref, conversationId: resolvedConversationId }; }; -const appendMessages = async ({ conversationId, userMessage, assistantReply }) => { - const { ref, conversationId: resolvedConversationId } = await createOrGetConversationRef( - conversationId - ); +const appendMessages = async ({ + conversationId, + userMessage, + assistantReply, +}) => { + const { ref, conversationId: resolvedConversationId } = + await createOrGetConversationRef(conversationId); const now = new Date().toISOString(); const batch = getFirestore().batch(); - const messagesCollection = ref.collection("messages"); + const messagesCollection = ref.collection('messages'); const userRef = messagesCollection.doc(); const assistantRef = messagesCollection.doc(); batch.set(userRef, { - role: "user", + role: 'user', content: userMessage, createdAt: now, }); batch.set(assistantRef, { - role: "assistant", + role: 'assistant', content: assistantReply, createdAt: now, }); @@ -68,8 +71,8 @@ const getConversationHistory = async (conversationId, maxMessages = 20) => { db .collection(COLLECTION) .doc(conversationId) - .collection("messages") - .orderBy("createdAt", "asc") + .collection('messages') + .orderBy('createdAt', 'asc') .limit(maxMessages) .get() ); diff --git a/backend/src/services/openRouterService.js b/backend/src/services/openRouterService.js index 88c597e5..3adac194 100644 --- a/backend/src/services/openRouterService.js +++ b/backend/src/services/openRouterService.js @@ -6,22 +6,22 @@ const { FALLBACK_RESPONSES, } = require("../config/chatbotConfig"); -const OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions"; -const DEFAULT_PRIMARY_MODEL = "openai/gpt-oss-120b:free"; +const OPENROUTER_URL = 'https://openrouter.ai/api/v1/chat/completions'; +const DEFAULT_PRIMARY_MODEL = 'openai/gpt-oss-120b:free'; const DEFAULT_FALLBACK_MODELS = [ - "nvidia/nemotron-3-super:free", - "deepseek/deepseek-v4-flash:free", - "qwen/qwen3-next-80b-a3b-instruct:free", - "meta-llama/llama-3.3-70b-instruct:free", - "google/gemma-4-31b:free", + 'nvidia/nemotron-3-super:free', + 'deepseek/deepseek-v4-flash:free', + 'qwen/qwen3-next-80b-a3b-instruct:free', + 'meta-llama/llama-3.3-70b-instruct:free', + 'google/gemma-4-31b:free', ]; const DEFAULT_MODEL_WEIGHTS = { [DEFAULT_PRIMARY_MODEL]: 100, - "nvidia/nemotron-3-super:free": 95, - "deepseek/deepseek-v4-flash:free": 90, - "qwen/qwen3-next-80b-a3b-instruct:free": 85, - "meta-llama/llama-3.3-70b-instruct:free": 80, - "google/gemma-4-31b:free": 75, + 'nvidia/nemotron-3-super:free': 95, + 'deepseek/deepseek-v4-flash:free': 90, + 'qwen/qwen3-next-80b-a3b-instruct:free': 85, + 'meta-llama/llama-3.3-70b-instruct:free': 80, + 'google/gemma-4-31b:free': 75, }; const getSystemPrompt = () => { @@ -57,23 +57,23 @@ const buildMessages = (history, message) => { role: item.role, content: item.content, })), - { role: "user", content: message }, + { role: 'user', content: message }, ]; }; const parseModelList = (value) => - String(value || "") - .split(",") + String(value || '') + .split(',') .map((model) => model.trim()) .filter(Boolean); const parseModelWeights = (value) => - String(value || "") - .split(",") + String(value || '') + .split(',') .map((entry) => entry.trim()) .filter(Boolean) .reduce((weights, entry) => { - const [model, rawWeight] = entry.split("=").map((part) => part.trim()); + const [model, rawWeight] = entry.split('=').map((part) => part.trim()); if (model && rawWeight && !Number.isNaN(Number(rawWeight))) { weights[model] = Number(rawWeight); @@ -100,20 +100,24 @@ const getModelCandidates = () => { ? parseModelList(process.env.OPENROUTER_FALLBACK_MODELS) : DEFAULT_FALLBACK_MODELS; const weightOverrides = process.env.OPENROUTER_MODEL_WEIGHTS - ? parseModelWeights(process.env.OPENROUTER_MODEL_WEIGHTS) - : {}; + ? parseModelWeights(process.env.OPENROUTER_MODEL_WEIGHTS) + : {}; - const uniqueFallbackModels = fallbackModels.filter((model, index, models) => models.indexOf(model) === index); - const orderedFallbackModels = uniqueFallbackModels.sort((leftModel, rightModel) => { - const rightWeight = getWeightForModel(rightModel, weightOverrides); - const leftWeight = getWeightForModel(leftModel, weightOverrides); + const uniqueFallbackModels = fallbackModels.filter( + (model, index, models) => models.indexOf(model) === index + ); + const orderedFallbackModels = uniqueFallbackModels.sort( + (leftModel, rightModel) => { + const rightWeight = getWeightForModel(rightModel, weightOverrides); + const leftWeight = getWeightForModel(leftModel, weightOverrides); - if (rightWeight !== leftWeight) { - return rightWeight - leftWeight; - } + if (rightWeight !== leftWeight) { + return rightWeight - leftWeight; + } - return leftModel.localeCompare(rightModel); - }); + return leftModel.localeCompare(rightModel); + } + ); return [primaryModel, ...orderedFallbackModels].filter( (model, index, models) => models.indexOf(model) === index @@ -121,10 +125,10 @@ const getModelCandidates = () => { }; const getRequestHeaders = (apiKey) => ({ - "Content-Type": "application/json", + 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}`, - "HTTP-Referer": process.env.OPENROUTER_SITE_URL || "https://devpath.tech", - "X-Title": process.env.OPENROUTER_APP_NAME || "DevPath Assistant", + 'HTTP-Referer': process.env.OPENROUTER_SITE_URL || 'https://devpath.tech', + 'X-Title': process.env.OPENROUTER_APP_NAME || 'DevPath Assistant', }); const isRetryableOpenRouterError = (statusCode) => @@ -145,7 +149,7 @@ const callOpenRouter = async ({ apiKey, model, message, history }) => { try { response = await fetch(OPENROUTER_URL, { - method: "POST", + method: 'POST', headers: getRequestHeaders(apiKey), body: JSON.stringify(payload), }); @@ -155,18 +159,23 @@ const callOpenRouter = async ({ apiKey, model, message, history }) => { throw new AppError( `OpenRouter network request failed for model ${model}: ${error.message}`, 502, - "OPENROUTER_NETWORK_ERROR" + 'OPENROUTER_NETWORK_ERROR' ); } if (!response.ok) { - const messageText = data?.error?.message || `OpenRouter request failed for model ${model}`; - throw new AppError(messageText, response.status, "OPENROUTER_ERROR"); + const messageText = + data?.error?.message || `OpenRouter request failed for model ${model}`; + throw new AppError(messageText, response.status, 'OPENROUTER_ERROR'); } const reply = data?.choices?.[0]?.message?.content?.trim(); if (!reply) { - throw new AppError(`Model reply is empty for ${model}`, 502, "EMPTY_MODEL_RESPONSE"); + throw new AppError( + `Model reply is empty for ${model}`, + 502, + 'EMPTY_MODEL_RESPONSE' + ); } return { @@ -179,7 +188,11 @@ const callOpenRouter = async ({ apiKey, model, message, history }) => { const createChatCompletion = async ({ message, history = [] }) => { const apiKey = process.env.OPENROUTER_API_KEY; if (!apiKey) { - throw new AppError("OPENROUTER_API_KEY is not configured", 500, "CONFIG_ERROR"); + throw new AppError( + 'OPENROUTER_API_KEY is not configured', + 500, + 'CONFIG_ERROR' + ); } const modelCandidates = getModelCandidates(); @@ -196,7 +209,7 @@ const createChatCompletion = async ({ message, history = [] }) => { const shouldRetry = error instanceof AppError && - error.code === "OPENROUTER_ERROR" && + error.code === 'OPENROUTER_ERROR' && isRetryableOpenRouterError(error.statusCode); if (!shouldRetry || index === modelCandidates.length - 1) { @@ -205,7 +218,10 @@ const createChatCompletion = async ({ message, history = [] }) => { } } - throw lastError || new AppError("OpenRouter request failed", 502, "OPENROUTER_ERROR"); + throw ( + lastError || + new AppError('OpenRouter request failed', 502, 'OPENROUTER_ERROR') + ); }; const createStreamingChatCompletionConfig = ({ message, history = [] }) => { @@ -215,10 +231,10 @@ const createStreamingChatCompletionConfig = ({ message, history = [] }) => { return { url: OPENROUTER_URL, headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}`, - "HTTP-Referer": process.env.OPENROUTER_SITE_URL || "https://devpath.tech", - "X-Title": process.env.OPENROUTER_APP_NAME || "DevPath Assistant", + 'HTTP-Referer': process.env.OPENROUTER_SITE_URL || 'https://devpath.tech', + 'X-Title': process.env.OPENROUTER_APP_NAME || 'DevPath Assistant', }, body: { model, diff --git a/backend/src/utils/AppError.js b/backend/src/utils/AppError.js index ca523f1e..2123012f 100644 --- a/backend/src/utils/AppError.js +++ b/backend/src/utils/AppError.js @@ -1,5 +1,5 @@ class AppError extends Error { - constructor(message, statusCode = 500, code = "INTERNAL_ERROR") { + constructor(message, statusCode = 500, code = 'INTERNAL_ERROR') { super(message); this.statusCode = statusCode; this.code = code; diff --git a/backend/src/utils/logger.js b/backend/src/utils/logger.js index 2b96e924..62b6be2e 100644 --- a/backend/src/utils/logger.js +++ b/backend/src/utils/logger.js @@ -1,9 +1,13 @@ -const { createLogger, format, transports } = require("winston"); +const { createLogger, format, transports } = require('winston'); const logger = createLogger({ - level: process.env.LOG_LEVEL || "info", - format: format.combine(format.timestamp(), format.errors({ stack: true }), format.json()), - defaultMeta: { service: "assistant-backend" }, + level: process.env.LOG_LEVEL || 'info', + format: format.combine( + format.timestamp(), + format.errors({ stack: true }), + format.json() + ), + defaultMeta: { service: 'assistant-backend' }, transports: [new transports.Console()], }); diff --git a/backend/src/validators/assistantValidators.js b/backend/src/validators/assistantValidators.js index dd207bf0..f87deea1 100644 --- a/backend/src/validators/assistantValidators.js +++ b/backend/src/validators/assistantValidators.js @@ -1,29 +1,29 @@ -const { body } = require("express-validator"); +const { body } = require('express-validator'); const chatValidationRules = [ - body("conversationId") + body('conversationId') .optional({ nullable: true }) .isString() - .withMessage("conversationId must be a string"), - body("message") + .withMessage('conversationId must be a string'), + body('message') .exists({ checkFalsy: true }) - .withMessage("message is required") + .withMessage('message is required') .isString() - .withMessage("message must be a string") + .withMessage('message must be a string') .isLength({ min: 1, max: 4000 }) - .withMessage("message must be between 1 and 4000 characters"), - body("history") + .withMessage('message must be between 1 and 4000 characters'), + body('history') .optional() .isArray({ max: 50 }) - .withMessage("history must be an array with max 50 items"), - body("history.*.role") + .withMessage('history must be an array with max 50 items'), + body('history.*.role') .optional() - .isIn(["user", "assistant", "system"]) - .withMessage("history role must be user, assistant or system"), - body("history.*.content") + .isIn(['user', 'assistant', 'system']) + .withMessage('history role must be user, assistant or system'), + body('history.*.content') .optional() .isString() - .withMessage("history content must be a string"), + .withMessage('history content must be a string'), ]; module.exports = { diff --git a/docker-compose.yml b/docker-compose.yml index c75ccfc4..a838f916 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,13 +6,13 @@ services: image: devpath-web:dev container_name: devpath-web ports: - - "3000:3000" + - '3000:3000' env_file: - path: .env.local required: false environment: - NEXT_TELEMETRY_DISABLED: "1" - WATCHPACK_POLLING: "true" + NEXT_TELEMETRY_DISABLED: '1' + WATCHPACK_POLLING: 'true' volumes: - .:/app - node_modules:/app/node_modules diff --git a/eslint.config.mjs b/eslint.config.mjs index 05e726d1..626ca82e 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,6 +1,6 @@ -import { defineConfig, globalIgnores } from "eslint/config"; -import nextVitals from "eslint-config-next/core-web-vitals"; -import nextTs from "eslint-config-next/typescript"; +import { defineConfig, globalIgnores } from 'eslint/config'; +import nextVitals from 'eslint-config-next/core-web-vitals'; +import nextTs from 'eslint-config-next/typescript'; const eslintConfig = defineConfig([ ...nextVitals, @@ -8,10 +8,10 @@ const eslintConfig = defineConfig([ // Override default ignores of eslint-config-next. globalIgnores([ // Default ignores of eslint-config-next: - ".next/**", - "out/**", - "build/**", - "next-env.d.ts", + '.next/**', + 'out/**', + 'build/**', + 'next-env.d.ts', ]), ]); diff --git a/firebase.json b/firebase.json index 7c13681f..660a4250 100644 --- a/firebase.json +++ b/firebase.json @@ -2,11 +2,7 @@ "hosting": { "public": "out", "cleanUrls": true, - "ignore": [ - "firebase.json", - "**/.*", - "**/node_modules/**" - ], + "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], "rewrites": [ { "source": "/u/**", @@ -35,4 +31,4 @@ "rules": "firestore.rules", "indexes": "firestore.indexes.json" } -} \ No newline at end of file +} diff --git a/firestore.indexes.json b/firestore.indexes.json index 662881fc..b5d9216b 100644 --- a/firestore.indexes.json +++ b/firestore.indexes.json @@ -5,14 +5,14 @@ "queryScope": "COLLECTION", "fields": [ { "fieldPath": "completed", "order": "ASCENDING" }, - { "fieldPath": "date", "order": "ASCENDING" } + { "fieldPath": "date", "order": "ASCENDING" } ] }, { "collectionGroup": "leaderboard", "queryScope": "COLLECTION", "fields": [ - { "fieldPath": "xp", "order": "DESCENDING" }, + { "fieldPath": "xp", "order": "DESCENDING" }, { "fieldPath": "displayName", "order": "ASCENDING" } ] }, @@ -20,7 +20,7 @@ "collectionGroup": "leaderboard", "queryScope": "COLLECTION", "fields": [ - { "fieldPath": "weeklyXP", "order": "DESCENDING" }, + { "fieldPath": "weeklyXP", "order": "DESCENDING" }, { "fieldPath": "displayName", "order": "ASCENDING" } ] }, @@ -28,7 +28,7 @@ "collectionGroup": "leaderboard", "queryScope": "COLLECTION", "fields": [ - { "fieldPath": "monthlyXP", "order": "DESCENDING" }, + { "fieldPath": "monthlyXP", "order": "DESCENDING" }, { "fieldPath": "displayName", "order": "ASCENDING" } ] }, @@ -36,9 +36,17 @@ "collectionGroup": "gamification", "queryScope": "COLLECTION", "fields": [ - { "fieldPath": "xp", "order": "DESCENDING" }, + { "fieldPath": "xp", "order": "DESCENDING" }, { "fieldPath": "userId", "order": "ASCENDING" } ] + }, + { + "collectionGroup": "portfolios", + "queryScope": "COLLECTION", + "fields": [ + { "fieldPath": "username", "order": "ASCENDING" }, + { "fieldPath": "isPublic", "order": "ASCENDING" } + ] } ], "fieldOverrides": [] diff --git a/firestore.rules b/firestore.rules index 3c5e243d..0c526d53 100644 --- a/firestore.rules +++ b/firestore.rules @@ -95,16 +95,12 @@ service cloud.firestore { allow update, delete: if request.auth != null && (request.auth.uid == userId || isSuperAdmin()); } - // ── 🆕 Gamification subcollection ────────────────────────────────────── - // Stores XP, streak data, earned badges, and activity calendar per user. - // xp and earnedBadges are protected — only super admin or Cloud Functions - // should increment them. Users can write activityDates and streakFreezes. + match /gamification/{docId} { - allow read: if true; // Public so leaderboard components can read + allow read: if true; allow create: if request.auth != null && request.auth.uid == userId; allow update: if request.auth != null && ( isSuperAdmin() || - // Users can only update non-sensitive fields (streak calendar, freeze usage) (request.auth.uid == userId && !request.resource.data.diff(resource.data).affectedKeys().hasAny(['xp', 'earnedBadges', 'weeklyXP', 'monthlyXP'])) ); @@ -119,8 +115,6 @@ service cloud.firestore { } // ─── Leaderboard ────────────────────────────────────────────────────────── - // Updated: super admin or a Cloud Function (via admin SDK) should write xp/points. - // Direct user writes are blocked for score fields to prevent cheating. match /leaderboard/{userId} { allow read: if true; allow create, delete: if (request.auth != null && request.auth.uid == userId) || isSuperAdmin(); @@ -217,5 +211,20 @@ service cloud.firestore { allow read: if true; allow write: if isSuperAdmin(); } + + // ─── Portfolios ────────────────────────────────────────────────────────── + match /portfolios/{userId} { + allow read: if resource.data.isPublic == true + || (request.auth != null && request.auth.uid == userId); + allow create: if request.auth != null + && request.auth.uid == userId + && request.resource.data.userId == userId; + allow update: if request.auth != null + && request.auth.uid == userId + && request.resource.data.userId == resource.data.userId + && request.resource.data.username == resource.data.username; + allow delete: if false; + } + } } diff --git a/next.config.ts b/next.config.ts index 0e4fdf53..c296fb6e 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,4 +1,4 @@ -import type { NextConfig } from "next"; +import type { NextConfig } from 'next'; const nextConfig: NextConfig = { /* config options here */ diff --git a/package-lock.json b/package-lock.json index e4fb7598..e9454ba4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "github-markdown-css": "^5.8.1", "gsap": "^3.14.2", "html2canvas": "^1.4.1", + "html2pdf.js": "^0.14.0", "jspdf": "^4.2.1", "lucide-react": "^0.577.0", "next": "^16.2.6", @@ -43,6 +44,7 @@ "@testing-library/react": "^16.3.2", "@types/canvas-confetti": "^1.9.0", "@types/dompurify": "^3.0.5", + "@types/html2pdf.js": "^0.10.0", "@types/jest": "^30.0.0", "@types/node": "^20", "@types/react": "^19", @@ -60,6 +62,7 @@ "jest-environment-jsdom": "^30.4.1", "postcss": "^8.5.10", "postcss-nesting": "^13.0.2", + "prettier": "^3.2.5", "tailwind-merge": "^3.4.0", "tailwindcss": "^3.4.17", "tailwindcss-animate": "^1.0.7", @@ -2215,21 +2218,21 @@ } }, "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.0.tgz", + "integrity": "sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.2.1", + "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.0.tgz", + "integrity": "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==", "license": "MIT", "optional": true, "dependencies": { @@ -2237,9 +2240,9 @@ } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "dev": true, "license": "MIT", "optional": true, @@ -3849,6 +3852,9 @@ "cpu": [ "arm" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -3865,6 +3871,9 @@ "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -3881,6 +3890,9 @@ "cpu": [ "ppc64" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -3897,6 +3909,9 @@ "cpu": [ "riscv64" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -3913,6 +3928,9 @@ "cpu": [ "s390x" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -3929,6 +3947,9 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -3945,6 +3966,9 @@ "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -3961,6 +3985,9 @@ "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -3977,6 +4004,9 @@ "cpu": [ "arm" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -3999,6 +4029,9 @@ "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -4021,6 +4054,9 @@ "cpu": [ "ppc64" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -4043,6 +4079,9 @@ "cpu": [ "riscv64" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -4065,6 +4104,9 @@ "cpu": [ "s390x" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -4087,6 +4129,9 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -4109,6 +4154,9 @@ "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -4131,6 +4179,9 @@ "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -5731,6 +5782,9 @@ "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -5747,6 +5801,9 @@ "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -5763,6 +5820,9 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -5779,6 +5839,9 @@ "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -5881,60 +5944,6 @@ "node": ">=12.4.0" } }, - "node_modules/@npmcli/agent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", - "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "agent-base": "^7.1.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.1", - "lru-cache": "^10.0.1", - "socks-proxy-agent": "^8.0.3" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/@npmcli/agent/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC", - "optional": true - }, - "node_modules/@npmcli/fs": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", - "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/@npmcli/fs/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, - "license": "ISC", - "optional": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@opentelemetry/api": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", @@ -7086,6 +7095,13 @@ "@types/unist": "*" } }, + "node_modules/@types/html2pdf.js": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@types/html2pdf.js/-/html2pdf.js-0.10.0.tgz", + "integrity": "sha512-XisuzaIQRGHsdu+Xxnymffh/+M2cu4lzpFMsoMTCoq3flN0EcEVFeYhqIO2uq5FE19NoA7tWYE62lS3SeOdMqA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/http-errors": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", @@ -7778,6 +7794,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -7792,6 +7811,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -7806,6 +7828,9 @@ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -7820,6 +7845,9 @@ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -7834,6 +7862,9 @@ "riscv64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -7848,6 +7879,9 @@ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -7862,6 +7896,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -7876,6 +7913,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -7960,14 +8000,14 @@ } }, "node_modules/abbrev": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", - "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", "dev": true, "license": "ISC", "optional": true, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/abort-controller": { @@ -9109,39 +9149,6 @@ "node": ">= 0.8" } }, - "node_modules/cacache": { - "version": "19.0.1", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", - "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "@npmcli/fs": "^4.0.0", - "fs-minipass": "^3.0.0", - "glob": "^10.2.2", - "lru-cache": "^10.0.1", - "minipass": "^7.0.3", - "minipass-collect": "^2.0.1", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "p-map": "^7.0.2", - "ssri": "^12.0.0", - "tar": "^7.4.3", - "unique-filename": "^4.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/cacache/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC", - "optional": true - }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -10769,29 +10776,6 @@ "node": ">= 0.8" } }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, - "node_modules/encoding/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -10838,14 +10822,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "dev": true, - "license": "MIT", - "optional": true - }, "node_modules/error-ex": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", @@ -12697,20 +12673,6 @@ "node": ">=12" } }, - "node_modules/fs-minipass": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", - "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -13823,13 +13785,16 @@ "node": ">=8.0.0" } }, - "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true + "node_modules/html2pdf.js": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/html2pdf.js/-/html2pdf.js-0.14.0.tgz", + "integrity": "sha512-yvNJgE/8yru2UeGflkPdjW8YEY+nDH5X7/2WG4uiuSCwYiCp8PZ8EKNiTAa6HxJ1NjC51fZSIEq6xld5CADKBQ==", + "license": "MIT", + "dependencies": { + "dompurify": "^3.3.1", + "html2canvas": "^1.0.0", + "jspdf": "^4.0.0" + } }, "node_modules/http-errors": { "version": "2.0.1", @@ -14057,15 +14022,18 @@ "license": "MIT" }, "node_modules/install-artifact-from-github": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/install-artifact-from-github/-/install-artifact-from-github-1.4.0.tgz", - "integrity": "sha512-+y6WywKZREw5rq7U2jvr2nmZpT7cbWbQQ0N/qfcseYnzHFz2cZz1Et52oY+XttYuYeTkI8Y+R2JNWj68MpQFSg==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/install-artifact-from-github/-/install-artifact-from-github-1.6.0.tgz", + "integrity": "sha512-wKsuzN8fy8QK7iEUqyWTQmvZ1QFGPn1xyl3/1iIIDthDjS7Hn9HoPwHlNakZirWbCsbad0lZMkr6Xfbpe1pUzw==", "dev": true, "license": "BSD-3-Clause", "optional": true, "bin": { "install-from-cache": "bin/install-from-cache.js", "save-to-github-cache": "bin/save-to-github-cache.js" + }, + "engines": { + "node": ">=18" } }, "node_modules/internal-slot": { @@ -16534,52 +16502,6 @@ "dev": true, "license": "ISC" }, - "node_modules/make-fetch-happen": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", - "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "@npmcli/agent": "^3.0.0", - "cacache": "^19.0.1", - "http-cache-semantics": "^4.1.1", - "minipass": "^7.0.2", - "minipass-fetch": "^4.0.0", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^1.0.0", - "proc-log": "^5.0.0", - "promise-retry": "^2.0.1", - "ssri": "^12.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/make-fetch-happen/node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/make-fetch-happen/node_modules/proc-log": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", - "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", - "dev": true, - "license": "ISC", - "optional": true, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -17681,147 +17603,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/minipass-collect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", - "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minipass-fetch": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", - "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "minipass": "^7.0.3", - "minipass-sized": "^1.0.3", - "minizlib": "^3.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - }, - "optionalDependencies": { - "encoding": "^0.1.13" - } - }, - "node_modules/minipass-flush": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-flush/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-flush/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC", - "optional": true - }, - "node_modules/minipass-pipeline": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", - "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-pipeline/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-pipeline/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC", - "optional": true - }, - "node_modules/minipass-sized": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", - "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-sized/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-sized/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC", - "optional": true - }, "node_modules/minizlib": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", @@ -17933,9 +17714,9 @@ } }, "node_modules/nan": { - "version": "2.24.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.24.0.tgz", - "integrity": "sha512-Vpf9qnVW1RaDkoNKFUvfxqAbtI8ncb8OJlqZ9wwpXzWPEsvsB1nvdUi6oYrHIkQ1Y/tMDnr1h4nczS0VB9Xykg==", + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.27.0.tgz", + "integrity": "sha512-hC+0LidcL3XE4rp1C4H54KujgXKzbfyTngZTwBByQxsOxCEKZT0MPQ4hOKUH2jU1OYstqdDH4onyHPDzcV0XdQ==", "dev": true, "license": "MIT", "optional": true @@ -18152,9 +17933,9 @@ } }, "node_modules/node-gyp": { - "version": "11.5.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.5.0.tgz", - "integrity": "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==", + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.4.0.tgz", + "integrity": "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==", "dev": true, "license": "MIT", "optional": true, @@ -18162,47 +17943,36 @@ "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", - "make-fetch-happen": "^14.0.3", - "nopt": "^8.0.0", - "proc-log": "^5.0.0", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", "semver": "^7.3.5", - "tar": "^7.4.3", + "tar": "^7.5.4", "tinyglobby": "^0.2.12", - "which": "^5.0.0" + "undici": "^6.25.0", + "which": "^6.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/node-gyp/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", - "dev": true, - "license": "ISC", - "optional": true, - "engines": { - "node": ">=16" - } - }, - "node_modules/node-gyp/node_modules/proc-log": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", - "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "optional": true, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": ">=20" } }, "node_modules/node-gyp/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", "dev": true, "license": "ISC", "optional": true, @@ -18214,20 +17984,20 @@ } }, "node_modules/node-gyp/node_modules/which": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", - "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", "dev": true, "license": "ISC", "optional": true, "dependencies": { - "isexe": "^3.1.1" + "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/node-int64": { @@ -18245,20 +18015,20 @@ "license": "MIT" }, "node_modules/nopt": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", - "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", "dev": true, "license": "ISC", "optional": true, "dependencies": { - "abbrev": "^3.0.0" + "abbrev": "^4.0.0" }, "bin": { "nopt": "bin/nopt.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/normalize-path": { @@ -18608,20 +18378,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-map": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", - "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/p-throttle": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/p-throttle/-/p-throttle-7.0.0.tgz", @@ -19444,6 +19200,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-format": { "version": "30.4.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", @@ -19473,6 +19245,17 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "dev": true, + "license": "ISC", + "optional": true, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -19507,32 +19290,6 @@ "dev": true, "license": "MIT" }, - "node_modules/promise-retry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/promise-retry/node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 4" - } - }, "node_modules/promise-worker-transferable": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz", @@ -19834,17 +19591,23 @@ } }, "node_modules/re2": { - "version": "1.22.3", - "resolved": "https://registry.npmjs.org/re2/-/re2-1.22.3.tgz", - "integrity": "sha512-002aE82U91DiaUA16U6vbiJusvPXn1OWiQukOxJkVUTXbzrSuQbFNHYKcGw8QK/uifRCfjl2Hd/vXYDanKkmaQ==", + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/re2/-/re2-1.24.1.tgz", + "integrity": "sha512-uRl9cLDKuobJQp+6lVz7E3AyVszubUJ0fqAMWout4ocUWTIFvdHgpqLwwMh/vuNGGGJGh2p2mJZJIQr9am9M/A==", "dev": true, "hasInstallScript": true, "license": "BSD-3-Clause", "optional": true, "dependencies": { - "install-artifact-from-github": "^1.4.0", - "nan": "^2.23.1", - "node-gyp": "^11.5.0" + "install-artifact-from-github": "^1.6.0", + "nan": "^2.27.0", + "node-gyp": "^12.3.0" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sponsors/uhop" } }, "node_modules/react": { @@ -21068,20 +20831,6 @@ "sql-formatter": "bin/sql-formatter-cli.cjs" } }, - "node_modules/ssri": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", - "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -22663,6 +22412,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.26.0.tgz", + "integrity": "sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18.17" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -22742,34 +22502,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/unique-filename": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", - "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "unique-slug": "^5.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/unique-slug": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", - "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/unique-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", diff --git a/package.json b/package.json index 773d0e8f..38d07aa1 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "github-markdown-css": "^5.8.1", "gsap": "^3.14.2", "html2canvas": "^1.4.1", + "html2pdf.js": "^0.14.0", "jspdf": "^4.2.1", "lucide-react": "^0.577.0", "next": "^16.2.6", @@ -52,6 +53,7 @@ "@testing-library/react": "^16.3.2", "@types/canvas-confetti": "^1.9.0", "@types/dompurify": "^3.0.5", + "@types/html2pdf.js": "^0.10.0", "@types/jest": "^30.0.0", "@types/node": "^20", "@types/react": "^19", diff --git a/postcss.config.js b/postcss.config.js index c6d60b1f..cdbe50f3 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,7 +1,7 @@ module.exports = { - plugins: { - 'tailwindcss/nesting': {}, - tailwindcss: {}, - autoprefixer: {}, - }, -} + plugins: { + 'tailwindcss/nesting': {}, + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/scripts/check-duplicates.ts b/scripts/check-duplicates.ts index a7d86792..3cd30642 100644 --- a/scripts/check-duplicates.ts +++ b/scripts/check-duplicates.ts @@ -1,52 +1,54 @@ - -import { initializeApp } from "firebase/app"; -import { getFirestore, collection, getDocs } from "firebase/firestore"; +import { initializeApp } from 'firebase/app'; +import { getFirestore, collection, getDocs } from 'firebase/firestore'; import * as dotenv from 'dotenv'; dotenv.config({ path: '.env.local' }); const firebaseConfig = { - apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY, - authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, - projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, - storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET, - messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID, - appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID, - measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID + apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY, + authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, + projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, + storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET, + messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID, + appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID, + measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID, }; const app = initializeApp(firebaseConfig); const db = getFirestore(app); async function checkDuplicates() { - console.log("Checking for duplicates..."); - try { - const membersRef = collection(db, 'members'); - const adminsRef = collection(db, 'admins'); - - const membersSnap = await getDocs(membersRef); - const adminsSnap = await getDocs(adminsRef); - - console.log("--- MEMBERS ---"); - membersSnap.forEach(doc => { - const data = doc.data(); - if (data.name === 'Tony Stark') { - console.log(`ID: ${doc.id}, Name: ${data.name}, Points: ${data.points}, Badges: ${JSON.stringify(data.achievements)}`); - } - }); - - console.log("--- ADMINS ---"); - adminsSnap.forEach(doc => { - const data = doc.data(); - if (data.name === 'Tony Stark') { - console.log(`ID: ${doc.id}, Name: ${data.name}, Points: ${data.points}, Badges: ${JSON.stringify(data.achievements)}`); - } - }); - - } catch (e) { - console.error(e); - } - process.exit(0); + console.log('Checking for duplicates...'); + try { + const membersRef = collection(db, 'members'); + const adminsRef = collection(db, 'admins'); + + const membersSnap = await getDocs(membersRef); + const adminsSnap = await getDocs(adminsRef); + + console.log('--- MEMBERS ---'); + membersSnap.forEach((doc) => { + const data = doc.data(); + if (data.name === 'Tony Stark') { + console.log( + `ID: ${doc.id}, Name: ${data.name}, Points: ${data.points}, Badges: ${JSON.stringify(data.achievements)}` + ); + } + }); + + console.log('--- ADMINS ---'); + adminsSnap.forEach((doc) => { + const data = doc.data(); + if (data.name === 'Tony Stark') { + console.log( + `ID: ${doc.id}, Name: ${data.name}, Points: ${data.points}, Badges: ${JSON.stringify(data.achievements)}` + ); + } + }); + } catch (e) { + console.error(e); + } + process.exit(0); } checkDuplicates(); diff --git a/scripts/check-env.ts b/scripts/check-env.ts index 2b6609e7..97fb1697 100644 --- a/scripts/check-env.ts +++ b/scripts/check-env.ts @@ -1,18 +1,17 @@ - import * as dotenv from 'dotenv'; dotenv.config({ path: '.env.local' }); console.log('Checking Environment Variables...'); const required = [ - 'NEXT_PUBLIC_FIREBASE_API_KEY', - 'NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN', - 'NEXT_PUBLIC_FIREBASE_PROJECT_ID' + 'NEXT_PUBLIC_FIREBASE_API_KEY', + 'NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN', + 'NEXT_PUBLIC_FIREBASE_PROJECT_ID', ]; -required.forEach(key => { - if (process.env[key]) { - console.log(`${key}: LOADED (${process.env[key]?.length} chars)`); - } else { - console.log(`${key}: MISSING`); - } +required.forEach((key) => { + if (process.env[key]) { + console.log(`${key}: LOADED (${process.env[key]?.length} chars)`); + } else { + console.log(`${key}: MISSING`); + } }); diff --git a/scripts/check-points.ts b/scripts/check-points.ts index 7aecd8ea..4cc4aefd 100644 --- a/scripts/check-points.ts +++ b/scripts/check-points.ts @@ -1,37 +1,47 @@ -import { initializeApp } from "firebase/app"; -import { getFirestore, collection, getDocs, doc, updateDoc, setDoc, getDoc } from "firebase/firestore"; +import { initializeApp } from 'firebase/app'; +import { + getFirestore, + collection, + getDocs, + doc, + updateDoc, + setDoc, + getDoc, +} from 'firebase/firestore'; import * as dotenv from 'dotenv'; dotenv.config({ path: '.env.local' }); const firebaseConfig = { - apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY, - authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, - projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, - storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET, - messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID, - appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID, - measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID + apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY, + authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, + projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, + storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET, + messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID, + appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID, + measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID, }; const app = initializeApp(firebaseConfig); const db = getFirestore(app); async function checkPoints() { - try { - const membersRef = collection(db, 'members'); - const snapshot = await getDocs(membersRef); + try { + const membersRef = collection(db, 'members'); + const snapshot = await getDocs(membersRef); - for (const memberDoc of snapshot.docs) { - const data = memberDoc.data(); - if (data.name === 'Tony Stark') { - console.log(`SUMMARY: User=${data.name}, Points=${data.points}, BadgeCount=${data.achievements?.length}`); - } - } - } catch (error) { - console.error("Error:", error); + for (const memberDoc of snapshot.docs) { + const data = memberDoc.data(); + if (data.name === 'Tony Stark') { + console.log( + `SUMMARY: User=${data.name}, Points=${data.points}, BadgeCount=${data.achievements?.length}` + ); + } } - process.exit(); + } catch (error) { + console.error('Error:', error); + } + process.exit(); } checkPoints(); diff --git a/scripts/cleanup-data.ts b/scripts/cleanup-data.ts index cae50407..8795304a 100644 --- a/scripts/cleanup-data.ts +++ b/scripts/cleanup-data.ts @@ -1,4 +1,3 @@ - import { initializeApp, cert } from 'firebase-admin/app'; import { getFirestore } from 'firebase-admin/firestore'; import { getAuth } from 'firebase-admin/auth'; @@ -6,115 +5,121 @@ import * as dotenv from 'dotenv'; dotenv.config({ path: '.env.local' }); -const serviceAccount = JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT_KEY || '{}'); +const serviceAccount = JSON.parse( + process.env.FIREBASE_SERVICE_ACCOUNT_KEY || '{}' +); if (!serviceAccount.project_id) { - console.error('Error: FIREBASE_SERVICE_ACCOUNT_KEY is missing or invalid.'); - process.exit(1); + console.error('Error: FIREBASE_SERVICE_ACCOUNT_KEY is missing or invalid.'); + process.exit(1); } initializeApp({ - credential: cert(serviceAccount), + credential: cert(serviceAccount), }); const db = getFirestore(); const auth = getAuth(); -const SUPER_ADMIN_EMAIL = "devpathind.community@gmail.com"; +const SUPER_ADMIN_EMAIL = 'devpathind.community@gmail.com'; async function deleteCollection(collectionPath: string, batchSize: number) { - const collectionRef = db.collection(collectionPath); - const query = collectionRef.orderBy('__name__').limit(batchSize); + const collectionRef = db.collection(collectionPath); + const query = collectionRef.orderBy('__name__').limit(batchSize); - return new Promise((resolve, reject) => { - deleteQueryBatch(db, query, resolve).catch(reject); - }); + return new Promise((resolve, reject) => { + deleteQueryBatch(db, query, resolve).catch(reject); + }); } -async function deleteQueryBatch(db: FirebaseFirestore.Firestore, query: FirebaseFirestore.Query, resolve: any) { - const snapshot = await query.get(); - - const batchSize = snapshot.size; - if (batchSize === 0) { - resolve(); - return; - } - - const batch = db.batch(); - snapshot.docs.forEach((doc) => { - batch.delete(doc.ref); - }); - await batch.commit(); - - process.nextTick(() => { - deleteQueryBatch(db, query, resolve); - }); +async function deleteQueryBatch( + db: FirebaseFirestore.Firestore, + query: FirebaseFirestore.Query, + resolve: any +) { + const snapshot = await query.get(); + + const batchSize = snapshot.size; + if (batchSize === 0) { + resolve(); + return; + } + + const batch = db.batch(); + snapshot.docs.forEach((doc) => { + batch.delete(doc.ref); + }); + await batch.commit(); + + process.nextTick(() => { + deleteQueryBatch(db, query, resolve); + }); } async function getAdminEmails() { - const adminsSnapshot = await db.collection('admins').get(); - const adminEmails = new Set+ {event.description} +
+{event.description}
-You must be logged in with a Super Admin account to access this page.
- -- You do not have the required privileges to access this portal. - This area is reserved for authorized Super Admin accounts only. - All unauthorized access attempts are logged. -
- - {/* Divider */} - - - {/* Actions */} -+ You must be logged in with a Super Admin account to access this page. +
+ +Restricted Area. Authorized Personnel Only.
-+ You do not have the required privileges to access this portal. This + area is reserved for authorized Super Admin accounts only. All + unauthorized access attempts are logged. +
+ + {/* Divider */} + + + {/* Actions */} ++ Restricted Area. Authorized Personnel Only. +
+- National Level 24-Hour Online Hackathon Results -
-- Congratulations to all participants! Check out the final standings and project scores below. -
-+ National Level 24-Hour Online Hackathon Results +
++ Congratulations to all participants! Check out the final standings + and project scores below. +
+- Discover how learners, contributors, and developers are growing - together through DevPath's collaborative community. -
-- Average Rating -
-- Active Developers -
-+ Discover how learners, contributors, and developers are growing + together through DevPath's collaborative community. +
+- Positive Reviews -
-Average Rating
+Active Developers
Positive Reviews
- {review.review} -
+ {/* Reviews */} +- {review.role} +
+ {review.review}
+ +{review.role}
+- Trusted by learners, contributors, and developers across the community. -
-+ Trusted by learners, contributors, and developers across the + community. +
+- {error.message || "We couldn't load the community. This may be a temporary issue — please try again."} -
++ {error.message || + "We couldn't load the community. This may be a temporary issue — please try again."} +
-+ Connect, discuss, and showcase your work. +
+Connect, discuss, and showcase your work.
-No discussions yet. Be the first to start one!
++ {searchQuery.trim() + ? 'No projects match your search.' + : 'No projects showcased yet.'} +
+No discussions yet. Be the first to start one!
-- {searchQuery.trim() - ? 'No projects match your search.' - : 'No projects showcased yet.'} -
-- DevPath is built by developers, for developers. Thank you to everyone who makes this possible! -
-+ DevPath is built by developers, for developers. Thank you to everyone + who makes this possible! +
++ Loading contributors podium... +
+Loading contributors podium...
-{contributor.handle}
- -{contributor.handle}
-
+ - A complete, free, and industry-recognized curriculum designed to take you from zero to hero. No paid courses, no scattered tutorials. -
-
- - Build beautiful, interactive user interfaces. Master the art of crafting responsive web experiences. -
-+ A complete, free, and industry-recognized curriculum designed to + take you from zero to hero. No paid courses, no scattered + tutorials. +
+- Power your applications with robust server-side logic, databases, and APIs. -
-
+ + Build beautiful, interactive user interfaces. Master the art of + crafting responsive web experiences. +
+- Combine frontend and backend skills to build complete, production-ready applications. -
-+ Power your applications with robust server-side logic, + databases, and APIs. +
+- We kick off with the HTML Masterclass, followed immediately by CSS Styling & Layouts. The perfect start for beginners. -
-- Python Programming and JavaScript Essentials. Learn to think like a programmer. -
-- Deep dive into React, Next.js, and Backend technologies. -
-+ Combine frontend and backend skills to build complete, + production-ready applications. +
+Earn free, verified certificates upon completion to showcase on your resume and LinkedIn.
-Don't just watch tutorials. Build real-world applications that matter.
-
- Quality education should be accessible to everyone. No hidden fees, ever.
-+ We kick off with the HTML Masterclass, followed immediately by + CSS Styling & Layouts. The perfect start for beginners. +
++ Python Programming and JavaScript Essentials. Learn to think + like a programmer. +
++ Deep dive into React, Next.js, and Backend technologies. +
++ Earn free, verified certificates upon completion to showcase + on your resume and LinkedIn. +
++ Don't just watch tutorials. Build real-world + applications that matter. +
+ + Quality education should be accessible to everyone. No + hidden fees, ever. +
+- {error.message || "An unexpected error occurred. Our team has been notified and is looking into it. Please try again."} -
- -+ {error.message || + 'An unexpected error occurred. Our team has been notified and is looking into it. Please try again.'} +
+ +- Experimental features from the future. Enable at your own risk. - State persists until you close this session. -
-+ Experimental features from the future. Enable at your own risk. + State persists until you close this session. +
+Advanced problem solving agents.
-Self-healing code capabilities.
-Collective intelligence network.
-Advanced problem solving agents.
+Self-healing code capabilities.
+Collective intelligence network.
+{flag.description}
-{flag.description}
+
- Don't have an account?{" "}
+ Don't have an account?{' '}
;
+ return
- Stay updated with your latest activities and announcements.
-
+ Stay updated with your latest activities and announcements.
+ You're all caught up! You're all caught up!
+ {notif.createdAt?.seconds
+ ? new Date(
+ notif.createdAt.seconds * 1000
+ ).toLocaleString()
+ : 'Just now'}
+
- {notif.createdAt?.seconds ? new Date(notif.createdAt.seconds * 1000).toLocaleString() : 'Just now'}
-
- {notif.message}
-
+ {notif.message}
+
- {error.message || "We couldn't load the open source dashboard. This may be a GitHub API or network issue — please try again."}
-
+ {error.message ||
+ "We couldn't load the open source dashboard. This may be a GitHub API or network issue — please try again."}
+
- Open source is the heartbeat of modern software. Join the global community of developers building the future together.
-
+ Open source is the heartbeat of modern software. Join the global
+ community of developers building the future together.
+ {repo.description}
- {repo.longDescription}
-
+ {repo.description}
+
+ {repo.longDescription}
+
- The world's largest platform for developer collaboration. Home to millions of open source projects.
-
- A complete DevOps platform delivered as a single application. Famous for its CI/CD capabilities.
-
+ The world's largest platform for developer collaboration.
+ Home to millions of open source projects.
+
- Git solution for professional teams. Deeply integrated with Jira and Trello.
-
+ A complete DevOps platform delivered as a single application.
+ Famous for its CI/CD capabilities.
+ Understand Git, Pull Requests, and Issues. These are the fundamental tools of open source. Look for projects with active maintainers and a welcoming community. Start small. Fix a typo, update documentation, or tackle a "Good First Issue".
+ Git solution for professional teams. Deeply integrated with Jira
+ and Trello.
+
+ Understand Git, Pull Requests, and Issues. These are the
+ fundamental tools of open source.
+
+ Look for projects with active maintainers and a welcoming
+ community.
+
+ Start small. Fix a typo, update documentation, or tackle a
+ "Good First Issue".
+
- {error.message || "We couldn't load the learning paths. Please try again."}
-
+ {error.message ||
+ "We couldn't load the learning paths. Please try again."}
+
- Earn Dev Points, climb the ranks, and become a Pathfinder. Your journey from Shishya to Master starts here.
-
+ Earn Dev Points, climb the ranks, and become a Pathfinder. Your
+ journey from Shishya to Master starts here.
+
- The Sanrakshak is the ultimate steward of the DevPath ecosystem.
- This role represents long-term ownership, trust, and responsibility for the platform's vision, governance, and continuity.
-
+ The Sanrakshak is the ultimate steward of the DevPath
+ ecosystem. This role represents long-term ownership, trust,
+ and responsibility for the platform's vision,
+ governance, and continuity.
+ Redeem your hard-earned Dev Points for exclusive perks and swag. {reward.desc} {reward.desc} {reward.desc} {reward.desc}
+ Redeem your hard-earned Dev Points for exclusive perks and swag.
+
+ {reward.desc}
+
+ {reward.desc}
+
+ {reward.desc}
+
+ {reward.desc}
+
- {error.message || "We couldn't load your profile. Please try again or return home."}
-
+ {error.message ||
+ "We couldn't load your profile. Please try again or return home."}
+ Status: {status} Status: {status}
- Minimum 6 characters
-
- Welcome to DevPath! Your profile has been set up.
-
- {currentStep === 6
- ? "Your DevPath journey begins now."
- : `Step ${currentStep} of ${totalSteps}`}
+ Set up your DevPath profile in a few steps.
- {repo.description}
- {repo.description}
- Use this snippet to get the DevPath web app running locally.
-
+ Use this snippet to get the DevPath web app running locally.
+ {member.name} {member.subRole ?? member.role}
+ {member.name}
+
+ {member.subRole ?? member.role}
+ {body} {body} {member.name} {member.role} {member.subRole}
+ {member.name}
+
+ {member.role}
+
+ {member.subRole}
+
+ DevPath Community
+
+ A mission-focused group of builders, mentors, and organizers
+ creating accessible learning pathways for everyone.
+ DevPath Community
- A mission-focused group of builders, mentors, and organizers creating accessible learning pathways for everyone.
- Our Mission
+ Our Mission
+
+ Explore
+
+ Understand how our team collaborates across mentorship, content,
+ and technical initiatives.
+
+ Opportunities
+
+ We are continuously expanding with volunteer and leadership
+ roles in multiple cities.
+
- Join our translation community and make DevPath accessible to developers worldwide.
-
+ Join our translation community and make DevPath accessible to
+ developers worldwide.
+ {lang.native} {lang.native} Select your native language from the list or request a new one. Translate strings using our easy-to-use web interface. Vote on translations from others to ensure quality.
- Don't see your language? Request it and we'll set it up!
-
+ Select your native language from the list or request a new one.
+
+ Translate strings using our easy-to-use web interface.
+
+ Vote on translations from others to ensure quality.
+
+ Don't see your language? Request it and we'll set it up!
+ {error || "The user you are looking for does not exist."}
+ {error || 'The user you are looking for does not exist.'}
+
- {(user.displayRole || user.role || 'MEMBER').toUpperCase()}
- {user.bio && • {user.bio}}
-
+
+ {(user.displayRole || user.role || 'MEMBER').toUpperCase()}
+
+ {user.bio && (
+ • {user.bio}
+ )}
+
-
- {event.type.replace('Event', '').replace(/([A-Z])/g, ' $1').trim()}
-
- {' '}on{' '}
-
- {event.repo.name}
-
-
- {new Date(event.created_at).toLocaleDateString()}
-
+
+ {event.type
+ .replace('Event', '')
+ .replace(/([A-Z])/g, ' $1')
+ .trim()}
+ {' '}
+ on{' '}
+
+ {event.repo.name}
+
+
+ {new Date(event.created_at).toLocaleDateString()}
+ This user hasn't connected their GitHub account yet.
+ This user hasn't connected their GitHub account yet.
+ No projects showcased yet. No projects showcased yet. {stripHtml(project.description)} Earned Badge No achievements yet.
+ {stripHtml(project.description)}
+
+ Earned Badge
+ No achievements yet. Please provide a user ID to view a profile.
+ Please provide a user ID to view a profile.
+ No articles found for “{query}”
- {results.length} article{results.length !== 1 ? "s" : ""} found for{" "}
- “{query}”
-
+ No articles found for “{query}”
+
+ {results.length} article{results.length !== 1 ? 's' : ''} found for{' '}
+ “{query}”
+
-
+
- {error.message || "We couldn't load the wiki content. Please try again."}
-
+ {error.message ||
+ "We couldn't load the wiki content. Please try again."}
+ Content coming soon...
- {this.state.error?.message || "An unexpected error occurred while loading this component."}
+ {this.state.error?.message ||
+ 'An unexpected error occurred while loading this component.'}
Personalised to your recent activity
+ Personalised to your recent activity
+ Explore DevPath and we will suggest your next step here.
+ Explore DevPath and we will suggest your next step here.
+ {rec.description}
+ {rec.description}
+ No notifications yet No notifications yet {notif.title} {notif.message}
- {notif.createdAt?.seconds ? new Date(notif.createdAt.seconds * 1000).toLocaleDateString() : 'Just now'}
-
+ {notif.title}
+
+ {notif.message}
+
+ {notif.createdAt?.seconds
+ ? new Date(
+ notif.createdAt.seconds * 1000
+ ).toLocaleDateString()
+ : 'Just now'}
+
+ {project.author}
+ {tech}
+ by {project.author}
+
+ {project.description ||
+ 'A detailed description of this amazing project would go here, explaining the problem it solves, technologies used, and implementation challenges overcome. This project demonstrates best practices in modern web development and has received significant community attention.'}
+ {project.author} {tech} by {project.author}
- {project.description || "A detailed description of this amazing project would go here, explaining the problem it solves, technologies used, and implementation challenges overcome. This project demonstrates best practices in modern web development and has received significant community attention."}
- Global Maintenance Mode
+ Blocks all non-admin users.
+ Global Maintenance Mode Blocks all non-admin users. {message || "We're updating our systems to serve you better."} Your AI companion Your AI companion
- How can I help you today? Pick an action below or ask me anything.
-
+ How can I help you today? Pick an action below or ask me
+ anything.
+ {card.title}
+ {card.title}
+
- Please enter the Admin Key to continue.
-
+ Please enter the Admin Key to continue.
+
- Celebrating the most innovative and impactful projects from HackFiesta.
-
+ Celebrating the most innovative and impactful projects from
+ HackFiesta.
+ {selectedNode.desc} {selectedNode.desc} {badge.label}
- Official participation certificates are ready. Verify your identity and download your certificate instantly.
-
+ Official participation certificates are ready. Verify your identity
+ and download your certificate instantly.
+
- Connect with developers worldwide, share knowledge, and stay updated with the latest tech trends.
- Get help when you're stuck and celebrate your wins together.
-
+ Connect with developers worldwide, share knowledge, and stay updated
+ with the latest tech trends. Get help when you're stuck and
+ celebrate your wins together.
+
-
-
+
+ No notifications found
+ No notifications found
-
+ {notif.title}
+
+
- {notif.title}
-
-
- Open Source Unavailable
-
+
+ Open Source Unavailable
+
-
+
-
- Connect, Contribute, and Grow
-
-
+ Connect, Contribute, and Grow
+
+
-
-
- Live Connection
-
-
-
- {repo.name}
-
+
+
+ Live Connection
+
+
+
+ {repo.name}
+
-
- GitHub
- GitLab
-
+
+ GitHub
+ Bitbucket
- GitLab
+ Start Your Journey
- Learn the Basics
- Find a Community
- Make Your First Contribution
- Bitbucket
+ Resources
-
-
- Start Your Journey
+ Learn the Basics
+ Find a Community
+
+ Make Your First Contribution
+
+ Resources
+
+
+
- Paths Unavailable
-
+
+ Paths Unavailable
+
-
+
- Learning Paths
-
- {/* Toggle Button */}
- Learning Paths
- {view === 'card' ?
- The DevPath Pathway
-
-
+ The DevPath Pathway
+
+ {user.name}
+
+ {calculateLevel(user.points || 0).currentLevel.name}
+
Leaderboard
+ {user.name}
-
- {calculateLevel(user.points || 0).currentLevel.name}
-
- Leaderboard
- Progression System
-
-
-
- {/* Sanrakshak Card */}
-
- Sanrakshak
-
-
- Progression System
+
+
+
+ {/* Sanrakshak Card */}
+
-
-
+ Sanrakshak
+
+
+
-
- PHASE 1 — RESOURCES & GUIDED LEARNING (FOUNDATION)
- {reward.name}
- PHASE 2 — PROJECTS, MENTORSHIP & CREDIBILITY
- {reward.name}
- PHASE 3 — PHYSICAL COMMUNITY REWARDS
- {reward.name}
-
+
+ PHASE 4 — PREMIUM PHYSICAL REWARDS (TOP TIER)
- {reward.name}
-
+
+
+ PHASE 1 — RESOURCES & GUIDED LEARNING (FOUNDATION)
+
+ {reward.name}
+
+ PHASE 2 — PROJECTS, MENTORSHIP & CREDIBILITY
+
+ {reward.name}
+
+ PHASE 3 — PHYSICAL COMMUNITY REWARDS
+
+ {reward.name}
+
+ PHASE 4 — PREMIUM PHYSICAL REWARDS (TOP TIER)
+
+ {reward.name}
+
- Profile Unavailable
-
+
+ Profile Unavailable
+
-
+
- Seed Resources
- Seed Resources
+
-
-
-
- Account Created Successfully!
-
-
- You will be redirected to your profile shortly.
-
- {currentStep === 6
- ? "Welcome aboard!"
- : "Create your account"}
+ Create your account
,
- techStack: ['Next.js 14', 'TypeScript', 'Firebase', 'Tailwind CSS', 'Framer Motion', 'Zustand'],
- features: ['Authentication & User Profiles', 'Interactive Learning Roadmaps', 'Event Management System', 'Community Wiki & Docs', 'Real-time Notifications'],
- link: 'https://github.com/devpathindcommunity-india/DevPath-Web',
- status: 'public' as const
- },
- {
- id: 'docs',
- title: 'devpath-docs',
- description: 'Documentation, guides, and learning path curriculum content.',
- longDescription: 'The central knowledge base for DevPath. This repository contains all the markdown content for our wiki, learning paths, and contributor guidelines. It is designed to be easily editable by the community.',
- icon: ,
+ techStack: [
+ 'Next.js 14',
+ 'TypeScript',
+ 'Firebase',
+ 'Tailwind CSS',
+ 'Framer Motion',
+ 'Zustand',
+ ],
+ features: [
+ 'Authentication & User Profiles',
+ 'Interactive Learning Roadmaps',
+ 'Event Management System',
+ 'Community Wiki & Docs',
+ 'Real-time Notifications',
+ ],
+ link: 'https://github.com/devpathindcommunity-india/DevPath-Web',
+ status: 'public' as const,
+ },
+ {
+ id: 'docs',
+ title: 'devpath-docs',
+ description: 'Documentation, guides, and learning path curriculum content.',
+ longDescription:
+ 'The central knowledge base for DevPath. This repository contains all the markdown content for our wiki, learning paths, and contributor guidelines. It is designed to be easily editable by the community.',
+ icon: DevPath is Open Source
- Built in public. Contribute, learn from the code, and help shape the future of developer education.
-
- DevPath is Open Source
+ Built in public. Contribute, learn from the code, and help shape the
+ future of developer education.
+ {repo.title}
- {repo.title}
+ How to Contribute
- How to Contribute
+ Quick Start Snippet
- Quick Start Snippet
+ Built With
- Built With
+ {title}
- {title}
+
- {member.responsibilities.slice(0, 2).map((item, idx) => (
-
- )}
-
+ {member.responsibilities.slice(0, 2).map((item, idx) => (
+
+ )}
+
+ Meet our team
+
+
- Meet our team
-
-
- We make community-driven learning practical, collaborative, and inclusive.
-
+
+ We make community-driven learning practical, collaborative, and
+ inclusive.
+
+
+ Learn more about the team values
+
+ Join the team
+ Help DevPath Speak Your Language
- Help DevPath Speak Your Language
+ {lang.name}
- {lang.name}
+ How to Contribute
- 1. Choose Language
- 2. Translate
- 3. Review
- Request a Language
- How to Contribute
+ 1. Choose Language
+ 2. Translate
+ 3. Review
+ Request a Language
+ Profile Not Found
- Profile Not Found
+ {user.name || 'User'}
- {user.name || 'User'}
+
+
+
-
-
+ Recent Contributions
+
+ Recent Contributions
- GitHub Not Connected
-
-
- GitHub Not Connected
+
+
+ Contribution Activity
- Contribution Activity
+ {project.title}
-
+ {project.title}
+
+ {badgeId.replace(/-/g, ' ')}
- {selectedProject.title}
-
+ {badgeId.replace(/-/g, ' ')}
+
+
+ {selectedProject.title}
+
+ No User Specified
- No User Specified
+ Release History
@@ -695,9 +695,11 @@ export default function UpdaterPage() {
-
+
+
-
- Wiki Unavailable
-
+
+ Wiki Unavailable
+
-
+
- },
- { id: 'python', title: 'Python for AI Roadmap', icon: },
- ],
- },
- {
- title: 'Community',
- items: [
- { id: 'community-offerings', title: 'What Community Offers', icon: },
- { id: 'wp-community', title: 'WhatsApp Community', icon: ,
+ },
+ {
+ id: 'python',
+ title: 'Python for AI Roadmap',
+ icon: ,
+ },
+ ],
+ },
+ {
+ title: 'Community',
+ items: [
+ {
+ id: 'community-offerings',
+ title: 'What Community Offers',
+ icon: ,
+ },
+ {
+ id: 'wp-community',
+ title: 'WhatsApp Community',
+ icon: {wikiContent[activeArticle as keyof typeof wikiContent]?.title ?? 'Introduction to DevPath'}
- Something went wrong
+
+ Something went wrong
+
Your next best action
-
+ Your next best action
+
+ {rec.title}
-
+ {rec.title}
+
+
+ Notifications
+
{unreadCount > 0 && (
-
+ Notifications
- {unreadCount > 0 && (
-
+ {project.title}
+
+
+ {/* Tech Stack */}
+
+ {project.title}
+
+
- {project.title}
-
-
- {/* Tech Stack */}
- {project.title}
- Admin Dashboard
+
+ {/* Maintenance Section */}
+
+
+ Admin Dashboard
-
- {/* Maintenance Section */}
-
-
- Under Maintenance
+
+ Under Maintenance
+
DevPath Assistant
-
+ DevPath Assistant
+
+ Hi there! 👋
-
+ Hi there! 👋
+
+ Admin Verification
- Admin Verification
+ Top 20 Rankings
-
+ Top 20 Rankings
+
+
-
-
+
+
-
-
- {code}
-
-
+
+
+ {code}
+
+ Start a Discussion
- Start a Discussion
+
-
- {selectedNode.label}
+
-
+
+ #{i + 1}
+
+
+
+ {u.displayName ?? 'Anonymous'}
+
+
- {u.displayName ?? "Anonymous"}
-
-
{tier.name}
- {u.xp?? 0} XP
+
+ {u.xp ?? 0} XP
+
- HackFiesta
-
-
- Certificate
-
+ HackFiesta
+
+
+
+ Certificate
+
+ Latest Tech News
- Latest Tech News
+ {item.title}
- {item.title}
+
- Join Our Thriving
-
- Developer Community
-
+ Join Our Thriving
+
+
+ Developer Community
+