Skip to content

Commit 95489a8

Browse files
committed
Add SpeakerCard
1 parent 0800a08 commit 95489a8

File tree

2 files changed

+148
-55
lines changed

2 files changed

+148
-55
lines changed
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import clsx from "clsx"
2+
import Image from "next-image-export-optimizer"
3+
import type { StaticImageData } from "next/image"
4+
5+
import TwitterXIcon from "@/icons/twitter.svg?svgr"
6+
import LinkedInIcon from "@/icons/linkedin.svg?svgr"
7+
import { eventsColors } from "../utils"
8+
9+
import { Anchor } from "../../_design-system/anchor"
10+
import { Tag } from "../../_design-system/tag"
11+
import { SchedSpeaker } from "../../2023/types"
12+
import {
13+
SocialMediaIcon,
14+
SocialMediaIconServiceType,
15+
} from "../../_components/speakers/social-media"
16+
17+
export interface SpeakerCardProps extends React.HTMLAttributes<HTMLDivElement> {
18+
imageUrl?: string | StaticImageData
19+
tags?: string[]
20+
isReturning?: boolean
21+
stripes?: string
22+
speaker: SchedSpeaker
23+
year: string
24+
}
25+
26+
function Stripes({ mask }: { mask?: string }) {
27+
return (
28+
<div
29+
role="presentation"
30+
className="pointer-events-none absolute inset-0 inset-y-[-20px]"
31+
style={{
32+
maskImage: mask,
33+
WebkitMaskImage: mask,
34+
}}
35+
>
36+
<div className="absolute inset-0 bg-gradient-to-b from-sec-dark/50 to-sec-light/50" />
37+
</div>
38+
)
39+
}
40+
41+
export function SpeakerCard({
42+
imageUrl,
43+
tags = [],
44+
className,
45+
speaker,
46+
year,
47+
...props
48+
}: SpeakerCardProps) {
49+
return (
50+
<article
51+
className={clsx(
52+
"relative flex flex-col overflow-hidden border border-neu-300",
53+
className,
54+
)}
55+
{...props}
56+
>
57+
<div className="flex gap-6 p-6">
58+
{imageUrl && (
59+
<div className="relative aspect-square size-[236px] shrink-0 overflow-hidden">
60+
<div className="absolute inset-0 z-[1] bg-sec-light opacity-90 mix-blend-multiply" />
61+
<Image
62+
src={imageUrl}
63+
alt=""
64+
width={312}
65+
height={312}
66+
className="size-full object-cover saturate-[0.1] transition-transform"
67+
/>
68+
<Stripes
69+
// TODO!
70+
mask={""}
71+
/>
72+
</div>
73+
)}
74+
<div className="flex flex-1 flex-col gap-2">
75+
<div className="flex flex-col gap-1">
76+
<h3 className="typography-body-lg">{speaker.name}</h3>
77+
<p className="text-neu-700 typography-body-sm">
78+
{[speaker.position, speaker.company].filter(Boolean).join(", ")}
79+
</p>
80+
</div>
81+
{speaker.about && (
82+
<p className="text-neu-700 typography-body-sm">{speaker.about}</p>
83+
)}
84+
{/* TODO: We'll have to collect it when fetching all sessions. */}
85+
{tags.length > 0 && (
86+
<div className="flex flex-wrap gap-2">
87+
{tags.map(tag => (
88+
<Tag color={eventsColors[tag] || "hsl(var(--color-sec-base))"}>
89+
{tag}
90+
</Tag>
91+
))}
92+
</div>
93+
)}
94+
<div className="flex gap-4">
95+
{speaker.socialurls?.length ? (
96+
<div className="mt-0 text-[#765e5e]">
97+
<div className="flex space-x-2">
98+
{speaker.socialurls.map(social => (
99+
<a
100+
key={social.url}
101+
href={social.url}
102+
target="_blank"
103+
rel="noreferrer"
104+
className="flex items-center text-blk"
105+
>
106+
<SocialMediaIcon
107+
service={
108+
social.service.toLowerCase() as SocialMediaIconServiceType
109+
}
110+
/>
111+
</a>
112+
))}
113+
</div>
114+
</div>
115+
) : null}
116+
</div>
117+
</div>
118+
</div>
119+
<Anchor
120+
href={`/conf/${year}/speakers/${speaker.username}`}
121+
className="absolute inset-0"
122+
title={`See ${speaker.name.split(" ")[0]}'s sessions`}
123+
/>
124+
</article>
125+
)
126+
}

src/app/conf/2025/schedule/[id]/page.tsx

Lines changed: 22 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { Tag } from "@/app/conf/_design-system/tag"
1919
import { eventsColors } from "../../utils"
2020
import { PinIcon } from "../../pixelarticons/pin-icon"
2121
import { CalendarIcon } from "../../pixelarticons/calendar-icon"
22+
import { SpeakerCard } from "../../components/speaker-card"
2223

2324
function getEventTitle(event: ScheduleSession, speakers: string[]): string {
2425
let { name } = event
@@ -80,14 +81,17 @@ export default function SessionPage({ params }: SessionProps) {
8081
)
8182

8283
return (
83-
<main className="gql-all-anchors-focusable gql-conf-container gql-conf-section">
84-
<NavbarPlaceholder className="top-0 bg-neu-0 before:bg-white/40 dark:bg-pri-darker dark:before:bg-blk/30" />
84+
<main className="gql-all-anchors-focusable">
85+
<NavbarPlaceholder className="top-0 bg-neu-0 before:bg-white/40 dark:bg-neu-0 dark:before:bg-blk/30" />
8586
<div className="gql-conf-container gql-conf-navbar-strip text-neu-900 before:bg-white/40 before:dark:bg-blk/30">
86-
<div className="py-10">
87+
<div className="mx-auto max-w-[1088px] py-10">
8788
<section className="mx-auto min-h-[80vh] flex-col justify-center px-2 sm:px-0 lg:justify-between">
8889
<SessionHeader event={event} eventTitle={eventTitle} />
8990
<SessionVideo event={event} eventTitle={eventTitle} />
90-
<p>{event.description}</p>
91+
<SessionSpeakers event={event} />
92+
<p className="py-8 typography-body-lg lg:py-10">
93+
{event.description}
94+
</p>
9195

9296
<div className="py-8">
9397
{event.files?.map(({ path }) => (
@@ -163,57 +167,20 @@ function SessionHeader({
163167
</div>
164168
<SessionTags session={event} />
165169
</div>
166-
167-
<div className="mt-8 flex flex-col flex-wrap gap-5 lg:flex-row">
168-
{event.speakers!.map(speaker => (
169-
<div
170-
className={`flex w-full items-center gap-3 ${event?.speakers?.length || 0 > 1 ? "max-w-[320px]" : ""}`}
171-
key={speaker.username}
172-
>
173-
<Avatar
174-
className="size-[100px] lg:size-[120px]"
175-
avatar={speaker.avatar}
176-
name={speaker.name}
177-
/>
178-
179-
<div className="flex flex-col gap-1.5 lg:gap-1">
180-
<a
181-
href={`/conf/2024/speakers/${speaker.username}`}
182-
className="mt-0 typography-body-lg"
183-
>
184-
{speaker.name}
185-
</a>
186-
187-
<span className="typography-body-sm">
188-
<span>{speaker.company}</span>
189-
{speaker.company && ", "}
190-
{speaker.position}
191-
</span>
192-
{speaker.socialurls?.length ? (
193-
<div className="mt-0 text-[#333333]">
194-
<div className="flex space-x-2">
195-
{speaker.socialurls.map(social => (
196-
<a
197-
key={social.url}
198-
href={social.url}
199-
target="_blank"
200-
rel="noreferrer"
201-
className="flex items-center text-blk"
202-
>
203-
<SocialMediaIcon
204-
service={
205-
social.service.toLowerCase() as SocialMediaIconServiceType
206-
}
207-
/>
208-
</a>
209-
))}
210-
</div>
211-
</div>
212-
) : null}
213-
</div>
214-
</div>
215-
))}
216-
</div>
217170
</header>
218171
)
219172
}
173+
174+
function SessionSpeakers({ event }: { event: ScheduleSession }) {
175+
return (
176+
<div className="mt-8 flex flex-col flex-wrap gap-5 lg:flex-row">
177+
{event.speakers!.map(speaker => (
178+
<SpeakerCard
179+
key={speaker.username}
180+
speaker={speaker}
181+
year="2025"
182+
/>
183+
))}
184+
</div>
185+
)
186+
}

0 commit comments

Comments
 (0)