add hosting plans via fossbilling
Deploy Nexus Portal to HestiaCP (FTP) / build-and-deploy (push) Successful in 33s

This commit is contained in:
2026-06-12 23:25:17 +02:00
parent 33c8babdd2
commit 48793034b1
8 changed files with 676 additions and 156 deletions
+406
View File
@@ -8,19 +8,24 @@
"name": "portail-gise",
"version": "0.0.0",
"dependencies": {
"lucide-react": "^1.3.0",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"react-router-dom": "^7.16.0"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@tailwindcss/vite": "^4.3.1",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"autoprefixer": "^10.5.0",
"eslint": "^10.3.0",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.6.0",
"postcss": "^8.5.15",
"tailwindcss": "^4.3.1",
"vite": "^8.0.12"
}
},
@@ -835,6 +840,278 @@
"dev": true,
"license": "MIT"
},
"node_modules/@tailwindcss/node": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.1.tgz",
"integrity": "sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/remapping": "^2.3.5",
"enhanced-resolve": "5.21.6",
"jiti": "^2.7.0",
"lightningcss": "1.32.0",
"magic-string": "^0.30.21",
"source-map-js": "^1.2.1",
"tailwindcss": "4.3.1"
}
},
"node_modules/@tailwindcss/oxide": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.1.tgz",
"integrity": "sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 20"
},
"optionalDependencies": {
"@tailwindcss/oxide-android-arm64": "4.3.1",
"@tailwindcss/oxide-darwin-arm64": "4.3.1",
"@tailwindcss/oxide-darwin-x64": "4.3.1",
"@tailwindcss/oxide-freebsd-x64": "4.3.1",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.1",
"@tailwindcss/oxide-linux-arm64-gnu": "4.3.1",
"@tailwindcss/oxide-linux-arm64-musl": "4.3.1",
"@tailwindcss/oxide-linux-x64-gnu": "4.3.1",
"@tailwindcss/oxide-linux-x64-musl": "4.3.1",
"@tailwindcss/oxide-wasm32-wasi": "4.3.1",
"@tailwindcss/oxide-win32-arm64-msvc": "4.3.1",
"@tailwindcss/oxide-win32-x64-msvc": "4.3.1"
}
},
"node_modules/@tailwindcss/oxide-android-arm64": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.1.tgz",
"integrity": "sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-darwin-arm64": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.1.tgz",
"integrity": "sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-darwin-x64": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.1.tgz",
"integrity": "sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-freebsd-x64": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.1.tgz",
"integrity": "sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.1.tgz",
"integrity": "sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.1.tgz",
"integrity": "sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.1.tgz",
"integrity": "sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.1.tgz",
"integrity": "sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.1.tgz",
"integrity": "sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.1.tgz",
"integrity": "sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==",
"bundleDependencies": [
"@napi-rs/wasm-runtime",
"@emnapi/core",
"@emnapi/runtime",
"@tybys/wasm-util",
"@emnapi/wasi-threads",
"tslib"
],
"cpu": [
"wasm32"
],
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.10.0",
"@emnapi/runtime": "^1.10.0",
"@emnapi/wasi-threads": "^1.2.1",
"@napi-rs/wasm-runtime": "^1.1.4",
"@tybys/wasm-util": "^0.10.2",
"tslib": "^2.8.1"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.1.tgz",
"integrity": "sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.1.tgz",
"integrity": "sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/vite": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.1.tgz",
"integrity": "sha512-hItDHuIIlEV61R+faXu66s1K36aTurO/Qw0e45Vskz57gXl9pWOT6eg3zmcEui6CZXddbN7zd41bwmvag4JGwQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@tailwindcss/node": "4.3.1",
"@tailwindcss/oxide": "4.3.1",
"tailwindcss": "4.3.1"
},
"peerDependencies": {
"vite": "^5.2.0 || ^6 || ^7 || ^8"
}
},
"node_modules/@tybys/wasm-util": {
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
@@ -953,6 +1230,43 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/autoprefixer": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz",
"integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/autoprefixer"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"browserslist": "^4.28.2",
"caniuse-lite": "^1.0.30001787",
"fraction.js": "^5.3.4",
"picocolors": "^1.1.1",
"postcss-value-parser": "^4.2.0"
},
"bin": {
"autoprefixer": "bin/autoprefixer"
},
"engines": {
"node": "^10 || ^12 || >=14"
},
"peerDependencies": {
"postcss": "^8.1.0"
}
},
"node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
@@ -1128,6 +1442,20 @@
"dev": true,
"license": "ISC"
},
"node_modules/enhanced-resolve": {
"version": "5.21.6",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz",
"integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
"tapable": "^2.3.3"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@@ -1423,6 +1751,20 @@
"dev": true,
"license": "ISC"
},
"node_modules/fraction.js": {
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
"integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "*"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -1474,6 +1816,13 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true,
"license": "ISC"
},
"node_modules/hermes-estree": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
@@ -1541,6 +1890,16 @@
"dev": true,
"license": "ISC"
},
"node_modules/jiti": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
"integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
"dev": true,
"license": "MIT",
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -1906,6 +2265,25 @@
"yallist": "^3.0.2"
}
},
"node_modules/lucide-react": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.3.0.tgz",
"integrity": "sha512-aKUU4jKpXe26Y3xmF3DGg+NXHteRCVT96r/+Jp7wziqvmFr0/x8ReRsES0wHgTvW7OLxiHvFLN+YeyDnK6h0dQ==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/minimatch": {
"version": "10.2.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
@@ -2084,6 +2462,13 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/postcss-value-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true,
"license": "MIT"
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -2252,6 +2637,27 @@
"node": ">=0.10.0"
}
},
"node_modules/tailwindcss": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.1.tgz",
"integrity": "sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==",
"dev": true,
"license": "MIT"
},
"node_modules/tapable": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
"integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
},
"node_modules/tinyglobby": {
"version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+5
View File
@@ -10,19 +10,24 @@
"preview": "vite preview"
},
"dependencies": {
"lucide-react": "^1.3.0",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"react-router-dom": "^7.16.0"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@tailwindcss/vite": "^4.3.1",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"autoprefixer": "^10.5.0",
"eslint": "^10.3.0",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.6.0",
"postcss": "^8.5.15",
"tailwindcss": "^4.3.1",
"vite": "^8.0.12"
}
}
+2
View File
@@ -16,6 +16,7 @@ import Home from './pages/public/Home';
// Import des Pages Privées (Espace Client)
import Dashboard from './pages/app/Dashboard';
import Store from './pages/app/Store';
//import Services from './pages/app/Services';
export default function App() {
@@ -42,6 +43,7 @@ export default function App() {
{/* Si autorisé, on charge l'interface avec la Sidebar */}
<Route element={<AppLayout />}>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/store" element={<Store />} />
{/* <Route path="/services" element={<Services />} /> */}
</Route>
+2
View File
@@ -1,3 +1,5 @@
@import "tailwindcss";
body {
margin: 0;
display:block;
+119 -154
View File
@@ -1,163 +1,128 @@
// src/pages/app/Dashboard.jsx
import { Link } from 'react-router-dom';
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { getClientOrders } from '../../services/api';
import { Server, Database, Cloud, Globe, Plus, AlertCircle, Loader } from 'lucide-react';
export default function Dashboard() {
// --- DESIGN SYSTEM DU DASHBOARD ---
const gridStyle = {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
gap: '25px',
marginTop: '30px'
};
const navigate = useNavigate();
const [orders, setOrders] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
const cardStyle = {
backgroundColor: '#121212',
border: '1px solid #222',
borderTop: '4px solid #00E5FF',
padding: '25px',
position: 'relative',
overflow: 'hidden'
};
// Chargement des données à l'ouverture du Sas
useEffect(() => {
const fetchInventory = async () => {
try {
// Appel à FOSSBilling via notre api.js
const data = await getClientOrders();
const statusBadgeStyle = {
position: 'absolute',
top: '20px',
right: '20px',
backgroundColor: 'rgba(0, 229, 255, 0.1)',
color: '#00E5FF',
padding: '4px 8px',
fontSize: '0.7rem',
fontWeight: 'bold',
letterSpacing: '1px'
};
// FOSSBilling renvoie souvent la liste dans data.list
setOrders(data.list || []);
} catch (err) {
setError(err.message || "Impossible de récupérer la télémétrie des services.");
} finally {
setIsLoading(false);
}
};
const btnStyle = {
display: 'inline-block',
width: '100%',
padding: '10px',
marginTop: '20px',
backgroundColor: 'transparent',
color: '#00E5FF',
border: '1px solid #00E5FF',
textAlign: 'center',
textDecoration: 'none',
textTransform: 'uppercase',
fontSize: '0.8rem',
cursor: 'pointer',
transition: 'all 0.2s',
boxSizing: 'border-box'
};
fetchInventory();
}, []);
return (
<div style={{ fontFamily: 'monospace' }}>
// Fonction Radar : Détecte le type de service selon son nom pour afficher la bonne icône
const getServiceIcon = (title) => {
const t = title.toLowerCase();
if (t.includes('vps') || t.includes('serveur')) return <Server className="w-10 h-10 text-cyan-400" />;
if (t.includes('cloud') || t.includes('nextcloud')) return <Cloud className="w-10 h-10 text-blue-400" />;
if (t.includes('db') || t.includes('base') || t.includes('sql')) return <Database className="w-10 h-10 text-purple-400" />;
return <Globe className="w-10 h-10 text-emerald-400" />; // Par défaut : Web / Hestia
};
{/* --- EN-TÊTE DU TABLEAU DE BORD --- */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', borderBottom: '1px solid #222', paddingBottom: '20px' }}>
<div>
<h2 style={{ color: '#FFF', margin: '0 0 5px 0', fontSize: '2rem', textTransform: 'uppercase' }}>
Console Opérationnelle
</h2>
<span style={{ color: '#888' }}>ID Réseau: <span style={{ color: '#E0E0E0' }}>usr_8472_alpha</span></span>
// Fonction d'état : Formate le statut du service
const getStatusBadge = (status) => {
switch (status) {
case 'active': return <span className="px-2 py-1 text-xs text-green-400 bg-green-400/10 border border-green-400/20 rounded">ACTIF</span>;
case 'pending_setup': return <span className="px-2 py-1 text-xs text-orange-400 bg-orange-400/10 border border-orange-400/20 rounded">EN PRÉPARATION</span>;
case 'suspended': return <span className="px-2 py-1 text-xs text-red-400 bg-red-400/10 border border-red-400/20 rounded">SUSPENDU</span>;
default: return <span className="px-2 py-1 text-xs text-gray-400 bg-gray-400/10 border border-gray-400/20 rounded">{status.toUpperCase()}</span>;
}
};
return (
<div className="w-full max-w-6xl p-6 mx-auto">
<header className="mb-8">
<h1 className="text-3xl font-bold text-white tracking-widest">
TERMINAL <span className="text-cyan-400">NEXUS</span>
</h1>
<p className="text-gray-400 mt-2">Aperçu de vos accréditations réseau et infrastructures.</p>
</header>
{/* GESTION DES ERREURS & CHARGEMENT */}
{isLoading && (
<div className="flex items-center space-x-3 text-cyan-400">
<Loader className="w-6 h-6 animate-spin" />
<span>Synchronisation avec l'orchestrateur en cours...</span>
</div>
)}
{error && (
<div className="flex items-center space-x-3 text-red-400 bg-red-400/10 border border-red-400 p-4 rounded-lg">
<AlertCircle className="w-6 h-6" />
<span>{error}</span>
</div>
)}
{/* GRILLE DES SERVICES */}
{!isLoading && !error && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{/* Boucle sur les services FOSSBilling */}
{orders.map((order) => (
<div
key={order.id}
className="bg-gray-900 border border-gray-800 p-6 rounded-xl hover:border-cyan-400/50 transition-colors group cursor-pointer flex flex-col justify-between"
onClick={() => navigate(`/services/${order.id}`)} // Redirection future vers les détails
>
<div>
<div className="flex justify-between items-start mb-4">
<div className="p-3 bg-black/50 rounded-lg group-hover:scale-110 transition-transform">
{getServiceIcon(order.title)}
</div>
{getStatusBadge(order.status)}
</div>
<h3 className="text-lg font-semibold text-white truncate" title={order.title}>
{order.title}
</h3>
<p className="text-sm text-gray-500 mt-1">
Facturation : {order.period}
</p>
</div>
<div className="mt-6 pt-4 border-t border-gray-800 flex justify-between items-center text-sm">
<span className="text-gray-400">ID Réseau: #{order.id}</span>
<span className="text-cyan-400 group-hover:underline">Gérer &gt;</span>
</div>
</div>
))}
{/* LA CARTE : OBTENIR UN NOUVEAU PRODUIT */}
<div
onClick={() => navigate('/store')} // Remplace /store par l'URL de ton catalogue
className="bg-transparent border-2 border-dashed border-gray-700 hover:border-cyan-400 p-6 rounded-xl transition-colors cursor-pointer flex flex-col items-center justify-center text-center group min-h-[200px]"
>
<div className="p-3 bg-gray-800/50 rounded-full group-hover:bg-cyan-400/20 transition-colors mb-4">
<Plus className="w-8 h-8 text-gray-400 group-hover:text-cyan-400" />
</div>
<h3 className="text-lg font-semibold text-white group-hover:text-cyan-400">
Demander une accréditation
</h3>
<p className="text-sm text-gray-500 mt-2">
Déployer un nouveau serveur Web, VPS ou Cloud.
</p>
</div>
</div>
)}
</div>
<div style={{ textAlign: 'right' }}>
<div style={{ color: '#00FF66', fontSize: '0.85rem', marginBottom: '5px' }}> SYSTÈME NOMINAL</div>
<div style={{ color: '#555', fontSize: '0.75rem' }}>Dernière connexion : Aujourd'hui</div>
</div>
</div>
{/* --- GRILLE DES MODULES DE L'INFRASTRUCTURE --- */}
<div style={gridStyle}>
{/* MODULE 1 : BARE-METAL (HESTIACP) */}
<div style={cardStyle}>
<div style={statusBadgeStyle}>ACTIF</div>
<h3 style={{ color: '#FFF', marginTop: 0, fontSize: '1.2rem' }}>// Serveur Web</h3>
<p style={{ color: '#888', fontSize: '0.85rem', lineHeight: '1.5' }}>
Nœud HestiaCP (panel.gise.be). Supervision des partitions web, DNS et bases de données.
</p>
<div style={{ margin: '20px 0', fontSize: '0.85rem', color: '#A0A0A0' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
<span>Domaines Actifs:</span> <span style={{ color: '#FFF' }}>1 / 5</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span>Bases SQL:</span> <span style={{ color: '#FFF' }}>2 / 10</span>
</div>
</div>
<a href="https://panel.gise.be" target="_blank" rel="noopener noreferrer" style={btnStyle}
onMouseOver={(e) => { e.target.style.backgroundColor = '#00E5FF'; e.target.style.color = '#000'; }}
onMouseOut={(e) => { e.target.style.backgroundColor = 'transparent'; e.target.style.color = '#00E5FF'; }}>
Accéder au Panel
</a>
</div>
{/* MODULE 2 : CLOUD (NEXTCLOUD) */}
<div style={cardStyle}>
<div style={statusBadgeStyle}>ACTIF</div>
<h3 style={{ color: '#FFF', marginTop: 0, fontSize: '1.2rem' }}>// Sanctuaire Cloud</h3>
<p style={{ color: '#888', fontSize: '0.85rem', lineHeight: '1.5' }}>
Stockage chiffré Nextcloud (cloud.gise.be). Synchronisation des terminaux et partage sécurisé.
</p>
<div style={{ margin: '20px 0', fontSize: '0.85rem', color: '#A0A0A0' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
<span>Espace Alloué:</span> <span style={{ color: '#FFF' }}>50 Go</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span>Espace Utilisé:</span> <span style={{ color: '#FFF' }}>1.2 Go</span>
</div>
</div>
<a href="https://cloud.gise.be" target="_blank" rel="noopener noreferrer" style={btnStyle}
onMouseOver={(e) => { e.target.style.backgroundColor = '#00E5FF'; e.target.style.color = '#000'; }}
onMouseOut={(e) => { e.target.style.backgroundColor = 'transparent'; e.target.style.color = '#00E5FF'; }}>
Ouvrir le Cloud
</a>
</div>
{/* MODULE 3 : FACTURATION (FOSSBILLING) */}
<div style={cardStyle}>
<div style={statusBadgeStyle} style={{ ...statusBadgeStyle, color: '#FFB800', backgroundColor: 'rgba(255, 184, 0, 0.1)' }}>EN ATTENTE</div>
<h3 style={{ color: '#FFF', marginTop: 0, fontSize: '1.2rem' }}>// Facturation</h3>
<p style={{ color: '#888', fontSize: '0.85rem', lineHeight: '1.5' }}>
Centre de gestion FOSSBilling. Historique des paiements et renouvellement des baux réseau.
</p>
<div style={{ margin: '20px 0', fontSize: '0.85rem', color: '#A0A0A0' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
<span>Solde Compte:</span> <span style={{ color: '#FFF' }}>0.00 </span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span>Prochaine Échéance:</span> <span style={{ color: '#FFF' }}>12/07/2026</span>
</div>
</div>
<Link to="/billing" style={{ ...btnStyle, borderColor: '#FFB800', color: '#FFB800' }}
onMouseOver={(e) => { e.target.style.backgroundColor = '#FFB800'; e.target.style.color = '#000'; }}
onMouseOut={(e) => { e.target.style.backgroundColor = 'transparent'; e.target.style.color = '#FFB800'; }}>
Régler la facture
</Link>
</div>
</div>
{/* --- TERMINAL DE LOGS (ESTHÉTIQUE SYSADMIN) --- */}
<div style={{ marginTop: '40px', backgroundColor: '#0A0A0A', border: '1px solid #1A1A1A', padding: '20px' }}>
<h4 style={{ color: '#555', marginTop: 0, fontSize: '0.8rem', letterSpacing: '1px' }}>&gt;_ SYSTEM_LOGS</h4>
<div style={{ color: '#444', fontSize: '0.75rem', lineHeight: '1.8' }}>
<div>[ OK ] Connexion chiffrée établie via TLS v1.3</div>
<div>[ OK ] Jetons d'authentification synchronisés avec FOSSBilling API</div>
<div>[ INFO ] Vérification des quotas de stockage Nextcloud... Terminée.</div>
<div>[ INFO ] 0 ticket(s) de support en attente.</div>
<div><span style={{ color: '#00E5FF' }}>&gt;</span> En attente d'instructions...<span style={{ animation: 'blink 1s step-end infinite' }}>_</span></div>
</div>
</div>
{/* Animation CSS pour le curseur clignotant du terminal */}
<style>
{`
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
`}
</style>
</div>
);
);
}
+135
View File
@@ -0,0 +1,135 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { getProductList } from '../../services/api';
import { Server, Database, Cloud, Globe, ShoppingCart, Loader, AlertCircle, Check } from 'lucide-react';
export default function Store() {
const navigate = useNavigate();
const [products, setProducts] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
// Récupération du catalogue
useEffect(() => {
const fetchCatalog = async () => {
try {
const data = await getProductList();
// FOSSBilling renvoie les produits dans data.list
setProducts(data.list || []);
} catch (err) {
setError("Impossible de contacter le serveur d'approvisionnement.");
} finally {
setIsLoading(false);
}
};
fetchCatalog();
}, []);
// Radar visuel : Associe une icône selon le nom de la catégorie ou du produit
const getCategoryIcon = (text) => {
const t = text.toLowerCase();
if (t.includes('vps') || t.includes('serveur')) return <Server className="w-8 h-8 text-cyan-400" />;
if (t.includes('cloud') || t.includes('nextcloud')) return <Cloud className="w-8 h-8 text-blue-400" />;
if (t.includes('db') || t.includes('base')) return <Database className="w-8 h-8 text-purple-400" />;
return <Globe className="w-8 h-8 text-emerald-400" />;
};
// Extracteur de prix : L'API FOSSBilling a une structure de prix assez profonde
const extractMonthlyPrice = (pricing) => {
if (pricing?.type === 'free') return 'Gratuit';
if (pricing?.type === 'once') return `${pricing.once.price} € (Une fois)`;
// CORRECTION ICI : Utilisation des crochets pour la clé numérique '1'
if (pricing?.type === 'recurrent' && pricing.recurrent['1']) {
return `${pricing.recurrent['1'].price} € / mois`;
}
return 'Prix sur demande';
};
return (
<div className="w-full max-w-7xl p-6 mx-auto">
<header className="mb-12 text-center">
<h1 className="text-4xl font-bold text-white tracking-widest mb-4">
CATALOGUE <span className="text-cyan-400">NEXUS</span>
</h1>
<p className="text-gray-400 max-w-2xl mx-auto">
Déployez de nouvelles instances et étendez votre infrastructure en quelques secondes. Provisionnement automatisé 24/7.
</p>
</header>
{/* GESTION DES ERREURS & CHARGEMENT */}
{isLoading && (
<div className="flex justify-center items-center space-x-3 text-cyan-400 my-20">
<Loader className="w-8 h-8 animate-spin" />
<span className="text-lg tracking-widest">TÉLÉCHARGEMENT DU CATALOGUE...</span>
</div>
)}
{error && (
<div className="flex justify-center my-20">
<div className="flex items-center space-x-3 text-red-400 bg-red-400/10 border border-red-400 p-6 rounded-lg max-w-lg">
<AlertCircle className="w-8 h-8 flex-shrink-0" />
<span>{error}</span>
</div>
</div>
)}
{/* GRILLE DU CATALOGUE */}
{!isLoading && !error && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{products.map((product) => (
<div
key={product.id}
className="bg-gray-900 border border-gray-800 rounded-2xl overflow-hidden hover:border-cyan-400/50 transition-all duration-300 group flex flex-col relative"
>
{/* En-tête de la carte */}
<div className="p-8 border-b border-gray-800 relative overflow-hidden">
<div className="absolute top-0 right-0 p-8 opacity-10 group-hover:scale-110 group-hover:opacity-20 transition-all duration-500">
{getCategoryIcon(product.category || product.title)}
</div>
<h3 className="text-2xl font-bold text-white mb-2 relative z-10">
{product.title}
</h3>
<div className="text-cyan-400 font-mono text-xl relative z-10">
{extractMonthlyPrice(product.pricing)}
</div>
</div>
{/* Description du produit */}
<div className="p-8 flex-grow flex flex-col justify-between">
{/* FOSSBilling permet de mettre du HTML dans la description.
Ici, on triche un peu en simulant des puces pour le design,
mais tu pourras utiliser dangerouslySetInnerHTML si tu as mis du vrai HTML. */}
<div className="text-gray-400 text-sm mb-8 space-y-3">
{product.description ? (
<p>{product.description}</p>
) : (
<>
<div className="flex items-center space-x-2"><Check className="w-4 h-4 text-cyan-400"/> <span>Provisionnement instantané</span></div>
<div className="flex items-center space-x-2"><Check className="w-4 h-4 text-cyan-400"/> <span>Support technique 24/7</span></div>
<div className="flex items-center space-x-2"><Check className="w-4 h-4 text-cyan-400"/> <span>SLA 99.9%</span></div>
</>
)}
</div>
{/* Bouton d'action */}
<button
onClick={() => navigate(`/checkout/${product.id}`)}
className="w-full bg-cyan-400/10 hover:bg-cyan-400 text-cyan-400 hover:text-gray-900 border border-cyan-400 py-3 rounded-lg font-bold tracking-widest transition-all flex justify-center items-center space-x-2 group-hover:shadow-[0_0_20px_rgba(34,211,238,0.2)]"
>
<ShoppingCart className="w-5 h-5" />
<span>COMMANDER</span>
</button>
</div>
</div>
))}
</div>
)}
</div>
);
}
+4
View File
@@ -42,6 +42,10 @@ export const getClientOrders = () =>
export const getOrderService = (order_id) =>
apiCall(`${BASE_URL}/api/client/order/service`, { id: order_id });
// Récupère le catalogue public des produits FOSSBilling
export const getProductList = () =>
apiCall(`${BASE_URL}/api/guest/product/get_list`);
// ==========================================
// ROUTES PERSONNALISÉES (CUSTOM API)
// ==========================================
+2 -1
View File
@@ -1,8 +1,9 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [react()],
plugins: [react(), tailwindcss()],
server: {
proxy: {
// Toutes les requêtes qui commencent par /api iront vers ton FOSSBilling