diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..0093f52
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,17 @@
+# Normalize line endings across platforms
+* text=auto eol=lf
+
+# Windows scripts that must keep CRLF
+*.bat text eol=crlf
+*.cmd text eol=crlf
+*.ps1 text eol=crlf
+
+# Binary assets
+*.png binary
+*.jpg binary
+*.jpeg binary
+*.gif binary
+*.ico binary
+*.webp binary
+*.woff binary
+*.woff2 binary
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..92e6ef9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,70 @@
+# ─── Dependencies ───────────────────────────────────────────────────────────
+node_modules/
+server/node_modules/
+client/node_modules/
+
+# ─── Environment & secrets (never commit) ───────────────────────────────────
+.env
+.env.*
+!.env.example
+server/.env
+client/.env
+client/.env.local
+client/.env.*.local
+
+# ─── Build output ───────────────────────────────────────────────────────────
+client/dist/
+dist/
+build/
+out/
+.vite/
+
+# ─── Logs ───────────────────────────────────────────────────────────────────
+logs/
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+# ─── Testing & coverage ─────────────────────────────────────────────────────
+coverage/
+.nyc_output/
+
+# ─── Caches ─────────────────────────────────────────────────────────────────
+.cache/
+.parcel-cache/
+.turbo/
+.eslintcache
+*.tsbuildinfo
+
+# ─── OS files ───────────────────────────────────────────────────────────────
+.DS_Store
+.DS_Store?
+._*
+.Spotlight-V100
+.Trashes
+ehthumbs.db
+Thumbs.db
+Desktop.ini
+
+# ─── Editors & IDEs ─────────────────────────────────────────────────────────
+.vscode/*
+!.vscode/extensions.json
+!.vscode/settings.json.example
+.idea/
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+*~
+
+# ─── Cursor / local tooling ─────────────────────────────────────────────────
+.cursor/
+
+# ─── Misc ───────────────────────────────────────────────────────────────────
+*.local
+.temp/
+tmp/
diff --git a/PRODUCT.md b/PRODUCT.md
new file mode 100644
index 0000000..f31ff5f
--- /dev/null
+++ b/PRODUCT.md
@@ -0,0 +1,545 @@
+# CrowdFAQ — Product Document
+
+> **Version:** 1.0.0 | **Stack:** MERN + Tailwind CSS v3 | **Status:** Production-Ready MVP
+
+---
+
+## 1. Product Overview
+
+**CrowdFAQ** is a full-stack, crowd-sourced FAQ and community Q&A platform where users collaboratively build a shared knowledge base. Anyone can browse answers; authenticated members can ask questions, submit answers, vote, and earn reputation. Moderators can approve or reject community content inline — no separate admin panel required.
+
+The platform was designed to solve a real problem: static FAQ pages go stale and are hard to maintain. CrowdFAQ turns knowledge management into a living, community-driven activity.
+
+### Target Users
+
+| Persona | Need |
+|---------|------|
+| **Guest / Reader** | Quickly find answers to common questions without signing up |
+| **Community Member** | Ask questions, contribute answers, track personal progress |
+| **Moderator / Admin** | Review, approve, or reject community-submitted content |
+
+---
+
+## 2. Feature Catalogue
+
+### 2.1 Core FAQ Browser (`/`)
+
+- **Searchable accordion list** of curated FAQs — expandable/collapsible with smooth CSS animation
+- **Debounced full-text search** (350 ms) hitting server-side MongoDB `$regex` across question body, answer body, tags, keywords, and category simultaneously
+- **Category filter bar** — sticky pill buttons (All + 8 categories); highlights active selection
+- **Sort modes** — Latest / Most Helpful / Most Viewed via a dropdown
+- **Yes / No voting** on every FAQ — localStorage deduplication prevents repeat votes; helpful percentage displayed after voting
+- **View count** tracking — each accordion open increments the FAQ's view counter
+- **Tag chips** displayed on each FAQ card
+- **Decorative hero section** with animated blur blobs and a live quick-stats bar
+- **Two-column desktop layout** with a sticky right sidebar
+
+### 2.2 Community Q&A (`/community`, `/community/:id`)
+
+- **Question list** sorted newest-first; each card shows status badge, category badge, answer count, relative timestamp, submitter name and tags
+- **Status filter tabs**: All · Pending (red count badge) · Approved · Rejected
+- **Search + category dropdown** filter on the community list
+- **Inline moderation** — Approve / Reject / Mark Pending buttons with per-button loading state directly on every question card and answer card
+- **Question detail page** — breadcrumb navigation, full question card, all answers with avatar initials and numbered badges
+- **Answer status tabs** — filter answers by status within a question
+- **Optimistic UI** — submitted answer appends to the list immediately without a round-trip refetch
+
+### 2.3 Ask a Question (`/ask`) — Auth Required
+
+- **Authenticated form**: name and email pre-filled from JWT; no manual entry
+- **Fields**: Question Title (200-char limit), Description (2000-char limit) with live character counters, Category dropdown, Tags
+- **Chip-based tag input** — Enter or comma to add, Backspace to remove, max 5 tags
+- **Real-time duplicate detection** — debounced 600 ms call to `/api/questions/similar`; collapsible SimilarPanel shows matching FAQs with expandable answers before the user submits
+- **Spam prevention** — server enforces one question per registered email per hour; returns `429` with remaining wait time if exceeded
+- **Animated success screen** with pending-review status message and navigation options
+
+### 2.4 Authentication (`/auth`)
+
+- **Tabbed login / signup UI** on a single page (`/auth`)
+- **JWT-based stateless auth** — 7-day expiry, stored in `localStorage`
+- **bcrypt password hashing** (salt rounds: 10) — passwords are never stored in plaintext
+- **Protected routes** — guests redirected to `/auth` with a return-path query param; after login they land back on the intended page
+- **`/api/auth/me`** — validates token and returns current user (used on app load for session restore)
+
+### 2.5 AI-Powered Answer Suggestions
+
+- Available to logged-in users on the question detail page
+- Collapsible **🤖 AI Suggest** panel above the answer textarea
+- On first expand, calls `GET /api/suggest?q=
`
+- **Keyword-overlap scoring engine** (no external API required):
+ 1. Tokenises the question, filters stop words
+ 2. Scores every approved FAQ in the database by `keyword hits / total keywords`
+ 3. Returns the best match if score ≥ 25% confidence threshold
+- Panel displays: **confidence percentage bar**, suggested answer text, and source FAQ citation
+- **"Use this suggestion"** button pre-fills the answer textarea for editing before submit
+
+### 2.6 AI Confidence Scores on Answers
+
+- Every submitted answer receives a computed **confidence score (0–100)** stored on the Answer document at submit time
+- Scoring formula: **60% question keyword coverage + 40% FAQ corpus alignment**
+- Displayed as a colour-coded animated progress bar under each answer:
+ - 🟢 ≥ 70% — High confidence
+ - 🟡 ≥ 40% — Medium confidence
+ - 🔴 < 40% — Low confidence
+- Visible on the **Question Detail page** and on the user's **Profile page**
+
+### 2.7 Reputation & Leaderboard System
+
+#### Points Table
+
+| Action | Points |
+|--------|--------|
+| Ask a question | +2 pts |
+| Submit an answer | +5 pts |
+| Answer gets approved | +15 pts |
+| Answer receives an upvote | +10 pts |
+
+#### Levels
+
+| Level | Threshold |
+|-------|-----------|
+| 🌱 Beginner | 0 – 49 pts |
+| ⭐ Contributor | 50 – 199 pts |
+| 🏆 Expert | 200+ pts |
+
+- Level auto-recalculated on every point award (`recalcLevel()` on the User model)
+- **Upvote toggle** on each answer — tracks voters in `upvotedBy[]` to prevent double-voting; requires auth; grayed out for guests with tooltip
+- **Leaderboard page** (`/leaderboard`) — ranked cards with progress bars, medal icons for top 3, level badges, stats row, and a points-system explanation panel
+
+### 2.8 User Profile Page (`/profile`) — Auth Required
+
+- **Gradient banner** — colour scheme matches the user's current level
+- **Gradient avatar** with first-initial, level badge, and points total
+- **Level progress bar** — shows current points and points needed for next level
+- **4-stat grid**: Questions Asked · Answers Given · Answers Approved · Upvotes Received
+- **Three-tab layout**:
+ - ❓ **Questions** — all questions asked with status badges and links
+ - 💬 **Answers** — all answers given with confidence score bars, status, and upvote count
+ - ❤️ **Saved FAQs** — all bookmarked FAQs with full content preview
+- Empty-state CTAs on each tab when no content exists
+
+### 2.9 Save FAQs (Heart Bookmark)
+
+- Heart icon rendered on every FAQ card when the user is logged in
+- Filled rose when saved, hollow grey when not; click toggles
+- Save state persisted in the database (`User.savedFAQs` array of FAQ ObjectIds)
+- `POST /api/auth/save-faq/:faqId` toggles and returns the updated saved list
+- AuthContext syncs state client-side; no page reload required
+
+### 2.10 "People Like You Also Asked" Sidebar
+
+- Sticky right sidebar on the FAQ page (desktop only; hidden on mobile)
+- Appears when any FAQ accordion is expanded
+- Calls `GET /api/faqs/related?category=<>&tags=<>&exclude=<>` — returns up to 4 FAQs in the same category or sharing tags, sorted by helpful votes + views
+- **Login CTA card** shown to guests to encourage saving FAQs
+
+### 2.11 Dark Mode
+
+- Sun/moon toggle in the Navbar
+- Persisted to `localStorage` key `crowdfaq-theme`
+- OS preference detected on first visit (`prefers-color-scheme`)
+- Blocking inline script in `index.html` applies the theme class before React paints — eliminates flash of wrong theme
+- All pages, cards, inputs, buttons, and badges have complete `dark:` variants
+
+---
+
+## 3. Architecture
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ Browser Client │
+│ React 18 + Vite · React Router v6 · Axios · Tailwind v3 │
+│ │
+│ Pages: FAQ · Auth · Ask · Community · Detail · Leaderboard │
+│ · Profile │
+│ Context: AuthContext · ThemeContext │
+└──────────────────────────┬──────────────────────────────────┘
+ │ /api/* proxy (Vite dev)
+ ▼
+┌─────────────────────────────────────────────────────────────┐
+│ Express API Server │
+│ Node.js · Express 4 · Mongoose · JWT · bcrypt │
+│ │
+│ Routes: /auth · /faqs · /questions · /answers │
+│ · /users · /suggest │
+│ Middleware: requireAuth · optionalAuth │
+└──────────────────────────┬──────────────────────────────────┘
+ │ Mongoose ODM
+ ▼
+┌─────────────────────────────────────────────────────────────┐
+│ MongoDB │
+│ Collections: users · faqs · questions · answers │
+└─────────────────────────────────────────────────────────────┘
+```
+
+### Key Design Decisions
+
+| Decision | Rationale |
+|----------|-----------|
+| Vite `/api` proxy | Eliminates CORS setup in development; no extra config |
+| `answerCount` denormalized on Question | Avoids expensive `countDocuments()` on every list render; atomically incremented |
+| Optimistic UI on answer submit | Answer appears instantly without a round-trip refetch |
+| `author` defaults to `"Anonymous"` | Legacy compatibility for pre-auth answers |
+| JWT in `localStorage` | Simple stateless auth; works seamlessly with Vite proxy |
+| Blocking theme script in `index.html` | Prevents flash of wrong theme before React hydrates |
+| `max-h` CSS trick for accordion | Smooth CSS animation with zero JS height measurement |
+| Global `.card`, `.btn-primary` in `index.css` | Consistent design tokens; avoids duplicating long Tailwind utility chains |
+| Confidence score stored at submit time | Pre-computed value ensures instant display; no on-the-fly scoring cost |
+| `concurrently` in root `package.json` | Single `npm run dev` starts both servers with colour-coded output |
+
+---
+
+## 4. Data Models
+
+### User
+| Field | Type | Notes |
+|-------|------|-------|
+| `name` | String | Required |
+| `email` | String | Unique, lowercase |
+| `password` | String | bcrypt-hashed, `select: false` |
+| `points` | Number | Default 0 |
+| `level` | Enum | Beginner / Contributor / Expert |
+| `stats` | Object | `questionsAsked`, `answersGiven`, `answersApproved`, `upvotesReceived` |
+| `savedFAQs` | [ObjectId] | Refs to FAQ collection |
+
+### FAQ
+| Field | Type | Notes |
+|-------|------|-------|
+| `question` | String | Required |
+| `answer` | String | Required |
+| `category` | String | — |
+| `tags` | [String] | — |
+| `keywords` | [String] | Used for AI scoring |
+| `status` | Enum | approved / pending / rejected |
+| `helpfulYes` | Number | Vote count |
+| `helpfulNo` | Number | Vote count |
+| `views` | Number | Incremented on accordion open |
+
+### Question
+| Field | Type | Notes |
+|-------|------|-------|
+| `title` | String | Required |
+| `description` | String | — |
+| `name` | String | From auth user |
+| `email` | String | From auth user |
+| `category` | String | — |
+| `tags` | [String] | — |
+| `status` | Enum | pending / approved / rejected |
+| `answerCount` | Number | Denormalized; incremented on answer POST |
+
+### Answer
+| Field | Type | Notes |
+|-------|------|-------|
+| `questionId` | ObjectId | Ref to Question |
+| `body` | String | Required |
+| `author` | String | From auth user name |
+| `authorId` | ObjectId | Ref to User (for point awards) |
+| `status` | Enum | pending / approved / rejected |
+| `confidence` | Number | 0–100; computed at submit |
+| `upvotes` | Number | Count |
+| `upvotedBy` | [String] | Voter emails; prevents double-voting |
+
+---
+
+## 5. API Reference
+
+### Authentication
+
+| Method | Endpoint | Auth | Description |
+|--------|----------|------|-------------|
+| `POST` | `/api/auth/register` | No | Create account |
+| `POST` | `/api/auth/login` | No | Log in, returns JWT |
+| `GET` | `/api/auth/me` | Yes | Current user + populated savedFAQs |
+| `POST` | `/api/auth/save-faq/:faqId` | Yes | Toggle save/unsave an FAQ |
+
+### FAQs
+
+| Method | Endpoint | Auth | Description |
+|--------|----------|------|-------------|
+| `GET` | `/api/faqs` | No | List FAQs (`?search`, `?category`, `?sort`) |
+| `GET` | `/api/faqs/:id` | No | Single FAQ |
+| `POST` | `/api/faqs/:id/vote` | No | Vote helpful / not helpful |
+| `GET` | `/api/faqs/related` | No | Related FAQs by category/tags |
+
+### Community Questions
+
+| Method | Endpoint | Auth | Description |
+|--------|----------|------|-------------|
+| `GET` | `/api/questions` | No | List questions (`?status`, `?search`, `?category`) |
+| `POST` | `/api/questions` | Yes | Create question (+2 pts) |
+| `GET` | `/api/questions/:id` | No | Single question |
+| `GET` | `/api/questions/similar` | No | Duplicate detection (`?q=`) |
+| `PATCH` | `/api/questions/:id/status` | No | Approve / Reject / Pending |
+
+### Answers
+
+| Method | Endpoint | Auth | Description |
+|--------|----------|------|-------------|
+| `GET` | `/api/answers/:questionId` | No | List answers for a question |
+| `POST` | `/api/answers` | Yes | Submit answer (+5 pts, confidence scored) |
+| `PATCH` | `/api/answers/:id/status` | No | Approve (+15 pts to author) / Reject / Pending |
+| `POST` | `/api/answers/:id/upvote` | Yes | Toggle upvote (+10 pts to author) |
+
+### AI & Users
+
+| Method | Endpoint | Auth | Description |
+|--------|----------|------|-------------|
+| `GET` | `/api/suggest?q=` | No | AI-suggested answer from FAQ corpus |
+| `GET` | `/api/users/leaderboard` | No | Top contributors sorted by points |
+| `GET` | `/api/users/:id` | No | Public user profile |
+| `GET` | `/api/users/me/profile` | Yes | Full profile: questions + answers + savedFAQs |
+| `GET` | `/api/health` | No | Health check |
+
+---
+
+## 6. Project Structure
+
+```
+crowdfaq/
+├── package.json # Root: dev, seed, build, start scripts
+├── .gitignore
+├── .gitattributes
+├── README.md
+├── CONTEXT.md # Full development history & decisions
+├── PRODUCT.md # This file
+│
+├── server/
+│ ├── index.js # Express app entry, route mounts
+│ ├── seed.js # Seeds 10 sample internship FAQs
+│ ├── .env.example # Template — copy to .env
+│ ├── middleware/
+│ │ └── auth.js # signToken, requireAuth, optionalAuth
+│ ├── models/
+│ │ ├── User.js
+│ │ ├── FAQ.js
+│ │ ├── Question.js
+│ │ └── Answer.js
+│ └── routes/
+│ ├── auth.js # register, login, me, save-faq
+│ ├── faqs.js # list, vote, related
+│ ├── questions.js # CRUD, similar, status
+│ ├── answers.js # CRUD, upvote, status, confidence
+│ ├── suggest.js # AI keyword-overlap engine
+│ └── users.js # leaderboard, profile
+│
+└── client/
+ ├── index.html # Theme flash-prevention script
+ ├── vite.config.js # Port 3000, /api proxy → :5000
+ ├── tailwind.config.js # darkMode: 'class', Inter font, custom animations
+ ├── postcss.config.js
+ └── src/
+ ├── main.jsx
+ ├── App.jsx # Router, AuthProvider, ThemeProvider
+ ├── index.css # Design tokens, dark variants, utilities
+ ├── context/
+ │ ├── AuthContext.jsx
+ │ └── ThemeContext.jsx
+ ├── services/
+ │ └── api.js # Axios instance + all API functions
+ ├── utils/
+ │ └── constants.js # CATEGORIES, timeAgo(), STATUS_META, CATEGORY_COLORS
+ ├── components/
+ │ ├── Navbar.jsx
+ │ ├── Footer.jsx
+ │ ├── AccordionItem.jsx
+ │ ├── Spinner.jsx
+ │ ├── ErrorMessage.jsx
+ │ └── ProtectedRoute.jsx
+ └── pages/
+ ├── FAQPage.jsx
+ ├── AuthPage.jsx
+ ├── AskQuestionPage.jsx
+ ├── CommunityPage.jsx
+ ├── QuestionDetailPage.jsx
+ ├── LeaderboardPage.jsx
+ └── ProfilePage.jsx
+```
+
+---
+
+## 7. Frontend Pages & Routes
+
+| Route | Auth Required | Description |
+|-------|:---:|-------------|
+| `/` | No | FAQ browser — search, filter, vote, AI sidebar, save |
+| `/auth` | No | Tabbed login / signup |
+| `/ask` | **Yes** | Submit a community question with duplicate detection |
+| `/community` | No | Browse & moderate community questions |
+| `/community/:id` | No (answer: Yes) | Question detail, answers, confidence bars, AI suggest |
+| `/leaderboard` | No | Top contributors ranked by reputation |
+| `/profile` | **Yes** | Personal stats, history, and saved FAQs |
+
+---
+
+## 8. Design System
+
+### Colour Palette
+- **Primary**: Indigo/Violet gradient (`indigo-600` → `violet-600`)
+- **Background (light)**: `slate-50`
+- **Background (dark)**: `gray-900` / `gray-800`
+- **Typography**: [Inter](https://fonts.google.com/specimen/Inter) from Google Fonts
+
+### Utility Classes (`index.css`)
+
+| Class | Purpose |
+|-------|---------|
+| `.btn-primary` | Gradient button with shadow glow, hover lift |
+| `.btn-secondary` | Outlined secondary action button |
+| `.btn-success` / `.btn-danger` | Status action buttons |
+| `.card` | White/dark surface with border and shadow |
+| `.card-hover` | Adds lift + shadow transition on hover |
+| `.stat-card` | Dashboard-style metric tile |
+| `.input-field` | Consistent form input styling |
+| `.select-field` | Dropdown select styling |
+| `.tag-chip` | Small pill for tag display |
+| `.badge` | Inline status badge |
+| `.glass` | Glassmorphism: `bg-white/80 backdrop-blur-md` |
+| `.gradient-text` | Indigo-to-violet gradient text |
+| `.confidence-bar-track` / `-fill` | Animated progress bar |
+| `.glow-primary` | Box-shadow glow in primary colour |
+| `.animate-slide-up` | Subtle entrance animation |
+| `.no-scrollbar` | Hides scrollbar cross-browser |
+
+### Animations (Tailwind config)
+- `fadeIn` — opacity 0→1, 0.3s
+- `slideDown` — translate-y -10px→0 + opacity, 0.3s
+
+---
+
+## 9. Environment Variables
+
+**File**: `server/.env` (gitignored — copy from `server/.env.example`)
+
+| Variable | Default | Description |
+|----------|---------|-------------|
+| `MONGO_URI` | `mongodb://localhost:27017/crowdfaq` | MongoDB connection string (local or Atlas) |
+| `PORT` | `5000` | Express API port |
+| `JWT_SECRET` | *(must set)* | Secret for signing auth tokens — **change before deploying** |
+
+---
+
+## 10. Getting Started
+
+### Prerequisites
+
+| Tool | Version |
+|------|---------|
+| Node.js | v18+ |
+| MongoDB | 6+ (local or Atlas) |
+
+### Setup
+
+```bash
+# 1. Clone
+git clone https://github.com/Just-Here-TO-Code/cs-faq.git
+cd cs-faq
+
+# 2. Install all dependencies (root + server + client)
+npm install
+
+# 3. Configure environment
+# macOS/Linux:
+cp server/.env.example server/.env
+# Windows PowerShell:
+Copy-Item server\.env.example server\.env
+
+# 4. Seed sample FAQs (required for AI suggestions to work)
+npm run seed
+
+# 5. Start both servers
+npm run dev
+```
+
+| Service | URL |
+|---------|-----|
+| Frontend (React + Vite) | http://localhost:3000 |
+| Backend (Express API) | http://localhost:5000 |
+
+### npm Scripts
+
+| Script | Description |
+|--------|-------------|
+| `npm install` | Install root + server + client dependencies |
+| `npm run dev` | Start API (nodemon) + frontend (Vite) concurrently |
+| `npm run seed` | Seed 10 sample internship FAQs into MongoDB |
+| `npm run build` | Build React app to `client/dist/` |
+| `npm run start` | Run Express in production mode |
+
+---
+
+## 11. Dependencies
+
+### Server
+
+| Package | Purpose |
+|---------|---------|
+| `express ^4.19` | HTTP server framework |
+| `mongoose ^8.4` | MongoDB ODM |
+| `bcryptjs` | Password hashing |
+| `jsonwebtoken` | JWT signing & verification |
+| `cors ^2.8` | CORS headers |
+| `dotenv ^16.4` | `.env` loader |
+| `nodemon ^3.1` (dev) | Auto-restart on file change |
+
+### Client
+
+| Package | Purpose |
+|---------|---------|
+| `react ^18.3` | UI library |
+| `react-dom ^18.3` | DOM renderer |
+| `react-router-dom ^6.24` | Client-side routing |
+| `axios ^1.7` | HTTP client |
+| `vite ^5.3` (dev) | Build tool & dev server |
+| `tailwindcss ^3.4` (dev) | Utility-first CSS |
+| `autoprefixer ^10.4` (dev) | CSS vendor prefixes |
+| `postcss ^8.4` (dev) | CSS processor |
+
+---
+
+## 12. Security Notes
+
+- `server/.env` is gitignored — only `.env.example` is committed
+- Passwords stored as bcrypt hashes (cost factor 10); never returned in API responses (`select: false`)
+- JWT tokens expire after **7 days**
+- Use a strong, randomly-generated `JWT_SECRET` in production
+- The AI suggestion engine runs entirely on the local MongoDB corpus — no external API keys or data sharing
+- For production: build the client (`npm run build`), serve `client/dist/` via a reverse proxy (nginx/Caddy), and run the Express API behind it
+
+---
+
+## 13. Troubleshooting
+
+| Problem | Solution |
+|---------|----------|
+| `MongoDB connection error` | Ensure MongoDB is running; verify `MONGO_URI` in `server/.env` |
+| `Port 5000 already in use` | Change `PORT` in `server/.env` and update Vite proxy in `client/vite.config.js` |
+| `Port 3000 already in use` | Change `server.port` in `client/vite.config.js` |
+| `401` on posting questions/answers | Log in first — JWT is required |
+| Empty FAQ page after install | Run `npm run seed` to populate sample data |
+| AI suggestion returns nothing | Seed must be loaded; try questions about eligibility, stipend, or certificates |
+| `npm install` fails | Use Node.js v18+; delete all `node_modules` folders and retry |
+
+---
+
+## 14. Roadmap
+
+The following features are scoped for future releases:
+
+| # | Feature | Notes |
+|---|---------|-------|
+| 1 | **Pagination** | `?page=` & `?limit=` on all list endpoints; pagination UI components |
+| 2 | **Admin Panel** | Dedicated `/admin` protected route; role-based access; direct FAQ CRUD |
+| 3 | **Email Notifications** | Nodemailer — notify question author when an answer is posted |
+| 4 | **Rich Text Editor** | Markdown support for answers (`react-markdown` + `remark`) |
+| 5 | **Delete Functionality** | Hard-delete questions and answers (currently only status changes supported) |
+
+---
+
+## 15. See Also
+
+- [`README.md`](./README.md) — Quick-start guide for developers
+- [`CONTEXT.md`](./CONTEXT.md) — Full development history, session log, and architecture decisions
+
+---
+
+*Last updated: 2026-06-28 — CrowdFAQ v1.0.0*
diff --git a/README.md b/README.md
index edec17a..343c644 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,324 @@
-# cs53
\ No newline at end of file
+# CrowdFAQ
+
+A crowd-sourced FAQ platform built with the **MERN stack** (MongoDB, Express, React, Node.js) and **Tailwind CSS v3**.
+
+Browse FAQs and community questions without an account. **Sign up or log in** to ask questions and submit answers. Includes dark mode, voting, categories, duplicate detection, a moderation workflow, AI-powered answer suggestions, and a reputation/leaderboard system.
+
+---
+
+## ✨ Features
+### Core
+- **FAQ browser** — search, filter by category, sort by latest / helpful / views, yes/no voting
+- **Community Q&A** — ask questions, view answers, moderation (approve / reject / pending)
+- **Authentication** — JWT login & signup; protected question and answer submission
+- **Dark mode** — toggle with persistence across sessions
+
+### Smart Features
+- **Duplicate detection** — warns when similar FAQs or questions already exist before submitting
+- **Spam prevention** — one question per account per hour (server-side enforcement)
+- **Multi-field search** — searches question text, answer text, tags, keywords, and category simultaneously
+- **Sort modes** — sort FAQs by Latest, Most Helpful, or Most Viewed
+
+### Moderation
+- **Question moderation** — Approve / Reject / Mark Pending on any community question
+- **Answer moderation** — Approve / Reject / Mark Pending on any individual answer
+- **Status tabs** — filter questions and answers by status (all / pending / approved / rejected)
+
+### 🤖 AI-Powered Suggested Answers
+- When a logged-in user opens a pending question, an **AI Suggest** panel appears above the answer form
+- Analyses question text against all approved FAQs using keyword-overlap scoring
+- Displays the best-matching FAQ answer with a **confidence percentage bar**
+- Admin/user can click **"Use this suggestion"** to pre-fill the answer textarea
+
+### 🎯 AI Confidence Scores on Answers
+- Every submitted answer receives a computed **confidence score (0–100%)**
+- Score blends: keyword coverage of the question (60%) + alignment with existing FAQ content (40%)
+- Displayed as a colour-coded bar under each answer (🟢 ≥70%, 🟡 ≥40%, 🔴 <40%)
+- Visible on the question detail page and on the user's profile page
+
+### 🏆 Reputation System
+- **Points** awarded for contributions:
+
+ | Action | Points |
+ |--------|--------|
+ | Ask a question | +2 pts |
+ | Submit an answer | +5 pts |
+ | Answer gets approved | +15 pts |
+ | Answer receives an upvote | +10 pts |
+
+- **Levels** based on total points: 🌱 Beginner (0–49) · ⭐ Contributor (50–199) · 🏆 Expert (200+)
+- **Upvote buttons** on each answer (toggle; logged-in users only)
+- **Leaderboard page** (`/leaderboard`) ranks top contributors with progress bars, level badges, and stats
+
+### 👤 User Profile Page
+- Beautiful profile page at `/profile` with gradient banner matching the user's level
+- **Level progress bar** — shows current points and points needed for next level
+- **4-stat grid**: Questions Asked, Answers Given, Answers Approved, Upvotes Received
+- **Tabbed sections**:
+ - ❓ **Questions** — all questions asked, with status badges, links to detail page
+ - 💬 **Answers** — all answers given, with confidence score bars, status, upvote count
+ - ❤️ **Saved FAQs** — all bookmarked FAQs with full content preview
+- Navbar avatar is now a clickable link to the profile page
+
+### ❤️ Save FAQs
+- Heart icon on every FAQ card (visible when logged in)
+- Toggles save/unsave; saved state persisted in database (User model)
+- Saved FAQs appear in the profile page's **Saved FAQs** tab
+
+### 👥 People Like You Also Asked
+- Sidebar panel on the FAQ page that appears when a FAQ is expanded
+- Shows up to 4 related FAQs sharing the same **category** or **tags**
+- On desktop (large screens) as a sticky right rail
+- Login CTA card shown to guests to encourage saving FAQs
+
+### 🎨 Modernised UI
+- `btn-primary` now uses gradient + shadow glow
+- `card-hover` adds lift on hover with shadow transition
+- `stat-card` component for dashboard-style stat tiles
+- `confidence-bar` utility classes for animated progress bars
+- No-scrollbar utility, `glow-primary`, `slide-up` animation
+- FAQ hero has decorative blur blobs and quick-stats bar with progress bars, level badges, and stats
+
+---
+
+## Tech Stack
+
+| Layer | Technology |
+|-------|------------|
+| Frontend | React 18, Vite, React Router, Tailwind CSS v3, Axios |
+| Backend | Node.js, Express 4, Mongoose |
+| Database | MongoDB |
+| Auth | JWT + bcrypt |
+
+---
+
+## Prerequisites
+
+Install these before cloning:
+
+| Tool | Version | Notes |
+|------|---------|-------|
+| [Node.js](https://nodejs.org/) | v18+ | Includes npm |
+| [MongoDB](https://www.mongodb.com/try/download/community) | 6+ | Local install **or** [MongoDB Atlas](https://www.mongodb.com/atlas) free tier |
+
+**MongoDB must be reachable** before starting the server.
+
+| Platform | Start MongoDB locally |
+|----------|----------------------|
+| Windows | `net start MongoDB` or open MongoDB Compass |
+| macOS | `brew services start mongodb-community` |
+| Linux | `sudo systemctl start mongod` |
+
+---
+
+## Getting Started (Clone & Run)
+
+### 1. Clone the repository
+
+```bash
+git clone https://github.com/Just-Here-TO-Code/cs-faq.git
+cd cs-faq
+```
+
+### 2. Install dependencies
+
+From the project root:
+
+```bash
+npm install
+```
+
+This installs root, `server/`, and `client/` packages automatically via the `postinstall` script.
+
+### 3. Configure environment variables
+
+Copy the example env file and edit if needed:
+
+**macOS / Linux**
+```bash
+cp server/.env.example server/.env
+```
+
+**Windows (PowerShell)**
+```powershell
+Copy-Item server\.env.example server\.env
+```
+
+Default `server/.env` values work for local development:
+
+```env
+MONGO_URI=mongodb://localhost:27017/crowdfaq
+PORT=5000
+JWT_SECRET=your-super-secret-jwt-key-change-me
+```
+
+| Variable | Description |
+|----------|-------------|
+| `MONGO_URI` | MongoDB connection string (local or Atlas) |
+| `PORT` | Express API port (default `5000`) |
+| `JWT_SECRET` | Secret for signing auth tokens — **change in production** |
+
+**Using MongoDB Atlas?** Replace `MONGO_URI` with your Atlas connection string, e.g.:
+
+```env
+MONGO_URI=mongodb+srv://:@.mongodb.net/crowdfaq
+```
+
+### 4. Seed sample data
+
+Populates the database with 10 sample FAQs (required for AI suggestions to work):
+
+```bash
+npm run seed
+```
+
+### 5. Start the development servers
+
+```bash
+npm run dev
+```
+
+| Service | URL |
+|---------|-----|
+| **Frontend** (React + Vite) | http://localhost:3000 |
+| **Backend** (Express API) | http://localhost:5000 |
+
+The Vite dev server proxies `/api/*` to the Express server, so no extra CORS setup is needed during development.
+
+### 6. Try it out
+
+1. Open http://localhost:3000 — browse FAQs
+2. Go to **Log In** → **Sign Up** to create an account
+3. Visit **Ask Question** to submit a community question
+4. Open **Community** to view and moderate questions
+5. Click a question → answer it → see the 🤖 AI suggestion panel
+6. Check **🏆 Leaderboard** to see top contributors
+
+---
+
+## Project Structure
+
+```
+crowdfaq/
+├── package.json # Root scripts (dev, seed, build)
+├── .gitignore
+├── README.md
+├── CONTEXT.md # Development history & decisions
+│
+├── server/ # Express API
+│ ├── index.js
+│ ├── seed.js
+│ ├── .env.example # Template — copy to .env (not committed)
+│ ├── middleware/auth.js
+│ ├── models/ # User, FAQ, Question, Answer
+│ └── routes/ # auth, faqs, questions, answers, users, suggest
+│
+└── client/ # React + Vite frontend
+ ├── vite.config.js # Dev proxy: /api → localhost:5000
+ └── src/
+ ├── pages/ # FAQ, Auth, Ask, Community, Detail, Leaderboard
+ ├── components/
+ ├── context/ # Theme + Auth providers
+ └── services/api.js
+```
+
+---
+
+## npm Scripts
+
+Run all commands from the **project root**:
+
+| Script | Description |
+|--------|-------------|
+| `npm install` | Install dependencies (root + server + client) |
+| `npm run dev` | Start API (nodemon) + frontend (Vite) together |
+| `npm run seed` | Seed 10 sample FAQs into MongoDB |
+| `npm run build` | Build React app to `client/dist/` |
+| `npm run start` | Run Express in production mode (`node index.js`) |
+
+---
+
+## Pages & Routes
+
+| Route | Auth | Description |
+|-------|------|-------------|
+| `/` | No | FAQ page with search, filters, voting, People-like-you sidebar |
+| `/auth` | No | Login & signup |
+| `/ask` | Yes | Submit a community question |
+| `/community` | No | Browse questions with moderation tabs |
+| `/community/:id` | No (answer: Yes) | Question detail + answers + confidence scores + AI suggestion |
+| `/leaderboard` | No | Top contributors ranked by reputation points |
+| `/profile` | Yes | Personal profile: stats, questions, answers, saved FAQs |
+
+---
+
+## API Overview
+
+| Method | Endpoint | Auth | Description |
+|--------|----------|------|-------------|
+| `POST` | `/api/auth/register` | No | Create account |
+| `POST` | `/api/auth/login` | No | Log in |
+| `GET` | `/api/auth/me` | Yes | Current user |
+| `GET` | `/api/faqs` | No | List FAQs (`?search`, `?category`, `?sort`) |
+| `POST` | `/api/faqs/:id/vote` | No | Vote helpful / not helpful |
+| `GET` | `/api/questions` | No | List community questions |
+| `POST` | `/api/questions` | Yes | Create question (+2 pts) |
+| `GET` | `/api/answers/:questionId` | No | List answers |
+| `POST` | `/api/answers` | Yes | Submit answer (+5 pts) |
+| `PATCH` | `/api/answers/:id/status` | No | Approve answer (+15 pts to author) |
+| `POST` | `/api/answers/:id/upvote` | Yes | Toggle upvote (+10 pts to author) |
+| `GET` | `/api/suggest?q=` | No | AI-suggested answer from FAQ data |
+| `GET` | `/api/users/leaderboard` | No | Top contributors by points |
+| `GET` | `/api/health` | No | Health check |
+
+---
+
+## Troubleshooting
+
+| Problem | Solution |
+|---------|----------|
+| `MongoDB connection error` | Ensure MongoDB is running; check `MONGO_URI` in `server/.env` |
+| `Port 5000 already in use` | Change `PORT` in `server/.env` and update Vite proxy in `client/vite.config.js` |
+| `Port 3000 already in use` | Change `server.port` in `client/vite.config.js` |
+| `401` when posting questions | Log in first — auth token is required |
+| Empty FAQ page | Run `npm run seed` to populate sample data |
+| AI suggestion returns nothing | The seeded FAQs need to match some keywords; try questions about eligibility, stipend, certificates, etc. |
+| `npm install` fails | Use Node.js v18+; delete `node_modules` folders and retry |
+
+---
+
+## Publishing to GitHub
+
+When you are ready to push this project:
+
+```bash
+git init
+git add .
+git status # verify .env and node_modules are NOT listed
+git commit -m "Initial commit: CrowdFAQ MERN app"
+git branch -M main
+git remote add origin https://github.com/Just-Here-TO-Code/cs-faq.git
+git push -u origin main
+```
+
+> **Never commit** `server/.env` — it is listed in `.gitignore`. Only `server/.env.example` is tracked.
+
+---
+
+## Environment & Security Notes
+
+- `server/.env` is gitignored; each developer copies from `.env.example`
+- Use a strong, unique `JWT_SECRET` in production
+- For production deployment, build the client (`npm run build`) and serve `client/dist/` behind a reverse proxy alongside the API
+
+---
+
+## License
+
+This project is provided as-is for learning and portfolio use.
+
+---
+
+## See Also
+
+- [`PRODUCT.md`](./PRODUCT.md) — full product specification, architecture, API reference, and feature catalogue
diff --git a/client/index.html b/client/index.html
new file mode 100644
index 0000000..b89d2b1
--- /dev/null
+++ b/client/index.html
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+ CrowdFAQ — Community Knowledge Base
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/client/package-lock.json b/client/package-lock.json
new file mode 100644
index 0000000..a9978eb
--- /dev/null
+++ b/client/package-lock.json
@@ -0,0 +1,3020 @@
+{
+ "name": "crowdfaq-client",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "crowdfaq-client",
+ "version": "0.0.0",
+ "dependencies": {
+ "axios": "^1.7.2",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-router-dom": "^6.24.0"
+ },
+ "devDependencies": {
+ "@types/react": "^18.3.3",
+ "@types/react-dom": "^18.3.0",
+ "@vitejs/plugin-react": "^4.3.1",
+ "autoprefixer": "^10.4.19",
+ "postcss": "^8.4.39",
+ "tailwindcss": "^3.4.4",
+ "vite": "^5.3.1"
+ }
+ },
+ "node_modules/@alloc/quick-lru": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
+ "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
+ "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
+ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-compilation-targets": "^7.29.7",
+ "@babel/helper-module-transforms": "^7.29.7",
+ "@babel/helpers": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
+ "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
+ "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.29.7",
+ "@babel/helper-validator-option": "^7.29.7",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
+ "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
+ "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
+ "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
+ "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
+ "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
+ "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
+ "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.7"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz",
+ "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz",
+ "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
+ "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
+ "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-globals": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
+ "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "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/@remix-run/router": {
+ "version": "1.23.3",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.3.tgz",
+ "integrity": "sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz",
+ "integrity": "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.1.tgz",
+ "integrity": "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz",
+ "integrity": "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.1.tgz",
+ "integrity": "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.1.tgz",
+ "integrity": "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.1.tgz",
+ "integrity": "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.1.tgz",
+ "integrity": "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.1.tgz",
+ "integrity": "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.1.tgz",
+ "integrity": "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.1.tgz",
+ "integrity": "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.1.tgz",
+ "integrity": "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.1.tgz",
+ "integrity": "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.1.tgz",
+ "integrity": "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.1.tgz",
+ "integrity": "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.1.tgz",
+ "integrity": "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.1.tgz",
+ "integrity": "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.1.tgz",
+ "integrity": "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz",
+ "integrity": "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.1.tgz",
+ "integrity": "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.1.tgz",
+ "integrity": "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.1.tgz",
+ "integrity": "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.1.tgz",
+ "integrity": "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.1.tgz",
+ "integrity": "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.1.tgz",
+ "integrity": "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.1.tgz",
+ "integrity": "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.15",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "18.3.31",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz",
+ "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "18.3.7",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
+ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^18.0.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.27",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.17.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/any-promise": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/arg": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/autoprefixer": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz",
+ "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.28.2",
+ "caniuse-lite": "^1.0.30001787",
+ "fraction.js": "^5.3.4",
+ "picocolors": "^1.1.1",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/axios": {
+ "version": "1.17.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.17.0.tgz",
+ "integrity": "sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.16.0",
+ "form-data": "^4.0.5",
+ "https-proxy-agent": "^5.0.1",
+ "proxy-from-env": "^2.1.0"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.34",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.34.tgz",
+ "integrity": "sha512-IMDedajPifLnHNY0X9n8hKxRTQ6/eTHwr5bDo04WnuqxyKw6LYtQywCuuqPZwhl3aBXMvQpJov42GLCwRRdQzw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "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/browserslist": {
+ "version": "4.28.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.10.12",
+ "caniuse-lite": "^1.0.30001782",
+ "electron-to-chromium": "^1.5.328",
+ "node-releases": "^2.0.36",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/camelcase-css": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001797",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001797.tgz",
+ "integrity": "sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chokidar/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/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/commander": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/didyoumean": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/dlv": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.370",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.370.tgz",
+ "integrity": "sha512-D5tSHJReAb/Kf3Hu9F/GO4lJuSWzEWHwvQ/kKSUP7pimNgvxkSKj+gUQhHpKKACwrin7rS3byU7IxreF56rl5g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+ "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "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/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/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/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==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fraction.js": {
+ "version": "5.3.4",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
+ "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/rawify"
+ }
+ },
+ "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/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "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/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.2",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
+ "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "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/jiti": {
+ "version": "1.21.7",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
+ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "bin/jiti.js"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/lilconfig": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
+ "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antonk52"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "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/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "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==",
+ "license": "MIT"
+ },
+ "node_modules/mz": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+ "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0",
+ "object-assign": "^4.0.1",
+ "thenify-all": "^1.0.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.12",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
+ "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.47",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz",
+ "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-hash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "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/pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pirates": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
+ "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.15",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
+ "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.12",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postcss-import": {
+ "version": "15.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
+ "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "postcss-value-parser": "^4.0.0",
+ "read-cache": "^1.0.0",
+ "resolve": "^1.1.7"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.0.0"
+ }
+ },
+ "node_modules/postcss-js": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
+ "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "camelcase-css": "^2.0.1"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >= 16"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.21"
+ }
+ },
+ "node_modules/postcss-load-config": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
+ "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "lilconfig": "^3.1.1"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "peerDependencies": {
+ "jiti": ">=1.21.0",
+ "postcss": ">=8.0.9",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ },
+ "postcss": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/postcss-nested": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
+ "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "postcss-selector-parser": "^6.1.1"
+ },
+ "engines": {
+ "node": ">=12.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.14"
+ }
+ },
+ "node_modules/postcss-selector-parser": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/proxy-from-env": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+ "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "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/react": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-router": {
+ "version": "6.30.4",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.4.tgz",
+ "integrity": "sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.23.3"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "6.30.4",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.4.tgz",
+ "integrity": "sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.23.3",
+ "react-router": "6.30.4"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
+ "node_modules/read-cache": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+ "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pify": "^2.3.0"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.12",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
+ "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "is-core-module": "^2.16.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "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/rollup": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.61.1.tgz",
+ "integrity": "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.9"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.61.1",
+ "@rollup/rollup-android-arm64": "4.61.1",
+ "@rollup/rollup-darwin-arm64": "4.61.1",
+ "@rollup/rollup-darwin-x64": "4.61.1",
+ "@rollup/rollup-freebsd-arm64": "4.61.1",
+ "@rollup/rollup-freebsd-x64": "4.61.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.61.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.61.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.61.1",
+ "@rollup/rollup-linux-arm64-musl": "4.61.1",
+ "@rollup/rollup-linux-loong64-gnu": "4.61.1",
+ "@rollup/rollup-linux-loong64-musl": "4.61.1",
+ "@rollup/rollup-linux-ppc64-gnu": "4.61.1",
+ "@rollup/rollup-linux-ppc64-musl": "4.61.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.61.1",
+ "@rollup/rollup-linux-riscv64-musl": "4.61.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.61.1",
+ "@rollup/rollup-linux-x64-gnu": "4.61.1",
+ "@rollup/rollup-linux-x64-musl": "4.61.1",
+ "@rollup/rollup-openbsd-x64": "4.61.1",
+ "@rollup/rollup-openharmony-arm64": "4.61.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.61.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.61.1",
+ "@rollup/rollup-win32-x64-gnu": "4.61.1",
+ "@rollup/rollup-win32-x64-msvc": "4.61.1",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "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/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "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/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/sucrase": {
+ "version": "3.35.1",
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
+ "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "commander": "^4.0.0",
+ "lines-and-columns": "^1.1.6",
+ "mz": "^2.7.0",
+ "pirates": "^4.0.1",
+ "tinyglobby": "^0.2.11",
+ "ts-interface-checker": "^0.1.9"
+ },
+ "bin": {
+ "sucrase": "bin/sucrase",
+ "sucrase-node": "bin/sucrase-node"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "3.4.19",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
+ "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@alloc/quick-lru": "^5.2.0",
+ "arg": "^5.0.2",
+ "chokidar": "^3.6.0",
+ "didyoumean": "^1.2.2",
+ "dlv": "^1.1.3",
+ "fast-glob": "^3.3.2",
+ "glob-parent": "^6.0.2",
+ "is-glob": "^4.0.3",
+ "jiti": "^1.21.7",
+ "lilconfig": "^3.1.3",
+ "micromatch": "^4.0.8",
+ "normalize-path": "^3.0.0",
+ "object-hash": "^3.0.0",
+ "picocolors": "^1.1.1",
+ "postcss": "^8.4.47",
+ "postcss-import": "^15.1.0",
+ "postcss-js": "^4.0.1",
+ "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
+ "postcss-nested": "^6.2.0",
+ "postcss-selector-parser": "^6.1.2",
+ "resolve": "^1.22.8",
+ "sucrase": "^3.35.0"
+ },
+ "bin": {
+ "tailwind": "lib/cli.js",
+ "tailwindcss": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/thenify": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+ "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0"
+ }
+ },
+ "node_modules/thenify-all": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+ "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "thenify": ">= 3.1.0 < 4"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyglobby/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "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/ts-interface-checker": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vite": {
+ "version": "5.4.21",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ }
+ }
+}
diff --git a/client/package.json b/client/package.json
new file mode 100644
index 0000000..2698112
--- /dev/null
+++ b/client/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "crowdfaq-client",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "axios": "^1.7.2",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-router-dom": "^6.24.0"
+ },
+ "devDependencies": {
+ "@types/react": "^18.3.3",
+ "@types/react-dom": "^18.3.0",
+ "@vitejs/plugin-react": "^4.3.1",
+ "autoprefixer": "^10.4.19",
+ "postcss": "^8.4.39",
+ "tailwindcss": "^3.4.4",
+ "vite": "^5.3.1"
+ }
+}
diff --git a/client/postcss.config.js b/client/postcss.config.js
new file mode 100644
index 0000000..2e7af2b
--- /dev/null
+++ b/client/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/client/src/App.jsx b/client/src/App.jsx
new file mode 100644
index 0000000..7202d88
--- /dev/null
+++ b/client/src/App.jsx
@@ -0,0 +1,39 @@
+import { BrowserRouter, Routes, Route } from 'react-router-dom'
+import { ThemeProvider } from './context/ThemeContext'
+import { AuthProvider } from './context/AuthContext'
+import Navbar from './components/Navbar'
+import Footer from './components/Footer'
+import ProtectedRoute from './components/ProtectedRoute'
+import FAQPage from './pages/FAQPage'
+import AskQuestionPage from './pages/AskQuestionPage'
+import CommunityPage from './pages/CommunityPage'
+import QuestionDetailPage from './pages/QuestionDetailPage'
+import AuthPage from './pages/AuthPage'
+import LeaderboardPage from './pages/LeaderboardPage'
+import ProfilePage from './pages/ProfilePage'
+
+export default function App() {
+ return (
+
+
+
+
+
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+
+
+
+
+
+ )
+}
diff --git a/client/src/components/AccordionItem.jsx b/client/src/components/AccordionItem.jsx
new file mode 100644
index 0000000..b25e291
--- /dev/null
+++ b/client/src/components/AccordionItem.jsx
@@ -0,0 +1,174 @@
+import { useState } from 'react'
+import { CATEGORY_COLORS } from '../utils/constants'
+import { voteFAQ } from '../services/api'
+import { useAuth } from '../context/AuthContext'
+
+export default function AccordionItem({ faq, index, relatedFAQs = [] }) {
+ const { user, saveFAQ, isFAQSaved } = useAuth()
+ const [open, setOpen] = useState(false)
+ const [votes, setVotes] = useState({ yes: faq.helpfulYes, no: faq.helpfulNo })
+ const [voted, setVoted] = useState(() => localStorage.getItem(`faq-vote-${faq._id}`) || null)
+ const [voting, setVoting] = useState(false)
+ const [saving, setSaving] = useState(false)
+
+ const saved = isFAQSaved(faq._id)
+ const catColor = CATEGORY_COLORS[faq.category] || CATEGORY_COLORS.Other
+
+ async function handleVote(v) {
+ if (voted || voting) return
+ setVoting(true)
+ try {
+ const updated = await voteFAQ(faq._id, v)
+ setVotes({ yes: updated.helpfulYes, no: updated.helpfulNo })
+ setVoted(v)
+ localStorage.setItem(`faq-vote-${faq._id}`, v)
+ } catch { /* silent */ }
+ finally { setVoting(false) }
+ }
+
+ async function handleSave(e) {
+ e.stopPropagation()
+ if (!user || saving) return
+ setSaving(true)
+ try { await saveFAQ(faq._id) }
+ catch { /* silent */ }
+ finally { setSaving(false) }
+ }
+
+ const total = votes.yes + votes.no
+ const pct = total > 0 ? Math.round((votes.yes / total) * 100) : null
+
+ return (
+
+ {/* Header */}
+
+
+ {/* Answer body */}
+
+
+
+
+ {/* Voting */}
+
+
Was this helpful?
+
+
handleVote('yes')}
+ disabled={!!voted || voting}
+ className={`flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-xs font-semibold border transition-all duration-150
+ ${voted === 'yes'
+ ? 'bg-emerald-600 text-white border-emerald-600 shadow-sm shadow-emerald-500/30'
+ : 'border-emerald-200 dark:border-emerald-800 text-emerald-700 dark:text-emerald-400 hover:bg-emerald-50 dark:hover:bg-emerald-900/20'
+ } disabled:cursor-not-allowed`}
+ >
+
+ Yes · {votes.yes}
+
+
handleVote('no')}
+ disabled={!!voted || voting}
+ className={`flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-xs font-semibold border transition-all duration-150
+ ${voted === 'no'
+ ? 'bg-red-500 text-white border-red-500 shadow-sm shadow-red-500/30'
+ : 'border-red-200 dark:border-red-800 text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20'
+ } disabled:cursor-not-allowed`}
+ >
+
+ No · {votes.no}
+
+
+
+ {/* Helpful % bar */}
+ {pct !== null && (
+
+ )}
+ {voted &&
Thanks! ✓}
+
+
+
+
+ )
+}
diff --git a/client/src/components/ErrorMessage.jsx b/client/src/components/ErrorMessage.jsx
new file mode 100644
index 0000000..b38aaed
--- /dev/null
+++ b/client/src/components/ErrorMessage.jsx
@@ -0,0 +1,15 @@
+export default function ErrorMessage({ message = 'Something went wrong. Please try again.' }) {
+ return (
+
+ )
+}
diff --git a/client/src/components/Footer.jsx b/client/src/components/Footer.jsx
new file mode 100644
index 0000000..a6ddbaf
--- /dev/null
+++ b/client/src/components/Footer.jsx
@@ -0,0 +1,26 @@
+import { Link } from 'react-router-dom'
+
+export default function Footer() {
+ return (
+
+ )
+}
diff --git a/client/src/components/Navbar.jsx b/client/src/components/Navbar.jsx
new file mode 100644
index 0000000..8e7b293
--- /dev/null
+++ b/client/src/components/Navbar.jsx
@@ -0,0 +1,139 @@
+import { useState } from 'react'
+import { NavLink, Link } from 'react-router-dom'
+import { useTheme } from '../context/ThemeContext'
+import { useAuth } from '../context/AuthContext'
+
+function SunIcon() {
+ return (
+
+ )
+}
+function MoonIcon() {
+ return (
+
+ )
+}
+
+export default function Navbar() {
+ const [menuOpen, setMenuOpen] = useState(false)
+ const { dark, toggle } = useTheme()
+ const { user, logout } = useAuth()
+
+ const linkClass = ({ isActive }) =>
+ isActive
+ ? 'text-primary-600 dark:text-primary-400 font-semibold border-b-2 border-primary-600 dark:border-primary-400 pb-0.5'
+ : 'text-slate-600 dark:text-slate-300 hover:text-primary-600 dark:hover:text-primary-400 font-medium transition-colors'
+
+ return (
+
+
+
+ {/* Logo */}
+
+
+
CrowdFAQ
+
+
+ {/* Desktop Nav */}
+
+
+ {/* Right actions */}
+
+ {/* Dark mode toggle */}
+
+ {dark ? : }
+
+ {user ? (
+
+
+
+ {user.name.charAt(0).toUpperCase()}
+
+
{user.name}
+
+
+
+
+
+ ) : (
+
+ Log In
+
+ )}
+ {/* CTA — desktop */}
+
+
+ Ask a Question
+
+ {/* Hamburger — mobile */}
+
setMenuOpen(!menuOpen)}
+ className="sm:hidden p-2 rounded-lg text-slate-500 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors"
+ aria-label="Toggle menu"
+ >
+ {menuOpen
+ ?
+ :
+ }
+
+
+
+
+ {/* Mobile menu */}
+ {menuOpen && (
+
+ {[['/', 'FAQ', true], ['/ask', 'Ask Question', false], ['/community', 'Community', false], ['/leaderboard', '🏆 Leaderboard', false]].map(([to, label, end]) => (
+ setMenuOpen(false)}
+ className="block px-3 py-2 rounded-lg text-slate-700 dark:text-slate-300 hover:bg-primary-50 dark:hover:bg-slate-700 hover:text-primary-600 dark:hover:text-primary-400 font-medium transition-colors">
+ {label}
+
+ ))}
+ {user ? (
+ <>
+ setMenuOpen(false)}
+ className="block px-3 py-2 rounded-lg text-slate-700 dark:text-slate-300 hover:bg-primary-50 dark:hover:bg-slate-700 hover:text-primary-600 dark:hover:text-primary-400 font-medium transition-colors">
+ 👤 My Profile
+
+ { logout(); setMenuOpen(false) }}
+ className="block w-full text-left px-3 py-2 rounded-lg text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 font-medium transition-colors">
+ Sign out ({user.name})
+
+ >
+ ) : (
+ setMenuOpen(false)}
+ className="block px-3 py-2 rounded-lg text-primary-600 dark:text-primary-400 hover:bg-primary-50 dark:hover:bg-slate-700 font-semibold transition-colors">
+ Log In / Sign Up
+
+ )}
+
+ )}
+
+
+ )
+}
diff --git a/client/src/components/ProtectedRoute.jsx b/client/src/components/ProtectedRoute.jsx
new file mode 100644
index 0000000..394e423
--- /dev/null
+++ b/client/src/components/ProtectedRoute.jsx
@@ -0,0 +1,22 @@
+import { Navigate, useLocation } from 'react-router-dom'
+import { useAuth } from '../context/AuthContext'
+import Spinner from './Spinner'
+
+export default function ProtectedRoute({ children }) {
+ const { user, loading } = useAuth()
+ const location = useLocation()
+
+ if (loading) {
+ return (
+
+
+
+ )
+ }
+
+ if (!user) {
+ return
+ }
+
+ return children
+}
diff --git a/client/src/components/Spinner.jsx b/client/src/components/Spinner.jsx
new file mode 100644
index 0000000..f2b5462
--- /dev/null
+++ b/client/src/components/Spinner.jsx
@@ -0,0 +1,10 @@
+export default function Spinner({ size = 'md', text = 'Loading...' }) {
+ const s = { sm: 'w-5 h-5', md: 'w-8 h-8', lg: 'w-12 h-12' }
+ return (
+
+ )
+}
diff --git a/client/src/context/AuthContext.jsx b/client/src/context/AuthContext.jsx
new file mode 100644
index 0000000..13861fc
--- /dev/null
+++ b/client/src/context/AuthContext.jsx
@@ -0,0 +1,76 @@
+import { createContext, useContext, useState, useEffect, useCallback } from 'react'
+import { login as apiLogin, register as apiRegister, fetchMe, setAuthToken, toggleSaveFAQ } from '../services/api'
+
+const AuthContext = createContext(null)
+const TOKEN_KEY = 'crowdfaq-token'
+
+export function AuthProvider({ children }) {
+ const [user, setUser] = useState(null)
+ const [loading, setLoading] = useState(true)
+
+ const logout = useCallback(() => {
+ localStorage.removeItem(TOKEN_KEY)
+ setAuthToken(null)
+ setUser(null)
+ }, [])
+
+ const applySession = useCallback((token, userData) => {
+ localStorage.setItem(TOKEN_KEY, token)
+ setAuthToken(token)
+ setUser(userData)
+ }, [])
+
+ useEffect(() => {
+ const token = localStorage.getItem(TOKEN_KEY)
+ if (!token) { setLoading(false); return }
+ setAuthToken(token)
+ fetchMe()
+ .then(data => setUser(data.user))
+ .catch(() => logout())
+ .finally(() => setLoading(false))
+ }, [logout])
+
+ async function login(email, password) {
+ const data = await apiLogin({ email, password })
+ applySession(data.token, data.user)
+ return data.user
+ }
+
+ async function register(name, email, password) {
+ const data = await apiRegister({ name, email, password })
+ applySession(data.token, data.user)
+ return data.user
+ }
+
+ /** Toggle save/unsave FAQ. Updates local user state. Returns { saved: bool } */
+ async function saveFAQ(faqId) {
+ if (!user) return { saved: false }
+ const result = await toggleSaveFAQ(faqId)
+ // Update the savedFAQs list in local state
+ setUser(u => ({
+ ...u,
+ savedFAQs: result.savedFAQs,
+ }))
+ return result
+ }
+
+ /** Check if a FAQ is saved by current user */
+ function isFAQSaved(faqId) {
+ if (!user?.savedFAQs) return false
+ return user.savedFAQs.some(id =>
+ (typeof id === 'object' ? id._id || id : id).toString() === faqId.toString()
+ )
+ }
+
+ return (
+
+ {children}
+
+ )
+}
+
+export function useAuth() {
+ const ctx = useContext(AuthContext)
+ if (!ctx) throw new Error('useAuth must be used within AuthProvider')
+ return ctx
+}
diff --git a/client/src/context/ThemeContext.jsx b/client/src/context/ThemeContext.jsx
new file mode 100644
index 0000000..2f78ce9
--- /dev/null
+++ b/client/src/context/ThemeContext.jsx
@@ -0,0 +1,38 @@
+import { createContext, useContext, useState, useEffect } from 'react'
+
+const ThemeContext = createContext(null)
+
+function applyTheme(dark) {
+ document.documentElement.classList.toggle('dark', dark)
+ document.documentElement.style.colorScheme = dark ? 'dark' : 'light'
+ localStorage.setItem('crowdfaq-theme', dark ? 'dark' : 'light')
+}
+
+export function ThemeProvider({ children }) {
+ const [dark, setDark] = useState(() => {
+ const stored = localStorage.getItem('crowdfaq-theme')
+ const isDark = stored
+ ? stored === 'dark'
+ : window.matchMedia('(prefers-color-scheme: dark)').matches
+ applyTheme(isDark)
+ return isDark
+ })
+
+ useEffect(() => {
+ applyTheme(dark)
+ }, [dark])
+
+ const toggle = () => setDark(d => !d)
+
+ return (
+
+ {children}
+
+ )
+}
+
+export function useTheme() {
+ const ctx = useContext(ThemeContext)
+ if (!ctx) throw new Error('useTheme must be used within ThemeProvider')
+ return ctx
+}
diff --git a/client/src/index.css b/client/src/index.css
new file mode 100644
index 0000000..03d6c57
--- /dev/null
+++ b/client/src/index.css
@@ -0,0 +1,156 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ * { box-sizing: border-box; }
+
+ html { scroll-behavior: smooth; }
+
+ body {
+ @apply bg-slate-50 text-slate-800 font-sans antialiased transition-colors duration-200 dark:bg-slate-900 dark:text-slate-100;
+ min-height: 100vh;
+ }
+
+ #root {
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+ }
+}
+
+@layer components {
+
+ /* ─── Buttons ─── */
+ .btn-primary {
+ @apply inline-flex items-center justify-center gap-2 px-5 py-2.5 rounded-xl
+ bg-gradient-to-r from-primary-600 to-violet-600 text-white font-semibold text-sm
+ shadow-md shadow-primary-500/20 hover:shadow-lg hover:shadow-primary-500/30
+ hover:from-primary-500 hover:to-violet-500 active:scale-95
+ transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-primary-500
+ focus:ring-offset-2 disabled:opacity-60 disabled:cursor-not-allowed
+ dark:focus:ring-offset-slate-900;
+ }
+
+ .btn-secondary {
+ @apply inline-flex items-center justify-center gap-2 px-5 py-2.5 rounded-xl
+ bg-white text-primary-600 font-semibold text-sm border border-primary-200
+ shadow-sm hover:bg-primary-50 hover:border-primary-300 active:scale-95
+ transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-primary-500
+ focus:ring-offset-2
+ dark:bg-slate-800 dark:border-slate-600 dark:text-primary-400
+ dark:hover:bg-slate-700 dark:focus:ring-offset-slate-900;
+ }
+
+ .btn-success {
+ @apply inline-flex items-center justify-center gap-2 px-4 py-2 rounded-xl
+ bg-emerald-600 text-white font-semibold text-xs shadow-sm
+ hover:bg-emerald-500 active:scale-95 transition-all duration-150
+ disabled:opacity-50 disabled:cursor-not-allowed;
+ }
+
+ .btn-danger {
+ @apply inline-flex items-center justify-center gap-2 px-4 py-2 rounded-xl
+ bg-red-50 text-red-600 font-semibold text-xs border border-red-200 shadow-sm
+ hover:bg-red-100 active:scale-95 transition-all duration-150
+ dark:bg-red-900/20 dark:border-red-800 dark:text-red-400 dark:hover:bg-red-900/40;
+ }
+
+ /* ─── Card ─── */
+ .card {
+ @apply bg-white rounded-2xl shadow-sm border border-slate-100 overflow-hidden
+ dark:bg-slate-800 dark:border-slate-700;
+ }
+
+ .card-hover {
+ @apply card hover:shadow-md hover:-translate-y-0.5 transition-all duration-200;
+ }
+
+ /* ─── Input ─── */
+ .input-field {
+ @apply w-full px-4 py-3 rounded-xl border border-slate-200 bg-white text-slate-800
+ placeholder-slate-400 text-sm
+ focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent
+ transition-all duration-150
+ dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100
+ dark:placeholder-slate-500 dark:focus:ring-primary-400;
+ }
+
+ .select-field {
+ @apply input-field cursor-pointer;
+ }
+
+ /* ─── Badges ─── */
+ .badge {
+ @apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium;
+ }
+
+ .tag-chip {
+ @apply inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium
+ bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-300;
+ }
+
+ /* ─── Section header ─── */
+ .page-header {
+ @apply bg-white border-b border-slate-100 dark:bg-slate-800 dark:border-slate-700;
+ }
+
+ /* ─── Stat card ─── */
+ .stat-card {
+ @apply card p-5 flex flex-col items-center justify-center text-center gap-1;
+ }
+
+ /* ─── Confidence bar ─── */
+ .confidence-bar-track {
+ @apply w-full bg-slate-100 dark:bg-slate-700 rounded-full h-1.5 overflow-hidden;
+ }
+ .confidence-bar-fill {
+ @apply h-1.5 rounded-full transition-all duration-700;
+ }
+}
+
+@layer utilities {
+ .gradient-text {
+ @apply bg-gradient-to-r from-primary-600 to-violet-600 bg-clip-text text-transparent;
+ }
+
+ .hero-gradient {
+ background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 50%, #6366f1 100%);
+ }
+
+ .glass {
+ @apply bg-white/80 backdrop-blur-md dark:bg-slate-900/80;
+ }
+
+ /* Hide scrollbar while keeping scroll functionality */
+ .no-scrollbar {
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+ }
+ .no-scrollbar::-webkit-scrollbar {
+ display: none;
+ }
+
+ /* Glow effects */
+ .glow-primary {
+ box-shadow: 0 0 20px rgba(99, 102, 241, 0.3);
+ }
+
+ .animate-fade-in {
+ animation: fadeIn 0.3s ease-in-out;
+ }
+
+ .animate-slide-up {
+ animation: slideUp 0.35s ease-out;
+ }
+
+ @keyframes fadeIn {
+ from { opacity: 0; }
+ to { opacity: 1; }
+ }
+
+ @keyframes slideUp {
+ from { opacity: 0; transform: translateY(10px); }
+ to { opacity: 1; transform: translateY(0); }
+ }
+}
\ No newline at end of file
diff --git a/client/src/main.jsx b/client/src/main.jsx
new file mode 100644
index 0000000..54b39dd
--- /dev/null
+++ b/client/src/main.jsx
@@ -0,0 +1,10 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import App from './App.jsx'
+import './index.css'
+
+ReactDOM.createRoot(document.getElementById('root')).render(
+
+
+ ,
+)
diff --git a/client/src/pages/AskQuestionPage.jsx b/client/src/pages/AskQuestionPage.jsx
new file mode 100644
index 0000000..32d3cd5
--- /dev/null
+++ b/client/src/pages/AskQuestionPage.jsx
@@ -0,0 +1,244 @@
+import { useState, useRef } from 'react'
+import { Link } from 'react-router-dom'
+import { createQuestion, fetchSimilar } from '../services/api'
+import { useAuth } from '../context/AuthContext'
+import { QUESTION_CATEGORIES } from '../utils/constants'
+
+function TagInput({ tags, setTags }) {
+ const [input, setInput] = useState('')
+ function add() {
+ const t = input.trim().toLowerCase()
+ if (t && !tags.includes(t) && tags.length < 5) {
+ setTags([...tags, t])
+ setInput('')
+ }
+ }
+ function onKey(e) {
+ if (e.key === 'Enter' || e.key === ',') { e.preventDefault(); add() }
+ if (e.key === 'Backspace' && !input && tags.length) setTags(tags.slice(0, -1))
+ }
+ return (
+
+ {tags.map(t => (
+
+ #{t}
+ setTags(tags.filter(x => x !== t))} className="ml-0.5 hover:text-red-500">×
+
+ ))}
+ setInput(e.target.value)} onKeyDown={onKey} onBlur={add}
+ placeholder={tags.length < 5 ? 'Add tag + Enter…' : ''}
+ className="flex-1 min-w-[100px] text-sm outline-none bg-transparent text-slate-800 dark:text-slate-100 placeholder-slate-400 dark:placeholder-slate-500"
+ />
+
+ )
+}
+
+function SimilarPanel({ similar }) {
+ const [openId, setOpenId] = useState(null)
+ if (!similar || (similar.faqs.length === 0 && similar.questions.length === 0)) return null
+ return (
+
+
+
+ Similar questions already exist — check before submitting!
+
+ {similar.faqs.map(f => (
+
+
setOpenId(openId === f._id ? null : f._id)}
+ className="text-sm font-semibold text-amber-800 dark:text-amber-300 hover:underline text-left w-full flex justify-between"
+ >
+ 📌 {f.question}
+ {openId === f._id ? '▲' : '▼'}
+
+ {openId === f._id && (
+
+ {f.answer}
+
+ )}
+
+ ))}
+ {similar.questions.map(q => (
+
+ 🔄 Already asked: {q.title} — {q.status}
+
+ ))}
+
+ )
+}
+
+export default function AskQuestionPage() {
+ const { user } = useAuth()
+ const [form, setForm] = useState({ title: '', description: '', category: 'General' })
+ const [tags, setTags] = useState([])
+ const [loading, setLoading] = useState(false)
+ const [success, setSuccess] = useState(false)
+ const [error, setError] = useState(null)
+ const [similar, setSimilar] = useState(null)
+ const [checking,setChecking]= useState(false)
+ const debRef = useRef(null)
+
+ function onChange(e) {
+ const { name, value } = e.target
+ setForm(p => ({ ...p, [name]: value }))
+ if (error) setError(null)
+ if (name === 'title') {
+ clearTimeout(debRef.current)
+ if (value.trim().length >= 5) {
+ setChecking(true)
+ debRef.current = setTimeout(async () => {
+ try { setSimilar(await fetchSimilar(value)) }
+ catch { /* silent */ }
+ finally { setChecking(false) }
+ }, 600)
+ } else { setSimilar(null) }
+ }
+ }
+
+ async function onSubmit(e) {
+ e.preventDefault()
+ const { title, description, category } = form
+ if (!title || !description)
+ return setError('Title and description are required.')
+ setLoading(true); setError(null)
+ try {
+ await createQuestion({ title, description, category, tags })
+ setSuccess(true)
+ setForm({ title: '', description: '', category: 'General' })
+ setTags([]); setSimilar(null)
+ } catch (err) {
+ setError(err.response?.data?.error || 'Submission failed. Please try again.')
+ } finally { setLoading(false) }
+ }
+
+ if (success) return (
+
+
+
+
Question Submitted!
+
+ Your question is now pending review and will appear in the Community section once approved.
+
+
+ setSuccess(false)} className="btn-primary">Ask Another
+ View Community
+
+
+
+ )
+
+ return (
+
+
+ {/* Header */}
+
+
+
Ask the Community
+
+ Post your question and get answers from people who've been there.
+
+
+
+
+
+
+ {user?.name?.charAt(0)?.toUpperCase() || '?'}
+
+
+
{user?.name}
+
{user?.email}
+
+
+
+
+
+
+
+ )
+}
diff --git a/client/src/pages/AuthPage.jsx b/client/src/pages/AuthPage.jsx
new file mode 100644
index 0000000..e8987c0
--- /dev/null
+++ b/client/src/pages/AuthPage.jsx
@@ -0,0 +1,162 @@
+import { useState, useEffect } from 'react'
+import { Link, useNavigate, useLocation } from 'react-router-dom'
+import { useAuth } from '../context/AuthContext'
+
+export default function AuthPage() {
+ const [mode, setMode] = useState('login')
+ const [form, setForm] = useState({ name: '', email: '', password: '' })
+ const [loading, setLoading] = useState(false)
+ const [error, setError] = useState(null)
+
+ const { login, register, isAuthenticated } = useAuth()
+ const navigate = useNavigate()
+ const location = useLocation()
+ const from = location.state?.from || '/'
+
+ useEffect(() => {
+ if (isAuthenticated) navigate(from, { replace: true })
+ }, [isAuthenticated, from, navigate])
+
+ function onChange(e) {
+ const { name, value } = e.target
+ setForm(p => ({ ...p, [name]: value }))
+ if (error) setError(null)
+ }
+
+ function switchMode(next) {
+ setMode(next)
+ setError(null)
+ setForm({ name: '', email: '', password: '' })
+ }
+
+ async function onSubmit(e) {
+ e.preventDefault()
+ setLoading(true)
+ setError(null)
+ try {
+ if (mode === 'login') {
+ await login(form.email, form.password)
+ } else {
+ await register(form.name, form.email, form.password)
+ }
+ navigate(from, { replace: true })
+ } catch (err) {
+ setError(err.response?.data?.error || 'Something went wrong. Please try again.')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+
+
+
+
+
+ {mode === 'login' ? 'Welcome back' : 'Create your account'}
+
+
+ {mode === 'login'
+ ? 'Sign in to ask questions and join the community.'
+ : 'Join CrowdFAQ to ask questions and share answers.'}
+
+
+
+
+
+ {['login', 'signup'].map(m => (
+ switchMode(m)}
+ className={`flex-1 py-2 text-sm font-semibold rounded-lg transition-all ${
+ mode === m
+ ? 'bg-white dark:bg-slate-800 text-primary-600 dark:text-primary-400 shadow-sm'
+ : 'text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-200'
+ }`}
+ >
+ {m === 'login' ? 'Log In' : 'Sign Up'}
+
+ ))}
+
+
+
+
+
+ Browse FAQs without an account.{' '}
+
+ Back to home
+
+
+
+
+
+ )
+}
diff --git a/client/src/pages/CommunityPage.jsx b/client/src/pages/CommunityPage.jsx
new file mode 100644
index 0000000..9237737
--- /dev/null
+++ b/client/src/pages/CommunityPage.jsx
@@ -0,0 +1,212 @@
+import { useState, useEffect, useCallback } from 'react'
+import { Link } from 'react-router-dom'
+import Spinner from '../components/Spinner'
+import { fetchQuestions, updateQuestionStatus } from '../services/api'
+import { QUESTION_CATEGORIES, STATUS_META, CATEGORY_COLORS, timeAgo } from '../utils/constants'
+
+const TABS = [
+ { value: 'all', label: 'All' },
+ { value: 'pending', label: 'Pending' },
+ { value: 'approved', label: 'Approved' },
+ { value: 'rejected', label: 'Rejected' },
+]
+
+export default function CommunityPage() {
+ const [questions, setQuestions] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState(null)
+ const [tab, setTab] = useState('all')
+ const [search, setSearch] = useState('')
+ const [debSearch, setDebSearch] = useState('')
+ const [category, setCategory] = useState('All')
+ const [updating, setUpdating] = useState({}) // { [id]: true }
+
+ useEffect(() => {
+ const t = setTimeout(() => setDebSearch(search), 350)
+ return () => clearTimeout(t)
+ }, [search])
+
+ const load = useCallback(async () => {
+ setLoading(true); setError(null)
+ try {
+ const params = {}
+ if (tab !== 'all') params.status = tab
+ if (debSearch) params.search = debSearch
+ if (category !== 'All') params.category = category
+ setQuestions(await fetchQuestions(params))
+ } catch (e) {
+ setError(e.response?.data?.error || 'Failed to load questions.')
+ } finally { setLoading(false) }
+ }, [tab, debSearch, category])
+
+ useEffect(() => { load() }, [load])
+
+ async function changeStatus(id, status) {
+ setUpdating(u => ({ ...u, [id]: true }))
+ try {
+ const updated = await updateQuestionStatus(id, status)
+ setQuestions(qs => qs.map(q => q._id === id ? updated : q))
+ } catch { /* silent */ }
+ finally { setUpdating(u => ({ ...u, [id]: false })) }
+ }
+
+ const pendingCount = questions.filter(q => q.status === 'pending').length
+
+ return (
+
+ {/* Page header */}
+
+
+
+
+
Community Questions
+
+ Browse, answer, and moderate community-submitted questions.
+
+
+
+
+ Ask a Question
+
+
+
+
+
+
+ {/* Controls */}
+
+ {/* Search */}
+
+
+
setSearch(e.target.value)}
+ className="input-field pl-10"/>
+
+
+
+ {/* Status tabs */}
+
+ {TABS.map(t => {
+ const count = t.value === 'pending' ? pendingCount : undefined
+ return (
+ setTab(t.value)}
+ className={`px-3.5 py-1.5 rounded-lg text-xs font-semibold transition-all duration-150 flex items-center gap-1.5 ${
+ tab === t.value
+ ? 'bg-primary-600 text-white shadow-sm'
+ : 'text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700'
+ }`}>
+ {t.label}
+ {count > 0 && {count}}
+
+ )
+ })}
+
+
+ {/* Category filter */}
+
+
+
+
+ {/* List */}
+ {loading ?
: error ? (
+
{error}
+ ) : questions.length === 0 ? (
+
+
+
No questions found
+
+ {search || category !== 'All' ? 'Try different filters.' : 'Be the first to ask!'}
+
+
Ask the First Question
+
+ ) : (
+ <>
+
+ {questions.length} {questions.length === 1 ? 'question' : 'questions'}
+
+
+ {questions.map(q => {
+ const sm = STATUS_META[q.status] || STATUS_META.pending
+ const cc = CATEGORY_COLORS[q.category] || CATEGORY_COLORS.Other
+ const isUpdating = updating[q._id]
+ return (
+
+
+
+ {/* Badges row */}
+
+ {sm.label}
+ {q.category}
+ 0 ? 'bg-emerald-50 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300' : 'bg-slate-100 text-slate-500 dark:bg-slate-700 dark:text-slate-400'}`}>
+ {q.answerCount} {q.answerCount === 1 ? 'answer' : 'answers'}
+
+ {timeAgo(q.createdAt)}
+
+
+ {q.title}
+
+
{q.description}
+ {/* Submitter info */}
+
+ By {q.name}
+ {' '}·{' '}{q.email}
+
+ {/* Tags */}
+ {q.tags?.length > 0 && (
+
+ {q.tags.map(t => #{t})}
+
+ )}
+
+
+ {/* Actions */}
+
+
+ View
+
+
+ {/* Status actions */}
+
+ {q.status !== 'approved' && (
+ changeStatus(q._id, 'approved')} disabled={isUpdating}
+ className="btn-success py-1 px-2.5 text-[11px]">
+ {isUpdating ? '…' : '✓ Approve'}
+
+ )}
+ {q.status !== 'rejected' && (
+ changeStatus(q._id, 'rejected')} disabled={isUpdating}
+ className="btn-danger py-1 px-2.5 text-[11px]">
+ {isUpdating ? '…' : '✕ Reject'}
+
+ )}
+ {q.status !== 'pending' && (
+ changeStatus(q._id, 'pending')} disabled={isUpdating}
+ className="px-2.5 py-1 text-[11px] rounded-xl border border-slate-200 dark:border-slate-600 text-slate-500 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-700 font-semibold">
+ {isUpdating ? '…' : '↺ Pending'}
+
+ )}
+
+
+
+
+ )
+ })}
+
+ >
+ )}
+
+
+ )
+}
diff --git a/client/src/pages/FAQPage.jsx b/client/src/pages/FAQPage.jsx
new file mode 100644
index 0000000..a86fcaa
--- /dev/null
+++ b/client/src/pages/FAQPage.jsx
@@ -0,0 +1,270 @@
+import { useState, useEffect, useCallback } from 'react'
+import { Link } from 'react-router-dom'
+import AccordionItem from '../components/AccordionItem'
+import Spinner from '../components/Spinner'
+import { fetchFAQs, fetchRelatedFAQs } from '../services/api'
+import { CATEGORIES } from '../utils/constants'
+import { useAuth } from '../context/AuthContext'
+
+const SORT_OPTIONS = [
+ { value: 'latest', label: '🕐 Latest' },
+ { value: 'helpful', label: '👍 Most Helpful' },
+ { value: 'views', label: '👁 Most Viewed' },
+]
+
+/** "People like you also asked" sidebar rail */
+function RelatedFAQs({ openFaq, allFaqs }) {
+ const [related, setRelated] = useState([])
+
+ useEffect(() => {
+ if (!openFaq) { setRelated([]); return }
+ fetchRelatedFAQs({
+ category: openFaq.category,
+ tags: openFaq.tags?.join(',') || '',
+ exclude: openFaq._id,
+ }).then(setRelated).catch(() => setRelated([]))
+ }, [openFaq?._id])
+
+ if (!related.length) return null
+
+ return (
+
+
+
+
People like you also asked
+
+
+ {related.map(r => (
+ -
+ {
+ const el = document.getElementById(`faq-${r._id}`)
+ if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' })
+ }}
+ >
+
+ {r.question}
+
+
+
+ {r.helpfulYes > 0 && `👍 ${r.helpfulYes}`}
+
+
+
+
+ ))}
+
+
+ )
+}
+
+export default function FAQPage() {
+ const { user } = useAuth()
+ const [faqs, setFaqs] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState(null)
+ const [search, setSearch] = useState('')
+ const [debSearch, setDebSearch] = useState('')
+ const [category, setCategory] = useState('All')
+ const [sort, setSort] = useState('latest')
+ const [openFaq, setOpenFaq] = useState(null) // track which FAQ is expanded
+
+ useEffect(() => {
+ const t = setTimeout(() => setDebSearch(search), 350)
+ return () => clearTimeout(t)
+ }, [search])
+
+ const load = useCallback(async () => {
+ setLoading(true); setError(null)
+ try {
+ const params = { sort }
+ if (debSearch) params.search = debSearch
+ if (category !== 'All') params.category = category
+ setFaqs(await fetchFAQs(params))
+ } catch (e) {
+ setError(e.response?.data?.error || 'Failed to load FAQs.')
+ } finally {
+ setLoading(false)
+ }
+ }, [debSearch, category, sort])
+
+ useEffect(() => { load() }, [load])
+
+ const hasMeta = (faq) => faq.tags?.length > 0 || faq.category !== 'General'
+
+ return (
+ <>
+ {/* ── Hero ── */}
+
+ {/* Decorative blobs */}
+
+
+
+
+
+
+ Community Knowledge Base
+
+
+ Find Answers,{' '}
+ Share Knowledge
+
+
+ Browse our community-curated FAQs. Search, filter by category, and vote on what helped.
+
+
+ {/* Search */}
+
+
+
setSearch(e.target.value)}
+ className="w-full pl-12 pr-10 py-4 rounded-2xl text-slate-800 placeholder-slate-400 bg-white shadow-xl shadow-black/20 text-sm sm:text-base focus:outline-none focus:ring-2 focus:ring-white/50"
+ />
+ {search && (
+
setSearch('')}
+ className="absolute right-4 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600">
+
+
+ )}
+
+
+ {/* Quick stats */}
+ {!loading && faqs.length > 0 && (
+
+
+ {faqs.length} FAQs
+
+
+
+ {faqs.reduce((s, f) => s + (f.helpfulYes || 0), 0)} helpful votes
+
+
+ )}
+
+
+
+ {/* ── Filters ── */}
+
+
+
+
+ {CATEGORIES.map(cat => (
+ setCategory(cat)}
+ className={`flex-shrink-0 px-3.5 py-1.5 rounded-full text-xs font-semibold transition-all duration-150 border ${
+ category === cat
+ ? 'bg-gradient-to-r from-primary-600 to-violet-600 text-white border-transparent shadow-md shadow-primary-500/20'
+ : 'bg-white dark:bg-slate-700 text-slate-600 dark:text-slate-300 border-slate-200 dark:border-slate-600 hover:border-primary-400 hover:text-primary-600 dark:hover:text-primary-400'
+ }`}
+ >
+ {cat}
+
+ ))}
+
+
+ Sort:
+
+
+
+
+
+
+ {/* ── FAQ list ── */}
+
+
+
+
+ {debSearch ? `Results for "${debSearch}"` : category === 'All' ? 'All FAQs' : `${category} FAQs`}
+
+ {!loading && (
+
+ {faqs.length} {faqs.length === 1 ? 'result' : 'results'} found
+
+ )}
+
+
+
+ Ask Question
+
+
+
+ {/* Two-column layout: FAQs + Related sidebar */}
+
+ {/* Main FAQ list */}
+
+ {loading ? (
+
+ ) : error ? (
+
{error}
+ ) : faqs.length === 0 ? (
+
+
+
No FAQs found
+
+ {debSearch || category !== 'All' ? 'Try a different search or category.' : 'Run npm run seed to add sample FAQs.'}
+
+
+ ) : (
+
+ {faqs.map((faq, i) => (
+
setOpenFaq(f => f?._id === faq._id ? null : faq)}
+ >
+
+
+ ))}
+
+ )}
+
+
+ {/* Sidebar: People like you also asked */}
+
+
+
+ {/* Login CTA for saving */}
+ {!user && (
+
+
+
Save FAQs
+
Sign in to bookmark FAQs to your profile.
+
Sign In
+
+ )}
+
+
+
+ >
+ )
+}
diff --git a/client/src/pages/LeaderboardPage.jsx b/client/src/pages/LeaderboardPage.jsx
new file mode 100644
index 0000000..a32fb3c
--- /dev/null
+++ b/client/src/pages/LeaderboardPage.jsx
@@ -0,0 +1,167 @@
+import { useState, useEffect } from 'react'
+import { fetchLeaderboard } from '../services/api'
+import Spinner from '../components/Spinner'
+
+const LEVEL_META = {
+ Beginner: { color: 'bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-300', icon: '🌱', pts: '0–49 pts' },
+ Contributor: { color: 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300', icon: '⭐', pts: '50–199 pts' },
+ Expert: { color: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300', icon: '🏆', pts: '200+ pts' },
+}
+
+const RANK_STYLES = [
+ 'bg-amber-400 text-white shadow-amber-200 dark:shadow-amber-900', // 1st
+ 'bg-slate-400 text-white shadow-slate-200 dark:shadow-slate-700', // 2nd
+ 'bg-orange-400 text-white shadow-orange-200 dark:shadow-orange-900', // 3rd
+]
+
+function RankBadge({ rank }) {
+ if (rank <= 3) {
+ return (
+
+ {rank === 1 ? '🥇' : rank === 2 ? '🥈' : '🥉'}
+
+ )
+ }
+ return (
+
+ {rank}
+
+ )
+}
+
+function PointsBar({ points, max }) {
+ const pct = max > 0 ? Math.min(100, (points / max) * 100) : 0
+ return (
+
+ )
+}
+
+export default function LeaderboardPage() {
+ const [users, setUsers] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState(null)
+
+ useEffect(() => {
+ fetchLeaderboard(20)
+ .then(setUsers)
+ .catch(e => setError(e.response?.data?.error || 'Failed to load leaderboard.'))
+ .finally(() => setLoading(false))
+ }, [])
+
+ const maxPts = users[0]?.points || 1
+
+ return (
+
+ {/* Hero */}
+
+
+
+ 🏆 Leaderboard
+
+
+ Top contributors who help keep CrowdFAQ accurate and helpful.
+
+
+ {/* Level legend */}
+
+ {Object.entries(LEVEL_META).map(([level, meta]) => (
+
+ {meta.icon}
+ {level}
+ ({meta.pts})
+
+ ))}
+
+
+
+
+ {loading ? (
+
+ ) : error ? (
+
{error}
+ ) : users.length === 0 ? (
+
+
🌱
+
No contributors yet
+
+ Be the first! Answer questions and earn points.
+
+
+ ) : (
+
+ {users.map((u, idx) => {
+ const rank = idx + 1
+ const lm = LEVEL_META[u.level] || LEVEL_META.Beginner
+ return (
+
+
+
+ {/* Avatar */}
+
+ {u.name.charAt(0).toUpperCase()}
+
+
+ {/* Info */}
+
+
+
+ {u.name}
+
+
+ {lm.icon} {u.level}
+
+
+
+
+
+ {/* Stats row */}
+
+ 💬 {u.stats?.answersGiven || 0} answers
+ ✅ {u.stats?.answersApproved || 0} approved
+ 👍 {u.stats?.upvotesReceived || 0} upvotes
+
+
+
+ {/* Points */}
+
+
+ )
+ })}
+
+ )}
+
+ {/* Points system explanation */}
+
+
How to Earn Points
+
+ {[
+ { action: 'Submit an answer', pts: '+5 pts', icon: '💬' },
+ { action: 'Answer gets approved', pts: '+15 pts', icon: '✅' },
+ { action: 'Answer receives an upvote', pts: '+10 pts', icon: '👍' },
+ ].map(row => (
+
+
+ {row.icon}{row.action}
+
+ {row.pts}
+
+ ))}
+
+
+
+
+ )
+}
diff --git a/client/src/pages/ProfilePage.jsx b/client/src/pages/ProfilePage.jsx
new file mode 100644
index 0000000..3db2cd7
--- /dev/null
+++ b/client/src/pages/ProfilePage.jsx
@@ -0,0 +1,311 @@
+import { useState, useEffect } from 'react'
+import { Link, useNavigate } from 'react-router-dom'
+import { useAuth } from '../context/AuthContext'
+import Spinner from '../components/Spinner'
+import { fetchMyProfile } from '../services/api'
+import { CATEGORY_COLORS, STATUS_META, timeAgo } from '../utils/constants'
+
+const LEVEL_META = {
+ Beginner: { color: 'from-slate-400 to-slate-500', icon: '🌱', bg: 'bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-300', pts: '0–49 pts' },
+ Contributor: { color: 'from-blue-500 to-indigo-600', icon: '⭐', bg: 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300', pts: '50–199 pts' },
+ Expert: { color: 'from-amber-400 to-orange-500', icon: '🏆', bg: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300', pts: '200+ pts' },
+}
+
+const NEXT_LEVEL = { Beginner: 50, Contributor: 200, Expert: Infinity }
+
+function StatCard({ icon, label, value, color = 'text-primary-600 dark:text-primary-400' }) {
+ return (
+
+ {icon}
+ {value}
+ {label}
+
+ )
+}
+
+function TabButton({ active, onClick, children }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default function ProfilePage() {
+ const { user, logout } = useAuth()
+ const navigate = useNavigate()
+ const [profile, setProfile] = useState(null)
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState(null)
+ const [activeTab, setActiveTab] = useState('questions')
+
+ useEffect(() => {
+ if (!user) { navigate('/auth'); return }
+ fetchMyProfile()
+ .then(setProfile)
+ .catch(e => setError(e.response?.data?.error || 'Failed to load profile.'))
+ .finally(() => setLoading(false))
+ }, [user])
+
+ if (!user) return null
+ if (loading) return
+ if (error) return (
+
+ )
+
+ const lm = LEVEL_META[profile.level] || LEVEL_META.Beginner
+ const nextPts = NEXT_LEVEL[profile.level]
+ const prevPts = profile.level === 'Beginner' ? 0 : profile.level === 'Contributor' ? 50 : 200
+ const progressPct = nextPts === Infinity
+ ? 100
+ : Math.min(100, Math.round(((profile.points - prevPts) / (nextPts - prevPts)) * 100))
+
+ const tabs = [
+ { id: 'questions', label: `❓ Questions (${profile.questions?.length || 0})` },
+ { id: 'answers', label: `💬 Answers (${profile.answers?.length || 0})` },
+ { id: 'saved', label: `❤️ Saved FAQs (${profile.savedFAQs?.length || 0})` },
+ ]
+
+ return (
+
+
+ {/* ── Profile hero banner ── */}
+
+ {/* Gradient banner */}
+
+
+ {/* Profile card overlapping the banner */}
+
+
+ {/* Avatar */}
+
+ {profile.name.charAt(0).toUpperCase()}
+
+
+
+
+
{profile.name}
+
+ {lm.icon} {profile.level}
+
+
+
{profile.email}
+
+ Member since {new Date(profile.createdAt).toLocaleDateString('en-US', { year: 'numeric', month: 'long' })}
+
+
+
+ {/* Logout button */}
+
{ logout(); navigate('/') }}
+ className="flex items-center gap-1.5 px-4 py-2 rounded-xl border border-slate-200 dark:border-slate-600 text-sm font-semibold text-slate-500 dark:text-slate-400 hover:bg-red-50 dark:hover:bg-red-900/20 hover:text-red-600 dark:hover:text-red-400 hover:border-red-200 dark:hover:border-red-800 transition-all duration-150"
+ >
+
+ Sign out
+
+
+
+
+
+
+
+ {/* ── Points & Level Progress ── */}
+
+
+
+
Reputation
+
{profile.points} pts
+
+
+
+ {nextPts === Infinity ? 'Max level reached 🏆' : `${nextPts - profile.points} pts to ${profile.level === 'Beginner' ? 'Contributor' : 'Expert'}`}
+
+
{progressPct}%
+
+
+
+
+ {/* Level badges */}
+
+ {Object.entries(LEVEL_META).map(([lvl, meta]) => (
+
+ {meta.icon} {lvl} ({meta.pts})
+
+ ))}
+
+
+
+ {/* ── Stats grid ── */}
+
+
+
+
+
+
+
+ {/* ── Tabs ── */}
+
+ {tabs.map(t => (
+ setActiveTab(t.id)}>
+ {t.label}
+
+ ))}
+
+
+ {/* ── Tab: Questions ── */}
+ {activeTab === 'questions' && (
+
+ {(!profile.questions || profile.questions.length === 0) ? (
+
+
❓
+
No questions yet
+
Ask your first question to the community.
+
Ask a Question
+
+ ) : (
+ profile.questions.map(q => {
+ const sm = STATUS_META[q.status] || STATUS_META.pending
+ const cc = CATEGORY_COLORS[q.category] || CATEGORY_COLORS.Other
+ return (
+
+
+ {sm.label}
+ {q.category}
+ {timeAgo(q.createdAt)}
+
+
+ {q.title}
+
+
{q.description}
+
+ 💬 {q.answerCount} {q.answerCount === 1 ? 'answer' : 'answers'}
+ {q.tags?.map(t => #{t})}
+
+
+ )
+ })
+ )}
+
+ )}
+
+ {/* ── Tab: Answers ── */}
+ {activeTab === 'answers' && (
+
+ {(!profile.answers || profile.answers.length === 0) ? (
+
+
💬
+
No answers yet
+
Help the community by answering questions.
+
Browse Questions
+
+ ) : (
+ profile.answers.map(ans => {
+ const sm = STATUS_META[ans.status] || STATUS_META.pending
+ const conf = ans.confidence
+ const confColor = conf >= 70 ? 'bg-emerald-500' : conf >= 40 ? 'bg-amber-500' : 'bg-red-400'
+ const confText = conf >= 70 ? 'text-emerald-600 dark:text-emerald-400' : conf >= 40 ? 'text-amber-600 dark:text-amber-400' : 'text-red-500 dark:text-red-400'
+ return (
+
+ {ans.questionId?.title && (
+
+ Re: {ans.questionId.title}
+
+ )}
+
{ans.body}
+
+
{sm.label}
+ {conf !== null && conf !== undefined && (
+
+
AI Confidence
+
+
{conf}%
+
+ )}
+
{timeAgo(ans.createdAt)}
+ {ans.upvotes > 0 && (
+
👍 {ans.upvotes}
+ )}
+
+
+ )
+ })
+ )}
+
+ )}
+
+ {/* ── Tab: Saved FAQs ── */}
+ {activeTab === 'saved' && (
+
+ {(!profile.savedFAQs || profile.savedFAQs.length === 0) ? (
+
+
❤️
+
No saved FAQs yet
+
+ Click the heart icon on any FAQ to save it here.
+
+
Browse FAQs
+
+ ) : (
+ profile.savedFAQs.map(faq => {
+ const cc = CATEGORY_COLORS[faq.category] || CATEGORY_COLORS.Other
+ const total = (faq.helpfulYes || 0) + (faq.helpfulNo || 0)
+ const pct = total > 0 ? Math.round((faq.helpfulYes / total) * 100) : null
+ return (
+
+
+
+
+ {faq.category}
+ {faq.tags?.map(t => #{t})}
+
+
{faq.question}
+
{faq.answer}
+
+ {pct !== null && 👍 {pct}% helpful}
+ {faq.views > 0 && 👁 {faq.views} views}
+
+
+ {/* Heart icon (filled) */}
+
+
+
+ )
+ })
+ )}
+
+ )}
+
+
+ )
+}
diff --git a/client/src/pages/QuestionDetailPage.jsx b/client/src/pages/QuestionDetailPage.jsx
new file mode 100644
index 0000000..d67c264
--- /dev/null
+++ b/client/src/pages/QuestionDetailPage.jsx
@@ -0,0 +1,459 @@
+import { useState, useEffect } from 'react'
+import { useParams, Link } from 'react-router-dom'
+import { useAuth } from '../context/AuthContext'
+import Spinner from '../components/Spinner'
+import {
+ fetchQuestion, fetchAnswers, createAnswer,
+ updateAnswerStatus, updateQuestionStatus,
+ fetchSuggestion, upvoteAnswer,
+} from '../services/api'
+import { STATUS_META, CATEGORY_COLORS, timeAgo } from '../utils/constants'
+
+function StatusBadge({ status }) {
+ const m = STATUS_META[status] || STATUS_META.pending
+ return {m.label}
+}
+
+// ── AI Suggestion Panel ────────────────────────────────────────────────────────
+function AISuggestionPanel({ questionTitle, questionDesc, onUse }) {
+ const [suggestion, setSuggestion] = useState(null)
+ const [basedOn, setBasedOn] = useState(null)
+ const [confidence, setConfidence] = useState(0)
+ const [loading, setLoading] = useState(false)
+ const [fetched, setFetched] = useState(false)
+ const [expanded, setExpanded] = useState(false)
+
+ async function loadSuggestion() {
+ if (fetched) { setExpanded(e => !e); return }
+ setLoading(true)
+ try {
+ const query = `${questionTitle} ${questionDesc}`
+ const data = await fetchSuggestion(query)
+ setSuggestion(data.suggestion)
+ setBasedOn(data.basedOn)
+ setConfidence(data.confidence)
+ setFetched(true)
+ setExpanded(true)
+ } catch { /* silent */ }
+ finally { setLoading(false) }
+ }
+
+ const pct = Math.round(confidence * 100)
+
+ return (
+
+ {/* Header */}
+
+
+ 🤖
+
+ AI-Powered Suggested Answer
+
+ {fetched && suggestion && (
+
+ {pct}% match
+
+ )}
+
+
+
+
+ {/* Body */}
+ {expanded && (
+
+ {loading ? (
+
+
+ Analysing existing FAQs…
+
+ ) : suggestion ? (
+ <>
+ {/* Confidence bar */}
+
+
Confidence
+
+
{pct}%
+
+
+ {/* Suggested text */}
+
+ {basedOn?.answer || suggestion}
+
+
+ {basedOn && (
+
+ Based on FAQ: “{basedOn.question}”
+
+ )}
+
+
onUse(basedOn?.answer || suggestion)}
+ className="btn-primary text-xs py-2 px-4"
+ >
+ ✏️ Use this suggestion
+
+ >
+ ) : (
+
+ No closely matching FAQ found. Please write a custom answer.
+
+ )}
+
+ )}
+
+ )
+}
+
+export default function QuestionDetailPage() {
+ const { id } = useParams()
+ const { user } = useAuth()
+ const [question, setQuestion] = useState(null)
+ const [answers, setAnswers] = useState([])
+ const [loadingQ, setLoadingQ] = useState(true)
+ const [loadingA, setLoadingA] = useState(true)
+ const [errorQ, setErrorQ] = useState(null)
+ const [form, setForm] = useState({ body: '' })
+ const [submitting, setSubmitting] = useState(false)
+ const [submitError, setSubmitError] = useState(null)
+ const [submitOk, setSubmitOk] = useState(false)
+ const [answerFilter, setAnswerFilter] = useState('all')
+ const [updatingA, setUpdatingA] = useState({})
+ const [updatingQ, setUpdatingQ] = useState(false)
+ const [upvoting, setUpvoting] = useState({})
+
+ useEffect(() => {
+ fetchQuestion(id)
+ .then(setQuestion)
+ .catch(e => setErrorQ(e.response?.data?.error || 'Question not found.'))
+ .finally(() => setLoadingQ(false))
+ }, [id])
+
+ useEffect(() => {
+ const params = answerFilter !== 'all' ? { status: answerFilter } : {}
+ setLoadingA(true)
+ fetchAnswers(id, params)
+ .then(setAnswers)
+ .catch(console.error)
+ .finally(() => setLoadingA(false))
+ }, [id, answerFilter])
+
+ async function onSubmitAnswer(e) {
+ e.preventDefault()
+ if (!form.body.trim()) return setSubmitError('Please write an answer.')
+ setSubmitting(true); setSubmitError(null)
+ try {
+ const saved = await createAnswer({ questionId: id, body: form.body })
+ setAnswers(a => [...a, saved])
+ setQuestion(q => ({ ...q, answerCount: (q.answerCount || 0) + 1 }))
+ setForm({ body: '' })
+ setSubmitOk(true)
+ setTimeout(() => setSubmitOk(false), 3500)
+ } catch (err) {
+ setSubmitError(err.response?.data?.error || 'Failed to submit.')
+ } finally { setSubmitting(false) }
+ }
+
+ async function changeAnswerStatus(answerId, status) {
+ setUpdatingA(u => ({ ...u, [answerId]: true }))
+ try {
+ const updated = await updateAnswerStatus(answerId, status)
+ setAnswers(as => as.map(a => a._id === answerId ? updated : a))
+ } catch { /* silent */ }
+ finally { setUpdatingA(u => ({ ...u, [answerId]: false })) }
+ }
+
+ async function changeQuestionStatus(status) {
+ setUpdatingQ(true)
+ try {
+ const updated = await updateQuestionStatus(id, status)
+ setQuestion(updated)
+ } catch { /* silent */ }
+ finally { setUpdatingQ(false) }
+ }
+
+ async function handleUpvote(answerId) {
+ if (!user) return
+ setUpvoting(u => ({ ...u, [answerId]: true }))
+ try {
+ const { upvotes, voted } = await upvoteAnswer(answerId)
+ setAnswers(as => as.map(a =>
+ a._id === answerId
+ ? {
+ ...a,
+ upvotes,
+ upvotedBy: voted
+ ? [...(a.upvotedBy || []), user.email]
+ : (a.upvotedBy || []).filter(e => e !== user.email),
+ }
+ : a
+ ))
+ } catch { /* silent */ }
+ finally { setUpvoting(u => ({ ...u, [answerId]: false })) }
+ }
+
+ if (loadingQ) return
+ if (errorQ) return (
+
+
{errorQ}
+
← Back to Community
+
+ )
+
+ const catColor = CATEGORY_COLORS[question.category] || CATEGORY_COLORS.Other
+ const displayedAnswers = answers
+
+ return (
+
+
+ {/* Breadcrumb */}
+
+
+ {/* Question card */}
+
+
+
+ {question.category}
+ 0 ? 'bg-emerald-50 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300' : 'bg-slate-100 text-slate-500 dark:bg-slate-700 dark:text-slate-400'}`}>
+ {question.answerCount} {question.answerCount === 1 ? 'answer' : 'answers'}
+
+ {timeAgo(question.createdAt)}
+
+
+
{question.title}
+
{question.description}
+
+ {question.tags?.length > 0 && (
+
+ {question.tags.map(t => #{t})}
+
+ )}
+
+
+ By {question.name}
+ {' '}· {question.email}
+
+
+ {/* Question moderation */}
+
+ Moderate:
+ {question.status !== 'approved' && (
+ changeQuestionStatus('approved')} disabled={updatingQ} className="btn-success text-xs py-1.5">
+ {updatingQ ? '…' : '✓ Approve'}
+
+ )}
+ {question.status !== 'rejected' && (
+ changeQuestionStatus('rejected')} disabled={updatingQ} className="btn-danger text-xs py-1.5">
+ {updatingQ ? '…' : '✕ Reject'}
+
+ )}
+ {question.status !== 'pending' && (
+ changeQuestionStatus('pending')} disabled={updatingQ}
+ className="px-3 py-1.5 text-xs rounded-xl border border-slate-200 dark:border-slate-600 text-slate-500 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-700 font-semibold">
+ {updatingQ ? '…' : '↺ Mark Pending'}
+
+ )}
+
+
+
+ {/* Answers section */}
+
+
+
+
+ {answers.length} {answers.length === 1 ? 'Answer' : 'Answers'}
+
+ {/* Answer filter */}
+
+ {['all', 'pending', 'approved', 'rejected'].map(s => (
+ setAnswerFilter(s)}
+ className={`px-2.5 py-1 rounded-lg text-xs font-semibold transition-all capitalize ${
+ answerFilter === s ? 'bg-primary-600 text-white' : 'text-slate-500 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-700'
+ }`}>
+ {s}
+
+ ))}
+
+
+
+ {loadingA ?
:
+ displayedAnswers.length === 0 ? (
+
+
No answers yet
+
Be the first to help!
+
+ ) : (
+
+ {displayedAnswers.map((ans, idx) => {
+ const hasUpvoted = user && (ans.upvotedBy || []).includes(user.email)
+ const conf = ans.confidence
+ const confColor = conf >= 70 ? 'bg-emerald-500' : conf >= 40 ? 'bg-amber-500' : 'bg-red-400'
+ return (
+
+
+
+ {(ans.author || 'A').charAt(0).toUpperCase()}
+
+
+
+ {ans.author}
+
+ {timeAgo(ans.createdAt)}
+ #{idx + 1}
+
+
{ans.body}
+
+ {/* Confidence score */}
+ {conf !== null && conf !== undefined && (
+
+
AI Confidence
+
+
= 70 ? 'text-emerald-600 dark:text-emerald-400' : conf >= 40 ? 'text-amber-600 dark:text-amber-400' : 'text-red-500 dark:text-red-400'}`}>
+ {conf}%
+
+
+ )}
+
+ {/* Upvote + moderation row */}
+
+ {/* Upvote button */}
+
handleUpvote(ans._id)}
+ disabled={!user || upvoting[ans._id]}
+ title={user ? (hasUpvoted ? 'Remove upvote' : 'Upvote this answer') : 'Log in to upvote'}
+ className={`flex items-center gap-1.5 px-2.5 py-1 rounded-xl text-xs font-semibold border transition-all ${
+ hasUpvoted
+ ? 'bg-primary-100 border-primary-300 text-primary-700 dark:bg-primary-900/40 dark:border-primary-700 dark:text-primary-300'
+ : 'border-slate-200 dark:border-slate-600 text-slate-500 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-700'
+ } ${!user ? 'opacity-50 cursor-not-allowed' : ''}`}
+ >
+
+ {upvoting[ans._id] ? '…' : ans.upvotes || 0}
+
+
+ {/* Moderation buttons */}
+ {ans.status !== 'approved' && (
+
changeAnswerStatus(ans._id, 'approved')} disabled={updatingA[ans._id]}
+ className="btn-success text-[11px] py-1 px-2.5">
+ {updatingA[ans._id] ? '…' : '✓ Approve'}
+
+ )}
+ {ans.status !== 'rejected' && (
+
changeAnswerStatus(ans._id, 'rejected')} disabled={updatingA[ans._id]}
+ className="btn-danger text-[11px] py-1 px-2.5">
+ {updatingA[ans._id] ? '…' : '✕ Reject'}
+
+ )}
+ {ans.status !== 'pending' && (
+
changeAnswerStatus(ans._id, 'pending')} disabled={updatingA[ans._id]}
+ className="px-2.5 py-1 text-[11px] rounded-xl border border-slate-200 dark:border-slate-600 text-slate-500 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-700 font-semibold">
+ {updatingA[ans._id] ? '…' : '↺ Pending'}
+
+ )}
+
+
+
+
+ )
+ })}
+
+ )}
+
+
+ {/* Submit answer form */}
+
+
+
+ Write Your Answer
+
+
+ {!user ? (
+
+
+ Sign in to share your answer with the community.
+
+
+ Log In or Sign Up
+
+
+ ) : (
+ <>
+ {/* AI Suggestion panel — shown inside write-answer section */}
+
setForm(f => ({ ...f, body: text }))}
+ />
+
+ {submitOk && (
+
+
+ Answer submitted! It will appear after approval.
+
+ )}
+
+
+ Posting as {user.name}
+
+
+
+ >
+ )}
+
+
+
+ )
+}
diff --git a/client/src/services/api.js b/client/src/services/api.js
new file mode 100644
index 0000000..cb59614
--- /dev/null
+++ b/client/src/services/api.js
@@ -0,0 +1,47 @@
+import axios from 'axios'
+
+const api = axios.create({ baseURL: '/api', headers: { 'Content-Type': 'application/json' } })
+
+export function setAuthToken(token) {
+ if (token) {
+ api.defaults.headers.common.Authorization = `Bearer ${token}`
+ } else {
+ delete api.defaults.headers.common.Authorization
+ }
+}
+
+// Auth
+export const register = (data) => api.post('/auth/register', data).then(r => r.data)
+export const login = (data) => api.post('/auth/login', data).then(r => r.data)
+export const fetchMe = () => api.get('/auth/me').then(r => r.data)
+export const toggleSaveFAQ = (faqId) => api.post(`/auth/save-faq/${faqId}`).then(r => r.data)
+
+// FAQs
+export const fetchFAQs = (params = {}) => api.get('/faqs', { params }).then(r => r.data)
+export const fetchFAQCategories = () => api.get('/faqs/categories').then(r => r.data)
+export const voteFAQ = (id, vote) => api.post(`/faqs/${id}/vote`, { vote }).then(r => r.data)
+export const fetchRelatedFAQs = (params = {}) => api.get('/faqs/related', { params }).then(r => r.data)
+
+// Questions
+export const fetchQuestions = (params = {}) => api.get('/questions', { params }).then(r => r.data)
+export const fetchQuestion = (id) => api.get(`/questions/${id}`).then(r => r.data)
+export const fetchSimilar = (q) => api.get('/questions/similar', { params: { q } }).then(r => r.data)
+export const createQuestion = (data) => api.post('/questions', data).then(r => r.data)
+export const updateQuestionStatus = (id, status) =>
+ api.patch(`/questions/${id}/status`, { status }).then(r => r.data)
+
+// Answers
+export const fetchAnswers = (questionId, params = {}) =>
+ api.get(`/answers/${questionId}`, { params }).then(r => r.data)
+export const createAnswer = (data) => api.post('/answers', data).then(r => r.data)
+export const updateAnswerStatus = (id, status) =>
+ api.patch(`/answers/${id}/status`, { status }).then(r => r.data)
+export const upvoteAnswer = (id) => api.post(`/answers/${id}/upvote`).then(r => r.data)
+
+// AI Suggestion
+export const fetchSuggestion = (q) => api.get('/suggest', { params: { q } }).then(r => r.data)
+
+// Users / Leaderboard / Profile
+export const fetchLeaderboard = (limit = 20) => api.get('/users/leaderboard', { params: { limit } }).then(r => r.data)
+export const fetchUserProfile = (id) => api.get(`/users/${id}/profile`).then(r => r.data)
+export const fetchMyProfile = () => api.get('/users/me/profile').then(r => r.data)
diff --git a/client/src/utils/constants.js b/client/src/utils/constants.js
new file mode 100644
index 0000000..30b24c6
--- /dev/null
+++ b/client/src/utils/constants.js
@@ -0,0 +1,41 @@
+export const CATEGORIES = [
+ 'All',
+ 'Applications',
+ 'Stipend',
+ 'Timeline',
+ 'Certificates',
+ 'Projects',
+ 'Eligibility',
+ 'General',
+ 'Other',
+]
+
+export const QUESTION_CATEGORIES = CATEGORIES.filter(c => c !== 'All')
+
+export function timeAgo(date) {
+ const s = Math.floor((Date.now() - new Date(date)) / 1000)
+ if (s < 60) return 'just now'
+ const m = Math.floor(s / 60)
+ if (m < 60) return `${m}m ago`
+ const h = Math.floor(m / 60)
+ if (h < 24) return `${h}h ago`
+ const d = Math.floor(h / 24)
+ return `${d}d ago`
+}
+
+export const STATUS_META = {
+ pending: { label: 'Pending', color: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300' },
+ approved: { label: 'Approved', color: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300' },
+ rejected: { label: 'Rejected', color: 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300' },
+}
+
+export const CATEGORY_COLORS = {
+ Applications: 'bg-blue-50 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300',
+ Stipend: 'bg-emerald-50 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300',
+ Timeline: 'bg-violet-50 text-violet-700 dark:bg-violet-900/30 dark:text-violet-300',
+ Certificates: 'bg-amber-50 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300',
+ Projects: 'bg-pink-50 text-pink-700 dark:bg-pink-900/30 dark:text-pink-300',
+ Eligibility: 'bg-cyan-50 text-cyan-700 dark:bg-cyan-900/30 dark:text-cyan-300',
+ General: 'bg-primary-50 text-primary-700 dark:bg-primary-900/30 dark:text-primary-300',
+ Other: 'bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-300',
+}
diff --git a/client/tailwind.config.js b/client/tailwind.config.js
new file mode 100644
index 0000000..4c4536a
--- /dev/null
+++ b/client/tailwind.config.js
@@ -0,0 +1,26 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
+ darkMode: 'class',
+ theme: {
+ extend: {
+ fontFamily: { sans: ['Inter', 'sans-serif'] },
+ colors: {
+ primary: {
+ 50: '#eef2ff', 100: '#e0e7ff', 200: '#c7d2fe', 300: '#a5b4fc',
+ 400: '#818cf8', 500: '#6366f1', 600: '#4f46e5', 700: '#4338ca',
+ 800: '#3730a3', 900: '#312e81',
+ },
+ },
+ animation: {
+ 'fade-in': 'fadeIn 0.2s ease-out',
+ 'slide-down': 'slideDown 0.25s ease-out',
+ },
+ keyframes: {
+ fadeIn: { '0%': { opacity: 0, transform: 'translateY(-4px)' }, '100%': { opacity: 1, transform: 'translateY(0)' } },
+ slideDown: { '0%': { opacity: 0, maxHeight: '0px' }, '100%': { opacity: 1, maxHeight: '500px' } },
+ },
+ },
+ },
+ plugins: [],
+}
diff --git a/client/vite.config.js b/client/vite.config.js
new file mode 100644
index 0000000..03f7294
--- /dev/null
+++ b/client/vite.config.js
@@ -0,0 +1,15 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ port: 3000,
+ proxy: {
+ '/api': {
+ target: 'http://localhost:5000',
+ changeOrigin: true,
+ },
+ },
+ },
+})
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..68ffbef
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,373 @@
+{
+ "name": "crowdfaq",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "crowdfaq",
+ "version": "1.0.0",
+ "hasInstallScript": true,
+ "devDependencies": {
+ "concurrently": "^8.2.2"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
+ "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "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/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/chalk/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/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "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/concurrently": {
+ "version": "8.2.2",
+ "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz",
+ "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.1.2",
+ "date-fns": "^2.30.0",
+ "lodash": "^4.17.21",
+ "rxjs": "^7.8.1",
+ "shell-quote": "^1.8.1",
+ "spawn-command": "0.0.2",
+ "supports-color": "^8.1.1",
+ "tree-kill": "^1.2.2",
+ "yargs": "^17.7.2"
+ },
+ "bin": {
+ "conc": "dist/bin/concurrently.js",
+ "concurrently": "dist/bin/concurrently.js"
+ },
+ "engines": {
+ "node": "^14.13.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
+ }
+ },
+ "node_modules/date-fns": {
+ "version": "2.30.0",
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
+ "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.21.0"
+ },
+ "engines": {
+ "node": ">=0.11"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/date-fns"
+ }
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "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/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
+ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rxjs": {
+ "version": "7.8.2",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
+ "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/shell-quote": {
+ "version": "1.8.4",
+ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz",
+ "integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/spawn-command": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz",
+ "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==",
+ "dev": true
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "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/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/tree-kill": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
+ "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "tree-kill": "cli.js"
+ }
+ },
+ "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==",
+ "dev": true,
+ "license": "0BSD"
+ },
+ "node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..bd60ec4
--- /dev/null
+++ b/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "crowdfaq",
+ "version": "1.0.0",
+ "description": "A community-driven FAQ platform built with the MERN stack",
+ "private": true,
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=9.0.0"
+ },
+ "scripts": {
+ "postinstall": "npm install --prefix server && npm install --prefix client",
+ "dev": "concurrently --names \"server,client\" --prefix-colors \"blue,green\" \"npm run dev --prefix server\" \"npm run dev --prefix client\"",
+ "seed": "npm run seed --prefix server",
+ "build": "npm run build --prefix client",
+ "start": "npm run start --prefix server"
+ },
+ "keywords": [
+ "mern",
+ "faq",
+ "react",
+ "express",
+ "mongodb",
+ "crowdsourcing",
+ "jwt"
+ ],
+ "devDependencies": {
+ "concurrently": "^8.2.2"
+ }
+}
diff --git a/server/.env.example b/server/.env.example
new file mode 100644
index 0000000..3f08f6d
--- /dev/null
+++ b/server/.env.example
@@ -0,0 +1,10 @@
+# MongoDB connection string
+# Local: mongodb://localhost:27017/crowdfaq
+# Atlas: mongodb+srv://:@.mongodb.net/crowdfaq
+MONGO_URI=mongodb://localhost:27017/crowdfaq
+
+# Express server port
+PORT=5000
+
+# JWT signing secret — change this to a long random string in production
+JWT_SECRET=your-super-secret-jwt-key-change-me
diff --git a/server/index.js b/server/index.js
new file mode 100644
index 0000000..1186daa
--- /dev/null
+++ b/server/index.js
@@ -0,0 +1,51 @@
+const express = require('express');
+const mongoose = require('mongoose');
+const cors = require('cors');
+require('dotenv').config();
+
+const faqRoutes = require('./routes/faqs');
+const questionRoutes = require('./routes/questions');
+const answerRoutes = require('./routes/answers');
+const authRoutes = require('./routes/auth');
+const userRoutes = require('./routes/users');
+const suggestRoutes = require('./routes/suggest');
+
+const app = express();
+
+// Middleware
+app.use(cors());
+app.use(express.json());
+
+// Routes
+app.use('/api/auth', authRoutes);
+app.use('/api/faqs', faqRoutes);
+app.use('/api/questions', questionRoutes);
+app.use('/api/answers', answerRoutes);
+app.use('/api/users', userRoutes);
+app.use('/api/suggest', suggestRoutes);
+
+// Health check
+app.get('/api/health', (req, res) => {
+ res.json({ status: 'ok', message: 'CrowdFAQ API is running' });
+});
+
+// Global error handler
+app.use((err, req, res, next) => {
+ console.error(err.stack);
+ res.status(500).json({ error: 'Something went wrong on the server.' });
+});
+
+// MongoDB connection + server start
+const PORT = process.env.PORT || 5000;
+const MONGO_URI = process.env.MONGO_URI || 'mongodb://localhost:27017/crowdfaq';
+
+mongoose
+ .connect(MONGO_URI)
+ .then(() => {
+ console.log('✅ Connected to MongoDB');
+ app.listen(PORT, () => console.log(`🚀 Server running on http://localhost:${PORT}`));
+ })
+ .catch((err) => {
+ console.error('❌ MongoDB connection error:', err.message);
+ process.exit(1);
+ });
diff --git a/server/middleware/auth.js b/server/middleware/auth.js
new file mode 100644
index 0000000..559297c
--- /dev/null
+++ b/server/middleware/auth.js
@@ -0,0 +1,41 @@
+const jwt = require('jsonwebtoken');
+const User = require('../models/User');
+
+const JWT_SECRET = process.env.JWT_SECRET || 'crowdfaq-dev-secret-change-in-production';
+
+function signToken(userId) {
+ return jwt.sign({ userId }, JWT_SECRET, { expiresIn: '7d' });
+}
+
+async function requireAuth(req, res, next) {
+ const header = req.headers.authorization;
+ if (!header || !header.startsWith('Bearer ')) {
+ return res.status(401).json({ error: 'Authentication required. Please log in.' });
+ }
+
+ try {
+ const payload = jwt.verify(header.slice(7), JWT_SECRET);
+ const user = await User.findById(payload.userId);
+ if (!user) return res.status(401).json({ error: 'User not found. Please log in again.' });
+ req.user = user;
+ next();
+ } catch {
+ return res.status(401).json({ error: 'Invalid or expired token. Please log in again.' });
+ }
+}
+
+async function optionalAuth(req, res, next) {
+ const header = req.headers.authorization;
+ if (!header || !header.startsWith('Bearer ')) return next();
+
+ try {
+ const payload = jwt.verify(header.slice(7), JWT_SECRET);
+ const user = await User.findById(payload.userId);
+ if (user) req.user = user;
+ } catch {
+ /* ignore invalid token for optional auth */
+ }
+ next();
+}
+
+module.exports = { signToken, requireAuth, optionalAuth, JWT_SECRET };
diff --git a/server/models/Answer.js b/server/models/Answer.js
new file mode 100644
index 0000000..952e50f
--- /dev/null
+++ b/server/models/Answer.js
@@ -0,0 +1,17 @@
+const mongoose = require('mongoose');
+
+const answerSchema = new mongoose.Schema(
+ {
+ questionId: { type: mongoose.Schema.Types.ObjectId, ref: 'Question', required: true },
+ body: { type: String, required: true, trim: true },
+ author: { type: String, default: 'Anonymous', trim: true },
+ authorId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', default: null },
+ status: { type: String, enum: ['pending', 'approved', 'rejected'], default: 'pending' },
+ upvotes: { type: Number, default: 0 },
+ upvotedBy: [{ type: String }], // user emails — prevents double-voting
+ confidence: { type: Number, default: null }, // 0–100 AI-derived confidence score
+ },
+ { timestamps: true }
+);
+
+module.exports = mongoose.model('Answer', answerSchema);
diff --git a/server/models/FAQ.js b/server/models/FAQ.js
new file mode 100644
index 0000000..ed03ee3
--- /dev/null
+++ b/server/models/FAQ.js
@@ -0,0 +1,18 @@
+const mongoose = require('mongoose');
+
+const faqSchema = new mongoose.Schema(
+ {
+ question: { type: String, required: true, trim: true },
+ answer: { type: String, required: true, trim: true },
+ category: { type: String, default: 'General', trim: true },
+ tags: [{ type: String, trim: true }],
+ keywords: [{ type: String, trim: true }],
+ status: { type: String, enum: ['approved', 'pending', 'rejected'], default: 'approved' },
+ helpfulYes: { type: Number, default: 0 },
+ helpfulNo: { type: Number, default: 0 },
+ views: { type: Number, default: 0 },
+ },
+ { timestamps: true }
+);
+
+module.exports = mongoose.model('FAQ', faqSchema);
diff --git a/server/models/Question.js b/server/models/Question.js
new file mode 100644
index 0000000..654df3c
--- /dev/null
+++ b/server/models/Question.js
@@ -0,0 +1,17 @@
+const mongoose = require('mongoose');
+
+const questionSchema = new mongoose.Schema(
+ {
+ name: { type: String, required: true, trim: true },
+ email: { type: String, required: true, trim: true, lowercase: true },
+ title: { type: String, required: true, trim: true },
+ description: { type: String, required: true, trim: true },
+ category: { type: String, default: 'General', trim: true },
+ tags: [{ type: String, trim: true }],
+ status: { type: String, enum: ['pending', 'approved', 'rejected'], default: 'pending' },
+ answerCount: { type: Number, default: 0 },
+ },
+ { timestamps: true }
+);
+
+module.exports = mongoose.model('Question', questionSchema);
diff --git a/server/models/User.js b/server/models/User.js
new file mode 100644
index 0000000..eb271ce
--- /dev/null
+++ b/server/models/User.js
@@ -0,0 +1,41 @@
+const mongoose = require('mongoose');
+const bcrypt = require('bcryptjs');
+
+const userSchema = new mongoose.Schema(
+ {
+ name: { type: String, required: true, trim: true },
+ email: { type: String, required: true, unique: true, trim: true, lowercase: true },
+ password: { type: String, required: true, minlength: 6, select: false },
+ // Reputation
+ points: { type: Number, default: 0 },
+ level: { type: String, enum: ['Beginner', 'Contributor', 'Expert'], default: 'Beginner' },
+ stats: {
+ answersGiven: { type: Number, default: 0 },
+ answersApproved: { type: Number, default: 0 },
+ questionsAsked: { type: Number, default: 0 },
+ upvotesReceived: { type: Number, default: 0 },
+ },
+ // Saved FAQs
+ savedFAQs: [{ type: mongoose.Schema.Types.ObjectId, ref: 'FAQ' }],
+ },
+ { timestamps: true }
+);
+
+userSchema.pre('save', async function (next) {
+ if (!this.isModified('password')) return next();
+ this.password = await bcrypt.hash(this.password, 10);
+ next();
+});
+
+userSchema.methods.comparePassword = function (candidate) {
+ return bcrypt.compare(candidate, this.password);
+};
+
+/** Recompute level based on current points */
+userSchema.methods.recalcLevel = function () {
+ if (this.points >= 200) this.level = 'Expert';
+ else if (this.points >= 50) this.level = 'Contributor';
+ else this.level = 'Beginner';
+};
+
+module.exports = mongoose.model('User', userSchema);
diff --git a/server/package-lock.json b/server/package-lock.json
new file mode 100644
index 0000000..299f238
--- /dev/null
+++ b/server/package-lock.json
@@ -0,0 +1,1595 @@
+{
+ "name": "crowdfaq-server",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "crowdfaq-server",
+ "version": "1.0.0",
+ "dependencies": {
+ "bcryptjs": "^3.0.3",
+ "cors": "^2.8.5",
+ "dotenv": "^16.4.5",
+ "express": "^4.19.2",
+ "jsonwebtoken": "^9.0.3",
+ "mongoose": "^8.4.0"
+ },
+ "devDependencies": {
+ "nodemon": "^3.1.4"
+ }
+ },
+ "node_modules/@mongodb-js/saslprep": {
+ "version": "1.4.11",
+ "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.11.tgz",
+ "integrity": "sha512-o9rAHc0IpIjuPSxRutWpE1F62x7n+4mVS4rCNHkzhIUMQcc18bb6xEq5wd2NdN0WjepIyXIppRshYI2kQDOZVA==",
+ "license": "MIT",
+ "dependencies": {
+ "sparse-bitfield": "^3.0.3"
+ }
+ },
+ "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==",
+ "license": "MIT"
+ },
+ "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==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/webidl-conversions": "*"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "license": "MIT"
+ },
+ "node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/bcryptjs": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
+ "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
+ "license": "BSD-3-Clause",
+ "bin": {
+ "bcrypt": "bin/bcrypt"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.5",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz",
+ "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "~1.2.0",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "on-finished": "~2.4.1",
+ "qs": "~6.15.1",
+ "raw-body": "~2.5.3",
+ "type-is": "~1.6.18",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
+ "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "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": "6.10.4",
+ "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz",
+ "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=16.20.1"
+ }
+ },
+ "node_modules/buffer-equal-constant-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
+ "license": "MIT"
+ },
+ "node_modules/cors": {
+ "version": "2.8.6",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
+ "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "16.6.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ecdsa-sig-formatter": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+ "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.22.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
+ "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "~1.20.5",
+ "content-disposition": "~0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "~0.7.1",
+ "cookie-signature": "~1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "~1.3.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "~0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "~6.15.1",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "~0.19.0",
+ "serve-static": "~1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "~2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "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/finalhandler": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
+ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "~2.0.2",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "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/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "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/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ignore-by-default": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
+ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "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/jsonwebtoken": {
+ "version": "9.0.3",
+ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
+ "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
+ "license": "MIT",
+ "dependencies": {
+ "jws": "^4.0.1",
+ "lodash.includes": "^4.3.0",
+ "lodash.isboolean": "^3.0.3",
+ "lodash.isinteger": "^4.0.4",
+ "lodash.isnumber": "^3.0.3",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.isstring": "^4.0.1",
+ "lodash.once": "^4.0.0",
+ "ms": "^2.1.1",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ }
+ },
+ "node_modules/jsonwebtoken/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==",
+ "license": "MIT"
+ },
+ "node_modules/jwa": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
+ "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer-equal-constant-time": "^1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/jws": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
+ "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
+ "license": "MIT",
+ "dependencies": {
+ "jwa": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/kareem": {
+ "version": "2.6.3",
+ "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz",
+ "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/lodash.includes": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
+ "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isboolean": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isinteger": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
+ "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isnumber": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+ "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isstring": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
+ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.once": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
+ "license": "MIT"
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "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==",
+ "license": "MIT"
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.5"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/mongodb": {
+ "version": "6.20.0",
+ "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz",
+ "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==",
+ "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/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==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/whatwg-url": "^11.0.2",
+ "whatwg-url": "^14.1.0 || ^13.0.0"
+ }
+ },
+ "node_modules/mongoose": {
+ "version": "8.24.0",
+ "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.24.0.tgz",
+ "integrity": "sha512-EEZwOibDPZ5uZN3bFapfnRskEbdljAf6sP9ln6u+P4e5IfkOAh6Tqw2g8/Tag++KHOAJ095WXT/c0uqRq4Vckg==",
+ "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/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/mpath": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz",
+ "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==",
+ "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==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "4.x"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/mquery/node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/mquery/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==",
+ "license": "MIT"
+ },
+ "node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/nodemon": {
+ "version": "3.1.14",
+ "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz",
+ "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chokidar": "^3.5.2",
+ "debug": "^4",
+ "ignore-by-default": "^1.0.1",
+ "minimatch": "^10.2.1",
+ "pstree.remy": "^1.1.8",
+ "semver": "^7.5.3",
+ "simple-update-notifier": "^2.0.0",
+ "supports-color": "^5.5.0",
+ "touch": "^3.1.0",
+ "undefsafe": "^2.0.5"
+ },
+ "bin": {
+ "nodemon": "bin/nodemon.js"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/nodemon"
+ }
+ },
+ "node_modules/nodemon/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/nodemon/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/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
+ "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
+ "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/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/pstree.remy": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
+ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.15.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
+ "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "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/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.3.tgz",
+ "integrity": "sha512-wnilbGyMxzbY7dNOl7jpKbLSjcfeweJWU5j4+u5qW+6/wuGD9KzIGOyZnQVSBM9E7DtWaaH3CyHkppYrKYoxwg==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/send": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
+ "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.1",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "~2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "~2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/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==",
+ "license": "MIT"
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.3",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
+ "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "~0.19.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
+ "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4",
+ "side-channel-list": "^1.0.1",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "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==",
+ "license": "MIT"
+ },
+ "node_modules/simple-update-notifier": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
+ "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "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==",
+ "license": "MIT",
+ "dependencies": {
+ "memory-pager": "^1.0.2"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "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/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/touch": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
+ "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "nodetouch": "bin/nodetouch.js"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
+ "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/undefsafe": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
+ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "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==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "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==",
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "^5.1.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ }
+ }
+}
diff --git a/server/package.json b/server/package.json
new file mode 100644
index 0000000..830fe54
--- /dev/null
+++ b/server/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "crowdfaq-server",
+ "version": "1.0.0",
+ "description": "Backend API for CrowdFAQ",
+ "main": "index.js",
+ "scripts": {
+ "dev": "nodemon index.js",
+ "start": "node index.js",
+ "seed": "node seed.js"
+ },
+ "dependencies": {
+ "bcryptjs": "^3.0.3",
+ "cors": "^2.8.5",
+ "dotenv": "^16.4.5",
+ "express": "^4.19.2",
+ "jsonwebtoken": "^9.0.3",
+ "mongoose": "^8.4.0"
+ },
+ "devDependencies": {
+ "nodemon": "^3.1.4"
+ }
+}
diff --git a/server/routes/answers.js b/server/routes/answers.js
new file mode 100644
index 0000000..6f79abe
--- /dev/null
+++ b/server/routes/answers.js
@@ -0,0 +1,184 @@
+const express = require('express');
+const router = express.Router();
+const Answer = require('../models/Answer');
+const Question = require('../models/Question');
+const FAQ = require('../models/FAQ');
+const User = require('../models/User');
+const { requireAuth } = require('../middleware/auth');
+
+// ── Points config ─────────────────────────────────────────────────────────────
+const PTS = {
+ ANSWER_SUBMITTED: 5,
+ ANSWER_APPROVED: 15,
+ UPVOTE_RECEIVED: 10,
+};
+
+async function awardPoints(userId, delta, statField) {
+ if (!userId) return;
+ try {
+ const inc = { points: delta };
+ if (statField) inc[`stats.${statField}`] = 1;
+ const user = await User.findByIdAndUpdate(userId, { $inc: inc }, { new: true });
+ if (user) {
+ user.recalcLevel();
+ await user.save();
+ }
+ } catch { /* silent — reputation is non-critical */ }
+}
+
+/**
+ * Compute a confidence score (0–100) for an answer body against the parent question.
+ * Uses keyword overlap between the answer and approved FAQs — same logic as /api/suggest.
+ */
+async function computeConfidence(questionTitle, questionDesc, answerBody) {
+ try {
+ const STOP = new Set([
+ 'what', 'when', 'where', 'which', 'who', 'whom', 'how', 'why',
+ 'this', 'that', 'these', 'those', 'with', 'from', 'have', 'will',
+ 'they', 'them', 'their', 'there', 'been', 'would', 'could', 'should',
+ 'does', 'just', 'some', 'than', 'then', 'also', 'into', 'about',
+ 'more', 'very', 'much', 'many', 'such', 'your', 'please',
+ ]);
+
+ const questionWords = new Set(
+ `${questionTitle} ${questionDesc}`.toLowerCase()
+ .split(/\W+/).filter(w => w.length > 3 && !STOP.has(w))
+ );
+ const answerWords = new Set(
+ answerBody.toLowerCase()
+ .split(/\W+/).filter(w => w.length > 3 && !STOP.has(w))
+ );
+
+ if (!questionWords.size || !answerWords.size) return null;
+
+ // Find related FAQs to cross-check answer relevance
+ const faqs = await FAQ.find({ status: 'approved' }).limit(100);
+ if (!faqs.length) return null;
+
+ // Score: what % of question keywords does the answer address?
+ const coverageWords = [...questionWords].filter(w => answerWords.has(w));
+ const coverage = coverageWords.length / questionWords.size;
+
+ // Also check how well the answer aligns with FAQ content
+ let bestFaqOverlap = 0;
+ for (const faq of faqs) {
+ const corpus = [faq.question, faq.answer, ...(faq.tags || []), ...(faq.keywords || [])]
+ .join(' ').toLowerCase().split(/\W+/).filter(w => w.length > 3);
+ const faqWords = new Set(corpus);
+ const overlap = [...answerWords].filter(w => faqWords.has(w)).length / Math.max(answerWords.size, 1);
+ if (overlap > bestFaqOverlap) bestFaqOverlap = overlap;
+ }
+
+ // Blend: 60% answer coverage of question + 40% FAQ knowledge alignment
+ const score = (coverage * 0.6 + bestFaqOverlap * 0.4);
+ return Math.min(100, Math.round(score * 100));
+ } catch {
+ return null;
+ }
+}
+
+// GET /api/answers/:questionId — optional ?status= filter
+router.get('/:questionId', async (req, res) => {
+ try {
+ const query = { questionId: req.params.questionId };
+ if (req.query.status) query.status = req.query.status;
+ const answers = await Answer.find(query).sort({ createdAt: 1 });
+ res.json(answers);
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// POST /api/answers — requires auth; awards points on submit; computes confidence
+router.post('/', requireAuth, async (req, res) => {
+ try {
+ const { questionId, body } = req.body;
+ if (!questionId || !body)
+ return res.status(400).json({ error: 'questionId and body are required.' });
+
+ const question = await Question.findById(questionId);
+ if (!question) return res.status(404).json({ error: 'Question not found' });
+
+ // Compute confidence score
+ const confidence = await computeConfidence(question.title, question.description, body);
+
+ const answer = new Answer({
+ questionId,
+ body,
+ author: req.user.name,
+ authorId: req.user._id,
+ status: 'pending',
+ confidence,
+ });
+ const saved = await answer.save();
+
+ await Question.findByIdAndUpdate(questionId, { $inc: { answerCount: 1 } });
+
+ // Award points for submitting
+ await awardPoints(req.user._id, PTS.ANSWER_SUBMITTED, 'answersGiven');
+
+ res.status(201).json(saved);
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// PATCH /api/answers/:id/status — approve | reject | pending
+router.patch('/:id/status', async (req, res) => {
+ try {
+ const { status } = req.body;
+ if (!['pending', 'approved', 'rejected'].includes(status))
+ return res.status(400).json({ error: 'Invalid status.' });
+
+ const prev = await Answer.findById(req.params.id);
+ if (!prev) return res.status(404).json({ error: 'Answer not found' });
+
+ const answer = await Answer.findByIdAndUpdate(req.params.id, { status }, { new: true });
+
+ // Award approval bonus (only when transitioning to approved)
+ if (status === 'approved' && prev.status !== 'approved' && prev.authorId) {
+ await awardPoints(prev.authorId, PTS.ANSWER_APPROVED, 'answersApproved');
+ }
+ // Reverse approval points if moving away from approved
+ if (prev.status === 'approved' && status !== 'approved' && prev.authorId) {
+ await awardPoints(prev.authorId, -PTS.ANSWER_APPROVED, null);
+ await User.findByIdAndUpdate(prev.authorId, { $inc: { 'stats.answersApproved': -1 } });
+ }
+
+ res.json(answer);
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// POST /api/answers/:id/upvote — toggle upvote (requires auth)
+router.post('/:id/upvote', requireAuth, async (req, res) => {
+ try {
+ const answer = await Answer.findById(req.params.id);
+ if (!answer) return res.status(404).json({ error: 'Answer not found' });
+
+ const voterEmail = req.user.email;
+ const alreadyVoted = answer.upvotedBy.includes(voterEmail);
+
+ if (alreadyVoted) {
+ answer.upvotes = Math.max(0, answer.upvotes - 1);
+ answer.upvotedBy = answer.upvotedBy.filter(e => e !== voterEmail);
+ await answer.save();
+ if (answer.authorId) {
+ await awardPoints(answer.authorId, -PTS.UPVOTE_RECEIVED, null);
+ await User.findByIdAndUpdate(answer.authorId, { $inc: { 'stats.upvotesReceived': -1 } });
+ }
+ } else {
+ answer.upvotes += 1;
+ answer.upvotedBy.push(voterEmail);
+ await answer.save();
+ if (answer.authorId) await awardPoints(answer.authorId, PTS.UPVOTE_RECEIVED, 'upvotesReceived');
+ }
+
+ res.json({ upvotes: answer.upvotes, voted: !alreadyVoted });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+module.exports = router;
diff --git a/server/routes/auth.js b/server/routes/auth.js
new file mode 100644
index 0000000..5be18ae
--- /dev/null
+++ b/server/routes/auth.js
@@ -0,0 +1,106 @@
+const express = require('express');
+const router = express.Router();
+const User = require('../models/User');
+const FAQ = require('../models/FAQ');
+const { signToken, requireAuth } = require('../middleware/auth');
+
+const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+
+function publicUser(user) {
+ return {
+ id: user._id,
+ name: user.name,
+ email: user.email,
+ points: user.points || 0,
+ level: user.level || 'Beginner',
+ stats: user.stats || {},
+ savedFAQs: user.savedFAQs || [],
+ createdAt: user.createdAt,
+ };
+}
+
+// POST /api/auth/register
+router.post('/register', async (req, res) => {
+ try {
+ const { name, email, password } = req.body;
+
+ if (!name?.trim() || !email?.trim() || !password) {
+ return res.status(400).json({ error: 'Name, email, and password are required.' });
+ }
+ if (!EMAIL_RE.test(email)) {
+ return res.status(400).json({ error: 'Please enter a valid email address.' });
+ }
+ if (password.length < 6) {
+ return res.status(400).json({ error: 'Password must be at least 6 characters.' });
+ }
+
+ const exists = await User.findOne({ email: email.toLowerCase() });
+ if (exists) return res.status(409).json({ error: 'An account with this email already exists.' });
+
+ const user = await User.create({ name: name.trim(), email: email.trim(), password });
+ const token = signToken(user._id);
+
+ res.status(201).json({ user: publicUser(user), token });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// POST /api/auth/login
+router.post('/login', async (req, res) => {
+ try {
+ const { email, password } = req.body;
+
+ if (!email?.trim() || !password) {
+ return res.status(400).json({ error: 'Email and password are required.' });
+ }
+
+ const user = await User.findOne({ email: email.toLowerCase() }).select('+password');
+ if (!user || !(await user.comparePassword(password))) {
+ return res.status(401).json({ error: 'Invalid email or password.' });
+ }
+
+ const token = signToken(user._id);
+ res.json({ user: publicUser(user), token });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// GET /api/auth/me — full profile with resolved saved FAQs
+router.get('/me', requireAuth, async (req, res) => {
+ try {
+ const user = await User.findById(req.user._id).populate('savedFAQs');
+ res.json({ user: { ...publicUser(user), savedFAQs: user.savedFAQs || [] } });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// POST /api/auth/save-faq/:faqId — toggle save/unsave
+router.post('/save-faq/:faqId', requireAuth, async (req, res) => {
+ try {
+ const { faqId } = req.params;
+ const user = await User.findById(req.user._id);
+ if (!user) return res.status(404).json({ error: 'User not found' });
+
+ const idx = user.savedFAQs.findIndex(id => id.toString() === faqId);
+ let saved;
+ if (idx === -1) {
+ // Verify FAQ exists
+ const faq = await FAQ.findById(faqId);
+ if (!faq) return res.status(404).json({ error: 'FAQ not found' });
+ user.savedFAQs.push(faqId);
+ saved = true;
+ } else {
+ user.savedFAQs.splice(idx, 1);
+ saved = false;
+ }
+ await user.save();
+ res.json({ saved, savedFAQs: user.savedFAQs });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+module.exports = router;
diff --git a/server/routes/faqs.js b/server/routes/faqs.js
new file mode 100644
index 0000000..5530e1c
--- /dev/null
+++ b/server/routes/faqs.js
@@ -0,0 +1,105 @@
+const express = require('express');
+const router = express.Router();
+const FAQ = require('../models/FAQ');
+
+// GET /api/faqs — search, category filter, sort
+router.get('/', async (req, res) => {
+ try {
+ const { search, category, sort = 'latest' } = req.query;
+
+ let query = { status: 'approved' };
+
+ if (category && category !== 'All') query.category = category;
+
+ if (search && search.trim()) {
+ const rx = { $regex: search.trim(), $options: 'i' };
+ query.$or = [
+ { question: rx }, { answer: rx },
+ { tags: rx }, { keywords: rx }, { category: rx },
+ ];
+ }
+
+ const sortMap = { helpful: { helpfulYes: -1 }, views: { views: -1 }, latest: { createdAt: -1 } };
+ const sortOption = sortMap[sort] || { createdAt: -1 };
+
+ const faqs = await FAQ.find(query).sort(sortOption);
+ res.json(faqs);
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// GET /api/faqs/categories — distinct approved categories
+router.get('/categories', async (req, res) => {
+ try {
+ const cats = await FAQ.distinct('category', { status: 'approved' });
+ res.json(cats.sort());
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+/**
+ * GET /api/faqs/related?category=&tags=&exclude=
+ * "People like you also asked" — finds FAQs in the same category / sharing tags.
+ * Returns up to 4 results, excluding the specified FAQ id.
+ */
+router.get('/related', async (req, res) => {
+ try {
+ const { category, tags, exclude } = req.query;
+ const tagList = tags ? tags.split(',').map(t => t.trim()).filter(Boolean) : [];
+
+ const query = { status: 'approved' };
+ if (exclude) query._id = { $ne: exclude };
+
+ // Build OR: same category OR any matching tag
+ const orClauses = [];
+ if (category && category !== 'General') orClauses.push({ category });
+ if (tagList.length) orClauses.push({ tags: { $in: tagList } });
+
+ if (orClauses.length) query.$or = orClauses;
+
+ const related = await FAQ.find(query)
+ .sort({ helpfulYes: -1, views: -1 })
+ .limit(4)
+ .select('question category tags helpfulYes views');
+
+ res.json(related);
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// GET /api/faqs/:id — single FAQ + increment views
+router.get('/:id', async (req, res) => {
+ try {
+ const faq = await FAQ.findByIdAndUpdate(
+ req.params.id,
+ { $inc: { views: 1 } },
+ { new: true }
+ );
+ if (!faq) return res.status(404).json({ error: 'FAQ not found' });
+ res.json(faq);
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// POST /api/faqs/:id/vote — { vote: 'yes' | 'no' }
+router.post('/:id/vote', async (req, res) => {
+ try {
+ const { vote } = req.body;
+ if (!['yes', 'no'].includes(vote))
+ return res.status(400).json({ error: 'vote must be "yes" or "no"' });
+
+ const inc = vote === 'yes' ? { helpfulYes: 1 } : { helpfulNo: 1 };
+ const faq = await FAQ.findByIdAndUpdate(req.params.id, { $inc: inc }, { new: true });
+ if (!faq) return res.status(404).json({ error: 'FAQ not found' });
+
+ res.json({ helpfulYes: faq.helpfulYes, helpfulNo: faq.helpfulNo });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+module.exports = router;
diff --git a/server/routes/questions.js b/server/routes/questions.js
new file mode 100644
index 0000000..246e4c4
--- /dev/null
+++ b/server/routes/questions.js
@@ -0,0 +1,128 @@
+const express = require('express');
+const router = express.Router();
+const Question = require('../models/Question');
+const FAQ = require('../models/FAQ');
+const User = require('../models/User');
+const { requireAuth } = require('../middleware/auth');
+
+async function awardPoints(userId, delta, statField) {
+ if (!userId) return;
+ try {
+ const inc = { points: delta };
+ if (statField) inc[`stats.${statField}`] = 1;
+ const user = await User.findByIdAndUpdate(userId, { $inc: inc }, { new: true });
+ if (user) { user.recalcLevel(); await user.save(); }
+ } catch { /* silent */ }
+}
+
+const SPAM_WINDOW_MS = 60 * 60 * 1000; // 1 hour
+
+// GET /api/questions — list with status/category/search filters
+router.get('/', async (req, res) => {
+ try {
+ const { status, category, search } = req.query;
+ let query = {};
+
+ if (status && status !== 'all') query.status = status;
+ if (category && category !== 'All') query.category = category;
+ if (search && search.trim()) {
+ const rx = { $regex: search.trim(), $options: 'i' };
+ query.$or = [{ title: rx }, { description: rx }, { name: rx }];
+ }
+
+ const questions = await Question.find(query).sort({ createdAt: -1 });
+ res.json(questions);
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// GET /api/questions/similar?q= — duplicate / similar detection
+router.get('/similar', async (req, res) => {
+ try {
+ const { q } = req.query;
+ if (!q || q.trim().length < 4) return res.json({ faqs: [], questions: [] });
+
+ // Build OR from meaningful words (>3 chars)
+ const words = q.trim().split(/\s+/).filter(w => w.length > 3);
+ if (!words.length) return res.json({ faqs: [], questions: [] });
+
+ const rx = { $regex: words.join('|'), $options: 'i' };
+
+ const [faqs, questions] = await Promise.all([
+ FAQ.find({ $or: [{ question: rx }, { keywords: rx }, { tags: rx }], status: 'approved' }).limit(4),
+ Question.find({ title: rx, status: { $in: ['pending', 'approved'] } }).limit(3),
+ ]);
+
+ res.json({ faqs, questions });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// GET /api/questions/:id
+router.get('/:id', async (req, res) => {
+ try {
+ const q = await Question.findById(req.params.id);
+ if (!q) return res.status(404).json({ error: 'Question not found' });
+ res.json(q);
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// POST /api/questions — requires auth; spam prevention by email
+router.post('/', requireAuth, async (req, res) => {
+ try {
+ const { title, description, category, tags } = req.body;
+ const name = req.user.name;
+ const email = req.user.email;
+
+ if (!title || !description)
+ return res.status(400).json({ error: 'Title and description are required.' });
+
+ // Spam check — same email within 1 hour
+ const oneHourAgo = new Date(Date.now() - SPAM_WINDOW_MS);
+ const recent = await Question.findOne({ email: email.toLowerCase(), createdAt: { $gte: oneHourAgo } });
+ if (recent) {
+ const waitMin = Math.ceil((recent.createdAt.getTime() + SPAM_WINDOW_MS - Date.now()) / 60000);
+ return res.status(429).json({
+ error: `You already submitted a question recently. Please wait ${waitMin} more minute(s).`,
+ });
+ }
+
+ const parsedTags = Array.isArray(tags)
+ ? tags
+ : (tags ? String(tags).split(',').map(t => t.trim()).filter(Boolean) : []);
+
+ const question = new Question({
+ name, email: email.toLowerCase(), title, description,
+ category: category || 'General', tags: parsedTags, status: 'pending',
+ });
+ const saved = await question.save();
+
+ // Award 2 points for asking a question
+ await awardPoints(req.user._id, 2, 'questionsAsked');
+
+ res.status(201).json(saved);
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// PATCH /api/questions/:id/status — approve | reject | pending
+router.patch('/:id/status', async (req, res) => {
+ try {
+ const { status } = req.body;
+ if (!['pending', 'approved', 'rejected'].includes(status))
+ return res.status(400).json({ error: 'Invalid status.' });
+
+ const q = await Question.findByIdAndUpdate(req.params.id, { status }, { new: true });
+ if (!q) return res.status(404).json({ error: 'Question not found' });
+ res.json(q);
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+module.exports = router;
diff --git a/server/routes/suggest.js b/server/routes/suggest.js
new file mode 100644
index 0000000..292e047
--- /dev/null
+++ b/server/routes/suggest.js
@@ -0,0 +1,87 @@
+const express = require('express');
+const router = express.Router();
+const FAQ = require('../models/FAQ');
+
+/**
+ * GET /api/suggest?q=
+ *
+ * AI-Powered Suggested Answer (no external API key required).
+ * Finds approved FAQs that closely match the question text using word-overlap
+ * scoring, then synthesises a suggestion from the best match.
+ *
+ * Response:
+ * { suggestion: string | null, basedOn: FAQ | null, confidence: number }
+ */
+router.get('/', async (req, res) => {
+ try {
+ const { q } = req.query;
+ if (!q || q.trim().length < 4) {
+ return res.json({ suggestion: null, basedOn: null, confidence: 0 });
+ }
+
+ const queryText = q.trim().toLowerCase();
+
+ // Tokenise — words > 3 chars, strip common stop words
+ const STOP = new Set([
+ 'what', 'when', 'where', 'which', 'who', 'whom', 'how', 'why',
+ 'this', 'that', 'these', 'those', 'with', 'from', 'have', 'will',
+ 'they', 'them', 'their', 'there', 'been', 'would', 'could', 'should',
+ 'does', 'just', 'some', 'than', 'then', 'also', 'into', 'about',
+ 'more', 'very', 'much', 'many', 'such', 'your', 'your', 'please',
+ ]);
+
+ const keywords = [...new Set(
+ queryText.split(/\W+/).filter(w => w.length > 3 && !STOP.has(w))
+ )];
+
+ if (!keywords.length) {
+ return res.json({ suggestion: null, basedOn: null, confidence: 0 });
+ }
+
+ // Pull all approved FAQs (capped to keep it fast)
+ const faqs = await FAQ.find({ status: 'approved' }).limit(200);
+
+ // Score each FAQ by word-overlap across question + answer + tags + keywords
+ const scored = faqs.map(faq => {
+ const corpus = [
+ faq.question,
+ faq.answer,
+ ...(faq.tags || []),
+ ...(faq.keywords || []),
+ ].join(' ').toLowerCase();
+
+ const hits = keywords.filter(w => corpus.includes(w)).length;
+ // Normalised score: hits / total keywords
+ const score = hits / keywords.length;
+ return { faq, score, hits };
+ }).filter(x => x.score > 0);
+
+ if (!scored.length) {
+ return res.json({ suggestion: null, basedOn: null, confidence: 0 });
+ }
+
+ // Pick best match
+ scored.sort((a, b) => b.score - a.score);
+ const best = scored[0];
+
+ // Compose a suggestion from the matching FAQ answer
+ const CONFIDENCE_THRESHOLD = 0.25; // at least 25 % keyword overlap
+ if (best.score < CONFIDENCE_THRESHOLD) {
+ return res.json({ suggestion: null, basedOn: null, confidence: best.score });
+ }
+
+ const suggestion =
+ `Based on existing FAQs: "${best.faq.answer}"` +
+ ` (Source: "${best.faq.question}")`;
+
+ res.json({
+ suggestion,
+ basedOn: best.faq,
+ confidence: parseFloat(best.score.toFixed(2)),
+ });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+module.exports = router;
diff --git a/server/routes/users.js b/server/routes/users.js
new file mode 100644
index 0000000..9e51d99
--- /dev/null
+++ b/server/routes/users.js
@@ -0,0 +1,65 @@
+const express = require('express');
+const router = express.Router();
+const User = require('../models/User');
+const Answer = require('../models/Answer');
+const Question = require('../models/Question');
+const { requireAuth } = require('../middleware/auth');
+
+// GET /api/users/leaderboard — top contributors by points
+router.get('/leaderboard', async (req, res) => {
+ try {
+ const limit = Math.min(parseInt(req.query.limit) || 20, 50);
+ const users = await User
+ .find({ points: { $gt: 0 } })
+ .select('name points level stats createdAt')
+ .sort({ points: -1 })
+ .limit(limit);
+ res.json(users);
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// GET /api/users/me/profile — authenticated detailed profile
+router.get('/me/profile', requireAuth, async (req, res) => {
+ try {
+ const user = await User.findById(req.user._id).populate('savedFAQs');
+ if (!user) return res.status(404).json({ error: 'User not found' });
+
+ // Fetch recent questions and answers by this user
+ const [questions, answers] = await Promise.all([
+ Question.find({ email: user.email }).sort({ createdAt: -1 }).limit(10),
+ Answer.find({ authorId: user._id }).sort({ createdAt: -1 }).limit(10).populate('questionId', 'title'),
+ ]);
+
+ res.json({
+ id: user._id,
+ name: user.name,
+ email: user.email,
+ points: user.points || 0,
+ level: user.level || 'Beginner',
+ stats: user.stats || {},
+ savedFAQs: user.savedFAQs || [],
+ createdAt: user.createdAt,
+ questions,
+ answers,
+ });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// GET /api/users/:id/profile — public profile
+router.get('/:id/profile', async (req, res) => {
+ try {
+ const user = await User
+ .findById(req.params.id)
+ .select('name points level stats createdAt');
+ if (!user) return res.status(404).json({ error: 'User not found' });
+ res.json(user);
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+module.exports = router;
diff --git a/server/seed.js b/server/seed.js
new file mode 100644
index 0000000..20eb309
--- /dev/null
+++ b/server/seed.js
@@ -0,0 +1,120 @@
+const mongoose = require('mongoose');
+require('dotenv').config();
+const FAQ = require('./models/FAQ');
+
+const MONGO_URI = process.env.MONGO_URI || 'mongodb://localhost:27017/crowdfaq';
+
+const sampleFAQs = [
+ {
+ question: 'Who is eligible to apply for the internship?',
+ answer: 'Students currently in their 2nd or 3rd year of undergraduate study are eligible to apply. Final-year students and postgraduate students may also apply depending on the program. Check the specific program announcement for exact eligibility criteria.',
+ category: 'Applications',
+ tags: ['eligibility', 'students', 'requirements'],
+ keywords: ['eligible', 'apply', 'year', 'undergraduate', 'student'],
+ status: 'approved',
+ helpfulYes: 42, helpfulNo: 2, views: 210,
+ },
+ {
+ question: 'How do I submit my application?',
+ answer: 'Applications are submitted through the official portal at the link provided in the program announcement. Fill in all required fields, attach your resume, a statement of purpose, and any supporting documents. Make sure to submit before the deadline — late applications are not accepted.',
+ category: 'Applications',
+ tags: ['application', 'portal', 'submission', 'documents'],
+ keywords: ['submit', 'apply', 'portal', 'form', 'deadline'],
+ status: 'approved',
+ helpfulYes: 38, helpfulNo: 1, views: 185,
+ },
+ {
+ question: 'What is the stipend for the internship?',
+ answer: 'The monthly stipend varies by program and role. Typically, stipends range from ₹10,000 to ₹25,000 per month based on your year of study, skills, and the specific project. The exact amount will be mentioned in your offer letter.',
+ category: 'Stipend',
+ tags: ['stipend', 'salary', 'pay', 'monthly'],
+ keywords: ['stipend', 'salary', 'money', 'pay', 'compensation'],
+ status: 'approved',
+ helpfulYes: 55, helpfulNo: 3, views: 320,
+ },
+ {
+ question: 'When will stipends be disbursed?',
+ answer: 'Stipends are typically disbursed on the last working day of each month. For the first month, disbursement may be delayed by 7–10 days due to onboarding processes. All payments are made directly to your registered bank account.',
+ category: 'Stipend',
+ tags: ['stipend', 'payment', 'disbursement', 'bank'],
+ keywords: ['when', 'paid', 'disbursed', 'payment date', 'bank'],
+ status: 'approved',
+ helpfulYes: 30, helpfulNo: 1, views: 150,
+ },
+ {
+ question: 'What is the duration and timeline of the internship?',
+ answer: 'The internship typically runs for 8–12 weeks during the summer (May–July) or winter (December–January) break. The exact start and end dates are communicated in the offer letter. Attendance for the full duration is mandatory.',
+ category: 'Timeline',
+ tags: ['duration', 'timeline', 'dates', 'schedule'],
+ keywords: ['duration', 'how long', 'timeline', 'weeks', 'start', 'end date'],
+ status: 'approved',
+ helpfulYes: 47, helpfulNo: 0, views: 240,
+ },
+ {
+ question: 'When does the application window open and close?',
+ answer: 'Applications typically open 6–8 weeks before the internship start date. The window remains open for 3–4 weeks. Specific dates are announced on the official website and social media channels. Enable notifications to stay updated.',
+ category: 'Timeline',
+ tags: ['deadline', 'application window', 'dates', 'open'],
+ keywords: ['deadline', 'when', 'open', 'close', 'window', 'application date'],
+ status: 'approved',
+ helpfulYes: 35, helpfulNo: 2, views: 175,
+ },
+ {
+ question: 'Will I receive a certificate after completing the internship?',
+ answer: 'Yes! All interns who successfully complete the program and meet the performance requirements receive an official completion certificate. The certificate is issued digitally within 2–4 weeks of the internship end date and can be verified online.',
+ category: 'Certificates',
+ tags: ['certificate', 'completion', 'credential', 'digital'],
+ keywords: ['certificate', 'certification', 'get', 'receive', 'completion', 'credential'],
+ status: 'approved',
+ helpfulYes: 61, helpfulNo: 1, views: 290,
+ },
+ {
+ question: 'What kind of projects will I work on?',
+ answer: 'Projects span multiple domains including software development, data science, machine learning, UI/UX design, research, and product management. You will be assigned to a real team and contribute to live projects. The specific project is shared after selection based on your skills and preferences.',
+ category: 'Projects',
+ tags: ['projects', 'domains', 'work', 'assignments', 'tech'],
+ keywords: ['project', 'work on', 'domain', 'assignment', 'type', 'task'],
+ status: 'approved',
+ helpfulYes: 50, helpfulNo: 2, views: 265,
+ },
+ {
+ question: 'Is the internship remote, hybrid, or on-site?',
+ answer: 'Most positions offer a hybrid mode — 2–3 days on-site per week. Some roles may be fully remote depending on the team. The mode of work is mentioned in the individual job listing. Relocation support is not provided for hybrid/on-site roles.',
+ category: 'General',
+ tags: ['remote', 'hybrid', 'on-site', 'work mode'],
+ keywords: ['remote', 'online', 'hybrid', 'office', 'work from home', 'on-site'],
+ status: 'approved',
+ helpfulYes: 44, helpfulNo: 3, views: 200,
+ },
+ {
+ question: 'Can I apply for multiple roles or programs?',
+ answer: 'Yes, you may apply to up to 2 different roles or programs simultaneously. However, if selected for more than one, you must accept only one offer within 48 hours of receiving it. Applying to multiple roles does not increase your chances — tailor each application to the specific role.',
+ category: 'Applications',
+ tags: ['multiple', 'roles', 'apply', 'programs'],
+ keywords: ['multiple', 'two', 'more than one', 'apply', 'roles', 'programs'],
+ status: 'approved',
+ helpfulYes: 28, helpfulNo: 4, views: 135,
+ },
+];
+
+async function seed() {
+ try {
+ await mongoose.connect(MONGO_URI);
+ console.log('✅ Connected to MongoDB');
+
+ await FAQ.deleteMany({});
+ console.log('🗑️ Cleared existing FAQs');
+
+ await FAQ.insertMany(sampleFAQs);
+ console.log(`🌱 Seeded ${sampleFAQs.length} FAQs successfully`);
+
+ await mongoose.disconnect();
+ console.log('🔌 Disconnected');
+ process.exit(0);
+ } catch (err) {
+ console.error('❌ Seeding error:', err.message);
+ process.exit(1);
+ }
+}
+
+seed();