tangled
alpha
login
or
join now
stevedylan.dev
/
steve.photo
2
fork
atom
My personal photography website
steve.phot
portfolio
photography
svelte
sveltekit
2
fork
atom
overview
issues
pulls
pipelines
feat: added base64 thumbnails
stevedylan.dev
2 weeks ago
6b63f8a8
4532bb43
+80
-36
13 changed files
expand all
collapse all
unified
split
bun.lock
package.json
schema.sql
src
lib
components
ProgressiveImage.svelte
feed.ts
types.ts
routes
+page.server.ts
+page.svelte
admin
+page.server.ts
+page.svelte
api
photos
+server.ts
photo
[slug]
+page.server.ts
+page.svelte
+1
bun.lock
···
16
16
"@sveltejs/kit": "^2.49.1",
17
17
"@sveltejs/vite-plugin-svelte": "^6.2.1",
18
18
"@tailwindcss/vite": "^4.1.17",
19
19
+
"sharp": "^0.34.5",
19
20
"svelte": "^5.45.6",
20
21
"svelte-check": "^4.3.4",
21
22
"tailwindcss": "^4.1.17",
+32
-31
package.json
···
1
1
{
2
2
-
"name": "steve.photo",
3
3
-
"private": true,
4
4
-
"version": "0.1.0",
5
5
-
"type": "module",
6
6
-
"scripts": {
7
7
-
"dev": "vite dev",
8
8
-
"build": "vite build",
9
9
-
"deploy": "bun run build && wrangler deploy --minify",
10
10
-
"preview": "vite preview",
11
11
-
"prepare": "svelte-kit sync || echo ''",
12
12
-
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
13
13
-
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
14
14
-
},
15
15
-
"devDependencies": {
16
16
-
"@sveltejs/adapter-auto": "^7.0.0",
17
17
-
"@sveltejs/adapter-cloudflare": "^7.2.6",
18
18
-
"@sveltejs/kit": "^2.49.1",
19
19
-
"@sveltejs/vite-plugin-svelte": "^6.2.1",
20
20
-
"@tailwindcss/vite": "^4.1.17",
21
21
-
"svelte": "^5.45.6",
22
22
-
"svelte-check": "^4.3.4",
23
23
-
"tailwindcss": "^4.1.17",
24
24
-
"typescript": "^5.9.3",
25
25
-
"vite": "^7.2.6"
26
26
-
},
27
27
-
"dependencies": {
28
28
-
"exifr": "^7.1.3",
29
29
-
"feed": "^5.2.0",
30
30
-
"from": "^0.1.7",
31
31
-
"import": "^0.0.6"
32
32
-
}
2
2
+
"name": "steve.photo",
3
3
+
"private": true,
4
4
+
"version": "0.1.0",
5
5
+
"type": "module",
6
6
+
"scripts": {
7
7
+
"dev": "vite dev",
8
8
+
"build": "vite build",
9
9
+
"deploy": "bun run build && wrangler deploy --minify",
10
10
+
"preview": "vite preview",
11
11
+
"prepare": "svelte-kit sync || echo ''",
12
12
+
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
13
13
+
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
14
14
+
},
15
15
+
"devDependencies": {
16
16
+
"@sveltejs/adapter-auto": "^7.0.0",
17
17
+
"@sveltejs/adapter-cloudflare": "^7.2.6",
18
18
+
"@sveltejs/kit": "^2.49.1",
19
19
+
"@sveltejs/vite-plugin-svelte": "^6.2.1",
20
20
+
"@tailwindcss/vite": "^4.1.17",
21
21
+
"sharp": "^0.34.5",
22
22
+
"svelte": "^5.45.6",
23
23
+
"svelte-check": "^4.3.4",
24
24
+
"tailwindcss": "^4.1.17",
25
25
+
"typescript": "^5.9.3",
26
26
+
"vite": "^7.2.6"
27
27
+
},
28
28
+
"dependencies": {
29
29
+
"exifr": "^7.1.3",
30
30
+
"feed": "^5.2.0",
31
31
+
"from": "^0.1.7",
32
32
+
"import": "^0.0.6"
33
33
+
}
33
34
}
+2
-1
schema.sql
···
13
13
focal_length TEXT,
14
14
iso TEXT,
15
15
make TEXT,
16
16
-
tags TEXT
16
16
+
tags TEXT,
17
17
+
blur_data TEXT
17
18
);
18
19
19
20
CREATE INDEX idx_photos_date ON photos(date DESC);
+5
-1
src/lib/components/ProgressiveImage.svelte
···
3
3
src,
4
4
thumb,
5
5
alt,
6
6
+
blurData,
6
7
class: className = "",
7
8
}: {
8
9
src: string;
9
10
thumb: string;
10
11
alt: string;
12
12
+
blurData?: string;
11
13
class?: string;
12
14
} = $props();
13
15
···
33
35
img.onload = null;
34
36
};
35
37
});
38
38
+
39
39
+
let placeholderSrc = $derived(blurData || thumb);
36
40
</script>
37
41
38
42
<div
···
41
45
>
42
46
<img
43
47
bind:this={thumbImg}
44
44
-
src={loaded ? src : thumb}
48
48
+
src={loaded ? src : placeholderSrc}
45
49
{alt}
46
50
class="{className} progressive-image"
47
51
class:progressive-loading={!loaded}
+1
src/lib/feed.ts
···
26
26
iso: row.iso as string,
27
27
make: row.make as string,
28
28
tags: [],
29
29
+
blurData: row.blur_data as string,
29
30
}));
30
31
31
32
return photos;
+1
src/lib/types.ts
···
13
13
iso: string;
14
14
make: string;
15
15
tags: string[];
16
16
+
blurData?: string;
16
17
};
17
18
18
19
export type ImageArray = {
+1
src/routes/+page.server.ts
···
31
31
focalLength: row.focal_length,
32
32
iso: row.iso,
33
33
make: row.make,
34
34
+
blurData: row.blur_data as string,
34
35
}));
35
36
36
37
return { photos, total, pageSize: PAGE_SIZE };
+2
src/routes/+page.svelte
···
92
92
class="max-w-full h-auto block"
93
93
src={image.image}
94
94
thumb={image.thumb}
95
95
+
blurData={image.blurData}
95
96
alt={image.title}
96
97
/>
97
98
</a>
···
124
125
class="w-full h-full block"
125
126
src={image.image}
126
127
thumb={image.thumb}
128
128
+
blurData={image.blurData}
127
129
alt={image.title}
128
130
/>
129
131
</a>
+4
-2
src/routes/admin/+page.server.ts
···
60
60
const focalLength = formData.get("focalLength") as string;
61
61
const iso = formData.get("iso") as string;
62
62
const make = formData.get("make") as string;
63
63
+
const blurData = formData.get("blur_data") as string;
63
64
64
65
if (!file || !title || !date) {
65
66
return fail(400, { error: "File, title, and date are required" });
···
114
115
// Insert into database
115
116
await db
116
117
.prepare(
117
117
-
`INSERT INTO photos (slug, title, date, image_key, thumb_key, camera, lens, aperture, exposure, focal_length, iso, make)
118
118
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
118
118
+
`INSERT INTO photos (slug, title, date, image_key, thumb_key, camera, lens, aperture, exposure, focal_length, iso, make, blur_data)
119
119
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
119
120
)
120
121
.bind(
121
122
slug,
···
130
131
focalLength || null,
131
132
iso || null,
132
133
make || null,
134
134
+
blurData || null,
133
135
)
134
136
.run();
135
137
+28
-1
src/routes/admin/+page.svelte
···
8
8
let selectedFile = $state<File | null>(null);
9
9
let previewUrl = $state<string | null>(null);
10
10
let thumbnailBlob = $state<Blob | null>(null);
11
11
+
let blurData = $state<string>("");
11
12
12
13
let title = $state("");
13
14
let date = $state("");
···
71
72
console.error("Failed to read EXIF data:", err);
72
73
}
73
74
74
74
-
// Generate thumbnail
75
75
+
// Generate thumbnail and blur placeholder
75
76
await generateThumbnail(file);
77
77
+
await generateBlurData(file);
76
78
}
77
79
78
80
async function generateThumbnail(file: File): Promise<void> {
···
118
120
});
119
121
}
120
122
123
123
+
async function generateBlurData(file: File): Promise<void> {
124
124
+
return new Promise((resolve) => {
125
125
+
const img = new Image();
126
126
+
img.onload = () => {
127
127
+
const targetWidth = 20;
128
128
+
const aspectRatio = img.height / img.width;
129
129
+
const targetHeight = Math.round(targetWidth * aspectRatio);
130
130
+
131
131
+
const canvas = document.createElement("canvas");
132
132
+
canvas.width = targetWidth;
133
133
+
canvas.height = targetHeight;
134
134
+
135
135
+
const ctx = canvas.getContext("2d");
136
136
+
if (ctx) {
137
137
+
ctx.drawImage(img, 0, 0, targetWidth, targetHeight);
138
138
+
blurData = canvas.toDataURL("image/jpeg", 0.3);
139
139
+
}
140
140
+
resolve();
141
141
+
};
142
142
+
img.src = URL.createObjectURL(file);
143
143
+
});
144
144
+
}
145
145
+
121
146
function handleSubmit() {
122
147
isLoading = true;
123
148
}
···
139
164
previewUrl = null;
140
165
}
141
166
thumbnailBlob = null;
167
167
+
blurData = "";
142
168
title = "";
143
169
date = "";
144
170
camera = "";
···
248
274
<input type="hidden" name="exposure" value={exposure} />
249
275
<input type="hidden" name="focalLength" value={focalLength} />
250
276
<input type="hidden" name="iso" value={iso} />
277
277
+
<input type="hidden" name="blur_data" value={blurData} />
251
278
</div>
252
279
</div>
253
280
{/if}
+1
src/routes/api/photos/+server.ts
···
28
28
focalLength: row.focal_length as string,
29
29
iso: row.iso as string,
30
30
make: row.make as string,
31
31
+
blurData: row.blur_data as string,
31
32
}));
32
33
33
34
return json({ photos });
+1
src/routes/photo/[slug]/+page.server.ts
···
28
28
iso: result.iso as string,
29
29
make: result.make as string,
30
30
tags: [],
31
31
+
blurData: result.blur_data as string,
31
32
};
32
33
33
34
return { photo };
+1
src/routes/photo/[slug]/+page.svelte
···
48
48
class="max-w-full h-auto block"
49
49
src={data.photo.image}
50
50
thumb={data.photo.thumb}
51
51
+
blurData={data.photo.blurData}
51
52
alt={data.photo.title}
52
53
/>
53
54
</div>