Compare commits
95 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fa19dbe504 | |||
| 29d40c6f10 | |||
| 9133928401 | |||
| 5e40b3105c | |||
| 354e2844d1 | |||
| 597d7ffb6f | |||
| 7072a21ed8 | |||
| 754b023435 | |||
| ac57b932c2 | |||
| e607310689 | |||
| 1f8906bc3d | |||
| d6ddf12c04 | |||
| 994263dc45 | |||
| 23d46e7c40 | |||
| d8e0bdc814 | |||
| f45a2525e6 | |||
| 5852fc6616 | |||
| aaf9afdc9f | |||
| 98a6df5081 | |||
| 2c3379026b | |||
| 84be8e5828 | |||
| 1a5de83d50 | |||
| 476b09499a | |||
| 974462e983 | |||
| 749f04c2c4 | |||
| 7de4e85a38 | |||
| 4266adc853 | |||
| f7be617e1d | |||
| 4dd2ba9d9d | |||
| f8209c3712 | |||
| d776b80fe0 | |||
| dd89d8388e | |||
| f5ec13701b | |||
| bbdbf0176e | |||
| 2fc90639f7 | |||
| 5cf3d8fe70 | |||
| e3d802eeef | |||
| ca59231480 | |||
| 1d0ece6940 | |||
| 5fcc7fdbf1 | |||
| 3b12557a89 | |||
| 666e4e67ad | |||
| 11f4759e78 | |||
| de4a05a6e6 | |||
| b682f3378c | |||
| 56891182d5 | |||
| d25d34b8cd | |||
| 016cb6671a | |||
| 9313c01637 | |||
| 96b1446292 | |||
| 14b1459767 | |||
| e2cc586584 | |||
| c71025d717 | |||
| e8aeecec43 | |||
| 4e63ae529e | |||
| a8c01b9cb2 | |||
| b6579f511d | |||
| db46b366ab | |||
| efa732f347 | |||
| a09a68ed39 | |||
| 81f7660768 | |||
| a53a91caeb | |||
| 841a1713d2 | |||
| 737495d186 | |||
| 6ed9b94d3a | |||
| 95336e4350 | |||
| 8a2b4a2ac5 | |||
| cf0b51eb7d | |||
| 3875d275e0 | |||
| 3f68c6e39a | |||
| 53f1b55157 | |||
| dc9b92bdff | |||
| 6e7ec194af | |||
| 3003084112 | |||
| 4592daff36 | |||
| 3c16c4d8e4 | |||
| 959af0cdd4 | |||
| 777ea4d74f | |||
| a9228a2557 | |||
| 2b745e3d78 | |||
| eada55b8d7 | |||
| b1f5ac03f6 | |||
| 2c78ab966d | |||
| cd003a12f8 | |||
| 752f8170ba | |||
| be1890582c | |||
| b74d49a836 | |||
| cba236b95f | |||
| 040cd4d3cb | |||
| 3649d2ee04 | |||
| 0a6d17c64e | |||
| 7a69dd0821 | |||
| 4053db4a3e | |||
| 35d64d6765 | |||
| 6000641e50 |
@@ -15,3 +15,23 @@ devscripts
|
|||||||
backend/data/stackpulse.db
|
backend/data/stackpulse.db
|
||||||
backend/data/stackpulse.db-shm
|
backend/data/stackpulse.db-shm
|
||||||
backend/data/stackpulse.db-wal
|
backend/data/stackpulse.db-wal
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
pnpm-lock.yaml
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
!.vscode/extensions.json
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
@@ -22,6 +22,9 @@ RUN npm run build
|
|||||||
# ===============================
|
# ===============================
|
||||||
FROM node:20-alpine AS runtime
|
FROM node:20-alpine AS runtime
|
||||||
|
|
||||||
|
#Install SSH-Client
|
||||||
|
RUN apk add --no-cache openssh-client
|
||||||
|
|
||||||
# Arbeitsverzeichnis
|
# Arbeitsverzeichnis
|
||||||
WORKDIR /app/backend
|
WORKDIR /app/backend
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
# 📦 StackPulse
|

|
||||||
|
|
||||||
|
# 📦 StackPulse 
|
||||||
|
|
||||||
**StackPulse** ist eine kleine Web-App, die über die Portainer-API deine Docker-Stacks verwaltet und aktualisiert.
|
**StackPulse** ist eine kleine Web-App, die über die Portainer-API deine Docker-Stacks verwaltet und aktualisiert.
|
||||||
|
Aktuell funktioniert StackPulse nur mit der Business-Edition von Portainer. Die Communitiy-Edition wird in einem späteren Release implementiert!
|
||||||
Sie besteht aus einem **Backend (Node.js/Express)** und einem **Frontend (React/Tailwind)**.
|
Sie besteht aus einem **Backend (Node.js/Express)** und einem **Frontend (React/Tailwind)**.
|
||||||
|
|
||||||
Ziel:
|
Ziel:
|
||||||
@@ -13,7 +16,7 @@ Ziel:
|
|||||||
## 🚀 Features & Roadmap
|
## 🚀 Features & Roadmap
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>✅ v0.1.0 – Initial Release</summary>
|
<summary>✅ v0.1 – Initial Release</summary>
|
||||||
|
|
||||||
- Projektstruktur mit Frontend & Backend
|
- Projektstruktur mit Frontend & Backend
|
||||||
- Lokales Startskript (`scripts/start-dev.sh`)
|
- Lokales Startskript (`scripts/start-dev.sh`)
|
||||||
@@ -24,8 +27,8 @@ Ziel:
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details open>
|
<details>
|
||||||
<summary>🟡 v0.2.0 – In Entwicklung</summary>
|
<summary>✅ v0.2 – Release</summary>
|
||||||
|
|
||||||
### Backend
|
### Backend
|
||||||
- [x] Anbindung einer SQLite-Datenbank
|
- [x] Anbindung einer SQLite-Datenbank
|
||||||
@@ -45,15 +48,55 @@ Ziel:
|
|||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>🔮 Geplante Features (v0.3+)</summary>
|
<summary>✅ v0.3 – Release</summary>
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- [x] Automatische Datenbereinigung: Duplikate bei Stack-IDs erkennen & entfernen
|
||||||
|
- [x] Erweiterung der Logs
|
||||||
|
- [x] API für Filter & Suche (Stacks nach Name/Status abrufen)
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- [x] Filter: Stacks nach Name oder Status durchsuchen
|
||||||
|
- [x] Benachrichtigungen im UI: erfolgreicher/fehlgeschlagener Redeploy (Toast)
|
||||||
|
- [x] Visualisierung der Datenbereinigung (Konflikt/Auto-Fix Meldungen)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- [x] Frontend-Filter für schnellere Navigation bei vielen Stacks
|
||||||
|
- [x] Echtzeit-Feedback im UI (Notifications)
|
||||||
|
- [x] Datenkonsistenz sichern: keine doppelten Stack-IDs mehr
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details open>
|
||||||
|
<summary>✅ v0.4 – Release</summary>
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- keine Änderungen
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- [x] GUI komplett überarbeitet
|
||||||
|
- [x] Umstellung auf Material Tailwind
|
||||||
|
- [x] Dashboard von Creative Tim als neues Frontend
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- [x] Neue, übersichtliche Oberfläche im Material Design
|
||||||
|
- [x] leicht erweiterbar durch vorgefertigte Komponenten
|
||||||
|
- [x] kleine Bugfixes
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details open>
|
||||||
|
<summary>🔮 Geplante Features (v0.5+)</summary>
|
||||||
|
|
||||||
- Notifications (z. B. via Webhooks oder Mail)
|
- Notifications (z. B. via Webhooks oder Mail)
|
||||||
- Authentifizierung & Benutzerverwaltung
|
- Authentifizierung & Benutzerverwaltung
|
||||||
- Monitoring (Status, CPU/RAM)
|
- Monitoring (Status, CPU/RAM)
|
||||||
- Verbesserte UI/UX
|
- Export/Import von Scheduler-Jobs (z. B. JSON)
|
||||||
|
- Integration Community Edition
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -62,6 +105,8 @@ Ziel:
|
|||||||
```bash
|
```bash
|
||||||
stackpulse/
|
stackpulse/
|
||||||
├── backend/ # Node.js Backend mit Express
|
├── backend/ # Node.js Backend mit Express
|
||||||
|
│ └── data # SQlite Datenbank
|
||||||
|
│ └── db # Datenbank-Integration
|
||||||
├── frontend/ # React Frontend mit Tailwind
|
├── frontend/ # React Frontend mit Tailwind
|
||||||
├── scripts/ # Lokale Hilfsskripte (nicht Teil des Images)
|
├── scripts/ # Lokale Hilfsskripte (nicht Teil des Images)
|
||||||
│ └── start-dev.sh #Skript für den lokalen Start
|
│ └── start-dev.sh #Skript für den lokalen Start
|
||||||
@@ -95,12 +140,32 @@ services:
|
|||||||
image: ghcr.io/mboehmlaender/stackpulse
|
image: ghcr.io/mboehmlaender/stackpulse
|
||||||
ports:
|
ports:
|
||||||
- '4001:4001'
|
- '4001:4001'
|
||||||
|
volumes:
|
||||||
|
- stackpulse_data:/app/backend/data
|
||||||
environment:
|
environment:
|
||||||
- PORTAINER_URL=Deine_Portainer_Adresse/
|
PORTAINER_URL: "Your_Portainer_Server_Address"
|
||||||
- PORTAINER_API_KEY=Dein_Portainer_API_Key
|
PORTAINER_API_KEY: "Your_Portainer_API_Key"
|
||||||
- PORTAINER_ENDPOINT_ID=Deine_Portainer_Endpoint_ID
|
PORTAINER_ENDPOINT_ID: "Your_Portainer_Endpoint_ID"
|
||||||
|
SELF_STACK_ID: "Stackpulse ID"
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
stackpulse_data:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Die PORTAINER_ENDPOINT_ID erhältst du, wenn du die die URL im Browser ansiehst, wenn du das Dashboard in Portainer öffnest:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Die 3 wäre in diesem Fall Endpoint-ID.
|
||||||
|
|
||||||
|
Die STACK_SELF_ID findest du, wenn du das Frontend von StackPulse öffnest:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Diese ID kann erst nach dem Deploy von Stackpulse ausgelesen werden. Vergiss daher nicht, nach dem Hinterlegen der ID in den Variablen das Stack noch einmal zu redeployen!
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📋 Voraussetzungen
|
## 📋 Voraussetzungen
|
||||||
|
|||||||
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
@@ -13,6 +13,8 @@ CREATE TABLE IF NOT EXISTS redeploy_logs (
|
|||||||
);
|
);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
db.exec(createRedeployLogsTable);
|
db.exec(createRedeployLogsTable);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -28,4 +30,17 @@ try {
|
|||||||
|
|
||||||
console.log('✅ redeploy_logs table ready');
|
console.log('✅ redeploy_logs table ready');
|
||||||
|
|
||||||
|
const createSettingsTable = `
|
||||||
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT,
|
||||||
|
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
`;
|
||||||
|
|
||||||
|
db.exec(createSettingsTable);
|
||||||
|
|
||||||
|
console.log('✅ settings table ready');
|
||||||
|
|
||||||
|
|
||||||
db.close();
|
db.close();
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { db } from './index.js';
|
||||||
|
|
||||||
|
const getSettingStmt = db.prepare('SELECT key, value, updated_at FROM settings WHERE key = ?');
|
||||||
|
const setSettingStmt = db.prepare(`
|
||||||
|
INSERT INTO settings (key, value, updated_at)
|
||||||
|
VALUES (@key, @value, CURRENT_TIMESTAMP)
|
||||||
|
ON CONFLICT(key) DO UPDATE SET
|
||||||
|
value = excluded.value,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
`);
|
||||||
|
const deleteSettingStmt = db.prepare('DELETE FROM settings WHERE key = ?');
|
||||||
|
|
||||||
|
export function getSetting(key) {
|
||||||
|
try {
|
||||||
|
return getSettingStmt.get(key) || null;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`❌ [Settings] Fehler beim Lesen des Settings "${key}":`, err.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setSetting(key, value) {
|
||||||
|
try {
|
||||||
|
setSettingStmt.run({ key, value });
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`❌ [Settings] Fehler beim Speichern des Settings "${key}":`, err.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteSetting(key) {
|
||||||
|
try {
|
||||||
|
deleteSettingStmt.run(key);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`❌ [Settings] Fehler beim Löschen des Settings "${key}":`, err.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getJsonSetting(key, defaultValue = null) {
|
||||||
|
const row = getSetting(key);
|
||||||
|
if (!row || row.value === null || row.value === undefined) return defaultValue;
|
||||||
|
try {
|
||||||
|
return JSON.parse(row.value);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`⚠️ [Settings] Konnte JSON für Setting "${key}" nicht parsen:`, err.message);
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setJsonSetting(key, value) {
|
||||||
|
try {
|
||||||
|
const payload = value === undefined ? null : JSON.stringify(value);
|
||||||
|
return setSetting(key, payload);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`❌ [Settings] Fehler beim Serialisieren von Setting "${key}":`, err.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { getJsonSetting, setJsonSetting } from '../db/settings.js';
|
||||||
|
|
||||||
|
const MAINTENANCE_KEY = 'maintenance_mode';
|
||||||
|
|
||||||
|
const DEFAULT_STATE = {
|
||||||
|
active: false,
|
||||||
|
message: null,
|
||||||
|
activatedAt: null,
|
||||||
|
updatedAt: null,
|
||||||
|
extra: null
|
||||||
|
};
|
||||||
|
|
||||||
|
let maintenanceState = loadState();
|
||||||
|
|
||||||
|
function loadState() {
|
||||||
|
const saved = getJsonSetting(MAINTENANCE_KEY, null);
|
||||||
|
if (!saved) {
|
||||||
|
const initial = { ...DEFAULT_STATE, updatedAt: new Date().toISOString() };
|
||||||
|
setJsonSetting(MAINTENANCE_KEY, initial);
|
||||||
|
return initial;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...DEFAULT_STATE,
|
||||||
|
...saved
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistState(state) {
|
||||||
|
maintenanceState = {
|
||||||
|
...DEFAULT_STATE,
|
||||||
|
...state,
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
setJsonSetting(MAINTENANCE_KEY, maintenanceState);
|
||||||
|
return maintenanceState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMaintenanceState() {
|
||||||
|
return { ...maintenanceState };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMaintenanceModeActive() {
|
||||||
|
return Boolean(maintenanceState?.active);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function activateMaintenanceMode({ message = null, extra = null } = {}) {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
return persistState({
|
||||||
|
active: true,
|
||||||
|
message,
|
||||||
|
extra,
|
||||||
|
activatedAt: now
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deactivateMaintenanceMode({ message = null } = {}) {
|
||||||
|
return persistState({
|
||||||
|
active: false,
|
||||||
|
message,
|
||||||
|
extra: null,
|
||||||
|
activatedAt: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 156 KiB |
@@ -1,12 +1,39 @@
|
|||||||
|
<!--
|
||||||
|
=========================================================
|
||||||
|
* Material Tailwind Dashboard React - v2.1.0
|
||||||
|
=========================================================
|
||||||
|
* Product Page: https://www.creative-tim.com/product/material-tailwind-dashboard-react
|
||||||
|
* Copyright 2023 Creative Tim (https://www.creative-tim.com)
|
||||||
|
* Licensed under MIT (https://github.com/creativetimofficial/material-tailwind-dashboard-react/blob/main/LICENSE.md)
|
||||||
|
* Coded by Creative Tim
|
||||||
|
=========================================================
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
-->
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/img/favicon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>StackPulse</title>
|
<title>Material Tailwind Dashboard React | By Creative Tim</title>
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700;900&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css"
|
||||||
|
integrity="sha512-xh6O/CkQoPOWDdYTDqeRdPCVd1SpvCA9XXcUnZS2FmJNp1coAFzvtCN9BmamE+4aHK8yyUHUSCcJHgXloTyT2A=="
|
||||||
|
crossorigin="anonymous"
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
/>
|
||||||
|
<script defer data-site="YOUR_DOMAIN_HERE" src="https://api.nepcha.com/js/nepcha-analytics.js"></script>
|
||||||
|
|
||||||
<script type="module" crossorigin src="/assets/index-a1f09468.js"></script>
|
<!-- Nepcha Analytics (nepcha.com) -->
|
||||||
<link rel="stylesheet" href="/assets/index-0842cd3a.css">
|
<!-- Nepcha is a easy-to-use web analytics. No cookies and fully compliant with GDPR, CCPA and PECR. -->
|
||||||
|
<script defer data-site="YOUR_DOMAIN_HERE" src="https://api.nepcha.com/js/nepcha-analytics.js"></script>
|
||||||
|
<script type="module" crossorigin src="/assets/index-12a5cbeb.js"></script>
|
||||||
|
<link rel="stylesheet" href="/assets/index-c94d5694.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -1,10 +1,37 @@
|
|||||||
|
<!--
|
||||||
|
=========================================================
|
||||||
|
* Material Tailwind Dashboard React - v2.1.0
|
||||||
|
=========================================================
|
||||||
|
* Product Page: https://www.creative-tim.com/product/material-tailwind-dashboard-react
|
||||||
|
* Copyright 2023 Creative Tim (https://www.creative-tim.com)
|
||||||
|
* Licensed under MIT (https://github.com/creativetimofficial/material-tailwind-dashboard-react/blob/main/LICENSE.md)
|
||||||
|
* Coded by Creative Tim
|
||||||
|
=========================================================
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
-->
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/img/favicon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>StackPulse</title>
|
<title>Material Tailwind Dashboard React | By Creative Tim</title>
|
||||||
<link rel="stylesheet" href="/src/index.css" />
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700;900&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css"
|
||||||
|
integrity="sha512-xh6O/CkQoPOWDdYTDqeRdPCVd1SpvCA9XXcUnZS2FmJNp1coAFzvtCN9BmamE+4aHK8yyUHUSCcJHgXloTyT2A=="
|
||||||
|
crossorigin="anonymous"
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
/>
|
||||||
|
<script defer data-site="YOUR_DOMAIN_HERE" src="https://api.nepcha.com/js/nepcha-analytics.js"></script>
|
||||||
|
|
||||||
|
<!-- Nepcha Analytics (nepcha.com) -->
|
||||||
|
<!-- Nepcha is a easy-to-use web analytics. No cookies and fully compliant with GDPR, CCPA and PECR. -->
|
||||||
|
<script defer data-site="YOUR_DOMAIN_HERE" src="https://api.nepcha.com/js/nepcha-analytics.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,23 +1,34 @@
|
|||||||
{
|
{
|
||||||
"name": "stackpulse-frontend",
|
"name": "material-tailwind-dashboard-react",
|
||||||
"version": "0.1.0",
|
"private": true,
|
||||||
|
"version": "2.1.0",
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.5",
|
"axios": "1.7.5",
|
||||||
"react": "^18.2.0",
|
"socket.io-client": "4.8.1",
|
||||||
"react-dom": "^18.2.0",
|
"@heroicons/react": "2.0.18",
|
||||||
"react-router-dom": "^6.26.2",
|
"@material-tailwind/react": "2.1.4",
|
||||||
"socket.io-client": "^4.8.1"
|
"apexcharts": "3.44.0",
|
||||||
},
|
"prop-types": "15.8.1",
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-apexcharts": "1.4.1",
|
||||||
|
"react-dom": "18.2.0",
|
||||||
|
"react-router-dom": "6.17.0"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-react": "^4.0.0",
|
"@types/react": "18.2.31",
|
||||||
"autoprefixer": "^10.4.21",
|
"@types/react-dom": "18.2.14",
|
||||||
"postcss": "^8.5.6",
|
"@vitejs/plugin-react": "4.1.0",
|
||||||
"tailwindcss": "^3.4.17",
|
"autoprefixer": "10.4.16",
|
||||||
"vite": "^4.4.9"
|
"postcss": "8.4.31",
|
||||||
|
"prettier": "3.0.3",
|
||||||
|
"prettier-plugin-tailwindcss": "0.5.6",
|
||||||
|
"tailwindcss": "3.3.4",
|
||||||
|
"vite": "4.5.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
module.exports = {
|
||||||
|
tailwindConfig: "./tailwind.config.cjs",
|
||||||
|
plugins: [require("prettier-plugin-tailwindcss")],
|
||||||
|
};
|
||||||
@@ -1,43 +1,14 @@
|
|||||||
import React from "react";
|
import { Routes, Route, Navigate } from "react-router-dom";
|
||||||
import { NavLink, Route, Routes } from "react-router-dom";
|
import { Dashboard, Auth } from "@/layouts";
|
||||||
import Stacks from "./Stacks.jsx";
|
|
||||||
import Logs from "./Logs.jsx";
|
|
||||||
import logo from "./assets/images/stackpulse.png";
|
|
||||||
|
|
||||||
const navLinkBase =
|
function App() {
|
||||||
"px-4 py-2 rounded-md font-medium transition-colors duration-150";
|
|
||||||
|
|
||||||
const getNavClass = ({ isActive }) =>
|
|
||||||
`${navLinkBase} ${isActive ? "bg-purple-600 text-white" : "text-gray-300 hover:bg-gray-700"}`;
|
|
||||||
|
|
||||||
export default function App() {
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-900 text-white">
|
<Routes>
|
||||||
<header className="bg-gray-800 shadow-md">
|
<Route path="/dashboard/*" element={<Dashboard />} />
|
||||||
<div className="max-w-6xl mx-auto px-6 py-6">
|
<Route path="/auth/*" element={<Auth />} />
|
||||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
|
<Route path="*" element={<Navigate to="/dashboard/stacks" replace />} />
|
||||||
<div className="flex flex-col items-end gap-1">
|
</Routes>
|
||||||
<span className="text-xs text-gray-500">v0.2</span>
|
|
||||||
<img src={logo} alt="StackPulse" className="h-10 w-auto" />
|
|
||||||
</div>
|
|
||||||
<nav className="flex gap-2 items-end">
|
|
||||||
<NavLink to="/" end className={getNavClass}>
|
|
||||||
Stacks
|
|
||||||
</NavLink>
|
|
||||||
<NavLink to="/logs" className={getNavClass}>
|
|
||||||
Logs
|
|
||||||
</NavLink>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<main className="max-w-6xl mx-auto px-6 py-6">
|
|
||||||
<Routes>
|
|
||||||
<Route path="/" element={<Stacks />} />
|
|
||||||
<Route path="/logs" element={<Logs />} />
|
|
||||||
<Route path="*" element={<Stacks />} />
|
|
||||||
</Routes>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
|||||||
@@ -1,297 +0,0 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import axios from "axios";
|
|
||||||
import { io } from "socket.io-client";
|
|
||||||
|
|
||||||
export default function Stacks() {
|
|
||||||
const [stacks, setStacks] = useState([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState("");
|
|
||||||
const [selectedStackIds, setSelectedStackIds] = useState([]);
|
|
||||||
|
|
||||||
const mergeStackState = (previousStacks, incomingStacks) => {
|
|
||||||
const prevMap = new Map(previousStacks.map((stack) => [stack.Id, stack]));
|
|
||||||
const sortedIncoming = [...incomingStacks].sort((a, b) => a.Name.localeCompare(b.Name));
|
|
||||||
|
|
||||||
return sortedIncoming.map((stack) => {
|
|
||||||
const previous = prevMap.get(stack.Id);
|
|
||||||
return {
|
|
||||||
...stack,
|
|
||||||
redeploying: previous?.redeploying || stack.redeploying || false,
|
|
||||||
redeployDisabled: stack.redeployDisabled ?? previous?.redeployDisabled ?? false
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const socket = io("/", {
|
|
||||||
path: "/socket.io",
|
|
||||||
transports: ["websocket"]
|
|
||||||
});
|
|
||||||
console.log("🔌 Socket connected");
|
|
||||||
|
|
||||||
socket.on("redeployStatus", async ({ stackId, status }) => {
|
|
||||||
console.log(`🔄 Stack ${stackId} Redeploy Status: ${status ? "running" : "finished"}`);
|
|
||||||
|
|
||||||
setStacks(prev =>
|
|
||||||
prev.map(stack => {
|
|
||||||
if (stack.Id !== stackId) return stack;
|
|
||||||
return {
|
|
||||||
...stack,
|
|
||||||
redeploying: status,
|
|
||||||
updateStatus: status ? stack.updateStatus : '✅'
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!status) {
|
|
||||||
// Status nach Redeploy neu vom Server holen
|
|
||||||
try {
|
|
||||||
const res = await axios.get("/api/stacks");
|
|
||||||
setStacks(prev => mergeStackState(prev, res.data));
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Fehler beim Aktualisieren nach Redeploy:", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => socket.disconnect();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const fetchStacks = async () => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const res = await axios.get("/api/stacks");
|
|
||||||
setStacks(prev => mergeStackState(prev, res.data));
|
|
||||||
} catch (err) {
|
|
||||||
console.error("❌ Fehler beim Abrufen der Stacks:", err);
|
|
||||||
setError("Fehler beim Laden der Stacks");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchStacks();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setSelectedStackIds(prev => {
|
|
||||||
const filtered = prev.filter(id => {
|
|
||||||
const match = stacks.find(stack => stack.Id === id);
|
|
||||||
return match && match.updateStatus !== '✅' && !match.redeployDisabled;
|
|
||||||
});
|
|
||||||
return filtered.length === prev.length ? prev : filtered;
|
|
||||||
});
|
|
||||||
}, [stacks]);
|
|
||||||
|
|
||||||
const toggleStackSelection = (stackId, disabled) => {
|
|
||||||
if (disabled) return;
|
|
||||||
setSelectedStackIds(prev =>
|
|
||||||
prev.includes(stackId)
|
|
||||||
? prev.filter(id => id !== stackId)
|
|
||||||
: [...prev, stackId]
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRedeploy = async (stackId) => {
|
|
||||||
const targetStack = stacks.find((stack) => stack.Id === stackId);
|
|
||||||
if (targetStack?.redeployDisabled) return;
|
|
||||||
|
|
||||||
setSelectedStackIds((prev) => prev.filter((id) => id !== stackId));
|
|
||||||
setStacks((prev) =>
|
|
||||||
prev.map((stack) =>
|
|
||||||
stack.Id === stackId ? { ...stack, redeploying: true } : stack
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await axios.put(`/api/stacks/${stackId}/redeploy`);
|
|
||||||
// Statusupdates kommen über Socket.IO
|
|
||||||
} catch (err) {
|
|
||||||
console.error("❌ Fehler beim Redeploy:", err);
|
|
||||||
setStacks((prev) =>
|
|
||||||
prev.map((stack) =>
|
|
||||||
stack.Id === stackId ? { ...stack, redeploying: false } : stack
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRedeployAll = async () => {
|
|
||||||
const outdatedStacks = stacks.filter((stack) => stack.updateStatus !== '✅' && !stack.redeployDisabled);
|
|
||||||
if (!outdatedStacks.length) return;
|
|
||||||
|
|
||||||
const outdatedIds = new Set(outdatedStacks.map((stack) => stack.Id));
|
|
||||||
|
|
||||||
setStacks(prev =>
|
|
||||||
prev.map(stack =>
|
|
||||||
outdatedIds.has(stack.Id)
|
|
||||||
? { ...stack, redeploying: true }
|
|
||||||
: stack
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await axios.put("/api/stacks/redeploy-all");
|
|
||||||
setSelectedStackIds((prev) => prev.filter((id) => !outdatedIds.has(id)));
|
|
||||||
// Statusupdates kommen über Socket.IO
|
|
||||||
} catch (err) {
|
|
||||||
console.error("❌ Fehler beim Redeploy ALL:", err);
|
|
||||||
setStacks(prev =>
|
|
||||||
prev.map(stack =>
|
|
||||||
outdatedIds.has(stack.Id)
|
|
||||||
? { ...stack, redeploying: false }
|
|
||||||
: stack
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRedeploySelection = async () => {
|
|
||||||
if (!selectedStackIds.length) return;
|
|
||||||
|
|
||||||
const eligibleIds = selectedStackIds.filter((id) => {
|
|
||||||
const stack = stacks.find((entry) => entry.Id === id);
|
|
||||||
return stack && stack.updateStatus !== '✅' && !stack.redeployDisabled;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!eligibleIds.length) {
|
|
||||||
setSelectedStackIds([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const eligibleSet = new Set(eligibleIds);
|
|
||||||
|
|
||||||
setStacks(prev =>
|
|
||||||
prev.map(stack =>
|
|
||||||
eligibleSet.has(stack.Id)
|
|
||||||
? { ...stack, redeploying: true }
|
|
||||||
: stack
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await axios.put("/api/stacks/redeploy-selection", { stackIds: eligibleIds });
|
|
||||||
setSelectedStackIds((prev) => prev.filter((id) => !eligibleSet.has(id)));
|
|
||||||
// Statusupdates kommen über Socket.IO
|
|
||||||
} catch (err) {
|
|
||||||
console.error("❌ Fehler beim Redeploy Auswahl:", err);
|
|
||||||
setStacks(prev =>
|
|
||||||
prev.map(stack =>
|
|
||||||
eligibleSet.has(stack.Id)
|
|
||||||
? { ...stack, redeploying: false }
|
|
||||||
: stack
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasSelection = selectedStackIds.length > 0;
|
|
||||||
const hasOutdatedStacks = stacks.some((stack) => stack.updateStatus !== '✅' && !stack.redeployDisabled);
|
|
||||||
const bulkButtonLabel = hasSelection
|
|
||||||
? `Redeploy Auswahl (${selectedStackIds.length})`
|
|
||||||
: 'Redeploy All';
|
|
||||||
|
|
||||||
const bulkActionDisabled = hasSelection
|
|
||||||
? selectedStackIds.length === 0 || selectedStackIds.every(id => {
|
|
||||||
const targetStack = stacks.find(stack => stack.Id === id);
|
|
||||||
return !targetStack || targetStack.redeploying || targetStack.updateStatus === '✅' || targetStack.redeployDisabled;
|
|
||||||
})
|
|
||||||
: !hasOutdatedStacks || stacks.every(stack => stack.updateStatus !== '✅' || stack.redeploying || stack.redeployDisabled);
|
|
||||||
|
|
||||||
const handleBulkRedeploy = () => {
|
|
||||||
if (hasSelection) {
|
|
||||||
handleRedeploySelection();
|
|
||||||
} else {
|
|
||||||
handleRedeployAll();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading) return <p className="text-gray-400">Lade Stacks...</p>;
|
|
||||||
if (error) return <p className="text-red-400">{error}</p>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-6">
|
|
||||||
<div className="flex justify-end mb-4">
|
|
||||||
<button
|
|
||||||
onClick={handleBulkRedeploy}
|
|
||||||
disabled={bulkActionDisabled}
|
|
||||||
className={`px-5 py-2 rounded-lg font-medium transition ${bulkActionDisabled ? 'bg-purple-900 cursor-not-allowed text-gray-400' : 'bg-purple-500 hover:bg-purple-600'}`}
|
|
||||||
>
|
|
||||||
{bulkButtonLabel}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
{stacks.map(stack => {
|
|
||||||
const isRedeploying = stack.redeploying;
|
|
||||||
const isSelected = selectedStackIds.includes(stack.Id);
|
|
||||||
const isCurrent = stack.updateStatus === '✅';
|
|
||||||
const isSelfStack = Boolean(stack.redeployDisabled);
|
|
||||||
const isSelectable = !isRedeploying && !isCurrent && !isSelfStack;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={stack.Id}
|
|
||||||
className={`flex justify-between items-center p-5 rounded-xl shadow-lg transition border
|
|
||||||
${isSelected ? 'border-purple-500 ring-1 ring-purple-500/40' : 'border-transparent'}
|
|
||||||
${isRedeploying ? 'bg-gray-700 cursor-wait' : 'bg-gray-800 hover:bg-gray-700'}
|
|
||||||
${!isSelectable && !isRedeploying ? 'opacity-75' : ''}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={isSelected}
|
|
||||||
onChange={() => toggleStackSelection(stack.Id, !isSelectable)}
|
|
||||||
className={`h-5 w-5 text-purple-500 focus:ring-purple-400 border-gray-600 bg-gray-900 rounded ${!isSelectable ? 'opacity-40 cursor-not-allowed' : ''}`}
|
|
||||||
disabled={!isSelectable}
|
|
||||||
/>
|
|
||||||
<div className={`w-12 h-12 flex items-center justify-center rounded-full
|
|
||||||
${stack.updateStatus === '✅' ? 'bg-green-500' :
|
|
||||||
stack.updateStatus === '⚠️' ? 'bg-yellow-500' :
|
|
||||||
'bg-red-500'}`}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<p className="text-lg font-semibold text-white">{stack.Name}</p>
|
|
||||||
<p className="text-sm text-gray-400">ID: {stack.Id}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col items-end gap-1 text-sm">
|
|
||||||
{isRedeploying ? (
|
|
||||||
<>
|
|
||||||
<span className="text-xs uppercase tracking-wide text-orange-300">Redeploy</span>
|
|
||||||
<span className="text-orange-200">läuft…</span>
|
|
||||||
</>
|
|
||||||
) : isSelfStack ? (
|
|
||||||
<>
|
|
||||||
<span className="text-xs uppercase tracking-wide text-gray-400">System</span>
|
|
||||||
<span className="text-gray-300">Redeploy deaktiviert</span>
|
|
||||||
</>
|
|
||||||
) : isCurrent ? (
|
|
||||||
<>
|
|
||||||
<span className="text-xs uppercase tracking-wide text-gray-400">Status</span>
|
|
||||||
<span className="text-green-300">Aktuell</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<span className="text-xs uppercase tracking-wide text-gray-400">Status</span>
|
|
||||||
<span className="text-amber-300">Veraltet</span>
|
|
||||||
<button
|
|
||||||
onClick={() => handleRedeploy(stack.Id)}
|
|
||||||
disabled={isRedeploying}
|
|
||||||
className="px-5 py-2 rounded-lg font-medium transition bg-blue-500 hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
Redeploy
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{stacks.length === 0 && <p className="text-gray-400">Keine Stacks gefunden.</p>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 156 KiB |
@@ -0,0 +1,166 @@
|
|||||||
|
import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
const MaintenanceContext = createContext(null);
|
||||||
|
|
||||||
|
const INITIAL_STATE = {
|
||||||
|
maintenance: null,
|
||||||
|
update: null,
|
||||||
|
script: null,
|
||||||
|
ssh: null,
|
||||||
|
loading: true,
|
||||||
|
error: "",
|
||||||
|
lastUpdated: null
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function MaintenanceProvider({ children }) {
|
||||||
|
const [state, setState] = useState(INITIAL_STATE);
|
||||||
|
const pollingRef = useRef(null);
|
||||||
|
const wasRunningRef = useRef(false);
|
||||||
|
|
||||||
|
const applyState = useCallback((partial) => {
|
||||||
|
setState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
...partial
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchConfig = useCallback(async () => {
|
||||||
|
applyState({ loading: true, error: "" });
|
||||||
|
try {
|
||||||
|
const response = await axios.get("/api/maintenance/config");
|
||||||
|
const data = response.data ?? {};
|
||||||
|
applyState({
|
||||||
|
maintenance: data.maintenance ?? null,
|
||||||
|
update: data.update ?? null,
|
||||||
|
script: data.script ?? null,
|
||||||
|
ssh: data.ssh ?? null,
|
||||||
|
loading: false,
|
||||||
|
error: "",
|
||||||
|
lastUpdated: new Date()
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
const message = err.response?.data?.error || err.message || "Fehler beim Laden der Wartungsdaten";
|
||||||
|
applyState({ loading: false, error: message });
|
||||||
|
}
|
||||||
|
}, [applyState]);
|
||||||
|
|
||||||
|
const refreshUpdateStatus = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get("/api/maintenance/update-status");
|
||||||
|
const data = response.data ?? {};
|
||||||
|
setState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
maintenance: data.maintenance ?? prev.maintenance,
|
||||||
|
update: data.update ?? prev.update,
|
||||||
|
error: ""
|
||||||
|
}));
|
||||||
|
} catch (err) {
|
||||||
|
const message = err.response?.data?.error || err.message || "Fehler beim Aktualisieren des Wartungsstatus";
|
||||||
|
setState((prev) => ({ ...prev, error: message }));
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchConfig();
|
||||||
|
}, [fetchConfig]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (state.update?.running) {
|
||||||
|
refreshUpdateStatus();
|
||||||
|
const intervalId = setInterval(() => {
|
||||||
|
refreshUpdateStatus();
|
||||||
|
}, 5000);
|
||||||
|
pollingRef.current = intervalId;
|
||||||
|
return () => clearInterval(intervalId);
|
||||||
|
}
|
||||||
|
if (pollingRef.current) {
|
||||||
|
clearInterval(pollingRef.current);
|
||||||
|
pollingRef.current = null;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}, [state.update?.running, refreshUpdateStatus]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentlyRunning = Boolean(state.update?.running);
|
||||||
|
if (!currentlyRunning && wasRunningRef.current) {
|
||||||
|
fetchConfig();
|
||||||
|
}
|
||||||
|
wasRunningRef.current = currentlyRunning;
|
||||||
|
}, [state.update?.running, fetchConfig]);
|
||||||
|
|
||||||
|
const triggerUpdate = useCallback(async (payload = {}) => {
|
||||||
|
await axios.post("/api/maintenance/portainer-update", payload);
|
||||||
|
await refreshUpdateStatus();
|
||||||
|
}, [refreshUpdateStatus]);
|
||||||
|
|
||||||
|
const saveScript = useCallback(async (script) => {
|
||||||
|
await axios.put("/api/maintenance/update-script", { script });
|
||||||
|
await fetchConfig();
|
||||||
|
}, [fetchConfig]);
|
||||||
|
|
||||||
|
const resetScript = useCallback(async () => {
|
||||||
|
await axios.delete("/api/maintenance/update-script");
|
||||||
|
await fetchConfig();
|
||||||
|
}, [fetchConfig]);
|
||||||
|
|
||||||
|
const saveSshConfig = useCallback(async (config) => {
|
||||||
|
await axios.put("/api/maintenance/ssh-config", config);
|
||||||
|
await fetchConfig();
|
||||||
|
}, [fetchConfig]);
|
||||||
|
|
||||||
|
const deleteSshConfig = useCallback(async () => {
|
||||||
|
await axios.delete("/api/maintenance/ssh-config");
|
||||||
|
await fetchConfig();
|
||||||
|
}, [fetchConfig]);
|
||||||
|
|
||||||
|
const setMaintenanceMode = useCallback(async ({ active, message } = {}) => {
|
||||||
|
const payload = { active: Boolean(active) };
|
||||||
|
if (message !== undefined) {
|
||||||
|
payload.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await axios.post("/api/maintenance/mode", payload);
|
||||||
|
const nextMaintenance = response.data?.maintenance ?? null;
|
||||||
|
applyState({ maintenance: nextMaintenance, error: "" });
|
||||||
|
return nextMaintenance;
|
||||||
|
}, [applyState]);
|
||||||
|
|
||||||
|
const testSshConnection = useCallback(async (config) => {
|
||||||
|
const response = await axios.post("/api/maintenance/test-ssh", config ?? {});
|
||||||
|
return response.data;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const value = useMemo(() => ({
|
||||||
|
maintenance: state.maintenance,
|
||||||
|
update: state.update,
|
||||||
|
script: state.script,
|
||||||
|
ssh: state.ssh,
|
||||||
|
loading: state.loading,
|
||||||
|
error: state.error,
|
||||||
|
lastUpdated: state.lastUpdated,
|
||||||
|
refreshConfig: fetchConfig,
|
||||||
|
refreshUpdateStatus,
|
||||||
|
setMaintenanceMode,
|
||||||
|
triggerUpdate,
|
||||||
|
saveScript,
|
||||||
|
resetScript,
|
||||||
|
saveSshConfig,
|
||||||
|
deleteSshConfig,
|
||||||
|
testSshConnection
|
||||||
|
}), [state, fetchConfig, refreshUpdateStatus, setMaintenanceMode, triggerUpdate, saveScript, resetScript, saveSshConfig, deleteSshConfig, testSshConnection]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MaintenanceContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</MaintenanceContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMaintenance() {
|
||||||
|
const context = useContext(MaintenanceContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useMaintenance must be used within a MaintenanceProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
import React, { createContext, useCallback, useContext, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
|
const DEFAULT_PER_PAGE_OPTIONS = [
|
||||||
|
{ value: "10", label: "10" },
|
||||||
|
{ value: "25", label: "25" },
|
||||||
|
{ value: "50", label: "50" },
|
||||||
|
{ value: "100", label: "100" },
|
||||||
|
{ value: "all", label: "Alle" }
|
||||||
|
];
|
||||||
|
|
||||||
|
const DEFAULT_PER_PAGE = "50";
|
||||||
|
|
||||||
|
const PageContext = createContext(null);
|
||||||
|
|
||||||
|
const normalizeNumber = (value, fallback) => {
|
||||||
|
const numeric = Number(value);
|
||||||
|
if (Number.isFinite(numeric) && numeric > 0) {
|
||||||
|
return numeric;
|
||||||
|
}
|
||||||
|
const fallbackNumeric = Number(fallback);
|
||||||
|
if (Number.isFinite(fallbackNumeric) && fallbackNumeric > 0) {
|
||||||
|
return fallbackNumeric;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function PageProvider({
|
||||||
|
children,
|
||||||
|
initialPage = 1,
|
||||||
|
initialPerPage = DEFAULT_PER_PAGE,
|
||||||
|
perPageOptions = DEFAULT_PER_PAGE_OPTIONS
|
||||||
|
}) {
|
||||||
|
const [page, setPage] = useState(initialPage);
|
||||||
|
const [perPage, setPerPage] = useState(initialPerPage);
|
||||||
|
const [totalItems, setTotalItems] = useState(0);
|
||||||
|
const [visibleItems, setVisibleItems] = useState(0);
|
||||||
|
|
||||||
|
const initialPageRef = useRef(initialPage);
|
||||||
|
const initialPerPageRef = useRef(initialPerPage);
|
||||||
|
|
||||||
|
const validPerPageValues = useMemo(() => new Set(
|
||||||
|
perPageOptions.map((option) => String(option.value))
|
||||||
|
), [perPageOptions]);
|
||||||
|
|
||||||
|
const perPageIsAll = perPage === "all";
|
||||||
|
|
||||||
|
const resolvePerPageNumber = useCallback(() => {
|
||||||
|
if (perPageIsAll) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return normalizeNumber(perPage, initialPerPage);
|
||||||
|
}, [perPage, perPageIsAll, initialPerPage]);
|
||||||
|
|
||||||
|
const updateTotals = useCallback((total, visible) => {
|
||||||
|
setTotalItems(Math.max(0, Number(total) || 0));
|
||||||
|
setVisibleItems(Math.max(0, Number(visible) || 0));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const resetPagination = useCallback(() => {
|
||||||
|
setPage(initialPageRef.current ?? 1);
|
||||||
|
setPerPage(initialPerPageRef.current ?? DEFAULT_PER_PAGE);
|
||||||
|
setTotalItems(0);
|
||||||
|
setVisibleItems(0);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const setPerPageValue = useCallback((value, { resetPage = true } = {}) => {
|
||||||
|
if (value === undefined || value === null) return;
|
||||||
|
const nextValue = String(value);
|
||||||
|
if (!validPerPageValues.has(nextValue)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setPerPage(nextValue);
|
||||||
|
if (resetPage) {
|
||||||
|
setPage(1);
|
||||||
|
}
|
||||||
|
}, [validPerPageValues]);
|
||||||
|
|
||||||
|
const handlePerPageChange = useCallback((eventOrValue) => {
|
||||||
|
const nextValue = typeof eventOrValue === "string"
|
||||||
|
? eventOrValue
|
||||||
|
: eventOrValue?.target?.value;
|
||||||
|
setPerPageValue(nextValue, { resetPage: true });
|
||||||
|
}, [setPerPageValue]);
|
||||||
|
|
||||||
|
const handlePageChange = useCallback((nextPage) => {
|
||||||
|
if (perPageIsAll) return;
|
||||||
|
|
||||||
|
const numericPage = Number(nextPage);
|
||||||
|
if (!Number.isFinite(numericPage) || numericPage < 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const perPageNumber = resolvePerPageNumber();
|
||||||
|
if (!perPageNumber) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxPages = Math.max(1, Math.ceil((totalItems || 0) / perPageNumber));
|
||||||
|
if (numericPage > maxPages) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPage(numericPage);
|
||||||
|
}, [perPageIsAll, resolvePerPageNumber, totalItems]);
|
||||||
|
|
||||||
|
const totalPages = useMemo(() => {
|
||||||
|
if (perPageIsAll) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const perPageNumber = resolvePerPageNumber();
|
||||||
|
if (!perPageNumber) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return Math.max(1, Math.ceil((totalItems || 0) / perPageNumber));
|
||||||
|
}, [perPageIsAll, resolvePerPageNumber, totalItems]);
|
||||||
|
|
||||||
|
const rangeStart = useMemo(() => {
|
||||||
|
if (!totalItems) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (perPageIsAll) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const perPageNumber = resolvePerPageNumber();
|
||||||
|
return (page - 1) * perPageNumber + 1;
|
||||||
|
}, [totalItems, perPageIsAll, resolvePerPageNumber, page]);
|
||||||
|
|
||||||
|
const rangeEnd = useMemo(() => {
|
||||||
|
if (!totalItems) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (perPageIsAll) {
|
||||||
|
return totalItems;
|
||||||
|
}
|
||||||
|
const perPageNumber = resolvePerPageNumber();
|
||||||
|
const estimatedEnd = (page - 1) * perPageNumber + (visibleItems || perPageNumber);
|
||||||
|
return Math.min(totalItems, estimatedEnd);
|
||||||
|
}, [totalItems, perPageIsAll, resolvePerPageNumber, page, visibleItems]);
|
||||||
|
|
||||||
|
const summaryLabel = useMemo(() => {
|
||||||
|
if (!totalItems) {
|
||||||
|
return "Keine Einträge";
|
||||||
|
}
|
||||||
|
if (perPageIsAll) {
|
||||||
|
return `Zeige alle ${totalItems.toLocaleString()} Einträge`;
|
||||||
|
}
|
||||||
|
return `Zeige ${rangeStart.toLocaleString()} – ${rangeEnd.toLocaleString()} von ${totalItems.toLocaleString()} Einträgen`;
|
||||||
|
}, [totalItems, perPageIsAll, rangeStart, rangeEnd]);
|
||||||
|
|
||||||
|
const value = useMemo(() => ({
|
||||||
|
page,
|
||||||
|
perPage,
|
||||||
|
perPageOptions,
|
||||||
|
perPageIsAll,
|
||||||
|
totalItems,
|
||||||
|
visibleItems,
|
||||||
|
totalPages,
|
||||||
|
rangeStart,
|
||||||
|
rangeEnd,
|
||||||
|
summaryLabel,
|
||||||
|
setPage,
|
||||||
|
setPerPage: setPerPageValue,
|
||||||
|
setTotals: updateTotals,
|
||||||
|
setTotalItems,
|
||||||
|
setVisibleItems,
|
||||||
|
handlePageChange,
|
||||||
|
handlePerPageChange,
|
||||||
|
validPerPageValues,
|
||||||
|
resetPagination
|
||||||
|
}), [
|
||||||
|
page,
|
||||||
|
perPage,
|
||||||
|
perPageOptions,
|
||||||
|
perPageIsAll,
|
||||||
|
totalItems,
|
||||||
|
visibleItems,
|
||||||
|
totalPages,
|
||||||
|
rangeStart,
|
||||||
|
rangeEnd,
|
||||||
|
summaryLabel,
|
||||||
|
updateTotals,
|
||||||
|
setPerPageValue,
|
||||||
|
handlePageChange,
|
||||||
|
handlePerPageChange,
|
||||||
|
validPerPageValues,
|
||||||
|
resetPagination
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</PageContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePage() {
|
||||||
|
const context = useContext(PageContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("usePage must be used within a PageProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PaginationControls({ disabled = false }) {
|
||||||
|
const {
|
||||||
|
summaryLabel,
|
||||||
|
perPageIsAll,
|
||||||
|
page,
|
||||||
|
totalPages,
|
||||||
|
handlePageChange
|
||||||
|
} = usePage();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
|
<span>{summaryLabel}</span>
|
||||||
|
{!perPageIsAll && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => handlePageChange(page - 1)}
|
||||||
|
disabled={disabled || page <= 1}
|
||||||
|
className="rounded-md border border-slate-300 p-2.5 text-center text-sm transition-all shadow-sm hover:shadow-lg text-slate-600 hover:text-white hover:bg-slate-800 hover:border-slate-800 focus:text-white focus:bg-slate-800 focus:border-slate-800 active:border-slate-800 active:text-white active:bg-slate-800 disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" className="h-4 w-4">
|
||||||
|
<path d="M11.03 3.97a.75.75 0 0 1 0 1.06l-6.22 6.22H21a.75.75 0 0 1 0 1.5H4.81l6.22 6.22a.75.75 0 1 1-1.06 1.06l-7.5-7.5a.75.75 0 0 1 0-1.06l7.5-7.5a.75.75 0 0 1 1.06 0Z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<p className="text-slate-600">
|
||||||
|
Seite <strong className="text-slate-800">{page}</strong> /
|
||||||
|
<strong className="text-slate-800">{totalPages}</strong>
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => handlePageChange(page + 1)}
|
||||||
|
disabled={disabled || page >= totalPages}
|
||||||
|
className="rounded-md border border-slate-300 p-2.5 text-center text-sm transition-all shadow-sm hover:shadow-lg text-slate-600 hover:text-white hover:bg-slate-800 hover:border-slate-800 focus:text-white focus:bg-slate-800 focus:border-slate-800 active:border-slate-800 active:text-white active:bg-slate-800 disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" className="h-4 w-4">
|
||||||
|
<path d="M12.97 3.97a.75.75 0 0 1 1.06 0l7.5 7.5a.75.75 0 0 1 0 1.06l-7.5 7.5a.75.75 0 1 1-1.06-1.06l6.22-6.22H3a.75.75 0 0 1 0-1.5h16.19l-6.22-6.22a.75.75 0 0 1 0-1.06Z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PAGE_DEFAULTS = {
|
||||||
|
perPage: DEFAULT_PER_PAGE,
|
||||||
|
options: DEFAULT_PER_PAGE_OPTIONS
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PageProvider;
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
|
const ToastContext = createContext(null);
|
||||||
|
|
||||||
|
const VARIANT_STYLES = {
|
||||||
|
success: {
|
||||||
|
container: "border-mossGreen-500/80 bg-mossGreen-900/90 text-white",
|
||||||
|
accent: "bg-mossGreen-400"
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
container: "border-warmAmberGlow-500/80 bg-warmAmberGlow-900/90 text-white",
|
||||||
|
accent: "bg-warmAmberGlow-400"
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
container: "border-sunsetCoral-500/80 bg-sunsetCoral-900/90 text-white",
|
||||||
|
accent: "bg-sunsetCoral-400"
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
container: "border-arcticBlue-500 bg-arcticBlue-900/90 text-white",
|
||||||
|
accent: "bg-arcticBlue-400"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_DURATION = 6000;
|
||||||
|
|
||||||
|
export default function ToastProvider({ children }) {
|
||||||
|
const [toasts, setToasts] = useState([]);
|
||||||
|
const timeoutsRef = useRef(new Map());
|
||||||
|
|
||||||
|
const dismissToast = useCallback((id) => {
|
||||||
|
setToasts((prev) => prev.filter((toast) => toast.id !== id));
|
||||||
|
|
||||||
|
const handle = timeoutsRef.current.get(id);
|
||||||
|
if (handle && typeof window !== "undefined") {
|
||||||
|
window.clearTimeout(handle);
|
||||||
|
}
|
||||||
|
timeoutsRef.current.delete(id);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const showToast = useCallback(({ id, title, description, variant = "info", duration = DEFAULT_DURATION } = {}) => {
|
||||||
|
const toastId = id ?? `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||||
|
const normalizedVariant = VARIANT_STYLES[variant] ? variant : "info";
|
||||||
|
|
||||||
|
setToasts((prev) => {
|
||||||
|
const withoutExisting = prev.filter((toast) => toast.id !== toastId);
|
||||||
|
return [
|
||||||
|
...withoutExisting,
|
||||||
|
{
|
||||||
|
id: toastId,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
variant: normalizedVariant
|
||||||
|
}
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
if (duration !== null && duration !== Infinity && typeof window !== "undefined") {
|
||||||
|
const timeoutHandle = window.setTimeout(() => {
|
||||||
|
dismissToast(toastId);
|
||||||
|
}, duration);
|
||||||
|
|
||||||
|
const existingHandle = timeoutsRef.current.get(toastId);
|
||||||
|
if (existingHandle) {
|
||||||
|
window.clearTimeout(existingHandle);
|
||||||
|
}
|
||||||
|
timeoutsRef.current.set(toastId, timeoutHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
return toastId;
|
||||||
|
}, [dismissToast]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
timeoutsRef.current.forEach((handle) => window.clearTimeout(handle));
|
||||||
|
timeoutsRef.current.clear();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const contextValue = useMemo(() => ({ showToast, dismissToast }), [showToast, dismissToast]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToastContext.Provider value={contextValue}>
|
||||||
|
{children}
|
||||||
|
<div className="pointer-events-none fixed right-4 top-4 z-50 flex w-80 max-w-full flex-col gap-3">
|
||||||
|
{toasts.map((toast) => {
|
||||||
|
const styles = VARIANT_STYLES[toast.variant] ?? VARIANT_STYLES.info;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={toast.id}
|
||||||
|
className={`pointer-events-auto overflow-hidden rounded-lg border px-4 py-3 shadow-xl backdrop-blur ${styles.container}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<span className={`mt-1 h-2 flex-shrink-0 w-2 rounded-full ${styles.accent}`}></span>
|
||||||
|
<div className="flex-1">
|
||||||
|
{toast.title && <p className="text-sm font-semibold">{toast.title}</p>}
|
||||||
|
{toast.description && <p className="mt-1 text-sm leading-snug text-white/80">{toast.description}</p>}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => dismissToast(toast.id)}
|
||||||
|
className="ml-2 rounded-md p-1 text-xs uppercase tracking-wide text-white/60 transition hover:text-white/90"
|
||||||
|
>
|
||||||
|
Schließen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</ToastContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useToast() {
|
||||||
|
const context = useContext(ToastContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useToast must be used within a ToastProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import React from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
export const MaterialTailwind = React.createContext(null);
|
||||||
|
MaterialTailwind.displayName = "MaterialTailwindContext";
|
||||||
|
|
||||||
|
export function reducer(state, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case "OPEN_SIDENAV": {
|
||||||
|
return { ...state, openSidenav: action.value };
|
||||||
|
}
|
||||||
|
case "SIDENAV_TYPE": {
|
||||||
|
return { ...state, sidenavType: action.value };
|
||||||
|
}
|
||||||
|
case "SIDENAV_COLOR": {
|
||||||
|
return { ...state, sidenavColor: action.value };
|
||||||
|
}
|
||||||
|
case "TRANSPARENT_NAVBAR": {
|
||||||
|
return { ...state, transparentNavbar: action.value };
|
||||||
|
}
|
||||||
|
case "FIXED_NAVBAR": {
|
||||||
|
return { ...state, fixedNavbar: action.value };
|
||||||
|
}
|
||||||
|
case "OPEN_CONFIGURATOR": {
|
||||||
|
return { ...state, openConfigurator: action.value };
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new Error(`Unhandled action type: ${action.type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MaterialTailwindControllerProvider({ children }) {
|
||||||
|
const initialState = {
|
||||||
|
openSidenav: false,
|
||||||
|
sidenavColor: "gray",
|
||||||
|
sidenavType: "white",
|
||||||
|
transparentNavbar: false,
|
||||||
|
fixedNavbar: true,
|
||||||
|
openConfigurator: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const [controller, dispatch] = React.useReducer(reducer, initialState);
|
||||||
|
const value = React.useMemo(
|
||||||
|
() => [controller, dispatch],
|
||||||
|
[controller, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MaterialTailwind.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</MaterialTailwind.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMaterialTailwindController() {
|
||||||
|
const context = React.useContext(MaterialTailwind);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
"useMaterialTailwindController should be used inside the MaterialTailwindControllerProvider."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialTailwindControllerProvider.displayName = "/src/context/index.jsx";
|
||||||
|
|
||||||
|
MaterialTailwindControllerProvider.propTypes = {
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setOpenSidenav = (dispatch, value) =>
|
||||||
|
dispatch({ type: "OPEN_SIDENAV", value });
|
||||||
|
export const setSidenavType = (dispatch, value) =>
|
||||||
|
dispatch({ type: "SIDENAV_TYPE", value });
|
||||||
|
export const setSidenavColor = (dispatch, value) =>
|
||||||
|
dispatch({ type: "SIDENAV_COLOR", value });
|
||||||
|
export const setTransparentNavbar = (dispatch, value) =>
|
||||||
|
dispatch({ type: "TRANSPARENT_NAVBAR", value });
|
||||||
|
export const setFixedNavbar = (dispatch, value) =>
|
||||||
|
dispatch({ type: "FIXED_NAVBAR", value });
|
||||||
|
export const setOpenConfigurator = (dispatch, value) =>
|
||||||
|
dispatch({ type: "OPEN_CONFIGURATOR", value });
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
export const chartsConfig = {
|
||||||
|
chart: {
|
||||||
|
toolbar: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
show: "",
|
||||||
|
},
|
||||||
|
dataLabels: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
xaxis: {
|
||||||
|
axisTicks: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
axisBorder: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
labels: {
|
||||||
|
style: {
|
||||||
|
colors: "#37474f",
|
||||||
|
fontSize: "13px",
|
||||||
|
fontFamily: "inherit",
|
||||||
|
fontWeight: 300,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
yaxis: {
|
||||||
|
labels: {
|
||||||
|
style: {
|
||||||
|
colors: "#37474f",
|
||||||
|
fontSize: "13px",
|
||||||
|
fontFamily: "inherit",
|
||||||
|
fontWeight: 300,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
show: true,
|
||||||
|
borderColor: "#dddddd",
|
||||||
|
strokeDashArray: 5,
|
||||||
|
xaxis: {
|
||||||
|
lines: {
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
padding: {
|
||||||
|
top: 5,
|
||||||
|
right: 20,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fill: {
|
||||||
|
opacity: 0.8,
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
theme: "dark",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default chartsConfig;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "@/configs/charts-config";
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
export const authorsTableData = [
|
||||||
|
{
|
||||||
|
img: "/img/team-2.jpeg",
|
||||||
|
name: "John Michael",
|
||||||
|
email: "john@creative-tim.com",
|
||||||
|
job: ["Manager", "Organization"],
|
||||||
|
online: true,
|
||||||
|
date: "23/04/18",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
img: "/img/team-1.jpeg",
|
||||||
|
name: "Alexa Liras",
|
||||||
|
email: "alexa@creative-tim.com",
|
||||||
|
job: ["Programator", "Developer"],
|
||||||
|
online: false,
|
||||||
|
date: "11/01/19",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
img: "/img/team-4.jpeg",
|
||||||
|
name: "Laurent Perrier",
|
||||||
|
email: "laurent@creative-tim.com",
|
||||||
|
job: ["Executive", "Projects"],
|
||||||
|
online: true,
|
||||||
|
date: "19/09/17",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
img: "/img/team-3.jpeg",
|
||||||
|
name: "Michael Levi",
|
||||||
|
email: "michael@creative-tim.com",
|
||||||
|
job: ["Programator", "Developer"],
|
||||||
|
online: true,
|
||||||
|
date: "24/12/08",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
img: "/img/bruce-mars.jpeg",
|
||||||
|
name: "Bruce Mars",
|
||||||
|
email: "bruce@creative-tim.com",
|
||||||
|
job: ["Manager", "Executive"],
|
||||||
|
online: false,
|
||||||
|
date: "04/10/21",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
img: "/img/team-2.jpeg",
|
||||||
|
name: "Alexander",
|
||||||
|
email: "alexander@creative-tim.com",
|
||||||
|
job: ["Programator", "Developer"],
|
||||||
|
online: false,
|
||||||
|
date: "14/09/20",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default authorsTableData;
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
export const conversationsData = [
|
||||||
|
{
|
||||||
|
img: "/img/team-1.jpeg",
|
||||||
|
name: "Sophie B.",
|
||||||
|
message: "Hi! I need more information...",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
img: "/img/team-2.jpeg",
|
||||||
|
name: "Alexander",
|
||||||
|
message: "Awesome work, can you...",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
img: "/img/team-3.jpeg",
|
||||||
|
name: "Ivanna",
|
||||||
|
message: "About files I can...",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
img: "/img/team-4.jpeg",
|
||||||
|
name: "Peterson",
|
||||||
|
message: "Have a great afternoon...",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
img: "/img/bruce-mars.jpeg",
|
||||||
|
name: "Bruce Mars",
|
||||||
|
message: "Hi! I need more information...",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default conversationsData;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export * from "@/data/statistics-cards-data";
|
||||||
|
export * from "@/data/statistics-charts-data";
|
||||||
|
export * from "@/data/projects-table-data";
|
||||||
|
export * from "@/data/orders-overview-data";
|
||||||
|
export * from "@/data/platform-settings-data";
|
||||||
|
export * from "@/data/conversations-data";
|
||||||
|
export * from "@/data/projects-data";
|
||||||
|
export * from "@/data/authors-table-data";
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import {
|
||||||
|
BellIcon,
|
||||||
|
PlusCircleIcon,
|
||||||
|
ShoppingCartIcon,
|
||||||
|
CreditCardIcon,
|
||||||
|
LockOpenIcon,
|
||||||
|
BanknotesIcon,
|
||||||
|
} from "@heroicons/react/24/solid";
|
||||||
|
|
||||||
|
export const ordersOverviewData = [
|
||||||
|
{
|
||||||
|
icon: BellIcon,
|
||||||
|
color: "text-blue-gray-300",
|
||||||
|
title: "$2400, Design changes",
|
||||||
|
description: "22 DEC 7:20 PM",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: PlusCircleIcon,
|
||||||
|
color: "text-blue-gray-300",
|
||||||
|
title: "New order #1832412",
|
||||||
|
description: "21 DEC 11 PM",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: ShoppingCartIcon,
|
||||||
|
color: "text-blue-gray-300",
|
||||||
|
title: "Server payments for April",
|
||||||
|
description: "21 DEC 9:34 PM",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: CreditCardIcon,
|
||||||
|
color: "text-blue-gray-300",
|
||||||
|
title: "New card added for order #4395133",
|
||||||
|
description: "20 DEC 2:20 AM",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: LockOpenIcon,
|
||||||
|
color: "text-blue-gray-300",
|
||||||
|
title: "Unlock packages for development",
|
||||||
|
description: "18 DEC 4:54 AM",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: BanknotesIcon,
|
||||||
|
color: "text-blue-gray-300",
|
||||||
|
title: "New order #9583120",
|
||||||
|
description: "17 DEC",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default ordersOverviewData;
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
export const platformSettingsData = [
|
||||||
|
{
|
||||||
|
title: "account",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
checked: true,
|
||||||
|
label: "Email me when someone follows me",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
checked: false,
|
||||||
|
label: "Email me when someone answers on my post",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
checked: true,
|
||||||
|
label: "Email me when someone mentions me",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "application",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
checked: false,
|
||||||
|
label: "New launches and projects",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
checked: true,
|
||||||
|
label: "Monthly product updates",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
checked: false,
|
||||||
|
label: "Subscribe to newsletter",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default platformSettingsData;
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
export const projectsData = [
|
||||||
|
{
|
||||||
|
img: "/img/home-decor-1.jpeg",
|
||||||
|
title: "Modern",
|
||||||
|
tag: "Project #1",
|
||||||
|
description:
|
||||||
|
"As Uber works through a huge amount of internal management turmoil.",
|
||||||
|
route: "/dashboard/profile",
|
||||||
|
members: [
|
||||||
|
{ img: "/img/team-1.jpeg", name: "Romina Hadid" },
|
||||||
|
{ img: "/img/team-2.jpeg", name: "Ryan Tompson" },
|
||||||
|
{ img: "/img/team-3.jpeg", name: "Jessica Doe" },
|
||||||
|
{ img: "/img/team-4.jpeg", name: "Alexander Smith" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
img: "/img/home-decor-2.jpeg",
|
||||||
|
title: "Scandinavian",
|
||||||
|
tag: "Project #2",
|
||||||
|
description:
|
||||||
|
"Music is something that every person has his or her own specific opinion about.",
|
||||||
|
route: "/dashboard/profile",
|
||||||
|
members: [
|
||||||
|
{ img: "/img/team-4.jpeg", name: "Alexander Smith" },
|
||||||
|
{ img: "/img/team-3.jpeg", name: "Jessica Doe" },
|
||||||
|
{ img: "/img/team-2.jpeg", name: "Ryan Tompson" },
|
||||||
|
{ img: "/img/team-1.jpeg", name: "Romina Hadid" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
img: "/img/home-decor-3.jpeg",
|
||||||
|
title: "Minimalist",
|
||||||
|
tag: "Project #3",
|
||||||
|
description:
|
||||||
|
"Different people have different taste, and various types of music.",
|
||||||
|
route: "/dashboard/profile",
|
||||||
|
members: [
|
||||||
|
{ img: "/img/team-1.jpeg", name: "Romina Hadid" },
|
||||||
|
{ img: "/img/team-2.jpeg", name: "Ryan Tompson" },
|
||||||
|
{ img: "/img/team-3.jpeg", name: "Jessica Doe" },
|
||||||
|
{ img: "/img/team-4.jpeg", name: "Alexander Smith" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
img: "/img/home-decor-4.jpeg",
|
||||||
|
title: "Gothic",
|
||||||
|
tag: "Project #4",
|
||||||
|
description:
|
||||||
|
"Why would anyone pick blue over pink? Pink is obviously a better color.",
|
||||||
|
route: "/dashboard/profile",
|
||||||
|
members: [
|
||||||
|
{ img: "/img/team-4.jpeg", name: "Alexander Smith" },
|
||||||
|
{ img: "/img/team-3.jpeg", name: "Jessica Doe" },
|
||||||
|
{ img: "/img/team-2.jpeg", name: "Ryan Tompson" },
|
||||||
|
{ img: "/img/team-1.jpeg", name: "Romina Hadid" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default projectsData;
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
export const projectsTableData = [
|
||||||
|
{
|
||||||
|
img: "/img/logo-xd.svg",
|
||||||
|
name: "Material XD Version",
|
||||||
|
members: [
|
||||||
|
{ img: "/img/team-1.jpeg", name: "Romina Hadid" },
|
||||||
|
{ img: "/img/team-2.jpeg", name: "Ryan Tompson" },
|
||||||
|
{ img: "/img/team-3.jpeg", name: "Jessica Doe" },
|
||||||
|
{ img: "/img/team-4.jpeg", name: "Alexander Smith" },
|
||||||
|
],
|
||||||
|
budget: "$14,000",
|
||||||
|
completion: 60,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
img: "/img/logo-atlassian.svg",
|
||||||
|
name: "Add Progress Track",
|
||||||
|
members: [
|
||||||
|
{ img: "/img/team-2.jpeg", name: "Ryan Tompson" },
|
||||||
|
{ img: "/img/team-4.jpeg", name: "Alexander Smith" },
|
||||||
|
],
|
||||||
|
budget: "$3,000",
|
||||||
|
completion: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
img: "/img/logo-slack.svg",
|
||||||
|
name: "Fix Platform Errors",
|
||||||
|
members: [
|
||||||
|
{ img: "/img/team-3.jpeg", name: "Jessica Doe" },
|
||||||
|
{ img: "/img/team-1.jpeg", name: "Romina Hadid" },
|
||||||
|
],
|
||||||
|
budget: "Not set",
|
||||||
|
completion: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
img: "/img/logo-spotify.svg",
|
||||||
|
name: "Launch our Mobile App",
|
||||||
|
members: [
|
||||||
|
{ img: "/img/team-4.jpeg", name: "Alexander Smith" },
|
||||||
|
{ img: "/img/team-3.jpeg", name: "Jessica Doe" },
|
||||||
|
{ img: "/img/team-2.jpeg", name: "Ryan Tompson" },
|
||||||
|
{ img: "/img/team-1.jpeg", name: "Romina Hadid" },
|
||||||
|
],
|
||||||
|
budget: "$20,500",
|
||||||
|
completion: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
img: "/img/logo-jira.svg",
|
||||||
|
name: "Add the New Pricing Page",
|
||||||
|
members: [{ img: "/img/team-4.jpeg", name: "Alexander Smith" }],
|
||||||
|
budget: "$500",
|
||||||
|
completion: 25,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
img: "/img/logo-invision.svg",
|
||||||
|
name: "Redesign New Online Shop",
|
||||||
|
members: [
|
||||||
|
{ img: "/img/team-1.jpeg", name: "Romina Hadid" },
|
||||||
|
{ img: "/img/team-4.jpeg", name: "Alexander Smith" },
|
||||||
|
],
|
||||||
|
budget: "$2,000",
|
||||||
|
completion: 40,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default projectsTableData;
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import {
|
||||||
|
BanknotesIcon,
|
||||||
|
UserPlusIcon,
|
||||||
|
UsersIcon,
|
||||||
|
ChartBarIcon,
|
||||||
|
} from "@heroicons/react/24/solid";
|
||||||
|
|
||||||
|
export const statisticsCardsData = [
|
||||||
|
{
|
||||||
|
color: "gray",
|
||||||
|
icon: BanknotesIcon,
|
||||||
|
title: "Today's Money",
|
||||||
|
value: "$53k",
|
||||||
|
footer: {
|
||||||
|
color: "text-green-500",
|
||||||
|
value: "+55%",
|
||||||
|
label: "than last week",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: "gray",
|
||||||
|
icon: UsersIcon,
|
||||||
|
title: "Today's Users",
|
||||||
|
value: "2,300",
|
||||||
|
footer: {
|
||||||
|
color: "text-green-500",
|
||||||
|
value: "+3%",
|
||||||
|
label: "than last month",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: "gray",
|
||||||
|
icon: UserPlusIcon,
|
||||||
|
title: "New Clients",
|
||||||
|
value: "3,462",
|
||||||
|
footer: {
|
||||||
|
color: "text-red-500",
|
||||||
|
value: "-2%",
|
||||||
|
label: "than yesterday",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: "gray",
|
||||||
|
icon: ChartBarIcon,
|
||||||
|
title: "Sales",
|
||||||
|
value: "$103,430",
|
||||||
|
footer: {
|
||||||
|
color: "text-green-500",
|
||||||
|
value: "+5%",
|
||||||
|
label: "than yesterday",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default statisticsCardsData;
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import { chartsConfig } from "@/configs";
|
||||||
|
|
||||||
|
const websiteViewsChart = {
|
||||||
|
type: "bar",
|
||||||
|
height: 220,
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: "Views",
|
||||||
|
data: [50, 20, 10, 22, 50, 10, 40],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
options: {
|
||||||
|
...chartsConfig,
|
||||||
|
colors: "#388e3c",
|
||||||
|
plotOptions: {
|
||||||
|
bar: {
|
||||||
|
columnWidth: "16%",
|
||||||
|
borderRadius: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
xaxis: {
|
||||||
|
...chartsConfig.xaxis,
|
||||||
|
categories: ["M", "T", "W", "T", "F", "S", "S"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const dailySalesChart = {
|
||||||
|
type: "line",
|
||||||
|
height: 220,
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: "Sales",
|
||||||
|
data: [50, 40, 300, 320, 500, 350, 200, 230, 500],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
options: {
|
||||||
|
...chartsConfig,
|
||||||
|
colors: ["#0288d1"],
|
||||||
|
stroke: {
|
||||||
|
lineCap: "round",
|
||||||
|
},
|
||||||
|
markers: {
|
||||||
|
size: 5,
|
||||||
|
},
|
||||||
|
xaxis: {
|
||||||
|
...chartsConfig.xaxis,
|
||||||
|
categories: [
|
||||||
|
"Apr",
|
||||||
|
"May",
|
||||||
|
"Jun",
|
||||||
|
"Jul",
|
||||||
|
"Aug",
|
||||||
|
"Sep",
|
||||||
|
"Oct",
|
||||||
|
"Nov",
|
||||||
|
"Dec",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const completedTaskChart = {
|
||||||
|
type: "line",
|
||||||
|
height: 220,
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: "Sales",
|
||||||
|
data: [50, 40, 300, 320, 500, 350, 200, 230, 500],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
options: {
|
||||||
|
...chartsConfig,
|
||||||
|
colors: ["#388e3c"],
|
||||||
|
stroke: {
|
||||||
|
lineCap: "round",
|
||||||
|
},
|
||||||
|
markers: {
|
||||||
|
size: 5,
|
||||||
|
},
|
||||||
|
xaxis: {
|
||||||
|
...chartsConfig.xaxis,
|
||||||
|
categories: [
|
||||||
|
"Apr",
|
||||||
|
"May",
|
||||||
|
"Jun",
|
||||||
|
"Jul",
|
||||||
|
"Aug",
|
||||||
|
"Sep",
|
||||||
|
"Oct",
|
||||||
|
"Nov",
|
||||||
|
"Dec",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const completedTasksChart = {
|
||||||
|
...completedTaskChart,
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: "Tasks",
|
||||||
|
data: [50, 40, 300, 220, 500, 250, 400, 230, 500],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const statisticsChartsData = [
|
||||||
|
{
|
||||||
|
color: "white",
|
||||||
|
title: "Website View",
|
||||||
|
description: "Last Campaign Performance",
|
||||||
|
footer: "campaign sent 2 days ago",
|
||||||
|
chart: websiteViewsChart,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: "white",
|
||||||
|
title: "Daily Sales",
|
||||||
|
description: "15% increase in today sales",
|
||||||
|
footer: "updated 4 min ago",
|
||||||
|
chart: dailySalesChart,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: "white",
|
||||||
|
title: "Completed Tasks",
|
||||||
|
description: "Last Campaign Performance",
|
||||||
|
footer: "just updated",
|
||||||
|
chart: completedTasksChart,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default statisticsChartsData;
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { Routes, Route } from "react-router-dom";
|
||||||
|
import {
|
||||||
|
ChartPieIcon,
|
||||||
|
UserIcon,
|
||||||
|
UserPlusIcon,
|
||||||
|
ArrowRightOnRectangleIcon,
|
||||||
|
} from "@heroicons/react/24/solid";
|
||||||
|
import { Navbar, Footer } from "@/widgets/layout";
|
||||||
|
import routes from "@/routes";
|
||||||
|
|
||||||
|
export function Auth() {
|
||||||
|
const navbarRoutes = [
|
||||||
|
{
|
||||||
|
name: "dashboard",
|
||||||
|
path: "/dashboard/home",
|
||||||
|
icon: ChartPieIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "profile",
|
||||||
|
path: "/dashboard/home",
|
||||||
|
icon: UserIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sign up",
|
||||||
|
path: "/auth/sign-up",
|
||||||
|
icon: UserPlusIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sign in",
|
||||||
|
path: "/auth/sign-in",
|
||||||
|
icon: ArrowRightOnRectangleIcon,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative min-h-screen w-full">
|
||||||
|
<Routes>
|
||||||
|
{routes.map(
|
||||||
|
({ layout, pages }) =>
|
||||||
|
layout === "auth" &&
|
||||||
|
pages.map(({ path, element }) => (
|
||||||
|
<Route exact path={path} element={element} />
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</Routes>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Auth.displayName = "/src/layout/Auth.jsx";
|
||||||
|
|
||||||
|
export default Auth;
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { Routes, Route } from "react-router-dom";
|
||||||
|
import { Cog6ToothIcon } from "@heroicons/react/24/solid";
|
||||||
|
import { IconButton } from "@material-tailwind/react";
|
||||||
|
import {
|
||||||
|
Sidenav,
|
||||||
|
DashboardNavbar,
|
||||||
|
Configurator,
|
||||||
|
Footer,
|
||||||
|
} from "@/widgets/layout";
|
||||||
|
import routes from "@/routes";
|
||||||
|
import { useMaterialTailwindController, setOpenConfigurator } from "@/components";
|
||||||
|
|
||||||
|
export function Dashboard() {
|
||||||
|
const [controller, dispatch] = useMaterialTailwindController();
|
||||||
|
const { sidenavType } = controller;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-blue-gray-50/50">
|
||||||
|
<Sidenav
|
||||||
|
routes={routes}
|
||||||
|
brandImg={
|
||||||
|
sidenavType === "dark" ? "/img/logo-ct.png" : "/img/logo-ct-dark.png"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className="p-4 xl:ml-80">
|
||||||
|
<DashboardNavbar />
|
||||||
|
<Configurator />
|
||||||
|
<IconButton
|
||||||
|
size="lg"
|
||||||
|
color="white"
|
||||||
|
className="fixed bottom-8 right-8 z-40 rounded-full shadow-blue-gray-900/10 xl:hidden"
|
||||||
|
ripple={false}
|
||||||
|
onClick={() => setOpenConfigurator(dispatch, true)}
|
||||||
|
>
|
||||||
|
<Cog6ToothIcon className="h-5 w-5" />
|
||||||
|
</IconButton>
|
||||||
|
<Routes>
|
||||||
|
{routes.map(
|
||||||
|
({ layout, pages }) =>
|
||||||
|
layout === "dashboard" &&
|
||||||
|
pages.map(({ path, element }) => (
|
||||||
|
<Route exact path={path} element={element} />
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</Routes>
|
||||||
|
<div className="text-blue-gray-600">
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Dashboard.displayName = "/src/layout/dashboard.jsx";
|
||||||
|
|
||||||
|
export default Dashboard;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "@/layouts/dashboard";
|
||||||
|
export * from "@/layouts/auth";
|
||||||
@@ -1,13 +1,39 @@
|
|||||||
|
/**
|
||||||
|
=========================================================
|
||||||
|
* Material Tailwind Dashboard React - v2.1.0
|
||||||
|
=========================================================
|
||||||
|
* Product Page: https://www.creative-tim.com/product/material-tailwind-dashboard-react
|
||||||
|
* Copyright 2023 Creative Tim (https://www.creative-tim.com)
|
||||||
|
* Licensed under MIT (https://github.com/creativetimofficial/material-tailwind-dashboard-react/blob/main/LICENSE.md)
|
||||||
|
* Coded by Creative Tim
|
||||||
|
=========================================================
|
||||||
|
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
*/
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
|
import App from "./App";
|
||||||
import { BrowserRouter } from "react-router-dom";
|
import { BrowserRouter } from "react-router-dom";
|
||||||
import App from "./App.jsx";
|
import { ThemeProvider } from "@material-tailwind/react";
|
||||||
import './index.css'; // Tailwind CSS
|
import { MaterialTailwindControllerProvider } from "@/components";
|
||||||
|
import "./tailwind.css";
|
||||||
|
import ToastProvider from "@/components/ToastProvider.jsx";
|
||||||
|
import MaintenanceProvider from "@/components/MaintenanceProvider.jsx";
|
||||||
|
import PageProvider from "@/components/PageProvider.jsx";
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root")).render(
|
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<App />
|
<MaintenanceProvider>
|
||||||
|
<ToastProvider>
|
||||||
|
<PageProvider>
|
||||||
|
<ThemeProvider>
|
||||||
|
<MaterialTailwindControllerProvider>
|
||||||
|
<App />
|
||||||
|
</MaterialTailwindControllerProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
</PageProvider>
|
||||||
|
</ToastProvider>
|
||||||
|
</MaintenanceProvider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "@/pages/auth/sign-in";
|
||||||
|
export * from "@/pages/auth/sign-up";
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import {
|
||||||
|
Card,
|
||||||
|
Input,
|
||||||
|
Checkbox,
|
||||||
|
Button,
|
||||||
|
Typography,
|
||||||
|
} from "@material-tailwind/react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
|
||||||
|
export function SignIn() {
|
||||||
|
return (
|
||||||
|
<section className="m-8 flex gap-4">
|
||||||
|
<div className="w-full lg:w-3/5 mt-24">
|
||||||
|
<div className="text-center">
|
||||||
|
<Typography variant="h2" className="font-bold mb-4">Sign In</Typography>
|
||||||
|
<Typography variant="paragraph" color="blue-gray" className="text-lg font-normal">Enter your email and password to Sign In.</Typography>
|
||||||
|
</div>
|
||||||
|
<form className="mt-8 mb-2 mx-auto w-80 max-w-screen-lg lg:w-1/2">
|
||||||
|
<div className="mb-1 flex flex-col gap-6">
|
||||||
|
<Typography variant="small" color="blue-gray" className="-mb-3 font-medium">
|
||||||
|
Your email
|
||||||
|
</Typography>
|
||||||
|
<Input
|
||||||
|
size="lg"
|
||||||
|
placeholder="name@mail.com"
|
||||||
|
className=" !border-t-blue-gray-200 focus:!border-t-gray-900"
|
||||||
|
labelProps={{
|
||||||
|
className: "before:content-none after:content-none",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Typography variant="small" color="blue-gray" className="-mb-3 font-medium">
|
||||||
|
Password
|
||||||
|
</Typography>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
size="lg"
|
||||||
|
placeholder="********"
|
||||||
|
className=" !border-t-blue-gray-200 focus:!border-t-gray-900"
|
||||||
|
labelProps={{
|
||||||
|
className: "before:content-none after:content-none",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Checkbox
|
||||||
|
label={
|
||||||
|
<Typography
|
||||||
|
variant="small"
|
||||||
|
color="gray"
|
||||||
|
className="flex items-center justify-start font-medium"
|
||||||
|
>
|
||||||
|
I agree the
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="font-normal text-black transition-colors hover:text-gray-900 underline"
|
||||||
|
>
|
||||||
|
Terms and Conditions
|
||||||
|
</a>
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
containerProps={{ className: "-ml-2.5" }}
|
||||||
|
/>
|
||||||
|
<Button className="mt-6" fullWidth>
|
||||||
|
Sign In
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between gap-2 mt-6">
|
||||||
|
<Checkbox
|
||||||
|
label={
|
||||||
|
<Typography
|
||||||
|
variant="small"
|
||||||
|
color="gray"
|
||||||
|
className="flex items-center justify-start font-medium"
|
||||||
|
>
|
||||||
|
Subscribe me to newsletter
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
containerProps={{ className: "-ml-2.5" }}
|
||||||
|
/>
|
||||||
|
<Typography variant="small" className="font-medium text-gray-900">
|
||||||
|
<a href="#">
|
||||||
|
Forgot Password
|
||||||
|
</a>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4 mt-8">
|
||||||
|
<Button size="lg" color="white" className="flex items-center gap-2 justify-center shadow-md" fullWidth>
|
||||||
|
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clipPath="url(#clip0_1156_824)">
|
||||||
|
<path d="M16.3442 8.18429C16.3442 7.64047 16.3001 7.09371 16.206 6.55872H8.66016V9.63937H12.9813C12.802 10.6329 12.2258 11.5119 11.3822 12.0704V14.0693H13.9602C15.4741 12.6759 16.3442 10.6182 16.3442 8.18429Z" fill="#4285F4" />
|
||||||
|
<path d="M8.65974 16.0006C10.8174 16.0006 12.637 15.2922 13.9627 14.0693L11.3847 12.0704C10.6675 12.5584 9.7415 12.8347 8.66268 12.8347C6.5756 12.8347 4.80598 11.4266 4.17104 9.53357H1.51074V11.5942C2.86882 14.2956 5.63494 16.0006 8.65974 16.0006Z" fill="#34A853" />
|
||||||
|
<path d="M4.16852 9.53356C3.83341 8.53999 3.83341 7.46411 4.16852 6.47054V4.40991H1.51116C0.376489 6.67043 0.376489 9.33367 1.51116 11.5942L4.16852 9.53356Z" fill="#FBBC04" />
|
||||||
|
<path d="M8.65974 3.16644C9.80029 3.1488 10.9026 3.57798 11.7286 4.36578L14.0127 2.08174C12.5664 0.72367 10.6469 -0.0229773 8.65974 0.000539111C5.63494 0.000539111 2.86882 1.70548 1.51074 4.40987L4.1681 6.4705C4.8001 4.57449 6.57266 3.16644 8.65974 3.16644Z" fill="#EA4335" />
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_1156_824">
|
||||||
|
<rect width="16" height="16" fill="white" transform="translate(0.5)" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
<span>Sign in With Google</span>
|
||||||
|
</Button>
|
||||||
|
<Button size="lg" color="white" className="flex items-center gap-2 justify-center shadow-md" fullWidth>
|
||||||
|
<img src="/img/twitter-logo.svg" height={24} width={24} alt="" />
|
||||||
|
<span>Sign in With Twitter</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Typography variant="paragraph" className="text-center text-blue-gray-500 font-medium mt-4">
|
||||||
|
Not registered?
|
||||||
|
<Link to="/auth/sign-up" className="text-gray-900 ml-1">Create account</Link>
|
||||||
|
</Typography>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div className="w-2/5 h-full hidden lg:block">
|
||||||
|
<img
|
||||||
|
src="/img/pattern.png"
|
||||||
|
className="h-full w-full object-cover rounded-3xl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SignIn;
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import {
|
||||||
|
Card,
|
||||||
|
Input,
|
||||||
|
Checkbox,
|
||||||
|
Button,
|
||||||
|
Typography,
|
||||||
|
} from "@material-tailwind/react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
|
||||||
|
export function SignUp() {
|
||||||
|
return (
|
||||||
|
<section className="m-8 flex">
|
||||||
|
<div className="w-2/5 h-full hidden lg:block">
|
||||||
|
<img
|
||||||
|
src="/img/pattern.png"
|
||||||
|
className="h-full w-full object-cover rounded-3xl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-full lg:w-3/5 flex flex-col items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<Typography variant="h2" className="font-bold mb-4">Join Us Today</Typography>
|
||||||
|
<Typography variant="paragraph" color="blue-gray" className="text-lg font-normal">Enter your email and password to register.</Typography>
|
||||||
|
</div>
|
||||||
|
<form className="mt-8 mb-2 mx-auto w-80 max-w-screen-lg lg:w-1/2">
|
||||||
|
<div className="mb-1 flex flex-col gap-6">
|
||||||
|
<Typography variant="small" color="blue-gray" className="-mb-3 font-medium">
|
||||||
|
Your email
|
||||||
|
</Typography>
|
||||||
|
<Input
|
||||||
|
size="lg"
|
||||||
|
placeholder="name@mail.com"
|
||||||
|
className=" !border-t-blue-gray-200 focus:!border-t-gray-900"
|
||||||
|
labelProps={{
|
||||||
|
className: "before:content-none after:content-none",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Checkbox
|
||||||
|
label={
|
||||||
|
<Typography
|
||||||
|
variant="small"
|
||||||
|
color="gray"
|
||||||
|
className="flex items-center justify-start font-medium"
|
||||||
|
>
|
||||||
|
I agree the
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="font-normal text-black transition-colors hover:text-gray-900 underline"
|
||||||
|
>
|
||||||
|
Terms and Conditions
|
||||||
|
</a>
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
containerProps={{ className: "-ml-2.5" }}
|
||||||
|
/>
|
||||||
|
<Button className="mt-6" fullWidth>
|
||||||
|
Register Now
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="space-y-4 mt-8">
|
||||||
|
<Button size="lg" color="white" className="flex items-center gap-2 justify-center shadow-md" fullWidth>
|
||||||
|
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clipPath="url(#clip0_1156_824)">
|
||||||
|
<path d="M16.3442 8.18429C16.3442 7.64047 16.3001 7.09371 16.206 6.55872H8.66016V9.63937H12.9813C12.802 10.6329 12.2258 11.5119 11.3822 12.0704V14.0693H13.9602C15.4741 12.6759 16.3442 10.6182 16.3442 8.18429Z" fill="#4285F4" />
|
||||||
|
<path d="M8.65974 16.0006C10.8174 16.0006 12.637 15.2922 13.9627 14.0693L11.3847 12.0704C10.6675 12.5584 9.7415 12.8347 8.66268 12.8347C6.5756 12.8347 4.80598 11.4266 4.17104 9.53357H1.51074V11.5942C2.86882 14.2956 5.63494 16.0006 8.65974 16.0006Z" fill="#34A853" />
|
||||||
|
<path d="M4.16852 9.53356C3.83341 8.53999 3.83341 7.46411 4.16852 6.47054V4.40991H1.51116C0.376489 6.67043 0.376489 9.33367 1.51116 11.5942L4.16852 9.53356Z" fill="#FBBC04" />
|
||||||
|
<path d="M8.65974 3.16644C9.80029 3.1488 10.9026 3.57798 11.7286 4.36578L14.0127 2.08174C12.5664 0.72367 10.6469 -0.0229773 8.65974 0.000539111C5.63494 0.000539111 2.86882 1.70548 1.51074 4.40987L4.1681 6.4705C4.8001 4.57449 6.57266 3.16644 8.65974 3.16644Z" fill="#EA4335" />
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_1156_824">
|
||||||
|
<rect width="16" height="16" fill="white" transform="translate(0.5)" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
<span>Sign in With Google</span>
|
||||||
|
</Button>
|
||||||
|
<Button size="lg" color="white" className="flex items-center gap-2 justify-center shadow-md" fullWidth>
|
||||||
|
<img src="/img/twitter-logo.svg" height={24} width={24} alt="" />
|
||||||
|
<span>Sign in With Twitter</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Typography variant="paragraph" className="text-center text-blue-gray-500 font-medium mt-4">
|
||||||
|
Already have an account?
|
||||||
|
<Link to="/auth/sign-in" className="text-gray-900 ml-1">Sign in</Link>
|
||||||
|
</Typography>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SignUp;
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
Typography,
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardBody,
|
||||||
|
IconButton,
|
||||||
|
Menu,
|
||||||
|
MenuHandler,
|
||||||
|
MenuList,
|
||||||
|
MenuItem,
|
||||||
|
Avatar,
|
||||||
|
Tooltip,
|
||||||
|
Progress,
|
||||||
|
} from "@material-tailwind/react";
|
||||||
|
import {
|
||||||
|
EllipsisVerticalIcon,
|
||||||
|
ArrowUpIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
|
import { StatisticsCard } from "@/widgets/cards";
|
||||||
|
import { StatisticsChart } from "@/widgets/charts";
|
||||||
|
import {
|
||||||
|
statisticsCardsData,
|
||||||
|
statisticsChartsData,
|
||||||
|
projectsTableData,
|
||||||
|
ordersOverviewData,
|
||||||
|
} from "@/data";
|
||||||
|
import { CheckCircleIcon, ClockIcon } from "@heroicons/react/24/solid";
|
||||||
|
|
||||||
|
export function Home() {
|
||||||
|
return (
|
||||||
|
<div className="mt-12">
|
||||||
|
<div className="mb-12 grid gap-y-10 gap-x-6 md:grid-cols-2 xl:grid-cols-4">
|
||||||
|
{statisticsCardsData.map(({ icon, title, footer, ...rest }) => (
|
||||||
|
<StatisticsCard
|
||||||
|
key={title}
|
||||||
|
{...rest}
|
||||||
|
title={title}
|
||||||
|
icon={React.createElement(icon, {
|
||||||
|
className: "w-6 h-6 text-white",
|
||||||
|
})}
|
||||||
|
footer={
|
||||||
|
<Typography className="font-normal text-blue-gray-600">
|
||||||
|
<strong className={footer.color}>{footer.value}</strong>
|
||||||
|
{footer.label}
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="mb-6 grid grid-cols-1 gap-y-12 gap-x-6 md:grid-cols-2 xl:grid-cols-3">
|
||||||
|
{statisticsChartsData.map((props) => (
|
||||||
|
<StatisticsChart
|
||||||
|
key={props.title}
|
||||||
|
{...props}
|
||||||
|
footer={
|
||||||
|
<Typography
|
||||||
|
variant="small"
|
||||||
|
className="flex items-center font-normal text-blue-gray-600"
|
||||||
|
>
|
||||||
|
<ClockIcon strokeWidth={2} className="h-4 w-4 text-blue-gray-400" />
|
||||||
|
{props.footer}
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="mb-4 grid grid-cols-1 gap-6 xl:grid-cols-3">
|
||||||
|
<Card className="overflow-hidden xl:col-span-2 border border-blue-gray-100 shadow-sm">
|
||||||
|
<CardHeader
|
||||||
|
floated={false}
|
||||||
|
shadow={false}
|
||||||
|
color="transparent"
|
||||||
|
className="m-0 flex items-center justify-between p-6"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Typography variant="h6" color="blue-gray" className="mb-1">
|
||||||
|
Projects
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="small"
|
||||||
|
className="flex items-center gap-1 font-normal text-blue-gray-600"
|
||||||
|
>
|
||||||
|
<CheckCircleIcon strokeWidth={3} className="h-4 w-4 text-blue-gray-200" />
|
||||||
|
<strong>30 done</strong> this month
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<Menu placement="left-start">
|
||||||
|
<MenuHandler>
|
||||||
|
<IconButton size="sm" variant="text" color="blue-gray">
|
||||||
|
<EllipsisVerticalIcon
|
||||||
|
strokeWidth={3}
|
||||||
|
fill="currenColor"
|
||||||
|
className="h-6 w-6"
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
</MenuHandler>
|
||||||
|
<MenuList>
|
||||||
|
<MenuItem>Action</MenuItem>
|
||||||
|
<MenuItem>Another Action</MenuItem>
|
||||||
|
<MenuItem>Something else here</MenuItem>
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
</CardHeader>
|
||||||
|
<CardBody className="overflow-x-scroll px-0 pt-0 pb-2">
|
||||||
|
<table className="w-full min-w-[640px] table-auto">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{["companies", "members", "budget", "completion"].map(
|
||||||
|
(el) => (
|
||||||
|
<th
|
||||||
|
key={el}
|
||||||
|
className="border-b border-blue-gray-50 py-3 px-6 text-left"
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="small"
|
||||||
|
className="text-[11px] font-medium uppercase text-blue-gray-400"
|
||||||
|
>
|
||||||
|
{el}
|
||||||
|
</Typography>
|
||||||
|
</th>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{projectsTableData.map(
|
||||||
|
({ img, name, members, budget, completion }, key) => {
|
||||||
|
const className = `py-3 px-5 ${
|
||||||
|
key === projectsTableData.length - 1
|
||||||
|
? ""
|
||||||
|
: "border-b border-blue-gray-50"
|
||||||
|
}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={name}>
|
||||||
|
<td className={className}>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Avatar src={img} alt={name} size="sm" />
|
||||||
|
<Typography
|
||||||
|
variant="small"
|
||||||
|
color="blue-gray"
|
||||||
|
className="font-bold"
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className={className}>
|
||||||
|
{members.map(({ img, name }, key) => (
|
||||||
|
<Tooltip key={name} content={name}>
|
||||||
|
<Avatar
|
||||||
|
src={img}
|
||||||
|
alt={name}
|
||||||
|
size="xs"
|
||||||
|
variant="circular"
|
||||||
|
className={`cursor-pointer border-2 border-white ${
|
||||||
|
key === 0 ? "" : "-ml-2.5"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</td>
|
||||||
|
<td className={className}>
|
||||||
|
<Typography
|
||||||
|
variant="small"
|
||||||
|
className="text-xs font-medium text-blue-gray-600"
|
||||||
|
>
|
||||||
|
{budget}
|
||||||
|
</Typography>
|
||||||
|
</td>
|
||||||
|
<td className={className}>
|
||||||
|
<div className="w-10/12">
|
||||||
|
<Typography
|
||||||
|
variant="small"
|
||||||
|
className="mb-1 block text-xs font-medium text-blue-gray-600"
|
||||||
|
>
|
||||||
|
{completion}%
|
||||||
|
</Typography>
|
||||||
|
<Progress
|
||||||
|
value={completion}
|
||||||
|
variant="gradient"
|
||||||
|
color={completion === 100 ? "green" : "blue"}
|
||||||
|
className="h-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
<Card className="border border-blue-gray-100 shadow-sm">
|
||||||
|
<CardHeader
|
||||||
|
floated={false}
|
||||||
|
shadow={false}
|
||||||
|
color="transparent"
|
||||||
|
className="m-0 p-6"
|
||||||
|
>
|
||||||
|
<Typography variant="h6" color="blue-gray" className="mb-2">
|
||||||
|
Orders Overview
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="small"
|
||||||
|
className="flex items-center gap-1 font-normal text-blue-gray-600"
|
||||||
|
>
|
||||||
|
<ArrowUpIcon
|
||||||
|
strokeWidth={3}
|
||||||
|
className="h-3.5 w-3.5 text-green-500"
|
||||||
|
/>
|
||||||
|
<strong>24%</strong> this month
|
||||||
|
</Typography>
|
||||||
|
</CardHeader>
|
||||||
|
<CardBody className="pt-0">
|
||||||
|
{ordersOverviewData.map(
|
||||||
|
({ icon, color, title, description }, key) => (
|
||||||
|
<div key={title} className="flex items-start gap-4 py-3">
|
||||||
|
<div
|
||||||
|
className={`relative p-1 after:absolute after:-bottom-6 after:left-2/4 after:w-0.5 after:-translate-x-2/4 after:bg-blue-gray-50 after:content-[''] ${
|
||||||
|
key === ordersOverviewData.length - 1
|
||||||
|
? "after:h-0"
|
||||||
|
: "after:h-4/6"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{React.createElement(icon, {
|
||||||
|
className: `!w-5 !h-5 ${color}`,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography
|
||||||
|
variant="small"
|
||||||
|
color="blue-gray"
|
||||||
|
className="block font-medium"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
as="span"
|
||||||
|
variant="small"
|
||||||
|
className="text-xs font-medium text-blue-gray-500"
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Home;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from "@/pages/dashboard/stacks";
|
||||||
|
export * from "@/pages/dashboard/maintenance";
|
||||||
|
export * from "@/pages/dashboard/logs";
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
Typography,
|
||||||
|
Alert,
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardBody,
|
||||||
|
} from "@material-tailwind/react";
|
||||||
|
import { InformationCircleIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
|
export function Notifications() {
|
||||||
|
const [showAlerts, setShowAlerts] = React.useState({
|
||||||
|
blue: true,
|
||||||
|
green: true,
|
||||||
|
orange: true,
|
||||||
|
red: true,
|
||||||
|
});
|
||||||
|
const [showAlertsWithIcon, setShowAlertsWithIcon] = React.useState({
|
||||||
|
blue: true,
|
||||||
|
green: true,
|
||||||
|
orange: true,
|
||||||
|
red: true,
|
||||||
|
});
|
||||||
|
const alerts = ["gray", "green", "orange", "red"];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto my-20 flex max-w-screen-lg flex-col gap-8">
|
||||||
|
<Card>
|
||||||
|
<CardHeader
|
||||||
|
color="transparent"
|
||||||
|
floated={false}
|
||||||
|
shadow={false}
|
||||||
|
className="m-0 p-4"
|
||||||
|
>
|
||||||
|
<Typography variant="h5" color="blue-gray">
|
||||||
|
Alerts
|
||||||
|
</Typography>
|
||||||
|
</CardHeader>
|
||||||
|
<CardBody className="flex flex-col gap-4 p-4">
|
||||||
|
{alerts.map((color) => (
|
||||||
|
<Alert
|
||||||
|
key={color}
|
||||||
|
open={showAlerts[color]}
|
||||||
|
color={color}
|
||||||
|
onClose={() => setShowAlerts((current) => ({ ...current, [color]: false }))}
|
||||||
|
>
|
||||||
|
A simple {color} alert with an <a href="#">example link</a>. Give
|
||||||
|
it a click if you like.
|
||||||
|
</Alert>
|
||||||
|
))}
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardHeader
|
||||||
|
color="transparent"
|
||||||
|
floated={false}
|
||||||
|
shadow={false}
|
||||||
|
className="m-0 p-4"
|
||||||
|
>
|
||||||
|
<Typography variant="h5" color="blue-gray">
|
||||||
|
Alerts with Icon
|
||||||
|
</Typography>
|
||||||
|
</CardHeader>
|
||||||
|
<CardBody className="flex flex-col gap-4 p-4">
|
||||||
|
{alerts.map((color) => (
|
||||||
|
<Alert
|
||||||
|
key={color}
|
||||||
|
open={showAlertsWithIcon[color]}
|
||||||
|
color={color}
|
||||||
|
icon={
|
||||||
|
<InformationCircleIcon strokeWidth={2} className="h-6 w-6" />
|
||||||
|
}
|
||||||
|
onClose={() => setShowAlertsWithIcon((current) => ({
|
||||||
|
...current,
|
||||||
|
[color]: false,
|
||||||
|
}))}
|
||||||
|
>
|
||||||
|
A simple {color} alert with an <a href="#">example link</a>. Give
|
||||||
|
it a click if you like.
|
||||||
|
</Alert>
|
||||||
|
))}
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Notifications;
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardBody,
|
||||||
|
CardHeader,
|
||||||
|
CardFooter,
|
||||||
|
Avatar,
|
||||||
|
Typography,
|
||||||
|
Tabs,
|
||||||
|
TabsHeader,
|
||||||
|
Tab,
|
||||||
|
Switch,
|
||||||
|
Tooltip,
|
||||||
|
Button,
|
||||||
|
} from "@material-tailwind/react";
|
||||||
|
import {
|
||||||
|
HomeIcon,
|
||||||
|
ChatBubbleLeftEllipsisIcon,
|
||||||
|
Cog6ToothIcon,
|
||||||
|
PencilIcon,
|
||||||
|
} from "@heroicons/react/24/solid";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { ProfileInfoCard, MessageCard } from "@/widgets/cards";
|
||||||
|
import { platformSettingsData, conversationsData, projectsData } from "@/data";
|
||||||
|
|
||||||
|
export function Profile() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="relative mt-8 h-72 w-full overflow-hidden rounded-xl bg-[url('/img/background-image.png')] bg-cover bg-center">
|
||||||
|
<div className="absolute inset-0 h-full w-full bg-gray-900/75" />
|
||||||
|
</div>
|
||||||
|
<Card className="mx-3 -mt-16 mb-6 lg:mx-4 border border-blue-gray-100">
|
||||||
|
<CardBody className="p-4">
|
||||||
|
<div className="mb-10 flex items-center justify-between flex-wrap gap-6">
|
||||||
|
<div className="flex items-center gap-6">
|
||||||
|
<Avatar
|
||||||
|
src="/img/bruce-mars.jpeg"
|
||||||
|
alt="bruce-mars"
|
||||||
|
size="xl"
|
||||||
|
variant="rounded"
|
||||||
|
className="rounded-lg shadow-lg shadow-blue-gray-500/40"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Typography variant="h5" color="blue-gray" className="mb-1">
|
||||||
|
Richard Davis
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="small"
|
||||||
|
className="font-normal text-blue-gray-600"
|
||||||
|
>
|
||||||
|
CEO / Co-Founder
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-96">
|
||||||
|
<Tabs value="app">
|
||||||
|
<TabsHeader>
|
||||||
|
<Tab value="app">
|
||||||
|
<HomeIcon className="-mt-1 mr-2 inline-block h-5 w-5" />
|
||||||
|
App
|
||||||
|
</Tab>
|
||||||
|
<Tab value="message">
|
||||||
|
<ChatBubbleLeftEllipsisIcon className="-mt-0.5 mr-2 inline-block h-5 w-5" />
|
||||||
|
Message
|
||||||
|
</Tab>
|
||||||
|
<Tab value="settings">
|
||||||
|
<Cog6ToothIcon className="-mt-1 mr-2 inline-block h-5 w-5" />
|
||||||
|
Settings
|
||||||
|
</Tab>
|
||||||
|
</TabsHeader>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="gird-cols-1 mb-12 grid gap-12 px-4 lg:grid-cols-2 xl:grid-cols-3">
|
||||||
|
<div>
|
||||||
|
<Typography variant="h6" color="blue-gray" className="mb-3">
|
||||||
|
Platform Settings
|
||||||
|
</Typography>
|
||||||
|
<div className="flex flex-col gap-12">
|
||||||
|
{platformSettingsData.map(({ title, options }) => (
|
||||||
|
<div key={title}>
|
||||||
|
<Typography className="mb-4 block text-xs font-semibold uppercase text-blue-gray-500">
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
{options.map(({ checked, label }) => (
|
||||||
|
<Switch
|
||||||
|
key={label}
|
||||||
|
id={label}
|
||||||
|
label={label}
|
||||||
|
defaultChecked={checked}
|
||||||
|
labelProps={{
|
||||||
|
className: "text-sm font-normal text-blue-gray-500",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ProfileInfoCard
|
||||||
|
title="Profile Information"
|
||||||
|
description="Hi, I'm Alec Thompson, Decisions: If you can't decide, the answer is no. If two equally difficult paths, choose the one more painful in the short term (pain avoidance is creating an illusion of equality)."
|
||||||
|
details={{
|
||||||
|
"first name": "Alec M. Thompson",
|
||||||
|
mobile: "(44) 123 1234 123",
|
||||||
|
email: "alecthompson@mail.com",
|
||||||
|
location: "USA",
|
||||||
|
social: (
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<i className="fa-brands fa-facebook text-blue-700" />
|
||||||
|
<i className="fa-brands fa-twitter text-blue-400" />
|
||||||
|
<i className="fa-brands fa-instagram text-purple-500" />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
action={
|
||||||
|
<Tooltip content="Edit Profile">
|
||||||
|
<PencilIcon className="h-4 w-4 cursor-pointer text-blue-gray-500" />
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Typography variant="h6" color="blue-gray" className="mb-3">
|
||||||
|
Platform Settings
|
||||||
|
</Typography>
|
||||||
|
<ul className="flex flex-col gap-6">
|
||||||
|
{conversationsData.map((props) => (
|
||||||
|
<MessageCard
|
||||||
|
key={props.name}
|
||||||
|
{...props}
|
||||||
|
action={
|
||||||
|
<Button variant="text" size="sm">
|
||||||
|
reply
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="px-4 pb-4">
|
||||||
|
<Typography variant="h6" color="blue-gray" className="mb-2">
|
||||||
|
Projects
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="small"
|
||||||
|
className="font-normal text-blue-gray-500"
|
||||||
|
>
|
||||||
|
Architects design houses
|
||||||
|
</Typography>
|
||||||
|
<div className="mt-6 grid grid-cols-1 gap-12 md:grid-cols-2 xl:grid-cols-4">
|
||||||
|
{projectsData.map(
|
||||||
|
({ img, title, description, tag, route, members }) => (
|
||||||
|
<Card key={title} color="transparent" shadow={false}>
|
||||||
|
<CardHeader
|
||||||
|
floated={false}
|
||||||
|
color="gray"
|
||||||
|
className="mx-0 mt-0 mb-4 h-64 xl:h-40"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={img}
|
||||||
|
alt={title}
|
||||||
|
className="h-full w-full object-cover"
|
||||||
|
/>
|
||||||
|
</CardHeader>
|
||||||
|
<CardBody className="py-0 px-1">
|
||||||
|
<Typography
|
||||||
|
variant="small"
|
||||||
|
className="font-normal text-blue-gray-500"
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="h5"
|
||||||
|
color="blue-gray"
|
||||||
|
className="mt-1 mb-2"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="small"
|
||||||
|
className="font-normal text-blue-gray-500"
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
</Typography>
|
||||||
|
</CardBody>
|
||||||
|
<CardFooter className="mt-6 flex items-center justify-between py-0 px-1">
|
||||||
|
<Link to={route}>
|
||||||
|
<Button variant="outlined" size="sm">
|
||||||
|
view project
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<div>
|
||||||
|
{members.map(({ img, name }, key) => (
|
||||||
|
<Tooltip key={name} content={name}>
|
||||||
|
<Avatar
|
||||||
|
src={img}
|
||||||
|
alt={name}
|
||||||
|
size="xs"
|
||||||
|
variant="circular"
|
||||||
|
className={`cursor-pointer border-2 border-white ${
|
||||||
|
key === 0 ? "" : "-ml-2.5"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Profile;
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import {
|
||||||
|
Square3Stack3DIcon,
|
||||||
|
WrenchScrewdriverIcon,
|
||||||
|
ListBulletIcon
|
||||||
|
} from "@heroicons/react/24/solid";
|
||||||
|
import { Stacks, Maintenance, Logs } from "@/pages/dashboard";
|
||||||
|
import { SignIn, SignUp } from "@/pages/auth";
|
||||||
|
|
||||||
|
const icon = {
|
||||||
|
className: "w-5 h-5 text-inherit",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const routes = [
|
||||||
|
{
|
||||||
|
layout: "dashboard",
|
||||||
|
pages: [
|
||||||
|
{
|
||||||
|
icon: <Square3Stack3DIcon {...icon} />,
|
||||||
|
name: "stacks",
|
||||||
|
path: "/stacks",
|
||||||
|
element: <Stacks />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <WrenchScrewdriverIcon {...icon} />,
|
||||||
|
name: "wartung",
|
||||||
|
path: "/maintenance",
|
||||||
|
element: <Maintenance />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <ListBulletIcon {...icon} />,
|
||||||
|
name: "logs",
|
||||||
|
path: "/logs",
|
||||||
|
element: <Logs />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// title: "auth pages",
|
||||||
|
// layout: "auth",
|
||||||
|
// pages: [
|
||||||
|
// {
|
||||||
|
// icon: <ServerStackIcon {...icon} />,
|
||||||
|
// name: "sign in",
|
||||||
|
// path: "/sign-in",
|
||||||
|
// element: <SignIn />,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// icon: <RectangleStackIcon {...icon} />,
|
||||||
|
// name: "sign up",
|
||||||
|
// path: "/sign-up",
|
||||||
|
// element: <SignUp />,
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default routes;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from "@/widgets/cards/statistics-card";
|
||||||
|
export * from "@/widgets/cards/profile-info-card";
|
||||||
|
export * from "@/widgets/cards/message-card";
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import PropTypes from "prop-types";
|
||||||
|
import { Avatar, Typography } from "@material-tailwind/react";
|
||||||
|
|
||||||
|
export function MessageCard({ img, name, message, action }) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Avatar
|
||||||
|
src={img}
|
||||||
|
alt={name}
|
||||||
|
variant="rounded"
|
||||||
|
className="shadow-lg shadow-blue-gray-500/25"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Typography
|
||||||
|
variant="small"
|
||||||
|
color="blue-gray"
|
||||||
|
className="mb-1 font-semibold"
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</Typography>
|
||||||
|
<Typography className="text-xs font-normal text-blue-gray-400">
|
||||||
|
{message}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{action}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageCard.defaultProps = {
|
||||||
|
action: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
MessageCard.propTypes = {
|
||||||
|
img: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
message: PropTypes.node.isRequired,
|
||||||
|
action: PropTypes.node,
|
||||||
|
};
|
||||||
|
|
||||||
|
MessageCard.displayName = "/src/widgets/cards/message-card.jsx";
|
||||||
|
|
||||||
|
export default MessageCard;
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import PropTypes from "prop-types";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardBody,
|
||||||
|
Typography,
|
||||||
|
} from "@material-tailwind/react";
|
||||||
|
|
||||||
|
export function ProfileInfoCard({ title, description, details, action }) {
|
||||||
|
return (
|
||||||
|
<Card color="transparent" shadow={false}>
|
||||||
|
<CardHeader
|
||||||
|
color="transparent"
|
||||||
|
shadow={false}
|
||||||
|
floated={false}
|
||||||
|
className="mx-0 mt-0 mb-4 flex items-center justify-between gap-4"
|
||||||
|
>
|
||||||
|
<Typography variant="h6" color="blue-gray">
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
{action}
|
||||||
|
</CardHeader>
|
||||||
|
<CardBody className="p-0">
|
||||||
|
{description && (
|
||||||
|
<Typography
|
||||||
|
variant="small"
|
||||||
|
className="font-normal text-blue-gray-500"
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
{description && details ? (
|
||||||
|
<hr className="my-8 border-blue-gray-50" />
|
||||||
|
) : null}
|
||||||
|
{details && (
|
||||||
|
<ul className="flex flex-col gap-4 p-0">
|
||||||
|
{Object.keys(details).map((el, key) => (
|
||||||
|
<li key={key} className="flex items-center gap-4">
|
||||||
|
<Typography
|
||||||
|
variant="small"
|
||||||
|
color="blue-gray"
|
||||||
|
className="font-semibold capitalize"
|
||||||
|
>
|
||||||
|
{el}:
|
||||||
|
</Typography>
|
||||||
|
{typeof details[el] === "string" ? (
|
||||||
|
<Typography
|
||||||
|
variant="small"
|
||||||
|
className="font-normal text-blue-gray-500"
|
||||||
|
>
|
||||||
|
{details[el]}
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
details[el]
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ProfileInfoCard.defaultProps = {
|
||||||
|
action: null,
|
||||||
|
description: null,
|
||||||
|
details: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
ProfileInfoCard.propTypes = {
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
description: PropTypes.node,
|
||||||
|
details: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
ProfileInfoCard.displayName = "/src/widgets/cards/profile-info-card.jsx";
|
||||||
|
|
||||||
|
export default ProfileInfoCard;
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardBody,
|
||||||
|
CardFooter,
|
||||||
|
Typography,
|
||||||
|
} from "@material-tailwind/react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
export function StatisticsCard({ color, icon, title, value, footer }) {
|
||||||
|
return (
|
||||||
|
<Card className="border border-blue-gray-100 shadow-sm">
|
||||||
|
<CardHeader
|
||||||
|
variant="gradient"
|
||||||
|
color={color}
|
||||||
|
floated={false}
|
||||||
|
shadow={false}
|
||||||
|
className="absolute grid h-12 w-12 place-items-center"
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</CardHeader>
|
||||||
|
<CardBody className="p-4 text-right">
|
||||||
|
<Typography variant="small" className="font-normal text-blue-gray-600">
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h4" color="blue-gray">
|
||||||
|
{value}
|
||||||
|
</Typography>
|
||||||
|
</CardBody>
|
||||||
|
{footer && (
|
||||||
|
<CardFooter className="border-t border-blue-gray-50 p-4">
|
||||||
|
{footer}
|
||||||
|
</CardFooter>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
StatisticsCard.defaultProps = {
|
||||||
|
color: "blue",
|
||||||
|
footer: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
StatisticsCard.propTypes = {
|
||||||
|
color: PropTypes.oneOf([
|
||||||
|
"white",
|
||||||
|
"blue-gray",
|
||||||
|
"gray",
|
||||||
|
"brown",
|
||||||
|
"deep-orange",
|
||||||
|
"orange",
|
||||||
|
"amber",
|
||||||
|
"yellow",
|
||||||
|
"lime",
|
||||||
|
"light-green",
|
||||||
|
"green",
|
||||||
|
"teal",
|
||||||
|
"cyan",
|
||||||
|
"light-blue",
|
||||||
|
"blue",
|
||||||
|
"indigo",
|
||||||
|
"deep-purple",
|
||||||
|
"purple",
|
||||||
|
"pink",
|
||||||
|
"red",
|
||||||
|
]),
|
||||||
|
icon: PropTypes.node.isRequired,
|
||||||
|
title: PropTypes.node.isRequired,
|
||||||
|
value: PropTypes.node.isRequired,
|
||||||
|
footer: PropTypes.node,
|
||||||
|
};
|
||||||
|
|
||||||
|
StatisticsCard.displayName = "/src/widgets/cards/statistics-card.jsx";
|
||||||
|
|
||||||
|
export default StatisticsCard;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "@/widgets/charts/statistics-chart";
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardBody,
|
||||||
|
CardFooter,
|
||||||
|
Typography,
|
||||||
|
} from "@material-tailwind/react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import Chart from "react-apexcharts";
|
||||||
|
|
||||||
|
export function StatisticsChart({ color, chart, title, description, footer }) {
|
||||||
|
return (
|
||||||
|
<Card className="border border-blue-gray-100 shadow-sm">
|
||||||
|
<CardHeader variant="gradient" color={color} floated={false} shadow={false}>
|
||||||
|
<Chart {...chart} />
|
||||||
|
</CardHeader>
|
||||||
|
<CardBody className="px-6 pt-0">
|
||||||
|
<Typography variant="h6" color="blue-gray">
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="small" className="font-normal text-blue-gray-600">
|
||||||
|
{description}
|
||||||
|
</Typography>
|
||||||
|
</CardBody>
|
||||||
|
{footer && (
|
||||||
|
<CardFooter className="border-t border-blue-gray-50 px-6 py-5">
|
||||||
|
{footer}
|
||||||
|
</CardFooter>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
StatisticsChart.defaultProps = {
|
||||||
|
color: "blue",
|
||||||
|
footer: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
StatisticsChart.propTypes = {
|
||||||
|
color: PropTypes.oneOf([
|
||||||
|
"white",
|
||||||
|
"blue-gray",
|
||||||
|
"gray",
|
||||||
|
"brown",
|
||||||
|
"deep-orange",
|
||||||
|
"orange",
|
||||||
|
"amber",
|
||||||
|
"yellow",
|
||||||
|
"lime",
|
||||||
|
"light-green",
|
||||||
|
"green",
|
||||||
|
"teal",
|
||||||
|
"cyan",
|
||||||
|
"light-blue",
|
||||||
|
"blue",
|
||||||
|
"indigo",
|
||||||
|
"deep-purple",
|
||||||
|
"purple",
|
||||||
|
"pink",
|
||||||
|
"red",
|
||||||
|
]),
|
||||||
|
chart: PropTypes.object.isRequired,
|
||||||
|
title: PropTypes.node.isRequired,
|
||||||
|
description: PropTypes.node.isRequired,
|
||||||
|
footer: PropTypes.node,
|
||||||
|
};
|
||||||
|
|
||||||
|
StatisticsChart.displayName = "/src/widgets/charts/statistics-chart.jsx";
|
||||||
|
|
||||||
|
export default StatisticsChart;
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { XMarkIcon } from "@heroicons/react/24/outline";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
IconButton,
|
||||||
|
Switch,
|
||||||
|
Typography,
|
||||||
|
Chip,
|
||||||
|
} from "@material-tailwind/react";
|
||||||
|
import {
|
||||||
|
useMaterialTailwindController,
|
||||||
|
setOpenConfigurator,
|
||||||
|
setSidenavColor,
|
||||||
|
setSidenavType,
|
||||||
|
setFixedNavbar,
|
||||||
|
} from "@/components";
|
||||||
|
|
||||||
|
function formatNumber(number, decPlaces) {
|
||||||
|
decPlaces = Math.pow(10, decPlaces);
|
||||||
|
|
||||||
|
const abbrev = ["K", "M", "B", "T"];
|
||||||
|
|
||||||
|
for (let i = abbrev.length - 1; i >= 0; i--) {
|
||||||
|
var size = Math.pow(10, (i + 1) * 3);
|
||||||
|
|
||||||
|
if (size <= number) {
|
||||||
|
number = Math.round((number * decPlaces) / size) / decPlaces;
|
||||||
|
|
||||||
|
if (number == 1000 && i < abbrev.length - 1) {
|
||||||
|
number = 1;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
number += abbrev[i];
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Configurator() {
|
||||||
|
const [controller, dispatch] = useMaterialTailwindController();
|
||||||
|
const { openConfigurator, sidenavColor, sidenavType, fixedNavbar } =
|
||||||
|
controller;
|
||||||
|
const [stars, setStars] = React.useState(0);
|
||||||
|
|
||||||
|
const sidenavColors = {
|
||||||
|
dark: "from-black to-black border-gray-200",
|
||||||
|
green: "from-green-400 to-green-600",
|
||||||
|
orange: "from-orange-400 to-orange-600",
|
||||||
|
red: "from-red-400 to-red-600",
|
||||||
|
pink: "from-pink-400 to-pink-600",
|
||||||
|
};
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const stars = fetch(
|
||||||
|
"https://api.github.com/repos/creativetimofficial/material-tailwind-dashboard-react"
|
||||||
|
)
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => setStars(formatNumber(data.stargazers_count, 1)));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<aside
|
||||||
|
className={`fixed top-0 right-0 z-50 h-screen w-96 bg-white px-2.5 shadow-lg transition-transform duration-300 ${
|
||||||
|
openConfigurator ? "translate-x-0" : "translate-x-96"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between px-6 pt-8 pb-6">
|
||||||
|
<div>
|
||||||
|
<Typography variant="h5" color="blue-gray">
|
||||||
|
Dashboard Configurator
|
||||||
|
</Typography>
|
||||||
|
<Typography className="font-normal text-blue-gray-600">
|
||||||
|
See our dashboard options.
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<IconButton
|
||||||
|
variant="text"
|
||||||
|
color="blue-gray"
|
||||||
|
onClick={() => setOpenConfigurator(dispatch, false)}
|
||||||
|
>
|
||||||
|
<XMarkIcon strokeWidth={2.5} className="h-5 w-5" />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
<div className="py-4 px-6">
|
||||||
|
<div className="mb-12">
|
||||||
|
<Typography variant="h6" color="blue-gray">
|
||||||
|
Sidenav Colors
|
||||||
|
</Typography>
|
||||||
|
<div className="mt-3 flex items-center gap-2">
|
||||||
|
{Object.keys(sidenavColors).map((color) => (
|
||||||
|
<span
|
||||||
|
key={color}
|
||||||
|
className={`h-6 w-6 cursor-pointer rounded-full border bg-gradient-to-br transition-transform hover:scale-105 ${
|
||||||
|
sidenavColors[color]
|
||||||
|
} ${
|
||||||
|
sidenavColor === color ? "border-black" : "border-transparent"
|
||||||
|
}`}
|
||||||
|
onClick={() => setSidenavColor(dispatch, color)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mb-12">
|
||||||
|
<hr />
|
||||||
|
<div className="flex items-center justify-between py-5">
|
||||||
|
<Typography variant="h6" color="blue-gray">
|
||||||
|
Navbar Fixed
|
||||||
|
</Typography>
|
||||||
|
<Switch defaultChecked
|
||||||
|
id="navbar-fixed"
|
||||||
|
value={fixedNavbar}
|
||||||
|
onChange={() => setFixedNavbar(dispatch, !fixedNavbar)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Configurator.displayName = "/src/widgets/layout/configurator.jsx";
|
||||||
|
|
||||||
|
export default Configurator;
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
import { useLocation, Link } from "react-router-dom";
|
||||||
|
import {
|
||||||
|
Navbar,
|
||||||
|
Typography,
|
||||||
|
Button,
|
||||||
|
IconButton,
|
||||||
|
Breadcrumbs,
|
||||||
|
Menu,
|
||||||
|
MenuHandler,
|
||||||
|
MenuList,
|
||||||
|
MenuItem,
|
||||||
|
Avatar,
|
||||||
|
} from "@material-tailwind/react";
|
||||||
|
import {
|
||||||
|
UserCircleIcon,
|
||||||
|
Cog6ToothIcon,
|
||||||
|
BellIcon,
|
||||||
|
ClockIcon,
|
||||||
|
CreditCardIcon,
|
||||||
|
Bars3Icon,
|
||||||
|
} from "@heroicons/react/24/solid";
|
||||||
|
import {
|
||||||
|
useMaterialTailwindController,
|
||||||
|
setOpenConfigurator,
|
||||||
|
setOpenSidenav,
|
||||||
|
} from "@/components";
|
||||||
|
|
||||||
|
export function DashboardNavbar() {
|
||||||
|
const [controller, dispatch] = useMaterialTailwindController();
|
||||||
|
const { fixedNavbar, openSidenav } = controller;
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
const [layout, page] = pathname.split("/").filter((el) => el !== "");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Navbar
|
||||||
|
color={fixedNavbar ? "white" : "transparent"}
|
||||||
|
className={`rounded-xl transition-all ${
|
||||||
|
fixedNavbar
|
||||||
|
? "sticky top-4 z-40 py-3 shadow-md shadow-blue-gray-500/5"
|
||||||
|
: "px-0 py-1"
|
||||||
|
}`}
|
||||||
|
fullWidth
|
||||||
|
blurred={fixedNavbar}
|
||||||
|
>
|
||||||
|
<div className="flex justify-between gap-6 flex-row items-center">
|
||||||
|
<div className="capitalize">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
variant="text"
|
||||||
|
color="blue-gray"
|
||||||
|
className="grid xl:hidden"
|
||||||
|
onClick={() => setOpenSidenav(dispatch, !openSidenav)}
|
||||||
|
>
|
||||||
|
<Bars3Icon strokeWidth={3} className="h-6 w-6 text-blue-gray-500" />
|
||||||
|
</IconButton>
|
||||||
|
{/* <Link to="/auth/sign-in">
|
||||||
|
<Button
|
||||||
|
variant="text"
|
||||||
|
color="blue-gray"
|
||||||
|
className="hidden items-center gap-1 px-4 xl:flex normal-case"
|
||||||
|
>
|
||||||
|
<UserCircleIcon className="h-5 w-5 text-blue-gray-500" />
|
||||||
|
Sign In
|
||||||
|
</Button>
|
||||||
|
<IconButton
|
||||||
|
variant="text"
|
||||||
|
color="blue-gray"
|
||||||
|
className="grid xl:hidden"
|
||||||
|
>
|
||||||
|
<UserCircleIcon className="h-5 w-5 text-blue-gray-500" />
|
||||||
|
</IconButton>
|
||||||
|
</Link>
|
||||||
|
<Menu>
|
||||||
|
<MenuHandler>
|
||||||
|
<IconButton variant="text" color="blue-gray">
|
||||||
|
<BellIcon className="h-5 w-5 text-blue-gray-500" />
|
||||||
|
</IconButton>
|
||||||
|
</MenuHandler>
|
||||||
|
<MenuList className="w-max border-0">
|
||||||
|
<MenuItem className="flex items-center gap-3">
|
||||||
|
<Avatar
|
||||||
|
src="https://demos.creative-tim.com/material-dashboard/assets/img/team-2.jpg"
|
||||||
|
alt="item-1"
|
||||||
|
size="sm"
|
||||||
|
variant="circular"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Typography
|
||||||
|
variant="small"
|
||||||
|
color="blue-gray"
|
||||||
|
className="mb-1 font-normal"
|
||||||
|
>
|
||||||
|
<strong>New message</strong> from Laur
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="small"
|
||||||
|
color="blue-gray"
|
||||||
|
className="flex items-center gap-1 text-xs font-normal opacity-60"
|
||||||
|
>
|
||||||
|
<ClockIcon className="h-3.5 w-3.5" /> 13 minutes ago
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem className="flex items-center gap-4">
|
||||||
|
<Avatar
|
||||||
|
src="https://demos.creative-tim.com/material-dashboard/assets/img/small-logos/logo-spotify.svg"
|
||||||
|
alt="item-1"
|
||||||
|
size="sm"
|
||||||
|
variant="circular"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Typography
|
||||||
|
variant="small"
|
||||||
|
color="blue-gray"
|
||||||
|
className="mb-1 font-normal"
|
||||||
|
>
|
||||||
|
<strong>New album</strong> by Travis Scott
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="small"
|
||||||
|
color="blue-gray"
|
||||||
|
className="flex items-center gap-1 text-xs font-normal opacity-60"
|
||||||
|
>
|
||||||
|
<ClockIcon className="h-3.5 w-3.5" /> 1 day ago
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem className="flex items-center gap-4">
|
||||||
|
<div className="grid h-9 w-9 place-items-center rounded-full bg-gradient-to-tr from-blue-gray-800 to-blue-gray-900">
|
||||||
|
<CreditCardIcon className="h-4 w-4 text-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography
|
||||||
|
variant="small"
|
||||||
|
color="blue-gray"
|
||||||
|
className="mb-1 font-normal"
|
||||||
|
>
|
||||||
|
Payment successfully completed
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="small"
|
||||||
|
color="blue-gray"
|
||||||
|
className="flex items-center gap-1 text-xs font-normal opacity-60"
|
||||||
|
>
|
||||||
|
<ClockIcon className="h-3.5 w-3.5" /> 2 days ago
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
</MenuList>
|
||||||
|
</Menu> */}
|
||||||
|
<IconButton
|
||||||
|
variant="text"
|
||||||
|
color="blue-gray"
|
||||||
|
onClick={() => setOpenConfigurator(dispatch, true)}
|
||||||
|
>
|
||||||
|
<Cog6ToothIcon className="h-5 w-5 text-blue-gray-500 " />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Navbar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DashboardNavbar.displayName = "/src/widgets/layout/dashboard-navbar.jsx";
|
||||||
|
|
||||||
|
export default DashboardNavbar;
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import PropTypes from "prop-types";
|
||||||
|
import { Typography } from "@material-tailwind/react";
|
||||||
|
import { HeartIcon } from "@heroicons/react/24/solid";
|
||||||
|
|
||||||
|
export function Footer() {
|
||||||
|
const year = new Date().getFullYear();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<footer>
|
||||||
|
<div className="flex w-full flex-wrap items-center justify-center md:justify-end">
|
||||||
|
<Typography variant="small" className="mt-1 text-xs text-stormGrey-500 block antialiased font-sans">
|
||||||
|
StackPulse: © {year} by Michael Böhmländer
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Footer.displayName = "/src/widgets/layout/footer.jsx";
|
||||||
|
export default Footer;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export * from "@/widgets/layout/sidenav";
|
||||||
|
export * from "@/widgets/layout/dashboard-navbar";
|
||||||
|
export * from "@/widgets/layout/configurator";
|
||||||
|
export * from "@/widgets/layout/footer";
|
||||||
|
export * from "@/widgets/layout/navbar";
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import React from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import {
|
||||||
|
Navbar as MTNavbar,
|
||||||
|
Collapse,
|
||||||
|
Typography,
|
||||||
|
Button,
|
||||||
|
IconButton,
|
||||||
|
} from "@material-tailwind/react";
|
||||||
|
import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
|
export function Navbar({ brandName, routes, action }) {
|
||||||
|
const [openNav, setOpenNav] = React.useState(false);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
window.addEventListener(
|
||||||
|
"resize",
|
||||||
|
() => window.innerWidth >= 960 && setOpenNav(false)
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const navList = (
|
||||||
|
<ul className="mb-4 mt-2 flex flex-col gap-2 lg:mb-0 lg:mt-0 lg:flex-row lg:items-center lg:gap-6">
|
||||||
|
{routes.map(({ name, path, icon }) => (
|
||||||
|
<Typography
|
||||||
|
key={name}
|
||||||
|
as="li"
|
||||||
|
variant="small"
|
||||||
|
color="blue-gray"
|
||||||
|
className="capitalize"
|
||||||
|
>
|
||||||
|
<Link to={path} className="flex items-center gap-1 p-1 font-normal">
|
||||||
|
{icon &&
|
||||||
|
React.createElement(icon, {
|
||||||
|
className: "w-[18px] h-[18px] opacity-50 mr-1",
|
||||||
|
})}
|
||||||
|
{name}
|
||||||
|
</Link>
|
||||||
|
</Typography>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MTNavbar className="p-3">
|
||||||
|
<div className="container mx-auto flex items-center justify-between text-blue-gray-900">
|
||||||
|
<Link to="/">
|
||||||
|
<Typography
|
||||||
|
variant="small"
|
||||||
|
className="mr-4 ml-2 cursor-pointer py-1.5 font-bold"
|
||||||
|
>
|
||||||
|
{brandName}
|
||||||
|
</Typography>
|
||||||
|
</Link>
|
||||||
|
<div className="hidden lg:block">{navList}</div>
|
||||||
|
{React.cloneElement(action, {
|
||||||
|
className: "hidden lg:inline-block",
|
||||||
|
})}
|
||||||
|
<IconButton
|
||||||
|
variant="text"
|
||||||
|
size="sm"
|
||||||
|
className="hml-auto text-inherit hover:bg-transparent focus:bg-transparent active:bg-transparent lg:hidden"
|
||||||
|
onClick={() => setOpenNav(!openNav)}
|
||||||
|
>
|
||||||
|
{openNav ? (
|
||||||
|
<XMarkIcon strokeWidth={2} className="h-6 w-6" />
|
||||||
|
) : (
|
||||||
|
<Bars3Icon strokeWidth={2} className="h-6 w-6" />
|
||||||
|
)}
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
<Collapse open={openNav}>
|
||||||
|
<div className="container mx-auto">
|
||||||
|
{navList}
|
||||||
|
{React.cloneElement(action, {
|
||||||
|
className: "w-full block lg:hidden",
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Collapse>
|
||||||
|
</MTNavbar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Navbar.defaultProps = {
|
||||||
|
brandName: "Material Tailwind React",
|
||||||
|
action: (
|
||||||
|
<a
|
||||||
|
href="https://www.creative-tim.com/product/material-tailwind-dashboard-react"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<Button variant="gradient" size="sm" fullWidth>
|
||||||
|
free download
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
Navbar.propTypes = {
|
||||||
|
brandName: PropTypes.string,
|
||||||
|
routes: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
action: PropTypes.node,
|
||||||
|
};
|
||||||
|
|
||||||
|
Navbar.displayName = "/src/widgets/layout/navbar.jsx";
|
||||||
|
|
||||||
|
export default Navbar;
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import PropTypes from "prop-types";
|
||||||
|
import { Link, NavLink } from "react-router-dom";
|
||||||
|
import { XMarkIcon } from "@heroicons/react/24/outline";
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Button,
|
||||||
|
IconButton,
|
||||||
|
Typography,
|
||||||
|
} from "@material-tailwind/react";
|
||||||
|
import { useMaterialTailwindController, setOpenSidenav } from "@/components";
|
||||||
|
import logo from "@/assets/images/stackpulse.png";
|
||||||
|
|
||||||
|
export function Sidenav({ brandImg, brandName, routes }) {
|
||||||
|
const [controller, dispatch] = useMaterialTailwindController();
|
||||||
|
const { sidenavColor, sidenavType, openSidenav } = controller;
|
||||||
|
const sidenavTypes = {
|
||||||
|
dark: "bg-gradient-to-br from-gray-800 to-gray-900",
|
||||||
|
white: "bg-white shadow-sm",
|
||||||
|
transparent: "bg-transparent",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<aside
|
||||||
|
className={`${sidenavTypes[sidenavType]} ${openSidenav ? "translate-x-0" : "-translate-x-80"
|
||||||
|
} fixed inset-0 z-50 my-4 ml-4 h-[calc(100vh-32px)] w-72 rounded-xl transition-transform duration-300 xl:translate-x-0 border border-blue-gray-100`}
|
||||||
|
>
|
||||||
|
<div className="relative py-6 px-8 text-right">
|
||||||
|
<img src={logo} alt="StackPulse" className="w-auto" /><span className="mt-1 text-xs text-stormGrey-500 block antialiased font-sans">v0.4</span>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
variant="text"
|
||||||
|
size="sm"
|
||||||
|
ripple={false}
|
||||||
|
className="absolute right-0 top-0 grid rounded-br-none rounded-tl-none"
|
||||||
|
onClick={() => setOpenSidenav(dispatch, false)}
|
||||||
|
>
|
||||||
|
<XMarkIcon strokeWidth={2.5} className="h-5 w-5 text-black" />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
<div className="m-4">
|
||||||
|
{routes.map(({ layout, title, pages }, key) => (
|
||||||
|
<ul key={key} className="mb-4 flex flex-col gap-1">
|
||||||
|
{title && (
|
||||||
|
<li className="mx-3.5 mt-4 mb-2">
|
||||||
|
<Typography
|
||||||
|
variant="small"
|
||||||
|
color={sidenavType === "dark" ? "white" : "blue-gray"}
|
||||||
|
className="font-black uppercase opacity-75"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{pages.map(({ icon, name, path }) => (
|
||||||
|
<li key={name}>
|
||||||
|
<NavLink to={`/${layout}${path}`}>
|
||||||
|
{({ isActive }) => (
|
||||||
|
<Button
|
||||||
|
variant={isActive ? "gradient" : "text"}
|
||||||
|
color={
|
||||||
|
isActive
|
||||||
|
? sidenavColor
|
||||||
|
: sidenavType === "dark"
|
||||||
|
? "white"
|
||||||
|
: "blue-gray"
|
||||||
|
}
|
||||||
|
className="flex items-center gap-4 px-4 capitalize"
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
<Typography
|
||||||
|
color="inherit"
|
||||||
|
className="font-medium capitalize"
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</Typography>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Sidenav.defaultProps = {
|
||||||
|
brandImg: "/img/logo-ct.png",
|
||||||
|
brandName: "Material Tailwind React",
|
||||||
|
};
|
||||||
|
|
||||||
|
Sidenav.propTypes = {
|
||||||
|
brandImg: PropTypes.string,
|
||||||
|
brandName: PropTypes.string,
|
||||||
|
routes: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
Sidenav.displayName = "/src/widgets/layout/sidnave.jsx";
|
||||||
|
|
||||||
|
export default Sidenav;
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
const withMT = require("@material-tailwind/react/utils/withMT");
|
||||||
|
const colors = require("tailwindcss/colors");
|
||||||
|
|
||||||
|
module.exports = withMT({
|
||||||
|
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
sky: colors.sky,
|
||||||
|
green: colors.green,
|
||||||
|
slate: colors.slate,
|
||||||
|
orange: colors.orange, // falls du z. B. text-orange-300 brauchst
|
||||||
|
auroraTeal: {50:'#f0fafb',100:'#e0f5f7',200:'#c3ecef',300:'#9de0e4',400:'#6ad3d8',500:'#38c6cc',600:'#2aa9b0',700:'#1f8d94',800:'#166e76',900:'#0c4f58'},
|
||||||
|
sunsetCoral: {50:'#fff6f5',100:'#ffece9',200:'#ffd7d3',300:'#ffb8b5',400:'#ff9189',500:'#ff6a5d',600:'#e65048',700:'#b93d38',800:'#8a2c28',900:'#5c1c1a'},
|
||||||
|
lavenderSmoke: {50:'#faf8fc',100:'#f5f1f9',200:'#ece2f3',300:'#dac4e6',400:'#b88acf',500:'#9670b8',600:'#7a5894',700:'#5d4373',800:'#412f52',900:'#291e38'},
|
||||||
|
mossGreen: {50:'#f5faf2',100:'#ebf5e5',200:'#d9ebca',300:'#b8d99e',400:'#8cc56f',500:'#60b240',600:'#4d8f33',700:'#3b6b27',800:'#2a491d',900:'#1c3012'},
|
||||||
|
deepOceanBlue: {50:'#f4faff',100:'#e9f5ff',200:'#cfe6ff',300:'#add1ff',400:'#7ab9ff',500:'#479fff',600:'#3c86e6',700:'#305eaf',800:'#23437f',900:'#162a50'},
|
||||||
|
warmAmberGlow: {50:'#fff9f2',100:'#fff3e5',200:'#ffe6cc',300:'#ffcc99',400:'#ffad4d',500:'#ff8f00',600:'#e67600',700:'#b35a00',800:'#804000',900:'#4d2600'},
|
||||||
|
peachMist: {50:'#fff8f7',100:'#fff0ed',200:'#ffe1db',300:'#ffc2b7',400:'#ff9a87',500:'#ff7257',600:'#e65f4b',700:'#b34938',800:'#803622',900:'#4d230f'},
|
||||||
|
slatePurple: {50:'#f7f5fa',100:'#efe9f5',200:'#ded1ea',300:'#bdb3d5',400:'#9584b5',500:'#6d5795',600:'#5c4a7c',700:'#473861',800:'#322648',900:'#1f1530'},
|
||||||
|
mintTea: {50:'#f2fcfa',100:'#e5f9f6',200:'#ccefee',300:'#99dfdc',400:'#66cfc9',500:'#33bfb6',600:'#2aa395',700:'#228174',800:'#195953',900:'#103330'},
|
||||||
|
copperRust: {50:'#faf7f6',100:'#f5efee',200:'#ebe0dc',300:'#d6c1b8',400:'#b28a86',500:'#8d4d55',600:'#7a444b',700:'#5f3338',800:'#442426',900:'#2d1718'},
|
||||||
|
// Neue 10
|
||||||
|
arcticBlue: {50:'#f2f9fd',100:'#e6f3fb',200:'#cce6f7',300:'#99ceef',400:'#66b6e7',500:'#339ede',600:'#1f86c3',700:'#16699b',800:'#0f4b6f',900:'#082d43'},
|
||||||
|
roseQuartz: {50:'#fdf7f9',100:'#faeff3',200:'#f5dfe6',300:'#ecbfd0',400:'#e08dad',500:'#d45b89',600:'#b94771',700:'#8e3556',800:'#63233b',900:'#391324'},
|
||||||
|
citrusPunch: {50:'#fffdf6',100:'#fffbe9',200:'#fff5cc',300:'#ffeb99',400:'#ffe066',500:'#ffd633',600:'#e6bd00',700:'#b39300',800:'#806900',900:'#4d3f00'},
|
||||||
|
midnightPlum: {50:'#faf7fb',100:'#f4ecf6',200:'#e4cfee',300:'#cba0de',400:'#a869cc',500:'#8431b9',600:'#6d299b',700:'#521f74',800:'#38154d',900:'#1f0b28'},
|
||||||
|
sandstone: {50:'#fbf9f7',100:'#f6f2ee',200:'#ece3da',300:'#d8c6b3',400:'#c0a27f',500:'#a87e4b',600:'#906a3e',700:'#6b4f2f',800:'#47341f',900:'#241a10'},
|
||||||
|
emeraldMist: {50:'#f3faf7',100:'#e7f6ef',200:'#ccecdc',300:'#9ddabf',400:'#6ec8a3',500:'#3fb586',600:'#359771',700:'#2a765a',800:'#1f5442',900:'#13332a'},
|
||||||
|
velvetRed: {50:'#fdf7f7',100:'#faeeee',200:'#f4d6d6',300:'#e7a8a8',400:'#d56666',500:'#c32424',600:'#a61e1e',700:'#801717',800:'#590f0f',900:'#330808'},
|
||||||
|
stormGrey: {50:'#f7f8fa',100:'#eef0f3',200:'#d9dce2',300:'#b6bbc8',400:'#8b91a4',500:'#606780',600:'#4f556b',700:'#3e4456',800:'#2c313f',900:'#1b1e29'},
|
||||||
|
neonLime: {50:'#f7ffe6',100:'#eeffc9',200:'#dbff96',300:'#c2ff52',400:'#a8ff0e',500:'#8de600',600:'#74bf00',700:'#5b9900',800:'#426f00',900:'#294600'},
|
||||||
|
skyMauve: {50:'#faf8fc',100:'#f4f0f9',200:'#e5dbf2',300:'#c9b2e6',400:'#a27dd4',500:'#7b48c2',600:'#633aa0',700:'#4b2b7c',800:'#341d58',900:'#1e1035'},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
});
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from "vite";
|
||||||
import react from '@vitejs/plugin-react';
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
resolve: {
|
||||||
|
alias: [{ find: "@", replacement: "/src" }],
|
||||||
|
},
|
||||||
server: {
|
server: {
|
||||||
host: true, // Hört auf allen Netzwerk-Interfaces, kein localhost notwendig
|
host: true, // Hört auf allen Netzwerk-Interfaces, kein localhost notwendig
|
||||||
port: 5173, // optional, Standardport für Vite
|
port: 5173, // optional, Standardport für Vite
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>StackPulse</title>
|
||||||
|
|
||||||
|
<script type="module" crossorigin src="/assets/index-e96128a6.js"></script>
|
||||||
|
<link rel="stylesheet" href="/assets/index-736c8c6c.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>StackPulse</title>
|
||||||
|
<link rel="stylesheet" href="/src/index.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -8,18 +8,27 @@
|
|||||||
"name": "stackpulse-frontend",
|
"name": "stackpulse-frontend",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.5",
|
"@heroicons/react": "2.0.18",
|
||||||
"react": "^18.2.0",
|
"@material-tailwind/react": "2.1.4",
|
||||||
"react-dom": "^18.2.0",
|
"apexcharts": "3.44.0",
|
||||||
"react-router-dom": "^6.26.2",
|
"axios": "1.7.5",
|
||||||
"socket.io-client": "^4.8.1"
|
"prop-types": "15.8.1",
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-apexcharts": "1.4.1",
|
||||||
|
"react-dom": "18.2.0",
|
||||||
|
"react-router-dom": "6.17.0",
|
||||||
|
"socket.io-client": "4.8.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-react": "^4.0.0",
|
"@types/react": "18.2.31",
|
||||||
"autoprefixer": "^10.4.21",
|
"@types/react-dom": "18.2.14",
|
||||||
"postcss": "^8.5.6",
|
"@vitejs/plugin-react": "4.1.0",
|
||||||
"tailwindcss": "^3.4.17",
|
"autoprefixer": "10.4.16",
|
||||||
"vite": "^4.4.9"
|
"postcss": "8.4.31",
|
||||||
|
"prettier": "3.0.3",
|
||||||
|
"prettier-plugin-tailwindcss": "0.5.6",
|
||||||
|
"tailwindcss": "3.3.4",
|
||||||
|
"vite": "4.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@alloc/quick-lru": {
|
"node_modules/@alloc/quick-lru": {
|
||||||
@@ -310,6 +319,21 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@emotion/is-prop-valid": {
|
||||||
|
"version": "0.8.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz",
|
||||||
|
"integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emotion/memoize": "0.7.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@emotion/memoize": {
|
||||||
|
"version": "0.7.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz",
|
||||||
|
"integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/@esbuild/android-arm": {
|
"node_modules/@esbuild/android-arm": {
|
||||||
"version": "0.18.20",
|
"version": "0.18.20",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
|
||||||
@@ -662,6 +686,62 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@floating-ui/core": {
|
||||||
|
"version": "1.7.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
|
||||||
|
"integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/utils": "^0.2.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/dom": {
|
||||||
|
"version": "1.7.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
|
||||||
|
"integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/core": "^1.7.3",
|
||||||
|
"@floating-ui/utils": "^0.2.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/react": {
|
||||||
|
"version": "0.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.19.0.tgz",
|
||||||
|
"integrity": "sha512-fgYvN4ksCi5OvmPXkyOT8o5a8PSKHMzPHt+9mR6KYWdF16IAjWRLZPAAziI2sznaWT23drRFrYw64wdvYqqaQw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/react-dom": "^1.2.2",
|
||||||
|
"aria-hidden": "^1.1.3",
|
||||||
|
"tabbable": "^6.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/react-dom": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-htwHm67Ji5E/pROEAr7f8IKFShuiCKHwUC/UY4vC3I5jiSvGFAYnSYiZO5MlGmads+QqvUkR9ANHEguGrDv72g==",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/dom": "^1.2.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/utils": {
|
||||||
|
"version": "0.2.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
|
||||||
|
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="
|
||||||
|
},
|
||||||
|
"node_modules/@heroicons/react": {
|
||||||
|
"version": "2.0.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.0.18.tgz",
|
||||||
|
"integrity": "sha512-7TyMjRrZZMBPa+/5Y8lN0iyvUU/01PeMGX2+RE7cQWpEUIcb4QotzUObFkJDejj/HUH4qjP/eQ0gzzKs2f+6Yw==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">= 16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@isaacs/cliui": {
|
"node_modules/@isaacs/cliui": {
|
||||||
"version": "8.0.2",
|
"version": "8.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||||
@@ -714,6 +794,84 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@material-tailwind/react": {
|
||||||
|
"version": "2.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@material-tailwind/react/-/react-2.1.4.tgz",
|
||||||
|
"integrity": "sha512-dRHnC+Ka0IJ8KSfToBmlJRy9gAPv6F1oVZb+ABOPcIR8LxBhAXXhMAjtQf9ilG0nq2VK9AISsYFtyWBsr8Y88A==",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/react": "0.19.0",
|
||||||
|
"classnames": "2.3.2",
|
||||||
|
"deepmerge": "4.2.2",
|
||||||
|
"framer-motion": "6.5.1",
|
||||||
|
"material-ripple-effects": "2.0.1",
|
||||||
|
"prop-types": "15.8.1",
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-dom": "18.2.0",
|
||||||
|
"tailwind-merge": "1.8.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16 || ^17 || ^18",
|
||||||
|
"react-dom": "^16 || ^17 || ^18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@motionone/animation": {
|
||||||
|
"version": "10.18.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@motionone/animation/-/animation-10.18.0.tgz",
|
||||||
|
"integrity": "sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@motionone/easing": "^10.18.0",
|
||||||
|
"@motionone/types": "^10.17.1",
|
||||||
|
"@motionone/utils": "^10.18.0",
|
||||||
|
"tslib": "^2.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@motionone/dom": {
|
||||||
|
"version": "10.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@motionone/dom/-/dom-10.12.0.tgz",
|
||||||
|
"integrity": "sha512-UdPTtLMAktHiqV0atOczNYyDd/d8Cf5fFsd1tua03PqTwwCe/6lwhLSQ8a7TbnQ5SN0gm44N1slBfj+ORIhrqw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@motionone/animation": "^10.12.0",
|
||||||
|
"@motionone/generators": "^10.12.0",
|
||||||
|
"@motionone/types": "^10.12.0",
|
||||||
|
"@motionone/utils": "^10.12.0",
|
||||||
|
"hey-listen": "^1.0.8",
|
||||||
|
"tslib": "^2.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@motionone/easing": {
|
||||||
|
"version": "10.18.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@motionone/easing/-/easing-10.18.0.tgz",
|
||||||
|
"integrity": "sha512-VcjByo7XpdLS4o9T8t99JtgxkdMcNWD3yHU/n6CLEz3bkmKDRZyYQ/wmSf6daum8ZXqfUAgFeCZSpJZIMxaCzg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@motionone/utils": "^10.18.0",
|
||||||
|
"tslib": "^2.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@motionone/generators": {
|
||||||
|
"version": "10.18.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@motionone/generators/-/generators-10.18.0.tgz",
|
||||||
|
"integrity": "sha512-+qfkC2DtkDj4tHPu+AFKVfR/C30O1vYdvsGYaR13W/1cczPrrcjdvYCj0VLFuRMN+lP1xvpNZHCRNM4fBzn1jg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@motionone/types": "^10.17.1",
|
||||||
|
"@motionone/utils": "^10.18.0",
|
||||||
|
"tslib": "^2.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@motionone/types": {
|
||||||
|
"version": "10.17.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@motionone/types/-/types-10.17.1.tgz",
|
||||||
|
"integrity": "sha512-KaC4kgiODDz8hswCrS0btrVrzyU2CSQKO7Ps90ibBVSQmjkrt2teqta6/sOG59v7+dPnKMAg13jyqtMKV2yJ7A=="
|
||||||
|
},
|
||||||
|
"node_modules/@motionone/utils": {
|
||||||
|
"version": "10.18.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@motionone/utils/-/utils-10.18.0.tgz",
|
||||||
|
"integrity": "sha512-3XVF7sgyTSI2KWvTf6uLlBJ5iAgRgmvp3bpuOiQJvInd4nZ19ET8lX5unn30SlmRH7hXbBbH+Gxd0m0klJ3Xtw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@motionone/types": "^10.17.1",
|
||||||
|
"hey-listen": "^1.0.8",
|
||||||
|
"tslib": "^2.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nodelib/fs.scandir": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
@@ -760,19 +918,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@remix-run/router": {
|
"node_modules/@remix-run/router": {
|
||||||
"version": "1.23.0",
|
"version": "1.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz",
|
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.10.0.tgz",
|
||||||
"integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==",
|
"integrity": "sha512-Lm+fYpMfZoEucJ7cMxgt4dYt8jLfbpwRCzAjm9UgSLOkmlqo9gupxt6YX3DY0Fk155NT9l17d/ydi+964uS9Lw==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/pluginutils": {
|
|
||||||
"version": "1.0.0-beta.27",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
|
|
||||||
"integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/@socket.io/component-emitter": {
|
"node_modules/@socket.io/component-emitter": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||||
@@ -819,26 +971,62 @@
|
|||||||
"@babel/types": "^7.28.2"
|
"@babel/types": "^7.28.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitejs/plugin-react": {
|
"node_modules/@types/prop-types": {
|
||||||
"version": "4.7.0",
|
"version": "15.7.15",
|
||||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
||||||
"integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
|
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/@types/react": {
|
||||||
|
"version": "18.2.31",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.31.tgz",
|
||||||
|
"integrity": "sha512-c2UnPv548q+5DFh03y8lEDeMfDwBn9G3dRwfkrxQMo/dOtRHUUO57k6pHvBIfH/VF4Nh+98mZ5aaSe+2echD5g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.28.0",
|
"@types/prop-types": "*",
|
||||||
"@babel/plugin-transform-react-jsx-self": "^7.27.1",
|
"@types/scheduler": "*",
|
||||||
"@babel/plugin-transform-react-jsx-source": "^7.27.1",
|
"csstype": "^3.0.2"
|
||||||
"@rolldown/pluginutils": "1.0.0-beta.27",
|
}
|
||||||
"@types/babel__core": "^7.20.5",
|
},
|
||||||
"react-refresh": "^0.17.0"
|
"node_modules/@types/react-dom": {
|
||||||
|
"version": "18.2.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.14.tgz",
|
||||||
|
"integrity": "sha512-V835xgdSVmyQmI1KLV2BEIUgqEuinxp9O4G6g3FqO/SqLac049E53aysv0oEFD2kHfejeKU+ZqL2bcFWj9gLAQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/scheduler": {
|
||||||
|
"version": "0.26.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.26.0.tgz",
|
||||||
|
"integrity": "sha512-WFHp9YUJQ6CKshqoC37iOlHnQSmxNc795UhB26CyBBttrN9svdIrUjl/NjnNmfcwtncN0h/0PPAFWv9ovP8mLA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/@vitejs/plugin-react": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-rM0SqazU9iqPUraQ2JlIvReeaxOoRj6n+PzB1C0cBzIbd8qP336nC39/R9yPi3wVcah7E7j/kdU1uCUqMEU4OQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/core": "^7.22.20",
|
||||||
|
"@babel/plugin-transform-react-jsx-self": "^7.22.5",
|
||||||
|
"@babel/plugin-transform-react-jsx-source": "^7.22.5",
|
||||||
|
"@types/babel__core": "^7.20.2",
|
||||||
|
"react-refresh": "^0.14.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^14.18.0 || >=16.0.0"
|
"node": "^14.18.0 || >=16.0.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
"vite": "^4.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@yr/monotone-cubic-spline": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA=="
|
||||||
|
},
|
||||||
"node_modules/ansi-regex": {
|
"node_modules/ansi-regex": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
|
||||||
@@ -882,21 +1070,46 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/apexcharts": {
|
||||||
|
"version": "3.44.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.44.0.tgz",
|
||||||
|
"integrity": "sha512-u7Xzrbcxc2yWznN78Jh5NMCYVAsWDfBjRl5ea++rVzFAqjU2hLz4RgKIFwYOBDRQtW1e/Qz8azJTqIJ1+Vu9Qg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@yr/monotone-cubic-spline": "^1.0.3",
|
||||||
|
"svg.draggable.js": "^2.2.2",
|
||||||
|
"svg.easing.js": "^2.0.0",
|
||||||
|
"svg.filter.js": "^2.0.2",
|
||||||
|
"svg.pathmorphing.js": "^0.1.3",
|
||||||
|
"svg.resize.js": "^1.4.3",
|
||||||
|
"svg.select.js": "^3.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/arg": {
|
"node_modules/arg": {
|
||||||
"version": "5.0.2",
|
"version": "5.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
|
||||||
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
|
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/aria-hidden": {
|
||||||
|
"version": "1.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
|
||||||
|
"integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/asynckit": {
|
"node_modules/asynckit": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||||
},
|
},
|
||||||
"node_modules/autoprefixer": {
|
"node_modules/autoprefixer": {
|
||||||
"version": "10.4.21",
|
"version": "10.4.16",
|
||||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
|
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz",
|
||||||
"integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==",
|
"integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -913,11 +1126,11 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"browserslist": "^4.24.4",
|
"browserslist": "^4.21.10",
|
||||||
"caniuse-lite": "^1.0.30001702",
|
"caniuse-lite": "^1.0.30001538",
|
||||||
"fraction.js": "^4.3.7",
|
"fraction.js": "^4.3.6",
|
||||||
"normalize-range": "^0.1.2",
|
"normalize-range": "^0.1.2",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.0.0",
|
||||||
"postcss-value-parser": "^4.2.0"
|
"postcss-value-parser": "^4.2.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -931,12 +1144,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "1.11.0",
|
"version": "1.7.5",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.5.tgz",
|
||||||
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
|
"integrity": "sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.15.6",
|
"follow-redirects": "^1.15.6",
|
||||||
"form-data": "^4.0.4",
|
"form-data": "^4.0.0",
|
||||||
"proxy-from-env": "^1.1.0"
|
"proxy-from-env": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1088,6 +1301,11 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/classnames": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
@@ -1158,6 +1376,12 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/csstype": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||||
@@ -1175,6 +1399,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/deepmerge": {
|
||||||
|
"version": "4.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
|
||||||
|
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/delayed-stream": {
|
"node_modules/delayed-stream": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
@@ -1461,6 +1693,34 @@
|
|||||||
"url": "https://github.com/sponsors/rawify"
|
"url": "https://github.com/sponsors/rawify"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/framer-motion": {
|
||||||
|
"version": "6.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-6.5.1.tgz",
|
||||||
|
"integrity": "sha512-o1BGqqposwi7cgDrtg0dNONhkmPsUFDaLcKXigzuTFC5x58mE8iyTazxSudFzmT6MEyJKfjjU8ItoMe3W+3fiw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@motionone/dom": "10.12.0",
|
||||||
|
"framesync": "6.0.1",
|
||||||
|
"hey-listen": "^1.0.8",
|
||||||
|
"popmotion": "11.0.3",
|
||||||
|
"style-value-types": "5.0.0",
|
||||||
|
"tslib": "^2.1.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@emotion/is-prop-valid": "^0.8.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8 || ^17.0.0 || ^18.0.0",
|
||||||
|
"react-dom": ">=16.8 || ^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/framesync": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/framesync/-/framesync-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-fUY88kXvGiIItgNC7wcTOl0SNRCVXMKSWW2Yzfmn7EKNc+MpCzcz9DhdHcdjbrtN3c6R4H5dTY2jiCpPdysEjA==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fsevents": {
|
"node_modules/fsevents": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
@@ -1606,6 +1866,11 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/hey-listen": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q=="
|
||||||
|
},
|
||||||
"node_modules/is-binary-path": {
|
"node_modules/is-binary-path": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||||
@@ -1769,6 +2034,11 @@
|
|||||||
"yallist": "^3.0.2"
|
"yallist": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/material-ripple-effects": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/material-ripple-effects/-/material-ripple-effects-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-hHlUkZAuXbP94lu02VgrPidbZ3hBtgXBtjlwR8APNqOIgDZMV8MCIcsclL8FmGJQHvnORyvoQgC965vPsiyXLQ=="
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
@@ -1904,7 +2174,6 @@
|
|||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -1997,10 +2266,21 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/popmotion": {
|
||||||
|
"version": "11.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/popmotion/-/popmotion-11.0.3.tgz",
|
||||||
|
"integrity": "sha512-Y55FLdj3UxkR7Vl3s7Qr4e9m0onSnP8W7d/xQLsoJM40vs6UKHFdygs6SWryasTZYqugMjm3BepCF4CWXDiHgA==",
|
||||||
|
"dependencies": {
|
||||||
|
"framesync": "6.0.1",
|
||||||
|
"hey-listen": "^1.0.8",
|
||||||
|
"style-value-types": "5.0.0",
|
||||||
|
"tslib": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.6",
|
"version": "8.4.31",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -2017,9 +2297,9 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.6",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.0.0",
|
||||||
"source-map-js": "^1.2.1"
|
"source-map-js": "^1.0.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
@@ -2140,6 +2420,103 @@
|
|||||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/prettier": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"prettier": "bin/prettier.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prettier-plugin-tailwindcss": {
|
||||||
|
"version": "0.5.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.5.6.tgz",
|
||||||
|
"integrity": "sha512-2Xgb+GQlkPAUCFi3sV+NOYcSI5XgduvDBL2Zt/hwJudeKXkyvRS65c38SB0yb9UB40+1rL83I6m0RtlOQ8eHdg==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.21.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@ianvs/prettier-plugin-sort-imports": "*",
|
||||||
|
"@prettier/plugin-pug": "*",
|
||||||
|
"@shopify/prettier-plugin-liquid": "*",
|
||||||
|
"@shufo/prettier-plugin-blade": "*",
|
||||||
|
"@trivago/prettier-plugin-sort-imports": "*",
|
||||||
|
"prettier": "^3.0",
|
||||||
|
"prettier-plugin-astro": "*",
|
||||||
|
"prettier-plugin-css-order": "*",
|
||||||
|
"prettier-plugin-import-sort": "*",
|
||||||
|
"prettier-plugin-jsdoc": "*",
|
||||||
|
"prettier-plugin-organize-attributes": "*",
|
||||||
|
"prettier-plugin-organize-imports": "*",
|
||||||
|
"prettier-plugin-style-order": "*",
|
||||||
|
"prettier-plugin-svelte": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@ianvs/prettier-plugin-sort-imports": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@prettier/plugin-pug": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@shopify/prettier-plugin-liquid": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@shufo/prettier-plugin-blade": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@trivago/prettier-plugin-sort-imports": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"prettier-plugin-astro": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"prettier-plugin-css-order": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"prettier-plugin-import-sort": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"prettier-plugin-jsdoc": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"prettier-plugin-marko": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"prettier-plugin-organize-attributes": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"prettier-plugin-organize-imports": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"prettier-plugin-style-order": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"prettier-plugin-svelte": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"prettier-plugin-twig-melody": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prop-types": {
|
||||||
|
"version": "15.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
|
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.4.0",
|
||||||
|
"object-assign": "^4.1.1",
|
||||||
|
"react-is": "^16.13.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/proxy-from-env": {
|
"node_modules/proxy-from-env": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
@@ -2166,9 +2543,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "18.3.1",
|
"version": "18.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
|
||||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
},
|
},
|
||||||
@@ -2176,33 +2553,50 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-dom": {
|
"node_modules/react-apexcharts": {
|
||||||
"version": "18.3.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-apexcharts/-/react-apexcharts-1.4.1.tgz",
|
||||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
"integrity": "sha512-G14nVaD64Bnbgy8tYxkjuXEUp/7h30Q0U33xc3AwtGFijJB9nHqOt1a6eG0WBn055RgRg+NwqbKGtqPxy15d0Q==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0",
|
"prop-types": "^15.8.1"
|
||||||
"scheduler": "^0.23.2"
|
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^18.3.1"
|
"apexcharts": "^3.41.0",
|
||||||
|
"react": ">=0.13"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-dom": {
|
||||||
|
"version": "18.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
|
||||||
|
"integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.1.0",
|
||||||
|
"scheduler": "^0.23.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-is": {
|
||||||
|
"version": "16.13.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||||
|
},
|
||||||
"node_modules/react-refresh": {
|
"node_modules/react-refresh": {
|
||||||
"version": "0.17.0",
|
"version": "0.14.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
|
||||||
"integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
|
"integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-router": {
|
"node_modules/react-router": {
|
||||||
"version": "6.30.1",
|
"version": "6.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.17.0.tgz",
|
||||||
"integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==",
|
"integrity": "sha512-YJR3OTJzi3zhqeJYADHANCGPUu9J+6fT5GLv82UWRGSxu6oJYCKVmxUcaBQuGm9udpWmPsvpme/CdHumqgsoaA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@remix-run/router": "1.23.0"
|
"@remix-run/router": "1.10.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
@@ -2212,12 +2606,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-router-dom": {
|
"node_modules/react-router-dom": {
|
||||||
"version": "6.30.1",
|
"version": "6.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.17.0.tgz",
|
||||||
"integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==",
|
"integrity": "sha512-qWHkkbXQX+6li0COUUPKAUkxjNNqPJuiBd27dVwQGDNsuFBdMbrS6UZ0CLYc4CsbdLYTckn4oB4tGDuPZpPhaQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@remix-run/router": "1.23.0",
|
"@remix-run/router": "1.10.0",
|
||||||
"react-router": "6.30.1"
|
"react-router": "6.17.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
@@ -2530,6 +2924,15 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/style-value-types": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-08yq36Ikn4kx4YU6RD7jWEv27v4V+PUsOGa4n/as8Et3CuODMJQ00ENeAVXAeydX4Z2j1XHZF1K2sX4mGl18fA==",
|
||||||
|
"dependencies": {
|
||||||
|
"hey-listen": "^1.0.8",
|
||||||
|
"tslib": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/sucrase": {
|
"node_modules/sucrase": {
|
||||||
"version": "3.35.0",
|
"version": "3.35.0",
|
||||||
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
|
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
|
||||||
@@ -2564,34 +2967,127 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/svg.draggable.js": {
|
||||||
|
"version": "2.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz",
|
||||||
|
"integrity": "sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==",
|
||||||
|
"dependencies": {
|
||||||
|
"svg.js": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svg.easing.js": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/svg.easing.js/-/svg.easing.js-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-//ctPdJMGy22YoYGV+3HEfHbm6/69LJUTAqI2/5qBvaNHZ9uUFVC82B0Pl299HzgH13rKrBgi4+XyXXyVWWthA==",
|
||||||
|
"dependencies": {
|
||||||
|
"svg.js": ">=2.3.x"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svg.filter.js": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/svg.filter.js/-/svg.filter.js-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-xkGBwU+dKBzqg5PtilaTb0EYPqPfJ9Q6saVldX+5vCRy31P6TlRCP3U9NxH3HEufkKkpNgdTLBJnmhDHeTqAkw==",
|
||||||
|
"dependencies": {
|
||||||
|
"svg.js": "^2.2.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svg.js": {
|
||||||
|
"version": "2.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/svg.js/-/svg.js-2.7.1.tgz",
|
||||||
|
"integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA=="
|
||||||
|
},
|
||||||
|
"node_modules/svg.pathmorphing.js": {
|
||||||
|
"version": "0.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz",
|
||||||
|
"integrity": "sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==",
|
||||||
|
"dependencies": {
|
||||||
|
"svg.js": "^2.4.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svg.resize.js": {
|
||||||
|
"version": "1.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/svg.resize.js/-/svg.resize.js-1.4.3.tgz",
|
||||||
|
"integrity": "sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==",
|
||||||
|
"dependencies": {
|
||||||
|
"svg.js": "^2.6.5",
|
||||||
|
"svg.select.js": "^2.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svg.resize.js/node_modules/svg.select.js": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"svg.js": "^2.2.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/svg.select.js": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==",
|
||||||
|
"dependencies": {
|
||||||
|
"svg.js": "^2.6.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tabbable": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="
|
||||||
|
},
|
||||||
|
"node_modules/tailwind-merge": {
|
||||||
|
"version": "1.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-1.8.1.tgz",
|
||||||
|
"integrity": "sha512-+fflfPxvHFr81hTJpQ3MIwtqgvefHZFUHFiIHpVIRXvG/nX9+gu2P7JNlFu2bfDMJ+uHhi/pUgzaYacMoXv+Ww=="
|
||||||
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "3.4.17",
|
"version": "3.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.4.tgz",
|
||||||
"integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
|
"integrity": "sha512-JXZNOkggUAc9T5E7nCrimoXHcSf9h3NWFe5sh36CGD/3M5TRLuQeFnQoDsit2uVTqgoOZHLx5rTykLUu16vsMQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alloc/quick-lru": "^5.2.0",
|
"@alloc/quick-lru": "^5.2.0",
|
||||||
"arg": "^5.0.2",
|
"arg": "^5.0.2",
|
||||||
"chokidar": "^3.6.0",
|
"chokidar": "^3.5.3",
|
||||||
"didyoumean": "^1.2.2",
|
"didyoumean": "^1.2.2",
|
||||||
"dlv": "^1.1.3",
|
"dlv": "^1.1.3",
|
||||||
"fast-glob": "^3.3.2",
|
"fast-glob": "^3.3.0",
|
||||||
"glob-parent": "^6.0.2",
|
"glob-parent": "^6.0.2",
|
||||||
"is-glob": "^4.0.3",
|
"is-glob": "^4.0.3",
|
||||||
"jiti": "^1.21.6",
|
"jiti": "^1.19.1",
|
||||||
"lilconfig": "^3.1.3",
|
"lilconfig": "^2.1.0",
|
||||||
"micromatch": "^4.0.8",
|
"micromatch": "^4.0.5",
|
||||||
"normalize-path": "^3.0.0",
|
"normalize-path": "^3.0.0",
|
||||||
"object-hash": "^3.0.0",
|
"object-hash": "^3.0.0",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.0.0",
|
||||||
"postcss": "^8.4.47",
|
"postcss": "^8.4.23",
|
||||||
"postcss-import": "^15.1.0",
|
"postcss-import": "^15.1.0",
|
||||||
"postcss-js": "^4.0.1",
|
"postcss-js": "^4.0.1",
|
||||||
"postcss-load-config": "^4.0.2",
|
"postcss-load-config": "^4.0.1",
|
||||||
"postcss-nested": "^6.2.0",
|
"postcss-nested": "^6.0.1",
|
||||||
"postcss-selector-parser": "^6.1.2",
|
"postcss-selector-parser": "^6.0.11",
|
||||||
"resolve": "^1.22.8",
|
"resolve": "^1.22.2",
|
||||||
"sucrase": "^3.35.0"
|
"sucrase": "^3.32.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"tailwind": "lib/cli.js",
|
"tailwind": "lib/cli.js",
|
||||||
@@ -2601,6 +3097,15 @@
|
|||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tailwindcss/node_modules/lilconfig": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/thenify": {
|
"node_modules/thenify": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
||||||
@@ -2640,6 +3145,11 @@
|
|||||||
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
|
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/tslib": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
||||||
|
},
|
||||||
"node_modules/update-browserslist-db": {
|
"node_modules/update-browserslist-db": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
|
||||||
@@ -2677,9 +3187,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "4.5.14",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.14.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz",
|
||||||
"integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==",
|
"integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.18.10",
|
"esbuild": "^0.18.10",
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "stackpulse-frontend",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "1.7.5",
|
||||||
|
"socket.io-client": "4.8.1",
|
||||||
|
"@heroicons/react": "2.0.18",
|
||||||
|
"@material-tailwind/react": "2.1.4",
|
||||||
|
"apexcharts": "3.44.0",
|
||||||
|
"prop-types": "15.8.1",
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-apexcharts": "1.4.1",
|
||||||
|
"react-dom": "18.2.0",
|
||||||
|
"react-router-dom": "6.17.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "18.2.31",
|
||||||
|
"@types/react-dom": "18.2.14",
|
||||||
|
"@vitejs/plugin-react": "4.1.0",
|
||||||
|
"autoprefixer": "10.4.16",
|
||||||
|
"postcss": "8.4.31",
|
||||||
|
"prettier": "3.0.3",
|
||||||
|
"prettier-plugin-tailwindcss": "0.5.6",
|
||||||
|
"tailwindcss": "3.3.4",
|
||||||
|
"vite": "4.5.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { NavLink, Route, Routes } from "react-router-dom";
|
||||||
|
import Stacks from "./Stacks.jsx";
|
||||||
|
import Logs from "./Logs.jsx";
|
||||||
|
import Maintenance from "./Maintenance.jsx";
|
||||||
|
import logo from "./assets/images/stackpulse.png";
|
||||||
|
import { useMaintenance } from "./context/MaintenanceContext.jsx";
|
||||||
|
|
||||||
|
const navLinkBase =
|
||||||
|
"px-4 py-2 rounded-md font-medium transition-colors duration-150";
|
||||||
|
|
||||||
|
const getNavClass = ({ isActive }) =>
|
||||||
|
`${navLinkBase} ${isActive ? "bg-purple-600 text-white" : "text-gray-300 hover:bg-gray-700"}`;
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const { maintenance, update } = useMaintenance();
|
||||||
|
const maintenanceActive = Boolean(maintenance?.active);
|
||||||
|
const maintenanceLabel = maintenance?.message || (update?.running ? "Portainer-Update läuft" : "Wartungsmodus aktiv");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-900 text-white">
|
||||||
|
<header className="bg-gray-800 shadow-md">
|
||||||
|
<div className="max-w-6xl mx-auto px-6 py-6">
|
||||||
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
|
||||||
|
<div className="flex flex-col items-end gap-1">
|
||||||
|
<span className="text-xs text-gray-500">v0.3</span>
|
||||||
|
{maintenanceActive && (
|
||||||
|
<span className="flex items-center gap-1 text-xs text-amber-300">
|
||||||
|
<span className="h-2 w-2 animate-pulse rounded-full bg-amber-400" />
|
||||||
|
{maintenanceLabel}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<img src={logo} alt="StackPulse" className="h-10 w-auto" />
|
||||||
|
</div>
|
||||||
|
<nav className="flex gap-2 items-end">
|
||||||
|
<NavLink to="/" end className={getNavClass}>
|
||||||
|
Stacks
|
||||||
|
</NavLink>
|
||||||
|
<NavLink to="/maintenance" className={getNavClass}>
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
Wartung
|
||||||
|
{maintenanceActive && (
|
||||||
|
<span className="h-2 w-2 animate-pulse rounded-full bg-amber-400" />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</NavLink>
|
||||||
|
|
||||||
|
<NavLink to="/logs" className={getNavClass}>
|
||||||
|
Logs
|
||||||
|
</NavLink>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main className="max-w-6xl mx-auto px-6 py-6">
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Stacks />} />
|
||||||
|
<Route path="/maintenance" element={<Maintenance />} />
|
||||||
|
<Route path="/logs" element={<Logs />} />
|
||||||
|
<Route path="*" element={<Stacks />} />
|
||||||
|
</Routes>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -547,22 +547,10 @@ export default function Logs() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="mx-auto max-w-6xl space-y-4 p-6">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
<h2 className="text-xl font-semibold text-gray-100">Redeploy-Logs</h2>
|
<h2 className="text-xl font-semibold text-gray-100">Redeploy-Logs</h2>
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
<div className="flex items-center gap-2 text-sm text-gray-400">
|
|
||||||
<span>Einträge pro Seite</span>
|
|
||||||
<select
|
|
||||||
value={perPage}
|
|
||||||
onChange={handlePerPageChange}
|
|
||||||
className="rounded-md border border-gray-700 bg-gray-900/70 px-3 py-1.5 text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500"
|
|
||||||
>
|
|
||||||
{PER_PAGE_OPTIONS.map(({ value, label }) => (
|
|
||||||
<option key={value} value={value}>{label}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => handleExport('txt')}
|
onClick={() => handleExport('txt')}
|
||||||
disabled={actionLoading || loading}
|
disabled={actionLoading || loading}
|
||||||
@@ -819,17 +807,30 @@ export default function Logs() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center justify-end gap-3">
|
<div className="flex flex-wrap items-center justify-end gap-3">
|
||||||
<button
|
<Button
|
||||||
onClick={handleResetFilters}
|
onClick={handleResetFilters}
|
||||||
className="rounded-md border border-gray-600 px-4 py-2 text-gray-200 transition hover:bg-gray-700"
|
className="rounded-md border border-gray-600 px-4 py-2 text-gray-200 transition hover:bg-gray-700"
|
||||||
>
|
>
|
||||||
Zurücksetzen
|
Zurücksetzen
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center justify-end gap-2 text-sm text-gray-300">
|
||||||
|
<span>Einträge pro Seite</span>
|
||||||
|
<select
|
||||||
|
value={perPage}
|
||||||
|
onChange={handlePerPageChange}
|
||||||
|
className="rounded-md border border-gray-700 bg-gray-900 px-2 py-1 text-sm text-gray-100 focus:border-purple-500 focus:outline-none focus:ring-1 focus:ring-purple-500"
|
||||||
|
>
|
||||||
|
{PER_PAGE_OPTIONS.map(({ value, label }) => (
|
||||||
|
<option key={value} value={value}>{label}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="overflow-x-auto bg-gray-800/60 rounded-xl border border-gray-700">
|
<div className="overflow-x-auto bg-gray-800/60 rounded-xl border border-gray-700">
|
||||||
<table className="min-w-full divide-y divide-gray-700">
|
<table className="min-w-full divide-y divide-gray-700">
|
||||||
<thead className="bg-gray-800">
|
<thead className="bg-gray-800">
|
||||||
|
After Width: | Height: | Size: 74 KiB |
@@ -0,0 +1,121 @@
|
|||||||
|
import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
|
const ToastContext = createContext(null);
|
||||||
|
|
||||||
|
const VARIANT_STYLES = {
|
||||||
|
success: {
|
||||||
|
container: "border-green-500/80 bg-green-950/90 text-green-100",
|
||||||
|
accent: "bg-green-400"
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
container: "border-yellow-500/80 bg-yellow-950/90 text-yellow-100",
|
||||||
|
accent: "bg-yellow-400"
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
container: "border-red-500/80 bg-red-950/90 text-red-100",
|
||||||
|
accent: "bg-red-400"
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
container: "border-sky-500/60 bg-sky-950/90 text-sky-100",
|
||||||
|
accent: "bg-sky-400"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_DURATION = 6000;
|
||||||
|
|
||||||
|
export default function ToastProvider({ children }) {
|
||||||
|
const [toasts, setToasts] = useState([]);
|
||||||
|
const timeoutsRef = useRef(new Map());
|
||||||
|
|
||||||
|
const dismissToast = useCallback((id) => {
|
||||||
|
setToasts((prev) => prev.filter((toast) => toast.id !== id));
|
||||||
|
|
||||||
|
const handle = timeoutsRef.current.get(id);
|
||||||
|
if (handle && typeof window !== "undefined") {
|
||||||
|
window.clearTimeout(handle);
|
||||||
|
}
|
||||||
|
timeoutsRef.current.delete(id);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const showToast = useCallback(({ id, title, description, variant = "info", duration = DEFAULT_DURATION } = {}) => {
|
||||||
|
const toastId = id ?? `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||||
|
const normalizedVariant = VARIANT_STYLES[variant] ? variant : "info";
|
||||||
|
|
||||||
|
setToasts((prev) => {
|
||||||
|
const withoutExisting = prev.filter((toast) => toast.id !== toastId);
|
||||||
|
return [
|
||||||
|
...withoutExisting,
|
||||||
|
{
|
||||||
|
id: toastId,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
variant: normalizedVariant
|
||||||
|
}
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
if (duration !== null && duration !== Infinity && typeof window !== "undefined") {
|
||||||
|
const timeoutHandle = window.setTimeout(() => {
|
||||||
|
dismissToast(toastId);
|
||||||
|
}, duration);
|
||||||
|
|
||||||
|
const existingHandle = timeoutsRef.current.get(toastId);
|
||||||
|
if (existingHandle) {
|
||||||
|
window.clearTimeout(existingHandle);
|
||||||
|
}
|
||||||
|
timeoutsRef.current.set(toastId, timeoutHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
return toastId;
|
||||||
|
}, [dismissToast]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
timeoutsRef.current.forEach((handle) => window.clearTimeout(handle));
|
||||||
|
timeoutsRef.current.clear();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const contextValue = useMemo(() => ({ showToast, dismissToast }), [showToast, dismissToast]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToastContext.Provider value={contextValue}>
|
||||||
|
{children}
|
||||||
|
<div className="pointer-events-none fixed right-4 top-4 z-50 flex w-80 max-w-full flex-col gap-3">
|
||||||
|
{toasts.map((toast) => {
|
||||||
|
const styles = VARIANT_STYLES[toast.variant] ?? VARIANT_STYLES.info;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={toast.id}
|
||||||
|
className={`pointer-events-auto overflow-hidden rounded-lg border px-4 py-3 shadow-xl backdrop-blur ${styles.container}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<span className={`mt-1 h-2 flex-shrink-0 w-2 rounded-full ${styles.accent}`}></span>
|
||||||
|
<div className="flex-1">
|
||||||
|
{toast.title && <p className="text-sm font-semibold">{toast.title}</p>}
|
||||||
|
{toast.description && <p className="mt-1 text-sm leading-snug text-white/80">{toast.description}</p>}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => dismissToast(toast.id)}
|
||||||
|
className="ml-2 rounded-md p-1 text-xs uppercase tracking-wide text-white/60 transition hover:text-white/90"
|
||||||
|
>
|
||||||
|
Schließen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</ToastContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useToast() {
|
||||||
|
const context = useContext(ToastContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useToast must be used within a ToastProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
const MaintenanceContext = createContext(null);
|
||||||
|
|
||||||
|
const INITIAL_STATE = {
|
||||||
|
maintenance: null,
|
||||||
|
update: null,
|
||||||
|
script: null,
|
||||||
|
ssh: null,
|
||||||
|
loading: true,
|
||||||
|
error: "",
|
||||||
|
lastUpdated: null
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function MaintenanceProvider({ children }) {
|
||||||
|
const [state, setState] = useState(INITIAL_STATE);
|
||||||
|
const pollingRef = useRef(null);
|
||||||
|
const wasRunningRef = useRef(false);
|
||||||
|
|
||||||
|
const applyState = useCallback((partial) => {
|
||||||
|
setState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
...partial
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchConfig = useCallback(async () => {
|
||||||
|
applyState({ loading: true, error: "" });
|
||||||
|
try {
|
||||||
|
const response = await axios.get("/api/maintenance/config");
|
||||||
|
const data = response.data ?? {};
|
||||||
|
applyState({
|
||||||
|
maintenance: data.maintenance ?? null,
|
||||||
|
update: data.update ?? null,
|
||||||
|
script: data.script ?? null,
|
||||||
|
ssh: data.ssh ?? null,
|
||||||
|
loading: false,
|
||||||
|
error: "",
|
||||||
|
lastUpdated: new Date()
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
const message = err.response?.data?.error || err.message || "Fehler beim Laden der Wartungsdaten";
|
||||||
|
applyState({ loading: false, error: message });
|
||||||
|
}
|
||||||
|
}, [applyState]);
|
||||||
|
|
||||||
|
const refreshUpdateStatus = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get("/api/maintenance/update-status");
|
||||||
|
const data = response.data ?? {};
|
||||||
|
setState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
maintenance: data.maintenance ?? prev.maintenance,
|
||||||
|
update: data.update ?? prev.update,
|
||||||
|
error: ""
|
||||||
|
}));
|
||||||
|
} catch (err) {
|
||||||
|
const message = err.response?.data?.error || err.message || "Fehler beim Aktualisieren des Wartungsstatus";
|
||||||
|
setState((prev) => ({ ...prev, error: message }));
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchConfig();
|
||||||
|
}, [fetchConfig]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (state.update?.running) {
|
||||||
|
refreshUpdateStatus();
|
||||||
|
const intervalId = setInterval(() => {
|
||||||
|
refreshUpdateStatus();
|
||||||
|
}, 5000);
|
||||||
|
pollingRef.current = intervalId;
|
||||||
|
return () => clearInterval(intervalId);
|
||||||
|
}
|
||||||
|
if (pollingRef.current) {
|
||||||
|
clearInterval(pollingRef.current);
|
||||||
|
pollingRef.current = null;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}, [state.update?.running, refreshUpdateStatus]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentlyRunning = Boolean(state.update?.running);
|
||||||
|
if (!currentlyRunning && wasRunningRef.current) {
|
||||||
|
fetchConfig();
|
||||||
|
}
|
||||||
|
wasRunningRef.current = currentlyRunning;
|
||||||
|
}, [state.update?.running, fetchConfig]);
|
||||||
|
|
||||||
|
const triggerUpdate = useCallback(async (payload = {}) => {
|
||||||
|
await axios.post("/api/maintenance/portainer-update", payload);
|
||||||
|
await refreshUpdateStatus();
|
||||||
|
}, [refreshUpdateStatus]);
|
||||||
|
|
||||||
|
const saveScript = useCallback(async (script) => {
|
||||||
|
await axios.put("/api/maintenance/update-script", { script });
|
||||||
|
await fetchConfig();
|
||||||
|
}, [fetchConfig]);
|
||||||
|
|
||||||
|
const resetScript = useCallback(async () => {
|
||||||
|
await axios.delete("/api/maintenance/update-script");
|
||||||
|
await fetchConfig();
|
||||||
|
}, [fetchConfig]);
|
||||||
|
|
||||||
|
const saveSshConfig = useCallback(async (config) => {
|
||||||
|
await axios.put("/api/maintenance/ssh-config", config);
|
||||||
|
await fetchConfig();
|
||||||
|
}, [fetchConfig]);
|
||||||
|
|
||||||
|
const deleteSshConfig = useCallback(async () => {
|
||||||
|
await axios.delete("/api/maintenance/ssh-config");
|
||||||
|
await fetchConfig();
|
||||||
|
}, [fetchConfig]);
|
||||||
|
|
||||||
|
const testSshConnection = useCallback(async (config) => {
|
||||||
|
const response = await axios.post("/api/maintenance/test-ssh", config ?? {});
|
||||||
|
return response.data;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const value = useMemo(() => ({
|
||||||
|
maintenance: state.maintenance,
|
||||||
|
update: state.update,
|
||||||
|
script: state.script,
|
||||||
|
ssh: state.ssh,
|
||||||
|
loading: state.loading,
|
||||||
|
error: state.error,
|
||||||
|
lastUpdated: state.lastUpdated,
|
||||||
|
refreshConfig: fetchConfig,
|
||||||
|
refreshUpdateStatus,
|
||||||
|
triggerUpdate,
|
||||||
|
saveScript,
|
||||||
|
resetScript,
|
||||||
|
saveSshConfig,
|
||||||
|
deleteSshConfig,
|
||||||
|
testSshConnection
|
||||||
|
}), [state, fetchConfig, refreshUpdateStatus, triggerUpdate, saveScript, resetScript, saveSshConfig, deleteSshConfig, testSshConnection]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MaintenanceContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</MaintenanceContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMaintenance() {
|
||||||
|
const context = useContext(MaintenanceContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useMaintenance must be used within a MaintenanceProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import { BrowserRouter } from "react-router-dom";
|
||||||
|
import App from "./App.jsx";
|
||||||
|
import ToastProvider from "./components/ToastProvider.jsx";
|
||||||
|
import MaintenanceProvider from "./context/MaintenanceContext.jsx";
|
||||||
|
import './index.css'; // Tailwind CSS
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<BrowserRouter>
|
||||||
|
<MaintenanceProvider>
|
||||||
|
<ToastProvider>
|
||||||
|
<App />
|
||||||
|
</ToastProvider>
|
||||||
|
</MaintenanceProvider>
|
||||||
|
</BrowserRouter>
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
host: true, // Hört auf allen Netzwerk-Interfaces, kein localhost notwendig
|
||||||
|
port: 5173, // optional, Standardport für Vite
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://127.0.0.1:4001', // Backend lokal auf Port 4001
|
||||||
|
changeOrigin: true
|
||||||
|
},
|
||||||
|
'/socket.io': {
|
||||||
|
target: 'http://127.0.0.1:4001', // WebSocket-Verbindungen über Proxy weiterleiten
|
||||||
|
ws: true,
|
||||||
|
changeOrigin: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||