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
33 changes: 19 additions & 14 deletions docs/amazon-insert-video.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,33 +52,36 @@ More generations → **more variety** in inserts, **not** denser inserts by defa

### Plain English

1. While someone browses a **product grid**, we sometimes drop a **fashion reel tile** into the stream — same size as a product card.
2. That tile is picked from the **full multi-generation pool** of category reels.
3. If they’re already in a room (e.g. Handbags), we **don’t** insert handbags reels — only **other** categories (cross-promo).
4. About **one reel every few products**, with a **cap** so the grid doesn’t turn into a video wall.
5. For a given list/filter, the mix is **stable for the UTC day** (not reshuffled every reload); **next day** it can reshuffle.
6. Tap → shop that category. Video is **muted**, loops, plays when on screen.
1. While someone browses a **product grid**, we drop **fashion reel tiles** into the stream — same size as a product card.
2. Pool = **category room reels** (all gens) **+ persona jet-set reels** (`REELS_JETSET`).
3. **Never repeat the same reel id on one page.**
4. Gaps between inserts are **random 2–6 products** (immersive / unpredictable).
5. If they’re already in a room (e.g. Handbags), we skip handbags reels — persona jet-set still allowed.
6. For a given list/filter, the mix is **stable for the UTC day**; **next day** it reshuffles.
7. Category reels → shop that room. **Jet-set persona reels → `/quiz` (vibe check).**
8. Video is **muted**, loops, plays when on screen.

### Where

| Surface | Density | Cap |
|---------|---------|-----|
| **Shop** grid | ~every **5** products | **6** inserts max |
| **Home** product rows | ~every **4** | **1–2** |
| **PDP** similar / also-like | ~every **3** | **2** |
| **Shop** grid | random **2–6** product gaps | **10** inserts max |
| **Home** product rows | random **2–6** | **2–4** |
| **PDP** similar / also-like | random **2–5** | **2** |
| Lists with **< 3** products | — | **no** inserts |

### Technical

| Piece | Detail |
|-------|--------|
| Pool | `CATEGORY_REELS` in `src/data/reels.ts` (all gens) |
| Interleave | `interleaveReelInserts` → `src/lib/reelInserts.ts` |
| Pool | `CATEGORY_REELS` = gen1 ∪ gen2 ∪ `REELS_JETSET` |
| Interleave | `interleaveReelInserts` → `src/lib/reelInserts.ts` (`minGap`/`maxGap`) |
| UI grid | `ProductGrid` + `ReelInsertCard` |
| Cross-promo | `excludeCategory` = current shop room / PDP category |
| Cross-promo | `excludeCategory` for room reels only |
| Persona link | `href: /quiz` on jet-set reels |
| Seed | `listName + exclude + YYYY-MM-DD (UTC) + product ids` |
| Click analytics | GA `reel_insert_click` |
| Full catalog | `/reels` (gen sections) |
| Full catalog | `/reels` (jet set + room sections) |

### Not a cron job

Expand All @@ -94,7 +97,9 @@ Shuffle is **computed on the client** when the grid renders. Same seed → same
| Gen 1 poster | `public/brand/videos/reels/posters/{category}.jpg` |
| Gen 2 video | `public/brand/videos/reels/{category}-g2.mp4` |
| Gen 2 poster | `public/brand/videos/reels/posters/{category}-g2.jpg` |
| R2 (optional) | `video/reels/{category}-g2.mp4` via mint — see [`imagine-r2-videos.md`](./imagine-r2-videos.md) |
| Jet-set video | `public/brand/videos/reels/jetset-{vibeId}.mp4` |
| Jet-set poster | `public/brand/videos/reels/posters/jetset-{vibeId}.jpg` |
| R2 (optional) | `video/…` via mint — see [`imagine-r2-videos.md`](./imagine-r2-videos.md) |

### Production pipeline

Expand Down
Binary file added public/brand/videos/reels/jetset-atelier.mp4
Binary file not shown.
Binary file added public/brand/videos/reels/jetset-dew.mp4
Binary file not shown.
Binary file added public/brand/videos/reels/jetset-gilded.mp4
Binary file not shown.
Binary file added public/brand/videos/reels/jetset-luxe.mp4
Binary file not shown.
Binary file added public/brand/videos/reels/jetset-muse.mp4
Binary file not shown.
Binary file added public/brand/videos/reels/jetset-sillage.mp4
Binary file not shown.
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.
12 changes: 11 additions & 1 deletion src/components/ProductGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ type Props = {
className?: string
compact?: boolean
every?: number
/** Min products between inserts (default 2) */
minGap?: number
/** Max products between inserts (default 6) */
maxGap?: number
maxInserts?: number
minProducts?: number
/** When false, pure product grid (default true) */
Expand All @@ -29,7 +33,7 @@ type Props = {

/**
* Product card grid with optional randomized fashion reel inserts.
* Insert density scales as CATEGORY_REELS grows.
* Gaps random 2–6 products; same reel never repeats on one page.
*/
export function ProductGrid({
products,
Expand All @@ -38,6 +42,8 @@ export function ProductGrid({
className = DEFAULT_COLS,
compact = false,
every,
minGap,
maxGap,
maxInserts,
minProducts,
inserts = true,
Expand All @@ -55,6 +61,8 @@ export function ProductGrid({
listName,
excludeCategory,
every,
minGap,
maxGap,
maxInserts,
minProducts,
}
Expand All @@ -64,6 +72,8 @@ export function ProductGrid({
listName,
excludeCategory,
every,
minGap,
maxGap,
maxInserts,
minProducts,
inserts,
Expand Down
46 changes: 29 additions & 17 deletions src/components/ReelInsertCard.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { Link } from 'react-router-dom'
import { Play, Sparkles } from 'lucide-react'
import { useEffect, useRef } from 'react'
import type { CategoryReel } from '../data/reels'
import {
reelCtaLabel,
reelHref,
type CategoryReel,
} from '../data/reels'
import { trackEvent } from '../lib/analytics'

/**
* Fashion film tile in product grids (same 4:5 well as ProductCard).
* Muted autoplay when in view; links to the related shop category.
* Muted autoplay when in view.
* Category reels → shop room; persona jet-set → vibe check (/quiz).
*/
export function ReelInsertCard({
reel,
Expand All @@ -18,7 +23,10 @@ export function ReelInsertCard({
compact?: boolean
}) {
const videoRef = useRef<HTMLVideoElement>(null)
const shopTo = `/shop?cat=${reel.category}`
const to = reelHref(reel)
const cta = reelCtaLabel(reel)
const isPersona = Boolean(reel.vibeId)
const kicker = reel.kicker ?? (isPersona ? 'House persona' : 'From the house')

useEffect(() => {
const el = videoRef.current
Expand All @@ -42,13 +50,15 @@ export function ReelInsertCard({
return (
<article className="card-soft group flex flex-col overflow-hidden ring-1 ring-bamboo/25">
<Link
to={shopTo}
to={to}
className="flex flex-col flex-1 min-h-0"
onClick={() =>
trackEvent('reel_insert_click', {
reel_category: reel.category,
reel_id: reel.id,
reel_category: reel.category ?? 'persona',
vibe_id: reel.vibeId,
list_name: listName,
engagement_type: 'cross_promo',
engagement_type: isPersona ? 'persona_jetset' : 'cross_promo',
})
}
>
Expand All @@ -68,7 +78,7 @@ export function ReelInsertCard({
<span className="absolute top-3 left-3 inline-flex items-center gap-1.5">
<span className="inline-flex items-center gap-1 rounded-full bg-white/95 text-ink text-[10px] font-bold uppercase tracking-wider px-2.5 py-1 shadow-sm border border-line/80">
<Sparkles className="size-3 text-bamboo" aria-hidden />
Discover
{isPersona ? 'Persona' : 'Discover'}
</span>
</span>
<span className="absolute top-3 right-3 inline-flex items-center justify-center size-8 rounded-full bg-black/45 text-white backdrop-blur-sm">
Expand All @@ -87,41 +97,43 @@ export function ReelInsertCard({
className={`flex flex-col flex-1 ${compact ? 'p-3.5 gap-1' : 'p-4 gap-1.5'}`}
>
<p className="text-[10px] font-semibold uppercase tracking-[0.14em] text-bamboo">
From the house
{kicker}
</p>
<h3
className={`font-display font-semibold leading-snug text-ink group-hover:text-bamboo transition ${
compact ? 'text-lg' : 'text-xl'
}`}
>
Shop {reel.title}
{isPersona ? 'Which woman are you today?' : cta}
</h3>
<p className="text-sm text-ink-soft line-clamp-2 leading-relaxed">
{reel.blurb}
</p>
<div className="mt-auto pt-3 flex items-end justify-between gap-2 border-t border-line/70">
<span className="text-xs font-semibold text-bamboo">
Open the room
</span>
<span className="text-xs font-semibold text-bamboo">{cta}</span>
<span className="text-[11px] font-semibold text-bamboo opacity-0 group-hover:opacity-100 transition">
Shop
Open
</span>
</div>
</div>
</Link>
<div className="px-4 pb-4">
<Link
to={shopTo}
to={to}
className="btn-primary !w-full !py-2.5 !text-xs text-center"
onClick={() =>
trackEvent('reel_insert_click', {
reel_category: reel.category,
reel_id: reel.id,
reel_category: reel.category ?? 'persona',
vibe_id: reel.vibeId,
list_name: listName,
engagement_type: 'cross_promo_cta',
engagement_type: isPersona
? 'persona_jetset_cta'
: 'cross_promo_cta',
})
}
>
Shop {reel.title}
{cta}
</Link>
</div>
</article>
Expand Down
Loading
Loading