diff --git a/client/package-lock.json b/client/package-lock.json index 43f9372..601d1df 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -15,6 +15,7 @@ "@vercel/analytics": "^1.5.0", "@vercel/blob": "^1.1.1", "@vercel/speed-insights": "^1.2.0", + "aos": "^2.3.4", "axios": "^1.11.0", "clsx": "^2.1.1", "framer-motion": "^12.23.12", @@ -37,6 +38,7 @@ "devDependencies": { "@eslint/js": "^9.33.0", "@tailwindcss/postcss": "^4.1.12", + "@types/aos": "^3.0.7", "@types/node": "^24.3.0", "@types/react": "^19.1.10", "@types/react-dom": "^19.1.7", @@ -1577,6 +1579,13 @@ "vite": "^5.2.0 || ^6 || ^7" } }, + "node_modules/@types/aos": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/aos/-/aos-3.0.7.tgz", + "integrity": "sha512-sEhyFqvKauUJZDbvAB3Pggynrq6g+2PS4XB3tmUr+mDL1gfDJnwslUC4QQ7/l8UD+LWpr3RxZVR/rHoZrLqZVg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2153,6 +2162,17 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/aos": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/aos/-/aos-2.3.4.tgz", + "integrity": "sha512-zh/ahtR2yME4I51z8IttIt4lC1Nw0ktsFtmeDzID1m9naJnWXhCoARaCgNOGXb5CLy3zm+wqmRAEgMYB5E2HUw==", + "license": "MIT", + "dependencies": { + "classlist-polyfill": "^1.0.3", + "lodash.debounce": "^4.0.6", + "lodash.throttle": "^4.0.1" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2433,6 +2453,12 @@ "node": ">=18" } }, + "node_modules/classlist-polyfill": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/classlist-polyfill/-/classlist-polyfill-1.2.0.tgz", + "integrity": "sha512-GzIjNdcEtH4ieA2S8NmrSxv7DfEV5fmixQeyTmqmRmRJPGpRBaSnA2a0VrCjyT8iW8JjEdMbKzDotAJf+ajgaQ==", + "license": "Unlicense" + }, "node_modules/cliui": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", @@ -4093,12 +4119,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", + "license": "MIT" + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", diff --git a/client/package.json b/client/package.json index e90ea5b..b078bb2 100644 --- a/client/package.json +++ b/client/package.json @@ -18,6 +18,7 @@ "@vercel/analytics": "^1.5.0", "@vercel/blob": "^1.1.1", "@vercel/speed-insights": "^1.2.0", + "aos": "^2.3.4", "axios": "^1.11.0", "clsx": "^2.1.1", "framer-motion": "^12.23.12", @@ -40,6 +41,7 @@ "devDependencies": { "@eslint/js": "^9.33.0", "@tailwindcss/postcss": "^4.1.12", + "@types/aos": "^3.0.7", "@types/node": "^24.3.0", "@types/react": "^19.1.10", "@types/react-dom": "^19.1.7", diff --git a/client/src/App.tsx b/client/src/App.tsx index 556900e..f5d6e16 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -44,6 +44,8 @@ import ScrollToTop from "./components/ui/ScrollToTop"; import ScrollToTopOnRouteChange from "./components/ui/ScrollToTopOnRouteChange"; import { isAuthenticated } from "./utils/auth"; import { SocketProvider } from "./contexts/SocketContext"; +import AOS from "aos"; +import "aos/dist/aos.css"; interface AppProps { } @@ -94,6 +96,13 @@ const App: React.FC = () => { checkAuthStatus(); }, [checkAuthStatus]); + useEffect(() => { + AOS.init({ + duration: 1000, + once: true, + }); + }, []); + return ( diff --git a/client/src/components/admin/AdminPanel.tsx b/client/src/components/admin/AdminPanel.tsx index 773d862..f366977 100644 --- a/client/src/components/admin/AdminPanel.tsx +++ b/client/src/components/admin/AdminPanel.tsx @@ -5,16 +5,11 @@ import { Bug, TrendingUp, Clock, - CheckCircle, - XCircle, AlertTriangle, - Users, - Filter, - Search, - Eye, Edit3, } from "lucide-react"; +/* ===================== TYPES ===================== */ interface FeatureSuggestion { id: string; title: string; @@ -38,11 +33,6 @@ interface BugReport { description: string; severity: string; status: string; - stepsToReproduce?: string; - expectedBehavior?: string; - actualBehavior?: string; - browserInfo?: string; - deviceInfo?: string; userName?: string; userEmail?: string; adminNotes?: string; @@ -54,31 +44,51 @@ interface BugReport { } interface AdminStats { - featureSuggestions: { - total: number; - pending: number; - }; - bugReports: { - total: number; - open: number; - critical: number; - }; + featureSuggestions: { total: number; pending: number }; + bugReports: { total: number; open: number; critical: number }; } +type TabId = "dashboard" | "features" | "bugs"; + +/* ===================== CONFIG ===================== */ +const TABS: { id: TabId; label: string; icon: any }[] = [ + { id: "dashboard", label: "Dashboard", icon: TrendingUp }, + { id: "features", label: "Feature Suggestions", icon: Lightbulb }, + { id: "bugs", label: "Bug Reports", icon: Bug }, +]; + +const FEATURE_STATUS = ["pending", "in-progress", "completed", "rejected"]; +const FEATURE_CATEGORIES = ["ui", "functionality", "performance", "other"]; + +const BUG_STATUS = ["open", "in-progress", "resolved", "closed"]; +const BUG_SEVERITY = ["low", "medium", "high", "critical"]; + +const STATUS_OPTIONS = { + feature: FEATURE_STATUS, + bug: BUG_STATUS, +}; + +const renderOptions = (items: string[], empty: string) => ( + <> + + {items.map((i) => ( + + ))} + +); + +/* ===================== COMPONENT ===================== */ const AdminPanel: React.FC = () => { - const [activeTab, setActiveTab] = useState<"dashboard" | "features" | "bugs">( - "dashboard" - ); + const [activeTab, setActiveTab] = useState("dashboard"); const [stats, setStats] = useState(null); - const [featureSuggestions, setFeatureSuggestions] = useState< - FeatureSuggestion[] - >([]); + const [featureSuggestions, setFeatureSuggestions] = useState([]); const [bugReports, setBugReports] = useState([]); const [loading, setLoading] = useState(true); - const [selectedItem, setSelectedItem] = useState< - FeatureSuggestion | BugReport | null - >(null); + const [selectedItem, setSelectedItem] = useState(null); const [showModal, setShowModal] = useState(false); + const [filters, setFilters] = useState({ features: { status: "", category: "" }, bugs: { status: "", severity: "" }, @@ -91,10 +101,7 @@ const AdminPanel: React.FC = () => { const fetchAdminData = async () => { try { const token = localStorage.getItem("auth_token"); - const headers = { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }; + const headers = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }; const [statsRes, featuresRes, bugsRes] = await Promise.all([ fetch("/api/feedback/admin/stats", { headers }), @@ -102,109 +109,42 @@ const AdminPanel: React.FC = () => { fetch("/api/feedback/bug-reports", { headers }), ]); - if (statsRes.ok) { - const statsData = await statsRes.json(); - setStats(statsData.stats); - } - - if (featuresRes.ok) { - const featuresData = await featuresRes.json(); - setFeatureSuggestions(featuresData.suggestions); - } - - if (bugsRes.ok) { - const bugsData = await bugsRes.json(); - setBugReports(bugsData.bugReports); - } - } catch (error) { - console.error("Error fetching admin data:", error); + if (statsRes.ok) setStats((await statsRes.json()).stats); + if (featuresRes.ok) setFeatureSuggestions((await featuresRes.json()).suggestions); + if (bugsRes.ok) setBugReports((await bugsRes.json()).bugReports); + } catch (e) { + console.error(e); } finally { setLoading(false); } }; - const updateFeatureSuggestion = async ( - id: string, - updates: Partial - ) => { - try { - const token = localStorage.getItem("auth_token"); - const response = await fetch(`/api/feedback/feature-suggestions/${id}`, { - method: "PUT", - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - body: JSON.stringify(updates), - }); - - if (response.ok) { - fetchAdminData(); - setShowModal(false); - } - } catch (error) { - console.error("Error updating feature suggestion:", error); - } + const updateFeatureSuggestion = async (id: string, updates: Partial) => { + const token = localStorage.getItem("auth_token"); + await fetch(`/api/feedback/feature-suggestions/${id}`, { + method: "PUT", + headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }, + body: JSON.stringify(updates), + }); + fetchAdminData(); + setShowModal(false); }; const updateBugReport = async (id: string, updates: Partial) => { - try { - const token = localStorage.getItem("auth_token"); - const response = await fetch(`/api/feedback/bug-reports/${id}`, { - method: "PUT", - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - body: JSON.stringify(updates), - }); - - if (response.ok) { - fetchAdminData(); - setShowModal(false); - } - } catch (error) { - console.error("Error updating bug report:", error); - } - }; - - const getStatusColor = (status: string) => { - switch (status) { - case "pending": - case "open": - return "text-yellow-400"; - case "in-progress": - return "text-blue-400"; - case "completed": - case "resolved": - return "text-green-400"; - case "rejected": - case "closed": - return "text-red-400"; - default: - return "text-gray-400"; - } - }; - - const getSeverityColor = (severity: string) => { - switch (severity) { - case "low": - return "text-green-400"; - case "medium": - return "text-yellow-400"; - case "high": - return "text-orange-400"; - case "critical": - return "text-red-400"; - default: - return "text-gray-400"; - } + const token = localStorage.getItem("auth_token"); + await fetch(`/api/feedback/bug-reports/${id}`, { + method: "PUT", + headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }, + body: JSON.stringify(updates), + }); + fetchAdminData(); + setShowModal(false); }; if (loading) { return ( -
-
+
+
); } @@ -212,446 +152,169 @@ const AdminPanel: React.FC = () => { return (
- {/* Header */} + + {/* HEADER */}
-
- +
+
-

- Admin Panel -

-

- Manage feature suggestions and bug reports -

+

Admin Panel

+

Manage feedback

- {/* Navigation Tabs */} + {/* TABS */}
- {[ - { id: "dashboard", label: "Dashboard", icon: TrendingUp }, - { id: "features", label: "Feature Suggestions", icon: Lightbulb }, - { id: "bugs", label: "Bug Reports", icon: Bug }, - ].map((tab) => ( + {TABS.map(({ id, label, icon: Icon }) => ( ))}
- {/* Dashboard Tab */} + {/* DASHBOARD */} {activeTab === "dashboard" && stats && ( -
-
-
- - - {stats.featureSuggestions.total} - -
-

- Feature Suggestions -

-

- {stats.featureSuggestions.pending} pending review -

-
- -
-
- - - {stats.bugReports.total} - -
-

- Bug Reports -

-

- {stats.bugReports.open} open issues -

-
- -
-
- - - {stats.bugReports.critical} - -
-

- Critical Bugs -

-

Require immediate attention

-
- -
-
- - - {stats.featureSuggestions.pending + stats.bugReports.open} - +
+ {[{ + icon: Lightbulb, + color: "text-alien-green", + value: stats.featureSuggestions.total, + title: "Feature Suggestions", + },{ + icon: Bug, + color: "text-red-400", + value: stats.bugReports.total, + title: "Bug Reports", + },{ + icon: AlertTriangle, + color: "text-orange-400", + value: stats.bugReports.critical, + title: "Critical Bugs", + },{ + icon: Clock, + color: "text-blue-400", + value: stats.featureSuggestions.pending + stats.bugReports.open, + title: "Pending Items", + }].map(({ icon: Icon, color, value, title }) => ( +
+
+ + {value} +
+

{title}

-

- Pending Items -

-

Awaiting review

-
+ ))}
)} - {/* Feature Suggestions Tab */} + {/* FEATURES */} {activeTab === "features" && (
-
-

- Feature Suggestions -

-
- - -
+
+ + +
-
- {featureSuggestions - .filter( - (item) => - (!filters.features.status || - item.status === filters.features.status) && - (!filters.features.category || - item.category === filters.features.category) - ) - .map((suggestion) => ( -
-
-
-

- {suggestion.title} -

-

- {suggestion.description} -

-
- - Category: {suggestion.category} - - - Priority: {suggestion.priority} - - - Status: {suggestion.status} - -
-
- -
-
- Submitted by:{" "} - {suggestion.user?.username || - suggestion.userName || - "Anonymous"}{" "} - •{new Date(suggestion.createdAt).toLocaleDateString()} -
+ {featureSuggestions.map((f) => ( +
+
+
+

{f.title}

+

{f.description}

- ))} -
+ +
+
+ ))}
)} - {/* Bug Reports Tab */} + {/* BUGS */} {activeTab === "bugs" && (
-
-

Bug Reports

-
- - -
-
- -
- {bugReports - .filter( - (item) => - (!filters.bugs.status || - item.status === filters.bugs.status) && - (!filters.bugs.severity || - item.severity === filters.bugs.severity) - ) - .map((bug) => ( -
-
-
-

- {bug.title} -

-

- {bug.description} -

-
- - Severity: {bug.severity} - - - Status: {bug.status} - -
-
- -
-
- Reported by:{" "} - {bug.user?.username || bug.userName || "Anonymous"} • - {new Date(bug.createdAt).toLocaleDateString()} -
-
- ))} +
+ + +
-
- )} - {/* Modal for editing items */} - {showModal && selectedItem && ( -
-
-

- {"severity" in selectedItem - ? "Edit Bug Report" - : "Edit Feature Suggestion"} -

- -
-
- - -
- - {"severity" in selectedItem ? ( -
- - -
- ) : ( + {bugReports.map((b) => ( +
+
- - +

{b.title}

+

{b.description}

- )} - -
- -