chore: update
This commit is contained in:
Vendored
+1510
File diff suppressed because one or more lines are too long
Vendored
-423
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Omnex — Plateforme de gestion de commandes & livraison</title>
|
||||
<meta name="description" content="Déployez en un clic une démo complète de la plateforme de gestion de commandes et de livraison." />
|
||||
<script type="module" crossorigin src="/assets/index-DRDLx_J5.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-BZ4jKugg.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
Generated
+70
-10
@@ -12,6 +12,9 @@
|
||||
"@chakra-ui/react": "^2.10.4",
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^7.3.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^7.3.1",
|
||||
"@fortawesome/react-fontawesome": "^3.5.0",
|
||||
"@saas-ui/react": "^2.11.0",
|
||||
"framer-motion": "^11.11.0",
|
||||
"react": "^18.3.1",
|
||||
@@ -89,6 +92,7 @@
|
||||
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.7",
|
||||
"@babel/generator": "^7.29.7",
|
||||
@@ -382,6 +386,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@chakra-ui/react/-/react-2.10.10.tgz",
|
||||
"integrity": "sha512-uahxAfb83/O9fcgBm3ew/nVeQiNgE0uQf8NdQQp3I6u/PCuR2OjSEneV4Ba86W8z50Ppwcxvf9XDk5btYceggQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@chakra-ui/hooks": "2.4.6",
|
||||
"@chakra-ui/styled-system": "2.12.5",
|
||||
@@ -407,6 +412,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@chakra-ui/styled-system/-/styled-system-2.12.5.tgz",
|
||||
"integrity": "sha512-sy39LJWnOvGcOHYBXQxT3bC5zj+R7GVUgxzkkoqx+auV5KVKOOE5aZD+POsciWEt86bqdEa+LpFm+E5pcdbkyg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@chakra-ui/utils": "2.2.6",
|
||||
"csstype": "^3.1.2"
|
||||
@@ -541,6 +547,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -564,6 +571,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -626,6 +634,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
|
||||
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.3",
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
@@ -669,6 +678,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz",
|
||||
"integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.3",
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
@@ -1105,6 +1115,53 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-common-types": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.3.1.tgz",
|
||||
"integrity": "sha512-k0C0sdHmZtAo6dRDtd1Z/qcpyHbL0CKsjV8seMY/21xGhY5Wsv0XRmiI/xEEH4y2c9b1+jvgNs/3EqhV27yUEA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-svg-core": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.3.1.tgz",
|
||||
"integrity": "sha512-BoxVN3PKnMbgStHhjoaky/oWdxHomDqmBVA24IA3KEmssFGeI7u9YT/BJceOjIum/t6TpPa/vcMKaVYQeIQ/3Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "7.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/free-solid-svg-icons": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-7.3.1.tgz",
|
||||
"integrity": "sha512-v0BLa0eqg7ubvVWeNSHVBs8fWH/GJicERZoJaxJ3FE/lj67VSqzoMg9pzfZVOfLMX10y0pGwQAuxoRVVH2patg==",
|
||||
"license": "(CC-BY-4.0 AND MIT)",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "7.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/react-fontawesome": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-3.5.0.tgz",
|
||||
"integrity": "sha512-63mlRr6fiBbJ0wjr1Cf6dsDGtP2lNvk9lnatKgxs/fIkhslsZT291hIUzJkuUkI9yr69ZvWnWfgb2qXm4QyVaA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "~6 || ~7",
|
||||
"react": "^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@hookform/resolvers": {
|
||||
"version": "3.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz",
|
||||
@@ -1739,6 +1796,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@saas-ui/react/-/react-2.11.4.tgz",
|
||||
"integrity": "sha512-rSit+RrsV1xkrli4tne1TjkEjGbkyMefNhP34iNfjbrWMuyu1F/csRx8F6tarYq7NRFmYbF6AnTY6ijgDQ2WBQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@chakra-ui/utils": "^2.2.3",
|
||||
"@saas-ui/core": "2.8.1",
|
||||
@@ -1948,8 +2006,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
@@ -2037,6 +2094,7 @@
|
||||
"integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.2.2"
|
||||
@@ -2048,6 +2106,7 @@
|
||||
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^18.0.0"
|
||||
}
|
||||
@@ -2288,7 +2347,6 @@
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -2299,7 +2357,6 @@
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@@ -2394,6 +2451,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.10.44",
|
||||
"caniuse-lite": "^1.0.30001806",
|
||||
@@ -2661,8 +2719,7 @@
|
||||
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
||||
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
@@ -2884,6 +2941,7 @@
|
||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.2.tgz",
|
||||
"integrity": "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"motion-dom": "^11.18.1",
|
||||
"motion-utils": "^11.18.1",
|
||||
@@ -3186,6 +3244,7 @@
|
||||
"integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cssstyle": "^4.1.0",
|
||||
"data-urls": "^5.0.0",
|
||||
@@ -3299,7 +3358,6 @@
|
||||
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"lz-string": "bin/bin.js"
|
||||
}
|
||||
@@ -3539,7 +3597,6 @@
|
||||
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1",
|
||||
"ansi-styles": "^5.0.0",
|
||||
@@ -3554,8 +3611,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.8.1",
|
||||
@@ -3583,6 +3639,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
@@ -3628,6 +3685,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.2"
|
||||
@@ -3670,6 +3728,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.82.0.tgz",
|
||||
"integrity": "sha512-Zw/uFZ2dO+02GHlBn7JFGn8kZJ7LdM33B/0BXOovzFay+CMhf94JMw5BVu+F1tVkUKjNvBuaE3fz5BJhga10Tg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
@@ -4233,6 +4292,7 @@
|
||||
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.43",
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
"@chakra-ui/react": "^2.10.4",
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^7.3.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^7.3.1",
|
||||
"@fortawesome/react-fontawesome": "^3.5.0",
|
||||
"@saas-ui/react": "^2.11.0",
|
||||
"framer-motion": "^11.11.0",
|
||||
"react": "^18.3.1",
|
||||
|
||||
+11
-2
@@ -1,8 +1,9 @@
|
||||
import { Navigate, Route, Routes } from 'react-router-dom'
|
||||
import { Navigate, Route, Routes, useLocation } from 'react-router-dom'
|
||||
import { Center, Spinner } from '@chakra-ui/react'
|
||||
import { Landing } from './pages/Landing'
|
||||
import { Pricing } from './pages/Pricing'
|
||||
import { Login } from './pages/Login'
|
||||
import { AdminLogin } from './pages/AdminLogin'
|
||||
import { Register } from './pages/Register'
|
||||
import { Demos } from './pages/backoffice/Demos'
|
||||
import { PremiumDemos } from './pages/backoffice/PremiumDemos'
|
||||
@@ -14,8 +15,13 @@ import { BackofficeLayout } from './components/BackofficeLayout'
|
||||
import { useAuth } from './lib/auth'
|
||||
import type { JSX } from 'react'
|
||||
|
||||
// Chemins admin uniquement : un accès non authentifié y renvoie vers la
|
||||
// page de connexion admin plutôt que la page client.
|
||||
const ADMIN_ONLY_PATHS = ['/app/demos', '/app/premium', '/app/codes']
|
||||
|
||||
function RequireAuth({ children }: { children: JSX.Element }) {
|
||||
const { isAuthenticated, initializing } = useAuth()
|
||||
const location = useLocation()
|
||||
if (initializing) {
|
||||
return (
|
||||
<Center h="100vh">
|
||||
@@ -23,7 +29,9 @@ function RequireAuth({ children }: { children: JSX.Element }) {
|
||||
</Center>
|
||||
)
|
||||
}
|
||||
return isAuthenticated ? children : <Navigate to="/login" replace />
|
||||
if (isAuthenticated) return children
|
||||
const isAdminPath = ADMIN_ONLY_PATHS.some((p) => location.pathname.startsWith(p))
|
||||
return <Navigate to={isAdminPath ? '/admin/login' : '/login'} replace />
|
||||
}
|
||||
|
||||
// Réservé à l'admin : provisioning des démos.
|
||||
@@ -52,6 +60,7 @@ export function App() {
|
||||
<Route path="/tarifs" element={<Pricing />} />
|
||||
</Route>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/admin/login" element={<AdminLogin />} />
|
||||
<Route path="/register" element={<Register />} />
|
||||
|
||||
{/* Back-office protégé */}
|
||||
|
||||
@@ -28,14 +28,17 @@ const HamburgerIcon = () => (
|
||||
</Box>
|
||||
)
|
||||
|
||||
const SUPPORT_TELEGRAM_URL = 'https://t.me/OMNEX_CORP'
|
||||
|
||||
export function BackofficeLayout() {
|
||||
const { logout, isAdmin, isClient } = useAuth()
|
||||
const navigate = useNavigate()
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
|
||||
const onLogout = async () => {
|
||||
const loginPath = isAdmin ? '/admin/login' : '/login'
|
||||
await logout()
|
||||
navigate('/login', { replace: true })
|
||||
navigate(loginPath, { replace: true })
|
||||
}
|
||||
|
||||
const roleLabel = isAdmin ? 'Admin' : isClient ? 'Client' : 'Utilisateur'
|
||||
@@ -57,6 +60,9 @@ export function BackofficeLayout() {
|
||||
<Spacer />
|
||||
<HStack spacing={2} display={{ base: 'none', md: 'flex' }}>
|
||||
<Badge colorScheme={roleColor}>{roleLabel}</Badge>
|
||||
<Button as="a" href={SUPPORT_TELEGRAM_URL} target="_blank" rel="noopener noreferrer" size="sm" variant="outline">
|
||||
Support
|
||||
</Button>
|
||||
<ColorModeToggle />
|
||||
<Button size="sm" variant="outline" onClick={onLogout}>
|
||||
Déconnexion
|
||||
@@ -115,6 +121,17 @@ export function BackofficeLayout() {
|
||||
<Divider my={6} />
|
||||
|
||||
<Stack spacing={3}>
|
||||
<Button
|
||||
as="a"
|
||||
href={SUPPORT_TELEGRAM_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="outline"
|
||||
justifyContent="flex-start"
|
||||
onClick={onClose}
|
||||
>
|
||||
Support
|
||||
</Button>
|
||||
<Button variant="outline" justifyContent="flex-start" onClick={onLogout}>
|
||||
Déconnexion
|
||||
</Button>
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
useToast,
|
||||
} from '@chakra-ui/react'
|
||||
import { api, ApiError, type CreateDemoParams, type StorageDriver } from '../lib/api'
|
||||
import { PasswordInput } from './PasswordInput'
|
||||
|
||||
interface CreateDemoModalProps {
|
||||
isOpen: boolean
|
||||
@@ -179,11 +180,10 @@ export function CreateDemoModal({ isOpen, onClose, onCreated }: CreateDemoModalP
|
||||
</FormControl>
|
||||
<FormControl isRequired isDisabled={submitting}>
|
||||
<FormLabel fontSize="sm">Mot de passe</FormLabel>
|
||||
<Input
|
||||
<PasswordInput
|
||||
placeholder="8 caractères min."
|
||||
value={adminPassword}
|
||||
onChange={(e) => setAdminPassword(e.target.value)}
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</FormControl>
|
||||
@@ -212,11 +212,10 @@ export function CreateDemoModal({ isOpen, onClose, onCreated }: CreateDemoModalP
|
||||
</FormControl>
|
||||
<FormControl isDisabled={submitting}>
|
||||
<FormLabel fontSize="sm">Token bot Telegram</FormLabel>
|
||||
<Input
|
||||
<PasswordInput
|
||||
placeholder="123456:ABC-DEF..."
|
||||
value={telegramBotToken}
|
||||
onChange={(e) => setTelegramBotToken(e.target.value)}
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</FormControl>
|
||||
@@ -237,21 +236,19 @@ export function CreateDemoModal({ isOpen, onClose, onCreated }: CreateDemoModalP
|
||||
<Stack spacing={3} pl={3} borderLeftWidth="2px" borderColor="primary.500">
|
||||
<FormControl isDisabled={submitting}>
|
||||
<FormLabel fontSize="sm">Clé API NowPayments</FormLabel>
|
||||
<Input
|
||||
<PasswordInput
|
||||
placeholder="clé API du compte marchand"
|
||||
value={nowPaymentsApiKey}
|
||||
onChange={(e) => setNowPaymentsApiKey(e.target.value)}
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl isDisabled={submitting}>
|
||||
<FormLabel fontSize="sm">Secret IPN NowPayments</FormLabel>
|
||||
<Input
|
||||
<PasswordInput
|
||||
placeholder="secret configuré côté NowPayments"
|
||||
value={nowPaymentsIpnSecret}
|
||||
onChange={(e) => setNowPaymentsIpnSecret(e.target.value)}
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
/>
|
||||
<Text fontSize="xs" color="gray.500" mt={1}>
|
||||
@@ -287,11 +284,10 @@ export function CreateDemoModal({ isOpen, onClose, onCreated }: CreateDemoModalP
|
||||
</FormControl>
|
||||
<FormControl isDisabled={submitting}>
|
||||
<FormLabel fontSize="sm">Bot 1 — token</FormLabel>
|
||||
<Input
|
||||
<PasswordInput
|
||||
placeholder="123456:ABC-DEF..."
|
||||
value={lbBot1Token}
|
||||
onChange={(e) => setLbBot1Token(e.target.value)}
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</FormControl>
|
||||
@@ -307,11 +303,10 @@ export function CreateDemoModal({ isOpen, onClose, onCreated }: CreateDemoModalP
|
||||
</FormControl>
|
||||
<FormControl isDisabled={submitting}>
|
||||
<FormLabel fontSize="sm">Bot 2 — token</FormLabel>
|
||||
<Input
|
||||
<PasswordInput
|
||||
placeholder="123456:ABC-DEF..."
|
||||
value={lbBot2Token}
|
||||
onChange={(e) => setLbBot2Token(e.target.value)}
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -62,7 +62,7 @@ export function Header() {
|
||||
<HStack spacing={2} display={{ base: 'none', md: 'flex' }}>
|
||||
<ColorModeToggle />
|
||||
<Button as={RouterLink} to="/login" variant="ghost" size="sm">
|
||||
Espace commercial
|
||||
Espace client
|
||||
</Button>
|
||||
<Button as={RouterLink} to="/register" colorScheme="primary" size="sm">
|
||||
Créer un compte
|
||||
@@ -109,7 +109,7 @@ export function Header() {
|
||||
justifyContent="flex-start"
|
||||
onClick={onClose}
|
||||
>
|
||||
Espace commercial
|
||||
Espace client
|
||||
</Button>
|
||||
<Button
|
||||
as={RouterLink}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { forwardRef, useState } from 'react'
|
||||
import { IconButton, Input, InputGroup, InputRightElement, type InputProps } from '@chakra-ui/react'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
// Input mot de passe avec bascule affichage clair/masqué (icône Font Awesome).
|
||||
export const PasswordInput = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
|
||||
const [visible, setVisible] = useState(false)
|
||||
|
||||
return (
|
||||
<InputGroup>
|
||||
<Input ref={ref} type={visible ? 'text' : 'password'} {...props} />
|
||||
<InputRightElement>
|
||||
<IconButton
|
||||
aria-label={visible ? 'Masquer le mot de passe' : 'Afficher le mot de passe'}
|
||||
icon={<FontAwesomeIcon icon={visible ? faEyeSlash : faEye} />}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
tabIndex={-1}
|
||||
onClick={() => setVisible((v) => !v)}
|
||||
/>
|
||||
</InputRightElement>
|
||||
</InputGroup>
|
||||
)
|
||||
})
|
||||
PasswordInput.displayName = 'PasswordInput'
|
||||
+11
-1
@@ -133,13 +133,20 @@ export interface TelegramInfo {
|
||||
telegram: string
|
||||
}
|
||||
|
||||
export interface AlertSettings {
|
||||
discord_webhook_url: string
|
||||
telegram_bot_token: string
|
||||
telegram_chat_id: string
|
||||
}
|
||||
|
||||
// --- Endpoints ---
|
||||
|
||||
export const api = {
|
||||
login: (username: string, password: string) =>
|
||||
login: (username: string, password: string, role: Role) =>
|
||||
request<{ token: string; token_type: string; role: Role }>('POST', '/auth/login', {
|
||||
username,
|
||||
password,
|
||||
role,
|
||||
}),
|
||||
register: (username: string, password: string) =>
|
||||
request<{ token: string; token_type: string; role: Role }>('POST', '/auth/register', {
|
||||
@@ -197,4 +204,7 @@ export const api = {
|
||||
getTelegram: () => request<TelegramInfo>('GET', '/profile/telegram'),
|
||||
setTelegram: (telegram: string) =>
|
||||
request<TelegramInfo>('POST', '/profile/telegram', { telegram }),
|
||||
getAlertSettings: () => request<AlertSettings>('GET', '/profile/alerts'),
|
||||
setAlertSettings: (settings: AlertSettings) =>
|
||||
request<AlertSettings>('POST', '/profile/alerts', settings),
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ interface AuthState {
|
||||
role: Role | null
|
||||
typeAbo: string | null
|
||||
initializing: boolean
|
||||
login: (username: string, password: string) => Promise<void>
|
||||
login: (username: string, password: string, role: Role) => Promise<void>
|
||||
register: (username: string, password: string) => Promise<void>
|
||||
logout: () => Promise<void>
|
||||
refreshAbo: () => Promise<void>
|
||||
@@ -62,8 +62,8 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const login = useCallback(async (username: string, password: string) => {
|
||||
const res = await api.login(username, password)
|
||||
const login = useCallback(async (username: string, password: string, role: Role) => {
|
||||
const res = await api.login(username, password, role)
|
||||
setToken(res.token)
|
||||
setTok(res.token)
|
||||
setRole(res.role)
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
CardBody,
|
||||
Container,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Heading,
|
||||
Input,
|
||||
Stack,
|
||||
Text,
|
||||
useToast,
|
||||
} from '@chakra-ui/react'
|
||||
import { Link as RouterLink, useNavigate } from 'react-router-dom'
|
||||
import { useAuth } from '../lib/auth'
|
||||
import { ApiError } from '../lib/api'
|
||||
import { ColorModeToggle } from '../components/ColorModeToggle'
|
||||
import { PasswordInput } from '../components/PasswordInput'
|
||||
|
||||
// Page de connexion admin, séparée de la page client (/login) : identifiants
|
||||
// admin et client ne sont pas interchangeables entre les deux pages (voir
|
||||
// contrôle du rôle côté backend, auth.Handler.Login). Pas de page
|
||||
// d'inscription pour l'admin — les comptes admin sont provisionnés
|
||||
// directement (seed), jamais auto-créés.
|
||||
export function AdminLogin() {
|
||||
const { login } = useAuth()
|
||||
const navigate = useNavigate()
|
||||
const toast = useToast()
|
||||
const [username, setUsername] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const onSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
try {
|
||||
await login(username.trim(), password, 'admin')
|
||||
navigate('/app', { replace: true })
|
||||
} catch (err) {
|
||||
const msg = err instanceof ApiError ? err.message : 'Connexion impossible'
|
||||
toast({ status: 'error', title: 'Échec de connexion', description: msg })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Container maxW="sm" py={20} position="relative">
|
||||
<Box position="absolute" top={4} right={4}>
|
||||
<ColorModeToggle />
|
||||
</Box>
|
||||
<Stack spacing={6}>
|
||||
<Box textAlign="center">
|
||||
<Heading size="lg">Espace admin</Heading>
|
||||
<Text color="gray.500">Connectez-vous pour administrer la plateforme.</Text>
|
||||
</Box>
|
||||
<Card>
|
||||
<CardBody>
|
||||
<form onSubmit={onSubmit}>
|
||||
<Stack spacing={4}>
|
||||
<FormControl isRequired>
|
||||
<FormLabel>Nom d'utilisateur</FormLabel>
|
||||
<Input
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
autoComplete="username"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl isRequired>
|
||||
<FormLabel>Mot de passe</FormLabel>
|
||||
<PasswordInput
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</FormControl>
|
||||
<Button type="submit" colorScheme="primary" isLoading={loading}>
|
||||
Se connecter
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
</CardBody>
|
||||
</Card>
|
||||
<Button as={RouterLink} to="/" variant="link" size="sm">
|
||||
← Retour au site
|
||||
</Button>
|
||||
</Stack>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import { Link as RouterLink, useNavigate } from 'react-router-dom'
|
||||
import { useAuth } from '../lib/auth'
|
||||
import { ApiError } from '../lib/api'
|
||||
import { ColorModeToggle } from '../components/ColorModeToggle'
|
||||
import { PasswordInput } from '../components/PasswordInput'
|
||||
|
||||
export function Login() {
|
||||
const { login } = useAuth()
|
||||
@@ -31,7 +32,7 @@ export function Login() {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
try {
|
||||
await login(username.trim(), password)
|
||||
await login(username.trim(), password, 'client')
|
||||
navigate('/app', { replace: true })
|
||||
} catch (err) {
|
||||
const msg = err instanceof ApiError ? err.message : 'Connexion impossible'
|
||||
@@ -48,8 +49,8 @@ export function Login() {
|
||||
</Box>
|
||||
<Stack spacing={6}>
|
||||
<Box textAlign="center">
|
||||
<Heading size="lg">Espace commercial</Heading>
|
||||
<Text color="gray.500">Connectez-vous pour gérer les démos.</Text>
|
||||
<Heading size="lg">Espace client</Heading>
|
||||
<Text color="gray.500">Connectez-vous pour gérer votre abonnement.</Text>
|
||||
</Box>
|
||||
<Card>
|
||||
<CardBody>
|
||||
@@ -65,8 +66,7 @@ export function Login() {
|
||||
</FormControl>
|
||||
<FormControl isRequired>
|
||||
<FormLabel>Mot de passe</FormLabel>
|
||||
<Input
|
||||
type="password"
|
||||
<PasswordInput
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
autoComplete="current-password"
|
||||
|
||||
@@ -19,6 +19,7 @@ import { Link as RouterLink, useNavigate } from 'react-router-dom'
|
||||
import { useAuth } from '../lib/auth'
|
||||
import { ApiError } from '../lib/api'
|
||||
import { ColorModeToggle } from '../components/ColorModeToggle'
|
||||
import { PasswordInput } from '../components/PasswordInput'
|
||||
|
||||
export function Register() {
|
||||
const { register } = useAuth()
|
||||
@@ -76,8 +77,7 @@ export function Register() {
|
||||
|
||||
<FormControl isRequired isInvalid={password.length > 0 && !passwordValid}>
|
||||
<FormLabel>Mot de passe</FormLabel>
|
||||
<Input
|
||||
type="password"
|
||||
<PasswordInput
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
autoComplete="new-password"
|
||||
@@ -87,8 +87,7 @@ export function Register() {
|
||||
|
||||
<FormControl isRequired isInvalid={confirm.length > 0 && !passwordsMatch}>
|
||||
<FormLabel>Confirmer le mot de passe</FormLabel>
|
||||
<Input
|
||||
type="password"
|
||||
<PasswordInput
|
||||
value={confirm}
|
||||
onChange={(e) => setConfirm(e.target.value)}
|
||||
autoComplete="new-password"
|
||||
|
||||
@@ -43,7 +43,7 @@ export function Codes() {
|
||||
const res = await api.listCodes()
|
||||
setCodes(res.items ?? [])
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError && err.status === 401) navigate('/login')
|
||||
if (err instanceof ApiError && err.status === 401) navigate('/admin/login')
|
||||
else toast({ status: 'error', title: 'Chargement des codes impossible' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
|
||||
@@ -55,7 +55,7 @@ export function Demos() {
|
||||
// les mélanger avec les démos d'essai ici.
|
||||
setDemos((res.items ?? []).filter((d) => d.type_abonnement !== 'premium'))
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError && err.status === 401) navigate('/login')
|
||||
if (err instanceof ApiError && err.status === 401) navigate('/admin/login')
|
||||
else toast({ status: 'error', title: 'Chargement des démos impossible' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
|
||||
@@ -2,11 +2,14 @@ import { Fragment, useCallback, useEffect, useState } from 'react'
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Collapse,
|
||||
Flex,
|
||||
Heading,
|
||||
HStack,
|
||||
Icon,
|
||||
Link,
|
||||
Spacer,
|
||||
Spinner,
|
||||
Table,
|
||||
TableContainer,
|
||||
@@ -22,6 +25,7 @@ import { useNavigate } from 'react-router-dom'
|
||||
import { api, ApiError, type Demo, type DemoDetails } from '../../lib/api'
|
||||
import { statusColor, statusLabel } from '../../lib/format'
|
||||
import { ChevronIcon, PodStatusPanel } from '../../components/PodStatusPanel'
|
||||
import { CreateDemoModal } from '../../components/CreateDemoModal'
|
||||
|
||||
// Un provisioning en cours => on rafraîchit régulièrement.
|
||||
const POLL_MS = 5000
|
||||
@@ -33,6 +37,7 @@ export function PremiumDemos() {
|
||||
const navigate = useNavigate()
|
||||
const [demos, setDemos] = useState<Demo[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [createModalOpen, setCreateModalOpen] = useState(false)
|
||||
|
||||
// --- Ligne dépliée (état live des pods) ---
|
||||
const [expandedId, setExpandedId] = useState<string | null>(null)
|
||||
@@ -44,7 +49,7 @@ export function PremiumDemos() {
|
||||
const res = await api.listDemos()
|
||||
setDemos((res.items ?? []).filter((d) => d.type_abonnement === 'premium'))
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError && err.status === 401) navigate('/login')
|
||||
if (err instanceof ApiError && err.status === 401) navigate('/admin/login')
|
||||
else toast({ status: 'error', title: 'Chargement des démos impossible' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
@@ -98,13 +103,25 @@ export function PremiumDemos() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading size="md" mb={2}>
|
||||
Démos Premium
|
||||
</Heading>
|
||||
<Flex mb={2} align="center" gap={4} wrap="wrap">
|
||||
<Heading size="md" mr={4}>
|
||||
Démos Premium
|
||||
</Heading>
|
||||
<Spacer />
|
||||
<Button colorScheme="primary" onClick={() => setCreateModalOpen(true)}>
|
||||
Déployer une plateforme
|
||||
</Button>
|
||||
</Flex>
|
||||
<Text color="gray.500" mb={6} fontSize="sm">
|
||||
Démos rattachées à un client passé en abonnement payant — stockage persistant, n'expirent plus.
|
||||
</Text>
|
||||
|
||||
<CreateDemoModal
|
||||
isOpen={createModalOpen}
|
||||
onClose={() => setCreateModalOpen(false)}
|
||||
onCreated={() => void load()}
|
||||
/>
|
||||
|
||||
{loading ? (
|
||||
<Spinner />
|
||||
) : demos.length === 0 ? (
|
||||
|
||||
@@ -11,9 +11,13 @@ import {
|
||||
EditableInput,
|
||||
EditablePreview,
|
||||
Flex,
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
FormLabel,
|
||||
Heading,
|
||||
HStack,
|
||||
IconButton,
|
||||
Input,
|
||||
SimpleGrid,
|
||||
Spinner,
|
||||
Stack,
|
||||
@@ -25,6 +29,8 @@ import {
|
||||
useToast,
|
||||
} from '@chakra-ui/react'
|
||||
import { CheckIcon, CloseIcon, EditIcon } from '@chakra-ui/icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { api, ApiError } from '../../lib/api'
|
||||
import { useAuth } from '../../lib/auth'
|
||||
@@ -101,18 +107,30 @@ export function Profile() {
|
||||
const [updatingUsername, setUpdatingUsername] = useState(false)
|
||||
const [updatingPassword, setUpdatingPassword] = useState(false)
|
||||
const [passwordVersion, setPasswordVersion] = useState(0)
|
||||
const [passwordVisible, setPasswordVisible] = useState(false)
|
||||
const [telegram, setTelegramState] = useState('')
|
||||
const [updatingTelegram, setUpdatingTelegram] = useState(false)
|
||||
const [addingTelegram, setAddingTelegram] = useState(false)
|
||||
|
||||
const [discordWebhookUrl, setDiscordWebhookUrl] = useState('')
|
||||
const [telegramBotToken, setTelegramBotToken] = useState('')
|
||||
const [telegramChatId, setTelegramChatId] = useState('')
|
||||
const [savingAlerts, setSavingAlerts] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
;(async () => {
|
||||
try {
|
||||
const [meRes, tgRes] = await Promise.all([api.me(), api.getTelegram()])
|
||||
if (!cancelled) {
|
||||
setMe(meRes)
|
||||
setTelegramState(tgRes.telegram ?? '')
|
||||
if (cancelled) return
|
||||
setMe(meRes)
|
||||
setTelegramState(tgRes.telegram ?? '')
|
||||
if (meRes.role === 'admin') {
|
||||
const alertsRes = await api.getAlertSettings()
|
||||
if (cancelled) return
|
||||
setDiscordWebhookUrl(alertsRes.discord_webhook_url)
|
||||
setTelegramBotToken(alertsRes.telegram_bot_token)
|
||||
setTelegramChatId(alertsRes.telegram_chat_id)
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError && err.status === 401) {
|
||||
@@ -127,6 +145,33 @@ export function Profile() {
|
||||
return () => { cancelled = true }
|
||||
}, [navigate, toast])
|
||||
|
||||
const handleSaveAlertSettings = async () => {
|
||||
setSavingAlerts(true)
|
||||
try {
|
||||
const res = await api.setAlertSettings({
|
||||
discord_webhook_url: discordWebhookUrl.trim(),
|
||||
telegram_bot_token: telegramBotToken.trim(),
|
||||
telegram_chat_id: telegramChatId.trim(),
|
||||
})
|
||||
setDiscordWebhookUrl(res.discord_webhook_url)
|
||||
setTelegramBotToken(res.telegram_bot_token)
|
||||
setTelegramChatId(res.telegram_chat_id)
|
||||
toast({ status: 'success', title: 'Alertes enregistrées' })
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError && err.status === 401) {
|
||||
navigate('/login')
|
||||
return
|
||||
}
|
||||
toast({
|
||||
status: 'error',
|
||||
title: "Impossible d'enregistrer les alertes",
|
||||
description: err instanceof ApiError ? err.message : undefined,
|
||||
})
|
||||
} finally {
|
||||
setSavingAlerts(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdateUsername = async (newUsername: string) => {
|
||||
const trimmed = newUsername.trim()
|
||||
if (!me || !trimmed || trimmed === me.username) return
|
||||
@@ -310,7 +355,19 @@ export function Profile() {
|
||||
>
|
||||
<HStack spacing={2}>
|
||||
<EditablePreview as={StatNumber} fontSize="md" fontFamily="mono" />
|
||||
<EditableInput type="password" fontSize="md" fontFamily="mono" />
|
||||
<EditableInput
|
||||
type={passwordVisible ? 'text' : 'password'}
|
||||
fontSize="md"
|
||||
fontFamily="mono"
|
||||
/>
|
||||
<IconButton
|
||||
aria-label={passwordVisible ? 'Masquer le mot de passe' : 'Afficher le mot de passe'}
|
||||
icon={<FontAwesomeIcon icon={passwordVisible ? faEyeSlash : faEye} />}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
tabIndex={-1}
|
||||
onClick={() => setPasswordVisible((v) => !v)}
|
||||
/>
|
||||
<EditableUsernameControls />
|
||||
</HStack>
|
||||
</Editable>
|
||||
@@ -359,6 +416,73 @@ export function Profile() {
|
||||
</Stat>
|
||||
</SimpleGrid>
|
||||
|
||||
{me.role === 'admin' && (
|
||||
<>
|
||||
<Divider borderColor="chakra-border-color" />
|
||||
|
||||
<Stack spacing={4}>
|
||||
<Box>
|
||||
<Heading size="sm">Alertes monitoring</Heading>
|
||||
<Text color="gray.500" fontSize="sm">
|
||||
Recevez une notification quand un pod d'une démo tombe en erreur (ou se
|
||||
rétablit). Réglages propres à votre compte.
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<FormControl>
|
||||
<FormLabel fontSize="sm">Webhook Discord</FormLabel>
|
||||
<Input
|
||||
fontFamily="mono"
|
||||
fontSize="sm"
|
||||
placeholder="https://discord.com/api/webhooks/..."
|
||||
value={discordWebhookUrl}
|
||||
onChange={(e) => setDiscordWebhookUrl(e.target.value)}
|
||||
isDisabled={savingAlerts}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<SimpleGrid columns={{ base: 1, sm: 2 }} spacing={4}>
|
||||
<FormControl>
|
||||
<FormLabel fontSize="sm">Bot Telegram (token)</FormLabel>
|
||||
<Input
|
||||
fontFamily="mono"
|
||||
fontSize="sm"
|
||||
placeholder="123456789:AAExemple..."
|
||||
value={telegramBotToken}
|
||||
onChange={(e) => setTelegramBotToken(e.target.value)}
|
||||
isDisabled={savingAlerts}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel fontSize="sm">Telegram (chat ID)</FormLabel>
|
||||
<Input
|
||||
fontFamily="mono"
|
||||
fontSize="sm"
|
||||
placeholder="-100123456789"
|
||||
value={telegramChatId}
|
||||
onChange={(e) => setTelegramChatId(e.target.value)}
|
||||
isDisabled={savingAlerts}
|
||||
/>
|
||||
<FormHelperText>
|
||||
Envoyez un message au bot puis récupérez le chat_id via son API.
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
</SimpleGrid>
|
||||
|
||||
<Flex justify="flex-end">
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme="primary"
|
||||
isLoading={savingAlerts}
|
||||
onClick={handleSaveAlertSettings}
|
||||
>
|
||||
Enregistrer les alertes
|
||||
</Button>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Divider borderColor="chakra-border-color" />
|
||||
|
||||
<Flex justify="flex-end">
|
||||
|
||||
@@ -93,7 +93,7 @@ export function Subscription() {
|
||||
return (
|
||||
<>
|
||||
<Flex mb={6} align="center">
|
||||
<Heading size="md">Ma démo</Heading>
|
||||
<Heading size="md">{isPremium ? 'Ma plateforme' : 'Ma démo'}</Heading>
|
||||
<Spacer />
|
||||
</Flex>
|
||||
|
||||
@@ -101,7 +101,9 @@ export function Subscription() {
|
||||
{demosLoading ? (
|
||||
<Spinner size="sm" />
|
||||
) : demos.length === 0 ? (
|
||||
<Text color="gray.500">Aucune démo pour le moment.</Text>
|
||||
<Text color="gray.500">
|
||||
{isPremium ? 'Aucune plateforme pour le moment.' : 'Aucune démo pour le moment.'}
|
||||
</Text>
|
||||
) : (
|
||||
<VStack align="stretch" spacing={3}>
|
||||
{demos.map((d) => (
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"root":["./src/App.tsx","./src/main.tsx","./src/setupTests.ts","./src/theme.ts","./src/vite-env.d.ts","./src/components/BackofficeLayout.tsx","./src/components/ColorModeToggle.tsx","./src/components/ConfirmDialog.test.tsx","./src/components/ConfirmDialog.tsx","./src/components/CreateDemoModal.tsx","./src/components/Footer.tsx","./src/components/Header.tsx","./src/components/PodStatusPanel.tsx","./src/components/PublicLayout.tsx","./src/lib/api.ts","./src/lib/auth.tsx","./src/lib/format.test.ts","./src/lib/format.ts","./src/pages/Landing.tsx","./src/pages/Login.tsx","./src/pages/Pricing.tsx","./src/pages/Register.tsx","./src/pages/backoffice/Codes.tsx","./src/pages/backoffice/Demos.tsx","./src/pages/backoffice/PremiumDemos.tsx","./src/pages/backoffice/Profile.tsx","./src/pages/backoffice/Subscription.tsx"],"version":"5.9.3"}
|
||||
{"root":["./src/App.tsx","./src/main.tsx","./src/setupTests.ts","./src/theme.ts","./src/vite-env.d.ts","./src/components/BackofficeLayout.tsx","./src/components/ColorModeToggle.tsx","./src/components/ConfirmDialog.test.tsx","./src/components/ConfirmDialog.tsx","./src/components/CreateDemoModal.tsx","./src/components/Footer.tsx","./src/components/Header.tsx","./src/components/PasswordInput.tsx","./src/components/PodStatusPanel.tsx","./src/components/PublicLayout.tsx","./src/lib/api.ts","./src/lib/auth.tsx","./src/lib/format.test.ts","./src/lib/format.ts","./src/pages/AdminLogin.tsx","./src/pages/Landing.tsx","./src/pages/Login.tsx","./src/pages/Pricing.tsx","./src/pages/Register.tsx","./src/pages/backoffice/Codes.tsx","./src/pages/backoffice/Demos.tsx","./src/pages/backoffice/PremiumDemos.tsx","./src/pages/backoffice/Profile.tsx","./src/pages/backoffice/Subscription.tsx"],"version":"5.9.3"}
|
||||
Reference in New Issue
Block a user