Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/amazon-insert-video.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ More generations → **more variety** in inserts, **not** denser inserts by defa

| Piece | Detail |
|-------|--------|
| Pool | `CATEGORY_REELS` = gen1 ∪ gen2 ∪ `REELS_JETSET``REELS_SKI` |
| Pool | `CATEGORY_REELS` = gen1 ∪ gen2 ∪ jet-setski ∪ Riviera |
| Interleave | `interleaveReelInserts` → `src/lib/reelInserts.ts` (`minGap`/`maxGap`) |
| UI grid | `ProductGrid` + `ReelInsertCard` |
| Cross-promo | `excludeCategory` for room reels only |
Expand All @@ -101,6 +101,8 @@ Shuffle is **computed on the client** when the grid renders. Same seed → same
| Jet-set poster | `public/brand/videos/reels/posters/jetset-{vibeId}.jpg` |
| Ski holiday video | `public/brand/videos/reels/ski-{vibeId}.mp4` |
| Ski holiday poster | `public/brand/videos/reels/posters/ski-{vibeId}.jpg` |
| Riviera video | `public/brand/videos/reels/riviera-{vibeId}.mp4` |
| Riviera poster | `public/brand/videos/reels/posters/riviera-{vibeId}.jpg` |
| R2 (optional) | `video/…` via mint — see [`imagine-r2-videos.md`](./imagine-r2-videos.md) |

### Rate limits (Imagine video)
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/brand/videos/reels/posters/ski-gilded.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/brand/videos/reels/riviera-atelier.mp4
Binary file not shown.
Binary file added public/brand/videos/reels/riviera-dew.mp4
Binary file not shown.
Binary file added public/brand/videos/reels/riviera-gilded.mp4
Binary file not shown.
Binary file added public/brand/videos/reels/riviera-luxe.mp4
Binary file not shown.
Binary file added public/brand/videos/reels/riviera-muse.mp4
Binary file not shown.
Binary file added public/brand/videos/reels/riviera-sillage.mp4
Binary file not shown.
Binary file modified public/brand/videos/reels/ski-gilded.mp4
Binary file not shown.
19 changes: 14 additions & 5 deletions src/components/ReelInsertCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,25 @@ export function ReelInsertCard({
const to = reelHref(reel)
const cta = reelCtaLabel(reel)
const series = reelSeries(reel)
const isPersona = series === 'jetset' || series === 'ski'
const isPersona =
series === 'jetset' || series === 'ski' || series === 'riviera'
const badge =
series === 'ski' ? 'Ski' : series === 'jetset' ? 'Persona' : 'Discover'
series === 'ski'
? 'Ski'
: series === 'riviera'
? 'Riviera'
: series === 'jetset'
? 'Persona'
: 'Discover'
const kicker =
reel.kicker ??
(series === 'ski'
? 'Ski holiday'
: series === 'jetset'
? 'House persona'
: 'From the house')
: series === 'riviera'
? 'Riviera'
: series === 'jetset'
? 'House persona'
: 'From the house')

useEffect(() => {
const el = videoRef.current
Expand Down
85 changes: 80 additions & 5 deletions src/data/reels.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Fashion reels for product-grid inserts and /reels.
*
* Pool includes category room clips + persona series (jet-set, ski holiday).
* Pool includes category room clips + persona series (jet-set, ski, Riviera).
* Product grids interleave unique reels with random 2–6 product gaps
* (see lib/reelInserts.ts).
*
Expand All @@ -12,11 +12,11 @@ import { CATEGORY_LABELS } from './catalog'
import { HEROES } from './categoryHeroes'
import { VIBE_LIST } from './vibes'

/** Creative wave — 1–2 rooms, 3 jet-set, 4 ski holiday */
export type ReelGeneration = 1 | 2 | 3 | 4
/** Creative wave — 1–2 rooms, 3 jet-set, 4 ski, 5 Riviera */
export type ReelGeneration = 1 | 2 | 3 | 4 | 5

/** Persona / room series for filters and badges */
export type ReelSeries = 'room' | 'jetset' | 'ski'
export type ReelSeries = 'room' | 'jetset' | 'ski' | 'riviera'

export type CategoryReel = {
/** Stable id for keys / analytics */
Expand Down Expand Up @@ -67,7 +67,7 @@ export const REEL_CATEGORIES: Category[] = [
]

/** Latest generation number in the catalog (bump when adding a wave). */
export const REEL_LATEST_GENERATION: ReelGeneration = 4
export const REEL_LATEST_GENERATION: ReelGeneration = 5

const MOTION_G1: Record<Category, string> = {
handbags: 'Soft light glide across structured leather',
Expand Down Expand Up @@ -285,6 +285,80 @@ export const REELS_SKI: CategoryReel[] = SKI.map((j) => {
}
})

/**
* Persona Riviera inserts — summer Mediterranean extravagance.
* Link to the vibe check (/quiz).
*/
const RIVIERA: {
vibeId: string
destination: string
motionLabel: string
blurb: string
}[] = [
{
vibeId: 'luxe',
destination: 'Cap Ferrat',
motionLabel: 'Infinity pool, cream silk',
blurb:
'Vivienne at the villa edge — gold low, sea glass calm, summer that never shouts.',
},
{
vibeId: 'muse',
destination: 'Mykonos night',
motionLabel: 'Fuchsia satin, island lights',
blurb:
'Camille in the whitewashed lane — soft glam against the Aegean after dark.',
},
{
vibeId: 'sillage',
destination: 'Positano sunset',
motionLabel: 'Cliff scent, violet silk',
blurb:
'Noor on the terrace — perfume first, pastel cliffs second. The trail holds in salt air.',
},
{
vibeId: 'atelier',
destination: 'Portofino harbor',
motionLabel: 'Ivory linen, gold bag',
blurb:
'Margot finishes the dock — Riva wood, structured gold, the harbor as runway.',
},
{
vibeId: 'dew',
destination: 'Santorini morning',
motionLabel: 'White linen, caldera light',
blurb:
'Isla on the terrace steps — barrier glow, blue domes, glass skin in thin sea air.',
},
{
vibeId: 'gilded',
destination: 'Saint-Tropez night',
motionLabel: 'Emerald on the jetty',
blurb:
'Aurelia at the yacht lights — bronze silk, emerald armor, collector summer.',
},
]

export const REELS_RIVIERA: CategoryReel[] = RIVIERA.map((j) => {
const vibe = VIBE_LIST.find((v) => v.id === j.vibeId)
const name = vibe?.avatar.name ?? j.vibeId
const personaTitle = vibe?.title ?? 'House persona'
return {
id: `riviera-${j.vibeId}`,
vibeId: j.vibeId,
series: 'riviera' as ReelSeries,
generation: 5 as ReelGeneration,
title: `${name} · ${j.destination}`,
blurb: j.blurb,
video: `/brand/videos/reels/riviera-${j.vibeId}.mp4`,
poster: `/brand/videos/reels/posters/riviera-${j.vibeId}.jpg`,
motionLabel: j.motionLabel,
href: '/quiz',
hrefLabel: 'Find your persona',
kicker: `${personaTitle} · Riviera`,
}
})

/**
* Full insert + /reels catalog (rooms + persona series).
* Product-grid randomizer uses this entire pool — unique per page.
Expand All @@ -294,6 +368,7 @@ export const CATEGORY_REELS: CategoryReel[] = [
...REELS_GEN2,
...REELS_JETSET,
...REELS_SKI,
...REELS_RIVIERA,
]

/** Resolve series for filters / badges */
Expand Down
14 changes: 12 additions & 2 deletions src/data/videoWatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
REELS_GEN1,
REELS_GEN2,
REELS_JETSET,
REELS_RIVIERA,
REELS_SKI,
reelCtaLabel,
reelHref,
Expand All @@ -25,7 +26,7 @@ export type WatchClip = {
poster?: string
aspect: WatchAspect
/** Filter chip */
group: 'category' | 'carpet' | 'cafe' | 'jetset' | 'ski' | 'all'
group: 'category' | 'carpet' | 'cafe' | 'jetset' | 'ski' | 'riviera' | 'all'
/** Optional deep link */
href?: string
hrefLabel?: string
Expand All @@ -34,7 +35,13 @@ export type WatchClip = {
function fromReel(reel: CategoryReel): WatchClip {
const series = reelSeries(reel)
const group =
series === 'ski' ? 'ski' : series === 'jetset' ? 'jetset' : 'category'
series === 'ski'
? 'ski'
: series === 'riviera'
? 'riviera'
: series === 'jetset'
? 'jetset'
: 'category'
return {
id: reel.id,
title: reel.title,
Expand Down Expand Up @@ -104,6 +111,7 @@ export const WATCH_CLIPS: WatchClip[] = [
export const WATCH_CATEGORY_CLIPS = WATCH_CLIPS.filter((c) => c.group === 'category')
export const WATCH_JETSET_CLIPS = WATCH_CLIPS.filter((c) => c.group === 'jetset')
export const WATCH_SKI_CLIPS = WATCH_CLIPS.filter((c) => c.group === 'ski')
export const WATCH_RIVIERA_CLIPS = WATCH_CLIPS.filter((c) => c.group === 'riviera')
export const WATCH_CARPET_CLIPS = WATCH_CLIPS.filter((c) => c.group === 'carpet')
export const WATCH_CAFE_CLIPS = WATCH_CLIPS.filter((c) => c.group === 'cafe')

Expand All @@ -113,11 +121,13 @@ export function watchClipCount() {
category: WATCH_CATEGORY_CLIPS.length,
jetset: WATCH_JETSET_CLIPS.length,
ski: WATCH_SKI_CLIPS.length,
riviera: WATCH_RIVIERA_CLIPS.length,
carpet: WATCH_CARPET_CLIPS.length,
cafe: WATCH_CAFE_CLIPS.length,
gen1: REELS_GEN1.length,
gen2: REELS_GEN2.length,
jetsetPool: REELS_JETSET.length,
skiPool: REELS_SKI.length,
rivieraPool: REELS_RIVIERA.length,
}
}
24 changes: 16 additions & 8 deletions src/pages/Reels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
REELS_GEN1,
REELS_GEN2,
REELS_JETSET,
REELS_RIVIERA,
REELS_SKI,
reelCtaLabel,
reelHref,
Expand All @@ -30,9 +31,9 @@ export function ReelsPage() {
Moving pictures
</h1>
<p className="mt-4 text-ink-soft max-w-xl leading-relaxed">
Ski holidays, jet-set arrivals, and short films from every room.
They appear in the shop scroll so the house finds you. Scroll to
watch.
Riviera summer, ski holidays, jet-set arrivalsand short films
from every room. They appear in the shop scroll so the house finds
you.
</p>
<div className="mt-6 flex flex-wrap gap-3">
<Link to="/quiz" className="btn-primary">
Expand All @@ -45,11 +46,16 @@ export function ReelsPage() {
</div>
</section>

<GenerationSection
label="Riviera"
blurb="Six house faces on the Mediterranean — Cap Ferrat, Mykonos, Positano, Portofino, Santorini, Saint-Tropez. Tap any film for the vibe check."
reels={REELS_RIVIERA}
featured
/>
<GenerationSection
label="Ski holiday"
blurb="Six house faces on extravagant alpine holidays — Courchevel, St. Moritz, Aspen, Gstaad, Zermatt, Verbier. Tap any film for the vibe check."
blurb="Alpine extravagance — Courchevel, St. Moritz, Aspen, Gstaad, Zermatt, Verbier."
reels={REELS_SKI}
featured
/>
<GenerationSection
label="Jet set"
Expand Down Expand Up @@ -153,9 +159,11 @@ function ReelCard({ reel }: { reel: CategoryReel }) {
<Play className="size-3 fill-current" aria-hidden />
{reelSeries(reel) === 'ski'
? 'Ski'
: reelSeries(reel) === 'jetset'
? 'Persona'
: 'Film'}
: reelSeries(reel) === 'riviera'
? 'Riviera'
: reelSeries(reel) === 'jetset'
? 'Persona'
: 'Film'}
</span>
</div>
<div className="absolute bottom-0 left-0 right-0 p-3.5 sm:p-4">
Expand Down
14 changes: 9 additions & 5 deletions src/pages/Watch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ import {
WATCH_CATEGORY_CLIPS,
WATCH_CLIPS,
WATCH_JETSET_CLIPS,
WATCH_RIVIERA_CLIPS,
WATCH_SKI_CLIPS,
watchClipCount,
type WatchClip,
} from '../data/videoWatch'
import { watchSeo } from '../lib/seoData'

type Filter = 'all' | 'category' | 'jetset' | 'ski' | 'carpet' | 'cafe'
type Filter = 'all' | 'category' | 'jetset' | 'ski' | 'riviera' | 'carpet' | 'cafe'

/**
* Visual short-film gallery — muted autoplay from the house library.
Expand All @@ -27,6 +28,7 @@ export function WatchPage() {
if (filter === 'category') return WATCH_CATEGORY_CLIPS
if (filter === 'jetset') return WATCH_JETSET_CLIPS
if (filter === 'ski') return WATCH_SKI_CLIPS
if (filter === 'riviera') return WATCH_RIVIERA_CLIPS
if (filter === 'carpet') return WATCH_CARPET_CLIPS
if (filter === 'cafe') return WATCH_CAFE_CLIPS
return WATCH_CLIPS
Expand All @@ -44,9 +46,8 @@ export function WatchPage() {
The house on film
</h1>
<p className="mt-4 text-ink-soft max-w-xl leading-relaxed">
{counts.total} short films — ski holidays, jet-set, the rooms, the
carpet, the café. Scroll to play. Sound stays off until you invite
it.
{counts.total} short films — Riviera summer, ski holidays, jet-set,
the rooms. Scroll to play. Sound stays off until you invite it.
</p>
<div className="mt-5 inline-flex items-center gap-2 rounded-full border border-line bg-card px-3.5 py-2 text-xs font-semibold text-ink-soft">
<VolumeX className="size-3.5" aria-hidden />
Expand All @@ -56,6 +57,7 @@ export function WatchPage() {
{(
[
['all', `All (${counts.total})`],
['riviera', `Riviera (${counts.riviera})`],
['ski', `Ski holiday (${counts.ski})`],
['jetset', `Jet set (${counts.jetset})`],
['category', `The rooms (${counts.category})`],
Expand Down Expand Up @@ -134,7 +136,9 @@ function WatchTile({ clip }: { clip: WatchClip }) {
? 'Jet set'
: clip.group === 'ski'
? 'Ski holiday'
: 'The rooms'
: clip.group === 'riviera'
? 'Riviera'
: 'The rooms'

const body = (
<>
Expand Down
Loading