moonlantern1 commited on
Commit
80ef9e5
·
1 Parent(s): 82786cc

Move Foodstar handles to reward

Browse files
src/app/api/public/reviews/submission/[submissionId]/route.ts CHANGED
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
2
  import {
3
  getSubmission,
4
  toPublicSubmitResult,
 
5
  } from '@/lib/server/reviewStore';
6
 
7
  export const dynamic = 'force-dynamic';
@@ -9,11 +10,17 @@ export const runtime = 'nodejs';
9
 
10
  type Params = { params: { submissionId: string } };
11
 
12
- const sanitizeText = (value: string | null, max = 200) => {
13
  if (typeof value !== 'string') return '';
14
  return value.trim().slice(0, max);
15
  };
16
 
 
 
 
 
 
 
17
  /**
18
  * GET /api/public/reviews/submission/[submissionId]?slug=…
19
  *
@@ -33,3 +40,27 @@ export async function GET(req: NextRequest, { params }: Params) {
33
 
34
  return NextResponse.json({ submission: toPublicSubmitResult(result.submission) });
35
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  import {
3
  getSubmission,
4
  toPublicSubmitResult,
5
+ updateSubmissionSocialHandles,
6
  } from '@/lib/server/reviewStore';
7
 
8
  export const dynamic = 'force-dynamic';
 
10
 
11
  type Params = { params: { submissionId: string } };
12
 
13
+ const sanitizeText = (value: unknown, max = 200) => {
14
  if (typeof value !== 'string') return '';
15
  return value.trim().slice(0, max);
16
  };
17
 
18
+ function normalizeHandle(value: unknown) {
19
+ const trimmed = sanitizeText(value, 80);
20
+ if (!trimmed) return null;
21
+ return trimmed.startsWith('@') ? trimmed : `@${trimmed}`;
22
+ }
23
+
24
  /**
25
  * GET /api/public/reviews/submission/[submissionId]?slug=…
26
  *
 
40
 
41
  return NextResponse.json({ submission: toPublicSubmitResult(result.submission) });
42
  }
43
+
44
+ export async function PATCH(req: NextRequest, { params }: Params) {
45
+ const body = (await req.json().catch(() => ({}))) as {
46
+ slug?: unknown;
47
+ instagramHandle?: unknown;
48
+ tiktokHandle?: unknown;
49
+ };
50
+ const slug = sanitizeText(body.slug, 120);
51
+ if (!slug) {
52
+ return NextResponse.json({ error: 'Missing campaign slug' }, { status: 400 });
53
+ }
54
+
55
+ const result = await updateSubmissionSocialHandles({
56
+ submissionId: params.submissionId,
57
+ slug,
58
+ instagramHandle: normalizeHandle(body.instagramHandle),
59
+ tiktokHandle: normalizeHandle(body.tiktokHandle),
60
+ });
61
+ if (!result.ok) {
62
+ return NextResponse.json({ error: result.error }, { status: result.status });
63
+ }
64
+
65
+ return NextResponse.json({ submission: toPublicSubmitResult(result.submission) });
66
+ }
src/app/c/[slug]/LandingClient.tsx CHANGED
@@ -6,7 +6,6 @@ import { Button } from '@/components/Button';
6
  import { MatchaCircle } from '@/components/MatchaCircle';
7
  import { MobileShell } from '@/components/MobileShell';
8
  import { recordingStore } from '@/lib/recordingStore';
9
- import { socialHandleSummary } from '@/lib/foodstar';
10
  import type { PublicReviewCampaign } from '@/lib/reviews/types';
11
 
12
  type Props = {
@@ -18,8 +17,6 @@ type Props = {
18
  export function LandingClient({ slug, tableId, campaign }: Props) {
19
  const router = useRouter();
20
  const [consentAccepted, setConsentAccepted] = useState(false);
21
- const [instagramHandle, setInstagramHandle] = useState('');
22
- const [tiktokHandle, setTiktokHandle] = useState('');
23
  const isFoodstar = campaign.theme === 'foodstar-humeo';
24
 
25
  useEffect(() => {
@@ -28,15 +25,6 @@ export function LandingClient({ slug, tableId, campaign }: Props) {
28
 
29
  const handleStart = () => {
30
  if (!consentAccepted) return;
31
- const instagram = normalizeHandle(instagramHandle);
32
- const tiktok = normalizeHandle(tiktokHandle);
33
- recordingStore.setMeta({
34
- slug,
35
- tableId,
36
- instagramHandle: instagram,
37
- tiktokHandle: tiktok,
38
- socialHandle: socialHandleSummary(instagram, tiktok),
39
- });
40
  window.sessionStorage.setItem(`matcha-moments-consent:${slug}`, 'true');
41
  const tableQuery = tableId ? `?t=${encodeURIComponent(tableId)}` : '';
42
  router.push(`/c/${encodeURIComponent(slug)}/record${tableQuery}`);
@@ -74,33 +62,7 @@ export function LandingClient({ slug, tableId, campaign }: Props) {
74
  </p>
75
  </div>
76
 
77
- <div className="mt-6 rounded-[8px] border border-[#706A5E] bg-[#15130F] p-4 shadow-[inset_0_1px_0_rgba(248,243,231,0.06)] [@media(max-height:740px)]:mt-4 [@media(max-height:740px)]:p-3">
78
- <div className="font-mono text-[10px] uppercase tracking-[0.18em] text-[#AEB9A0]">
79
- Tag me
80
- </div>
81
- <div className="mt-3 grid gap-2.5">
82
- <input
83
- value={instagramHandle}
84
- onChange={(event) => setInstagramHandle(event.target.value)}
85
- placeholder="Instagram handle"
86
- className="h-12 rounded-full border border-[#3D392F] bg-[#201E18] px-4 text-[14px] text-[#DCD4C4] outline-none selection:bg-[#A9B898] selection:text-[#12130F] placeholder:text-[#81796A] focus:border-[#9EAD8E] focus:bg-[#232018]"
87
- autoCapitalize="none"
88
- autoCorrect="off"
89
- inputMode="text"
90
- />
91
- <input
92
- value={tiktokHandle}
93
- onChange={(event) => setTiktokHandle(event.target.value)}
94
- placeholder="TikTok handle"
95
- className="h-12 rounded-full border border-[#3D392F] bg-[#201E18] px-4 text-[14px] text-[#DCD4C4] outline-none selection:bg-[#A9B898] selection:text-[#12130F] placeholder:text-[#81796A] focus:border-[#9EAD8E] focus:bg-[#232018]"
96
- autoCapitalize="none"
97
- autoCorrect="off"
98
- inputMode="text"
99
- />
100
- </div>
101
- </div>
102
-
103
- <label className="mt-4 flex cursor-pointer items-start gap-3 rounded-[8px] border border-[#3D392F] bg-[#15130F] px-4 py-3 text-left">
104
  <input
105
  type="checkbox"
106
  checked={consentAccepted}
@@ -201,9 +163,3 @@ export function LandingClient({ slug, tableId, campaign }: Props) {
201
  </MobileShell>
202
  );
203
  }
204
-
205
- function normalizeHandle(value: string) {
206
- const trimmed = value.trim();
207
- if (!trimmed) return '';
208
- return trimmed.startsWith('@') ? trimmed : `@${trimmed}`;
209
- }
 
6
  import { MatchaCircle } from '@/components/MatchaCircle';
7
  import { MobileShell } from '@/components/MobileShell';
8
  import { recordingStore } from '@/lib/recordingStore';
 
9
  import type { PublicReviewCampaign } from '@/lib/reviews/types';
10
 
11
  type Props = {
 
17
  export function LandingClient({ slug, tableId, campaign }: Props) {
18
  const router = useRouter();
19
  const [consentAccepted, setConsentAccepted] = useState(false);
 
 
20
  const isFoodstar = campaign.theme === 'foodstar-humeo';
21
 
22
  useEffect(() => {
 
25
 
26
  const handleStart = () => {
27
  if (!consentAccepted) return;
 
 
 
 
 
 
 
 
 
28
  window.sessionStorage.setItem(`matcha-moments-consent:${slug}`, 'true');
29
  const tableQuery = tableId ? `?t=${encodeURIComponent(tableId)}` : '';
30
  router.push(`/c/${encodeURIComponent(slug)}/record${tableQuery}`);
 
62
  </p>
63
  </div>
64
 
65
+ <label className="mt-8 flex cursor-pointer items-start gap-3 rounded-[8px] border border-[#3D392F] bg-[#15130F] px-4 py-3 text-left">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  <input
67
  type="checkbox"
68
  checked={consentAccepted}
 
163
  </MobileShell>
164
  );
165
  }
 
 
 
 
 
 
src/app/preview/page.tsx CHANGED
@@ -48,7 +48,7 @@ export default function PreviewPage() {
48
  const submissionId =
49
  phase.kind === 'polling' || phase.kind === 'ready' ? phase.result.submissionId : null;
50
 
51
- const { result: pollResult } = useSubmissionPolling(submissionId, store.slug, 1500);
52
 
53
  useEffect(() => {
54
  if (!pollResult) return;
@@ -122,9 +122,6 @@ export default function PreviewPage() {
122
  const result = await submitSession({
123
  slug: activeSlug,
124
  consentAccepted: true,
125
- socialHandle: store.socialHandle || undefined,
126
- instagramHandle: store.instagramHandle || undefined,
127
- tiktokHandle: store.tiktokHandle || undefined,
128
  deviceKey: ensureDeviceKey(),
129
  tableId: store.tableId,
130
  sessionId: store.sessionId,
@@ -158,9 +155,6 @@ export default function PreviewPage() {
158
  const result = await submit({
159
  slug: activeSlug,
160
  consentAccepted: true,
161
- socialHandle: store.socialHandle || undefined,
162
- instagramHandle: store.instagramHandle || undefined,
163
- tiktokHandle: store.tiktokHandle || undefined,
164
  deviceKey: ensureDeviceKey(),
165
  tableId: store.tableId,
166
  durationSeconds: finalVideo.durationSeconds,
@@ -179,12 +173,9 @@ export default function PreviewPage() {
179
  }, [
180
  activeSlug,
181
  router,
182
- store.instagramHandle,
183
  store.orderedClips,
184
  store.sessionId,
185
- store.socialHandle,
186
  store.tableId,
187
- store.tiktokHandle,
188
  ]);
189
 
190
  useEffect(() => {
@@ -201,7 +192,9 @@ export default function PreviewPage() {
201
  const handleApproveFinal = () => {
202
  if (phase.kind === 'ready') {
203
  router.push(
204
- `/reward?code=${encodeURIComponent(phase.result.reward?.value ?? '')}&slug=${encodeURIComponent(
 
 
205
  activeSlug,
206
  )}`,
207
  );
 
48
  const submissionId =
49
  phase.kind === 'polling' || phase.kind === 'ready' ? phase.result.submissionId : null;
50
 
51
+ const { result: pollResult } = useSubmissionPolling(submissionId, activeSlug, 1500);
52
 
53
  useEffect(() => {
54
  if (!pollResult) return;
 
122
  const result = await submitSession({
123
  slug: activeSlug,
124
  consentAccepted: true,
 
 
 
125
  deviceKey: ensureDeviceKey(),
126
  tableId: store.tableId,
127
  sessionId: store.sessionId,
 
155
  const result = await submit({
156
  slug: activeSlug,
157
  consentAccepted: true,
 
 
 
158
  deviceKey: ensureDeviceKey(),
159
  tableId: store.tableId,
160
  durationSeconds: finalVideo.durationSeconds,
 
173
  }, [
174
  activeSlug,
175
  router,
 
176
  store.orderedClips,
177
  store.sessionId,
 
178
  store.tableId,
 
179
  ]);
180
 
181
  useEffect(() => {
 
192
  const handleApproveFinal = () => {
193
  if (phase.kind === 'ready') {
194
  router.push(
195
+ `/reward?code=${encodeURIComponent(phase.result.reward?.value ?? '')}&submissionId=${encodeURIComponent(
196
+ phase.result.submissionId,
197
+ )}&slug=${encodeURIComponent(
198
  activeSlug,
199
  )}`,
200
  );
src/app/reward/page.tsx CHANGED
@@ -2,11 +2,12 @@
2
 
3
  import { Hand } from 'lucide-react';
4
  import { useRouter, useSearchParams } from 'next/navigation';
5
- import { Suspense } from 'react';
6
  import { Button } from '@/components/Button';
7
  import { Confetti } from '@/components/Confetti';
8
  import { MobileShell } from '@/components/MobileShell';
9
  import { RewardCard } from '@/components/RewardCard';
 
10
  import { RASA_RASA_SLUG } from '@/lib/foodstar';
11
  import { recordingStore } from '@/lib/recordingStore';
12
 
@@ -24,12 +25,33 @@ function RewardScreen() {
24
  const slug = params.get('slug') || RASA_RASA_SLUG;
25
  const isFoodstar = slug === RASA_RASA_SLUG;
26
  const code = params.get('code') || (isFoodstar ? 'FREE-CHICKEN' : 'MATCHA-7K2Q');
 
 
 
 
27
 
28
  const handleRestart = () => {
29
  recordingStore.reset();
30
  router.replace(isFoodstar ? '/' : `/c/${encodeURIComponent(slug)}`);
31
  };
32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  return (
34
  <MobileShell tone={isFoodstar ? 'dark' : 'cream'}>
35
  <main
@@ -44,7 +66,7 @@ function RewardScreen() {
44
  >
45
  <Confetti />
46
 
47
- <section className="relative z-10 flex flex-1 flex-col items-center px-7 pt-10 text-center">
48
  <div className={`text-eyebrow mb-2 ${isFoodstar ? 'text-[#B8C9A8]' : 'text-matcha'}`}>
49
  submitted / thank you
50
  </div>
@@ -66,7 +88,7 @@ function RewardScreen() {
66
  : 'Your video is on its way to the team. Now, about that matcha.'}
67
  </p>
68
 
69
- <div className="my-7 w-full">
70
  <RewardCard code={code} />
71
  </div>
72
 
@@ -79,7 +101,55 @@ function RewardScreen() {
79
  Show this screen to your server
80
  </div>
81
 
82
- {isFoodstar ? null : (
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  <p className="max-w-[300px] text-[11px] leading-[1.5] text-muted">
84
  Want a copy of your video?{' '}
85
  <span className="cursor-pointer text-matcha underline">Send it to my email -&gt;</span>
@@ -104,3 +174,9 @@ function RewardScreen() {
104
  </MobileShell>
105
  );
106
  }
 
 
 
 
 
 
 
2
 
3
  import { Hand } from 'lucide-react';
4
  import { useRouter, useSearchParams } from 'next/navigation';
5
+ import { Suspense, useState } from 'react';
6
  import { Button } from '@/components/Button';
7
  import { Confetti } from '@/components/Confetti';
8
  import { MobileShell } from '@/components/MobileShell';
9
  import { RewardCard } from '@/components/RewardCard';
10
+ import { updateSubmissionHandles } from '@/lib/humeoApi';
11
  import { RASA_RASA_SLUG } from '@/lib/foodstar';
12
  import { recordingStore } from '@/lib/recordingStore';
13
 
 
25
  const slug = params.get('slug') || RASA_RASA_SLUG;
26
  const isFoodstar = slug === RASA_RASA_SLUG;
27
  const code = params.get('code') || (isFoodstar ? 'FREE-CHICKEN' : 'MATCHA-7K2Q');
28
+ const submissionId = params.get('submissionId') || '';
29
+ const [instagramHandle, setInstagramHandle] = useState('');
30
+ const [tiktokHandle, setTiktokHandle] = useState('');
31
+ const [tagStatus, setTagStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle');
32
 
33
  const handleRestart = () => {
34
  recordingStore.reset();
35
  router.replace(isFoodstar ? '/' : `/c/${encodeURIComponent(slug)}`);
36
  };
37
 
38
+ const handleSaveHandles = async () => {
39
+ if (!submissionId || tagStatus === 'saving') return;
40
+
41
+ setTagStatus('saving');
42
+ try {
43
+ await updateSubmissionHandles({
44
+ slug,
45
+ submissionId,
46
+ instagramHandle: normalizeHandle(instagramHandle),
47
+ tiktokHandle: normalizeHandle(tiktokHandle),
48
+ });
49
+ setTagStatus('saved');
50
+ } catch {
51
+ setTagStatus('error');
52
+ }
53
+ };
54
+
55
  return (
56
  <MobileShell tone={isFoodstar ? 'dark' : 'cream'}>
57
  <main
 
66
  >
67
  <Confetti />
68
 
69
+ <section className="relative z-10 flex flex-1 flex-col items-center px-7 pt-9 text-center">
70
  <div className={`text-eyebrow mb-2 ${isFoodstar ? 'text-[#B8C9A8]' : 'text-matcha'}`}>
71
  submitted / thank you
72
  </div>
 
88
  : 'Your video is on its way to the team. Now, about that matcha.'}
89
  </p>
90
 
91
+ <div className="my-6 w-full">
92
  <RewardCard code={code} />
93
  </div>
94
 
 
101
  Show this screen to your server
102
  </div>
103
 
104
+ {isFoodstar ? (
105
+ <div className="w-full rounded-[8px] border border-[#3D392F] bg-[#15130F] p-4 text-left shadow-[inset_0_1px_0_rgba(248,243,231,0.06)]">
106
+ <div className="font-mono text-[10px] uppercase tracking-[0.18em] text-[#AEB9A0]">
107
+ Tag me optional
108
+ </div>
109
+ <div className="mt-3 grid gap-2.5">
110
+ <input
111
+ value={instagramHandle}
112
+ onChange={(event) => {
113
+ setInstagramHandle(event.target.value);
114
+ if (tagStatus !== 'idle') setTagStatus('idle');
115
+ }}
116
+ placeholder="Instagram handle"
117
+ className="h-12 rounded-full border border-[#3D392F] bg-[#201E18] px-4 text-[14px] text-[#DCD4C4] outline-none selection:bg-[#A9B898] selection:text-[#12130F] placeholder:text-[#81796A] focus:border-[#9EAD8E] focus:bg-[#232018]"
118
+ autoCapitalize="none"
119
+ autoCorrect="off"
120
+ inputMode="text"
121
+ />
122
+ <input
123
+ value={tiktokHandle}
124
+ onChange={(event) => {
125
+ setTiktokHandle(event.target.value);
126
+ if (tagStatus !== 'idle') setTagStatus('idle');
127
+ }}
128
+ placeholder="TikTok handle"
129
+ className="h-12 rounded-full border border-[#3D392F] bg-[#201E18] px-4 text-[14px] text-[#DCD4C4] outline-none selection:bg-[#A9B898] selection:text-[#12130F] placeholder:text-[#81796A] focus:border-[#9EAD8E] focus:bg-[#232018]"
130
+ autoCapitalize="none"
131
+ autoCorrect="off"
132
+ inputMode="text"
133
+ />
134
+ </div>
135
+ <button
136
+ type="button"
137
+ onClick={handleSaveHandles}
138
+ disabled={!submissionId || tagStatus === 'saving'}
139
+ className="mt-3 h-11 w-full rounded-full bg-[#A9B898] px-4 text-[13px] font-semibold text-[#151711] transition hover:bg-[#C6D1B8] disabled:cursor-not-allowed disabled:opacity-55"
140
+ >
141
+ {tagStatus === 'saving' ? 'Saving handles...' : 'Save tag handles'}
142
+ </button>
143
+ {tagStatus === 'saved' ? (
144
+ <p className="mt-2 text-center text-[12px] text-[#AEB9A0]">Handles saved.</p>
145
+ ) : null}
146
+ {tagStatus === 'error' ? (
147
+ <p className="mt-2 text-center text-[12px] text-[#D7B89A]">
148
+ Video is saved. Handles did not update.
149
+ </p>
150
+ ) : null}
151
+ </div>
152
+ ) : (
153
  <p className="max-w-[300px] text-[11px] leading-[1.5] text-muted">
154
  Want a copy of your video?{' '}
155
  <span className="cursor-pointer text-matcha underline">Send it to my email -&gt;</span>
 
174
  </MobileShell>
175
  );
176
  }
177
+
178
+ function normalizeHandle(value: string) {
179
+ const trimmed = value.trim();
180
+ if (!trimmed) return '';
181
+ return trimmed.startsWith('@') ? trimmed : `@${trimmed}`;
182
+ }
src/lib/humeoApi.ts CHANGED
@@ -94,6 +94,13 @@ export type SubmitSessionInput = Omit<
94
  }>;
95
  };
96
 
 
 
 
 
 
 
 
97
  export async function submit(input: SubmitInput): Promise<PublicSubmitResult> {
98
  const form = new FormData();
99
  form.append('slug', input.slug);
@@ -222,4 +229,28 @@ export async function getSubmission(
222
  return body.submission;
223
  }
224
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
  export { POLLING_SUBMISSION_STATUSES };
 
94
  }>;
95
  };
96
 
97
+ export type UpdateSubmissionHandlesInput = {
98
+ slug: string;
99
+ submissionId: string;
100
+ instagramHandle?: string;
101
+ tiktokHandle?: string;
102
+ };
103
+
104
  export async function submit(input: SubmitInput): Promise<PublicSubmitResult> {
105
  const form = new FormData();
106
  form.append('slug', input.slug);
 
229
  return body.submission;
230
  }
231
 
232
+ export async function updateSubmissionHandles(
233
+ input: UpdateSubmissionHandlesInput,
234
+ ): Promise<PublicSubmitResult> {
235
+ const res = await fetch(
236
+ endpoint(`/api/public/reviews/submission/${encodeURIComponent(input.submissionId)}`),
237
+ {
238
+ method: 'PATCH',
239
+ headers: { 'Content-Type': 'application/json' },
240
+ body: JSON.stringify({
241
+ slug: input.slug,
242
+ instagramHandle: input.instagramHandle ?? '',
243
+ tiktokHandle: input.tiktokHandle ?? '',
244
+ }),
245
+ },
246
+ );
247
+
248
+ const body = await res.json().catch(() => ({}));
249
+ if (!res.ok) {
250
+ throw new Error((body as { error?: string }).error || `Handle update failed (${res.status})`);
251
+ }
252
+
253
+ return (body as { submission: PublicSubmitResult }).submission;
254
+ }
255
+
256
  export { POLLING_SUBMISSION_STATUSES };
src/lib/server/reviewStore.ts CHANGED
@@ -720,6 +720,39 @@ export async function getSubmission(
720
  return { ok: true, submission };
721
  }
722
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
723
  export async function listAdminReviewSubmissions(): Promise<AdminReviewSubmission[]> {
724
  const client = getSupabaseAdmin();
725
  const supabaseSubmissions = client ? await listSupabaseSubmissions(client) : [];
 
720
  return { ok: true, submission };
721
  }
722
 
723
+ export async function updateSubmissionSocialHandles(input: {
724
+ submissionId: string;
725
+ slug: string;
726
+ instagramHandle?: string | null;
727
+ tiktokHandle?: string | null;
728
+ }): Promise<
729
+ | { ok: true; submission: LocalSubmission }
730
+ | { ok: false; status: number; error: string }
731
+ > {
732
+ const campaign = getCampaignBySlug(input.slug);
733
+ if (!campaign) return { ok: false, status: 404, error: 'Campaign not found' };
734
+
735
+ const client = getSupabaseAdmin();
736
+ const submission =
737
+ (client ? await loadSupabaseSubmission(client, input.submissionId) : null) ??
738
+ state.submissions.get(input.submissionId);
739
+
740
+ if (!submission || submission.campaignSlug !== input.slug) {
741
+ return { ok: false, status: 404, error: 'Submission not found' };
742
+ }
743
+
744
+ const instagramHandle = input.instagramHandle?.trim() || null;
745
+ const tiktokHandle = input.tiktokHandle?.trim() || null;
746
+ submission.instagramHandle = instagramHandle;
747
+ submission.tiktokHandle = tiktokHandle;
748
+ submission.socialHandle = socialHandleSummary(instagramHandle, tiktokHandle) || null;
749
+ submission.updatedAt = new Date().toISOString();
750
+
751
+ await persistSubmission(submission);
752
+
753
+ return { ok: true, submission };
754
+ }
755
+
756
  export async function listAdminReviewSubmissions(): Promise<AdminReviewSubmission[]> {
757
  const client = getSupabaseAdmin();
758
  const supabaseSubmissions = client ? await listSupabaseSubmissions(client) : [];