Compare commits
163 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 | |||
| d349d31d02 | |||
| 4241bb3114 | |||
| 08c37245e1 | |||
| ef33a0d669 | |||
| ec2d998bc5 | |||
| 7cb1201b51 | |||
| 8fa5ffc49c | |||
| e10d0dec1c | |||
| 5483ee9092 | |||
| 02fe83d65c | |||
| d99ffc1488 | |||
| 2b051d5085 | |||
| f46ba1c820 | |||
| f14c3aa6dd | |||
| 044164a440 | |||
| c37b046e39 | |||
| cc7d80e569 | |||
| deb05814e4 | |||
| 2b284195fb | |||
| 24c98aff3e | |||
| b33e9e587b | |||
| b1fdd074ec | |||
| 3a961270d7 | |||
| 0a65f3e60f | |||
| 38b96da4e7 | |||
| 241a509a15 | |||
| 520e36fe85 | |||
| 1f248e3920 | |||
| ba0905e2b9 | |||
| 876acfa763 | |||
| ccd7fffe5c | |||
| ee7ccd0a39 | |||
| 6687a86802 | |||
| c7d63a28c4 | |||
| d91655aee9 | |||
| 8001a2be37 | |||
| 7f241d352d | |||
| 32af9d2584 | |||
| 55a83267a6 | |||
| dc56f0cbeb | |||
| 66d975ae75 | |||
| 571b185b9b | |||
| 8c59fc5445 | |||
| 6bf4f7aab4 | |||
| cd7933ebc7 | |||
| 3b0bd3c632 | |||
| 281aeb40a6 | |||
| 429721d9da | |||
| 47f632f8fc | |||
| 2e7ebfe06d | |||
| 03b3766a22 | |||
| d1adcb1188 | |||
| e38269e23e | |||
| 81f0e7eaf0 | |||
| 7980837eaf | |||
| 31b506ceb8 | |||
| 44e3754fcb | |||
| 73d225cc79 | |||
| a1e20f188e | |||
| 421e6e47dd | |||
| 79e420d3e3 | |||
| ee3b907cd6 | |||
| 960832af6b | |||
| 3489ea55a2 | |||
| ca148247cb | |||
| bff6521658 | |||
| ef8920cb26 | |||
| 279ad3ffd1 |
+1
-1
@@ -11,4 +11,4 @@ frontend/dist
|
|||||||
backend/.env
|
backend/.env
|
||||||
backend/node_modules
|
backend/node_modules
|
||||||
frontend/node_modules
|
frontend/node_modules
|
||||||
|
backend/data/*.db*
|
||||||
|
|||||||
+24
-1
@@ -11,4 +11,27 @@ backend/node_modules
|
|||||||
backend/.env
|
backend/.env
|
||||||
frontend/node_modules
|
frontend/node_modules
|
||||||
docker-compose.dev.yml
|
docker-compose.dev.yml
|
||||||
scripts/docker-release.sh
|
devscripts
|
||||||
|
backend/data/stackpulse.db
|
||||||
|
backend/data/stackpulse.db-shm
|
||||||
|
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?
|
||||||
+11
@@ -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
|
||||||
|
|
||||||
@@ -31,10 +34,17 @@ RUN npm ci --only=production
|
|||||||
|
|
||||||
# Backend-Code kopieren
|
# Backend-Code kopieren
|
||||||
COPY backend/ ./
|
COPY backend/ ./
|
||||||
|
COPY backend/docker-entrypoint.sh ./docker-entrypoint.sh
|
||||||
|
|
||||||
# public leeren
|
# public leeren
|
||||||
RUN rm -rf ./public/*
|
RUN rm -rf ./public/*
|
||||||
|
|
||||||
|
# frische Datenbank vorbereiten
|
||||||
|
RUN rm -rf ./data && mkdir -p ./data && chown node:node ./data
|
||||||
|
|
||||||
|
# EntryPoint vorbereiten
|
||||||
|
RUN chmod +x ./docker-entrypoint.sh && chown node:node ./docker-entrypoint.sh
|
||||||
|
|
||||||
# Inhalt von dist inklusive Unterordner direkt nach public kopieren
|
# Inhalt von dist inklusive Unterordner direkt nach public kopieren
|
||||||
COPY --from=frontend-build /app/frontend/dist/. ./public/
|
COPY --from=frontend-build /app/frontend/dist/. ./public/
|
||||||
|
|
||||||
@@ -50,4 +60,5 @@ EXPOSE 5173
|
|||||||
USER node
|
USER node
|
||||||
|
|
||||||
# Container startet das Backend (liefert statisches Frontend)
|
# Container startet das Backend (liefert statisches Frontend)
|
||||||
|
ENTRYPOINT ["./docker-entrypoint.sh"]
|
||||||
CMD ["node", "index.js"]
|
CMD ["node", "index.js"]
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -10,14 +13,90 @@ Ziel:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🚀 Features (0.1 Roadmap)
|
## 🚀 Features & Roadmap
|
||||||
|
|
||||||
- [x] Projektstruktur mit Frontend & Backend
|
<details>
|
||||||
- [x] Lokales Startskript (`scripts/start-dev.sh`)
|
<summary>✅ v0.1 – Initial Release</summary>
|
||||||
- [x] Frontend zeigt Stacks an (über Backend)
|
|
||||||
- [x] API-Verbindung zu Portainer
|
- Projektstruktur mit Frontend & Backend
|
||||||
- [x] Stack Redeploy
|
- Lokales Startskript (`scripts/start-dev.sh`)
|
||||||
- [x] Docker Image im ghcr zur Verfügung stellen
|
- Frontend zeigt Stacks an (über Backend)
|
||||||
|
- API-Verbindung zu Portainer
|
||||||
|
- Stack Redeploy
|
||||||
|
- Bereitstellung eines Docker Images über GHCR
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>✅ v0.2 – Release</summary>
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- [x] Anbindung einer SQLite-Datenbank
|
||||||
|
- [x] Logging der Redeploy-Aktionen in SQLite speichern
|
||||||
|
- [x] API-Endpunkte für Log-Abfragen
|
||||||
|
- [x] Funktionen für Pagination, Löschen und Export
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- [x] Anzeige der Logs (inkl. Statusfarben)
|
||||||
|
- [x] UI-Komponenten für Log-Details
|
||||||
|
- [x] Filterfunktionen für die Logs
|
||||||
|
- [x] Pagination, Lösch- und Exportanzeigen
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- [x] Selektive Auswahl: einzelne Stacks oder Services neu deployen
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<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)
|
||||||
|
- Authentifizierung & Benutzerverwaltung
|
||||||
|
- Monitoring (Status, CPU/RAM)
|
||||||
|
- Export/Import von Scheduler-Jobs (z. B. JSON)
|
||||||
|
- Integration Community Edition
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -26,13 +105,11 @@ 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)
|
||||||
│ ├── git-push.sh #nicht relevant
|
│ └── start-dev.sh #Skript für den lokalen Start
|
||||||
│ └── merge-dev-to-master.sh #nicht relevant
|
|
||||||
│ └── merge-feature-to-dev.sh #nicht relevant
|
|
||||||
│ └── start-dev.sh #Skript für den lokalen Start
|
|
||||||
│ └── switch-branch.sh #nicht relevant
|
|
||||||
├── Dockerfile # Multi-Stage Build für Frontend + Backend
|
├── Dockerfile # Multi-Stage Build für Frontend + Backend
|
||||||
├── docker-compose.yml
|
├── docker-compose.yml
|
||||||
└── README.md
|
└── README.md
|
||||||
@@ -63,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
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
@@ -0,0 +1 @@
|
|||||||
|
stackpulse.db
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import Database from 'better-sqlite3';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
const dataDir = path.join(__dirname, '..', 'data');
|
||||||
|
const dbFile = path.join(dataDir, 'stackpulse.db');
|
||||||
|
|
||||||
|
if (!fs.existsSync(dataDir)) {
|
||||||
|
fs.mkdirSync(dataDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = new Database(dbFile);
|
||||||
|
db.pragma('journal_mode = WAL');
|
||||||
|
export { db, dbFile };
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { db } from './index.js';
|
||||||
|
|
||||||
|
const createRedeployLogsTable = `
|
||||||
|
CREATE TABLE IF NOT EXISTS redeploy_logs (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
stack_id TEXT NOT NULL,
|
||||||
|
stack_name TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL,
|
||||||
|
message TEXT,
|
||||||
|
endpoint INTEGER,
|
||||||
|
redeploy_type TEXT
|
||||||
|
);
|
||||||
|
`;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
db.exec(createRedeployLogsTable);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const columns = db.prepare('PRAGMA table_info(redeploy_logs)').all();
|
||||||
|
const hasRedeployType = columns.some((column) => column.name === 'redeploy_type');
|
||||||
|
if (!hasRedeployType) {
|
||||||
|
db.exec('ALTER TABLE redeploy_logs ADD COLUMN redeploy_type TEXT');
|
||||||
|
console.log('ℹ️ redeploy_type column hinzugefügt');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('⚠️ Konnte redeploy_type Spalte nicht prüfen/erstellen:', err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
import { db } from './index.js';
|
||||||
|
|
||||||
|
const valueToArray = (value) => {
|
||||||
|
if (!value && value !== 0) return [];
|
||||||
|
const base = Array.isArray(value) ? value : [value];
|
||||||
|
return base
|
||||||
|
.flatMap((entry) => String(entry).split(','))
|
||||||
|
.map((entry) => entry.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
};
|
||||||
|
|
||||||
|
const singleValue = (value) => {
|
||||||
|
if (value === undefined || value === null) return undefined;
|
||||||
|
return Array.isArray(value) ? value[0] : value;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function buildLogFilter(queryParams = {}) {
|
||||||
|
const filters = [];
|
||||||
|
const params = {};
|
||||||
|
|
||||||
|
const ids = valueToArray(queryParams.ids ?? queryParams.id)
|
||||||
|
.map((entry) => Number(entry))
|
||||||
|
.filter((value) => !Number.isNaN(value));
|
||||||
|
if (ids.length) {
|
||||||
|
const placeholders = ids.map((_, idx) => `@id${idx}`);
|
||||||
|
filters.push(`id IN (${placeholders.join(', ')})`);
|
||||||
|
ids.forEach((value, idx) => {
|
||||||
|
params[`id${idx}`] = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const stackIds = valueToArray(queryParams.stackIds ?? queryParams.stackId);
|
||||||
|
if (stackIds.length) {
|
||||||
|
const placeholders = stackIds.map((_, idx) => `@stackId${idx}`);
|
||||||
|
filters.push(`stack_id IN (${placeholders.join(', ')})`);
|
||||||
|
stackIds.forEach((stack, idx) => {
|
||||||
|
params[`stackId${idx}`] = stack;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const statuses = valueToArray(queryParams.statuses ?? queryParams.status);
|
||||||
|
if (statuses.length) {
|
||||||
|
const placeholders = statuses.map((_, idx) => `@status${idx}`);
|
||||||
|
filters.push(`status IN (${placeholders.join(', ')})`);
|
||||||
|
statuses.forEach((entry, idx) => {
|
||||||
|
params[`status${idx}`] = entry;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const endpoints = valueToArray(queryParams.endpoints ?? queryParams.endpoint);
|
||||||
|
if (endpoints.length) {
|
||||||
|
const placeholders = endpoints.map((_, idx) => `@endpoint${idx}`);
|
||||||
|
filters.push(`endpoint IN (${placeholders.join(', ')})`);
|
||||||
|
endpoints.forEach((entry, idx) => {
|
||||||
|
const numeric = Number(entry);
|
||||||
|
params[`endpoint${idx}`] = Number.isNaN(numeric) ? entry : numeric;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const redeployTypes = valueToArray(queryParams.redeployTypes ?? queryParams.redeployType);
|
||||||
|
if (redeployTypes.length) {
|
||||||
|
const placeholders = redeployTypes.map((_, idx) => `@redeployType${idx}`);
|
||||||
|
filters.push(`redeploy_type IN (${placeholders.join(', ')})`);
|
||||||
|
redeployTypes.forEach((entry, idx) => {
|
||||||
|
params[`redeployType${idx}`] = entry;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageQuery = singleValue(queryParams.message);
|
||||||
|
if (messageQuery && String(messageQuery).trim()) {
|
||||||
|
filters.push('message LIKE @message');
|
||||||
|
params.message = `%${String(messageQuery).trim()}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const from = singleValue(queryParams.from);
|
||||||
|
if (from) {
|
||||||
|
filters.push('timestamp >= @from');
|
||||||
|
params.from = from;
|
||||||
|
}
|
||||||
|
|
||||||
|
const to = singleValue(queryParams.to);
|
||||||
|
if (to) {
|
||||||
|
filters.push('timestamp <= @to');
|
||||||
|
params.to = to;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
whereClause: filters.length ? `WHERE ${filters.join(' AND ')}` : '',
|
||||||
|
params,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertRedeployLogStmt = db.prepare(`
|
||||||
|
INSERT INTO redeploy_logs (stack_id, stack_name, status, message, endpoint, redeploy_type)
|
||||||
|
VALUES (@stackId, @stackName, @status, @message, @endpoint, @redeployType)
|
||||||
|
`);
|
||||||
|
|
||||||
|
export function logRedeployEvent({ stackId, stackName, status, message = null, endpoint = null, redeployType = null }) {
|
||||||
|
try {
|
||||||
|
insertRedeployLogStmt.run({
|
||||||
|
stackId: String(stackId),
|
||||||
|
stackName: stackName ?? 'Unknown',
|
||||||
|
status,
|
||||||
|
message,
|
||||||
|
endpoint,
|
||||||
|
redeployType: redeployType ?? null
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('❌ Fehler beim Speichern des Redeploy-Logs:', err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteLogById(id) {
|
||||||
|
const stmt = db.prepare('DELETE FROM redeploy_logs WHERE id = ?');
|
||||||
|
const info = stmt.run(id);
|
||||||
|
return info.changes || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteLogsByFilters(queryParams = {}) {
|
||||||
|
const { whereClause, params } = buildLogFilter(queryParams);
|
||||||
|
const stmt = db.prepare(`DELETE FROM redeploy_logs ${whereClause}`);
|
||||||
|
const info = stmt.run(params);
|
||||||
|
return info.changes || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function exportLogsByFilters(queryParams = {}, format = 'txt') {
|
||||||
|
const { whereClause, params } = buildLogFilter(queryParams);
|
||||||
|
const rows = db.prepare(`
|
||||||
|
SELECT id, timestamp, stack_id AS stackId, stack_name AS stackName, status, message, endpoint, redeploy_type AS redeployType
|
||||||
|
FROM redeploy_logs
|
||||||
|
${whereClause}
|
||||||
|
ORDER BY datetime(timestamp) DESC
|
||||||
|
`).all(params);
|
||||||
|
|
||||||
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
|
|
||||||
|
if (format === 'sql') {
|
||||||
|
const statements = rows.map((row) => {
|
||||||
|
const columns = ['id', 'timestamp', 'stack_id', 'stack_name', 'status', 'message', 'endpoint', 'redeploy_type'];
|
||||||
|
const values = [
|
||||||
|
row.id,
|
||||||
|
row.timestamp,
|
||||||
|
row.stackId,
|
||||||
|
row.stackName,
|
||||||
|
row.status,
|
||||||
|
row.message,
|
||||||
|
row.endpoint,
|
||||||
|
row.redeployType
|
||||||
|
].map((value) => {
|
||||||
|
if (value === null || value === undefined) return 'NULL';
|
||||||
|
return `'${String(value).replace(/'/g, "''")}'`;
|
||||||
|
});
|
||||||
|
return `INSERT INTO redeploy_logs (${columns.join(', ')}) VALUES (${values.join(', ')});`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
filename: `redeploy-logs-${timestamp}.sql`,
|
||||||
|
contentType: 'application/sql; charset=utf-8',
|
||||||
|
content: statements.join('\n')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = rows.map((row) => {
|
||||||
|
const parts = [
|
||||||
|
`[${row.id}]`,
|
||||||
|
row.timestamp,
|
||||||
|
`Stack: ${row.stackName ?? 'Unbekannt'} (ID: ${row.stackId})`,
|
||||||
|
`Status: ${row.status}`,
|
||||||
|
`Endpoint: ${row.endpoint ?? '-'}`,
|
||||||
|
`Nachricht: ${row.message ?? '-'}`,
|
||||||
|
`Redeploy: ${row.redeployType ?? '-'}`
|
||||||
|
];
|
||||||
|
return parts.join(' | ');
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
filename: `redeploy-logs-${timestamp}.txt`,
|
||||||
|
contentType: 'text/plain; charset=utf-8',
|
||||||
|
content: lines.join('\n')
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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,8 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🔧 Starte Migration (idempotent)..."
|
||||||
|
node db/migrate.js
|
||||||
|
|
||||||
|
echo "✅ Migration abgeschlossen. Starte Anwendung..."
|
||||||
|
exec "$@"
|
||||||
+1775
-66
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
Generated
+219
@@ -9,6 +9,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.0",
|
"axios": "^1.7.0",
|
||||||
|
"better-sqlite3": "^9.6.0",
|
||||||
"dockerode": "^4.0.7",
|
"dockerode": "^4.0.7",
|
||||||
"dotenv": "^16.0.0",
|
"dotenv": "^16.0.0",
|
||||||
"express": "^4.18.0",
|
"express": "^4.18.0",
|
||||||
@@ -230,6 +231,24 @@
|
|||||||
"tweetnacl": "^0.14.3"
|
"tweetnacl": "^0.14.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/better-sqlite3": {
|
||||||
|
"version": "9.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-9.6.0.tgz",
|
||||||
|
"integrity": "sha512-yR5HATnqeYNVnkaUTf4bOP2dJSnyhP4puJN/QPRyx4YkBEEUxib422n2XzPqDEHjQQqazoYoADdAm5vE15+dAQ==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"dependencies": {
|
||||||
|
"bindings": "^1.5.0",
|
||||||
|
"prebuild-install": "^7.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bindings": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"file-uri-to-path": "1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bl": {
|
"node_modules/bl": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||||
@@ -441,6 +460,28 @@
|
|||||||
"ms": "2.0.0"
|
"ms": "2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/decompress-response": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"mimic-response": "^3.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/deep-extend": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0.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",
|
||||||
@@ -466,6 +507,14 @@
|
|||||||
"npm": "1.2.8000 || >= 1.4.16"
|
"npm": "1.2.8000 || >= 1.4.16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/detect-libc": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/docker-modem": {
|
"node_modules/docker-modem": {
|
||||||
"version": "5.0.6",
|
"version": "5.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.6.tgz",
|
||||||
@@ -686,6 +735,14 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expand-template": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/express": {
|
"node_modules/express": {
|
||||||
"version": "4.21.2",
|
"version": "4.21.2",
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
|
||||||
@@ -731,6 +788,11 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/file-uri-to-path": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="
|
||||||
|
},
|
||||||
"node_modules/finalhandler": {
|
"node_modules/finalhandler": {
|
||||||
"version": "1.3.1",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
|
||||||
@@ -854,6 +916,11 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/github-from-package": {
|
||||||
|
"version": "0.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
|
||||||
|
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="
|
||||||
|
},
|
||||||
"node_modules/gopd": {
|
"node_modules/gopd": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
@@ -951,6 +1018,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/ini": {
|
||||||
|
"version": "1.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
||||||
|
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
|
||||||
|
},
|
||||||
"node_modules/ipaddr.js": {
|
"node_modules/ipaddr.js": {
|
||||||
"version": "1.9.1",
|
"version": "1.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||||
@@ -1039,6 +1111,25 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mimic-response": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/minimist": {
|
||||||
|
"version": "1.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||||
|
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mkdirp-classic": {
|
"node_modules/mkdirp-classic": {
|
||||||
"version": "0.5.3",
|
"version": "0.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||||
@@ -1055,6 +1146,11 @@
|
|||||||
"integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==",
|
"integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/napi-build-utils": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="
|
||||||
|
},
|
||||||
"node_modules/negotiator": {
|
"node_modules/negotiator": {
|
||||||
"version": "0.6.3",
|
"version": "0.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||||
@@ -1063,6 +1159,17 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/node-abi": {
|
||||||
|
"version": "3.77.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.77.0.tgz",
|
||||||
|
"integrity": "sha512-DSmt0OEcLoK4i3NuscSbGjOf3bqiDEutejqENSplMSFA/gmB8mkED9G4pKWnPl7MDU4rSHebKPHeitpDfyH0cQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"semver": "^7.3.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/object-assign": {
|
"node_modules/object-assign": {
|
||||||
"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",
|
||||||
@@ -1114,6 +1221,31 @@
|
|||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="
|
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/prebuild-install": {
|
||||||
|
"version": "7.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
||||||
|
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
|
||||||
|
"dependencies": {
|
||||||
|
"detect-libc": "^2.0.0",
|
||||||
|
"expand-template": "^2.0.3",
|
||||||
|
"github-from-package": "0.0.0",
|
||||||
|
"minimist": "^1.2.3",
|
||||||
|
"mkdirp-classic": "^0.5.3",
|
||||||
|
"napi-build-utils": "^2.0.0",
|
||||||
|
"node-abi": "^3.3.0",
|
||||||
|
"pump": "^3.0.0",
|
||||||
|
"rc": "^1.2.7",
|
||||||
|
"simple-get": "^4.0.0",
|
||||||
|
"tar-fs": "^2.0.0",
|
||||||
|
"tunnel-agent": "^0.6.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"prebuild-install": "bin.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/protobufjs": {
|
"node_modules/protobufjs": {
|
||||||
"version": "7.5.4",
|
"version": "7.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
|
||||||
@@ -1199,6 +1331,20 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rc": {
|
||||||
|
"version": "1.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
|
||||||
|
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
|
||||||
|
"dependencies": {
|
||||||
|
"deep-extend": "^0.6.0",
|
||||||
|
"ini": "~1.3.0",
|
||||||
|
"minimist": "^1.2.0",
|
||||||
|
"strip-json-comments": "~2.0.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"rc": "cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/readable-stream": {
|
"node_modules/readable-stream": {
|
||||||
"version": "3.6.2",
|
"version": "3.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||||
@@ -1244,6 +1390,17 @@
|
|||||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||||
},
|
},
|
||||||
|
"node_modules/semver": {
|
||||||
|
"version": "7.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||||
|
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/send": {
|
"node_modules/send": {
|
||||||
"version": "0.19.0",
|
"version": "0.19.0",
|
||||||
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
|
||||||
@@ -1367,6 +1524,49 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/simple-concat": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/simple-get": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"decompress-response": "^6.0.0",
|
||||||
|
"once": "^1.3.1",
|
||||||
|
"simple-concat": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/socket.io": {
|
"node_modules/socket.io": {
|
||||||
"version": "4.8.1",
|
"version": "4.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
|
||||||
@@ -1530,6 +1730,14 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/strip-json-comments": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tar-fs": {
|
"node_modules/tar-fs": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz",
|
||||||
@@ -1564,6 +1772,17 @@
|
|||||||
"node": ">=0.6"
|
"node": ">=0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tunnel-agent": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tweetnacl": {
|
"node_modules/tweetnacl": {
|
||||||
"version": "0.14.5",
|
"version": "0.14.5",
|
||||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
||||||
|
|||||||
+17
-1
@@ -1 +1,17 @@
|
|||||||
{"name":"backend","version":"1.0.0","type":"module","scripts":{"start":"node index.js"},"dependencies":{"axios":"^1.7.0","dockerode":"^4.0.7","dotenv":"^16.0.0","express":"^4.18.0","socket.io":"^4.8.1"}}
|
{
|
||||||
|
"name": "backend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node index.js",
|
||||||
|
"migrate": "node db/migrate.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.7.0",
|
||||||
|
"better-sqlite3": "^9.6.0",
|
||||||
|
"dockerode": "^4.0.7",
|
||||||
|
"dotenv": "^16.0.0",
|
||||||
|
"express": "^4.18.0",
|
||||||
|
"socket.io": "^4.8.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
|
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>
|
||||||
|
|||||||
@@ -6,8 +6,14 @@ services:
|
|||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
ports:
|
ports:
|
||||||
- "4001:4001"
|
- "4001:4001"
|
||||||
|
volumes:
|
||||||
|
- stackpulse_data:/app/backend/data
|
||||||
environment:
|
environment:
|
||||||
PORTAINER_URL: "Your_Portainer_Server_Address"
|
PORTAINER_URL: "Your_Portainer_Server_Address"
|
||||||
PORTAINER_API_KEY: "Your_Portainer_API_Key"
|
PORTAINER_API_KEY: "Your_Portainer_API_Key"
|
||||||
PORTAINER_ENDPOINT_ID: "Your_Portainer_Endpoint_ID"
|
PORTAINER_ENDPOINT_ID: "Your_Portainer_Endpoint_ID"
|
||||||
|
SELF_STACK_ID: "Stackpulse ID"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
stackpulse_data:
|
||||||
|
|||||||
+30
-3
@@ -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/*"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
+25
-13
@@ -1,22 +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",
|
||||||
"socket.io-client": "^4.8.1"
|
"@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": {
|
"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")],
|
||||||
|
};
|
||||||
+10
-12
@@ -1,16 +1,14 @@
|
|||||||
import React from "react";
|
import { Routes, Route, Navigate } from "react-router-dom";
|
||||||
import Stacks from "./Stacks.jsx";
|
import { Dashboard, Auth } from "@/layouts";
|
||||||
|
|
||||||
export default function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-900 text-white">
|
<Routes>
|
||||||
<header className="p-6 bg-gray-800 shadow-md">
|
<Route path="/dashboard/*" element={<Dashboard />} />
|
||||||
<h1 className="text-2xl font-bold text-white">StackPulse</h1>
|
<Route path="/auth/*" element={<Auth />} />
|
||||||
<p className="text-gray-400 mt-1">Verwalte deine Docker Stacks</p>
|
<Route path="*" element={<Navigate to="/dashboard/stacks" replace />} />
|
||||||
</header>
|
</Routes>
|
||||||
<main className="p-6">
|
|
||||||
<Stacks />
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
|||||||
@@ -1,112 +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("");
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const socket = io("/", { transports: ["websocket"] });
|
|
||||||
console.log("🔌 Socket connected");
|
|
||||||
|
|
||||||
socket.on("redeployStatus", async ({ stackId, status }) => {
|
|
||||||
console.log(`🔄 Stack ${stackId} Redeploy Status: ${status ? "running" : "finished"}`);
|
|
||||||
|
|
||||||
if (!status) {
|
|
||||||
// Status nach Redeploy neu vom Server holen
|
|
||||||
try {
|
|
||||||
const res = await axios.get("/api/stacks");
|
|
||||||
setStacks(res.data.sort((a, b) => a.Name.localeCompare(b.Name)));
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Fehler beim Aktualisieren nach Redeploy:", err);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// UI direkt auf redeploying setzen
|
|
||||||
setStacks(prev =>
|
|
||||||
prev.map(stack =>
|
|
||||||
stack.Id === stackId ? { ...stack, redeploying: true } : stack
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => socket.disconnect();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const fetchStacks = async () => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const res = await axios.get("/api/stacks");
|
|
||||||
setStacks(res.data.map(stack => ({ ...stack, redeploying: stack.redeploying || false })));
|
|
||||||
} catch (err) {
|
|
||||||
console.error("❌ Fehler beim Abrufen der Stacks:", err);
|
|
||||||
setError("Fehler beim Laden der Stacks");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchStacks();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleRedeploy = async (stackId) => {
|
|
||||||
setStacks(prev =>
|
|
||||||
prev.map(stack => stack.Id === stackId ? { ...stack, redeploying: true } : stack)
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await axios.put(`/api/stacks/${stackId}/redeploy`);
|
|
||||||
// Socket.IO Event aktualisiert Status automatisch
|
|
||||||
} catch (err) {
|
|
||||||
console.error("❌ Fehler beim Redeploy:", err);
|
|
||||||
setStacks(prev =>
|
|
||||||
prev.map(stack => stack.Id === stackId ? { ...stack, redeploying: false } : stack)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading) return <p className="text-gray-400">Lade Stacks...</p>;
|
|
||||||
if (error) return <p className="text-red-400">{error}</p>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 p-6">
|
|
||||||
{stacks.map(stack => {
|
|
||||||
const isRedeploying = stack.redeploying;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={stack.Id}
|
|
||||||
className={`flex justify-between items-center p-5 rounded-xl shadow-lg transition
|
|
||||||
${isRedeploying ? "bg-gray-700 cursor-not-allowed" : "bg-gray-800 hover:bg-gray-700"}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => handleRedeploy(stack.Id)}
|
|
||||||
disabled={isRedeploying}
|
|
||||||
className={`px-5 py-2 rounded-lg font-medium transition
|
|
||||||
${isRedeploying ? "bg-orange-500 cursor-not-allowed" :
|
|
||||||
"bg-blue-500 hover:bg-blue-600"}`}
|
|
||||||
>
|
|
||||||
{isRedeploying ? "Redeploying" : "Redeploy"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{stacks.length === 0 && <p className="text-gray-400">Keine Stacks gefunden.</p>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
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";
|
||||||
+32
-3
@@ -1,10 +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.jsx";
|
import App from "./App";
|
||||||
import './index.css'; // Tailwind CSS
|
import { BrowserRouter } from "react-router-dom";
|
||||||
|
import { ThemeProvider } from "@material-tailwind/react";
|
||||||
|
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>
|
||||||
<App />
|
<BrowserRouter>
|
||||||
|
<MaintenanceProvider>
|
||||||
|
<ToastProvider>
|
||||||
|
<PageProvider>
|
||||||
|
<ThemeProvider>
|
||||||
|
<MaterialTailwindControllerProvider>
|
||||||
|
<App />
|
||||||
|
</MaterialTailwindControllerProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
</PageProvider>
|
||||||
|
</ToastProvider>
|
||||||
|
</MaintenanceProvider>
|
||||||
|
</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";
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
|
|||||||
+1
File diff suppressed because one or more lines are too long
+87
File diff suppressed because one or more lines are too long
Vendored
+15
@@ -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>
|
||||||
+623
-74
@@ -8,17 +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",
|
||||||
"socket.io-client": "^4.8.1"
|
"axios": "1.7.5",
|
||||||
|
"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": {
|
||||||
@@ -309,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",
|
||||||
@@ -661,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",
|
||||||
@@ -713,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",
|
||||||
@@ -758,11 +917,13 @@
|
|||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/pluginutils": {
|
"node_modules/@remix-run/router": {
|
||||||
"version": "1.0.0-beta.27",
|
"version": "1.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
|
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.10.0.tgz",
|
||||||
"integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
|
"integrity": "sha512-Lm+fYpMfZoEucJ7cMxgt4dYt8jLfbpwRCzAjm9UgSLOkmlqo9gupxt6YX3DY0Fk155NT9l17d/ydi+964uS9Lw==",
|
||||||
"dev": true
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@socket.io/component-emitter": {
|
"node_modules/@socket.io/component-emitter": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
@@ -810,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",
|
||||||
@@ -873,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": [
|
||||||
{
|
{
|
||||||
@@ -904,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": {
|
||||||
@@ -922,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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1079,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",
|
||||||
@@ -1149,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",
|
||||||
@@ -1166,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",
|
||||||
@@ -1452,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",
|
||||||
@@ -1597,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",
|
||||||
@@ -1760,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",
|
||||||
@@ -1895,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"
|
||||||
}
|
}
|
||||||
@@ -1988,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": [
|
||||||
{
|
{
|
||||||
@@ -2008,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"
|
||||||
@@ -2131,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",
|
||||||
@@ -2157,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"
|
||||||
},
|
},
|
||||||
@@ -2167,27 +2553,74 @@
|
|||||||
"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": {
|
||||||
|
"version": "6.17.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.17.0.tgz",
|
||||||
|
"integrity": "sha512-YJR3OTJzi3zhqeJYADHANCGPUu9J+6fT5GLv82UWRGSxu6oJYCKVmxUcaBQuGm9udpWmPsvpme/CdHumqgsoaA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@remix-run/router": "1.10.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-router-dom": {
|
||||||
|
"version": "6.17.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.17.0.tgz",
|
||||||
|
"integrity": "sha512-qWHkkbXQX+6li0COUUPKAUkxjNNqPJuiBd27dVwQGDNsuFBdMbrS6UZ0CLYc4CsbdLYTckn4oB4tGDuPZpPhaQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@remix-run/router": "1.10.0",
|
||||||
|
"react-router": "6.17.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8",
|
||||||
|
"react-dom": ">=16.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/read-cache": {
|
"node_modules/read-cache": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||||
@@ -2491,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",
|
||||||
@@ -2525,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",
|
||||||
@@ -2562,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",
|
||||||
@@ -2601,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",
|
||||||
@@ -2638,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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,935 @@
|
|||||||
|
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
const STATUS_COLORS = {
|
||||||
|
success: "text-green-400",
|
||||||
|
warning: "text-yellow-400",
|
||||||
|
error: "text-red-400",
|
||||||
|
started: "text-blue-300"
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTimestamp = (value) => {
|
||||||
|
if (!value) return "-";
|
||||||
|
const date = new Date(value);
|
||||||
|
if (Number.isNaN(date.getTime())) return value;
|
||||||
|
return date.toLocaleString("de-DE", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeDateParam = (value) => {
|
||||||
|
if (!value) return undefined;
|
||||||
|
return value.replace("T", " ");
|
||||||
|
};
|
||||||
|
|
||||||
|
const FILTER_STORAGE_KEY = "redeployLogFilters";
|
||||||
|
const ALL_OPTION_VALUE = "__all__";
|
||||||
|
const ALL_OPTION_LABEL = "- Alle -";
|
||||||
|
const PER_PAGE_DEFAULT = "50";
|
||||||
|
const PER_PAGE_OPTIONS = [
|
||||||
|
{ value: "10", label: "10" },
|
||||||
|
{ value: "25", label: "25" },
|
||||||
|
{ value: "50", label: "50" },
|
||||||
|
{ value: "100", label: "100" },
|
||||||
|
{ value: "all", label: "Alle" }
|
||||||
|
];
|
||||||
|
const VALID_PER_PAGE_VALUES = new Set(PER_PAGE_OPTIONS.map((option) => option.value));
|
||||||
|
|
||||||
|
const REDEPLOY_TYPE_LABELS = {
|
||||||
|
Einzeln: "Einzeln",
|
||||||
|
Alle: "Alle",
|
||||||
|
Auswahl: "Auswahl",
|
||||||
|
single: "Einzeln",
|
||||||
|
all: "Alle",
|
||||||
|
selection: "Auswahl"
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasActiveFilters = (filters) => Boolean(
|
||||||
|
(filters.stacks && filters.stacks.length) ||
|
||||||
|
(filters.statuses && filters.statuses.length) ||
|
||||||
|
(filters.endpoints && filters.endpoints.length) ||
|
||||||
|
(filters.redeployTypes && filters.redeployTypes.length) ||
|
||||||
|
(filters.message && filters.message.trim()) ||
|
||||||
|
(filters.from && filters.from.trim()) ||
|
||||||
|
(filters.to && filters.to.trim())
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function Logs() {
|
||||||
|
const [logs, setLogs] = useState([]);
|
||||||
|
const [totalLogs, setTotalLogs] = useState(0);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [actionLoading, setActionLoading] = useState(false);
|
||||||
|
|
||||||
|
const [stackOptions, setStackOptions] = useState([]);
|
||||||
|
const [statusOptions, setStatusOptions] = useState([]);
|
||||||
|
const [endpointOptions, setEndpointOptions] = useState([]);
|
||||||
|
const [redeployTypeOptions, setRedeployTypeOptions] = useState([]);
|
||||||
|
|
||||||
|
const [selectedStacks, setSelectedStacks] = useState([]);
|
||||||
|
const [selectedStatuses, setSelectedStatuses] = useState([]);
|
||||||
|
const [selectedEndpoints, setSelectedEndpoints] = useState([]);
|
||||||
|
const [selectedRedeployTypes, setSelectedRedeployTypes] = useState([]);
|
||||||
|
const [messageQuery, setMessageQuery] = useState("");
|
||||||
|
const [fromDate, setFromDate] = useState("");
|
||||||
|
const [toDate, setToDate] = useState("");
|
||||||
|
|
||||||
|
const [filtersOpen, setFiltersOpen] = useState(false);
|
||||||
|
const [filtersReady, setFiltersReady] = useState(false);
|
||||||
|
const [optionsInitialized, setOptionsInitialized] = useState(false);
|
||||||
|
const [refreshSignal, setRefreshSignal] = useState(0);
|
||||||
|
|
||||||
|
const [perPage, setPerPage] = useState(PER_PAGE_DEFAULT);
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
|
||||||
|
const updateFilterOptions = useCallback((payload) => {
|
||||||
|
const logsPayload = Array.isArray(payload) ? payload : payload?.items ?? [];
|
||||||
|
|
||||||
|
const stackMap = new Map();
|
||||||
|
const statusSet = new Set();
|
||||||
|
const endpointSet = new Set();
|
||||||
|
const redeployTypeSet = new Set();
|
||||||
|
|
||||||
|
logsPayload.forEach((log) => {
|
||||||
|
if (log.stackId) {
|
||||||
|
const value = String(log.stackId);
|
||||||
|
const label = log.stackName || `Stack ${value}`;
|
||||||
|
stackMap.set(value, label);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (log.status) {
|
||||||
|
statusSet.add(log.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (log.endpoint !== null && log.endpoint !== undefined && log.endpoint !== "") {
|
||||||
|
endpointSet.add(String(log.endpoint));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (log.redeployType) {
|
||||||
|
redeployTypeSet.add(log.redeployType);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setStackOptions(Array.from(stackMap.entries())
|
||||||
|
.map(([value, label]) => ({ value, label }))
|
||||||
|
.sort((a, b) => a.label.localeCompare(b.label)));
|
||||||
|
|
||||||
|
setStatusOptions(Array.from(statusSet).sort());
|
||||||
|
|
||||||
|
setEndpointOptions(Array.from(endpointSet)
|
||||||
|
.sort((a, b) => a.localeCompare(b, undefined, { numeric: true })));
|
||||||
|
|
||||||
|
setRedeployTypeOptions(Array.from(redeployTypeSet).sort());
|
||||||
|
setOptionsInitialized(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const stackLabelMap = useMemo(() => {
|
||||||
|
const map = new Map();
|
||||||
|
stackOptions.forEach((option) => {
|
||||||
|
map.set(option.value, option.label);
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
}, [stackOptions]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!optionsInitialized) return;
|
||||||
|
setSelectedStacks((prev) => {
|
||||||
|
const valid = prev.filter((value) => stackOptions.some((option) => option.value === value));
|
||||||
|
return valid.length === prev.length ? prev : valid;
|
||||||
|
});
|
||||||
|
}, [optionsInitialized, stackOptions]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!optionsInitialized) return;
|
||||||
|
setSelectedStatuses((prev) => {
|
||||||
|
const valid = prev.filter((value) => statusOptions.includes(value));
|
||||||
|
return valid.length === prev.length ? prev : valid;
|
||||||
|
});
|
||||||
|
}, [optionsInitialized, statusOptions]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!optionsInitialized) return;
|
||||||
|
setSelectedEndpoints((prev) => {
|
||||||
|
const valid = prev.filter((value) => endpointOptions.includes(value));
|
||||||
|
return valid.length === prev.length ? prev : valid;
|
||||||
|
});
|
||||||
|
}, [optionsInitialized, endpointOptions]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!optionsInitialized) return;
|
||||||
|
setSelectedRedeployTypes((prev) => {
|
||||||
|
const valid = prev.filter((value) => redeployTypeOptions.includes(value));
|
||||||
|
return valid.length === prev.length ? prev : valid;
|
||||||
|
});
|
||||||
|
}, [optionsInitialized, redeployTypeOptions]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
setFiltersReady(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const storedValue = window.localStorage.getItem(FILTER_STORAGE_KEY);
|
||||||
|
if (storedValue) {
|
||||||
|
const parsed = JSON.parse(storedValue);
|
||||||
|
const storedFilters = parsed?.filters ?? parsed ?? {};
|
||||||
|
const storedPagination = parsed?.pagination ?? {};
|
||||||
|
|
||||||
|
setSelectedStacks(storedFilters.stacks || []);
|
||||||
|
setSelectedStatuses(storedFilters.statuses || []);
|
||||||
|
setSelectedEndpoints(storedFilters.endpoints || []);
|
||||||
|
setSelectedRedeployTypes(storedFilters.redeployTypes || []);
|
||||||
|
setMessageQuery(storedFilters.message || "");
|
||||||
|
setFromDate(storedFilters.from || "");
|
||||||
|
setToDate(storedFilters.to || "");
|
||||||
|
setFiltersOpen(hasActiveFilters(storedFilters));
|
||||||
|
|
||||||
|
const rawPerPage = storedPagination.perPage;
|
||||||
|
if (rawPerPage !== undefined) {
|
||||||
|
const parsedPerPage = String(rawPerPage);
|
||||||
|
if (VALID_PER_PAGE_VALUES.has(parsedPerPage)) {
|
||||||
|
setPerPage(parsedPerPage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawPage = storedPagination.page;
|
||||||
|
if (typeof rawPage === 'number' && rawPage > 0) {
|
||||||
|
setPage(rawPage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (storageError) {
|
||||||
|
console.error("❌ Fehler beim Laden der gespeicherten Log-Filter:", storageError);
|
||||||
|
} finally {
|
||||||
|
setFiltersReady(true);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const buildFilterParams = useCallback(() => {
|
||||||
|
const params = {};
|
||||||
|
|
||||||
|
if (selectedStacks.length) {
|
||||||
|
params.stackIds = selectedStacks.join(",");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedStatuses.length) {
|
||||||
|
params.statuses = selectedStatuses.join(",");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedEndpoints.length) {
|
||||||
|
params.endpoints = selectedEndpoints.join(",");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedRedeployTypes.length) {
|
||||||
|
params.redeployTypes = selectedRedeployTypes.join(",");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messageQuery.trim()) {
|
||||||
|
params.message = messageQuery.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
const fromParam = normalizeDateParam(fromDate);
|
||||||
|
if (fromParam) {
|
||||||
|
params.from = fromParam;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toParam = normalizeDateParam(toDate);
|
||||||
|
if (toParam) {
|
||||||
|
params.to = toParam;
|
||||||
|
}
|
||||||
|
|
||||||
|
return params;
|
||||||
|
}, [selectedStacks, selectedStatuses, selectedEndpoints, selectedRedeployTypes, messageQuery, fromDate, toDate]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!filtersReady) return;
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
axios.get("/api/logs", { params: { perPage: 'all', page: 1 } })
|
||||||
|
.then((response) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
updateFilterOptions(response.data);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
console.error("⚠️ Konnte Filteroptionen nicht aktualisieren:", err);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [filtersReady, updateFilterOptions, refreshSignal]);
|
||||||
|
|
||||||
|
const currentFilters = useMemo(() => ({
|
||||||
|
stacks: selectedStacks,
|
||||||
|
statuses: selectedStatuses,
|
||||||
|
endpoints: selectedEndpoints,
|
||||||
|
redeployTypes: selectedRedeployTypes,
|
||||||
|
message: messageQuery,
|
||||||
|
from: fromDate,
|
||||||
|
to: toDate
|
||||||
|
}), [selectedStacks, selectedStatuses, selectedEndpoints, selectedRedeployTypes, messageQuery, fromDate, toDate]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!filtersReady) return;
|
||||||
|
|
||||||
|
const params = { ...buildFilterParams() };
|
||||||
|
if (perPage === 'all') {
|
||||||
|
params.perPage = 'all';
|
||||||
|
params.page = 1;
|
||||||
|
} else {
|
||||||
|
params.perPage = perPage;
|
||||||
|
params.page = page;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
setLoading(true);
|
||||||
|
setError("");
|
||||||
|
|
||||||
|
axios.get("/api/logs", { params })
|
||||||
|
.then((response) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
const data = response.data || {};
|
||||||
|
const items = Array.isArray(data) ? data : data.items ?? [];
|
||||||
|
const total = Array.isArray(data) ? items.length : data.total ?? items.length;
|
||||||
|
|
||||||
|
setLogs(items);
|
||||||
|
setTotalLogs(total);
|
||||||
|
|
||||||
|
if (perPage === 'all') {
|
||||||
|
if (page !== 1) setPage(1);
|
||||||
|
} else {
|
||||||
|
const totalPages = Math.max(1, Math.ceil((total || 0) / Number(perPage)));
|
||||||
|
const nextPage = Math.min(Math.max(page, 1), totalPages);
|
||||||
|
if (nextPage !== page) {
|
||||||
|
setPage(nextPage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
console.error("❌ Fehler beim Laden der Logs:", err);
|
||||||
|
setError("Fehler beim Laden der Redeploy-Logs");
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (cancelled) return;
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [filtersReady, buildFilterParams, updateFilterOptions, refreshSignal, perPage, page]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!filtersReady) return;
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(
|
||||||
|
FILTER_STORAGE_KEY,
|
||||||
|
JSON.stringify({
|
||||||
|
filters: currentFilters,
|
||||||
|
pagination: {
|
||||||
|
perPage,
|
||||||
|
page
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (storageError) {
|
||||||
|
console.error("⚠️ Konnte Filter nicht speichern:", storageError);
|
||||||
|
}
|
||||||
|
}, [filtersReady, currentFilters, perPage, page]);
|
||||||
|
|
||||||
|
const handleMultiSelectChange = (setter) => (event) => {
|
||||||
|
const values = Array.from(event.target.selectedOptions).map((option) => option.value);
|
||||||
|
if (values.includes(ALL_OPTION_VALUE)) {
|
||||||
|
setter([]);
|
||||||
|
} else {
|
||||||
|
setter(values);
|
||||||
|
}
|
||||||
|
setPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOptionMouseDown = (event, currentValues, setter) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
const { value } = event.target;
|
||||||
|
if (value === ALL_OPTION_VALUE) {
|
||||||
|
if (currentValues.length) {
|
||||||
|
setter([]);
|
||||||
|
setPage(1);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextValues = currentValues.includes(value)
|
||||||
|
? currentValues.filter((entry) => entry !== value)
|
||||||
|
: [...currentValues, value];
|
||||||
|
|
||||||
|
setter(nextValues);
|
||||||
|
setPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResetFilters = () => {
|
||||||
|
setSelectedStacks([]);
|
||||||
|
setSelectedStatuses([]);
|
||||||
|
setSelectedEndpoints([]);
|
||||||
|
setSelectedRedeployTypes([]);
|
||||||
|
setMessageQuery("");
|
||||||
|
setFromDate("");
|
||||||
|
setToDate("");
|
||||||
|
setFiltersOpen(false);
|
||||||
|
setPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleFilters = () => {
|
||||||
|
setFiltersOpen((prev) => !prev);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRefresh = () => {
|
||||||
|
setRefreshSignal((prev) => prev + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePerPageChange = (event) => {
|
||||||
|
const value = event.target.value;
|
||||||
|
if (!VALID_PER_PAGE_VALUES.has(value)) return;
|
||||||
|
setPerPage(value);
|
||||||
|
setPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePageChange = (nextPage) => {
|
||||||
|
if (perPage === 'all') return;
|
||||||
|
if (nextPage < 1) return;
|
||||||
|
const totalPages = Math.max(1, Math.ceil((totalLogs || 0) / Number(perPage)));
|
||||||
|
if (nextPage > totalPages) return;
|
||||||
|
setPage(nextPage);
|
||||||
|
};
|
||||||
|
|
||||||
|
const stackSelectOptions = useMemo(() => {
|
||||||
|
const entries = stackOptions.filter((option) => option.value !== ALL_OPTION_VALUE);
|
||||||
|
return [
|
||||||
|
{ value: ALL_OPTION_VALUE, label: ALL_OPTION_LABEL },
|
||||||
|
...entries
|
||||||
|
];
|
||||||
|
}, [stackOptions]);
|
||||||
|
|
||||||
|
const statusSelectOptions = useMemo(() => {
|
||||||
|
const entries = statusOptions
|
||||||
|
.filter((status) => status !== ALL_OPTION_VALUE)
|
||||||
|
.map((status) => ({ value: status, label: status }));
|
||||||
|
return [
|
||||||
|
{ value: ALL_OPTION_VALUE, label: ALL_OPTION_LABEL },
|
||||||
|
...entries
|
||||||
|
];
|
||||||
|
}, [statusOptions]);
|
||||||
|
|
||||||
|
const endpointSelectOptions = useMemo(() => {
|
||||||
|
const entries = endpointOptions
|
||||||
|
.filter((endpoint) => endpoint !== ALL_OPTION_VALUE)
|
||||||
|
.map((endpoint) => ({ value: endpoint, label: endpoint }));
|
||||||
|
return [
|
||||||
|
{ value: ALL_OPTION_VALUE, label: ALL_OPTION_LABEL },
|
||||||
|
...entries
|
||||||
|
];
|
||||||
|
}, [endpointOptions]);
|
||||||
|
|
||||||
|
const redeployTypeSelectOptions = useMemo(() => {
|
||||||
|
const entries = redeployTypeOptions
|
||||||
|
.filter((type) => type !== ALL_OPTION_VALUE)
|
||||||
|
.map((type) => ({ value: type, label: REDEPLOY_TYPE_LABELS[type] ?? type }));
|
||||||
|
return [
|
||||||
|
{ value: ALL_OPTION_VALUE, label: ALL_OPTION_LABEL },
|
||||||
|
...entries
|
||||||
|
];
|
||||||
|
}, [redeployTypeOptions]);
|
||||||
|
|
||||||
|
const activeFilterCount = useMemo(() => {
|
||||||
|
let count = 0;
|
||||||
|
if (selectedStacks.length) count += selectedStacks.length;
|
||||||
|
if (selectedStatuses.length) count += selectedStatuses.length;
|
||||||
|
if (selectedEndpoints.length) count += selectedEndpoints.length;
|
||||||
|
if (selectedRedeployTypes.length) count += selectedRedeployTypes.length;
|
||||||
|
if (messageQuery.trim()) count += 1;
|
||||||
|
if (fromDate) count += 1;
|
||||||
|
if (toDate) count += 1;
|
||||||
|
return count;
|
||||||
|
}, [selectedStacks, selectedStatuses, selectedEndpoints, messageQuery, fromDate, toDate]);
|
||||||
|
|
||||||
|
const totalPages = useMemo(() => {
|
||||||
|
if (perPage === 'all') return 1;
|
||||||
|
const numeric = Number(perPage) || Number(PER_PAGE_DEFAULT);
|
||||||
|
return Math.max(1, Math.ceil((totalLogs || 0) / numeric));
|
||||||
|
}, [perPage, totalLogs]);
|
||||||
|
|
||||||
|
const rangeStart = useMemo(() => {
|
||||||
|
if (totalLogs === 0) return 0;
|
||||||
|
if (perPage === 'all') return 1;
|
||||||
|
return (page - 1) * Number(perPage) + 1;
|
||||||
|
}, [totalLogs, perPage, page]);
|
||||||
|
|
||||||
|
const rangeEnd = useMemo(() => {
|
||||||
|
if (totalLogs === 0) return 0;
|
||||||
|
if (perPage === 'all') return totalLogs;
|
||||||
|
return Math.min(totalLogs, (page - 1) * Number(perPage) + logs.length);
|
||||||
|
}, [totalLogs, perPage, page, logs.length]);
|
||||||
|
|
||||||
|
const handleDeleteLog = async (id) => {
|
||||||
|
if (!window.confirm("Diesen Log-Eintrag dauerhaft löschen?")) return;
|
||||||
|
setActionLoading(true);
|
||||||
|
setError("");
|
||||||
|
try {
|
||||||
|
await axios.delete(`/api/logs/${id}`);
|
||||||
|
setRefreshSignal((prev) => prev + 1);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ Fehler beim Löschen des Logs:", err);
|
||||||
|
setError("Fehler beim Löschen des Redeploy-Logs");
|
||||||
|
} finally {
|
||||||
|
setActionLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteFiltered = async () => {
|
||||||
|
if (!logs.length) return;
|
||||||
|
if (!window.confirm("Alle angezeigten Logs (entsprechend Filter) löschen?")) return;
|
||||||
|
setActionLoading(true);
|
||||||
|
setError("");
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
...buildFilterParams(),
|
||||||
|
ids: logs.map((log) => log.id).join(',')
|
||||||
|
};
|
||||||
|
await axios.delete("/api/logs", { params });
|
||||||
|
setRefreshSignal((prev) => prev + 1);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ Fehler beim Löschen der gefilterten Logs:", err);
|
||||||
|
setError("Fehler beim Löschen der gefilterten Logs");
|
||||||
|
} finally {
|
||||||
|
setActionLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExport = async (format) => {
|
||||||
|
setActionLoading(true);
|
||||||
|
setError("");
|
||||||
|
try {
|
||||||
|
const response = await axios.get("/api/logs/export", {
|
||||||
|
params: { ...buildFilterParams(), format },
|
||||||
|
responseType: "blob"
|
||||||
|
});
|
||||||
|
|
||||||
|
const contentType = response.headers["content-type"] || (format === "sql" ? "application/sql" : "text/plain");
|
||||||
|
const disposition = response.headers["content-disposition"] || "";
|
||||||
|
const match = disposition.match(/filename="?([^";]+)"?/i);
|
||||||
|
const filename = match?.[1] || `redeploy-logs.${format}`;
|
||||||
|
|
||||||
|
const blob = new Blob([response.data], { type: contentType });
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = url;
|
||||||
|
link.download = filename;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ Fehler beim Export der Logs:", err);
|
||||||
|
setError("Fehler beim Export der Redeploy-Logs");
|
||||||
|
} finally {
|
||||||
|
setActionLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto max-w-6xl space-y-4 p-6">
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-100">Redeploy-Logs</h2>
|
||||||
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => handleExport('txt')}
|
||||||
|
disabled={actionLoading || loading}
|
||||||
|
className="rounded-md border border-gray-600 px-3 py-1.5 text-sm text-gray-200 transition hover:bg-gray-700 disabled:opacity-60"
|
||||||
|
>
|
||||||
|
Export TXT
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleExport('sql')}
|
||||||
|
disabled={actionLoading || loading}
|
||||||
|
className="rounded-md border border-gray-600 px-3 py-1.5 text-sm text-gray-200 transition hover:bg-gray-700 disabled:opacity-60"
|
||||||
|
>
|
||||||
|
Export SQL
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleDeleteFiltered}
|
||||||
|
disabled={actionLoading || loading || logs.length === 0}
|
||||||
|
className="rounded-md border border-red-600 px-3 py-1.5 text-sm text-red-300 transition hover:bg-red-600/20 disabled:opacity-60"
|
||||||
|
>
|
||||||
|
Angezeigte löschen
|
||||||
|
</button>
|
||||||
|
{loading && <span className="text-sm text-gray-400">Aktualisiere…</span>}
|
||||||
|
<button
|
||||||
|
onClick={handleRefresh}
|
||||||
|
disabled={actionLoading}
|
||||||
|
className="px-4 py-2 rounded-md font-medium transition bg-purple-500 hover:bg-purple-600 disabled:opacity-60"
|
||||||
|
>
|
||||||
|
Aktualisieren
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-900/20 border border-red-500/40 text-red-300 px-4 py-3 rounded-lg">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="rounded-xl border border-gray-700 bg-gray-800/60">
|
||||||
|
<button
|
||||||
|
onClick={handleToggleFilters}
|
||||||
|
className="flex w-full items-center justify-between px-4 py-3 text-left text-sm font-medium text-gray-200 hover:bg-gray-700/40"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Filter {activeFilterCount > 0 && (
|
||||||
|
<span className="ml-2 rounded-full bg-purple-500/20 px-2 py-0.5 text-xs text-purple-300">
|
||||||
|
{activeFilterCount} aktiv
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs uppercase tracking-wide text-gray-400">
|
||||||
|
{filtersOpen ? "Ausblenden" : "Anzeigen"}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{filtersOpen && (
|
||||||
|
<div className="space-y-4 border-t border-gray-700 px-4 py-4">
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-gray-300">Stack</label>
|
||||||
|
<select
|
||||||
|
multiple
|
||||||
|
value={selectedStacks}
|
||||||
|
onChange={handleMultiSelectChange(setSelectedStacks)}
|
||||||
|
className="w-full min-h-[8rem] rounded-md border border-gray-700 bg-gray-900/70 px-3 py-2 text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500"
|
||||||
|
>
|
||||||
|
{stackSelectOptions.map(({ value, label }) => (
|
||||||
|
<option
|
||||||
|
key={value}
|
||||||
|
value={value}
|
||||||
|
onMouseDown={(event) => handleOptionMouseDown(event, selectedStacks, setSelectedStacks)}
|
||||||
|
className={`bg-gray-900 text-gray-200 ${value === ALL_OPTION_VALUE ? 'font-semibold text-gray-100' : ''}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<div className="mt-2 min-h-[1.5rem] text-xs text-gray-400">
|
||||||
|
{selectedStacks.length === 0 ? (
|
||||||
|
<span className="rounded-full bg-gray-700/60 px-2 py-0.5 text-gray-300">
|
||||||
|
Alle Stacks
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{selectedStacks.map((stackId) => (
|
||||||
|
<span
|
||||||
|
key={stackId}
|
||||||
|
className="rounded-full bg-purple-500/20 px-2 py-0.5 text-purple-200"
|
||||||
|
>
|
||||||
|
{stackLabelMap.get(stackId) ?? `Stack ${stackId}`}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-gray-300">Status</label>
|
||||||
|
<select
|
||||||
|
multiple
|
||||||
|
value={selectedStatuses}
|
||||||
|
onChange={handleMultiSelectChange(setSelectedStatuses)}
|
||||||
|
className="w-full min-h-[8rem] rounded-md border border-gray-700 bg-gray-900/70 px-3 py-2 text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500"
|
||||||
|
>
|
||||||
|
{statusSelectOptions.map(({ value, label }) => (
|
||||||
|
<option
|
||||||
|
key={value}
|
||||||
|
value={value}
|
||||||
|
onMouseDown={(event) => handleOptionMouseDown(event, selectedStatuses, setSelectedStatuses)}
|
||||||
|
className={`bg-gray-900 text-gray-200 ${value === ALL_OPTION_VALUE ? 'font-semibold text-gray-100' : ''}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<div className="mt-2 min-h-[1.5rem] text-xs text-gray-400">
|
||||||
|
{selectedStatuses.length === 0 ? (
|
||||||
|
<span className="rounded-full bg-gray-700/60 px-2 py-0.5 text-gray-300">
|
||||||
|
Alle Status
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{selectedStatuses.map((status) => (
|
||||||
|
<span
|
||||||
|
key={status}
|
||||||
|
className="rounded-full bg-amber-500/20 px-2 py-0.5 text-amber-200"
|
||||||
|
>
|
||||||
|
{status}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-gray-300">Endpoint</label>
|
||||||
|
<select
|
||||||
|
multiple
|
||||||
|
value={selectedEndpoints}
|
||||||
|
onChange={handleMultiSelectChange(setSelectedEndpoints)}
|
||||||
|
className="w-full min-h-[8rem] rounded-md border border-gray-700 bg-gray-900/70 px-3 py-2 text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500"
|
||||||
|
>
|
||||||
|
{endpointSelectOptions.map(({ value, label }) => (
|
||||||
|
<option
|
||||||
|
key={value}
|
||||||
|
value={value}
|
||||||
|
onMouseDown={(event) => handleOptionMouseDown(event, selectedEndpoints, setSelectedEndpoints)}
|
||||||
|
className={`bg-gray-900 text-gray-200 ${value === ALL_OPTION_VALUE ? 'font-semibold text-gray-100' : ''}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<div className="mt-2 min-h-[1.5rem] text-xs text-gray-400">
|
||||||
|
{selectedEndpoints.length === 0 ? (
|
||||||
|
<span className="rounded-full bg-gray-700/60 px-2 py-0.5 text-gray-300">
|
||||||
|
Alle Endpoints
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{selectedEndpoints.map((endpoint) => (
|
||||||
|
<span
|
||||||
|
key={endpoint}
|
||||||
|
className="rounded-full bg-blue-500/20 px-2 py-0.5 text-blue-200"
|
||||||
|
>
|
||||||
|
{endpoint}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-gray-300">Redeploy-Typ</label>
|
||||||
|
<select
|
||||||
|
multiple
|
||||||
|
value={selectedRedeployTypes}
|
||||||
|
onChange={handleMultiSelectChange(setSelectedRedeployTypes)}
|
||||||
|
className="w-full min-h-[8rem] rounded-md border border-gray-700 bg-gray-900/70 px-3 py-2 text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500"
|
||||||
|
>
|
||||||
|
{redeployTypeSelectOptions.map(({ value, label }) => (
|
||||||
|
<option
|
||||||
|
key={value}
|
||||||
|
value={value}
|
||||||
|
onMouseDown={(event) => handleOptionMouseDown(event, selectedRedeployTypes, setSelectedRedeployTypes)}
|
||||||
|
className={`bg-gray-900 text-gray-200 ${value === ALL_OPTION_VALUE ? 'font-semibold text-gray-100' : ''}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<div className="mt-2 min-h-[1.5rem] text-xs text-gray-400">
|
||||||
|
{selectedRedeployTypes.length === 0 ? (
|
||||||
|
<span className="rounded-full bg-gray-700/60 px-2 py-0.5 text-gray-300">
|
||||||
|
Alle Typen
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{selectedRedeployTypes.map((type) => (
|
||||||
|
<span
|
||||||
|
key={type}
|
||||||
|
className="rounded-full bg-teal-500/20 px-2 py-0.5 text-teal-200"
|
||||||
|
>
|
||||||
|
{REDEPLOY_TYPE_LABELS[type] ?? type}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="md:col-span-2 lg:col-span-4">
|
||||||
|
<label className="mb-2 block text-sm font-medium text-gray-300">Nachricht (Freitext)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={messageQuery}
|
||||||
|
onChange={(event) => {
|
||||||
|
setMessageQuery(event.target.value);
|
||||||
|
setPage(1);
|
||||||
|
}}
|
||||||
|
placeholder="Textsuche in Log-Nachrichten..."
|
||||||
|
className="w-full rounded-md border border-gray-700 bg-gray-900/70 px-3 py-2 text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-gray-300">Von</label>
|
||||||
|
<input
|
||||||
|
type="datetime-local"
|
||||||
|
value={fromDate}
|
||||||
|
onChange={(event) => {
|
||||||
|
setFromDate(event.target.value);
|
||||||
|
setPage(1);
|
||||||
|
}}
|
||||||
|
className="w-full rounded-md border border-gray-700 bg-gray-900/70 px-3 py-2 text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-gray-300">Bis</label>
|
||||||
|
<input
|
||||||
|
type="datetime-local"
|
||||||
|
value={toDate}
|
||||||
|
onChange={(event) => {
|
||||||
|
setToDate(event.target.value);
|
||||||
|
setPage(1);
|
||||||
|
}}
|
||||||
|
className="w-full rounded-md border border-gray-700 bg-gray-900/70 px-3 py-2 text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center justify-end gap-3">
|
||||||
|
<Button
|
||||||
|
onClick={handleResetFilters}
|
||||||
|
className="rounded-md border border-gray-600 px-4 py-2 text-gray-200 transition hover:bg-gray-700"
|
||||||
|
>
|
||||||
|
Zurücksetzen
|
||||||
|
</Button>
|
||||||
|
</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">
|
||||||
|
<table className="min-w-full divide-y divide-gray-700">
|
||||||
|
<thead className="bg-gray-800">
|
||||||
|
<tr className="text-left text-sm uppercase tracking-wide text-gray-400">
|
||||||
|
<th className="px-4 py-3">Zeitpunkt</th>
|
||||||
|
<th className="px-4 py-3">Stack</th>
|
||||||
|
<th className="px-4 py-3">Art</th>
|
||||||
|
<th className="px-4 py-3">Status</th>
|
||||||
|
<th className="px-4 py-3">Nachricht</th>
|
||||||
|
<th className="px-4 py-3">Endpoint</th>
|
||||||
|
<th className="px-4 py-3 text-right">Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-700 text-sm">
|
||||||
|
{logs.length === 0 && !loading && (
|
||||||
|
<tr>
|
||||||
|
<td colSpan="7" className="px-4 py-6 text-center text-gray-400">
|
||||||
|
Keine Logs vorhanden.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
{logs.map((log) => {
|
||||||
|
const statusClass = STATUS_COLORS[log.status] || "text-blue-300";
|
||||||
|
const stackDisplayName = log.stackName || "Unbekannt";
|
||||||
|
const showStackId = stackDisplayName !== '---' && log.stackId !== undefined && log.stackId !== null;
|
||||||
|
const redeployTypeLabel = log.redeployType
|
||||||
|
? (REDEPLOY_TYPE_LABELS[log.redeployType] ?? log.redeployType)
|
||||||
|
: '---';
|
||||||
|
return (
|
||||||
|
<tr key={log.id} className="hover:bg-gray-700/40">
|
||||||
|
<td className="px-4 py-3 whitespace-nowrap text-gray-300">
|
||||||
|
{formatTimestamp(log.timestamp)}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-gray-200">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">{stackDisplayName}</span>
|
||||||
|
{showStackId && (
|
||||||
|
<span className="text-xs text-gray-400">ID: {log.stackId}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-gray-300">
|
||||||
|
{redeployTypeLabel}
|
||||||
|
</td>
|
||||||
|
<td className={`px-4 py-3 font-semibold ${statusClass}`}>
|
||||||
|
{log.status}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-gray-300">
|
||||||
|
{log.message || "-"}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-gray-400">
|
||||||
|
{log.endpoint ?? "-"}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-right">
|
||||||
|
<button
|
||||||
|
onClick={() => handleDeleteLog(log.id)}
|
||||||
|
disabled={actionLoading}
|
||||||
|
className="rounded-md border border-red-600 px-3 py-1 text-xs text-red-300 transition hover:bg-red-600/20 disabled:opacity-60"
|
||||||
|
>
|
||||||
|
Löschen
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-3 text-sm text-gray-300">
|
||||||
|
<span>
|
||||||
|
{totalLogs === 0
|
||||||
|
? "Keine Einträge"
|
||||||
|
: perPage === 'all'
|
||||||
|
? `Zeige alle ${totalLogs} Einträge`
|
||||||
|
: `Zeige ${rangeStart.toLocaleString()} – ${rangeEnd.toLocaleString()} von ${totalLogs.toLocaleString()} Einträgen`}
|
||||||
|
</span>
|
||||||
|
{perPage !== 'all' && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => handlePageChange(page - 1)}
|
||||||
|
disabled={page <= 1 || actionLoading}
|
||||||
|
className="rounded-md border border-gray-600 px-3 py-1 text-gray-200 transition hover:bg-gray-700 disabled:opacity-60"
|
||||||
|
>
|
||||||
|
Zurück
|
||||||
|
</button>
|
||||||
|
<span className="text-gray-400">
|
||||||
|
Seite {page} / {totalPages}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => handlePageChange(page + 1)}
|
||||||
|
disabled={page >= totalPages || actionLoading}
|
||||||
|
className="rounded-md border border-gray-600 px-3 py-1 text-gray-200 transition hover:bg-gray-700 disabled:opacity-60"
|
||||||
|
>
|
||||||
|
Weiter
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Aktuellen Branch herausfinden
|
|
||||||
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
|
||||||
|
|
||||||
# Branch bestätigen
|
|
||||||
read -p "Aktueller Branch: $BRANCH. Ist das korrekt? (y/n): " CONFIRM
|
|
||||||
if [[ "$CONFIRM" != "y" && "$CONFIRM" != "Y" ]]; then
|
|
||||||
echo "Abgebrochen."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Commit-Nachricht
|
|
||||||
read -p "Bitte Commit-Nachricht eingeben (default: 'Update'): " COMMIT_MSG
|
|
||||||
COMMIT_MSG=${COMMIT_MSG:-Update}
|
|
||||||
|
|
||||||
# Änderungen stagen und committen
|
|
||||||
echo "Änderungen werden gestaged..."
|
|
||||||
git add .
|
|
||||||
echo "Commit wird erstellt..."
|
|
||||||
git commit -m "$COMMIT_MSG" || echo "Nichts zu committen"
|
|
||||||
|
|
||||||
# Prüfen, ob Branch master ist
|
|
||||||
if [[ "$BRANCH" == "master" ]]; then
|
|
||||||
# Versionsnummer verpflichtend abfragen
|
|
||||||
while true; do
|
|
||||||
read -p "Bitte Versionsnummer für Master-Release Tag eingeben (z.B. v0.2.0): " VERSION_TAG
|
|
||||||
if [[ -n "$VERSION_TAG" ]]; then
|
|
||||||
break
|
|
||||||
else
|
|
||||||
echo "Versionsnummer darf nicht leer sein. Bitte eingeben."
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Push Branch
|
|
||||||
echo "Push nach origin/$BRANCH..."
|
|
||||||
git push origin "$BRANCH"
|
|
||||||
|
|
||||||
# Tag setzen und pushen, nur bei master
|
|
||||||
if [[ "$BRANCH" == "master" ]]; then
|
|
||||||
git tag -a "$VERSION_TAG" -m "Release $VERSION_TAG"
|
|
||||||
git push origin "$VERSION_TAG"
|
|
||||||
echo "Tag $VERSION_TAG gesetzt und gepusht."
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Push abgeschlossen."
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Skript: Dev in Master mergen
|
|
||||||
|
|
||||||
DEV_BRANCH="dev"
|
|
||||||
MASTER_BRANCH="master"
|
|
||||||
|
|
||||||
# Auf master wechseln und aktuell holen
|
|
||||||
git checkout $MASTER_BRANCH
|
|
||||||
git pull origin $MASTER_BRANCH
|
|
||||||
|
|
||||||
# Dev aktuell holen
|
|
||||||
git checkout $DEV_BRANCH
|
|
||||||
git pull origin $DEV_BRANCH
|
|
||||||
|
|
||||||
# Merge Dev in Master
|
|
||||||
git checkout $MASTER_BRANCH
|
|
||||||
git merge --no-ff $DEV_BRANCH -m "Merge $DEV_BRANCH into $MASTER_BRANCH"
|
|
||||||
|
|
||||||
# Push Master auf Remote
|
|
||||||
git push origin $MASTER_BRANCH
|
|
||||||
|
|
||||||
echo "Branch $DEV_BRANCH wurde in $MASTER_BRANCH gemerged."
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Skript: Feature in Dev mergen mit Auswahl der Feature-Branches
|
|
||||||
|
|
||||||
DEV_BRANCH="dev"
|
|
||||||
|
|
||||||
# Auflisten aller Feature-Branches (lokal und remote)
|
|
||||||
echo "Verfügbare Feature-Branches:"
|
|
||||||
FEATURE_BRANCHES=$(git branch -r | grep 'origin/feature/' | sed 's|origin/||')
|
|
||||||
PS3="Bitte wähle einen Feature-Branch zum Mergen: "
|
|
||||||
|
|
||||||
# in ein Array konvertieren
|
|
||||||
BRANCH_ARRAY=($FEATURE_BRANCHES)
|
|
||||||
|
|
||||||
# Auswahlmenü anzeigen
|
|
||||||
select CHOSEN_BRANCH in "${BRANCH_ARRAY[@]}"; do
|
|
||||||
if [[ -n "$CHOSEN_BRANCH" ]]; then
|
|
||||||
FEATURE_BRANCH=$CHOSEN_BRANCH
|
|
||||||
break
|
|
||||||
else
|
|
||||||
echo "Ungültige Auswahl. Bitte erneut versuchen."
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "Ausgewählter Feature-Branch: $FEATURE_BRANCH"
|
|
||||||
|
|
||||||
# Dev aktuell holen
|
|
||||||
git checkout $DEV_BRANCH
|
|
||||||
git pull origin $DEV_BRANCH
|
|
||||||
|
|
||||||
# Feature-Branch aktuell holen
|
|
||||||
git checkout $FEATURE_BRANCH
|
|
||||||
git pull origin $FEATURE_BRANCH
|
|
||||||
|
|
||||||
# Merge Feature in Dev
|
|
||||||
git checkout $DEV_BRANCH
|
|
||||||
git merge --no-ff $FEATURE_BRANCH -m "Merge $FEATURE_BRANCH into $DEV_BRANCH"
|
|
||||||
|
|
||||||
# Push Dev auf Remote
|
|
||||||
git push origin $DEV_BRANCH
|
|
||||||
|
|
||||||
echo "Feature $FEATURE_BRANCH wurde erfolgreich in $DEV_BRANCH gemerged."
|
|
||||||
@@ -6,6 +6,7 @@ echo "🚀 Starte StackPulse Dev-Umgebung..."
|
|||||||
# --- Backend ---
|
# --- Backend ---
|
||||||
cd backend
|
cd backend
|
||||||
npm install
|
npm install
|
||||||
|
npm run migrate
|
||||||
npm start &
|
npm start &
|
||||||
BACK_PID=$!
|
BACK_PID=$!
|
||||||
cd ..
|
cd ..
|
||||||
|
|||||||
@@ -1,87 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# ===============================
|
|
||||||
# Dateien sichern, die nicht gepusht werden
|
|
||||||
# ===============================
|
|
||||||
UNVERSIONED_FILES=("scripts/docker-release.sh")
|
|
||||||
for f in "${UNVERSIONED_FILES[@]}"; do
|
|
||||||
if [[ -f $f ]]; then
|
|
||||||
mkdir -p /tmp/git_safe_backup
|
|
||||||
cp "$f" "/tmp/git_safe_backup/$(basename "$f")"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# ===============================
|
|
||||||
# Alle Branches sammeln
|
|
||||||
# ===============================
|
|
||||||
# Alle lokalen Branches holen, führende Sternchen und Leerzeichen entfernen
|
|
||||||
LOCAL_BRANCHES=$(git branch | sed 's/* //' | sed 's/^[[:space:]]*//')
|
|
||||||
|
|
||||||
# Alle Remote-Branches holen, 'origin/' entfernen, HEAD ignorieren, führende Leerzeichen entfernen
|
|
||||||
REMOTE_BRANCHES=$(git branch -r | grep -v 'HEAD' | sed 's|origin/||' | sed 's/^[[:space:]]*//')
|
|
||||||
|
|
||||||
# Beide Listen zusammenführen und Duplikate entfernen
|
|
||||||
ALL_BRANCHES=$(echo -e "$LOCAL_BRANCHES\n$REMOTE_BRANCHES" | sort -u)
|
|
||||||
|
|
||||||
# Master und dev extrahieren, restliche feature-Branches alphabetisch
|
|
||||||
MASTER_BRANCH=$(echo "$ALL_BRANCHES" | grep -x 'master')
|
|
||||||
DEV_BRANCH=$(echo "$ALL_BRANCHES" | grep -x 'dev')
|
|
||||||
FEATURE_BRANCHES=$(echo "$ALL_BRANCHES" | grep '^feature/' | sort)
|
|
||||||
|
|
||||||
# Zusammenführen
|
|
||||||
SORTED_BRANCHES=()
|
|
||||||
[[ -n "$MASTER_BRANCH" ]] && SORTED_BRANCHES+=("$MASTER_BRANCH")
|
|
||||||
[[ -n "$DEV_BRANCH" ]] && SORTED_BRANCHES+=("$DEV_BRANCH")
|
|
||||||
for branch in $FEATURE_BRANCHES; do
|
|
||||||
SORTED_BRANCHES+=("$branch")
|
|
||||||
done
|
|
||||||
|
|
||||||
# Branches nummerieren
|
|
||||||
echo "Verfügbare Branches:"
|
|
||||||
i=1
|
|
||||||
declare -A BRANCH_MAP
|
|
||||||
for branch in "${SORTED_BRANCHES[@]}"; do
|
|
||||||
echo "$i) $branch"
|
|
||||||
BRANCH_MAP[$i]=$branch
|
|
||||||
((i++))
|
|
||||||
done
|
|
||||||
|
|
||||||
# Auswahl abfragen
|
|
||||||
read -p "Wähle einen Branch (Nummer): " choice
|
|
||||||
|
|
||||||
# Validierung
|
|
||||||
if [[ -z "${BRANCH_MAP[$choice]}" ]]; then
|
|
||||||
echo "Ungültige Auswahl!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
SELECTED_BRANCH=${BRANCH_MAP[$choice]}
|
|
||||||
echo "Wechsle zu Branch: $SELECTED_BRANCH"
|
|
||||||
|
|
||||||
# Remote-Stand holen
|
|
||||||
git fetch origin
|
|
||||||
|
|
||||||
# Branch wechseln (lokal erstellen, falls nur remote vorhanden)
|
|
||||||
if git show-ref --verify --quiet refs/heads/$SELECTED_BRANCH; then
|
|
||||||
git checkout $SELECTED_BRANCH
|
|
||||||
else
|
|
||||||
git checkout -b $SELECTED_BRANCH origin/$SELECTED_BRANCH
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Arbeitsverzeichnis exakt auf Remote-Branch zurücksetzen
|
|
||||||
git reset --hard origin/$SELECTED_BRANCH
|
|
||||||
git clean -fd
|
|
||||||
|
|
||||||
# ===============================
|
|
||||||
# Gesicherte unversionierte Dateien zurückkopieren
|
|
||||||
# ===============================
|
|
||||||
for f in "${UNVERSIONED_FILES[@]}"; do
|
|
||||||
if [[ -f "/tmp/git_safe_backup/$(basename "$f")" ]]; then
|
|
||||||
mkdir -p "$(dirname "$f")"
|
|
||||||
mv "/tmp/git_safe_backup/$(basename "$f")" "$f"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
rm -rf /tmp/git_safe_backup
|
|
||||||
|
|
||||||
echo "Branch '$SELECTED_BRANCH' ist nun aktiv. Arbeitsverzeichnis entspricht exakt dem Remote-Stand."
|
|
||||||
echo "Unversionierte Dateien wurden wiederhergestellt."
|
|
||||||
Reference in New Issue
Block a user