feat(share): configurable call-to-action button on shared videos#1907
feat(share): configurable call-to-action button on shared videos#1907alexis-morain wants to merge 2 commits into
Conversation
Adds an optional CTA button rendered in the top-right of the video player on the public share page, similar to Loom. The owner configures a label and an https link from a dialog in the share header; viewers see the button and it opens the link in a new tab. The config is stored per video in the existing videos.metadata JSON column (no schema migration). URLs are validated server-side and must be https. - packages/database/types/metadata.ts: VideoCta type + metadata.cta field - actions/videos/edit-cta.ts: owner-only server action with https validation - _components/CtaButton.tsx: overlay anchor shown over the player - _components/CtaDialog.tsx: owner config dialog - wire cta through CapVideoPlayer and HLSVideoPlayer, plus a trigger in ShareHeader Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| Cancel | ||
| </Button> | ||
| <Button | ||
| size="sm" |
There was a problem hiding this comment.
Save enabled for non-https URLs
The Save button is disabled only when url is empty, but not when it contains a valid-looking non-https value (e.g. http://example.com). In that case the button is enabled, the server action runs, throws "CTA URL must start with https://", and the user sees a toast error. Adding !url.trim().startsWith("https://") to the disabled condition provides immediate inline feedback that matches the server-side constraint.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/web/app/s/[videoId]/_components/CtaDialog.tsx
Line: 127
Comment:
**Save enabled for non-https URLs**
The Save button is disabled only when `url` is empty, but not when it contains a valid-looking non-https value (e.g. `http://example.com`). In that case the button is enabled, the server action runs, throws "CTA URL must start with https://", and the user sees a toast error. Adding `!url.trim().startsWith("https://")` to the disabled condition provides immediate inline feedback that matches the server-side constraint.
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Addressed in 6a6b5dc: the Save button is now disabled when the URL does not start with https:// (the same validation editCta enforces server-side via new URL()).
| import { useEffect, useId, useState } from "react"; | ||
| import { toast } from "sonner"; | ||
| import { editCta } from "@/actions/videos/edit-cta"; | ||
|
|
There was a problem hiding this comment.
Duplicated
MAX_LABEL_LENGTH constant
The same value (40) is declared independently in CtaDialog.tsx and in apps/web/actions/videos/edit-cta.ts. If either is updated without touching the other, the maxLength HTML attribute on the input and the server-side slice(0, MAX_LABEL_LENGTH) will silently diverge, potentially allowing the client to accept labels that the server then truncates. Exporting the constant from one canonical location (e.g. the types package or the server action) and importing it in the dialog would keep them in sync.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/web/app/s/[videoId]/_components/CtaDialog.tsx
Line: 22
Comment:
**Duplicated `MAX_LABEL_LENGTH` constant**
The same value (`40`) is declared independently in `CtaDialog.tsx` and in `apps/web/actions/videos/edit-cta.ts`. If either is updated without touching the other, the `maxLength` HTML attribute on the input and the server-side `slice(0, MAX_LABEL_LENGTH)` will silently diverge, potentially allowing the client to accept labels that the server then truncates. Exporting the constant from one canonical location (e.g. the types package or the server action) and importing it in the dialog would keep them in sync.
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Addressed in 6a6b5dc: MAX_CTA_LABEL_LENGTH is now imported from @cap/database/types instead of being redeclared locally, so there is a single source of truth shared with the server action.
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
- disable Save when the URL is not https (immediate inline feedback matching the server-side constraint, instead of only erroring on submit) - dedupe the label length limit: export MAX_CTA_LABEL_LENGTH from @cap/database/types and use it in both the dialog and the server action Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
What
Adds an optional call-to-action button in the top-right of the video player on the public share page (
/s/[videoId]), similar to Loom's CTA. The video owner sets a label and anhttpslink; viewers see the button and clicking it opens the link in a new tab.Typical use: a "Book a meeting" / "Talk to sales" button linking to Cal.com, Calendly, etc.
How it works
Storage: a
ctaobject on the existingvideos.metadataJSON column. No schema migration.Config UI: a "Call to action" button in
ShareHeader(owner-only) opens a dialog to toggle, set the label, and set the URL.Server action
editCta: owner-only (ownerIdcheck), trims the label (max 40 chars), and validates the URL withnew URL()requiringhttps:. Disabling removes the key.Display: a small
CtaButtonoverlay anchor (target="_blank" rel="noopener noreferrer") rendered inside bothCapVideoPlayer(MP4) andHLSVideoPlayer(HLS), so it stays visible in fullscreen. Hidden in the background-preview variant.Notes
tsc -bpasses.Test plan
🤖 Generated with Claude Code
Greptile Summary
This PR adds a configurable call-to-action button to the public video share page, allowing owners to set a label and https URL that viewers see as an overlay in the top-right of the player.
edit-cta.ts): ownership check, https-only URL validation vianew URL(), label trim/truncation, andrevalidatePathon both the share and dashboard routes.CtaButton.tsx): rendered inside bothCapVideoPlayerandHLSVideoPlayer(guarded by!isBackgroundPreviewin the HLS path); usesrel="noopener noreferrer"and React's auto-escaping so no XSS surface.CtaDialog.tsx): owner-only entry point gated byisOwnerinShareHeader; state resets correctly on dialog open; two minor UX/maintenance issues flagged below.Confidence Score: 4/5
Safe to merge; the two findings are non-blocking quality notes in the dialog component only.
The core security path (ownership gate, https enforcement, XSS handling) is solid. The two findings are confined to CtaDialog.tsx: the Save button accepting non-https URLs before the server rejects them, and the duplicated MAX_LABEL_LENGTH constant that could silently drift. Neither affects correctness of the stored data or security.
apps/web/app/s/[videoId]/_components/CtaDialog.tsx — client-side URL validation and duplicated constant.
Important Files Changed
new URL(), label trimming, and cache revalidation. Logic is sound.rel="noopener noreferrer"and React's auto-escaping. Handles null/disabled cta gracefully.!isBackgroundPreview; prop threaded cleanly.isOwnerbranch. CtaDialog mounted unconditionally but open-state controlled; server re-validates ownership anyway.data.metadata?.ctato both video players; no issues.VideoCtainterface added alongsideVideoMetadata.ctaoptional field. Purely additive, no migration required.Prompt To Fix All With AI
Reviews (1): Last reviewed commit: "feat(share): add a configurable call-to-..." | Re-trigger Greptile