Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 51 additions & 51 deletions scripts/sync-sched/schedule-2025.json

Large diffs are not rendered by default.

184 changes: 99 additions & 85 deletions scripts/sync-sched/speakers.json

Large diffs are not rendered by default.

66 changes: 38 additions & 28 deletions src/app/conf/2025/schedule/[id]/format-description.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,45 @@
import React from "react"

const URL_REGEX = /https?:\/\/[^\s]+/g
const LINK_REGEX = /<a\s+([^>]*href\s*=\s*[^>]*)>/gi

export function formatDescription(text: string): string {
// we coerce all existing anchor tags to have target="_blank" rel="noopener noreferrer" and typography-link class
const result = text.replace(LINK_REGEX, (_, attributes) => {
let attrs = attributes

if (!attrs.includes("target=")) {
attrs += ' target="_blank"'
}

if (!attrs.includes("rel=")) {
attrs += ' rel="noopener noreferrer"'
}

if (!attrs.includes("class=")) {
attrs += ' class="typography-link"'
} else if (!attrs.includes("typography-link")) {
attrs = attrs.replace(
/class\s*=\s*["']([^"']*)/gi,
'class="$1 typography-link',
)
}

return `<a ${attrs}>`
})

export function formatDescription(text: string): React.ReactNode {
const res: React.ReactNode[] = []
// then we format plain URLs that are not already inside an anchor tag
return result.replace(URL_REGEX, (url, offset) => {
const beforeUrl = result.slice(0, offset)
const afterUrl = result.slice(offset + url.length)

let lastIndex = 0
let match: RegExpExecArray | null
const lastOpenTag = beforeUrl.lastIndexOf("<")
const lastCloseTag = beforeUrl.lastIndexOf(">")
const nextCloseTag = afterUrl.indexOf(">")

while ((match = URL_REGEX.exec(text)) !== null) {
if (match.index > lastIndex) {
res.push(text.slice(lastIndex, match.index))
if (lastOpenTag > lastCloseTag && nextCloseTag !== -1) {
return url
}

res.push(
<a
href={match[0]}
target="_blank"
rel="noopener noreferrer"
className="typography-link"
>
{match[0].replace(/^https?:\/\//, "")}
</a>,
)

lastIndex = match.index + match[0].length
}

if (lastIndex < text.length) {
res.push(text.slice(lastIndex))
}

return <>{res}</>
const displayUrl = url.replace(/^https?:\/\//, "")
return `<a href="${url}" target="_blank" rel="noopener noreferrer" class="typography-link">${displayUrl}</a>`
})
}
28 changes: 18 additions & 10 deletions src/app/conf/2025/schedule/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,13 +104,17 @@ export default function SessionPage({ params }: SessionProps) {
</>
)}

<h3 className="typography-h2 my-8 max-w-[408px] px-2 sm:px-3 lg:my-16">
Session speakers
</h3>
<SessionSpeakers
session={session}
className="-mx-px -mb-px last:xl:pb-24"
/>
{!!session.speakers?.length && (
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cruise doesn't have speakers.

<>
<h3 className="typography-h2 my-8 max-w-[408px] px-2 sm:px-3 lg:my-16">
Session speakers
</h3>
<SessionSpeakers
session={session}
className="-mx-px -mb-px last:xl:pb-24"
/>
</>
)}

{!!session.files?.length && (
<>
Expand Down Expand Up @@ -255,9 +259,13 @@ function SessionDescription({ session }: { session: ScheduleSession }) {
return (
<div className="mt-8 flex gap-4 px-2 pb-8 max-lg:flex-col sm:px-3 lg:mt-16 lg:gap-8 xl:pb-16">
<h3 className="typography-h2 min-w-[320px]">Session description</h3>
<p className="typography-body-lg whitespace-pre-wrap">
{formattedDescription}
</p>
<p
className="typography-body-lg whitespace-pre-wrap"
dangerouslySetInnerHTML={{
// the description was partially sanitized when syncinc data from sched
__html: formattedDescription,
}}
/>
</div>
)
}
13 changes: 8 additions & 5 deletions src/app/conf/2025/schedule/_components/schedule-session-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
MenuItems,
Transition,
} from "@headlessui/react"
import { stripHtml } from "string-strip-html"

import { SchedSpeaker, ScheduleSession } from "@/app/conf/_api/sched-types"
import { Anchor } from "@/app/conf/_design-system/anchor"
Expand Down Expand Up @@ -132,10 +133,12 @@ export function ScheduleSessionCard({
</span>
)}
<span className="mt-4 flex items-center gap-2 xl:mt-6">
<span className="typography-body-xs flex items-center gap-0.5">
<PinIcon className="size-4 text-pri-base [@container(width<240px)]:hidden" />
{session.venue}
</span>
{session.venue && (
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cruise doesn't have a venue.

<span className="typography-body-xs flex items-center gap-0.5">
<PinIcon className="size-4 text-pri-base [@container(width<240px)]:hidden" />
{session.venue}
</span>
)}
{blockTimeFraction < 1 && (
<span className="typography-body-xs flex items-center gap-0.5">
<ClockIcon className="size-4 text-pri-base [@container(width<240px)]:hidden" />
Expand Down Expand Up @@ -176,7 +179,7 @@ function AddToCalendarLink({
title: eventTitle,
start: session.event_start,
end: session.event_end,
description: session.description,
description: stripHtml(session.description).result,
location: session.venue,
organizer: {
name: `GraphQLConf ${new Date().getFullYear()}`,
Expand Down
3 changes: 2 additions & 1 deletion src/app/conf/2025/speakers/[id]/long-session-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import React, { Fragment } from "react"
import { SessionTags } from "../../components/session-tags"
import { Menu, MenuItem, MenuItems, Transition } from "@headlessui/react"
import { MenuButton } from "@headlessui/react"
import { stripHtml } from "string-strip-html"

export interface LongSessionCardProps
extends React.HTMLAttributes<HTMLDivElement> {
Expand Down Expand Up @@ -172,7 +173,7 @@ function AddToCalendarLink({
title: eventTitle,
start: session.event_start,
end: session.event_end,
description: session.description,
description: stripHtml(session.description).result,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As session.description can have HTML now we need to get rid of it for calendar events.

location: session.venue,
organizer: {
name: `GraphQLConf ${new Date().getFullYear()}`,
Expand Down
15 changes: 12 additions & 3 deletions src/app/conf/_api/sched-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,9 @@ export async function getSchedule(
...session,
event_type,
event_subtype,
description: preprocessDescription(description),
description: preprocessDescription(description, {
allowSomeHtml: true,
}),
}
})

Expand Down Expand Up @@ -183,15 +185,22 @@ export async function getSpeakerDetails(
return shapeSpeaker(data as SchedSpeaker)
}

function preprocessDescription(description: string | undefined | null): string {
function preprocessDescription(
description: string | undefined | null,
options: { allowSomeHtml?: boolean } = {},
): string {
let res = description || ""

// we respect manual line breaks
res = res.replace(/<br\s*\/?>/g, "\n")

// respecting <li> and <a> tags doesn't make sense, because speakers don't use them consistently
// we'll improve how the descriptions look later down the tree in the session details page
return stripHtml(res).result
return stripHtml(res, {
ignoreTags: options.allowSomeHtml
? ["a", "b", "i", "em", "strong", "code", "pre", "ul", "ol", "li"]
: [],
}).result
}

function shapeSpeaker(user: SchedSpeaker): SchedSpeaker {
Expand Down
3 changes: 3 additions & 0 deletions src/app/conf/_api/sched-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ export type ScheduleSession = {
id: string
active: "Y" | "N"
audience: string
/**
* can include HTML tags
*/
description: string
event_end: string
event_start: string
Expand Down