My personal photography website steve.phot
portfolio photography svelte sveltekit

chore: added formatting

+474 -431
+5
biome.json
··· 12 12 "enabled": true, 13 13 "indentStyle": "tab" 14 14 }, 15 + "css": { 16 + "parser": { 17 + "tailwindDirectives": true 18 + } 19 + }, 15 20 "linter": { 16 21 "enabled": false, 17 22 "rules": {
+19
bun.lock
··· 11 11 "import": "^0.0.6", 12 12 }, 13 13 "devDependencies": { 14 + "@biomejs/biome": "^2.4.4", 14 15 "@sveltejs/adapter-auto": "^7.0.0", 15 16 "@sveltejs/adapter-cloudflare": "^7.2.6", 16 17 "@sveltejs/kit": "^2.49.1", ··· 26 27 }, 27 28 }, 28 29 "packages": { 30 + "@biomejs/biome": ["@biomejs/biome@2.4.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.4", "@biomejs/cli-darwin-x64": "2.4.4", "@biomejs/cli-linux-arm64": "2.4.4", "@biomejs/cli-linux-arm64-musl": "2.4.4", "@biomejs/cli-linux-x64": "2.4.4", "@biomejs/cli-linux-x64-musl": "2.4.4", "@biomejs/cli-win32-arm64": "2.4.4", "@biomejs/cli-win32-x64": "2.4.4" }, "bin": { "biome": "bin/biome" } }, "sha512-tigwWS5KfJf0cABVd52NVaXyAVv4qpUXOWJ1rxFL8xF1RVoeS2q/LK+FHgYoKMclJCuRoCWAPy1IXaN9/mS61Q=="], 31 + 32 + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jZ+Xc6qvD6tTH5jM6eKX44dcbyNqJHssfl2nnwT6vma6B1sj7ZLTGIk6N5QwVBs5xGN52r3trk5fgd3sQ9We9A=="], 33 + 34 + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-Dh1a/+W+SUCXhEdL7TiX3ArPTFCQKJTI1mGncZNWfO+6suk+gYA4lNyJcBB+pwvF49uw0pEbUS49BgYOY4hzUg=="], 35 + 36 + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-V/NFfbWhsUU6w+m5WYbBenlEAz8eYnSqRMDMAW3K+3v0tYVkNyZn8VU0XPxk/lOqNXLSCCrV7FmV/u3SjCBShg=="], 37 + 38 + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-+sPAXq3bxmFwhVFJnSwkSF5Rw2ZAJMH3MF6C9IveAEOdSpgajPhoQhbbAK12SehN9j2QrHpk4J/cHsa/HqWaYQ=="], 39 + 40 + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.4", "", { "os": "linux", "cpu": "x64" }, "sha512-R4+ZCDtG9kHArasyBO+UBD6jr/FcFCTH8QkNTOCu0pRJzCWyWC4EtZa2AmUZB5h3e0jD7bRV2KvrENcf8rndBg=="], 41 + 42 + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gGvFTGpOIQDb5CQ2VC0n9Z2UEqlP46c4aNgHmAMytYieTGEcfqhfCFnhs6xjt0S3igE6q5GLuIXtdQt3Izok+g=="], 43 + 44 + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-trzCqM7x+Gn832zZHgr28JoYagQNX4CZkUZhMUac2YxvvyDRLJDrb5m9IA7CaZLlX6lTQmADVfLEKP1et1Ma4Q=="], 45 + 46 + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.4", "", { "os": "win32", "cpu": "x64" }, "sha512-gnOHKVPFAAPrpoPt2t+Q6FZ7RPry/FDV3GcpU53P3PtLNnQjBmKyN2Vh/JtqXet+H4pme8CC76rScwdjDcT1/A=="], 47 + 29 48 "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.2", "", {}, "sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ=="], 30 49 31 50 "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.11.0", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": "^1.20260115.0" }, "optionalPeers": ["workerd"] }, "sha512-z3hxFajL765VniNPGV0JRStZolNz63gU3B3AktwoGdDlnQvz5nP+Ah4RL04PONlZQjwmDdGHowEStJ94+RsaJg=="],
+3 -1
package.json
··· 10 10 "preview": "vite preview", 11 11 "prepare": "svelte-kit sync || echo ''", 12 12 "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 13 - "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" 13 + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 14 + "lint": "biome format --write src/" 14 15 }, 15 16 "devDependencies": { 17 + "@biomejs/biome": "^2.4.4", 16 18 "@sveltejs/adapter-auto": "^7.0.0", 17 19 "@sveltejs/adapter-cloudflare": "^7.2.6", 18 20 "@sveltejs/kit": "^2.49.1",
+9 -9
src/hooks.server.ts
··· 2 2 import { verifySession } from "$lib"; 3 3 4 4 export const handle: Handle = async ({ event, resolve }) => { 5 - const sessionCookie = event.cookies.get("session"); 6 - const secret = event.platform?.env?.SESSION_SECRET; 5 + const sessionCookie = event.cookies.get("session"); 6 + const secret = event.platform?.env?.SESSION_SECRET; 7 7 8 - if (sessionCookie && secret) { 9 - const isValid = await verifySession(sessionCookie, secret); 10 - event.locals.user = isValid ? { authenticated: true } : null; 11 - } else { 12 - event.locals.user = null; 13 - } 8 + if (sessionCookie && secret) { 9 + const isValid = await verifySession(sessionCookie, secret); 10 + event.locals.user = isValid ? { authenticated: true } : null; 11 + } else { 12 + event.locals.user = null; 13 + } 14 14 15 - return resolve(event); 15 + return resolve(event); 16 16 };
+33 -33
src/lib/components/ProgressiveImage.svelte
··· 1 1 <script lang="ts"> 2 - let { 3 - src, 4 - thumb, 5 - alt, 6 - blurData, 7 - class: className = "", 8 - }: { 9 - src: string; 10 - thumb: string; 11 - alt: string; 12 - blurData?: string; 13 - class?: string; 14 - } = $props(); 2 + let { 3 + src, 4 + thumb, 5 + alt, 6 + blurData, 7 + class: className = "", 8 + }: { 9 + src: string; 10 + thumb: string; 11 + alt: string; 12 + blurData?: string; 13 + class?: string; 14 + } = $props(); 15 15 16 - let loaded = $state(false); 17 - let thumbAspect = $state(0); 18 - let thumbImg: HTMLImageElement; 16 + let loaded = $state(false); 17 + let thumbAspect = $state(0); 18 + let thumbImg: HTMLImageElement; 19 19 20 - function onThumbLoad() { 21 - if (thumbImg.naturalWidth && thumbImg.naturalHeight) { 22 - thumbAspect = thumbImg.naturalWidth / thumbImg.naturalHeight; 23 - } 24 - } 20 + function onThumbLoad() { 21 + if (thumbImg.naturalWidth && thumbImg.naturalHeight) { 22 + thumbAspect = thumbImg.naturalWidth / thumbImg.naturalHeight; 23 + } 24 + } 25 25 26 - $effect(() => { 27 - loaded = false; 28 - const img = new Image(); 29 - img.onload = () => { 30 - loaded = true; 31 - }; 32 - img.src = src; 26 + $effect(() => { 27 + loaded = false; 28 + const img = new Image(); 29 + img.onload = () => { 30 + loaded = true; 31 + }; 32 + img.src = src; 33 33 34 - return () => { 35 - img.onload = null; 36 - }; 37 - }); 34 + return () => { 35 + img.onload = null; 36 + }; 37 + }); 38 38 39 - let placeholderSrc = $derived(blurData || thumb); 39 + let placeholderSrc = $derived(blurData || thumb); 40 40 </script> 41 41 42 42 <div
+30 -24
src/lib/feed.ts
··· 2 2 3 3 const R2_BASE_URL = "https://r2.steve.photo"; 4 4 5 - export async function getPhotos(platform: App.Platform | undefined): Promise<ImageItem[]> { 6 - const db = platform?.env?.DB; 5 + export async function getPhotos( 6 + platform: App.Platform | undefined, 7 + ): Promise<ImageItem[]> { 8 + const db = platform?.env?.DB; 7 9 8 - if (!db) { 9 - return []; 10 - } 10 + if (!db) { 11 + return []; 12 + } 11 13 12 - const result = await db.prepare("SELECT * FROM photos ORDER BY date DESC").all(); 14 + const result = await db 15 + .prepare("SELECT * FROM photos ORDER BY date DESC") 16 + .all(); 13 17 14 - const photos: ImageItem[] = result.results.map((row: Record<string, unknown>) => ({ 15 - slug: row.slug as string, 16 - title: row.title as string, 17 - date: row.date as string, 18 - image: `${R2_BASE_URL}/${row.image_key}`, 19 - thumb: `${R2_BASE_URL}/${row.thumb_key}`, 20 - type: row.type as string, 21 - camera: row.camera as string, 22 - lens: row.lens as string, 23 - aperture: row.aperture as string, 24 - exposure: row.exposure as string, 25 - focalLength: row.focal_length as string, 26 - iso: row.iso as string, 27 - make: row.make as string, 28 - tags: [], 29 - blurData: row.blur_data as string, 30 - })); 18 + const photos: ImageItem[] = result.results.map( 19 + (row: Record<string, unknown>) => ({ 20 + slug: row.slug as string, 21 + title: row.title as string, 22 + date: row.date as string, 23 + image: `${R2_BASE_URL}/${row.image_key}`, 24 + thumb: `${R2_BASE_URL}/${row.thumb_key}`, 25 + type: row.type as string, 26 + camera: row.camera as string, 27 + lens: row.lens as string, 28 + aperture: row.aperture as string, 29 + exposure: row.exposure as string, 30 + focalLength: row.focal_length as string, 31 + iso: row.iso as string, 32 + make: row.make as string, 33 + tags: [], 34 + blurData: row.blur_data as string, 35 + }), 36 + ); 31 37 32 - return photos; 38 + return photos; 33 39 }
+12 -12
src/routes/+layout.svelte
··· 1 1 <script lang="ts"> 2 - import { onNavigate } from "$app/navigation"; 3 - import "./layout.css"; 2 + import { onNavigate } from "$app/navigation"; 3 + import "./layout.css"; 4 4 5 - let { children } = $props(); 5 + let { children } = $props(); 6 6 7 - onNavigate((navigation) => { 8 - if (!document.startViewTransition) return; 7 + onNavigate((navigation) => { 8 + if (!document.startViewTransition) return; 9 9 10 - return new Promise((resolve) => { 11 - document.startViewTransition(async () => { 12 - resolve(); 13 - await navigation.complete; 14 - }); 15 - }); 16 - }); 10 + return new Promise((resolve) => { 11 + document.startViewTransition(async () => { 12 + resolve(); 13 + await navigation.complete; 14 + }); 15 + }); 16 + }); 17 17 </script> 18 18 19 19 <svelte:head>
+28 -24
src/routes/+page.server.ts
··· 7 7 const PAGE_SIZE = 15; 8 8 9 9 export const load: PageServerLoad = async ({ platform }) => { 10 - const db = platform?.env?.DB; 10 + const db = platform?.env?.DB; 11 11 12 - const result = await db 13 - .prepare("SELECT * FROM photos ORDER BY date DESC LIMIT ?") 14 - .bind(PAGE_SIZE) 15 - .all(); 12 + const result = await db 13 + .prepare("SELECT * FROM photos ORDER BY date DESC LIMIT ?") 14 + .bind(PAGE_SIZE) 15 + .all(); 16 16 17 - const countResult = await db.prepare("SELECT COUNT(*) as total FROM photos").first(); 18 - const total = (countResult?.total as number) || 0; 17 + const countResult = await db 18 + .prepare("SELECT COUNT(*) as total FROM photos") 19 + .first(); 20 + const total = (countResult?.total as number) || 0; 19 21 20 - const photos: ImageItem[] = result.results.map((row: Record<string, unknown>) => ({ 21 - slug: row.slug, 22 - title: row.title, 23 - date: row.date, 24 - image: `${R2_BASE_URL}/${row.image_key}`, 25 - thumb: `${R2_BASE_URL}/${row.thumb_key}`, 26 - type: row.type, 27 - camera: row.camera, 28 - lens: row.lens, 29 - aperture: row.aperture, 30 - exposure: row.exposure, 31 - focalLength: row.focal_length, 32 - iso: row.iso, 33 - make: row.make, 34 - blurData: row.blur_data as string, 35 - })); 22 + const photos: ImageItem[] = result.results.map( 23 + (row: Record<string, unknown>) => ({ 24 + slug: row.slug, 25 + title: row.title, 26 + date: row.date, 27 + image: `${R2_BASE_URL}/${row.image_key}`, 28 + thumb: `${R2_BASE_URL}/${row.thumb_key}`, 29 + type: row.type, 30 + camera: row.camera, 31 + lens: row.lens, 32 + aperture: row.aperture, 33 + exposure: row.exposure, 34 + focalLength: row.focal_length, 35 + iso: row.iso, 36 + make: row.make, 37 + blurData: row.blur_data as string, 38 + }), 39 + ); 36 40 37 - return { photos, total, pageSize: PAGE_SIZE }; 41 + return { photos, total, pageSize: PAGE_SIZE }; 38 42 };
+53 -53
src/routes/+page.svelte
··· 1 1 <script lang="ts"> 2 - import { browser } from "$app/environment"; 3 - import type { PageData } from "./$types"; 4 - import ProgressiveImage from "$lib/components/ProgressiveImage.svelte"; 5 - type ImageItem = PageData["photos"][number]; 6 - let { data }: { data: PageData } = $props(); 2 + import { browser } from "$app/environment"; 3 + import type { PageData } from "./$types"; 4 + import ProgressiveImage from "$lib/components/ProgressiveImage.svelte"; 5 + type ImageItem = PageData["photos"][number]; 6 + let { data }: { data: PageData } = $props(); 7 7 8 - let viewMode = $state<'feed' | 'grid'>('feed'); 9 - let photos = $state<ImageItem[]>([]); 10 - let loading = $state(false); 11 - let hasMore = $derived(photos.length < data.total); 8 + let viewMode = $state<"feed" | "grid">("feed"); 9 + let photos = $state<ImageItem[]>([]); 10 + let loading = $state(false); 11 + let hasMore = $derived(photos.length < data.total); 12 12 13 - if (browser) { 14 - const saved = localStorage.getItem('viewMode'); 15 - if (saved === 'feed' || saved === 'grid') { 16 - viewMode = saved; 17 - } 18 - } 13 + if (browser) { 14 + const saved = localStorage.getItem("viewMode"); 15 + if (saved === "feed" || saved === "grid") { 16 + viewMode = saved; 17 + } 18 + } 19 19 20 - function toggleViewMode() { 21 - viewMode = viewMode === 'feed' ? 'grid' : 'feed'; 22 - if (browser) { 23 - localStorage.setItem('viewMode', viewMode); 24 - } 25 - } 20 + function toggleViewMode() { 21 + viewMode = viewMode === "feed" ? "grid" : "feed"; 22 + if (browser) { 23 + localStorage.setItem("viewMode", viewMode); 24 + } 25 + } 26 26 27 - $effect(() => { 28 - photos = data.photos; 29 - }); 30 - let sentinel: HTMLDivElement; 27 + $effect(() => { 28 + photos = data.photos; 29 + }); 30 + let sentinel: HTMLDivElement; 31 31 32 - async function loadMore() { 33 - if (loading || !hasMore) return; 34 - loading = true; 32 + async function loadMore() { 33 + if (loading || !hasMore) return; 34 + loading = true; 35 35 36 - try { 37 - const response = await fetch(`/api/photos?offset=${photos.length}`); 38 - const result = await response.json(); 39 - if (result.photos.length > 0) { 40 - photos = [...photos, ...result.photos]; 41 - } 42 - } catch (error) { 43 - console.error("Failed to load more photos:", error); 44 - } finally { 45 - loading = false; 46 - } 47 - } 36 + try { 37 + const response = await fetch(`/api/photos?offset=${photos.length}`); 38 + const result = await response.json(); 39 + if (result.photos.length > 0) { 40 + photos = [...photos, ...result.photos]; 41 + } 42 + } catch (error) { 43 + console.error("Failed to load more photos:", error); 44 + } finally { 45 + loading = false; 46 + } 47 + } 48 48 49 - $effect(() => { 50 - if (!sentinel) return; 49 + $effect(() => { 50 + if (!sentinel) return; 51 51 52 - const observer = new IntersectionObserver( 53 - (entries) => { 54 - if (entries[0].isIntersecting && hasMore && !loading) { 55 - loadMore(); 56 - } 57 - }, 58 - { rootMargin: "200px" }, 59 - ); 52 + const observer = new IntersectionObserver( 53 + (entries) => { 54 + if (entries[0].isIntersecting && hasMore && !loading) { 55 + loadMore(); 56 + } 57 + }, 58 + { rootMargin: "200px" }, 59 + ); 60 60 61 - observer.observe(sentinel); 61 + observer.observe(sentinel); 62 62 63 - return () => observer.disconnect(); 64 - }); 63 + return () => observer.disconnect(); 64 + }); 65 65 </script> 66 66 67 67 <div class="bg-[#121113] min-h-screen text-white">
+156 -154
src/routes/admin/+page.svelte
··· 1 1 <script lang="ts"> 2 - import { enhance } from "$app/forms"; 3 - import exifr from "exifr"; 2 + import { enhance } from "$app/forms"; 3 + import exifr from "exifr"; 4 4 5 - let { data, form } = $props(); 5 + let { data, form } = $props(); 6 6 7 - let fileInput = $state<HTMLInputElement | null>(null); 8 - let selectedFile = $state<File | null>(null); 9 - let previewUrl = $state<string | null>(null); 10 - let thumbnailBlob = $state<Blob | null>(null); 11 - let blurData = $state<string>(""); 7 + let fileInput = $state<HTMLInputElement | null>(null); 8 + let selectedFile = $state<File | null>(null); 9 + let previewUrl = $state<string | null>(null); 10 + let thumbnailBlob = $state<Blob | null>(null); 11 + let blurData = $state<string>(""); 12 12 13 - let title = $state(""); 14 - let date = $state(""); 15 - let camera = $state(""); 16 - let lens = $state(""); 17 - let aperture = $state(""); 18 - let exposure = $state(""); 19 - let focalLength = $state(""); 20 - let iso = $state(""); 21 - let make = $state(""); 13 + let title = $state(""); 14 + let date = $state(""); 15 + let camera = $state(""); 16 + let lens = $state(""); 17 + let aperture = $state(""); 18 + let exposure = $state(""); 19 + let focalLength = $state(""); 20 + let iso = $state(""); 21 + let make = $state(""); 22 22 23 - let isLoading = $state(false); 24 - let editingId = $state<number | null>(null); 25 - let editingTitle = $state(""); 26 - let deleteConfirmId = $state<number | null>(null); 23 + let isLoading = $state(false); 24 + let editingId = $state<number | null>(null); 25 + let editingTitle = $state(""); 26 + let deleteConfirmId = $state<number | null>(null); 27 27 28 - async function handleFileSelect(event: Event) { 29 - const input = event.target as HTMLInputElement; 30 - const file = input.files?.[0]; 31 - if (!file) return; 28 + async function handleFileSelect(event: Event) { 29 + const input = event.target as HTMLInputElement; 30 + const file = input.files?.[0]; 31 + if (!file) return; 32 32 33 - selectedFile = file; 34 - previewUrl = URL.createObjectURL(file); 33 + selectedFile = file; 34 + previewUrl = URL.createObjectURL(file); 35 35 36 - // Auto-populate title from filename 37 - const nameWithoutExt = file.name.replace(/\.[^/.]+$/, ""); 38 - title = nameWithoutExt.replace(/[-_]/g, " "); 36 + // Auto-populate title from filename 37 + const nameWithoutExt = file.name.replace(/\.[^/.]+$/, ""); 38 + title = nameWithoutExt.replace(/[-_]/g, " "); 39 39 40 - // Extract EXIF data 41 - try { 42 - const exif = await exifr.parse(file, { 43 - pick: [ 44 - "DateTimeOriginal", 45 - "Model", 46 - "LensModel", 47 - "FNumber", 48 - "ExposureTime", 49 - "FocalLength", 50 - "ISO", 51 - "Make", 52 - ], 53 - }); 40 + // Extract EXIF data 41 + try { 42 + const exif = await exifr.parse(file, { 43 + pick: [ 44 + "DateTimeOriginal", 45 + "Model", 46 + "LensModel", 47 + "FNumber", 48 + "ExposureTime", 49 + "FocalLength", 50 + "ISO", 51 + "Make", 52 + ], 53 + }); 54 54 55 - if (exif) { 56 - if (exif.DateTimeOriginal) { 57 - const d = new Date(exif.DateTimeOriginal); 58 - date = d.toISOString().split("T")[0]; 59 - } 60 - if (exif.Model) camera = exif.Model; 61 - if (exif.LensModel) lens = exif.LensModel; 62 - if (exif.FNumber) aperture = `f/${exif.FNumber}`; 63 - if (exif.ExposureTime) { 64 - exposure = 65 - exif.ExposureTime < 1 ? `1/${Math.round(1 / exif.ExposureTime)}s` : `${exif.ExposureTime}s`; 66 - } 67 - if (exif.FocalLength) focalLength = `${exif.FocalLength}mm`; 68 - if (exif.ISO) iso = String(exif.ISO); 69 - if (exif.Make) make = exif.Make; 55 + if (exif) { 56 + if (exif.DateTimeOriginal) { 57 + const d = new Date(exif.DateTimeOriginal); 58 + date = d.toISOString().split("T")[0]; 59 + } 60 + if (exif.Model) camera = exif.Model; 61 + if (exif.LensModel) lens = exif.LensModel; 62 + if (exif.FNumber) aperture = `f/${exif.FNumber}`; 63 + if (exif.ExposureTime) { 64 + exposure = 65 + exif.ExposureTime < 1 66 + ? `1/${Math.round(1 / exif.ExposureTime)}s` 67 + : `${exif.ExposureTime}s`; 70 68 } 71 - } catch (err) { 72 - console.error("Failed to read EXIF data:", err); 69 + if (exif.FocalLength) focalLength = `${exif.FocalLength}mm`; 70 + if (exif.ISO) iso = String(exif.ISO); 71 + if (exif.Make) make = exif.Make; 73 72 } 74 - 75 - // Generate thumbnail and blur placeholder 76 - await generateThumbnail(file); 77 - await generateBlurData(file); 73 + } catch (err) { 74 + console.error("Failed to read EXIF data:", err); 78 75 } 79 76 80 - async function generateThumbnail(file: File): Promise<void> { 81 - return new Promise((resolve) => { 82 - const img = new Image(); 83 - img.onload = () => { 84 - const maxSize = 400; 85 - let width = img.width; 86 - let height = img.height; 77 + // Generate thumbnail and blur placeholder 78 + await generateThumbnail(file); 79 + await generateBlurData(file); 80 + } 87 81 88 - if (width > height) { 89 - if (width > maxSize) { 90 - height = (height * maxSize) / width; 91 - width = maxSize; 92 - } 93 - } else { 94 - if (height > maxSize) { 95 - width = (width * maxSize) / height; 96 - height = maxSize; 97 - } 82 + async function generateThumbnail(file: File): Promise<void> { 83 + return new Promise((resolve) => { 84 + const img = new Image(); 85 + img.onload = () => { 86 + const maxSize = 400; 87 + let width = img.width; 88 + let height = img.height; 89 + 90 + if (width > height) { 91 + if (width > maxSize) { 92 + height = (height * maxSize) / width; 93 + width = maxSize; 94 + } 95 + } else { 96 + if (height > maxSize) { 97 + width = (width * maxSize) / height; 98 + height = maxSize; 98 99 } 100 + } 99 101 100 - const canvas = document.createElement("canvas"); 101 - canvas.width = width; 102 - canvas.height = height; 102 + const canvas = document.createElement("canvas"); 103 + canvas.width = width; 104 + canvas.height = height; 103 105 104 - const ctx = canvas.getContext("2d"); 105 - if (ctx) { 106 - ctx.drawImage(img, 0, 0, width, height); 107 - canvas.toBlob( 108 - (blob) => { 109 - thumbnailBlob = blob; 110 - resolve(); 111 - }, 112 - "image/jpeg", 113 - 0.8 114 - ); 115 - } else { 116 - resolve(); 117 - } 118 - }; 119 - img.src = URL.createObjectURL(file); 120 - }); 121 - } 106 + const ctx = canvas.getContext("2d"); 107 + if (ctx) { 108 + ctx.drawImage(img, 0, 0, width, height); 109 + canvas.toBlob( 110 + (blob) => { 111 + thumbnailBlob = blob; 112 + resolve(); 113 + }, 114 + "image/jpeg", 115 + 0.8, 116 + ); 117 + } else { 118 + resolve(); 119 + } 120 + }; 121 + img.src = URL.createObjectURL(file); 122 + }); 123 + } 122 124 123 - async function generateBlurData(file: File): Promise<void> { 124 - return new Promise((resolve) => { 125 - const img = new Image(); 126 - img.onload = () => { 127 - const targetWidth = 20; 128 - const aspectRatio = img.height / img.width; 129 - const targetHeight = Math.round(targetWidth * aspectRatio); 125 + async function generateBlurData(file: File): Promise<void> { 126 + return new Promise((resolve) => { 127 + const img = new Image(); 128 + img.onload = () => { 129 + const targetWidth = 20; 130 + const aspectRatio = img.height / img.width; 131 + const targetHeight = Math.round(targetWidth * aspectRatio); 132 + 133 + const canvas = document.createElement("canvas"); 134 + canvas.width = targetWidth; 135 + canvas.height = targetHeight; 130 136 131 - const canvas = document.createElement("canvas"); 132 - canvas.width = targetWidth; 133 - canvas.height = targetHeight; 137 + const ctx = canvas.getContext("2d"); 138 + if (ctx) { 139 + ctx.drawImage(img, 0, 0, targetWidth, targetHeight); 140 + blurData = canvas.toDataURL("image/jpeg", 0.3); 141 + } 142 + resolve(); 143 + }; 144 + img.src = URL.createObjectURL(file); 145 + }); 146 + } 134 147 135 - const ctx = canvas.getContext("2d"); 136 - if (ctx) { 137 - ctx.drawImage(img, 0, 0, targetWidth, targetHeight); 138 - blurData = canvas.toDataURL("image/jpeg", 0.3); 139 - } 140 - resolve(); 141 - }; 142 - img.src = URL.createObjectURL(file); 143 - }); 144 - } 148 + function handleSubmit() { 149 + isLoading = true; 150 + } 145 151 146 - function handleSubmit() { 147 - isLoading = true; 148 - } 152 + function startEdit(photo: { id: number; title: string }) { 153 + editingId = photo.id; 154 + editingTitle = photo.title; 155 + } 149 156 150 - function startEdit(photo: { id: number; title: string }) { 151 - editingId = photo.id; 152 - editingTitle = photo.title; 153 - } 157 + function cancelEdit() { 158 + editingId = null; 159 + editingTitle = ""; 160 + } 154 161 155 - function cancelEdit() { 156 - editingId = null; 157 - editingTitle = ""; 162 + function resetUploadForm() { 163 + selectedFile = null; 164 + if (previewUrl) { 165 + URL.revokeObjectURL(previewUrl); 166 + previewUrl = null; 158 167 } 159 - 160 - function resetUploadForm() { 161 - selectedFile = null; 162 - if (previewUrl) { 163 - URL.revokeObjectURL(previewUrl); 164 - previewUrl = null; 165 - } 166 - thumbnailBlob = null; 167 - blurData = ""; 168 - title = ""; 169 - date = ""; 170 - camera = ""; 171 - lens = ""; 172 - aperture = ""; 173 - exposure = ""; 174 - focalLength = ""; 175 - iso = ""; 176 - make = ""; 177 - if (fileInput) { 178 - fileInput.value = ""; 179 - } 168 + thumbnailBlob = null; 169 + blurData = ""; 170 + title = ""; 171 + date = ""; 172 + camera = ""; 173 + lens = ""; 174 + aperture = ""; 175 + exposure = ""; 176 + focalLength = ""; 177 + iso = ""; 178 + make = ""; 179 + if (fileInput) { 180 + fileInput.value = ""; 180 181 } 182 + } 181 183 </script> 182 184 183 185 <svelte:head>
+25 -23
src/routes/api/photos/+server.ts
··· 6 6 const PAGE_SIZE = 15; 7 7 8 8 export const GET: RequestHandler = async ({ url, platform }) => { 9 - const db = platform?.env?.DB; 10 - const offset = parseInt(url.searchParams.get("offset") || "0", 10); 9 + const db = platform?.env?.DB; 10 + const offset = parseInt(url.searchParams.get("offset") || "0", 10); 11 11 12 - const result = await db 13 - .prepare("SELECT * FROM photos ORDER BY date DESC LIMIT ? OFFSET ?") 14 - .bind(PAGE_SIZE, offset) 15 - .all(); 12 + const result = await db 13 + .prepare("SELECT * FROM photos ORDER BY date DESC LIMIT ? OFFSET ?") 14 + .bind(PAGE_SIZE, offset) 15 + .all(); 16 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 - blurData: row.blur_data as string, 32 - })); 17 + const photos: ImageItem[] = result.results.map( 18 + (row: Record<string, unknown>) => ({ 19 + slug: row.slug as string, 20 + title: row.title as string, 21 + date: row.date as string, 22 + image: `${R2_BASE_URL}/${row.image_key}`, 23 + thumb: `${R2_BASE_URL}/${row.thumb_key}`, 24 + type: row.type as string, 25 + camera: row.camera as string, 26 + lens: row.lens as string, 27 + aperture: row.aperture as string, 28 + exposure: row.exposure as string, 29 + focalLength: row.focal_length as string, 30 + iso: row.iso as string, 31 + make: row.make as string, 32 + blurData: row.blur_data as string, 33 + }), 34 + ); 33 35 34 - return json({ photos }); 36 + return json({ photos }); 35 37 };
+3 -3
src/routes/layout.css
··· 1 - @import 'tailwindcss'; 1 + @import "tailwindcss"; 2 2 3 - html { 4 - @apply bg-[#121113] min-h-screen w-full font-mono text-white; 3 + html { 4 + @apply bg-[#121113] min-h-screen w-full font-mono text-white; 5 5 }
+29 -29
src/routes/login/+page.server.ts
··· 3 3 import { verifyPassword, createSession } from "$lib"; 4 4 5 5 export const load: PageServerLoad = async ({ locals }) => { 6 - if (locals.user?.authenticated) { 7 - throw redirect(302, "/admin"); 8 - } 9 - return {}; 6 + if (locals.user?.authenticated) { 7 + throw redirect(302, "/admin"); 8 + } 9 + return {}; 10 10 }; 11 11 12 12 export const actions: Actions = { 13 - default: async ({ request, platform, cookies }) => { 14 - const data = await request.formData(); 15 - const password = data.get("password"); 13 + default: async ({ request, platform, cookies }) => { 14 + const data = await request.formData(); 15 + const password = data.get("password"); 16 16 17 - if (!password || typeof password !== "string") { 18 - return fail(400, { error: "Password is required" }); 19 - } 17 + if (!password || typeof password !== "string") { 18 + return fail(400, { error: "Password is required" }); 19 + } 20 20 21 - const secret = platform?.env?.SESSION_SECRET; 22 - const passwordHash = platform?.env?.ADMIN_PASSWORD_HASH; 21 + const secret = platform?.env?.SESSION_SECRET; 22 + const passwordHash = platform?.env?.ADMIN_PASSWORD_HASH; 23 23 24 - if (!secret || !passwordHash) { 25 - return fail(500, { error: "Server configuration error" }); 26 - } 24 + if (!secret || !passwordHash) { 25 + return fail(500, { error: "Server configuration error" }); 26 + } 27 27 28 - const isValid = await verifyPassword(password, passwordHash, secret); 28 + const isValid = await verifyPassword(password, passwordHash, secret); 29 29 30 - if (!isValid) { 31 - return fail(401, { error: "Invalid password" }); 32 - } 30 + if (!isValid) { 31 + return fail(401, { error: "Invalid password" }); 32 + } 33 33 34 - const session = await createSession(secret); 34 + const session = await createSession(secret); 35 35 36 - cookies.set("session", session, { 37 - path: "/", 38 - httpOnly: true, 39 - secure: true, 40 - sameSite: "strict", 41 - maxAge: 60 * 60 * 24, // 24 hours 42 - }); 36 + cookies.set("session", session, { 37 + path: "/", 38 + httpOnly: true, 39 + secure: true, 40 + sameSite: "strict", 41 + maxAge: 60 * 60 * 24, // 24 hours 42 + }); 43 43 44 - throw redirect(302, "/admin"); 45 - }, 44 + throw redirect(302, "/admin"); 45 + }, 46 46 };
+2 -2
src/routes/login/+page.svelte
··· 1 1 <script lang="ts"> 2 - import { enhance } from "$app/forms"; 2 + import { enhance } from "$app/forms"; 3 3 4 - let { form } = $props(); 4 + let { form } = $props(); 5 5 </script> 6 6 7 7 <svelte:head>
+26 -23
src/routes/photo/[slug]/+page.server.ts
··· 5 5 const R2_BASE_URL = "https://r2.steve.photo"; 6 6 7 7 export const load: PageServerLoad = async ({ platform, params }) => { 8 - const db = platform?.env?.DB; 8 + const db = platform?.env?.DB; 9 9 10 - const result = await db.prepare("SELECT * FROM photos WHERE slug = ?").bind(params.slug).first(); 10 + const result = await db 11 + .prepare("SELECT * FROM photos WHERE slug = ?") 12 + .bind(params.slug) 13 + .first(); 11 14 12 - if (!result) { 13 - throw error(404, "Photo not found"); 14 - } 15 + if (!result) { 16 + throw error(404, "Photo not found"); 17 + } 15 18 16 - const photo: ImageItem = { 17 - slug: result.slug as string, 18 - title: result.title as string, 19 - date: result.date as string, 20 - image: `${R2_BASE_URL}/${result.image_key}`, 21 - thumb: `${R2_BASE_URL}/${result.thumb_key}`, 22 - type: result.type as string, 23 - camera: result.camera as string, 24 - lens: result.lens as string, 25 - aperture: result.aperture as string, 26 - exposure: result.exposure as string, 27 - focalLength: result.focal_length as string, 28 - iso: result.iso as string, 29 - make: result.make as string, 30 - tags: [], 31 - blurData: result.blur_data as string, 32 - }; 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 + blurData: result.blur_data as string, 35 + }; 33 36 34 - return { photo }; 37 + return { photo }; 35 38 };
+41 -41
src/routes/rss.xml/+server.ts
··· 3 3 import type { RequestHandler } from "./$types"; 4 4 5 5 export const GET: RequestHandler = async ({ platform }) => { 6 - const feed = new Feed({ 7 - title: "steve.photo", 8 - description: "Personal photography portolio of Steve Simkins", 9 - id: "https://steve.photo", 10 - link: "https://steve.photo/", 11 - language: "en", 12 - favicon: "https://steve.photo/favicon.ico", 13 - copyright: `Copyright ${new Date().getFullYear().toString()}, Steve Simkins`, 14 - feedLinks: { 15 - rss: "https://steve.photo/rss.xml", 16 - }, 17 - author: { 18 - name: "Steve Simkins", 19 - email: "contact@stevedylan.dev", 20 - link: "https://stevedylan.dev", 21 - }, 22 - ttl: 60, 23 - }); 6 + const feed = new Feed({ 7 + title: "steve.photo", 8 + description: "Personal photography portolio of Steve Simkins", 9 + id: "https://steve.photo", 10 + link: "https://steve.photo/", 11 + language: "en", 12 + favicon: "https://steve.photo/favicon.ico", 13 + copyright: `Copyright ${new Date().getFullYear().toString()}, Steve Simkins`, 14 + feedLinks: { 15 + rss: "https://steve.photo/rss.xml", 16 + }, 17 + author: { 18 + name: "Steve Simkins", 19 + email: "contact@stevedylan.dev", 20 + link: "https://stevedylan.dev", 21 + }, 22 + ttl: 60, 23 + }); 24 24 25 - const photos = await getPhotos(platform); 25 + const photos = await getPhotos(platform); 26 26 27 - for (const photo of photos) { 28 - feed.addItem({ 29 - title: photo.title, 30 - id: `https://steve.photo/photo/${photo.slug}`, 31 - link: `https://steve.photo/photo/${photo.slug}`, 32 - date: new Date(photo.date), 33 - image: photo.image, 34 - author: [ 35 - { 36 - name: "Steve Simkins", 37 - email: "contact@stevedylan.dev", 38 - link: "https://stevedylan.dev", 39 - }, 40 - ], 41 - content: `<img src="${photo.image}" alt="${photo.title}" /><p>Camera: ${photo.camera} | Lens: ${photo.lens} | ${photo.aperture} | ${photo.exposure} | ISO ${photo.iso}</p>`, 42 - }); 43 - } 27 + for (const photo of photos) { 28 + feed.addItem({ 29 + title: photo.title, 30 + id: `https://steve.photo/photo/${photo.slug}`, 31 + link: `https://steve.photo/photo/${photo.slug}`, 32 + date: new Date(photo.date), 33 + image: photo.image, 34 + author: [ 35 + { 36 + name: "Steve Simkins", 37 + email: "contact@stevedylan.dev", 38 + link: "https://stevedylan.dev", 39 + }, 40 + ], 41 + content: `<img src="${photo.image}" alt="${photo.title}" /><p>Camera: ${photo.camera} | Lens: ${photo.lens} | ${photo.aperture} | ${photo.exposure} | ISO ${photo.iso}</p>`, 42 + }); 43 + } 44 44 45 - return new Response(feed.rss2(), { 46 - headers: { 47 - "Content-Type": "application/xml; charset=utf-8", 48 - }, 49 - }); 45 + return new Response(feed.rss2(), { 46 + headers: { 47 + "Content-Type": "application/xml; charset=utf-8", 48 + }, 49 + }); 50 50 };