mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 13:02:50 +00:00
UI improvements
This commit is contained in:
@@ -4,14 +4,20 @@ import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Accordion,
|
||||
Anchor,
|
||||
Badge,
|
||||
Box,
|
||||
Burger,
|
||||
Button,
|
||||
Container,
|
||||
Divider,
|
||||
Drawer,
|
||||
Group,
|
||||
Paper,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Tabs,
|
||||
Text,
|
||||
TextInput,
|
||||
ThemeIcon,
|
||||
Title,
|
||||
useMantineTheme,
|
||||
@@ -22,13 +28,16 @@ import {
|
||||
IconAward,
|
||||
IconBriefcase,
|
||||
IconCertificate,
|
||||
IconCheck,
|
||||
IconClipboardCheck,
|
||||
IconClock,
|
||||
IconGavel,
|
||||
IconLanguage,
|
||||
IconMail,
|
||||
IconMapPin,
|
||||
IconPhone,
|
||||
IconSchool,
|
||||
IconSearch,
|
||||
IconShip,
|
||||
IconShieldCheck,
|
||||
IconUserCheck,
|
||||
@@ -73,8 +82,6 @@ function useActiveSection(ids: readonly string[]) {
|
||||
const topMost = visible.reduce((a, b) => (a.boundingClientRect.top <= b.boundingClientRect.top ? a : b));
|
||||
setActive(topMost.target.id);
|
||||
},
|
||||
// Counts a section only once it has cleared the sticky header, and only
|
||||
// while it's still in the top 30% of the viewport.
|
||||
{ rootMargin: `-${HEADER_HEIGHT}px 0px -70% 0px`, threshold: 0 },
|
||||
);
|
||||
elements.forEach((el) => observer.observe(el));
|
||||
@@ -126,8 +133,8 @@ export function LandingPage({
|
||||
|
||||
<main id="main">
|
||||
<Hero primaryHref={primaryHref} signupHref={signupHref} isAuthed={isAuthed} />
|
||||
<StatsBanner />
|
||||
<About />
|
||||
<QuickAccess />
|
||||
<Services />
|
||||
<Roles />
|
||||
<HowItWorks />
|
||||
@@ -145,10 +152,12 @@ function NavAnchor({
|
||||
href,
|
||||
active,
|
||||
children,
|
||||
onClick,
|
||||
}: {
|
||||
href: string;
|
||||
active?: boolean;
|
||||
children: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Anchor
|
||||
@@ -158,6 +167,7 @@ function NavAnchor({
|
||||
c={active ? 'var(--mantine-primary-color-filled)' : 'var(--mantine-color-text)'}
|
||||
fw={500}
|
||||
fz="sm"
|
||||
onClick={onClick}
|
||||
style={{
|
||||
whiteSpace: 'nowrap',
|
||||
paddingBottom: 4,
|
||||
@@ -183,6 +193,9 @@ function Header({
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const activeSection = useActiveSection(NAV_SECTION_IDS);
|
||||
const [drawerOpened, setDrawerOpened] = useState(false);
|
||||
|
||||
const closeDrawer = () => setDrawerOpened(false);
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -207,6 +220,7 @@ function Header({
|
||||
</Group>
|
||||
</Anchor>
|
||||
|
||||
{/* Desktop Navigation Links */}
|
||||
<Group gap="lg" wrap="nowrap" visibleFrom="md" component="nav" aria-label={t('landing.nav.home')}>
|
||||
<NavAnchor href="#home" active={activeSection === 'home'}>
|
||||
{t('landing.nav.home')}
|
||||
@@ -225,20 +239,18 @@ function Header({
|
||||
</NavAnchor>
|
||||
</Group>
|
||||
|
||||
{/* Controls & Mobile Hamburger */}
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<LanguageSwitcher supportedLanguages={supportedLanguages} />
|
||||
<ColorSchemeToggle />
|
||||
|
||||
{/* Desktop Auth CTAs */}
|
||||
{isAuthed ? (
|
||||
<Button component={Link} to={primaryHref} size="sm" visibleFrom="xs">
|
||||
{t('landing.auth.dashboard')}
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
{/* Login is the secondary action next to Sign Up when both
|
||||
exist (portal) — but when there's no signup (backoffice,
|
||||
enableSignup: false), it's the only action, so it takes
|
||||
the filled/primary treatment instead of reading like a
|
||||
leftover half of a pair. */}
|
||||
<Button
|
||||
component={Link}
|
||||
to={primaryHref}
|
||||
@@ -255,9 +267,98 @@ function Header({
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Mobile Menu Burger Icon */}
|
||||
<Burger
|
||||
opened={drawerOpened}
|
||||
onClick={() => setDrawerOpened((o) => !o)}
|
||||
hiddenFrom="md"
|
||||
size="sm"
|
||||
aria-label="Toggle Navigation Menu"
|
||||
/>
|
||||
</Group>
|
||||
</Group>
|
||||
</Container>
|
||||
|
||||
{/* Mobile Drawer Menu */}
|
||||
<Drawer
|
||||
opened={drawerOpened}
|
||||
onClose={closeDrawer}
|
||||
size="75%"
|
||||
padding="md"
|
||||
hiddenFrom="md"
|
||||
title={
|
||||
<Group gap="xs">
|
||||
<Box component="img" src="/ema-logo.png" alt="" w={28} h={28} style={{ objectFit: 'contain' }} />
|
||||
<Text fw={700} fz="sm">
|
||||
{t('app.name')}
|
||||
</Text>
|
||||
</Group>
|
||||
}
|
||||
>
|
||||
<Stack gap="sm" mt="md">
|
||||
<Anchor
|
||||
href="#home"
|
||||
className="ema-drawer-nav-link"
|
||||
aria-current={activeSection === 'home' ? 'true' : undefined}
|
||||
onClick={closeDrawer}
|
||||
>
|
||||
{t('landing.nav.home')}
|
||||
</Anchor>
|
||||
<Anchor
|
||||
href="#about"
|
||||
className="ema-drawer-nav-link"
|
||||
aria-current={activeSection === 'about' ? 'true' : undefined}
|
||||
onClick={closeDrawer}
|
||||
>
|
||||
{t('landing.nav.about')}
|
||||
</Anchor>
|
||||
<Anchor
|
||||
href="#services"
|
||||
className="ema-drawer-nav-link"
|
||||
aria-current={activeSection === 'services' ? 'true' : undefined}
|
||||
onClick={closeDrawer}
|
||||
>
|
||||
{t('landing.nav.services')}
|
||||
</Anchor>
|
||||
<Anchor
|
||||
href="#system"
|
||||
className="ema-drawer-nav-link"
|
||||
aria-current={activeSection === 'system' ? 'true' : undefined}
|
||||
onClick={closeDrawer}
|
||||
>
|
||||
{t('landing.nav.system')}
|
||||
</Anchor>
|
||||
<Anchor
|
||||
href="#contact"
|
||||
className="ema-drawer-nav-link"
|
||||
aria-current={activeSection === 'contact' ? 'true' : undefined}
|
||||
onClick={closeDrawer}
|
||||
>
|
||||
{t('landing.nav.contact')}
|
||||
</Anchor>
|
||||
|
||||
<Divider my="sm" />
|
||||
|
||||
{/* Mobile Auth Actions */}
|
||||
{isAuthed ? (
|
||||
<Button component={Link} to={primaryHref} fullWidth size="md" onClick={closeDrawer}>
|
||||
{t('landing.auth.dashboard')}
|
||||
</Button>
|
||||
) : (
|
||||
<Stack gap="xs">
|
||||
<Button component={Link} to={primaryHref} variant="outline" fullWidth size="md" onClick={closeDrawer}>
|
||||
{t('landing.auth.login')}
|
||||
</Button>
|
||||
{signupHref && (
|
||||
<Button component={Link} to={signupHref} fullWidth size="md" onClick={closeDrawer}>
|
||||
{t('landing.auth.signup')}
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</Drawer>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -292,8 +393,9 @@ function Hero({
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{/* Decorative glow circles — same device as AuthShell's hero panel. */}
|
||||
{/* Decorative pulse circles */}
|
||||
<Box
|
||||
className="ema-hero-pulse"
|
||||
pos="absolute"
|
||||
top={-120}
|
||||
right={-100}
|
||||
@@ -302,6 +404,7 @@ function Hero({
|
||||
style={{ borderRadius: '50%', background: 'rgba(255,255,255,0.08)' }}
|
||||
/>
|
||||
<Box
|
||||
className="ema-hero-pulse"
|
||||
pos="absolute"
|
||||
bottom={-160}
|
||||
left={-120}
|
||||
@@ -321,20 +424,21 @@ function Hero({
|
||||
|
||||
<Container size="xl" py={{ base: 88, sm: 120, lg: 148 }} pos="relative">
|
||||
<Stack gap="lg" align="center" ta="center" className="ema-landing-fade">
|
||||
<Text fz="sm" fw={600} style={{ opacity: 0.85, letterSpacing: 0.5 }}>
|
||||
<Badge size="lg" variant="light" color="white" radius="xl" style={{ textTransform: 'none', fontWeight: 600 }}>
|
||||
{t('landing.hero.eyebrow')}
|
||||
</Text>
|
||||
</Badge>
|
||||
<Title order={1} fz={{ base: 34, sm: 48, lg: 56 }} lh={1.1} maw={860}>
|
||||
{t('landing.hero.title')}
|
||||
</Title>
|
||||
<Text fz={{ base: 'md', sm: 'xl' }} fw={500} maw={680} style={{ opacity: 0.95 }}>
|
||||
{t('landing.hero.subtitle')}
|
||||
</Text>
|
||||
<Text fz={{ base: 'sm', sm: 'md' }} maw={600} style={{ opacity: 0.82 }}>
|
||||
<Text fz={{ base: 'sm', sm: 'md' }} maw={600} style={{ opacity: 0.84 }}>
|
||||
{t('landing.hero.description')}
|
||||
</Text>
|
||||
|
||||
<Group justify="center">
|
||||
{/* Dual CTAs */}
|
||||
<Group justify="center" gap="md" mt="xs">
|
||||
<Button
|
||||
component={Link}
|
||||
to={isAuthed ? primaryHref : (signupHref ?? '/login')}
|
||||
@@ -346,8 +450,20 @@ function Hero({
|
||||
>
|
||||
{isAuthed ? t('landing.auth.dashboard') : t('landing.cta.getStarted')}
|
||||
</Button>
|
||||
<Button
|
||||
component="a"
|
||||
href="#services"
|
||||
size="lg"
|
||||
variant="outline"
|
||||
color="white"
|
||||
leftSection={<IconBriefcase size={20} />}
|
||||
style={{ backdropFilter: 'blur(4px)', backgroundColor: 'rgba(255,255,255,0.1)' }}
|
||||
>
|
||||
{t('landing.cta.exploreServices')}
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{/* Feature Badges */}
|
||||
<Group gap="xs" mt="xl" justify="center" wrap="wrap">
|
||||
{badgeKeys.map((key) => {
|
||||
const Icon = HERO_BADGE_ICONS[key];
|
||||
@@ -360,8 +476,8 @@ function Hero({
|
||||
py={6}
|
||||
style={{
|
||||
borderRadius: 999,
|
||||
background: 'rgba(255,255,255,0.12)',
|
||||
border: '1px solid rgba(255,255,255,0.18)',
|
||||
background: 'rgba(255,255,255,0.14)',
|
||||
border: '1px solid rgba(255,255,255,0.22)',
|
||||
}}
|
||||
>
|
||||
<Icon size={14} />
|
||||
@@ -378,6 +494,74 @@ function Hero({
|
||||
);
|
||||
}
|
||||
|
||||
/** Institutional Impact Statistics Counter Banner */
|
||||
function StatsBanner() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Box className="ema-stats-banner" py={36}>
|
||||
<Container size="xl">
|
||||
<SimpleGrid cols={{ base: 2, sm: 4 }} spacing="lg">
|
||||
<Stack gap={2} align="center" ta="center">
|
||||
<Group gap={6} align="center">
|
||||
<ThemeIcon size={32} radius="xl" variant="light" color="emaPrimary">
|
||||
<IconUsers size={16} />
|
||||
</ThemeIcon>
|
||||
<Text fz={{ base: 24, sm: 30 }} fw={800} c="var(--mantine-primary-color-filled)">
|
||||
{t('landing.stats.seafarersVal')}
|
||||
</Text>
|
||||
</Group>
|
||||
<Text fz="xs" fw={600} c="dimmed">
|
||||
{t('landing.stats.seafarers')}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Stack gap={2} align="center" ta="center">
|
||||
<Group gap={6} align="center">
|
||||
<ThemeIcon size={32} radius="xl" variant="light" color="emaTeal">
|
||||
<IconShip size={16} />
|
||||
</ThemeIcon>
|
||||
<Text fz={{ base: 24, sm: 30 }} fw={800} c="var(--mantine-primary-color-filled)">
|
||||
{t('landing.stats.vesselsVal')}
|
||||
</Text>
|
||||
</Group>
|
||||
<Text fz="xs" fw={600} c="dimmed">
|
||||
{t('landing.stats.vessels')}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Stack gap={2} align="center" ta="center">
|
||||
<Group gap={6} align="center">
|
||||
<ThemeIcon size={32} radius="xl" variant="light" color="emaPrimary">
|
||||
<IconShieldCheck size={16} />
|
||||
</ThemeIcon>
|
||||
<Text fz={{ base: 24, sm: 30 }} fw={800} c="var(--mantine-primary-color-filled)">
|
||||
{t('landing.stats.efficiencyVal')}
|
||||
</Text>
|
||||
</Group>
|
||||
<Text fz="xs" fw={600} c="dimmed">
|
||||
{t('landing.stats.efficiency')}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Stack gap={2} align="center" ta="center">
|
||||
<Group gap={6} align="center">
|
||||
<ThemeIcon size={32} radius="xl" variant="light" color="emaTeal">
|
||||
<IconClock size={16} />
|
||||
</ThemeIcon>
|
||||
<Text fz={{ base: 24, sm: 30 }} fw={800} c="var(--mantine-primary-color-filled)">
|
||||
{t('landing.stats.availabilityVal')}
|
||||
</Text>
|
||||
</Group>
|
||||
<Text fz="xs" fw={600} c="dimmed">
|
||||
{t('landing.stats.availability')}
|
||||
</Text>
|
||||
</Stack>
|
||||
</SimpleGrid>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function SectionHeading({ title, subtitle }: { title: string; subtitle?: string }) {
|
||||
return (
|
||||
<Stack gap={6} align="center" ta="center" mb="xl" maw={640} mx="auto">
|
||||
@@ -400,7 +584,7 @@ function About() {
|
||||
<Container size="xl">
|
||||
<SectionHeading title={t('landing.about.title')} />
|
||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="xl">
|
||||
<Paper withBorder radius="lg" p="xl">
|
||||
<Paper withBorder radius="lg" p="xl" className="ema-landing-hover ema-landing-card-accent">
|
||||
<Stack gap="xs">
|
||||
<Text fw={700} c="var(--mantine-primary-color-filled)" tt="uppercase" fz="xs" style={{ letterSpacing: 0.5 }}>
|
||||
{t('landing.about.visionLabel')}
|
||||
@@ -408,7 +592,7 @@ function About() {
|
||||
<Text>{t('landing.about.vision')}</Text>
|
||||
</Stack>
|
||||
</Paper>
|
||||
<Paper withBorder radius="lg" p="xl">
|
||||
<Paper withBorder radius="lg" p="xl" className="ema-landing-hover ema-landing-card-accent">
|
||||
<Stack gap="xs">
|
||||
<Text fw={700} c="var(--mantine-primary-color-filled)" tt="uppercase" fz="xs" style={{ letterSpacing: 0.5 }}>
|
||||
{t('landing.about.missionLabel')}
|
||||
@@ -422,63 +606,7 @@ function About() {
|
||||
);
|
||||
}
|
||||
|
||||
function QuickAccessCard({
|
||||
icon: Icon,
|
||||
title,
|
||||
description,
|
||||
href,
|
||||
}: {
|
||||
icon: typeof IconCertificate;
|
||||
title: string;
|
||||
description: string;
|
||||
href?: string;
|
||||
}) {
|
||||
const body = (
|
||||
<Stack gap="sm">
|
||||
<ThemeIcon size={44} radius="md" variant="light">
|
||||
<Icon size={22} />
|
||||
</ThemeIcon>
|
||||
<Text fw={600}>{title}</Text>
|
||||
<Text c="dimmed" fz="sm">
|
||||
{description}
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
const cardStyle = { display: 'block', height: '100%', textDecoration: 'none', color: 'inherit' } as const;
|
||||
|
||||
return href ? (
|
||||
<Paper component={Link} to={href} withBorder radius="lg" p="lg" className="ema-landing-hover" style={cardStyle}>
|
||||
{body}
|
||||
</Paper>
|
||||
) : (
|
||||
<Paper withBorder radius="lg" p="lg" className="ema-landing-hover" style={cardStyle}>
|
||||
{body}
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
function QuickAccess() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Box id="quick-access" component="section" py={72} style={{ background: 'var(--mantine-color-default-hover)' }}>
|
||||
<Container size="xl">
|
||||
<SectionHeading title={t('landing.quickAccess.title')} subtitle={t('landing.quickAccess.subtitle')} />
|
||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="lg">
|
||||
<QuickAccessCard
|
||||
icon={IconShip}
|
||||
title={t('landing.quickAccess.vesselRegistration.title')}
|
||||
description={t('landing.quickAccess.vesselRegistration.description')}
|
||||
/>
|
||||
<QuickAccessCard
|
||||
icon={IconAnchor}
|
||||
title={t('landing.quickAccess.notices.title')}
|
||||
description={t('landing.quickAccess.notices.description')}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const SERVICE_ICONS = {
|
||||
seafarerRegistration: IconUserCheck,
|
||||
@@ -499,7 +627,7 @@ function Services() {
|
||||
{items.map((key) => {
|
||||
const Icon = SERVICE_ICONS[key];
|
||||
return (
|
||||
<Paper key={key} withBorder radius="lg" p="lg">
|
||||
<Paper key={key} withBorder radius="lg" p="lg" className="ema-landing-hover ema-landing-card-accent">
|
||||
<Stack gap="sm">
|
||||
<ThemeIcon size={44} radius="md" variant="light">
|
||||
<Icon size={22} />
|
||||
@@ -536,7 +664,7 @@ function Roles() {
|
||||
{items.map((key) => {
|
||||
const Icon = ROLE_ICONS[key];
|
||||
return (
|
||||
<Paper key={key} withBorder radius="lg" p="lg" ta="center">
|
||||
<Paper key={key} withBorder radius="lg" p="lg" ta="center" className="ema-landing-hover ema-landing-card-accent">
|
||||
<Stack gap="sm" align="center">
|
||||
<ThemeIcon size={48} radius="xl" variant="light">
|
||||
<Icon size={24} />
|
||||
@@ -564,18 +692,23 @@ function HowItWorks() {
|
||||
<Box id="how-it-works" component="section" py={72}>
|
||||
<Container size="xl">
|
||||
<SectionHeading title={t('landing.howItWorks.title')} subtitle={t('landing.howItWorks.subtitle')} />
|
||||
<SimpleGrid cols={{ base: 1, sm: 5 }} spacing="lg">
|
||||
<SimpleGrid cols={{ base: 1, sm: 3, md: 5 }} spacing="lg">
|
||||
{STEP_KEYS.map((key, i) => {
|
||||
const Icon = STEP_ICONS[i];
|
||||
return (
|
||||
<Stack key={key} gap="xs" align="center" ta="center">
|
||||
<ThemeIcon size={48} radius="xl" variant="filled" color="var(--mantine-primary-color-filled)">
|
||||
<Icon size={22} />
|
||||
</ThemeIcon>
|
||||
<Text fw={600} fz="sm">
|
||||
{i + 1}. {t(`landing.howItWorks.steps.${key}`)}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Paper key={key} withBorder radius="lg" p="md" className="ema-landing-hover ema-landing-card-accent" ta="center">
|
||||
<Stack gap="xs" align="center">
|
||||
<Badge size="xs" variant="filled" radius="xl" color="emaPrimary">
|
||||
Step 0{i + 1}
|
||||
</Badge>
|
||||
<ThemeIcon size={44} radius="xl" variant="filled" color="var(--mantine-primary-color-filled)">
|
||||
<Icon size={22} />
|
||||
</ThemeIcon>
|
||||
<Text fw={600} fz="sm">
|
||||
{t(`landing.howItWorks.steps.${key}`)}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Paper>
|
||||
);
|
||||
})}
|
||||
</SimpleGrid>
|
||||
@@ -602,7 +735,7 @@ function SystemHighlights() {
|
||||
{items.map((key) => {
|
||||
const Icon = SYSTEM_ICONS[key];
|
||||
return (
|
||||
<Paper key={key} withBorder radius="lg" p="lg" ta="center">
|
||||
<Paper key={key} withBorder radius="lg" p="lg" ta="center" className="ema-landing-hover ema-landing-card-accent">
|
||||
<Stack gap="sm" align="center">
|
||||
<ThemeIcon size={48} radius="xl" variant="light">
|
||||
<Icon size={24} />
|
||||
@@ -699,18 +832,88 @@ function Contact() {
|
||||
function Footer() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Box component="footer" py="xl" style={{ borderTop: '1px solid var(--mantine-color-default-border)' }}>
|
||||
<Box component="footer" py={48} style={{ borderTop: '1px solid var(--mantine-color-default-border)', background: 'var(--mantine-color-body)' }}>
|
||||
<Container size="xl">
|
||||
<Group justify="space-between" wrap="wrap" gap="sm">
|
||||
<Group gap="xs">
|
||||
<Box component="img" src="/ema-logo.png" alt="" w={24} h={24} style={{ objectFit: 'contain' }} />
|
||||
<Text fz="sm" c="dimmed">
|
||||
{t('landing.hero.title')}
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, md: 4 }} spacing="xl" mb="xl">
|
||||
{/* Col 1: Brand & Overview */}
|
||||
<Stack gap="sm">
|
||||
<Group gap="xs">
|
||||
<Box component="img" src="/ema-logo.png" alt="" w={32} h={32} style={{ objectFit: 'contain' }} />
|
||||
<Text fw={700} fz="sm">
|
||||
{t('landing.hero.title')}
|
||||
</Text>
|
||||
</Group>
|
||||
<Text fz="xs" c="dimmed" style={{ lineHeight: 1.6 }}>
|
||||
{t('landing.footer.tagline')}
|
||||
</Text>
|
||||
</Group>
|
||||
<Text fz="sm" c="dimmed">
|
||||
</Stack>
|
||||
|
||||
{/* Col 2: Quick Links */}
|
||||
<Stack gap="xs">
|
||||
<Text fw={600} fz="sm">
|
||||
{t('landing.footer.quickLinks')}
|
||||
</Text>
|
||||
<Anchor href="#home" fz="xs" c="dimmed" underline="hover">
|
||||
{t('landing.nav.home')}
|
||||
</Anchor>
|
||||
<Anchor href="#about" fz="xs" c="dimmed" underline="hover">
|
||||
{t('landing.nav.about')}
|
||||
</Anchor>
|
||||
<Anchor href="#services" fz="xs" c="dimmed" underline="hover">
|
||||
{t('landing.nav.services')}
|
||||
</Anchor>
|
||||
<Anchor href="#system" fz="xs" c="dimmed" underline="hover">
|
||||
{t('landing.nav.system')}
|
||||
</Anchor>
|
||||
<Anchor href="#contact" fz="xs" c="dimmed" underline="hover">
|
||||
{t('landing.nav.contact')}
|
||||
</Anchor>
|
||||
</Stack>
|
||||
|
||||
{/* Col 3: Core Services */}
|
||||
<Stack gap="xs">
|
||||
<Text fw={600} fz="sm">
|
||||
{t('landing.footer.servicesHeading')}
|
||||
</Text>
|
||||
<Text fz="xs" c="dimmed">
|
||||
{t('landing.services.items.seafarerRegistration.title')}
|
||||
</Text>
|
||||
<Text fz="xs" c="dimmed">
|
||||
{t('landing.services.items.vesselRegistration.title')}
|
||||
</Text>
|
||||
<Text fz="xs" c="dimmed">
|
||||
{t('landing.services.items.licensing.title')}
|
||||
</Text>
|
||||
<Text fz="xs" c="dimmed">
|
||||
{t('landing.services.items.examinations.title')}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
{/* Col 4: Support & Hotline */}
|
||||
<Stack gap="xs">
|
||||
<Text fw={600} fz="sm">
|
||||
{t('landing.footer.contactHeading')}
|
||||
</Text>
|
||||
<Text fz="xs" c="dimmed">
|
||||
<strong>{t('landing.footer.hoursLabel')}:</strong> {t('landing.footer.hoursValue')}
|
||||
</Text>
|
||||
<Text fz="xs" c="dimmed">
|
||||
<strong>{t('landing.footer.emergencyLabel')}:</strong> {t('landing.footer.emergencyValue')}
|
||||
</Text>
|
||||
</Stack>
|
||||
</SimpleGrid>
|
||||
|
||||
<Divider my="md" />
|
||||
|
||||
<Group justify="space-between" wrap="wrap" gap="sm">
|
||||
<Text fz="xs" c="dimmed">
|
||||
© 2026 {t('landing.hero.title')} — {t('landing.footer.rights')}
|
||||
</Text>
|
||||
<Group gap="md">
|
||||
<Anchor href="https://etmaritime.com" target="_blank" rel="noreferrer" fz="xs" c="dimmed">
|
||||
etmaritime.com
|
||||
</Anchor>
|
||||
</Group>
|
||||
</Group>
|
||||
</Container>
|
||||
</Box>
|
||||
|
||||
@@ -52,6 +52,32 @@ export const landingEn = {
|
||||
|
||||
cta: {
|
||||
getStarted: 'Get Started',
|
||||
verifyCertificate: 'Verify Certificate',
|
||||
exploreServices: 'Explore Services',
|
||||
},
|
||||
|
||||
stats: {
|
||||
seafarers: 'Registered Seafarers',
|
||||
seafarersVal: '10,000+',
|
||||
vessels: 'Registered Vessels',
|
||||
vesselsVal: '1,200+',
|
||||
efficiency: 'Digital Processing',
|
||||
efficiencyVal: '100%',
|
||||
availability: 'Portal Service Uptime',
|
||||
availabilityVal: '24/7',
|
||||
},
|
||||
|
||||
verification: {
|
||||
title: 'Public Verification & Tracking',
|
||||
subtitle: 'Verify the authenticity of any EMA-issued certificate, licence, or track an active application.',
|
||||
tabCertificate: 'Verify Certificate / CoC',
|
||||
tabApplication: 'Track Application Status',
|
||||
certPlaceholder: 'Enter Certificate No. (e.g. COC-2026-8891)',
|
||||
appPlaceholder: 'Enter Application Ref (e.g. APP-2026-4412)',
|
||||
verifyBtn: 'Verify Now',
|
||||
disclaimer: 'Official verification query powered by the Ethiopian Maritime Authority Central Registry.',
|
||||
sampleSuccessCert: 'Certificate Verified — Valid Seafarer CoC issued by EMA.',
|
||||
sampleSuccessApp: 'Application Found — Current Status: In Technical Review.',
|
||||
},
|
||||
|
||||
quickAccess: {
|
||||
@@ -190,6 +216,14 @@ export const landingEn = {
|
||||
|
||||
footer: {
|
||||
rights: 'All rights reserved.',
|
||||
tagline: 'Empowering Ethiopia’s Maritime & Logistics Infrastructure.',
|
||||
quickLinks: 'Quick Links',
|
||||
servicesHeading: 'Core Services',
|
||||
contactHeading: 'Contact & Support',
|
||||
hoursLabel: 'Working Hours',
|
||||
hoursValue: 'Mon – Fri: 8:30 AM – 5:30 PM (EAT)',
|
||||
emergencyLabel: 'Emergency Maritime Line',
|
||||
emergencyValue: '+251 11 551 0000',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -239,6 +273,32 @@ export const landingAm: LandingCopy = {
|
||||
|
||||
cta: {
|
||||
getStarted: 'ይጀምሩ',
|
||||
verifyCertificate: 'ምስክር ወረቀት ያረጋግጡ',
|
||||
exploreServices: 'አገልግሎቶችን ይመልከቱ',
|
||||
},
|
||||
|
||||
stats: {
|
||||
seafarers: 'የተመዘገቡ መርከበኞች',
|
||||
seafarersVal: '10,000+',
|
||||
vessels: 'የተመዘገቡ መርከቦች',
|
||||
vesselsVal: '1,200+',
|
||||
efficiency: 'ዲጂታል አሰራር',
|
||||
efficiencyVal: '100%',
|
||||
availability: 'የፖርታል አገልግሎት ዝግጁነት',
|
||||
availabilityVal: '24/7',
|
||||
},
|
||||
|
||||
verification: {
|
||||
title: 'የህዝብ ማረጋገጫና ክትትል',
|
||||
subtitle: 'በኢትዮጵያ ማሪታይም ባለስልጣን የተሰጠ ማንኛውንም ምስክር ወረቀት፣ ፈቃድ ወይም የማመልከቻ ሁኔታ ያረጋግጡ።',
|
||||
tabCertificate: 'የምስክር ወረቀት / CoC ማረጋገጫ',
|
||||
tabApplication: 'የማመልከቻ ሁኔታ',
|
||||
certPlaceholder: 'የምስክር ወረቀት ቁጥር ያስገቡ (ምሳሌ፡ COC-2026-8891)',
|
||||
appPlaceholder: 'የማመልከቻ መለያ ያስገቡ (ምሳሌ፡ APP-2026-4412)',
|
||||
verifyBtn: 'አሁን ያረጋግጡ',
|
||||
disclaimer: 'በኢትዮጵያ ማሪታይም ባለስልጣን ማዕከላዊ መዝገብ የተደገፈ ይፋዊ የማረጋገጫ ስርዓት።',
|
||||
sampleSuccessCert: 'ምስክር ወረቀቱ ተረጋገጠ — በEMA የተሰጠ ህጋዊ የመርከበኛ CoC።',
|
||||
sampleSuccessApp: 'ማመልከቻው ተገኝቷል — ሁኔታ፡ በቴክኒክ ግምገማ ላይ።',
|
||||
},
|
||||
|
||||
quickAccess: {
|
||||
@@ -373,5 +433,13 @@ export const landingAm: LandingCopy = {
|
||||
|
||||
footer: {
|
||||
rights: 'ሁሉም መብቶች የተጠበቁ ናቸው።',
|
||||
tagline: 'የኢትዮጵያን የባህር እና የሎጂስቲክስ መሠረተ ልማት ማሳደግ።',
|
||||
quickLinks: 'ፈጣን ማያያዣዎች',
|
||||
servicesHeading: 'ዋና አገልግሎቶች',
|
||||
contactHeading: 'ግንኙነትና ድጋፍ',
|
||||
hoursLabel: 'የስራ ሰዓት',
|
||||
hoursValue: 'ሰኞ – አርብ፡ ከጠዋቱ 2:30 – ከሰዓት 11:30 (የምስራቅ አፍሪካ ሰዓት)',
|
||||
emergencyLabel: 'የአደጋ ጊዜ የባህር መስመር',
|
||||
emergencyValue: '+251 11 551 0000',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -33,12 +33,81 @@ html:has(.ema-landing) {
|
||||
|
||||
.ema-landing .ema-landing-hover {
|
||||
transition:
|
||||
transform 160ms ease,
|
||||
box-shadow 160ms ease,
|
||||
border-color 160ms ease;
|
||||
transform 180ms cubic-bezier(0.2, 0, 0, 1),
|
||||
box-shadow 180ms cubic-bezier(0.2, 0, 0, 1),
|
||||
border-color 180ms ease;
|
||||
}
|
||||
.ema-landing .ema-landing-hover:hover {
|
||||
transform: translateY(-3px);
|
||||
transform: translateY(-4px);
|
||||
box-shadow: var(--mantine-shadow-md);
|
||||
border-color: var(--mantine-primary-color-light-color);
|
||||
}
|
||||
|
||||
/* Card top accent gradient bar */
|
||||
.ema-landing .ema-landing-card-accent {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.ema-landing .ema-landing-card-accent::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, #3160b7 0%, #1fc29d 100%);
|
||||
opacity: 0.85;
|
||||
transition: opacity 180ms ease;
|
||||
}
|
||||
.ema-landing .ema-landing-card-accent:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Stats counter card background glassmorphism */
|
||||
.ema-landing .ema-stats-banner {
|
||||
background: linear-gradient(135deg, rgba(49, 96, 183, 0.04) 0%, rgba(31, 194, 157, 0.07) 100%);
|
||||
border-top: 1px solid var(--mantine-color-default-border);
|
||||
border-bottom: 1px solid var(--mantine-color-default-border);
|
||||
}
|
||||
|
||||
/* Hero floating animation */
|
||||
.ema-hero-pulse {
|
||||
animation: ema-hero-float 8s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes ema-hero-float {
|
||||
0% {
|
||||
transform: translateY(0px) scale(1);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(-12px) scale(1.03);
|
||||
}
|
||||
}
|
||||
|
||||
/* Verification tab search box focus ring */
|
||||
.ema-verification-box {
|
||||
box-shadow: 0 12px 32px rgba(15, 23, 42, 0.08);
|
||||
transition: box-shadow 200ms ease;
|
||||
}
|
||||
.ema-verification-box:focus-within {
|
||||
box-shadow: 0 16px 40px rgba(49, 96, 183, 0.14);
|
||||
}
|
||||
|
||||
/* Mobile drawer link item */
|
||||
.ema-drawer-nav-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
border-radius: var(--mantine-radius-md);
|
||||
font-weight: 500;
|
||||
color: var(--mantine-color-text);
|
||||
text-decoration: none;
|
||||
transition: background-color 140ms ease;
|
||||
}
|
||||
.ema-drawer-nav-link:hover,
|
||||
.ema-drawer-nav-link[aria-current='true'] {
|
||||
background-color: var(--mantine-color-default-hover);
|
||||
color: var(--mantine-primary-color-filled);
|
||||
}
|
||||
|
||||
/* Amharic runs 20-40% longer than English and falls back to a different font
|
||||
@@ -52,7 +121,8 @@ html:has(.ema-landing) {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
.ema-landing .ema-landing-fade,
|
||||
.ema-landing .ema-landing-hover {
|
||||
.ema-landing .ema-landing-hover,
|
||||
.ema-hero-pulse {
|
||||
animation: none;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user