diff --git a/apps/web/public/showcase/theming-morph-poster.jpg b/apps/web/public/showcase/theming-morph-poster.jpg new file mode 100644 index 0000000..6314cb7 Binary files /dev/null and b/apps/web/public/showcase/theming-morph-poster.jpg differ diff --git a/apps/web/public/showcase/theming-morph.mp4 b/apps/web/public/showcase/theming-morph.mp4 new file mode 100644 index 0000000..0ba56fe Binary files /dev/null and b/apps/web/public/showcase/theming-morph.mp4 differ diff --git a/apps/web/public/showcase/theming-morph.webm b/apps/web/public/showcase/theming-morph.webm new file mode 100644 index 0000000..10f041a Binary files /dev/null and b/apps/web/public/showcase/theming-morph.webm differ diff --git a/apps/web/src/app/products/webshops/page.tsx b/apps/web/src/app/products/webshops/page.tsx index e58ce7e..3e4c324 100644 --- a/apps/web/src/app/products/webshops/page.tsx +++ b/apps/web/src/app/products/webshops/page.tsx @@ -1,6 +1,8 @@ import { Header } from "@/components/layout/header"; import { MobileNav } from "@/components/layout/mobile-nav"; import { DemosGallery } from "@/components/products/demos-gallery"; +import { ShowcaseVideo } from "@/components/showcase/showcase-video"; +import { BrowserFrame } from "@/components/showcase/browser-frame"; import { Card } from "@repo/ui/components/card"; import { Button } from "@repo/ui/components/button"; import { JsonLd, breadcrumbLd, faqLd } from "@/components/seo/json-ld"; @@ -110,6 +112,22 @@ export default function WebshopsPage() { maintain it on revenue share, so we only win when you make sales.

+ {/* Theming morph: the same platform themed as five real demo brands. */} +
+ + + +
+ One platform, five real storefronts. Every shop here runs the same engine. +
+
+ {/* Theming spotlight, the headline story */}

diff --git a/apps/web/src/components/showcase/browser-frame.tsx b/apps/web/src/components/showcase/browser-frame.tsx new file mode 100644 index 0000000..359019a --- /dev/null +++ b/apps/web/src/components/showcase/browser-frame.tsx @@ -0,0 +1,37 @@ +import type { ReactNode } from 'react'; + +/** + * A lightweight, purely-decorative browser chrome (title bar + traffic lights + + * a URL pill) to wrap a storefront screenshot or the theming-morph clip, so it + * reads as "a real shop in a real browser." Static CSS, no JS. + */ +export function BrowserFrame({ + url = 'your-brand.com', + children, + className, +}: { + url?: string; + children: ReactNode; + className?: string; +}) { + return ( +
+ + {children} +
+ ); +} diff --git a/apps/web/src/components/showcase/showcase-video.tsx b/apps/web/src/components/showcase/showcase-video.tsx new file mode 100644 index 0000000..01c2c44 --- /dev/null +++ b/apps/web/src/components/showcase/showcase-video.tsx @@ -0,0 +1,70 @@ +'use client'; + +import { useEffect, useRef, useState } from 'react'; + +type ShowcaseVideoProps = { + webm: string; + mp4: string; + poster: string; + /** Accessible description of what the clip shows. */ + label: string; + className?: string; +}; + +/** + * Perf-safe showcase video. The studio's motion budget, enforced in one place: + * - The poster paints instantly; the video is `preload="none"`, so it + * downloads NOTHING until it is actually played. + * - It only plays while in the viewport (IntersectionObserver) and pauses + * when scrolled away or the tab is hidden. No offscreen decoders. + * - `prefers-reduced-motion`: the video is never played, so with + * `preload="none"` it is never even downloaded. The poster is the whole + * experience for those visitors. + * - `muted` + `playsInline` so mobile autoplay-on-view is permitted (one + * video at a time; this component is for a single hero/section clip, not a + * grid — a grid would gate to one visible at a time). + */ +export function ShowcaseVideo({ webm, mp4, poster, label, className }: ShowcaseVideoProps) { + const ref = useRef(null); + const [allowMotion, setAllowMotion] = useState(true); + + useEffect(() => { + setAllowMotion(!window.matchMedia('(prefers-reduced-motion: reduce)').matches); + }, []); + + useEffect(() => { + const v = ref.current; + if (!v || !allowMotion || typeof IntersectionObserver === 'undefined') return; + const io = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + // .play() on a preload="none" element triggers the deferred load, + // then plays once buffered. Ignore the rejection browsers throw when + // a play() is interrupted by a quick pause() on fast scroll. + v.play().catch(() => {}); + } else { + v.pause(); + } + }, + { threshold: 0.25 }, + ); + io.observe(v); + return () => io.disconnect(); + }, [allowMotion]); + + return ( + + ); +}