chore: added gradual loading
3fe6c955
3 file(s) · +96 −3
| 4 | 4 | // R2 public URL - update this after enabling public access on your bucket |
|
| 5 | 5 | const R2_BASE_URL = "https://r2.steve.photo"; |
|
| 6 | 6 | ||
| 7 | + | const PAGE_SIZE = 15; |
|
| 8 | + | ||
| 7 | 9 | export const load: PageServerLoad = async ({ platform }) => { |
|
| 8 | 10 | const db = platform?.env?.DB; |
|
| 9 | 11 | ||
| 10 | - | const result = await db.prepare("SELECT * FROM photos ORDER BY date DESC").all(); |
|
| 12 | + | const result = await db |
|
| 13 | + | .prepare("SELECT * FROM photos ORDER BY date DESC LIMIT ?") |
|
| 14 | + | .bind(PAGE_SIZE) |
|
| 15 | + | .all(); |
|
| 16 | + | ||
| 17 | + | const countResult = await db.prepare("SELECT COUNT(*) as total FROM photos").first(); |
|
| 18 | + | const total = (countResult?.total as number) || 0; |
|
| 11 | 19 | ||
| 12 | 20 | const photos: ImageItem[] = result.results.map((row: Record<string, unknown>) => ({ |
|
| 13 | 21 | slug: row.slug, |
|
| 25 | 33 | make: row.make, |
|
| 26 | 34 | })); |
|
| 27 | 35 | ||
| 28 | - | return { photos }; |
|
| 36 | + | return { photos, total, pageSize: PAGE_SIZE }; |
|
| 29 | 37 | }; |
|
| 3 | 3 | import ProgressiveImage from "$lib/components/ProgressiveImage.svelte"; |
|
| 4 | 4 | type ImageItem = PageData["photos"][number]; |
|
| 5 | 5 | let { data }: { data: PageData } = $props(); |
|
| 6 | + | ||
| 7 | + | let photos = $state<ImageItem[]>([]); |
|
| 8 | + | let loading = $state(false); |
|
| 9 | + | let hasMore = $derived(photos.length < data.total); |
|
| 10 | + | ||
| 11 | + | $effect(() => { |
|
| 12 | + | photos = data.photos; |
|
| 13 | + | }); |
|
| 14 | + | let sentinel: HTMLDivElement; |
|
| 15 | + | ||
| 16 | + | async function loadMore() { |
|
| 17 | + | if (loading || !hasMore) return; |
|
| 18 | + | loading = true; |
|
| 19 | + | ||
| 20 | + | try { |
|
| 21 | + | const response = await fetch(`/api/photos?offset=${photos.length}`); |
|
| 22 | + | const result = await response.json(); |
|
| 23 | + | if (result.photos.length > 0) { |
|
| 24 | + | photos = [...photos, ...result.photos]; |
|
| 25 | + | } |
|
| 26 | + | } catch (error) { |
|
| 27 | + | console.error("Failed to load more photos:", error); |
|
| 28 | + | } finally { |
|
| 29 | + | loading = false; |
|
| 30 | + | } |
|
| 31 | + | } |
|
| 32 | + | ||
| 33 | + | $effect(() => { |
|
| 34 | + | if (!sentinel) return; |
|
| 35 | + | ||
| 36 | + | const observer = new IntersectionObserver( |
|
| 37 | + | (entries) => { |
|
| 38 | + | if (entries[0].isIntersecting && hasMore && !loading) { |
|
| 39 | + | loadMore(); |
|
| 40 | + | } |
|
| 41 | + | }, |
|
| 42 | + | { rootMargin: "200px" }, |
|
| 43 | + | ); |
|
| 44 | + | ||
| 45 | + | observer.observe(sentinel); |
|
| 46 | + | ||
| 47 | + | return () => observer.disconnect(); |
|
| 48 | + | }); |
|
| 6 | 49 | </script> |
|
| 7 | 50 | ||
| 8 | 51 | <div class="bg-[#121113] min-h-screen text-white"> |
|
| 36 | 79 | {/snippet} |
|
| 37 | 80 | ||
| 38 | 81 | <div class="flex flex-col gap-2 pt-12"> |
|
| 39 | - | {#each data.photos as image} |
|
| 82 | + | {#each photos as image} |
|
| 40 | 83 | {@render figure(image)} |
|
| 41 | 84 | {/each} |
|
| 85 | + | ||
| 86 | + | <div bind:this={sentinel} class="h-4"></div> |
|
| 87 | + | ||
| 88 | + | {#if loading} |
|
| 89 | + | <div class="flex justify-center py-8"> |
|
| 90 | + | <div class="text-neutral-400 text-sm">Loading...</div> |
|
| 91 | + | </div> |
|
| 92 | + | {/if} |
|
| 42 | 93 | </div> |
|
| 43 | 94 | </div> |
|
| 1 | + | import { json } from "@sveltejs/kit"; |
|
| 2 | + | import type { RequestHandler } from "./$types"; |
|
| 3 | + | import type { ImageItem } from "$lib"; |
|
| 4 | + | ||
| 5 | + | const R2_BASE_URL = "https://r2.steve.photo"; |
|
| 6 | + | const PAGE_SIZE = 15; |
|
| 7 | + | ||
| 8 | + | export const GET: RequestHandler = async ({ url, platform }) => { |
|
| 9 | + | const db = platform?.env?.DB; |
|
| 10 | + | const offset = parseInt(url.searchParams.get("offset") || "0", 10); |
|
| 11 | + | ||
| 12 | + | const result = await db |
|
| 13 | + | .prepare("SELECT * FROM photos ORDER BY date DESC LIMIT ? OFFSET ?") |
|
| 14 | + | .bind(PAGE_SIZE, offset) |
|
| 15 | + | .all(); |
|
| 16 | + | ||
| 17 | + | const photos: ImageItem[] = result.results.map((row: Record<string, unknown>) => ({ |
|
| 18 | + | slug: row.slug as string, |
|
| 19 | + | title: row.title as string, |
|
| 20 | + | date: row.date as string, |
|
| 21 | + | image: `${R2_BASE_URL}/${row.image_key}`, |
|
| 22 | + | thumb: `${R2_BASE_URL}/${row.thumb_key}`, |
|
| 23 | + | type: row.type as string, |
|
| 24 | + | camera: row.camera as string, |
|
| 25 | + | lens: row.lens as string, |
|
| 26 | + | aperture: row.aperture as string, |
|
| 27 | + | exposure: row.exposure as string, |
|
| 28 | + | focalLength: row.focal_length as string, |
|
| 29 | + | iso: row.iso as string, |
|
| 30 | + | make: row.make as string, |
|
| 31 | + | })); |
|
| 32 | + | ||
| 33 | + | return json({ photos }); |
|
| 34 | + | }; |