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.
- package/README.md +390 -0
- package/index.js +78 -0
- package/lib/addDB.js +77 -0
- package/lib/addRoute.js +264 -0
- package/lib/copyProject.js +25 -0
- package/lib/dataTypes.js +79 -0
- package/lib/installDeps.js +11 -0
- package/lib/prompts.js +289 -0
- package/lib/setupExtraDB.js +172 -0
- package/lib/setupMainDB.js +9 -0
- package/lib/testDBConnection.js +31 -0
- package/lib/utils.js +39 -0
- package/package.json +45 -0
- package/template/Backend/.env +7 -0
- package/template/Backend/DB/DBInit.js +28 -0
- package/template/Backend/DB/dbConfigs.js +4 -0
- package/template/Backend/Models/index.js +54 -0
- package/template/Backend/README.md +535 -0
- package/template/Backend/middleware/authMiddleware.js +19 -0
- package/template/Backend/package-lock.json +2997 -0
- package/template/Backend/package.json +32 -0
- package/template/Backend/routes/authRoutes.js +15 -0
- package/template/Backend/routes/dashboardRoutes.js +13 -0
- package/template/Backend/routes/index.js +15 -0
- package/template/Backend/routes/settingsRoutes.js +9 -0
- package/template/Backend/server.js +70 -0
- package/template/Backend/services/authService.js +68 -0
- package/template/Backend/services/cryptoService.js +14 -0
- package/template/Backend/services/dashboardService.js +39 -0
- package/template/Backend/services/settingsService.js +43 -0
- package/template/Frontend/.env +3 -0
- package/template/Frontend/README.md +576 -0
- package/template/Frontend/eslint.config.js +29 -0
- package/template/Frontend/index.html +13 -0
- package/template/Frontend/package-lock.json +3690 -0
- package/template/Frontend/package.json +39 -0
- package/template/Frontend/public/PMDLogo.png +0 -0
- package/template/Frontend/public/pp.jpg +0 -0
- package/template/Frontend/public/tabicon.png +0 -0
- package/template/Frontend/src/App.jsx +71 -0
- package/template/Frontend/src/assets/fonts/ArticulatCFDemiBold/font.woff +0 -0
- package/template/Frontend/src/assets/fonts/ArticulatCFDemiBold/font.woff2 +0 -0
- package/template/Frontend/src/assets/fonts/ArticulatCFNormal/font.woff +0 -0
- package/template/Frontend/src/assets/fonts/ArticulatCFNormal/font.woff2 +0 -0
- package/template/Frontend/src/assets/fonts/ArticulatCFRegular/font.woff +0 -0
- package/template/Frontend/src/assets/fonts/ArticulatCFRegular/font.woff2 +0 -0
- package/template/Frontend/src/assets/fonts/MixtaProRegularItalic/font.woff +0 -0
- package/template/Frontend/src/assets/fonts/MixtaProRegularItalic/font.woff2 +0 -0
- package/template/Frontend/src/assets/fonts/fonts_sohne/OTF/S/303/266hneMono-Buch.otf +0 -0
- package/template/Frontend/src/assets/fonts/fonts_sohne/OTF/S/303/266hneMono-Leicht.otf +0 -0
- package/template/Frontend/src/assets/fonts/fonts_sohne/WOFF2/soehne-mono-buch.woff2 +0 -0
- package/template/Frontend/src/assets/fonts/fonts_sohne/WOFF2/soehne-mono-leicht.woff2 +0 -0
- package/template/Frontend/src/components/Layout.jsx +61 -0
- package/template/Frontend/src/components/Loader.jsx +19 -0
- package/template/Frontend/src/components/ProtectedRoute.jsx +19 -0
- package/template/Frontend/src/components/Sidebar.jsx +286 -0
- package/template/Frontend/src/components/ThemeToggle.jsx +30 -0
- package/template/Frontend/src/config/axiosClient.js +46 -0
- package/template/Frontend/src/config/encryption.js +11 -0
- package/template/Frontend/src/config/routes.js +65 -0
- package/template/Frontend/src/contexts/AuthContext.jsx +144 -0
- package/template/Frontend/src/contexts/ThemeContext.jsx +69 -0
- package/template/Frontend/src/index.css +88 -0
- package/template/Frontend/src/main.jsx +11 -0
- package/template/Frontend/src/pages/Dashboard.jsx +137 -0
- package/template/Frontend/src/pages/Login.jsx +195 -0
- package/template/Frontend/src/pages/NotFound.jsx +70 -0
- package/template/Frontend/src/pages/Settings.jsx +69 -0
- package/template/Frontend/tailwind.config.js +90 -0
- package/template/Frontend/vite.config.js +37 -0
- package/template/Readme.md +0 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
|
|
2
|
+
import {
|
|
3
|
+
Network,
|
|
4
|
+
Settings
|
|
5
|
+
} from 'lucide-react';
|
|
6
|
+
|
|
7
|
+
export const sidebarRoutes = [
|
|
8
|
+
{
|
|
9
|
+
id: 'dashboard',
|
|
10
|
+
name: 'Dashboard',
|
|
11
|
+
path: '/dashboard',
|
|
12
|
+
icon: Network,
|
|
13
|
+
category: 'monitoring'
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: 'settings',
|
|
17
|
+
name: 'Settings',
|
|
18
|
+
path: '/settings',
|
|
19
|
+
icon: Settings,
|
|
20
|
+
category: 'account'
|
|
21
|
+
}
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
// Category configurations for future expansion
|
|
25
|
+
export const routeCategories = {
|
|
26
|
+
monitoring: {
|
|
27
|
+
name: 'Monitoring',
|
|
28
|
+
order: 1,
|
|
29
|
+
showHeader: true
|
|
30
|
+
},
|
|
31
|
+
account: {
|
|
32
|
+
name: 'Account',
|
|
33
|
+
order: 2,
|
|
34
|
+
showHeader: true
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Get routes grouped by category
|
|
39
|
+
export const getGroupedRoutes = () => {
|
|
40
|
+
const grouped = {};
|
|
41
|
+
|
|
42
|
+
sidebarRoutes.forEach(route => {
|
|
43
|
+
if (!grouped[route.category]) {
|
|
44
|
+
grouped[route.category] = [];
|
|
45
|
+
}
|
|
46
|
+
grouped[route.category].push(route);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Sort categories by order
|
|
50
|
+
const sortedCategories = Object.keys(grouped).sort((a, b) => {
|
|
51
|
+
return routeCategories[a].order - routeCategories[b].order;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const result = {};
|
|
55
|
+
sortedCategories.forEach(category => {
|
|
56
|
+
result[category] = grouped[category];
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return result;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Find route by path
|
|
63
|
+
export const findRouteByPath = (path) => {
|
|
64
|
+
return sidebarRoutes.find(route => route.path === path);
|
|
65
|
+
};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { createContext, useContext, useState, useEffect } from 'react';
|
|
2
|
+
import { axiosAuthClient } from '../config/axiosClient';
|
|
3
|
+
import { encryptPassword } from '../config/encryption';
|
|
4
|
+
|
|
5
|
+
const AuthContext = createContext();
|
|
6
|
+
|
|
7
|
+
export const useAuth = () => {
|
|
8
|
+
const context = useContext(AuthContext);
|
|
9
|
+
if (!context) {
|
|
10
|
+
throw new Error('useAuth must be used within an AuthProvider');
|
|
11
|
+
}
|
|
12
|
+
return context;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const AuthProvider = ({ children }) => {
|
|
16
|
+
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
|
17
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
18
|
+
const [user, setUser] = useState(null);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
checkAuthState();
|
|
22
|
+
}, []);
|
|
23
|
+
|
|
24
|
+
const checkAuthState = async () => {
|
|
25
|
+
try {
|
|
26
|
+
setIsLoading(true);
|
|
27
|
+
const token = localStorage.getItem('authToken');
|
|
28
|
+
const storedUser = localStorage.getItem('user');
|
|
29
|
+
|
|
30
|
+
if (token && storedUser) {
|
|
31
|
+
// Verify token is still valid by fetching current user
|
|
32
|
+
try {
|
|
33
|
+
const response = await axiosAuthClient.get('/me');
|
|
34
|
+
const userData = response.data.user;
|
|
35
|
+
|
|
36
|
+
setUser(userData);
|
|
37
|
+
setIsAuthenticated(true);
|
|
38
|
+
|
|
39
|
+
// Update stored user data
|
|
40
|
+
localStorage.setItem('user', JSON.stringify(userData));
|
|
41
|
+
} catch (error) {
|
|
42
|
+
// Token is invalid or expired
|
|
43
|
+
console.error('Token verification failed:', error);
|
|
44
|
+
handleLogout();
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
setIsAuthenticated(false);
|
|
48
|
+
setUser(null);
|
|
49
|
+
}
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error('Auth state check failed:', error);
|
|
52
|
+
setIsAuthenticated(false);
|
|
53
|
+
setUser(null);
|
|
54
|
+
} finally {
|
|
55
|
+
setIsLoading(false);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const handleLogin = async (email, password) => {
|
|
60
|
+
try {
|
|
61
|
+
const response = await axiosAuthClient.post('/login', {
|
|
62
|
+
email,
|
|
63
|
+
password: encryptPassword(password)
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const { token, user: userData } = response.data;
|
|
67
|
+
|
|
68
|
+
// Store token and user data
|
|
69
|
+
localStorage.setItem('authToken', token);
|
|
70
|
+
localStorage.setItem('user', JSON.stringify(userData));
|
|
71
|
+
|
|
72
|
+
setUser(userData);
|
|
73
|
+
setIsAuthenticated(true);
|
|
74
|
+
|
|
75
|
+
return { success: true };
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error('Login error:', error);
|
|
78
|
+
|
|
79
|
+
let errorMessage = 'Failed to sign in. Please try again.';
|
|
80
|
+
|
|
81
|
+
if (error.response) {
|
|
82
|
+
errorMessage = error.response.data.message || errorMessage;
|
|
83
|
+
} else if (error.request) {
|
|
84
|
+
errorMessage = 'Network error. Please check your connection.';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { success: false, error: errorMessage };
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const handleLogout = async () => {
|
|
92
|
+
try {
|
|
93
|
+
// Optional: Call logout endpoint
|
|
94
|
+
await axiosAuthClient.post('/logout').catch(() => {
|
|
95
|
+
// Ignore errors on logout
|
|
96
|
+
});
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error('Logout error:', error);
|
|
99
|
+
} finally {
|
|
100
|
+
// Clear local storage and state
|
|
101
|
+
localStorage.removeItem('authToken');
|
|
102
|
+
localStorage.removeItem('user');
|
|
103
|
+
setIsAuthenticated(false);
|
|
104
|
+
setUser(null);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const refreshUserData = async () => {
|
|
109
|
+
if (isAuthenticated) {
|
|
110
|
+
await checkAuthState();
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const getEmail = () => {
|
|
115
|
+
if (!user) return null;
|
|
116
|
+
return user.email || null;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const getToken = () => {
|
|
120
|
+
return localStorage.getItem('authToken');
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const value = {
|
|
124
|
+
isAuthenticated,
|
|
125
|
+
isLoading,
|
|
126
|
+
|
|
127
|
+
// User data
|
|
128
|
+
user,
|
|
129
|
+
getEmail,
|
|
130
|
+
getToken,
|
|
131
|
+
|
|
132
|
+
// Actions
|
|
133
|
+
handleLogin,
|
|
134
|
+
handleLogout,
|
|
135
|
+
refreshUserData,
|
|
136
|
+
checkAuthState
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<AuthContext.Provider value={value}>
|
|
141
|
+
{children}
|
|
142
|
+
</AuthContext.Provider>
|
|
143
|
+
);
|
|
144
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createContext, useContext, useState, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
const ThemeContext = createContext();
|
|
4
|
+
|
|
5
|
+
export const useTheme = () => {
|
|
6
|
+
const context = useContext(ThemeContext);
|
|
7
|
+
if (!context) {
|
|
8
|
+
throw new Error('useTheme must be used within a ThemeProvider');
|
|
9
|
+
}
|
|
10
|
+
return context;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const ThemeProvider = ({ children }) => {
|
|
14
|
+
const [theme, setTheme] = useState('light');
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
// Check for saved theme in localStorage
|
|
18
|
+
const savedTheme = localStorage.getItem('theme');
|
|
19
|
+
|
|
20
|
+
// Check for system preference if no saved theme
|
|
21
|
+
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
22
|
+
|
|
23
|
+
const initialTheme = savedTheme || systemTheme;
|
|
24
|
+
setTheme(initialTheme);
|
|
25
|
+
applyTheme(initialTheme);
|
|
26
|
+
|
|
27
|
+
// Listen for system theme changes
|
|
28
|
+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
29
|
+
const handleSystemThemeChange = (e) => {
|
|
30
|
+
if (!localStorage.getItem('theme')) {
|
|
31
|
+
const newTheme = e.matches ? 'dark' : 'light';
|
|
32
|
+
setTheme(newTheme);
|
|
33
|
+
applyTheme(newTheme);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
mediaQuery.addListener(handleSystemThemeChange);
|
|
38
|
+
return () => mediaQuery.removeListener(handleSystemThemeChange);
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
const applyTheme = (newTheme) => {
|
|
42
|
+
document.documentElement.setAttribute('data-theme', newTheme);
|
|
43
|
+
document.documentElement.classList.toggle('dark', newTheme === 'dark');
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const toggleTheme = () => {
|
|
47
|
+
const newTheme = theme === 'light' ? 'dark' : 'light';
|
|
48
|
+
setTheme(newTheme);
|
|
49
|
+
localStorage.setItem('theme', newTheme);
|
|
50
|
+
applyTheme(newTheme);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const setSpecificTheme = (newTheme) => {
|
|
54
|
+
setTheme(newTheme);
|
|
55
|
+
localStorage.setItem('theme', newTheme);
|
|
56
|
+
applyTheme(newTheme);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<ThemeContext.Provider value={{
|
|
61
|
+
theme,
|
|
62
|
+
toggleTheme,
|
|
63
|
+
setTheme: setSpecificTheme,
|
|
64
|
+
isDark: theme === 'dark'
|
|
65
|
+
}}>
|
|
66
|
+
{children}
|
|
67
|
+
</ThemeContext.Provider>
|
|
68
|
+
);
|
|
69
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
/* Custom font faces */
|
|
4
|
+
@font-face {
|
|
5
|
+
font-family: "Articulat CF";
|
|
6
|
+
src: url("./assets/fonts/ArticulatCFRegular/font.woff2") format("woff2"),
|
|
7
|
+
url("./assets/fonts/ArticulatCFRegular/font.woff") format("woff");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
@font-face {
|
|
11
|
+
font-family: "ArticulatCFDemiBold";
|
|
12
|
+
src: url("./assets/fonts/ArticulatCFDemiBold/font.woff2") format("woff2"),
|
|
13
|
+
url("./assets/fonts/ArticulatCFDemiBold/font.woff") format("woff");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@font-face {
|
|
17
|
+
font-family: "Mixta Pro";
|
|
18
|
+
src: url("./assets/fonts/MixtaProRegularItalic/font.woff2") format("woff2"),
|
|
19
|
+
url("./assets/fonts/MixtaProRegularItalic/font.woff") format("woff");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@font-face {
|
|
23
|
+
font-family: "ArticulatCFNormal";
|
|
24
|
+
src: url("./assets/fonts/ArticulatCFNormal/font.woff2") format("woff2"),
|
|
25
|
+
url("./assets/fonts/ArticulatCFNormal/font.woff") format("woff");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@font-face {
|
|
29
|
+
font-family: "SoehneMonoBuch";
|
|
30
|
+
src: url("./assets/fonts/fonts_sohne/WOFF2/soehne-mono-buch.woff2") format("woff2"),
|
|
31
|
+
url("./assets/fonts/fonts_sohne/OTF/SöhneMono-Buch.otf") format("otf");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@font-face {
|
|
35
|
+
font-family: "SoehneMonoLeicht";
|
|
36
|
+
src: url("./assets/fonts/fonts_sohne/WOFF2/soehne-mono-leicht.woff2") format("woff2"),
|
|
37
|
+
url("./assets/fonts/fonts_sohne/OTF/SöhneMono-Leicht.otf") format("otf");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@theme {
|
|
41
|
+
--font-articulat-cf: "Articulat CF", sans-serif;
|
|
42
|
+
--font-articulatcf-demibold: "ArticulatCFDemiBold", sans-serif;
|
|
43
|
+
--font-articulat-normal: "ArticulatCFNormal", sans-serif;
|
|
44
|
+
--font-mixta-pro: "Mixta Pro", sans-serif;
|
|
45
|
+
--font-sohne-mono-buch: "SoehneMonoBuch", monospace;
|
|
46
|
+
--font-sohne-mono-leicht: "SoehneMonoLeicht", monospace;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* Remove conflicting base styles that interfere with Tailwind/DaisyUI */
|
|
50
|
+
* {
|
|
51
|
+
box-sizing: border-box;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
body {
|
|
55
|
+
margin: 0;
|
|
56
|
+
min-height: 100vh;
|
|
57
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#root {
|
|
61
|
+
min-height: 100vh;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* Theme transition effects */
|
|
65
|
+
* {
|
|
66
|
+
transition: background-color 200ms ease-in-out,
|
|
67
|
+
border-color 200ms ease-in-out,
|
|
68
|
+
color 200ms ease-in-out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* Custom scrollbar for webkit browsers */
|
|
72
|
+
::-webkit-scrollbar {
|
|
73
|
+
width: 6px;
|
|
74
|
+
height: 6px;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
::-webkit-scrollbar-track {
|
|
78
|
+
background: hsl(var(--b2, 210 40% 98%));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
::-webkit-scrollbar-thumb {
|
|
82
|
+
background: hsl(var(--b3, 210 40% 94%));
|
|
83
|
+
border-radius: 3px;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
::-webkit-scrollbar-thumb:hover {
|
|
87
|
+
background: hsl(var(--bc, 215 28% 17%) / 0.5);
|
|
88
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { useTheme } from '../contexts/ThemeContext';
|
|
3
|
+
import { axiosDashboardClient } from '../config/axiosClient';
|
|
4
|
+
import SpinningLoader from '../components/Loader';
|
|
5
|
+
|
|
6
|
+
const Dashboard = () => {
|
|
7
|
+
const { isDark } = useTheme();
|
|
8
|
+
const [sections, setSections] = useState([]);
|
|
9
|
+
const [loading, setLoading] = useState(false);
|
|
10
|
+
const [error, setError] = useState(null);
|
|
11
|
+
|
|
12
|
+
const fetchDashboardInfo = async () => {
|
|
13
|
+
setLoading(true);
|
|
14
|
+
try {
|
|
15
|
+
const response = await axiosDashboardClient.get('getDashboardHealth');
|
|
16
|
+
setSections(response.data);
|
|
17
|
+
} catch (err) {
|
|
18
|
+
setError(err.message);
|
|
19
|
+
} finally {
|
|
20
|
+
setLoading(false);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
fetchDashboardInfo();
|
|
26
|
+
}, []);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className={`min-h-screen transition-colors duration-300
|
|
30
|
+
${isDark ? "bg-[#0a0a0a]" : "bg-[#fafafa]"}`}>
|
|
31
|
+
|
|
32
|
+
{/* Content Container */}
|
|
33
|
+
<div className="max-w-6xl mx-auto py-12 px-4 sm:px-6 lg:px-8">
|
|
34
|
+
|
|
35
|
+
{/* Header */}
|
|
36
|
+
<div className="mb-12">
|
|
37
|
+
<h1 className={`text-4xl sm:text-5xl font-semibold mb-3 tracking-tight
|
|
38
|
+
${isDark ? "text-white" : "text-gray-900"}`}
|
|
39
|
+
style={{ fontFamily: "'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif" }}>
|
|
40
|
+
Dashboard Overview
|
|
41
|
+
</h1>
|
|
42
|
+
<p className={`text-lg ${isDark ? "text-gray-400" : "text-gray-600"}`}>
|
|
43
|
+
System architecture and configuration
|
|
44
|
+
</p>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
{/* Loading State */}
|
|
48
|
+
{loading && (
|
|
49
|
+
<div className="flex justify-center items-center py-20">
|
|
50
|
+
<SpinningLoader size={50} />
|
|
51
|
+
</div>
|
|
52
|
+
)}
|
|
53
|
+
|
|
54
|
+
{/* Error State */}
|
|
55
|
+
{error && (
|
|
56
|
+
<div className={`p-4 rounded-xl border ${isDark
|
|
57
|
+
? "bg-red-900/20 border-red-800/50 text-red-400"
|
|
58
|
+
: "bg-red-50 border-red-200 text-red-700"}`}>
|
|
59
|
+
<p className="font-medium">Error: {error}</p>
|
|
60
|
+
</div>
|
|
61
|
+
)}
|
|
62
|
+
|
|
63
|
+
{/* Dashboard Cards */}
|
|
64
|
+
{!loading && !error && (
|
|
65
|
+
<div className="space-y-6">
|
|
66
|
+
{sections.map((section, index) => (
|
|
67
|
+
<div
|
|
68
|
+
key={index}
|
|
69
|
+
className={`group rounded-2xl transition-all duration-300 overflow-hidden
|
|
70
|
+
${isDark
|
|
71
|
+
? "bg-[#141414] border border-gray-800/50 hover:border-gray-700/80"
|
|
72
|
+
: "bg-white border border-gray-200 hover:border-gray-300 shadow-sm hover:shadow-md"}`}
|
|
73
|
+
style={{
|
|
74
|
+
animation: `fadeInUp 0.5s ease-out ${index * 0.1}s both`
|
|
75
|
+
}}>
|
|
76
|
+
|
|
77
|
+
{/* Card Header */}
|
|
78
|
+
<div className={`px-8 py-6 border-b ${isDark ? "border-gray-800/50" : "border-gray-100"}`}>
|
|
79
|
+
<div className="flex items-center gap-4">
|
|
80
|
+
<div className={`flex-shrink-0 w-12 h-12 flex items-center justify-center rounded-xl text-2xl
|
|
81
|
+
${isDark ? "bg-gray-800/80" : "bg-gray-100"}`}>
|
|
82
|
+
{section.icon}
|
|
83
|
+
</div>
|
|
84
|
+
<h2 className={`text-xl font-semibold ${isDark ? "text-gray-100" : "text-gray-900"}`}
|
|
85
|
+
style={{ fontFamily: "'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif" }}>
|
|
86
|
+
{section.title}
|
|
87
|
+
</h2>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
{/* Card Content */}
|
|
92
|
+
<div className="px-8 py-6">
|
|
93
|
+
<ul className="space-y-4">
|
|
94
|
+
{section.items.map((item, i) => (
|
|
95
|
+
<li
|
|
96
|
+
key={i}
|
|
97
|
+
className="flex items-start gap-3 group/item">
|
|
98
|
+
<div className={`flex-shrink-0 w-1.5 h-1.5 rounded-full mt-2.5 transition-all duration-200
|
|
99
|
+
${isDark
|
|
100
|
+
? "bg-gray-600 group-hover/item:bg-blue-500"
|
|
101
|
+
: "bg-gray-400 group-hover/item:bg-blue-600"}`}>
|
|
102
|
+
</div>
|
|
103
|
+
<span className={`text-[15px] leading-relaxed transition-colors duration-200
|
|
104
|
+
${isDark
|
|
105
|
+
? "text-gray-400 group-hover/item:text-gray-300"
|
|
106
|
+
: "text-gray-600 group-hover/item:text-gray-900"}`}
|
|
107
|
+
style={{ fontFamily: "'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif" }}>
|
|
108
|
+
{item}
|
|
109
|
+
</span>
|
|
110
|
+
</li>
|
|
111
|
+
))}
|
|
112
|
+
</ul>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
))}
|
|
116
|
+
</div>
|
|
117
|
+
)}
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
{/* Minimal Animations */}
|
|
121
|
+
<style>{`
|
|
122
|
+
@keyframes fadeInUp {
|
|
123
|
+
from {
|
|
124
|
+
opacity: 0;
|
|
125
|
+
transform: translateY(20px);
|
|
126
|
+
}
|
|
127
|
+
to {
|
|
128
|
+
opacity: 1;
|
|
129
|
+
transform: translateY(0);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
`}</style>
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export default Dashboard;
|