mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
fix: remove unused routes and errors
This commit is contained in:
13
apps/portal/src/app/components/ProtectedRoute.tsx
Normal file
13
apps/portal/src/app/components/ProtectedRoute.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Navigate, useLocation } from 'react-router-dom';
|
||||
import { useAppSelector } from '../store/hooks';
|
||||
|
||||
export function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
||||
const isAuthenticated = useAppSelector((s) => s.auth.isAuthenticated);
|
||||
const location = useLocation();
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to="/login" state={{ from: location }} replace />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
import { Box, Button, Group, Stack, Text, Title } from '@mantine/core';
|
||||
import { IconFilePlus, IconCategory, IconAnchor } from '@tabler/icons-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export function DashboardHero({ name }: { name?: string }) {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Box
|
||||
p={{ base: 'lg', sm: 'xl' }}
|
||||
style={{
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
borderRadius: 'var(--mantine-radius-lg)',
|
||||
background: 'linear-gradient(135deg, #2453a2 0%, #3160b7 45%, #1fc29d 130%)',
|
||||
color: 'white',
|
||||
boxShadow: 'var(--mantine-shadow-md)',
|
||||
}}
|
||||
>
|
||||
{/* decorative wash */}
|
||||
<IconAnchor
|
||||
size={220}
|
||||
stroke={1}
|
||||
aria-hidden
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: -30,
|
||||
bottom: -50,
|
||||
opacity: 0.12,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
aria-hidden
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: -60,
|
||||
right: 120,
|
||||
width: 200,
|
||||
height: 200,
|
||||
borderRadius: '50%',
|
||||
background: 'rgba(255,255,255,0.08)',
|
||||
}}
|
||||
/>
|
||||
|
||||
<Stack gap="md" style={{ position: 'relative', maxWidth: 620 }}>
|
||||
<Stack gap={4}>
|
||||
<Text fz="sm" fw={600} style={{ opacity: 0.85 }}>
|
||||
{t('dashboard.hero.greeting')}
|
||||
{name ? ',' : ''}
|
||||
</Text>
|
||||
{name && (
|
||||
<Title order={2} c="white">
|
||||
{name}
|
||||
</Title>
|
||||
)}
|
||||
<Text fz={{ base: 'sm', sm: 'md' }} style={{ opacity: 0.92 }} maw={520}>
|
||||
{t('dashboard.hero.subtitle')}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Group gap="sm">
|
||||
<Button
|
||||
variant="white"
|
||||
c="emaPrimary.7"
|
||||
leftSection={<IconFilePlus size={18} />}
|
||||
onClick={() => navigate('/apply')}
|
||||
>
|
||||
{t('dashboard.newApplication')}
|
||||
</Button>
|
||||
<Button
|
||||
leftSection={<IconCategory size={18} />}
|
||||
onClick={() => navigate('/services')}
|
||||
styles={{
|
||||
root: {
|
||||
background: 'rgba(255,255,255,0.16)',
|
||||
color: 'white',
|
||||
border: '1px solid rgba(255,255,255,0.35)',
|
||||
backdropFilter: 'blur(4px)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{t('dashboard.hero.browse')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -18,6 +18,8 @@ export const am: Translations = {
|
||||
seafarerRegistry: 'የመርከበኞች ምዝገባ',
|
||||
profile: 'መገለጫ',
|
||||
support: 'እገዛና ድጋፍ',
|
||||
collapseSidebar: 'ሰብስብ',
|
||||
expandSidebar: 'ዘርጋ',
|
||||
},
|
||||
|
||||
common: {
|
||||
|
||||
@@ -16,6 +16,8 @@ export const en = {
|
||||
seafarerRegistry: 'Seafarer Registry',
|
||||
profile: 'Profile',
|
||||
support: 'Help & Support',
|
||||
collapseSidebar: 'Collapse',
|
||||
expandSidebar: 'Expand sidebar',
|
||||
},
|
||||
|
||||
common: {
|
||||
|
||||
@@ -27,7 +27,10 @@ import {
|
||||
} from '@tabler/icons-react';
|
||||
import type { Icon } from '@tabler/icons-react';
|
||||
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { notify } from '@ema-platform/ui';
|
||||
import { logout } from '@ema-platform/auth';
|
||||
import { LanguageSwitcher } from '../components/LanguageSwitcher';
|
||||
import { ColorSchemeToggle } from '../components/ColorSchemeToggle';
|
||||
import { BrandMark } from '@ema-platform/auth';
|
||||
@@ -40,47 +43,44 @@ interface NavItem {
|
||||
soon?: boolean;
|
||||
}
|
||||
|
||||
const NAV_ITEMS: NavItem[] = [
|
||||
{ to: '/dashboard', label: 'Dashboard', icon: IconLayoutDashboard },
|
||||
{ to: '/seafarer-registry', label: 'Seafarer Registry', icon: IconList },
|
||||
{ to: '/profile', label: 'Profile', icon: IconUser },
|
||||
{ to: '/support', label: 'Support', icon: IconLifebuoy },
|
||||
const NAV_ITEMS: (NavItem & { i18nKey: string })[] = [
|
||||
{ to: '/dashboard', label: 'Dashboard', i18nKey: 'nav.dashboard', icon: IconLayoutDashboard },
|
||||
{ to: '/seafarer-registry', label: 'Seafarer Registry', i18nKey: 'nav.seafarerRegistry', icon: IconList },
|
||||
{ to: '/profile', label: 'Profile', i18nKey: 'nav.profile', icon: IconUser },
|
||||
{ to: '/support', label: 'Support', i18nKey: 'nav.support', icon: IconLifebuoy },
|
||||
];
|
||||
|
||||
const PAGE_META: Record<string, { title: string; subtitle: string }> = {
|
||||
const PAGE_META: Record<string, { i18nKey: string; subtitleKey: string }> = {
|
||||
'/dashboard': {
|
||||
title: 'Dashboard',
|
||||
subtitle: new Date().toLocaleDateString('en-GB', {
|
||||
weekday: 'long',
|
||||
day: '2-digit',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
}),
|
||||
i18nKey: 'nav.dashboard',
|
||||
subtitleKey: 'dashboard.title',
|
||||
},
|
||||
'/seafarer-registry': {
|
||||
title: 'Seafarer Registry',
|
||||
subtitle: 'Manage all registered seafarers',
|
||||
i18nKey: 'nav.seafarerRegistry',
|
||||
subtitleKey: '',
|
||||
},
|
||||
'/profile': {
|
||||
title: 'Profile',
|
||||
subtitle: 'Manage your account and preferences',
|
||||
i18nKey: 'nav.profile',
|
||||
subtitleKey: '',
|
||||
},
|
||||
};
|
||||
|
||||
export function PortalLayout() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const dispatch = useDispatch();
|
||||
const [navOpened, { toggle: toggleNav, close: closeNav }] = useDisclosure();
|
||||
const [sidebarCollapsed, { toggle: toggleSidebar }] = useDisclosure(false);
|
||||
|
||||
// Breadcrumb trail: always rooted at "Home", then any matched page segments.
|
||||
const segments = location.pathname.split('/').filter(Boolean);
|
||||
const crumbs = [
|
||||
{ label: 'Home', path: '/dashboard' },
|
||||
{ label: t('nav.dashboard'), path: '/dashboard' },
|
||||
...segments
|
||||
.map((_, i) => '/' + segments.slice(0, i + 1).join('/'))
|
||||
.filter((path) => PAGE_META[path] && path !== '/dashboard')
|
||||
.map((path) => ({ label: PAGE_META[path].title, path })),
|
||||
.map((path) => ({ label: t(PAGE_META[path].i18nKey), path })),
|
||||
];
|
||||
|
||||
const go = (item: NavItem) => {
|
||||
@@ -95,6 +95,7 @@ export function PortalLayout() {
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
dispatch(logout());
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
@@ -381,7 +382,7 @@ export function PortalLayout() {
|
||||
color: 'var(--mantine-color-primary-7)',
|
||||
}}
|
||||
>
|
||||
EMA Portal
|
||||
{t('app.name')}
|
||||
</Text>
|
||||
<Text
|
||||
size="xs"
|
||||
@@ -393,7 +394,7 @@ export function PortalLayout() {
|
||||
lineHeight: 1.3,
|
||||
}}
|
||||
>
|
||||
Ethiopian Maritime Authority
|
||||
{t('app.authority')}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
@@ -407,8 +408,9 @@ export function PortalLayout() {
|
||||
const active = activeNavItem(item);
|
||||
|
||||
if (sidebarCollapsed) {
|
||||
const label = t(item.i18nKey);
|
||||
return (
|
||||
<Tooltip key={item.label} label={item.label} position="right" withArrow>
|
||||
<Tooltip key={item.label} label={label} position="right" withArrow>
|
||||
<UnstyledButton
|
||||
onClick={() => go(item)}
|
||||
style={{
|
||||
@@ -428,11 +430,12 @@ export function PortalLayout() {
|
||||
);
|
||||
}
|
||||
|
||||
const label = t(item.i18nKey);
|
||||
return (
|
||||
<NavLink
|
||||
key={item.label}
|
||||
active={active}
|
||||
label={item.label}
|
||||
label={label}
|
||||
leftSection={<ItemIcon size={19} stroke={1.6} />}
|
||||
onClick={() => go(item)}
|
||||
variant="light"
|
||||
@@ -452,7 +455,7 @@ export function PortalLayout() {
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
label={sidebarCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
||||
label={sidebarCollapsed ? t('nav.expandSidebar', 'Expand sidebar') : t('nav.collapseSidebar', 'Collapse sidebar')}
|
||||
position="right"
|
||||
withArrow
|
||||
disabled={!sidebarCollapsed}
|
||||
@@ -490,7 +493,7 @@ export function PortalLayout() {
|
||||
) : (
|
||||
<>
|
||||
<IconChevronLeft size={18} stroke={1.6} />
|
||||
<span>Collapse</span>
|
||||
<span>{t('nav.collapseSidebar', 'Collapse')}</span>
|
||||
</>
|
||||
)}
|
||||
</UnstyledButton>
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { ReactNode } from 'react';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { i18n } from './i18n/config';
|
||||
import { PortalLayout } from './layouts/PortalLayout';
|
||||
import { ProtectedRoute } from './components/ProtectedRoute';
|
||||
|
||||
// Auth (standalone pages, no portal chrome)
|
||||
import { LoginPage, SignupPage, OTPVerificationPage, ForgotPasswordPage } from '@ema-platform/auth';
|
||||
@@ -28,19 +29,28 @@ function IsolatedIam({ children }: { children: ReactNode }) {
|
||||
}
|
||||
|
||||
export const router = createBrowserRouter([
|
||||
// Standalone auth pages
|
||||
// Public auth pages
|
||||
{ path: '/login', element: <LoginPage /> },
|
||||
{ path: '/signup', element: <SignupPage /> },
|
||||
{ path: '/otp-verify', element: <OTPVerificationPage /> },
|
||||
{ path: '/forgot-password', element: <ForgotPasswordPage /> },
|
||||
|
||||
// Portal — wrapped in the portal's own i18n instance so it is isolated from
|
||||
// the global i18next singleton that `@tria-plc/iamui-common` initializes.
|
||||
// Protected auth pages
|
||||
{
|
||||
element: <ProtectedRoute><OTPVerificationPage /></ProtectedRoute>,
|
||||
path: '/otp-verify',
|
||||
},
|
||||
{
|
||||
element: <ProtectedRoute><ForgotPasswordPage /></ProtectedRoute>,
|
||||
path: '/forgot-password',
|
||||
},
|
||||
|
||||
// Portal — protected
|
||||
{
|
||||
element: (
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<PortalLayout />
|
||||
</I18nextProvider>
|
||||
<ProtectedRoute>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<PortalLayout />
|
||||
</I18nextProvider>
|
||||
</ProtectedRoute>
|
||||
),
|
||||
children: [
|
||||
{ path: '/', element: <Navigate to="/dashboard" replace /> },
|
||||
@@ -53,12 +63,14 @@ export const router = createBrowserRouter([
|
||||
],
|
||||
},
|
||||
|
||||
// IAM admin user management (isolated providers)
|
||||
// IAM admin user management (isolated providers) — protected
|
||||
{
|
||||
element: (
|
||||
<IsolatedIam>
|
||||
<UserManagementLayout />
|
||||
</IsolatedIam>
|
||||
<ProtectedRoute>
|
||||
<IsolatedIam>
|
||||
<UserManagementLayout />
|
||||
</IsolatedIam>
|
||||
</ProtectedRoute>
|
||||
),
|
||||
children: [{ path: '/users', element: <UserManagementPage /> }],
|
||||
},
|
||||
|
||||
@@ -7,11 +7,14 @@ import {
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
UnstyledButton,
|
||||
rem,
|
||||
useComputedColorScheme,
|
||||
useMantineColorScheme,
|
||||
useMantineTheme,
|
||||
type BoxProps,
|
||||
} from '@mantine/core';
|
||||
import { IconCheck } from '@tabler/icons-react';
|
||||
import { IconCheck, IconMoon, IconSun } from '@tabler/icons-react';
|
||||
import { useAuthConfig } from '../AuthConfig';
|
||||
|
||||
export function BrandMark({ size = 44, ...boxProps }: BoxProps & { size?: number }) {
|
||||
@@ -29,6 +32,34 @@ export function BrandMark({ size = 44, ...boxProps }: BoxProps & { size?: number
|
||||
);
|
||||
}
|
||||
|
||||
function ThemeToggle() {
|
||||
const { setColorScheme } = useMantineColorScheme();
|
||||
const computed = useComputedColorScheme('light');
|
||||
const isDark = computed === 'dark';
|
||||
|
||||
return (
|
||||
<UnstyledButton
|
||||
onClick={() => setColorScheme(isDark ? 'light' : 'dark')}
|
||||
aria-label="Toggle theme"
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: rem(36),
|
||||
height: rem(36),
|
||||
borderRadius: rem(10),
|
||||
border: '1px solid var(--mantine-color-default-border)',
|
||||
background: 'var(--mantine-color-body)',
|
||||
color: 'var(--mantine-color-dimmed)',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 150ms ease',
|
||||
}}
|
||||
>
|
||||
{isDark ? <IconSun size={18} /> : <IconMoon size={18} />}
|
||||
</UnstyledButton>
|
||||
);
|
||||
}
|
||||
|
||||
const FEATURES = [
|
||||
'Submit applications online, 24/7',
|
||||
'Real-time status tracking & alerts',
|
||||
@@ -49,114 +80,137 @@ export function AuthShell({
|
||||
const theme = useMantineTheme();
|
||||
const { logoUrl } = useAuthConfig();
|
||||
const heroGradient = theme.other.heroGradient as string;
|
||||
const computed = useComputedColorScheme('light');
|
||||
const isDark = computed === 'dark';
|
||||
|
||||
return (
|
||||
<Flex
|
||||
<Box
|
||||
mih="100vh"
|
||||
align="center"
|
||||
justify="center"
|
||||
style={{ background: '#f7f8fa' }}
|
||||
style={{
|
||||
background: 'var(--mantine-color-body)',
|
||||
position: 'relative',
|
||||
}}
|
||||
p="md"
|
||||
>
|
||||
{/* Theme toggle — top-right corner */}
|
||||
<Box
|
||||
mx="auto"
|
||||
maw={1280}
|
||||
w="100%"
|
||||
bg="white"
|
||||
p={12}
|
||||
style={{
|
||||
borderRadius: rem(16),
|
||||
boxShadow: '0 20px 60px rgba(0,0,0,0.15)',
|
||||
overflow: 'hidden',
|
||||
position: 'absolute',
|
||||
top: rem(16),
|
||||
right: rem(16),
|
||||
zIndex: 10,
|
||||
}}
|
||||
>
|
||||
<Flex direction={{ base: 'column', lg: 'row' }}>
|
||||
<Box
|
||||
w={{ base: '100%', lg: '50%' }}
|
||||
px={48}
|
||||
py={48}
|
||||
style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center' }}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
<ThemeToggle />
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
visibleFrom="lg"
|
||||
w="50%"
|
||||
p="lg"
|
||||
pos="relative"
|
||||
style={{ background: heroGradient, overflow: 'hidden' }}
|
||||
>
|
||||
<Flex
|
||||
mih="100vh"
|
||||
align="center"
|
||||
justify="center"
|
||||
>
|
||||
<Box
|
||||
mx="auto"
|
||||
maw={1280}
|
||||
w="100%"
|
||||
p={12}
|
||||
style={{
|
||||
borderRadius: rem(16),
|
||||
background: 'var(--mantine-color-body)',
|
||||
boxShadow: isDark
|
||||
? '0 20px 60px rgba(0,0,0,0.5)'
|
||||
: '0 20px 60px rgba(0,0,0,0.15)',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Flex direction={{ base: 'column', lg: 'row' }}>
|
||||
<Box
|
||||
pos="absolute"
|
||||
top={-80}
|
||||
right={-40}
|
||||
w={240}
|
||||
h={240}
|
||||
style={{ borderRadius: '50%', background: 'rgba(255,255,255,0.10)' }}
|
||||
/>
|
||||
<Box
|
||||
pos="absolute"
|
||||
bottom={-80}
|
||||
left={-60}
|
||||
w={220}
|
||||
h={220}
|
||||
style={{ borderRadius: '50%', background: 'rgba(255,255,255,0.08)' }}
|
||||
/>
|
||||
|
||||
<Stack
|
||||
gap="lg"
|
||||
pos="relative"
|
||||
style={{ zIndex: 1 }}
|
||||
h="100%"
|
||||
justify="center"
|
||||
w={{ base: '100%', lg: '50%' }}
|
||||
px={48}
|
||||
py={48}
|
||||
style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center' }}
|
||||
>
|
||||
<Center>
|
||||
<Box
|
||||
component="img"
|
||||
src={logoUrl}
|
||||
alt="EMA Portal"
|
||||
style={{ display: 'block', maxWidth: '60%', height: 'auto' }}
|
||||
/>
|
||||
</Center>
|
||||
{children}
|
||||
</Box>
|
||||
|
||||
<Stack gap={6}>
|
||||
<Title order={2} c="white" fz={rem(28)} lh={1.2} fw={700}>
|
||||
{brandTitle}
|
||||
</Title>
|
||||
<Text fz="sm" lh={1.6} style={{ color: 'rgba(255,255,255,0.85)' }}>
|
||||
{brandSubtitle}
|
||||
<Box
|
||||
visibleFrom="lg"
|
||||
w="50%"
|
||||
p="lg"
|
||||
pos="relative"
|
||||
style={{ background: heroGradient, overflow: 'hidden' }}
|
||||
>
|
||||
<Box
|
||||
pos="absolute"
|
||||
top={-80}
|
||||
right={-40}
|
||||
w={240}
|
||||
h={240}
|
||||
style={{ borderRadius: '50%', background: 'rgba(255,255,255,0.10)' }}
|
||||
/>
|
||||
<Box
|
||||
pos="absolute"
|
||||
bottom={-80}
|
||||
left={-60}
|
||||
w={220}
|
||||
h={220}
|
||||
style={{ borderRadius: '50%', background: 'rgba(255,255,255,0.08)' }}
|
||||
/>
|
||||
|
||||
<Stack
|
||||
gap="lg"
|
||||
pos="relative"
|
||||
style={{ zIndex: 1 }}
|
||||
h="100%"
|
||||
justify="center"
|
||||
>
|
||||
<Center>
|
||||
<Box
|
||||
component="img"
|
||||
src={logoUrl}
|
||||
alt="EMA Portal"
|
||||
style={{ display: 'block', maxWidth: '60%', height: 'auto' }}
|
||||
/>
|
||||
</Center>
|
||||
|
||||
<Stack gap={6}>
|
||||
<Title order={2} c="white" fz={rem(28)} lh={1.2} fw={700}>
|
||||
{brandTitle}
|
||||
</Title>
|
||||
<Text fz="sm" lh={1.6} style={{ color: 'rgba(255,255,255,0.85)' }}>
|
||||
{brandSubtitle}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Stack gap="sm">
|
||||
{FEATURES.map((feature) => (
|
||||
<Group key={feature} gap="sm" wrap="nowrap">
|
||||
<Center
|
||||
w={22}
|
||||
h={22}
|
||||
style={{
|
||||
borderRadius: '50%',
|
||||
background: 'rgba(255,255,255,0.2)',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<IconCheck size={12} color="white" stroke={2.4} />
|
||||
</Center>
|
||||
<Text c="white" fz="sm" fw={500}>
|
||||
{feature}
|
||||
</Text>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
<Text fz="xs" style={{ color: 'rgba(255,255,255,0.7)' }}>
|
||||
© 2026 Ethiopian Maritime Authority
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Stack gap="sm">
|
||||
{FEATURES.map((feature) => (
|
||||
<Group key={feature} gap="sm" wrap="nowrap">
|
||||
<Center
|
||||
w={22}
|
||||
h={22}
|
||||
style={{
|
||||
borderRadius: '50%',
|
||||
background: 'rgba(255,255,255,0.2)',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<IconCheck size={12} color="white" stroke={2.4} />
|
||||
</Center>
|
||||
<Text c="white" fz="sm" fw={500}>
|
||||
{feature}
|
||||
</Text>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
<Text fz="xs" style={{ color: 'rgba(255,255,255,0.7)' }}>
|
||||
© 2026 Ethiopian Maritime Authority
|
||||
</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user