chore: added formatting
f850fa0b
16 file(s) · +474 −431
| 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": { |
| 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=="], |
|
| 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", |
| 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 | }; |
| 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 |
| 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 | } |
| 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> |
| 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 | }; |
| 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"> |
| 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> |
| 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 | }; |
| 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 | } |
| 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 | }; |
| 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> |
| 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 | }; |
| 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 | }; |