tangled
alpha
login
or
join now
adam.tngl.sh
/
photos
0
fork
atom
an attempt at a lightweight photo/album viewer
0
fork
atom
overview
issues
pulls
1
pipelines
refine album card display and make it the entry point
adam.tngl.sh
2 months ago
8ac1a792
3e986c97
+116
-21
8 changed files
expand all
collapse all
unified
split
frontend
dist
album-cards.css
album.html
base.css
index.html
styles.css
src
albums.ts
gallery.mjs
server
main.ts
+11
-4
frontend/dist/album-cards.css
···
21
21
.album-card {
22
22
position: relative;
23
23
background: #fff;
24
24
-
border-radius: 8px;
25
24
overflow: hidden;
26
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
28
+
}
29
29
+
30
30
+
.album-card img {
31
31
+
border-radius: 8px;
30
32
}
31
33
32
34
.album-cover {
···
40
42
padding: 12px;
41
43
}
42
44
45
45
+
.album-title-container {
46
46
+
padding: 8px 4px;
47
47
+
}
48
48
+
.album-title-container input {
49
49
+
padding:0;
50
50
+
}
43
51
.album-title {
44
44
-
margin: 0;
52
52
+
padding:2px;
45
53
font-size: 1rem;
46
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
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
1
+
body {
2
2
+
margin: 0;
3
3
+
font-family: 'Roboto', sans-serif;
4
4
+
background-color: #fff;
5
5
+
color: #212529;
6
6
+
}
7
7
+
8
8
+
h1 {
9
9
+
margin: 10px 20px 0 20px;
10
10
+
font-size: 2rem;
11
11
+
font-weight: 300;
12
12
+
color: #333;
13
13
+
text-shadow: 0 1px 3px rgba(0,0,0,0.1);
14
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
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
1
-
body {
2
2
-
margin: 0;
3
3
-
font-family: 'Roboto', sans-serif;
4
4
-
background-color: #fff;
5
5
-
color: #212529;
6
6
-
}
7
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
18
-
}
19
19
-
20
20
-
#album-title {
21
21
-
margin: 0;
22
22
-
font-size: 2rem;
23
23
-
font-weight: 300;
24
24
-
color: #333;
25
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
96
-
: `<h3 class="album-title">${this.data.title}</h3>`}
96
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
13
+
14
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
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
112
-
document.getElementById('album-title').textContent += `${width}/`;
115
115
+
if (debug) {
116
116
+
document.getElementById('album-title').textContent += `${width}/`;
117
117
+
}
118
118
+
113
119
config = {
114
120
containerWidth: width,
115
121
targetRowHeight: 350,
+81
server/main.ts
···
1
1
+
import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
2
2
+
import { Scalar } from "@scalar/hono-api-reference";
3
3
+
import { createSelectSchema } from "drizzle-zod";
4
4
+
import { PinoLogger, pinoLogger } from "hono-pino";
5
5
+
import { writeFileSync } from "node:fs";
6
6
+
import pino from "pino";
7
7
+
import pretty from "pino-pretty";
8
8
+
import * as HttpStatusCodes from "stoker/http-status-codes";
9
9
+
import { notFound, onError } from "stoker/middlewares";
10
10
+
import jsonContent from "stoker/openapi/helpers/json-content";
11
11
+
import db from "./src/db/index.ts";
12
12
+
import { album } from "./src/db/schema/album.ts";
13
13
+
14
14
+
interface AppBindings {
15
15
+
Variables: {
16
16
+
logger: PinoLogger
17
17
+
}
18
18
+
}
19
19
+
20
20
+
function configureOpenApi(app: OpenAPIHono<AppBindings>) {
21
21
+
app.doc("/doc", {
22
22
+
openapi: "3.0.0",
23
23
+
info: {
24
24
+
version: "0.0.1",
25
25
+
title: "gallery backend"
26
26
+
}
27
27
+
});
28
28
+
29
29
+
app.get(
30
30
+
"/reference",
31
31
+
Scalar({
32
32
+
url: "/doc"
33
33
+
})
34
34
+
)
35
35
+
}
36
36
+
37
37
+
const app = new OpenAPIHono<AppBindings>()
38
38
+
39
39
+
configureOpenApi(app);
40
40
+
41
41
+
const typedApp = app.openapi(
42
42
+
createRoute({
43
43
+
path: "/albums",
44
44
+
method: "get",
45
45
+
responses: {
46
46
+
[HttpStatusCodes.OK]: jsonContent(z.array(createSelectSchema(album)), "all albums")
47
47
+
}
48
48
+
}),
49
49
+
async (c) => c.json(await db.query.album.findMany())
50
50
+
);
51
51
+
52
52
+
const doc = typedApp.getOpenAPI31Document({
53
53
+
openapi: '3.1.0',
54
54
+
info: {
55
55
+
version: '1.0.0',
56
56
+
title: 'Albums API',
57
57
+
},
58
58
+
});
59
59
+
writeFileSync('build/openapi.json', JSON.stringify(doc, null, 2));
60
60
+
61
61
+
export type GalleryApi = typeof typedApp;
62
62
+
63
63
+
app.use(pinoLogger({
64
64
+
pino: pino(pretty())
65
65
+
}))
66
66
+
67
67
+
app.get("/error", (c) => {
68
68
+
c.status(422);
69
69
+
c.var.logger.error("an error endpoint hit");
70
70
+
throw new Error();
71
71
+
})
72
72
+
73
73
+
app.notFound(notFound)
74
74
+
app.onError(onError)
75
75
+
76
76
+
app.get('/', (c) => {
77
77
+
return c.text('Hello Hono!')
78
78
+
})
79
79
+
80
80
+
81
81
+
Deno.serve(app.fetch)