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
create homepage and start seeding content
ericwood.org
2 months ago
70c4e809
83dde85c
0/0
Waiting for spindle ...
+203
-26
17 changed files
expand all
collapse all
unified
split
src
config.rs
main.rs
routes
home
index.rs
mod.rs
mod.rs
styles
app.css
posts.css
templates.rs
views
blog
index
template.jinja
home
index
mod.rs
style.css
template.jinja
mod.rs
mod.rs
photos
index
script.js
templates
layout.jinja
posts.jinja
+2
src/config.rs
···
10
10
pub environment: Environment,
11
11
pub cache_path: String,
12
12
pub auto_reload_templates: bool,
13
13
+
pub content_path: String,
13
14
pub blog_posts_path: String,
14
15
pub projects_path: String,
15
16
pub photos_db_path: String,
···
50
51
environment,
51
52
cache_path,
52
53
auto_reload_templates,
54
54
+
content_path,
53
55
blog_posts_path,
54
56
projects_path,
55
57
photos_db_path,
+2
-1
src/main.rs
···
110
110
.route("/blog", get(routes::blog::index))
111
111
.route("/blog/{slug}", get(routes::blog::show))
112
112
.route("/projects", get(routes::projects::index))
113
113
-
.route("/projects/{slug}", get(routes::projects::show));
113
113
+
.route("/projects/{slug}", get(routes::projects::show))
114
114
+
.route("/", get(routes::home::index));
114
115
115
116
let app_state = Arc::new(AppState {
116
117
config,
+31
src/routes/home/index.rs
···
1
1
+
use std::sync::Arc;
2
2
+
3
3
+
use crate::{
4
4
+
AppState, Response,
5
5
+
db::{self, Pagination, PhotoQuery, Sort, SortDirection, SortField},
6
6
+
views::{View, home::HomeIndex},
7
7
+
};
8
8
+
use axum::{extract::State, response::Html};
9
9
+
10
10
+
pub async fn index(State(state): State<Arc<AppState>>) -> Response {
11
11
+
let blog_posts = state.blog_store.all();
12
12
+
let projects = state.project_store.all();
13
13
+
14
14
+
let (_, photos) = db::get_photos(
15
15
+
&state.photos_db_pool,
16
16
+
PhotoQuery {
17
17
+
sort: Sort {
18
18
+
field: SortField::TakenAt,
19
19
+
direction: SortDirection::Desc,
20
20
+
},
21
21
+
pagination: Pagination { limit: 10, page: 1 },
22
22
+
tags: vec![],
23
23
+
},
24
24
+
)
25
25
+
.await?;
26
26
+
27
27
+
let view = HomeIndex::new(blog_posts, projects, photos);
28
28
+
let html = view.render(&state.reloader)?;
29
29
+
30
30
+
Ok(Html(html))
31
31
+
}
+3
src/routes/home/mod.rs
···
1
1
+
mod index;
2
2
+
3
3
+
pub use index::index;
+1
src/routes/mod.rs
···
1
1
pub mod blog;
2
2
+
pub mod home;
2
3
pub mod photos;
3
4
pub mod projects;
+13
-3
src/styles/app.css
···
42
42
border-bottom: solid var(--foreground) 3px;
43
43
}
44
44
45
45
-
header h1 {
45
45
+
header .home {
46
46
color: var(--foreground);
47
47
text-transform: uppercase;
48
48
-
font-weight: 900;
49
48
margin: 0;
50
50
-
letter-spacing: 3px;
51
49
padding: 10px 30px;
50
50
+
letter-spacing: 3px;
52
51
border-right: solid var(--foreground) 3px;
52
52
+
text-decoration: none;
53
53
+
}
54
54
+
55
55
+
header .home h1 {
56
56
+
font-weight: 900;
57
57
+
margin: 0;
58
58
+
}
59
59
+
60
60
+
header .home:hover {
61
61
+
color: var(--background);
62
62
+
background-color: var(--foreground);
53
63
}
54
64
55
65
header nav ul {
+2
-2
src/styles/posts.css
···
3
3
margin: 30px 0;
4
4
}
5
5
6
6
-
.posts__tags {
6
6
+
.posts__heading {
7
7
display: flex;
8
8
align-items: center;
9
9
gap: 10px;
···
11
11
border-bottom: solid var(--foreground) 2px;
12
12
}
13
13
14
14
-
.posts__tags-title {
14
14
+
.posts__heading-title {
15
15
font-size: 10pt;
16
16
text-transform: uppercase;
17
17
font-weight: 600;
+3
-3
src/templates.rs
···
15
15
16
16
pub fn load_templates_dyn(config: &Config) -> AutoReloader {
17
17
let should_autoreload = config.auto_reload_templates;
18
18
-
let blog_posts_path_str = config.blog_posts_path.clone();
18
18
+
let content_path_str = config.content_path.clone();
19
19
let is_prod = config.is_prod();
20
20
21
21
AutoReloader::new(move |notifier| {
···
32
32
let views_path = Path::new("src/views");
33
33
notifier.watch_path(views_path, true);
34
34
35
35
-
let blog_posts_path = Path::new(&blog_posts_path_str);
36
36
-
notifier.watch_path(blog_posts_path, true);
35
35
+
let content_path = Path::new(&content_path_str);
36
36
+
notifier.watch_path(content_path, true);
37
37
}
38
38
env.add_function("url_escape", url_escape);
39
39
env.add_function("inline_style", inline_style);
-5
src/views/blog/index/template.jinja
···
12
12
{{ posts_macro.header("Blog", tag=tag) }}
13
13
{{ posts_macro.table(posts, root_path="/blog", tag=tag) }}
14
14
</div>
15
15
-
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="display: none">
16
16
-
<defs>
17
17
-
<path id="close-icon" d="M20.7457 3.32851C20.3552 2.93798 19.722 2.93798 19.3315 3.32851L12.0371 10.6229L4.74275 3.32851C4.35223 2.93798 3.71906 2.93798 3.32854 3.32851C2.93801 3.71903 2.93801 4.3522 3.32854 4.74272L10.6229 12.0371L3.32856 19.3314C2.93803 19.722 2.93803 20.3551 3.32856 20.7457C3.71908 21.1362 4.35225 21.1362 4.74277 20.7457L12.0371 13.4513L19.3315 20.7457C19.722 21.1362 20.3552 21.1362 20.7457 20.7457C21.1362 20.3551 21.1362 19.722 20.7457 19.3315L13.4513 12.0371L20.7457 4.74272C21.1362 4.3522 21.1362 3.71903 20.7457 3.32851Z" fill="currentColor"/>
18
18
-
</defs>
19
19
-
</svg>
20
15
{% endblock %}
+48
src/views/home/index/mod.rs
···
1
1
+
use std::sync::Arc;
2
2
+
3
3
+
use minijinja::context;
4
4
+
use minijinja_autoreload::AutoReloader;
5
5
+
6
6
+
use crate::{models::Photo, post::Post, templates::render, views::View};
7
7
+
8
8
+
pub struct HomeIndex {
9
9
+
blog_posts: Vec<Arc<Post>>,
10
10
+
projects: Vec<Arc<Post>>,
11
11
+
photos: Vec<Photo>,
12
12
+
}
13
13
+
14
14
+
impl HomeIndex {
15
15
+
pub fn new(blog_posts: Vec<Arc<Post>>, projects: Vec<Arc<Post>>, photos: Vec<Photo>) -> Self {
16
16
+
let blog_posts: Vec<Arc<Post>> = first_n(&blog_posts, 5);
17
17
+
let projects: Vec<Arc<Post>> = first_n(&projects, 5);
18
18
+
19
19
+
Self {
20
20
+
blog_posts,
21
21
+
projects,
22
22
+
photos,
23
23
+
}
24
24
+
}
25
25
+
}
26
26
+
27
27
+
impl View for HomeIndex {
28
28
+
fn render(&self, reloader: &AutoReloader) -> anyhow::Result<String> {
29
29
+
let html = render(
30
30
+
reloader,
31
31
+
"views/home/index",
32
32
+
context! {
33
33
+
blog_posts => self.blog_posts,
34
34
+
projects => self.projects,
35
35
+
photos => self.photos,
36
36
+
},
37
37
+
)?;
38
38
+
39
39
+
Ok(html)
40
40
+
}
41
41
+
}
42
42
+
43
43
+
fn first_n<T>(collection: &[T], n: usize) -> Vec<T>
44
44
+
where
45
45
+
T: Clone,
46
46
+
{
47
47
+
collection[..std::cmp::min(n, collection.len())].to_vec()
48
48
+
}
+16
src/views/home/index/style.css
···
1
1
+
.summary {
2
2
+
margin-bottom: 30px;
3
3
+
}
4
4
+
5
5
+
.summary a {
6
6
+
color: var(--foreground);
7
7
+
}
8
8
+
9
9
+
.summary .photos {
10
10
+
margin: 16px 0;
11
11
+
gap: 16px;
12
12
+
}
13
13
+
14
14
+
.summary .photos > * {
15
15
+
max-height: 300px;
16
16
+
}
+50
src/views/home/index/template.jinja
···
1
1
+
{% extends "layout" %}
2
2
+
{% block title %}Eric, online{% endblock %}
3
3
+
{% block head %}
4
4
+
{{ inline_style("src/styles/posts.css") }}
5
5
+
{{ inline_style("src/views/home/index/style.css") }}
6
6
+
{{ inline_style("src/views/photos/index/style.css") }}
7
7
+
{% endblock %}
8
8
+
{% import "posts" as posts_macro %}
9
9
+
10
10
+
{% block body %}
11
11
+
<div class="layout">
12
12
+
<h2>Hello!</h2>
13
13
+
<p>my name is <b>Eric Wood</b> and this is my website</p>
14
14
+
<p>I am an engineer in <b>Golden, Colorado</b></p>
15
15
+
<p>this website contains things I love</p>
16
16
+
<section class="summary">
17
17
+
{{ posts_macro.table(blog_posts, root_path="/blog", title="Recent blog posts", show_tags=false) }}
18
18
+
<a href="/blog">See all →</a>
19
19
+
</section>
20
20
+
<section class="summary">
21
21
+
{{ posts_macro.table(projects, root_path="/projects", title="Recent projects", show_tags=false) }}
22
22
+
<a href="/projects">See all →</a>
23
23
+
</section>
24
24
+
<section class="summary">
25
25
+
<div class="posts__heading">
26
26
+
<span class="posts__heading-title">Recent photos</span>
27
27
+
</div>
28
28
+
<div class="photos" data-rows="2">
29
29
+
{% for photo in photos %}
30
30
+
<a
31
31
+
href="/photos/{{ photo.id }}"
32
32
+
id="{{ photo.id }}"
33
33
+
data-width="{{ photo.width }}"
34
34
+
data-height="{{ photo.height }}"
35
35
+
style="aspect-ratio: {{ photo.width / photo.height }}"
36
36
+
>
37
37
+
<img
38
38
+
{% if loop.index < 6 %}
39
39
+
fetchpriority="high"
40
40
+
{% endif %}
41
41
+
src="{{ photo_thumbnail_url(photo.id) }}"
42
42
+
>
43
43
+
</a>
44
44
+
{% endfor %}
45
45
+
</div>
46
46
+
<a href="/photos">See all →</a>
47
47
+
</section>
48
48
+
</div>
49
49
+
{{ inline_script("src/views/photos/index/script.js") }}
50
50
+
{% endblock %}
+2
src/views/home/mod.rs
···
1
1
+
mod index;
2
2
+
pub use index::HomeIndex;
+1
src/views/mod.rs
···
1
1
use minijinja_autoreload::AutoReloader;
2
2
3
3
pub mod blog;
4
4
+
pub mod home;
4
5
pub mod photos;
5
6
pub mod projects;
6
7
+16
-4
src/views/photos/index/script.js
···
7
7
id: photo.id,
8
8
aspectRatio: width / height,
9
9
element: photo,
10
10
+
shown: true,
10
11
};
11
12
}
12
13
);
13
14
14
15
const MAX_HEIGHT = parseFloat(getComputedStyle(photos[0].element).maxHeight.replace('px', ''))
15
16
const container = document.querySelector(".photos");
17
17
+
const maxRows = container.dataset.rows;
16
18
const gap = parseFloat(getComputedStyle(container).gap.replace('px', ''));
17
19
18
20
const recalculateHeights = () => {
···
30
32
const grid = [newRow()];
31
33
let currentRow = 0;
32
34
photos.forEach((photo) => {
35
35
+
if (maxRows && currentRow >= maxRows) {
36
36
+
photo.shown = false
37
37
+
return;
38
38
+
}
39
39
+
40
40
+
photo.shown = true;
33
41
const row = grid[currentRow];
34
42
const aspectRatio = row.aspectRatio + photo.aspectRatio;
35
43
row.aspectRatio = aspectRatio;
···
51
59
});
52
60
});
53
61
54
54
-
photos.forEach(({ element, width, height }) => {
55
55
-
element.style.height = `${height}px`;
56
56
-
element.style.maxWidth = `${width}px`;
57
57
-
element.style.display = 'initial';
62
62
+
photos.forEach(({ element, width, height, shown }) => {
63
63
+
if (shown) {
64
64
+
element.style.height = `${height}px`;
65
65
+
element.style.maxWidth = `${width}px`;
66
66
+
element.style.display = 'initial';
67
67
+
} else {
68
68
+
element.style.display = 'none';
69
69
+
}
58
70
});
59
71
};
60
72
+2
-2
templates/layout.jinja
···
31
31
<div class="container">
32
32
<div>
33
33
<header>
34
34
-
<div>
34
34
+
<a href="/" class="home">
35
35
<h1>Eric Wood</h1>
36
36
-
</div>
36
36
+
</a>
37
37
<nav>
38
38
<ul>
39
39
{% set active_page = active_page|default('') %}
+11
-6
templates/posts.jinja
···
7
7
</h2>
8
8
{% endmacro %}
9
9
10
10
-
{% macro table(posts, show_tags=true, root_path="", tag=None) %}
11
11
-
{% if show_tags %}
12
12
-
<div class="posts__tags">
13
13
-
<span class="posts__tags-title">Categories</span>
10
10
+
{% macro table(posts, show_tags=true, root_path="", tag=None, title="Categories") %}
11
11
+
<div class="posts__heading">
12
12
+
<span class="posts__heading-title">{{ title }}</span>
13
13
+
{% if show_tags %}
14
14
<ul class="posts__tag-list">
15
15
{% if tag %}
16
16
<li>
···
30
30
{% endif %}
31
31
{% endfor %}
32
32
</ul>
33
33
-
</div>
34
34
-
{% endif %}
33
33
+
{% endif %}
34
34
+
</div>
35
35
+
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="display: none">
36
36
+
<defs>
37
37
+
<path id="close-icon" d="M20.7457 3.32851C20.3552 2.93798 19.722 2.93798 19.3315 3.32851L12.0371 10.6229L4.74275 3.32851C4.35223 2.93798 3.71906 2.93798 3.32854 3.32851C2.93801 3.71903 2.93801 4.3522 3.32854 4.74272L10.6229 12.0371L3.32856 19.3314C2.93803 19.722 2.93803 20.3551 3.32856 20.7457C3.71908 21.1362 4.35225 21.1362 4.74277 20.7457L12.0371 13.4513L19.3315 20.7457C19.722 21.1362 20.3552 21.1362 20.7457 20.7457C21.1362 20.3551 21.1362 19.722 20.7457 19.3315L13.4513 12.0371L20.7457 4.74272C21.1362 4.3522 21.1362 3.71903 20.7457 3.32851Z" fill="currentColor"/>
38
38
+
</defs>
39
39
+
</svg>
35
40
<ul class="posts__entries">
36
41
{% for post in posts %}
37
42
<li>