File size: 7,381 Bytes
8da7616
 
a733514
 
8da7616
a733514
 
8da7616
 
 
a733514
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
---
title: Grabby Voice
colorFrom: green
colorTo: yellow
sdk: docker
app_port: 7860
fullWidth: true
pinned: false
---

# Matcha Moments β€” frontend

Cafe-aesthetic, mobile-first PWA that walks a customer through a 5-clip guided
video review and, on submit, hands them a matcha redemption code.

This repo is a **standalone Next.js app**. It calls Humeo's deployed public
review APIs (`https://humeo.app/api/public/reviews/*`) β€” no backend changes
needed in the Humeo monorepo for v1.

The original Humeo codebase lives at `reference/` for type / pattern lookups.
It's gitignored, so it never ships with this repo.

---

## Tech stack

- **Next.js 14** (App Router) + TypeScript + Tailwind CSS
- **`@ffmpeg/ffmpeg`** (ffmpeg.wasm) β€” client-side concatenation of the 5 recorded clips into one video before upload
- **`getUserMedia` + `MediaRecorder`** β€” standard browser camera APIs (no native install required)
- **`zod`** β€” shared validation schemas, mirrors the ones in Humeo's `src/lib/reviews/types.ts`

Why a PWA over Expo: the customer-facing flow has to start in a tabletop QR scan
in a cafe. Asking the customer to install an app kills conversion. Browser-based
flow opens in 2 seconds, no install, works on iOS Safari and Android Chrome.

---

## What it does (5 screens)

1. `/` β€” QR landing context screen (dev-only; in prod, the cafe's QR deep-links straight to `/c/[slug]`)
2. `/c/[slug]` β€” Cafe landing: brand, big "Free matcha, on the house" headline, consent copy, primary CTA
3. `/c/[slug]/record` β€” Guided 5-clip recorder (video preview β†’ prompt card β†’ record button β†’ auto-advance)
4. `/preview` β€” ffmpeg.wasm stitches the clips, uploads to Humeo, polls submission status, shows the rendered preview
5. `/reward` β€” Confetti, reward code in a dark card, "show this screen to your server"

The `[slug]` route is a real Next.js dynamic segment that fetches its campaign
from `${NEXT_PUBLIC_HUMEO_API_URL}/api/public/reviews/campaign/[slug]`, the same
endpoint Humeo's existing public review flow already uses
(`reference/src/app/api/public/reviews/campaign/[slug]/route.ts`).

---

## Getting started

```bash

cp .env.example .env.local      # then edit NEXT_PUBLIC_HUMEO_API_URL if needed

npm install

npm run dev                     # http://localhost:3000

```

### Test on your phone (recommended)

`getUserMedia` only works on `https://` (or `http://localhost`). Easiest path:

```bash

npx ngrok http 3000

```

Then open the `https://*.ngrok-free.app` URL on your phone, scan the QR or load
`/c/sageandstone` directly. iOS Safari and Android Chrome will prompt for
camera and mic. Allow both.

---

## How it talks to Humeo

```

matcha-moments PWA                   Humeo backend (deployed)

-----------------                    ------------------------

GET  /c/[slug]            ──────►    GET  /api/public/reviews/campaign/[slug]

                          ◄──────    { id, slug, restaurantName, rulesConfig, ... }



stitch clips locally (ffmpeg.wasm)



POST /preview submit      ──────►    POST /api/public/reviews/submit

                                       FormData: video, slug, consentAccepted,

                                                 deviceKey, durationSeconds, tableId

                          ◄──────    { submissionId, status, decision, reward }



poll every 6s             ──────►    GET  /api/public/reviews/submission/[id]?slug=...

                          ◄──────    { status, decision, feedback, reward }

```

`src/lib/humeoApi.ts` is the only place that fetches from `humeo.app`. If
Humeo's `review_campaigns` row doesn't yet have `mode` / `prompts` / `theme`
columns, `getCampaign()` augments the response with a hardcoded fallback
prompts list β€” flagged with `TODO` so we drop it once the BE migration ships.

### Fields Humeo's BE will eventually need

To remove the fallback, Humeo's `review_campaigns` schema would add:
- `mode text` β€” `'single_take' | 'guided_clips'`
- `prompts jsonb` β€” array of `{ step, title, tip, camera, maxSeconds }`
- `theme text` β€” `'default' | 'cafe-cream'`

Until then the matcha-moments app silently injects the cafe defaults.

---

## Why client-side ffmpeg.wasm?

Humeo's `/api/public/reviews/submit` accepts a single video file. We want a
multi-clip guided UX without forking Humeo's submit flow. Stitching the 5
recordings in the browser solves that with zero backend changes.

Trade-offs:
- 8MB WASM download, lazy-loaded only after the customer finishes recording
- 3-6 seconds of stitch time on a modern phone for ~50s of total video
- `next.config.js` sets COOP/COEP headers (required for `SharedArrayBuffer`)

If cafe staff start hearing complaints about phone heat, swap to a
multi-clip upload + server-side ffmpeg endpoint. Humeo's worker
(`reference/src/lib/server/processInterview.ts`) already uses ffmpeg, so the
migration is mostly a new submit endpoint.

---

## Project layout

```

src/

  app/

    layout.tsx                    Fonts (Fraunces / DM Sans / DM Mono), global CSS

    page.tsx                      QR landing (dev-only)

    c/[slug]/

      page.tsx                    Server component β€” fetches campaign

      LandingClient.tsx           Cafe landing screen

      record/

        page.tsx                  Server component β€” fetches campaign

        GuidedRecordingClient.tsx 5-clip guided recorder

    preview/page.tsx              Stitch + upload + preview

    reward/page.tsx               Reward code reveal

    globals.css

  components/                     Button, MatchaCircle, ProgressPips, PromptCard,

                                  RecordButton, RecordingBadge, RenderShimmer,

                                  Confetti, RewardCard

  hooks/

    useGuidedRecording.ts         getUserMedia + MediaRecorder + hard cap

    useSubmissionPolling.ts       Mirrors Humeo's PublicReviewRecordingClient polling

  lib/

    humeoApi.ts                   Typed fetch wrapper for /api/public/reviews/*

    ffmpeg.ts                     ffmpeg.wasm concatClips() helper

    recordingStore.ts             In-tab clip store, useRecordingStore() hook

    utils.ts                      cn(), ensureDeviceKey()

    reviews/

      types.ts                    Zod schemas + types (mirrors Humeo's)

      public.ts                   Display helpers (verbatim from Humeo)

reference/                        Humeo's repo (gitignored, read-only library)

matcha-moments-prototype_2.html   Original wireframe (visual spec)

```

---

## Out of scope (v1)

- Per-clip re-record (current "re-record" wipes all 5 β€” flagged as a known
  product call in `matcha-moments-prototype_2.html` dev notes)
- Email-me-a-copy of the final video
- Staff-side redemption screen
- Reward expiry / redemption tracking
- Native iOS/Android wrapping (Humeo can ship this as a Capacitor or PWA-installed shortcut later)

---

## Deploying

Vercel β€” connect this repo, set `NEXT_PUBLIC_HUMEO_API_URL=https://humeo.app`,
done. The COOP/COEP headers in `next.config.js` carry over on Vercel.

CORS: confirm `humeo.app` allow-lists this app's deploy domain
(e.g. `matcha.humeo.app`) on the `/api/public/reviews/*` routes.