From 1b29b1d10428ce1a5b755503309cb3c052d6a0e4 Mon Sep 17 00:00:00 2001 From: jmartin Date: Fri, 24 Jul 2026 12:21:03 -0400 Subject: [PATCH] Handle Vimeo embed privacy for non-embeddable clips O3r and future imports with embed_privacy=nowhere show a poster link to Vimeo instead of a blocked iframe player. --- client/src/pages/ProjectPage.scss | 70 ++++++++++++++++++++++++++ client/src/pages/ProjectPage.tsx | 76 +++++++++++++++++++++-------- server/src/services/vimeo-import.ts | 25 ++++++++-- 3 files changed, 149 insertions(+), 22 deletions(-) diff --git a/client/src/pages/ProjectPage.scss b/client/src/pages/ProjectPage.scss index 4b0233c..780011e 100644 --- a/client/src/pages/ProjectPage.scss +++ b/client/src/pages/ProjectPage.scss @@ -60,12 +60,14 @@ border-radius: var(--radius); overflow: hidden; border: 1px solid var(--border); + position: relative; iframe, video { width: 100%; height: 100%; border: 0; + display: block; } p { @@ -77,6 +79,74 @@ } } + &__video-link { + display: block; + position: relative; + width: 100%; + height: 100%; + color: #fff; + text-decoration: none; + } + + &__video-poster { + width: 100%; + height: 100%; + object-fit: cover; + display: block; + opacity: 0.85; + transition: opacity 0.2s; + + &--empty { + background: linear-gradient(145deg, #141416, #0c0c0e); + } + + .project-page__video-link:hover & { + opacity: 1; + } + } + + &__video-cta { + position: absolute; + inset: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.65rem; + font-weight: 500; + font-size: 1rem; + text-shadow: 0 2px 12px rgba(0, 0, 0, 0.7); + background: rgba(0, 0, 0, 0.25); + transition: background 0.2s; + padding: 1rem; + text-align: center; + + .project-page__video-link:hover & { + background: rgba(0, 0, 0, 0.4); + } + } + + &__play { + display: grid; + place-items: center; + width: 56px; + height: 56px; + border-radius: 50%; + background: var(--accent); + color: #0a0a0b; + font-size: 1.1rem; + padding-left: 3px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4); + } + + &__video-note { + font-size: 0.75rem; + font-weight: 400; + color: rgba(255, 255, 255, 0.75); + max-width: 22rem; + line-height: 1.35; + } + &__gallery { display: grid; grid-template-columns: 1fr; diff --git a/client/src/pages/ProjectPage.tsx b/client/src/pages/ProjectPage.tsx index 8cf3e51..e4922ba 100644 --- a/client/src/pages/ProjectPage.tsx +++ b/client/src/pages/ProjectPage.tsx @@ -60,6 +60,10 @@ export function ProjectPage() { const images = (project.media || []).filter((m) => m.type === "image"); const videos = (project.media || []).filter((m) => m.type === "video"); const slides = images.map((m) => ({ src: mediaSrc(m.url) })); + const poster = + mediaSrc(project.thumbnail?.thumbnailUrl || project.thumbnail?.url) || + mediaSrc(images[0]?.thumbnailUrl || images[0]?.url) || + ""; return (
@@ -96,25 +100,59 @@ export function ProjectPage() { {videos.length > 0 && (
- {videos.map((v) => ( -
- {v.videoSource === "youtube" || v.videoSource === "vimeo" ? ( -