chore: added gradual loading 3fe6c955
Steve · 2026-01-26 21:47 3 file(s) · +96 −3
src/routes/+page.server.ts +10 −2
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
};
src/routes/+page.svelte +52 −1
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>
src/routes/api/photos/+server.ts (added) +34 −0
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 +
};