From 0797d9eb28eb7d8a7040706a74830e0d00653d5b Mon Sep 17 00:00:00 2001 From: Dhevesh Date: Sat, 30 May 2026 18:39:32 +0530 Subject: [PATCH 01/28] feat: complete FAQ system with admin dashboard, voting, dark mode, unresolved query saving --- client/src/App.jsx | 44 +- .../src/components/layout/DashboardLayout.jsx | 19 + client/src/components/layout/Header.jsx | 18 + client/src/components/layout/Sidebar.jsx | 56 +++ client/src/pages/AdminArea.jsx | 238 +++++++++++ client/src/pages/AnswerCenter.jsx | 60 +++ client/src/pages/AskQuestion.jsx | 151 +++++++ client/src/pages/DashboardHome.jsx | 53 +++ client/src/pages/MyQuestions.jsx | 61 +++ client/src/pages/QuestionDetail.jsx | 157 +++++++ client/src/pages/UserPage.jsx | 392 ++++++++---------- client/src/services/adminService.js | 30 ++ client/src/services/answerService.js | 39 ++ client/src/services/authService.js | 20 + client/src/services/faqService.js | 41 ++ client/src/services/questionService.js | 41 ++ client/src/styles/auth.css | 157 +++++++ server/controllers/adminController.js | 51 ++- server/controllers/answerController.js | 104 +++++ server/controllers/questionController.js | 76 ++++ server/controllers/searchController.js | 21 + server/models/Answer.js | 28 ++ server/models/Question.js | 24 ++ server/routes/adminRoutes.js | 3 +- server/routes/answerRoutes.js | 15 + server/routes/questionRoutes.js | 17 + server/routes/searchRoutes.js | 22 +- server/server.js | 4 + 28 files changed, 1685 insertions(+), 257 deletions(-) create mode 100644 client/src/components/layout/DashboardLayout.jsx create mode 100644 client/src/components/layout/Header.jsx create mode 100644 client/src/components/layout/Sidebar.jsx create mode 100644 client/src/pages/AdminArea.jsx create mode 100644 client/src/pages/AnswerCenter.jsx create mode 100644 client/src/pages/AskQuestion.jsx create mode 100644 client/src/pages/DashboardHome.jsx create mode 100644 client/src/pages/MyQuestions.jsx create mode 100644 client/src/pages/QuestionDetail.jsx create mode 100644 client/src/services/adminService.js create mode 100644 client/src/services/answerService.js create mode 100644 client/src/services/authService.js create mode 100644 client/src/services/faqService.js create mode 100644 client/src/services/questionService.js create mode 100644 server/controllers/answerController.js create mode 100644 server/controllers/questionController.js create mode 100644 server/models/Answer.js create mode 100644 server/models/Question.js create mode 100644 server/routes/answerRoutes.js create mode 100644 server/routes/questionRoutes.js diff --git a/client/src/App.jsx b/client/src/App.jsx index a7a544e..6c005ae 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -3,10 +3,17 @@ import { Toaster } from 'react-hot-toast'; import { AuthProvider } from './context/AuthContext'; import ProtectedRoute from './components/ProtectedRoute'; import AuthLayout from './components/AuthLayout'; +import DashboardLayout from './components/layout/DashboardLayout'; import LoginPage from './pages/LoginPage'; import RegisterPage from './pages/RegisterPage'; import UserPage from './pages/UserPage'; import QueryPage from './pages/QueryPage'; +import DashboardHome from './pages/DashboardHome'; +import AskQuestion from './pages/AskQuestion'; +import MyQuestions from './pages/MyQuestions'; +import QuestionDetail from './pages/QuestionDetail'; +import AnswerCenter from './pages/AnswerCenter'; +import AdminArea from './pages/AdminArea'; import './styles/auth.css'; export default function App() { @@ -30,28 +37,29 @@ export default function App() { }} /> + {/* Public routes */} } /> } /> - - - - } - /> - - - - } - /> + + {/* Standalone pages */} + } /> + } /> + + {/* Dashboard layout routes */} + }> + } /> + } /> + } /> + } /> + } /> + } /> + + + {/* Redirects */} } /> - } /> + } /> ); -} +} \ No newline at end of file diff --git a/client/src/components/layout/DashboardLayout.jsx b/client/src/components/layout/DashboardLayout.jsx new file mode 100644 index 0000000..3adeef5 --- /dev/null +++ b/client/src/components/layout/DashboardLayout.jsx @@ -0,0 +1,19 @@ +import { Outlet } from 'react-router-dom'; +import Sidebar from './Sidebar'; +import Header from './Header'; + +const DashboardLayout = () => { + return ( +
+ +
+
+
+ +
+
+
+ ); +}; + +export default DashboardLayout; diff --git a/client/src/components/layout/Header.jsx b/client/src/components/layout/Header.jsx new file mode 100644 index 0000000..a13f08e --- /dev/null +++ b/client/src/components/layout/Header.jsx @@ -0,0 +1,18 @@ +import { useAuth } from '../../context/AuthContext'; + +const Header = () => { + const { user } = useAuth(); + + return ( +
+
+ Dashboard / Overview +
+
+ {user?.role === 'admin' && ADMIN MODE} +
+
+ ); +}; + +export default Header; \ No newline at end of file diff --git a/client/src/components/layout/Sidebar.jsx b/client/src/components/layout/Sidebar.jsx new file mode 100644 index 0000000..f031a47 --- /dev/null +++ b/client/src/components/layout/Sidebar.jsx @@ -0,0 +1,56 @@ +import { Link, useLocation } from 'react-router-dom'; +import { useAuth } from '../../context/AuthContext'; +import { useEffect, useState } from 'react'; + +const Sidebar = () => { + const { user, logout } = useAuth(); + const location = useLocation(); + const [dark, setDark] = useState(() => localStorage.getItem('theme') === 'dark'); + + useEffect(() => { + document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light'); + localStorage.setItem('theme', dark ? 'dark' : 'light'); + }, [dark]); + + const isActive = (path) => location.pathname === path ? 'active' : ''; + + return ( +
+
+

Crowd Sourced FAQ Generation Web App

+
+
+ FAQ Hub + Dashboard + Ask Question + My Questions + Answer Center + {user?.role === 'admin' && ( + Admin Area + )} +
+
+
+
+ {user?.name || user?.email} +
+
+ {user?.role === 'admin' ? 'Administrator' : 'Member'} +
+
+ + +
+
+ ); +}; + +export default Sidebar; \ No newline at end of file diff --git a/client/src/pages/AdminArea.jsx b/client/src/pages/AdminArea.jsx new file mode 100644 index 0000000..323890d --- /dev/null +++ b/client/src/pages/AdminArea.jsx @@ -0,0 +1,238 @@ +import { useState, useEffect } from 'react'; +import adminService from '../services/adminService'; +import faqService from '../services/faqService'; + +const AdminArea = () => { + const [stats, setStats] = useState(null); + const [error, setError] = useState(null); + const [questions, setQuestions] = useState([]); + const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(true); + const [activeTab, setActiveTab] = useState('overview'); + + useEffect(() => { fetchData(); }, []); + + const fetchData = async () => { + try { + const statsData = await adminService.getStats(); + setStats(statsData); + const { default: questionService } = await import('../services/questionService'); + const qs = await questionService.getQuestions(); + setQuestions(qs); + const usersData = await adminService.getUsers(); + setUsers(usersData.users || []); + } catch (err) { + setError(err.response?.data?.message || 'Error loading admin data'); + } finally { + setLoading(false); + } + }; + + const handleDeleteQuestion = async (id) => { + if (!window.confirm('Delete this question?')) return; + try { + const { default: questionService } = await import('../services/questionService'); + await questionService.deleteQuestion(id); + fetchData(); + } catch { alert('Failed to delete question'); } + }; + + const handlePromoteToFaq = async (q) => { + const answerText = prompt('Enter the official answer for this FAQ:'); + if (!answerText) return; + try { + await faqService.createFaq({ question: q.title, answer: answerText, category: 'general' }); + alert('Successfully promoted to FAQ!'); + } catch { alert('Failed to promote to FAQ'); } + }; + + const handlePromoteUser = async (id) => { + if (!window.confirm('Promote this user to admin?')) return; + try { + await adminService.promoteUser(id); + fetchData(); + } catch { alert('Failed to promote user'); } + }; + + const handleDeleteUser = async (id) => { + if (!window.confirm('Delete this user? This cannot be undone.')) return; + try { + await adminService.deleteUser(id); + fetchData(); + } catch { alert('Failed to delete user'); } + }; + + if (loading) return
Loading admin area...
; + if (error) return
{error}
; + + const tabs = ['overview', 'questions', 'users']; + + return ( +
+
+

Admin Dashboard

+

Manage users, questions, and content

+
+ + {/* Stats Cards */} + {stats && ( +
+ {[ + { label: 'Total Users', value: stats.totalUsers, tab: 'users', color: '#6366f1' }, + { label: 'Total Questions', value: stats.totalQuestions, tab: 'questions', color: '#f59e0b' }, + { label: 'Total Answers', value: stats.totalAnswers, tab: null, color: '#10b981' }, + { label: 'Total FAQs', value: stats.totalFaqs, tab: null, color: '#3b82f6' }, + ].map(({ label, value, tab, color }) => ( +
tab && setActiveTab(tab)} + style={{ + background: 'var(--bg-card)', border: '1px solid var(--border)', + borderRadius: 'var(--radius-md)', padding: '20px 24px', + cursor: tab ? 'pointer' : 'default', + transition: 'all 0.2s', + borderLeft: `4px solid ${color}` + }} + onMouseOver={e => { if (tab) e.currentTarget.style.borderColor = color; }} + onMouseOut={e => { if (tab) e.currentTarget.style.borderColor = 'var(--border)'; }}> +
{label}
+
{value}
+ {tab &&
Click to view β†’
} +
+ ))} +
+ )} + + {/* Tabs */} +
+ {tabs.map(tab => ( + + ))} +
+ + {/* Overview Tab */} + {activeTab === 'overview' && ( +
+
+

Recent Questions

+ {questions.slice(0, 3).map(q => ( +
+
{q.title}
+
Status: {q.status}
+
+ ))} + +
+
+

Recent Users

+ {users.slice(0, 3).map(u => ( +
+
+
{u.name}
+
{u.email}
+
+ {u.role} +
+ ))} + +
+
+ )} + + {/* Questions Tab */} + {activeTab === 'questions' && ( +
+ {questions.length === 0 &&

No questions yet.

} + {questions.map(q => ( +
+
+
{q.title}
+
+ {q.status?.toUpperCase()} + + {new Date(q.createdAt).toLocaleDateString()} + +
+
+
+ + +
+
+ ))} +
+ )} + + {/* Users Tab */} + {activeTab === 'users' && ( +
+ {users.length === 0 &&

No users found.

} + {users.map(u => ( +
+
+
{u.name?.charAt(0)?.toUpperCase()}
+
+
{u.name}
+
{u.email}
+
+
+
+ {u.role} + {u.role !== 'admin' && ( + + )} + +
+
+ ))} +
+ )} +
+ ); +}; + +export default AdminArea; \ No newline at end of file diff --git a/client/src/pages/AnswerCenter.jsx b/client/src/pages/AnswerCenter.jsx new file mode 100644 index 0000000..78ee859 --- /dev/null +++ b/client/src/pages/AnswerCenter.jsx @@ -0,0 +1,60 @@ +import { useState, useEffect } from 'react'; +import questionService from '../services/questionService'; +import { Link } from 'react-router-dom'; + +const AnswerCenter = () => { + const [questions, setQuestions] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchQuestions = async () => { + try { + const data = await questionService.getQuestions(); + // Show pending questions first, then answered + const sorted = data.sort((a, b) => a.status === 'pending' ? -1 : 1); + setQuestions(sorted); + } catch (error) { + console.error(error); + } finally { + setLoading(false); + } + }; + fetchQuestions(); + }, []); + + if (loading) return
Loading...
; + + return ( +
+

Answer Center

+

Help the community by providing answers to open questions.

+ +
+ {questions.length === 0 ? ( +

No questions have been asked yet.

+ ) : ( + questions.map(q => ( +
+
+

{q.title}

+
+ + {q.status.toUpperCase()} + + + By {q.author?.username || 'Unknown'} + +
+
+ + View & Answer + +
+ )) + )} +
+
+ ); +}; + +export default AnswerCenter; diff --git a/client/src/pages/AskQuestion.jsx b/client/src/pages/AskQuestion.jsx new file mode 100644 index 0000000..948ba59 --- /dev/null +++ b/client/src/pages/AskQuestion.jsx @@ -0,0 +1,151 @@ +import { useState, useEffect, useRef } from 'react'; +import questionService from '../services/questionService'; +import { useNavigate } from 'react-router-dom'; +import api from '../api/axios'; + +const DRAFT_KEY = 'ask_question_draft'; + +const AskQuestion = () => { + const [title, setTitle] = useState(''); + const [description, setDescription] = useState(''); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + const [draftSaved, setDraftSaved] = useState(false); + const [duplicates, setDuplicates] = useState([]); + const [dupLoading, setDupLoading] = useState(false); + const navigate = useNavigate(); + const debounceRef = useRef(null); + + useEffect(() => { + const draft = localStorage.getItem(DRAFT_KEY); + if (draft) { + const { title: t, description: d } = JSON.parse(draft); + if (t) setTitle(t); + if (d) setDescription(d); + } + }, []); + + useEffect(() => { + if (!title && !description) return; + const timer = setTimeout(() => { + localStorage.setItem(DRAFT_KEY, JSON.stringify({ title, description })); + setDraftSaved(true); + setTimeout(() => setDraftSaved(false), 2000); + }, 1000); + return () => clearTimeout(timer); + }, [title, description]); + + const checkDuplicates = async (text) => { + if (!text || text.trim().length < 10) { setDuplicates([]); return; } + setDupLoading(true); + try { + const { data } = await api.get(`/search/suggestions?q=${encodeURIComponent(text)}`); + setDuplicates(data.results?.slice(0, 3) || []); + } catch { + setDuplicates([]); + } finally { + setDupLoading(false); + } + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + if (!title.trim()) { + setError('Title is required'); + return; + } + setLoading(true); + setError(null); + try { + await questionService.createQuestion({ title, description }); + localStorage.removeItem(DRAFT_KEY); + navigate('/my-questions'); + } catch (err) { + setError(err.response?.data?.message || 'Failed to submit question'); + } finally { + setLoading(false); + } + }; + + const clearDraft = () => { + localStorage.removeItem(DRAFT_KEY); + setTitle(''); + setDescription(''); + setDuplicates([]); + }; + + return ( +
+

Ask a Question

+

+ Have a question that isn't answered in the FAQ? Ask it here! +

+ {error &&
{error}
} +
+
+
+ + { + setTitle(e.target.value); + if (debounceRef.current) clearTimeout(debounceRef.current); + debounceRef.current = setTimeout(() => checkDuplicates(e.target.value), 600); + }} + placeholder="e.g., How do I reset my password?" + /> + {dupLoading &&
Checking for similar questions...
} + {duplicates.length > 0 && ( +
+
+ ⚠️ Similar questions already exist β€” check these first: +
+ {duplicates.map((d, i) => ( +
+ β†’ {d.question} +
+ ))} +
+ )} +
+
+ +