Files
sharper.ly/apps/web/app/components/ui/Card/CardTeam.tsx
T

138 lines
4.3 KiB
TypeScript
Raw Normal View History

2026-09-11 02:46:56 +02:00
/* ============================================================================
Card.Team - Figma 25:765 (280 x 295)
----------------------------------------------------------------------------
Portrait in an accent frame, name, role, affiliation and up to three social
links. The social links are real links, so the card itself is only stretched
over a link when `href` is given - and the links are lifted above the
overlay so they stay clickable.
USAGE
-----
<ul className="grid gap-lg sm:grid-cols-2 lg:grid-cols-4">
<Card.Team
as="li"
name="Dr. Helen Vance"
role="Chief Programme Director"
affiliation="SHARP-ER Governance Initiative"
portrait={{ src: "/media/team/helen-vance.jpg", alt: "" }}
socials={[
{ kind: "linkedin", href: "https://www.linkedin.com/in/example" },
{ kind: "mail", href: "mailto:helen.vance@example.org", label: "Email" },
]}
/>
</ul>
// Optional bio page - the whole card becomes clickable, socials still work.
<Card.Team href="/en/about/helen-vance" name="Dr. Helen Vance" … />
========================================================================= */
import { CardLink, CardMedia, CardShell, CardIcon, cx } from "./CardShell";
import type { CardElement, CardIconName, CardImage, CardTitleElement } from "./types";
2026-09-23 22:33:58 +02:00
export type CardTeamSocialKind = "linkedin" | "mail";
2026-09-11 02:46:56 +02:00
export interface CardTeamSocial {
kind: CardTeamSocialKind;
href: string;
/**
* Accessible name of the link. Defaults to the platform name; pass a
2026-09-23 22:33:58 +02:00
* translated string on Arabic pages ("البريد الإلكتروني").
2026-09-11 02:46:56 +02:00
*/
label?: string;
}
export interface CardTeamProps {
name: string;
role?: string;
/** Team, unit or initiative the person belongs to. */
affiliation?: string;
portrait?: CardImage;
socials?: CardTeamSocial[];
/** Optional bio page. Given, the whole card links to it. */
href?: string;
as?: CardElement;
titleAs?: CardTitleElement;
className?: string;
}
const SOCIAL_ICON: Record<CardTeamSocialKind, CardIconName> = {
linkedin: "linkedin",
mail: "mail",
};
const SOCIAL_LABEL: Record<CardTeamSocialKind, string> = {
linkedin: "LinkedIn",
mail: "Email",
};
export function CardTeam({
name,
role,
affiliation,
portrait,
socials,
href,
as = "article",
titleAs = "h3",
className,
}: CardTeamProps) {
const Title = titleAs;
return (
<CardShell
as={as}
linked={Boolean(href)}
className={cx("flex-col items-center gap-5 p-xl text-center", className)}
>
{/* 96px portrait inside the 4px accent frame, exactly as drawn. */}
{portrait ? (
<CardMedia
image={portrait}
className="size-24 shrink-0 rounded-surface border-4 border-border-accent"
/>
) : null}
<div className="flex w-full flex-col gap-xs">
<Title className="sh-card-title body-md-bold text-text-primary">
{href ? (
<CardLink href={href} className="sh-card-link">
{name}
</CardLink>
) : (
name
)}
</Title>
{role ? <p className="body-sm text-text-secondary">{role}</p> : null}
{affiliation ? (
<p className="body-sm text-text-accent">{affiliation}</p>
) : null}
</div>
{socials && socials.length > 0 ? (
/* sh-card-above keeps these clickable through the stretched-link
overlay when the card as a whole is a link. */
<ul className="sh-card-above flex items-start justify-center gap-sm">
{socials.map((social) => (
<li key={`${social.kind}-${social.href}`}>
{/* Figma strokes these icons in text/accent on a background/accent
chip - green on green, invisible. Painted with the always-white
on-accent role instead. */}
<CardLink
href={social.href}
className="flex size-8 items-center justify-center rounded-surface bg-surface-accent text-text-on-accent"
>
<CardIcon name={SOCIAL_ICON[social.kind]} size={14} />
<span className="sr-only">
{`${social.label ?? SOCIAL_LABEL[social.kind]} - ${name}`}
</span>
</CardLink>
</li>
))}
</ul>
) : null}
</CardShell>
);
}