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
-| Category | Technology | -|----------|------------| -| **Framework** | Next.js 16 (App Router) | -| **Frontend Library** | React 19 | -| **Language** | TypeScript | -| **Styling** | Tailwind CSS | -| **Animations** | Framer Motion & GSAP | -| **Icons** | Lucide React | -| **Linting & Code Quality** | ESLint | -| **Package Manager** | npm | -| **Backend Services** | Firebase | -| **Deployment** | Vercel | +| Category | Technology | +| -------------------------- | ----------------------- | +| **Framework** | Next.js 16 (App Router) | +| **Frontend Library** | React 19 | +| **Language** | TypeScript | +| **Styling** | Tailwind CSS | +| **Animations** | Framer Motion & GSAP | +| **Icons** | Lucide React | +| **Linting & Code Quality** | ESLint | +| **Package Manager** | npm | +| **Backend Services** | Firebase | +| **Deployment** | Vercel |
- --- ### Architecture Overview @@ -86,20 +85,20 @@ DevPath India is built using a modern, scalable, and developer-friendly technolo ```mermaid flowchart LR User["👤 User"] - + User --> UI["⚛️ React 19 UI"] - + UI --> Next["▲ Next.js 16"] - + Next --> TS["📘 TypeScript"] Next --> Tailwind["🎨 Tailwind CSS"] Next --> Motion["✨ Framer Motion / GSAP"] - + Next --> Firebase["🔥 Firebase"] - + Firebase --> Auth["🔐 Authentication"] Firebase --> DB["📂 Database"] - + Next --> Vercel["☁️ Vercel Deployment"] ``` @@ -136,11 +135,10 @@ DevPath-Web/ └── eslint.config.mjs # ESLint rules and configuration ``` - ## 📸 Screenshots -| Home Page | Community | -| :---: | :---: | +| Home Page | Community | +| :-------------------------------------------------------------------: | :------------------------------------------------------------------------: | | Home Page | Community | --- @@ -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(); - adminEmails.add(SUPER_ADMIN_EMAIL); - - adminsSnapshot.forEach(doc => { - if (doc.id.includes('@')) { - adminEmails.add(doc.id); - } else if (doc.data().email) { - adminEmails.add(doc.data().email); - } - }); - - return adminEmails; -} + const adminsSnapshot = await db.collection('admins').get(); + const adminEmails = new Set(); + adminEmails.add(SUPER_ADMIN_EMAIL); + + adminsSnapshot.forEach((doc) => { + if (doc.id.includes('@')) { + adminEmails.add(doc.id); + } else if (doc.data().email) { + adminEmails.add(doc.data().email); + } + }); -async function deleteAllAuthUsers(adminEmails: Set) { - console.log('Fetching all users...'); - let nextPageToken; - let deletedCount = 0; - - do { - const listUsersResult = await auth.listUsers(1000, nextPageToken); - const usersToDelete = listUsersResult.users - .filter(user => user.email && !adminEmails.has(user.email)) - .map(user => user.uid); - - if (usersToDelete.length > 0) { - await auth.deleteUsers(usersToDelete); - deletedCount += usersToDelete.length; - console.log(`Deleted ${usersToDelete.length} users.`); - } - - nextPageToken = listUsersResult.pageToken; - } while (nextPageToken); - - console.log(`Total users deleted: ${deletedCount}`); + return adminEmails; } -async function main() { - console.log('Starting cleanup...'); - - try { - // 1. Get Admins to preserve - const adminEmails = await getAdminEmails(); - console.log('Preserving Admins:', Array.from(adminEmails)); - - // 2. Delete Auth Users - console.log('Deleting Auth users...'); - await deleteAllAuthUsers(adminEmails); +async function deleteAllAuthUsers(adminEmails: Set) { + console.log('Fetching all users...'); + let nextPageToken; + let deletedCount = 0; + + do { + const listUsersResult = await auth.listUsers(1000, nextPageToken); + const usersToDelete = listUsersResult.users + .filter((user) => user.email && !adminEmails.has(user.email)) + .map((user) => user.uid); + + if (usersToDelete.length > 0) { + await auth.deleteUsers(usersToDelete); + deletedCount += usersToDelete.length; + console.log(`Deleted ${usersToDelete.length} users.`); + } - // 3. Delete Collections - console.log('Deleting members collection...'); - await deleteCollection('members', 50); - console.log('Deleted members collection.'); + nextPageToken = listUsersResult.pageToken; + } while (nextPageToken); - console.log('Deleting leaderboard collection...'); - await deleteCollection('leaderboard', 50); - console.log('Deleted leaderboard collection.'); + console.log(`Total users deleted: ${deletedCount}`); +} - console.log('Cleanup complete.'); - } catch (error) { - console.error('Error during cleanup:', error); - } +async function main() { + console.log('Starting cleanup...'); + + try { + // 1. Get Admins to preserve + const adminEmails = await getAdminEmails(); + console.log('Preserving Admins:', Array.from(adminEmails)); + + // 2. Delete Auth Users + console.log('Deleting Auth users...'); + await deleteAllAuthUsers(adminEmails); + + // 3. Delete Collections + console.log('Deleting members collection...'); + await deleteCollection('members', 50); + console.log('Deleted members collection.'); + + console.log('Deleting leaderboard collection...'); + await deleteCollection('leaderboard', 50); + console.log('Deleted leaderboard collection.'); + + console.log('Cleanup complete.'); + } catch (error) { + console.error('Error during cleanup:', error); + } } main(); diff --git a/scripts/convert_csv.js b/scripts/convert_csv.js index 515610a5..99fc621f 100644 --- a/scripts/convert_csv.js +++ b/scripts/convert_csv.js @@ -1,52 +1,56 @@ const fs = require('fs'); const path = require('path'); -const csvPath = path.join(__dirname, '../hackfiestacertificates/HackFiesta.csv'); +const csvPath = path.join( + __dirname, + '../hackfiestacertificates/HackFiesta.csv' +); const outputPath = path.join(__dirname, '../src/data/participants.ts'); try { - const data = fs.readFileSync(csvPath, 'utf8'); - const lines = data.split('\n').filter(line => line.trim() !== ''); + const data = fs.readFileSync(csvPath, 'utf8'); + const lines = data.split('\n').filter((line) => line.trim() !== ''); - // Remove header - const header = lines.shift(); + // Remove header + const header = lines.shift(); - const participants = lines.map(line => { - // Simple CSV parse handling quotes - const match = line.match(/(".*?"|[^",\s]+)(?=\s*,|\s*$)/g); - if (!match) return null; + const participants = lines + .map((line) => { + // Simple CSV parse handling quotes + const match = line.match(/(".*?"|[^",\s]+)(?=\s*,|\s*$)/g); + if (!match) return null; - let [firstName, lastName, project, teamName] = match; + let [firstName, lastName, project, teamName] = match; - // Clean quotes - firstName = firstName ? firstName.replace(/^"|"$/g, '').trim() : ''; - lastName = lastName ? lastName.replace(/^"|"$/g, '').trim() : ''; - project = project ? project.replace(/^"|"$/g, '').trim() : ''; - teamName = teamName ? teamName.replace(/^"|"$/g, '').trim() : ''; + // Clean quotes + firstName = firstName ? firstName.replace(/^"|"$/g, '').trim() : ''; + lastName = lastName ? lastName.replace(/^"|"$/g, '').trim() : ''; + project = project ? project.replace(/^"|"$/g, '').trim() : ''; + teamName = teamName ? teamName.replace(/^"|"$/g, '').trim() : ''; - if (!firstName) return null; + if (!firstName) return null; - const finalTeam = (teamName && teamName.trim() !== '') ? teamName : 'N/A'; + const finalTeam = teamName && teamName.trim() !== '' ? teamName : 'N/A'; - return { - name: `${firstName} ${lastName}`.trim(), - firstName, - lastName, - team: finalTeam - }; - }).filter(p => p !== null); + return { + name: `${firstName} ${lastName}`.trim(), + firstName, + lastName, + team: finalTeam, + }; + }) + .filter((p) => p !== null); - const fileContent = `export const PARTICIPANTS = ${JSON.stringify(participants, null, 4)};`; + const fileContent = `export const PARTICIPANTS = ${JSON.stringify(participants, null, 4)};`; - // Ensure dir exists - const dir = path.dirname(outputPath); - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); - } - - fs.writeFileSync(outputPath, fileContent); - console.log(`Generated ${participants.length} participants in ${outputPath}`); + // Ensure dir exists + const dir = path.dirname(outputPath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + fs.writeFileSync(outputPath, fileContent); + console.log(`Generated ${participants.length} participants in ${outputPath}`); } catch (err) { - console.error('Error converting CSV:', err); + console.error('Error converting CSV:', err); } diff --git a/scripts/create-super-admin-auth.ts b/scripts/create-super-admin-auth.ts index aa4e8c25..f981deee 100644 --- a/scripts/create-super-admin-auth.ts +++ b/scripts/create-super-admin-auth.ts @@ -1,37 +1,41 @@ -import { initializeApp } from "firebase/app"; -import { getAuth, createUserWithEmailAndPassword } from "firebase/auth"; +import { initializeApp } from 'firebase/app'; +import { getAuth, createUserWithEmailAndPassword } from 'firebase/auth'; 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 auth = getAuth(app); async function createSuperAdmin() { - try { - console.log("Creating Super Admin Auth user..."); - await createUserWithEmailAndPassword(auth, process.env.SUPER_ADMIN_EMAIL as string, process.env.SUPER_ADMIN_PASSWORD as string); - console.log("Super Admin Auth user created successfully."); - process.exit(0); - } catch (error: any) { - if (error.code === 'auth/email-already-in-use') { - console.log("User already exists."); - process.exit(0); - } else { - console.error("Error creating user:", error); - process.exit(1); - } + try { + console.log('Creating Super Admin Auth user...'); + await createUserWithEmailAndPassword( + auth, + process.env.SUPER_ADMIN_EMAIL as string, + process.env.SUPER_ADMIN_PASSWORD as string + ); + console.log('Super Admin Auth user created successfully.'); + process.exit(0); + } catch (error: any) { + if (error.code === 'auth/email-already-in-use') { + console.log('User already exists.'); + process.exit(0); + } else { + console.error('Error creating user:', error); + process.exit(1); } + } } createSuperAdmin(); diff --git a/scripts/create-super-admin-simple.js b/scripts/create-super-admin-simple.js index 94e3ddc7..edc31cb5 100644 --- a/scripts/create-super-admin-simple.js +++ b/scripts/create-super-admin-simple.js @@ -1,4 +1,3 @@ - const { initializeApp, cert } = require('firebase-admin/app'); const { getAuth } = require('firebase-admin/auth'); const { getFirestore } = require('firebase-admin/firestore'); @@ -9,11 +8,15 @@ require('dotenv').config({ path: path.join(__dirname, '..', '.env.local') }); // Hardcode path to service account key (assuming it's in project root) // Script is in scripts/, so go up one level -const serviceAccountPath = path.join(__dirname, '..', 'service-account-key.json'); +const serviceAccountPath = path.join( + __dirname, + '..', + 'service-account-key.json' +); const serviceAccount = require(serviceAccountPath); initializeApp({ - credential: cert(serviceAccount) + credential: cert(serviceAccount), }); const auth = getAuth(); @@ -23,51 +26,56 @@ const SUPER_ADMIN_EMAIL = process.env.SUPER_ADMIN_EMAIL; const SUPER_ADMIN_PASSWORD = process.env.SUPER_ADMIN_PASSWORD; async function createSuperAdmin() { - console.log(`Creating/Updating Super Admin: ${SUPER_ADMIN_EMAIL}...`); + console.log(`Creating/Updating Super Admin: ${SUPER_ADMIN_EMAIL}...`); + try { + let userRecord; try { - let userRecord; - try { - userRecord = await auth.getUserByEmail(SUPER_ADMIN_EMAIL); - console.log("User exists. Updating password..."); - await auth.updateUser(userRecord.uid, { - password: SUPER_ADMIN_PASSWORD, - emailVerified: true, - displayName: "Super Admin" - }); - } catch (error) { - if (error.code === 'auth/user-not-found') { - console.log("User not found. Creating new user..."); - userRecord = await auth.createUser({ - email: SUPER_ADMIN_EMAIL, - password: SUPER_ADMIN_PASSWORD, - emailVerified: true, - displayName: "Super Admin" - }); - } else { - throw error; - } - } - - // Set Custom Claims - await auth.setCustomUserClaims(userRecord.uid, { super_admin: true, role: 'admin' }); - console.log("Set custom claims: { super_admin: true, role: 'admin' }"); + userRecord = await auth.getUserByEmail(SUPER_ADMIN_EMAIL); + console.log('User exists. Updating password...'); + await auth.updateUser(userRecord.uid, { + password: SUPER_ADMIN_PASSWORD, + emailVerified: true, + displayName: 'Super Admin', + }); + } catch (error) { + if (error.code === 'auth/user-not-found') { + console.log('User not found. Creating new user...'); + userRecord = await auth.createUser({ + email: SUPER_ADMIN_EMAIL, + password: SUPER_ADMIN_PASSWORD, + emailVerified: true, + displayName: 'Super Admin', + }); + } else { + throw error; + } + } - // Create/Update Admin Document in Firestore - await db.collection('admins').doc(SUPER_ADMIN_EMAIL).set({ - uid: userRecord.uid, - email: SUPER_ADMIN_EMAIL, - name: "Super Admin", - role: "admin", - isSuperAdmin: true, - createdAt: new Date().toISOString() - }, { merge: true }); + // Set Custom Claims + await auth.setCustomUserClaims(userRecord.uid, { + super_admin: true, + role: 'admin', + }); + console.log("Set custom claims: { super_admin: true, role: 'admin' }"); - console.log("Successfully setup Super Admin!"); + // Create/Update Admin Document in Firestore + await db.collection('admins').doc(SUPER_ADMIN_EMAIL).set( + { + uid: userRecord.uid, + email: SUPER_ADMIN_EMAIL, + name: 'Super Admin', + role: 'admin', + isSuperAdmin: true, + createdAt: new Date().toISOString(), + }, + { merge: true } + ); - } catch (error) { - console.error("Error creating Super Admin:", error); - } + console.log('Successfully setup Super Admin!'); + } catch (error) { + console.error('Error creating Super Admin:', error); + } } createSuperAdmin(); diff --git a/scripts/create-super-admin.js b/scripts/create-super-admin.js index 2c7fe50b..d7c5d6ce 100644 --- a/scripts/create-super-admin.js +++ b/scripts/create-super-admin.js @@ -1,4 +1,3 @@ - const { initializeApp, cert } = require('firebase-admin/app'); const { getAuth } = require('firebase-admin/auth'); const { getFirestore } = require('firebase-admin/firestore'); @@ -8,7 +7,7 @@ require('dotenv').config(); const serviceAccount = require('../service-account-key.json'); initializeApp({ - credential: cert(serviceAccount) + credential: cert(serviceAccount), }); const auth = getAuth(); @@ -18,51 +17,56 @@ const SUPER_ADMIN_EMAIL = process.env.SUPER_ADMIN_EMAIL; const SUPER_ADMIN_PASSWORD = process.env.SUPER_ADMIN_PASSWORD; async function createSuperAdmin() { - console.log(`Creating/Updating Super Admin: ${SUPER_ADMIN_EMAIL}...`); + console.log(`Creating/Updating Super Admin: ${SUPER_ADMIN_EMAIL}...`); + try { + let userRecord; try { - let userRecord; - try { - userRecord = await auth.getUserByEmail(SUPER_ADMIN_EMAIL); - console.log("User exists. Updating password..."); - await auth.updateUser(userRecord.uid, { - password: SUPER_ADMIN_PASSWORD, - emailVerified: true, - displayName: "Super Admin" - }); - } catch (error) { - if (error.code === 'auth/user-not-found') { - console.log("User not found. Creating new user..."); - userRecord = await auth.createUser({ - email: SUPER_ADMIN_EMAIL, - password: SUPER_ADMIN_PASSWORD, - emailVerified: true, - displayName: "Super Admin" - }); - } else { - throw error; - } - } - - // Set Custom Claims - await auth.setCustomUserClaims(userRecord.uid, { super_admin: true, role: 'admin' }); - console.log("Set custom claims: { super_admin: true, role: 'admin' }"); + userRecord = await auth.getUserByEmail(SUPER_ADMIN_EMAIL); + console.log('User exists. Updating password...'); + await auth.updateUser(userRecord.uid, { + password: SUPER_ADMIN_PASSWORD, + emailVerified: true, + displayName: 'Super Admin', + }); + } catch (error) { + if (error.code === 'auth/user-not-found') { + console.log('User not found. Creating new user...'); + userRecord = await auth.createUser({ + email: SUPER_ADMIN_EMAIL, + password: SUPER_ADMIN_PASSWORD, + emailVerified: true, + displayName: 'Super Admin', + }); + } else { + throw error; + } + } - // Create/Update Admin Document in Firestore - await db.collection('admins').doc(SUPER_ADMIN_EMAIL).set({ - uid: userRecord.uid, - email: SUPER_ADMIN_EMAIL, - name: "Super Admin", - role: "admin", - isSuperAdmin: true, - createdAt: new Date().toISOString() - }, { merge: true }); + // Set Custom Claims + await auth.setCustomUserClaims(userRecord.uid, { + super_admin: true, + role: 'admin', + }); + console.log("Set custom claims: { super_admin: true, role: 'admin' }"); - console.log("Successfully setup Super Admin!"); + // Create/Update Admin Document in Firestore + await db.collection('admins').doc(SUPER_ADMIN_EMAIL).set( + { + uid: userRecord.uid, + email: SUPER_ADMIN_EMAIL, + name: 'Super Admin', + role: 'admin', + isSuperAdmin: true, + createdAt: new Date().toISOString(), + }, + { merge: true } + ); - } catch (error) { - console.error("Error creating Super Admin:", error); - } + console.log('Successfully setup Super Admin!'); + } catch (error) { + console.error('Error creating Super Admin:', error); + } } createSuperAdmin(); diff --git a/scripts/create-super-admin.ts b/scripts/create-super-admin.ts index 8a6fda0f..c4c2b160 100644 --- a/scripts/create-super-admin.ts +++ b/scripts/create-super-admin.ts @@ -1,4 +1,3 @@ - import { initializeApp, cert } from 'firebase-admin/app'; import { getAuth } from 'firebase-admin/auth'; import { getFirestore } from 'firebase-admin/firestore'; @@ -9,7 +8,7 @@ dotenv.config(); const serviceAccount = require('../service-account-key.json'); initializeApp({ - credential: cert(serviceAccount) + credential: cert(serviceAccount), }); const auth = getAuth(); @@ -19,55 +18,62 @@ const SUPER_ADMIN_EMAIL = process.env.SUPER_ADMIN_EMAIL; const SUPER_ADMIN_PASSWORD = process.env.SUPER_ADMIN_PASSWORD; async function createSuperAdmin() { - if (!SUPER_ADMIN_EMAIL || !SUPER_ADMIN_PASSWORD) { - console.error("Error: SUPER_ADMIN_EMAIL or SUPER_ADMIN_PASSWORD environment variables are not configured."); - return; - } - console.log(`Creating/Updating Super Admin: ${SUPER_ADMIN_EMAIL}...`); + if (!SUPER_ADMIN_EMAIL || !SUPER_ADMIN_PASSWORD) { + console.error( + 'Error: SUPER_ADMIN_EMAIL or SUPER_ADMIN_PASSWORD environment variables are not configured.' + ); + return; + } + console.log(`Creating/Updating Super Admin: ${SUPER_ADMIN_EMAIL}...`); + try { + let userRecord; try { - let userRecord; - try { - userRecord = await auth.getUserByEmail(SUPER_ADMIN_EMAIL); - console.log("User exists. Updating password..."); - await auth.updateUser(userRecord.uid, { - password: SUPER_ADMIN_PASSWORD, - emailVerified: true, - displayName: "Super Admin" - }); - } catch (error: any) { - if (error.code === 'auth/user-not-found') { - console.log("User not found. Creating new user..."); - userRecord = await auth.createUser({ - email: SUPER_ADMIN_EMAIL, - password: SUPER_ADMIN_PASSWORD, - emailVerified: true, - displayName: "Super Admin" - }); - } else { - throw error; - } - } - - // Set Custom Claims - await auth.setCustomUserClaims(userRecord.uid, { super_admin: true, role: 'admin' }); - console.log("Set custom claims: { super_admin: true, role: 'admin' }"); + userRecord = await auth.getUserByEmail(SUPER_ADMIN_EMAIL); + console.log('User exists. Updating password...'); + await auth.updateUser(userRecord.uid, { + password: SUPER_ADMIN_PASSWORD, + emailVerified: true, + displayName: 'Super Admin', + }); + } catch (error: any) { + if (error.code === 'auth/user-not-found') { + console.log('User not found. Creating new user...'); + userRecord = await auth.createUser({ + email: SUPER_ADMIN_EMAIL, + password: SUPER_ADMIN_PASSWORD, + emailVerified: true, + displayName: 'Super Admin', + }); + } else { + throw error; + } + } - // Create/Update Admin Document in Firestore - await db.collection('admins').doc(SUPER_ADMIN_EMAIL).set({ - uid: userRecord.uid, - email: SUPER_ADMIN_EMAIL, - name: "Super Admin", - role: "admin", - isSuperAdmin: true, - createdAt: new Date().toISOString() - }, { merge: true }); + // Set Custom Claims + await auth.setCustomUserClaims(userRecord.uid, { + super_admin: true, + role: 'admin', + }); + console.log("Set custom claims: { super_admin: true, role: 'admin' }"); - console.log("Successfully setup Super Admin!"); + // Create/Update Admin Document in Firestore + await db.collection('admins').doc(SUPER_ADMIN_EMAIL).set( + { + uid: userRecord.uid, + email: SUPER_ADMIN_EMAIL, + name: 'Super Admin', + role: 'admin', + isSuperAdmin: true, + createdAt: new Date().toISOString(), + }, + { merge: true } + ); - } catch (error) { - console.error("Error creating Super Admin:", error); - } + console.log('Successfully setup Super Admin!'); + } catch (error) { + console.error('Error creating Super Admin:', error); + } } createSuperAdmin(); diff --git a/scripts/debug-data.ts b/scripts/debug-data.ts index b5fb1ee5..aa2d02db 100644 --- a/scripts/debug-data.ts +++ b/scripts/debug-data.ts @@ -1,4 +1,3 @@ - import { initializeApp } from 'firebase/app'; import { getFirestore, doc, getDoc } from 'firebase/firestore'; @@ -6,49 +5,49 @@ 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 debugData() { - const email = 'ap8548328@gmail.com'; - console.log(`--- Debugging Data for ${email} ---`); - - // 1. Check Admin Doc - try { - const adminDoc = await getDoc(doc(db, 'admins', email)); - if (adminDoc.exists()) { - console.log('Admin Document Found:'); - console.log(JSON.stringify(adminDoc.data(), null, 2)); - - const roleId = adminDoc.data().roleId; - if (roleId) { - console.log(`\nChecking Role Document: ${roleId}`); - // 2. Check Role Doc - const roleDoc = await getDoc(doc(db, 'roles', roleId)); - if (roleDoc.exists()) { - console.log('Role Document Found:'); - console.log(JSON.stringify(roleDoc.data(), null, 2)); - } else { - console.error('ERROR: Role Document NOT FOUND!'); - } - } else { - console.error('ERROR: roleId is MISSING in Admin Document!'); - } + const email = 'ap8548328@gmail.com'; + console.log(`--- Debugging Data for ${email} ---`); + + // 1. Check Admin Doc + try { + const adminDoc = await getDoc(doc(db, 'admins', email)); + if (adminDoc.exists()) { + console.log('Admin Document Found:'); + console.log(JSON.stringify(adminDoc.data(), null, 2)); + + const roleId = adminDoc.data().roleId; + if (roleId) { + console.log(`\nChecking Role Document: ${roleId}`); + // 2. Check Role Doc + const roleDoc = await getDoc(doc(db, 'roles', roleId)); + if (roleDoc.exists()) { + console.log('Role Document Found:'); + console.log(JSON.stringify(roleDoc.data(), null, 2)); } else { - console.error('ERROR: Admin Document NOT FOUND!'); + console.error('ERROR: Role Document NOT FOUND!'); } - } catch (error) { - console.error('Error fetching data:', error); + } else { + console.error('ERROR: roleId is MISSING in Admin Document!'); + } + } else { + console.error('ERROR: Admin Document NOT FOUND!'); } + } catch (error) { + console.error('Error fetching data:', error); + } } debugData(); diff --git a/scripts/debug-db.ts b/scripts/debug-db.ts index 3da9c670..06d96c71 100644 --- a/scripts/debug-db.ts +++ b/scripts/debug-db.ts @@ -1,4 +1,3 @@ - import * as admin from 'firebase-admin'; import * as dotenv from 'dotenv'; import { resolve } from 'path'; @@ -8,44 +7,53 @@ dotenv.config({ path: resolve(process.cwd(), '.env.local') }); // Initialize Firebase Admin if (!admin.apps.length) { - try { - const serviceAccount = require('../service-account.json'); - admin.initializeApp({ - credential: admin.credential.cert(serviceAccount) - }); - } catch (error) { - console.error("Failed to load service-account.json. Make sure it exists in the root directory."); - process.exit(1); - } + try { + const serviceAccount = require('../service-account.json'); + admin.initializeApp({ + credential: admin.credential.cert(serviceAccount), + }); + } catch (error) { + console.error( + 'Failed to load service-account.json. Make sure it exists in the root directory.' + ); + process.exit(1); + } } const db = admin.firestore(); async function debugDB() { - console.log("--- Debugging Firestore ---"); - - try { - const collections = await db.listCollections(); - console.log("Collections found:", collections.map(c => c.id)); - - if (collections.some(c => c.id === 'members')) { - console.log("\nChecking 'members' collection:"); - const membersSnapshot = await db.collection('members').limit(5).get(); - if (membersSnapshot.empty) { - console.log(" - Collection 'members' exists but is EMPTY."); - } else { - console.log(` - Found ${membersSnapshot.size} documents (showing first 5):`); - membersSnapshot.docs.forEach(doc => { - console.log(` - ID: ${doc.id}, Data:`, JSON.stringify(doc.data(), null, 2)); - }); - } - } else { - console.log("\nCollection 'members' does NOT exist."); - } - - } catch (error) { - console.error("Error debugging DB:", error); + console.log('--- Debugging Firestore ---'); + + try { + const collections = await db.listCollections(); + console.log( + 'Collections found:', + collections.map((c) => c.id) + ); + + if (collections.some((c) => c.id === 'members')) { + console.log("\nChecking 'members' collection:"); + const membersSnapshot = await db.collection('members').limit(5).get(); + if (membersSnapshot.empty) { + console.log(" - Collection 'members' exists but is EMPTY."); + } else { + console.log( + ` - Found ${membersSnapshot.size} documents (showing first 5):` + ); + membersSnapshot.docs.forEach((doc) => { + console.log( + ` - ID: ${doc.id}, Data:`, + JSON.stringify(doc.data(), null, 2) + ); + }); + } + } else { + console.log("\nCollection 'members' does NOT exist."); } + } catch (error) { + console.error('Error debugging DB:', error); + } } debugDB(); diff --git a/scripts/debug-projects-fetch.js b/scripts/debug-projects-fetch.js index f1b7265f..01377cb6 100644 --- a/scripts/debug-projects-fetch.js +++ b/scripts/debug-projects-fetch.js @@ -5,63 +5,62 @@ const DATABASE_ID = '(default)'; const BASE_URL = `https://firestore.googleapis.com/v1/projects/${PROJECT_ID}/databases/${DATABASE_ID}/documents`; async function debugProjects() { - console.log("Starting Debug via Fetch..."); + console.log('Starting Debug via Fetch...'); - try { - // 1. Get Token - const account = auth.getGlobalDefaultAccount(); - if (!account || !account.tokens || !account.tokens.access_token) { - throw new Error("No access token. Run 'firebase login'."); - } - const token = account.tokens.access_token; - console.log("Token retrieved."); - - const headers = { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - }; + try { + // 1. Get Token + const account = auth.getGlobalDefaultAccount(); + if (!account || !account.tokens || !account.tokens.access_token) { + throw new Error("No access token. Run 'firebase login'."); + } + const token = account.tokens.access_token; + console.log('Token retrieved.'); - // 2. Run Collection Group Query - console.log("Running Collection Group Query for 'projects'..."); - const query = { - structuredQuery: { - from: [{ collectionId: 'projects', allDescendants: true }], - limit: 10 - } - }; + const headers = { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }; - const queryRes = await fetch(`${BASE_URL}:runQuery`, { - method: 'POST', - headers, - body: JSON.stringify(query) - }); + // 2. Run Collection Group Query + console.log("Running Collection Group Query for 'projects'..."); + const query = { + structuredQuery: { + from: [{ collectionId: 'projects', allDescendants: true }], + limit: 10, + }, + }; - if (!queryRes.ok) { - const errText = await queryRes.text(); - throw new Error(`Query failed: ${queryRes.status} ${errText}`); - } + const queryRes = await fetch(`${BASE_URL}:runQuery`, { + method: 'POST', + headers, + body: JSON.stringify(query), + }); - const queryData = await queryRes.json(); + if (!queryRes.ok) { + const errText = await queryRes.text(); + throw new Error(`Query failed: ${queryRes.status} ${errText}`); + } - // queryData is an array of objects, each containing 'document' or 'readTime' - const documents = queryData - .filter(item => item.document) - .map(item => item.document); + const queryData = await queryRes.json(); - if (documents.length === 0) { - console.log("No projects found in Collection Group."); - } else { - console.log(`Found ${documents.length} projects in Collection Group:`); - documents.forEach(doc => { - const title = doc.fields.title?.stringValue || 'Untitled'; - const starCount = doc.fields.starCount?.integerValue || 0; - console.log(` - ${doc.name} | Title: ${title} | Stars: ${starCount}`); - }); - } + // queryData is an array of objects, each containing 'document' or 'readTime' + const documents = queryData + .filter((item) => item.document) + .map((item) => item.document); - } catch (err) { - console.error("Error:", err.message); + if (documents.length === 0) { + console.log('No projects found in Collection Group.'); + } else { + console.log(`Found ${documents.length} projects in Collection Group:`); + documents.forEach((doc) => { + const title = doc.fields.title?.stringValue || 'Untitled'; + const starCount = doc.fields.starCount?.integerValue || 0; + console.log(` - ${doc.name} | Title: ${title} | Stars: ${starCount}`); + }); } + } catch (err) { + console.error('Error:', err.message); + } } debugProjects(); diff --git a/scripts/debug-projects.js b/scripts/debug-projects.js index bbebe4d7..c220c566 100644 --- a/scripts/debug-projects.js +++ b/scripts/debug-projects.js @@ -2,38 +2,46 @@ const admin = require('firebase-admin'); const serviceAccount = require('../serviceAccountKey.json'); if (!admin.apps.length) { - admin.initializeApp({ - credential: admin.credential.cert(serviceAccount) - }); + admin.initializeApp({ + credential: admin.credential.cert(serviceAccount), + }); } const db = admin.firestore(); async function debugProjects() { - console.log('--- Debugging Projects ---'); - - // 1. Check Root Collection - console.log('\n1. Checking Root "projects" collection...'); - const rootProjects = await db.collection('projects').get(); - console.log(`Found ${rootProjects.size} projects in root collection.`); - rootProjects.forEach(doc => { - console.log(` - ${doc.id}: ${doc.data().title} (createdAt: ${doc.data().createdAt?.toDate()})`); - }); - - // 2. Check Subcollections via Collection Group - console.log('\n2. Checking "projects" Collection Group...'); - const groupProjects = await db.collectionGroup('projects').get(); - console.log(`Found ${groupProjects.size} projects in collection group.`); - - groupProjects.forEach(doc => { - console.log(` - [${doc.ref.path}] ${doc.data().title} (StarCount: ${doc.data().starCount}, CreatedAt: ${doc.data().createdAt?.toDate()})`); - }); - - if (groupProjects.size === 0) { - console.log('\nWARNING: No projects found in collection group. The Showcase will be empty.'); - } else { - console.log('\nData exists. If Showcase is empty, it is likely an Index or Rule issue.'); - } + console.log('--- Debugging Projects ---'); + + // 1. Check Root Collection + console.log('\n1. Checking Root "projects" collection...'); + const rootProjects = await db.collection('projects').get(); + console.log(`Found ${rootProjects.size} projects in root collection.`); + rootProjects.forEach((doc) => { + console.log( + ` - ${doc.id}: ${doc.data().title} (createdAt: ${doc.data().createdAt?.toDate()})` + ); + }); + + // 2. Check Subcollections via Collection Group + console.log('\n2. Checking "projects" Collection Group...'); + const groupProjects = await db.collectionGroup('projects').get(); + console.log(`Found ${groupProjects.size} projects in collection group.`); + + groupProjects.forEach((doc) => { + console.log( + ` - [${doc.ref.path}] ${doc.data().title} (StarCount: ${doc.data().starCount}, CreatedAt: ${doc.data().createdAt?.toDate()})` + ); + }); + + if (groupProjects.size === 0) { + console.log( + '\nWARNING: No projects found in collection group. The Showcase will be empty.' + ); + } else { + console.log( + '\nData exists. If Showcase is empty, it is likely an Index or Rule issue.' + ); + } } debugProjects().catch(console.error); diff --git a/scripts/debug-root-projects.js b/scripts/debug-root-projects.js index 2689fe20..62779e4c 100644 --- a/scripts/debug-root-projects.js +++ b/scripts/debug-root-projects.js @@ -5,43 +5,44 @@ const DATABASE_ID = '(default)'; const BASE_URL = `https://firestore.googleapis.com/v1/projects/${PROJECT_ID}/databases/${DATABASE_ID}/documents`; async function debugRootProjects() { - console.log("Checking Root 'projects' Collection..."); - - try { - const account = auth.getGlobalDefaultAccount(); - if (!account || !account.tokens || !account.tokens.access_token) { - throw new Error("No access token. Run 'firebase login'."); - } - const token = account.tokens.access_token; - - const headers = { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - }; - - // List documents in root 'projects' collection - const res = await fetch(`${BASE_URL}/projects?pageSize=10`, { headers }); - - if (!res.ok) { - throw new Error(`Failed to fetch: ${res.status} ${await res.text()}`); - } - - const data = await res.json(); - const documents = data.documents || []; - - if (documents.length === 0) { - console.log("RESULT: Root 'projects' collection is EMPTY."); - } else { - console.log(`RESULT: Found ${documents.length} projects in ROOT collection.`); - documents.forEach(doc => { - const title = doc.fields.title?.stringValue || 'Untitled'; - console.log(` - ${doc.name.split('/').pop()} | ${title}`); - }); - } - - } catch (err) { - console.error("Error:", err.message); + console.log("Checking Root 'projects' Collection..."); + + try { + const account = auth.getGlobalDefaultAccount(); + if (!account || !account.tokens || !account.tokens.access_token) { + throw new Error("No access token. Run 'firebase login'."); + } + const token = account.tokens.access_token; + + const headers = { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }; + + // List documents in root 'projects' collection + const res = await fetch(`${BASE_URL}/projects?pageSize=10`, { headers }); + + if (!res.ok) { + throw new Error(`Failed to fetch: ${res.status} ${await res.text()}`); + } + + const data = await res.json(); + const documents = data.documents || []; + + if (documents.length === 0) { + console.log("RESULT: Root 'projects' collection is EMPTY."); + } else { + console.log( + `RESULT: Found ${documents.length} projects in ROOT collection.` + ); + documents.forEach((doc) => { + const title = doc.fields.title?.stringValue || 'Untitled'; + console.log(` - ${doc.name.split('/').pop()} | ${title}`); + }); } + } catch (err) { + console.error('Error:', err.message); + } } debugRootProjects(); diff --git a/scripts/delete-duplicate-user.ts b/scripts/delete-duplicate-user.ts index 8f0032b9..1f7b4f8d 100644 --- a/scripts/delete-duplicate-user.ts +++ b/scripts/delete-duplicate-user.ts @@ -1,60 +1,70 @@ -import { initializeApp } from "firebase/app"; -import { getFirestore, collection, getDocs, query, where, deleteDoc, doc } from "firebase/firestore"; +import { initializeApp } from 'firebase/app'; +import { + getFirestore, + collection, + getDocs, + query, + where, + deleteDoc, + doc, +} 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 deleteDuplicate() { - console.log("Searching for duplicate Pranav Khaire to delete..."); + console.log('Searching for duplicate Pranav Khaire to delete...'); - const membersRef = collection(db, 'members'); - const q = query(membersRef, where("name", "==", "Pranav Khaire")); - const snapshot = await getDocs(q); + const membersRef = collection(db, 'members'); + const q = query(membersRef, where('name', '==', 'Pranav Khaire')); + const snapshot = await getDocs(q); - let deletedCount = 0; + let deletedCount = 0; - for (const memberDoc of snapshot.docs) { - const data = memberDoc.data(); - const points = data.points || 0; + for (const memberDoc of snapshot.docs) { + const data = memberDoc.data(); + const points = data.points || 0; - // Target the one with ~103 points (less than 200 to be safe) - if (points < 200) { - console.log(`Deleting duplicate user: ${memberDoc.id} with ${points} points...`); + // Target the one with ~103 points (less than 200 to be safe) + if (points < 200) { + console.log( + `Deleting duplicate user: ${memberDoc.id} with ${points} points...` + ); - // Delete from members - await deleteDoc(doc(db, 'members', memberDoc.id)); - console.log(`- Deleted from members`); + // Delete from members + await deleteDoc(doc(db, 'members', memberDoc.id)); + console.log(`- Deleted from members`); - // Delete from leaderboard - await deleteDoc(doc(db, 'leaderboard', memberDoc.id)); - console.log(`- Deleted from leaderboard`); + // Delete from leaderboard + await deleteDoc(doc(db, 'leaderboard', memberDoc.id)); + console.log(`- Deleted from leaderboard`); - deletedCount++; - } else { - console.log(`Keeping user: ${memberDoc.id} with ${points} points.`); - } - } - - if (deletedCount === 0) { - console.log("No duplicate found with < 200 points."); + deletedCount++; } else { - console.log(`Successfully deleted ${deletedCount} duplicate(s).`); + console.log(`Keeping user: ${memberDoc.id} with ${points} points.`); } + } + + if (deletedCount === 0) { + console.log('No duplicate found with < 200 points.'); + } else { + console.log(`Successfully deleted ${deletedCount} duplicate(s).`); + } - process.exit(); + process.exit(); } deleteDuplicate(); diff --git a/scripts/fix-admin-role.ts b/scripts/fix-admin-role.ts index 5b29dee8..a1c023c8 100644 --- a/scripts/fix-admin-role.ts +++ b/scripts/fix-admin-role.ts @@ -1,4 +1,3 @@ - import { initializeApp } from 'firebase/app'; import { getFirestore, doc, setDoc, getDoc } from 'firebase/firestore'; @@ -6,39 +5,43 @@ 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 fixAdminRole() { - const email = 'ap8548328@gmail.com'; - console.log(`Fixing role for ${email}...`); - - try { - await setDoc(doc(db, 'admins', email), { - roleId: 'community-head' - }, { merge: true }); - - console.log('Update command sent.'); - - // Verify immediately - const docSnap = await getDoc(doc(db, 'admins', email)); - if (docSnap.exists()) { - console.log('Verification:', JSON.stringify(docSnap.data(), null, 2)); - } else { - console.log('Document still does not exist!'); - } - } catch (error) { - console.error('Error:', error); + const email = 'ap8548328@gmail.com'; + console.log(`Fixing role for ${email}...`); + + try { + await setDoc( + doc(db, 'admins', email), + { + roleId: 'community-head', + }, + { merge: true } + ); + + console.log('Update command sent.'); + + // Verify immediately + const docSnap = await getDoc(doc(db, 'admins', email)); + if (docSnap.exists()) { + console.log('Verification:', JSON.stringify(docSnap.data(), null, 2)); + } else { + console.log('Document still does not exist!'); } + } catch (error) { + console.error('Error:', error); + } } fixAdminRole(); diff --git a/scripts/full-recalc.ts b/scripts/full-recalc.ts index 4a95a1a2..9140283c 100644 --- a/scripts/full-recalc.ts +++ b/scripts/full-recalc.ts @@ -1,19 +1,26 @@ - -import { initializeApp } from "firebase/app"; -import { getFirestore, collection, getDocs, doc, updateDoc, writeBatch, serverTimestamp } from "firebase/firestore"; -import { getAuth, signInWithEmailAndPassword } from "firebase/auth"; +import { initializeApp } from 'firebase/app'; +import { + getFirestore, + collection, + getDocs, + doc, + updateDoc, + writeBatch, + serverTimestamp, +} from 'firebase/firestore'; +import { getAuth, signInWithEmailAndPassword } from 'firebase/auth'; 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); @@ -24,140 +31,155 @@ const SUPER_ADMIN_EMAIL = process.env.SUPER_ADMIN_EMAIL; const SUPER_ADMIN_PASSWORD = process.env.SUPER_ADMIN_PASSWORD; const POINTS = { - DAILY_LOGIN: 0, - WEEKLY_STREAK_BONUS: 50, - FOLLOW_COMMUNITY: 500, - BADGE_EARNED: 20, - SOCIAL_BADGE_EARNED: 50, - FOLLOWER_GAINED: 10, - PROJECT_STAR: 50, - CREATE_PROJECT: 200, - EVENT_PARTICIPATION: 500, - HACKATHON_WIN: 5000, - STREAK_BONUS_PER_DAY: 1 + DAILY_LOGIN: 0, + WEEKLY_STREAK_BONUS: 50, + FOLLOW_COMMUNITY: 500, + BADGE_EARNED: 20, + SOCIAL_BADGE_EARNED: 50, + FOLLOWER_GAINED: 10, + PROJECT_STAR: 50, + CREATE_PROJECT: 200, + EVENT_PARTICIPATION: 500, + HACKATHON_WIN: 5000, + STREAK_BONUS_PER_DAY: 1, }; const SOCIAL_BADGES = ['social-github', 'social-linkedin', 'social-instagram']; async function fullRecalc() { - if (!SUPER_ADMIN_EMAIL || !SUPER_ADMIN_PASSWORD) { - console.error("Error: SUPER_ADMIN_EMAIL or SUPER_ADMIN_PASSWORD environment variables are not configured."); - return; - } - console.log("Starting Full Recalculation (Reset & Recount)..."); - try { - await signInWithEmailAndPassword(auth, SUPER_ADMIN_EMAIL, SUPER_ADMIN_PASSWORD); - console.log("Signed in as Super Admin."); - - const membersRef = collection(db, 'members'); - const snapshot = await getDocs(membersRef); - let batch = writeBatch(db); - let count = 0; - - console.log(`Found ${snapshot.size} members.`); - - for (const memberDoc of snapshot.docs) { - const data = memberDoc.data(); - const uid = memberDoc.id; - const name = data.name || 'Unknown'; - - // --- 1. RECOUNT BADGES --- - const newAchievements: string[] = []; - - // Preserve Early Adopter if exists (or we could check creation date if available) - if (data.achievements?.includes('early-adopter')) { - newAchievements.push('early-adopter'); - } - - // Profile Perfect - if (data.name && data.bio && data.photoURL && data.role) { - newAchievements.push('profile-perfect'); - } - - // Connector (All 3) - if (data.github && data.linkedin && data.instagram) { - newAchievements.push('connector-social'); - } - - // Social Badges - if (data.github) newAchievements.push('social-github'); - if (data.linkedin) newAchievements.push('social-linkedin'); - if (data.instagram) newAchievements.push('social-instagram'); - - // Basic Profile - if (data.bio && data.bio.length > 20) newAchievements.push('storyteller'); - if (data.photoURL) newAchievements.push('face-of-community'); - if (data.city || data.state) newAchievements.push('local-hero'); - - // Projects - const projectsRef = collection(db, 'members', uid, 'projects'); - const projectsSnap = await getDocs(projectsRef); - const projectCount = projectsSnap.size; - let totalStars = 0; - projectsSnap.forEach(p => totalStars += (p.data().stars || []).length); - - if (projectCount >= 1) newAchievements.push('builder-1'); - if (projectCount >= 3) newAchievements.push('builder-3'); - if (projectCount >= 5) newAchievements.push('builder-5'); - if (projectCount >= 10) newAchievements.push('builder-10'); - - // Streak - const streak = data.streak || 0; - if (streak >= 7) newAchievements.push('streak-7'); - - // --- 2. CALCULATE POINTS --- - let badgePoints = 0; - newAchievements.forEach((badgeId: string) => { - if (SOCIAL_BADGES.includes(badgeId)) { - badgePoints += POINTS.SOCIAL_BADGE_EARNED; - } else { - badgePoints += POINTS.BADGE_EARNED; - } - }); - - const followers = data.followers || []; - const followerPoints = followers.length * POINTS.FOLLOWER_GAINED; - const projectPoints = (projectCount * 50) + (totalStars * POINTS.PROJECT_STAR); - const streakPoints = streak * POINTS.STREAK_BONUS_PER_DAY; - const weeklyBonuses = Math.floor(streak / 7) * POINTS.WEEKLY_STREAK_BONUS; - - const calculatedPoints = badgePoints + followerPoints + projectPoints + streakPoints + weeklyBonuses; - - console.log(`User: ${name} (${uid})`); - console.log(` Badges: ${newAchievements.length} (${newAchievements.join(', ')})`); - console.log(` Points: ${calculatedPoints}`); - - // Update Member - const memberRef = doc(db, 'members', uid); - batch.update(memberRef, { - points: calculatedPoints, - achievements: newAchievements, - lastBadgeScan: Date.now() // Set scan time - }); - - // Update Leaderboard - const leaderboardRef = doc(db, 'leaderboard', uid); - batch.set(leaderboardRef, { points: calculatedPoints }, { merge: true }); - - count++; - if (count % 400 === 0) { - await batch.commit(); - console.log("Batch committed."); - batch = writeBatch(db); - } - } - - if (count % 400 !== 0) { - await batch.commit(); - console.log("Final batch committed."); + if (!SUPER_ADMIN_EMAIL || !SUPER_ADMIN_PASSWORD) { + console.error( + 'Error: SUPER_ADMIN_EMAIL or SUPER_ADMIN_PASSWORD environment variables are not configured.' + ); + return; + } + console.log('Starting Full Recalculation (Reset & Recount)...'); + try { + await signInWithEmailAndPassword( + auth, + SUPER_ADMIN_EMAIL, + SUPER_ADMIN_PASSWORD + ); + console.log('Signed in as Super Admin.'); + + const membersRef = collection(db, 'members'); + const snapshot = await getDocs(membersRef); + let batch = writeBatch(db); + let count = 0; + + console.log(`Found ${snapshot.size} members.`); + + for (const memberDoc of snapshot.docs) { + const data = memberDoc.data(); + const uid = memberDoc.id; + const name = data.name || 'Unknown'; + + // --- 1. RECOUNT BADGES --- + const newAchievements: string[] = []; + + // Preserve Early Adopter if exists (or we could check creation date if available) + if (data.achievements?.includes('early-adopter')) { + newAchievements.push('early-adopter'); + } + + // Profile Perfect + if (data.name && data.bio && data.photoURL && data.role) { + newAchievements.push('profile-perfect'); + } + + // Connector (All 3) + if (data.github && data.linkedin && data.instagram) { + newAchievements.push('connector-social'); + } + + // Social Badges + if (data.github) newAchievements.push('social-github'); + if (data.linkedin) newAchievements.push('social-linkedin'); + if (data.instagram) newAchievements.push('social-instagram'); + + // Basic Profile + if (data.bio && data.bio.length > 20) newAchievements.push('storyteller'); + if (data.photoURL) newAchievements.push('face-of-community'); + if (data.city || data.state) newAchievements.push('local-hero'); + + // Projects + const projectsRef = collection(db, 'members', uid, 'projects'); + const projectsSnap = await getDocs(projectsRef); + const projectCount = projectsSnap.size; + let totalStars = 0; + projectsSnap.forEach( + (p) => (totalStars += (p.data().stars || []).length) + ); + + if (projectCount >= 1) newAchievements.push('builder-1'); + if (projectCount >= 3) newAchievements.push('builder-3'); + if (projectCount >= 5) newAchievements.push('builder-5'); + if (projectCount >= 10) newAchievements.push('builder-10'); + + // Streak + const streak = data.streak || 0; + if (streak >= 7) newAchievements.push('streak-7'); + + // --- 2. CALCULATE POINTS --- + let badgePoints = 0; + newAchievements.forEach((badgeId: string) => { + if (SOCIAL_BADGES.includes(badgeId)) { + badgePoints += POINTS.SOCIAL_BADGE_EARNED; + } else { + badgePoints += POINTS.BADGE_EARNED; } + }); + + const followers = data.followers || []; + const followerPoints = followers.length * POINTS.FOLLOWER_GAINED; + const projectPoints = + projectCount * 50 + totalStars * POINTS.PROJECT_STAR; + const streakPoints = streak * POINTS.STREAK_BONUS_PER_DAY; + const weeklyBonuses = Math.floor(streak / 7) * POINTS.WEEKLY_STREAK_BONUS; + + const calculatedPoints = + badgePoints + + followerPoints + + projectPoints + + streakPoints + + weeklyBonuses; + + console.log(`User: ${name} (${uid})`); + console.log( + ` Badges: ${newAchievements.length} (${newAchievements.join(', ')})` + ); + console.log(` Points: ${calculatedPoints}`); + + // Update Member + const memberRef = doc(db, 'members', uid); + batch.update(memberRef, { + points: calculatedPoints, + achievements: newAchievements, + lastBadgeScan: Date.now(), // Set scan time + }); + + // Update Leaderboard + const leaderboardRef = doc(db, 'leaderboard', uid); + batch.set(leaderboardRef, { points: calculatedPoints }, { merge: true }); + + count++; + if (count % 400 === 0) { + await batch.commit(); + console.log('Batch committed.'); + batch = writeBatch(db); + } + } - console.log("Full Recalculation Complete."); - - } catch (error) { - console.error("Error recalculating:", error); + if (count % 400 !== 0) { + await batch.commit(); + console.log('Final batch committed.'); } - process.exit(0); + + console.log('Full Recalculation Complete.'); + } catch (error) { + console.error('Error recalculating:', error); + } + process.exit(0); } fullRecalc(); diff --git a/scripts/get-badges.ts b/scripts/get-badges.ts index 46131001..dfbb9ffb 100644 --- a/scripts/get-badges.ts +++ b/scripts/get-badges.ts @@ -1,40 +1,39 @@ - -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 getBadges() { - try { - const membersRef = collection(db, 'members'); - const snapshot = await getDocs(membersRef); + try { + const membersRef = collection(db, 'members'); + const snapshot = await getDocs(membersRef); - for (const doc of snapshot.docs) { - const data = doc.data(); - if (data.name === 'Tony Stark') { - console.log(`User: ${data.name}`); - console.log(`Achievements: ${JSON.stringify(data.achievements || [])}`); - console.log('---'); - } - } - } catch (e) { - console.error(e); + for (const doc of snapshot.docs) { + const data = doc.data(); + if (data.name === 'Tony Stark') { + console.log(`User: ${data.name}`); + console.log(`Achievements: ${JSON.stringify(data.achievements || [])}`); + console.log('---'); + } } - process.exit(0); + } catch (e) { + console.error(e); + } + process.exit(0); } getBadges(); diff --git a/scripts/get-counts.ts b/scripts/get-counts.ts index d850197f..940687a5 100644 --- a/scripts/get-counts.ts +++ b/scripts/get-counts.ts @@ -1,18 +1,17 @@ - -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); @@ -21,46 +20,48 @@ const db = getFirestore(app); const SOCIAL_BADGES = ['social-github', 'social-linkedin', 'social-instagram']; async function getCounts() { - try { - const membersRef = collection(db, 'members'); - const snapshot = await getDocs(membersRef); + try { + const membersRef = collection(db, 'members'); + const snapshot = await getDocs(membersRef); - for (const doc of snapshot.docs) { - const data = doc.data(); - if (data.name === 'Tony Stark' || data.name === 'Aditya Patil') { - const uid = doc.id; - const achievements = data.achievements || []; - let socialBadges = 0; - let standardBadges = 0; - achievements.forEach((b: string) => { - if (SOCIAL_BADGES.includes(b)) socialBadges++; - else standardBadges++; - }); + for (const doc of snapshot.docs) { + const data = doc.data(); + if (data.name === 'Tony Stark' || data.name === 'Aditya Patil') { + const uid = doc.id; + const achievements = data.achievements || []; + let socialBadges = 0; + let standardBadges = 0; + achievements.forEach((b: string) => { + if (SOCIAL_BADGES.includes(b)) socialBadges++; + else standardBadges++; + }); - const followers = (data.followers || []).length; - const streak = data.streak || 0; + const followers = (data.followers || []).length; + const streak = data.streak || 0; - // Fetch projects - const projectsRef = collection(db, 'members', uid, 'projects'); - const projectsSnap = await getDocs(projectsRef); - const projectCount = projectsSnap.size; - let totalStars = 0; - projectsSnap.forEach(p => totalStars += (p.data().stars || []).length); + // Fetch projects + const projectsRef = collection(db, 'members', uid, 'projects'); + const projectsSnap = await getDocs(projectsRef); + const projectCount = projectsSnap.size; + let totalStars = 0; + projectsSnap.forEach( + (p) => (totalStars += (p.data().stars || []).length) + ); - console.log(`User: ${data.name}`); - console.log(` Social Badges: ${socialBadges}`); - console.log(` Standard Badges: ${standardBadges}`); - console.log(` Followers: ${followers}`); - console.log(` Projects: ${projectCount}`); - console.log(` Stars: ${totalStars}`); - console.log(` Streak: ${streak}`); - console.log('---'); - } - } - } catch (e) { - console.error(e); + console.log(`User: ${data.name}`); + console.log(` Social Badges: ${socialBadges}`); + console.log(` Standard Badges: ${standardBadges}`); + console.log(` Followers: ${followers}`); + console.log(` Projects: ${projectCount}`); + console.log(` Stars: ${totalStars}`); + console.log(` Streak: ${streak}`); + console.log('---'); + } } - process.exit(0); + } catch (e) { + console.error(e); + } + process.exit(0); } getCounts(); diff --git a/scripts/get-user-data.ts b/scripts/get-user-data.ts index 77b9f3a8..f19d363c 100644 --- a/scripts/get-user-data.ts +++ b/scripts/get-user-data.ts @@ -1,42 +1,47 @@ - -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 getData() { - console.log("Fetching raw data..."); - try { - const snapshot = await getDocs(collection(db, 'members')); - for (const doc of snapshot.docs) { - const data = doc.data(); - if (data.name === 'Tony Stark' || data.name === 'Aditya Patil') { - console.log(`User: ${data.name} (${doc.id})`); - console.log(` Stored Points: ${data.points}`); - console.log(` Followers: ${JSON.stringify(data.followers || [])} (Count: ${data.followers?.length || 0})`); - console.log(` Following: ${JSON.stringify(data.following || [])} (Count: ${data.following?.length || 0})`); - console.log(` Achievements: ${JSON.stringify(data.achievements || [])}`); - console.log(` Streak: ${data.streak || 0}`); - console.log('--------------------------------------------------'); - } - } - } catch (e) { - console.error(e); + console.log('Fetching raw data...'); + try { + const snapshot = await getDocs(collection(db, 'members')); + for (const doc of snapshot.docs) { + const data = doc.data(); + if (data.name === 'Tony Stark' || data.name === 'Aditya Patil') { + console.log(`User: ${data.name} (${doc.id})`); + console.log(` Stored Points: ${data.points}`); + console.log( + ` Followers: ${JSON.stringify(data.followers || [])} (Count: ${data.followers?.length || 0})` + ); + console.log( + ` Following: ${JSON.stringify(data.following || [])} (Count: ${data.following?.length || 0})` + ); + console.log( + ` Achievements: ${JSON.stringify(data.achievements || [])}` + ); + console.log(` Streak: ${data.streak || 0}`); + console.log('--------------------------------------------------'); + } } - process.exit(0); + } catch (e) { + console.error(e); + } + process.exit(0); } getData(); diff --git a/scripts/mark-hackfiesta-complete.js b/scripts/mark-hackfiesta-complete.js index 0fef79f0..9a0a6699 100644 --- a/scripts/mark-hackfiesta-complete.js +++ b/scripts/mark-hackfiesta-complete.js @@ -1,56 +1,71 @@ const { initializeApp } = require('firebase/app'); -const { getFirestore, collection, getDocs, query, updateDoc, doc } = require('firebase/firestore'); +const { + getFirestore, + collection, + getDocs, + query, + updateDoc, + doc, +} = require('firebase/firestore'); const dotenv = require('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, + 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, }; const app = initializeApp(firebaseConfig); const db = getFirestore(app); async function main() { - console.log('Fetching all events...'); + console.log('Fetching all events...'); - const q = query(collection(db, 'events')); - const snapshot = await getDocs(q); + const q = query(collection(db, 'events')); + const snapshot = await getDocs(q); - if (snapshot.empty) { - console.log('No events found in collection "events".'); - return; - } + if (snapshot.empty) { + console.log('No events found in collection "events".'); + return; + } + + const events = snapshot.docs.map((d) => ({ id: d.id, ...d.data() })); + + console.log('Events found:'); + events.forEach((e) => { + console.log( + `ID: ${e.id} | Title: "${e.title}" | Completed: ${e.completed}` + ); + }); + + // Try to find one that looks like HackFiesta + const fiesta = events.find( + (e) => + e.title && + (e.title.includes('Hack') || + e.title.includes('Fiesta') || + e.title.includes('Flesta')) + ); - const events = snapshot.docs.map(d => ({ id: d.id, ...d.data() })); - - console.log('Events found:'); - events.forEach(e => { - console.log(`ID: ${e.id} | Title: "${e.title}" | Completed: ${e.completed}`); - }); - - // Try to find one that looks like HackFiesta - const fiesta = events.find(e => e.title && (e.title.includes('Hack') || e.title.includes('Fiesta') || e.title.includes('Flesta'))); - - if (fiesta) { - console.log(`\nMatch found: ${fiesta.title} (${fiesta.id})`); - if (fiesta.completed !== true) { - console.log('Updating to completed: true...'); - await updateDoc(doc(db, 'events', fiesta.id), { - completed: true - }); - console.log('Update successful!'); - } else { - console.log('Already marked as completed.'); - } + if (fiesta) { + console.log(`\nMatch found: ${fiesta.title} (${fiesta.id})`); + if (fiesta.completed !== true) { + console.log('Updating to completed: true...'); + await updateDoc(doc(db, 'events', fiesta.id), { + completed: true, + }); + console.log('Update successful!'); } else { - console.log('\nNo match found for Hack/Fiesta.'); + console.log('Already marked as completed.'); } + } else { + console.log('\nNo match found for Hack/Fiesta.'); + } } main().catch(console.error); diff --git a/scripts/migrate-fetch.js b/scripts/migrate-fetch.js index d5bbf891..c9b1dd1a 100644 --- a/scripts/migrate-fetch.js +++ b/scripts/migrate-fetch.js @@ -1,4 +1,3 @@ - const auth = require('firebase-tools/lib/auth'); const PROJECT_ID = 'devpath-website'; @@ -6,89 +5,90 @@ const DATABASE_ID = '(default)'; const BASE_URL = `https://firestore.googleapis.com/v1/projects/${PROJECT_ID}/databases/${DATABASE_ID}/documents`; async function migrate() { - console.log("Starting Migration via Fetch..."); - - try { - // 1. Get Token - const account = auth.getGlobalDefaultAccount(); - if (!account || !account.tokens || !account.tokens.access_token) { - throw new Error("No access token. Run 'firebase login'."); - } - const token = account.tokens.access_token; - console.log("Token retrieved."); - - const headers = { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - }; - - // 2. List Projects - console.log("Fetching projects..."); - const listRes = await fetch(`${BASE_URL}/projects?pageSize=300`, { headers }); - - if (!listRes.ok) { - const errText = await listRes.text(); - throw new Error(`Failed to list projects: ${listRes.status} ${errText}`); - } + console.log('Starting Migration via Fetch...'); - const listData = await listRes.json(); - const documents = listData.documents; + try { + // 1. Get Token + const account = auth.getGlobalDefaultAccount(); + if (!account || !account.tokens || !account.tokens.access_token) { + throw new Error("No access token. Run 'firebase login'."); + } + const token = account.tokens.access_token; + console.log('Token retrieved.'); + + const headers = { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }; + + // 2. List Projects + console.log('Fetching projects...'); + const listRes = await fetch(`${BASE_URL}/projects?pageSize=300`, { + headers, + }); + + if (!listRes.ok) { + const errText = await listRes.text(); + throw new Error(`Failed to list projects: ${listRes.status} ${errText}`); + } - if (!documents || documents.length === 0) { - console.log("No projects found."); - return; - } + const listData = await listRes.json(); + const documents = listData.documents; - console.log(`Found ${documents.length} projects.`); + if (!documents || documents.length === 0) { + console.log('No projects found.'); + return; + } - let success = 0; - let errors = 0; + console.log(`Found ${documents.length} projects.`); - // 3. Migrate - for (const doc of documents) { - const docName = doc.name; - const projectId = docName.split('/').pop(); - const fields = doc.fields; + let success = 0; + let errors = 0; - const userId = fields.userId?.stringValue; + // 3. Migrate + for (const doc of documents) { + const docName = doc.name; + const projectId = docName.split('/').pop(); + const fields = doc.fields; - if (!userId) { - console.warn(`Skipping ${projectId}: No userId.`); - continue; - } + const userId = fields.userId?.stringValue; - console.log(`Migrating ${projectId} to user ${userId}...`); + if (!userId) { + console.warn(`Skipping ${projectId}: No userId.`); + continue; + } - const targetPath = `members/${userId}/projects/${projectId}`; - const url = `${BASE_URL}/${targetPath}`; + console.log(`Migrating ${projectId} to user ${userId}...`); - try { - const patchRes = await fetch(url, { - method: 'PATCH', - headers, - body: JSON.stringify({ fields }) - }); + const targetPath = `members/${userId}/projects/${projectId}`; + const url = `${BASE_URL}/${targetPath}`; - if (!patchRes.ok) { - const errText = await patchRes.text(); - throw new Error(`Failed to write: ${patchRes.status} ${errText}`); - } + try { + const patchRes = await fetch(url, { + method: 'PATCH', + headers, + body: JSON.stringify({ fields }), + }); - console.log(`Success: ${projectId}`); - success++; - } catch (err) { - console.error(`Error migrating ${projectId}:`, err.message); - errors++; - } + if (!patchRes.ok) { + const errText = await patchRes.text(); + throw new Error(`Failed to write: ${patchRes.status} ${errText}`); } - console.log("--------------------------------------------------"); - console.log(`Migration Complete. Success: ${success}, Errors: ${errors}`); - console.log("--------------------------------------------------"); - - } catch (err) { - console.error("Fatal Error:", err); + console.log(`Success: ${projectId}`); + success++; + } catch (err) { + console.error(`Error migrating ${projectId}:`, err.message); + errors++; + } } + + console.log('--------------------------------------------------'); + console.log(`Migration Complete. Success: ${success}, Errors: ${errors}`); + console.log('--------------------------------------------------'); + } catch (err) { + console.error('Fatal Error:', err); + } } migrate(); diff --git a/scripts/migrate-projects-v2.js b/scripts/migrate-projects-v2.js index f5e7e2db..bcbf7c1a 100644 --- a/scripts/migrate-projects-v2.js +++ b/scripts/migrate-projects-v2.js @@ -1,84 +1,90 @@ - const { initializeApp, applicationDefault } = require('firebase-admin/app'); const { getFirestore } = require('firebase-admin/firestore'); // Attempt to initialize with default credentials (gcloud/firebase CLI) try { - initializeApp({ - credential: applicationDefault(), - projectId: 'devpath-website' // Hardcoded from environment check - }); + initializeApp({ + credential: applicationDefault(), + projectId: 'devpath-website', // Hardcoded from environment check + }); } catch (e) { - console.error("Failed to initialize Firebase Admin:", e); - process.exit(1); + console.error('Failed to initialize Firebase Admin:', e); + process.exit(1); } const db = getFirestore(); async function migrateProjects() { - console.log("Starting Project Migration (Server-Side)..."); - - try { - // 1. Get all projects from the root 'projects' collection - const projectsSnapshot = await db.collection('projects').get(); - - if (projectsSnapshot.empty) { - console.log("No projects found in root collection."); - return; - } - - console.log(`Found ${projectsSnapshot.size} projects to migrate.`); - - let successCount = 0; - let errorCount = 0; - let skippedCount = 0; + console.log('Starting Project Migration (Server-Side)...'); - // 2. Iterate and move - for (const doc of projectsSnapshot.docs) { - const projectData = doc.data(); - const projectId = doc.id; - const userId = projectData.userId; + try { + // 1. Get all projects from the root 'projects' collection + const projectsSnapshot = await db.collection('projects').get(); - if (!userId) { - console.warn(`Skipping project ${projectId}: No userId found.`); - errorCount++; - continue; - } - - try { - // 3. Write to subcollection: members/{userId}/projects/{projectId} - const targetRef = db.collection('members').doc(userId).collection('projects').doc(projectId); - - // Check if already exists - const targetDoc = await targetRef.get(); - if (targetDoc.exists) { - console.log(`Project ${projectId} already exists in subcollection. Skipping.`); - skippedCount++; - } else { - await targetRef.set(projectData); - console.log(`Migrated project ${projectId} to user ${userId}`); - successCount++; - } + if (projectsSnapshot.empty) { + console.log('No projects found in root collection.'); + return; + } - } catch (err) { - console.error(`Failed to migrate project ${projectId}:`, err); - errorCount++; - } + console.log(`Found ${projectsSnapshot.size} projects to migrate.`); + + let successCount = 0; + let errorCount = 0; + let skippedCount = 0; + + // 2. Iterate and move + for (const doc of projectsSnapshot.docs) { + const projectData = doc.data(); + const projectId = doc.id; + const userId = projectData.userId; + + if (!userId) { + console.warn(`Skipping project ${projectId}: No userId found.`); + errorCount++; + continue; + } + + try { + // 3. Write to subcollection: members/{userId}/projects/{projectId} + const targetRef = db + .collection('members') + .doc(userId) + .collection('projects') + .doc(projectId); + + // Check if already exists + const targetDoc = await targetRef.get(); + if (targetDoc.exists) { + console.log( + `Project ${projectId} already exists in subcollection. Skipping.` + ); + skippedCount++; + } else { + await targetRef.set(projectData); + console.log(`Migrated project ${projectId} to user ${userId}`); + successCount++; } + } catch (err) { + console.error(`Failed to migrate project ${projectId}:`, err); + errorCount++; + } + } - console.log("--------------------------------------------------"); - console.log(`Migration Complete.`); - console.log(`Success: ${successCount}`); - console.log(`Skipped: ${skippedCount}`); - console.log(`Errors: ${errorCount}`); - console.log("--------------------------------------------------"); - - } catch (error) { - console.error("Fatal Error during migration:", error); - if (error.code === 7) { // PERMISSION_DENIED or similar - console.error("Error: Permission Denied. Please ensure you are logged in with 'gcloud auth application-default login' or have a valid service account."); - } + console.log('--------------------------------------------------'); + console.log(`Migration Complete.`); + console.log(`Success: ${successCount}`); + console.log(`Skipped: ${skippedCount}`); + console.log(`Errors: ${errorCount}`); + console.log('--------------------------------------------------'); + } catch (error) { + console.error('Fatal Error during migration:', error); + if (error.code === 7) { + // PERMISSION_DENIED or similar + console.error( + "Error: Permission Denied. Please ensure you are logged in with 'gcloud auth application-default login' or have a valid service account." + ); } + } } migrateProjects(); diff --git a/scripts/migrate-rest.js b/scripts/migrate-rest.js index 26bcad62..8485aa2d 100644 --- a/scripts/migrate-rest.js +++ b/scripts/migrate-rest.js @@ -1,4 +1,3 @@ - const apiv2 = require('firebase-tools/lib/apiv2'); const logger = require('firebase-tools/lib/logger'); @@ -11,64 +10,63 @@ const FIRESTORE_ORIGIN = 'https://firestore.googleapis.com'; const BASE_PATH = `/v1/projects/${PROJECT_ID}/databases/${DATABASE_ID}/documents`; async function migrate() { - console.log("Starting REST-based Migration (apiv2)..."); - - try { - const client = new apiv2.Client({ - urlPrefix: FIRESTORE_ORIGIN, - auth: true - }); - - // 1. List all projects - console.log("Fetching projects..."); - const response = await client.get(`${BASE_PATH}/projects`); - - const documents = response.body.documents; - if (!documents || documents.length === 0) { - console.log("No projects found."); - return; - } - - console.log(`Found ${documents.length} projects.`); - - let success = 0; - let errors = 0; + console.log('Starting REST-based Migration (apiv2)...'); + + try { + const client = new apiv2.Client({ + urlPrefix: FIRESTORE_ORIGIN, + auth: true, + }); + + // 1. List all projects + console.log('Fetching projects...'); + const response = await client.get(`${BASE_PATH}/projects`); + + const documents = response.body.documents; + if (!documents || documents.length === 0) { + console.log('No projects found.'); + return; + } - for (const doc of documents) { - const docName = doc.name; - const projectId = docName.split('/').pop(); - const fields = doc.fields; + console.log(`Found ${documents.length} projects.`); - const userId = fields.userId?.stringValue; + let success = 0; + let errors = 0; - if (!userId) { - console.warn(`Skipping ${projectId}: No userId.`); - continue; - } + for (const doc of documents) { + const docName = doc.name; + const projectId = docName.split('/').pop(); + const fields = doc.fields; - console.log(`Migrating ${projectId} to user ${userId}...`); + const userId = fields.userId?.stringValue; - // 2. Write to new location - const targetPath = `${BASE_PATH}/members/${userId}/projects/${projectId}`; + if (!userId) { + console.warn(`Skipping ${projectId}: No userId.`); + continue; + } - try { - // Use PATCH to upsert - await client.patch(targetPath, { - fields: fields - }); - console.log(`Success: ${projectId}`); - success++; - } catch (err) { - console.error(`Failed to write ${projectId}:`, err.message || err); - errors++; - } - } + console.log(`Migrating ${projectId} to user ${userId}...`); - console.log(`Migration Complete. Success: ${success}, Errors: ${errors}`); + // 2. Write to new location + const targetPath = `${BASE_PATH}/members/${userId}/projects/${projectId}`; - } catch (err) { - console.error("Migration Fatal Error:", err); + try { + // Use PATCH to upsert + await client.patch(targetPath, { + fields: fields, + }); + console.log(`Success: ${projectId}`); + success++; + } catch (err) { + console.error(`Failed to write ${projectId}:`, err.message || err); + errors++; + } } + + console.log(`Migration Complete. Success: ${success}, Errors: ${errors}`); + } catch (err) { + console.error('Migration Fatal Error:', err); + } } migrate(); diff --git a/scripts/migrate-sub-to-root.js b/scripts/migrate-sub-to-root.js index 36d25e71..d2eb04c7 100644 --- a/scripts/migrate-sub-to-root.js +++ b/scripts/migrate-sub-to-root.js @@ -5,91 +5,94 @@ const DATABASE_ID = '(default)'; const BASE_URL = `https://firestore.googleapis.com/v1/projects/${PROJECT_ID}/databases/${DATABASE_ID}/documents`; async function migrateToRoot() { - console.log("Starting Migration: Subcollections -> Root Collection..."); + console.log('Starting Migration: Subcollections -> Root Collection...'); - try { - // 1. Get Token - const account = auth.getGlobalDefaultAccount(); - if (!account || !account.tokens || !account.tokens.access_token) { - throw new Error("No access token. Run 'firebase login'."); - } - const token = account.tokens.access_token; - const headers = { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - }; - - // 2. Find all projects using Collection Group Query - console.log("Fetching all projects from subcollections..."); - const query = { - structuredQuery: { - from: [{ collectionId: 'projects', allDescendants: true }] - } - }; - - const queryRes = await fetch(`${BASE_URL}:runQuery`, { - method: 'POST', - headers, - body: JSON.stringify(query) - }); + try { + // 1. Get Token + const account = auth.getGlobalDefaultAccount(); + if (!account || !account.tokens || !account.tokens.access_token) { + throw new Error("No access token. Run 'firebase login'."); + } + const token = account.tokens.access_token; + const headers = { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }; + + // 2. Find all projects using Collection Group Query + console.log('Fetching all projects from subcollections...'); + const query = { + structuredQuery: { + from: [{ collectionId: 'projects', allDescendants: true }], + }, + }; + + const queryRes = await fetch(`${BASE_URL}:runQuery`, { + method: 'POST', + headers, + body: JSON.stringify(query), + }); + + if (!queryRes.ok) { + throw new Error( + `Query failed: ${queryRes.status} ${await queryRes.text()}` + ); + } - if (!queryRes.ok) { - throw new Error(`Query failed: ${queryRes.status} ${await queryRes.text()}`); - } + const queryData = await queryRes.json(); + const documents = queryData + .filter((item) => item.document) + .map((item) => item.document); - const queryData = await queryRes.json(); - const documents = queryData - .filter(item => item.document) - .map(item => item.document); - - console.log(`Found ${documents.length} projects to migrate.`); - - let success = 0; - let errors = 0; - - // 3. Copy to Root Collection - for (const doc of documents) { - const docId = doc.name.split('/').pop(); - const fields = doc.fields; - - // Ensure userId is present - if (!fields.userId) { - console.warn(`Skipping ${docId}: No userId field.`); - continue; - } - - console.log(`Migrating ${docId} (${fields.title?.stringValue})...`); - - const targetUrl = `${BASE_URL}/projects/${docId}`; - - try { - // Check if already exists in root (optional, but good to avoid overwrite if not needed, - // but here we want to ensure latest data, so we overwrite/patch) - const patchRes = await fetch(targetUrl, { - method: 'PATCH', - headers, - body: JSON.stringify({ fields }) - }); - - if (!patchRes.ok) { - throw new Error(`Failed to write: ${patchRes.status} ${await patchRes.text()}`); - } - - console.log(` - Copied to root/projects/${docId}`); - success++; - } catch (err) { - console.error(` - Error copying ${docId}:`, err.message); - errors++; - } - } + console.log(`Found ${documents.length} projects to migrate.`); + + let success = 0; + let errors = 0; + + // 3. Copy to Root Collection + for (const doc of documents) { + const docId = doc.name.split('/').pop(); + const fields = doc.fields; - console.log("--------------------------------------------------"); - console.log(`Migration Complete. Success: ${success}, Errors: ${errors}`); - console.log("--------------------------------------------------"); + // Ensure userId is present + if (!fields.userId) { + console.warn(`Skipping ${docId}: No userId field.`); + continue; + } - } catch (err) { - console.error("Fatal Error:", err.message); + console.log(`Migrating ${docId} (${fields.title?.stringValue})...`); + + const targetUrl = `${BASE_URL}/projects/${docId}`; + + try { + // Check if already exists in root (optional, but good to avoid overwrite if not needed, + // but here we want to ensure latest data, so we overwrite/patch) + const patchRes = await fetch(targetUrl, { + method: 'PATCH', + headers, + body: JSON.stringify({ fields }), + }); + + if (!patchRes.ok) { + throw new Error( + `Failed to write: ${patchRes.status} ${await patchRes.text()}` + ); + } + + console.log(` - Copied to root/projects/${docId}`); + success++; + } catch (err) { + console.error(` - Error copying ${docId}:`, err.message); + errors++; + } } + + console.log('--------------------------------------------------'); + console.log(`Migration Complete. Success: ${success}, Errors: ${errors}`); + console.log('--------------------------------------------------'); + } catch (err) { + console.error('Fatal Error:', err.message); + } } migrateToRoot(); diff --git a/scripts/migrate-with-token.js b/scripts/migrate-with-token.js index d3a45d10..30eea781 100644 --- a/scripts/migrate-with-token.js +++ b/scripts/migrate-with-token.js @@ -1,92 +1,97 @@ - const { initializeApp } = require('firebase-admin/app'); const { getFirestore } = require('firebase-admin/firestore'); const auth = require('firebase-tools/lib/auth'); const config = require('firebase-tools/lib/config'); async function migrate() { - console.log("Authenticating via firebase-tools..."); + console.log('Authenticating via firebase-tools...'); - try { - // Get the access token from the CLI session - const account = auth.getGlobalDefaultAccount(); + try { + // Get the access token from the CLI session + const account = auth.getGlobalDefaultAccount(); - if (!account || !account.tokens || !account.tokens.access_token) { - throw new Error("No access token available. Please run 'firebase login' first."); - } + if (!account || !account.tokens || !account.tokens.access_token) { + throw new Error( + "No access token available. Please run 'firebase login' first." + ); + } - const accessToken = account.tokens.access_token; - console.log("Access token retrieved successfully."); - - // Initialize Firebase Admin with the token - initializeApp({ - credential: { - getAccessToken: () => Promise.resolve({ - access_token: accessToken, - expires_in: 3600 - }) - }, - projectId: 'devpath-website' - }); - - const db = getFirestore(); - console.log("Firestore initialized."); - - // Migration Logic - console.log("Starting Project Migration..."); - const projectsSnapshot = await db.collection('projects').get(); - - if (projectsSnapshot.empty) { - console.log("No projects found in root collection."); - return; - } + const accessToken = account.tokens.access_token; + console.log('Access token retrieved successfully.'); + + // Initialize Firebase Admin with the token + initializeApp({ + credential: { + getAccessToken: () => + Promise.resolve({ + access_token: accessToken, + expires_in: 3600, + }), + }, + projectId: 'devpath-website', + }); + + const db = getFirestore(); + console.log('Firestore initialized.'); + + // Migration Logic + console.log('Starting Project Migration...'); + const projectsSnapshot = await db.collection('projects').get(); + + if (projectsSnapshot.empty) { + console.log('No projects found in root collection.'); + return; + } - console.log(`Found ${projectsSnapshot.size} projects to migrate.`); - - let successCount = 0; - let errorCount = 0; - let skippedCount = 0; - - for (const doc of projectsSnapshot.docs) { - const projectData = doc.data(); - const projectId = doc.id; - const userId = projectData.userId; - - if (!userId) { - console.warn(`Skipping project ${projectId}: No userId found.`); - errorCount++; - continue; - } - - try { - const targetRef = db.collection('members').doc(userId).collection('projects').doc(projectId); - const targetDoc = await targetRef.get(); - - if (targetDoc.exists) { - console.log(`Project ${projectId} already exists. Skipping.`); - skippedCount++; - } else { - await targetRef.set(projectData); - console.log(`Migrated project ${projectId} to user ${userId}`); - successCount++; - } - } catch (err) { - console.error(`Failed to migrate project ${projectId}:`, err); - errorCount++; - } + console.log(`Found ${projectsSnapshot.size} projects to migrate.`); + + let successCount = 0; + let errorCount = 0; + let skippedCount = 0; + + for (const doc of projectsSnapshot.docs) { + const projectData = doc.data(); + const projectId = doc.id; + const userId = projectData.userId; + + if (!userId) { + console.warn(`Skipping project ${projectId}: No userId found.`); + errorCount++; + continue; + } + + try { + const targetRef = db + .collection('members') + .doc(userId) + .collection('projects') + .doc(projectId); + const targetDoc = await targetRef.get(); + + if (targetDoc.exists) { + console.log(`Project ${projectId} already exists. Skipping.`); + skippedCount++; + } else { + await targetRef.set(projectData); + console.log(`Migrated project ${projectId} to user ${userId}`); + successCount++; } - - console.log("--------------------------------------------------"); - console.log(`Migration Complete.`); - console.log(`Success: ${successCount}`); - console.log(`Skipped: ${skippedCount}`); - console.log(`Errors: ${errorCount}`); - console.log("--------------------------------------------------"); - - } catch (error) { - console.error("Migration Failed:", error); - process.exit(1); + } catch (err) { + console.error(`Failed to migrate project ${projectId}:`, err); + errorCount++; + } } + + console.log('--------------------------------------------------'); + console.log(`Migration Complete.`); + console.log(`Success: ${successCount}`); + console.log(`Skipped: ${skippedCount}`); + console.log(`Errors: ${errorCount}`); + console.log('--------------------------------------------------'); + } catch (error) { + console.error('Migration Failed:', error); + process.exit(1); + } } migrate(); diff --git a/scripts/recalculate-all-points.ts b/scripts/recalculate-all-points.ts index 92650067..808c9a2d 100644 --- a/scripts/recalculate-all-points.ts +++ b/scripts/recalculate-all-points.ts @@ -1,19 +1,25 @@ - -import { initializeApp } from "firebase/app"; -import { getFirestore, collection, getDocs, doc, updateDoc, writeBatch } from "firebase/firestore"; -import { getAuth, signInWithEmailAndPassword } from "firebase/auth"; +import { initializeApp } from 'firebase/app'; +import { + getFirestore, + collection, + getDocs, + doc, + updateDoc, + writeBatch, +} from 'firebase/firestore'; +import { getAuth, signInWithEmailAndPassword } from 'firebase/auth'; 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); @@ -24,107 +30,120 @@ const SUPER_ADMIN_EMAIL = process.env.SUPER_ADMIN_EMAIL; const SUPER_ADMIN_PASSWORD = process.env.SUPER_ADMIN_PASSWORD; const POINTS = { - DAILY_LOGIN: 0, - WEEKLY_STREAK_BONUS: 50, - FOLLOW_COMMUNITY: 500, - BADGE_EARNED: 20, - SOCIAL_BADGE_EARNED: 50, - FOLLOWER_GAINED: 10, - PROJECT_STAR: 50, - CREATE_PROJECT: 200, - EVENT_PARTICIPATION: 500, - HACKATHON_WIN: 5000, - STREAK_BONUS_PER_DAY: 1 + DAILY_LOGIN: 0, + WEEKLY_STREAK_BONUS: 50, + FOLLOW_COMMUNITY: 500, + BADGE_EARNED: 20, + SOCIAL_BADGE_EARNED: 50, + FOLLOWER_GAINED: 10, + PROJECT_STAR: 50, + CREATE_PROJECT: 200, + EVENT_PARTICIPATION: 500, + HACKATHON_WIN: 5000, + STREAK_BONUS_PER_DAY: 1, }; const SOCIAL_BADGES = ['social-github', 'social-linkedin', 'social-instagram']; async function recalculatePoints() { - if (!SUPER_ADMIN_EMAIL || !SUPER_ADMIN_PASSWORD) { - console.error("Error: SUPER_ADMIN_EMAIL or SUPER_ADMIN_PASSWORD environment variables are not configured."); - return; - } - console.log("Starting Point Recalculation (Original Values)..."); - try { - await signInWithEmailAndPassword(auth, SUPER_ADMIN_EMAIL, SUPER_ADMIN_PASSWORD); - console.log("Signed in as Super Admin."); - - const membersRef = collection(db, 'members'); - const snapshot = await getDocs(membersRef); - let batch = writeBatch(db); - let count = 0; - - console.log(`Found ${snapshot.size} members.`); - - for (const memberDoc of snapshot.docs) { - const data = memberDoc.data(); - const uid = memberDoc.id; - const name = data.name || 'Unknown'; - - // 1. Calculate Badge Points - let badgePoints = 0; - const achievements = data.achievements || []; - achievements.forEach((badgeId: string) => { - if (SOCIAL_BADGES.includes(badgeId)) { - badgePoints += POINTS.SOCIAL_BADGE_EARNED; - } else { - badgePoints += POINTS.BADGE_EARNED; - } - }); - - // 2. Calculate Follower Points - const followers = data.followers || []; - const followerPoints = followers.length * POINTS.FOLLOWER_GAINED; - - // 3. Calculate Project Points - // Fetch projects subcollection - const projectsRef = collection(db, 'members', uid, 'projects'); - const projectsSnap = await getDocs(projectsRef); - let projectCount = projectsSnap.size; - let totalStars = 0; - projectsSnap.forEach(p => { - totalStars += (p.data().stars || []).length; - }); - - const projectPoints = (projectCount * 50) + (totalStars * POINTS.PROJECT_STAR); - - // 4. Calculate Streak Points - const streak = data.streak || 0; - const streakPoints = streak * POINTS.STREAK_BONUS_PER_DAY; - const weeklyBonuses = Math.floor(streak / 7) * POINTS.WEEKLY_STREAK_BONUS; - - const calculatedPoints = badgePoints + followerPoints + projectPoints + streakPoints + weeklyBonuses; - - console.log(`User: ${name} (${uid})`); - console.log(` Old Points: ${data.points} -> New Points: ${calculatedPoints}`); - - // Update Member - const memberRef = doc(db, 'members', uid); - batch.update(memberRef, { points: calculatedPoints }); - - // Update Leaderboard - const leaderboardRef = doc(db, 'leaderboard', uid); - batch.set(leaderboardRef, { points: calculatedPoints }, { merge: true }); - - count++; - if (count % 400 === 0) { - await batch.commit(); - console.log("Batch committed."); - batch = writeBatch(db); - } - } - - if (count % 400 !== 0) { - await batch.commit(); - console.log("Final batch committed."); + if (!SUPER_ADMIN_EMAIL || !SUPER_ADMIN_PASSWORD) { + console.error( + 'Error: SUPER_ADMIN_EMAIL or SUPER_ADMIN_PASSWORD environment variables are not configured.' + ); + return; + } + console.log('Starting Point Recalculation (Original Values)...'); + try { + await signInWithEmailAndPassword( + auth, + SUPER_ADMIN_EMAIL, + SUPER_ADMIN_PASSWORD + ); + console.log('Signed in as Super Admin.'); + + const membersRef = collection(db, 'members'); + const snapshot = await getDocs(membersRef); + let batch = writeBatch(db); + let count = 0; + + console.log(`Found ${snapshot.size} members.`); + + for (const memberDoc of snapshot.docs) { + const data = memberDoc.data(); + const uid = memberDoc.id; + const name = data.name || 'Unknown'; + + // 1. Calculate Badge Points + let badgePoints = 0; + const achievements = data.achievements || []; + achievements.forEach((badgeId: string) => { + if (SOCIAL_BADGES.includes(badgeId)) { + badgePoints += POINTS.SOCIAL_BADGE_EARNED; + } else { + badgePoints += POINTS.BADGE_EARNED; } + }); + + // 2. Calculate Follower Points + const followers = data.followers || []; + const followerPoints = followers.length * POINTS.FOLLOWER_GAINED; + + // 3. Calculate Project Points + // Fetch projects subcollection + const projectsRef = collection(db, 'members', uid, 'projects'); + const projectsSnap = await getDocs(projectsRef); + let projectCount = projectsSnap.size; + let totalStars = 0; + projectsSnap.forEach((p) => { + totalStars += (p.data().stars || []).length; + }); + + const projectPoints = + projectCount * 50 + totalStars * POINTS.PROJECT_STAR; + + // 4. Calculate Streak Points + const streak = data.streak || 0; + const streakPoints = streak * POINTS.STREAK_BONUS_PER_DAY; + const weeklyBonuses = Math.floor(streak / 7) * POINTS.WEEKLY_STREAK_BONUS; + + const calculatedPoints = + badgePoints + + followerPoints + + projectPoints + + streakPoints + + weeklyBonuses; + + console.log(`User: ${name} (${uid})`); + console.log( + ` Old Points: ${data.points} -> New Points: ${calculatedPoints}` + ); + + // Update Member + const memberRef = doc(db, 'members', uid); + batch.update(memberRef, { points: calculatedPoints }); + + // Update Leaderboard + const leaderboardRef = doc(db, 'leaderboard', uid); + batch.set(leaderboardRef, { points: calculatedPoints }, { merge: true }); + + count++; + if (count % 400 === 0) { + await batch.commit(); + console.log('Batch committed.'); + batch = writeBatch(db); + } + } - console.log("Recalculation Complete."); - - } catch (error) { - console.error("Error recalculating points:", error); + if (count % 400 !== 0) { + await batch.commit(); + console.log('Final batch committed.'); } - process.exit(0); + + console.log('Recalculation Complete.'); + } catch (error) { + console.error('Error recalculating points:', error); + } + process.exit(0); } recalculatePoints(); diff --git a/scripts/recalculate-points.ts b/scripts/recalculate-points.ts index eef5f310..19704952 100644 --- a/scripts/recalculate-points.ts +++ b/scripts/recalculate-points.ts @@ -1,108 +1,123 @@ -import { initializeApp } from "firebase/app"; -import { getFirestore, collection, getDocs, doc, updateDoc, setDoc } from "firebase/firestore"; +import { initializeApp } from 'firebase/app'; +import { + getFirestore, + collection, + getDocs, + doc, + updateDoc, + setDoc, +} 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); const POINTS = { - DAILY_LOGIN: 0, - WEEKLY_STREAK_BONUS: 50, - FOLLOW_COMMUNITY: 500, - BADGE_EARNED: 20, - SOCIAL_BADGE_EARNED: 50, - FOLLOWER_GAINED: 10, - PROJECT_STAR: 50, - CREATE_PROJECT: 200, - CREATE_DISCUSSION: 100, - EVENT_PARTICIPATION: 500, - HACKATHON_WIN: 5000, - STREAK_BONUS_PER_DAY: 1 + DAILY_LOGIN: 0, + WEEKLY_STREAK_BONUS: 50, + FOLLOW_COMMUNITY: 500, + BADGE_EARNED: 20, + SOCIAL_BADGE_EARNED: 50, + FOLLOWER_GAINED: 10, + PROJECT_STAR: 50, + CREATE_PROJECT: 200, + CREATE_DISCUSSION: 100, + EVENT_PARTICIPATION: 500, + HACKATHON_WIN: 5000, + STREAK_BONUS_PER_DAY: 1, }; async function recalculatePoints() { - console.log("Starting points recalculation..."); - try { - const membersRef = collection(db, 'members'); - const snapshot = await getDocs(membersRef); - - console.log(`Found ${snapshot.size} members.`); - - for (const memberDoc of snapshot.docs) { - const data = memberDoc.data(); - const uid = memberDoc.id; - - console.log(`Processing: ${data.name || uid}`); - - let totalPoints = 0; - - // 1. Projects - const projectsRef = collection(db, `members/${uid}/projects`); - const projectsSnapshot = await getDocs(projectsRef); - const projectCount = projectsSnapshot.size; - let starCount = 0; - projectsSnapshot.forEach(p => { - starCount += (p.data().starCount || 0); - }); - - const projectPoints = projectCount * POINTS.CREATE_PROJECT; - const starPoints = starCount * POINTS.PROJECT_STAR; - totalPoints += projectPoints + starPoints; - - // 2. Badges - const achievements = data.achievements || []; - let badgePoints = 0; - achievements.forEach((rawBadgeId: string) => { - const badgeId = rawBadgeId.trim(); - let pts = POINTS.BADGE_EARNED; - if (['social-github', 'social-linkedin', 'social-instagram'].includes(badgeId)) { - pts = POINTS.SOCIAL_BADGE_EARNED; - } - badgePoints += pts; - }); - totalPoints += badgePoints; - - // 3. Followers - const followers = data.followers || []; - const followerPoints = followers.length * POINTS.FOLLOWER_GAINED; - totalPoints += followerPoints; - - // 4. Activity - const loginDates = data.loginDates || []; - const activityPoints = loginDates.length * POINTS.STREAK_BONUS_PER_DAY; - totalPoints += activityPoints; - - console.log(` > New Points: ${totalPoints}`); - - // Update - await updateDoc(doc(db, 'members', uid), { points: totalPoints }); - await setDoc(doc(db, 'leaderboard', uid), { - uid: uid, - name: data.name, - photoURL: data.photoURL, - points: totalPoints, - role: data.role || 'member', - lastActive: new Date().toISOString().split('T')[0] - }, { merge: true }); - console.log(" > Updated DB"); + console.log('Starting points recalculation...'); + try { + const membersRef = collection(db, 'members'); + const snapshot = await getDocs(membersRef); + + console.log(`Found ${snapshot.size} members.`); + + for (const memberDoc of snapshot.docs) { + const data = memberDoc.data(); + const uid = memberDoc.id; + + console.log(`Processing: ${data.name || uid}`); + + let totalPoints = 0; + + // 1. Projects + const projectsRef = collection(db, `members/${uid}/projects`); + const projectsSnapshot = await getDocs(projectsRef); + const projectCount = projectsSnapshot.size; + let starCount = 0; + projectsSnapshot.forEach((p) => { + starCount += p.data().starCount || 0; + }); + + const projectPoints = projectCount * POINTS.CREATE_PROJECT; + const starPoints = starCount * POINTS.PROJECT_STAR; + totalPoints += projectPoints + starPoints; + + // 2. Badges + const achievements = data.achievements || []; + let badgePoints = 0; + achievements.forEach((rawBadgeId: string) => { + const badgeId = rawBadgeId.trim(); + let pts = POINTS.BADGE_EARNED; + if ( + ['social-github', 'social-linkedin', 'social-instagram'].includes( + badgeId + ) + ) { + pts = POINTS.SOCIAL_BADGE_EARNED; } - console.log("Done."); - } catch (error) { - console.error("Error:", error); + badgePoints += pts; + }); + totalPoints += badgePoints; + + // 3. Followers + const followers = data.followers || []; + const followerPoints = followers.length * POINTS.FOLLOWER_GAINED; + totalPoints += followerPoints; + + // 4. Activity + const loginDates = data.loginDates || []; + const activityPoints = loginDates.length * POINTS.STREAK_BONUS_PER_DAY; + totalPoints += activityPoints; + + console.log(` > New Points: ${totalPoints}`); + + // Update + await updateDoc(doc(db, 'members', uid), { points: totalPoints }); + await setDoc( + doc(db, 'leaderboard', uid), + { + uid: uid, + name: data.name, + photoURL: data.photoURL, + points: totalPoints, + role: data.role || 'member', + lastActive: new Date().toISOString().split('T')[0], + }, + { merge: true } + ); + console.log(' > Updated DB'); } - process.exit(); + console.log('Done.'); + } catch (error) { + console.error('Error:', error); + } + process.exit(); } recalculatePoints(); diff --git a/scripts/refresh-db.ts b/scripts/refresh-db.ts index 21963590..1e9c3993 100644 --- a/scripts/refresh-db.ts +++ b/scripts/refresh-db.ts @@ -1,4 +1,3 @@ - import * as admin from 'firebase-admin'; import * as fs from 'fs'; import * as path from 'path'; @@ -6,74 +5,78 @@ import * as path from 'path'; // Initialize Firebase Admin const serviceAccountPath = path.join(process.cwd(), 'service-account-key.json'); if (!fs.existsSync(serviceAccountPath)) { - console.error("Service account key not found at:", serviceAccountPath); - process.exit(1); + console.error('Service account key not found at:', serviceAccountPath); + process.exit(1); } const serviceAccount = JSON.parse(fs.readFileSync(serviceAccountPath, 'utf8')); if (!admin.apps.length) { - admin.initializeApp({ - credential: admin.credential.cert(serviceAccount) - }); + admin.initializeApp({ + credential: admin.credential.cert(serviceAccount), + }); } const db = admin.firestore(); async function refreshDB() { - console.log("Starting DB Refresh..."); + console.log('Starting DB Refresh...'); - // 1. Refresh Members - console.log("Refreshing Members..."); - const membersSnapshot = await db.collection('members').get(); - let updatedMembers = 0; + // 1. Refresh Members + console.log('Refreshing Members...'); + const membersSnapshot = await db.collection('members').get(); + let updatedMembers = 0; - for (const doc of membersSnapshot.docs) { - const data = doc.data(); - const updates: any = {}; + for (const doc of membersSnapshot.docs) { + const data = doc.data(); + const updates: any = {}; - // Ensure fields exist - if (!data.points) updates.points = 0; - if (!data.streak) updates.streak = 0; - if (!data.followers) updates.followers = []; - if (!data.following) updates.following = []; - if (!data.achievements) updates.achievements = []; - if (!data.loginDates) updates.loginDates = []; + // Ensure fields exist + if (!data.points) updates.points = 0; + if (!data.streak) updates.streak = 0; + if (!data.followers) updates.followers = []; + if (!data.following) updates.following = []; + if (!data.achievements) updates.achievements = []; + if (!data.loginDates) updates.loginDates = []; - // Recalculate Level (Optional, logic is in client but good to have consistent) - // We won't store level in DB explicitly if we calculate it on fly, but if we did: - // updates.level = calculateLevel(data.points || 0).currentLevel.name; + // Recalculate Level (Optional, logic is in client but good to have consistent) + // We won't store level in DB explicitly if we calculate it on fly, but if we did: + // updates.level = calculateLevel(data.points || 0).currentLevel.name; - if (Object.keys(updates).length > 0) { - await doc.ref.update(updates); - updatedMembers++; - } + if (Object.keys(updates).length > 0) { + await doc.ref.update(updates); + updatedMembers++; } - console.log(`Updated ${updatedMembers} members.`); + } + console.log(`Updated ${updatedMembers} members.`); - // 2. Refresh Leaderboard - console.log("Refreshing Leaderboard..."); - const leaderboardSnapshot = await db.collection('leaderboard').get(); - const batch = db.batch(); + // 2. Refresh Leaderboard + console.log('Refreshing Leaderboard...'); + const leaderboardSnapshot = await db.collection('leaderboard').get(); + const batch = db.batch(); - // Clear old leaderboard (optional, or just overwrite) - // Actually, let's just ensure every member is in leaderboard - for (const doc of membersSnapshot.docs) { - const data = doc.data(); - const leaderboardRef = db.collection('leaderboard').doc(doc.id); - batch.set(leaderboardRef, { - uid: doc.id, - name: data.name || 'Anonymous', - photoURL: data.photoURL || null, - points: data.points || 0, - role: data.role || 'member', - lastActive: new Date().toISOString().split('T')[0] // Approximate - }, { merge: true }); - } - await batch.commit(); - console.log("Leaderboard refreshed."); + // Clear old leaderboard (optional, or just overwrite) + // Actually, let's just ensure every member is in leaderboard + for (const doc of membersSnapshot.docs) { + const data = doc.data(); + const leaderboardRef = db.collection('leaderboard').doc(doc.id); + batch.set( + leaderboardRef, + { + uid: doc.id, + name: data.name || 'Anonymous', + photoURL: data.photoURL || null, + points: data.points || 0, + role: data.role || 'member', + lastActive: new Date().toISOString().split('T')[0], // Approximate + }, + { merge: true } + ); + } + await batch.commit(); + console.log('Leaderboard refreshed.'); - console.log("DB Refresh Complete."); + console.log('DB Refresh Complete.'); } refreshDB().catch(console.error); diff --git a/scripts/seed-admins.ts b/scripts/seed-admins.ts index d1646c3b..f9fb02d1 100644 --- a/scripts/seed-admins.ts +++ b/scripts/seed-admins.ts @@ -1,18 +1,18 @@ -import { initializeApp } from "firebase/app"; -import { getFirestore, doc, setDoc } from "firebase/firestore"; -import { getAuth, signInWithEmailAndPassword } from "firebase/auth"; +import { initializeApp } from 'firebase/app'; +import { getFirestore, doc, setDoc } from 'firebase/firestore'; +import { getAuth, signInWithEmailAndPassword } from 'firebase/auth'; 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); @@ -20,68 +20,72 @@ const db = getFirestore(app); const auth = getAuth(app); const admins = [ - { - name: "Aditya Patil", - email: "ap8548328@gmail.com", - role: "Community Head & Founder", - photoURL: "", // TODO: Add photo URL - github: "", - instagram: "", - linkedin: "" - }, - { - name: "Aditya Akolkar", - email: "aditya.akolkar@example.com", // Placeholder - role: "Technical Head", - photoURL: "", - github: "", - instagram: "", - linkedin: "" - }, - { - name: "Pranav Khaire", - email: "pranav.khaire@example.com", // Placeholder - role: "Content Head", - photoURL: "", - github: "", - instagram: "", - linkedin: "" - }, - { - name: "Dev Mukherjee", - email: "dev.mukherjee@example.com", // Placeholder - role: "Partnerships Head", - photoURL: "", - github: "", - instagram: "", - linkedin: "" - } + { + name: 'Aditya Patil', + email: 'ap8548328@gmail.com', + role: 'Community Head & Founder', + photoURL: '', // TODO: Add photo URL + github: '', + instagram: '', + linkedin: '', + }, + { + name: 'Aditya Akolkar', + email: 'aditya.akolkar@example.com', // Placeholder + role: 'Technical Head', + photoURL: '', + github: '', + instagram: '', + linkedin: '', + }, + { + name: 'Pranav Khaire', + email: 'pranav.khaire@example.com', // Placeholder + role: 'Content Head', + photoURL: '', + github: '', + instagram: '', + linkedin: '', + }, + { + name: 'Dev Mukherjee', + email: 'dev.mukherjee@example.com', // Placeholder + role: 'Partnerships Head', + photoURL: '', + github: '', + instagram: '', + linkedin: '', + }, ]; async function seedAdmins() { - try { - // Login as Super Admin to have write access - console.log("Logging in as Super Admin..."); - await signInWithEmailAndPassword(auth, process.env.SUPER_ADMIN_EMAIL as string, process.env.SUPER_ADMIN_PASSWORD as string); - console.log("Logged in successfully."); + try { + // Login as Super Admin to have write access + console.log('Logging in as Super Admin...'); + await signInWithEmailAndPassword( + auth, + process.env.SUPER_ADMIN_EMAIL as string, + process.env.SUPER_ADMIN_PASSWORD as string + ); + console.log('Logged in successfully.'); - for (const admin of admins) { - console.log(`Seeding admin: ${admin.name}`); - // Use email as ID (sanitized) or auto-id. Using email for easier lookup/auth mapping if needed. - // But for public profile, maybe auto-id is safer? - // User said "Create Collection for Them... where the Name of Admin...". - // Let's use a sanitized email or just a unique ID. - // Actually, using the email as the document ID makes it easy to check "is this user an admin?" - // But we need to be careful about PII. - // Let's use the email as the ID for now as it's a unique identifier provided. - await setDoc(doc(db, "admins", admin.email), admin); - } - console.log("Seeding complete."); - process.exit(0); - } catch (error) { - console.error("Error seeding admins:", error); - process.exit(1); + for (const admin of admins) { + console.log(`Seeding admin: ${admin.name}`); + // Use email as ID (sanitized) or auto-id. Using email for easier lookup/auth mapping if needed. + // But for public profile, maybe auto-id is safer? + // User said "Create Collection for Them... where the Name of Admin...". + // Let's use a sanitized email or just a unique ID. + // Actually, using the email as the document ID makes it easy to check "is this user an admin?" + // But we need to be careful about PII. + // Let's use the email as the ID for now as it's a unique identifier provided. + await setDoc(doc(db, 'admins', admin.email), admin); } + console.log('Seeding complete.'); + process.exit(0); + } catch (error) { + console.error('Error seeding admins:', error); + process.exit(1); + } } seedAdmins(); diff --git a/scripts/seed-new-admins.ts b/scripts/seed-new-admins.ts index 92dbce7f..b52a60dc 100644 --- a/scripts/seed-new-admins.ts +++ b/scripts/seed-new-admins.ts @@ -1,4 +1,3 @@ - import { initializeApp } from 'firebase/app'; import { getFirestore, doc, setDoc } from 'firebase/firestore'; @@ -6,84 +5,84 @@ 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); const newRoles = { - 'technical-head': { - title: "Technical Head", - tasks: [ - "Lead technical architecture and decisions", - "Oversee development of community projects", - "Mentor technical team members", - "Manage code quality and deployment standards" - ] - }, - 'content-graphics-head': { - title: "Content and Graphics Head", - tasks: [ - "Oversee all content creation and strategy", - "Manage brand identity and graphics", - "Lead content and design teams", - "Ensure consistency across all platforms" - ] - } + 'technical-head': { + title: 'Technical Head', + tasks: [ + 'Lead technical architecture and decisions', + 'Oversee development of community projects', + 'Mentor technical team members', + 'Manage code quality and deployment standards', + ], + }, + 'content-graphics-head': { + title: 'Content and Graphics Head', + tasks: [ + 'Oversee all content creation and strategy', + 'Manage brand identity and graphics', + 'Lead content and design teams', + 'Ensure consistency across all platforms', + ], + }, }; const newAdmins = [ - { - email: 'adi.akolkar12@gmail.com', - data: { - name: "Aditya Akolkar", - role: "admin", - roleId: "technical-head", - bio: "Technical Head at DevPath", - github: "https://github.com/adi-akolkar", // Placeholder - linkedin: "https://linkedin.com/in/adi-akolkar" // Placeholder - } + { + email: 'adi.akolkar12@gmail.com', + data: { + name: 'Aditya Akolkar', + role: 'admin', + roleId: 'technical-head', + bio: 'Technical Head at DevPath', + github: 'https://github.com/adi-akolkar', // Placeholder + linkedin: 'https://linkedin.com/in/adi-akolkar', // Placeholder }, - { - email: 'khairepranav246@gmail.com', - data: { - name: "Pranav Khaire", - role: "admin", - roleId: "content-graphics-head", - bio: "Content and Graphics Head at DevPath", - github: "https://github.com/pranav-khaire", // Placeholder - linkedin: "https://linkedin.com/in/pranav-khaire" // Placeholder - } - } + }, + { + email: 'khairepranav246@gmail.com', + data: { + name: 'Pranav Khaire', + role: 'admin', + roleId: 'content-graphics-head', + bio: 'Content and Graphics Head at DevPath', + github: 'https://github.com/pranav-khaire', // Placeholder + linkedin: 'https://linkedin.com/in/pranav-khaire', // Placeholder + }, + }, ]; async function seedNewAdmins() { - try { - console.log('Seeding New Roles and Admins...'); - - // 1. Seed New Roles - for (const [id, data] of Object.entries(newRoles)) { - await setDoc(doc(db, 'roles', id), data); - console.log(`Seeded role: ${id}`); - } + try { + console.log('Seeding New Roles and Admins...'); - // 2. Seed New Admins - for (const admin of newAdmins) { - await setDoc(doc(db, 'admins', admin.email), admin.data, { merge: true }); - console.log(`Seeded admin: ${admin.email}`); - } + // 1. Seed New Roles + for (const [id, data] of Object.entries(newRoles)) { + await setDoc(doc(db, 'roles', id), data); + console.log(`Seeded role: ${id}`); + } - console.log('New roles and admins seeded successfully!'); - } catch (error) { - console.error('Error seeding data:', error); + // 2. Seed New Admins + for (const admin of newAdmins) { + await setDoc(doc(db, 'admins', admin.email), admin.data, { merge: true }); + console.log(`Seeded admin: ${admin.email}`); } + + console.log('New roles and admins seeded successfully!'); + } catch (error) { + console.error('Error seeding data:', error); + } } seedNewAdmins(); diff --git a/scripts/seed-roles.ts b/scripts/seed-roles.ts index 942b0bb1..43a62425 100644 --- a/scripts/seed-roles.ts +++ b/scripts/seed-roles.ts @@ -1,75 +1,77 @@ - import { initializeApp } from 'firebase/app'; import { getFirestore, doc, setDoc } 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); const roles = { - 'community-head': { - title: "Community Head & Founder", - tasks: [ - "Oversee all community operations", - "Strategic planning and growth", - "Manage Core Team and Leads", - "Final decision making on all projects" - ] - }, - 'core-team': { - title: "Core Team Member", - tasks: [ - "Lead specific domains (Tech, Events, etc.)", - "Mentor community members", - "Organize and manage events", - "Contribute to open source projects" - ] - }, - 'member': { - title: "Community Member", - tasks: [ - "Participate in events and workshops", - "Contribute to community projects", - "Learn and grow with peers", - "Network with other developers" - ] - } + 'community-head': { + title: 'Community Head & Founder', + tasks: [ + 'Oversee all community operations', + 'Strategic planning and growth', + 'Manage Core Team and Leads', + 'Final decision making on all projects', + ], + }, + 'core-team': { + title: 'Core Team Member', + tasks: [ + 'Lead specific domains (Tech, Events, etc.)', + 'Mentor community members', + 'Organize and manage events', + 'Contribute to open source projects', + ], + }, + member: { + title: 'Community Member', + tasks: [ + 'Participate in events and workshops', + 'Contribute to community projects', + 'Learn and grow with peers', + 'Network with other developers', + ], + }, }; async function seedRoles() { - try { - console.log('Seeding Roles...'); + try { + console.log('Seeding Roles...'); - // 1. Seed Roles - for (const [id, data] of Object.entries(roles)) { - await setDoc(doc(db, 'roles', id), data); - console.log(`Seeded role: ${id}`); - } + // 1. Seed Roles + for (const [id, data] of Object.entries(roles)) { + await setDoc(doc(db, 'roles', id), data); + console.log(`Seeded role: ${id}`); + } - // 2. Update Super Admin with 'community-head' role - const superAdminEmail = 'ap8548328@gmail.com'; - console.log(`Updating Super Admin (${superAdminEmail})...`); - await setDoc(doc(db, 'admins', superAdminEmail), { - roleId: 'community-head' - }, { merge: true }); + // 2. Update Super Admin with 'community-head' role + const superAdminEmail = 'ap8548328@gmail.com'; + console.log(`Updating Super Admin (${superAdminEmail})...`); + await setDoc( + doc(db, 'admins', superAdminEmail), + { + roleId: 'community-head', + }, + { merge: true } + ); - console.log('Roles seeded and Super Admin updated successfully!'); - } catch (error) { - console.error('Error seeding roles:', error); - } + console.log('Roles seeded and Super Admin updated successfully!'); + } catch (error) { + console.error('Error seeding roles:', error); + } } seedRoles(); diff --git a/scripts/setup-project.ts b/scripts/setup-project.ts index 7ff80514..746c5a83 100644 --- a/scripts/setup-project.ts +++ b/scripts/setup-project.ts @@ -6,94 +6,106 @@ import * as readline from 'readline'; 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); const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout + input: process.stdin, + output: process.stdout, }); const question = (query: string): Promise => { - return new Promise((resolve) => { - rl.question(query, resolve); - }); + return new Promise((resolve) => { + rl.question(query, resolve); + }); }; async function setupProject() { - console.log('\n🚀 Starting DevPath Project Setup...\n'); + console.log('\n🚀 Starting DevPath Project Setup...\n'); - // 1. Check Environment Variables - if (!process.env.NEXT_PUBLIC_FIREBASE_API_KEY) { - console.error('❌ Error: Environment variables not found. Please create .env.local from .env.example.'); - process.exit(1); - } - console.log('✅ Environment variables loaded.'); + // 1. Check Environment Variables + if (!process.env.NEXT_PUBLIC_FIREBASE_API_KEY) { + console.error( + '❌ Error: Environment variables not found. Please create .env.local from .env.example.' + ); + process.exit(1); + } + console.log('✅ Environment variables loaded.'); - // 2. Super Admin Setup - console.log('\n🔑 Super Admin Setup'); - console.log('To manage the database, you need to be a Super Admin.'); - console.log('Since this is your personal Firebase instance, you can add yourself manually.'); + // 2. Super Admin Setup + console.log('\n🔑 Super Admin Setup'); + console.log('To manage the database, you need to be a Super Admin.'); + console.log( + 'Since this is your personal Firebase instance, you can add yourself manually.' + ); - const email = await question('Enter your email address to make yourself a Super Admin: '); + const email = await question( + 'Enter your email address to make yourself a Super Admin: ' + ); - if (email) { - try { - await setDoc(doc(db, 'super_admins', email), { - email: email, - createdAt: new Date().toISOString(), - createdBy: 'setup-script' - }); - console.log(`✅ Added ${email} as Super Admin.`); - } catch (error: any) { - console.error('❌ Error adding Super Admin:', error.message); - console.log('⚠️ Note: If you get a permission error, you might need to manually create the "super_admins" collection in Firebase Console first, or temporarily disable rules.'); - } - } else { - console.log('Skipping Super Admin setup.'); + if (email) { + try { + await setDoc(doc(db, 'super_admins', email), { + email: email, + createdAt: new Date().toISOString(), + createdBy: 'setup-script', + }); + console.log(`✅ Added ${email} as Super Admin.`); + } catch (error: any) { + console.error('❌ Error adding Super Admin:', error.message); + console.log( + '⚠️ Note: If you get a permission error, you might need to manually create the "super_admins" collection in Firebase Console first, or temporarily disable rules.' + ); } + } else { + console.log('Skipping Super Admin setup.'); + } - // 3. Seed Roles - console.log('\n🌱 Seeding Roles...'); - const roles = [ - { id: 'admin', name: 'Admin', description: 'Administrator with full access' }, - { id: 'member', name: 'Member', description: 'Regular community member' }, - { id: 'moderator', name: 'Moderator', description: 'Community moderator' } - ]; + // 3. Seed Roles + console.log('\n🌱 Seeding Roles...'); + const roles = [ + { + id: 'admin', + name: 'Admin', + description: 'Administrator with full access', + }, + { id: 'member', name: 'Member', description: 'Regular community member' }, + { id: 'moderator', name: 'Moderator', description: 'Community moderator' }, + ]; - for (const role of roles) { - try { - await setDoc(doc(db, 'roles', role.id), role); - console.log(` - Role '${role.name}' seeded.`); - } catch (error: any) { - console.error(` ❌ Failed to seed role '${role.name}':`, error.message); - } - } - - // 4. Seed Admin Key - console.log('\n🔑 Seeding Admin Key...'); + for (const role of roles) { try { - await setDoc(doc(db, 'admin_keys', 'config'), { - value: 'DevPath-AKDP' // Default dev key - }); - console.log('✅ Admin Key seeded (Value: DevPath-AKDP).'); + await setDoc(doc(db, 'roles', role.id), role); + console.log(` - Role '${role.name}' seeded.`); } catch (error: any) { - console.error('❌ Error seeding Admin Key:', error.message); + console.error(` ❌ Failed to seed role '${role.name}':`, error.message); } + } + + // 4. Seed Admin Key + console.log('\n🔑 Seeding Admin Key...'); + try { + await setDoc(doc(db, 'admin_keys', 'config'), { + value: 'DevPath-AKDP', // Default dev key + }); + console.log('✅ Admin Key seeded (Value: DevPath-AKDP).'); + } catch (error: any) { + console.error('❌ Error seeding Admin Key:', error.message); + } - console.log('\n🎉 Setup Complete!'); - console.log('You can now run "npm run dev" to start the application.'); - rl.close(); - process.exit(0); + console.log('\n🎉 Setup Complete!'); + console.log('You can now run "npm run dev" to start the application.'); + rl.close(); + process.exit(0); } setupProject(); diff --git a/scripts/verify-admin.ts b/scripts/verify-admin.ts index 44de77be..1431a175 100644 --- a/scripts/verify-admin.ts +++ b/scripts/verify-admin.ts @@ -1,31 +1,31 @@ // Import the central admin utilities we updated in Step 1 // Adjust this path relative to where this verification script file lives -const { getFirestore } = require("./path-to-your-init-file"); +const { getFirestore } = require('./path-to-your-init-file'); const path = require('path'); // Configure dotenv to read from the project root .env.local require('dotenv').config({ path: path.join(__dirname, '..', '.env.local') }); async function verifyAdmin() { - try { - const email = 'ap8548328@gmail.com'; - console.log(`Fetching admin via Admin SDK: ${email}`); - - // Use the safe backend firestore instance - const db = getFirestore(); + try { + const email = 'ap8548328@gmail.com'; + console.log(`Fetching admin via Admin SDK: ${email}`); - // Admin SDK uses .collection().doc().get() instead of the client getDoc() function - const docRef = db.collection('admins').doc(email); - const docSnap = await docRef.get(); + // Use the safe backend firestore instance + const db = getFirestore(); - if (docSnap.exists) { - console.log('Admin Data:', JSON.stringify(docSnap.data(), null, 2)); - } else { - console.log('No such document found in admins collection!'); - } - } catch (error) { - console.error('Error fetching admin securely:', error); + // Admin SDK uses .collection().doc().get() instead of the client getDoc() function + const docRef = db.collection('admins').doc(email); + const docSnap = await docRef.get(); + + if (docSnap.exists) { + console.log('Admin Data:', JSON.stringify(docSnap.data(), null, 2)); + } else { + console.log('No such document found in admins collection!'); } + } catch (error) { + console.error('Error fetching admin securely:', error); + } } -verifyAdmin(); \ No newline at end of file +verifyAdmin(); diff --git a/scripts/verify-full.ts b/scripts/verify-full.ts index 30323dbb..cfcf744e 100644 --- a/scripts/verify-full.ts +++ b/scripts/verify-full.ts @@ -1,4 +1,3 @@ - import { initializeApp } from 'firebase/app'; import { getFirestore, doc, getDoc } from 'firebase/firestore'; @@ -7,45 +6,48 @@ 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 verifyData() { - try { - console.log('--- Verifying Admin Data ---'); - const email = 'ap8548328@gmail.com'; - const adminDoc = await getDoc(doc(db, 'admins', email)); - - if (adminDoc.exists()) { - const data = adminDoc.data(); - console.log('Admin Document:', JSON.stringify(data, null, 2)); - - if (data.roleId) { - console.log(`\n--- Verifying Role Data (${data.roleId}) ---`); - const roleDoc = await getDoc(doc(db, 'roles', data.roleId)); - if (roleDoc.exists()) { - console.log('Role Document:', JSON.stringify(roleDoc.data(), null, 2)); - } else { - console.error('ERROR: Role document does not exist!'); - } - } else { - console.error('ERROR: Admin document missing roleId!'); - } + try { + console.log('--- Verifying Admin Data ---'); + const email = 'ap8548328@gmail.com'; + const adminDoc = await getDoc(doc(db, 'admins', email)); + + if (adminDoc.exists()) { + const data = adminDoc.data(); + console.log('Admin Document:', JSON.stringify(data, null, 2)); + + if (data.roleId) { + console.log(`\n--- Verifying Role Data (${data.roleId}) ---`); + const roleDoc = await getDoc(doc(db, 'roles', data.roleId)); + if (roleDoc.exists()) { + console.log( + 'Role Document:', + JSON.stringify(roleDoc.data(), null, 2) + ); } else { - console.error('ERROR: Admin document does not exist!'); + console.error('ERROR: Role document does not exist!'); } - } catch (error) { - console.error('Error:', error); + } else { + console.error('ERROR: Admin document missing roleId!'); + } + } else { + console.error('ERROR: Admin document does not exist!'); } + } catch (error) { + console.error('Error:', error); + } } verifyData(); diff --git a/scripts/verify-leaderboard-points.ts b/scripts/verify-leaderboard-points.ts index 7c5d3946..5b356872 100644 --- a/scripts/verify-leaderboard-points.ts +++ b/scripts/verify-leaderboard-points.ts @@ -1,92 +1,100 @@ - -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); const POINTS = { - DAILY_LOGIN: 0, - WEEKLY_STREAK_BONUS: 50, - FOLLOW_COMMUNITY: 500, - BADGE_EARNED: 20, - SOCIAL_BADGE_EARNED: 50, - FOLLOWER_GAINED: 10, - PROJECT_STAR: 50, - CREATE_PROJECT: 200, - EVENT_PARTICIPATION: 500, - HACKATHON_WIN: 5000, - STREAK_BONUS_PER_DAY: 1 + DAILY_LOGIN: 0, + WEEKLY_STREAK_BONUS: 50, + FOLLOW_COMMUNITY: 500, + BADGE_EARNED: 20, + SOCIAL_BADGE_EARNED: 50, + FOLLOWER_GAINED: 10, + PROJECT_STAR: 50, + CREATE_PROJECT: 200, + EVENT_PARTICIPATION: 500, + HACKATHON_WIN: 5000, + STREAK_BONUS_PER_DAY: 1, }; const SOCIAL_BADGES = ['social-github', 'social-linkedin', 'social-instagram']; async function verifyPoints() { - console.log("Starting Partial Verification..."); - require('fs').writeFileSync('verification_result.txt', "Starting Partial Verification...\n"); - try { - const membersRef = collection(db, 'members'); - const snapshot = await getDocs(membersRef); - console.log(`Found ${snapshot.size} members.`); - - for (const doc of snapshot.docs) { - const data = doc.data(); - const name = data.name || 'Unknown'; - - if (name !== 'Tony Stark' && name !== 'Aditya Patil') continue; - - const uid = doc.id; - const currentPoints = data.points || 0; - - // 1. Calculate Badge Points - let badgePoints = 0; - const achievements = data.achievements || []; - achievements.forEach((badgeId: string) => { - if (SOCIAL_BADGES.includes(badgeId)) { - badgePoints += POINTS.SOCIAL_BADGE_EARNED; - } else { - badgePoints += POINTS.BADGE_EARNED; - } - }); - - // 2. Calculate Follower Points - const followers = data.followers || []; - const followerPoints = followers.length * POINTS.FOLLOWER_GAINED; - - // 3. Calculate Streak Points - const streak = data.streak || 0; - const streakPoints = streak * POINTS.STREAK_BONUS_PER_DAY; - const weeklyBonuses = Math.floor(streak / 7) * POINTS.WEEKLY_STREAK_BONUS; - - // Partial Total (No Projects) - const partialPoints = badgePoints + followerPoints + streakPoints + weeklyBonuses; - - const logMsg = `User: ${name} (${uid})\n` + - ` Stored: ${currentPoints}, Partial Calc (No Projects): ${partialPoints}\n` + - ` Breakdown: Badges=${badgePoints}, Followers=${followerPoints}, Streak=${streakPoints + weeklyBonuses}\n` + - "-".repeat(30) + "\n"; - - console.log(logMsg); - require('fs').appendFileSync('verification_result.txt', logMsg); + console.log('Starting Partial Verification...'); + require('fs').writeFileSync( + 'verification_result.txt', + 'Starting Partial Verification...\n' + ); + try { + const membersRef = collection(db, 'members'); + const snapshot = await getDocs(membersRef); + console.log(`Found ${snapshot.size} members.`); + + for (const doc of snapshot.docs) { + const data = doc.data(); + const name = data.name || 'Unknown'; + + if (name !== 'Tony Stark' && name !== 'Aditya Patil') continue; + + const uid = doc.id; + const currentPoints = data.points || 0; + + // 1. Calculate Badge Points + let badgePoints = 0; + const achievements = data.achievements || []; + achievements.forEach((badgeId: string) => { + if (SOCIAL_BADGES.includes(badgeId)) { + badgePoints += POINTS.SOCIAL_BADGE_EARNED; + } else { + badgePoints += POINTS.BADGE_EARNED; } - } catch (error) { - console.error("Error:", error); - require('fs').appendFileSync('verification_result.txt', `Error: ${error}\n`); + }); + + // 2. Calculate Follower Points + const followers = data.followers || []; + const followerPoints = followers.length * POINTS.FOLLOWER_GAINED; + + // 3. Calculate Streak Points + const streak = data.streak || 0; + const streakPoints = streak * POINTS.STREAK_BONUS_PER_DAY; + const weeklyBonuses = Math.floor(streak / 7) * POINTS.WEEKLY_STREAK_BONUS; + + // Partial Total (No Projects) + const partialPoints = + badgePoints + followerPoints + streakPoints + weeklyBonuses; + + const logMsg = + `User: ${name} (${uid})\n` + + ` Stored: ${currentPoints}, Partial Calc (No Projects): ${partialPoints}\n` + + ` Breakdown: Badges=${badgePoints}, Followers=${followerPoints}, Streak=${streakPoints + weeklyBonuses}\n` + + '-'.repeat(30) + + '\n'; + + console.log(logMsg); + require('fs').appendFileSync('verification_result.txt', logMsg); } - process.exit(0); + } catch (error) { + console.error('Error:', error); + require('fs').appendFileSync( + 'verification_result.txt', + `Error: ${error}\n` + ); + } + process.exit(0); } verifyPoints(); diff --git a/scripts/verify-new-admins.ts b/scripts/verify-new-admins.ts index a27960c4..516af0a4 100644 --- a/scripts/verify-new-admins.ts +++ b/scripts/verify-new-admins.ts @@ -1,4 +1,3 @@ - import { initializeApp } from 'firebase/app'; import { getFirestore, doc, getDoc } from 'firebase/firestore'; @@ -6,54 +5,55 @@ 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 verifyNewAdmins() { - const admins = [ - { email: 'adi.akolkar12@gmail.com', roleId: 'technical-head' }, - { email: 'khairepranav246@gmail.com', roleId: 'content-graphics-head' } - ]; - - console.log('--- Verifying New Admins ---'); - - for (const admin of admins) { - try { - console.log(`Checking ${admin.email}...`); - const docSnap = await getDoc(doc(db, 'admins', admin.email)); - if (docSnap.exists()) { - const data = docSnap.data(); - console.log(` Found! Role ID: ${data.roleId}`); - if (data.roleId === admin.roleId) { - console.log(' ✅ Role ID matches.'); - } else { - console.error(` ❌ Role ID MISMATCH! Expected ${admin.roleId}, got ${data.roleId}`); - } - - // Verify Role Doc exists - const roleDoc = await getDoc(doc(db, 'roles', admin.roleId)); - if (roleDoc.exists()) { - console.log(` ✅ Role Document (${admin.roleId}) exists.`); - } else { - console.error(` ❌ Role Document (${admin.roleId}) MISSING!`); - } - - } else { - console.error(` ❌ Admin Document NOT FOUND!`); - } - } catch (error) { - console.error('Error verifying:', error); + const admins = [ + { email: 'adi.akolkar12@gmail.com', roleId: 'technical-head' }, + { email: 'khairepranav246@gmail.com', roleId: 'content-graphics-head' }, + ]; + + console.log('--- Verifying New Admins ---'); + + for (const admin of admins) { + try { + console.log(`Checking ${admin.email}...`); + const docSnap = await getDoc(doc(db, 'admins', admin.email)); + if (docSnap.exists()) { + const data = docSnap.data(); + console.log(` Found! Role ID: ${data.roleId}`); + if (data.roleId === admin.roleId) { + console.log(' ✅ Role ID matches.'); + } else { + console.error( + ` ❌ Role ID MISMATCH! Expected ${admin.roleId}, got ${data.roleId}` + ); + } + + // Verify Role Doc exists + const roleDoc = await getDoc(doc(db, 'roles', admin.roleId)); + if (roleDoc.exists()) { + console.log(` ✅ Role Document (${admin.roleId}) exists.`); + } else { + console.error(` ❌ Role Document (${admin.roleId}) MISSING!`); } + } else { + console.error(` ❌ Admin Document NOT FOUND!`); + } + } catch (error) { + console.error('Error verifying:', error); } + } } verifyNewAdmins(); diff --git a/src/app/admin/events/page.tsx b/src/app/admin/events/page.tsx index e6413098..f52dbcc0 100644 --- a/src/app/admin/events/page.tsx +++ b/src/app/admin/events/page.tsx @@ -1,224 +1,316 @@ -"use client"; +'use client'; import { useState, useEffect } from 'react'; import Image from 'next/image'; import { db } from '@/lib/firebase'; -import { collection, getDocs, addDoc, deleteDoc, doc, updateDoc, query, orderBy } from 'firebase/firestore'; -import { Plus, Trash2, Edit2, Calendar, Link as LinkIcon, Image as ImageIcon, Save, X } from 'lucide-react'; +import { + collection, + getDocs, + addDoc, + deleteDoc, + doc, + updateDoc, + query, + orderBy, +} from 'firebase/firestore'; +import { + Plus, + Trash2, + Edit2, + Calendar, + Link as LinkIcon, + Image as ImageIcon, + Save, + X, +} from 'lucide-react'; export default function AdminEventsPage() { - const [events, setEvents] = useState([]); - const [loading, setLoading] = useState(true); - const [isModalOpen, setIsModalOpen] = useState(false); - const [editingEvent, setEditingEvent] = useState(null); + const [events, setEvents] = useState([]); + const [loading, setLoading] = useState(true); + const [isModalOpen, setIsModalOpen] = useState(false); + const [editingEvent, setEditingEvent] = useState(null); - // Form State - const [formData, setFormData] = useState({ + // Form State + const [formData, setFormData] = useState({ + title: '', + description: '', + date: '', + registerLink: '', + image: '', + location: 'Online', + }); + + useEffect(() => { + fetchEvents(); + }, []); + + const fetchEvents = async () => { + setLoading(true); + try { + const q = query(collection(db!, 'events'), orderBy('date', 'asc')); + const snapshot = await getDocs(q); + setEvents(snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() }))); + } catch (error) { + console.error('Error fetching events:', error); + } finally { + setLoading(false); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + try { + if (editingEvent) { + await updateDoc(doc(db!, 'events', editingEvent.id), formData); + } else { + await addDoc(collection(db!, 'events'), { + ...formData, + createdAt: new Date().toISOString(), + }); + } + setIsModalOpen(false); + setEditingEvent(null); + setFormData({ title: '', description: '', date: '', registerLink: '', image: '', - location: 'Online' - }); + location: 'Online', + }); + fetchEvents(); + } catch (error) { + console.error('Error saving event:', error); + alert('Failed to save event.'); + } + }; - useEffect(() => { - fetchEvents(); - }, []); + const handleDelete = async (id: string) => { + if (!confirm('Are you sure you want to delete this event?')) return; + try { + await deleteDoc(doc(db!, 'events', id)); + fetchEvents(); + } catch (error) { + console.error('Error deleting event:', error); + } + }; - const fetchEvents = async () => { - setLoading(true); - try { - const q = query(collection(db!, 'events'), orderBy('date', 'asc')); - const snapshot = await getDocs(q); - setEvents(snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }))); - } catch (error) { - console.error("Error fetching events:", error); - } finally { - setLoading(false); - } - }; + const handleEdit = (event: any) => { + setEditingEvent(event); + setFormData({ + title: event.title, + description: event.description, + date: event.date, + registerLink: event.registerLink, + image: event.image, + location: event.location || 'Online', + }); + setIsModalOpen(true); + }; - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - try { - if (editingEvent) { - await updateDoc(doc(db!, 'events', editingEvent.id), formData); - } else { - await addDoc(collection(db!, 'events'), { - ...formData, - createdAt: new Date().toISOString() - }); - } - setIsModalOpen(false); + return ( +
+
+

Manage Events

+ +
- const handleDelete = async (id: string) => { - if (!confirm("Are you sure you want to delete this event?")) return; - try { - await deleteDoc(doc(db!, 'events', id)); - fetchEvents(); - } catch (error) { - console.error("Error deleting event:", error); - } - }; - - const handleEdit = (event: any) => { - setEditingEvent(event); - setFormData({ - title: event.title, - description: event.description, - date: event.date, - registerLink: event.registerLink, - image: event.image, - location: event.location || 'Online' - }); - setIsModalOpen(true); - }; - - return ( -
-
-

Manage Events

- + {loading ? ( +
Loading events...
+ ) : ( +
+ {events.map((event) => ( +
+
+ {event.image ? ( + {event.title + ) : ( +
+ +
+ )} +
+
+
+

+ {event.title} +

+ + {event.location} + +
+

+ {event.description} +

+
+ + {new Date(event.date).toLocaleDateString()} +
+
+ + +
+
+ ))} +
+ )} - {loading ? ( -
Loading events...
- ) : ( -
- {events.map(event => ( -
-
- {event.image ? ( - {event.title - ) : ( -
- -
- )} -
-
-
-

{event.title}

- {event.location} -
-

{event.description}

-
- - {new Date(event.date).toLocaleDateString()} -
-
- - -
-
-
- ))} + {isModalOpen && ( +
+
+
+

+ {editingEvent ? 'Edit Event' : 'New Event'} +

+ +
+
+
+ + + setFormData({ ...formData, title: e.target.value }) + } + className="w-full p-2 bg-background border border-border rounded-md" + /> +
+
+ +