mirror of
https://github.com/Such-Software/such.software.git
synced 2026-10-17 05:32:05 +02:00
feat(site): theming-morph showcase hero on /products/webshops
The 'render once, serve cheap pixels' showcase, live. A 7s reel of the same platform themed as five real demo brands (artisan -> merch -> crypto -> yoga -> coaching), inside a decorative browser frame, at the top of the webshops page. - ShowcaseVideo: poster-first, preload='none' (no download until played), plays only while in the viewport (IntersectionObserver), pauses offscreen, and prefers-reduced-motion -> poster only (video never loads). muted + playsInline for mobile autoplay-on-view. - BrowserFrame: static decorative chrome so the reel reads as a real shop. - 16:9 assets ~330-355KB (webm/mp4) + 50KB poster, all within the motion budget.
This commit is contained in:
@@ -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.
|
||||
</p>
|
||||
|
||||
{/* Theming morph: the same platform themed as five real demo brands. */}
|
||||
<figure className="mb-16">
|
||||
<BrowserFrame url="your-brand.com">
|
||||
<ShowcaseVideo
|
||||
webm="/showcase/theming-morph.webm"
|
||||
mp4="/showcase/theming-morph.mp4"
|
||||
poster="/showcase/theming-morph-poster.jpg"
|
||||
label="The same commerce platform themed as five distinct brands, dissolving one into the next: an artisan shop, a merch store, a crypto shop, a yoga studio, and a coaching site."
|
||||
className="block aspect-video w-full"
|
||||
/>
|
||||
</BrowserFrame>
|
||||
<figcaption className="mt-3 text-center text-sm text-muted-foreground">
|
||||
One platform, five real storefronts. Every shop here runs the same engine.
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
{/* Theming spotlight, the headline story */}
|
||||
<section aria-labelledby="theming-heading" className="section-container border-l-4 border-emerald-500 mb-16">
|
||||
<h2 id="theming-heading" className="text-2xl md:text-3xl font-bold mb-4">
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
className={`overflow-hidden rounded-xl border border-white/10 bg-slate-900/60 shadow-2xl ${className ?? ''}`}
|
||||
>
|
||||
<div
|
||||
className="flex items-center gap-2 border-b border-white/10 bg-slate-800/70 px-4 py-2.5"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span className="flex gap-1.5">
|
||||
<span className="h-3 w-3 rounded-full bg-red-400/70" />
|
||||
<span className="h-3 w-3 rounded-full bg-amber-400/70" />
|
||||
<span className="h-3 w-3 rounded-full bg-emerald-400/70" />
|
||||
</span>
|
||||
<span className="ml-2 flex-1 truncate rounded-md bg-slate-950/50 px-3 py-1 text-center text-xs text-slate-400">
|
||||
{url}
|
||||
</span>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<HTMLVideoElement>(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 (
|
||||
<video
|
||||
ref={ref}
|
||||
className={className}
|
||||
poster={poster}
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
preload="none"
|
||||
aria-label={label}
|
||||
>
|
||||
<source src={webm} type="video/webm" />
|
||||
<source src={mp4} type="video/mp4" />
|
||||
</video>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user