mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 22:58:17 +00:00
Merge pull request #1497 from Tria-plc/nati-main
feat(landing): redesign front door with official logo, photo hero and…
This commit is contained in:
BIN
apps/edr-landing/public/brand/edr-logo-green.png
Normal file
BIN
apps/edr-landing/public/brand/edr-logo-green.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
BIN
apps/edr-landing/public/brand/edr-logo-white.png
Normal file
BIN
apps/edr-landing/public/brand/edr-logo-white.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
BIN
apps/edr-landing/public/brand/edr-mark-green.png
Normal file
BIN
apps/edr-landing/public/brand/edr-mark-green.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
BIN
apps/edr-landing/public/brand/edr-mark-white.png
Normal file
BIN
apps/edr-landing/public/brand/edr-mark-white.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
BIN
apps/edr-landing/public/photos/freight-train.jpg
Normal file
BIN
apps/edr-landing/public/photos/freight-train.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 120 KiB |
BIN
apps/edr-landing/public/photos/station.jpg
Normal file
BIN
apps/edr-landing/public/photos/station.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 179 KiB |
@@ -4,24 +4,53 @@
|
|||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
body {
|
body {
|
||||||
@apply bg-white text-slate-900 antialiased;
|
@apply bg-white text-edr-ink antialiased;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
/* Motion — lifted from the freight portal's landing page so the two */
|
/* Motion */
|
||||||
/* public surfaces share one visual language. */
|
/* */
|
||||||
|
/* Every hidden-by-default state is scoped to `html.js`, which an */
|
||||||
|
/* inline script in the layout sets. With JavaScript off the page */
|
||||||
|
/* renders plainly instead of staying invisible — fatal for a front */
|
||||||
|
/* door whose whole job is two links. */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
/* One-shot page-load choreography for the hero copy. */
|
||||||
|
.js .edr-rise {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(18px);
|
||||||
|
animation: edr-rise 0.9s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||||
|
animation-delay: var(--rise-delay, 0ms);
|
||||||
|
}
|
||||||
|
@keyframes edr-rise {
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hero photos drift slowly while they are on screen. */
|
||||||
|
.js .edr-kenburns {
|
||||||
|
animation: edr-kenburns 16s ease-in-out infinite alternate;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
@keyframes edr-kenburns {
|
||||||
|
from {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: scale(1.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scroll reveal for the sections below the fold. */
|
||||||
.edr-reveal {
|
.edr-reveal {
|
||||||
transition:
|
transition:
|
||||||
opacity 0.7s cubic-bezier(0.16, 1, 0.3, 1),
|
opacity 0.7s cubic-bezier(0.16, 1, 0.3, 1),
|
||||||
transform 0.7s cubic-bezier(0.16, 1, 0.3, 1);
|
transform 0.7s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The hidden state is scoped to `html.js`, set by an inline script in the
|
|
||||||
layout. Without it a reader with JavaScript off would get a blank page —
|
|
||||||
fatal for a front door whose whole job is two links. */
|
|
||||||
.js .edr-reveal {
|
.js .edr-reveal {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(24px);
|
transform: translateY(24px);
|
||||||
@@ -31,45 +60,6 @@
|
|||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hero: rail streaks racing toward the horizon */
|
|
||||||
.edr-rails {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
background-image: repeating-linear-gradient(
|
|
||||||
100deg,
|
|
||||||
transparent 0 46px,
|
|
||||||
rgba(52, 211, 153, 0.16) 46px 48px
|
|
||||||
);
|
|
||||||
mask-image: radial-gradient(120% 90% at 78% 40%, #000 0%, transparent 70%);
|
|
||||||
-webkit-mask-image: radial-gradient(120% 90% at 78% 40%, #000 0%, transparent 70%);
|
|
||||||
animation: edr-rails 9s linear infinite;
|
|
||||||
}
|
|
||||||
@keyframes edr-rails {
|
|
||||||
to {
|
|
||||||
background-position: 480px 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.edr-hero-glow {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
background:
|
|
||||||
radial-gradient(46% 60% at 12% 22%, rgba(10, 138, 95, 0.45), transparent 70%),
|
|
||||||
radial-gradient(40% 55% at 88% 82%, rgba(242, 165, 22, 0.22), transparent 70%);
|
|
||||||
animation: edr-breathe 11s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
@keyframes edr-breathe {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
opacity: 0.75;
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1.06);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Pulsing live dot */
|
/* Pulsing live dot */
|
||||||
.edr-pulse-dot {
|
.edr-pulse-dot {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -79,6 +69,9 @@
|
|||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
background: #34d399;
|
background: #34d399;
|
||||||
}
|
}
|
||||||
|
.edr-pulse-dot--green {
|
||||||
|
background: #008550;
|
||||||
|
}
|
||||||
.edr-pulse-dot::after {
|
.edr-pulse-dot::after {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -90,6 +83,18 @@
|
|||||||
opacity: 0.55;
|
opacity: 0.55;
|
||||||
animation: edr-ping 1.8s cubic-bezier(0, 0, 0.2, 1) infinite;
|
animation: edr-ping 1.8s cubic-bezier(0, 0, 0.2, 1) infinite;
|
||||||
}
|
}
|
||||||
|
.edr-ping {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.edr-ping::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: -4px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: currentColor;
|
||||||
|
opacity: 0.35;
|
||||||
|
animation: edr-ping 1.8s cubic-bezier(0, 0, 0.2, 1) infinite;
|
||||||
|
}
|
||||||
@keyframes edr-ping {
|
@keyframes edr-ping {
|
||||||
0% {
|
0% {
|
||||||
transform: scale(0.6);
|
transform: scale(0.6);
|
||||||
@@ -103,23 +108,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* The corridor, with a train light running its length */
|
/* The corridor, with a train light running its length */
|
||||||
.edr-corridor {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
height: 18px;
|
|
||||||
}
|
|
||||||
.edr-corridor::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
top: 50%;
|
|
||||||
height: 2px;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
background: rgba(52, 211, 153, 0.28);
|
|
||||||
}
|
|
||||||
.edr-corridor-node {
|
.edr-corridor-node {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
@@ -159,16 +147,90 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mockups ported from the freight portal's landing page */
|
||||||
|
.edr-float {
|
||||||
|
animation: edr-float 7s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes edr-float {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-8px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.edr-minitrack {
|
||||||
|
position: relative;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
.edr-minitrack::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 50%;
|
||||||
|
height: 2px;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: #c9e9db;
|
||||||
|
border-radius: 9999px;
|
||||||
|
}
|
||||||
|
.edr-minitrack-dot {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
width: 9px;
|
||||||
|
height: 9px;
|
||||||
|
margin-top: -4.5px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: #008550;
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 4px rgba(0, 133, 80, 0.18),
|
||||||
|
0 0 14px 2px rgba(0, 133, 80, 0.5);
|
||||||
|
animation: edr-run-dot 6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes edr-run-dot {
|
||||||
|
0%,
|
||||||
|
8% {
|
||||||
|
left: 0%;
|
||||||
|
}
|
||||||
|
92%,
|
||||||
|
100% {
|
||||||
|
left: calc(100% - 9px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.edr-grow {
|
||||||
|
transform-origin: bottom;
|
||||||
|
animation: edr-grow 0.8s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||||
|
}
|
||||||
|
@keyframes edr-grow {
|
||||||
|
from {
|
||||||
|
transform: scaleY(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: scaleY(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.js .edr-rise,
|
||||||
|
.js .edr-kenburns {
|
||||||
|
opacity: 1;
|
||||||
|
transform: none;
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
.js .edr-reveal {
|
.js .edr-reveal {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: none;
|
transform: none;
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
.edr-rails,
|
|
||||||
.edr-hero-glow,
|
|
||||||
.edr-pulse-dot::after,
|
.edr-pulse-dot::after,
|
||||||
.edr-corridor-train {
|
.edr-ping::after,
|
||||||
|
.edr-corridor-train,
|
||||||
|
.edr-float,
|
||||||
|
.edr-minitrack-dot,
|
||||||
|
.edr-grow {
|
||||||
animation: none !important;
|
animation: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,17 +27,26 @@ export const metadata: Metadata = {
|
|||||||
title: "Ethio-Djibouti Railway — Passenger & Freight",
|
title: "Ethio-Djibouti Railway — Passenger & Freight",
|
||||||
description:
|
description:
|
||||||
"Book a seat or ship a container on the same railway. Pick the service you need.",
|
"Book a seat or ship a container on the same railway. Pick the service you need.",
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: "/photos/station.jpg",
|
||||||
|
width: 1280,
|
||||||
|
height: 720,
|
||||||
|
alt: "Furi-Lebu station of the Ethio-Djibouti Railway",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
card: "summary_large_image",
|
card: "summary_large_image",
|
||||||
title: "Ethio-Djibouti Railway — Passenger & Freight",
|
title: "Ethio-Djibouti Railway — Passenger & Freight",
|
||||||
description:
|
description:
|
||||||
"Book a seat or ship a container on the same railway. Pick the service you need.",
|
"Book a seat or ship a container on the same railway. Pick the service you need.",
|
||||||
|
images: ["/photos/station.jpg"],
|
||||||
},
|
},
|
||||||
icons: {
|
icons: {
|
||||||
icon: "/edr-logo.png",
|
icon: "/brand/edr-mark-green.png",
|
||||||
shortcut: "/edr-logo.png",
|
shortcut: "/brand/edr-mark-green.png",
|
||||||
apple: "/edr-logo.png",
|
apple: "/brand/edr-mark-green.png",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -52,7 +61,7 @@ const organizationSchema = {
|
|||||||
"@type": "Organization",
|
"@type": "Organization",
|
||||||
name: "Ethio-Djibouti Railway (EDR)",
|
name: "Ethio-Djibouti Railway (EDR)",
|
||||||
url: siteUrl,
|
url: siteUrl,
|
||||||
logo: `${siteUrl}/edr-logo.png`,
|
logo: `${siteUrl}/brand/edr-logo-green.png`,
|
||||||
contactPoint: {
|
contactPoint: {
|
||||||
"@type": "ContactPoint",
|
"@type": "ContactPoint",
|
||||||
telephone: "9546",
|
telephone: "9546",
|
||||||
@@ -85,7 +94,7 @@ export default function RootLayout({
|
|||||||
crossOrigin="anonymous"
|
crossOrigin="anonymous"
|
||||||
/>
|
/>
|
||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
{/* Gates the reveal animation's hidden state (see globals.css) so the
|
{/* Gates the reveal animation's hidden state (see globals.css) so the
|
||||||
|
|||||||
@@ -1,70 +1,9 @@
|
|||||||
import {
|
import { Corridor } from "@/components/corridor";
|
||||||
ArrowRight,
|
import { Footer } from "@/components/footer";
|
||||||
Container,
|
import { Header } from "@/components/header";
|
||||||
Mail,
|
import { Hero } from "@/components/hero";
|
||||||
MapPin,
|
import { Portals } from "@/components/portals";
|
||||||
Phone,
|
import { Stats } from "@/components/stats";
|
||||||
TrainFront,
|
|
||||||
} from "lucide-react";
|
|
||||||
|
|
||||||
import { CountUp, Reveal } from "@/components/motion";
|
|
||||||
import { FREIGHT_URL, PASSENGER_URL } from "@/lib/apps";
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
|
||||||
/* Content */
|
|
||||||
/* ------------------------------------------------------------------ */
|
|
||||||
|
|
||||||
const doors = [
|
|
||||||
{
|
|
||||||
key: "passenger",
|
|
||||||
icon: TrainFront,
|
|
||||||
name: "Passenger",
|
|
||||||
lede: "Travelling yourself.",
|
|
||||||
href: PASSENGER_URL,
|
|
||||||
cta: "Open Passenger",
|
|
||||||
points: [
|
|
||||||
"Search trains and book a seat",
|
|
||||||
"Choose your class and seat",
|
|
||||||
"Pay with Telebirr or CBE Birr",
|
|
||||||
"Manage or reschedule a trip",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "freight",
|
|
||||||
icon: Container,
|
|
||||||
name: "Freight",
|
|
||||||
lede: "Shipping goods.",
|
|
||||||
href: FREIGHT_URL,
|
|
||||||
cta: "Open Freight",
|
|
||||||
points: [
|
|
||||||
"Book containers or bulk cargo",
|
|
||||||
"Track every wagon live",
|
|
||||||
"Customs-ready documents",
|
|
||||||
"Invoices and settlement",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
const stations = [
|
|
||||||
{ name: "Addis Ababa", kind: "hub" },
|
|
||||||
{ name: "Mojo", kind: "stop" },
|
|
||||||
{ name: "Adama", kind: "stop" },
|
|
||||||
{ name: "Awash", kind: "stop" },
|
|
||||||
{ name: "Dire Dawa", kind: "hub" },
|
|
||||||
{ name: "Dewele", kind: "border" },
|
|
||||||
{ name: "Djibouti Port", kind: "hub" },
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
const stats = [
|
|
||||||
{ value: 752, suffix: " km", label: "Addis Ababa to Djibouti, electrified standard gauge" },
|
|
||||||
{ value: 16, prefix: "~", suffix: " h", label: "Mojo Dry Port to Djibouti Port transit" },
|
|
||||||
{ value: 2, label: "Countries connected by one railway" },
|
|
||||||
{ text: "24/7", label: "Booking and tracking online" },
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
|
||||||
/* Page */
|
|
||||||
/* ------------------------------------------------------------------ */
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
@@ -78,9 +17,10 @@ export default function Home() {
|
|||||||
|
|
||||||
<Header />
|
<Header />
|
||||||
|
|
||||||
<main>
|
<main id="top">
|
||||||
<Hero />
|
<Hero />
|
||||||
<Corridor />
|
<Corridor />
|
||||||
|
<Portals />
|
||||||
<Stats />
|
<Stats />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@@ -88,308 +28,3 @@ export default function Home() {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Wordmark({ subtitle = true }: { subtitle?: boolean }) {
|
|
||||||
return (
|
|
||||||
<span className="flex items-center gap-3">
|
|
||||||
<span className="flex size-10 shrink-0 items-center justify-center rounded-xl bg-edr-primary text-white shadow-lg shadow-emerald-500/25">
|
|
||||||
<TrainFront className="size-5" />
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className="leading-tight">
|
|
||||||
<span className="block text-lg font-bold text-white">EDR</span>
|
|
||||||
{subtitle ? (
|
|
||||||
<span className="block text-[11px] tracking-wide text-slate-400">
|
|
||||||
Ethio-Djibouti Railway
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Header() {
|
|
||||||
return (
|
|
||||||
<header className="sticky top-0 z-50 border-b border-white/10 bg-edr-ink/90 backdrop-blur-xl">
|
|
||||||
<div className="mx-auto flex h-20 max-w-6xl items-center justify-between px-6">
|
|
||||||
<a href="#choose" aria-label="Ethio-Djibouti Railway, back to top">
|
|
||||||
<Wordmark />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<nav className="hidden items-center gap-8 sm:flex">
|
|
||||||
<a
|
|
||||||
href={PASSENGER_URL}
|
|
||||||
className="text-sm font-medium text-slate-300 transition hover:text-white"
|
|
||||||
>
|
|
||||||
Passenger
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href={FREIGHT_URL}
|
|
||||||
className="text-sm font-medium text-slate-300 transition hover:text-white"
|
|
||||||
>
|
|
||||||
Freight
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="#contact"
|
|
||||||
className="text-sm font-medium text-slate-300 transition hover:text-white"
|
|
||||||
>
|
|
||||||
Contact
|
|
||||||
</a>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Hero() {
|
|
||||||
return (
|
|
||||||
<section id="choose" className="relative overflow-hidden bg-edr-ink">
|
|
||||||
<div className="edr-rails" aria-hidden />
|
|
||||||
<div className="edr-hero-glow" aria-hidden />
|
|
||||||
|
|
||||||
<div className="relative z-10 mx-auto max-w-6xl px-6 py-12 lg:py-14">
|
|
||||||
<div className="mx-auto max-w-3xl text-center">
|
|
||||||
<Reveal>
|
|
||||||
<span className="inline-flex items-center gap-2 rounded-full border border-white/15 bg-white/5 py-1.5 pl-2 pr-3 text-[11px] font-medium text-white sm:text-[13px]">
|
|
||||||
<span className="edr-pulse-dot" />
|
|
||||||
Addis Ababa – Djibouti · 752 km electrified corridor
|
|
||||||
</span>
|
|
||||||
</Reveal>
|
|
||||||
|
|
||||||
<Reveal delay={80}>
|
|
||||||
<h1 className="mt-5 text-4xl font-bold leading-[1.08] tracking-tight text-white md:text-5xl">
|
|
||||||
The Ethio-Djibouti Railway,
|
|
||||||
<span className="block text-edr-light">online.</span>
|
|
||||||
</h1>
|
|
||||||
</Reveal>
|
|
||||||
|
|
||||||
<Reveal delay={160}>
|
|
||||||
<p className="mx-auto mt-4 max-w-xl text-base leading-relaxed text-slate-300 md:text-lg">
|
|
||||||
Book a seat or ship a container on the same railway. Pick the
|
|
||||||
service you need.
|
|
||||||
</p>
|
|
||||||
</Reveal>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* The two doors — the reason this page exists. */}
|
|
||||||
<div className="mt-9 grid gap-6 md:grid-cols-2">
|
|
||||||
{doors.map((door, index) => (
|
|
||||||
<Reveal key={door.key} delay={240 + index * 100}>
|
|
||||||
<Door {...door} />
|
|
||||||
</Reveal>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Reveal delay={460}>
|
|
||||||
<p className="mt-6 text-center text-sm text-slate-400">
|
|
||||||
Travelling yourself?{" "}
|
|
||||||
<span className="font-semibold text-white">Passenger.</span>{" "}
|
|
||||||
Shipping goods?{" "}
|
|
||||||
<span className="font-semibold text-white">Freight.</span>
|
|
||||||
</p>
|
|
||||||
</Reveal>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Door({
|
|
||||||
icon: Icon,
|
|
||||||
name,
|
|
||||||
lede,
|
|
||||||
href,
|
|
||||||
cta,
|
|
||||||
points,
|
|
||||||
}: (typeof doors)[number]) {
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
href={href}
|
|
||||||
className="group flex h-full flex-col rounded-3xl bg-white p-6 shadow-2xl ring-1 ring-white/15 transition duration-300 hover:-translate-y-1 hover:ring-4 hover:ring-edr-light focus-visible:-translate-y-1 focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-edr-light md:p-7"
|
|
||||||
>
|
|
||||||
<span className="flex size-12 items-center justify-center rounded-2xl bg-emerald-50 text-edr-primary">
|
|
||||||
<Icon className="size-6" />
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<h2 className="mt-4 text-2xl font-bold tracking-tight text-edr-ink">
|
|
||||||
{name}
|
|
||||||
</h2>
|
|
||||||
<p className="mt-1 text-base font-medium text-edr-primary">{lede}</p>
|
|
||||||
|
|
||||||
<ul className="mt-4 space-y-2 text-[15px] text-slate-600">
|
|
||||||
{points.map((point) => (
|
|
||||||
<li key={point} className="flex items-start gap-2.5">
|
|
||||||
<span
|
|
||||||
className="mt-[7px] size-1.5 shrink-0 rounded-full bg-edr-light"
|
|
||||||
aria-hidden
|
|
||||||
/>
|
|
||||||
{point}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<span className="mt-6 flex items-center justify-center gap-2 rounded-xl bg-edr-primary px-6 py-3.5 font-semibold text-white transition group-hover:bg-edr-primary-dark">
|
|
||||||
{cta}
|
|
||||||
<ArrowRight className="size-4 transition group-hover:translate-x-0.5" />
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Corridor() {
|
|
||||||
return (
|
|
||||||
<section className="bg-edr-ink-2 py-14">
|
|
||||||
<div className="mx-auto max-w-4xl px-10 sm:px-14">
|
|
||||||
<Reveal>
|
|
||||||
<div className="relative pb-8">
|
|
||||||
<span
|
|
||||||
className="absolute inset-x-0 top-2 h-0.5 -translate-y-1/2 bg-edr-light/25"
|
|
||||||
aria-hidden
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
className="edr-corridor-train"
|
|
||||||
style={{ top: 8 }}
|
|
||||||
aria-hidden
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ol className="relative flex justify-between">
|
|
||||||
{stations.map((station, index) => {
|
|
||||||
const isFirst = index === 0;
|
|
||||||
const isLast = index === stations.length - 1;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li key={station.name} className="relative flex h-4 items-center">
|
|
||||||
<span
|
|
||||||
className={`edr-corridor-node ${
|
|
||||||
station.kind === "hub"
|
|
||||||
? "is-hub"
|
|
||||||
: station.kind === "border"
|
|
||||||
? "is-border"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Absolute so a long label never widens its column — the
|
|
||||||
nodes have to stay evenly spaced along the rail. */}
|
|
||||||
<span
|
|
||||||
className={`absolute top-7 whitespace-nowrap text-[11px] text-slate-400 ${
|
|
||||||
isFirst
|
|
||||||
? "left-0"
|
|
||||||
: isLast
|
|
||||||
? "right-0"
|
|
||||||
: "left-1/2 -translate-x-1/2"
|
|
||||||
} ${isFirst || isLast ? "" : "hidden sm:block"}`}
|
|
||||||
>
|
|
||||||
{station.name}
|
|
||||||
{station.kind === "border" ? " (border)" : ""}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</Reveal>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Stats() {
|
|
||||||
return (
|
|
||||||
<section className="bg-edr-ink-2 pb-20">
|
|
||||||
<div className="mx-auto max-w-6xl px-6">
|
|
||||||
<Reveal>
|
|
||||||
<dl className="grid grid-cols-2 gap-x-6 gap-y-10 border-t border-white/10 pt-12 lg:grid-cols-4">
|
|
||||||
{stats.map((stat) => (
|
|
||||||
<div key={stat.label}>
|
|
||||||
<dt className="text-3xl font-bold tracking-tight text-white md:text-4xl">
|
|
||||||
{"text" in stat ? (
|
|
||||||
stat.text
|
|
||||||
) : (
|
|
||||||
<CountUp
|
|
||||||
value={stat.value}
|
|
||||||
prefix={"prefix" in stat ? stat.prefix : ""}
|
|
||||||
suffix={"suffix" in stat ? stat.suffix : ""}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</dt>
|
|
||||||
<dd className="mt-2 text-sm leading-relaxed text-slate-400">
|
|
||||||
{stat.label}
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</dl>
|
|
||||||
</Reveal>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Footer() {
|
|
||||||
return (
|
|
||||||
<footer id="contact" className="bg-edr-ink">
|
|
||||||
<div className="mx-auto max-w-6xl px-6 py-14">
|
|
||||||
<div className="grid gap-10 sm:grid-cols-3">
|
|
||||||
<div>
|
|
||||||
<Wordmark />
|
|
||||||
<p className="mt-4 max-w-xs text-sm leading-relaxed text-slate-400">
|
|
||||||
The electrified standard-gauge railway connecting Ethiopia and
|
|
||||||
Djibouti.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h2 className="text-sm font-semibold text-white">Services</h2>
|
|
||||||
<ul className="mt-4 space-y-3 text-sm">
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href={PASSENGER_URL}
|
|
||||||
className="text-slate-400 transition hover:text-white"
|
|
||||||
>
|
|
||||||
Passenger portal
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href={FREIGHT_URL}
|
|
||||||
className="text-slate-400 transition hover:text-white"
|
|
||||||
>
|
|
||||||
Freight portal
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h2 className="text-sm font-semibold text-white">Contact</h2>
|
|
||||||
<ul className="mt-4 space-y-3 text-sm text-slate-400">
|
|
||||||
<li className="flex items-center gap-2.5">
|
|
||||||
<Phone className="size-4 shrink-0 text-edr-light" />
|
|
||||||
<a href="tel:9546" className="transition hover:text-white">
|
|
||||||
9546
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center gap-2.5">
|
|
||||||
<Mail className="size-4 shrink-0 text-edr-light" />
|
|
||||||
<a
|
|
||||||
href="mailto:edr_@edrsc.com"
|
|
||||||
className="transition hover:text-white"
|
|
||||||
>
|
|
||||||
edr_@edrsc.com
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center gap-2.5">
|
|
||||||
<MapPin className="size-4 shrink-0 text-edr-light" />
|
|
||||||
Addis Ababa, Ethiopia
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="mt-12 border-t border-white/10 pt-6 text-xs text-slate-500">
|
|
||||||
© {new Date().getFullYear()} Ethio-Djibouti Railway. All rights
|
|
||||||
reserved.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
42
apps/edr-landing/src/components/brand.tsx
Normal file
42
apps/edr-landing/src/components/brand.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
// The official EDR logo, keyed from the brand JPEGs into transparent PNGs
|
||||||
|
// (see public/brand). `lockup` is the mark plus wordmark; `mark` is the swoosh.
|
||||||
|
const assets = {
|
||||||
|
lockup: {
|
||||||
|
white: "/brand/edr-logo-white.png",
|
||||||
|
green: "/brand/edr-logo-green.png",
|
||||||
|
width: 1600,
|
||||||
|
height: 668,
|
||||||
|
},
|
||||||
|
mark: {
|
||||||
|
white: "/brand/edr-mark-white.png",
|
||||||
|
green: "/brand/edr-mark-green.png",
|
||||||
|
width: 1234,
|
||||||
|
height: 386,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export function Logo({
|
||||||
|
tone = "white",
|
||||||
|
variant = "lockup",
|
||||||
|
decorative = false,
|
||||||
|
className = "",
|
||||||
|
}: {
|
||||||
|
tone?: "white" | "green";
|
||||||
|
variant?: "lockup" | "mark";
|
||||||
|
decorative?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const asset = assets[variant];
|
||||||
|
|
||||||
|
return (
|
||||||
|
// eslint-disable-next-line @next/next/no-img-element -- static export, no optimizer
|
||||||
|
<img
|
||||||
|
src={asset[tone]}
|
||||||
|
width={asset.width}
|
||||||
|
height={asset.height}
|
||||||
|
alt={decorative ? "" : "Ethio-Djibouti Railway"}
|
||||||
|
decoding="async"
|
||||||
|
className={className}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
77
apps/edr-landing/src/components/corridor.tsx
Normal file
77
apps/edr-landing/src/components/corridor.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
const stations = [
|
||||||
|
{ name: "Addis Ababa", kind: "hub", note: "Km 0" },
|
||||||
|
{ name: "Mojo", kind: "stop", note: "Dry port" },
|
||||||
|
{ name: "Adama", kind: "stop" },
|
||||||
|
{ name: "Awash", kind: "stop" },
|
||||||
|
{ name: "Dire Dawa", kind: "hub" },
|
||||||
|
{ name: "Dewele", kind: "border", note: "Customs" },
|
||||||
|
{ name: "Djibouti Port", kind: "hub", note: "Km 752" },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/** The line itself: seven stations on one rail, with a train light running it. */
|
||||||
|
export function Corridor() {
|
||||||
|
return (
|
||||||
|
<section aria-label="The Addis Ababa to Djibouti corridor" className="bg-edr-ink-2 py-12">
|
||||||
|
<div className="mx-auto max-w-4xl px-10 sm:px-14">
|
||||||
|
<div className="relative pb-12">
|
||||||
|
<span
|
||||||
|
className="absolute inset-x-0 top-2 h-0.5 -translate-y-1/2 bg-edr-light/25"
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
<span className="edr-corridor-train" style={{ top: 8 }} aria-hidden />
|
||||||
|
|
||||||
|
<ol className="relative flex justify-between">
|
||||||
|
{stations.map((station, index) => {
|
||||||
|
const isFirst = index === 0;
|
||||||
|
const isLast = index === stations.length - 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li key={station.name} className="relative flex h-4 items-center">
|
||||||
|
<span
|
||||||
|
className={`edr-corridor-node ${
|
||||||
|
station.kind === "hub"
|
||||||
|
? "is-hub"
|
||||||
|
: station.kind === "border"
|
||||||
|
? "is-border"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Absolute so a long label never widens its column — the
|
||||||
|
nodes have to stay evenly spaced along the rail. */}
|
||||||
|
<span
|
||||||
|
className={`absolute top-7 whitespace-nowrap text-[11px] leading-tight ${
|
||||||
|
isFirst
|
||||||
|
? "left-0 text-left"
|
||||||
|
: isLast
|
||||||
|
? "right-0 text-right"
|
||||||
|
: "left-1/2 -translate-x-1/2 text-center"
|
||||||
|
} ${
|
||||||
|
// Only the ends and the hub fit under a phone-width rail.
|
||||||
|
isFirst || isLast || station.kind === "hub"
|
||||||
|
? ""
|
||||||
|
: "hidden sm:block"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="block font-medium text-slate-300">
|
||||||
|
{station.name}
|
||||||
|
</span>
|
||||||
|
{"note" in station ? (
|
||||||
|
<span
|
||||||
|
className={`block ${
|
||||||
|
station.kind === "border" ? "text-edr-amber" : "text-slate-500"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{station.note}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
65
apps/edr-landing/src/components/footer.tsx
Normal file
65
apps/edr-landing/src/components/footer.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { Mail, MapPin, Phone } from "lucide-react";
|
||||||
|
|
||||||
|
import { FREIGHT_URL, PASSENGER_URL } from "@/lib/apps";
|
||||||
|
|
||||||
|
import { Logo } from "./brand";
|
||||||
|
|
||||||
|
export function Footer() {
|
||||||
|
return (
|
||||||
|
<footer id="contact" className="bg-edr-ink">
|
||||||
|
<div className="mx-auto max-w-6xl px-6 py-14">
|
||||||
|
<div className="grid gap-10 sm:grid-cols-3">
|
||||||
|
<div>
|
||||||
|
<Logo className="h-12 w-auto" />
|
||||||
|
<p className="mt-5 max-w-xs text-sm leading-relaxed text-slate-400">
|
||||||
|
The electrified standard-gauge railway connecting Ethiopia and
|
||||||
|
Djibouti.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 className="text-sm font-semibold text-white">Services</h2>
|
||||||
|
<ul className="mt-4 space-y-3 text-sm">
|
||||||
|
<li>
|
||||||
|
<a href={PASSENGER_URL} className="text-slate-400 transition hover:text-white">
|
||||||
|
Passenger portal
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href={FREIGHT_URL} className="text-slate-400 transition hover:text-white">
|
||||||
|
Freight portal
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 className="text-sm font-semibold text-white">Contact</h2>
|
||||||
|
<ul className="mt-4 space-y-3 text-sm text-slate-400">
|
||||||
|
<li className="flex items-center gap-2.5">
|
||||||
|
<Phone className="size-4 shrink-0 text-edr-light" />
|
||||||
|
<a href="tel:9546" className="transition hover:text-white">
|
||||||
|
9546
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center gap-2.5">
|
||||||
|
<Mail className="size-4 shrink-0 text-edr-light" />
|
||||||
|
<a href="mailto:edr_@edrsc.com" className="transition hover:text-white">
|
||||||
|
edr_@edrsc.com
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center gap-2.5">
|
||||||
|
<MapPin className="size-4 shrink-0 text-edr-light" />
|
||||||
|
Addis Ababa, Ethiopia
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="mt-12 border-t border-white/10 pt-6 text-xs text-slate-500">
|
||||||
|
© {new Date().getFullYear()} Ethio-Djibouti Railway. All rights reserved.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
57
apps/edr-landing/src/components/header.tsx
Normal file
57
apps/edr-landing/src/components/header.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { FREIGHT_URL, PASSENGER_URL, PUBLICATIONS_URL } from "@/lib/apps";
|
||||||
|
|
||||||
|
import { Logo } from "./brand";
|
||||||
|
|
||||||
|
const link =
|
||||||
|
"rounded-md text-sm font-semibold text-white/80 transition hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-edr-light";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sits transparently over the hero photos, then turns solid once the reader
|
||||||
|
* scrolls so it stays legible over the light sections.
|
||||||
|
*/
|
||||||
|
export function Header() {
|
||||||
|
const [scrolled, setScrolled] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onScroll = () => setScrolled(window.scrollY > 40);
|
||||||
|
onScroll();
|
||||||
|
window.addEventListener("scroll", onScroll, { passive: true });
|
||||||
|
return () => window.removeEventListener("scroll", onScroll);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header
|
||||||
|
className={`fixed inset-x-0 top-0 z-50 transition-colors duration-300 ${
|
||||||
|
scrolled
|
||||||
|
? "border-b border-white/10 bg-edr-ink/85 backdrop-blur-xl"
|
||||||
|
: "bg-gradient-to-b from-edr-ink/70 to-transparent"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="mx-auto flex h-20 max-w-7xl items-center justify-between px-6">
|
||||||
|
<a
|
||||||
|
href="#top"
|
||||||
|
aria-label="Ethio-Djibouti Railway, back to top"
|
||||||
|
className="rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-edr-light"
|
||||||
|
>
|
||||||
|
<Logo className="h-9 w-auto sm:h-11" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<nav aria-label="Primary" className="flex items-center gap-5 sm:gap-8">
|
||||||
|
<a href={PASSENGER_URL} className={link}>
|
||||||
|
Passenger
|
||||||
|
</a>
|
||||||
|
<a href={FREIGHT_URL} className={link}>
|
||||||
|
Freight
|
||||||
|
</a>
|
||||||
|
<a href={PUBLICATIONS_URL} className={`hidden sm:block ${link}`}>
|
||||||
|
Publications
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
158
apps/edr-landing/src/components/hero.tsx
Normal file
158
apps/edr-landing/src/components/hero.tsx
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ArrowRight, Container, TrainFront } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { FREIGHT_URL, PASSENGER_URL } from "@/lib/apps";
|
||||||
|
|
||||||
|
const doors = [
|
||||||
|
{
|
||||||
|
key: "passenger",
|
||||||
|
icon: TrainFront,
|
||||||
|
title: "Passenger",
|
||||||
|
body: "Book a seat, choose your class, pay with Telebirr or CBE Birr.",
|
||||||
|
href: PASSENGER_URL,
|
||||||
|
cta: "Open Passenger",
|
||||||
|
photo: {
|
||||||
|
src: "/photos/station.jpg",
|
||||||
|
width: 1280,
|
||||||
|
height: 720,
|
||||||
|
alt: "Furi-Lebu station in Addis Ababa, the Ethio-Djibouti Railway's passenger terminal",
|
||||||
|
position: "center 30%",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "freight",
|
||||||
|
icon: Container,
|
||||||
|
title: "Freight",
|
||||||
|
body: "Book wagons and containers, track every one of them live.",
|
||||||
|
href: FREIGHT_URL,
|
||||||
|
cta: "Open Freight",
|
||||||
|
photo: {
|
||||||
|
src: "/photos/freight-train.jpg",
|
||||||
|
width: 1280,
|
||||||
|
height: 1134,
|
||||||
|
alt: "An Ethio-Djibouti Railway locomotive hauling container wagons",
|
||||||
|
position: "45% 45%",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const ROTATE_MS = 7000;
|
||||||
|
|
||||||
|
const rise = (ms: number) =>
|
||||||
|
({ "--rise-delay": `${ms}ms` }) as React.CSSProperties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One photograph at a time behind one message. The background alternates
|
||||||
|
* between the two portals' worlds on its own, and settles on whichever card
|
||||||
|
* the reader is hovering.
|
||||||
|
*/
|
||||||
|
export function Hero() {
|
||||||
|
const [active, setActive] = useState(0);
|
||||||
|
const [pinned, setPinned] = useState<number | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (pinned !== null) return;
|
||||||
|
if (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) return;
|
||||||
|
|
||||||
|
const id = window.setInterval(
|
||||||
|
() => setActive((index) => (index + 1) % doors.length),
|
||||||
|
ROTATE_MS,
|
||||||
|
);
|
||||||
|
return () => window.clearInterval(id);
|
||||||
|
}, [pinned]);
|
||||||
|
|
||||||
|
const shown = pinned ?? active;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
id="choose"
|
||||||
|
aria-labelledby="hero-title"
|
||||||
|
className="relative flex min-h-[100svh] items-center overflow-hidden bg-edr-ink"
|
||||||
|
>
|
||||||
|
{doors.map((door, index) => (
|
||||||
|
// eslint-disable-next-line @next/next/no-img-element -- static export
|
||||||
|
<img
|
||||||
|
key={door.key}
|
||||||
|
src={door.photo.src}
|
||||||
|
width={door.photo.width}
|
||||||
|
height={door.photo.height}
|
||||||
|
alt={door.photo.alt}
|
||||||
|
fetchPriority={index === 0 ? "high" : "low"}
|
||||||
|
decoding="async"
|
||||||
|
className={`edr-kenburns absolute inset-0 h-full w-full object-cover transition-opacity duration-[1400ms] ease-out ${
|
||||||
|
index === shown ? "opacity-100" : "opacity-0"
|
||||||
|
}`}
|
||||||
|
style={{ objectPosition: door.photo.position }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Backdrop: a heavy ink wash so the message and cards always read. */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 bg-gradient-to-b from-edr-ink/75 via-edr-ink/60 to-edr-ink"
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 bg-[radial-gradient(70%_60%_at_50%_45%,transparent,rgba(12,26,43,0.55))]"
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="relative z-10 mx-auto w-full max-w-5xl px-6 pb-20 pt-36 text-center lg:pb-24">
|
||||||
|
<p
|
||||||
|
className="edr-rise text-sm font-semibold text-edr-light sm:text-base"
|
||||||
|
style={rise(0)}
|
||||||
|
>
|
||||||
|
Addis Ababa to Djibouti, by rail
|
||||||
|
</p>
|
||||||
|
<h1
|
||||||
|
id="hero-title"
|
||||||
|
className="edr-rise mx-auto mt-4 max-w-4xl text-5xl font-extrabold leading-[1.0] tracking-[-0.025em] text-white sm:text-6xl lg:text-[84px]"
|
||||||
|
style={rise(90)}
|
||||||
|
>
|
||||||
|
One railway.
|
||||||
|
<br />
|
||||||
|
Two ways to move.
|
||||||
|
</h1>
|
||||||
|
<p
|
||||||
|
className="edr-rise mx-auto mt-6 max-w-xl text-lg leading-relaxed text-slate-200 sm:text-xl"
|
||||||
|
style={rise(180)}
|
||||||
|
>
|
||||||
|
Book a seat or ship a container on the Ethio-Djibouti Railway. Pick
|
||||||
|
the service you need.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mx-auto mt-12 grid max-w-2xl gap-4 sm:grid-cols-2">
|
||||||
|
{doors.map(({ key, icon: Icon, title, body, href, cta }, index) => (
|
||||||
|
<a
|
||||||
|
key={key}
|
||||||
|
href={href}
|
||||||
|
onMouseEnter={() => setPinned(index)}
|
||||||
|
onMouseLeave={() => setPinned(null)}
|
||||||
|
onFocus={() => setPinned(index)}
|
||||||
|
onBlur={() => setPinned(null)}
|
||||||
|
className={`edr-rise group flex flex-col rounded-2xl border p-5 text-left backdrop-blur-xl transition duration-300 hover:-translate-y-1 focus-visible:-translate-y-1 focus-visible:outline-none ${
|
||||||
|
index === shown
|
||||||
|
? "border-edr-light/60 bg-white/15 shadow-[0_24px_60px_-20px_rgba(52,211,153,0.35)]"
|
||||||
|
: "border-white/15 bg-white/10 hover:border-edr-light/60 hover:bg-white/15"
|
||||||
|
}`}
|
||||||
|
style={rise(280 + index * 90)}
|
||||||
|
>
|
||||||
|
<span className="flex size-10 items-center justify-center rounded-xl bg-edr-primary text-white shadow-lg shadow-emerald-900/40">
|
||||||
|
<Icon className="size-5" />
|
||||||
|
</span>
|
||||||
|
<span className="mt-4 text-xl font-bold text-white">{title}</span>
|
||||||
|
<span className="mt-1 text-sm leading-relaxed text-slate-300">
|
||||||
|
{body}
|
||||||
|
</span>
|
||||||
|
<span className="mt-4 inline-flex items-center gap-1.5 text-sm font-semibold text-edr-light">
|
||||||
|
{cta}
|
||||||
|
<ArrowRight className="size-4 transition group-hover:translate-x-0.5" />
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
253
apps/edr-landing/src/components/portals.tsx
Normal file
253
apps/edr-landing/src/components/portals.tsx
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
import {
|
||||||
|
ArrowLeftRight,
|
||||||
|
ArrowRight,
|
||||||
|
Armchair,
|
||||||
|
CalendarDays,
|
||||||
|
FileCheck,
|
||||||
|
MapPin,
|
||||||
|
Radar,
|
||||||
|
Receipt,
|
||||||
|
Smartphone,
|
||||||
|
Ticket,
|
||||||
|
Users,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
import { FREIGHT_URL, PASSENGER_URL } from "@/lib/apps";
|
||||||
|
|
||||||
|
import { Reveal } from "./motion";
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Passenger — the trip search, as it looks in the portal */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
function Field({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
icon: Icon,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
icon: React.ElementType;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-3 rounded-xl border-2 border-edr-border bg-edr-surface px-3.5 py-2.5">
|
||||||
|
<Icon className="size-4 shrink-0 text-edr-primary" />
|
||||||
|
<span className="min-w-0">
|
||||||
|
<span className="block text-[11px] font-medium text-edr-muted">{label}</span>
|
||||||
|
<span className="block truncate text-[15px] font-semibold">{value}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SearchMock() {
|
||||||
|
return (
|
||||||
|
<div className="rounded-3xl border border-edr-border bg-white p-5 shadow-[0_24px_60px_-24px_rgba(12,26,43,0.35)] sm:p-6">
|
||||||
|
<div className="inline-flex rounded-xl bg-slate-100 p-1 text-sm font-semibold">
|
||||||
|
<span className="rounded-lg bg-white px-3.5 py-1.5 text-edr-primary shadow-sm">
|
||||||
|
One way
|
||||||
|
</span>
|
||||||
|
<span className="px-3.5 py-1.5 text-edr-muted">Round trip</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 grid items-center gap-2.5 sm:grid-cols-[1fr_auto_1fr]">
|
||||||
|
<Field label="From" value="Addis Ababa" icon={MapPin} />
|
||||||
|
<span className="mx-auto flex size-9 items-center justify-center rounded-full border-2 border-edr-border bg-edr-surface text-slate-500">
|
||||||
|
<ArrowLeftRight className="size-4" />
|
||||||
|
</span>
|
||||||
|
<Field label="To" value="Dire Dawa" icon={MapPin} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-2.5 grid grid-cols-2 gap-2.5">
|
||||||
|
<Field label="Date" value="Fri 12 Sep" icon={CalendarDays} />
|
||||||
|
<Field label="Passengers" value="1 adult" icon={Users} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span className="mt-4 flex items-center justify-center rounded-xl bg-edr-primary py-3 text-[15px] font-semibold text-white">
|
||||||
|
Search trains
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Freight — the live tracking card from the freight portal */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
const trackingStops = [
|
||||||
|
{ name: "Mojo Dry Port", time: "Departed 06:10", state: "done" },
|
||||||
|
{ name: "Adama", time: "08:05", state: "done" },
|
||||||
|
{ name: "Awash", time: "Now · 64 km/h", state: "current" },
|
||||||
|
{ name: "Dire Dawa", time: "ETA 14:30", state: "idle" },
|
||||||
|
{ name: "Dewele customs", time: "ETA 17:50", state: "idle" },
|
||||||
|
{ name: "Djibouti Port", time: "ETA 22:10", state: "idle" },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
function TrackingMock() {
|
||||||
|
return (
|
||||||
|
<div className="edr-float overflow-hidden rounded-3xl border border-edr-border bg-white shadow-[0_24px_60px_-24px_rgba(12,26,43,0.35)]">
|
||||||
|
<div className="flex items-center justify-between gap-4 border-b border-edr-border px-6 py-4">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="truncate text-base font-bold">CN-2026-08417</p>
|
||||||
|
<p className="truncate text-xs text-edr-muted">
|
||||||
|
Mojo Dry Port → Djibouti Port · 40ft × 12
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span className="inline-flex shrink-0 items-center gap-1.5 rounded-full bg-emerald-50 px-2.5 py-1 text-xs font-semibold text-emerald-700">
|
||||||
|
<span className="edr-pulse-dot edr-pulse-dot--green" />
|
||||||
|
In transit
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ol className="px-6 py-5">
|
||||||
|
{trackingStops.map((stop, index) => {
|
||||||
|
const isLast = index === trackingStops.length - 1;
|
||||||
|
const done = stop.state === "done";
|
||||||
|
const current = stop.state === "current";
|
||||||
|
const nextDone = !isLast && trackingStops[index + 1].state === "done";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li key={stop.name} className="flex items-stretch gap-3.5">
|
||||||
|
<span className="flex w-4 flex-col items-center">
|
||||||
|
<span
|
||||||
|
className={`size-3 shrink-0 rounded-full border-2 ${
|
||||||
|
done
|
||||||
|
? "border-edr-primary bg-edr-primary"
|
||||||
|
: current
|
||||||
|
? "edr-ping border-edr-amber bg-edr-amber text-edr-amber"
|
||||||
|
: "border-slate-300 bg-white"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
{!isLast ? (
|
||||||
|
<span
|
||||||
|
className={`my-1 w-0.5 flex-1 ${
|
||||||
|
done && nextDone ? "bg-edr-primary" : "bg-edr-border"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
className={`flex flex-1 items-center justify-between gap-3 ${
|
||||||
|
isLast ? "" : "pb-4"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`text-sm ${
|
||||||
|
current
|
||||||
|
? "font-semibold"
|
||||||
|
: done
|
||||||
|
? "font-medium"
|
||||||
|
: "font-medium text-edr-muted"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{stop.name}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-edr-muted">{stop.time}</span>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-3 bg-edr-surface px-6 pb-5 pt-4">
|
||||||
|
{[
|
||||||
|
["Train", "TS-14 · 2 locos"],
|
||||||
|
["Wagons", "12 / 12 loaded"],
|
||||||
|
["Next gate", "Dewele customs"],
|
||||||
|
].map(([key, value]) => (
|
||||||
|
<div key={key}>
|
||||||
|
<p className="text-[11px] font-medium text-edr-muted">{key}</p>
|
||||||
|
<p className="mt-0.5 text-[13px] font-semibold">{value}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Section */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
const portals = [
|
||||||
|
{
|
||||||
|
key: "passenger",
|
||||||
|
title: "Passenger",
|
||||||
|
lede: "Search a train, choose your class and seat, and pay with Telebirr or CBE Birr. Your e-ticket comes to your phone.",
|
||||||
|
points: [
|
||||||
|
{ icon: Armchair, text: "Economy, standard and VIP coaches" },
|
||||||
|
{ icon: Smartphone, text: "Pay with Telebirr, CBE Birr or card" },
|
||||||
|
{ icon: Ticket, text: "QR e-ticket by SMS, shown at the gate" },
|
||||||
|
],
|
||||||
|
href: PASSENGER_URL,
|
||||||
|
cta: "Open Passenger",
|
||||||
|
Mock: SearchMock,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "freight",
|
||||||
|
title: "Freight",
|
||||||
|
lede: "Book wagons and containers, watch every train move along the line, and keep customs documents and invoices in one place.",
|
||||||
|
points: [
|
||||||
|
{ icon: Radar, text: "Live GPS position of every wagon" },
|
||||||
|
{ icon: FileCheck, text: "Interchange and customs documents, signed online" },
|
||||||
|
{ icon: Receipt, text: "Invoices settled with Telebirr, CBE Birr or eBirr" },
|
||||||
|
],
|
||||||
|
href: FREIGHT_URL,
|
||||||
|
cta: "Open Freight",
|
||||||
|
Mock: TrackingMock,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export function Portals() {
|
||||||
|
return (
|
||||||
|
<section id="portals" className="scroll-mt-24 bg-white">
|
||||||
|
<div className="mx-auto max-w-6xl px-6">
|
||||||
|
{portals.map(({ key, title, lede, points, href, cta, Mock }, index) => {
|
||||||
|
const flip = index % 2 === 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Reveal
|
||||||
|
key={key}
|
||||||
|
className={`grid items-center gap-12 py-20 lg:grid-cols-2 lg:gap-20 lg:py-28 ${
|
||||||
|
index > 0 ? "border-t border-edr-border" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className={flip ? "lg:order-2" : ""}>
|
||||||
|
<h2 className="text-3xl font-extrabold leading-[1.05] tracking-[-0.02em] sm:text-4xl">
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
<p className="mt-4 max-w-md text-lg leading-relaxed text-edr-muted">
|
||||||
|
{lede}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul className="mt-8 space-y-4">
|
||||||
|
{points.map(({ icon: Icon, text }) => (
|
||||||
|
<li key={text} className="flex items-center gap-3.5 text-[15px]">
|
||||||
|
<span className="flex size-9 shrink-0 items-center justify-center rounded-xl bg-emerald-50 text-edr-primary">
|
||||||
|
<Icon className="size-4" />
|
||||||
|
</span>
|
||||||
|
{text}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href={href}
|
||||||
|
className="mt-10 inline-flex items-center gap-2 rounded-xl bg-edr-primary px-6 py-3.5 font-semibold text-white transition hover:bg-edr-primary-dark focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-edr-primary focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
{cta}
|
||||||
|
<ArrowRight className="size-4" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={`mx-auto w-full max-w-md ${flip ? "lg:order-1" : ""}`}>
|
||||||
|
<Mock />
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
37
apps/edr-landing/src/components/stats.tsx
Normal file
37
apps/edr-landing/src/components/stats.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { CountUp, Reveal } from "./motion";
|
||||||
|
|
||||||
|
const stats = [
|
||||||
|
{ value: 752, suffix: " km", label: "Addis Ababa to Djibouti, electrified standard gauge" },
|
||||||
|
{ value: 16, prefix: "~", suffix: " h", label: "Mojo Dry Port to Djibouti Port transit" },
|
||||||
|
{ value: 2, label: "Countries connected by one railway" },
|
||||||
|
{ text: "24/7", label: "Booking and tracking online" },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export function Stats() {
|
||||||
|
return (
|
||||||
|
<section className="border-t border-edr-border bg-edr-surface py-16">
|
||||||
|
<div className="mx-auto max-w-6xl px-6">
|
||||||
|
<Reveal>
|
||||||
|
<dl className="grid grid-cols-2 gap-x-6 gap-y-10 lg:grid-cols-4">
|
||||||
|
{stats.map((stat) => (
|
||||||
|
<div key={stat.label}>
|
||||||
|
<dt className="text-3xl font-extrabold tracking-tight text-edr-ink md:text-4xl">
|
||||||
|
{"text" in stat ? (
|
||||||
|
stat.text
|
||||||
|
) : (
|
||||||
|
<CountUp
|
||||||
|
value={stat.value}
|
||||||
|
prefix={"prefix" in stat ? stat.prefix : ""}
|
||||||
|
suffix={"suffix" in stat ? stat.suffix : ""}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</dt>
|
||||||
|
<dd className="mt-2 text-sm leading-relaxed text-edr-muted">{stat.label}</dd>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
|
</Reveal>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -11,6 +11,10 @@ export const PASSENGER_URL = origin(
|
|||||||
|
|
||||||
// The freight app serves its own marketing landing at `/`, so link past it:
|
// The freight app serves its own marketing landing at `/`, so link past it:
|
||||||
// `/portal` is the app itself, and redirects to /login when signed out.
|
// `/portal` is the app itself, and redirects to /login when signed out.
|
||||||
export const FREIGHT_URL = `${origin(
|
const FREIGHT_ORIGIN = origin(
|
||||||
process.env.NEXT_PUBLIC_FREIGHT_URL ?? "https://freight.edrsc.com",
|
process.env.NEXT_PUBLIC_FREIGHT_URL ?? "https://freight.edrsc.com",
|
||||||
)}/portal`;
|
);
|
||||||
|
export const FREIGHT_URL = `${FREIGHT_ORIGIN}/portal`;
|
||||||
|
|
||||||
|
// Public notices and tariffs, published from the freight app.
|
||||||
|
export const PUBLICATIONS_URL = `${FREIGHT_ORIGIN}/publications`;
|
||||||
|
|||||||
@@ -4,19 +4,31 @@ export default {
|
|||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
// Palette lifted from the freight landing page so the two public
|
// One palette for the whole railway: the green is sampled from the
|
||||||
// surfaces read as one railway.
|
// official logo, the dark inks and light surfaces come from the
|
||||||
|
// freight portal so the two public surfaces read as one brand.
|
||||||
edr: {
|
edr: {
|
||||||
ink: "#0c1a2b",
|
ink: "#0c1a2b",
|
||||||
"ink-2": "#10202f",
|
"ink-2": "#10202f",
|
||||||
primary: "#0a8a5f",
|
primary: "#008550",
|
||||||
"primary-dark": "#087049",
|
"primary-dark": "#006b41",
|
||||||
light: "#34d399",
|
light: "#34d399",
|
||||||
amber: "#f2a516",
|
amber: "#f2a516",
|
||||||
|
surface: "#f7fafc",
|
||||||
|
border: "#e6ecf2",
|
||||||
|
muted: "#6b7c8e",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ["Inter", "ui-sans-serif", "system-ui", "sans-serif"],
|
sans: [
|
||||||
|
'"Plus Jakarta Sans"',
|
||||||
|
"ui-sans-serif",
|
||||||
|
"system-ui",
|
||||||
|
"-apple-system",
|
||||||
|
"Segoe UI",
|
||||||
|
"Roboto",
|
||||||
|
"sans-serif",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user