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>
|
Checking in on this one. It adds an optional Loom-style call-to-action button to the share page (owner sets a label + https link, stored on the existing Before going further: is this a feature the team would want upstream? If so I'm happy to add a short demo GIF and adjust the button placement / styling to match your design conventions. |
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.
Demo
The owner sets a label +
httpslink; viewers see the button in the top-right of the player (here linking to a booking page). Shown on the public share page: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