an attempt at a lightweight photo/album viewer

refine album card display and make it the entry point

+116 -21
+11 -4
frontend/dist/album-cards.css
··· 21 21 .album-card { 22 22 position: relative; 23 23 background: #fff; 24 - border-radius: 8px; 25 24 overflow: hidden; 26 - box-shadow: 0 4px 6px rgba(0,0,0,0.1); 27 25 display: flex; 28 26 flex-direction: column; 29 27 cursor: pointer; 28 + } 29 + 30 + .album-card img { 31 + border-radius: 8px; 30 32 } 31 33 32 34 .album-cover { ··· 40 42 padding: 12px; 41 43 } 42 44 45 + .album-title-container { 46 + padding: 8px 4px; 47 + } 48 + .album-title-container input { 49 + padding:0; 50 + } 43 51 .album-title { 44 - margin: 0; 52 + padding:2px; 45 53 font-size: 1rem; 46 - font-weight: 600; 47 54 color: #333; 48 55 /* Basic line clamping for long titles */ 49 56 -webkit-line-clamp: 2;
+1
frontend/dist/album.html
··· 4 4 <meta charset="UTF-8"> 5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 6 <title>Gallery</title> 7 + <link href="base.css" rel="stylesheet"> 7 8 <link href="styles.css" rel="stylesheet"> 8 9 <link href="scrobbler.css" rel="stylesheet"> 9 10 </head>
+14
frontend/dist/base.css
··· 1 + body { 2 + margin: 0; 3 + font-family: 'Roboto', sans-serif; 4 + background-color: #fff; 5 + color: #212529; 6 + } 7 + 8 + h1 { 9 + margin: 10px 20px 0 20px; 10 + font-size: 2rem; 11 + font-weight: 300; 12 + color: #333; 13 + text-shadow: 0 1px 3px rgba(0,0,0,0.1); 14 + }
+1
frontend/dist/index.html
··· 4 4 <meta charset="UTF-8"> 5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 6 <title>Albums</title> 7 + <link rel="stylesheet" href="base.css"> 7 8 <link rel="stylesheet" href="album-cards.css"> 8 9 </head> 9 10 <body>
-15
frontend/dist/styles.css
··· 1 - body { 2 - margin: 0; 3 - font-family: 'Roboto', sans-serif; 4 - background-color: #fff; 5 - color: #212529; 6 - } 7 - 8 1 header { 9 2 z-index: 1001; 10 3 padding: 1rem; ··· 15 8 border-bottom: 1px solid rgba(255, 255, 255, 0.2); 16 9 margin-bottom: -6rem; /* Pull the grid up */ 17 10 padding-bottom: 4rem; /* Add space for the title */ 18 - } 19 - 20 - #album-title { 21 - margin: 0; 22 - font-size: 2rem; 23 - font-weight: 300; 24 - color: #333; 25 - text-shadow: 0 1px 3px rgba(0,0,0,0.1); 26 11 } 27 12 28 13 .scrubbable-grid {
+1 -1
frontend/src/albums.ts
··· 93 93 <div class="album-title-container"> 94 94 ${this.isEditing 95 95 ? `<input type="text" class="edit-input" value="${this.data.title}">` 96 - : `<h3 class="album-title">${this.data.title}</h3>`} 96 + : `<span class="album-title">${this.data.title}</span>`} 97 97 </div> 98 98 <div class="action-area"> 99 99 ${this.renderActions()}
+7 -1
frontend/src/gallery.mjs
··· 10 10 }; 11 11 } 12 12 13 + 14 + const debug = false 13 15 const urlParams = new URLSearchParams(window.location.search); 14 16 const album = urlParams.get("album"); 15 17 const apiBase = process.env.API_ENDPOINT_POSTFIX ? ··· 19 21 20 22 let thumbUrlParams = "th=wf3&cache=i&_=1liSY&raster" 21 23 let sectionStore = () => fetch(`${albumUrl}/store.geo.json`).then(res => res.json()); 24 + //let sectionStore = () => fetch(`${process.env.METADATA_API}/album/1`).then(res => res.json()).then(alb => alb.sections); 22 25 let regionStore = `${apiBase}/${process.env.GEO_API_ENDPOINT}` 23 26 const geo = new Geo(regionStore); 24 27 ··· 109 112 if (width == config.containerWidth) { 110 113 return; 111 114 } 112 - document.getElementById('album-title').textContent += `${width}/`; 115 + if (debug) { 116 + document.getElementById('album-title').textContent += `${width}/`; 117 + } 118 + 113 119 config = { 114 120 containerWidth: width, 115 121 targetRowHeight: 350,
+81
server/main.ts
··· 1 + import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi"; 2 + import { Scalar } from "@scalar/hono-api-reference"; 3 + import { createSelectSchema } from "drizzle-zod"; 4 + import { PinoLogger, pinoLogger } from "hono-pino"; 5 + import { writeFileSync } from "node:fs"; 6 + import pino from "pino"; 7 + import pretty from "pino-pretty"; 8 + import * as HttpStatusCodes from "stoker/http-status-codes"; 9 + import { notFound, onError } from "stoker/middlewares"; 10 + import jsonContent from "stoker/openapi/helpers/json-content"; 11 + import db from "./src/db/index.ts"; 12 + import { album } from "./src/db/schema/album.ts"; 13 + 14 + interface AppBindings { 15 + Variables: { 16 + logger: PinoLogger 17 + } 18 + } 19 + 20 + function configureOpenApi(app: OpenAPIHono<AppBindings>) { 21 + app.doc("/doc", { 22 + openapi: "3.0.0", 23 + info: { 24 + version: "0.0.1", 25 + title: "gallery backend" 26 + } 27 + }); 28 + 29 + app.get( 30 + "/reference", 31 + Scalar({ 32 + url: "/doc" 33 + }) 34 + ) 35 + } 36 + 37 + const app = new OpenAPIHono<AppBindings>() 38 + 39 + configureOpenApi(app); 40 + 41 + const typedApp = app.openapi( 42 + createRoute({ 43 + path: "/albums", 44 + method: "get", 45 + responses: { 46 + [HttpStatusCodes.OK]: jsonContent(z.array(createSelectSchema(album)), "all albums") 47 + } 48 + }), 49 + async (c) => c.json(await db.query.album.findMany()) 50 + ); 51 + 52 + const doc = typedApp.getOpenAPI31Document({ 53 + openapi: '3.1.0', 54 + info: { 55 + version: '1.0.0', 56 + title: 'Albums API', 57 + }, 58 + }); 59 + writeFileSync('build/openapi.json', JSON.stringify(doc, null, 2)); 60 + 61 + export type GalleryApi = typeof typedApp; 62 + 63 + app.use(pinoLogger({ 64 + pino: pino(pretty()) 65 + })) 66 + 67 + app.get("/error", (c) => { 68 + c.status(422); 69 + c.var.logger.error("an error endpoint hit"); 70 + throw new Error(); 71 + }) 72 + 73 + app.notFound(notFound) 74 + app.onError(onError) 75 + 76 + app.get('/', (c) => { 77 + return c.text('Hello Hono!') 78 + }) 79 + 80 + 81 + Deno.serve(app.fetch)