From d261148b35c95e70699b7eb8b6757cda6e4f72ed Mon Sep 17 00:00:00 2001 From: Kirubel-Kibru-Yaltopia Date: Thu, 21 May 2026 20:35:59 +0300 Subject: [PATCH] Enhance footer and hero with brand backgrounds --- .git-push-log.sh | 26 +++ app/globals.css | 70 +++++- components/brand/FooterTopoPattern.tsx | 47 ++-- components/brand/GeometricMessBackground.tsx | 108 +++++++++ .../brand/LowPolyGeometricBackground.tsx | 1 + .../brand/RoundedRockVoronoiBackground.tsx | 95 ++++++++ .../brand/StainedGlassGradientBackground.tsx | 1 + .../brand/WavyContourLinesBackground.tsx | 78 +++++++ components/brand/WavyTessellationMesh.tsx | 94 ++++++++ components/home/Hero.tsx | 9 +- components/home/LastYearWinnerMark.tsx | 61 +++++ components/home/LastYearWinnersScroll.tsx | 62 +++++ components/home/PartnerMarquee.tsx | 6 +- components/home/StatsGrid.tsx | 1 - components/layout/FooterNewsletter.tsx | 4 +- components/layout/FooterSocialLinks.tsx | 68 ++++++ components/layout/FooterSurface.tsx | 21 ++ components/layout/PageRiftHeader.tsx | 2 + components/layout/Section.tsx | 20 ++ components/layout/SiteFooter.tsx | 85 ++++--- components/partners/PartnershipCtaBand.tsx | 2 + content/last-year-winners.ts | 47 ++++ content/site.ts | 6 + lib/footer-curve-edge.ts | 61 +++++ lib/voronoi-mesh.ts | 213 ++++++++++++++++++ lib/wavy-contour-lines.ts | 38 ++++ public/branding/winners/.gitkeep | 0 public/branding/winners/globedock-academy.png | Bin 0 -> 14488 bytes public/branding/winners/lifeline-addis.png | Bin 0 -> 1541 bytes public/branding/winners/muyalogy.png | Bin 0 -> 814 bytes scripts/download-assets.mjs | 12 + 31 files changed, 1174 insertions(+), 64 deletions(-) create mode 100644 .git-push-log.sh create mode 100644 components/brand/GeometricMessBackground.tsx create mode 100644 components/brand/LowPolyGeometricBackground.tsx create mode 100644 components/brand/RoundedRockVoronoiBackground.tsx create mode 100644 components/brand/StainedGlassGradientBackground.tsx create mode 100644 components/brand/WavyContourLinesBackground.tsx create mode 100644 components/brand/WavyTessellationMesh.tsx create mode 100644 components/home/LastYearWinnerMark.tsx create mode 100644 components/home/LastYearWinnersScroll.tsx create mode 100644 components/layout/FooterSocialLinks.tsx create mode 100644 components/layout/FooterSurface.tsx create mode 100644 content/last-year-winners.ts create mode 100644 lib/footer-curve-edge.ts create mode 100644 lib/voronoi-mesh.ts create mode 100644 lib/wavy-contour-lines.ts create mode 100644 public/branding/winners/.gitkeep create mode 100644 public/branding/winners/globedock-academy.png create mode 100644 public/branding/winners/lifeline-addis.png create mode 100644 public/branding/winners/muyalogy.png diff --git a/.git-push-log.sh b/.git-push-log.sh new file mode 100644 index 0000000..7bc1501 --- /dev/null +++ b/.git-push-log.sh @@ -0,0 +1,26 @@ +#!/bin/bash +set -e +cd /root/Yaltopia-Project/GRV-Summit-Site +LOG=/root/Yaltopia-Project/GRV-Summit-Site/.deploy-log.txt +{ + echo "=== STATUS ===" + git status -sb + echo "=== ADD ===" + git add -A + git reset HEAD .env 2>/dev/null || true + echo "=== COMMIT ===" + git commit -m "$(cat <<'EOF' +Enhance footer and hero with brand geometric backgrounds. + +Add low-poly footer facets with curved midline, hero last-year winners +scroll, footer surfaces and social links, and supporting brand components. +EOF +)" || echo "COMMIT_SKIPPED: nothing to commit or failed" + echo "=== PUSH ===" + git push origin main + echo "=== LOG ===" + git log -1 --oneline + echo "=== GH RUNS ===" + gh run list --limit 3 2>&1 || echo "gh not available" +} > "$LOG" 2>&1 +echo DONE >> "$LOG" diff --git a/app/globals.css b/app/globals.css index 7120d15..98c0b37 100644 --- a/app/globals.css +++ b/app/globals.css @@ -103,10 +103,56 @@ background-color: #1a5c38; color: #fafafa; } + + @keyframes geometric-mess-drift { + 0% { + transform: translate(0, 0) scale(1); + } + 50% { + transform: translate(0.4%, -0.3%) scale(1.008); + } + 100% { + transform: translate(-0.3%, 0.2%) scale(1.004); + } + } + .geometric-mess-layer { + animation: geometric-mess-drift 56s ease-in-out infinite alternate; + } + .grain { background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.04'/%3E%3C/svg%3E"); } + /* White-section contour lines — slow parallax drift */ + @keyframes wavy-contour-drift-a { + 0% { + transform: translate(0, 0) scale(1); + } + 50% { + transform: translate(2.5%, -1.2%) scale(1.03); + } + 100% { + transform: translate(-1.5%, 0.8%) scale(1.01); + } + } + @keyframes wavy-contour-drift-b { + 0% { + transform: translate(0, 0) scale(1.02); + } + 50% { + transform: translate(-2%, 1%) scale(1.04); + } + 100% { + transform: translate(1.8%, -0.6%) scale(1.01); + } + } + .wavy-contour-layer--primary { + animation: wavy-contour-drift-a 48s ease-in-out infinite alternate; + } + .wavy-contour-layer--secondary { + animation: wavy-contour-drift-b 62s ease-in-out infinite alternate-reverse; + } + /* Full-page atmosphere — brand wash drift (behind grain + lines) */ @keyframes backdrop-bloom-a { 0%, @@ -157,6 +203,9 @@ .marquee { animation: marquee 40s linear infinite; } + .marquee-winners { + animation: marquee 55s linear infinite; + } @keyframes marquee { from { transform: translateX(0); @@ -166,7 +215,8 @@ } } @media (prefers-reduced-motion: reduce) { - .marquee { + .marquee, + .marquee-winners { animation: none; } .backdrop-bloom-a, @@ -523,11 +573,22 @@ z-index: 10; } + /* Landing hero — topography only, no contour-line overlay */ + [data-section-hero] .wavy-contour-lines { + display: none; + } + /* Readable brand-green copy on all white sections */ .section-white { color: #0d3d26; } + .section-white .topo-content-layer, + .section-white .topo-card-surface, + .section-white [data-slot="card"] { + isolation: isolate; + } + .section-white .topo-content-layer, .section-white .topo-content-readable, .section-white .topo-content-layer :is(h1, h2, h3, h4, h5, h6, p, li, label, summary), @@ -1042,6 +1103,13 @@ } @media (prefers-reduced-motion: reduce) { + .wavy-contour-layer--primary, + .wavy-contour-layer--secondary, + .wavy-contour-layer, + .geometric-mess-layer { + animation: none !important; + } + .rift-hero-intro .rift-floor-glow, .rift-hero-intro .rift-channel-open, .rift-hero-intro .rift-contour-path, diff --git a/components/brand/FooterTopoPattern.tsx b/components/brand/FooterTopoPattern.tsx index 955e02c..57a52fa 100644 --- a/components/brand/FooterTopoPattern.tsx +++ b/components/brand/FooterTopoPattern.tsx @@ -1,38 +1,45 @@ -import { MAIN_WHITE_PATTERN_SRC } from "@/content/topo-patterns"; +import { buildWavyTessellationMesh } from "@/components/brand/WavyTessellationMesh"; import { cn } from "@/lib/utils"; type Props = { className?: string; }; -/** White topography pattern anchored to the bottom of the green footer only. */ +/** Wavy interlocking tessellation across the green footer (GRV palette). */ +const FOOTER_MESH = buildWavyTessellationMesh(20, 38, 100, 100, { + diagonalSkew: 0.85, + amplitude: 0.68, +}); + export function FooterTopoPattern({ className }: Props) { return (
-
- {/* eslint-disable-next-line @next/next/no-img-element */} - -
-
+ {FOOTER_MESH.map((cell, i) => ( + + ))} + +
); } diff --git a/components/brand/GeometricMessBackground.tsx b/components/brand/GeometricMessBackground.tsx new file mode 100644 index 0000000..514c29b --- /dev/null +++ b/components/brand/GeometricMessBackground.tsx @@ -0,0 +1,108 @@ +import { buildVoronoiMesh, type VoronoiCell } from "@/lib/voronoi-mesh"; +import { + buildFooterMidlineCurve, + midlineCurveToClipPath, + midlineCurveToClipPathPercent, +} from "@/lib/footer-curve-edge"; +import { cn } from "@/lib/utils"; + +const VIEW_W = 160; +const VIEW_H = 100; +const MIDLINE_Y = VIEW_H * 0.5; +const CURVE_SEED = 0xa3f21c; +const MIDLINE_CURVE = buildFooterMidlineCurve(VIEW_W, 20, CURVE_SEED, MIDLINE_Y, 11); +const FACET_CLIP_PATH = midlineCurveToClipPath(MIDLINE_CURVE, VIEW_W, VIEW_H); +const FACET_CLIP_CSS = midlineCurveToClipPathPercent(MIDLINE_CURVE, VIEW_W, VIEW_H); + +const FACET_TONES = [ + "#f5fbf7", + "#ecf8f0", + "#dff3e6", + "#ccebd8", + "#b5e4c8", + "#9ed9b4", + "#84d09f", + "#6bc98a", + "#52c878", + "#4aad6e", +] as const; + +const LOW_POLY_MESH = buildVoronoiMesh(64, VIEW_W, VIEW_H, CURVE_SEED, { + siteGenerator: "poisson", + shape: "sharp", + minDist: 6.5, +}); + +function hexToRgb(hex: string) { + const n = parseInt(hex.slice(1), 16); + return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 }; +} + +function mixHex(a: string, b: string, t: number) { + const c1 = hexToRgb(a); + const c2 = hexToRgb(b); + const u = (v: number) => Math.round(v).toString(16).padStart(2, "0"); + return `#${u(c1.r + (c2.r - c1.r) * t)}${u(c1.g + (c2.g - c1.g) * t)}${u(c1.b + (c2.b - c1.b) * t)}`; +} + +function facetFill(cell: VoronoiCell, index: number) { + const cx = cell.points.reduce((s, p) => s + p[0], 0) / cell.points.length; + const cy = cell.points.reduce((s, p) => s + p[1], 0) / cell.points.length; + const xNorm = cx / VIEW_W; + const yNorm = Math.max(0, (cy - MIDLINE_Y) / (VIEW_H - MIDLINE_Y)); + const tone = FACET_TONES[(index * 9 + Math.floor(cx * 0.55)) % FACET_TONES.length]; + const wash = mixHex("#ffffff", "#e8f5ec", 0.35); + const mix = 0.42 + (1 - xNorm) * 0.2 + yNorm * 0.12; + return mixHex(wash, tone, mix); +} + +const FACET_CELLS = LOW_POLY_MESH.map((cell, i) => ({ + d: cell.d, + fill: facetFill(cell, i), +})); + +type Props = { + className?: string; +}; + +export function GeometricMessBackground({ className }: Props) { + return ( +
+
+ + + + + + + + + {FACET_CELLS.map((cell, i) => ( + + ))} + + +
+ + {/* White wash above the curve only — keeps facet tops along the midline visible */} +
+
+ ); +} diff --git a/components/brand/LowPolyGeometricBackground.tsx b/components/brand/LowPolyGeometricBackground.tsx new file mode 100644 index 0000000..f7bd276 --- /dev/null +++ b/components/brand/LowPolyGeometricBackground.tsx @@ -0,0 +1 @@ +export { RoundedRockVoronoiBackground as LowPolyGeometricBackground } from "@/components/brand/RoundedRockVoronoiBackground"; diff --git a/components/brand/RoundedRockVoronoiBackground.tsx b/components/brand/RoundedRockVoronoiBackground.tsx new file mode 100644 index 0000000..02832ff --- /dev/null +++ b/components/brand/RoundedRockVoronoiBackground.tsx @@ -0,0 +1,95 @@ +import type { CSSProperties } from "react"; +import { buildVoronoiMesh, type VoronoiCell } from "@/lib/voronoi-mesh"; +import { cn } from "@/lib/utils"; + +/** Wider mesh plane — pairs with slice scaling so pebbles are not horizontally stretched. */ +const MESH_W = 140; +const MESH_H = 100; + +const ROCK_TONES = ["#0d3d26", "#1a5c38", "#246b45", "#2f7a52", "#3d9a66", "#52b87a"] as const; +const GROUT = "rgba(8, 38, 22, 0.42)"; + +const ROCK_MESH = buildVoronoiMesh(42, MESH_W, MESH_H, 0x475256, { + siteGenerator: "poisson", +}); + +function hexToRgb(hex: string) { + const n = parseInt(hex.slice(1), 16); + return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 }; +} + +function mixHex(a: string, b: string, t: number) { + const c1 = hexToRgb(a); + const c2 = hexToRgb(b); + const u = (v: number) => Math.round(v).toString(16).padStart(2, "0"); + return `#${u(c1.r + (c2.r - c1.r) * t)}${u(c1.g + (c2.g - c1.g) * t)}${u(c1.b + (c2.b - c1.b) * t)}`; +} + +function rockFill(cell: VoronoiCell, index: number) { + const cx = cell.points.reduce((s, p) => s + p[0], 0) / cell.points.length; + const cy = cell.points.reduce((s, p) => s + p[1], 0) / cell.points.length; + const yNorm = cy / MESH_H; + const gradient = mixHex("#4aad6e", "#0d3d26", Math.min(1, yNorm * 0.85 + 0.08)); + const tone = ROCK_TONES[(index * 11 + Math.floor(cx * 0.55)) % ROCK_TONES.length]; + return mixHex(gradient, tone, 0.54); +} + +type Props = { + className?: string; + style?: CSSProperties; + /** Slight vertical wash (two-days section). */ + gradient?: boolean; +}; + +export function RoundedRockVoronoiBackground({ + className, + style, + gradient = false, +}: Props) { + const cells = ROCK_MESH.map((cell, i) => ({ + d: cell.d, + fill: rockFill(cell, i), + })); + + return ( +
+ + {cells.map((cell, i) => ( + + ))} + +
+ {gradient && ( +
+ )} +
+
+ ); +} diff --git a/components/brand/StainedGlassGradientBackground.tsx b/components/brand/StainedGlassGradientBackground.tsx new file mode 100644 index 0000000..cd22a8f --- /dev/null +++ b/components/brand/StainedGlassGradientBackground.tsx @@ -0,0 +1 @@ +export { RoundedRockVoronoiBackground as StainedGlassGradientBackground } from "@/components/brand/RoundedRockVoronoiBackground"; diff --git a/components/brand/WavyContourLinesBackground.tsx b/components/brand/WavyContourLinesBackground.tsx new file mode 100644 index 0000000..769b94b --- /dev/null +++ b/components/brand/WavyContourLinesBackground.tsx @@ -0,0 +1,78 @@ +import { buildWavyContourPaths } from "@/lib/wavy-contour-lines"; +import { cn } from "@/lib/utils"; + +const VIEW_W = 160; +const VIEW_H = 100; + +const CONTOUR_PRIMARY = buildWavyContourPaths(28, VIEW_W, VIEW_H, 0x8a3c21); +const CONTOUR_SECONDARY = buildWavyContourPaths(22, VIEW_W, VIEW_H, 0x5c2e18); + +type Props = { + className?: string; +}; + +const frameClass = + "absolute left-1/2 top-1/2 h-[118%] w-[118%] min-h-full min-w-full -translate-x-1/2 -translate-y-1/2"; + +/** + * Topographic waves on white sections — visible hairlines with slow drift. + * Not used on the home hero (keeps existing topography art). + */ +export function WavyContourLinesBackground({ className }: Props) { + return ( +
+
+
+ + {CONTOUR_PRIMARY.map((d, i) => ( + + ))} + +
+
+
+
+ + {CONTOUR_SECONDARY.map((d, i) => ( + + ))} + +
+
+
+ ); +} diff --git a/components/brand/WavyTessellationMesh.tsx b/components/brand/WavyTessellationMesh.tsx new file mode 100644 index 0000000..f9dd3dc --- /dev/null +++ b/components/brand/WavyTessellationMesh.tsx @@ -0,0 +1,94 @@ +/** GRV greens for wavy interlocking footer tessellation. */ +export const WAVY_TESS_PALETTE = [ + "#0d3d26", + "#1a5c38", + "#256b45", + "#2d7a52", + "#3d9a66", + "#5cb87a", +] as const; + +export type WavyTessCell = { d: string; fill: string }; + +function hashCell(col: number, row: number, cols: number) { + return (col * 17 + row * 31 + ((col + row) % cols) * 11) % WAVY_TESS_PALETTE.length; +} + +type MeshOptions = { + /** Tilts wave flow diagonally (higher = more angled, less vertical). */ + diagonalSkew?: number; + amplitude?: number; +}; + +/** + * Diagonal wavy columns + horizontal ripples — interlocking mosaic (not flat vertical stripes). + */ +export function buildWavyTessellationMesh( + cols: number, + rows: number, + width = 100, + height = 100, + options: MeshOptions = {} +): WavyTessCell[] { + const skew = options.diagonalSkew ?? 0.72; + const amp = (width / cols) * (options.amplitude ?? 0.62); + const ampRow = amp * 0.48; + const loops = 3.6; + + const boundaryX = (col: number, yNorm: number) => { + const base = (col / cols) * width; + const phase = col * 1.13 + 0.42; + const u = yNorm + (col / cols) * skew; + const v = yNorm - (col / cols) * skew * 0.45; + const wave = + Math.sin(u * Math.PI * 2 * loops + phase) + + 0.52 * Math.sin(v * Math.PI * 2 * (loops + 0.85) - col * 0.4) + + 0.28 * Math.cos((u + v) * Math.PI * loops * 0.55 + phase * 0.6); + return base + amp * wave * 0.42; + }; + + const boundaryY = (row: number, xNorm: number) => { + const base = (row / rows) * height; + const phase = row * 0.91 + 0.18; + const u = xNorm + (row / rows) * skew; + const wave = + Math.sin(u * Math.PI * 2 * (loops * 0.9) + phase) + + 0.4 * Math.cos(u * Math.PI * 2 * (loops * 1.15) - row * 0.55); + return base + ampRow * wave * 0.38; + }; + + const cells: WavyTessCell[] = []; + + const corner = (col: number, row: number) => { + const yn = row / rows; + const x = boundaryX(col, yn); + const y = boundaryY(row, x / width); + return { x, y }; + }; + + for (let r = 0; r < rows; r++) { + for (let c = 0; c < cols; c++) { + const bl = corner(c, r); + const br = corner(c + 1, r); + const tl = corner(c, r + 1); + const tr = corner(c + 1, r + 1); + const ymL = (bl.y + tl.y) / 2; + const ymR = (br.y + tr.y) / 2; + + const d = [ + `M ${bl.x.toFixed(2)} ${bl.y.toFixed(2)}`, + `Q ${bl.x.toFixed(2)} ${ymL.toFixed(2)} ${tl.x.toFixed(2)} ${tl.y.toFixed(2)}`, + `L ${tr.x.toFixed(2)} ${tr.y.toFixed(2)}`, + `Q ${br.x.toFixed(2)} ${ymR.toFixed(2)} ${br.x.toFixed(2)} ${br.y.toFixed(2)}`, + "Z", + ].join(" "); + + const layer = (r + Math.floor(c / 2)) % 3; + const fill = WAVY_TESS_PALETTE[hashCell(c + layer, r, cols)]; + + cells.push({ d, fill }); + } + } + + return cells; +} diff --git a/components/home/Hero.tsx b/components/home/Hero.tsx index eafb3e8..e9227cf 100644 --- a/components/home/Hero.tsx +++ b/components/home/Hero.tsx @@ -5,6 +5,7 @@ import Link from "next/link"; import { ArrowRight } from "lucide-react"; import { site } from "@/content/site"; import { HeroGrantLine } from "@/components/home/HeroGrantLine"; +import { LastYearWinnersScroll } from "@/components/home/LastYearWinnersScroll"; import { TopoCurvyExtend } from "@/components/brand/TopoCurvyExtend"; import { HeroTopographyBackground } from "@/components/home/HeroTopographyBackground"; import { HeroRiftParticles } from "@/components/home/HeroRiftParticles"; @@ -60,6 +61,8 @@ export function Hero() { return (
-
- +
+

{site.dates.label} · {site.venue.address} @@ -128,6 +131,8 @@ export function Hero() {

+ +
diff --git a/components/home/LastYearWinnerMark.tsx b/components/home/LastYearWinnerMark.tsx new file mode 100644 index 0000000..c034f08 --- /dev/null +++ b/components/home/LastYearWinnerMark.tsx @@ -0,0 +1,61 @@ +"use client"; + +import { useState } from "react"; +import Image from "next/image"; +import type { LastYearWinner } from "@/content/last-year-winners"; +import { cn } from "@/lib/utils"; + +const GRV_LOGO = "/branding/logo-icon.png"; + +type Props = { + company: LastYearWinner; + className?: string; +}; + +export function LastYearWinnerMark({ company, className }: Props) { + const [failed, setFailed] = useState(false); + const src = company.logoSrc ?? GRV_LOGO; + const showImage = !failed && src; + const logoOnly = !company.name; + + return ( +
+
+ {showImage ? ( + setFailed(true)} + /> + ) : company.initials ? ( + + {company.initials} + + ) : ( + + )} +
+ {company.name ? ( + + {company.name} + + ) : null} +
+ ); +} diff --git a/components/home/LastYearWinnersScroll.tsx b/components/home/LastYearWinnersScroll.tsx new file mode 100644 index 0000000..0e96743 --- /dev/null +++ b/components/home/LastYearWinnersScroll.tsx @@ -0,0 +1,62 @@ +import { lastYearWinners, lastYearWinnersCopy } from "@/content/last-year-winners"; +import { LastYearWinnerMark } from "@/components/home/LastYearWinnerMark"; +import { cn } from "@/lib/utils"; + +type Props = { + /** Green stats section vs white hero */ + variant?: "on-green" | "on-light"; + className?: string; +}; + +export function LastYearWinnersScroll({ variant = "on-green", className }: Props) { + const items = [...lastYearWinners, ...lastYearWinners]; + const onLight = variant === "on-light"; + + return ( +
+

+ {lastYearWinnersCopy.eyebrow} +

+

+ {lastYearWinnersCopy.headline} +

+
+
+
+
+ {items.map((company, i) => ( + + ))} +
+
+
+ ); +} diff --git a/components/home/PartnerMarquee.tsx b/components/home/PartnerMarquee.tsx index d1ceb60..0fb668c 100644 --- a/components/home/PartnerMarquee.tsx +++ b/components/home/PartnerMarquee.tsx @@ -1,14 +1,16 @@ import { PartnerLogoPlaceholder } from "@/components/brand/PartnerLogoPlaceholder"; +import { WavyContourLinesBackground } from "@/components/brand/WavyContourLinesBackground"; export function PartnerMarquee() { const slots = Array.from({ length: 8 }, (_, i) => i); return (
-

+ +

With the support of

-
+
{[...slots, ...slots].map((i) => ( diff --git a/components/home/StatsGrid.tsx b/components/home/StatsGrid.tsx index 5b77319..ff439d3 100644 --- a/components/home/StatsGrid.tsx +++ b/components/home/StatsGrid.tsx @@ -2,7 +2,6 @@ import { site } from "@/content/site"; import { Section } from "@/components/layout/Section"; import { TopoProseSurface } from "@/components/layout/TopoProseSurface"; import { CyclingGrantAmount } from "@/components/grants/CyclingGrantAmount"; - export function StatsGrid() { return (
diff --git a/components/layout/FooterNewsletter.tsx b/components/layout/FooterNewsletter.tsx index cba8ebf..7fac037 100644 --- a/components/layout/FooterNewsletter.tsx +++ b/components/layout/FooterNewsletter.tsx @@ -52,11 +52,11 @@ export function FooterNewsletter() { } return ( -
+
-

Stay up to date!

+

Stay up to date!

Get announcements about tickets, lineup, and the next Great Rift Valley Innovation Summit edition before anyone else. diff --git a/components/layout/FooterSocialLinks.tsx b/components/layout/FooterSocialLinks.tsx new file mode 100644 index 0000000..ee7bb89 --- /dev/null +++ b/components/layout/FooterSocialLinks.tsx @@ -0,0 +1,68 @@ +import Link from "next/link"; +import { site } from "@/content/site"; +import { cn } from "@/lib/utils"; + +type IconProps = { className?: string }; + +function TikTokIcon({ className }: IconProps) { + return ( + + + + ); +} + +function LinkedInIcon({ className }: IconProps) { + return ( + + + + ); +} + +function FacebookIcon({ className }: IconProps) { + return ( + + + + ); +} + +function InstagramIcon({ className }: IconProps) { + return ( + + + + ); +} + +const SOCIAL = [ + { id: "tiktok", label: "TikTok", href: site.social.tiktok, Icon: TikTokIcon }, + { id: "linkedin", label: "LinkedIn", href: site.social.linkedin, Icon: LinkedInIcon }, + { id: "facebook", label: "Facebook", href: site.social.facebook, Icon: FacebookIcon }, + { id: "instagram", label: "Instagram", href: site.social.instagram, Icon: InstagramIcon }, +] as const; + +type Props = { + className?: string; +}; + +export function FooterSocialLinks({ className }: Props) { + return ( +

+ Follow GRV Summit + {SOCIAL.map(({ id, label, href, Icon }) => ( + + + + ))} +
+ ); +} diff --git a/components/layout/FooterSurface.tsx b/components/layout/FooterSurface.tsx new file mode 100644 index 0000000..3d70e16 --- /dev/null +++ b/components/layout/FooterSurface.tsx @@ -0,0 +1,21 @@ +import type { ReactNode } from "react"; +import { cn } from "@/lib/utils"; + +type Props = { + children: ReactNode; + className?: string; +}; + +/** White panel so footer copy and icons stay readable over the geometric pattern. */ +export function FooterSurface({ children, className }: Props) { + return ( +
+ {children} +
+ ); +} diff --git a/components/layout/PageRiftHeader.tsx b/components/layout/PageRiftHeader.tsx index 2b86698..8bc612b 100644 --- a/components/layout/PageRiftHeader.tsx +++ b/components/layout/PageRiftHeader.tsx @@ -1,5 +1,6 @@ import type { ReactNode } from "react"; import { TopoCurvyExtend } from "@/components/brand/TopoCurvyExtend"; +import { WavyContourLinesBackground } from "@/components/brand/WavyContourLinesBackground"; import { TopoProseSurface } from "@/components/layout/TopoProseSurface"; import { TopoSectionProvider } from "@/components/layout/TopoSectionContext"; import { cn } from "@/lib/utils"; @@ -43,6 +44,7 @@ export function PageRiftHeader({ className )} > +
diff --git a/components/layout/Section.tsx b/components/layout/Section.tsx index ca6baf8..aca6514 100644 --- a/components/layout/Section.tsx +++ b/components/layout/Section.tsx @@ -1,5 +1,7 @@ import type { ReactNode } from "react"; +import { RoundedRockVoronoiBackground } from "@/components/brand/RoundedRockVoronoiBackground"; import { TopoCurvyExtend } from "@/components/brand/TopoCurvyExtend"; +import { WavyContourLinesBackground } from "@/components/brand/WavyContourLinesBackground"; import { ScrollReveal } from "@/components/motion/ScrollReveal"; import { TopoSectionProvider } from "@/components/layout/TopoSectionContext"; import { toneFromSectionVariant, type TopoPatternId } from "@/content/topo-patterns"; @@ -9,6 +11,8 @@ type Props = { id?: string; className?: string; children: ReactNode; + /** Override section backdrop (rock on green, contours on white); pass `null` to hide. */ + background?: ReactNode | null; variant?: "default" | "muted" | "inverse"; /** @deprecated Patterns are hero + footer only; kept for API compatibility */ riftPattern?: TopoPatternId; @@ -20,6 +24,7 @@ export function Section({ id, className, children, + background, variant = "default", riftPattern, riftFlow, @@ -42,6 +47,21 @@ export function Section({ )} data-section-tone={tone} > + {isGreen && + (background !== undefined ? ( + background + ) : ( + + ))} + {!isGreen && + (background !== undefined ? ( + background + ) : ( + + ))} {!isGreen && } - +
+ -
+
-
-
-
- {footerColumns.map((col) => ( -
-

{col.title}

-
    - {col.links.map((link) => ( -
  • - - {link.label} - -
  • - ))} -
+
+ +
+ {footerColumns.map((col) => ( +
+

{col.title}

+
    + {col.links.map((link) => ( +
  • + + {link.label} + +
  • + ))} +
+
+ ))}
- ))} -
-
-
-

- {site.shortName} · {site.dates.label} · Presented by {site.presentedBy} -

-

- © {new Date().getFullYear()} Ethiopian Diaspora Trust Fund. All rights reserved. -

-
+
+
+

+ Follow us +

+ +
+
+ + + +
+

+ {site.shortName} · {site.dates.label} · Presented by {site.presentedBy} +

+

+ © {new Date().getFullYear()} Ethiopian Diaspora Trust Fund. All rights reserved. +

+
+
diff --git a/components/partners/PartnershipCtaBand.tsx b/components/partners/PartnershipCtaBand.tsx index 274dd13..ac8c7cd 100644 --- a/components/partners/PartnershipCtaBand.tsx +++ b/components/partners/PartnershipCtaBand.tsx @@ -1,3 +1,4 @@ +import { RoundedRockVoronoiBackground } from "@/components/brand/RoundedRockVoronoiBackground"; import { partnershipCta } from "@/content/partners"; import { TopoProseSurface } from "@/components/layout/TopoProseSurface"; import { TopoSectionProvider } from "@/components/layout/TopoSectionContext"; @@ -10,6 +11,7 @@ export function PartnershipCtaBand() { id="partnership-form" className="group/topo-section section-green relative isolate overflow-hidden bg-[#1a5c38] py-16 md:py-24" > +
diff --git a/content/last-year-winners.ts b/content/last-year-winners.ts new file mode 100644 index 0000000..9ac3280 --- /dev/null +++ b/content/last-year-winners.ts @@ -0,0 +1,47 @@ +export type LastYearWinner = { + id: string; + /** Display name; omit for logo-only GRV placeholders */ + name?: string; + /** Local path under /public; drop files in public/branding/winners/ */ + logoSrc?: string; + /** Shown when logo image is missing */ + initials?: string; +}; + +export const lastYearWinnersCopy = { + eyebrow: "Last year's summit", + headline: "18+ companies supported", +} as const; + +const GRV_LOGO = "/branding/logo-icon.png"; + +/** Featured alumni — replace logo files in public/branding/winners/ when ready */ +const featured: LastYearWinner[] = [ + { + id: "lifeline-addis", + name: "Lifeline Addis", + logoSrc: "/branding/winners/lifeline-addis.png", + initials: "LA", + }, + { + id: "globedock-academy", + name: "Globedock Academy", + logoSrc: "/branding/winners/globedock-academy.png", + initials: "GD", + }, + { + id: "muyalogy", + name: "Muyalogy", + logoSrc: "/branding/winners/muyalogy.png", + initials: "MY", + }, +]; + +/** Placeholders — GRV mark until logos are added */ +const placeholderCount = 15; +const placeholders: LastYearWinner[] = Array.from({ length: placeholderCount }, (_, i) => ({ + id: `alumni-${i + 1}`, + logoSrc: GRV_LOGO, +})); + +export const lastYearWinners: LastYearWinner[] = [...featured, ...placeholders]; diff --git a/content/site.ts b/content/site.ts index 8eb9f99..6ac1a86 100644 --- a/content/site.ts +++ b/content/site.ts @@ -22,6 +22,12 @@ export const site = { legacySite: "https://grvsummit.com/", calendarIcs: "/calendar", }, + social: { + tiktok: "https://www.tiktok.com/@grvsummit", + linkedin: "https://www.linkedin.com/company/grv-summit", + facebook: "https://www.facebook.com/grvsummit", + instagram: "https://www.instagram.com/grvsummit", + }, stats: [ { type: "static", value: "500+", label: "Attendees" }, { type: "cycling", label: "Grant funding" }, diff --git a/lib/footer-curve-edge.ts b/lib/footer-curve-edge.ts new file mode 100644 index 0000000..e6edf69 --- /dev/null +++ b/lib/footer-curve-edge.ts @@ -0,0 +1,61 @@ +import type { Vec2 } from "@/lib/voronoi-mesh"; + +function mulberry32(seed: number) { + let s = seed >>> 0; + return () => { + s = (s + 0x6d2b79f5) >>> 0; + let t = Math.imul(s ^ (s >>> 15), 1 | s); + t ^= t + Math.imul(t ^ (t >>> 7), 61 | t); + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +/** Gentle wave along the footer midline — peaks cross 50% so facet tops stay visible. */ +export function buildFooterMidlineCurve( + width: number, + segments: number, + seed: number, + midY: number, + amplitude: number +): Vec2[] { + const rand = mulberry32(seed); + const pts: Vec2[] = [[0, midY + (rand() - 0.5) * amplitude * 0.35]]; + for (let i = 1; i <= segments; i++) { + const x = (width * i) / segments; + const t = i / segments; + const wave = + Math.sin(t * Math.PI * 3.8 + seed * 0.002) * amplitude * 0.5 + + Math.sin(t * Math.PI * 7.4 + 0.8) * amplitude * 0.22; + const jitter = (rand() - 0.5) * amplitude * 0.4; + const y = midY + wave + jitter; + pts.push([x, Math.max(midY - amplitude * 0.85, Math.min(midY + amplitude * 0.9, y))]); + } + return pts; +} + +/** Closed SVG path: curved top, square bottom (clip region for facets). */ +export function midlineCurveToClipPath(edge: Vec2[], width: number, height: number): string { + if (edge.length < 2) return ""; + const [x0, y0] = edge[0]; + let d = `M ${x0},${y0}`; + for (let i = 1; i < edge.length; i++) { + const [x, y] = edge[i]; + const [px, py] = edge[i - 1]; + const cx = (px + x) / 2; + d += ` Q ${cx},${py} ${x},${y}`; + } + d += ` L ${width},${height} L 0,${height} Z`; + return d; +} + +/** CSS clip-path (percent) — matches SVG curve for the HTML layer. */ +export function midlineCurveToClipPathPercent( + edge: Vec2[], + width: number, + height: number +): string { + const pct = (x: number, y: number) => + `${((x / width) * 100).toFixed(2)}% ${((y / height) * 100).toFixed(2)}%`; + const top = edge.map(([x, y]) => pct(x, y)).join(", "); + return `polygon(${top}, 100% 100%, 0% 100%)`; +} diff --git a/lib/voronoi-mesh.ts b/lib/voronoi-mesh.ts new file mode 100644 index 0000000..ba44289 --- /dev/null +++ b/lib/voronoi-mesh.ts @@ -0,0 +1,213 @@ +export type Vec2 = [number, number]; + +export type VoronoiCell = { + d: string; + points: Vec2[]; +}; + +function distSq(a: Vec2, b: Vec2) { + const dx = a[0] - b[0]; + const dy = a[1] - b[1]; + return dx * dx + dy * dy; +} + +function intersectEdge(a: Vec2, b: Vec2, keep: Vec2, other: Vec2): Vec2 { + const da = distSq(a, keep) - distSq(a, other); + const db = distSq(b, keep) - distSq(b, other); + const denom = da - db; + const t = Math.abs(denom) < 1e-9 ? 0.5 : da / denom; + return [a[0] + t * (b[0] - a[0]), a[1] + t * (b[1] - a[1])]; +} + +function clipPolygonByHalfPlane(poly: Vec2[], keep: Vec2, other: Vec2): Vec2[] { + if (poly.length === 0) return []; + const inside = (p: Vec2) => distSq(p, keep) <= distSq(p, other) + 1e-6; + const out: Vec2[] = []; + + for (let i = 0; i < poly.length; i++) { + const curr = poly[i]; + const prev = poly[(i - 1 + poly.length) % poly.length]; + const currIn = inside(curr); + const prevIn = inside(prev); + + if (prevIn && currIn) out.push(curr); + else if (prevIn && !currIn) out.push(intersectEdge(prev, curr, keep, other)); + else if (!prevIn && currIn) { + out.push(intersectEdge(prev, curr, keep, other)); + out.push(curr); + } + } + + return out; +} + +function voronoiPolygon(site: Vec2, sites: Vec2[], bounds: Vec2[]): Vec2[] { + let poly = bounds; + for (const other of sites) { + if (other === site) continue; + poly = clipPolygonByHalfPlane(poly, site, other); + if (poly.length < 3) return []; + } + return poly; +} + +export function polygonPath(points: Vec2[]): string { + if (points.length < 3) return ""; + return ( + points + .map((p, i) => `${i === 0 ? "M" : "L"} ${p[0].toFixed(2)} ${p[1].toFixed(2)}`) + .join(" ") + " Z" + ); +} + +function len(a: Vec2, b: Vec2) { + return Math.hypot(b[0] - a[0], b[1] - a[1]); +} + +/** Soft pebble edges — rounded corners on each Voronoi cell. */ +export function roundedPolygonPath(points: Vec2[], radius = 1.35): string { + const n = points.length; + if (n < 3) return polygonPath(points); + + const parts: string[] = []; + + for (let i = 0; i < n; i++) { + const prev = points[(i - 1 + n) % n]; + const curr = points[i]; + const next = points[(i + 1) % n]; + + const e1: Vec2 = [prev[0] - curr[0], prev[1] - curr[1]]; + const e2: Vec2 = [next[0] - curr[0], next[1] - curr[1]]; + const l1 = Math.hypot(e1[0], e1[1]) || 1; + const l2 = Math.hypot(e2[0], e2[1]) || 1; + const cut = Math.min(radius, l1 * 0.38, l2 * 0.38); + + const p1: Vec2 = [curr[0] + (e1[0] / l1) * cut, curr[1] + (e1[1] / l1) * cut]; + const p2: Vec2 = [curr[0] + (e2[0] / l2) * cut, curr[1] + (e2[1] / l2) * cut]; + + if (i === 0) parts.push(`M ${p1[0].toFixed(2)} ${p1[1].toFixed(2)}`); + else parts.push(`L ${p1[0].toFixed(2)} ${p1[1].toFixed(2)}`); + parts.push(`Q ${curr[0].toFixed(2)} ${curr[1].toFixed(2)} ${p2[0].toFixed(2)} ${p2[1].toFixed(2)}`); + } + + return parts.join(" ") + " Z"; +} + +/** Radial pebble field — smaller cells near center, larger toward edges (rock mosaic). */ +export function generateRadialRockSites( + count: number, + width: number, + height: number, + seed: number +): Vec2[] { + const rng = createRng(seed); + const cx = width / 2; + const cy = height / 2; + const sites: Vec2[] = []; + + for (let i = 0; i < count; i++) { + const angle = rng() * Math.PI * 2; + const r = Math.pow(rng(), 0.55) * 0.46; + sites.push([cx + Math.cos(angle) * r * width, cy + Math.sin(angle) * r * height * 0.92]); + } + + return sites; +} + +export function createRng(seed: number) { + let s = seed % 2147483646 || 1; + return () => { + s = (s * 16807) % 2147483647; + return (s - 1) / 2147483646; + }; +} + +export function generatePoissonSites( + count: number, + width: number, + height: number, + seed: number, + minDist: number +): Vec2[] { + const rng = createRng(seed); + const sites: Vec2[] = []; + let attempts = 0; + + while (sites.length < count && attempts < count * 120) { + const p: Vec2 = [rng() * width, rng() * height]; + if (sites.every((s) => distSq(s, p) >= minDist * minDist)) sites.push(p); + attempts++; + } + + return sites; +} + +/** Ghost sites pull tessellation to the edges (stained-glass border). */ +export function borderGhostSites(width: number, height: number, pad = 18): Vec2[] { + const ghosts: Vec2[] = []; + const steps = 5; + for (let i = 0; i <= steps; i++) { + const t = i / steps; + ghosts.push([-pad, height * t]); + ghosts.push([width + pad, height * t]); + ghosts.push([width * t, -pad]); + ghosts.push([width * t, height + pad]); + } + return ghosts; +} + +export type VoronoiMeshOptions = { + siteGenerator?: "poisson" | "radial"; + shape?: "rounded" | "sharp"; + /** Poisson minimum spacing — lower = finer cells */ + minDist?: number; + cornerRadiusMax?: number; + cornerRadiusFactor?: number; +}; + +export function buildVoronoiMesh( + siteCount: number, + width: number, + height: number, + seed: number, + options: VoronoiMeshOptions = {} +): VoronoiCell[] { + const { + siteGenerator = "poisson", + shape = "rounded", + minDist = 8, + cornerRadiusMax = 2.2, + cornerRadiusFactor = 0.22, + } = options; + const sites = + siteGenerator === "radial" + ? generateRadialRockSites(siteCount, width, height, seed) + : generatePoissonSites(siteCount, width, height, seed, minDist); + const allSites = [...sites, ...borderGhostSites(width, height)]; + const bounds: Vec2[] = [ + [0, 0], + [width, 0], + [width, height], + [0, height], + ]; + + const cells: VoronoiCell[] = []; + + for (const site of sites) { + const points = voronoiPolygon(site, allSites, bounds); + if (points.length < 3) continue; + const avgEdge = + points.reduce((sum, p, i) => sum + len(p, points[(i + 1) % points.length]), 0) / + points.length; + const d = + shape === "sharp" + ? polygonPath(points) + : roundedPolygonPath( + points, + Math.min(cornerRadiusMax, avgEdge * cornerRadiusFactor) + ); + if (d) cells.push({ d, points }); + } + + return cells; +} diff --git a/lib/wavy-contour-lines.ts b/lib/wavy-contour-lines.ts new file mode 100644 index 0000000..c7baf17 --- /dev/null +++ b/lib/wavy-contour-lines.ts @@ -0,0 +1,38 @@ +import { createRng } from "@/lib/voronoi-mesh"; + +/** Organic contour paths for light backgrounds (topographic wave lines). */ +export function buildWavyContourPaths( + lineCount: number, + width: number, + height: number, + seed: number +): string[] { + const rng = createRng(seed); + const paths: string[] = []; + + for (let i = 0; i < lineCount; i++) { + const yBase = ((i + 0.5) / lineCount) * height; + const amp = 2 + rng() * 4.5; + const freq1 = 0.035 + rng() * 0.038; + const freq2 = 0.008 + rng() * 0.014; + const phase1 = rng() * Math.PI * 2; + const phase2 = rng() * Math.PI * 2; + const drift = (rng() - 0.5) * 3; + const steps = 80; + let d = ""; + + for (let s = 0; s <= steps; s++) { + const x = (s / steps) * width; + const y = + yBase + + drift * (x / width - 0.5) + + amp * Math.sin(x * freq1 + phase1) + + amp * 0.42 * Math.sin(x * freq2 + phase2); + d += s === 0 ? `M ${x.toFixed(2)} ${y.toFixed(2)}` : ` L ${x.toFixed(2)} ${y.toFixed(2)}`; + } + + paths.push(d); + } + + return paths; +} diff --git a/public/branding/winners/.gitkeep b/public/branding/winners/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/public/branding/winners/globedock-academy.png b/public/branding/winners/globedock-academy.png new file mode 100644 index 0000000000000000000000000000000000000000..e5c42e013192530ae76fa9916190768061a9359a GIT binary patch literal 14488 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�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 literal 0 HcmV?d00001 diff --git a/public/branding/winners/muyalogy.png b/public/branding/winners/muyalogy.png new file mode 100644 index 0000000000000000000000000000000000000000..c7d7020dab55ed23939dd29a37119123828bc77a GIT binary patch literal 814 zcmV+}1JV46P)yM2Ly%C)i`OTul40Hw-3MFb+k>E}JmgCTH5gDVY8AO$E+NJR)ZpaxhK zzKZ>+)sjTbI;?>O&|IJYpYCKq0^H63J6TbxJd*&x<+qu>V`2^Ytv8pEfKNB;0D$wa zGRKbW|A8*qesCa3IrCUQyEYcEL(cV85@MGcKnmn38rIJruh+$tpL>}T_9SFAbN>|_0KVUASRKGGs!~8|v$BWI{}Pg- zFR+s7PDaxB+??~HKl`F@2!6fOuym)2R1<*h`{Ul(W9^>UAvL9=cGBd^84X_8y2AaO zSndV%RLqBR9;Ditn<6ibilxt2TT@aBz#Zj!omzD<3g1JNpj+pdzQ9SPP04{$OfO`h z)aP{`t}k(FTl0-Xe}GIK0}=2#%X;0yVLksi{dhc~M=`}ytn5w^^+Q_N=$$g9COxQF z!-{&vg5x$d0Qh!yJaNC{o`Vte#zrcE(|$h}CK7->$tG8NWKBb%WxR_`}a-#>~ z*VCp=002%t+b4G-N*xlmk?YKf5dpv3O@r$_N{=t?lf>AqUx--Pt^i+bTA1Rq{AgQ~ zh^3OvjTkusc9xp)XStRm3wzcJ`ETmX_ayM41OVXh6PrYpI&*Tn%QASBfR;m#_9#|o sH{CJdN(8tZe7GCDq(s2ZVjPsqAK_MqAW=oC`v3p{07*qoM6N<$g1^dk0RR91 literal 0 HcmV?d00001 diff --git a/scripts/download-assets.mjs b/scripts/download-assets.mjs index c3d47b0..e46fe1d 100644 --- a/scripts/download-assets.mjs +++ b/scripts/download-assets.mjs @@ -28,6 +28,18 @@ const assets = [ { url: `${base}/2025/02/lulite_edited-removebg-preview.png`, dest: "public/branding/speakers/lulite.png" }, { url: `${base}/2025/02/dagmawit_edited-removebg-preview.png`, dest: "public/branding/speakers/dagmawit.png" }, { url: `${base}/2025/02/samiya_edited-removebg-preview.png`, dest: "public/branding/speakers/samiya.png" }, + { + url: "https://www.google.com/s2/favicons?domain=lifelineaddis.com&sz=128", + dest: "public/branding/winners/lifeline-addis.png", + }, + { + url: "https://www.google.com/s2/favicons?domain=globedock.et&sz=128", + dest: "public/branding/winners/globedock-academy.png", + }, + { + url: "https://www.google.com/s2/favicons?domain=muyalogy.com&sz=128", + dest: "public/branding/winners/muyalogy.png", + }, ]; async function download(url, dest) {