+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
|
||||
|
||||
Reference in New Issue
Block a user