From e2802aeb596d7d518f97bb0a3b0357eebfe7a731 Mon Sep 17 00:00:00 2001 From: Xor290 Date: Mon, 3 Aug 2026 11:23:42 +0200 Subject: [PATCH] chore: update --- web/src/components/DemoCard.tsx | 137 +++++++++++ web/src/pages/Pricing.tsx | 86 ++++--- web/src/pages/backoffice/Demos.tsx | 266 ++++++++++++---------- web/src/pages/backoffice/PremiumDemos.tsx | 190 +++++++++------- web/tsconfig.tsbuildinfo | 2 +- 5 files changed, 440 insertions(+), 241 deletions(-) create mode 100644 web/src/components/DemoCard.tsx diff --git a/web/src/components/DemoCard.tsx b/web/src/components/DemoCard.tsx new file mode 100644 index 0000000..76e1ada --- /dev/null +++ b/web/src/components/DemoCard.tsx @@ -0,0 +1,137 @@ +import type { ReactNode } from 'react' +import { + Badge, + Box, + Collapse, + HStack, + Icon, + IconButton, + Link, + Spinner, + Stack, + Text, +} from '@chakra-ui/react' +import { EditIcon } from '@chakra-ui/icons' +import type { Demo, DemoDetails } from '../lib/api' +import { statusColor, statusLabel } from '../lib/format' +import { ChevronIcon, PodStatusPanel } from './PodStatusPanel' + +interface DemoCardProps { + demo: Demo + isOpen: boolean + onToggle: () => void + detailsLoading: boolean + details: DemoDetails | null + onEditDomain: () => void + /** Affiché sous le badge de statut si fourni (ex. "Expire dans 12 jours"). */ + expiresLabel?: string + /** Boutons d'action (extend/détruire...) affichés en bas de carte. */ + actions?: ReactNode +} + +// DemoCard : équivalent "carte" d'une ligne de tableau démo, pour mobile — +// évite le scroll horizontal du tableau. Tap sur la carte (hors liens/ +// boutons) = déplie l'état live des pods, identique au clic de ligne en +// desktop. +export function DemoCard({ + demo, + isOpen, + onToggle, + detailsLoading, + details, + onEditDomain, + expiresLabel, + actions, +}: DemoCardProps) { + return ( + + + + + + + {demo.namespace} + + + + {statusLabel(demo.status)} + + + + + + Client + {demo.username || '—'} + + + + URL + + + {demo.status === 'ready' ? ( + e.stopPropagation()} + > + {demo.url} + + ) : ( + + )} + } + size="xs" + variant="ghost" + flexShrink={0} + onClick={(e) => { + e.stopPropagation() + onEditDomain() + }} + /> + + + {expiresLabel && ( + + Expire dans + {expiresLabel} + + )} + + + {actions && ( + e.stopPropagation()}> + {actions} + + )} + + + + + {detailsLoading && !details ? ( + + + + ) : details ? ( + + ) : ( + + Aucune donnée. + + )} + + + + ) +} diff --git a/web/src/pages/Pricing.tsx b/web/src/pages/Pricing.tsx index 1bf0dbe..73c1b11 100644 --- a/web/src/pages/Pricing.tsx +++ b/web/src/pages/Pricing.tsx @@ -14,6 +14,8 @@ import { } from '@chakra-ui/react' import { Link as RouterLink } from 'react-router-dom' +const SUPPORT_TELEGRAM_URL = 'https://t.me/OMNEX_CORP' + interface Plan { name: string price: string @@ -21,9 +23,14 @@ interface Plan { description: string features: string[] cta: string + ctaHref: string + ctaExternal?: boolean highlighted?: boolean } +// Seuls 2 types d'abonnement existent réellement dans le système : Démo +// (essai gratuit 30 jours) et Premium (stockage persistant, n'expire plus — +// voir demos.Status/TypeAbo côté backend). const plans: Plan[] = [ { name: 'Démo', @@ -33,41 +40,27 @@ const plans: Plan[] = [ features: [ 'Plateforme complète en conditions réelles', 'Environnement dédié et isolé', - 'Données de démonstration pré-remplies', 'Disponible 30 jours', 'Accompagnement commercial', ], cta: 'Créer un compte', + ctaHref: '/register', }, { - name: 'Pro', + name: 'Premium', price: 'Sur devis', - period: 'par mois', - description: 'Pour déployer la plateforme en production sur votre activité.', + period: '', + description: 'Passez en production : stockage persistant, votre démo n\'expire plus.', features: [ 'Tout ce qui est inclus dans Démo', - 'Déploiement production dédié', - 'WAF, TLS, sauvegardes', - 'GPS, paiements et notifications', + 'Stockage persistant, n\'expire plus', 'Support prioritaire', ], cta: 'Nous contacter', + ctaHref: SUPPORT_TELEGRAM_URL, + ctaExternal: true, highlighted: true, }, - { - name: 'Entreprise', - price: 'Sur mesure', - period: '', - description: 'Multi-sites, SLA et intégrations spécifiques.', - features: [ - 'Tout ce qui est inclus dans Pro', - 'Haute disponibilité multi-régions', - 'SLA et supervision 24/7', - 'Intégrations sur mesure', - 'Accompagnement dédié', - ], - cta: 'Nous contacter', - }, ] export function Pricing() { @@ -76,18 +69,19 @@ export function Pricing() { Tarifs - Commencez par une démo gratuite de 30 jours, puis passez en production quand vous êtes prêt. + Commencez par une démo gratuite de 30 jours, puis passez en premium quand vous êtes prêt. - + {plans.map((plan) => ( ))} - Besoin d'un devis précis ? Créez un compte — un + Besoin d'un devis précis ?{' '} + Contactez-nous — un commercial vous recontacte. @@ -144,22 +138,44 @@ function PlanCard({ plan }: { plan: Plan }) { ))} - + {plan.ctaExternal ? ( + + ) : ( + + )} ) } -function RouterLinkText({ to, children }: { to: string; children: React.ReactNode }) { +function ExternalLinkText({ href, children }: { href: string; children: React.ReactNode }) { return ( - + {children} ) diff --git a/web/src/pages/backoffice/Demos.tsx b/web/src/pages/backoffice/Demos.tsx index be39428..da91230 100644 --- a/web/src/pages/backoffice/Demos.tsx +++ b/web/src/pages/backoffice/Demos.tsx @@ -12,6 +12,7 @@ import { Link, Spacer, Spinner, + Stack, Table, TableContainer, Tbody, @@ -29,6 +30,7 @@ import { statusColor, statusLabel, timeRemaining } from '../../lib/format' import { ConfirmDialog } from '../../components/ConfirmDialog' import { CreateDemoModal } from '../../components/CreateDemoModal' import { EditDomainModal } from '../../components/EditDomainModal' +import { DemoCard } from '../../components/DemoCard' import { ChevronIcon, PodStatusPanel } from '../../components/PodStatusPanel' // Un provisioning en cours => on rafraîchit régulièrement. @@ -144,6 +146,34 @@ export function Demos() { const isAlive = (s: Demo['status']) => s !== 'expired' && s !== 'failed' + const renderActions = (d: Demo) => ( + <> + + + + ) + return ( <> @@ -167,131 +197,127 @@ export function Demos() { ) : demos.length === 0 ? ( Aucune démo active. Lancez-en une avec le bouton ci-dessus. ) : ( - - - - - - - - - - - - - - {demos.map((d) => { - const isOpen = expandedId === d.id - return ( - - toggleRow(d)} - > - - - - + + + + + + + + ) + })} + +
NamespaceClientStatutURLExpire dans
- - - {d.namespace} - - - {d.username ? ( - {d.username} - ) : ( - - )} - - {statusLabel(d.status)} - - - {d.status === 'ready' ? ( - e.stopPropagation()} - > - {d.url} - + <> + {/* Desktop/tablette : tableau. */} + + + + + + + + + + + + + + {demos.map((d) => { + const isOpen = expandedId === d.id + return ( + + toggleRow(d)} + > + + - - - - - + + - - - ) - })} - -
NamespaceClientStatutURLExpire dans
+ + + {d.namespace} + + + {d.username ? ( + {d.username} ) : ( )} - } - size="xs" - variant="ghost" - onClick={(e) => { - e.stopPropagation() - setEditingDomain(d) - }} - /> - - {isAlive(d.status) ? timeRemaining(d.expires_at) : '—'} - - - - -
- - - {detailsLoading && !details ? ( - - - - ) : details ? ( - + + {statusLabel(d.status)} + + + {d.status === 'ready' ? ( + e.stopPropagation()} + > + {d.url} + ) : ( - - Aucune donnée. - + )} - - -
-
+ } + size="xs" + variant="ghost" + onClick={(e) => { + e.stopPropagation() + setEditingDomain(d) + }} + /> +
+
{isAlive(d.status) ? timeRemaining(d.expires_at) : '—'} + {renderActions(d)} +
+ + + {detailsLoading && !details ? ( + + + + ) : details ? ( + + ) : ( + + Aucune donnée. + + )} + + +
+
+ + {/* Mobile : cartes (pas de scroll horizontal). */} + + {demos.map((d) => ( + toggleRow(d)} + detailsLoading={detailsLoading} + details={expandedId === d.id ? details : null} + onEditDomain={() => setEditingDomain(d)} + expiresLabel={isAlive(d.status) ? timeRemaining(d.expires_at) : '—'} + actions={renderActions(d)} + /> + ))} + + )} on rafraîchit régulièrement. const POLL_MS = 5000 @@ -131,93 +133,111 @@ export function PremiumDemos() { ) : demos.length === 0 ? ( Aucune démo premium pour le moment. ) : ( - - - - - - - - - - - - {demos.map((d) => { - const isOpen = expandedId === d.id - return ( - - toggleRow(d)} - > - - - - - - - + + + + + + ) + })} + +
NamespaceClientStatutURL
- - - {d.namespace} - - {d.username || } - {statusLabel(d.status)} - - - {d.status === 'ready' ? ( - e.stopPropagation()} - > - {d.url} - - ) : ( - - )} - } - size="xs" - variant="ghost" - onClick={(e) => { - e.stopPropagation() - setEditingDomain(d) - }} - /> - -
- - - {detailsLoading && !details ? ( - - ) : details ? ( - + <> + {/* Desktop/tablette : tableau. */} + + + + + + + + + + + + {demos.map((d) => { + const isOpen = expandedId === d.id + return ( + + toggleRow(d)} + > + + + + - - - ) - })} - -
NamespaceClientStatutURL
+ + + {d.namespace} + + {d.username || } + {statusLabel(d.status)} + + + {d.status === 'ready' ? ( + e.stopPropagation()} + > + {d.url} + ) : ( - - Aucune donnée. - + )} - - -
-
+ } + size="xs" + variant="ghost" + onClick={(e) => { + e.stopPropagation() + setEditingDomain(d) + }} + /> + +
+ + + {detailsLoading && !details ? ( + + ) : details ? ( + + ) : ( + + Aucune donnée. + + )} + + +
+
+ + {/* Mobile : cartes (pas de scroll horizontal). */} + + {demos.map((d) => ( + toggleRow(d)} + detailsLoading={detailsLoading} + details={expandedId === d.id ? details : null} + onEditDomain={() => setEditingDomain(d)} + /> + ))} + + )}