From 803e51c7b19061b865805a3bc39f314dd9f846aa Mon Sep 17 00:00:00 2001 From: Grayson Adams <51373669+GraysonCAdams@users.noreply.github.com> Date: Sun, 1 Mar 2026 16:55:53 -0600 Subject: [PATCH 1/9] refactor: use dynamic bottom nav height via ResizeObserver Replace hardcoded env(safe-area-inset-bottom) calculations with a measured --bottom-nav-height CSS variable, adapting to real device safe-area insets automatically. --- src/lib/components/ActionSidebar.svelte | 2 +- src/lib/components/MusicDisc.svelte | 2 +- src/lib/components/ProgressBar.svelte | 2 +- src/lib/components/ReelOverlay.svelte | 2 +- src/lib/components/SkeletonReel.svelte | 4 +-- src/lib/components/ToastStack.svelte | 2 +- .../components/settings/ClipsManager.svelte | 2 +- src/routes/(app)/+layout.svelte | 28 ++++++++++++++++--- 8 files changed, 32 insertions(+), 12 deletions(-) diff --git a/src/lib/components/ActionSidebar.svelte b/src/lib/components/ActionSidebar.svelte index 97be29a..0db02fe 100644 --- a/src/lib/components/ActionSidebar.svelte +++ b/src/lib/components/ActionSidebar.svelte @@ -167,7 +167,7 @@ .action-sidebar { position: absolute; right: var(--space-lg); - bottom: calc(148px + env(safe-area-inset-bottom)); + bottom: calc(var(--bottom-nav-height, 64px) + 68px); display: flex; flex-direction: column; align-items: center; diff --git a/src/lib/components/MusicDisc.svelte b/src/lib/components/MusicDisc.svelte index 1dc5591..75cccac 100644 --- a/src/lib/components/MusicDisc.svelte +++ b/src/lib/components/MusicDisc.svelte @@ -84,7 +84,7 @@ .music-disc-area { position: absolute; right: var(--space-lg); - bottom: calc(90px + env(safe-area-inset-bottom)); + bottom: calc(var(--bottom-nav-height, 64px) + 10px); display: flex; align-items: center; gap: var(--space-sm); diff --git a/src/lib/components/ProgressBar.svelte b/src/lib/components/ProgressBar.svelte index 54298e9..4bec6b4 100644 --- a/src/lib/components/ProgressBar.svelte +++ b/src/lib/components/ProgressBar.svelte @@ -108,7 +108,7 @@ diff --git a/src/routes/api/push/test/+server.ts b/src/routes/api/push/test/+server.ts new file mode 100644 index 0000000..b30b676 --- /dev/null +++ b/src/routes/api/push/test/+server.ts @@ -0,0 +1,30 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { withAuth } from '$lib/server/api-utils'; +import { sendNotification } from '$lib/server/push'; +import { db } from '$lib/server/db'; +import { pushSubscriptions } from '$lib/server/db/schema'; +import { eq } from 'drizzle-orm'; + +export const POST: RequestHandler = withAuth(async (_event, { user }) => { + const subs = await db.query.pushSubscriptions.findMany({ + where: eq(pushSubscriptions.userId, user.id) + }); + + if (subs.length === 0) { + return json({ error: 'No push subscriptions found' }, { status: 400 }); + } + + // Wait 10 seconds server-side so the notification arrives even if the user + // locks their phone or navigates away + await new Promise((resolve) => setTimeout(resolve, 10_000)); + + await sendNotification(user.id, { + title: 'Test notification', + body: 'Push notifications are working!', + tag: 'test-notification', + url: '/settings' + }); + + return json({ sent: true, sentAt: Date.now() }); +}); From e9a049b10dc4a544e40ba782bc1abb6b6dbf942e Mon Sep 17 00:00:00 2001 From: Grayson Adams <51373669+GraysonCAdams@users.noreply.github.com> Date: Sun, 1 Mar 2026 16:59:13 -0600 Subject: [PATCH 4/9] feat: add shortcut validation before save Validate iCloud shortcut URLs before saving by hitting a validation endpoint first. Show blockers, soft warnings, and unreachable states with appropriate save/retry actions. Extract ValidationResults into a shared component to keep ShortcutManager under the line limit. --- .../settings/ShortcutManager.svelte | 144 +++++++++------- .../settings/ValidationResults.svelte | 154 ++++++++++++++++++ src/routes/share/setup/+page.svelte | 49 +++++- 3 files changed, 285 insertions(+), 62 deletions(-) create mode 100644 src/lib/components/settings/ValidationResults.svelte diff --git a/src/lib/components/settings/ShortcutManager.svelte b/src/lib/components/settings/ShortcutManager.svelte index 891107e..e0ad521 100644 --- a/src/lib/components/settings/ShortcutManager.svelte +++ b/src/lib/components/settings/ShortcutManager.svelte @@ -9,6 +9,7 @@ import AppleLogoIcon from 'phosphor-svelte/lib/AppleLogoIcon'; import AndroidLogoIcon from 'phosphor-svelte/lib/AndroidLogoIcon'; import CheckCircleIcon from 'phosphor-svelte/lib/CheckCircleIcon'; + import ValidationResults from './ValidationResults.svelte'; const ICLOUD_SHORTCUT_RE = /^https:\/\/www\.icloud\.com\/shortcuts\/[a-f0-9]{32}\/?$/; @@ -20,48 +21,73 @@ shortcutToken: string | null; } = $props(); + const SOFT_CODES = ['bad_name', 'localhost_url']; + const UNREACHABLE_CODES = ['fetch_failed']; + let savedUrl = $state(propUrl ?? ''); let shortcutUrl = $state(propUrl ?? ''); let saving = $state(false); + let validating = $state(false); let validationError = $state(''); + let validationWarnings = $state<{ code: string; message: string }[]>([]); + let validated = $state(false); let token = $state(propToken); let rotating = $state(false); let showSheet = $state(false); const isConfigured = $derived(savedUrl.length > 0); const isDirty = $derived(shortcutUrl.trim() !== savedUrl); - const canSave = $derived(isDirty && !saving); + const canSave = $derived(isDirty && !saving && !validating); + + const blockers = $derived( + validationWarnings.filter( + (w) => !SOFT_CODES.includes(w.code) && !UNREACHABLE_CODES.includes(w.code) + ) + ); + const softWarnings = $derived(validationWarnings.filter((w) => SOFT_CODES.includes(w.code))); + const isUnreachable = $derived( + validationWarnings.some((w) => UNREACHABLE_CODES.includes(w.code)) + ); + const hasBlockers = $derived(blockers.length > 0); + const hasSoftOnly = $derived(softWarnings.length > 0 && !hasBlockers && !isUnreachable); + + async function doSave(trimmed: string | null) { + saving = true; + try { + const res = await fetch('/api/group/shortcut', { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ shortcutUrl: trimmed }) + }); + if (res.ok) { + const data = await res.json(); + shortcutUrl = data.shortcutUrl ?? ''; + savedUrl = data.shortcutUrl ?? ''; + validationWarnings = []; + validated = false; + toast.success(trimmed ? 'Shortcut link saved' : 'Shortcut link removed'); + } else { + const data = await res.json(); + toast.error(data.error || 'Failed to save'); + } + } catch { + toast.error('Failed to save'); + } finally { + saving = false; + } + } - async function saveShortcutUrl() { + async function validateAndSave() { const trimmed = shortcutUrl.trim(); if (trimmed === savedUrl) { validationError = ''; return; } - // Allow clearing the field + // Allow clearing the field without validation if (!trimmed) { validationError = ''; - saving = true; - try { - const res = await fetch('/api/group/shortcut', { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ shortcutUrl: null }) - }); - if (res.ok) { - shortcutUrl = ''; - savedUrl = ''; - toast.success('Shortcut link removed'); - } else { - const data = await res.json(); - toast.error(data.error || 'Failed to save'); - } - } catch { - toast.error('Failed to save'); - } finally { - saving = false; - } + await doSave(null); return; } @@ -73,38 +99,49 @@ } validationError = ''; - saving = true; + validating = true; + validated = false; + validationWarnings = []; try { - const res = await fetch('/api/group/shortcut', { - method: 'PATCH', + const res = await fetch('/api/group/shortcut/validate', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ shortcutUrl: trimmed }) }); if (res.ok) { const data = await res.json(); - shortcutUrl = data.shortcutUrl ?? ''; - savedUrl = data.shortcutUrl ?? ''; - toast.success('Shortcut link saved'); + validationWarnings = data.warnings ?? []; + validated = true; + + // Auto-save only when fully clean + if (validationWarnings.length === 0) { + await doSave(trimmed); + } } else { const data = await res.json(); - toast.error(data.error || 'Failed to save'); + toast.error(data.error || 'Validation failed'); } } catch { - toast.error('Failed to save'); + toast.error('Could not validate shortcut'); } finally { - saving = false; + validating = false; } } function handleKeydown(e: KeyboardEvent) { if (e.key === 'Enter') { e.preventDefault(); - if (canSave) saveShortcutUrl(); + if (canSave) validateAndSave(); } } function handleInput() { if (validationError) validationError = ''; + // Reset validation state when the user edits + if (validated) { + validated = false; + validationWarnings = []; + } } async function rotateToken() { @@ -199,15 +236,17 @@ onkeydown={handleKeydown} oninput={handleInput} placeholder="https://www.icloud.com/shortcuts/..." - disabled={saving} + disabled={saving || validating} /> + + + {:else if hasBlockers} + {#each blockers as warning (warning.code + warning.message)} +
+ + + {@html warning.message} +
+ {/each} +
+ +
+ {:else if hasSoftOnly} + {#each softWarnings as warning (warning.code + warning.message)} +
+ + + {@html warning.message} +
+ {/each} +
+ +
+ {/if} + + + diff --git a/src/routes/share/setup/+page.svelte b/src/routes/share/setup/+page.svelte index 653400b..7e604d6 100644 --- a/src/routes/share/setup/+page.svelte +++ b/src/routes/share/setup/+page.svelte @@ -54,6 +54,8 @@ const isUnreachable = $derived( validationWarnings.some((w) => UNREACHABLE_CODES.includes(w.code)) ); + const softWarnings = $derived(validationWarnings.filter((w) => SOFT_CODES.includes(w.code))); + const hasSoftOnly = $derived(softWarnings.length > 0 && blockers.length === 0 && !isUnreachable); const hasBlockers = $derived(blockers.length > 0); const hasExtraPrompts = $derived(blockers.some((w) => w.code === 'extra_prompts')); const hasTrailingSlash = $derived(blockers.some((w) => w.code === 'trailing_slash')); @@ -113,11 +115,8 @@ shortcutName = data.name ?? null; validated = true; - // Auto-save when clean or only soft warnings (bad_name) — name is not critical - if ( - validationWarnings.length === 0 || - validationWarnings.every((w: { code: string }) => SOFT_CODES.includes(w.code)) - ) { + // Auto-save only when fully clean — soft warnings are shown but don't block + if (validationWarnings.length === 0) { await doSave(url); } } else { @@ -441,8 +440,21 @@ {validating ? 'Validating…' : 'Re-validate'} - {:else} - + {:else if hasSoftOnly} + {#each softWarnings as warning (warning.code + warning.message)} +
+ + + {@html warning.message} +
+ {/each} + {/if} {/if} @@ -786,6 +798,29 @@ color: var(--text-primary); font-weight: 600; } + .validation-soft { + display: flex; + align-items: flex-start; + gap: var(--space-sm); + padding: var(--space-sm) var(--space-md); + background: color-mix(in srgb, var(--warning) 8%, transparent); + border: 1px solid color-mix(in srgb, var(--warning) 20%, transparent); + border-radius: var(--radius-sm); + } + .validation-soft :global(svg) { + flex-shrink: 0; + color: var(--warning); + margin-top: 2px; + } + .validation-soft span { + font-size: 0.9375rem; + color: var(--text-secondary); + line-height: 1.5; + } + .validation-soft span :global(b) { + color: var(--text-primary); + font-weight: 600; + } .blocker-link { display: inline; background: none; From 00b04bd40cd70a97d83df802b8a5cc70575d2a0a Mon Sep 17 00:00:00 2001 From: Grayson Adams <51373669+GraysonCAdams@users.noreply.github.com> Date: Sun, 1 Mar 2026 16:59:24 -0600 Subject: [PATCH 5/9] fix: use accent-colored inline SVG for brand logo Switch brand icon from img element to inline SVG with currentColor so it inherits the group accent color dynamically. --- src/lib/assets/icon.svg | 2 +- src/routes/join/+page.svelte | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/lib/assets/icon.svg b/src/lib/assets/icon.svg index fbc58c6..4936657 100644 --- a/src/lib/assets/icon.svg +++ b/src/lib/assets/icon.svg @@ -1,5 +1,5 @@ - + diff --git a/src/routes/join/+page.svelte b/src/routes/join/+page.svelte index d3bc5ec..29fc981 100644 --- a/src/routes/join/+page.svelte +++ b/src/routes/join/+page.svelte @@ -1,7 +1,7 @@
- + +

scrolly

your crew's private feed

@@ -343,6 +344,12 @@ width: 72px; height: 72px; margin-bottom: var(--space-lg); + color: var(--accent-primary); + } + + .brand-logo :global(svg) { + width: 100%; + height: 100%; } h1 { From c9bbcb3948fd51abcee638531a7f09f278a83283 Mon Sep 17 00:00:00 2001 From: Grayson Adams <51373669+GraysonCAdams@users.noreply.github.com> Date: Sun, 1 Mar 2026 16:59:31 -0600 Subject: [PATCH 6/9] fix: update service worker notification icon paths Point push notification icon and badge to the new SVG icon paths. --- src/service-worker.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/service-worker.ts b/src/service-worker.ts index ace8ab8..572eec2 100644 --- a/src/service-worker.ts +++ b/src/service-worker.ts @@ -88,8 +88,8 @@ sw.addEventListener('push', (event) => { const notificationOptions: NotificationOptions & { image?: string } = { body: body || '', - icon: icon || '/icons/icon-192.png', - badge: '/icons/badge-72.png', + icon: icon || '/icon/icon-192.svg', + badge: '/icon/badge-72.svg', tag: tag || undefined, data: { url: url || '/' } }; From 6d2c9b76e55af6c821526c4a9a9bd08fd8b2b650 Mon Sep 17 00:00:00 2001 From: Grayson Adams <51373669+GraysonCAdams@users.noreply.github.com> Date: Sun, 1 Mar 2026 16:59:38 -0600 Subject: [PATCH 7/9] fix: improve feed scroll snap and wheel behavior Add one-reel-at-a-time wheel scrolling to prevent trackpad inertia from skipping multiple reels. Enable scroll-snap-stop: always, hide the scrollbar, and navigate to the correct filter tab on deep-link. --- src/routes/(app)/+page.svelte | 69 +++++++++++++++++++++++++++++++---- 1 file changed, 61 insertions(+), 8 deletions(-) diff --git a/src/routes/(app)/+page.svelte b/src/routes/(app)/+page.svelte index 6cd0c5f..d66237f 100644 --- a/src/routes/(app)/+page.svelte +++ b/src/routes/(app)/+page.svelte @@ -21,7 +21,7 @@ import { onMount, onDestroy } from 'svelte'; import { page } from '$app/state'; import type { FeedClip } from '$lib/types'; - import type { FeedFilter } from '$lib/feed'; + import type { FeedFilter, FeedSort } from '$lib/feed'; import { fetchClips, fetchMoreClips, @@ -37,6 +37,7 @@ let clips = $state([]); let filter = $state('unwatched'); + let sort = $state((page.data.user?.feedSortOrder as FeedSort) || 'oldest'); let loading = $state(true); let activeIndex = $state(0); let scrollContainer: HTMLDivElement | null = $state(null); @@ -96,7 +97,7 @@ loading = true; currentOffset = 0; hasMore = true; - const data = await fetchClips(filter, PAGE_SIZE); + const data = await fetchClips(filter, PAGE_SIZE, sort); if (data) { clips = data.clips; hasMore = data.hasMore; @@ -110,7 +111,7 @@ async function loadMore() { if (loadingMore || !hasMore || loading) return; loadingMore = true; - const data = await fetchMoreClips(filter, currentOffset, PAGE_SIZE); + const data = await fetchMoreClips(filter, currentOffset, PAGE_SIZE, sort); if (data) { const existingIds = new Set(clips.map((c) => c.id)); const newClips = data.clips.filter((c) => !existingIds.has(c.id)); @@ -372,7 +373,7 @@ async function refresh() { isRefreshing = true; const previousIds = new Set(clips.map((c) => c.id)); - const data = await fetchClips(filter, PAGE_SIZE); + const data = await fetchClips(filter, PAGE_SIZE, sort); if (data) { clips = data.clips; hasMore = data.hasMore; @@ -397,6 +398,40 @@ fetchUnwatchedCount(); } + // Intercept wheel events on the scroll container to enforce one-reel-at-a-time scrolling. + // Trackpad inertia generates a long stream of wheel events — we scroll once on the first + // event, then swallow everything until the events fully stop (200ms gap = gesture over). + $effect(() => { + if (!scrollContainer) return; + const el = scrollContainer; + let scrollLocked = false; + let idleTimer: ReturnType | null = null; + + function handleWheel(e: WheelEvent) { + e.preventDefault(); + if (clips.length === 0) return; + + // Reset the idle timer on every event — unlock only after events stop + if (idleTimer) clearTimeout(idleTimer); + idleTimer = setTimeout(() => { + scrollLocked = false; + }, 200); + + if (scrollLocked) return; + scrollLocked = true; + const direction = e.deltaY > 0 ? 1 : -1; + const target = activeIndex + direction; + if (target < 0 || target >= clips.length) return; + scrollToIndex(target); + } + + el.addEventListener('wheel', handleWheel, { passive: false }); + return () => { + el.removeEventListener('wheel', handleWheel); + if (idleTimer) clearTimeout(idleTimer); + }; + }); + $effect(() => { if (!scrollContainer) return; // eslint-disable-next-line sonarjs/void-use -- triggers $effect re-run when clips change @@ -480,12 +515,25 @@ if (!targetClipId) return; viewClipSignal.set(null); (async () => { - filter = 'all' as FeedFilter; + // Fetch the target clip to determine its watched status + const targetClip = await fetchSingleClip(targetClipId); + const targetFilter: FeedFilter = targetClip?.watched ? 'watched' : 'unwatched'; + + filter = targetFilter; currentOffset = 0; hasMore = true; - const data = await fetchClips('all', PAGE_SIZE); + const data = await fetchClips(targetFilter, PAGE_SIZE, sort); if (data) { - clips = data.clips; + // Ensure target clip is in the list (it may be beyond the first page) + const inPage = data.clips.some((c) => c.id === targetClipId); + if (inPage) { + clips = data.clips; + } else if (targetClip) { + // Prepend target clip and de-dupe from the rest + clips = [targetClip, ...data.clips.filter((c) => c.id !== targetClipId)]; + } else { + clips = data.clips; + } hasMore = data.hasMore; currentOffset = data.clips.length; } @@ -773,15 +821,20 @@ } .reel-scroll { height: 100dvh; - overflow-y: scroll; + overflow-y: auto; scroll-snap-type: y mandatory; -webkit-overflow-scrolling: touch; overscroll-behavior-y: none; + scrollbar-width: none; + } + .reel-scroll::-webkit-scrollbar { + display: none; } .reel-slot { height: 100dvh; width: 100%; scroll-snap-align: start; + scroll-snap-stop: always; position: relative; overflow: hidden; } From 00e0697ac5c16bab6f6f4562d137a879b9c347e9 Mon Sep 17 00:00:00 2001 From: Grayson Adams <51373669+GraysonCAdams@users.noreply.github.com> Date: Sun, 1 Mar 2026 17:02:33 -0600 Subject: [PATCH 8/9] refactor: extract ShortcutSheet from ShortcutManager Move the iOS shortcut sheet content (iCloud URL input, validation, token management) into a dedicated ShortcutSheet component to keep ShortcutManager under the line limit. --- .../settings/ShortcutManager.svelte | 419 +----------------- .../components/settings/ShortcutSheet.svelte | 416 +++++++++++++++++ 2 files changed, 424 insertions(+), 411 deletions(-) create mode 100644 src/lib/components/settings/ShortcutSheet.svelte diff --git a/src/lib/components/settings/ShortcutManager.svelte b/src/lib/components/settings/ShortcutManager.svelte index e0ad521..64a9697 100644 --- a/src/lib/components/settings/ShortcutManager.svelte +++ b/src/lib/components/settings/ShortcutManager.svelte @@ -1,17 +1,9 @@
@@ -209,86 +54,11 @@
{#if showSheet} - (showSheet = false)}> -
- - - - - - -
-
iCloud Shortcut Link
-

- Paste the iCloud link so iOS members see a "Get Shortcut" button. -

-
- - -
- {#if validationError} -

{validationError}

- {/if} - - {#if validated && validationWarnings.length > 0} - doSave(shortcutUrl.trim())} - /> - {/if} -
- - {#if token} -
-
Shortcut Token
-

Authenticates shortcut requests. Rotate if compromised.

-
- {token.slice(0, 8)}…{token.slice(-4)} - -
-
- {/if} -
-
+ (showSheet = false)} + /> {/if} diff --git a/src/lib/components/settings/ShortcutSheet.svelte b/src/lib/components/settings/ShortcutSheet.svelte new file mode 100644 index 0000000..47830ae --- /dev/null +++ b/src/lib/components/settings/ShortcutSheet.svelte @@ -0,0 +1,416 @@ + + + +
+ + + + + + +
+
iCloud Shortcut Link
+

+ Paste the iCloud link so iOS members see a "Get Shortcut" button. +

+
+ + +
+ {#if validationError} +

{validationError}

+ {/if} + + {#if validated && validationWarnings.length > 0} + doSave(shortcutUrl.trim())} + /> + {/if} +
+ + {#if token} +
+
Shortcut Token
+

Authenticates shortcut requests. Rotate if compromised.

+
+ {token.slice(0, 8)}…{token.slice(-4)} + +
+
+ {/if} +
+
+ + From d4ad5908fd152fa3286c4e6ec8844329ed79ac54 Mon Sep 17 00:00:00 2001 From: Grayson Adams <51373669+GraysonCAdams@users.noreply.github.com> Date: Sun, 1 Mar 2026 17:09:28 -0600 Subject: [PATCH 9/9] fix: update tests for ready-only feed filter Adjust clip tests to match the new behavior where only ready clips appear in the feed. Add a second ready clip to the seed for pagination coverage and simulate download completion in the auto-watched test. --- src/routes/api/__tests__/clips.test.ts | 11 ++++++++--- tests/helpers/seed.ts | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/routes/api/__tests__/clips.test.ts b/src/routes/api/__tests__/clips.test.ts index 7ec4505..a90ab2c 100644 --- a/src/routes/api/__tests__/clips.test.ts +++ b/src/routes/api/__tests__/clips.test.ts @@ -52,7 +52,7 @@ describe('GET /api/clips', () => { expect(body.error).toBeDefined(); }); - it('returns clips for the user group', async () => { + it('returns ready clips for the user group', async () => { const event = createMockEvent({ method: 'GET', path: '/api/clips', @@ -64,9 +64,9 @@ describe('GET /api/clips', () => { const body = await res.json(); expect(body.clips).toBeInstanceOf(Array); expect(body.clips.length).toBeGreaterThanOrEqual(2); - // Both seed clips belong to the same group const ids = body.clips.map((c: any) => c.id); - expect(ids).toContain(data.clip.id); + // Only ready clips appear in the feed (downloading clips are excluded) + expect(ids).not.toContain(data.clip.id); expect(ids).toContain(data.readyClip.id); }); @@ -286,6 +286,11 @@ describe('POST /api/clips', () => { expect(res.status).toBe(201); const body = await res.json(); + // Simulate download completion — feed only returns ready clips + const { clips } = await import('$lib/server/db/schema'); + const { eq } = await import('drizzle-orm'); + db.update(clips).set({ status: 'ready' }).where(eq(clips.id, body.clip.id)).run(); + // Verify the clip shows as watched when fetching clips for this user const getEvent = createMockEvent({ method: 'GET', diff --git a/tests/helpers/seed.ts b/tests/helpers/seed.ts index bbe3bdf..b0e8878 100644 --- a/tests/helpers/seed.ts +++ b/tests/helpers/seed.ts @@ -20,6 +20,7 @@ export async function seed(db: TestDb): Promise { const otherUserId = uuid(); const clipId = uuid(); const readyClipId = uuid(); + const readyClip2Id = uuid(); const now = new Date(); db.insert(schema.groups) @@ -98,6 +99,20 @@ export async function seed(db: TestDb): Promise { title: 'Test Video', durationSeconds: 30, createdAt: new Date(now.getTime() - 60000) + }, + { + id: readyClip2Id, + groupId, + addedBy: memberId, + originalUrl: 'https://www.youtube.com/shorts/789', + platform: 'youtube', + contentType: 'video', + status: 'ready', + videoPath: 'videos/test-video-2.mp4', + thumbnailPath: 'videos/test-thumb-2.jpg', + title: 'Another Video', + durationSeconds: 15, + createdAt: new Date(now.getTime() - 30000) } ]) .run();