GRV-Summit-Site/components/home/LastYearWinnerTip.tsx
kirukib 03d439e97b
Some checks failed
Deploy to Cloudflare Workers (OpenNext) / deploy (push) Has been cancelled
Update hero alumni strip, 2027 dates, and Antebas font loading.
Add a bottom cut-out panel with larger winner cards (four visible), move summit dates to Feb 21–22 2027, and limit demo Antebas to letters so symbols render via DM Sans.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 00:25:43 +03:00

217 lines
6.4 KiB
TypeScript

"use client";
import { useState } from "react";
import Image from "next/image";
import Link from "next/link";
import { ExternalLink, HandHeart, X } 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;
showClose?: boolean;
onClose?: () => void;
size?: "default" | "large";
};
function externalLinkProps(href: string) {
return href.startsWith("http") ? { target: "_blank" as const, rel: "noopener noreferrer" } : {};
}
function FounderPhoto({
company,
large,
}: {
company: LastYearWinner;
large?: boolean;
}) {
const [failed, setFailed] = useState(false);
const src = company.founderImageSrc;
if (src && !failed) {
return (
<Image
src={src}
alt=""
width={large ? 56 : 48}
height={large ? 56 : 48}
className={cn(
"shrink-0 rounded-full border-2 border-[#37a47a]/15 object-cover",
large ? "size-14" : "size-12"
)}
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={cn(
"flex shrink-0 items-center justify-center rounded-full border-2 border-[#37a47a]/15 bg-[#e8f2ec] font-bold text-[#37a47a]",
large ? "size-14 text-base" : "size-12 text-sm"
)}
aria-hidden
>
{initials}
</div>
);
}
export function LastYearWinnerTip({
company,
className,
showClose,
onClose,
size = "default",
}: Props) {
const isLarge = size === "large";
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;
const showFooter = showClose || viewHref || donateHref;
return (
<div
className={cn(
"box-border max-w-full shrink-0 overflow-hidden",
isLarge ? "w-full" : "w-72",
className
)}
>
<div className={cn("flex min-w-0 px-3 pt-3 pb-2", isLarge ? "gap-3.5" : "gap-2.5")}>
<FounderPhoto company={company} large={isLarge} />
<div className="min-w-0 flex-1">
<p
className={cn(
"truncate font-semibold uppercase tracking-wider text-[#37a47a]",
isLarge ? "text-xs md:text-sm" : "text-[11px]"
)}
>
{name}
</p>
{company.founderName ? (
<p
className={cn(
"truncate text-[#5b5b5b]/80",
isLarge ? "text-xs" : "text-[10px]"
)}
>
{company.founderName}
</p>
) : null}
<p
className={cn(
"mt-1 line-clamp-2 leading-snug text-[#5b5b5b]",
isLarge ? "text-xs md:text-sm" : "text-[11px]"
)}
>
{impact.summary}
</p>
</div>
</div>
{headline ? (
<div
className={cn(
"mx-3 mb-2 overflow-hidden rounded-lg border border-[#37a47a]/12 bg-[#e8f2ec] text-center",
isLarge ? "px-3 py-3" : "px-2.5 py-2"
)}
>
<p
className={cn(
"truncate font-bold uppercase tracking-wider text-[#37a47a]/75",
isLarge ? "text-xs" : "text-[10px]"
)}
>
{headline.label}
</p>
<p
className={cn(
"winner-impact-value mt-0.5 truncate font-display font-extrabold tracking-tight text-[#30614c]",
isLarge ? "text-2xl md:text-3xl" : "text-xl"
)}
>
{headline.value}
</p>
</div>
) : null}
{supporting.length > 0 ? (
<dl className="grid min-w-0 grid-cols-2 gap-1.5 px-3 pb-2">
{supporting.map((m) => (
<div key={m.label} className="min-w-0 rounded-md bg-[#f7faf8] px-1.5 py-1.5 text-center">
<dt className="truncate text-[9px] font-semibold uppercase tracking-wide text-[#37a47a]/65">
{m.label}
</dt>
<dd className="truncate text-xs font-bold text-[#30614c]">{m.value}</dd>
</div>
))}
</dl>
) : null}
{showFooter ? (
<div
className={cn(
"flex w-full min-w-0 items-center gap-1.5 border-t border-[#37a47a]/10 bg-[#f7faf8]/80 px-2.5 py-2",
showClose && onClose ? "justify-between" : "justify-end"
)}
>
{showClose && onClose ? (
<button
type="button"
data-winner-interactive
onClick={onClose}
className="inline-flex size-7 shrink-0 items-center justify-center rounded-full border border-[#37a47a]/15 bg-white text-[#37a47a] shadow-sm transition-colors hover:bg-[#e8f2ec]"
aria-label="Close impact details"
>
<X className="size-3.5" aria-hidden />
</button>
) : null}
<div className="flex shrink-0 items-center gap-1.5">
{viewHref ? (
<Button
variant="outline"
size="icon-sm"
className="rounded-full border-[#37a47a]/20 text-[#37a47a] hover:bg-[#e8f2ec]"
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-[#37a47a] text-white hover:bg-[#30614c]"
asChild
>
<Link href={donateHref} aria-label="Donate" {...externalLinkProps(donateHref)}>
<HandHeart className="size-4" />
</Link>
</Button>
) : null}
</div>
</div>
) : null}
</div>
);
}