feat: added individual photos 17b6dfaa
Steve · 2026-01-25 14:11 5 file(s) · +80 −22
package.json +1 −0
6 6
	"scripts": {
7 7
		"dev": "vite dev",
8 8
		"build": "vite build",
9 +
		"deploy": "wrangler deploy --minify",
9 10
		"preview": "vite preview",
10 11
		"prepare": "svelte-kit sync || echo ''",
11 12
		"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
src/routes/+page.server.ts +11 −18
7 7
export const load: PageServerLoad = async ({ platform }) => {
8 8
	const db = platform?.env?.DB;
9 9
10 -
	if (!db) {
11 -
		// Fallback for local dev without D1
12 -
		const data = await import("$lib/data.json");
13 -
		return { photos: data.default as ImageItem[] };
14 -
	}
15 -
16 10
	const result = await db
17 11
		.prepare("SELECT * FROM photos ORDER BY date DESC")
18 12
		.all();
19 13
20 14
	const photos: ImageItem[] = result.results.map(
21 15
		(row: Record<string, unknown>) => ({
22 -
			slug: row.slug as string,
23 -
			title: row.title as string,
24 -
			date: row.date as string,
16 +
			slug: row.slug,
17 +
			title: row.title,
18 +
			date: row.date,
25 19
			image: `${R2_BASE_URL}/${row.image_key}`,
26 20
			thumb: `${R2_BASE_URL}/${row.thumb_key}`,
27 -
			type: row.type as string,
28 -
			camera: row.camera as string,
29 -
			lens: row.lens as string,
30 -
			aperture: row.aperture as string,
31 -
			exposure: row.exposure as string,
32 -
			focalLength: row.focal_length as string,
33 -
			iso: row.iso as string,
34 -
			make: row.make as string,
35 -
			tags: JSON.parse((row.tags as string) || "[]"),
21 +
			type: row.type,
22 +
			camera: row.camera,
23 +
			lens: row.lens,
24 +
			aperture: row.aperture,
25 +
			exposure: row.exposure,
26 +
			focalLength: row.focal_length,
27 +
			iso: row.iso,
28 +
			make: row.make,
36 29
		}),
37 30
	);
38 31
src/routes/+page.svelte +3 −4
1 1
<script lang="ts">
2 2
import type { PageData } from "./$types";
3 -
import type { ImageItem } from "$lib/types";
4 -
3 +
type ImageItem = PageData["photos"][number];
5 4
let { data }: { data: PageData } = $props();
6 5
</script>
7 6
12 11
13 12
  {#snippet figure(image: ImageItem)}
14 13
  <div class="flex gap-2 px-8 pt-2">
15 -
    <div class="flex-2 min-w-0">
14 +
    <a href="/photo/{image.slug}" class="flex-2 min-w-0">
16 15
     	<img class="max-w-full h-auto block" src={image.image} alt={image.title} />
17 -
    </div>
16 +
    </a>
18 17
    <div class="flex flex-col gap-1 flex-1 min-w-0 p-4">
19 18
      <h2 class="text-lg">{image.title.toUpperCase()}</h2>
20 19
      <h3 class="text-sm">{image.make} {image.camera}</h3>
src/routes/photo/[slug]/+page.server.ts (added) +37 −0
1 +
import type { PageServerLoad } from "./$types";
2 +
import type { ImageItem } from "$lib/types";
3 +
import { error } from "@sveltejs/kit";
4 +
5 +
const R2_BASE_URL = "https://r2.steve.photo";
6 +
7 +
export const load: PageServerLoad = async ({ platform, params }) => {
8 +
	const db = platform?.env?.DB;
9 +
10 +
	const result = await db
11 +
		.prepare("SELECT * FROM photos WHERE slug = ?")
12 +
		.bind(params.slug)
13 +
		.first();
14 +
15 +
	if (!result) {
16 +
		throw error(404, "Photo not found");
17 +
	}
18 +
19 +
	const photo: ImageItem = {
20 +
		slug: result.slug as string,
21 +
		title: result.title as string,
22 +
		date: result.date as string,
23 +
		image: `${R2_BASE_URL}/${result.image_key}`,
24 +
		thumb: `${R2_BASE_URL}/${result.thumb_key}`,
25 +
		type: result.type as string,
26 +
		camera: result.camera as string,
27 +
		lens: result.lens as string,
28 +
		aperture: result.aperture as string,
29 +
		exposure: result.exposure as string,
30 +
		focalLength: result.focal_length as string,
31 +
		iso: result.iso as string,
32 +
		make: result.make as string,
33 +
		tags: [],
34 +
	};
35 +
36 +
	return { photo };
37 +
};
src/routes/photo/[slug]/+page.svelte (added) +28 −0
1 +
<script lang="ts">
2 +
	import type { PageData } from "./$types";
3 +
	let { data }: { data: PageData } = $props();
4 +
</script>
5 +
6 +
<div class="bg-[#121113] min-h-screen text-white">
7 +
	<div class="fixed bg-[#121113] w-full py-4 px-8">
8 +
		<a href="/" class="text-sm hover:underline">steve.photo</a>
9 +
	</div>
10 +
11 +
	<div class="flex gap-2 px-8 pt-16">
12 +
		<div class="flex-2 min-w-0">
13 +
			<img class="max-w-full h-auto block" src={data.photo.image} alt={data.photo.title} />
14 +
		</div>
15 +
		<div class="flex flex-col gap-1 flex-1 min-w-0 p-4">
16 +
			<h2 class="text-lg">{data.photo.title.toUpperCase()}</h2>
17 +
			<h3 class="text-sm">{data.photo.make} {data.photo.camera}</h3>
18 +
			<div class="flex flex-col gap-2 text-neutral-400 font-thin text-xs mt-4">
19 +
				<p>{data.photo.focalLength}</p>
20 +
				<p>{data.photo.aperture}</p>
21 +
				<p>{data.photo.exposure}</p>
22 +
				<p>ISO {data.photo.iso}</p>
23 +
				<p>-</p>
24 +
				<p class="text-neutral-700 text-xs">{new Date(data.photo.date).toLocaleDateString()}</p>
25 +
			</div>
26 +
		</div>
27 +
	</div>
28 +
</div>