UI improvements

This commit is contained in:
Estifo77
2026-08-14 15:10:41 +03:00
parent 7733fe8efd
commit c00a19dd99
3 changed files with 439 additions and 98 deletions

View File

@@ -4,14 +4,20 @@ import { useTranslation } from 'react-i18next';
import { import {
Accordion, Accordion,
Anchor, Anchor,
Badge,
Box, Box,
Burger,
Button, Button,
Container, Container,
Divider,
Drawer,
Group, Group,
Paper, Paper,
SimpleGrid, SimpleGrid,
Stack, Stack,
Tabs,
Text, Text,
TextInput,
ThemeIcon, ThemeIcon,
Title, Title,
useMantineTheme, useMantineTheme,
@@ -22,13 +28,16 @@ import {
IconAward, IconAward,
IconBriefcase, IconBriefcase,
IconCertificate, IconCertificate,
IconCheck,
IconClipboardCheck, IconClipboardCheck,
IconClock,
IconGavel, IconGavel,
IconLanguage, IconLanguage,
IconMail, IconMail,
IconMapPin, IconMapPin,
IconPhone, IconPhone,
IconSchool, IconSchool,
IconSearch,
IconShip, IconShip,
IconShieldCheck, IconShieldCheck,
IconUserCheck, IconUserCheck,
@@ -73,8 +82,6 @@ function useActiveSection(ids: readonly string[]) {
const topMost = visible.reduce((a, b) => (a.boundingClientRect.top <= b.boundingClientRect.top ? a : b)); const topMost = visible.reduce((a, b) => (a.boundingClientRect.top <= b.boundingClientRect.top ? a : b));
setActive(topMost.target.id); 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 }, { rootMargin: `-${HEADER_HEIGHT}px 0px -70% 0px`, threshold: 0 },
); );
elements.forEach((el) => observer.observe(el)); elements.forEach((el) => observer.observe(el));
@@ -126,8 +133,8 @@ export function LandingPage({
<main id="main"> <main id="main">
<Hero primaryHref={primaryHref} signupHref={signupHref} isAuthed={isAuthed} /> <Hero primaryHref={primaryHref} signupHref={signupHref} isAuthed={isAuthed} />
<StatsBanner />
<About /> <About />
<QuickAccess />
<Services /> <Services />
<Roles /> <Roles />
<HowItWorks /> <HowItWorks />
@@ -145,10 +152,12 @@ function NavAnchor({
href, href,
active, active,
children, children,
onClick,
}: { }: {
href: string; href: string;
active?: boolean; active?: boolean;
children: React.ReactNode; children: React.ReactNode;
onClick?: () => void;
}) { }) {
return ( return (
<Anchor <Anchor
@@ -158,6 +167,7 @@ function NavAnchor({
c={active ? 'var(--mantine-primary-color-filled)' : 'var(--mantine-color-text)'} c={active ? 'var(--mantine-primary-color-filled)' : 'var(--mantine-color-text)'}
fw={500} fw={500}
fz="sm" fz="sm"
onClick={onClick}
style={{ style={{
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
paddingBottom: 4, paddingBottom: 4,
@@ -183,6 +193,9 @@ function Header({
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const activeSection = useActiveSection(NAV_SECTION_IDS); const activeSection = useActiveSection(NAV_SECTION_IDS);
const [drawerOpened, setDrawerOpened] = useState(false);
const closeDrawer = () => setDrawerOpened(false);
return ( return (
<Box <Box
@@ -207,6 +220,7 @@ function Header({
</Group> </Group>
</Anchor> </Anchor>
{/* Desktop Navigation Links */}
<Group gap="lg" wrap="nowrap" visibleFrom="md" component="nav" aria-label={t('landing.nav.home')}> <Group gap="lg" wrap="nowrap" visibleFrom="md" component="nav" aria-label={t('landing.nav.home')}>
<NavAnchor href="#home" active={activeSection === 'home'}> <NavAnchor href="#home" active={activeSection === 'home'}>
{t('landing.nav.home')} {t('landing.nav.home')}
@@ -225,20 +239,18 @@ function Header({
</NavAnchor> </NavAnchor>
</Group> </Group>
{/* Controls & Mobile Hamburger */}
<Group gap="xs" wrap="nowrap"> <Group gap="xs" wrap="nowrap">
<LanguageSwitcher supportedLanguages={supportedLanguages} /> <LanguageSwitcher supportedLanguages={supportedLanguages} />
<ColorSchemeToggle /> <ColorSchemeToggle />
{/* Desktop Auth CTAs */}
{isAuthed ? ( {isAuthed ? (
<Button component={Link} to={primaryHref} size="sm" visibleFrom="xs"> <Button component={Link} to={primaryHref} size="sm" visibleFrom="xs">
{t('landing.auth.dashboard')} {t('landing.auth.dashboard')}
</Button> </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 <Button
component={Link} component={Link}
to={primaryHref} 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>
</Group> </Group>
</Container> </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> </Box>
); );
} }
@@ -292,8 +393,9 @@ function Hero({
overflow: 'hidden', overflow: 'hidden',
}} }}
> >
{/* Decorative glow circles — same device as AuthShell's hero panel. */} {/* Decorative pulse circles */}
<Box <Box
className="ema-hero-pulse"
pos="absolute" pos="absolute"
top={-120} top={-120}
right={-100} right={-100}
@@ -302,6 +404,7 @@ function Hero({
style={{ borderRadius: '50%', background: 'rgba(255,255,255,0.08)' }} style={{ borderRadius: '50%', background: 'rgba(255,255,255,0.08)' }}
/> />
<Box <Box
className="ema-hero-pulse"
pos="absolute" pos="absolute"
bottom={-160} bottom={-160}
left={-120} left={-120}
@@ -321,20 +424,21 @@ function Hero({
<Container size="xl" py={{ base: 88, sm: 120, lg: 148 }} pos="relative"> <Container size="xl" py={{ base: 88, sm: 120, lg: 148 }} pos="relative">
<Stack gap="lg" align="center" ta="center" className="ema-landing-fade"> <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')} {t('landing.hero.eyebrow')}
</Text> </Badge>
<Title order={1} fz={{ base: 34, sm: 48, lg: 56 }} lh={1.1} maw={860}> <Title order={1} fz={{ base: 34, sm: 48, lg: 56 }} lh={1.1} maw={860}>
{t('landing.hero.title')} {t('landing.hero.title')}
</Title> </Title>
<Text fz={{ base: 'md', sm: 'xl' }} fw={500} maw={680} style={{ opacity: 0.95 }}> <Text fz={{ base: 'md', sm: 'xl' }} fw={500} maw={680} style={{ opacity: 0.95 }}>
{t('landing.hero.subtitle')} {t('landing.hero.subtitle')}
</Text> </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')} {t('landing.hero.description')}
</Text> </Text>
<Group justify="center"> {/* Dual CTAs */}
<Group justify="center" gap="md" mt="xs">
<Button <Button
component={Link} component={Link}
to={isAuthed ? primaryHref : (signupHref ?? '/login')} to={isAuthed ? primaryHref : (signupHref ?? '/login')}
@@ -346,8 +450,20 @@ function Hero({
> >
{isAuthed ? t('landing.auth.dashboard') : t('landing.cta.getStarted')} {isAuthed ? t('landing.auth.dashboard') : t('landing.cta.getStarted')}
</Button> </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> </Group>
{/* Feature Badges */}
<Group gap="xs" mt="xl" justify="center" wrap="wrap"> <Group gap="xs" mt="xl" justify="center" wrap="wrap">
{badgeKeys.map((key) => { {badgeKeys.map((key) => {
const Icon = HERO_BADGE_ICONS[key]; const Icon = HERO_BADGE_ICONS[key];
@@ -360,8 +476,8 @@ function Hero({
py={6} py={6}
style={{ style={{
borderRadius: 999, borderRadius: 999,
background: 'rgba(255,255,255,0.12)', background: 'rgba(255,255,255,0.14)',
border: '1px solid rgba(255,255,255,0.18)', border: '1px solid rgba(255,255,255,0.22)',
}} }}
> >
<Icon size={14} /> <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 }) { function SectionHeading({ title, subtitle }: { title: string; subtitle?: string }) {
return ( return (
<Stack gap={6} align="center" ta="center" mb="xl" maw={640} mx="auto"> <Stack gap={6} align="center" ta="center" mb="xl" maw={640} mx="auto">
@@ -400,7 +584,7 @@ function About() {
<Container size="xl"> <Container size="xl">
<SectionHeading title={t('landing.about.title')} /> <SectionHeading title={t('landing.about.title')} />
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="xl"> <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"> <Stack gap="xs">
<Text fw={700} c="var(--mantine-primary-color-filled)" tt="uppercase" fz="xs" style={{ letterSpacing: 0.5 }}> <Text fw={700} c="var(--mantine-primary-color-filled)" tt="uppercase" fz="xs" style={{ letterSpacing: 0.5 }}>
{t('landing.about.visionLabel')} {t('landing.about.visionLabel')}
@@ -408,7 +592,7 @@ function About() {
<Text>{t('landing.about.vision')}</Text> <Text>{t('landing.about.vision')}</Text>
</Stack> </Stack>
</Paper> </Paper>
<Paper withBorder radius="lg" p="xl"> <Paper withBorder radius="lg" p="xl" className="ema-landing-hover ema-landing-card-accent">
<Stack gap="xs"> <Stack gap="xs">
<Text fw={700} c="var(--mantine-primary-color-filled)" tt="uppercase" fz="xs" style={{ letterSpacing: 0.5 }}> <Text fw={700} c="var(--mantine-primary-color-filled)" tt="uppercase" fz="xs" style={{ letterSpacing: 0.5 }}>
{t('landing.about.missionLabel')} {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 = { const SERVICE_ICONS = {
seafarerRegistration: IconUserCheck, seafarerRegistration: IconUserCheck,
@@ -499,7 +627,7 @@ function Services() {
{items.map((key) => { {items.map((key) => {
const Icon = SERVICE_ICONS[key]; const Icon = SERVICE_ICONS[key];
return ( 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"> <Stack gap="sm">
<ThemeIcon size={44} radius="md" variant="light"> <ThemeIcon size={44} radius="md" variant="light">
<Icon size={22} /> <Icon size={22} />
@@ -536,7 +664,7 @@ function Roles() {
{items.map((key) => { {items.map((key) => {
const Icon = ROLE_ICONS[key]; const Icon = ROLE_ICONS[key];
return ( 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"> <Stack gap="sm" align="center">
<ThemeIcon size={48} radius="xl" variant="light"> <ThemeIcon size={48} radius="xl" variant="light">
<Icon size={24} /> <Icon size={24} />
@@ -564,18 +692,23 @@ function HowItWorks() {
<Box id="how-it-works" component="section" py={72}> <Box id="how-it-works" component="section" py={72}>
<Container size="xl"> <Container size="xl">
<SectionHeading title={t('landing.howItWorks.title')} subtitle={t('landing.howItWorks.subtitle')} /> <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) => { {STEP_KEYS.map((key, i) => {
const Icon = STEP_ICONS[i]; const Icon = STEP_ICONS[i];
return ( return (
<Stack key={key} gap="xs" align="center" ta="center"> <Paper key={key} withBorder radius="lg" p="md" className="ema-landing-hover ema-landing-card-accent" ta="center">
<ThemeIcon size={48} radius="xl" variant="filled" color="var(--mantine-primary-color-filled)"> <Stack gap="xs" align="center">
<Icon size={22} /> <Badge size="xs" variant="filled" radius="xl" color="emaPrimary">
</ThemeIcon> Step 0{i + 1}
<Text fw={600} fz="sm"> </Badge>
{i + 1}. {t(`landing.howItWorks.steps.${key}`)} <ThemeIcon size={44} radius="xl" variant="filled" color="var(--mantine-primary-color-filled)">
</Text> <Icon size={22} />
</Stack> </ThemeIcon>
<Text fw={600} fz="sm">
{t(`landing.howItWorks.steps.${key}`)}
</Text>
</Stack>
</Paper>
); );
})} })}
</SimpleGrid> </SimpleGrid>
@@ -602,7 +735,7 @@ function SystemHighlights() {
{items.map((key) => { {items.map((key) => {
const Icon = SYSTEM_ICONS[key]; const Icon = SYSTEM_ICONS[key];
return ( 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"> <Stack gap="sm" align="center">
<ThemeIcon size={48} radius="xl" variant="light"> <ThemeIcon size={48} radius="xl" variant="light">
<Icon size={24} /> <Icon size={24} />
@@ -699,18 +832,88 @@ function Contact() {
function Footer() { function Footer() {
const { t } = useTranslation(); const { t } = useTranslation();
return ( 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"> <Container size="xl">
<Group justify="space-between" wrap="wrap" gap="sm"> <SimpleGrid cols={{ base: 1, sm: 2, md: 4 }} spacing="xl" mb="xl">
<Group gap="xs"> {/* Col 1: Brand & Overview */}
<Box component="img" src="/ema-logo.png" alt="" w={24} h={24} style={{ objectFit: 'contain' }} /> <Stack gap="sm">
<Text fz="sm" c="dimmed"> <Group gap="xs">
{t('landing.hero.title')} <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> </Text>
</Group> </Stack>
<Text fz="sm" c="dimmed">
{/* 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')} © 2026 {t('landing.hero.title')} {t('landing.footer.rights')}
</Text> </Text>
<Group gap="md">
<Anchor href="https://etmaritime.com" target="_blank" rel="noreferrer" fz="xs" c="dimmed">
etmaritime.com
</Anchor>
</Group>
</Group> </Group>
</Container> </Container>
</Box> </Box>

View File

@@ -52,6 +52,32 @@ export const landingEn = {
cta: { cta: {
getStarted: 'Get Started', 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: { quickAccess: {
@@ -190,6 +216,14 @@ export const landingEn = {
footer: { footer: {
rights: 'All rights reserved.', rights: 'All rights reserved.',
tagline: 'Empowering Ethiopias 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: { cta: {
getStarted: 'ይጀምሩ', 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: { quickAccess: {
@@ -373,5 +433,13 @@ export const landingAm: LandingCopy = {
footer: { footer: {
rights: 'ሁሉም መብቶች የተጠበቁ ናቸው።', rights: 'ሁሉም መብቶች የተጠበቁ ናቸው።',
tagline: 'የኢትዮጵያን የባህር እና የሎጂስቲክስ መሠረተ ልማት ማሳደግ።',
quickLinks: 'ፈጣን ማያያዣዎች',
servicesHeading: 'ዋና አገልግሎቶች',
contactHeading: 'ግንኙነትና ድጋፍ',
hoursLabel: 'የስራ ሰዓት',
hoursValue: 'ሰኞ አርብ፡ ከጠዋቱ 2:30 ከሰዓት 11:30 (የምስራቅ አፍሪካ ሰዓት)',
emergencyLabel: 'የአደጋ ጊዜ የባህር መስመር',
emergencyValue: '+251 11 551 0000',
}, },
}; };

View File

@@ -33,12 +33,81 @@ html:has(.ema-landing) {
.ema-landing .ema-landing-hover { .ema-landing .ema-landing-hover {
transition: transition:
transform 160ms ease, transform 180ms cubic-bezier(0.2, 0, 0, 1),
box-shadow 160ms ease, box-shadow 180ms cubic-bezier(0.2, 0, 0, 1),
border-color 160ms ease; border-color 180ms ease;
} }
.ema-landing .ema-landing-hover:hover { .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 /* 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; scroll-behavior: auto;
} }
.ema-landing .ema-landing-fade, .ema-landing .ema-landing-fade,
.ema-landing .ema-landing-hover { .ema-landing .ema-landing-hover,
.ema-hero-pulse {
animation: none; animation: none;
transition: none; transition: none;
} }