feat: add profile route and ui
This commit is contained in:
@@ -10,6 +10,7 @@ import { Demos } from './pages/backoffice/Demos'
|
||||
import { Messages } from './pages/backoffice/Contact'
|
||||
import { Codes } from './pages/backoffice/Codes'
|
||||
import { Subscription } from './pages/backoffice/Subscription'
|
||||
import { Profile } from './pages/backoffice/Profile'
|
||||
import { PublicLayout } from './components/PublicLayout'
|
||||
import { BackofficeLayout } from './components/BackofficeLayout'
|
||||
import { Contact } from './pages/Contact'
|
||||
@@ -49,6 +50,7 @@ function BackofficeHome() {
|
||||
return <Navigate to="/app/demos" replace />
|
||||
case 'client':
|
||||
return <Navigate to="/app/subscription" replace />
|
||||
|
||||
default:
|
||||
return <Navigate to="/app/leads" replace />
|
||||
}
|
||||
@@ -113,6 +115,7 @@ export function App() {
|
||||
}
|
||||
/>
|
||||
<Route path="subscription" element={<Subscription />} />
|
||||
<Route path="profile" element={<Profile />} />
|
||||
</Route>
|
||||
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
|
||||
+7
-1
@@ -100,6 +100,10 @@ export interface DemoDetails {
|
||||
state: DemoState
|
||||
}
|
||||
|
||||
export interface UpdateUsername {
|
||||
username: string
|
||||
}
|
||||
|
||||
|
||||
// --- Endpoints ---
|
||||
|
||||
@@ -148,5 +152,7 @@ export const api = {
|
||||
request<{ success: string }>('POST', '/send/message', { username, telegram, sujet, message }),
|
||||
getMessage: () => request<{ messages: Contact[] }>('GET', '/messages'),
|
||||
getDemoDetails: (username: string) =>
|
||||
request<DemoDetails>('POST', '/demos/details', { username }),
|
||||
request<DemoDetails>('POST', '/demos/details', { username }),
|
||||
updateUsername: (username: string) =>
|
||||
request<UpdateUsername>('POST', '/profile/username', { username }),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import {
|
||||
Avatar,
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Container,
|
||||
Divider,
|
||||
Flex,
|
||||
Heading,
|
||||
HStack,
|
||||
SimpleGrid,
|
||||
Spinner,
|
||||
Stack,
|
||||
Stat,
|
||||
StatLabel,
|
||||
StatNumber,
|
||||
Text,
|
||||
useToast,
|
||||
} from '@chakra-ui/react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { api, ApiError } from '../../lib/api'
|
||||
import { useAuth } from '../../lib/auth'
|
||||
|
||||
interface MeResponse {
|
||||
user_id: string
|
||||
username: string
|
||||
role: 'admin' | 'client'
|
||||
type_abonnement: string
|
||||
expired_at: string
|
||||
}
|
||||
|
||||
function roleLabel(role: string) {
|
||||
return role === 'admin' ? 'Administrateur' : 'Client'
|
||||
}
|
||||
|
||||
function roleColor(role: string) {
|
||||
return role === 'admin' ? 'purple' : 'blue'
|
||||
}
|
||||
|
||||
function subscriptionColor(type: string) {
|
||||
const t = type?.toLowerCase() ?? ''
|
||||
if (t.includes('premium') || t.includes('pro')) return 'green'
|
||||
if (t.includes('expired') || t === '') return 'red'
|
||||
return 'gray'
|
||||
}
|
||||
|
||||
function formatDate(iso: string) {
|
||||
if (!iso) return '—'
|
||||
const d = new Date(iso)
|
||||
if (Number.isNaN(d.getTime())) return '—'
|
||||
return d.toLocaleDateString('fr-FR', {
|
||||
day: '2-digit',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})
|
||||
}
|
||||
|
||||
export function Profile() {
|
||||
const toast = useToast()
|
||||
const navigate = useNavigate()
|
||||
const { logout: authLogout } = useAuth()
|
||||
const [me, setMe] = useState<MeResponse | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [loggingOut, setLoggingOut] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
;(async () => {
|
||||
try {
|
||||
const res = await api.me()
|
||||
if (!cancelled) setMe(res)
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError && err.status === 401) {
|
||||
navigate('/login')
|
||||
return
|
||||
}
|
||||
toast({ status: 'error', title: 'Impossible de charger le profil' })
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false)
|
||||
}
|
||||
})()
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [navigate, toast])
|
||||
|
||||
const handleLogout = async () => {
|
||||
setLoggingOut(true)
|
||||
try {
|
||||
await api.logout()
|
||||
authLogout?.()
|
||||
navigate('/login')
|
||||
} catch {
|
||||
toast({ status: 'error', title: 'Déconnexion impossible' })
|
||||
} finally {
|
||||
setLoggingOut(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box bg="chakra-subtle-bg" py={{ base: 10, md: 14 }} minH="100%">
|
||||
<Container maxW="container.md">
|
||||
<Stack spacing={3} mb={8}>
|
||||
<Heading size="lg">Mon profil</Heading>
|
||||
<Text color="gray.400" fontSize="md">
|
||||
Informations de votre compte et de votre abonnement.
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Box
|
||||
bg="bg-surface"
|
||||
borderWidth="1px"
|
||||
borderColor="chakra-border-color"
|
||||
borderRadius="xl"
|
||||
p={{ base: 6, md: 10 }}
|
||||
boxShadow="lg"
|
||||
>
|
||||
{loading ? (
|
||||
<Flex justify="center" py={10}>
|
||||
<Spinner />
|
||||
</Flex>
|
||||
) : !me ? (
|
||||
<Text color="gray.500">Aucune information disponible.</Text>
|
||||
) : (
|
||||
<Stack spacing={8}>
|
||||
<Flex align="center" gap={5} wrap="wrap">
|
||||
<Avatar name={me.username} size="xl" />
|
||||
<Box>
|
||||
<Heading size="md" fontFamily="mono">
|
||||
{me.username}
|
||||
</Heading>
|
||||
<HStack mt={2} spacing={2}>
|
||||
<Badge colorScheme={roleColor(me.role)}>{roleLabel(me.role)}</Badge>
|
||||
{me.type_abonnement && (
|
||||
<Badge colorScheme={subscriptionColor(me.type_abonnement)}>
|
||||
{me.type_abonnement}
|
||||
</Badge>
|
||||
)}
|
||||
</HStack>
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
<Divider borderColor="chakra-border-color" />
|
||||
|
||||
<SimpleGrid columns={{ base: 1, sm: 2 }} spacing={6}>
|
||||
<Stat>
|
||||
<StatLabel>Identifiant</StatLabel>
|
||||
<StatNumber fontSize="md" fontFamily="mono">
|
||||
{me.user_id}
|
||||
</StatNumber>
|
||||
</Stat>
|
||||
<Stat>
|
||||
<StatLabel>Rôle</StatLabel>
|
||||
<StatNumber fontSize="md">{roleLabel(me.role)}</StatNumber>
|
||||
</Stat>
|
||||
<Stat>
|
||||
<StatLabel>Type d'abonnement</StatLabel>
|
||||
<StatNumber fontSize="md">{me.type_abonnement || '—'}</StatNumber>
|
||||
</Stat>
|
||||
<Stat>
|
||||
<StatLabel>Expire le</StatLabel>
|
||||
<StatNumber fontSize="md">{formatDate(me.expired_at)}</StatNumber>
|
||||
</Stat>
|
||||
</SimpleGrid>
|
||||
|
||||
<Divider borderColor="chakra-border-color" />
|
||||
|
||||
<Flex justify="flex-end">
|
||||
<Button
|
||||
colorScheme="red"
|
||||
variant="outline"
|
||||
isLoading={loggingOut}
|
||||
onClick={handleLogout}
|
||||
>
|
||||
Se déconnecter
|
||||
</Button>
|
||||
</Flex>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
</Container>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user