Skip to content

Add talk thumbnail rendering for slides and download#75

Open
samholmes wants to merge 3 commits intomainfrom
sam/talk-thumbnails
Open

Add talk thumbnail rendering for slides and download#75
samholmes wants to merge 3 commits intomainfrom
sam/talk-thumbnails

Conversation

@samholmes
Copy link
Contributor

@samholmes samholmes commented Feb 10, 2026

Note

Medium Risk
Touches talk submission persistence (talk_hook) and adds client-side image fetching/rasterization for downloads, which may surface runtime/CORS issues or require matching DB schema changes.

Overview
Adds a new TalkThumbnail client component that renders a DEVx-styled 16:9 SVG thumbnail (background + hook text + optional circular speaker photo + branding) with basic text wrapping.

Updates submit-talk to collect an optional "Hook" string, fetch profile_photo for the logged-in user, preview the generated thumbnail in-form, and persist the hook via the new talk_hook field on talk_submissions.

Introduces a new talk-thumbnail-gen page that can load a photo via file upload or Supabase handle lookup and export the rendered SVG as downloadable PNG/JPG by embedding image assets as data URLs before rasterizing to canvas. Also bumps the Supabase CLI version and adds a dev:supabase script; includes a small JSON formatting change in a slides metadata file.

Written by Cursor Bugbot for commit 399b3a6. This will update automatically on new commits. Configure here.

img.src = svgBlobUrl
},
[talkTitle]
)
Copy link

Choose a reason for hiding this comment

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

SVG percentage dimensions cause blurry rasterized downloads

High Severity

The TalkThumbnail SVG uses width="100%" and height="100%", and buildEmbeddedSvgString clones this SVG as-is. When the serialized SVG is loaded as a standalone image via blob URL for canvas rasterization, percentage dimensions have no parent to resolve against. Per the CSS spec, the browser falls back to a default object size (typically 300×150), so the SVG is rasterized at very low resolution and then upscaled to 1280×720 on the canvas, producing a blurry downloaded image. The cloned SVG needs explicit pixel width and height attributes before serialization.

Additional Locations (1)

Fix in Cursor Fix in Web

{profilePhotoUrl ? (
<clipPath id="thumbPhotoClip">
<circle cx={photoCx} cy={photoCy} r={photoRadius} />
</clipPath>
Copy link

Choose a reason for hiding this comment

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

Hardcoded SVG clipPath ID causes multi-instance collision

Medium Severity

The clipPath uses a hardcoded id="thumbPhotoClip". Since TalkThumbnail is an exported reusable component, rendering multiple instances on the same page (e.g., a talk listing) would create duplicate DOM IDs. The clipPath="url(#thumbPhotoClip)" reference would resolve to the first instance's clip circle, causing all other thumbnails' photos to be clipped incorrectly. A unique ID per instance (e.g., using useId) would fix this.

Fix in Cursor Fix in Web

Copy link
Contributor

@tomatrow tomatrow left a comment

Choose a reason for hiding this comment

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

Only issue might be lack of speaker name on the talk card.

Looks good to me otherwise.

Rename the TalkThumbnail `talkTitle` prop to `hook` to better reflect
its purpose as short, punchy thumbnail text. Add a dedicated Hook field
to the talk submission form (defaults to talk title via placeholder) and
persist it as `talk_hook` in the database with a 50-char limit.
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 5 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

)
}

img.src = svgBlobUrl
Copy link

Choose a reason for hiding this comment

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

Failed downloads leak blob URLs

Low Severity

downloadRaster revokes svgBlobUrl only inside img.onload. If SVG image decoding fails, that URL is never revoked, so repeated failed exports accumulate unreleased blob URLs in memory.

Fix in Cursor Fix in Web

.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "")
.slice(0, 50)
return `${slug}-thumbnail.${ext}`
Copy link

Choose a reason for hiding this comment

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

Sanitized filename can become invalidly empty

Low Severity

makeFilename sanitizes title into slug but does not recover when sanitization produces an empty string. For whitespace or punctuation-only hooks, downloads get filenames like -thumbnail.png, which is malformed and inconsistent.

Fix in Cursor Fix in Web


const blobUrl = URL.createObjectURL(file)
setPhotoUrl(blobUrl)
setPhotoSource("upload")
Copy link

Choose a reason for hiding this comment

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

Upload blob URL lacks unmount cleanup

Low Severity

handleFileUpload creates a blob URL with URL.createObjectURL, but there is no component-unmount cleanup path. If users navigate away without calling clearPhoto, the blob URL stays allocated in the SPA session and can accumulate memory over repeated visits.

Fix in Cursor Fix in Web

}
setPhotoUrl(profile.profile_photo)
setPhotoSource("handle")
} else {
Copy link

Choose a reason for hiding this comment

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

Lookup path leaves stale file input value

Low Severity

When handleLookup switches photo source to a handle image, it does not clear fileInputRef.current.value. The file input can still hold the old upload, so re-selecting that same file may not trigger onChange, leaving photo replacement seemingly broken.

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants