Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
5011260
chore: add uploads folder with .gitkeep to ensure folder structure is…
osctoss May 29, 2026
2581a0c
Latest FAQ updates
abhi03241 May 29, 2026
cab03a9
Merge branch 'main' of https://github.com/vicharanashala/cs20.git int…
osctoss May 29, 2026
d78da25
feat(chunks 7-10): global search, RTQ category filter, QP wire-up, ac…
abhi03241 May 29, 2026
e70f4ee
Merge branch 'main' of https://github.com/vicharanashala/cs20.git int…
osctoss May 29, 2026
f9ce1db
fix: disable transformer embedder (USE_TRANSFORMER=false) to avoid im…
abhi03241 May 29, 2026
a6ef685
fix: disable transformer embedder, add admin controller methods, wire…
abhi03241 May 30, 2026
1156202
Merge branch 'main' of https://github.com/vicharanashala/cs20 into FAQ-m
osctoss May 30, 2026
8974953
fix: FAQ categories display and API bugs - sync client/server categor…
abhi03241 May 30, 2026
5151212
chore: add test script for aggregation debugging
abhi03241 May 30, 2026
445fa2c
feat: integrate Sentence Transformer + Qdrant Cloud ANN duplicate det…
osctoss May 30, 2026
282cc14
Merge remote branch 'origin/teammate' into FAQ-m
osctoss May 30, 2026
3630ed1
Merge pull request #1 from osctoss/teammate
abhi03241 May 30, 2026
e692765
Merge pull request #2 from vicharanashala/teammate
abhi03241 May 30, 2026
263f7a5
Hide Ask a Question button for Admin/Senior and remove Notifications …
osctoss May 30, 2026
0c36300
Update CONTEXT.md with role-based UI constraints and dashboard refine…
osctoss May 30, 2026
30c5cea
Merge pull request #3 from osctoss/main
abhi03241 May 30, 2026
3b3f4c0
Standardize FAQ_CATEGORIES to the 10 requested clean categories and a…
osctoss May 30, 2026
d918b88
Update CONTEXT.md to document standardized FAQ/RTQ categories and mig…
osctoss May 30, 2026
9da8d82
Fix All Categories filter on FAQPage showing only General category
osctoss May 30, 2026
49196e1
Update CONTEXT.md with FAQPage All Categories filter fix
osctoss May 30, 2026
f4ba6bc
Fix category upvotes for non-General categories by normalizing groupe…
osctoss May 30, 2026
681e8da
Implement default category sorting by upvotes and fix item sorting fi…
osctoss May 30, 2026
bc9d592
Fix category upvoting, category filtering, and toggleable upvotes on …
osctoss May 30, 2026
d551677
Merge pull request #4 from osctoss/main
abhi03241 Jun 1, 2026
ef84a51
feat: implement RTQ (Real-Time Question) system including core API co…
osctoss Jun 2, 2026
4786cc7
Merge branch 'main' of https://github.com/vicharanashala/cs20
osctoss Jun 2, 2026
44b5c68
feat: show user role status badge on dashboards
osctoss Jun 2, 2026
fb0f1aa
docs: document dashboard role status badges in CONTEXT.md
osctoss Jun 2, 2026
f3a5cb8
feat: complete moderator feature audit and implement missing capabili…
osctoss Jun 2, 2026
74acc8f
Refactor moderation controls with icons, implement decision toggling …
osctoss Jun 2, 2026
0331558
Merge branch 'abhi03241:main' into FAQ-m
osctoss Jun 2, 2026
0e01bc7
Merge pull request #5 from osctoss/teammate
abhi03241 Jun 2, 2026
3f20ba6
Merge pull request #6 from vicharanashala/teammate
abhi03241 Jun 2, 2026
b28a196
fix: admin answer resolves RTQ; no QP awarded after RTQ is already re…
abhi03241 Jun 2, 2026
07ed89f
Merge branch 'origin/main' and resolve conflicts in rtq.controller.js
osctoss Jun 2, 2026
157a6f6
Merge branch 'FAQ-m' of https://github.com/osctoss/FAQ into FAQ-m
osctoss Jun 2, 2026
f193abb
Merge pull request #7 from osctoss/teammate
abhi03241 Jun 2, 2026
a37adb8
Merge pull request #8 from vicharanashala/teammate
abhi03241 Jun 2, 2026
ee9840c
Implement Senior RTQ moderation features, dynamic QP rules, and prior…
osctoss Jun 2, 2026
f27b46d
Implement controlled FAQ conversion flow with Senior Review Edit Moda…
osctoss Jun 2, 2026
334a98f
Upgrade working history page to filter and list only the questions co…
osctoss Jun 2, 2026
ad06032
Update CONTEXT.md with Controlled Senior FAQ Flow, Bidirectional Trac…
osctoss Jun 2, 2026
302ec8c
Upgrade Notifications page UI with colorful circular category tags, c…
osctoss Jun 2, 2026
37182cf
feat: leaderboard UI, block/unblock users, email change restriction, …
abhi03241 Jun 2, 2026
463d818
Merge pull request #9 from osctoss/teammate
abhi03241 Jun 2, 2026
b35b870
Merge branch 'origin/main' into FAQ-m resolving conflicts, keeping Mo…
osctoss Jun 2, 2026
2dc5d0a
style: fix font import order in index.css to resolve build warning
osctoss Jun 2, 2026
7fdc235
docs: update CONTEXT.md with git merge conflict resolution and css wa…
osctoss Jun 2, 2026
b58b731
fix: align moderator question accept status, prevent auto-resolve and…
osctoss Jun 2, 2026
e76ad05
feat: implement toggle markedForReview for RTQ and FAQ review flags
osctoss Jun 2, 2026
503988e
Merge branch 'vicharanashala:teammate' into teammate
osctoss Jun 2, 2026
7862c8b
feat: implement users list toggling, rename tiers to Peers and Senior…
osctoss Jun 3, 2026
4e55a76
Merge remote-tracking branch 'my-cs20-fork/teammate' into FAQ-m
osctoss Jun 3, 2026
e8e93cc
feat: implement role-based highlighting and track question status dro…
osctoss Jun 3, 2026
a9b3a58
Merge pull request #10 from osctoss/teammate
abhi03241 Jun 3, 2026
492b8f3
Merge pull request #11 from vicharanashala/teammate
abhi03241 Jun 3, 2026
9538cc2
feat: public FAQ mode, QP history page, notification delete, QP syste…
abhi03241 Jun 3, 2026
1477258
added /
abhi03241 Jun 3, 2026
b72b71e
UI/UX enhancements: premium widgets, category scroller, profile layou…
osctoss Jun 3, 2026
f3c131e
UI/UX enhancements: restore LoginModal popup for anonymous upvotes an…
osctoss Jun 3, 2026
38b3edb
feat: integrate brand logo icon & warm amber-bronze theme, and add re…
osctoss Jun 3, 2026
3b3b926
UI/UX enhancements: add About page, logo symbolism, platform features…
osctoss Jun 4, 2026
c1d7f44
UI/UX: fix crash on About page by importing useAuth and defining user
osctoss Jun 4, 2026
9634a63
UI/UX: Refine logo symbolism interactive section symbols and resolve …
osctoss Jun 4, 2026
7e469da
UI/UX: Add directed release animations and fix Q-card text overflow
osctoss Jun 4, 2026
40244d1
docs: Update CONTEXT.md with About Page interactive symbolism details
osctoss Jun 4, 2026
4834ae8
Fix FAQ conversion request bugs and welcome bonus QP allocation
osctoss Jun 7, 2026
2443f4c
Rewrite README.md with comprehensive project docs and UI screenshots
osctoss Jun 7, 2026
73fa7b6
Add custom favicon and web manifest for PippaQ branding
osctoss Jun 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ CHANGELOG.md
# yarn.lock

# Uploads / media
uploads/
uploads/*
!uploads/.gitkeep

# Test coverage
coverage/
366 changes: 196 additions & 170 deletions CONTEXT.md

Large diffs are not rendered by default.

475 changes: 398 additions & 77 deletions README.md

Large diffs are not rendered by default.

570 changes: 570 additions & 0 deletions README_for_reference.md

Large diffs are not rendered by default.

Binary file added assets/PippaQ1.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/UI_Visuals/About_name.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/UI_Visuals/Login.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/UI_Visuals/QP_history.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/UI_Visuals/about_icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/UI_Visuals/about_pages.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/UI_Visuals/about_qp.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/UI_Visuals/admin_access_request.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/UI_Visuals/admin_whitelist.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/UI_Visuals/moderator_faq.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/UI_Visuals/moderator_rtq.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/UI_Visuals/moderator_rtq_ops.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/UI_Visuals/notification.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/UI_Visuals/peer_dashboard.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/UI_Visuals/peer_duplicate_check.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/UI_Visuals/peer_leaderboard.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/UI_Visuals/peer_rtq.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/UI_Visuals/peer_track_question.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/UI_Visuals/profile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/UI_Visuals/request_approval.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/UI_Visuals/senior_add_question.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/UI_Visuals/senior_dashboard.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/UI_Visuals/senior_faq.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/UI_Visuals/senior_history.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/UI_Visuals/senior_leaderboard.png
Binary file added assets/UI_Visuals/senior_rtq.png
Binary file added assets/UI_Visuals/senior_rtq_ops.png
Binary file added assets/UI_Visuals/sign_up.png
Binary file added assets/favicon_io/android-chrome-192x192.png
Binary file added assets/favicon_io/android-chrome-512x512.png
Binary file added assets/favicon_io/apple-touch-icon.png
Binary file added assets/favicon_io/favicon-16x16.png
Binary file added assets/favicon_io/favicon-32x32.png
Binary file added assets/favicon_io/favicon.ico
Binary file not shown.
1 change: 1 addition & 0 deletions assets/favicon_io/site.webmanifest
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
13 changes: 11 additions & 2 deletions client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,17 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Q&A Platform</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🐛</text></svg>" />
<meta name="description" content="PippaQ — A premium semantic Q&A platform with an intelligent FAQ knowledge base, community-driven answers, and a gamified QP reputation economy." />
<meta name="theme-color" content="#4f46e5" />
<title>PippaQ — Premium Q&A Platform</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400..900;1,400..900&family=Outfit:wght@100..900&display=swap" rel="stylesheet">
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
</head>
<body>
<div id="root"></div>
Expand Down
Binary file added client/public/PippaQ1.webp
Binary file added client/public/android-chrome-192x192.png
Binary file added client/public/android-chrome-512x512.png
Binary file added client/public/apple-touch-icon.png
Binary file added client/public/favicon-16x16.png
Binary file added client/public/favicon-32x32.png
Binary file added client/public/favicon.ico
Binary file not shown.
11 changes: 11 additions & 0 deletions client/public/site.webmanifest
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "PippaQ",
"short_name": "PippaQ",
"icons": [
{ "src": "/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" }
],
"theme_color": "#4f46e5",
"background_color": "#ffffff",
"display": "standalone"
}
109 changes: 75 additions & 34 deletions client/src/App.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="min-h-screen flex items-center justify-center bg-surface">
<div className="text-muted">Loading...</div>
<div className="min-h-screen flex items-center justify-center bg-mesh">
<div className="flex flex-col items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-accent to-violet-600 flex items-center justify-center animate-pulse">
<span className="text-white font-brand font-bold text-lg">P</span>
</div>
<div className="text-muted text-sm font-medium">Loading...</div>
</div>
</div>
);
}
Expand All @@ -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 <LoadingScreen />;
Expand All @@ -51,41 +63,70 @@ function DashboardRoute() {
return <StudentDashboard />;
}

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 (
<div className="min-h-screen bg-surface">
{user && !isPublic && <Nav refreshUser={refreshUser} />}
<Routes>
{/* Public */}
<Route path="/login" element={<PublicOnly><LoginPage /></PublicOnly>} />
<Route path="/signup" element={<PublicOnly><SignupPage /></PublicOnly>} />

{/* Dashboard — role-aware */}
<Route path="/dashboard" element={<DashboardRoute />} />

{/* Protected — all authenticated users */}
<Route path="/faq" element={<ProtectedRoute><FAQPage /></ProtectedRoute>} />
<Route path="/rtq" element={<ProtectedRoute><RTQPage /></ProtectedRoute>} />
<Route path="/profile" element={<ProtectedRoute><ProfilePage /></ProtectedRoute>} />
<Route path="/users" element={<ProtectedRoute><UserListPage /></ProtectedRoute>} />
<Route path="/track" element={<ProtectedRoute><TrackQuestionPage /></ProtectedRoute>} />
<Route path="/history" element={<ProtectedRoute><WorkingHistoryPage /></ProtectedRoute>} />
<Route path="/notifications" element={<ProtectedRoute><NotificationsPage /></ProtectedRoute>} />

{/* Role-restricted */}
<Route path="/raise-question" element={<ProtectedRoute allowedRoles={['student', 'moderator']}><RaiseQuestionPage /></ProtectedRoute>} />
<Route path="/add-faq" element={<ProtectedRoute allowedRoles={['senior', 'admin']}><AddFAQPage /></ProtectedRoute>} />

{/* Redirects */}
<Route path="/" element={<Navigate to="/login" replace />} />
<Route path="*" element={<Navigate to="/login" replace />} />
</Routes>
<ScrollToTop />
{user && !['/login', '/signup'].includes(location.pathname) && <Nav refreshUser={refreshUser} />}
{searchOpen && <GlobalSearch onClose={() => setSearchOpen(false)} />}
<div key={location.pathname} className="page-enter">
<Routes>
<Route path="/login" element={<PublicOnly><LoginPage /></PublicOnly>} />
<Route path="/signup" element={<PublicOnly><SignupPage /></PublicOnly>} />
<Route path="/dashboard" element={<DashboardRoute />} />
<Route path="/faq" element={<FAQPage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/faq/edit/:id" element={<ProtectedRoute allowedRoles={['senior', 'admin']}><FAQEditPage /></ProtectedRoute>} />
<Route path="/rtq" element={<ProtectedRoute><RTQPage /></ProtectedRoute>} />
<Route path="/rtq/:id" element={<ProtectedRoute><RTQDetailPage /></ProtectedRoute>} />
<Route path="/profile" element={<ProtectedRoute><ProfilePage /></ProtectedRoute>} />
<Route path="/users" element={<ProtectedRoute><UserListPage /></ProtectedRoute>} />
<Route path="/users/:id" element={<ProtectedRoute><UserProfilePage /></ProtectedRoute>} />
<Route path="/track" element={<ProtectedRoute allowedRoles={['student', 'moderator']}><TrackQuestionPage /></ProtectedRoute>} />
<Route path="/history" element={<ProtectedRoute><WorkingHistoryPage /></ProtectedRoute>} />
<Route path="/notifications" element={<ProtectedRoute><NotificationsPage /></ProtectedRoute>} />
<Route path="/qp-history" element={<ProtectedRoute><QPHistoryPage /></ProtectedRoute>} />
<Route path="/raise-question" element={<ProtectedRoute allowedRoles={['student', 'moderator']}><RaiseQuestionPage /></ProtectedRoute>} />
<Route path="/add-faq" element={<ProtectedRoute allowedRoles={['senior', 'admin']}><AddFAQPage /></ProtectedRoute>} />
<Route path="/" element={<Navigate to="/faq" replace />} />
<Route path="*" element={<Navigate to="/faq" replace />} />
</Routes>
</div>
</div>
);
}
}

export default function App() {
return (
<ErrorBoundary>
<ToastProvider>
<AppLayout />
</ToastProvider>
</ErrorBoundary>
);
}
7 changes: 5 additions & 2 deletions client/src/components/AnswerCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,13 @@ export default function AnswerCard({ answer, onUpvote, showModeratorControls = f
<p className="text-xs text-muted">
Answered by <span className="font-medium text-primary">{answer.userId.name}</span>
{answer.userId?.role === 'moderator' && (
<span className="ml-1 text-xs bg-purple-100 text-purple-700 px-1.5 py-0.5 rounded">Moderator</span>
<span className="ml-1 text-xs bg-blue-100 text-blue-700 px-1.5 py-0.5 rounded">Moderator</span>
)}
{answer.userId?.role === 'senior' && (
<span className="ml-1 text-xs bg-blue-100 text-blue-700 px-1.5 py-0.5 rounded">Senior</span>
<span className="ml-1 text-xs bg-purple-100 text-purple-700 px-1.5 py-0.5 rounded">Senior</span>
)}
{answer.userId?.role === 'admin' && (
<span className="ml-1 text-xs bg-purple-100 text-purple-700 px-1.5 py-0.5 rounded">Admin</span>
)}
</p>
)}
Expand Down
43 changes: 43 additions & 0 deletions client/src/components/Avatar.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div
className={`${SIZE_CLASSES[size] || SIZE_CLASSES.md} ${bgClass} ${className}`}
title={name || 'User'}
>
{initial}
</div>
);
}
31 changes: 31 additions & 0 deletions client/src/components/BackToTop.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<button
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
className="fixed bottom-6 right-6 z-40 w-11 h-11 rounded-full shadow-card-elevated
bg-gradient-to-br from-accent to-violet-600 text-white
flex items-center justify-center
hover:shadow-glow-lg hover:scale-105
active:scale-95
transition-all duration-200
animate-slideUp"
title="Back to top"
aria-label="Back to top"
>
<ChevronUp className="w-5 h-5" />
</button>
);
}
59 changes: 59 additions & 0 deletions client/src/components/Badge.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<span className={config.className}>
<Icon className="w-3 h-3" />
{config.label}
</span>
);
}

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 <span className={config.className}>{config.label}</span>;
}

export function BoolBadge({ value, trueLabel = 'Yes', falseLabel = 'No' }) {
return value
? <span className="badge-success">{trueLabel}</span>
: <span className="badge-neutral">{falseLabel}</span>;
}

export function CountBadge({ count, label }) {
if (!count && count !== 0) return null;
return (
<span className="badge-neutral">
{label ? `${count} ${label}` : count}
</span>
);
}
24 changes: 24 additions & 0 deletions client/src/components/Breadcrumb.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ChevronRight, Home } from 'lucide-react';
import { Link } from 'react-router-dom';

export default function Breadcrumb({ items = [] }) {
return (
<nav className="flex items-center gap-1.5 text-sm text-muted mb-5" aria-label="Breadcrumb">
<Link to="/dashboard" className="hover:text-primary transition-colors p-0.5">
<Home className="w-3.5 h-3.5" />
</Link>
{items.map((item, i) => (
<span key={i} className="flex items-center gap-1.5">
<ChevronRight className="w-3.5 h-3.5 text-slate-300" />
{item.to ? (
<Link to={item.to} className="hover:text-primary transition-colors font-medium">
{item.label}
</Link>
) : (
<span className="text-primary font-medium">{item.label}</span>
)}
</span>
))}
</nav>
);
}
Loading