From bac9c24a473b704bf0fca5884b812854cd20c9c4 Mon Sep 17 00:00:00 2001 From: ghost2023 Date: Fri, 4 Sep 2026 15:41:11 +0300 Subject: [PATCH] style: publication --- .../portal/src/components/PublicNavbar.tsx | 101 ++++ .../src/pages/EDRFreightLandingPage.tsx | 79 +-- .../pages/publications/PublicationsPage.tsx | 449 ++++++++++++++---- .../portal/src/pages/support/DocShell.tsx | 43 +- 4 files changed, 485 insertions(+), 187 deletions(-) create mode 100644 apps/edr-freight-web/portal/src/components/PublicNavbar.tsx diff --git a/apps/edr-freight-web/portal/src/components/PublicNavbar.tsx b/apps/edr-freight-web/portal/src/components/PublicNavbar.tsx new file mode 100644 index 000000000..7ee7fa422 --- /dev/null +++ b/apps/edr-freight-web/portal/src/components/PublicNavbar.tsx @@ -0,0 +1,101 @@ +import { ArrowRight, Menu, TrainFront } from "lucide-react"; +import { Link } from "react-router-dom"; + +/** + * Top-level navigation for the public marketing pages. Entries beginning with + * `#` scroll within the landing page; entries beginning with `/` are real + * routes and need router navigation. + */ +export const navLinks = [ + { label: "Features", href: "#features" }, + { label: "Live ops", href: "#showcase" }, + { label: "Corridors", href: "#corridors" }, + { label: "How it works", href: "#how" }, + { label: "Publications", href: "/publications" }, + { label: "Contact", href: "#contact" }, +]; + +/** + * The dark navbar shared by every public page that is not behind the app + * shell — the landing page and /publications. Extracted from the landing page + * so the two cannot drift: a link added here shows up on both. + * + * The anchor entries only resolve on the landing page itself, so away from it + * they are rendered as links back to the homepage's section instead of as + * same-page anchors that would go nowhere. + */ +export function PublicNavbar({ onLanding = false }: { onLanding?: boolean }) { + return ( +
+
+ + + + + + + EDR Freight + + Rail Logistics Platform + + + + + + +
+ + Log in + + + + Get started + + + + +
+
+
+ ); +} + +export default PublicNavbar; diff --git a/apps/edr-freight-web/portal/src/pages/EDRFreightLandingPage.tsx b/apps/edr-freight-web/portal/src/pages/EDRFreightLandingPage.tsx index ef1daac18..94e44f0d2 100644 --- a/apps/edr-freight-web/portal/src/pages/EDRFreightLandingPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/EDRFreightLandingPage.tsx @@ -14,7 +14,6 @@ import { Mail, Map as MapIcon, MapPin, - Menu, Package, PackageSearch, Phone, @@ -30,6 +29,8 @@ import { Truck, } from "lucide-react"; +import { PublicNavbar } from "@/components/PublicNavbar"; + /* ------------------------------------------------------------------ */ /* Motion helpers */ /* ------------------------------------------------------------------ */ @@ -137,15 +138,6 @@ function CountUp({ /* Content */ /* ------------------------------------------------------------------ */ -const navLinks = [ - { label: "Features", href: "#features" }, - { label: "Live ops", href: "#showcase" }, - { label: "Corridors", href: "#corridors" }, - { label: "How it works", href: "#how" }, - { label: "Publications", href: "/publications" }, - { label: "Contact", href: "#contact" }, -]; - const heroTrust = [ "Telebirr & CBE Birr payments", "Fayda ID verified", @@ -518,72 +510,7 @@ export default function EDRFreightLandingPage() {
- {/* Navbar */} -
-
- - - - - - - EDR Freight - - Rail Logistics Platform - - - - - - -
- - Log in - - - - Get started - - - - -
-
-
+ {/* Hero */}
diff --git a/apps/edr-freight-web/portal/src/pages/publications/PublicationsPage.tsx b/apps/edr-freight-web/portal/src/pages/publications/PublicationsPage.tsx index 99c00d478..210a0ca20 100644 --- a/apps/edr-freight-web/portal/src/pages/publications/PublicationsPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/publications/PublicationsPage.tsx @@ -1,38 +1,55 @@ import type { PublicationSummary } from "@edr/types"; import { useFileViewer } from "@edr/ui-common"; -import { FileText, Presentation, Download, X } from "lucide-react"; -import { useState } from "react"; +import { + Download, + FileText, + Library, + Presentation, + Search, + X, +} from "lucide-react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; +import { PublicNavbar } from "@/components/PublicNavbar"; import { publicationFileUrl } from "@/constants/apiConfig"; import { usePublications } from "@/hooks/usePublications"; import { Markdown } from "../support/Markdown"; -import { DocShell } from "../support/DocShell"; +import { DocFooter } from "../support/DocShell"; const MARKDOWN_MIMES = new Set(["text/markdown", "text/x-markdown"]); -function isMarkdown(pub: PublicationSummary): boolean { - return MARKDOWN_MIMES.has(pub.fileMimeType) || pub.fileName.toLowerCase().endsWith(".md"); +type PublicationKind = "pdf" | "markdown" | "slides" | "other"; + +function kindOf(pub: PublicationSummary): PublicationKind { + if (MARKDOWN_MIMES.has(pub.fileMimeType) || pub.fileName.toLowerCase().endsWith(".md")) { + return "markdown"; + } + if (pub.fileMimeType === "application/pdf") return "pdf"; + if ( + pub.fileMimeType.includes("powerpoint") || + pub.fileMimeType.includes("presentationml") + ) { + return "slides"; + } + return "other"; } -function fileKindIcon(pub: PublicationSummary) { - if (isMarkdown(pub)) return FileText; - if (pub.fileMimeType.includes("powerpoint") || pub.fileMimeType.includes("presentationml")) { - return Presentation; - } - return FileText; -} - -function fileKindLabel(pub: PublicationSummary): string { - if (pub.fileMimeType === "application/pdf") return "PDF"; - if (isMarkdown(pub)) return "Markdown"; - if (pub.fileMimeType.includes("powerpoint") || pub.fileMimeType.includes("presentationml")) { - return "PowerPoint"; - } - return "Document"; -} +const KIND_META: Record< + PublicationKind, + { label: string; icon: typeof FileText; accent: string } +> = { + pdf: { label: "PDF", icon: FileText, accent: "from-rose-500/15 to-rose-500/5" }, + markdown: { label: "Markdown", icon: FileText, accent: "from-sky-500/15 to-sky-500/5" }, + slides: { + label: "PowerPoint", + icon: Presentation, + accent: "from-amber-500/15 to-amber-500/5", + }, + other: { label: "Document", icon: FileText, accent: "from-slate-500/15 to-slate-500/5" }, +}; function formatSize(bytes: number): string { if (bytes < 1024) return `${bytes} B`; @@ -40,31 +57,235 @@ function formatSize(bytes: number): string { return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } +function formatDate(value: string | null): string | null { + if (!value) return null; + return new Date(value).toLocaleDateString(undefined, { + month: "short", + year: "numeric", + }); +} + +/** True once the element has scrolled into view — and stays true afterwards. */ +function useInView() { + const ref = useRef(null); + const [seen, setSeen] = useState(false); + + useEffect(() => { + const node = ref.current; + if (!node || seen) return; + if (typeof IntersectionObserver === "undefined") { + setSeen(true); + return; + } + + const observer = new IntersectionObserver( + (entries) => { + if (entries.some((entry) => entry.isIntersecting)) { + setSeen(true); + observer.disconnect(); + } + }, + { rootMargin: "200px" }, + ); + observer.observe(node); + return () => observer.disconnect(); + }, [seen]); + + return { ref, seen }; +} + +/** + * The card's preview panel. + * + * A PDF renders its own first page in a muted, non-interactive iframe, and a + * Markdown file shows the opening lines of its actual text. Both only load + * once the card is near the viewport — a grid of publications would otherwise + * pull every file on first paint. + * + * Slides get a drawn cover rather than a real thumbnail: the Office Online + * viewer is the only thing that can rasterise a .pptx here, and embedding it + * per card is far too heavy for a listing. Clicking through still opens the + * real thing. + */ +function PublicationPreview({ pub }: { pub: PublicationSummary }) { + const kind = kindOf(pub); + const { ref, seen } = useInView(); + const [excerpt, setExcerpt] = useState(null); + + useEffect(() => { + if (kind !== "markdown" || !seen || excerpt !== null) return; + let cancelled = false; + + void fetch(publicationFileUrl(pub.id)) + .then((response) => response.text()) + .then((text) => { + if (!cancelled) setExcerpt(text.slice(0, 600)); + }) + .catch(() => { + // A failed preview is cosmetic — the card still opens and downloads. + if (!cancelled) setExcerpt(""); + }); + + return () => { + cancelled = true; + }; + }, [kind, seen, excerpt, pub.id]); + + const { icon: Icon, accent } = KIND_META[kind]; + + return ( +
+ {kind === "pdf" && seen ? ( +