create-fullstack-boilerplate 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +390 -0
  2. package/index.js +78 -0
  3. package/lib/addDB.js +77 -0
  4. package/lib/addRoute.js +264 -0
  5. package/lib/copyProject.js +25 -0
  6. package/lib/dataTypes.js +79 -0
  7. package/lib/installDeps.js +11 -0
  8. package/lib/prompts.js +289 -0
  9. package/lib/setupExtraDB.js +172 -0
  10. package/lib/setupMainDB.js +9 -0
  11. package/lib/testDBConnection.js +31 -0
  12. package/lib/utils.js +39 -0
  13. package/package.json +45 -0
  14. package/template/Backend/.env +7 -0
  15. package/template/Backend/DB/DBInit.js +28 -0
  16. package/template/Backend/DB/dbConfigs.js +4 -0
  17. package/template/Backend/Models/index.js +54 -0
  18. package/template/Backend/README.md +535 -0
  19. package/template/Backend/middleware/authMiddleware.js +19 -0
  20. package/template/Backend/package-lock.json +2997 -0
  21. package/template/Backend/package.json +32 -0
  22. package/template/Backend/routes/authRoutes.js +15 -0
  23. package/template/Backend/routes/dashboardRoutes.js +13 -0
  24. package/template/Backend/routes/index.js +15 -0
  25. package/template/Backend/routes/settingsRoutes.js +9 -0
  26. package/template/Backend/server.js +70 -0
  27. package/template/Backend/services/authService.js +68 -0
  28. package/template/Backend/services/cryptoService.js +14 -0
  29. package/template/Backend/services/dashboardService.js +39 -0
  30. package/template/Backend/services/settingsService.js +43 -0
  31. package/template/Frontend/.env +3 -0
  32. package/template/Frontend/README.md +576 -0
  33. package/template/Frontend/eslint.config.js +29 -0
  34. package/template/Frontend/index.html +13 -0
  35. package/template/Frontend/package-lock.json +3690 -0
  36. package/template/Frontend/package.json +39 -0
  37. package/template/Frontend/public/PMDLogo.png +0 -0
  38. package/template/Frontend/public/pp.jpg +0 -0
  39. package/template/Frontend/public/tabicon.png +0 -0
  40. package/template/Frontend/src/App.jsx +71 -0
  41. package/template/Frontend/src/assets/fonts/ArticulatCFDemiBold/font.woff +0 -0
  42. package/template/Frontend/src/assets/fonts/ArticulatCFDemiBold/font.woff2 +0 -0
  43. package/template/Frontend/src/assets/fonts/ArticulatCFNormal/font.woff +0 -0
  44. package/template/Frontend/src/assets/fonts/ArticulatCFNormal/font.woff2 +0 -0
  45. package/template/Frontend/src/assets/fonts/ArticulatCFRegular/font.woff +0 -0
  46. package/template/Frontend/src/assets/fonts/ArticulatCFRegular/font.woff2 +0 -0
  47. package/template/Frontend/src/assets/fonts/MixtaProRegularItalic/font.woff +0 -0
  48. package/template/Frontend/src/assets/fonts/MixtaProRegularItalic/font.woff2 +0 -0
  49. package/template/Frontend/src/assets/fonts/fonts_sohne/OTF/S/303/266hneMono-Buch.otf +0 -0
  50. package/template/Frontend/src/assets/fonts/fonts_sohne/OTF/S/303/266hneMono-Leicht.otf +0 -0
  51. package/template/Frontend/src/assets/fonts/fonts_sohne/WOFF2/soehne-mono-buch.woff2 +0 -0
  52. package/template/Frontend/src/assets/fonts/fonts_sohne/WOFF2/soehne-mono-leicht.woff2 +0 -0
  53. package/template/Frontend/src/components/Layout.jsx +61 -0
  54. package/template/Frontend/src/components/Loader.jsx +19 -0
  55. package/template/Frontend/src/components/ProtectedRoute.jsx +19 -0
  56. package/template/Frontend/src/components/Sidebar.jsx +286 -0
  57. package/template/Frontend/src/components/ThemeToggle.jsx +30 -0
  58. package/template/Frontend/src/config/axiosClient.js +46 -0
  59. package/template/Frontend/src/config/encryption.js +11 -0
  60. package/template/Frontend/src/config/routes.js +65 -0
  61. package/template/Frontend/src/contexts/AuthContext.jsx +144 -0
  62. package/template/Frontend/src/contexts/ThemeContext.jsx +69 -0
  63. package/template/Frontend/src/index.css +88 -0
  64. package/template/Frontend/src/main.jsx +11 -0
  65. package/template/Frontend/src/pages/Dashboard.jsx +137 -0
  66. package/template/Frontend/src/pages/Login.jsx +195 -0
  67. package/template/Frontend/src/pages/NotFound.jsx +70 -0
  68. package/template/Frontend/src/pages/Settings.jsx +69 -0
  69. package/template/Frontend/tailwind.config.js +90 -0
  70. package/template/Frontend/vite.config.js +37 -0
  71. package/template/Readme.md +0 -0
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "full-stack-template",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "@heroicons/react": "^2.2.0",
14
+ "@tailwindcss/vite": "^4.1.13",
15
+ "axios": "^1.12.2",
16
+ "crypto-js": "^4.2.0",
17
+ "daisyui": "^5.1.14",
18
+ "jwt-decode": "^4.0.0",
19
+ "lucide-react": "^0.544.0",
20
+ "react": "^19.1.1",
21
+ "react-dom": "^19.1.1",
22
+ "react-is": "^19.1.1",
23
+ "react-router-dom": "^7.9.2",
24
+ "react-spinners": "^0.17.0",
25
+ "recharts": "^3.2.1",
26
+ "tailwindcss": "^4.1.13"
27
+ },
28
+ "devDependencies": {
29
+ "@eslint/js": "^9.36.0",
30
+ "@types/react": "^19.1.13",
31
+ "@types/react-dom": "^19.1.9",
32
+ "@vitejs/plugin-react-swc": "^4.1.0",
33
+ "eslint": "^9.36.0",
34
+ "eslint-plugin-react-hooks": "^5.2.0",
35
+ "eslint-plugin-react-refresh": "^0.4.20",
36
+ "globals": "^16.4.0",
37
+ "vite": "^7.1.7"
38
+ }
39
+ }
Binary file
@@ -0,0 +1,71 @@
1
+ import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
2
+ import { AuthProvider, useAuth } from './contexts/AuthContext';
3
+ import { ThemeProvider, useTheme } from './contexts/ThemeContext';
4
+ import SpinningLoader from './components/Loader';
5
+ import Layout from './components/Layout';
6
+ import ProtectedRoute from './components/ProtectedRoute';
7
+ import Login from './pages/Login';
8
+ import Settings from './pages/Settings';
9
+ import Dashboard from './pages/Dashboard';
10
+ import NotFound from './pages/NotFound';
11
+
12
+ // Reusable wrapper component for protected routes with layout
13
+ const ProtectedLayout = ({ component: Component }) => {
14
+ return (
15
+ <ProtectedRoute>
16
+ <Layout>
17
+ <Component />
18
+ </Layout>
19
+ </ProtectedRoute>
20
+ );
21
+ };
22
+
23
+ const AppContent = () => {
24
+ const { isAuthenticated, isLoading, handleLogout } = useAuth();
25
+ const { theme } = useTheme();
26
+
27
+ if (isLoading) {
28
+ return <SpinningLoader size={50} />;
29
+ }
30
+
31
+ return (
32
+ <div className={`min-h-screen ${theme}`}>
33
+ <Routes>
34
+ {!isAuthenticated ? (
35
+ <>
36
+ <Route path="/login" element={<Login />} />
37
+ <Route path="*" element={<Navigate to="/login" replace />} />
38
+ </>
39
+ ) : (
40
+ <>
41
+ {/* Redirect auth pages to dashboard */}
42
+ <Route path="/login" element={<Navigate to="/dashboard" replace />} />
43
+
44
+ {/* Protected routes */}
45
+ <Route path="/" element={<Navigate to="/dashboard" replace />} />
46
+ <Route path="/dashboard" element={<ProtectedLayout component={Dashboard} />} />
47
+ <Route path="/settings" element={<ProtectedLayout component={Settings} />} />
48
+
49
+ {/* 404 */}
50
+ <Route path="*" element={<NotFound />} />
51
+ </>
52
+ )}
53
+ </Routes>
54
+ </div>
55
+ );
56
+ };
57
+
58
+ // Main App component with providers
59
+ const App = () => {
60
+ return (
61
+ <Router>
62
+ <AuthProvider>
63
+ <ThemeProvider>
64
+ <AppContent />
65
+ </ThemeProvider>
66
+ </AuthProvider>
67
+ </Router>
68
+ );
69
+ };
70
+
71
+ export default App;
@@ -0,0 +1,61 @@
1
+ import { useState } from 'react';
2
+ import { useTheme } from '../contexts/ThemeContext';
3
+ import Sidebar from './Sidebar';
4
+ import ThemeToggle from './ThemeToggle';
5
+
6
+ const Layout = ({ children, onLogout }) => {
7
+ const { isDark } = useTheme();
8
+ const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
9
+ const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
10
+
11
+ return (
12
+ <div className="flex h-screen overflow-hidden">
13
+ {/* Sidebar */}
14
+ <Sidebar
15
+ isCollapsed={isSidebarCollapsed}
16
+ setIsCollapsed={setIsSidebarCollapsed}
17
+ isMobileMenuOpen={isMobileMenuOpen}
18
+ setIsMobileMenuOpen={setIsMobileMenuOpen}
19
+ onLogout={onLogout}
20
+ />
21
+
22
+ {/* Mobile overlay */}
23
+ {isMobileMenuOpen && (
24
+ <div
25
+ className="fixed inset-0 z-40 bg-black/50 lg:hidden"
26
+ onClick={() => setIsMobileMenuOpen(false)}
27
+ />
28
+ )}
29
+
30
+ {/* Main Content */}
31
+ <main className={`flex-1 flex flex-col overflow-hidden transition-all duration-300 ${isSidebarCollapsed ? 'lg:ml-16' : 'lg:ml-64'
32
+ } ml-0`}>
33
+ {/* Mobile Header */}
34
+ <div className={`lg:hidden ${isDark ? 'bg-gray-800 border-gray-700' : 'bg-white border-gray-200'
35
+ } border-b p-4 flex items-center justify-between transition-colors duration-200`}>
36
+ <button
37
+ onClick={() => setIsMobileMenuOpen(true)}
38
+ className={`p-2 rounded-lg transition-colors duration-200 ${isDark ? 'hover:bg-gray-700 text-gray-300' : 'hover:bg-gray-100 text-gray-600'
39
+ }`}
40
+ >
41
+ <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
42
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
43
+ </svg>
44
+ </button>
45
+ <h1 className={`text-xl font-articulatcf-demibold ${isDark ? 'text-white' : 'text-gray-800'
46
+ } transition-colors duration-200`}>
47
+ Maxiom
48
+ </h1>
49
+ <ThemeToggle size="sm" />
50
+ </div>
51
+
52
+ {/* Content Area */}
53
+ <div className="flex-1 overflow-auto">
54
+ {children}
55
+ </div>
56
+ </main>
57
+ </div>
58
+ );
59
+ };
60
+
61
+ export default Layout;
@@ -0,0 +1,19 @@
1
+ import { useTheme } from '../contexts/ThemeContext';
2
+ import { BarLoader } from 'react-spinners';
3
+
4
+ const SpinningLoader = ({ size = 10 }) => {
5
+ const { isDark } = useTheme()
6
+ return (
7
+ <div className={`flex flex-col justify-center items-center gap-4 h-full w-full ${isDark ? 'bg-gray-900' : 'bg-white'}`}>
8
+ <img
9
+ src={'/PMDLogo.png'}
10
+ alt="Loading..."
11
+ style={{ width: "auto", height: size }}
12
+ className="animate-pulse"
13
+ />
14
+ <BarLoader color={isDark ? 'white' : "black"} width={200} className='w-full' />
15
+ </div>
16
+ );
17
+ };
18
+
19
+ export default SpinningLoader;
@@ -0,0 +1,19 @@
1
+ import { Navigate } from 'react-router-dom';
2
+ import { useAuth } from '../contexts/AuthContext';
3
+ import SpinningLoader from './Loader';
4
+
5
+ const ProtectedRoute = ({ children, fallback = null }) => {
6
+ const { isAuthenticated, isLoading } = useAuth();
7
+
8
+ if (isLoading) {
9
+ return <SpinningLoader size={50} />;
10
+ }
11
+
12
+ if (!isAuthenticated) {
13
+ return fallback || <Navigate to="/login" replace />;
14
+ }
15
+
16
+ return children;
17
+ };
18
+
19
+ export default ProtectedRoute;
@@ -0,0 +1,286 @@
1
+ import { useState } from 'react';
2
+ import { Link, useLocation } from 'react-router-dom';
3
+ import { ChevronLeft, ChevronRight, LogOut } from 'lucide-react';
4
+ import { useAuth } from '../contexts/AuthContext';
5
+ import { useTheme } from '../contexts/ThemeContext';
6
+ import { getGroupedRoutes, routeCategories } from '../config/routes';
7
+ import ClipLoader from 'react-spinners/ClipLoader'
8
+ import ThemeToggle from './ThemeToggle';
9
+
10
+ const Sidebar = ({
11
+ isCollapsed,
12
+ setIsCollapsed,
13
+ isMobileMenuOpen,
14
+ setIsMobileMenuOpen,
15
+ onLogout
16
+ }) => {
17
+ const location = useLocation();
18
+ const { handleLogout, getEmail } = useAuth();
19
+ const { isDark } = useTheme();
20
+ const groupedRoutes = getGroupedRoutes();
21
+ const [loggingOut, isLoggingOut] = useState(false)
22
+
23
+ const handleLogoutClick = async () => {
24
+ try {
25
+ isLoggingOut(true)
26
+ await handleLogout();
27
+ isLoggingOut(false)
28
+ } catch (error) {
29
+ console.error('Logout error:', error);
30
+ isLoggingOut(false)
31
+ }
32
+ };
33
+
34
+ const closeMobileMenu = () => {
35
+ setIsMobileMenuOpen(false);
36
+ };
37
+
38
+ const SidebarContent = ({ isMobile = false }) => (
39
+ <div className={`flex flex-col h-full ${isDark ? 'bg-gradient-to-b from-gray-900 via-gray-900 to-gray-800' : 'bg-white'
40
+ } transition-all duration-300 ${isMobile ? 'shadow-2xl' : 'shadow-2xl border-r'} ${isDark && !isMobile ? 'border-gray-800' : !isMobile ? 'border-gray-100' : ''
41
+ }`}>
42
+
43
+ {/* Header with Logo */}
44
+ <div className={`flex items-center ${!isMobile && isCollapsed ? 'justify-center px-4' : 'justify-between px-6'
45
+ } py-5 ${isDark ? 'border-gray-800' : 'border-gray-100'
46
+ } border-b transition-all duration-300`}>
47
+ <div className={`flex items-center justify-center w-full space-x-3 overflow-hidden transition-all duration-300 ${!isMobile && isCollapsed ? 'opacity-0 w-0' : 'opacity-100 w-auto'
48
+ }`}>
49
+ <img
50
+ src={'/pp.jpg'}
51
+ alt="Cat"
52
+ className='h-20 w-auto object-contain rounded-full'
53
+ />
54
+ </div>
55
+
56
+ {!isMobile ? (
57
+ <button
58
+ onClick={() => setIsCollapsed(!isCollapsed)}
59
+ className={`p-2.5 rounded-xl transition-all duration-200 group hover:cursor-pointer ${isDark
60
+ ? 'hover:bg-gray-800 text-gray-400 hover:text-blue-400'
61
+ : 'hover:bg-blue-50 text-gray-500 hover:text-blue-600'
62
+ }`}
63
+ >
64
+ <div className="relative">
65
+ {isCollapsed ? (
66
+ <ChevronRight className="w-5 h-5 transition-transform group-hover:translate-x-0.5" />
67
+ ) : (
68
+ <ChevronLeft className="w-5 h-5 transition-transform group-hover:-translate-x-0.5" />
69
+ )}
70
+ </div>
71
+ </button>
72
+ ) : (
73
+ <button
74
+ onClick={closeMobileMenu}
75
+ className={`p-2.5 rounded-xl transition-all duration-200 ${isDark
76
+ ? 'hover:bg-gray-800 text-gray-400 hover:text-blue-400'
77
+ : 'hover:bg-blue-50 text-gray-500 hover:text-blue-600'
78
+ }`}
79
+ >
80
+ <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
81
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
82
+ </svg>
83
+ </button>
84
+ )}
85
+ </div>
86
+
87
+ {/* User Profile Card */}
88
+ <div className={`transition-all duration-300 ${!isMobile && isCollapsed ? 'px-2 py-4' : 'px-3 py-5'
89
+ }`}>
90
+ <div className={`relative rounded-2xl transition-all duration-300 overflow-hidden ${isDark
91
+ ? 'bg-gradient-to-br from-gray-800 via-gray-800 to-gray-800/80 border border-gray-700/50'
92
+ : 'bg-gradient-to-br from-blue-50 via-white to-blue-50/50 border border-blue-100'
93
+ } ${!isMobile && isCollapsed ? 'p-2' : 'p-4'}`}>
94
+ {/* Subtle gradient overlay */}
95
+ <div className={`absolute inset-0 bg-gradient-to-br ${isDark ? 'from-blue-500/5 to-transparent' : 'from-blue-400/5 to-transparent'
96
+ } pointer-events-none`}></div>
97
+
98
+ <div className={`relative flex items-center ${!isMobile && isCollapsed ? 'justify-center' : 'space-x-3'
99
+ }`}>
100
+ <div className={`flex-shrink-0 relative ${!isMobile && isCollapsed ? 'w-8 h-8' : 'w-10 h-10'
101
+ } rounded-xl bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center font-bold text-white shadow-lg shadow-blue-500/20 transition-all duration-300`}>
102
+ <div className="absolute inset-0 rounded-xl bg-white/10"></div>
103
+ <span className={`relative ${!isMobile && isCollapsed ? 'text-sm' : 'text-base'}`}>
104
+ {getEmail()?.charAt(0).toUpperCase() || 'U'}
105
+ </span>
106
+ </div>
107
+
108
+ <div className={`flex-1 min-w-0 overflow-hidden transition-all duration-300 ${!isMobile && isCollapsed ? 'opacity-0 w-0' : 'opacity-100 w-auto'
109
+ }`}>
110
+ <div className={`font-semibold truncate text-sm ${isDark ? 'text-white' : 'text-gray-800'
111
+ }`}>
112
+ {getEmail()?.split('@')[0] || 'User'}
113
+ </div>
114
+ <div className={`text-xs truncate ${isDark ? 'text-gray-400' : 'text-gray-500'
115
+ }`}>
116
+ {getEmail() || 'user@email.com'}
117
+ </div>
118
+ </div>
119
+ </div>
120
+ </div>
121
+ </div>
122
+
123
+ {/* Navigation */}
124
+ <nav className={`flex-1 overflow-y-auto overflow-x-hidden ${!isMobile && isCollapsed ? 'px-2' : 'px-4'
125
+ } pb-4 scrollbar-thin ${isDark ? 'scrollbar-thumb-gray-700 scrollbar-track-gray-800' : 'scrollbar-thumb-gray-300 scrollbar-track-gray-100'
126
+ }`}>
127
+ <div className="space-y-8 py-2">
128
+ {Object.entries(groupedRoutes).map(([categoryKey, routes], categoryIndex) => {
129
+ const category = routeCategories[categoryKey];
130
+
131
+ return (
132
+ <div
133
+ key={categoryKey}
134
+ style={{ animationDelay: `${categoryIndex * 50}ms` }}
135
+ className="nav-item-enter"
136
+ >
137
+ {/* Category Header */}
138
+ {category.showHeader && (!isCollapsed || isMobile) && (
139
+ <div className="mb-6 px-3">
140
+ <div className="flex items-center space-x-2">
141
+ <div className={`flex-1 h-px ${isDark ? 'bg-gradient-to-r from-gray-400 to-transparent' : 'bg-gradient-to-r from-blue-900 to-transparent'
142
+ }`}></div>
143
+ <h3 className={`text-xs font-bold uppercase tracking-widest ${isDark ? 'text-white' : 'text-blue-900'
144
+ }`}>
145
+ {category.name}
146
+ </h3>
147
+ <div className={`flex-1 h-px ${isDark ? 'bg-gradient-to-l from-gray-400 to-transparent' : 'bg-gradient-to-l from-blue-900 to-transparent'
148
+ }`}></div>
149
+ </div>
150
+ </div>
151
+ )}
152
+
153
+ {/* Category Routes */}
154
+ <div className="space-y-1.5">
155
+ {routes.map((route, routeIndex) => {
156
+ const Icon = route.icon;
157
+ const isActive = location.pathname === route.path;
158
+
159
+ return (
160
+ <Link
161
+ key={route.id}
162
+ to={route.path}
163
+ onClick={isMobile ? closeMobileMenu : undefined}
164
+ style={{ animationDelay: `${(categoryIndex * routes.length + routeIndex) * 30}ms` }}
165
+ className={`group relative flex items-center transition-all duration-200 ${!isMobile && isCollapsed
166
+ ? 'justify-center p-3 mx-auto w-12 h-12'
167
+ : 'space-x-3 px-4 py-3.5'
168
+ } rounded-xl ${isActive
169
+ ? isDark
170
+ ? 'bg-gradient-to-r from-blue-600 to-indigo-600 text-white shadow-lg shadow-blue-600/25'
171
+ : 'bg-gradient-to-r from-blue-600 to-indigo-600 text-white shadow-lg shadow-blue-600/20'
172
+ : isDark
173
+ ? 'text-gray-400 hover:bg-gray-800/50 hover:text-blue-400'
174
+ : 'text-gray-600 hover:bg-blue-50 hover:text-blue-600'
175
+ } nav-item-enter`}
176
+ title={!isMobile && isCollapsed ? route.name : undefined}
177
+ >
178
+ {/* Active indicator */}
179
+ {isActive && (isMobile || !isCollapsed) && (
180
+ <div className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-8 bg-white rounded-r-full shadow-lg"></div>
181
+ )}
182
+
183
+ {/* Icon with animation */}
184
+ <div className={`relative ${isActive ? 'scale-110' : 'group-hover:scale-105'
185
+ } transition-transform duration-200`}>
186
+ <Icon
187
+ strokeWidth={isActive ? 2.5 : 2}
188
+ className="w-5 h-5 flex-shrink-0"
189
+ />
190
+ {isActive && (
191
+ <div className="absolute inset-0 animate-ping opacity-20">
192
+ <Icon className="w-5 h-5" />
193
+ </div>
194
+ )}
195
+ </div>
196
+
197
+ {/* Route name with smooth fade */}
198
+ <span className={`font-semibold text-sm truncate transition-all duration-300 ${!isMobile && isCollapsed ? 'opacity-0 w-0' : 'opacity-100 w-auto'
199
+ }`}>
200
+ {route.name}
201
+ </span>
202
+
203
+ {/* Hover effect overlay */}
204
+ {!isActive && (
205
+ <div className={`absolute inset-0 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-200 ${isDark
206
+ ? 'bg-gradient-to-r from-blue-500/5 to-indigo-500/5'
207
+ : 'bg-gradient-to-r from-blue-400/5 to-indigo-400/5'
208
+ }`}></div>
209
+ )}
210
+ </Link>
211
+ );
212
+ })}
213
+ </div>
214
+ </div>
215
+ );
216
+ })}
217
+ </div>
218
+ </nav>
219
+
220
+ {/* Footer */}
221
+ <div className={`${!isMobile && isCollapsed ? 'px-2' : 'px-6'
222
+ } py-4 space-y-3 border-t ${isDark ? 'border-gray-800 bg-gray-900/50' : 'border-gray-100 bg-gray-50/50'
223
+ } transition-all duration-300`}>
224
+ {/* Theme Toggle */}
225
+ <div className={`flex items-center ${!isMobile && isCollapsed ? 'justify-center' : 'justify-between'
226
+ } transition-all duration-300`}>
227
+ <span className={`text-sm font-medium ${isDark ? 'text-white' : 'text-gray-600'
228
+ } transition-all duration-300 ${!isMobile && isCollapsed ? 'opacity-0 w-0' : 'opacity-100 w-auto'
229
+ }`}>
230
+ Theme
231
+ </span>
232
+ <ThemeToggle size="sm" />
233
+ </div>
234
+
235
+ {/* Logout Button */}
236
+ <button
237
+ onClick={handleLogoutClick}
238
+ disabled={loggingOut}
239
+ className={`w-full flex items-center ${!isMobile && isCollapsed ? 'justify-center p-3' : 'justify-center space-x-2 px-4 py-3'
240
+ } rounded-xl font-semibold text-sm transition-all duration-200 group relative overflow-hidden ${isDark
241
+ ? 'bg-red-900/20 text-red-400 hover:bg-red-900/30 border border-red-800/50 hover:border-red-700/50'
242
+ : 'bg-red-50 text-red-600 hover:bg-red-100 border border-red-200 hover:border-red-300'
243
+ } disabled:opacity-50 disabled:cursor-not-allowed`}
244
+ title={!isMobile && isCollapsed ? "Logout" : undefined}
245
+ >
246
+ {/* Animated background on hover */}
247
+ <div className={`absolute inset-0 ${isDark ? 'bg-red-500/10' : 'bg-red-500/5'
248
+ } transform scale-x-0 group-hover:scale-x-100 transition-transform duration-300 origin-left`}></div>
249
+
250
+ <LogOut className={`w-5 h-5 flex-shrink-0 relative z-10 ${loggingOut ? 'animate-pulse' : 'group-hover:rotate-12 transition-transform duration-200'
251
+ }`} />
252
+
253
+ <span className={`relative z-10 transition-all duration-300 ${!isMobile && isCollapsed ? 'opacity-0 w-0' : 'opacity-100 w-auto'
254
+ }`}>
255
+ {loggingOut ? 'Logging out...' : 'Logout'}
256
+ </span>
257
+
258
+ {loggingOut && (isMobile || !isCollapsed) && (
259
+ <ClipLoader size={16} color={isDark ? '#f87171' : '#dc2626'} className='ml-2 relative z-10' />
260
+ )}
261
+ </button>
262
+ </div>
263
+ </div>
264
+ );
265
+
266
+
267
+ return (
268
+ <>
269
+ {/* Desktop Sidebar - Fixed with smooth transitions */}
270
+ <div
271
+ className={`hidden lg:flex lg:flex-col lg:fixed lg:inset-y-0 lg:z-50 transition-all duration-300 ease-in-out ${isCollapsed ? 'lg:w-16' : 'lg:w-64'
272
+ }`}
273
+ >
274
+ <SidebarContent isMobile={false} />
275
+ </div>
276
+
277
+ {/* Mobile Sidebar with slide animation */}
278
+ <div className={`fixed inset-y-0 left-0 z-50 w-72 transform transition-transform duration-300 ease-in-out lg:hidden ${isMobileMenuOpen ? 'translate-x-0' : '-translate-x-full'
279
+ }`}>
280
+ <SidebarContent isMobile={true} />
281
+ </div>
282
+ </>
283
+ );
284
+ };
285
+
286
+ export default Sidebar;
@@ -0,0 +1,30 @@
1
+ import React from 'react';
2
+ import { useTheme } from '../contexts/ThemeContext';
3
+ import { SunIcon, MoonIcon } from '@heroicons/react/24/outline';
4
+
5
+ const ThemeToggle = ({ size = 'md', className = '' }) => {
6
+ const { theme, toggleTheme } = useTheme();
7
+
8
+ const sizeClasses = {
9
+ sm: 'btn-sm',
10
+ md: '',
11
+ lg: 'btn-lg'
12
+ };
13
+
14
+ return (
15
+ <button
16
+ onClick={toggleTheme}
17
+ className={`btn btn-ghost cursor-pointer ${sizeClasses[size]} ${className}`}
18
+ aria-label={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
19
+ title={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
20
+ >
21
+ {theme === 'light' ? (
22
+ <MoonIcon strokeWidth={'2'} className="w-6 h-6 text-blue-500" />
23
+ ) : (
24
+ <SunIcon strokeWidth={'2'} className="w-6 h-6 text-yellow-400" />
25
+ )}
26
+ </button>
27
+ );
28
+ };
29
+
30
+ export default ThemeToggle;
@@ -0,0 +1,46 @@
1
+ import axios from "axios";
2
+
3
+ const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
4
+
5
+ // Helper function to create a client with common interceptors
6
+ function createAxiosClient(baseURL) {
7
+ const client = axios.create({
8
+ baseURL,
9
+ timeout: 10000,
10
+ headers: {
11
+ "Content-Type": "application/json",
12
+ },
13
+ });
14
+
15
+ // Request interceptor (attach token)
16
+ client.interceptors.request.use(
17
+ (config) => {
18
+ const token = localStorage.getItem("authToken");
19
+ if (token) {
20
+ config.headers.Authorization = `Bearer ${token}`;
21
+ }
22
+ return config;
23
+ },
24
+ (error) => Promise.reject(error)
25
+ );
26
+
27
+ // Response interceptor (handle 401)
28
+ client.interceptors.response.use(
29
+ (response) => response,
30
+ (error) => {
31
+ if (error.response?.status === 401) {
32
+ localStorage.removeItem("authToken");
33
+ localStorage.removeItem("user");
34
+ window.location.href = "/login";
35
+ }
36
+ return Promise.reject(error);
37
+ }
38
+ );
39
+
40
+ return client;
41
+ }
42
+
43
+ // Export separate clients
44
+ export const axiosAuthClient = createAxiosClient(`${API_BASE_URL}/auth`);
45
+ export const axiosDashboardClient = createAxiosClient(`${API_BASE_URL}/dashboard`);
46
+ export const axiosSettingsClient = createAxiosClient(`${API_BASE_URL}/settings`);
@@ -0,0 +1,11 @@
1
+ import CryptoJS from 'crypto-js';
2
+
3
+ const AES_KEY = import.meta.env.VITE_PASSWORD_ENCRYPTION_KEY;
4
+
5
+ // Encrypt plain password -> return base64-like string
6
+ export function encryptPassword(password) {
7
+ if (!AES_KEY) {
8
+ throw new Error('Encryption key not configured in VITE_PASSWORD_ENCRYPTION_KEY');
9
+ }
10
+ return CryptoJS.AES.encrypt(password, AES_KEY).toString();
11
+ }