tangled
alpha
login
or
join now
ericwood.org
/
photos-site
1
fork
atom
A little app to serve my photography from my personal website
1
fork
atom
overview
issues
pulls
pipelines
map index page functionality to the UI
ericwood.org
5 months ago
971b497f
f3fc9dee
+80
-9
6 changed files
expand all
collapse all
unified
split
assets
app.js
src
db.rs
main.rs
routes
photos.rs
templates
layout.jinja
photos
index.jinja
+35
assets/app.js
···
1
1
+
const initTimestamps = () => {
2
2
+
document.querySelectorAll('time').forEach((el) => {
3
3
+
const timeStr = el.attributes.datetime;
4
4
+
if (!timeStr || !timeStr.value) {
5
5
+
return;
6
6
+
}
7
7
+
8
8
+
const date = new Date(timeStr.value);
9
9
+
const formatted = new Intl.DateTimeFormat(undefined, {
10
10
+
month: "long",
11
11
+
day: "numeric",
12
12
+
year: "numeric"
13
13
+
}).format(date);
14
14
+
15
15
+
el.innerText = formatted;
16
16
+
})
17
17
+
};
18
18
+
19
19
+
const initSort = () => {
20
20
+
const select = document.getElementById("sort");
21
21
+
if (!select) {
22
22
+
return;
23
23
+
}
24
24
+
25
25
+
select.addEventListener("change", (event) => {
26
26
+
const url = new URL(window.location);
27
27
+
url.searchParams.set('sort', event.target.value);
28
28
+
window.location = url.search;
29
29
+
});
30
30
+
}
31
31
+
32
32
+
document.addEventListener("DOMContentLoaded", () => {
33
33
+
initTimestamps();
34
34
+
initSort();
35
35
+
});
+3
-3
src/db.rs
···
1
1
use crate::{Photo, Tag};
2
2
-
use serde::Deserialize;
2
2
+
use serde::{Deserialize, Serialize};
3
3
use sqlx::{QueryBuilder, Sqlite, SqlitePool, query::QueryAs};
4
4
5
5
-
#[derive(Deserialize, Clone, Copy)]
5
5
+
#[derive(Deserialize, Serialize, Clone, Copy)]
6
6
#[serde(rename_all = "snake_case")]
7
7
pub enum SortDirection {
8
8
Asc,
···
19
19
}
20
20
}
21
21
22
22
-
#[derive(Deserialize, Clone, Copy)]
22
22
+
#[derive(Deserialize, Serialize, Clone, Copy)]
23
23
#[serde(rename_all = "snake_case")]
24
24
pub enum SortField {
25
25
TakenAt,
+1
src/main.rs
···
32
32
.route("/", get(routes::photos::index))
33
33
.route("/{id}", get(routes::photos::show))
34
34
.with_state(app_state)
35
35
+
.nest_service("/assets", ServeDir::new(&env::var("ASSETS_PATH")?))
35
36
.nest_service("/thumbnails", ServeDir::new(&env::var("THUMBNAIL_PATH")?))
36
37
.nest_service("/images", ServeDir::new(&env::var("IMAGE_PATH")?));
37
38
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
+9
src/routes/photos.rs
···
34
34
}
35
35
}
36
36
37
37
+
static SORT_FIELDS: [SortField; 2] = [SortField::TakenAt, SortField::CreatedAt];
38
38
+
37
39
pub async fn index(
38
40
query: Query<IndexParams>,
39
41
State(state): State<Arc<AppState>>,
···
54
56
)
55
57
.await?;
56
58
59
59
+
let current_tag = query.tag.clone();
57
60
let tags = db::get_tags(&state.pool).await?;
61
61
+
let sort_dir = query.dir;
62
62
+
let sort_field = query.sort;
58
63
let template = state.template_env.get_template("photos/index")?;
59
64
let rendered = template.render(context! {
60
65
photos => photos,
61
66
tags => tags,
67
67
+
current_tag => current_tag,
68
68
+
sort_dir => sort_dir,
69
69
+
sort_field => sort_field,
70
70
+
sort_fields => SORT_FIELDS,
62
71
})?;
63
72
64
73
Ok(Html(rendered))
+1
templates/layout.jinja
···
5
5
</head>
6
6
<body>
7
7
{% block body %}{% endblock %}
8
8
+
<script src="/assets/app.js" type="text/javascript"></script>
8
9
</body>
9
10
</html>
+31
-6
templates/photos/index.jinja
···
1
1
{% extends "layout" %}
2
2
{% block title %}Photos{% endblock %}
3
3
{% block body %}
4
4
-
<ul>
5
5
-
{% for tag in tags %}
6
6
-
<li>{{ tag.name }} ({{ tag.count }})</li>
7
7
-
{% endfor %}
8
8
-
</ul>
4
4
+
<nav>
5
5
+
{{ dir }}
6
6
+
<ul>
7
7
+
{% for tag in tags %}
8
8
+
<li>
9
9
+
{% if current_tag == tag.name -%}
10
10
+
{{ tag.name }} ({{ tag.count }})
11
11
+
{% else -%}
12
12
+
<a href="?tag={{ tag.name }}">
13
13
+
{{ tag.name }} ({{ tag.count }})
14
14
+
</a>
15
15
+
{% endif %}
16
16
+
</li>
17
17
+
{% endfor %}
18
18
+
</ul>
19
19
+
<p>
20
20
+
Sort:
21
21
+
{% if sort_dir == "asc" -%}
22
22
+
<a href="/?dir=desc">↑</a>
23
23
+
{% else -%}
24
24
+
<a href="/?dir=asc">↓</a>
25
25
+
{% endif %}
26
26
+
</p>
27
27
+
<select id="sort" name="sort">
28
28
+
{% for field in sort_fields %}
29
29
+
<option value="{{ field }}" {% if field == sort_field -%}selected="true"{% endif %}>{{ field }}</option>
30
30
+
{% endfor %}
31
31
+
</select>
32
32
+
</select>
33
33
+
</nav>
9
34
{% for photo in photos %}
10
35
<a href="/{{ photo.id }}">
11
11
-
<p>{{ photo.taken_at }}</p>
36
36
+
<p><time datetime="{{ photo.taken_at }}">{{ photo.taken_at }}</time></p>
12
37
<img src="/thumbnails/{{ photo.id }}.webp">
13
38
</a>
14
39
{% endfor %}