This commit is contained in:
parent
d261148b35
commit
06936e7a4f
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -24,6 +24,8 @@
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.deploy-log.txt
|
||||||
|
.cursor-git-deploy.sh
|
||||||
*.pem
|
*.pem
|
||||||
.env*.local
|
.env*.local
|
||||||
.env
|
.env
|
||||||
|
|
|
||||||
|
|
@ -206,6 +206,37 @@
|
||||||
.marquee-winners {
|
.marquee-winners {
|
||||||
animation: marquee 55s linear infinite;
|
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 {
|
@keyframes marquee {
|
||||||
from {
|
from {
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
|
|
@ -219,6 +250,9 @@
|
||||||
.marquee-winners {
|
.marquee-winners {
|
||||||
animation: none;
|
animation: none;
|
||||||
}
|
}
|
||||||
|
.winner-impact-value {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
.backdrop-bloom-a,
|
.backdrop-bloom-a,
|
||||||
.backdrop-bloom-b,
|
.backdrop-bloom-b,
|
||||||
.valley-pan-slow,
|
.valley-pan-slow,
|
||||||
|
|
|
||||||
|
|
@ -1,61 +1,143 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useCallback, useRef, useState } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import type { LastYearWinner } from "@/content/last-year-winners";
|
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";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const GRV_LOGO = "/branding/logo-icon.png";
|
const GRV_LOGO = "/branding/logo-icon.png";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
company: LastYearWinner;
|
company: LastYearWinner;
|
||||||
|
tipKey: string;
|
||||||
|
isTipOpen: boolean;
|
||||||
|
onTipOpen: (key: string) => void;
|
||||||
|
onTipClose: () => void;
|
||||||
|
variant?: "on-green" | "on-light";
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function LastYearWinnerMark({ company, className }: Props) {
|
export function LastYearWinnerMark({
|
||||||
|
company,
|
||||||
|
tipKey,
|
||||||
|
isTipOpen,
|
||||||
|
onTipOpen,
|
||||||
|
onTipClose,
|
||||||
|
variant = "on-light",
|
||||||
|
className,
|
||||||
|
}: Props) {
|
||||||
|
const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const [failed, setFailed] = useState(false);
|
const [failed, setFailed] = useState(false);
|
||||||
const src = company.logoSrc ?? GRV_LOGO;
|
const src = company.logoSrc ?? GRV_LOGO;
|
||||||
const showImage = !failed && src;
|
const showImage = !failed && src;
|
||||||
const logoOnly = !company.name;
|
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 (
|
return (
|
||||||
<div
|
<Popover open={isTipOpen} onOpenChange={handleOpenChange}>
|
||||||
className={cn(
|
<PopoverTrigger asChild>
|
||||||
"flex h-10 shrink-0 items-center gap-2 rounded-lg border border-white/25 bg-white/90 px-2.5 shadow-sm",
|
<button
|
||||||
logoOnly && "px-2",
|
type="button"
|
||||||
className
|
className={cn(
|
||||||
)}
|
"shrink-0 rounded-lg outline-none focus-visible:ring-2 focus-visible:ring-[#1a5c38]/40",
|
||||||
title={company.name}
|
isTipOpen && "relative z-30"
|
||||||
>
|
)}
|
||||||
<div className="relative flex size-7 shrink-0 items-center justify-center overflow-hidden rounded-md bg-white">
|
aria-label={
|
||||||
{showImage ? (
|
company.name
|
||||||
<Image
|
? `${company.name} — hover for impact details`
|
||||||
src={src}
|
: "GRV alumni — hover for impact details"
|
||||||
alt=""
|
}
|
||||||
width={28}
|
onMouseEnter={handleEnter}
|
||||||
height={28}
|
onMouseLeave={handleLeave}
|
||||||
className="size-7 object-contain p-0.5"
|
onFocus={handleEnter}
|
||||||
onError={() => setFailed(true)}
|
onBlur={handleLeave}
|
||||||
/>
|
>
|
||||||
) : company.initials ? (
|
<div
|
||||||
<span className="text-[10px] font-bold leading-none text-[#1a5c38]">
|
className={cn(
|
||||||
{company.initials}
|
"flex h-10 shrink-0 items-center gap-2 rounded-lg border px-2.5 shadow-sm transition-colors",
|
||||||
</span>
|
onLight
|
||||||
) : (
|
? "border-[#1a5c38]/15 bg-white/95 hover:border-[#1a5c38]/30 hover:bg-white"
|
||||||
<Image
|
: "border-white/25 bg-white/90 hover:border-white/50 hover:bg-white",
|
||||||
src={GRV_LOGO}
|
logoOnly && "px-2",
|
||||||
alt=""
|
isTipOpen &&
|
||||||
width={28}
|
(onLight
|
||||||
height={28}
|
? "border-[#1a5c38]/40 ring-2 ring-[#1a5c38]/15"
|
||||||
className="size-7 object-contain p-0.5"
|
: "border-white/60 ring-2 ring-white/25"),
|
||||||
/>
|
className
|
||||||
)}
|
)}
|
||||||
</div>
|
>
|
||||||
{company.name ? (
|
<div className="relative flex size-7 shrink-0 items-center justify-center overflow-hidden rounded-md bg-white">
|
||||||
<span className="max-w-[7.5rem] truncate text-[11px] font-medium text-[#1a5c38]">
|
{showImage ? (
|
||||||
{company.name}
|
<Image
|
||||||
</span>
|
src={src}
|
||||||
|
alt=""
|
||||||
|
width={28}
|
||||||
|
height={28}
|
||||||
|
className="size-7 object-contain p-0.5"
|
||||||
|
onError={() => setFailed(true)}
|
||||||
|
/>
|
||||||
|
) : company.initials ? (
|
||||||
|
<span className="text-[10px] font-bold leading-none text-[#1a5c38]">
|
||||||
|
{company.initials}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<Image
|
||||||
|
src={GRV_LOGO}
|
||||||
|
alt=""
|
||||||
|
width={28}
|
||||||
|
height={28}
|
||||||
|
className="size-7 object-contain p-0.5"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{company.name ? (
|
||||||
|
<span className="max-w-[7.5rem] truncate text-[11px] font-medium text-[#1a5c38]">
|
||||||
|
{company.name}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
{isTipOpen ? (
|
||||||
|
<PopoverContent
|
||||||
|
side="top"
|
||||||
|
align="center"
|
||||||
|
sideOffset={8}
|
||||||
|
className="z-50 border-[#1a5c38]/15 bg-white p-0 shadow-lg"
|
||||||
|
onMouseEnter={handleEnter}
|
||||||
|
onMouseLeave={handleLeave}
|
||||||
|
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<LastYearWinnerTip company={company} />
|
||||||
|
</PopoverContent>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</Popover>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
136
components/home/LastYearWinnerTip.tsx
Normal file
136
components/home/LastYearWinnerTip.tsx
Normal file
|
|
@ -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 (
|
||||||
|
<Image
|
||||||
|
src={src}
|
||||||
|
alt=""
|
||||||
|
width={48}
|
||||||
|
height={48}
|
||||||
|
className="size-12 shrink-0 rounded-full border-2 border-[#1a5c38]/15 object-cover"
|
||||||
|
onError={() => setFailed(true)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const initials =
|
||||||
|
company.initials ??
|
||||||
|
(company.name
|
||||||
|
? company.name
|
||||||
|
.split(/\s+/)
|
||||||
|
.map((w) => w[0])
|
||||||
|
.join("")
|
||||||
|
.slice(0, 2)
|
||||||
|
.toUpperCase()
|
||||||
|
: "GR");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex size-12 shrink-0 items-center justify-center rounded-full border-2 border-[#1a5c38]/15 bg-[#f0f5f2] text-sm font-bold text-[#1a5c38]"
|
||||||
|
aria-hidden
|
||||||
|
>
|
||||||
|
{initials}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className={cn("w-[min(calc(100vw-2rem),16.5rem)]", className)}>
|
||||||
|
<div className="flex gap-3 px-3 pt-3 pb-2">
|
||||||
|
<FounderPhoto company={company} />
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="truncate text-[11px] font-semibold uppercase tracking-wider text-[#1a5c38]">
|
||||||
|
{name}
|
||||||
|
</p>
|
||||||
|
{company.founderName ? (
|
||||||
|
<p className="truncate text-[10px] text-[#3d5248]/80">{company.founderName}</p>
|
||||||
|
) : null}
|
||||||
|
<p className="mt-1 line-clamp-2 text-[11px] leading-snug text-[#3d5248]">
|
||||||
|
{impact.summary}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{headline ? (
|
||||||
|
<div className="mx-3 mb-2 rounded-lg border border-[#1a5c38]/12 bg-[#f0f5f2] px-3 py-2.5 text-center">
|
||||||
|
<p className="text-[10px] font-bold uppercase tracking-wider text-[#1a5c38]/75">
|
||||||
|
{headline.label}
|
||||||
|
</p>
|
||||||
|
<p className="winner-impact-value mt-0.5 font-display text-2xl font-extrabold tracking-tight text-[#0d3d26]">
|
||||||
|
{headline.value}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{supporting.length > 0 ? (
|
||||||
|
<dl className="grid grid-cols-2 gap-1.5 px-3 pb-2">
|
||||||
|
{supporting.map((m) => (
|
||||||
|
<div key={m.label} className="rounded-md bg-[#f7faf8] px-2 py-1.5 text-center">
|
||||||
|
<dt className="text-[9px] font-semibold uppercase tracking-wide text-[#1a5c38]/65">
|
||||||
|
{m.label}
|
||||||
|
</dt>
|
||||||
|
<dd className="text-xs font-bold text-[#0d3d26]">{m.value}</dd>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{(viewHref || donateHref) && (
|
||||||
|
<div className="flex items-center justify-end gap-2 border-t border-[#1a5c38]/10 bg-[#f7faf8]/80 px-3 py-2">
|
||||||
|
{viewHref ? (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon-sm"
|
||||||
|
className="rounded-full border-[#1a5c38]/20 text-[#1a5c38] hover:bg-[#f0f5f2]"
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<Link href={viewHref} aria-label="View startup" {...externalLinkProps(viewHref)}>
|
||||||
|
<ExternalLink className="size-4" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
{donateHref ? (
|
||||||
|
<Button
|
||||||
|
size="icon-sm"
|
||||||
|
className="rounded-full bg-[#1a5c38] text-white hover:bg-[#0d3d26]"
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<Link href={donateHref} aria-label="Donate" {...externalLinkProps(donateHref)}>
|
||||||
|
<HandHeart className="size-4" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
import { lastYearWinners, lastYearWinnersCopy } from "@/content/last-year-winners";
|
import { lastYearWinners, lastYearWinnersCopy } from "@/content/last-year-winners";
|
||||||
import { LastYearWinnerMark } from "@/components/home/LastYearWinnerMark";
|
import { LastYearWinnerMark } from "@/components/home/LastYearWinnerMark";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
/** Green stats section vs white hero */
|
|
||||||
variant?: "on-green" | "on-light";
|
variant?: "on-green" | "on-light";
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
@ -11,6 +13,15 @@ type Props = {
|
||||||
export function LastYearWinnersScroll({ variant = "on-green", className }: Props) {
|
export function LastYearWinnersScroll({ variant = "on-green", className }: Props) {
|
||||||
const items = [...lastYearWinners, ...lastYearWinners];
|
const items = [...lastYearWinners, ...lastYearWinners];
|
||||||
const onLight = variant === "on-light";
|
const onLight = variant === "on-light";
|
||||||
|
const [activeTipKey, setActiveTipKey] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const openTip = useCallback((key: string) => {
|
||||||
|
setActiveTipKey(key);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const closeTip = useCallback(() => {
|
||||||
|
setActiveTipKey(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -29,12 +40,17 @@ export function LastYearWinnersScroll({ variant = "on-green", className }: Props
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
className={cn(
|
className={cn(
|
||||||
"mt-1 text-center text-sm font-medium",
|
"mt-2 text-center text-lg font-bold tracking-tight md:text-xl",
|
||||||
onLight ? "text-[#1a5c38]/80" : "text-white/85"
|
onLight ? "text-[#0d3d26]" : "text-white"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{lastYearWinnersCopy.headline}
|
{lastYearWinnersCopy.headline}
|
||||||
</p>
|
</p>
|
||||||
|
<p className="mt-2 text-center">
|
||||||
|
<span className="inline-block rounded-full bg-white px-3 py-1 text-[11px] font-medium text-[#1a5c38] shadow-sm">
|
||||||
|
{lastYearWinnersCopy.hoverHint}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
<div
|
<div
|
||||||
className="relative mt-5 overflow-hidden"
|
className="relative mt-5 overflow-hidden"
|
||||||
aria-label="Companies supported at last year's summit"
|
aria-label="Companies supported at last year's summit"
|
||||||
|
|
@ -51,10 +67,21 @@ export function LastYearWinnersScroll({ variant = "on-green", className }: Props
|
||||||
onLight ? "from-white" : "from-[#1a5c38]"
|
onLight ? "from-white" : "from-[#1a5c38]"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div className="marquee-winners flex w-max shrink-0 items-center gap-3 py-1">
|
<div className="marquee-winners pointer-events-auto flex w-max shrink-0 items-center gap-3 py-1">
|
||||||
{items.map((company, i) => (
|
{items.map((company, i) => {
|
||||||
<LastYearWinnerMark key={`${company.id}-${i}`} company={company} />
|
const tipKey = `${company.id}-${i}`;
|
||||||
))}
|
return (
|
||||||
|
<LastYearWinnerMark
|
||||||
|
key={tipKey}
|
||||||
|
tipKey={tipKey}
|
||||||
|
company={company}
|
||||||
|
variant={variant}
|
||||||
|
isTipOpen={activeTipKey === tipKey}
|
||||||
|
onTipOpen={openTip}
|
||||||
|
onTipClose={closeTip}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
export type LastYearWinner = {
|
||||||
id: string;
|
id: string;
|
||||||
/** Display name; omit for logo-only GRV placeholders */
|
|
||||||
name?: string;
|
name?: string;
|
||||||
/** Local path under /public; drop files in public/branding/winners/ */
|
|
||||||
logoSrc?: string;
|
logoSrc?: string;
|
||||||
/** Shown when logo image is missing */
|
|
||||||
initials?: string;
|
initials?: string;
|
||||||
|
founderName?: string;
|
||||||
|
/** Under public/, e.g. /branding/winners/founders/lifeline-addis-founder.png */
|
||||||
|
founderImageSrc?: string;
|
||||||
|
sector?: WinnerSector;
|
||||||
|
impact?: WinnerImpact;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const lastYearWinnersCopy = {
|
export const lastYearWinnersCopy = {
|
||||||
eyebrow: "Last year's summit",
|
eyebrow: "Last year's summit",
|
||||||
headline: "18+ companies supported",
|
headline: "18+ companies supported",
|
||||||
|
hoverHint: "Hover a company to see summit impact",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const GRV_LOGO = "/branding/logo-icon.png";
|
const GRV_LOGO = "/branding/logo-icon.png";
|
||||||
|
|
||||||
/** Featured alumni — replace logo files in public/branding/winners/ when ready */
|
|
||||||
const featured: LastYearWinner[] = [
|
const featured: LastYearWinner[] = [
|
||||||
{
|
{
|
||||||
id: "lifeline-addis",
|
id: "lifeline-addis",
|
||||||
name: "Lifeline Addis",
|
name: "Lifeline Addis",
|
||||||
logoSrc: "/branding/winners/lifeline-addis.png",
|
logoSrc: "/branding/winners/lifeline-addis.png",
|
||||||
initials: "LA",
|
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",
|
id: "globedock-academy",
|
||||||
name: "Globedock Academy",
|
name: "Globedock Academy",
|
||||||
logoSrc: "/branding/winners/globedock-academy.png",
|
logoSrc: "/branding/winners/globedock-academy.png",
|
||||||
initials: "GD",
|
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",
|
id: "muyalogy",
|
||||||
name: "Muyalogy",
|
name: "Muyalogy",
|
||||||
logoSrc: "/branding/winners/muyalogy.png",
|
logoSrc: "/branding/winners/muyalogy.png",
|
||||||
initials: "MY",
|
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 placeholderCount = 15;
|
||||||
const placeholders: LastYearWinner[] = Array.from({ length: placeholderCount }, (_, i) => ({
|
const placeholders: LastYearWinner[] = Array.from({ length: placeholderCount }, (_, i) => ({
|
||||||
id: `alumni-${i + 1}`,
|
id: `alumni-${i + 1}`,
|
||||||
|
|
@ -45,3 +121,22 @@ const placeholders: LastYearWinner[] = Array.from({ length: placeholderCount },
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const lastYearWinners: LastYearWinner[] = [...featured, ...placeholders];
|
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;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user