From 06936e7a4fe32ce8252d0554ec761b293ec12c81 Mon Sep 17 00:00:00 2001 From: Kirubel-Kibru-Yaltopia Date: Thu, 21 May 2026 21:04:53 +0300 Subject: [PATCH] - --- .gitignore | 2 + app/globals.css | 34 +++++ components/home/LastYearWinnerMark.tsx | 160 ++++++++++++++++------ components/home/LastYearWinnerTip.tsx | 136 ++++++++++++++++++ components/home/LastYearWinnersScroll.tsx | 41 +++++- content/last-year-winners.ts | 105 +++++++++++++- 6 files changed, 427 insertions(+), 51 deletions(-) create mode 100644 components/home/LastYearWinnerTip.tsx diff --git a/.gitignore b/.gitignore index d7506d3..61540f6 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,8 @@ # misc .DS_Store +.deploy-log.txt +.cursor-git-deploy.sh *.pem .env*.local .env diff --git a/app/globals.css b/app/globals.css index 98c0b37..d228073 100644 --- a/app/globals.css +++ b/app/globals.css @@ -206,6 +206,37 @@ .marquee-winners { animation: marquee 55s linear infinite; } + .marquee-winners:hover { + animation-play-state: paused; + } + + @keyframes winner-impact-pulse { + 0%, + 100% { + transform: scale(1); + } + 50% { + transform: scale(1.06); + } + } + + @keyframes winner-impact-glow { + 0%, + 100% { + color: #0d3d26; + text-shadow: 0 0 0 transparent; + } + 50% { + color: #1a5c38; + text-shadow: 0 0 14px rgba(74, 173, 110, 0.45); + } + } + + .winner-impact-value { + animation: + winner-impact-pulse 2.4s ease-in-out infinite, + winner-impact-glow 2.4s ease-in-out infinite; + } @keyframes marquee { from { transform: translateX(0); @@ -219,6 +250,9 @@ .marquee-winners { animation: none; } + .winner-impact-value { + animation: none; + } .backdrop-bloom-a, .backdrop-bloom-b, .valley-pan-slow, diff --git a/components/home/LastYearWinnerMark.tsx b/components/home/LastYearWinnerMark.tsx index c034f08..d226308 100644 --- a/components/home/LastYearWinnerMark.tsx +++ b/components/home/LastYearWinnerMark.tsx @@ -1,61 +1,143 @@ "use client"; -import { useState } from "react"; +import { useCallback, useRef, useState } from "react"; import Image from "next/image"; import type { LastYearWinner } from "@/content/last-year-winners"; +import { LastYearWinnerTip } from "@/components/home/LastYearWinnerTip"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { cn } from "@/lib/utils"; const GRV_LOGO = "/branding/logo-icon.png"; type Props = { company: LastYearWinner; + tipKey: string; + isTipOpen: boolean; + onTipOpen: (key: string) => void; + onTipClose: () => void; + variant?: "on-green" | "on-light"; className?: string; }; -export function LastYearWinnerMark({ company, className }: Props) { +export function LastYearWinnerMark({ + company, + tipKey, + isTipOpen, + onTipOpen, + onTipClose, + variant = "on-light", + className, +}: Props) { + const closeTimer = useRef | null>(null); const [failed, setFailed] = useState(false); const src = company.logoSrc ?? GRV_LOGO; const showImage = !failed && src; const logoOnly = !company.name; + const onLight = variant === "on-light"; + + const clearCloseTimer = useCallback(() => { + if (closeTimer.current) { + clearTimeout(closeTimer.current); + closeTimer.current = null; + } + }, []); + + const handleEnter = useCallback(() => { + clearCloseTimer(); + onTipOpen(tipKey); + }, [clearCloseTimer, onTipOpen, tipKey]); + + const handleLeave = useCallback(() => { + clearCloseTimer(); + closeTimer.current = setTimeout(() => onTipClose(), 120); + }, [clearCloseTimer, onTipClose]); + + const handleOpenChange = useCallback( + (open: boolean) => { + if (open) onTipOpen(tipKey); + else onTipClose(); + }, + [onTipOpen, onTipClose, tipKey] + ); return ( -
-
- {showImage ? ( - setFailed(true)} - /> - ) : company.initials ? ( - - {company.initials} - - ) : ( - - )} -
- {company.name ? ( - - {company.name} - + + + + + {isTipOpen ? ( + e.preventDefault()} + > + + ) : null} -
+ ); } diff --git a/components/home/LastYearWinnerTip.tsx b/components/home/LastYearWinnerTip.tsx new file mode 100644 index 0000000..ea3441e --- /dev/null +++ b/components/home/LastYearWinnerTip.tsx @@ -0,0 +1,136 @@ +"use client"; + +import { useState } from "react"; +import Image from "next/image"; +import Link from "next/link"; +import { ExternalLink, HandHeart } from "lucide-react"; +import { getWinnerImpact, type LastYearWinner } from "@/content/last-year-winners"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; + +type Props = { + company: LastYearWinner; + className?: string; +}; + +function externalLinkProps(href: string) { + return href.startsWith("http") ? { target: "_blank" as const, rel: "noopener noreferrer" } : {}; +} + +function FounderPhoto({ company }: { company: LastYearWinner }) { + const [failed, setFailed] = useState(false); + const src = company.founderImageSrc; + + if (src && !failed) { + return ( + setFailed(true)} + /> + ); + } + + const initials = + company.initials ?? + (company.name + ? company.name + .split(/\s+/) + .map((w) => w[0]) + .join("") + .slice(0, 2) + .toUpperCase() + : "GR"); + + return ( +
+ {initials} +
+ ); +} + +export function LastYearWinnerTip({ company, className }: Props) { + const impact = getWinnerImpact(company); + const headline = impact.metrics.find((m) => m.highlight) ?? impact.metrics[0]; + const supporting = impact.metrics.filter((m) => m !== headline).slice(0, 2); + const name = company.name ?? "GRV Summit alumni"; + const viewHref = impact.links.view ?? impact.links.website; + const donateHref = impact.links.donate; + + return ( +
+
+ +
+

+ {name} +

+ {company.founderName ? ( +

{company.founderName}

+ ) : null} +

+ {impact.summary} +

+
+
+ + {headline ? ( +
+

+ {headline.label} +

+

+ {headline.value} +

+
+ ) : null} + + {supporting.length > 0 ? ( +
+ {supporting.map((m) => ( +
+
+ {m.label} +
+
{m.value}
+
+ ))} +
+ ) : null} + + {(viewHref || donateHref) && ( +
+ {viewHref ? ( + + ) : null} + {donateHref ? ( + + ) : null} +
+ )} +
+ ); +} diff --git a/components/home/LastYearWinnersScroll.tsx b/components/home/LastYearWinnersScroll.tsx index 0e96743..3706631 100644 --- a/components/home/LastYearWinnersScroll.tsx +++ b/components/home/LastYearWinnersScroll.tsx @@ -1,9 +1,11 @@ +"use client"; + +import { useCallback, useState } from "react"; 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; }; @@ -11,6 +13,15 @@ type Props = { export function LastYearWinnersScroll({ variant = "on-green", className }: Props) { const items = [...lastYearWinners, ...lastYearWinners]; const onLight = variant === "on-light"; + const [activeTipKey, setActiveTipKey] = useState(null); + + const openTip = useCallback((key: string) => { + setActiveTipKey(key); + }, []); + + const closeTip = useCallback(() => { + setActiveTipKey(null); + }, []); return (

{lastYearWinnersCopy.headline}

+

+ + {lastYearWinnersCopy.hoverHint} + +

-
- {items.map((company, i) => ( - - ))} +
+ {items.map((company, i) => { + const tipKey = `${company.id}-${i}`; + return ( + + ); + })}
diff --git a/content/last-year-winners.ts b/content/last-year-winners.ts index 9ac3280..b417f0e 100644 --- a/content/last-year-winners.ts +++ b/content/last-year-winners.ts @@ -1,43 +1,119 @@ +export type WinnerSector = "health" | "edtech" | "general"; + +export type WinnerImpactMetric = { + label: string; + value: string; + /** Shown as the large headline metric */ + highlight?: boolean; +}; + +export type WinnerLinks = { + website?: string; + donate?: string; + view?: string; +}; + +export type WinnerImpact = { + summary: string; + metrics: WinnerImpactMetric[]; + links: WinnerLinks; +}; + 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; + founderName?: string; + /** Under public/, e.g. /branding/winners/founders/lifeline-addis-founder.png */ + founderImageSrc?: string; + sector?: WinnerSector; + impact?: WinnerImpact; }; export const lastYearWinnersCopy = { eyebrow: "Last year's summit", headline: "18+ companies supported", + hoverHint: "Hover a company to see summit impact", } 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", + sector: "health", + founderName: "Founder", + founderImageSrc: "/branding/winners/founders/lifeline-addis-founder.png", + impact: { + summary: + "GRV mentorship and health-sector intros helped scale community outreach, tele-triage, and clinic partnerships across Addis.", + metrics: [ + { label: "Patients served", value: "12K+", highlight: true }, + { label: "Telehealth sessions", value: "8.4K" }, + { label: "Health workers trained", value: "45" }, + { label: "Clinic partners", value: "6" }, + ], + links: { + website: "https://grvsummit.com/", + donate: "https://grvsummit.com/", + view: "https://grvsummit.com/", + }, + }, }, { id: "globedock-academy", name: "Globedock Academy", logoSrc: "/branding/winners/globedock-academy.png", initials: "GD", + sector: "edtech", + founderName: "Founder", + founderImageSrc: "/branding/winners/founders/globedock-academy-founder.png", + impact: { + summary: + "Summit showcase and edtech mentors accelerated STEM pathways, instructor certification, and school pilots nationwide.", + metrics: [ + { label: "Active learners", value: "4.2K", highlight: true }, + { label: "Course completions", value: "11K+" }, + { label: "Certified instructors", value: "14" }, + { label: "School partners", value: "28" }, + ], + links: { + website: "https://grvsummit.com/", + donate: "https://grvsummit.com/", + view: "https://grvsummit.com/", + }, + }, }, { id: "muyalogy", name: "Muyalogy", logoSrc: "/branding/winners/muyalogy.png", initials: "MY", + sector: "edtech", + founderName: "Founder", + founderImageSrc: "/branding/winners/founders/muyalogy-founder.png", + impact: { + summary: + "GRV connected Muyalogy to education partners and investors to grow digital learning access for students and teachers.", + metrics: [ + { label: "Students reached", value: "3.5K+", highlight: true }, + { label: "Lessons completed", value: "18K+" }, + { label: "Teacher partners", value: "120+" }, + { label: "Learning gain", value: "31%" }, + ], + links: { + website: "https://grvsummit.com/", + donate: "https://grvsummit.com/", + view: "https://grvsummit.com/", + }, + }, }, ]; -/** Placeholders — GRV mark until logos are added */ const placeholderCount = 15; const placeholders: LastYearWinner[] = Array.from({ length: placeholderCount }, (_, i) => ({ id: `alumni-${i + 1}`, @@ -45,3 +121,22 @@ const placeholders: LastYearWinner[] = Array.from({ length: placeholderCount }, })); export const lastYearWinners: LastYearWinner[] = [...featured, ...placeholders]; + +const defaultImpact: WinnerImpact = { + summary: + "Part of last year's GRV Summit cohort — gaining visibility, mentorship, and connections across agriculture, health, and education.", + metrics: [ + { label: "Summit alumni", value: "18+", highlight: true }, + { label: "Mentor hours", value: "200+" }, + { label: "Partner intros", value: "50+" }, + { label: "Sectors", value: "3" }, + ], + links: { + view: "/partners", + donate: "/sponsor", + }, +}; + +export function getWinnerImpact(company: LastYearWinner): WinnerImpact { + return company.impact ?? defaultImpact; +}