@@ -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 (
|
||||
<Box borderWidth="1px" borderRadius="lg" overflow="hidden" bg="bg-surface">
|
||||
<Box p={4} cursor="pointer" onClick={onToggle} _active={{ bg: 'chakra-subtle-bg' }}>
|
||||
<HStack justify="space-between" align="start">
|
||||
<HStack spacing={2} minW={0}>
|
||||
<Icon
|
||||
as={ChevronIcon}
|
||||
boxSize={3}
|
||||
color="gray.400"
|
||||
flexShrink={0}
|
||||
transform={isOpen ? 'rotate(90deg)' : undefined}
|
||||
transition="transform 0.15s"
|
||||
/>
|
||||
<Text fontFamily="mono" fontSize="sm" noOfLines={1} wordBreak="break-all">
|
||||
{demo.namespace}
|
||||
</Text>
|
||||
</HStack>
|
||||
<Badge colorScheme={statusColor(demo.status)} flexShrink={0}>
|
||||
{statusLabel(demo.status)}
|
||||
</Badge>
|
||||
</HStack>
|
||||
|
||||
<Stack spacing={1} mt={3} fontSize="sm">
|
||||
<HStack justify="space-between">
|
||||
<Text color="gray.500">Client</Text>
|
||||
<Text>{demo.username || '—'}</Text>
|
||||
</HStack>
|
||||
<HStack justify="space-between" align="start">
|
||||
<Text color="gray.500" flexShrink={0}>
|
||||
URL
|
||||
</Text>
|
||||
<HStack spacing={1} minW={0}>
|
||||
{demo.status === 'ready' ? (
|
||||
<Link
|
||||
href={demo.url}
|
||||
color="primary.500"
|
||||
isExternal
|
||||
noOfLines={1}
|
||||
wordBreak="break-all"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{demo.url}
|
||||
</Link>
|
||||
) : (
|
||||
<Text color="gray.400">—</Text>
|
||||
)}
|
||||
<IconButton
|
||||
aria-label="Modifier le domaine"
|
||||
icon={<EditIcon />}
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
flexShrink={0}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onEditDomain()
|
||||
}}
|
||||
/>
|
||||
</HStack>
|
||||
</HStack>
|
||||
{expiresLabel && (
|
||||
<HStack justify="space-between">
|
||||
<Text color="gray.500">Expire dans</Text>
|
||||
<Text>{expiresLabel}</Text>
|
||||
</HStack>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{actions && (
|
||||
<HStack mt={3} spacing={2} onClick={(e) => e.stopPropagation()}>
|
||||
{actions}
|
||||
</HStack>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Collapse in={isOpen} unmountOnExit animateOpacity>
|
||||
<Box p={4} bg="chakra-subtle-bg" borderTopWidth="1px">
|
||||
{detailsLoading && !details ? (
|
||||
<HStack justify="center" py={2}>
|
||||
<Spinner size="sm" />
|
||||
</HStack>
|
||||
) : details ? (
|
||||
<PodStatusPanel state={details.state} />
|
||||
) : (
|
||||
<Text color="gray.500" fontSize="sm">
|
||||
Aucune donnée.
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Collapse>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
+51
-35
@@ -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() {
|
||||
<Stack spacing={4} textAlign="center" mb={12} align="center">
|
||||
<Heading size="2xl">Tarifs</Heading>
|
||||
<Text fontSize="lg" color="gray.600" maxW="2xl">
|
||||
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.
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<SimpleGrid columns={{ base: 1, md: 3 }} spacing={8} alignItems="stretch">
|
||||
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={8} alignItems="stretch" maxW="2xl" mx="auto">
|
||||
{plans.map((plan) => (
|
||||
<PlanCard key={plan.name} plan={plan} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
|
||||
<Text textAlign="center" color="gray.500" mt={10} fontSize="sm">
|
||||
Besoin d'un devis précis ? <RouterLinkText to="/register">Créez un compte</RouterLinkText> — un
|
||||
Besoin d'un devis précis ?{' '}
|
||||
<ExternalLinkText href={SUPPORT_TELEGRAM_URL}>Contactez-nous</ExternalLinkText> — un
|
||||
commercial vous recontacte.
|
||||
</Text>
|
||||
</Container>
|
||||
@@ -144,22 +138,44 @@ function PlanCard({ plan }: { plan: Plan }) {
|
||||
))}
|
||||
</List>
|
||||
|
||||
<Button
|
||||
as={RouterLink}
|
||||
to="/register"
|
||||
colorScheme="primary"
|
||||
variant={plan.highlighted ? 'solid' : 'outline'}
|
||||
size="lg"
|
||||
>
|
||||
{plan.cta}
|
||||
</Button>
|
||||
{plan.ctaExternal ? (
|
||||
<Button
|
||||
as="a"
|
||||
href={plan.ctaHref}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
colorScheme="primary"
|
||||
variant={plan.highlighted ? 'solid' : 'outline'}
|
||||
size="lg"
|
||||
>
|
||||
{plan.cta}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
as={RouterLink}
|
||||
to={plan.ctaHref}
|
||||
colorScheme="primary"
|
||||
variant={plan.highlighted ? 'solid' : 'outline'}
|
||||
size="lg"
|
||||
>
|
||||
{plan.cta}
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
function RouterLinkText({ to, children }: { to: string; children: React.ReactNode }) {
|
||||
function ExternalLinkText({ href, children }: { href: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<Box as={RouterLink} to={to} color="primary.500" fontWeight="medium" display="inline">
|
||||
<Box
|
||||
as="a"
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
color="primary.500"
|
||||
fontWeight="medium"
|
||||
display="inline"
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
|
||||
+146
-120
@@ -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) => (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
isDisabled={!isAlive(d.status) || busy === d.id}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
extend(d)
|
||||
}}
|
||||
>
|
||||
+30 j
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme="red"
|
||||
variant="outline"
|
||||
isDisabled={!isAlive(d.status)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setToDelete(d)
|
||||
}}
|
||||
>
|
||||
Détruire
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex mb={6} align="center" gap={4} wrap="wrap">
|
||||
@@ -167,131 +197,127 @@ export function Demos() {
|
||||
) : demos.length === 0 ? (
|
||||
<Text color="gray.500">Aucune démo active. Lancez-en une avec le bouton ci-dessus.</Text>
|
||||
) : (
|
||||
<TableContainer borderWidth="1px" borderRadius="lg">
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>Namespace</Th>
|
||||
<Th>Client</Th>
|
||||
<Th>Statut</Th>
|
||||
<Th>URL</Th>
|
||||
<Th>Expire dans</Th>
|
||||
<Th></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{demos.map((d) => {
|
||||
const isOpen = expandedId === d.id
|
||||
return (
|
||||
<Fragment key={d.id}>
|
||||
<Tr
|
||||
cursor="pointer"
|
||||
bg={isOpen ? 'chakra-subtle-bg' : undefined}
|
||||
_hover={{ bg: 'chakra-subtle-bg' }}
|
||||
onClick={() => toggleRow(d)}
|
||||
>
|
||||
<Td fontFamily="mono">
|
||||
<HStack spacing={2}>
|
||||
<Icon
|
||||
as={ChevronIcon}
|
||||
boxSize={3}
|
||||
color="gray.400"
|
||||
transform={isOpen ? 'rotate(90deg)' : undefined}
|
||||
transition="transform 0.15s"
|
||||
/>
|
||||
<Text>{d.namespace}</Text>
|
||||
</HStack>
|
||||
</Td>
|
||||
<Td>
|
||||
{d.username ? (
|
||||
<Text>{d.username}</Text>
|
||||
) : (
|
||||
<Text color="gray.400">—</Text>
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
<Badge colorScheme={statusColor(d.status)}>{statusLabel(d.status)}</Badge>
|
||||
</Td>
|
||||
<Td>
|
||||
<HStack spacing={1}>
|
||||
{d.status === 'ready' ? (
|
||||
<Link
|
||||
href={d.url}
|
||||
color="primary.500"
|
||||
isExternal
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{d.url}
|
||||
</Link>
|
||||
<>
|
||||
{/* Desktop/tablette : tableau. */}
|
||||
<TableContainer borderWidth="1px" borderRadius="lg" display={{ base: 'none', md: 'block' }}>
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>Namespace</Th>
|
||||
<Th>Client</Th>
|
||||
<Th>Statut</Th>
|
||||
<Th>URL</Th>
|
||||
<Th>Expire dans</Th>
|
||||
<Th></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{demos.map((d) => {
|
||||
const isOpen = expandedId === d.id
|
||||
return (
|
||||
<Fragment key={d.id}>
|
||||
<Tr
|
||||
cursor="pointer"
|
||||
bg={isOpen ? 'chakra-subtle-bg' : undefined}
|
||||
_hover={{ bg: 'chakra-subtle-bg' }}
|
||||
onClick={() => toggleRow(d)}
|
||||
>
|
||||
<Td fontFamily="mono">
|
||||
<HStack spacing={2}>
|
||||
<Icon
|
||||
as={ChevronIcon}
|
||||
boxSize={3}
|
||||
color="gray.400"
|
||||
transform={isOpen ? 'rotate(90deg)' : undefined}
|
||||
transition="transform 0.15s"
|
||||
/>
|
||||
<Text>{d.namespace}</Text>
|
||||
</HStack>
|
||||
</Td>
|
||||
<Td>
|
||||
{d.username ? (
|
||||
<Text>{d.username}</Text>
|
||||
) : (
|
||||
<Text color="gray.400">—</Text>
|
||||
)}
|
||||
<IconButton
|
||||
aria-label="Modifier le domaine"
|
||||
icon={<EditIcon />}
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setEditingDomain(d)
|
||||
}}
|
||||
/>
|
||||
</HStack>
|
||||
</Td>
|
||||
<Td>{isAlive(d.status) ? timeRemaining(d.expires_at) : '—'}</Td>
|
||||
<Td textAlign="right">
|
||||
<HStack justify="flex-end">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
isDisabled={!isAlive(d.status) || busy === d.id}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
extend(d)
|
||||
}}
|
||||
>
|
||||
+30 j
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme="red"
|
||||
variant="outline"
|
||||
isDisabled={!isAlive(d.status)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setToDelete(d)
|
||||
}}
|
||||
>
|
||||
Détruire
|
||||
</Button>
|
||||
</HStack>
|
||||
</Td>
|
||||
</Tr>
|
||||
<Tr>
|
||||
<Td p={0} border={isOpen ? undefined : 'none'} colSpan={6}>
|
||||
<Collapse in={isOpen} unmountOnExit animateOpacity>
|
||||
<Box p={4} bg="chakra-subtle-bg" borderTopWidth="1px">
|
||||
{detailsLoading && !details ? (
|
||||
<Flex justify="center" py={4}>
|
||||
<Spinner size="sm" />
|
||||
</Flex>
|
||||
) : details ? (
|
||||
<PodStatusPanel state={details.state} />
|
||||
</Td>
|
||||
<Td>
|
||||
<Badge colorScheme={statusColor(d.status)}>{statusLabel(d.status)}</Badge>
|
||||
</Td>
|
||||
<Td>
|
||||
<HStack spacing={1}>
|
||||
{d.status === 'ready' ? (
|
||||
<Link
|
||||
href={d.url}
|
||||
color="primary.500"
|
||||
isExternal
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{d.url}
|
||||
</Link>
|
||||
) : (
|
||||
<Text color="gray.500" fontSize="sm">
|
||||
Aucune donnée.
|
||||
</Text>
|
||||
<Text color="gray.400">—</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Collapse>
|
||||
</Td>
|
||||
</Tr>
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<IconButton
|
||||
aria-label="Modifier le domaine"
|
||||
icon={<EditIcon />}
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setEditingDomain(d)
|
||||
}}
|
||||
/>
|
||||
</HStack>
|
||||
</Td>
|
||||
<Td>{isAlive(d.status) ? timeRemaining(d.expires_at) : '—'}</Td>
|
||||
<Td textAlign="right">
|
||||
<HStack justify="flex-end">{renderActions(d)}</HStack>
|
||||
</Td>
|
||||
</Tr>
|
||||
<Tr>
|
||||
<Td p={0} border={isOpen ? undefined : 'none'} colSpan={6}>
|
||||
<Collapse in={isOpen} unmountOnExit animateOpacity>
|
||||
<Box p={4} bg="chakra-subtle-bg" borderTopWidth="1px">
|
||||
{detailsLoading && !details ? (
|
||||
<Flex justify="center" py={4}>
|
||||
<Spinner size="sm" />
|
||||
</Flex>
|
||||
) : details ? (
|
||||
<PodStatusPanel state={details.state} />
|
||||
) : (
|
||||
<Text color="gray.500" fontSize="sm">
|
||||
Aucune donnée.
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Collapse>
|
||||
</Td>
|
||||
</Tr>
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
{/* Mobile : cartes (pas de scroll horizontal). */}
|
||||
<Stack spacing={3} display={{ base: 'flex', md: 'none' }}>
|
||||
{demos.map((d) => (
|
||||
<DemoCard
|
||||
key={d.id}
|
||||
demo={d}
|
||||
isOpen={expandedId === d.id}
|
||||
onToggle={() => toggleRow(d)}
|
||||
detailsLoading={detailsLoading}
|
||||
details={expandedId === d.id ? details : null}
|
||||
onEditDomain={() => setEditingDomain(d)}
|
||||
expiresLabel={isAlive(d.status) ? timeRemaining(d.expires_at) : '—'}
|
||||
actions={renderActions(d)}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
|
||||
<ConfirmDialog
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Link,
|
||||
Spacer,
|
||||
Spinner,
|
||||
Stack,
|
||||
Table,
|
||||
TableContainer,
|
||||
Tbody,
|
||||
@@ -29,6 +30,7 @@ import { statusColor, statusLabel } from '../../lib/format'
|
||||
import { ChevronIcon, PodStatusPanel } from '../../components/PodStatusPanel'
|
||||
import { CreateDemoModal } from '../../components/CreateDemoModal'
|
||||
import { EditDomainModal } from '../../components/EditDomainModal'
|
||||
import { DemoCard } from '../../components/DemoCard'
|
||||
|
||||
// Un provisioning en cours => on rafraîchit régulièrement.
|
||||
const POLL_MS = 5000
|
||||
@@ -131,93 +133,111 @@ export function PremiumDemos() {
|
||||
) : demos.length === 0 ? (
|
||||
<Text color="gray.500">Aucune démo premium pour le moment.</Text>
|
||||
) : (
|
||||
<TableContainer borderWidth="1px" borderRadius="lg">
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>Namespace</Th>
|
||||
<Th>Client</Th>
|
||||
<Th>Statut</Th>
|
||||
<Th>URL</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{demos.map((d) => {
|
||||
const isOpen = expandedId === d.id
|
||||
return (
|
||||
<Fragment key={d.id}>
|
||||
<Tr
|
||||
cursor="pointer"
|
||||
bg={isOpen ? 'chakra-subtle-bg' : undefined}
|
||||
_hover={{ bg: 'chakra-subtle-bg' }}
|
||||
onClick={() => toggleRow(d)}
|
||||
>
|
||||
<Td fontFamily="mono">
|
||||
<HStack spacing={2}>
|
||||
<Icon
|
||||
as={ChevronIcon}
|
||||
boxSize={3}
|
||||
color="gray.400"
|
||||
transform={isOpen ? 'rotate(90deg)' : undefined}
|
||||
transition="transform 0.15s"
|
||||
/>
|
||||
<Text>{d.namespace}</Text>
|
||||
</HStack>
|
||||
</Td>
|
||||
<Td>{d.username || <Text color="gray.400">—</Text>}</Td>
|
||||
<Td>
|
||||
<Badge colorScheme={statusColor(d.status)}>{statusLabel(d.status)}</Badge>
|
||||
</Td>
|
||||
<Td>
|
||||
<HStack spacing={1}>
|
||||
{d.status === 'ready' ? (
|
||||
<Link
|
||||
href={d.url}
|
||||
color="primary.500"
|
||||
isExternal
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{d.url}
|
||||
</Link>
|
||||
) : (
|
||||
<Text color="gray.400">—</Text>
|
||||
)}
|
||||
<IconButton
|
||||
aria-label="Modifier le domaine"
|
||||
icon={<EditIcon />}
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setEditingDomain(d)
|
||||
}}
|
||||
/>
|
||||
</HStack>
|
||||
</Td>
|
||||
</Tr>
|
||||
<Tr>
|
||||
<Td p={0} border={isOpen ? undefined : 'none'} colSpan={4}>
|
||||
<Collapse in={isOpen} unmountOnExit animateOpacity>
|
||||
<Box p={4} bg="chakra-subtle-bg" borderTopWidth="1px">
|
||||
{detailsLoading && !details ? (
|
||||
<Spinner size="sm" />
|
||||
) : details ? (
|
||||
<PodStatusPanel state={details.state} />
|
||||
<>
|
||||
{/* Desktop/tablette : tableau. */}
|
||||
<TableContainer borderWidth="1px" borderRadius="lg" display={{ base: 'none', md: 'block' }}>
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>Namespace</Th>
|
||||
<Th>Client</Th>
|
||||
<Th>Statut</Th>
|
||||
<Th>URL</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{demos.map((d) => {
|
||||
const isOpen = expandedId === d.id
|
||||
return (
|
||||
<Fragment key={d.id}>
|
||||
<Tr
|
||||
cursor="pointer"
|
||||
bg={isOpen ? 'chakra-subtle-bg' : undefined}
|
||||
_hover={{ bg: 'chakra-subtle-bg' }}
|
||||
onClick={() => toggleRow(d)}
|
||||
>
|
||||
<Td fontFamily="mono">
|
||||
<HStack spacing={2}>
|
||||
<Icon
|
||||
as={ChevronIcon}
|
||||
boxSize={3}
|
||||
color="gray.400"
|
||||
transform={isOpen ? 'rotate(90deg)' : undefined}
|
||||
transition="transform 0.15s"
|
||||
/>
|
||||
<Text>{d.namespace}</Text>
|
||||
</HStack>
|
||||
</Td>
|
||||
<Td>{d.username || <Text color="gray.400">—</Text>}</Td>
|
||||
<Td>
|
||||
<Badge colorScheme={statusColor(d.status)}>{statusLabel(d.status)}</Badge>
|
||||
</Td>
|
||||
<Td>
|
||||
<HStack spacing={1}>
|
||||
{d.status === 'ready' ? (
|
||||
<Link
|
||||
href={d.url}
|
||||
color="primary.500"
|
||||
isExternal
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{d.url}
|
||||
</Link>
|
||||
) : (
|
||||
<Text color="gray.500" fontSize="sm">
|
||||
Aucune donnée.
|
||||
</Text>
|
||||
<Text color="gray.400">—</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Collapse>
|
||||
</Td>
|
||||
</Tr>
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<IconButton
|
||||
aria-label="Modifier le domaine"
|
||||
icon={<EditIcon />}
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setEditingDomain(d)
|
||||
}}
|
||||
/>
|
||||
</HStack>
|
||||
</Td>
|
||||
</Tr>
|
||||
<Tr>
|
||||
<Td p={0} border={isOpen ? undefined : 'none'} colSpan={4}>
|
||||
<Collapse in={isOpen} unmountOnExit animateOpacity>
|
||||
<Box p={4} bg="chakra-subtle-bg" borderTopWidth="1px">
|
||||
{detailsLoading && !details ? (
|
||||
<Spinner size="sm" />
|
||||
) : details ? (
|
||||
<PodStatusPanel state={details.state} />
|
||||
) : (
|
||||
<Text color="gray.500" fontSize="sm">
|
||||
Aucune donnée.
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Collapse>
|
||||
</Td>
|
||||
</Tr>
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
{/* Mobile : cartes (pas de scroll horizontal). */}
|
||||
<Stack spacing={3} display={{ base: 'flex', md: 'none' }}>
|
||||
{demos.map((d) => (
|
||||
<DemoCard
|
||||
key={d.id}
|
||||
demo={d}
|
||||
isOpen={expandedId === d.id}
|
||||
onToggle={() => toggleRow(d)}
|
||||
detailsLoading={detailsLoading}
|
||||
details={expandedId === d.id ? details : null}
|
||||
onEditDomain={() => setEditingDomain(d)}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
|
||||
<EditDomainModal
|
||||
|
||||
@@ -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/EditDomainModal.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"}
|
||||
{"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/DemoCard.tsx","./src/components/EditDomainModal.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