From f655cc085d929c01162aadae638ec37fb05bd622 Mon Sep 17 00:00:00 2001 From: Sharanagouda Patil Date: Sun, 19 Jul 2026 11:56:27 +0530 Subject: [PATCH 01/23] feat: add internship roadmap UI --- .../src/pages/InternshipRoadmapPage.tsx | 80 +++++++++++++++++++ .../frontend/src/pages/WelcomePackagePage.tsx | 16 +++- apps/frontend/src/routes/AppRoutes.tsx | 10 +++ 3 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 apps/frontend/src/pages/InternshipRoadmapPage.tsx diff --git a/apps/frontend/src/pages/InternshipRoadmapPage.tsx b/apps/frontend/src/pages/InternshipRoadmapPage.tsx new file mode 100644 index 00000000..4377e45a --- /dev/null +++ b/apps/frontend/src/pages/InternshipRoadmapPage.tsx @@ -0,0 +1,80 @@ +export default function InternshipRoadmapPage() { + return ( +
+
+ +

+ πŸš€ Internship Roadmap +

+ +

+ Follow this roadmap to successfully complete your internship from onboarding + to project contribution. +

+ +
+
+ Overall Progress + 0% + +
+ + + +
+
+ +
+

+ πŸ“‹ Internship Onboarding Checklist +

+ +
    +
  • βœ… Log in to samagama.in
  • +
  • βœ… Upload and verify required documents (NOC, Offer Letter, Participation Agreement, Honor Code)
  • +
  • βœ… Submit your Zoom ID and GitHub ID
  • +
  • βœ… Join the daily Zoom stand-up meeting
  • +
  • βœ… Attend polls during the stand-up session
  • +
+
+ + +
+

+ 🎯 Eligibility Criteria +

+ +
+
+

85%

+

+ Stand-up Meeting Attendance +

+
+ +
+

85%

+

+ Poll Participation +

+
+ +
+

50%

+

+ Poll Accuracy +

+
+
+ +

+ ⚠️ Students who do not meet the eligibility criteria may be excluded from the internship. +

+
+
+
+ +
+
+ ); +} \ No newline at end of file diff --git a/apps/frontend/src/pages/WelcomePackagePage.tsx b/apps/frontend/src/pages/WelcomePackagePage.tsx index 5bd021ec..27969180 100644 --- a/apps/frontend/src/pages/WelcomePackagePage.tsx +++ b/apps/frontend/src/pages/WelcomePackagePage.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useRef } from 'react'; -import { Link } from 'react-router-dom'; +import { Link, useNavigate } from 'react-router-dom'; import { motion, AnimatePresence } from 'framer-motion'; import OrientationTab from '../components/welcome/OrientationTab'; import ProjectTimelineTab from '../components/welcome/ProjectTimelineTab'; @@ -19,6 +19,7 @@ import { spatialNavPill } from '../styles/style_config'; export default function WelcomePackagePage() { const { user } = useAuth(); const { currentProgram } = useProgram(); + const navigate = useNavigate(); // v1.69 β€” Conditional layout state: WelcomePackagePage fetches // orientation + resources in parallel and uses the loaded state @@ -186,6 +187,19 @@ export default function WelcomePackagePage() { ))} +
+

πŸš€ Internship Roadmap

+

+ Start your internship journey with a step-by-step roadmap. +

+ + +
diff --git a/apps/frontend/src/routes/AppRoutes.tsx b/apps/frontend/src/routes/AppRoutes.tsx index 639e31b5..8138907b 100644 --- a/apps/frontend/src/routes/AppRoutes.tsx +++ b/apps/frontend/src/routes/AppRoutes.tsx @@ -6,6 +6,7 @@ import Spinner from '../components/ui/Spinner'; import ErrorBoundary from '../components/ui/ErrorBoundary'; import { FeatureGate } from '../components/support/FeatureGate'; + /** * Per-route error boundary. Without this, a single page crash unmounts * the whole React tree and leaves a blank SPA. Phase 4 Β§4.5 outstanding @@ -26,6 +27,7 @@ import MainLayout from '../components/layout/MainLayout'; import AskAIButton from '../components/askai/AskAIButton'; import AccountRoute from './guards/AccountRoute'; import AdminRoute from './guards/AdminRoute'; +import InternshipRoadmapPage from "../pages/InternshipRoadmapPage"; // User pages const AccountPage = lazy(() => import('../pages/AccountPage')); @@ -185,6 +187,14 @@ export default function AppRoutes() { } /> + + + + } +/> Date: Mon, 20 Jul 2026 00:12:27 +0530 Subject: [PATCH 02/23] feat: add Bronze and Silver phase to internship roadmap --- .../src/pages/InternshipRoadmapPage.tsx | 445 +++++++++++++++--- 1 file changed, 381 insertions(+), 64 deletions(-) diff --git a/apps/frontend/src/pages/InternshipRoadmapPage.tsx b/apps/frontend/src/pages/InternshipRoadmapPage.tsx index 4377e45a..9185aedf 100644 --- a/apps/frontend/src/pages/InternshipRoadmapPage.tsx +++ b/apps/frontend/src/pages/InternshipRoadmapPage.tsx @@ -1,79 +1,396 @@ -export default function InternshipRoadmapPage() { - return ( -
-
+"use client"; -

- πŸš€ Internship Roadmap -

+import type { ReactNode } from "react"; -

- Follow this roadmap to successfully complete your internship from onboarding - to project contribution. -

+const colors = { + emerald: "#1B8A5A", + emeraldSoft: "#DCF1E5", + emeraldDeep: "#0E5C3B", + crimson: "#C0392E", + crimsonSoft: "#FAE1DD", + crimsonDeep: "#7E241B", + amber: "#C9821E", + amberSoft: "#F7E7D0", + amberDeep: "#7A4C11", + slate: "#57657A", + slateSoft: "#E7EAEE", + slateDeep: "#33404F", + board: "#0E141C", +}; -
-
- Overall Progress - 0% - -
+const boardingRequirements = [ + "Log in to samagama.in", + "Upload and verify required documents β€” NOC, offer letter, participation agreement, honor code", + "Submit your Zoom ID and GitHub ID", + "Join the daily Zoom stand-up meeting", + "Attend polls during the stand-up session", +]; - - -
-
- -
-

- πŸ“‹ Internship Onboarding Checklist -

- -
    -
  • βœ… Log in to samagama.in
  • -
  • βœ… Upload and verify required documents (NOC, Offer Letter, Participation Agreement, Honor Code)
  • -
  • βœ… Submit your Zoom ID and GitHub ID
  • -
  • βœ… Join the daily Zoom stand-up meeting
  • -
  • βœ… Attend polls during the stand-up session
  • -
-
- - -
-

- 🎯 Eligibility Criteria -

- -
-
-

85%

-

- Stand-up Meeting Attendance -

-
+const silverSteps = [ + "Work in a team of 10 members.", + "Choose one meaningful feature.", + "Complete development and testing.", + "Push your code to your Git branch.", + "Create a pull request (PR).", +]; + +const spurProjects = ["PyBe", "Ajrasakha", "FLN", "ViBe", "Tenali", "Spandan", "Spurti"]; + +const overviewStops = [ + { label: "Start", dot: colors.emerald }, + { label: "Gate", dot: colors.crimson, seg: colors.crimson }, + { label: "AI basics", dot: colors.amber, seg: colors.amber }, + { label: "MERN", dot: colors.amber, seg: colors.amber }, + { label: "Junction", dot: colors.slate, seg: colors.slate }, + { label: "Delivery", dot: "#8B94A0", seg: colors.slate }, +]; + +function CheckIcon({ color }: { color: string }) { + return ( + + + + ); +} -
-

85%

-

- Poll Participation -

+function Marker({ + code, + color, + shape = "circle", +}: { + code: string; + color: string; + shape?: "circle" | "hexagon" | "solid"; +}) { + const base = + "absolute -left-[70px] top-0.5 w-[58px] h-[58px] flex items-center justify-center bg-white border-[3px] shadow-md z-10"; + const shapeClass = shape === "hexagon" ? "rounded-none" : "rounded-full"; + const clip = + shape === "hexagon" + ? { clipPath: "polygon(50% 0%, 95% 25%, 95% 75%, 50% 100%, 5% 75%, 5% 25%)" } + : {}; + const bg = shape === "solid" ? colors.board : "#ffffff"; + return ( +
+ + {code} +
+ ); +} -
-

50%

-

- Poll Accuracy -

+function Card({ + accent, + dark, + children, + className = "", +}: { + accent?: string; + dark?: boolean; + children: ReactNode; + className?: string; +}) { + return ( +
+ {children}
-
+ ); +} + +function Eyebrow({ color, children }: { color: string; children: ReactNode }) { + return ( +

+ {children} +

+ ); +} + +export default function InternshipRoadmapPage() { + return ( +
+
+ {/* Departure board header */} +
+
+ + LINE 01 Β· AI INTERNSHIP + + DEPARTURES +
+ +

+ The Internship Express +

+

+ ORIGIN Onboarding  β†’  TERMINUS{" "} + Project Delivery +

+ +
+ PROGRESS +
+
+
+ 0% +
-

- ⚠️ Students who do not meet the eligibility criteria may be excluded from the internship. -

-
+ {/* mini overview line map */} +
+ {overviewStops.map((s, i) => ( +
+ {i > 0 && ( +
+ )} +
+ {s.label} +
+ ))}
+ {/* Journey */} +
+ {/* rail */} +
+
+
+
+ + {/* START */} +
+
+ + + + + + You are here + +
+ + + Departure Β· Platform 1 +

Onboarding

+

+ Board the internship by completing these requirements first. +

+
    + {boardingRequirements.map((item) => ( +
  • + + + + {item} +
  • + ))} +
+
+
+ + {/* GATE */} +
+ + + Ticket check +

+ Eligibility gate +

+

+ Fall short of any of these three and you may be held at the platform. +

+
+ {[ + { value: "85%", label: "Stand-up attendance" }, + { value: "85%", label: "Poll participation" }, + { value: "50%", label: "Poll accuracy" }, + ].map((m) => ( +
+ + {m.value} + + {m.label} +
+ ))} +
+

+ + + + + Students who don't meet these thresholds may be excluded from the internship. +

+
+
+ + {/* BRONZE 1 */} +
+ + + Bronze line Β· Stop 1 +

Fundamentals of AI

+

+ Core concepts before you touch a line of project code. +

+ + + + + + Due: DOJ + 1 week + +
+
+ + {/* BRONZE 2 */} +
+ + + Bronze line Β· Stop 2 +

MERN stack development

+

+ Full-stack fundamentals, on a two-checkpoint schedule. +

+
+ {[ + { pct: "50%", when: "DOJ + 2 weeks" }, + { pct: "100%", when: "DOJ + 3 weeks" }, + ].map((s) => ( +
+ {s.pct} + {s.when} +
+ ))} +
+
+
+ + {/* SILVER JUNCTION */} +
+ + + Silver line Β· Junction +

+ Phase 1 β€” Crowd source FAQ +

+

+ Interchange from learning to real project contribution. +

+
    + {silverSteps.map((step, i) => ( +
  • + + {i + 1} + + {step} +
  • + ))} +
+ + Team of 10 Β· Shared branch + + + {/* Phase 2 spur */} +
+
+

+ Phase 2 spur Β· additional project stations +

+
+ {spurProjects.map((name) => ( +
+ + + {name} + +
+ ))} +
+
+ +
+ + {/* END */} +
+ + +
+ {Array.from({ length: 12 }).map((_, i) => ( + + ))} +
+

+ Arrival Β· Final stop +

+

Project delivery

+

+ Internship complete. Thanks for riding the line β€” welcome to the platform. +

+
+
+
); From f8ef295bdba4195f09b79ac576db58b505e0cad7 Mon Sep 17 00:00:00 2001 From: Ashishpatra0-0 Date: Mon, 20 Jul 2026 10:57:47 +0530 Subject: [PATCH 03/23] Add email notifications for question answered/resolved events --- apps/backend/package.json | 2 + .../community/post-lifecycle.controller.ts | 3 +- apps/backend/src/scripts/testEmail.ts | 20 + .../src/services/notifications.service.ts | 76 +- apps/backend/src/utils/email/sendEmail.ts | 64 + .../src/utils/http/notificationDispatcher.ts | 10 +- package-lock.json | 3815 +++++++++++++++++ pnpm-lock.yaml | 19 + 8 files changed, 4001 insertions(+), 8 deletions(-) create mode 100644 apps/backend/src/scripts/testEmail.ts create mode 100644 apps/backend/src/utils/email/sendEmail.ts create mode 100644 package-lock.json diff --git a/apps/backend/package.json b/apps/backend/package.json index 3a8e48b9..51256c03 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -56,6 +56,7 @@ "mongoose": "^8.0.3", "morgan": "^1.10.1", "multer": "^2.1.1", + "nodemailer": "^9.0.3", "obscenity": "^0.4.6", "openai": "^6.39.0", "pdf-parse": "^2.4.5", @@ -74,6 +75,7 @@ "@types/morgan": "^1.9.10", "@types/multer": "^2.1.0", "@types/node": "^25.9.1", + "@types/nodemailer": "^8.0.1", "@types/range-parser": "^1.2.7", "@types/uuid": "^10.0.0", "nodemon": "^3.0.2", diff --git a/apps/backend/src/modules/community/post-lifecycle.controller.ts b/apps/backend/src/modules/community/post-lifecycle.controller.ts index d7648974..aed41946 100644 --- a/apps/backend/src/modules/community/post-lifecycle.controller.ts +++ b/apps/backend/src/modules/community/post-lifecycle.controller.ts @@ -108,7 +108,8 @@ export const resolvePost = async (req: Request, res: Response): Promise => recipientId: post.author, eventType: 'post_resolved', link: `/community?post=${post._id}`, - title: 'Your question was resolved!', + title: post.title, + message: `Your question "${post.title}" has been answered: ${post.answer}`, }).catch((err) => { communityLog.warn(`[post] Failed to dispatch post resolved notification: ${(err as Error).message}`); }); diff --git a/apps/backend/src/scripts/testEmail.ts b/apps/backend/src/scripts/testEmail.ts new file mode 100644 index 00000000..ae044e9d --- /dev/null +++ b/apps/backend/src/scripts/testEmail.ts @@ -0,0 +1,20 @@ +/** + * scripts/testEmail.ts + * One-off script to verify the email utility works before wiring it + * into the real notification flow. Run with: + * pnpm --filter yaksha-faq-backend exec tsx src/scripts/testEmail.ts + */ +import 'dotenv/config'; +import { sendEmail } from '../utils/email/sendEmail.js'; + +async function main() { + const result = await sendEmail({ + to: 'test@example.com', // Mailtrap catches all emails regardless of the "to" address + subject: 'Test email from CSFAQ local setup', + text: 'If you see this in your Mailtrap inbox, the email utility works!', + }); + console.log('Email sent successfully:', result); + process.exit(result ? 0 : 1); +} + +main(); \ No newline at end of file diff --git a/apps/backend/src/services/notifications.service.ts b/apps/backend/src/services/notifications.service.ts index 77e6bd12..31da65ea 100644 --- a/apps/backend/src/services/notifications.service.ts +++ b/apps/backend/src/services/notifications.service.ts @@ -25,6 +25,11 @@ import NotificationOutbox from '../models/NotificationOutbox.js'; import { logger } from '../utils/http/logger.js'; import { startSession } from 'mongoose'; +// === EMAIL FEATURE β€” new imports === +import { sendEmail } from '../utils/email/sendEmail.js'; +import User from '../modules/auth/user.model.js'; +// === END === + // ─── Public types ───────────────────────────────────────────────────────── export type DispatchEventType = @@ -150,12 +155,73 @@ class NotificationsService { link: input.link, read: false, }); + // === EMAIL FEATURE START === + if (input.eventType === 'question_answered' || input.eventType === 'post_resolved') { + const user = await User.findById(recipientId).select('email name').lean(); + if (user?.email) { + void sendEmail({ + to: user.email, + subject: title, + text: message, + html: `
+ +
+ Yaksha FAQ +
+ + +
+

Hi there,

+

+ Your question "${title}" just got a response. +

+ + +
+

${message}

+
+ +

+ Read the full thread here:
+ + ${process.env.CLIENT_URL ?? ''}${input.link} + +

+ +

The Yaksha FAQ Team

+
+ + + +
+

+ You're receiving this because you have an active question on Yaksha FAQ Portal. +

+

+ Yaksha FAQ Portal · Vicharanashala Lab, IIT Ropar +

+
+
`, + }); + } + } + // === EMAIL FEATURE END === return { delivered: true, viaOutbox: false, notificationId: doc._id }; } catch (err) { const error = err as Error; logger.warn( `[notifications] direct dispatch failed for ${String(recipientId)}: ${error.message}. ` + - `Routing to outbox.`, + `Routing to outbox.`, ); // Direct dispatch failed β€” schedule for the drain. try { @@ -177,7 +243,7 @@ class NotificationsService { // caller's operation must continue. logger.error( `[notifications] OUTBOX write also failed for ${String(recipientId)}: ` + - `${(outboxErr as Error).message}. Notification permanently lost.`, + `${(outboxErr as Error).message}. Notification permanently lost.`, ); return { delivered: false, viaOutbox: false }; } @@ -204,7 +270,7 @@ class NotificationsService { await NotificationOutbox.deleteMany({ _id: { $in: ids } }); logger.warn( `[notifications] outbox cap exceeded (${total} > ${OUTBOX_MAX_ROWS}). ` + - `Dropped ${ids.length} oldest pending rows.`, + `Dropped ${ids.length} oldest pending rows.`, ); } @@ -259,7 +325,7 @@ class NotificationsService { // Give up β€” log a hard error and drop the row. logger.error( `[notifications] outbox row ${String(row._id)} exceeded ${OUTBOX_MAX_ATTEMPTS} attempts. ` + - `Dropping. Last error: ${(err as Error).message}`, + `Dropping. Last error: ${(err as Error).message}`, ); await NotificationOutbox.deleteOne({ _id: row._id }); dropped++; @@ -287,7 +353,7 @@ class NotificationsService { if (delivered > 0 || dropped > 0) { logger.info( `[notifications] drain: ${attempted} attempted, ${delivered} delivered, ` + - `${rescheduled} rescheduled, ${dropped} dropped`, + `${rescheduled} rescheduled, ${dropped} dropped`, ); } return { attempted, delivered, rescheduled, dropped }; diff --git a/apps/backend/src/utils/email/sendEmail.ts b/apps/backend/src/utils/email/sendEmail.ts new file mode 100644 index 00000000..9a1fe861 --- /dev/null +++ b/apps/backend/src/utils/email/sendEmail.ts @@ -0,0 +1,64 @@ +/** + * utils/email/sendEmail.ts + * + * Small, self-contained email-sending utility using nodemailer. + * Added as a new capability alongside the existing in-app-only + * notifications.service.ts β€” does not modify any existing logic. + * + * Usage: + * import { sendEmail } from '../utils/email/sendEmail.js'; + * await sendEmail({ + * to: 'user@example.com', + * subject: 'Your question was answered', + * text: 'Someone replied to your question on Yaksha FAQ.', + * }); + */ +import nodemailer from 'nodemailer'; +import { logger } from '../http/logger.js'; + +interface SendEmailInput { + to: string; + subject: string; + text: string; + html?: string; +} + +let transporter: nodemailer.Transporter | null = null; + +function getTransporter(): nodemailer.Transporter | null { + if (!process.env.MAIL_HOST || !process.env.MAIL_USER || !process.env.MAIL_PASS) { + logger.warn('[email] MAIL_HOST/MAIL_USER/MAIL_PASS not fully configured β€” email sending disabled.'); + return null; + } + if (!transporter) { + transporter = nodemailer.createTransport({ + host: process.env.MAIL_HOST, + port: Number(process.env.MAIL_PORT ?? 2525), + auth: { + user: process.env.MAIL_USER, + pass: process.env.MAIL_PASS, + }, + }); + } + return transporter; +} + +export async function sendEmail(input: SendEmailInput): Promise { + const t = getTransporter(); + if (!t) return false; + + try { + await t.sendMail({ + from: '"Yaksha FAQ Portal" ', + to: input.to, + subject: input.subject, + text: input.text, + html: input.html, + }); + logger.info(`[email] sent to ${input.to}: ${input.subject}`); + return true; + } catch (err) { + logger.error(`[email] failed to send to ${input.to}: ${(err as Error).message}`); + return false; + } +} \ No newline at end of file diff --git a/apps/backend/src/utils/http/notificationDispatcher.ts b/apps/backend/src/utils/http/notificationDispatcher.ts index 57673bf2..dbb7f71f 100644 --- a/apps/backend/src/utils/http/notificationDispatcher.ts +++ b/apps/backend/src/utils/http/notificationDispatcher.ts @@ -91,6 +91,11 @@ interface DispatchOptions { * When omitted a sensible default is derived from eventType. */ title?: string; + /** + * Optional custom message override (e.g. real question title + answer). + * Falls back to random text-bank message when omitted. + */ + message?: string; } /** @@ -107,17 +112,18 @@ export const dispatchNotification = async ({ eventType, link, title, + message, }: DispatchOptions): Promise => { const bank = notificationTextBank[eventType]; if (!bank || bank.length === 0) return; // Unknown eventType β€” no-op silently - const message = bank[Math.floor(Math.random() * bank.length)]; + const finalMessage = message ?? bank[Math.floor(Math.random() * bank.length)]; await notificationsService.dispatch({ recipientId, eventType, link, title, - message, + message: finalMessage, }); }; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..ca1c81ab --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3815 @@ +{ + "name": "crowd-source-faq", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "crowd-source-faq", + "version": "0.1.0", + "dependencies": { + "sharp": "^0.34.5" + }, + "devDependencies": { + "@playwright/test": "^1.44.1", + "@types/node": "^25.9.1", + "@types/range-parser": "^1.2.7", + "@typescript-eslint/eslint-plugin": "^7.1.0", + "@typescript-eslint/parser": "^7.1.0", + "eslint": "^8.57.0", + "mongodb-memory-server": "^9.2.0", + "mongoose": "^8.0.3", + "prettier": "^3.2.5", + "tsx": "^4.22.3", + "turbo": "^2.5.0", + "typescript": "^6.0.3" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.4.12", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.12.tgz", + "integrity": "sha512-QAfAMwNgnYxZ2C6D1HgeP7Gc4i/uvJRim415PCIL9ptRxWMNbWeLBYb2/9R4pGKny/s1FVu2JA2cxCUBUOggrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@playwright/test": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz", + "integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@turbo/darwin-64": { + "version": "2.10.5", + "resolved": "https://registry.npmjs.org/@turbo/darwin-64/-/darwin-64-2.10.5.tgz", + "integrity": "sha512-ENvPwy3x5yS7MwNYHeWjqOBXkwIMp39Pd+/zXC6PoiNzF8EIvvLZOZZ+ny6L9x4WgS5vxUii2LM5gM+zjPdnWw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@turbo/darwin-arm64": { + "version": "2.10.5", + "resolved": "https://registry.npmjs.org/@turbo/darwin-arm64/-/darwin-arm64-2.10.5.tgz", + "integrity": "sha512-rqROo9zsF/P9RqsdtbLD1nFJicjSrYyvQ9kNJC38AbxA3pAs6VAlATvtvOFx7bqOv6vicf20SP9kF33avJjy2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@turbo/linux-64": { + "version": "2.10.5", + "resolved": "https://registry.npmjs.org/@turbo/linux-64/-/linux-64-2.10.5.tgz", + "integrity": "sha512-RoSSiNFUxi27zLJuM9F6GyWWjHgLch9t6nwD6K0FkXRirZkTLlzIj6IhFnK8H9++nefLtdFqylE4vGjZAv6AAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@turbo/linux-arm64": { + "version": "2.10.5", + "resolved": "https://registry.npmjs.org/@turbo/linux-arm64/-/linux-arm64-2.10.5.tgz", + "integrity": "sha512-4ZComcpzmHGmVynQqvvi+iZOSq/tBvY1SltXB8g4NZRsrA01W8E+yRL8RNM+PLoyWsrCnJa8xa+DkWkv+xg4iQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@turbo/windows-64": { + "version": "2.10.5", + "resolved": "https://registry.npmjs.org/@turbo/windows-64/-/windows-64-2.10.5.tgz", + "integrity": "sha512-eL2Iyj4DbMINq1Sr1w0iAi6nAiZOF16KSlRGwCJpVh+IWZeY33MAsLHVOBMj1xoFtncVJXclCVpTPL2nBoYkFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@turbo/windows-arm64": { + "version": "2.10.5", + "resolved": "https://registry.npmjs.org/@turbo/windows-arm64/-/windows-arm64-2.10.5.tgz", + "integrity": "sha512-sog+wP+8YSJrdWZ/rUJg8xghVTrwoG+BrSlDQpnK5fzSgJHn1INRWXbVWRH0d3vX8dBI01E3yxXRre9Dn+OXQA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/node": { + "version": "25.9.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.5.tgz", + "integrity": "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz", + "integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/webidl-conversions": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", + "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/type-utils": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", + "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", + "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", + "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", + "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", + "dev": true, + "license": "ISC" + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/async-mutex": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.1.tgz", + "integrity": "sha512-WfoBo4E/TbCX1G95XTjbWTE3X2XLG0m1Xbv2cwOtuPdyH9CZvnaA5nCt1ucjaKEgW2A5IF71hxrRhr83Je5xjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/b4a": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/bare-events": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz", + "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.4.tgz", + "integrity": "sha512-y1kC+ffIx/tPLdTE693uNjHfzTfr+ravR5tvWlMXe25nELbkqV400S71qHDwbkAQ1FVEZobB1NFRzFbCCcyBCQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.1.tgz", + "integrity": "sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/bare-stream": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz", + "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.8.1", + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.5.tgz", + "integrity": "sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bson": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/bson/-/bson-5.5.1.tgz", + "integrity": "sha512-ix0EwukN2EpC0SRWIj/7B5+A6uQMQy6KMREI9qQqvgpkV2frH63T0UDVd1SYedL6dNCmDBYB3QtXi4ISk9YT+g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14.20.1" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/kareem": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", + "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mongodb": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.9.2.tgz", + "integrity": "sha512-H60HecKO4Bc+7dhOv4sJlgvenK4fQNqqUIlXxZYQNbfEWSALGAwGoyJd/0Qwk4TttFXUOHJ2ZJQe/52ScaUwtQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bson": "^5.5.0", + "mongodb-connection-string-url": "^2.6.0", + "socks": "^2.7.1" + }, + "engines": { + "node": ">=14.20.1" + }, + "optionalDependencies": { + "@mongodb-js/saslprep": "^1.1.0" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.0.0", + "kerberos": "^1.0.0 || ^2.0.0", + "mongodb-client-encryption": ">=2.3.0 <3", + "snappy": "^7.2.2" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", + "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^8.2.1", + "whatwg-url": "^11.0.0" + } + }, + "node_modules/mongodb-memory-server": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/mongodb-memory-server/-/mongodb-memory-server-9.5.0.tgz", + "integrity": "sha512-In3zRT40cLlVtpy7FK6b96Lby6JBAdXj8Kf9YrH4p1Aa2X4ptojq7SmiRR3x47Lo0/UCXXIwhJpkdbYY8kRZAw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "mongodb-memory-server-core": "9.5.0", + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=14.20.1" + } + }, + "node_modules/mongodb-memory-server-core": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/mongodb-memory-server-core/-/mongodb-memory-server-core-9.5.0.tgz", + "integrity": "sha512-Jb/V80JeYAKWaF4bPFme7SmTR6ew1PWgkpPUepLDfRraeN49i1cruxICeA4zz4T33W/o31N+zazP8wI8ebf7yw==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-mutex": "^0.4.1", + "camelcase": "^6.3.0", + "debug": "^4.3.7", + "find-cache-dir": "^3.3.2", + "follow-redirects": "^1.15.9", + "https-proxy-agent": "^7.0.5", + "mongodb": "^5.9.2", + "new-find-package-json": "^2.0.0", + "semver": "^7.6.3", + "tar-stream": "^3.1.7", + "tslib": "^2.6.3", + "yauzl": "^3.1.3" + }, + "engines": { + "node": ">=14.20.1" + } + }, + "node_modules/mongoose": { + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.24.1.tgz", + "integrity": "sha512-UpHBA0l5kHyKJQFjmBaFYQFo5sgz1DK0TRqDkOyBLYbqiIbKKhIvBpHWBXqeo0rgW4kGI1UhhAw+kTQZoj1BdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bson": "^6.10.4", + "kareem": "2.6.3", + "mongodb": "~6.20.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mongoose/node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/mongoose/node_modules/bson": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", + "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/mongoose/node_modules/mongodb": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", + "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.2" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.3.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongoose/node_modules/mongodb-connection-string-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, + "node_modules/mongoose/node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/mongoose/node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/new-find-package-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/new-find-package-json/-/new-find-package-json-2.0.0.tgz", + "integrity": "sha512-lDcBsjBSMlj3LXH2v/FW3txlh2pYTjmbOXPYJD93HI5EwuLzI11tdHSIpUMmfq/IOsldj4Ps8M8flhm+pCK4Ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/playwright": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.5.tgz", + "integrity": "sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/streamx": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.0.tgz", + "integrity": "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", + "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "devOptional": true, + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz", + "integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/turbo": { + "version": "2.10.5", + "resolved": "https://registry.npmjs.org/turbo/-/turbo-2.10.5.tgz", + "integrity": "sha512-07Y/C7OUp23l4P92PJoYtFNbHjLhftrZH5Ce7dbczS4kX2Re+wtbXvZLoxn/pUtzgsQaRCBaRuZPJp4zmAn0WQ==", + "dev": true, + "license": "MIT", + "bin": { + "turbo": "bin/turbo" + }, + "optionalDependencies": { + "@turbo/darwin-64": "2.10.5", + "@turbo/darwin-arm64": "2.10.5", + "@turbo/linux-64": "2.10.5", + "@turbo/linux-arm64": "2.10.5", + "@turbo/windows-64": "2.10.5", + "@turbo/windows-arm64": "2.10.5" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/yauzl": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.4.0.tgz", + "integrity": "sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8c772583..de032656 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -111,6 +111,9 @@ importers: multer: specifier: ^2.1.1 version: 2.2.0 + nodemailer: + specifier: ^9.0.3 + version: 9.0.3 obscenity: specifier: ^0.4.6 version: 0.4.6 @@ -160,6 +163,9 @@ importers: '@types/node': specifier: ^25.9.1 version: 25.9.4 + '@types/nodemailer': + specifier: ^8.0.1 + version: 8.0.1 '@types/range-parser': specifier: ^1.2.7 version: 1.2.7 @@ -1603,6 +1609,9 @@ packages: '@types/node@25.9.4': resolution: {integrity: sha512-dszCsrKb5U7ZsVZBWiHFklTloVl0mSEnWH/iZXfZUlI4rzCUnsvGmgqfuVRHL54ugE7/wRuxEIXRa2iMZ+BG6g==} + '@types/nodemailer@8.0.1': + resolution: {integrity: sha512-PxpaInm8V1JQDd4j0ds5HfvWQk8JupS1C0Picb96QJsrrRDjBH+DlK7L4ZdNSqNULhiZRQHc40nLVShaGxXAMw==} + '@types/prop-types@15.7.15': resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} @@ -3239,6 +3248,10 @@ packages: resolution: {integrity: sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA==} engines: {node: '>=18'} + nodemailer@9.0.3: + resolution: {integrity: sha512-n+YP+NKwR5zRWa60k3GiQ6Q3B4KXCoAw40dAKeCtYn020iNN74aWK2liXIC3ZEATeGql7we3tE3t8QwhY0eskw==} + engines: {node: '>=6.0.0'} + nodemon@3.1.14: resolution: {integrity: sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==} engines: {node: '>=10'} @@ -5371,6 +5384,10 @@ snapshots: dependencies: undici-types: 7.24.6 + '@types/nodemailer@8.0.1': + dependencies: + '@types/node': 25.9.4 + '@types/prop-types@15.7.15': {} '@types/qs@6.15.1': {} @@ -7146,6 +7163,8 @@ snapshots: node-releases@2.0.48: {} + nodemailer@9.0.3: {} + nodemon@3.1.14: dependencies: chokidar: 3.6.0 From 7647e425d9fa55001fe973f2e86cc9c47fdfa212 Mon Sep 17 00:00:00 2001 From: Not4Rohith Date: Wed, 22 Jul 2026 19:28:25 +0530 Subject: [PATCH 04/23] fixed unable to edit/delete my own posts --- .../src/components/community/ThreadDetail.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/frontend/src/components/community/ThreadDetail.tsx b/apps/frontend/src/components/community/ThreadDetail.tsx index c47a8151..40562cec 100644 --- a/apps/frontend/src/components/community/ThreadDetail.tsx +++ b/apps/frontend/src/components/community/ThreadDetail.tsx @@ -96,7 +96,7 @@ export default function ThreadDetail({ postId, onClose }: ThreadDetailProps) { const { user } = useAuth(); const gate = useAuthGate(); const navigate = useNavigate(); - const currentUserId = user?._id ?? ''; + const currentUserId = user?._id || user?.id || ''; const userRole = user?.role ?? ''; const [post, setPost] = useState(null); @@ -131,6 +131,9 @@ export default function ThreadDetail({ postId, onClose }: ThreadDetailProps) { const isPrivileged = userRole === 'admin' || userRole === 'moderator'; const topLevelComments = post?.comments ?? []; + const postAuthorId = typeof post?.author === 'string' ? post.author : post?.author?._id; + const isPostAuthor = currentUserId !== '' && currentUserId === postAuthorId; + /** Safe navigation: external URLs open in new tab, internal ones use SPA routing. */ const navigateTo = (url: string) => { if (url.startsWith('http') || url.startsWith('//')) { @@ -429,9 +432,10 @@ export default function ThreadDetail({ postId, onClose }: ThreadDetailProps) { /> {/* Author / privileged actions */} - {(currentUserId === post.author?._id || isPrivileged) && ( + {(isPostAuthor || isPrivileged) && ( <>
+ {isPrivileged && ( + )} {!isEditing && (
); -} \ No newline at end of file +} + +/* +Samagama:- https://samagama.in +Matrix Mystics:- https://sudarshansudarshan.github.io/codershigh/matrixmystics/ +Endorsement:- https://samagama.in/spa +Discourse:- https://vicharanashala.discourse.group/ +ViBe:- https://vibe.vicharanashala.ai/student +CSFAQ (Phase 1):- https://github.com/vicharanashala/crowd-source-faq +*/ From 3ff4c95bb9e930f7cf1c9e49db127f1e64c34772 Mon Sep 17 00:00:00 2001 From: Sharanagouda Patil Date: Thu, 23 Jul 2026 23:41:05 +0530 Subject: [PATCH 06/23] add the matrix mystics and their links --- .../src/pages/InternshipRoadmapPage.tsx | 387 +++++++++++++----- 1 file changed, 282 insertions(+), 105 deletions(-) diff --git a/apps/frontend/src/pages/InternshipRoadmapPage.tsx b/apps/frontend/src/pages/InternshipRoadmapPage.tsx index c9b2d91e..c7938ab1 100644 --- a/apps/frontend/src/pages/InternshipRoadmapPage.tsx +++ b/apps/frontend/src/pages/InternshipRoadmapPage.tsx @@ -18,30 +18,81 @@ const colors = { board: "#0E141C", }; -const boardingRequirements = [ +// Drop each portal's URL in here β€” leave "" until you have the link. +const portalLinks = { + start: "", + registration: "https://samagama.in", + standup: "", + vibe: "https://vibe.vicharanashala.ai/student/course-registration/6a14258a4fa5339bade5d733", + matrix: "https://sudarshansudarshan.github.io/codershigh/matrixmystics/", + matrixEndorsement: "https://samagama.in/spa", + phase1: "https://github.com/vicharanashala/crowd-source-faq", + phase2: "", + completion: "", +}; + +const registrationChecklist = [ "Log in to samagama.in", - "Upload and verify required documents β€” NOC, offer letter, participation agreement, honor code", - "Submit your Zoom ID and GitHub ID", - "Join the daily Zoom stand-up meeting", - "Attend polls during the stand-up session", + "Upload & verify: No Objection Certificate (NOC)", + "Upload & verify: Offer Letter", + "Upload & verify: Participation Agreement", + "Upload & verify: Honor Code", + "Submit your Zoom ID", + "Submit your GitHub ID", +]; + +const standupChecklist = [ + "Join the Zoom meeting using the daily link shared by email", + "Attend the session using a laptop", + "Keep your camera ON throughout the meeting", + "Participate in all polls during the session", +]; + +const vibeCourses = ["Onboarding", "Fundamentals of AI", "MERN Stack Development"]; + +const vibeWeeks = [ + { week: "Week 1", items: ["Complete Onboarding", "Complete 70% of Fundamentals of AI"] }, + { week: "Week 2", items: ["Complete 100% of Fundamentals of AI", "Complete 50% of MERN"] }, + { week: "Week 3", items: ["Complete 80% of MERN"] }, + { week: "Week 4", items: ["Complete 100% of all three courses"] }, +]; + +const phase1Steps = [ + "Form a team of 10 members.", + "Team Lead forks the official repository.", + "Add all team members as collaborators.", + "Clone the repository.", + "Discuss new feature ideas.", + "Develop the assigned feature.", + "Test the feature.", + "Push the code.", + "Create a Pull Request.", + "Submit the Pull Request link on Samagama.", ]; -const silverSteps = [ - "Work in a team of 10 members.", - "Choose one meaningful feature.", - "Complete development and testing.", - "Push your code to your Git branch.", - "Create a pull request (PR).", +const phase2Mentors = [ + { project: "PyBe", mentor: "Prakash Hegade" }, + { project: "FLN", mentor: "Pavani Ma'am" }, + { project: "Spandan", mentor: "Rohit Sir" }, + { project: "Tenali", mentor: "Jinal Ma'am" }, ]; -const spurProjects = ["PyBe", "Ajrasakha", "FLN", "ViBe", "Tenali", "Spandan", "Spurti"]; +const completionSteps = [ + "Attend daily mentor meetings.", + "Discuss features with your mentor.", + "Develop approved features.", + "Submit Pull Requests.", + "Once your Pull Requests are approved by the mentor, your internship is successfully completed.", +]; const overviewStops = [ { label: "Start", dot: colors.emerald }, - { label: "Gate", dot: colors.crimson, seg: colors.crimson }, - { label: "AI basics", dot: colors.amber, seg: colors.amber }, - { label: "MERN", dot: colors.amber, seg: colors.amber }, - { label: "Junction", dot: colors.slate, seg: colors.slate }, + { label: "Register", dot: colors.emerald, seg: colors.emerald }, + { label: "Stand-up", dot: colors.crimson, seg: colors.crimson }, + { label: "ViBe", dot: colors.amber, seg: colors.amber }, + { label: "Matrix", dot: colors.amber, seg: colors.amber }, + { label: "Phase 1", dot: colors.slate, seg: colors.slate }, + { label: "Phase 2", dot: colors.slate, seg: colors.slate }, { label: "Delivery", dot: "#8B94A0", seg: colors.slate }, ]; @@ -113,6 +164,38 @@ function Eyebrow({ color, children }: { color: string; children: ReactNode }) { ); } +function PortalLink({ href, color, label = "Portal link" }: { href?: string; color: string; label?: string }) { + if (!href) { + return ( +
+ + + + + {label} β€” paste URL here +
+ ); + } + return ( + + + + + + {label} β†— + + ); +} + export default function InternshipRoadmapPage() { return (
@@ -133,8 +216,8 @@ export default function InternshipRoadmapPage() { The Internship Express

- ORIGIN Onboarding  β†’  TERMINUS{" "} - Project Delivery + ORIGIN Registration  β†’  TERMINUS{" "} + Internship Completion

@@ -169,7 +252,7 @@ export default function InternshipRoadmapPage() {
Departure Β· Platform 1 -

Onboarding

+

+ Internship Journey Begins +

+

+ Welcome to the AI Internship. Complete the following steps in order to successfully + finish your internship. +

+ +
+
+ + {/* STOP 1 β€” Registration */} +
+ + + Stop 1 Β· Platform 1 +

+ Internship Registration +

Board the internship by completing these requirements first.

    - {boardingRequirements.map((item) => ( + {registrationChecklist.map((item) => (
  • ))}
+
- {/* GATE */} + {/* STOP 2 β€” Mandatory Daily Stand-up + Eligibility Gate */}
- Ticket check + Stop 2 Β· Ticket check

- Eligibility gate + Mandatory Daily Stand-up Sessions

-

- Fall short of any of these three and you may be held at the platform. +

    + {standupChecklist.map((item) => ( +
  • + + + + {item} +
  • + ))} +
+ +

+ Eligibility requirements

{[ @@ -258,73 +378,110 @@ export default function InternshipRoadmapPage() { - Students who don't meet these thresholds may be excluded from the internship. + Students who do not meet these requirements may be removed from the internship.

+
- {/* BRONZE 1 */} + {/* STOP 3 β€” ViBe Learning Platform */}
- Bronze line Β· Stop 1 -

Fundamentals of AI

-

- Core concepts before you touch a line of project code. + Bronze line Β· Stop 3 +

+ ViBe Learning Platform +

+

Courses on this line:

+
+ {vibeCourses.map((c) => ( + + {c} + + ))} +
+ +

+ Week-wise plan

- - - - - - Due: DOJ + 1 week - +
+ {vibeWeeks.map((w) => ( +
+ + {w.week} + +
    + {w.items.map((it) => ( +
  • + β€’ {it} +
  • + ))} +
+
+ ))} +
+
- {/* BRONZE 2 */} + {/* STOP 4 β€” Matrix Mystics */}
- Bronze line Β· Stop 2 -

MERN stack development

-

- Full-stack fundamentals, on a two-checkpoint schedule. + Bronze line Β· Stop 4 +

Matrix Mystics

+
+
+ 53 + Mathematics questions +
+
+ 2 Months + Time limit +
+
+

+ Endorsement rule +

+

+ Students who already solved a question will conduct a viva for another student. If the + student answers correctly, they receive an endorsement, and can then conduct vivas for + other students and endorse them β€” creating a peer-learning cycle.

- {[ - { pct: "50%", when: "DOJ + 2 weeks" }, - { pct: "100%", when: "DOJ + 3 weeks" }, - ].map((s) => ( -
- {s.pct} - {s.when} -
- ))} + +
- {/* SILVER JUNCTION */} -
- + {/* STOP 5 β€” Phase 1: Crowd Source FAQ */} +
+ - Silver line Β· Junction + Silver line Β· Stop 5

- Phase 1 β€” Crowd source FAQ + Phase 1 – Crowd Source FAQ Project

Interchange from learning to real project contribution.

    - {silverSteps.map((step, i) => ( + {phase1Steps.map((step, i) => (
  • Team of 10 Β· Shared branch + + +
- {/* Phase 2 spur */} -
-
-

- Phase 2 spur Β· additional project stations -

-
- {spurProjects.map((name) => ( -
- - + + + Silver line Β· Stop 6 +

+ Phase 2 – Mentor Projects +

+

+ After completing Phase 1, students move to Phase 2 and contribute to one of the + mentor-guided projects below. +

+
+ + + + + + + + + {phase2Mentors.map((row, i) => ( + + + + + ))} + +
- {name} - - - ))} - + Project + + Mentor +
{row.project}{row.mentor}
+
- {/* END */} + {/* FINAL STOP β€” Internship Completion */}
@@ -384,23 +559,25 @@ export default function InternshipRoadmapPage() {

Arrival Β· Final stop

-

Project delivery

-

- Internship complete. Thanks for riding the line β€” welcome to the platform. -

+

+ Internship Completion +

+
    + {completionSteps.map((step) => ( +
  • + + {step} +
  • + ))} +
+
); -} - -/* -Samagama:- https://samagama.in -Matrix Mystics:- https://sudarshansudarshan.github.io/codershigh/matrixmystics/ -Endorsement:- https://samagama.in/spa -Discourse:- https://vicharanashala.discourse.group/ -ViBe:- https://vibe.vicharanashala.ai/student -CSFAQ (Phase 1):- https://github.com/vicharanashala/crowd-source-faq -*/ +} \ No newline at end of file From 9f7cbc513ef2a4b01ebbce6415b1db0cbbd1973e Mon Sep 17 00:00:00 2001 From: Ashishpatra0-0 Date: Fri, 24 Jul 2026 11:23:29 +0530 Subject: [PATCH 07/23] Fix overlapping buttons/text in search feedback popup --- .../src/components/faq/SearchFeedback.tsx | 52 ++++++++++++------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/apps/frontend/src/components/faq/SearchFeedback.tsx b/apps/frontend/src/components/faq/SearchFeedback.tsx index 31f8e8b9..c21665d4 100644 --- a/apps/frontend/src/components/faq/SearchFeedback.tsx +++ b/apps/frontend/src/components/faq/SearchFeedback.tsx @@ -86,24 +86,40 @@ export default function SearchFeedback({ searchQuery, resultFaqId }: SearchFeedb
{phase === 'prompt' ? ( -
-

Did this answer your question?

- - -
+
+

+ Did this answer your question? +

+ +
+ + + +
+
) : (
From 175a75eb38b2149ffdf6c1ce8510c9bac1354a32 Mon Sep 17 00:00:00 2001 From: Sharanagouda Patil Date: Fri, 24 Jul 2026 14:39:02 +0530 Subject: [PATCH 08/23] add the all phase2 project and their mentors --- .../src/pages/InternshipRoadmapPage.tsx | 151 +++++++++++++----- 1 file changed, 109 insertions(+), 42 deletions(-) diff --git a/apps/frontend/src/pages/InternshipRoadmapPage.tsx b/apps/frontend/src/pages/InternshipRoadmapPage.tsx index c7938ab1..a5d5d003 100644 --- a/apps/frontend/src/pages/InternshipRoadmapPage.tsx +++ b/apps/frontend/src/pages/InternshipRoadmapPage.tsx @@ -20,15 +20,12 @@ const colors = { // Drop each portal's URL in here β€” leave "" until you have the link. const portalLinks = { - start: "", registration: "https://samagama.in", - standup: "", + discussion: "https://vicharanashala.discourse.group/", vibe: "https://vibe.vicharanashala.ai/student/course-registration/6a14258a4fa5339bade5d733", matrix: "https://sudarshansudarshan.github.io/codershigh/matrixmystics/", matrixEndorsement: "https://samagama.in/spa", phase1: "https://github.com/vicharanashala/crowd-source-faq", - phase2: "", - completion: "", }; const registrationChecklist = [ @@ -71,10 +68,12 @@ const phase1Steps = [ ]; const phase2Mentors = [ - { project: "PyBe", mentor: "Prakash Hegade" }, - { project: "FLN", mentor: "Pavani Ma'am" }, + { project: "ViBe", mentor: "Minakshi Madam" }, { project: "Spandan", mentor: "Rohit Sir" }, - { project: "Tenali", mentor: "Jinal Ma'am" }, + { project: "PyBe", mentor: "Prakash Sir" }, + { project: "Tenali", mentor: "Jinal Madam" }, + { project: "Spurti", mentor: "Sakshi Madam" }, + { project: "FLN", mentor: "Pavani Madam" }, ]; const completionSteps = [ @@ -96,6 +95,16 @@ const overviewStops = [ { label: "Delivery", dot: "#8B94A0", seg: colors.slate }, ]; +function initials(name: string) { + return name + .replace(/madam|sir/gi, "") + .trim() + .split(/\s+/) + .map((w) => w[0]) + .join("") + .toUpperCase(); +} + function CheckIcon({ color }: { color: string }) { return ( @@ -104,6 +113,32 @@ function CheckIcon({ color }: { color: string }) { ); } +function LinkIcon() { + return ( + + + + + ); +} + +function MailIcon() { + return ( + + + + + ); +} + +function ChatIcon() { + return ( + + + + ); +} + function Marker({ code, color, @@ -122,11 +157,11 @@ function Marker({ : {}; const bg = shape === "solid" ? colors.board : "#ffffff"; return ( -
- +
+ {code}
@@ -171,10 +206,7 @@ function PortalLink({ href, color, label = "Portal link" }: { href?: string; col className="mt-4 flex items-center gap-2 rounded-lg border border-dashed px-3.5 py-2.5 font-mono text-[12.5px] opacity-70" style={{ borderColor: color, color }} > - - - - + {label} β€” paste URL here
); @@ -187,10 +219,7 @@ function PortalLink({ href, color, label = "Portal link" }: { href?: string; col className="mt-4 inline-flex items-center gap-2 rounded-lg border px-3.5 py-2.5 font-mono text-[12.5px] transition-opacity hover:opacity-75" style={{ borderColor: color, color }} > - - - - + {label} β†— ); @@ -215,11 +244,22 @@ export default function InternshipRoadmapPage() {

The Internship Express

-

+

ORIGIN Registration  β†’  TERMINUS{" "} Internship Completion

+ + + Discussion Forum β†— + +
PROGRESS
@@ -258,8 +298,7 @@ export default function InternshipRoadmapPage() {
@@ -291,7 +330,7 @@ export default function InternshipRoadmapPage() { Welcome to the AI Internship. Complete the following steps in order to successfully finish your internship.

- +
@@ -371,16 +410,31 @@ export default function InternshipRoadmapPage() { ))}

- + Students who do not meet these requirements may be removed from the internship.

- + +
+ + Daily Zoom link is sent to your registered email every morning +
@@ -405,9 +459,7 @@ export default function InternshipRoadmapPage() { ))}
-

- Week-wise plan -

+

Week-wise plan

{vibeWeeks.map((w) => (
-

- Endorsement rule -

-

+

Endorsement rule

+

Students who already solved a question will conduct a viva for another student. If the student answers correctly, they receive an endorsement, and can then conduct vivas for other students and endorse them β€” creating a peer-learning cycle. @@ -535,9 +585,26 @@ export default function InternshipRoadmapPage() { {phase2Mentors.map((row, i) => ( - + {row.project} - {row.mentor} + +

+ + {initials(row.mentor)} + + {row.mentor} +
+ ))} @@ -562,18 +629,18 @@ export default function InternshipRoadmapPage() {

Internship Completion

-
    +
      {completionSteps.map((step) => ( -
    • +
    • {step}
    • ))}
    - +
    + + +
From 9c1fd232fdca09b4cf49be44272815a039d100dd Mon Sep 17 00:00:00 2001 From: Sharanagouda Patil Date: Fri, 24 Jul 2026 14:42:24 +0530 Subject: [PATCH 09/23] add all step links --- apps/frontend/src/pages/InternshipRoadmapPage.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/apps/frontend/src/pages/InternshipRoadmapPage.tsx b/apps/frontend/src/pages/InternshipRoadmapPage.tsx index a5d5d003..4fd6ffd4 100644 --- a/apps/frontend/src/pages/InternshipRoadmapPage.tsx +++ b/apps/frontend/src/pages/InternshipRoadmapPage.tsx @@ -610,7 +610,6 @@ export default function InternshipRoadmapPage() {
-
@@ -637,10 +636,7 @@ export default function InternshipRoadmapPage() { ))} -
- - -
+
From 6e193e7f76dd05dc9998a265a5988b88b3a95966 Mon Sep 17 00:00:00 2001 From: Sharanagouda Patil Date: Fri, 24 Jul 2026 15:03:54 +0530 Subject: [PATCH 10/23] styling of the page --- .../src/pages/InternshipRoadmapPage.tsx | 1073 ++++++++++------- 1 file changed, 639 insertions(+), 434 deletions(-) diff --git a/apps/frontend/src/pages/InternshipRoadmapPage.tsx b/apps/frontend/src/pages/InternshipRoadmapPage.tsx index 4fd6ffd4..4eede773 100644 --- a/apps/frontend/src/pages/InternshipRoadmapPage.tsx +++ b/apps/frontend/src/pages/InternshipRoadmapPage.tsx @@ -1,24 +1,25 @@ "use client"; -import type { ReactNode } from "react"; - +import React, { useEffect, useRef, useState, useCallback } from "react"; +import { + motion, + useScroll, + useSpring, + useTransform, + useMotionValueEvent, + AnimatePresence, +} from "framer-motion"; + +// --- Colors & Data Configuration --- const colors = { - emerald: "#1B8A5A", - emeraldSoft: "#DCF1E5", - emeraldDeep: "#0E5C3B", - crimson: "#C0392E", - crimsonSoft: "#FAE1DD", - crimsonDeep: "#7E241B", - amber: "#C9821E", - amberSoft: "#F7E7D0", - amberDeep: "#7A4C11", - slate: "#57657A", - slateSoft: "#E7EAEE", - slateDeep: "#33404F", - board: "#0E141C", + emerald: "#10B981", + crimson: "#EF4444", + amber: "#F59E0B", + cyan: "#06B6D4", + purple: "#A855F7", + board: "#0B132B", }; -// Drop each portal's URL in here β€” leave "" until you have the link. const portalLinks = { registration: "https://samagama.in", discussion: "https://vicharanashala.discourse.group/", @@ -39,32 +40,26 @@ const registrationChecklist = [ ]; const standupChecklist = [ - "Join the Zoom meeting using the daily link shared by email", - "Attend the session using a laptop", - "Keep your camera ON throughout the meeting", + "Join the Zoom meeting using daily email link", + "Attend session using laptop with camera ON", "Participate in all polls during the session", ]; const vibeCourses = ["Onboarding", "Fundamentals of AI", "MERN Stack Development"]; const vibeWeeks = [ - { week: "Week 1", items: ["Complete Onboarding", "Complete 70% of Fundamentals of AI"] }, - { week: "Week 2", items: ["Complete 100% of Fundamentals of AI", "Complete 50% of MERN"] }, - { week: "Week 3", items: ["Complete 80% of MERN"] }, - { week: "Week 4", items: ["Complete 100% of all three courses"] }, + { week: "Week 1", items: ["Onboarding", "70% Fundamentals of AI"] }, + { week: "Week 2", items: ["100% Fundamentals of AI", "50% MERN"] }, + { week: "Week 3", items: ["80% MERN"] }, + { week: "Week 4", items: ["100% All Courses"] }, ]; const phase1Steps = [ - "Form a team of 10 members.", - "Team Lead forks the official repository.", - "Add all team members as collaborators.", - "Clone the repository.", - "Discuss new feature ideas.", - "Develop the assigned feature.", - "Test the feature.", - "Push the code.", - "Create a Pull Request.", - "Submit the Pull Request link on Samagama.", + "Form a team of 10 members", + "Team Lead forks official repository", + "Add all team members as collaborators", + "Develop and test assigned feature", + "Submit Pull Request link on Samagama", ]; const phase2Mentors = [ @@ -77,22 +72,20 @@ const phase2Mentors = [ ]; const completionSteps = [ - "Attend daily mentor meetings.", - "Discuss features with your mentor.", - "Develop approved features.", - "Submit Pull Requests.", - "Once your Pull Requests are approved by the mentor, your internship is successfully completed.", + "Attend daily mentor meetings", + "Develop and present approved features", + "Submit Pull Requests for final sign-off", ]; const overviewStops = [ - { label: "Start", dot: colors.emerald }, - { label: "Register", dot: colors.emerald, seg: colors.emerald }, - { label: "Stand-up", dot: colors.crimson, seg: colors.crimson }, - { label: "ViBe", dot: colors.amber, seg: colors.amber }, - { label: "Matrix", dot: colors.amber, seg: colors.amber }, - { label: "Phase 1", dot: colors.slate, seg: colors.slate }, - { label: "Phase 2", dot: colors.slate, seg: colors.slate }, - { label: "Delivery", dot: "#8B94A0", seg: colors.slate }, + { label: "Start", dot: colors.emerald, id: "start" }, + { label: "Register", dot: colors.emerald, id: "stop-1" }, + { label: "Stand-up", dot: colors.crimson, id: "stop-2" }, + { label: "ViBe", dot: colors.amber, id: "stop-3" }, + { label: "Matrix", dot: colors.amber, id: "stop-4" }, + { label: "Phase 1", dot: colors.cyan, id: "stop-5" }, + { label: "Phase 2", dot: colors.cyan, id: "stop-6" }, + { label: "Finish", dot: colors.purple, id: "stop-end" }, ]; function initials(name: string) { @@ -105,9 +98,10 @@ function initials(name: string) { .toUpperCase(); } -function CheckIcon({ color }: { color: string }) { +// --- Icons --- +function CheckIcon({ color = "currentColor" }: { color?: string }) { return ( - + ); @@ -115,99 +109,263 @@ function CheckIcon({ color }: { color: string }) { function LinkIcon() { return ( - + ); } -function MailIcon() { - return ( - - - - - ); +// --- Canvas Ocean Wave Background --- +function OceanBackground() { + const canvasRef = useRef(null); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + let animId: number; + let width = (canvas.width = window.innerWidth); + let height = (canvas.height = window.innerHeight); + + const handleResize = () => { + if (!canvas) return; + width = canvas.width = window.innerWidth; + height = canvas.height = window.innerHeight; + }; + window.addEventListener("resize", handleResize); + + const particles = Array.from({ length: 35 }).map(() => ({ + x: Math.random() * width, + y: Math.random() * height, + radius: Math.random() * 2.5 + 1, + alpha: Math.random() * 0.5 + 0.1, + speedY: Math.random() * 0.3 + 0.1, + speedX: (Math.random() - 0.5) * 0.2, + })); + + let step = 0; + + const render = () => { + step += 0.006; + ctx.clearRect(0, 0, width, height); + + const grad = ctx.createLinearGradient(0, 0, 0, height); + grad.addColorStop(0, "#020617"); + grad.addColorStop(0.5, "#07172F"); + grad.addColorStop(1, "#0B2545"); + ctx.fillStyle = grad; + ctx.fillRect(0, 0, width, height); + + const waves = [ + { color: "rgba(14, 165, 233, 0.07)", speed: 1, heightOffset: 0.2, length: 0.004, amp: 45 }, + { color: "rgba(6, 182, 212, 0.1)", speed: 1.4, heightOffset: 0.55, length: 0.007, amp: 30 }, + { color: "rgba(59, 130, 246, 0.05)", speed: 0.8, heightOffset: 0.85, length: 0.003, amp: 55 }, + ]; + + waves.forEach((w) => { + ctx.beginPath(); + ctx.fillStyle = w.color; + const waveY = height * w.heightOffset; + ctx.moveTo(0, height); + ctx.lineTo(0, waveY); + + for (let x = 0; x <= width; x += 12) { + const y = + Math.sin(x * w.length + step * w.speed) * w.amp + + Math.cos(x * 0.002 + step * 0.4) * (w.amp * 0.4) + + waveY; + ctx.lineTo(x, y); + } + + ctx.lineTo(width, height); + ctx.closePath(); + ctx.fill(); + }); + + particles.forEach((p) => { + p.y -= p.speedY; + p.x += p.speedX; + if (p.y < 0) p.y = height; + if (p.x < 0) p.x = width; + if (p.x > width) p.x = 0; + + ctx.beginPath(); + ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2); + ctx.fillStyle = `rgba(56, 189, 248, ${p.alpha})`; + ctx.fill(); + }); + + animId = requestAnimationFrame(render); + }; + + render(); + return () => { + window.removeEventListener("resize", handleResize); + cancelAnimationFrame(animId); + }; + }, []); + + return ; } -function ChatIcon() { - return ( - - - - ); -} - -function Marker({ - code, - color, - shape = "circle", +// --- Interactive Engine Unit --- +function ExpressTrainEngine({ + isMoving, + headlightOn, + onWhistle, + whistleActive, }: { - code: string; - color: string; - shape?: "circle" | "hexagon" | "solid"; + isMoving: boolean; + headlightOn: boolean; + onWhistle: () => void; + whistleActive: boolean; }) { - const base = - "absolute -left-[70px] top-0.5 w-[58px] h-[58px] flex items-center justify-center bg-white border-[3px] shadow-md z-10"; - const shapeClass = shape === "hexagon" ? "rounded-none" : "rounded-full"; - const clip = - shape === "hexagon" - ? { clipPath: "polygon(50% 0%, 95% 25%, 95% 75%, 50% 100%, 5% 75%, 5% 25%)" } - : {}; - const bg = shape === "solid" ? colors.board : "#ffffff"; return ( -
- - {code} - +
+ {/* Whistle Steam Plume Effect */} + + {whistleActive && ( + + + πŸ’¨ CHOON! CHOON! + + + )} + + + {/* Dynamic Motion Smoke Puff */} +
+ {isMoving && ( + <> + + + + )} +
+ + {/* Train Engine Vector Body */} + + + + {/* Headlight Beam Cone */} + {headlightOn && ( + + )} + + {/* Engine Shell */} + + + + + {/* Engine Bonnet */} + + + + + {/* Steam Pipe Chimney */} + + + + {/* Headlight Bulb */} + + + + {/* Wheels / Pistons */} + + + + + + + + + + + +
); } -function Card({ +// --- Glassmorphism Milestone Card --- +function GlassCard({ accent, - dark, children, className = "", + glow = false, }: { accent?: string; - dark?: boolean; - children: ReactNode; + children: React.ReactNode; className?: string; + glow?: boolean; }) { return ( -
- {children} -
+
+
{children}
+ ); } -function Eyebrow({ color, children }: { color: string; children: ReactNode }) { +// --- Eyebrow Tag --- +function Eyebrow({ color, children }: { color: string; children: React.ReactNode }) { return ( -

+

+ {children}

); } -function PortalLink({ href, color, label = "Portal link" }: { href?: string; color: string; label?: string }) { +// --- Interactive Link Button --- +function PortalLink({ href, color, label = "Portal Link" }: { href?: string; color: string; label?: string }) { if (!href) { return ( -
+
- {label} β€” paste URL here + {label} β€” Pending Link
); } @@ -216,8 +374,8 @@ function PortalLink({ href, color, label = "Portal link" }: { href?: string; col href={href} target="_blank" rel="noopener noreferrer" - className="mt-4 inline-flex items-center gap-2 rounded-lg border px-3.5 py-2.5 font-mono text-[12.5px] transition-opacity hover:opacity-75" - style={{ borderColor: color, color }} + className="mt-4 inline-flex items-center gap-2 rounded-xl border border-white/20 bg-white/10 px-4 py-2.5 font-mono text-xs font-semibold text-white transition-all hover:bg-white/20 hover:scale-[1.03] active:scale-[0.97]" + style={{ boxShadow: `0 4px 14px ${color}33` }} > {label} β†— @@ -225,420 +383,467 @@ function PortalLink({ href, color, label = "Portal link" }: { href?: string; col ); } +// --- Main Interactive Roadmap --- export default function InternshipRoadmapPage() { + const containerRef = useRef(null); + const trackRef = useRef(null); + + const [trainY, setTrainY] = useState(0); + const [isMoving, setIsMoving] = useState(false); + const [headlightOn, setHeadlightOn] = useState(true); + const [whistleActive, setWhistleActive] = useState(false); + const [activeStop, setActiveStop] = useState("start"); + + const scrollTimeoutRef = useRef(null); + + // Native Scroll bindings + const { scrollYProgress } = useScroll({ + target: containerRef, + offset: ["start start", "end end"], + }); + + const smoothProgress = useSpring(scrollYProgress, { + stiffness: 140, + damping: 26, + restDelta: 0.001, + }); + + // Calculate position along railway line + useEffect(() => { + const updatePosition = () => { + if (!trackRef.current) return; + const trackHeight = trackRef.current.getBoundingClientRect().height; + const progress = smoothProgress.get(); + const newY = Math.max(0, Math.min(trackHeight - 80, progress * trackHeight)); + setTrainY(newY); + + const totalStops = overviewStops.length; + const idx = Math.min(totalStops - 1, Math.floor(progress * totalStops)); + setActiveStop(overviewStops[idx].id); + }; + + const unsub = smoothProgress.on("change", () => { + updatePosition(); + setIsMoving(true); + + if (scrollTimeoutRef.current) clearTimeout(scrollTimeoutRef.current); + scrollTimeoutRef.current = setTimeout(() => setIsMoving(false), 160); + }); + + window.addEventListener("resize", updatePosition); + updatePosition(); + + return () => { + unsub(); + window.removeEventListener("resize", updatePosition); + if (scrollTimeoutRef.current) clearTimeout(scrollTimeoutRef.current); + }; + }, [smoothProgress]); + + // Audio / Visual Whistle Trigger + const triggerWhistle = useCallback(() => { + setWhistleActive(true); + setTimeout(() => setWhistleActive(false), 800); + }, []); + + // Keyboard Navigation Controls (Drive train with Arrow Keys or 'W' Whistle) + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "ArrowDown") { + window.scrollBy({ top: 120, behavior: "smooth" }); + } else if (e.key === "ArrowUp") { + window.scrollBy({ top: -120, behavior: "smooth" }); + } else if (e.key.toLowerCase() === "w") { + triggerWhistle(); + } else if (e.key.toLowerCase() === "h") { + setHeadlightOn((prev) => !prev); + } + }; + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [triggerWhistle]); + + const progressPercent = useTransform(smoothProgress, [0, 1], [0, 100]); + const [progressText, setProgressText] = useState("0%"); + + useMotionValueEvent(progressPercent, "change", (v) => { + setProgressText(`${Math.round(v)}%`); + }); + + const scrollToSection = (id: string) => { + const el = document.getElementById(id); + if (el) { + el.scrollIntoView({ behavior: "smooth" }); + } + }; + return ( -
-
- {/* Departure board header */} -
-
- - LINE 01 Β· AI INTERNSHIP +
+ + + {/* Floating HUD Controller Deck */} +
+
+ + +
+ + +
+
+ + {/* Main Roadmap Wrapper */} +
+ + {/* Departure Board Header */} + +
+ + + LINE 01 Β· AI INTERNSHIP EXPRESS - DEPARTURES + KEYBOARD DRIVE ENABLED
-

+

The Internship Express

-

- ORIGIN Registration  β†’  TERMINUS{" "} - Internship Completion +

+ ORIGIN: Registration  β†’  TERMINUS:{" "} + Internship Completion

- - - Discussion Forum β†— - - -
- PROGRESS -
-
+ + + {/* Progress Bar */} +
+ PROGRESS +
+
- 0% + {progressText}
- {/* mini overview line map */} -
- {overviewStops.map((s, i) => ( -
- {i > 0 && ( -
- )} -
- {s.label} -
- ))} + {/* Mini Interactive Station Line Map */} +
+ {overviewStops.map((stop, i) => { + const isActive = activeStop === stop.id; + return ( + + ); + })}
-
+ + + {/* Railway Journey Grid */} +
+ + {/* Railway Tracks */} +
+
+
+
+
+
- {/* Journey */} -
- {/* rail */} -
-
-
+ + {/* Interactive Dynamic Train Unit */} +
+
- {/* START */} -
-
- - - - - - You are here - -
- - - Departure Β· Platform 1 -

- Internship Journey Begins -

-

- Welcome to the AI Internship. Complete the following steps in order to successfully - finish your internship. + {/* START MILESTONE */} +

+ + Platform 01 Β· Departure +

Internship Journey Begins

+

+ Welcome to the AI Internship. Drive along the tracks to navigate through mandatory setup, daily stand-up requirements, and team projects.

- +
- {/* STOP 1 β€” Registration */} -
- - - Stop 1 Β· Platform 1 -

- Internship Registration -

-

- Board the internship by completing these requirements first. + {/* STOP 1: Registration */} +

+ + Stop 1 Β· Boarding Pass +

Internship Registration

+

+ Complete all mandatory document uploads and profile verification steps.

-
    +
      {registrationChecklist.map((item) => ( -
    • - - +
    • + + {item}
    • ))}
    - - + +
- {/* STOP 2 β€” Mandatory Daily Stand-up + Eligibility Gate */} -
- - - Stop 2 Β· Ticket check -

- Mandatory Daily Stand-up Sessions -

-
    - {standupChecklist.map((item) => ( -
  • - - - - {item} -
  • - ))} -
- -

- Eligibility requirements + {/* STOP 2: Mandatory Stand-ups */} +

+ + Stop 2 Β· Ticket Inspection +

Mandatory Daily Stand-up Sessions

+

+ Active engagement in daily sessions is necessary to remain eligible for project allocation.

-
+ +
{[ - { value: "85%", label: "Stand-up attendance" }, - { value: "85%", label: "Poll participation" }, - { value: "50%", label: "Poll accuracy" }, + { value: "85%", label: "Stand-up Attendance" }, + { value: "85%", label: "Poll Participation" }, + { value: "50%", label: "Poll Accuracy" }, ].map((m) => ( -
- - {m.value} - - {m.label} +
+ {m.value} + {m.label}
))}
-

- - - - - Students who do not meet these requirements may be removed from the internship. -

-
- - Daily Zoom link is sent to your registered email every morning -
- +
    + {standupChecklist.map((item) => ( +
  • + + {item} +
  • + ))} +
+
- {/* STOP 3 β€” ViBe Learning Platform */} -
- - - Bronze line Β· Stop 3 -

- ViBe Learning Platform -

-

Courses on this line:

-
+ {/* STOP 3: ViBe Learning Platform */} +
+ + Stop 3 Β· Knowledge Hub +

ViBe Learning Platform

+

+ Complete coursework according to the week-wise plan below: +

+ +
{vibeCourses.map((c) => ( - + {c} ))}
-

Week-wise plan

-
+
{vibeWeeks.map((w) => ( -
- - {w.week} - -
    +
    + {w.week} +
      {w.items.map((it) => ( -
    • - β€’ {it} +
    • + β–Έ {it}
    • ))}
    ))}
- - + + +
- {/* STOP 4 β€” Matrix Mystics */} -
- - - Bronze line Β· Stop 4 -

Matrix Mystics

-
-
- 53 - Mathematics questions + {/* STOP 4: Matrix Mystics */} +
+ + Stop 4 Β· Peer Assessment +

Matrix Mystics

+ +
+
+ 53 + Math Problems
-
- 2 Months - Time limit +
+ 2 Months + Time Limit
-

Endorsement rule

-

- Students who already solved a question will conduct a viva for another student. If the - student answers correctly, they receive an endorsement, and can then conduct vivas for - other students and endorse them β€” creating a peer-learning cycle. -

+
- - + +
- +
- {/* STOP 5 β€” Phase 1: Crowd Source FAQ */} -
- - - Silver line Β· Stop 5 -

- Phase 1 – Crowd Source FAQ Project -

-

- Interchange from learning to real project contribution. + {/* STOP 5: Phase 1 */} +

+ + Stop 5 Β· Engineering Sprint +

Phase 1 – Crowd Source FAQ

+

+ Form teams of 10 and submit open-source contributions.

-
    - {phase1Steps.map((step, i) => ( -
  • - - {i + 1} + +
    + {phase1Steps.map((step, idx) => ( +
    + + {idx + 1} {step} -
  • +
))} - - - Team of 10 Β· Shared branch - - -
+
+ + +
- {/* STOP 6 β€” Phase 2: Mentor Projects */} -
- - - Silver line Β· Stop 6 -

- Phase 2 – Mentor Projects -

-

- After completing Phase 1, students move to Phase 2 and contribute to one of the - mentor-guided projects below. -

-
- - - - - + {/* STOP 6: Phase 2 Mentor Projects */} +
+ + Stop 6 Β· Industry Mentorship +

Phase 2 – Mentor Projects

+ +
+
- Project - - Mentor -
+ + + + - - {phase2Mentors.map((row, i) => ( - - - + {phase2Mentors.map((row) => ( + + + ))}
ProjectMentor
{row.project} -
- - {initials(row.mentor)} - - {row.mentor} -
+
{row.project} + + {initials(row.mentor)} + + {row.mentor}
-
+
- {/* FINAL STOP β€” Internship Completion */} -
- - -
- {Array.from({ length: 12 }).map((_, i) => ( - - ))} -
-

- Arrival Β· Final stop + {/* FINAL STOP */} +

+ + Final Destination Β· Completion +

Internship Completion

+

+ Submit PRs approved by your mentor to successfully graduate.

-

- Internship Completion -

-
    + +
      {completionSteps.map((step) => ( -
    • - +
    • + {step}
    • ))}
    - - +
+
From ec11c5bdc931c6a12e36ea40ae49cd28beb2e3c4 Mon Sep 17 00:00:00 2001 From: Ayushi Date: Sat, 25 Jul 2026 00:13:43 +0530 Subject: [PATCH 11/23] Add promote to FAQ feature for Admins --- .../src/modules/admin/admin.controller.ts | 103 +++++++++++++++++- .../backend/src/modules/admin/admin.routes.ts | 7 +- .../src/admin/pages/AdminCommunity.tsx | 37 ++++++- 3 files changed, 144 insertions(+), 3 deletions(-) diff --git a/apps/backend/src/modules/admin/admin.controller.ts b/apps/backend/src/modules/admin/admin.controller.ts index 81e6006d..eb45206c 100644 --- a/apps/backend/src/modules/admin/admin.controller.ts +++ b/apps/backend/src/modules/admin/admin.controller.ts @@ -678,6 +678,107 @@ export const getCommunityPosts = async (req: Request, res: Response): Promise => { + try { + const post = await CommunityPost.findById(req.params.id); + if (!post) { + res.status(404).json({ message: 'Post not found.' }); + return; + } + // Per-program scope check β€” same pattern as deleteCommunityPost above. + if (post.batchId && req.programContext?.batchId && + post.batchId.toString() !== req.programContext.batchId) { + res.status(404).json({ message: 'Post not found.' }); + return; + } + + const answer = (post.answer ?? '').trim(); + if (!answer) { + res.status(400).json({ message: 'This post has no answer yet β€” nothing to promote.' }); + return; + } + + // Idempotent: don't create a duplicate FAQ if this post was already promoted. + const existing = await FAQ.findOne({ sourceCommunityPostId: post._id }); + if (existing) { + res.status(409).json({ message: 'This post has already been promoted to an FAQ.', faq: existing }); + return; + } + + // Best-effort embedding β€” never block the promotion on it. + let embedding: number[] | undefined; + try { + const text = `Question: ${post.title}. Answer: ${answer}`; + const timeout = new Promise((_, reject) => + setTimeout(() => reject(new Error('generateEmbedding timeout (5s)')), 5000) + ); + embedding = (await Promise.race([generateEmbedding(text), timeout])) as any; + } catch (embErr) { + console.warn('[admin] promoteCommunityPostToFAQ: embedding skipped:', (embErr as Error).message); + } + + const now = new Date(); + const faq = await FAQ.create({ + question: sanitizeHtml(post.title), + answer: sanitizeHtml(answer), + category: 'Community', + tags: post.tags ?? [], + status: 'approved', + embedding, + batchId: post.batchId ?? null, + createdBy: post.author, + trustLevel: 'expert', // straight to "Admin Approved" β€” an admin made the call directly + sourceType: 'community_promotion', + sourceCommunityPostId: post._id, + promotedAt: now, + objectionStatus: 'none', + promotionMetadata: { + upvotesAtPromotion: post.upvotes?.length ?? 0, + communityAnswerAuthorId: post.answerAuthorId ?? null, + promotedBy: req.user!._id, + }, + }); + + // Fast-forward the post's lifecycle straight to converted_to_faq β€” + // skip the community_accepted / ai_validated / admin_accepted stages + // since none of those gates applied here. + post.lifecycle ??= { status: 'open', statusHistory: [] } as any; + const fromStatus = post.lifecycle.status ?? 'open'; + post.lifecycle.statusHistory ??= []; + (post.lifecycle.statusHistory as any).push({ + from: fromStatus, + to: 'converted_to_faq', + changedBy: req.user!._id, + changedAt: now, + note: 'Promoted directly to FAQ by admin (discretionary, no eligibility checks)', + }); + post.lifecycle.status = 'converted_to_faq'; + post.lifecycle.convertedToFaqAt = now; + await post.save(); + + await Promise.all([ + invalidateCache().catch((e) => console.error('[admin] invalidateCache after promote:', e)), + ]); + invalidatePublicCaches(); + + await logAction(req.user!._id.toString(), 'promote_community_post_to_faq', faq._id.toString(), 'faq', faq.question); + + res.json({ message: 'Post promoted to FAQ.', faq }); + } catch (error) { + res.status(500).json({ message: 'Server error', error: (error as Error).message }); + } +}; + // DELETE /api/admin/community/:id export const deleteCommunityPost = async (req: Request, res: Response): Promise => { try { @@ -703,4 +804,4 @@ export const deleteCommunityPost = async (req: Request, res: Response): Promise< } catch (error) { res.status(500).json({ message: 'Server error', /* error: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined */ }); } -}; +}; \ No newline at end of file diff --git a/apps/backend/src/modules/admin/admin.routes.ts b/apps/backend/src/modules/admin/admin.routes.ts index 2e7f60bc..2616fc86 100644 --- a/apps/backend/src/modules/admin/admin.routes.ts +++ b/apps/backend/src/modules/admin/admin.routes.ts @@ -17,6 +17,7 @@ import { getUserActivityChart, getCommunityPosts, deleteCommunityPost, + promoteCommunityPostToFAQ, } from './admin.controller.js'; import { getCommunityPendingFAQs, @@ -175,6 +176,10 @@ router.patch('/faq/:id', updateFAQ); router.patch('/faqs/:id', updateFAQ); router.delete('/faq/:id', deleteFAQ); router.delete('/community/:id', deleteCommunityPost); +// Admin-discretion: instantly promote a community post's Q&A to FAQ, +// no eligibility checks (contrast with /community-promotions/* below, +// which is the gated auto-pipeline). +router.post('/community/:id/promote-to-faq', promoteCommunityPostToFAQ); // FAQ promotion management (trust levels) β€” from promotionService router.get('/faqs/community-pending', getCommunityPendingFAQs); @@ -190,4 +195,4 @@ router.get('/community-promotions/queue', getPromotionQueue); router.get('/queue/stats', getQueueStats); router.get('/queue/jobs/:id', getQueueJob); -export default router; +export default router; \ No newline at end of file diff --git a/apps/frontend/src/admin/pages/AdminCommunity.tsx b/apps/frontend/src/admin/pages/AdminCommunity.tsx index 40dcbf1c..0bd5b72c 100644 --- a/apps/frontend/src/admin/pages/AdminCommunity.tsx +++ b/apps/frontend/src/admin/pages/AdminCommunity.tsx @@ -47,6 +47,7 @@ export default function AdminCommunity() { const [categoryFilter, setCategoryFilter] = useState(''); const [toast, setToast] = useState(null); const [viewPost, setViewPost] = useState(null); + const [promoting, setPromoting] = useState(null); const debouncedSearch = useDebounce(search, 350); const showToast = (msg: string, type: Toast['type'] = 'success') => { setToast({ msg, type }); setTimeout(() => setToast(null), 3000); }; @@ -67,6 +68,21 @@ export default function AdminCommunity() { useEffect(() => { fetchPosts(); }, [fetchPosts]); useEffect(() => { setPage(1); }, [debouncedSearch, statusFilter, categoryFilter]); const handleDelete = async (id: string) => { if (!confirm('Delete this post?')) return; try { await adminApi.delete(`/admin/community/${id}`); showToast('Deleted', 'error'); fetchPosts(); } catch { showToast('Delete failed', 'error'); } }; + const handlePromote = async (post: CommunityPost) => { + if (!post.answer?.trim()) { showToast('This post has no answer yet', 'warn'); return; } + if (!confirm(`Promote "${post.title}" to FAQ now? This publishes it immediately β€” no review needed.`)) return; + setPromoting(post._id); + try { + await adminApi.post(`/admin/community/${post._id}/promote-to-faq`); + showToast('Promoted to FAQ', 'success'); + setViewPost(null); + fetchPosts(); + } catch (err: any) { + showToast(err?.response?.data?.message || 'Promote failed', 'error'); + } finally { + setPromoting(null); + } + }; return (
@@ -195,6 +211,16 @@ export default function AdminCommunity() { e.stopPropagation()}>
+ {post.answer?.trim() && ( + + )}
@@ -259,6 +285,15 @@ export default function AdminCommunity() { {new Date(viewPost.createdAt).toLocaleString('en-IN')}
+ {viewPost.answer?.trim() && ( + + )}
@@ -267,4 +302,4 @@ export default function AdminCommunity() {
); -} +} \ No newline at end of file From 159a3cef7bd316562b5b5599a55d9a6c8605643e Mon Sep 17 00:00:00 2001 From: CHANDANA V Date: Sat, 25 Jul 2026 23:33:12 +0530 Subject: [PATCH 12/23] Enhance community search bar with real-time filtering and clear button --- apps/frontend/src/pages/CommunityPage.tsx | 74 ++++++++++++++--------- 1 file changed, 46 insertions(+), 28 deletions(-) diff --git a/apps/frontend/src/pages/CommunityPage.tsx b/apps/frontend/src/pages/CommunityPage.tsx index eaf00ecb..227808d9 100644 --- a/apps/frontend/src/pages/CommunityPage.tsx +++ b/apps/frontend/src/pages/CommunityPage.tsx @@ -254,10 +254,21 @@ export default function CommunityPage() { fetchPosts(true); }; - const visible = (() => { - if (search.trim()) return searchResults; +const visible = (() => { + let list = posts; - return [...posts].sort((a, b) => { + if (search.trim()) { + const q = search.toLowerCase().trim(); + const localFiltered = posts.filter( + (p) => + p.title?.toLowerCase().includes(q) || + p.content?.toLowerCase().includes(q) || + p.author?.name?.toLowerCase().includes(q) + ); + list = searchResults.length > 0 ? searchResults : localFiltered; + } + + return [...list].sort((a, b) => { if (sort === 'newest') return new Date(b.createdAt ?? 0).getTime() - new Date(a.createdAt ?? 0).getTime(); if (sort === 'oldest') return new Date(a.createdAt ?? 0).getTime() - new Date(b.createdAt ?? 0).getTime(); if (sort === 'popular') return ((b.upvotes?.length ?? 0)) - ((a.upvotes?.length ?? 0)); @@ -349,31 +360,38 @@ export default function CommunityPage() { - {!loading && total > 0 && ( -
- - - - - setSearch(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter' && !e.shiftKey && search.trim().length >= 3) { - runSemanticSearch(search.trim()); - } - }} - placeholder="Search questions…" - className="w-full pl-9 pr-4 py-2.5 rounded-xl border border-border bg-card text-sm text-ink placeholder-ink-faint focus:outline-none focus:ring-2 focus:ring-accent/25 transition-all" - /> - {searchLoading && ( -
-
-
- )} -
- )} +
+ + + + + setSearch(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter' && !e.shiftKey && search.trim().length >= 3) { + runSemanticSearch(search.trim()); + } + }} + placeholder="Search community discussions..." + className="w-full pl-9 pr-10 py-2.5 rounded-xl border border-border bg-card text-sm text-ink placeholder-ink-faint focus:outline-none focus:ring-2 focus:ring-accent/25 transition-all" + /> + {search.trim() && ( + + )} + {searchLoading && ( +
+
+
+ )} +
{!loading && !error && (
From 88709b4593c3fc22e3be0463b61c7837f7773737 Mon Sep 17 00:00:00 2001 From: CHANDANA V Date: Sat, 25 Jul 2026 23:57:18 +0530 Subject: [PATCH 13/23] Add popular search tags, result count banner, and custom empty search state --- apps/frontend/src/pages/CommunityPage.tsx | 118 ++++++++++++++++------ 1 file changed, 85 insertions(+), 33 deletions(-) diff --git a/apps/frontend/src/pages/CommunityPage.tsx b/apps/frontend/src/pages/CommunityPage.tsx index 227808d9..45f7e638 100644 --- a/apps/frontend/src/pages/CommunityPage.tsx +++ b/apps/frontend/src/pages/CommunityPage.tsx @@ -16,6 +16,7 @@ import CreatePostDialog from '../components/community/CreatePostDialog'; import { buttonCommunityAsk } from '../styles/style_config'; // ─── Main Page ──────────────────────────────────────────────────────────────── +const POPULAR_TAGS = ['Assignment', 'Deadline', 'Submission', 'React', 'Vite']; export default function CommunityPage() { const { user } = useAuth(); // Active program from ProgramContext β€” used to scope the community feed @@ -44,6 +45,7 @@ export default function CommunityPage() { const [sort, setSort] = useState('newest'); const [showAllPrograms, setShowAllPrograms] = useState(false); const [search, setSearch] = useState(() => { + const POPULAR_TAGS = ['Assignment', 'Deadline', 'Submission', 'React', 'Vite']; const params = new URLSearchParams(window.location.search); return params.get('search') || ''; }); @@ -360,35 +362,67 @@ const visible = (() => { -
- - - - - setSearch(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter' && !e.shiftKey && search.trim().length >= 3) { - runSemanticSearch(search.trim()); - } - }} - placeholder="Search community discussions..." - className="w-full pl-9 pr-10 py-2.5 rounded-xl border border-border bg-card text-sm text-ink placeholder-ink-faint focus:outline-none focus:ring-2 focus:ring-accent/25 transition-all" - /> + {/* Search Bar, Quick Tags, and Results Counter */} +
+
+ + + + + setSearch(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter' && !e.shiftKey && search.trim().length >= 3) { + runSemanticSearch(search.trim()); + } + }} + placeholder="Search community discussions..." + className="w-full pl-9 pr-10 py-2.5 rounded-xl border border-border bg-card text-sm text-ink placeholder-ink-faint focus:outline-none focus:ring-2 focus:ring-accent/25 transition-all" + /> + {search.trim() && ( + + )} + {searchLoading && ( +
+
+
+ )} +
+ + {/* Quick Clickable Search Tags */} +
+ Popular searches: + {POPULAR_TAGS.map((tag) => ( + + ))} +
+ + {/* Search Results Counter & Clear Link */} {search.trim() && ( - - )} - {searchLoading && ( -
-
+
+ + Showing results for "{search.trim()}" ({displayedPosts.length} found) + +
)}
@@ -474,11 +508,29 @@ const visible = (() => {
)} - {!loading && !searchLoading && !error && total > 0 && displayedPosts.length === 0 && ( -

- No posts match your current filters. -

- )} + {!loading && !searchLoading && !error && displayedPosts.length === 0 && ( +
+
+ πŸ” +
+

+ {search.trim() ? `No results found for "${search.trim()}"` : 'No posts match your current filters'} +

+

+ {search.trim() + ? 'Try checking for spelling errors, using different keywords, or clear the search query.' + : 'Try switching your status filter or program feed.'} +

+ {search.trim() && ( + + )} +
+)} {!loading && !searchLoading && !error && displayedPosts.length > 0 && (
From 07203a8852a4b7727e420b6e6cae0eba45d6cad2 Mon Sep 17 00:00:00 2001 From: CHANDANA V Date: Sun, 26 Jul 2026 00:12:02 +0530 Subject: [PATCH 14/23] Add keyboard shortcut '/', recent search history, and enhanced UI badges --- apps/frontend/src/pages/CommunityPage.tsx | 99 ++++++++++++++++++++++- 1 file changed, 96 insertions(+), 3 deletions(-) diff --git a/apps/frontend/src/pages/CommunityPage.tsx b/apps/frontend/src/pages/CommunityPage.tsx index 45f7e638..8491720c 100644 --- a/apps/frontend/src/pages/CommunityPage.tsx +++ b/apps/frontend/src/pages/CommunityPage.tsx @@ -45,6 +45,40 @@ export default function CommunityPage() { const [sort, setSort] = useState('newest'); const [showAllPrograms, setShowAllPrograms] = useState(false); const [search, setSearch] = useState(() => { + // ── Keyboard Shortcut & Recent Searches ────────────────────────────────── + const searchInputRef = useRef(null); + const [recentSearches, setRecentSearches] = useState(() => { + try { + return JSON.parse(localStorage.getItem('csfaq_recent_searches') || '[]'); + } catch { + return []; + } + }); + + const saveToRecent = (term: string) => { + const q = term.trim(); + if (!q || q.length < 2) return; + setRecentSearches((prev) => { + const updated = [q, ...prev.filter((item) => item.toLowerCase() !== q.toLowerCase())].slice(0, 4); + localStorage.setItem('csfaq_recent_searches', JSON.stringify(updated)); + return updated; + }); + }; + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if ( + (e.key === '/' || ((e.metaKey || e.ctrlKey) && e.key === 'k')) && + document.activeElement?.tagName !== 'INPUT' && + document.activeElement?.tagName !== 'TEXTAREA' + ) { + e.preventDefault(); + searchInputRef.current?.focus(); + } + }; + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, []); const POPULAR_TAGS = ['Assignment', 'Deadline', 'Submission', 'React', 'Vite']; const params = new URLSearchParams(window.location.search); return params.get('search') || ''; @@ -363,6 +397,7 @@ const visible = (() => { {/* Search Bar, Quick Tags, and Results Counter */} + {/* Search Bar, Quick Tags, Recent Searches, and Results Counter */}
@@ -370,17 +405,26 @@ const visible = (() => { setSearch(e.target.value)} onKeyDown={(e) => { - if (e.key === 'Enter' && !e.shiftKey && search.trim().length >= 3) { - runSemanticSearch(search.trim()); + if (e.key === 'Enter' && search.trim()) { + saveToRecent(search.trim()); + if (!e.shiftKey && search.trim().length >= 3) { + runSemanticSearch(search.trim()); + } } }} placeholder="Search community discussions..." - className="w-full pl-9 pr-10 py-2.5 rounded-xl border border-border bg-card text-sm text-ink placeholder-ink-faint focus:outline-none focus:ring-2 focus:ring-accent/25 transition-all" + className="w-full pl-9 pr-16 py-2.5 rounded-xl border border-border bg-card text-sm text-ink placeholder-ink-faint focus:outline-none focus:ring-2 focus:ring-accent/25 transition-all" /> + {!search && ( + + / + + )} {search.trim() && (
+ {/* Quick Clickable Search Tags */} +
+ Popular searches: + {POPULAR_TAGS.map((tag) => ( + + ))} +
+ + {/* Recent Searches (persisted via localStorage) */} + {recentSearches.length > 0 && !search && ( +
+ Recent: + {recentSearches.map((term) => ( + + ))} +
+ )} + + {/* Search Results Counter & Clear Link */} + {search.trim() && ( +
+ + Showing results for "{search.trim()}" ({displayedPosts.length} found) + + +
+ )} +
+ {/* Quick Clickable Search Tags */}
Popular searches: From a2aca34f05be1908f471997dc78c2076f791aabb Mon Sep 17 00:00:00 2001 From: CHANDANA V Date: Sun, 26 Jul 2026 00:23:41 +0530 Subject: [PATCH 15/23] Clean up JSX tree, fix duplicate tags, and refine search empty state --- apps/frontend/src/pages/CommunityPage.tsx | 198 +++++++--------------- 1 file changed, 60 insertions(+), 138 deletions(-) diff --git a/apps/frontend/src/pages/CommunityPage.tsx b/apps/frontend/src/pages/CommunityPage.tsx index 8491720c..1fee20bf 100644 --- a/apps/frontend/src/pages/CommunityPage.tsx +++ b/apps/frontend/src/pages/CommunityPage.tsx @@ -17,13 +17,9 @@ import { buttonCommunityAsk } from '../styles/style_config'; // ─── Main Page ──────────────────────────────────────────────────────────────── const POPULAR_TAGS = ['Assignment', 'Deadline', 'Submission', 'React', 'Vite']; + export default function CommunityPage() { const { user } = useAuth(); - // Active program from ProgramContext β€” used to scope the community feed - // to the chosen program so summership questions don't leak into - // winternship (and vice versa). Falls back to `undefined` when no - // program is selected yet, in which case we deliberately send NO - // batchId (matches previous behaviour while the picker takes over). const { currentBatch } = useBatch(); const activeBatchId = currentBatch?._id ?? undefined; const gate = useAuthGate(); @@ -31,6 +27,7 @@ export default function CommunityPage() { () => setShowCreate(true), 'Sign in to ask a question in the community.' ); + const [posts, setPosts] = useState([]); const [total, setTotal] = useState(0); const [nextCursor, setNextCursor] = useState(null); @@ -44,8 +41,20 @@ export default function CommunityPage() { const [filter, setFilter] = useState('all'); const [sort, setSort] = useState('newest'); const [showAllPrograms, setShowAllPrograms] = useState(false); + const [search, setSearch] = useState(() => { - // ── Keyboard Shortcut & Recent Searches ────────────────────────────────── + const params = new URLSearchParams(window.location.search); + return params.get('search') || ''; + }); + + const [searchResults, setSearchResults] = useState([]); + const [searchLoading, setSearchLoading] = useState(false); + + const [selectedPostId, setSelectedPostId] = useState(null); + const [showCreate, setShowCreate] = useState(false); + const [createPrefillTitle, setCreatePrefillTitle] = useState(''); + + // ── Keyboard Shortcut & Recent Searches ────────────────────────────────── const searchInputRef = useRef(null); const [recentSearches, setRecentSearches] = useState(() => { try { @@ -79,26 +88,7 @@ export default function CommunityPage() { window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, []); - const POPULAR_TAGS = ['Assignment', 'Deadline', 'Submission', 'React', 'Vite']; - const params = new URLSearchParams(window.location.search); - return params.get('search') || ''; - }); - - const [searchResults, setSearchResults] = useState([]); - const [searchLoading, setSearchLoading] = useState(false); - - const [selectedPostId, setSelectedPostId] = useState(null); - const [showCreate, setShowCreate] = useState(false); - const [createPrefillTitle, setCreatePrefillTitle] = useState(''); - - // Backend uses cursor-based pagination. The previous version sent `?page=2` - // which the backend silently ignored β€” so every "Load more" call returned - // the FIRST batch and we got duplicates. Send the cursor instead. - // - // v1.69 β€” program scoping: always send the active program's batchId - // when the user is NOT on "All Programs Feed". Without it, the - // backend returns every post across every program because the - // community routes don't have the programScope middleware attached +// community routes don't have the programScope middleware attached // and the controllers fall back to "no batchId filter". The // explicit-`undefined`-when-no-program-selected case preserves the // legacy behaviour while the program picker takes over. @@ -116,7 +106,7 @@ export default function CommunityPage() { }) .then((res) => { const incoming = res.data.posts || []; - setPosts((prev) => reset ? incoming : [...prev, ...incoming]); + setPosts((prev) => (reset ? incoming : [...prev, ...incoming])); setTotal(res.data.total || 0); setHasMore(res.data.hasMore ?? false); setNextCursor(res.data.nextCursor ?? null); @@ -137,10 +127,7 @@ export default function CommunityPage() { useEffect(() => { const params = new URLSearchParams(window.location.search); - // ?ask=true β€” open the create dialog. The navbar's "Ask Question" button - // already gates this behind the auth modal, so by the time we get here - // (after the gate's pending-action replay) the user is authenticated. - // We still double-check user here as a safety net. + // ?ask=true β€” open the create dialog. if (params.get('ask') === 'true') { if (user) { const prefilledTitle = params.get('title') || ''; @@ -148,8 +135,6 @@ export default function CommunityPage() { setShowCreate(true); window.history.replaceState({}, '', window.location.pathname); } else { - // Not authenticated β€” clear the param so the gate can re-trigger it - // after login if the user chooses to sign in. window.history.replaceState({}, '', window.location.pathname); } } @@ -161,59 +146,45 @@ export default function CommunityPage() { if (found) { setSelectedPostId(postId); } else { - // Post not in list β€” fetch individually api.get(`/community/${postId}`) .then((res) => { const post = res.data; if (post && post._id) { setSelectedPostId(postId); - // Prepend to posts so it's in cache - setPosts(prev => [post, ...prev]); + setPosts((prev) => [post, ...prev]); } }) - .catch(() => { - // Post not found or access denied β€” silently fail, don't open thread - }); + .catch(() => {}); } window.history.replaceState({}, '', window.location.pathname); } - }, [posts, user, window.location.search]); + }, [posts, user]); - // Reset cursor + posts when filter/sort changes so we paginate the - // newly-filtered set from the beginning. activeBatchId is also in this - // list β€” switching programs in the header must wipe the visible feed. + // Reset cursor + posts when filter/sort changes so we paginate the newly-filtered set useEffect(() => { setNextCursor(null); setPosts([]); }, [filter, sort, showAllPrograms, activeBatchId]); - // 2-D (MEDIUM) β€” previously this page had TWO effects both keyed on - // [filter, sort, ...] that each fired `fetchPosts(true)` in the same - // React commit, racing against each other. The earlier one (above) - // handled showAllPrograms, this one below handled the search-active - // branch. Merge them into one effect whose body is the union of - // both branches, removing the duplicate dispatch. // When filter or sort changes β€” refresh posts (if no search active) or re-filter existing results useEffect(() => { if (search.trim()) { - // Search is active β€” re-apply filter/sort client-side to existing searchResults - setSearchResults(prev => { + setSearchResults((prev) => { if (!prev.length) return prev; let filtered = [...prev]; - if (filter === 'answered') filtered = filtered.filter(p => p.status === 'answered'); - else if (filter === 'unanswered') filtered = filtered.filter(p => p.status === 'unanswered'); + if (filter === 'answered') filtered = filtered.filter((p) => p.status === 'answered'); + else if (filter === 'unanswered') filtered = filtered.filter((p) => p.status === 'unanswered'); if (sort === 'newest') filtered.sort((a, b) => new Date(b.createdAt ?? 0).getTime() - new Date(a.createdAt ?? 0).getTime()); else if (sort === 'oldest') filtered.sort((a, b) => new Date(a.createdAt ?? 0).getTime() - new Date(b.createdAt ?? 0).getTime()); - else if (sort === 'popular') filtered.sort((a, b) => ((b.upvotes?.length ?? 0)) - ((a.upvotes?.length ?? 0))); - else if (sort === 'discussed') filtered.sort((a, b) => ((b.comments?.length ?? 0)) - ((a.comments?.length ?? 0))); + else if (sort === 'popular') filtered.sort((a, b) => (b.upvotes?.length ?? 0) - (a.upvotes?.length ?? 0)); + else if (sort === 'discussed') filtered.sort((a, b) => (b.comments?.length ?? 0) - (a.comments?.length ?? 0)); return filtered; }); return; } fetchPosts(true); }, [filter, sort, showAllPrograms, activeBatchId]); - - // ── Infinite scroll β€” fetch the next page when the sentinel enters view ──── +// ── Infinite scroll β€” fetch the next page when the sentinel enters view ──── const sentinelRef = useRef(null); useEffect(() => { if (!sentinelRef.current) return; @@ -225,11 +196,11 @@ export default function CommunityPage() { fetchPosts(false); } }, - { rootMargin: '300px 0px' } // start loading 300px before the sentinel + { rootMargin: '300px 0px' } ); observer.observe(sentinelRef.current); return () => observer.disconnect(); - }, [hasMore, loading, loadingMore, nextCursor, filter, sort, showAllPrograms, activeBatchId]); + }, [hasMore, loading, loadingMore, nextCursor, filter, sort, showAllPrograms, activeBatchId, fetchPosts]); const runSemanticSearch = useCallback(async (q: string) => { setSearchLoading(true); @@ -246,10 +217,6 @@ export default function CommunityPage() { } }, [showAllPrograms, activeBatchId]); - // v2 β€” search is now Enter-only. We still keep the trimmed query handy - // for downstream effects (filter/sort re-apply on existing results). - // The auto-fire debounce was removed so results don't pop while the user - // is still typing. useEffect(() => { const q = search.trim(); if (!q || q.length < 3) { @@ -279,7 +246,7 @@ export default function CommunityPage() { const handleShareCommunity = async () => { const url = window.location.origin + '/csfaq/community'; - try { await navigator.clipboard.writeText(url); } catch { /* ignore */ } + try { await navigator.clipboard.writeText(url); } catch {} setToast('Community link copied'); setTimeout(() => setToast(''), 2500); }; @@ -290,7 +257,7 @@ export default function CommunityPage() { fetchPosts(true); }; -const visible = (() => { + const visible = (() => { let list = posts; if (search.trim()) { @@ -306,9 +273,9 @@ const visible = (() => { return [...list].sort((a, b) => { if (sort === 'newest') return new Date(b.createdAt ?? 0).getTime() - new Date(a.createdAt ?? 0).getTime(); - if (sort === 'oldest') return new Date(a.createdAt ?? 0).getTime() - new Date(b.createdAt ?? 0).getTime(); - if (sort === 'popular') return ((b.upvotes?.length ?? 0)) - ((a.upvotes?.length ?? 0)); - if (sort === 'discussed') return ((b.comments?.length ?? 0)) - ((a.comments?.length ?? 0)); + if (sort === 'oldest') return new Date(a.createdAt ?? 0).getTime() - new Date(a.createdAt ?? 0).getTime(); + if (sort === 'popular') return (b.upvotes?.length ?? 0) - (a.upvotes?.length ?? 0); + if (sort === 'discussed') return (b.comments?.length ?? 0) - (a.comments?.length ?? 0); return 0; }); })(); @@ -319,19 +286,11 @@ const visible = (() => { const answeredCount = posts.filter((p) => p.status === 'answered').length; const unansweredCount = posts.filter((p) => p.status !== 'answered').length; - return (
- {/* v1.69 β€” Phase 12: persistent "browsing program" pill - so the user always knows which program's community - feed they're scrolling. The pill reads from - BatchContext. The actual data fetch below already - uses currentBatch._id for the ?batchId=... filter - β€” this commit is a UX improvement, not a backend - change. */}
@@ -395,8 +354,6 @@ const visible = (() => {
- - {/* Search Bar, Quick Tags, and Results Counter */} {/* Search Bar, Quick Tags, Recent Searches, and Results Counter */}
@@ -490,37 +447,7 @@ const visible = (() => { )}
- {/* Quick Clickable Search Tags */} -
- Popular searches: - {POPULAR_TAGS.map((tag) => ( - - ))} -
- - {/* Search Results Counter & Clear Link */} - {search.trim() && ( -
- - Showing results for "{search.trim()}" ({displayedPosts.length} found) - - -
- )} -
- - {!loading && !error && ( + {!loading && !error && (
@@ -601,29 +528,29 @@ const visible = (() => {
)} - {!loading && !searchLoading && !error && displayedPosts.length === 0 && ( -
-
- πŸ” -
-

- {search.trim() ? `No results found for "${search.trim()}"` : 'No posts match your current filters'} -

-

- {search.trim() - ? 'Try checking for spelling errors, using different keywords, or clear the search query.' - : 'Try switching your status filter or program feed.'} -

- {search.trim() && ( - - )} -
-)} + {!loading && !searchLoading && !error && displayedPosts.length === 0 && total > 0 && ( +
+
+ πŸ” +
+

+ {search.trim() ? `No results found for "${search.trim()}"` : 'No posts match your current filters'} +

+

+ {search.trim() + ? 'Try checking for spelling errors, using different keywords, or clear the search query.' + : 'Try switching your status filter or program feed.'} +

+ {search.trim() && ( + + )} +
+ )} {!loading && !searchLoading && !error && displayedPosts.length > 0 && (
@@ -663,11 +590,6 @@ const visible = (() => {
- {/* Thread detail β€” full-page overlay replaces the list view. - z-30 (below the navbar's z-50) so the navbar floats on top of - the page-cover background. The inner modal at z-[60] escapes - this stacking context via its higher z-index and sits above - the navbar. */} {selectedPostId && (
{ )}
); -} +} \ No newline at end of file From 050282008e743df9fe13683fa020171fb8641d12 Mon Sep 17 00:00:00 2001 From: CHANDANA V Date: Sun, 26 Jul 2026 00:41:56 +0530 Subject: [PATCH 16/23] Add search result counter, quick filter tags, and custom empty search state --- docs/sentry-setup.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/sentry-setup.md b/docs/sentry-setup.md index d46702d7..5edcbca1 100644 --- a/docs/sentry-setup.md +++ b/docs/sentry-setup.md @@ -43,11 +43,11 @@ SENTRY_DEBUG=false # set true in dev for verbose logs ### Frontend (apps/frontend/.env) ```bash -VITE_SENTRY_DSN=https://@.ingest.sentry.io/ +fefe-project-id> VITE_SENTRY_TRACES_SAMPLE_RATE=0.1 VITE_SENTRY_ENV=development -VITE_SENTRY_RELEASE=v1.0.0+1 -``` +VITE_SENTRY_RELEASE=v1.0.0-key>@.ingest.sentry.io/<+1 +```VITE_SENTRY_DSN=https://< Both envs use `git rev-parse --short HEAD` for `SENTRY_RELEASE` if you want auto-versioning. From 64ebf1922fae89f1abde64a4dc442f9531541d5c Mon Sep 17 00:00:00 2001 From: Samruddhi Chate Date: Sun, 26 Jul 2026 12:02:40 +0530 Subject: [PATCH 17/23] feat: add draft persistence for community posts --- .../components/community/CreatePostDialog.tsx | 158 +++++++++++++----- 1 file changed, 120 insertions(+), 38 deletions(-) diff --git a/apps/frontend/src/components/community/CreatePostDialog.tsx b/apps/frontend/src/components/community/CreatePostDialog.tsx index 2a94008c..4852f97f 100644 --- a/apps/frontend/src/components/community/CreatePostDialog.tsx +++ b/apps/frontend/src/components/community/CreatePostDialog.tsx @@ -103,7 +103,8 @@ interface CreatePostDialogProps { prefillTitle?: string; } -export default function CreatePostDialog({ onClose, onCreated, prefillTitle = '' }: CreatePostDialogProps) { + + export default function CreatePostDialog({ onClose, onCreated, prefillTitle = '' }: CreatePostDialogProps) { const { user } = useAuth(); const { openModal } = useAuthModal(); // Guard: if rendered without an authenticated user, close the dialog and @@ -150,27 +151,12 @@ export default function CreatePostDialog({ onClose, onCreated, prefillTitle = '' setAttachments((prev) => prev.filter((a) => a.objectPath !== objectPath)); }; - // Restore draft from sessionStorage on mount - const [title, setTitle] = useState(() => { - try { - const draft = sessionStorage.getItem(DRAFT_KEY); - if (draft) { - const { t } = JSON.parse(draft); - return t || prefillTitle || ''; - } - } catch { void 0 } - return prefillTitle || ''; - }); - const [body, setBody] = useState(() => { - try { - const draft = sessionStorage.getItem(DRAFT_KEY); - if (draft) { - const { b } = JSON.parse(draft); - return b || ''; - } - } catch { void 0 } - return ''; - }); + // Restore draft from localStorage on mount + //My code Start + const [title, setTitle] = useState(''); + const [body, setBody] = useState(''); + console.log("Component rendered", title, body); + //My code end const [loading, setLoading] = useState(false); // Synchronous re-entry guard. The button's `disabled` prop already // prevents the second click once React re-renders, but between the @@ -191,21 +177,54 @@ export default function CreatePostDialog({ onClose, onCreated, prefillTitle = '' // Toast state const [toast, setToast] = useState<{ msg: string; type: 'success' | 'warn' | 'info' } | null>(null); +// My code start + const [showDraftPrompt, setShowDraftPrompt] = useState(false); +// My code end const showToast = (msg: string, type: 'success' | 'warn' | 'info' = 'info') => { setToast({ msg, type }); setTimeout(() => setToast(null), 3500); }; +// My code start +// Check for existing draft when dialog opens + useEffect(() => { + try { + const draft = localStorage.getItem(DRAFT_KEY); + if (draft) { + const parsed = JSON.parse(draft); + + if ( + (parsed.t && parsed.t.trim() !== '') || + (parsed.b && parsed.b.trim() !== '') + ) { + setShowDraftPrompt(true); + } + } + } catch (err) { + console.error('Failed to load draft:', err); + } +}, []); +// My code end + // Save draft on field changes const handleTitleChange = (val: string) => { - setTitle(val); - try { sessionStorage.setItem(DRAFT_KEY, JSON.stringify({ t: val, b: body })); } catch { void 0 } - }; - const handleBodyChange = (val: string) => { - setBody(val); - try { sessionStorage.setItem(DRAFT_KEY, JSON.stringify({ t: title, b: val })); } catch { void 0 } - }; + setTitle(val); + try { + localStorage.setItem(DRAFT_KEY, JSON.stringify({ t: val, b: body })); + } catch {} +}; + +const handleBodyChange = (val: string) => { + setBody(val); + try { + localStorage.setItem(DRAFT_KEY, JSON.stringify({ t: title, b: val })); + } catch {} +}; + //My Code starts + // Auto-save draft whenever title or body changes + + //My Code End useEffect(() => { const dialog = dialogRef.current; @@ -232,6 +251,10 @@ export default function CreatePostDialog({ onClose, onCreated, prefillTitle = '' } return () => dialog.removeEventListener('close', handleClose); }, [onClose]); + //My code Start + // Show draft prompt when dialog opens + + //MY code end useEffect(() => { if (duplicateCheckTimerRef.current) clearTimeout(duplicateCheckTimerRef.current); @@ -308,7 +331,7 @@ export default function CreatePostDialog({ onClose, onCreated, prefillTitle = '' { headers: { 'Idempotency-Key': idempotencyKey } }, ); // Clear draft on success - try { sessionStorage.removeItem(DRAFT_KEY); } catch { void 0 } + try { localStorage.removeItem(DRAFT_KEY); } catch { void 0 } // Show toast with duplicate check result const dupCount = duplicateMatch?.matches?.length ?? 0; if (dupCount > 0) { @@ -349,15 +372,17 @@ export default function CreatePostDialog({ onClose, onCreated, prefillTitle = '' // The "+" pick-file button is already disabled while attaching // (so the user can see the spinner), but the submit button was // not β€” this commit closes that gap. - const isSubmitDisabled = - !title.trim() || - !body.trim() || - tags.length === 0 || - hasHighConfidenceFaqMatch || - checkingDuplicates || - loading || - attaching; +console.log("title =", title, typeof title); +console.log("body =", body, typeof body); +const isSubmitDisabled = + !(title ?? "").trim() || + !(body ?? "").trim() || + tags.length === 0 || + hasHighConfidenceFaqMatch || + checkingDuplicates || + loading || + attaching; return ( {error}

)} +// My code Start +{showDraftPrompt && ( +
+
+

+ Keep this draft? +

+ +

+ We saved your progress automatically. +

+ +
+ + + + +
+
+
+)} +//My code ends {toast && (
Date: Sun, 26 Jul 2026 14:47:26 +0530 Subject: [PATCH 18/23] fix(community): remove duplicate search clear button and hardcoded popular tags --- apps/frontend/src/pages/CommunityPage.tsx | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/apps/frontend/src/pages/CommunityPage.tsx b/apps/frontend/src/pages/CommunityPage.tsx index 1fee20bf..7f3e4c37 100644 --- a/apps/frontend/src/pages/CommunityPage.tsx +++ b/apps/frontend/src/pages/CommunityPage.tsx @@ -16,7 +16,6 @@ import CreatePostDialog from '../components/community/CreatePostDialog'; import { buttonCommunityAsk } from '../styles/style_config'; // ─── Main Page ──────────────────────────────────────────────────────────────── -const POPULAR_TAGS = ['Assignment', 'Deadline', 'Submission', 'React', 'Vite']; export default function CommunityPage() { const { user } = useAuth(); @@ -363,7 +362,7 @@ export default function CommunityPage() { setSearch(e.target.value)} onKeyDown={(e) => { @@ -399,21 +398,7 @@ export default function CommunityPage() {
{/* Quick Clickable Search Tags */} -
- Popular searches: - {POPULAR_TAGS.map((tag) => ( - - ))} -
+ {/* Recent Searches (persisted via localStorage) */} {recentSearches.length > 0 && !search && ( From e7d462ba59021a1fc6eef14299fc0e414502a325 Mon Sep 17 00:00:00 2001 From: Not4Rohith Date: Sun, 26 Jul 2026 17:10:01 +0530 Subject: [PATCH 19/23] Necessary FAQ SearchBar optimizations --- .../src/components/faq/SearchDropdown.tsx | 110 +++++++--- .../src/components/search/SearchArea.tsx | 190 ++++++++++++++++++ .../src/components/search/SearchBar.tsx | 57 +++--- apps/frontend/src/pages/FAQPage.tsx | 45 ++--- apps/frontend/src/pages/HomePage.tsx | 44 ++-- 5 files changed, 338 insertions(+), 108 deletions(-) create mode 100644 apps/frontend/src/components/search/SearchArea.tsx diff --git a/apps/frontend/src/components/faq/SearchDropdown.tsx b/apps/frontend/src/components/faq/SearchDropdown.tsx index be7cc81a..94b402f8 100644 --- a/apps/frontend/src/components/faq/SearchDropdown.tsx +++ b/apps/frontend/src/components/faq/SearchDropdown.tsx @@ -1,4 +1,5 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; +import SearchFeedback from './SearchFeedback'; import { FAQItem, getCategoryIcon, formatCategoryName, getQuestionTitle, getAnswerText } from './faqUtils'; import { flexRowBetween, @@ -20,7 +21,7 @@ interface SearchDropdownProps { items: FAQItem[]; categories: string[]; onSelectQuestion: (item: FAQItem) => void; - onSelectCategory: (name: string) => void; + onSelectCategory: (name: string) => void; onClear: () => void; loading: boolean; } @@ -34,7 +35,34 @@ export default function SearchDropdown({ onClear, loading, }: SearchDropdownProps) { + const [activeCategories, setActiveCategories] = useState([]); + + useEffect(() => { + setActiveCategories([]); + }, [query]); + + const displayItems = activeCategories.length > 0 + ? items.filter((item) => { + const activeLower = activeCategories.map(c => c.toLowerCase()); + const matchCategory = item.category + && activeLower.includes(item.category.toLowerCase()); + const matchTags = item.tags && Array.isArray(item.tags) + && item.tags.some((tag: string) => activeLower.includes(tag.toLowerCase())); + + return matchCategory || matchTags; + }) + : items; + + const toggleCategory = (name: string) => { + setActiveCategories((prev) => + prev.includes(name) + ? prev.filter((c) => c !== name) + : [...prev, name] + ); + }; + return ( + <>
@@ -47,7 +75,10 @@ export default function SearchDropdown({

{loading && ( @@ -68,14 +99,16 @@ export default function SearchDropdown({
)) )} - {!loading && items.length === 0 && ( + {!loading && displayItems.length === 0 && (

- No matches yet. Keep typing or browse a category. + {activeCategories.length > 0 + ? "No matches found in the selected categories." + : "No matches yet. Keep typing or browse a category."}

)} - {!loading && items.map((item, idx) => ( + {!loading && displayItems.map((item, idx) => (
- {/* 1.12 (LOW) β€” empty-state for the categories column when - no categories are available. Merged with PR #144's - style_config.ts refactor: the `group` Tailwind class - on the button below is required for the icon's - `group-hover:opacity-100` to actually fire. */} -

- Categories -

+
+

Categories

+ {activeCategories.length > 0 && ( + + )} +
+ {categories.length === 0 ? (

@@ -109,21 +146,42 @@ export default function SearchDropdown({

) : (
- {categories.slice(0, 7).map((name) => ( - - ))} + {categories.slice(0, 7).map((name) => { + const isActive = activeCategories.includes(name); + + return ( + + ); + })}
)}
+ {!loading && displayItems.length > 0 && ( +
+ +
+ )} + ); -} +} \ No newline at end of file diff --git a/apps/frontend/src/components/search/SearchArea.tsx b/apps/frontend/src/components/search/SearchArea.tsx new file mode 100644 index 00000000..11764f60 --- /dev/null +++ b/apps/frontend/src/components/search/SearchArea.tsx @@ -0,0 +1,190 @@ +/* +Handles 'SearchBar' component and 'SearchDropdown' simultaneously. +Adds darker background to avoid overlapping with background. +Filter features to help a better search +*/ + +import React, { useState, useEffect } from 'react'; +import SearchBar from '../../components/search/SearchBar'; +import SearchDropdown from '../../components/faq/SearchDropdown'; +import { FAQItem } from '../../components/faq/faqUtils'; +import type { SearchResult } from '../../types/ui'; + +interface SearchAreaProps { + searchQuery: string; + placeholder?: string; + onSearchChange: (value: string) => void; + onSubmit?: (query: string) => void; + onClear: () => void; + onResults?: (results: SearchResult[] | null) => void; + onLoading?: (loading: boolean) => void; + onError?: (error: string | null) => void; + dropdownItems?: any[]; + categories?: string[]; + onSelectQuestion?: (item: FAQItem) => void; + onSelectCategory?: (name: string) => void; + searchLoading?: boolean; + children?: React.ReactNode; +} + +export default function SearchArea({ + searchQuery, + placeholder = "Search...", + onSearchChange, + onSubmit, + onClear, + onResults, + onLoading, + onError, + dropdownItems = [], + categories = [], + onSelectQuestion, + onSelectCategory, + searchLoading = false, + children +}: SearchAreaProps) { + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + + const [activeTab, setActiveTab] = useState<'faq' | 'all' | 'community'>('all'); + + useEffect(() => { + if (searchQuery.trim().length > 0) { + setIsDropdownOpen(true); + } else { + setIsDropdownOpen(false); + setActiveTab('all'); + } + }, [searchQuery]); + + const handleClear = () => { + setIsDropdownOpen(false); + setActiveTab('all'); + onClear(); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault(); + setIsDropdownOpen(false); + if (onSubmit) onSubmit(searchQuery.trim()); + } + }; + + + const filteredDropdownItems = dropdownItems.filter((item) => { + if (activeTab === 'all') return true; + + const itemSource = item.source || 'faq'; + return itemSource === activeTab; + }); + + return ( +
+ {/* The Global Backdrop */} + {isDropdownOpen && ( +
setIsDropdownOpen(false)} + aria-hidden="true" + /> + )} + + {/* Elevated Container */} +
+ + {isDropdownOpen && ( +
+
+ + + + + + +
+
+ )} + + {/* The Search Input */} + {})} + onLoading={onLoading || (() => {})} + onError={onError || (() => {})} + placeholder={placeholder} + disableSuggestions={true} + onSubmit={(q) => { + setIsDropdownOpen(false); + if (onSubmit) onSubmit(q); + }} + onClear={handleClear} + /> + {/* The Dropdown Panel */} + {isDropdownOpen && ( + { + setIsDropdownOpen(false); + if (onSelectQuestion) onSelectQuestion(item); + }} + onSelectCategory={(cat) => { + setIsDropdownOpen(false); + if (onSelectCategory) onSelectCategory(cat); + }} + onClear={handleClear} + loading={searchLoading} + /> + )} +
+ + {/* Render the full page results underneath when dropdown is CLOSED */} + {!isDropdownOpen && children} +
+ ); +} \ No newline at end of file diff --git a/apps/frontend/src/components/search/SearchBar.tsx b/apps/frontend/src/components/search/SearchBar.tsx index 1a13a061..40eaa8b8 100644 --- a/apps/frontend/src/components/search/SearchBar.tsx +++ b/apps/frontend/src/components/search/SearchBar.tsx @@ -31,6 +31,8 @@ interface SearchBarProps { className?: string; disableSuggestions?: boolean; variant?: 'default' | 'compact'; + onSubmit?: (query: string) => void; + onClear?: () => void; } const SearchBar = React.forwardRef(function SearchBar( @@ -46,6 +48,8 @@ const SearchBar = React.forwardRef(function Se className = '', disableSuggestions = false, variant = 'default', + onSubmit, + onClear, }, ref ) { @@ -60,10 +64,7 @@ const SearchBar = React.forwardRef(function Se const query = isControlled ? (value ?? '') : internalQuery; const suggestDebounceRef = useRef | null>(null); const searchDebounceRef = useRef | null>(null); - // 1.6 β€” tracks the suggestionError auto-dismiss timer so we can - // clear it on the next click / unmount. const suggestErrorTimerRef = useRef | null>(null); - const wrapperRef = useRef(null); const handleSearch = async (searchQuery: string) => { @@ -82,9 +83,7 @@ const SearchBar = React.forwardRef(function Se }); onResults(res.data.results ?? null); } catch (err: any) { - if (axios.isCancel(err)) { - return; // Ignore cancelled requests - } + if (axios.isCancel(err)) return; onResults([]); onError?.('Search failed. Please check your connection and try again.'); } finally { @@ -107,10 +106,6 @@ const SearchBar = React.forwardRef(function Se } }; - // v2 β€” Suggestions stay live as the user types (250ms debounce). Search - // results also stream live as the user types (300ms debounce) β€” but they - // appear INSIDE the glassmorphic dropdown bubble on the host page, not as - // a page swap. Enter skips the wait and fires immediately. const handleChange = (e: ChangeEvent) => { const val = e.target.value; if (isControlled) { @@ -119,19 +114,15 @@ const SearchBar = React.forwardRef(function Se setInternalQuery(val); } - // Live suggestions under the input. if (!disableSuggestions) { if (suggestDebounceRef.current) clearTimeout(suggestDebounceRef.current); suggestDebounceRef.current = setTimeout(() => fetchSuggestions(val), 250); } - // Live results β€” same source the post-Enter flow uses, so the - // dropdown and the in-page panel can never disagree on counts. if (searchDebounceRef.current) clearTimeout(searchDebounceRef.current); if (val.trim().length >= 3) { searchDebounceRef.current = setTimeout(() => handleSearch(val), 300); } else { - // Below threshold β€” wipe results so the dropdown's empty state shows. onResults(null); onError?.(null); } @@ -141,6 +132,7 @@ const SearchBar = React.forwardRef(function Se if (searchDebounceRef.current) clearTimeout(searchDebounceRef.current); setShowSuggestions(false); handleSearch(query); + onSubmit?.(query); }; const handleSubmit = (e: FormEvent) => { @@ -152,9 +144,6 @@ const SearchBar = React.forwardRef(function Se setShowSuggestions(false); setSuggestions([]); setSuggestionError(null); - // 1.6 (LOW) β€” clear any stale suggestionError on every click so it - // doesn't linger indefinitely if the user stopped typing. The - // 4-second auto-dismiss below still applies for fresh errors. if (suggestErrorTimerRef.current) { clearTimeout(suggestErrorTimerRef.current); suggestErrorTimerRef.current = null; @@ -163,8 +152,6 @@ const SearchBar = React.forwardRef(function Se const res = await api.get<{ _id: string; question: string; answer: string; category: string }>(`/faq/${faqId}`); sessionStorage.setItem('yaksha_faq_highlight', JSON.stringify(res.data)); } catch { - // 1.6 (LOW) β€” auto-dismiss after 4 seconds so the red banner - // doesn't linger until the next fetchSuggestions cycle. setSuggestionError('Could not load FAQ. Navigating anyway.'); suggestErrorTimerRef.current = setTimeout(() => { setSuggestionError(null); @@ -174,8 +161,6 @@ const SearchBar = React.forwardRef(function Se navigate(`/faq/${faqId}`); }; - // 1.6 β€” clear pending auto-dismiss timer on unmount so we don't - // try to setState after the component is gone. useEffect(() => { return () => { if (suggestErrorTimerRef.current) { @@ -185,10 +170,8 @@ const SearchBar = React.forwardRef(function Se }; }, []); - // Close suggestions on outside click const handleBlur = (e: React.FocusEvent) => { onBlur?.(); - // Delay so click on suggestion registers first setTimeout(() => { if (wrapperRef.current && !wrapperRef.current.contains(document.activeElement)) { setShowSuggestions(false); @@ -219,10 +202,35 @@ const SearchBar = React.forwardRef(function Se onFocus={onFocus} onBlur={handleBlur} placeholder={placeholder} - className={variant === 'compact' ? searchInputCompact : searchInputDefault} + className={`${variant === 'compact' ? searchInputCompact : searchInputDefault} ${variant === 'default' ? 'pr-36' : 'pr-10'}`} autoComplete="off" /> + {/*Cross Mark (Clear Button) */} + {query.trim().length > 0 && ( + + )} + {variant === 'default' && (
)} - {/* Suggestion click error */} {suggestionError && (
{suggestionError} diff --git a/apps/frontend/src/pages/FAQPage.tsx b/apps/frontend/src/pages/FAQPage.tsx index 8f154e79..bf12ee26 100644 --- a/apps/frontend/src/pages/FAQPage.tsx +++ b/apps/frontend/src/pages/FAQPage.tsx @@ -11,7 +11,7 @@ import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react' import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; import Footer from '../components/layout/Footer'; import UserActiveProgramIndicator from '../components/layout/UserActiveProgramIndicator'; -import SearchBar from '../components/search/SearchBar'; +import SearchArea from '@/components/search/SearchArea'; import { HomeDoodles } from '../components/ui/PageDoodles'; import api from '../utils/api'; import type { TrendingQuery } from '../types/ui'; @@ -320,31 +320,21 @@ export default function FAQPage() { {/* ─── SEARCH BAR ───────────────────────────────────────────── */} -
-
- setSearchResults(res as unknown as FAQItem[])} - onLoading={setSearchLoading} - onError={(err) => setError(err || '')} - placeholder="Ask anything about your internship..." - disableSuggestions={true} - /> - - {showDropdown && ( - - )} -
+
+ setSearchResults(res as unknown as FAQItem[])} + onLoading={setSearchLoading} + onError={(err) => setError(err || '')} + onClear={handleClearSearch} + dropdownItems={dropdownItems} + categories={categories} + onSelectQuestion={handleQuestionOpen} + onSelectCategory={handleCategoryOpen} + searchLoading={searchLoading} + > +
{/* ─── CATEGORY FILTER PILLS ─────────────────────────────────── */} @@ -607,9 +597,6 @@ export default function FAQPage() {
- {searchActive && searchResults && searchResults.length > 0 && ( - - )}
); } \ No newline at end of file diff --git a/apps/frontend/src/pages/HomePage.tsx b/apps/frontend/src/pages/HomePage.tsx index f283e178..6148e7c9 100644 --- a/apps/frontend/src/pages/HomePage.tsx +++ b/apps/frontend/src/pages/HomePage.tsx @@ -23,6 +23,7 @@ import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react' import { useNavigate, useParams } from 'react-router-dom'; import Footer from '../components/layout/Footer'; import SearchBar from '../components/search/SearchBar'; +import SearchArea from '@/components/search/SearchArea'; import { HomeDoodles } from '../components/ui/PageDoodles'; import api from '../utils/api'; import { useBatch } from '../context/BatchContext'; @@ -634,31 +635,21 @@ export default function HomePage() { )} {/* ─── SEARCH BAR ─── */} -
-
- setSearchResults(res as unknown as FAQItem[])} - onLoading={setSearchLoading} - onError={(err) => setError(err || '')} - placeholder="Ask anything about your internship..." - disableSuggestions={true} - /> - - {showDropdown && ( - - )} -
+
+ setSearchResults(res as unknown as FAQItem[])} + onLoading={setSearchLoading} + onError={(err) => setError(err || '')} + onClear={handleClearSearch} + dropdownItems={dropdownItems} + categories={categories} + onSelectQuestion={handleQuestionOpen} + onSelectCategory={handleCategoryOpen} + searchLoading={searchLoading} + > +
@@ -926,9 +917,6 @@ export default function HomePage() {
- {searchActive && searchResults && searchResults.length > 0 && ( - - )}
); } \ No newline at end of file From e1c9d0c4e0777ba7a545df9e931ccaa35a76a62c Mon Sep 17 00:00:00 2001 From: Not4Rohith Date: Sun, 26 Jul 2026 17:32:28 +0530 Subject: [PATCH 20/23] reverted lost comments --- apps/frontend/src/components/faq/SearchDropdown.tsx | 6 +++++- apps/frontend/src/components/search/SearchBar.tsx | 10 +++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/apps/frontend/src/components/faq/SearchDropdown.tsx b/apps/frontend/src/components/faq/SearchDropdown.tsx index 94b402f8..74e5ff05 100644 --- a/apps/frontend/src/components/faq/SearchDropdown.tsx +++ b/apps/frontend/src/components/faq/SearchDropdown.tsx @@ -124,7 +124,11 @@ export default function SearchDropdown({ ))}
- +{/* 1.12 (LOW) β€” empty-state for the categories column when + no categories are available. Merged with PR #144's + style_config.ts refactor: the `group` Tailwind class + on the button below is required for the icon's + `group-hover:opacity-100` to actually fire. */}

Categories

diff --git a/apps/frontend/src/components/search/SearchBar.tsx b/apps/frontend/src/components/search/SearchBar.tsx index 40eaa8b8..780057f1 100644 --- a/apps/frontend/src/components/search/SearchBar.tsx +++ b/apps/frontend/src/components/search/SearchBar.tsx @@ -105,6 +105,10 @@ const SearchBar = React.forwardRef(function Se setSuggestions([]); } }; +// v2 β€” Suggestions stay live as the user types (250ms debounce). Search + // results also stream live as the user types (300ms debounce) β€” but they + // appear INSIDE the glassmorphic dropdown bubble on the host page, not as + // a page swap. Enter skips the wait and fires immediately. const handleChange = (e: ChangeEvent) => { const val = e.target.value; @@ -139,7 +143,9 @@ const SearchBar = React.forwardRef(function Se e.preventDefault(); runSearchNow(); }; - + // 1.6 (LOW) β€” clear any stale suggestionError on every click so it + // doesn't linger indefinitely if the user stopped typing. The + // 4-second auto-dismiss below still applies for fresh errors. const handleSuggestionClick = async (faqId: string) => { setShowSuggestions(false); setSuggestions([]); @@ -152,6 +158,8 @@ const SearchBar = React.forwardRef(function Se const res = await api.get<{ _id: string; question: string; answer: string; category: string }>(`/faq/${faqId}`); sessionStorage.setItem('yaksha_faq_highlight', JSON.stringify(res.data)); } catch { + // 1.6 (LOW) β€” auto-dismiss after 4 seconds so the red banner + // doesn't linger until the next fetchSuggestions cycle. setSuggestionError('Could not load FAQ. Navigating anyway.'); suggestErrorTimerRef.current = setTimeout(() => { setSuggestionError(null); From 91c8af4e34b17497fda0e45d0747f1ac2ddf11ee Mon Sep 17 00:00:00 2001 From: Sharanagouda Patil Date: Mon, 27 Jul 2026 13:07:33 +0530 Subject: [PATCH 21/23] Complete Internship Roadmap UI --- .../src/components/community/ThreadDetail.tsx | 11 +- .../src/components/faq/SearchDropdown.tsx | 110 +++++++--- .../src/components/search/SearchArea.tsx | 190 ++++++++++++++++++ .../src/components/search/SearchBar.tsx | 59 ++++-- apps/frontend/src/pages/FAQPage.tsx | 45 ++--- apps/frontend/src/pages/HomePage.tsx | 44 ++-- 6 files changed, 353 insertions(+), 106 deletions(-) create mode 100644 apps/frontend/src/components/search/SearchArea.tsx diff --git a/apps/frontend/src/components/community/ThreadDetail.tsx b/apps/frontend/src/components/community/ThreadDetail.tsx index c47a8151..40562cec 100644 --- a/apps/frontend/src/components/community/ThreadDetail.tsx +++ b/apps/frontend/src/components/community/ThreadDetail.tsx @@ -96,7 +96,7 @@ export default function ThreadDetail({ postId, onClose }: ThreadDetailProps) { const { user } = useAuth(); const gate = useAuthGate(); const navigate = useNavigate(); - const currentUserId = user?._id ?? ''; + const currentUserId = user?._id || user?.id || ''; const userRole = user?.role ?? ''; const [post, setPost] = useState(null); @@ -131,6 +131,9 @@ export default function ThreadDetail({ postId, onClose }: ThreadDetailProps) { const isPrivileged = userRole === 'admin' || userRole === 'moderator'; const topLevelComments = post?.comments ?? []; + const postAuthorId = typeof post?.author === 'string' ? post.author : post?.author?._id; + const isPostAuthor = currentUserId !== '' && currentUserId === postAuthorId; + /** Safe navigation: external URLs open in new tab, internal ones use SPA routing. */ const navigateTo = (url: string) => { if (url.startsWith('http') || url.startsWith('//')) { @@ -429,9 +432,10 @@ export default function ThreadDetail({ postId, onClose }: ThreadDetailProps) { /> {/* Author / privileged actions */} - {(currentUserId === post.author?._id || isPrivileged) && ( + {(isPostAuthor || isPrivileged) && ( <>
+ {isPrivileged && ( + )} {!isEditing && (
{loading && ( @@ -68,14 +99,16 @@ export default function SearchDropdown({
)) )} - {!loading && items.length === 0 && ( + {!loading && displayItems.length === 0 && (

- No matches yet. Keep typing or browse a category. + {activeCategories.length > 0 + ? "No matches found in the selected categories." + : "No matches yet. Keep typing or browse a category."}

)} - {!loading && items.map((item, idx) => ( + {!loading && displayItems.map((item, idx) => (
- -
- {/* 1.12 (LOW) β€” empty-state for the categories column when +{/* 1.12 (LOW) β€” empty-state for the categories column when no categories are available. Merged with PR #144's style_config.ts refactor: the `group` Tailwind class on the button below is required for the icon's `group-hover:opacity-100` to actually fire. */} -

- Categories -

+
+
+

Categories

+ {activeCategories.length > 0 && ( + + )} +
+ {categories.length === 0 ? (

@@ -109,21 +150,42 @@ export default function SearchDropdown({

) : (
- {categories.slice(0, 7).map((name) => ( - - ))} + {categories.slice(0, 7).map((name) => { + const isActive = activeCategories.includes(name); + + return ( + + ); + })}
)}
+ {!loading && displayItems.length > 0 && ( +
+ +
+ )} + ); -} +} \ No newline at end of file diff --git a/apps/frontend/src/components/search/SearchArea.tsx b/apps/frontend/src/components/search/SearchArea.tsx new file mode 100644 index 00000000..11764f60 --- /dev/null +++ b/apps/frontend/src/components/search/SearchArea.tsx @@ -0,0 +1,190 @@ +/* +Handles 'SearchBar' component and 'SearchDropdown' simultaneously. +Adds darker background to avoid overlapping with background. +Filter features to help a better search +*/ + +import React, { useState, useEffect } from 'react'; +import SearchBar from '../../components/search/SearchBar'; +import SearchDropdown from '../../components/faq/SearchDropdown'; +import { FAQItem } from '../../components/faq/faqUtils'; +import type { SearchResult } from '../../types/ui'; + +interface SearchAreaProps { + searchQuery: string; + placeholder?: string; + onSearchChange: (value: string) => void; + onSubmit?: (query: string) => void; + onClear: () => void; + onResults?: (results: SearchResult[] | null) => void; + onLoading?: (loading: boolean) => void; + onError?: (error: string | null) => void; + dropdownItems?: any[]; + categories?: string[]; + onSelectQuestion?: (item: FAQItem) => void; + onSelectCategory?: (name: string) => void; + searchLoading?: boolean; + children?: React.ReactNode; +} + +export default function SearchArea({ + searchQuery, + placeholder = "Search...", + onSearchChange, + onSubmit, + onClear, + onResults, + onLoading, + onError, + dropdownItems = [], + categories = [], + onSelectQuestion, + onSelectCategory, + searchLoading = false, + children +}: SearchAreaProps) { + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + + const [activeTab, setActiveTab] = useState<'faq' | 'all' | 'community'>('all'); + + useEffect(() => { + if (searchQuery.trim().length > 0) { + setIsDropdownOpen(true); + } else { + setIsDropdownOpen(false); + setActiveTab('all'); + } + }, [searchQuery]); + + const handleClear = () => { + setIsDropdownOpen(false); + setActiveTab('all'); + onClear(); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault(); + setIsDropdownOpen(false); + if (onSubmit) onSubmit(searchQuery.trim()); + } + }; + + + const filteredDropdownItems = dropdownItems.filter((item) => { + if (activeTab === 'all') return true; + + const itemSource = item.source || 'faq'; + return itemSource === activeTab; + }); + + return ( +
+ {/* The Global Backdrop */} + {isDropdownOpen && ( +
setIsDropdownOpen(false)} + aria-hidden="true" + /> + )} + + {/* Elevated Container */} +
+ + {isDropdownOpen && ( +
+
+ + + + + + +
+
+ )} + + {/* The Search Input */} + {})} + onLoading={onLoading || (() => {})} + onError={onError || (() => {})} + placeholder={placeholder} + disableSuggestions={true} + onSubmit={(q) => { + setIsDropdownOpen(false); + if (onSubmit) onSubmit(q); + }} + onClear={handleClear} + /> + {/* The Dropdown Panel */} + {isDropdownOpen && ( + { + setIsDropdownOpen(false); + if (onSelectQuestion) onSelectQuestion(item); + }} + onSelectCategory={(cat) => { + setIsDropdownOpen(false); + if (onSelectCategory) onSelectCategory(cat); + }} + onClear={handleClear} + loading={searchLoading} + /> + )} +
+ + {/* Render the full page results underneath when dropdown is CLOSED */} + {!isDropdownOpen && children} +
+ ); +} \ No newline at end of file diff --git a/apps/frontend/src/components/search/SearchBar.tsx b/apps/frontend/src/components/search/SearchBar.tsx index 1a13a061..780057f1 100644 --- a/apps/frontend/src/components/search/SearchBar.tsx +++ b/apps/frontend/src/components/search/SearchBar.tsx @@ -31,6 +31,8 @@ interface SearchBarProps { className?: string; disableSuggestions?: boolean; variant?: 'default' | 'compact'; + onSubmit?: (query: string) => void; + onClear?: () => void; } const SearchBar = React.forwardRef(function SearchBar( @@ -46,6 +48,8 @@ const SearchBar = React.forwardRef(function Se className = '', disableSuggestions = false, variant = 'default', + onSubmit, + onClear, }, ref ) { @@ -60,10 +64,7 @@ const SearchBar = React.forwardRef(function Se const query = isControlled ? (value ?? '') : internalQuery; const suggestDebounceRef = useRef | null>(null); const searchDebounceRef = useRef | null>(null); - // 1.6 β€” tracks the suggestionError auto-dismiss timer so we can - // clear it on the next click / unmount. const suggestErrorTimerRef = useRef | null>(null); - const wrapperRef = useRef(null); const handleSearch = async (searchQuery: string) => { @@ -82,9 +83,7 @@ const SearchBar = React.forwardRef(function Se }); onResults(res.data.results ?? null); } catch (err: any) { - if (axios.isCancel(err)) { - return; // Ignore cancelled requests - } + if (axios.isCancel(err)) return; onResults([]); onError?.('Search failed. Please check your connection and try again.'); } finally { @@ -106,11 +105,11 @@ const SearchBar = React.forwardRef(function Se setSuggestions([]); } }; - - // v2 β€” Suggestions stay live as the user types (250ms debounce). Search +// v2 β€” Suggestions stay live as the user types (250ms debounce). Search // results also stream live as the user types (300ms debounce) β€” but they // appear INSIDE the glassmorphic dropdown bubble on the host page, not as // a page swap. Enter skips the wait and fires immediately. + const handleChange = (e: ChangeEvent) => { const val = e.target.value; if (isControlled) { @@ -119,19 +118,15 @@ const SearchBar = React.forwardRef(function Se setInternalQuery(val); } - // Live suggestions under the input. if (!disableSuggestions) { if (suggestDebounceRef.current) clearTimeout(suggestDebounceRef.current); suggestDebounceRef.current = setTimeout(() => fetchSuggestions(val), 250); } - // Live results β€” same source the post-Enter flow uses, so the - // dropdown and the in-page panel can never disagree on counts. if (searchDebounceRef.current) clearTimeout(searchDebounceRef.current); if (val.trim().length >= 3) { searchDebounceRef.current = setTimeout(() => handleSearch(val), 300); } else { - // Below threshold β€” wipe results so the dropdown's empty state shows. onResults(null); onError?.(null); } @@ -141,20 +136,20 @@ const SearchBar = React.forwardRef(function Se if (searchDebounceRef.current) clearTimeout(searchDebounceRef.current); setShowSuggestions(false); handleSearch(query); + onSubmit?.(query); }; const handleSubmit = (e: FormEvent) => { e.preventDefault(); runSearchNow(); }; - + // 1.6 (LOW) β€” clear any stale suggestionError on every click so it + // doesn't linger indefinitely if the user stopped typing. The + // 4-second auto-dismiss below still applies for fresh errors. const handleSuggestionClick = async (faqId: string) => { setShowSuggestions(false); setSuggestions([]); setSuggestionError(null); - // 1.6 (LOW) β€” clear any stale suggestionError on every click so it - // doesn't linger indefinitely if the user stopped typing. The - // 4-second auto-dismiss below still applies for fresh errors. if (suggestErrorTimerRef.current) { clearTimeout(suggestErrorTimerRef.current); suggestErrorTimerRef.current = null; @@ -174,8 +169,6 @@ const SearchBar = React.forwardRef(function Se navigate(`/faq/${faqId}`); }; - // 1.6 β€” clear pending auto-dismiss timer on unmount so we don't - // try to setState after the component is gone. useEffect(() => { return () => { if (suggestErrorTimerRef.current) { @@ -185,10 +178,8 @@ const SearchBar = React.forwardRef(function Se }; }, []); - // Close suggestions on outside click const handleBlur = (e: React.FocusEvent) => { onBlur?.(); - // Delay so click on suggestion registers first setTimeout(() => { if (wrapperRef.current && !wrapperRef.current.contains(document.activeElement)) { setShowSuggestions(false); @@ -219,10 +210,35 @@ const SearchBar = React.forwardRef(function Se onFocus={onFocus} onBlur={handleBlur} placeholder={placeholder} - className={variant === 'compact' ? searchInputCompact : searchInputDefault} + className={`${variant === 'compact' ? searchInputCompact : searchInputDefault} ${variant === 'default' ? 'pr-36' : 'pr-10'}`} autoComplete="off" /> + {/*Cross Mark (Clear Button) */} + {query.trim().length > 0 && ( + + )} + {variant === 'default' && (
)} - {/* Suggestion click error */} {suggestionError && (
{suggestionError} diff --git a/apps/frontend/src/pages/FAQPage.tsx b/apps/frontend/src/pages/FAQPage.tsx index 8f154e79..bf12ee26 100644 --- a/apps/frontend/src/pages/FAQPage.tsx +++ b/apps/frontend/src/pages/FAQPage.tsx @@ -11,7 +11,7 @@ import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react' import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; import Footer from '../components/layout/Footer'; import UserActiveProgramIndicator from '../components/layout/UserActiveProgramIndicator'; -import SearchBar from '../components/search/SearchBar'; +import SearchArea from '@/components/search/SearchArea'; import { HomeDoodles } from '../components/ui/PageDoodles'; import api from '../utils/api'; import type { TrendingQuery } from '../types/ui'; @@ -320,31 +320,21 @@ export default function FAQPage() { {/* ─── SEARCH BAR ───────────────────────────────────────────── */} -
-
- setSearchResults(res as unknown as FAQItem[])} - onLoading={setSearchLoading} - onError={(err) => setError(err || '')} - placeholder="Ask anything about your internship..." - disableSuggestions={true} - /> - - {showDropdown && ( - - )} -
+
+ setSearchResults(res as unknown as FAQItem[])} + onLoading={setSearchLoading} + onError={(err) => setError(err || '')} + onClear={handleClearSearch} + dropdownItems={dropdownItems} + categories={categories} + onSelectQuestion={handleQuestionOpen} + onSelectCategory={handleCategoryOpen} + searchLoading={searchLoading} + > +
{/* ─── CATEGORY FILTER PILLS ─────────────────────────────────── */} @@ -607,9 +597,6 @@ export default function FAQPage() {
- {searchActive && searchResults && searchResults.length > 0 && ( - - )}
); } \ No newline at end of file diff --git a/apps/frontend/src/pages/HomePage.tsx b/apps/frontend/src/pages/HomePage.tsx index f283e178..6148e7c9 100644 --- a/apps/frontend/src/pages/HomePage.tsx +++ b/apps/frontend/src/pages/HomePage.tsx @@ -23,6 +23,7 @@ import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react' import { useNavigate, useParams } from 'react-router-dom'; import Footer from '../components/layout/Footer'; import SearchBar from '../components/search/SearchBar'; +import SearchArea from '@/components/search/SearchArea'; import { HomeDoodles } from '../components/ui/PageDoodles'; import api from '../utils/api'; import { useBatch } from '../context/BatchContext'; @@ -634,31 +635,21 @@ export default function HomePage() { )} {/* ─── SEARCH BAR ─── */} -
-
- setSearchResults(res as unknown as FAQItem[])} - onLoading={setSearchLoading} - onError={(err) => setError(err || '')} - placeholder="Ask anything about your internship..." - disableSuggestions={true} - /> - - {showDropdown && ( - - )} -
+
+ setSearchResults(res as unknown as FAQItem[])} + onLoading={setSearchLoading} + onError={(err) => setError(err || '')} + onClear={handleClearSearch} + dropdownItems={dropdownItems} + categories={categories} + onSelectQuestion={handleQuestionOpen} + onSelectCategory={handleCategoryOpen} + searchLoading={searchLoading} + > +
@@ -926,9 +917,6 @@ export default function HomePage() {
- {searchActive && searchResults && searchResults.length > 0 && ( - - )}
); } \ No newline at end of file From 99663179a5ceaa3912f39f25d57ecf34bcccfec5 Mon Sep 17 00:00:00 2001 From: ANIL-Dudi Date: Thu, 23 Jul 2026 14:24:49 +0530 Subject: [PATCH 22/23] fix(ai): normalize RRF scores before merging with knowledge scores in RAG re-ranking Score scales were incompatible across retrieval sources: FAQ/Community RRF scores are capped at ~0.033 while Knowledge cosine scores go up to 1.0. This caused weak Knowledge hits to always outrank strong FAQ/ Community hits in citation order and context-window truncation. Fixes by rescaling RRF scores onto the same [0,1] ceiling. Adds regression tests (4 passing). --- .../__tests__/rag.score-normalization.test.ts | 52 +++++++++++++++++++ apps/backend/src/modules/ai/rag.service.ts | 43 +++++++++++++-- 2 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 apps/backend/src/modules/ai/__tests__/rag.score-normalization.test.ts diff --git a/apps/backend/src/modules/ai/__tests__/rag.score-normalization.test.ts b/apps/backend/src/modules/ai/__tests__/rag.score-normalization.test.ts new file mode 100644 index 00000000..9dae5cba --- /dev/null +++ b/apps/backend/src/modules/ai/__tests__/rag.score-normalization.test.ts @@ -0,0 +1,52 @@ +/** + * rag.score-normalization.test β€” regression guard for the cross-source + * score scale bug in the /ask-ai RAG pipeline. + * + * Bug: runRag() in rag.service.ts merges hits from three retrieval + * sources and sorts them by `.score` to decide citation order AND + * which sources survive the MAX_CONTEXT_CHARS truncation. FAQ/Community + * hits carry a Reciprocal Rank Fusion score (max ~0.033, see + * searchFaqs()/searchCommunity()); Knowledge hits carry an independent + * cosine-similarity-based score already normalized to [0, 1] (see + * scoreAndSort() in knowledge-base.service.ts). Sorting these unmodified + * meant a barely-relevant Knowledge hit (score just over its 0.05 floor) + * always outranked even a rank-#1-in-both-searches FAQ/Community hit β€” + * silently degrading answer quality. + * + * Fix: normalizeRrfScore() rescales the RRF score onto the same [0, 1] + * ceiling as the Knowledge score before the merged sources are sorted. + * These tests pin that behavior down with pure-function assertions β€” + * no Mongo/Atlas Search/HTTP required. + */ + +import { describe, it, expect } from 'vitest'; +import { normalizeRrfScore, RRF_SCORE_CEILING } from '../rag.service.js'; + +describe('normalizeRrfScore', () => { + it('maps the RRF ceiling (rank #1 in both vector + text search) to exactly 1', () => { + const bestPossibleRrf = 1 / 60 + 1 / 60; // rank 0 in both lists + expect(normalizeRrfScore(bestPossibleRrf)).toBeCloseTo(1, 10); + }); + + it('maps a single rank-#1 hit (only in one of the two lists) to 0.5', () => { + const singleListTopRank = 1 / 60; + expect(normalizeRrfScore(singleListTopRank)).toBeCloseTo(0.5, 10); + }); + + it('never exceeds 1 even if given an out-of-range value', () => { + expect(normalizeRrfScore(RRF_SCORE_CEILING * 5)).toBe(1); + }); + + it('demonstrates the pre-fix bug is resolved: a top-ranked FAQ hit now outranks a barely-passing Knowledge hit', () => { + const topFaqRrfScore = 1 / 60 + 1 / 60; // best possible FAQ RRF score + const weakKnowledgeScore = 0.06; // just above the 0.05 floor in scoreAndSort() + + // Before the fix, sources were compared as raw scores: + const rawFaqScore = topFaqRrfScore; // ~0.0333 + expect(rawFaqScore < weakKnowledgeScore).toBe(true); // this was the bug β€” FAQ always lost + + // After the fix, scores are normalized onto a comparable [0, 1] scale: + const normalizedFaqScore = normalizeRrfScore(topFaqRrfScore); + expect(normalizedFaqScore > weakKnowledgeScore).toBe(true); // FAQ now correctly outranks it + }); +}); diff --git a/apps/backend/src/modules/ai/rag.service.ts b/apps/backend/src/modules/ai/rag.service.ts index 18ff3467..5cb08c3c 100644 --- a/apps/backend/src/modules/ai/rag.service.ts +++ b/apps/backend/src/modules/ai/rag.service.ts @@ -33,7 +33,12 @@ export interface RagSource { snippet: string; /** URL the client can deep-link to. */ url: string; - /** Confidence in [0, 1] (vector cosine + keyword overlap). */ + /** + * Confidence in [0, 1], normalized across sources so scores are + * comparable for merged sort/truncation (see normalizeRrfScore() + * below for how FAQ/Community RRF scores are rescaled onto this + * same ceiling as the Knowledge cosine-based score). + */ score: number; } @@ -45,6 +50,20 @@ export interface RagResult { } const TOP_K_PER_SOURCE = 4; + +/** + * FAQ/Community hits are scored via Reciprocal Rank Fusion β€” the sum of + * 1/(60+rank) across the vector-search and text-search result lists. Its + * theoretical ceiling is 1/60 + 1/60 = 1/30 (a document ranked #1 in both + * lists). Knowledge hits (see scoreAndSort() in knowledge-base.service.ts) + * are scored on an independent, already-normalized [0, 1] cosine-similarity + * scale. Exported as a pure function so the rescaling can be unit tested + * without standing up Mongo/Atlas Search. + */ +export const RRF_SCORE_CEILING = 2 / 60; // = 1/30 +export function normalizeRrfScore(raw: number): number { + return Math.min(1, raw / RRF_SCORE_CEILING); +} const MAX_CONTEXT_CHARS = 14000; // leave headroom under typical 16k context windows interface FaqHit { _id: unknown; question: string; answer: string; category?: string; trustLevel?: string; score: number } @@ -269,6 +288,20 @@ export async function runRag(question: string, attachments: RagAttachment[] = [] ]); // Normalize each source into the common shape. + // + // IMPORTANT: faqHits/postHits carry a Reciprocal Rank Fusion score + // (sum of 1/(60+rank) across vector+text search) whose theoretical + // ceiling is 1/60 + 1/60 = 1/30 β‰ˆ 0.033. knowledgeHits carry a + // cosine-similarity-based score already normalized to [0, 1] + // (see scoreAndSort() in knowledge-base.service.ts). Sorting these + // scores together unmodified means a barely-relevant Knowledge hit + // (score just over its 0.05 floor) always outranks even a + // rank-1-in-both-searches FAQ/Community hit (score capped at 0.033) β€” + // silently degrading citation order and, worse, causing strong + // FAQ/Community sources to be dropped first by the MAX_CONTEXT_CHARS + // truncation in buildContext(). Rescale RRF scores onto the same + // [0, 1] ceiling as the knowledge score so all three sources are + // comparable before the merged sort (see normalizeRrfScore() above). const sources: RagSource[] = [ ...faqHits.map((h) => ({ id: `faq:${String(h._id)}`, @@ -276,7 +309,7 @@ export async function runRag(question: string, attachments: RagAttachment[] = [] title: h.question, snippet: h.answer.slice(0, 600), url: `/faq/${String(h._id)}`, - score: h.score, + score: normalizeRrfScore(h.score), })), ...postHits.map((h) => ({ id: `community:${String(h._id)}`, @@ -284,7 +317,7 @@ export async function runRag(question: string, attachments: RagAttachment[] = [] title: h.title, snippet: h.body.slice(0, 600), url: `/community?post=${String(h._id)}`, - score: h.score, + score: normalizeRrfScore(h.score), })), ...knowledgeHits.map((h) => ({ id: `knowledge:${h._id}`, @@ -295,11 +328,11 @@ export async function runRag(question: string, attachments: RagAttachment[] = [] // or to the admin KB if we know the meeting. For now, a stable // deep-link to a future /knowledge/:id is best-effort. url: `/community?post=${h._id}`, - score: h.score, + score: h.score, // already normalized to [0, 1] in scoreAndSort() })), ]; - // Re-rank by score so the LLM sees the strongest sources first. + // Re-rank by (now comparable) score so the LLM sees the strongest sources first. sources.sort((a, b) => b.score - a.score); // If we found nothing at all, skip the LLM call β€” just say "no answer". From e8d7adf3a7d2b96efad9f6306c4ed1f0e8d701e2 Mon Sep 17 00:00:00 2001 From: Sharanagouda Patil Date: Tue, 28 Jul 2026 17:09:13 +0530 Subject: [PATCH 23/23] docs: update README with project overview and Phase 1 enhancements --- README.md | 388 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 317 insertions(+), 71 deletions(-) diff --git a/README.md b/README.md index 79dc06b0..65aa8bba 100644 --- a/README.md +++ b/README.md @@ -1,129 +1,375 @@ -# Crowd Source FAQ (Yaksha FAQ Portal) +

+ + +

-Full-stack FAQ portal with semantic vector search, AI-powered community moderation, and an expert promotion layer. Built to handle 1 million registered users. +

Crowd Source FAQ (CSFAQ)

-GitHub: https://github.com/vicharanashala/crowd-source-faq -Full reference: [`docs/`](docs/README.md) Β· [Contributing](./CONTRIBUTING.md) Β· [Code of Conduct](./CODE_OF_CONDUCT.md) Β· [License](./LICENSE) +

+ AI-Powered Knowledge Management & Community FAQ Platform +

+ +

+ +![React](https://img.shields.io/badge/React-61DAFB?style=flat-square&logo=react&logoColor=black) +![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=flat-square&logo=typescript&logoColor=white) +![Node.js](https://img.shields.io/badge/Node.js-339933?style=flat-square&logo=node.js&logoColor=white) +![Express](https://img.shields.io/badge/Express-000000?style=flat-square&logo=express&logoColor=white) +![MongoDB](https://img.shields.io/badge/MongoDB-47A248?style=flat-square&logo=mongodb&logoColor=white) +![Vite](https://img.shields.io/badge/Vite-646CFF?style=flat-square&logo=vite&logoColor=white) +![Tailwind](https://img.shields.io/badge/TailwindCSS-06B6D4?style=flat-square&logo=tailwindcss&logoColor=white) + +

+ +

+ Repository β€’ + Installation β€’ + Our Contributions β€’ + Contributors +

--- -## Vision +## Table of Contents + +- [Overview](#overview) +- [Key Features](#key-features) +- [Phase 1 Feature Enhancements](#phase-1-feature-enhancements) +- [Bug Fixes](#bug-fixes) +- [Technology Stack](#technology-stack) +- [System Architecture](#system-architecture) +- [Project Structure](#project-structure) +- [Installation](#installation) +- [Environment Variables](#environment-variables) +- [Development Workflow](#development-workflow) +- [Testing](#testing) +- [Challenges & Limitations](#challenges--limitations) +- [Future Enhancements](#future-enhancements) +- [Contributors](#contributors) +- [Contributing](#contributing) +- [Acknowledgements](#acknowledgements) +- [License](#license) -**Automate the FAQ lifecycle end-to-end. Zero people in the loop. Reduce the operational FAQ culture.** +--- -Every question a user has has been asked before β€” and most will be asked again. The right answer should be there before the user finishes typing. The platform achieves this through four zero-touch pillars: +## Overview -- **Zero-touch ingestion** β€” Zoom meetings, webhooks, and manual uploads feed the knowledge base without human scheduling, categorising, or approval. -- **Zero-touch answering** β€” A 24-hour scheduler matches unanswered posts against the knowledge base; high-confidence matches auto-post, low-confidence escalate to humans. -- **Zero-touch quality control** β€” Approved FAQs are re-evaluated every 6 hours; drift, contradictions, and staleness are detected and flagged automatically. -- **Zero-touch user lifecycle** β€” Deletion is anonymisation, not destruction. Reputation, attribution, and audit history persist. +Crowd Source FAQ (CSFAQ) is an AI-powered knowledge management platform that lets users search FAQs, participate in community discussions, ask questions, and get intelligent, source-cited answers through semantic search and AI-assisted retrieval. -The platform is the operator. People handle exceptions, not the steady state. +The platform combines community-driven knowledge sharing with AI-powered search to reduce repetitive questions, improve response quality, and give both users and administrators a single, organized, self-maintaining knowledge base. + +This repository reflects our team's contributions during **Phase 1** of the AI internship programme, built on top of the existing CSFAQ (Yaksha FAQ Portal) codebase. --- -## About +## Key Features + + + + + + + +
+ +**User** +- AI-powered FAQ Search +- Community Discussion Forum +- Ask Questions +- My Questions Dashboard +- Saved Knowledge +- Draft Persistence +- Internship Roadmap +- Welcome Package +- Program Portal +- Support Ticket System +- Golden Ticket Module +- Email Notifications + + + +**AI** +- Semantic Search +- Intelligent Knowledge Retrieval +- AI Answer Ranking Optimisation +- Multi-source Response Selection +- Context-aware Search + + + +**Admin** +- FAQ Management +- Community Moderation +- Manual FAQ Promotion +- User Management +- AI Configuration +- Feature Flag Management +- Knowledge Management +- Support Dashboard +- Program & Batch Management +- Analytics Dashboard + +
+ +**Search Enhancements:** Real-time search Β· Dynamic filtering Β· Multi-category search Β· Keyboard shortcuts Β· Recent searches Β· Search suggestions Β· Responsive search interface + +--- + +## Phase 1 Feature Enhancements + +During Phase 1 of the internship, our team designed, built, and shipped the following enhancements into the existing production codebase. + +### πŸ—ΊοΈ Internship Roadmap +New interns struggled to understand the sequence of internship activities and where to find resources β€” everything was scattered across sections. We built **"The Internship Express,"** a centralized, transit-map-style visual timeline covering the full journey: Registration (Samagama) β†’ Onboarding β†’ Mandatory Sessions β†’ Phase 1 (Crowd Source FAQ) β†’ Phase 2 (Mentor Projects) β†’ Completion β€” complete with milestone tracking and quick links to Samagama, ViBe, and Matrix Mystics. +> **Impact:** Simplifies onboarding, improves resource accessibility, enables visual progress tracking. + +### πŸ“§ Email Notifications for Resolved Questions +Previously, users could only find out their question was answered by checking the in-app bell icon. We built a `sendEmail.ts` utility (Nodemailer, env-configured), extended `notificationDispatcher.ts` with an optional `message` field without breaking ~12 existing call sites, and wired the real question + answer content into a branded HTML email. Verified end-to-end with Mailtrap. +> **Impact:** Users are notified the instant their question is resolved β€” no manual checking required. + +### πŸ“ Draft Persistence for Community Posts +Users lost their in-progress questions if they closed the "Ask Question" dialog, refreshed, or navigated away. We added automatic `localStorage` draft saving with **Keep Draft** / **Discard Draft** on reopen, and fixed a bug where autosave was silently overwriting stored drafts on component init β€” all without touching duplicate detection, posting, categories, or attachments. +> **Impact:** No more lost work; drafts persist reliably across refreshes and dialog reopenings. + +### πŸ‘€ My Questions Dashboard +There was no way to see just your own questions without scrolling the entire community feed. We added a dedicated view pulling from existing post data β€” purely additive, zero changes to existing routes or logic. +> **Impact:** Fast, personal access to your own submissions. + +### ⭐ Manual FAQ Promotion +Turning a great community answer into an official FAQ required manual database work. We built a backend endpoint and Admin Community UI integration to promote posts to FAQs in one click. +> **Impact:** Faster knowledge-base curation, no manual DB operations, no schema changes required. -Samagama (internally "Yaksha FAQ Portal") turns an organisation's accumulated conversations into a searchable, self-maintaining FAQ. It combines hybrid vector + keyword search with a community Q&A board and a fully automated ingestion pipeline that pulls transcripts from Zoom, extracts Q&A with AI, and indexes them for retrieval in seconds. +### πŸ” Enhanced Community Search +Community search lacked speed and memory. We added real-time client-side filtering, keyboard shortcuts (`/` and `Ctrl/Cmd+K`), persistent Recent Searches, a clear button, a result counter, and better empty states β€” while removing redundant/duplicate UI code. +> **Impact:** Faster, more accessible content discovery. -Built for organisations whose community generates more questions than a human team can answer β€” student cohorts, open-source projects, internal forums, customer-success communities. Target scale: 1 million registered users with constant conversational input. +### πŸ€– AI Answer Ranking Optimisation +The AI assistant pulls from FAQs, Community Q&A, and Zoom lecture notes β€” each scored on different scales, which skewed answer selection. We introduced a **score-normalization layer** to compare all sources fairly on a common scale, backed by automated unit tests. +> **Impact:** Fairer, more accurate multi-source answer selection. -See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the architecture deep-dive and [docs/PIPELINES.md](docs/PIPELINES.md) for pipeline internals. +### πŸŽ›οΈ Search Bar & Search Experience Improvements +The search bar lacked flexible filters and consistent behavior. We redesigned it with a segmented filter (FAQ / All / Community Posts), multi-select category filters, unified Enter-key/button submission, an inline clear (X) button, and improved dropdown/loading/empty states β€” all client-side, no extra API calls. +> **Impact:** Instant filtering, cleaner UI, no unnecessary navigation. + +### πŸ› οΈ Bug Fixes & Performance Improvements +Stability improvements, UI fixes, and optimization across modules (see [Bug Fixes](#bug-fixes) below for the two headline fixes). + +| Feature | Description | +|---------|-------------| +| Internship Roadmap | Interactive roadmap with milestones, learning resources, and progress tracking | +| Email Notifications | Automatic emails when admins resolve user questions | +| Draft Persistence | Auto-save community drafts with restore/discard | +| My Questions | Dashboard of all questions created by the logged-in user | +| Manual FAQ Promotion | Convert community discussions into official FAQs | +| Enhanced Community Search | Real-time filtering, categories, recent searches, keyboard shortcuts | +| AI Answer Ranking Optimisation | Normalized, fair answer selection across knowledge sources | +| Search Bar Improvements | Segmented search, multi-select categories, better dropdown UX | +| Bug Fixes & Performance | Stability, UI, and edit/delete fixes across modules | + +--- + +## Bug Fixes + +| Issue | Root Cause | Fix | +|---|---|---| +| **Unable to edit/delete own posts** | Incorrect ownership validation blocked rightful owners from modifying their own posts | Corrected ownership-check logic in the Community module while preserving existing authorization rules | +| **Search feedback popup layout overlap** | "Did this answer your question?" popup's text and buttons collided in a narrow fixed-width container | Fixed the flex layout and shortened button labels so the prompt and both buttons fit on one line, verified across multiple searches | --- -## Tech Stack +## Technology Stack | Layer | Technologies | -|---|---| -| Frontend | React 18, Vite, TypeScript, Tailwind CSS, Framer Motion, Axios, Recharts, React Router 6, Vitest | -| Backend | Node.js, Express 4, TypeScript (ESM), Mongoose 8, JWT, bcryptjs, Helmet, CORS, Morgan, Multer, Zod, express-rate-limit, dotenv, OpenAI SDK, Vitest | -| Database & Storage | MongoDB Atlas (with Vector Search), Upstash Redis (optional), LRU cache, Cloudinary | -| Search & AI | Xenova/transformers (768-dim local embeddings), Atlas Vector Search, $text search, Reciprocal Rank Fusion | -| AI Providers | Anthropic, OpenAI, XAI, MiniMax (per-pipeline configurable) | -| DevOps | Sentry, Ngrok (local webhook tunnel), Twilio (SMS), SMTP, Vitest | +|--------|--------------| +| Frontend | React 18, TypeScript, Vite, Tailwind CSS, Framer Motion, Recharts, React Router 6 | +| Backend | Node.js, Express.js, Mongoose | +| Database | MongoDB (Atlas Vector Search) | +| Authentication | JWT, bcryptjs | +| AI | Semantic Search, AI Answer Ranking, Hybrid Retrieval | +| Email | Nodemailer | +| Storage | Cloudinary | +| Version Control | Git & GitHub | + +--- + +## System Architecture + +```text + Users + β”‚ + β–Ό + React + TypeScript (Frontend) + β”‚ + REST API Requests + β”‚ + β–Ό + Express.js Backend + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β–Ό β–Ό β–Ό + MongoDB AI Services External Services + (Database) Knowledge Engine SMTP β€’ Cloudinary β€’ Zoom +``` --- -## Quick Start +## Project Structure + +```text +crowd-source-faq +β”‚ +β”œβ”€β”€ apps +β”‚ β”œβ”€β”€ frontend # React + Vite SPA +β”‚ └── backend # Express + TypeScript API +β”‚ +β”œβ”€β”€ packages +β”œβ”€β”€ docs +β”œβ”€β”€ public +└── README.md +``` + +--- + +## Installation + +Clone the repository + +```bash +git clone https://github.com/sharanu-patil33/crowd-source-faq.git +cd crowd-source-faq +``` + +Install dependencies ```bash -./run.sh # Full-stack runner: env setup, ngrok, backend + frontend +pnpm install ``` -`run.sh` prompts for `MONGODB_URI` and `JWT_SECRET` on first run, then saves them to `apps/backend/.env.local`. The script will not overwrite existing values. Session logs are written to `logs/session_*.txt`. +**Quick start (alternative):** run `./run.sh` from the project root β€” it handles env setup, ngrok, and starts both backend and frontend in one step. It will prompt for `MONGODB_URI` and `JWT_SECRET` on first run and save them to `apps/backend/.env.local` without overwriting existing values. --- -## Key Features +## Running the Project + +```bash +pnpm dev +``` + +| Service | URL | +|---|---| +| Frontend | http://localhost:5173 | +| Backend | http://localhost:6767 | -Eight flagship capabilities define this platform: +--- -- **Zoom transcript ingestion with per-user OAuth** β€” Each user connects their own Zoom account via OAuth. Webhook-fired downloads parse VTT transcripts, extract Q&A pairs via AI, and dual-publish: `ZoomInsight` (admin-reviewed) and `TranscriptKnowledge` (auto-approved, immediately vector-searchable). Includes retry + dead-letter queue for failed meetings and admin backfill for historical meetings. See [docs/PIPELINES.md#4-zoom-ingestion-pipeline](docs/PIPELINES.md). -- **AI auto-answer pipeline for community posts** β€” A scheduler (every 24h) β€” and a one-click **Run AI** button in the admin dashboard β€” finds unanswered posts and searches **all three knowledge sources in parallel** (FAQ + Community Q&A + Transcript Knowledge). Best match above β‰₯0.85 confidence auto-posts; 0.60–0.84 queues for human review; below 0.60 (or sensitive content) escalates. When no direct match exists, the LLM is given the top gathered context to synthesize an answer. Per-pipeline AI provider configurable. See [docs/PIPELINES.md#1-auto-answer-pipeline](docs/PIPELINES.md). -- **FAQ audit pipeline** β€” A scheduler (every 6h) re-evaluates approved FAQs against the live knowledge base (TranscriptKnowledge + ZoomInsights). Uses AI to judge correctness and emits verdicts: `correct` (β‰₯0.80), `drift_detected` (0.60–0.79), `contradiction` (<0.60), or `stale`. Flagged FAQs land in `/admin/faqs/review` with `reviewStatus='pending_review'`, `flagType='auto'`, and an incremented `reviewCycle`. See [docs/PIPELINES.md#2-faq-audit-pipeline](docs/PIPELINES.md). -- **FAQ freshness & staleness detection** β€” Every approved FAQ carries a `freshness_tier` (`evergreen` / `seasonal` / `volatile`) and a per-tier review interval. A daily cron auto-flags FAQs whose last-verified date exceeds the interval, opening a peer-review window on `/admin/faqs/review`. Anyone can vote `still_accurate` / `needs_update`; the threshold auto-verifies, otherwise it escalates to a moderator. `FreshnessBadge` on the public FAQ card surfaces verified-vs-under-review status. See [docs/PIPELINES.md#7-faq-freshness-pipeline](docs/PIPELINES.md). -- **Golden Ticket β€” Spurti Points escalation** β€” A premium user-driven escalation channel. Users spend Spurti Points (SP) to bump a time-sensitive query to the top of the admin queue (higher SP = higher priority). SP is consumed on submission; a 48h cooldown blocks repeat submissions. Admins resolve or reject β€” no penalty, no ban, just a single unified cooldown rule. Includes a live Escalation Queue (right column on `/golden`) sorted by SP spend, anonymous to non-admin viewers. Toggleable from `/admin/features`. -- **Batch management + public guest FAQ portal** β€” FAQs, categories, and analytics are scoped to a `Batch` (cohort, term, program). A first-class `Category` model replaces the old free-text field. Guests land on `/explore/select` to pick a batch, then browse the public FAQ at `/faq` with no account required. Admin can create/archive batches and promote FAQs between them. See [docs/BATCH_MANAGEMENT_PLAN.md](docs/BATCH_MANAGEMENT_PLAN.md). -- **Schema-driven context fields per support category** β€” Each support category (`internet`, `camera`, `microphone`, `device`, `power`, `other`) has an admin-editable schema of context fields (text, textarea, number, date, boolean, dropdown). Admins add, edit, reorder, or archive fields from `/admin/support/categories` without redeploying. The frontend renders dynamic inputs from the live schema. See [docs/SCHEMA_DRIVEN_CONTEXT_PLAN.md](docs/SCHEMA_DRIVEN_CONTEXT_PLAN.md). -- **Soft-delete with anonymization** β€” Deleting a user never hard-deletes their records. The account is anonymised: `isDeleted=true`, `deletedAt` timestamp, `name` becomes `Deleted User`, `email` is rewritten to a non-routable placeholder, `password` is replaced with a random UUID to break login. All posts, comments, votes, reputation logs, and audit trail entries remain intact β€” preserving referential integrity, attribution history, and regulatory compliance. +## Environment Variables -Other capabilities: semantic hybrid search, community Q&A board, reputation system + badges + leaderboard, SpillTheTea event-driven notifications, per-user Zoom OAuth, RAG-powered `/ask-ai` assistant with image + file attachments, soft user lifecycle, experimental feature flags, support tickets (troubleshoot β†’ admin triage β†’ resolution). +| Variable | Required | Purpose | +|---|---|---| +| `MONGODB_URI` | βœ… | Database connection | +| `JWT_SECRET` | βœ… | Auth token signing | +| `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` / `XAI_API_KEY` / `MINIMAX_API_KEY` | At least one | AI provider access | +| `MAIL_HOST` / `MAIL_PORT` / `MAIL_USER` / `MAIL_PASS` | Optional | Email notifications | +| `CLOUDINARY_*` | Optional | Image uploads | +| `UPSTASH_REDIS_*` | Optional | Caching | --- -## Admin Dashboard +## Development Workflow + +```text +Fork Repository β†’ Create Feature Branch β†’ Implement Feature β†’ Commit Changes + β†’ Push Branch β†’ Open Pull Request β†’ Code Review β†’ Merge into Main + β†’ Final Pull Request to Official Repository +``` + +--- -The admin panel at `/admin` (mounted at `/api/admin/*`) provides telemetry, moderation, and operational control. Key areas: +## Testing -- **Telemetry & analytics** β€” live stats, FAQ growth, top categories, search insights, user-activity charts, activity feed, failed-query analytics, unresolved-search tracker -- **Operational pages** β€” AdminDashboard, AdminFAQs, FaqReview, AdminFAQAudit, AdminAutoAnswerQueue, AdminCommunity, AdminUsers, AdminModeration, AdminZoomMeetings, AdminZoomInsights, AdminLeaderboard, AdminUnresolvedSearch, AdminAISettings, AdminSettings, AdminLogin -- **Moderation** β€” every ban, suspend, warn, and soft-delete recorded in `ModerationLog`; every reputation change (+2 upvote, +5 accepted answer, -2/-5 on removal) recorded in `ReputationLog` -- **AI pipeline visibility** β€” unified `PipelineResult` collection (30-day TTL) for both auto-answer and audit outcomes; Zoom health endpoint reports OAuth/API circuit state, cache hit rate, failing-meetings count, dead-letter count, pending-retry count; Prometheus metrics at `/api/metrics` (search latency, cache hits, RAG duration, queue depth) +The following areas were manually and automatically verified: -For the full admin route map and per-page behaviour, see [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md). +- User Authentication +- FAQ Module +- Community Module (including edit/delete ownership fix) +- AI Search & Answer Ranking +- Internship Roadmap +- Draft Persistence (save, restore, discard) +- Email Notifications (verified via Mailtrap) +- Search Optimisation +- Admin Dashboard +- Performance & Stability --- -## User Experience +## Challenges & Limitations + +**Challenges** +- Understanding a large, multi-module existing codebase before implementing new features +- Integrating changes without regressions across interconnected modules +- Maintaining UI/coding consistency across multiple contributors +- Extensive scenario testing to avoid breaking existing behavior +- Understanding the existing AI retrieval/ranking pipeline before improving it + +**Limitations** +- Dependence on external services (email, AI providers) affects overall reliability +- Continued scalability optimization needed as users, posts, and FAQs grow +- Platform is intentionally an evolving system β€” built for continuous enhancement -The user-facing app (`/`, `/faq`, `/community`, `/saved`, `/account`, `/leaderboard`) gives authenticated users full participation in the knowledge loop: +--- -- **Discover** β€” hybrid semantic + keyword search at `/api/search` (public), semantic suggestions at `/api/search/suggest`, category browsing -- **Ask the community** β€” post creation with Zod validation, debounced duplicate detection against the FAQ base (banner + block on match), auto-normalised tags, Cloudinary image attachments -- **Engage** β€” upvotes with reputation-farming prevention (reverses on removal), bookmarks at `/saved`, nested comment threads with optimistic UI, accept-answer (locks verified/expert comments from edit), edit/delete own comments, share via clipboard, report and flag-outdated -- **Notifications** β€” in-app bell, SpillTheTea event stream (`post_answered`, `post_deleted`, etc.), per-event-type settings, email + SMS delivery -- **Reputation** β€” points for accepted answers, badges at thresholds, expert promotion by peer vote, public leaderboard -- **AI assistant** β€” RAG-powered `/ask-ai` (5/day anonymous quota via localStorage, unlimited for authenticated users), sources cited, **accepts file and image attachments (max 4 files, 10 MB each) β€” images sent as vision input, text files inlined into the prompt** -- **Zoom integration** β€” per-user OAuth from `/account`, manual `.vtt` / `.txt` / raw-text upload, last-synced status card, no admin required -- **Search feedback** β€” "Report missing FAQ" on zero results, admin-promotable to FAQ +## Future Enhancements -For per-route behaviour and field schemas, see [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md). +- AI-powered personalization (recommendations based on user activity/interests) +- More advanced semantic search & intelligent query suggestions +- Richer analytics/reporting dashboards for admins +- Multilingual support +- Mobile application +- Newer/better AI models with improved context understanding +- General performance optimisation --- -## Project Structure +## Contributors -``` -Crowd Source FAQ/ -β”œβ”€β”€ apps/ -β”‚ β”œβ”€β”€ backend/ # Express + TypeScript API -β”‚ └── frontend/ # React + Vite SPA -β”œβ”€β”€ docs/ # Full documentation -└── run.sh # Local dev runner (env setup, ngrok, backend + frontend) -``` +This project was developed collaboratively during the internship programme. + +| Name | Role | +|------|------| +| Sharanagouda Policepatil| Team Lead | +| Akshay Vishwakarma | Team Member | +| Anil Dudi | Team Member | +| Ashish Patra | Team Member | +| Ayushi Karn | Team Member | +| Chandana V | Team Member | +| Durgesh Manoj Mahajan | Team Member | +| Rohith N R | Team Member | +| Samruddhi Bhaskar Chate | Team Member | + +Team Lead Responsibilities: repository & branch management, pull request reviews, merge conflict resolution, feature integration, testing & validation, final project integration, final PR submission to the official repository. --- -## Environment Variables +## Contributing + +1. Fork the repository +2. Create a new feature branch +3. Commit your changes +4. Push the branch +5. Open a Pull Request -Required: `MONGODB_URI`, `JWT_SECRET` -Optional: at least one AI provider key (`ANTHROPIC_API_KEY` / `OPENAI_API_KEY` / `XAI_API_KEY` / `MINIMAX_API_KEY`), Zoom OAuth credentials, `CLOUDINARY_*`, `SENTRY_DSN`, Twilio + SMTP for notifications, `UPSTASH_REDIS_*` +Please follow the project's coding standards and review guidelines before submitting a Pull Request. -See [docs/ARCHITECTURE.md#10-env-variables-reference](docs/ARCHITECTURE.md#10-env-variables-reference) for the full list. +--- + +## Acknowledgements + +This project was developed as part of an internship programme focused on collaborative software development, AI-assisted knowledge management, and modern GitHub workflows. It provided hands-on experience in feature development, collaborative version control, pull request reviews, code integration, testing, and maintaining a production-scale application. --- -## License +## Repository + +**GitHub Repository:** https://github.com/sharanu-patil33/crowd-source-faq -[MIT](./LICENSE) Β© 2026 vicharanashala +--- \ No newline at end of file