diff --git a/.gitignore b/.gitignore index 29c7eca..50ebd38 100644 --- a/.gitignore +++ b/.gitignore @@ -32,7 +32,8 @@ CHANGELOG.md # yarn.lock # Uploads / media -uploads/ +uploads/* +!uploads/.gitkeep # Test coverage coverage/ \ No newline at end of file diff --git a/CONTEXT.md b/CONTEXT.md index 63f97dc..a2a791a 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -1,57 +1,67 @@ -# CONTEXT.md β€” Q&A Platform Project Context +# CONTEXT.md β€” PippaQ Project Context -> Generated: 2026-05-28 | Last updated: 2026-05-29 (FAQ category upvoting & Qdrant UUID fix) +> Last updated: 2026-06-04 | About Page Symbolism Icons, Directed Release Animations, Card Hover Clipping Fix, and Q-Card Text Overflow Resolution --- ## πŸ“Œ What This Is -A semantic query-resolution and FAQ generation platform with a **QP (Quality Point) reputation economy**, **role-based access control**, **Qdrant Cloud vector search**, and **admin-controlled email whitelist signup**. Users raise real-time queries, get peer/moderator/senior answers, and high-quality content graduates into an approved FAQ knowledge base. +**PippaQ** is a high-performance, semantic query-resolution and FAQ generation platform featuring a **QP (Quality Point) reputation economy**, **role-based access control**, **Qdrant Cloud vector search**, **local Sentence Transformers**, and **admin-controlled email whitelist signup**. Users raise real-time queries (RTQs), get peer/moderator/senior answers, and high-quality content graduates into an approved FAQ knowledge base. -**Status:** All 19 diagnosed bugs fixed. Qdrant Cloud integrated. Admin role system active. Email whitelist signup gate active. Server on port 5000, client on port 3000. +**Status:** Fully operational. All legacy TF-IDF in-memory vector DBs have been replaced with a production-ready **Sentence Transformer + Qdrant Cloud ANN** pipeline. Complete auto-upvote, multi-moderator RTQ loops, decision rollback transactions, and QP execution loops are fully integrated. Premium typography and branding (Playfair Display & Outfit) are applied. --- ## πŸ—‚οΈ Directory Structure -``` +```text FAQ-main/ β”œβ”€β”€ SPEC.md β”œβ”€β”€ CONTEXT.md +β”œβ”€β”€ README.md β”œβ”€β”€ client/ β”‚ └── src/ β”‚ β”œβ”€β”€ App.jsx # Role-based dashboard routing; Nav included +β”‚ β”œβ”€β”€ index.css # Custom elegant typography definitions (Playfair Display & Outfit) β”‚ β”œβ”€β”€ components/ -β”‚ β”‚ β”œβ”€β”€ Nav.jsx # Persistent nav bar +β”‚ β”‚ β”œβ”€β”€ Nav.jsx # Persistent navbar styled with PippaQ branding + QP animation β”‚ β”‚ β”œβ”€β”€ AnswerCard.jsx # Real user ID upvote check β”‚ β”‚ β”œβ”€β”€ QPBadge.jsx β”‚ β”‚ β”œβ”€β”€ QuestionCard.jsx β”‚ β”‚ β”œβ”€β”€ RoleGuard.jsx -β”‚ β”‚ └── UpvoteButton.jsx +β”‚ β”‚ β”œβ”€β”€ UpvoteButton.jsx +β”‚ β”‚ β”œβ”€β”€ GlobalSearch.jsx # "/" shortcut β†’ overlay search FAQ+RTQ +β”‚ β”‚ └── MiniChart.jsx # SVG sparkline for 7-day trend charts β”‚ β”œβ”€β”€ context/ β”‚ β”‚ β”œβ”€β”€ AuthContext.jsx # JWT role+qp; requestAccess β”‚ β”‚ └── QPContext.jsx β”‚ β”œβ”€β”€ pages/ -β”‚ β”‚ β”œβ”€β”€ SignupPage.jsx # Email whitelist gate + request approval flow +β”‚ β”‚ β”œβ”€β”€ SignupPage.jsx # Whitelist Signup + PippaQ community reference +β”‚ β”‚ β”œβ”€β”€ LoginPage.jsx # styled PippaQ brand logo β”‚ β”‚ β”œβ”€β”€ UserListPage.jsx # Admin: Users + Whitelist + Access Requests tabs -β”‚ β”‚ β”œβ”€β”€ FAQPage.jsx # Category upvote buttons + ranked sorting +β”‚ β”‚ β”œβ”€β”€ FAQPage.jsx # Category upvote buttons + settings gear icon dropdown menu +β”‚ β”‚ β”œβ”€β”€ UserProfilePage.jsx # User profile at /users/:id β”‚ β”‚ └── ... (all other pages) β”‚ β”œβ”€β”€ routes/ β”‚ └── services/ β”‚ β”œβ”€β”€ auth.service.js # requestAccess method -β”‚ β”œβ”€β”€ faq.service.js # listCategoriesRanked, upvoteCategory -β”‚ └── admin.service.js # Whitelist + Access Request API +β”‚ β”œβ”€β”€ faq.service.js # listCategoriesRanked, upvoteCategory, reviewFAQ, toggleTrendingFAQ +β”‚ β”œβ”€β”€ admin.service.js # Whitelist + Access Request API +β”‚ └── dashboard.service.js # Dashboard stats + activity feed β”œβ”€β”€ server/ β”‚ └── src/ β”‚ β”œβ”€β”€ config/ β”‚ β”‚ β”œβ”€β”€ db.js β”‚ β”‚ β”œβ”€β”€ env.js -β”‚ β”‚ └── qdrant.js # NEW: Qdrant Cloud singleton client +β”‚ β”‚ └── qdrant.js # Qdrant Cloud singleton client β”‚ β”œβ”€β”€ controllers/ β”‚ β”‚ β”œβ”€β”€ admin.controller.js # Full user CRUD; role management; block/unblock -β”‚ β”‚ β”œβ”€β”€ admin.whitelist.controller.js # NEW: whitelist + access request mgmt +β”‚ β”‚ β”œβ”€β”€ admin.whitelist.controller.js # Whitelist + access request mgmt β”‚ β”‚ β”œβ”€β”€ auth.controller.js # signup restriction; requestAccessUser -β”‚ β”‚ β”œβ”€β”€ categoryUpvote.controller.js # NEW: list ranked categories + toggle upvote +β”‚ β”‚ β”œβ”€β”€ categoryUpvote.controller.js # list ranked categories + toggle upvote +β”‚ β”‚ β”œβ”€β”€ rtq.controller.js # Submit question + RAG evaluation + QP/Upvote loops + multi-moderator +β”‚ β”‚ β”œβ”€β”€ faq.controller.js # FAQ CRUD + Qdrant vectors + review/trending toggles +β”‚ β”‚ β”œβ”€β”€ rag.controller.js # Evaluate + Qdrant-based vector rebuild controllers β”‚ β”‚ └── ... (existing) β”‚ β”œβ”€β”€ middleware/ β”‚ β”‚ β”œβ”€β”€ auth.middleware.js # JWT with role+qp in payload @@ -59,178 +69,194 @@ FAQ-main/ β”‚ β”œβ”€β”€ models/ β”‚ β”‚ β”œβ”€β”€ User.model.js # role: student|moderator|senior|admin β”‚ β”‚ β”œβ”€β”€ RoleRequest.model.js # Blocked-user re-access requests -β”‚ β”‚ β”œβ”€β”€ EmailWhitelist.model.js # NEW: admin-controlled signup email list -β”‚ β”‚ β”œβ”€β”€ AccessRequest.model.js # NEW: non-whitelisted signup requests -β”‚ β”‚ └── CategoryUpvote.model.js # NEW: category upvotes + upvotedBy tracking +β”‚ β”‚ β”œβ”€β”€ EmailWhitelist.model.js # admin-controlled signup email list +β”‚ β”‚ β”œβ”€β”€ AccessRequest.model.js # non-whitelisted signup requests +β”‚ β”‚ β”œβ”€β”€ CategoryUpvote.model.js # category upvotes + upvotedBy tracking +β”‚ β”‚ β”œβ”€β”€ FAQ.model.js # markedForReview and isTrending flags +β”‚ β”‚ └── Answer.model.js # approvals/rejections Arrays + markedForReview flag β”‚ β”œβ”€β”€ routes/ β”‚ β”‚ β”œβ”€β”€ admin.routes.js # Admin-only routes (whitelist + access requests) β”‚ β”‚ β”œβ”€β”€ auth.routes.js # /request-access endpoint -β”‚ β”‚ β”œβ”€β”€ categoryUpvote.routes.js # NEW: /api/faq/categories/ranked, /upvote/:name -β”‚ β”‚ └── vector.routes.js # NEW: /api/vector/health, /api/vector/rebuild +β”‚ β”‚ β”œβ”€β”€ faq.routes.js # /review-faq and /toggle-trending endpoints +β”‚ β”‚ β”œβ”€β”€ categoryUpvote.routes.js # /api/faq/categories/ranked, /upvote/:name +β”‚ β”‚ └── rag.routes.js # RAG endpoints: evaluate-question + rebuild-vectors β”‚ └── services/ β”‚ β”œβ”€β”€ auth.service.js # signup checks whitelist; JWT: {id, role, qp} -β”‚ β”œβ”€β”€ qp.service.js -β”‚ β”œβ”€β”€ vector/ # NEW: Qdrant vector services +β”‚ β”œβ”€β”€ qp.service.js # awardQP / deductQP service methods +β”‚ β”œβ”€β”€ autoupvote.service.js # consolidated atomic FAQ & RTQ auto-upvote +β”‚ β”œβ”€β”€ vector/ # Qdrant vector services β”‚ β”‚ β”œβ”€β”€ collection.service.js # Auto-create collections (HNSW, cosine, 384-dim) -β”‚ β”‚ β”œβ”€β”€ embedding.service.js # TF-IDF n-gram embedder +β”‚ β”‚ β”œβ”€β”€ embedding.service.js # Sentence Transformer embeddings +β”‚ β”‚ β”œβ”€β”€ transformer.service.js # Local @xenova/transformers (all-MiniLM-L6-v2) + LRU Cache β”‚ β”‚ β”œβ”€β”€ faq.vector.service.js # FAQ vector CRUD in Qdrant -β”‚ β”‚ β”œβ”€β”€ rtq.vector.service.js # RTQ vector CRUD in Qdrant -β”‚ β”‚ └── similarity.service.js -β”‚ └── sync/ # NEW: MongoDB ↔ Qdrant sync +β”‚ β”‚ └── rtq.vector.service.js # RTQ vector CRUD in Qdrant +β”‚ └── sync/ # MongoDB ↔ Qdrant sync β”‚ β”œβ”€β”€ sync.events.js # Event emitter β”‚ β”œβ”€β”€ faq.sync.service.js # FAQ sync + rollback β”‚ β”œβ”€β”€ rtq.sync.service.js # RTQ sync + rollback -β”‚ └── sync.repair.service.js # Missing/stray vector detection + reindex +β”‚ └── sync.repair.service.js # Missing/stray vector detection + rebuild vectors β”œβ”€β”€ rag-engine/ -β”‚ β”œβ”€β”€ embedding/embedder.js # Corpus-aware TF-IDF n-gram embedder (384-dim) -β”‚ └── vectorDB/ # Legacy in-memory vector DB (superseded by Qdrant) +β”‚ └── decision-engine/ +β”‚ └── decision.tree.js # RAG Duplicate Detection Engine using Qdrant ANN search └── shared/constants.js # FAQ_CATEGORIES, QP_RULES, ROLES, etc. ``` --- -## βœ… Fixes Applied (19 issues) + New Features - -### πŸ”΄ Critical (original 19) - -| # | File | Fix | -|---|------|-----| -| 1 | `rtq.controller.js` | RAG evaluated before RTQ is created; rejected questions no longer persist | -| 2 | `rtq.routes.js` | All static paths placed before `/:id` | -| 3 | `rtq.controller.js` | `convertToFAQ` populates `answers.userId`; sorts by upvotes in JS | -| 4 | `qp.service.js` | `QPTransaction.amount` positive; `type` encodes direction | -| 5 | `ProfilePage.jsx` | `tx.reason` (was `tx.description`) | -| 6 | `NotificationsPage.jsx` | `notif.qpImpact` (was `notif.qpChange`) | -| 7 | `user.service.js` | Duplication noted | -| 8 | `App.jsx` | No double-routing; role-based dashboard | -| 9 | `TrackQuestionPage.jsx` | Status dropdown matches enum: `unresolved / partial / resolved` | -| 10 | `user.controller.js` | Removed duplicate `getLeaderboard` | -| 11 | `embedder.js` etc. | Corpus-aware IDF via `rebuildVocabulary` | -| 12 | `rtq.controller.js` (`markAccepted`) | No duplicate QP for questioner | -| 13 | `qp.service.js` | `checkAutoPromotion` called inside `awardQP` | -| 14 | `QPContext.jsx` | `useEffect` fetches QP on mount | -| 15 | `AnswerCard.jsx` | Upvote uses `user._id` from `useAuth()` | -| 16 | `Nav.jsx` (new), `App.jsx` | Persistent nav bar on all authenticated pages | -| 17 | `App.jsx` | `/dashboard` β†’ `SeniorDashboard` for senior/admin | -| 18 | `RaiseQuestionPage.jsx` | Dead `FAQ_MATCH` status removed | -| 19 | Routes audit | `PATCH /api/admin/assign-role` is the correct live endpoint | - -### 🟒 Qdrant Cloud Integration (NEW β€” v2) - -| # | File | Purpose | -|---|------|---------| -| Q1 | `config/qdrant.js` | Singleton Qdrant client, retry logic, connection validation | -| Q2 | `services/vector/collection.service.js` | Auto-create faq_collection + rtq_collection (HNSW, cosine, 384-dim, payload indexes) | -| Q3 | `services/vector/embedding.service.js` | Text preprocessing + `generateEmbedding()` using rag-engine embedder | -| Q4 | `services/vector/similarity.service.js` | Cosine/dot/euclidean similarity | -| Q5 | `services/vector/faq.vector.service.js` | insert/search/update/delete FAQ vectors | -| Q6 | `services/vector/rtq.vector.service.js` | insert/search/update/delete RTQ vectors | -| Q7 | `services/sync/sync.events.js` | Event emitter for sync operations | -| Q8 | `services/sync/faq.sync.service.js` | FAQ MongoDB↔Qdrant sync with rollback | -| Q9 | `services/sync/rtq.sync.service.js` | RTQ MongoDB↔Qdrant sync with rollback | -| Q10 | `services/sync/sync.repair.service.js` | Missing/stray vector detection + full reindex | -| Q11 | `routes/vector.routes.js` | `GET /api/vector/health`, `POST /api/vector/rebuild` | -| Q12 | `server.js` | Startup: validate Qdrant β†’ initialize collections | -| Q13 | `app.js` | Mounted `/api/vector` routes | -| Q14 | `rtq.controller.js` | Wired syncRTQInsert on accept, syncRTQDelete on remove, syncRTQDelete+syncFAQInsert on convert | -| Q15 | `faq.controller.js` | Wired syncFAQInsert on create, syncFAQUpdate on update, syncFAQDelete on delete | -| Q16 | `services/vector/*.js` | Format MongoDB 24-character ObjectId to standard 36-character UUID for Qdrant point IDs to resolve Bad Request error | - -### 🟑 Admin Role System (NEW β€” v2) - -| # | File | Purpose | -|---|------|---------| -| A1 | `models/RoleRequest.model.js` | Blocked-user re-access requests (pre-existing) | -| A2 | `auth.service.js` | JWT now includes `{id, role, qp}` β€” role enforced via middleware | -| A3 | `auth.controller.js` | `requestReAccessUser` for blocked users | -| A4 | `server.js` | `INITIAL_ADMIN_EMAIL` env var β€” auto-promotes user to admin on startup | -| A5 | `admin.controller.js` | Full CRUD: getUsers, addUser, updateUser, deleteUser, assignRole, blockUser, unblockUser, reactivateUser | -| A6 | `admin.routes.js` | All routes require `authorizeRoles('admin')`; `/assign-role` back-compat route | - -### πŸ”΅ Email Whitelist System (NEW β€” v3) - -| # | File | Purpose | -|---|------|---------| -| W1 | `models/EmailWhitelist.model.js` | Stores admin-approved signup emails | -| W2 | `models/AccessRequest.model.js` | Stores pending signup requests from non-whitelisted users | -| W3 | `auth.service.js` (`signupUser`) | Checks whitelist before creating user; returns `{restricted: true}` if not in list | -| W4 | `auth.service.js` (`requestAccess`) | Creates AccessRequest for non-whitelisted users | -| W5 | `controllers/admin.whitelist.controller.js` | `getWhitelist`, `addToWhitelist`, `removeFromWhitelist`, `getAccessRequests`, `approveAccessRequest`, `rejectAccessRequest` | -| W6 | `admin.routes.js` | `GET/POST/DELETE /admin/whitelist`, `GET/POST /admin/access-requests/:id/approve|reject` | -| W7 | `pages/SignupPage.jsx` | Shows "Access Restricted" β†’ "Request Approval" button β†’ "Request Submitted" on non-whitelisted signup | -| W8 | `pages/UserListPage.jsx` | Admin tabs: **Users** + **Email Whitelist** + **Access Requests** | - -### 🟠 FAQ Category Upvoting (NEW β€” v4) - -| # | File | Purpose | -|---|------|---------| -| C1 | `models/CategoryUpvote.model.js` | Stores `categoryName` (unique), `upvotes`, `upvotedBy` (ObjectId[]), `lastActivity` | -| C2 | `controllers/categoryUpvote.controller.js` | `listCategoriesWithUpvotes` (merge FAQ_CATEGORIES + DB data, sort by popularity), `upvoteCategory` (toggle on/off, duplicate prevention) | -| C3 | `routes/categoryUpvote.routes.js` | `GET /api/faq/categories/ranked`, `POST /api/faq/categories/upvote/:categoryName` | -| C4 | `app.js` | Mounted `/api/faq/categories` **before** `/api/faq` to avoid `:id` param collision | -| C5 | `services/faq.service.js` (client) | Added `listCategoriesRanked()`, `upvoteCategory(name)` | -| C6 | `pages/FAQPage.jsx` | Fetches ranked categories on mount; sorts category groups by upvotes desc; inline upvote button per category header; optimistic UI with rollback | - -**Design decisions:** -- Dedicated `CategoryUpvote` collection β€” zero changes to existing `FAQ.model.js` schema -- Toggle pattern (upvote/un-upvote) matches existing `upvoteFAQ` behavior -- Category ranking affects only UI display order β€” no impact on RAG, QP, or vector sync +## 🧠 Core Systems & Implementations + +### 1. Semantic Embedding Layer (Sentence Transformers) +* **Model:** `@xenova/transformers` (local WebAssembly/ONNX execution of `all-MiniLM-L6-v2` generating 384-dimensional dense vectors). +* **Caching:** Built-in **LRU Embedding Cache** (maximum 500 entries) in [transformer.service.js](file:///d:/FAQs/FAQ/server/src/services/vector/transformer.service.js) to avoid re-generating embeddings for duplicate texts during the same server session. +* **Warmup:** Loaded and warmed up dynamically on server startup in [server.js](file:///d:/FAQs/FAQ/server/src/server.js) so the first query is lightning-fast (<10ms). +* **rebuild-vectors:** Updated the rebuild pipeline to automatically pull all entries from MongoDB, compute Sentence Transformer vectors, and re-index the Qdrant database. Ran successfully for **68/68 FAQs** and **1/1 RTQ**. + +### 2. Semantic Duplicate Detection (RAG Decision Tree) +The decision tree inside [decision.tree.js](file:///d:/FAQs/FAQ/rag-engine/decision-engine/decision.tree.js) performs semantic search against the active collections in Qdrant Cloud. +* **F1 (FAQ > 80%):** REJECT, deduct 5 QP, auto-upvote matching FAQ. +* **F2+R1 (FAQ 50-80%, RTQ > 60%):** REJECT, deduct 5 QP, auto-upvote matching FAQ. +* **F2+R2 (FAQ 50-80%, RTQ 20-60%):** REJECT, no penalty. +* **F2+R3 (FAQ 50-80%, RTQ ≀ 20%):** ACCEPT β†’ route to RTQ. +* **F3+R1 (FAQ ≀ 50%, RTQ > 60%):** REJECT, no penalty, auto-upvote matching RTQ. +* **F3+R2/R3 (FAQ ≀ 50%, RTQ ≀ 60%):** ACCEPT β†’ route to RTQ. + +### 3. Consolidated Auto-Upvote Engine +Implemented a new service [autoupvote.service.js](file:///d:/FAQs/FAQ/server/src/services/autoupvote.service.js) to handle duplicate-prevention auto-upvotes: +* **Atomic Operations:** Uses atomic `$inc` and `$addToSet` to prevent a user from upvoting the same question twice. +* **QP Integration:** Automatically fetches the original author's ID and awards the `QUESTION_UPVOTE_BONUS` (+5 QP) while sending a notification to the author. +* Fully integrated with the decision tree outcomes in [rtq.controller.js](file:///d:/FAQs/FAQ/server/src/controllers/rtq.controller.js). + +### 4. Premium Branding & Font Stack (PippaQ) +* **Fonts:** Added preconnect tags and loaded **Playfair Display** (elegant serif brand accent) and **Outfit** (sleek sans-serif body/headers) from Google Fonts in [client/index.html](file:///d:/FAQs/FAQ/client/index.html). +* **Visuals:** Updated the navbar, login page, and signup pages to feature the elegant, high-contrast **PippaQ** brand name with customized letter tracking. + +### 5. Role-Based UI Constraints & Dashboard UX Refinements +* **Ask a Question button:** Restructured [RTQPage.jsx](file:///d:/FAQs/FAQ/client/src/pages/RTQPage.jsx) so the `+ Ask a Question` button is hidden for `'admin'` and `'senior'` roles, remaining visible only for `'student'` and `'moderator'` users. +* **Dashboard Layouts:** Removed the redundant "Notifications" quick link cards from both [StudentDashboard.jsx](file:///d:/FAQs/FAQ/client/src/pages/StudentDashboard.jsx) and [SeniorDashboard.jsx](file:///d:/FAQs/FAQ/client/src/pages/SeniorDashboard.jsx) (as a dedicated bell indicator exists in the header). +* **Grid Balancing:** Balanced the dashboards' remaining quick link cards (5 on Student, 3 on Senior) to fill the grid rows perfectly without gaps and updated corresponding skeleton loading layout animations. + +### 6. Standardized FAQ/RTQ Categories & Migration Utility +* **Standardized Categories:** Locked the master list of categories to 10 standardized, clean, non-index-prefixed values across the entire platform in both client utils and shared constants. +* **Migration Utility:** Created a dedicated database migration tool [migrate-categories.js](file:///d:/FAQs/FAQ/scripts/migrate-categories.js) that cleans up numeric prefixes, maps older categories to correct equivalents, and seeds the `CategoryUpvote` database collection for these 10 clean values. +* **FAQPage Filter Fix:** Fixed "All Categories" in [FAQPage.jsx](file:///d:/FAQs/FAQ/client/src/pages/FAQPage.jsx) showing only General. +* **Category Upvote Normalization & Dash Cleaning:** Added frontend normalization in `loadFAQs` that strips numeric prefixes (e.g. `"9. Rosetta β€” your internship journal"` β†’ `"Rosetta - your internship journal"`) from `grouped` keys on API response and converts Unicode em-dashes (`β€”` or `–`) into standard regular hyphens (`-`) with standardized spacing. +* **Retractable/Toggleable Upvotes:** Fixed a UI constraint in [UpvoteButton.jsx](file:///d:/FAQs/FAQ/client/src/components/UpvoteButton.jsx) that was disabling the button when `hasUpvoted` was true. Toggling off upvotes is now fully enabled, allowing users to retract their upvote by clicking the active button again. +* **Flexible Backend Category Matcher:** Modified the category filters in [faq.controller.js](file:///d:/FAQs/FAQ/server/src/controllers/faq.controller.js) and [rtq.controller.js](file:///d:/FAQs/FAQ/server/src/controllers/rtq.controller.js) to query categories using a robust regex pattern. +* **Default Category & Item Sorting:** Updated `filteredCategories` on the FAQPage to sort rendered categories according to their upvote counts (`sortedCategoryNames`) by default, placing the category with the most upvotes at the top. Also added a `sortItems` utility that sorts the FAQs inside each category on the frontend according to the active sort filter selection ('Most Upvoted', 'Newest First', 'Oldest First') to guarantee perfect sorting alignment under all conditions. + +### 7. Question Status Marking System +* **Models:** Updated `RTQ.model.js` and `Question.model.js` status enums to `['unresolved', 'partially_resolved', 'resolved']`, with default status as `'unresolved'`. +* **API Endpoints:** Added route `PATCH /rtq/status/:questionId` mapping to the secure `updateRTQStatus` controller (accessible only by the question owner). +* **Auto-Update Hooks:** Integrated a senior answering auto-resolve hook inside `addAnswer` so that when a Senior submits an answer to an RTQ, the question status automatically changes to `'resolved'`. +* **Frontend Division of Concerns:** + - **Public RTQ Listing & Details Pages**: Display status as premium read-only badges (`Unresolved`, `Partially Resolved`, `Resolved`) using custom visual highlights (Red, Amber, Green). + - **Track Questions Page**: Renders interactive status dropdown selectors for the owner's RTQ submissions, allowing manual resolution tracking directly from their tracking dashboard. + +### 8. Whitelist Request Access System +* **Context & Rules:** Users attempting signup with emails not in the admin whitelist are restricted (`403 Forbidden`). +* **Request Access Flow:** + - Signup page displays a distinct **"Access Restricted"** message and a **"Request Approval"** button. + - Submitting a request creates an `AccessRequest` document in the database (`pending` status). + - Admin view (`UserListPage.jsx` > `Access Requests` tab) can **Approve** (adds email to whitelist, creates active `'student'` user) or **Reject** (marks request as rejected). +* **Fix & Alignment:** Aligned [SignupPage.jsx](file:///d:/FAQs/FAQ/client/src/pages/SignupPage.jsx) error handlers to resolve a mapping mismatch with the custom Axios response interceptor (`api.js`), which rejects promises directly with the payload data object, restoring full visibility of the "Request Approval" flow. + +### 9. Dashboard Role Status Visibility +* **Dashboard Header Badges:** Integrated a beautiful, styled, and role-based **Role Status Badge** inside the header of both `StudentDashboard.jsx` and `SeniorDashboard.jsx`. +* **Dynamic Role Representation:** Displays high-contrast HSL badges corresponding to the user's role: + - `'student'`: Slate badge (`Student`) + - `'moderator'`: Purple badge (`Moderator`) + - `'senior'`: Blue badge (`Senior`) + - `'admin'`: Red badge (`Admin` / `Admin Dashboard` header) +* **Compatibility:** Completely non-disruptive, preserves all existing stats, rank queries, and grid balancing actions. + +### 10. Multi-Moderator RTQ Moderation & QP Economy +* **Multi-Moderator Answer Approvals**: + - Allows multiple moderator approvals per answer (capped at max 2 approvals per question per moderator to prevent collusion). + - Approvals reward the answerer `+5 QP` and the moderator `+3 QP`. +* **Answer Rejections**: + - Allows moderator rejections. Rejections penalize the answerer `-3 QP` and reward the moderator `+3 QP`. +* **Question Acceptance**: + - Accepting an RTQ question changes status to `'resolved'`, sets `isAccepted = true`, and rewards `+5 QP` to the questioner and `+3 QP` to the moderator. +* **Question Rejection (Multi-Moderator)**: + - First moderator rejection sets status to `'rejected'` and rewards `+3 QP` to the moderator. + - A second moderator rejection by a different moderator triggers permanent deletion of the question from MongoDB and Qdrant, deducts `5 QP` from the questioner, and rewards `+3 QP` to the second moderator. +* **Decision Transitions & QP Rollbacks**: + - Implemented automatic, idempotent decision rollback and QP adjustment logic when a moderator changes their decision (e.g. from Accept to Reject, or Approve to Reject): + - Retracts the previous mark and calculates the precise QP points to deduct or refund from both the user and the moderator before applying the new decision. +* **Sleek Icon-Only Frontend Controls**: + - Refactored `RTQPage.jsx` and `RTQDetailPage.jsx` selection controls and action panels to use sleek Lucide icons instead of text: + - Selection/cancel actions are represented by a clean settings gear (`Settings`) icon. + - Action buttons render as compact, modern check (`Check`), close (`X`), and flag (`Flag`) icons with supportive tooltips. +* **Universal Badge Visibility**: + - Status marks (`βœ“ Moderator Accepted`, `βœ— Moderator Rejected`, `⚠️ Marked for Review`, `βœ“ Moderator Approved`, `βœ— Moderator Rejected`) are rendered universally, making them visible to all users (including students) across the RTQ list, RTQ detail, track questions, and working history views. +* **FAQ Page Moderator Actions (Settings Dropdown Menu)**: + - Refactored `FAQPage.jsx` to render a small Lucide `Settings` gear icon button for moderator actions on any FAQ card, hidden completely from student users. + - Clicking this gear opens a premium popover dropdown menu containing `Flag for Review` (if not reviewed yet), `Set on Trending` / `Remove Trending` (which toggles trending status via `PATCH /faq/toggle-trending/:id`), and senior's `Edit FAQ` and `Delete FAQ` actions. + +### 11. Controlled Senior "Add to FAQ" Workflow & Bidirectional Traceability +* **Bidirectional Mapping**: + - Added reference field `faqId` on the `RTQ` model and reference field `rtqId` on the `FAQ` model, guaranteeing 100% bi-directional traceability between original resolved questions and approved FAQ entries. +* **Smart Frontend Auto-Selection**: + - Expanding an RTQ card executes a local 4-tier selection priority scheme to pre-select the best answer: + 1. Senior's own answer (highest priority). + 2. Senior-approved answer. + 3. Moderator-approved answer. + 4. Fallback to the highest upvoted answer. +* **Review Edit Modal Panel**: + - Replaced immediate RTQ β†’ FAQ conversion with a multi-step popup modal. When a Senior clicks `"Add to FAQ (Initiate)"`, it opens the panel pre-filled with the auto-selected answer, category, and tags. + - Seniors have full editorial control to modify the answer, choose a standardized category from a dropdown, and customize comma-separated tags before confirming. +* **Traceable Creation Endpoint**: + - `convertToFAQ` controller parses custom body payloads (`answerId`, `answer`, `category`, `tags`), links documents, resolves original RTQ status to accepted, awards `+10 QP` to the Senior, awards `+10 QP` to the student answerer, and prevents duplicate conversions via `rtq.faqId` checks. + +### 12. Senior Personal Working History +* **Personal Audit Trail**: + - Upgraded the working history backend `listRTQs` to support `filter === 'history'`. + - Queries `FAQ` entries created by the currently authenticated Senior (`req.user._id`) that originated from an RTQ (`rtqId` present) and lists only those original RTQs. + - The `WorkingHistoryPage` now acts as a dedicated personal work history listing for the active Senior. + +### 13. Git Merge Resolution & Alignment +* **Unified Moderation Gear Panel**: + - Integrated features from `origin/main` and unified the settings gear moderation actions in [RTQPage.jsx](file:///d:/FAQs/FAQ/client/src/pages/RTQPage.jsx) and [RTQDetailPage.jsx](file:///d:/FAQs/FAQ/client/src/pages/RTQDetailPage.jsx). + - Standard moderators see the "Request FAQ Conversion" button (`FileText` icon) under the gear. + - Seniors & Admins see ONLY the permanent remove (`Trash2` icon) button under the gear, keeping the controlled `Add to FAQ (Initiate)` review modal workflow triggered only from the card bottom/expanded views. + - Owners see the `Mark as Resolved` (`Check` icon) button. +* **Vite CSS Import Warning Cleaned**: + - Reordered the font `@import` declaration in [index.css](file:///d:/FAQs/FAQ/client/src/index.css) to precede all `@tailwind` statements, ensuring a completely clean product build output with zero warnings or errors. + +### 14. Leaderboard Segmented Toggle & Tier Renames +* **Peers & Seniors Separation:** Partitioned the user lists on the Leaderboard page (`UserListPage.jsx`) into **Peers** (Student/Moderator) and **Seniors** (Senior/Admin). +* **Segmented Toggle Group:** Added a modern tab-like button switcher group at the top of the user list. Privilege-holders (Seniors/Admins) can toggle between the "Peers" list and "Seniors" list. Non-privileged users (Students/Moderators) only see the "Peers" list. +* **Independent Ranking Tracks:** Refactored rankings so each tier runs its own leaderboard starting at rank #1, awarding Crown and Trophy badges to top performers in both groups. + +### 15. FAQ Conversion Requests Fixes & FAQ Page Relocation +* **Client Service Alignment:** Fixed TypeError crashes in `RTQPage.jsx` and `RTQDetailPage.jsx` by updating legacy `rtqService.requestConversion` calls to `faqService.requestConversion`. +* **Section Relocation:** Shifted the FAQ conversion request review list from the Users page to a collapsible dashboard at the top of the main `FAQPage.jsx` view. +* **Role Expansion:** Updated backend routing (`faq.routes.js`) to grant permissions to the `'senior'` role for listing, approving, and rejecting FAQ requests, in addition to `'admin'`. +* **Database Schema Field:** Added the missing `requestedAt` field to `FAQConversionRequest.model.js` to enable creation timestamping and sorting. +* **Qdrant Vector indexing:** Linked Qdrant synchronization into `approveConversionRequest` so newly approved conversion requests are automatically indexed in Qdrant (with rollback safety). Also populated `requestedBy` to dynamically resolve user roles for notifications. + +### 16. Role-Based Highlight Badges and Status Tags +* **Custom Dynamic Highlighting**: + - Status badges for approved/accepted actions dynamically change styling based on the user's role: + - **Moderator actions** (Accepted/Approved by a moderator) display a **blue status tag** (`badge-info`) and a **blue "moderator" badge**. + - **Admin/Senior actions** (Accepted/Approved by an Admin/Senior) display a **purple status tag** (`badge-purple`) and a **purple "senior"/"admin" badge**. + - **Negative actions (Rejected)** are universally styled in **red (`badge-danger`)**. + - Role labels rendered next to answer authors have been updated inline to match the blue (moderator) and purple (senior/admin) highlighted themes. +* **Backend Role Exposing**: + - Modified the backend RTQ controllers to populate the `acceptedBy` field for questions and the `approvedBy` field for answers, exposing roles to the client. +* **Track Question Status Select Dropdown**: + - Adjusted the status select dropdown styling in `TrackQuestionPage.jsx` so that the options and container are color-coded (Resolved as green, Partially Resolved as lite blue, and Unresolved as red). + +### 17. About Page Interactive Logo Symbolism & Styling Refinements +* **Refined Symbol Icons**: Replaced the diamond symbol for VαΉ›tta with a circle symbol (SVG + label), and the leaf symbol for Jaṭā with a custom waving SVG representing the matted hair of a sage. +* **Directed Release Animations**: Added CSS keyframe animations releasing streams of colored circles (blue, green, orange) from the center logo that travel directly toward their respective pointed cards on hover. +* **Hotspot Symmetrical Pulsing**: Fixed standard ping animations on hotspots by using inline `transformOrigin` styles to make them pulse symmetrically in-place. +* **Cropping & Layout Resolution**: Expanded all SVG `` container dimensions (e.g., 310x180, 240x240, 500x130) and centered cards inside using custom margins to prevent box-shadow and hover transformations from clipping. +* **Text Overflow Resolution**: Increased the Q Symbol highlight statement card to `height: 92px` (and its `` to `height: 135px`) to fully fit the paragraph text without letting the last word overflow the bottom border. --- -## 🧠 Architecture Notes - -### Qdrant vs MongoDB Responsibilities -- **MongoDB Atlas**: Application data (users, questions, answers, QP transactions, notifications) -- **Qdrant Cloud**: Vector embeddings only (384-dim TF-IDF/BPE vectors for FAQ + RTQ semantic search) -- Separation enables O(log n) ANN similarity search instead of O(n) brute-force - -### MongoDB ↔ Qdrant Sync Model -- MongoDB is source of truth -- Qdrant synced on: RTQ create/delete/status-change, FAQ create/update/delete, RTQβ†’FAQ conversion -- If Qdrant fails: MongoDB operation is rolled back (insert/delete) or logged (update/delete) -- `sync.repair.service.js` can detect missing/stray vectors and full-reindex - -### Embedding Model Improvements (Chunks 1–7) - -All 7 chunks have been implemented in `rag-engine/embedding/embedder.js`: - -| Chunk | Feature | Impact | -|-------|---------|--------| -| 1 | Hash-based stable indexing + importance truncation | Deterministic vectors; top 384 n-grams by weight | -| 2 | Separate IDF vocabularies per corpus (FAQ / RTQ) | No cross-contamination between corpra | -| 3 | Persist IDF vocabulary to disk (`vocab-faq.json`, `vocab-rtq.json`) | Cold start recovery β€” no IDF degradation | -| 4 | Stop word filtering + Porter stemming | Cleaner token streams; unified morphology | -| 5 | Word-level n-grams (1-2) alongside char n-grams | Phrase-level matching ("reset password" as unit) | -| 6 | Pure-JS BPE tokenizer (opt-in) | Better subword capture; enable with `{ useBPE: true }` | -| 7 | Transformer service stub (`transformer.service.js`) | Ready for `@xenova/transformers`; falls back to TF-IDF | - -**To enable BPE mode:** -```js -const embedder = new Embedder({ useBPE: true, bpeVocabSize: 4000 }); -embedder.trainBPE(corpusTexts, 4000); // call once at startup -``` - -**To enable transformer embeddings:** -```bash -npm install @xenova/transformers -``` -Then call `getTransformerEmbedder()` instead of the TF-IDF embedder. Model: `Xenova/all-MiniLM-L6-v2` (384-dim). - -### Email Whitelist Gate -- ALL signups require email in `EmailWhitelist` collection -- Non-whitelisted users see "Access Restricted" β†’ submit `AccessRequest` -- Admin approves β†’ email added to whitelist + user account auto-created -- `INITIAL_ADMIN_EMAIL` ensures first admin can always be bootstrapped - ---- - -## πŸ”§ Next Steps - -1. ~~Wire email sending (OTP console-logged in dev)~~ β€” still dev-only -2. Add `docs/` content -3. ~~Qdrant Cloud integrated~~ β€” add credentials to `.env` when ready -4. Add rate limiting to `/api/auth/signup` and `/api/rag/evaluate-question` -5. Run `POST /api/vector/rebuild` with `{collection: 'faq'}` or `'rtq'` to reindex after bulk import -6. Add email sending (SendGrid/Resend) for production OTP delivery -7. ~~FAQ category upvoting~~ β€” implemented (v4) +## πŸ› οΈ Verification & Development Notes +* **Dev Server Command:** `npm run dev` starts the frontend on port 3000 and the backend on port 5000. +* **Validation:** All updated javascript files have passed linter and syntax checks (`node --check`). +* **Git Status:** Clean and pushing successfully to remote repositories. diff --git a/README.md b/README.md index 8788f0e..84fd578 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,18 @@ -<<<<<<< HEAD -# Q&A Platform +# PippaQ β€” Premium Q&A & FAQ Platform -A semantic query-resolution and FAQ generation platform with a **QP (Quality Point) reputation economy** and **role-based access control**. Built with React, Express, MongoDB, and a custom RAG engine. +A high-performance, semantic query-resolution and FAQ generation platform with a **QP (Quality Point) reputation economy**, **role-based access control**, and a production-ready **RAG (Retrieval-Augmented Generation) duplicate detection engine**. + +Built with React, Express, MongoDB, Qdrant Cloud, and local Sentence Transformers. --- ## πŸ—οΈ Architecture -``` +```text D:\faq\ -β”œβ”€β”€ client/ React + Vite frontend (Tailwind CSS) -β”œβ”€β”€ server/ Express.js backend (MongoDB/Mongoose) -β”œβ”€β”€ rag-engine/ Custom TF-IDF RAG pipeline (no external AI API) +β”œβ”€β”€ client/ React + Vite frontend (Tailwind CSS + Premium Typography) +β”œβ”€β”€ server/ Express.js backend (MongoDB/Mongoose + Qdrant Integration) +β”œβ”€β”€ rag-engine/ Semantic Decision Engine (Sentence Transformer + Qdrant ANN) β”œβ”€β”€ shared/ Shared constants (QP rules, RAG thresholds, roles) └── docs/ Project documentation ``` @@ -30,12 +31,12 @@ npm run install:all ```bash cp server/.env.example server/.env -# Edit server/.env β€” set your MongoDB URI and JWT secret +# Edit server/.env β€” set your MongoDB URI, JWT secret, and QDRANT_URL ``` -### 3. Start MongoDB +### 3. Start MongoDB & Qdrant -Make sure MongoDB is running locally or update `MONGO_URI` in `server/.env`. +Ensure your local or cloud MongoDB and Qdrant instances are accessible. ### 4. Run the app @@ -43,8 +44,8 @@ Make sure MongoDB is running locally or update `MONGO_URI` in `server/.env`. npm run dev ``` -- **Client**: http://localhost:3000 -- **Server**: http://localhost:5000 +* **Client**: http://localhost:3000 +* **Server**: http://localhost:5000 --- @@ -80,29 +81,41 @@ npm run dev | Question removed | -5 | ### Thresholds -- **QP < 50** β†’ Cannot raise questions -- **QP β‰₯ 500** β†’ Auto-request Moderator promotion +* **QP < 50** β†’ Cannot raise questions. +* **QP β‰₯ 500** β†’ Auto-request Moderator promotion. --- -## 🧠 RAG Decision Tree - -Questions are evaluated against the FAQ and RTQ vector stores: - -``` -User Question β†’ TF-IDF Embed (384-dim) β†’ Compare with FAQ/RTQ vectors - -F1: FAQ similarity > 80% β†’ REJECT + -5 QP (duplicate) -F2: 50–80% FAQ similarity - + R1: RTQ > 60% β†’ REJECT + -5 QP (similar RTQ exists) - + R2: 20–60% RTQ β†’ REJECT (no penalty) - + R3: RTQ ≀ 20% β†’ ACCEPT β†’ Add to RTQ -F3: FAQ ≀ 50% similarity - + R1: RTQ > 60% β†’ REJECT (no penalty) - + R2/R3: RTQ ≀ 60% β†’ ACCEPT β†’ Add to RTQ +## 🧠 RAG Duplicate Detection Engine + +Questions are evaluated semantically against the FAQ and RTQ collections stored in Qdrant: + +```text +User Question + β”‚ + β–Ό +Generate Embedding ──► Local Sentence Transformer (all-MiniLM-L6-v2, 384-dim) + β”‚ + β–Ό +Qdrant HNSW ANN Search ──► Compare with FAQ/RTQ Collections + β”‚ + β–Ό +Apply Decision Tree Rules: +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ F1: FAQ similarity > 80% β”‚ REJECT, -5 QP, upvote FAQ β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ F2: FAQ similarity 50–80% β”‚ β”‚ +β”‚ β”œβ”€β”€ R1: RTQ > 60% β”‚ REJECT, -5 QP, upvote FAQ β”‚ +β”‚ β”œβ”€β”€ R2: RTQ 20–60% β”‚ REJECT (no penalty) β”‚ +β”‚ └── R3: RTQ ≀ 20% β”‚ ACCEPT β†’ Route to RTQ β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ F3: FAQ similarity ≀ 50% β”‚ β”‚ +β”‚ β”œβ”€β”€ R1: RTQ > 60% β”‚ REJECT (no penalty), upvote RTQβ”‚ +β”‚ └── R2/R3: RTQ ≀ 60% β”‚ ACCEPT β†’ Route to RTQ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` -> Uses TF-IDF character n-grams (no external AI API required). Swap for OpenAI/sentence-transformers in production. +> **Performance Optimization:** Includes an **LRU Embedding Cache** (500 entries) and model warmup on server startup to ensure lightning-fast semantic queries under 10ms. --- @@ -124,23 +137,8 @@ F3: FAQ ≀ 50% similarity ## πŸ› οΈ Tech Stack -- **Frontend**: React 18, Vite, React Router v6, Axios, Tailwind CSS -- **Backend**: Express.js, Mongoose, JWT (bcryptjs for passwords) -- **RAG Engine**: Custom TF-IDF n-gram embedder (384-dim), cosine similarity, in-memory vector DB -- **Database**: MongoDB (vectors stored in documents β€” swap for Qdrant/Pinecone in production) -- **Dev**: `concurrently` to run client + server together - ---- - -## πŸ“ Notes - -- OTP is **console-logged** in development. Wire to an email provider (SendGrid, Resend) for production. -- Vector DB is **in-memory** (vectors stored in MongoDB documents). For production, swap to Qdrant or Pinecone. -- RESTRICTED users (flagged by seniors) are blocked from asking questions and answering. - ---- - -*Maintained with git. Run `git log --oneline` for full history.* -======= -# FAQ ->>>>>>> 655d58505ffd99f5232d6f5c4fb351452148d89f +* **Frontend**: React 18, Vite, React Router v6, Axios, Tailwind CSS + Premium Typography (Playfair Display & Outfit) +* **Backend**: Express.js, Mongoose, JWT (bcryptjs) +* **Vector Store**: Qdrant Cloud (HNSW indexes, cosine distance, metadata payload filters) +* **Embeddings**: Native Node.js `@xenova/transformers` (local WebAssembly ONNX execution of `all-MiniLM-L6-v2`) +* **Dev Tools**: `concurrently` to run client + server together diff --git a/assets/PippaQ1.webp b/assets/PippaQ1.webp new file mode 100644 index 0000000..fd9bbad Binary files /dev/null and b/assets/PippaQ1.webp differ diff --git a/client/index.html b/client/index.html index 3bbd874..f221a1f 100644 --- a/client/index.html +++ b/client/index.html @@ -3,8 +3,13 @@ - Q&A Platform - + + + PippaQ β€” Premium Q&A Platform + + + +
diff --git a/client/public/PippaQ1.webp b/client/public/PippaQ1.webp new file mode 100644 index 0000000..fd9bbad Binary files /dev/null and b/client/public/PippaQ1.webp differ diff --git a/client/src/App.jsx b/client/src/App.jsx index b9f6876..1876a1e 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -1,26 +1,39 @@ import { Routes, Route, Navigate, useLocation } from 'react-router-dom'; +import { useState, useEffect } from 'react'; import { useAuth } from './context/AuthContext'; import Nav from './components/Nav'; +import ErrorBoundary from './components/ErrorBoundary'; +import { ToastProvider } from './components/Toast'; +import GlobalSearch from './components/GlobalSearch'; -// Pages import LoginPage from './pages/LoginPage'; import SignupPage from './pages/SignupPage'; import FAQPage from './pages/FAQPage'; +import FAQEditPage from './pages/FAQEditPage'; import RTQPage from './pages/RTQPage'; +import RTQDetailPage from './pages/RTQDetailPage'; import StudentDashboard from './pages/StudentDashboard'; import SeniorDashboard from './pages/SeniorDashboard'; import AddFAQPage from './pages/AddFAQPage'; import RaiseQuestionPage from './pages/RaiseQuestionPage'; import ProfilePage from './pages/ProfilePage'; import UserListPage from './pages/UserListPage'; +import UserProfilePage from './pages/UserProfilePage'; import TrackQuestionPage from './pages/TrackQuestionPage'; import WorkingHistoryPage from './pages/WorkingHistoryPage'; import NotificationsPage from './pages/NotificationsPage'; +import QPHistoryPage from './pages/QPHistoryPage'; +import AboutPage from './pages/AboutPage'; function LoadingScreen() { return ( -
-
Loading...
+
+
+
+ P +
+
Loading...
+
); } @@ -42,7 +55,6 @@ function PublicOnly({ children }) { return children; } -// Dashboard renders correct page based on role function DashboardRoute() { const { user, loading } = useAuth(); if (loading) return ; @@ -51,41 +63,70 @@ function DashboardRoute() { return ; } -const PUBLIC_PATHS = ['/login', '/signup']; -export default function App() { +// Scroll to top on route change +function ScrollToTop() { + const { pathname } = useLocation(); + useEffect(() => { + window.scrollTo(0, 0); + }, [pathname]); + return null; +} + +function AppLayout() { const location = useLocation(); const { user, refreshUser } = useAuth(); - const isPublic = PUBLIC_PATHS.includes(location.pathname); + const [searchOpen, setSearchOpen] = useState(false); + + useEffect(() => { + const onKey = (e) => { + if (e.key === '/' && !e.ctrlKey && !e.metaKey && document.activeElement.tagName !== 'INPUT' && document.activeElement.tagName !== 'TEXTAREA') { + e.preventDefault(); + setSearchOpen(true); + } + }; + window.addEventListener('keydown', onKey); + return () => window.removeEventListener('keydown', onKey); + }, []); return (
- {user && !isPublic &&
); -} \ No newline at end of file +} + +export default function App() { + return ( + + + + + + ); +} diff --git a/client/src/components/AnswerCard.jsx b/client/src/components/AnswerCard.jsx index c09483d..c65c7e4 100644 --- a/client/src/components/AnswerCard.jsx +++ b/client/src/components/AnswerCard.jsx @@ -31,10 +31,13 @@ export default function AnswerCard({ answer, onUpvote, showModeratorControls = f

Answered by {answer.userId.name} {answer.userId?.role === 'moderator' && ( - Moderator + Moderator )} {answer.userId?.role === 'senior' && ( - Senior + Senior + )} + {answer.userId?.role === 'admin' && ( + Admin )}

)} diff --git a/client/src/components/Avatar.jsx b/client/src/components/Avatar.jsx new file mode 100644 index 0000000..65f8bb7 --- /dev/null +++ b/client/src/components/Avatar.jsx @@ -0,0 +1,43 @@ +import { useMemo } from 'react'; + +const ROLE_COLORS = { + student: 'bg-slate-500', + moderator: 'bg-blue-600', + senior: 'bg-purple-600', + admin: 'bg-violet-600', +}; + +const ROLE_GRADIENTS = { + student: 'from-slate-400 to-slate-600', + moderator: 'from-blue-500 to-blue-700', + senior: 'from-purple-500 to-violet-600', + admin: 'from-violet-500 to-purple-700', +}; + +const SIZE_CLASSES = { + xs: 'avatar-xs', + sm: 'avatar-sm', + md: 'avatar-md', + lg: 'avatar-lg', + xl: 'avatar-xl', +}; + +export default function Avatar({ name, role, size = 'md', className = '', gradient = false }) { + const initial = useMemo(() => { + if (!name) return '?'; + return name.charAt(0).toUpperCase(); + }, [name]); + + const bgClass = gradient + ? `bg-gradient-to-br ${ROLE_GRADIENTS[role] || ROLE_GRADIENTS.student}` + : ROLE_COLORS[role] || ROLE_COLORS.student; + + return ( +
+ {initial} +
+ ); +} diff --git a/client/src/components/BackToTop.jsx b/client/src/components/BackToTop.jsx new file mode 100644 index 0000000..2464fb4 --- /dev/null +++ b/client/src/components/BackToTop.jsx @@ -0,0 +1,31 @@ +import { useState, useEffect } from 'react'; +import { ChevronUp } from 'lucide-react'; + +export default function BackToTop() { + const [show, setShow] = useState(false); + + useEffect(() => { + const handleScroll = () => setShow(window.scrollY > 400); + window.addEventListener('scroll', handleScroll, { passive: true }); + return () => window.removeEventListener('scroll', handleScroll); + }, []); + + if (!show) return null; + + return ( + + ); +} \ No newline at end of file diff --git a/client/src/components/Badge.jsx b/client/src/components/Badge.jsx new file mode 100644 index 0000000..9ede99f --- /dev/null +++ b/client/src/components/Badge.jsx @@ -0,0 +1,59 @@ +import { CheckCircle, XCircle, AlertTriangle, Clock, Circle } from 'lucide-react'; + +export function StatusBadge({ status, role }) { + const map = { + resolved: { label: 'Resolved', icon: CheckCircle, className: 'badge-success' }, + partially_resolved: { label: 'Partially Resolved', icon: Clock, className: 'badge-warning' }, + unresolved: { label: 'Unresolved', icon: Circle, className: 'badge-danger' }, + rejected: { label: 'Rejected', icon: XCircle, className: 'badge-danger' }, + trending: { label: 'Trending', icon: CheckCircle, className: 'badge-info' }, + markedForReview:{ label: 'Flagged', icon: AlertTriangle, className: 'badge-warning' }, + approved: { label: 'Approved', icon: CheckCircle, className: 'badge-success' }, + accepted: { label: 'Accepted', icon: CheckCircle, className: 'badge-info' }, + pending: { label: 'Pending', icon: Clock, className: 'badge-warning' }, + }; + + const config = { ...(map[status] || { label: status, icon: Circle, className: 'badge-neutral' }) }; + if (status === 'accepted' || status === 'approved') { + if (role === 'senior' || role === 'admin') { + config.className = 'badge-purple'; + } else { + config.className = 'badge-info'; + } + } + const Icon = config.icon; + + return ( + + + {config.label} + + ); +} + +export function RoleBadge({ role }) { + const map = { + student: { label: 'Student', className: 'badge-student' }, + moderator: { label: 'Moderator', className: 'badge-moderator' }, + senior: { label: 'Senior', className: 'badge-senior' }, + admin: { label: 'Admin', className: 'badge-admin' }, + }; + + const config = map[role] || { label: role, className: 'badge-neutral' }; + return {config.label}; +} + +export function BoolBadge({ value, trueLabel = 'Yes', falseLabel = 'No' }) { + return value + ? {trueLabel} + : {falseLabel}; +} + +export function CountBadge({ count, label }) { + if (!count && count !== 0) return null; + return ( + + {label ? `${count} ${label}` : count} + + ); +} \ No newline at end of file diff --git a/client/src/components/Breadcrumb.jsx b/client/src/components/Breadcrumb.jsx new file mode 100644 index 0000000..2a6e1cc --- /dev/null +++ b/client/src/components/Breadcrumb.jsx @@ -0,0 +1,24 @@ +import { ChevronRight, Home } from 'lucide-react'; +import { Link } from 'react-router-dom'; + +export default function Breadcrumb({ items = [] }) { + return ( + + ); +} \ No newline at end of file diff --git a/client/src/components/EmptyState.jsx b/client/src/components/EmptyState.jsx new file mode 100644 index 0000000..3540a70 --- /dev/null +++ b/client/src/components/EmptyState.jsx @@ -0,0 +1,25 @@ +import { Link } from 'react-router-dom'; + +export default function EmptyState({ icon: Icon, title, description, actionLabel, actionTo, onAction, className = '' }) { + return ( +
+ {Icon && ( +
+ +
+ )} + {title &&

{title}

} + {description &&

{description}

} + {actionLabel && actionTo && ( + + {actionLabel} + + )} + {actionLabel && onAction && !actionTo && ( + + )} +
+ ); +} diff --git a/client/src/components/ErrorBoundary.jsx b/client/src/components/ErrorBoundary.jsx new file mode 100644 index 0000000..8ef6879 --- /dev/null +++ b/client/src/components/ErrorBoundary.jsx @@ -0,0 +1,40 @@ +import { Component } from 'react'; +import { AlertTriangle } from 'lucide-react'; + +export default class ErrorBoundary extends Component { + constructor(props) { + super(props); + this.state = { hasError: false, error: null }; + } + + static getDerivedStateFromError(error) { + return { hasError: true, error }; + } + + componentDidCatch(error, info) { + console.error('[ErrorBoundary]', error, info); + } + + render() { + if (this.state.hasError) { + return ( +
+
+ +

Something went wrong

+

+ {this.state.error?.message || 'An unexpected error occurred.'} +

+ +
+
+ ); + } + return this.props.children; + } +} \ No newline at end of file diff --git a/client/src/components/GlobalSearch.jsx b/client/src/components/GlobalSearch.jsx new file mode 100644 index 0000000..3b9b9f8 --- /dev/null +++ b/client/src/components/GlobalSearch.jsx @@ -0,0 +1,149 @@ +import { useState, useEffect, useRef, useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; +import faqService from '../services/faq.service'; +import rtqService from '../services/rtq.service'; +import { BookOpen, MessageCircle } from 'lucide-react'; + +export default function GlobalSearch({ onClose }) { + const [query, setQuery] = useState(''); + const [loading, setLoading] = useState(false); + const [faqResults, setFaqResults] = useState([]); + const [rtqResults, setRtqResults] = useState([]); + const inputRef = useRef(null); + const navigate = useNavigate(); + + useEffect(() => { + inputRef.current?.focus(); + }, []); + + useEffect(() => { + const onKey = (e) => { + if (e.key === 'Escape') onClose(); + }; + window.addEventListener('keydown', onKey); + return () => window.removeEventListener('keydown', onKey); + }, [onClose]); + + const search = useCallback(async (q) => { + if (!q.trim()) { + setFaqResults([]); + setRtqResults([]); + return; + } + setLoading(true); + try { + const [faqs, rtqs] = await Promise.all([ + faqService.list({ sort: 'upvotes' }).catch(() => ({ grouped: {} })), + rtqService.list({ sort: 'upvotes' }).catch(() => []), + ]); + + const ql = q.toLowerCase(); + const faqItems = Object.values(faqs.grouped || {}).flat(); + const rtqItems = Array.isArray(rtqs) ? rtqs : (rtqs.data || []); + + setFaqResults( + faqItems + .filter(f => f.question.toLowerCase().includes(ql) || f.answer.toLowerCase().includes(ql)) + .slice(0, 5) + ); + setRtqResults( + rtqItems + .filter(r => r.question.toLowerCase().includes(ql)) + .slice(0, 5) + ); + } catch { + // silent + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + const t = setTimeout(() => search(query), 300); + return () => clearTimeout(t); + }, [query, search]); + + const go = (path) => { + navigate(path); + onClose(); + }; + + return ( +
+
+
+ + + + setQuery(e.target.value)} + placeholder="Search FAQs and RTQs..." + className="flex-1 bg-transparent text-primary placeholder-muted outline-none text-base" + /> + {loading && ( + + + + + )} + Esc +
+ +
+ {!query.trim() && ( +
+ Type to search FAQs and RTQs... +
+ )} + + {query.trim() && !loading && faqResults.length === 0 && rtqResults.length === 0 && ( +
+ No results for "{query}" +
+ )} + + {faqResults.length > 0 && ( +
+
FAQs
+ {faqResults.map(faq => ( + + ))} +
+ )} + + {rtqResults.length > 0 && ( +
+
RTQs
+ {rtqResults.map(rtq => ( + + ))} +
+ )} +
+
+
+ ); +} diff --git a/client/src/components/LoginModal.jsx b/client/src/components/LoginModal.jsx new file mode 100644 index 0000000..78e9bae --- /dev/null +++ b/client/src/components/LoginModal.jsx @@ -0,0 +1,34 @@ +import { Link } from 'react-router-dom'; +import { LogIn } from 'lucide-react'; + +export default function LoginModal({ isOpen, onClose }) { + if (!isOpen) return null; + + return ( +
+
e.stopPropagation()} + > +
+
+ +
+

Sign in required

+

You need to be logged in to perform this action.

+
+
+ + Sign in + + + Create account + + +
+
+
+ ); +} \ No newline at end of file diff --git a/client/src/components/MiniChart.jsx b/client/src/components/MiniChart.jsx new file mode 100644 index 0000000..8766a20 --- /dev/null +++ b/client/src/components/MiniChart.jsx @@ -0,0 +1,38 @@ +export default function MiniChart({ data, color = '#6366f1', height = 40 }) { + if (!data || data.length === 0) { + return
; + } + + const counts = data.map(d => d.count); + const max = Math.max(...counts, 1); + const width = 120; + const barWidth = Math.max(4, Math.floor((width - (data.length - 1) * 2) / data.length)); + const totalWidth = data.length * (barWidth + 2) - 2; + + return ( + + {data.map((d, i) => { + const barHeight = Math.max(2, Math.round((d.count / max) * (height - 4))); + const x = i * (barWidth + 2); + const y = height - barHeight - 2; + return ( + + ); + })} + + ); +} diff --git a/client/src/components/Nav.jsx b/client/src/components/Nav.jsx index 5b79b0d..74fe555 100644 --- a/client/src/components/Nav.jsx +++ b/client/src/components/Nav.jsx @@ -1,12 +1,92 @@ +import { useState, useEffect, useRef } from 'react'; import { Link, useNavigate, useLocation } from 'react-router-dom'; import { useAuth } from '../context/AuthContext'; import { useQP } from '../context/QPContext'; -import { useEffect } from 'react'; +import notificationService from '../services/notification.service'; +import { timeAgo } from '../utils/helpers'; +import { Bell, LogOut, Menu, X, Zap } from 'lucide-react'; +import Avatar from './Avatar'; export default function Nav({ refreshUser }) { const { user, logout } = useAuth(); + const { qp } = useQP(); const navigate = useNavigate(); const location = useLocation(); + const [unreadCount, setUnreadCount] = useState(0); + const [recentNotifs, setRecentNotifs] = useState([]); + const [bellOpen, setBellOpen] = useState(false); + const [mobileOpen, setMobileOpen] = useState(false); + const [qpAnimate, setQpAnimate] = useState(false); + const prevQpRef = useRef(qp); + const bellRef = useRef(null); + + // Close mobile menu on route change + useEffect(() => { setMobileOpen(false); }, [location.pathname]); + + useEffect(() => { + if (prevQpRef.current !== qp && user) { + setQpAnimate(true); + prevQpRef.current = qp; + const t = setTimeout(() => setQpAnimate(false), 800); + return () => clearTimeout(t); + } + }, [qp, user]); + + useEffect(() => { + if (!user) return; + const fetchNotifs = async () => { + try { + const [count, notifs] = await Promise.all([ + notificationService.getUnreadCount(), + notificationService.getNotifications().catch(() => ({ notifications: [] })) + ]); + setUnreadCount(count.count || 0); + const all = notifs.notifications || notifs || []; + setRecentNotifs(all.slice(0, 5)); + } catch { + // silent + } + }; + fetchNotifs(); + const interval = setInterval(fetchNotifs, 30000); + return () => clearInterval(interval); + }, [user]); + + useEffect(() => { + const handleClick = (e) => { + if (bellRef.current && !bellRef.current.contains(e.target)) { + setBellOpen(false); + } + }; + document.addEventListener('mousedown', handleClick); + return () => document.removeEventListener('mousedown', handleClick); + }, []); + + useEffect(() => { + if (bellOpen) { + notificationService.getNotifications() + .then(data => { + const all = data.notifications || data || []; + setRecentNotifs(all.slice(0, 5)); + }) + .catch(() => {}); + } + }, [bellOpen, unreadCount]); + + const handleMarkAllRead = async (e) => { + e.stopPropagation(); + try { + const all = recentNotifs; + await Promise.all( + all.filter(n => !n.read).map(n => notificationService.markAsRead(n._id)) + ); + setUnreadCount(0); + setRecentNotifs(prev => prev.map(n => ({ ...n, read: true }))); + } catch { + // silent + } + setBellOpen(false); + }; const handleLogout = () => { logout(); @@ -20,41 +100,195 @@ export default function Nav({ refreshUser }) { { to: '/faq', label: 'FAQs' }, { to: '/rtq', label: 'RTQ' }, ...(user?.role === 'student' || user?.role === 'moderator' - ? [{ to: '/raise-question', label: 'Ask' }] + ? [{ to: '/track', label: 'Track' }, { to: '/raise-question', label: 'Ask' }] : []), ...(user?.role === 'senior' || user?.role === 'admin' ? [{ to: '/add-faq', label: 'Add FAQ' }, { to: '/history', label: 'History' }] : []), - { to: '/notifications', label: 'Notifications' }, { to: '/users', label: 'Users' }, - { to: '/profile', label: 'Profile' }, ]; return ( -
); } diff --git a/client/src/components/SegmentedControl.jsx b/client/src/components/SegmentedControl.jsx new file mode 100644 index 0000000..9008b84 --- /dev/null +++ b/client/src/components/SegmentedControl.jsx @@ -0,0 +1,24 @@ +export default function SegmentedControl({ options, value, onChange, className = '' }) { + return ( +
+ {options.map(opt => ( + + ))} +
+ ); +} diff --git a/client/src/components/SkeletonLoader.jsx b/client/src/components/SkeletonLoader.jsx new file mode 100644 index 0000000..49d0232 --- /dev/null +++ b/client/src/components/SkeletonLoader.jsx @@ -0,0 +1,56 @@ +export function SkeletonCard({ className = '' }) { + return ( +
+
+
+
+
+
+
+
+
+
+
+
+ ); +} + +export function SkeletonRow({ className = '' }) { + return ( +
+
+
+
+
+
+
+
+ ); +} + +export function SkeletonStat({ className = '' }) { + return ( +
+
+
+
+
+ ); +} + +export function SkeletonAvatar({ size = 'md', className = '' }) { + const sizes = { sm: 'w-8 h-8', md: 'w-10 h-10', lg: 'w-12 h-12', xl: 'w-16 h-16' }; + return
; +} + +export function Spinner({ size = 'md', className = '' }) { + const sizes = { sm: 'w-4 h-4', md: 'w-6 h-6', lg: 'w-8 h-8' }; + return ( +
+ + + + +
+ ); +} \ No newline at end of file diff --git a/client/src/components/Toast.jsx b/client/src/components/Toast.jsx new file mode 100644 index 0000000..1807788 --- /dev/null +++ b/client/src/components/Toast.jsx @@ -0,0 +1,66 @@ +import { useState, useEffect, useCallback, createContext, useContext } from 'react'; +import { CheckCircle, XCircle, AlertTriangle, MessageCircle } from 'lucide-react'; + +const ToastContext = createContext(null); + +let toastId = 0; + +export function ToastProvider({ children }) { + const [toasts, setToasts] = useState([]); + + const addToast = useCallback((message, type = 'info', duration = 4000) => { + const id = ++toastId; + setToasts(prev => [...prev, { id, message, type }]); + if (duration > 0) { + setTimeout(() => { + setToasts(prev => prev.filter(t => t.id !== id)); + }, duration); + } + return id; + }, []); + + const removeToast = useCallback((id) => { + setToasts(prev => prev.filter(t => t.id !== id)); + }, []); + + return ( + + {children} +
+ {toasts.map(toast => ( +
+ {toast.type === 'success' && } + {toast.type === 'error' && } + {toast.type === 'warning' && } + {toast.type === 'info' && } + {toast.message} + +
+ ))} +
+
+ ); +} + +export function useToast() { + const ctx = useContext(ToastContext); + return ctx || { addToast: () => {} }; +} + +export function toast(message, type = 'info') { + const { addToast } = useToast(); + addToast(message, type); +} diff --git a/client/src/components/UpvoteButton.jsx b/client/src/components/UpvoteButton.jsx index 1e65122..a68f84e 100644 --- a/client/src/components/UpvoteButton.jsx +++ b/client/src/components/UpvoteButton.jsx @@ -1,33 +1,39 @@ import { useState } from 'react'; import { ArrowBigUp } from 'lucide-react'; -import { cn } from '../utils/helpers'; -export default function UpvoteButton({ upvotes, onUpvote, hasUpvoted }) { - const [loading, setLoading] = useState(false); +export default function UpvoteButton({ upvotes, onUpvote, hasUpvoted, disabled }) { + const [animating, setAnimating] = useState(false); - const handleClick = async () => { - if (loading || hasUpvoted) return; - setLoading(true); - try { - await onUpvote(); - } finally { - setLoading(false); - } + const handleClick = (e) => { + e.stopPropagation(); + if (disabled) return; + setAnimating(true); + onUpvote?.(); + setTimeout(() => setAnimating(false), 350); }; return ( ); } \ No newline at end of file diff --git a/client/src/context/QPContext.jsx b/client/src/context/QPContext.jsx index 9ac5483..a3a8a46 100644 --- a/client/src/context/QPContext.jsx +++ b/client/src/context/QPContext.jsx @@ -32,8 +32,20 @@ export function QPProvider({ children }) { } }, [user?._id, fetchQP]); - const awardQP = (amount) => setQP(prev => prev + amount); - const deductQP = (amount) => setQP(prev => prev - Math.abs(amount)); + const awardQP = async (amount) => { + setQP(prev => prev + amount); + try { + const score = await qpService.getMyScore(); + setQP(typeof score === 'number' ? score : score?.qp || 0); + } catch { /* silent β€” next refresh will correct */ } + }; + const deductQP = async (amount) => { + setQP(prev => prev - Math.abs(amount)); + try { + const score = await qpService.getMyScore(); + setQP(typeof score === 'number' ? score : score?.qp || 0); + } catch { /* silent β€” next refresh will correct */ } + }; const syncQP = (newQP) => setQP(newQP); return ( diff --git a/client/src/index.css b/client/src/index.css index 7d90d32..ab14211 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -1,53 +1,512 @@ +@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&display=swap'); + @tailwind base; @tailwind components; @tailwind utilities; @layer base { + * { + font-family: 'Outfit', sans-serif; + } + body { - @apply bg-surface text-primary font-sans antialiased; + @apply bg-surface text-primary antialiased; + } + + h1, h2, h3, h4, h5, h6 { + @apply font-semibold tracking-tight; + } + + :focus-visible { + @apply outline-none ring-2 ring-accent/20 ring-offset-2; + } + + ::selection { + @apply bg-accent/10 text-accent-700; + } + + ::-webkit-scrollbar { + width: 6px; + height: 6px; + } + ::-webkit-scrollbar-track { + @apply bg-transparent; + } + ::-webkit-scrollbar-thumb { + @apply bg-slate-300/60 rounded-full; + } + ::-webkit-scrollbar-thumb:hover { + @apply bg-slate-400/60; } } @layer components { + /* ─── Cards ─── */ .card { - @apply bg-card border border-border rounded-card shadow-card; + @apply bg-card border border-border/60 rounded-2xl shadow-card transition-all duration-200; } - + .card-hover { - @apply transition-all duration-200 hover:shadow-card-hover hover:border-slate-300; + @apply hover:shadow-card-hover hover:border-slate-300/80 hover:-translate-y-0.5; + } + + .card-padded { + @apply card p-5; } + .card-glass { + @apply bg-white/70 backdrop-blur-lg border border-white/40 rounded-2xl shadow-card; + } + + .card-accent { + @apply card border-l-4 border-l-accent; + } + + /* ─── Primary buttons ─── */ .btn-primary { - @apply bg-primary text-white rounded-card px-4 py-2 font-medium - transition-all duration-200 hover:bg-slate-800 - focus:outline-none focus:ring-2 focus:ring-slate-900 focus:ring-offset-2; + @apply bg-primary text-white rounded-xl px-4 py-2 font-medium + transition-all duration-150 hover:bg-slate-800 active:scale-[0.98] + disabled:opacity-50 disabled:cursor-not-allowed; + } + + .btn-sm { + @apply btn-primary text-sm px-3 py-1.5 rounded-lg; + } + + /* ─── Gradient button ─── */ + .btn-gradient { + @apply text-white rounded-xl px-5 py-2.5 font-semibold + transition-all duration-200 active:scale-[0.97] + disabled:opacity-50 disabled:cursor-not-allowed + shadow-md hover:shadow-glow; + background: linear-gradient(135deg, #d97706, #9a3412); + } + .btn-gradient:hover { + background: linear-gradient(135deg, #b45309, #7c2d12); + } + + .btn-gradient-sm { + @apply btn-gradient text-sm px-4 py-2 rounded-lg; + } + + /* ─── Action buttons ─── */ + .btn-success { + @apply bg-emerald-600 text-white rounded-xl px-4 py-2 font-medium + transition-all duration-150 hover:bg-emerald-700 active:scale-[0.98] + disabled:opacity-50 disabled:cursor-not-allowed; + } + + .btn-success-sm { + @apply btn-success text-sm px-3 py-1.5 rounded-lg; + } + + .btn-danger { + @apply bg-red-600 text-white rounded-xl px-4 py-2 font-medium + transition-all duration-150 hover:bg-red-700 active:scale-[0.98] + disabled:opacity-50 disabled:cursor-not-allowed; + } + + .btn-danger-sm { + @apply btn-danger text-sm px-3 py-1.5 rounded-lg; + } + + .btn-warning { + @apply bg-amber-500 text-white rounded-xl px-4 py-2 font-medium + transition-all duration-150 hover:bg-amber-600 active:scale-[0.98] + disabled:opacity-50 disabled:cursor-not-allowed; + } + + .btn-warning-sm { + @apply btn-warning text-sm px-3 py-1.5 rounded-lg; + } + + .btn-outline { + @apply bg-white border border-border text-primary rounded-xl px-4 py-2 font-medium + transition-all duration-150 hover:bg-slate-50 hover:border-slate-300 active:scale-[0.98] + disabled:opacity-50 disabled:cursor-not-allowed; + } + + .btn-outline-sm { + @apply btn-outline text-sm px-3 py-1.5 rounded-lg; + } + + .btn-ghost { + @apply text-muted rounded-xl px-4 py-2 font-medium + transition-all duration-150 hover:bg-slate-100 hover:text-primary + disabled:opacity-50 disabled:cursor-not-allowed; } - .btn-secondary { - @apply bg-white border border-border text-primary rounded-card px-4 py-2 font-medium - transition-all duration-200 hover:bg-slate-50 hover:border-slate-300; + .btn-ghost-sm { + @apply btn-ghost text-sm px-3 py-1.5 rounded-lg; } - .btn-muted { - @apply bg-surface border border-border text-muted rounded-card px-4 py-2 font-medium - transition-all duration-200 hover:bg-slate-100; + /* ─── Icon button ─── */ + .btn-icon { + @apply p-2 rounded-xl transition-all duration-150 hover:bg-slate-100 text-muted + hover:text-primary active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed; } + .btn-icon-sm { + @apply btn-icon p-1.5 rounded-lg; + } + + /* ─── Input ─── */ .input { - @apply w-full border border-border rounded-lg px-3 py-2 text-sm - bg-white focus:outline-none focus:border-primary focus:ring-2 - focus:ring-slate-900/10 transition-all duration-200; + @apply w-full border border-border rounded-xl px-3.5 py-2.5 text-sm + bg-white placeholder:text-muted/50 + focus:outline-none focus:border-accent focus:ring-2 + focus:ring-accent/10 transition-all duration-200; + } + + .input-sm { + @apply input text-xs px-2.5 py-1.5; + } + + .input-icon { + @apply input pl-10; + } + + /* ─── Select ─── */ + .select { + @apply input appearance-none cursor-pointer bg-no-repeat bg-[right_0.75rem_center] + bg-[length:16px_16px] pr-9; } + /* ─── Label ─── */ .label { - @apply block text-sm font-medium text-primary mb-1; + @apply block text-sm font-medium text-primary mb-1.5; } + .label-sm { + @apply label text-xs mb-1; + } + + /* ─── Page title ─── */ .page-title { - @apply text-2xl font-bold text-primary; + @apply text-2xl font-bold text-primary tracking-tight; + } + + .page-subtitle { + @apply text-sm text-muted mt-1; } + /* ─── Section title ─── */ .section-title { - @apply text-lg font-semibold text-primary; + @apply text-base font-semibold text-primary; + } + + /* ─── Divider ─── */ + .divider { + @apply border-t border-border; + } + + /* ─── Status badges ─── */ + .badge { + @apply inline-flex items-center gap-1 text-xs font-semibold px-2.5 py-1 rounded-full border; + } + + .badge-success { + @apply badge bg-emerald-50 text-emerald-700 border-emerald-200; + } + + .badge-warning { + @apply badge bg-amber-50 text-amber-700 border-amber-200; + } + + .badge-danger { + @apply badge bg-red-50 text-red-700 border-red-200; + } + + .badge-info { + @apply badge bg-blue-50 text-blue-700 border-blue-200; + } + + .badge-purple { + @apply badge bg-purple-50 text-purple-700 border-purple-200; + } + + .badge-neutral { + @apply badge bg-slate-100 text-slate-600 border-slate-200; + } + + .badge-accent { + @apply badge bg-accent-50 text-accent-700 border-accent-200; + } + + /* ─── Role badges ─── */ + .badge-role { + @apply badge text-xs px-2 py-0.5 rounded-md; + } + + .badge-student { + @apply badge-role bg-slate-100 text-slate-600 border-slate-200; + } + + .badge-moderator { + @apply badge-role bg-blue-100 text-blue-700 border-blue-200; + } + + .badge-senior { + @apply badge-role bg-purple-100 text-purple-700 border-purple-200; + } + + .badge-admin { + @apply badge-role bg-purple-100 text-purple-700 border-purple-200; + } + + /* ─── QuickLink accent cards ─── */ + .quick-card { + @apply card border-2 p-4 flex flex-col gap-2 cursor-pointer card-hover; + } + + /* ─── Page container ─── */ + .page-container { + @apply max-w-5xl mx-auto px-4 py-6; + } + + /* ─── Dropdown / Popover base ─── */ + .dropdown { + @apply bg-white/90 backdrop-blur-xl border border-border/60 rounded-2xl p-2 shadow-card-elevated; + } + + .dropdown-item { + @apply flex items-center gap-2 px-3 py-2 text-sm text-primary rounded-lg + transition-colors duration-100 hover:bg-slate-100 cursor-pointer; + } + + /* ─── Avatar ─── */ + .avatar { + @apply inline-flex items-center justify-center rounded-full font-semibold text-white shrink-0 select-none; + } + .avatar-xs { + @apply avatar w-6 h-6 text-[10px]; + } + .avatar-sm { + @apply avatar w-8 h-8 text-xs; + } + .avatar-md { + @apply avatar w-10 h-10 text-sm; + } + .avatar-lg { + @apply avatar w-12 h-12 text-base; + } + .avatar-xl { + @apply avatar w-16 h-16 text-lg; + } + + /* ─── Pill navigation / filter tabs ─── */ + .pill-group { + @apply flex items-center gap-1 p-1 bg-slate-100/80 rounded-xl; + } + .pill-item { + @apply px-3.5 py-1.5 text-sm font-medium rounded-lg cursor-pointer transition-all duration-200 + text-muted hover:text-primary; + } + .pill-active { + @apply pill-item bg-white text-primary shadow-sm; + } + + /* ─── Segmented control ─── */ + .segmented-control { + @apply relative flex items-center p-1 bg-slate-100 rounded-xl; + } + .segmented-item { + @apply relative z-10 px-4 py-2 text-sm font-medium rounded-lg cursor-pointer transition-all duration-200 + text-muted hover:text-primary; + } + .segmented-active { + @apply segmented-item bg-white text-primary shadow-sm; + } + + /* ─── Empty state ─── */ + .empty-state { + @apply flex flex-col items-center justify-center py-16 px-6 text-center; + } + .empty-state-icon { + @apply w-16 h-16 text-slate-300 mb-4; + } + .empty-state-title { + @apply text-lg font-semibold text-primary mb-1; + } + .empty-state-desc { + @apply text-sm text-muted max-w-sm; + } + + /* ─── Gradient text ─── */ + .text-gradient { + background: linear-gradient(135deg, #d97706, #9a3412); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + } + + /* ─── Gradient border (for cards) ─── */ + .gradient-border { + position: relative; + } + .gradient-border::before { + content: ''; + position: absolute; + inset: 0; + border-radius: inherit; + padding: 1px; + background: linear-gradient(135deg, #d97706, #9a3412); + -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; + pointer-events: none; + opacity: 0; + transition: opacity 0.3s; + } + .gradient-border:hover::before { + opacity: 1; + } + + /* ─── Status accent borders ─── */ + .status-resolved { + @apply border-l-4 border-l-emerald-400; + } + .status-partial { + @apply border-l-4 border-l-amber-400; + } + .status-unresolved { + @apply border-l-4 border-l-red-400; + } + .status-rejected { + @apply border-l-4 border-l-red-500; + } + .status-accepted { + @apply border-l-4 border-l-emerald-500; + } + + /* ─── Thread connector ─── */ + .thread-line { + @apply relative; + } + .thread-line::before { + content: ''; + @apply absolute left-5 top-12 bottom-0 w-0.5 bg-border; + } + + /* ─── Stat card with accent stripe ─── */ + .stat-card { + @apply card-padded flex items-center gap-4 border-l-4; + } + + /* ─── Accordion ─── */ + .accordion-content { + @apply overflow-hidden transition-all duration-300 ease-in-out; + } +} + +@layer utilities { + .animate-in { + animation: slideIn 0.15s ease-out; + } + + @keyframes slideIn { + from { opacity: 0; transform: translateY(-4px); } + to { opacity: 1; transform: translateY(0); } + } + + .fade-in { + animation: fadeIn 0.2s ease-out; + } + + @keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } + } + + .text-balance { + text-wrap: balance; + } + + /* Page enter animation */ + .page-enter { + animation: pageEnter 0.35s ease-out; + } + + @keyframes pageEnter { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } + } + + /* Shimmer effect for skeletons */ + .skeleton-shimmer { + background: linear-gradient( + 90deg, + #f1f5f9 25%, + #e2e8f0 50%, + #f1f5f9 75% + ); + background-size: 200% 100%; + animation: shimmer 1.5s ease-in-out infinite; + } + + @keyframes shimmer { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } + } + + /* Gradient background mesh for auth pages */ + .bg-mesh { + background-color: #fdfbf7; + background-image: + radial-gradient(at 20% 20%, rgba(217, 119, 6, 0.07) 0px, transparent 50%), + radial-gradient(at 80% 20%, rgba(154, 52, 18, 0.05) 0px, transparent 50%), + radial-gradient(at 50% 80%, rgba(217, 119, 6, 0.03) 0px, transparent 50%); + } + + /* Floating decorative blobs */ + .blob-accent { + @apply absolute rounded-full blur-3xl opacity-15 pointer-events-none; + background: linear-gradient(135deg, #d97706, #9a3412); + } + + /* Glassmorphism overlay */ + .glass-overlay { + @apply bg-slate-900/50 backdrop-blur-md; + } + + /* QP glow animation */ + .qp-glow { + animation: qpGlow 0.8s ease-out; + } + + @keyframes qpGlow { + 0% { box-shadow: 0 0 0 0 rgba(79, 70, 229, 0.4); transform: scale(1.05); } + 100% { box-shadow: 0 0 0 0 transparent; transform: scale(1); } + } + + /* Upvote bounce */ + .upvote-bounce { + animation: upvoteBounce 0.35s ease-out; + } + + @keyframes upvoteBounce { + 0% { transform: scale(1); } + 40% { transform: scale(1.25); } + 100% { transform: scale(1); } + } + + /* Gradient underline for section headers */ + .gradient-underline { + @apply relative pb-2; + } + .gradient-underline::after { + content: ''; + @apply absolute bottom-0 left-0 h-0.5 rounded-full; + width: 40px; + background: linear-gradient(90deg, #d97706, #9a3412); + } + + /* Hide scrollbar utility */ + .scrollbar-none::-webkit-scrollbar { + display: none; + } + .scrollbar-none { + -ms-overflow-style: none; + scrollbar-width: none; } } \ No newline at end of file diff --git a/client/src/pages/AboutPage.jsx b/client/src/pages/AboutPage.jsx new file mode 100644 index 0000000..2476034 --- /dev/null +++ b/client/src/pages/AboutPage.jsx @@ -0,0 +1,1065 @@ +import { useState, useEffect } from 'react'; +import { Link } from 'react-router-dom'; +import { useAuth } from '../context/AuthContext'; +import SegmentedControl from '../components/SegmentedControl'; +import Breadcrumb from '../components/Breadcrumb'; +import { + HelpCircle, + BookOpen, + Layers, + Award, + AlertTriangle, + Flame, + Shield, + ShieldAlert, + Terminal, + Zap, + Users, + Search, + CheckCircle2, + FileText, + Activity, + ArrowRight +} from 'lucide-react'; + +export default function AboutPage() { + const { user } = useAuth(); + const [activeTab, setActiveTab] = useState('story'); + const [activeFeature, setActiveFeature] = useState('fire'); + const [isHovered, setIsHovered] = useState(false); + + useEffect(() => { + if (isHovered) return; + const features = ['fire', 'jata', 'circle', 'q']; + const interval = setInterval(() => { + setActiveFeature((prev) => { + const currentIndex = features.indexOf(prev); + const nextIndex = (currentIndex + 1) % features.length; + return features[nextIndex]; + }); + }, 4000); + return () => clearInterval(interval); + }, [isHovered]); + + const tabOptions = [ + { value: 'story', label: 'Sage Pippalāda & Origins' }, + { value: 'symbolism', label: 'Logo Symbolism' }, + { value: 'features', label: 'Platform Features' }, + { value: 'economy', label: 'Reputation Economy' }, + ]; + + return ( +
+ + + {/* Hero Header */} +
+
+
+
+ PippaQ Logo +
+

+ About PippaQ +

+

+ The fusion of ancient sage wisdom, structured verification, and modern AI intelligence. +

+
+
+ + {/* Segmented Control Tabs */} +
+ +
+ + {/* Tab Contents */} +
+ {activeTab === 'story' && ( +
+
+
+ +
+

🧠 PippaQ β€” The Wisdom of Sage Pippalāda

+
+ +

+ PippaQ is inspired by the ancient Sage Pippalāda, a legendary philosopher and scholar in Indian history. He is a symbol of deep wisdom, clarity, and the ultimate ability to answer profound questions about existence, knowledge, and truth. +

+ +
+ "Just like Sage Pippalāda guided seekers with precise answers to complex questions, PippaQ is a modern AI-powered FAQ and Query Resolution system designed to structure, validate, and sustain community knowledge." +
+ +
+
+
+ + Answer Intelligently +
+

+ Leverages state-of-the-art semantic searches to resolve user queries with pre-approved knowledge. +

+
+ +
+
+ + Validate Logic +
+

+ Applies structured, hierarchical validation flows from peers, moderators, and admins. +

+
+ +
+
+ + Filter Duplicates +
+

+ Uses RAG decision systems to keep the database tidy and penalize redundant submissions. +

+
+ +
+
+ + Evolving Knowledge +
+

+ Promotes highly upvoted real-time queries into permanent structured FAQ articles. +

+
+
+ +
+ πŸ‘‰ PippaQ represents the perfect fusion of ancient wisdom and modern AI-driven FAQ systems. +
+
+ )} + + {activeTab === 'symbolism' && ( +
+ + +
+
+ +
+

🎨 Logo Meaning & Symbolism

+
+ + {/* Desktop / Large Screen: Interactive Diagram */} +
+ {/* Dot grid pattern overlay */} +
+ + + {/* SVG Connection Lines */} + + {/* VαΉ›tta connection line */} + + + {/* Jaṭā connection line */} + + + {/* Jvālā connection line */} + + + + {/* Hotspots */} + + {/* VαΉ›tta Hotspot */} + + {activeFeature === 'circle' && ( + + )} + + {/* Jaṭā Hotspot */} + + {activeFeature === 'jata' && ( + + )} + + {/* Jvālā Hotspot */} + + {activeFeature === 'fire' && ( + + )} + + + {/* Released Flowing Circles */} + + {activeFeature === 'circle' && ( + + + + + )} + {activeFeature === 'jata' && ( + + + + + )} + {activeFeature === 'fire' && ( + + + + + )} + + + {/* VαΉ›tta Card (Top Left) */} + +
{ setActiveFeature('circle'); setIsHovered(true); }} + onMouseLeave={() => setIsHovered(false)} + > +
+
+
+ + + +
+

VαΉ›tta

+
+ Circle +
+
    +
  • + β†’ + Control layer of knowledge +
  • +
  • + β†’ + Stability and validation system +
  • +
  • + β†’ + Ensures structured flow while maintaining order +
  • +
+
+
+ + {/* Jaṭā Card (Top Right) */} + +
{ setActiveFeature('jata'); setIsHovered(true); }} + onMouseLeave={() => setIsHovered(false)} + > +
+
+
+ + + + + +
+

Jaṭā

+
+ Matted Hair +
+
    +
  • + β†’ + Knowledge base +
  • +
  • + β†’ + Continuous structured growth +
  • +
  • + β†’ + Expansion of knowledge flow +
  • +
+
+
+ + {/* Centerpiece Logo (Perfect Circle Boundary focus) */} + +
{ setActiveFeature('q'); setIsHovered(true); }} + onMouseLeave={() => setIsHovered(false)} + > + {/* Radial Spotlight Overlay */} +
+ + {/* perfectly circular logo container */} +
+ PippaQ Logo + + {/* Jvālā (Flame) Glow Highlight: Bottom section glow */} +
+ + {/* Jaṭā Glow Highlight: Top-right region glow */} +
+ + {/* VαΉ›tta Glow Highlight: Glowing outer ring around entire logo inside */} +
+
+
+ + + {/* Jvālā Card (Bottom) */} + +
{ setActiveFeature('fire'); setIsHovered(true); }} + onMouseLeave={() => setIsHovered(false)} + > +
+
+
+ +
+

Jvālā

+
+ Flame +
+
    +
  • + β†’ + Tapa: Knowledge Discipline +
  • +
  • + β†’ + Continuous learning +
  • +
  • + β†’ + Knowledge generation through effort +
  • +
+
+
+ + {/* Q Highlight Statement (Center Bottom) */} + +
{ setActiveFeature('q'); setIsHovered(true); }} + onMouseLeave={() => setIsHovered(false)} + > +

+ ✳️ The Unified Symbol of Q +

+

+ The complete structure forms a stylized β€œQ”, representing a modern Query Intelligence System where knowledge is continuously created (Jvālā), structured (Jaṭā), and validated (VαΉ›tta). +

+
+
+ +
+ + {/* Mobile Screen: Responsive Tabs & Cards */} +
+ {/* Centerpiece logo with spotlight */} +
+
+
+ PippaQ Logo + {/* Jvālā Mobile Glow (Bottom section of circle) */} +
+ {/* Jaṭā Mobile Glow (Top-right region) */} +
+ {/* VαΉ›tta Mobile Glow (Outer ring border) */} +
+
+
+ + {/* Tabs list */} +
+ + + + +
+ + {/* Information Cards */} +
+ {activeFeature === 'fire' && ( +
+
+
+ +
+

Jvālā

+
+

+ Represents knowledge and continuous learning. It symbolizes Tapa (Knowledge Discipline)β€”the continuous effort required to gain and maintain wisdom, reflected as effort and persistence. +

+
    +
  • + + Tapa: Knowledge Discipline +
  • +
  • + + Continuous learning +
  • +
  • + + Knowledge generation through effort +
  • +
+
+ )} + + {activeFeature === 'jata' && ( +
+
+
+ + + + + +
+

Jaṭā

+
+

+ Represents the expanding knowledge base of the platform. Matted hair-like structure representing the continuous flow and growth of structured intelligence. +

+
    +
  • + + Knowledge base +
  • +
  • + + Continuous structured growth +
  • +
  • + + Expansion of knowledge flow +
  • +
+
+ )} + + {activeFeature === 'circle' && ( +
+
+
+ + + +
+

VαΉ›tta

+
+

+ The circular boundary represents the control layer of knowledge. It functions as a stability and validation system, ensuring a structured flow while maintaining order. +

+
    +
  • + + Control layer of knowledge +
  • +
  • + + Stability and validation system +
  • +
  • + + Ensures structured flow while maintaining order +
  • +
+
+ )} + + {activeFeature === 'q' && ( +
+
+ πŸ”· +

The Unified Symbol of Q

+
+

+ The complete structure forms a stylized β€œQ”, representing a modern Query Intelligence System where knowledge is continuously created (Jvālā), structured (Jaṭā), and validated (VαΉ›tta). +

+
    +
  • + + Query Intelligence System +
  • +
  • + + Fusion of Jvālā, Jaṭā, and VαΉ›tta +
  • +
+
+ )} +
+
+ + {/* Core Interpretation final block */} +
+

+ 🧠 Core Interpretation +

+
+

+ Jvālā (Flame) represents knowledge and continuous Tapa required to gain and maintain wisdom. +

+

+ Jaṭā (Matted Hair) represents the knowledge base β€” the continuous flow and growth of structured intelligence. +

+

+ VαΉ›tta (Circle) represents control and stability, ensuring knowledge remains validated and structured while allowing flow beyond boundaries. +

+
+
+ πŸ‘‰ Together, they form a unified β€œQ” symbol, representing a modern Query-based intelligence system. +
+
+ +
+ πŸ‘‰ The logo is a perfect blend of Ancient Wisdom + Structured Intelligence + Modern Query Systems. +
+
+ )} + + {activeTab === 'features' && ( +
+
+
+ +
+

βš™οΈ Platform Architecture & Pages

+
+ +
+
+ + + FAQ PAGE (Verified Base) + +

+ Houses verified, permanent, and category-organized questions. Features snap-scrolling categories, upvoting, and instant search. +

+
+ +
+ + + RTQ PAGE (Real-Time Queries) + +

+ Live forum where students raise unresolved questions, submit multiple answers, and vote on peer feedback under Moderator and Senior supervision. +

+
+ +
+ + + TRACK MY ISSUES + +

+ A dedicated dashboard for tracking submitted questions. Updates status in real-time (Green for resolved, Lite Blue for partially resolved, Red for unresolved). +

+
+ +
+ + + RAISE QUESTION & RAG SCAN + +

+ Allows students to submit questions. Uses semantic evaluation to verify duplicates, applying QP penalties for lazy submissions. +

+
+ +
+ + + USER & ADMIN MANAGEMENT + +

+ Enables Seniors and Admins to govern the platform, assign roles, monitor Quality Point balances, and approve FAQ conversion requests. +

+
+
+
+ )} + + {activeTab === 'economy' && ( +
+
+
+ +
+

πŸ‘₯ Roles & Quality Point (QP) System

+
+ + {/* Roles Descriptions */} +
+
+
+ + Student +
+

+ Can raise questions, submit answers on RTQ forum, upvote content, and track submitted queries. +

+
+ +
+
+ + Moderator +
+

+ Has student abilities plus the power to accept/reject questions and approve/reject answers. +

+
+ +
+
+ + Senior / Admin +
+

+ Highest authority. Can add FAQs, toggle reviews, and convert forum discussions into permanent FAQs. +

+
+
+ + {/* QP Tables */} +
+ {/* Student Rules */} +
+

+ + Student QP Ledger +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ActionQP Effect
Raise Question - Rejected-5 QP
Raise Question - Accepted+5 QP
Raise Question - Added to FAQ+20 QP
Post RTQ Answer+2 QP
Answer Approved by Mod/Senior+5 QP
Answer Removed-3 QP
Answer Selected for FAQ+10 QP
Every 10 upvotes on content+1 QP
+
+
+ + {/* Seniors & Mods Rules */} +
+

+ + Staff QP Ledger +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ActionQP Effect
Moderator Approval / Rejection+3 QP
Senior Approve / Remove Content+5 QP
Senior Answer Posted+5 QP
RTQ to FAQ Conversion+10 QP
Manual FAQ Creation+15 QP
+
+
+
+ + {/* Restriction Rules */} +
+ +
+

⚠️ Global System Restrictions

+
    +
  • Every user starts with a base balance of 100 QP.
  • +
  • If your Quality Point balance falls below 50 QP, you will be restricted from raising new questions.
  • +
  • If your Quality Point balance drops into the negative (QP < 0), your account enters Restriction Mode (Read-Only). You will be unable to raise questions, resolve queries, or post answers.
  • +
+
+
+
+ )} +
+ + {/* Footervision */} +
+

πŸš€ Final Vision

+

+ β€œPippaQ is not just a FAQ system. It is a knowledge intelligence ecosystem inspired by ancient wisdom and powered by modern AI: Where every question is validated, every answer is refined, and knowledge continuously evolves.” +

+ {!user && ( +
+ + Access the Platform + +
+ )} +
+
+ ); +} diff --git a/client/src/pages/AddFAQPage.jsx b/client/src/pages/AddFAQPage.jsx index d5517c3..7eda663 100644 --- a/client/src/pages/AddFAQPage.jsx +++ b/client/src/pages/AddFAQPage.jsx @@ -3,6 +3,8 @@ import { useNavigate } from 'react-router-dom'; import faqService from '../services/faq.service'; import { useQP } from '../context/QPContext'; import { FAQ_CATEGORIES } from '../utils/constants'; +import Breadcrumb from '../components/Breadcrumb'; +import { BookOpen, X } from 'lucide-react'; export default function AddFAQPage() { const [form, setForm] = useState({ question: '', answer: '', category: '', tags: '' }); @@ -36,73 +38,122 @@ export default function AddFAQPage() { } }; + const tagsList = form.tags.split(',').map(t => t.trim()).filter(Boolean); + return ( -
-

Add New FAQ

-
-
- {error && ( -
- {error} -
- )} -
- -