diff --git a/src/routes/admin/+page.server.ts b/src/routes/admin/+page.server.ts index bfc432b..111a265 100644 --- a/src/routes/admin/+page.server.ts +++ b/src/routes/admin/+page.server.ts @@ -3,8 +3,36 @@ import { fail, redirect, isRedirect } from "@sveltejs/kit"; const R2_BASE_URL = "https://r2.steve.photo"; -export const load: PageServerLoad = async () => { - return {}; +type PhotoRow = { + id: number; + slug: string; + title: string; + date: string; + image_key: string; + thumb_key: string; +}; + +export const load: PageServerLoad = async ({ platform }) => { + const db = platform?.env?.DB; + if (!db) { + return { photos: [] }; + } + + const result = await db + .prepare( + "SELECT id, slug, title, date, image_key, thumb_key FROM photos ORDER BY date DESC", + ) + .all(); + + const photos = result.results.map((row: PhotoRow) => ({ + id: row.id, + slug: row.slug, + title: row.title, + date: row.date, + thumb: `${R2_BASE_URL}/${row.thumb_key}`, + })); + + return { photos }; }; export const actions: Actions = { @@ -115,4 +143,84 @@ export const actions: Actions = { return fail(500, { error: `Failed to upload photo: ${errorMessage}` }); } }, + + edit: async ({ request, platform, locals }) => { + if (!locals.user?.authenticated) { + return fail(401, { error: "Unauthorized" }); + } + + const db = platform?.env?.DB; + if (!db) { + return fail(500, { error: "Server configuration error" }); + } + + const formData = await request.formData(); + const id = formData.get("id") as string; + const title = formData.get("title") as string; + + if (!id || !title) { + return fail(400, { error: "ID and title are required" }); + } + + try { + await db + .prepare("UPDATE photos SET title = ? WHERE id = ?") + .bind(title, parseInt(id, 10)) + .run(); + + return { success: true }; + } catch (err) { + const errorMessage = err instanceof Error ? err.message : String(err); + console.error("Edit error:", errorMessage); + return fail(500, { error: `Failed to update photo: ${errorMessage}` }); + } + }, + + delete: async ({ request, platform, locals }) => { + if (!locals.user?.authenticated) { + return fail(401, { error: "Unauthorized" }); + } + + const db = platform?.env?.DB; + const bucket = platform?.env?.PHOTOS; + + if (!db || !bucket) { + return fail(500, { error: "Server configuration error" }); + } + + const formData = await request.formData(); + const id = formData.get("id") as string; + + if (!id) { + return fail(400, { error: "ID is required" }); + } + + try { + // Get photo details first to delete from R2 + const photo = await db + .prepare("SELECT image_key, thumb_key FROM photos WHERE id = ?") + .bind(parseInt(id, 10)) + .first<{ image_key: string; thumb_key: string }>(); + + if (!photo) { + return fail(404, { error: "Photo not found" }); + } + + // Delete from R2 + await bucket.delete(photo.image_key); + await bucket.delete(photo.thumb_key); + + // Delete from database + await db + .prepare("DELETE FROM photos WHERE id = ?") + .bind(parseInt(id, 10)) + .run(); + + return { success: true }; + } catch (err) { + const errorMessage = err instanceof Error ? err.message : String(err); + console.error("Delete error:", errorMessage); + return fail(500, { error: `Failed to delete photo: ${errorMessage}` }); + } + }, }; diff --git a/src/routes/admin/+page.svelte b/src/routes/admin/+page.svelte index 7dd9fc7..19a1695 100644 --- a/src/routes/admin/+page.svelte +++ b/src/routes/admin/+page.svelte @@ -2,7 +2,7 @@ import { enhance } from "$app/forms"; import exifr from "exifr"; - let { form } = $props(); + let { data, form } = $props(); let fileInput = $state(null); let selectedFile = $state(null); @@ -20,6 +20,9 @@ let make = $state(""); let isLoading = $state(false); + let editingId = $state(null); + let editingTitle = $state(""); + let deleteConfirmId = $state(null); async function handleFileSelect(event: Event) { const input = event.target as HTMLInputElement; @@ -118,6 +121,16 @@ function handleSubmit() { isLoading = true; } + + function startEdit(photo: { id: number; title: string }) { + editingId = photo.id; + editingTitle = photo.title; + } + + function cancelEdit() { + editingId = null; + editingTitle = ""; + } @@ -125,9 +138,9 @@
-
+
-

Upload Photo

+

Admin

-
{ - handleSubmit(); - // Append thumbnail blob as file - if (thumbnailBlob) { - formData.append("thumbnail", thumbnailBlob, "thumbnail.jpg"); - } - return async ({ update }) => { - isLoading = false; - await update(); - }; - }} - class="space-y-6" - > -
- - -
- - {#if previewUrl} -
- Preview -
- {/if} - - - -
-
- - -
- + +
+

Upload New Photo

+ + { + handleSubmit(); + if (thumbnailBlob) { + formData.append("thumbnail", thumbnailBlob, "thumbnail.jpg"); + } + return async ({ update }) => { + isLoading = false; + await update(); + }; + }} + class="space-y-6" + >
- +
-
-
-

EXIF Data

- -
-
- - -
- -
- - -
- -
- - -
- -
- - + Preview
+ {/if} +
- +
- +
+
-
- - +
+

EXIF Data

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
-
- {#if form?.error} -

{form.error}

- {/if} + {#if form?.error} +

{form.error}

+ {/if} + + + + + + + + {#if data.photos.length > 0} +
+

Photos ({data.photos.length})

+
+ {#each data.photos as photo (photo.id)} +
+ + {photo.title} + + +
+ {#if editingId === photo.id} +
{ + return async ({ update }) => { + editingId = null; + await update(); + }; + }} + class="flex items-center gap-2" + > + + + + +
+ {:else} + +

{photo.title}

+

{photo.date}

+
+ {/if} +
+ + {#if editingId !== photo.id} +
+ + + {#if deleteConfirmId === photo.id} +
{ + return async ({ update }) => { + deleteConfirmId = null; + await update(); + }; + }} + class="flex items-center gap-1" + > + + + +
+ {:else} + + {/if} +
+ {/if} +
+ {/each} +
+
+ {/if} - - -

+

← Back to gallery