A little app to serve my photography from my personal website

create homepage and start seeding content

ericwood.org 70c4e809 83dde85c

Waiting for spindle ...
+203 -26
+2
src/config.rs
··· 10 10 pub environment: Environment, 11 11 pub cache_path: String, 12 12 pub auto_reload_templates: bool, 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 + 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 - .route("/projects/{slug}", get(routes::projects::show)); 113 + .route("/projects/{slug}", get(routes::projects::show)) 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 + use std::sync::Arc; 2 + 3 + use crate::{ 4 + AppState, Response, 5 + db::{self, Pagination, PhotoQuery, Sort, SortDirection, SortField}, 6 + views::{View, home::HomeIndex}, 7 + }; 8 + use axum::{extract::State, response::Html}; 9 + 10 + pub async fn index(State(state): State<Arc<AppState>>) -> Response { 11 + let blog_posts = state.blog_store.all(); 12 + let projects = state.project_store.all(); 13 + 14 + let (_, photos) = db::get_photos( 15 + &state.photos_db_pool, 16 + PhotoQuery { 17 + sort: Sort { 18 + field: SortField::TakenAt, 19 + direction: SortDirection::Desc, 20 + }, 21 + pagination: Pagination { limit: 10, page: 1 }, 22 + tags: vec![], 23 + }, 24 + ) 25 + .await?; 26 + 27 + let view = HomeIndex::new(blog_posts, projects, photos); 28 + let html = view.render(&state.reloader)?; 29 + 30 + Ok(Html(html)) 31 + }
+3
src/routes/home/mod.rs
··· 1 + mod index; 2 + 3 + pub use index::index;
+1
src/routes/mod.rs
··· 1 1 pub mod blog; 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 - header h1 { 45 + header .home { 46 46 color: var(--foreground); 47 47 text-transform: uppercase; 48 - font-weight: 900; 49 48 margin: 0; 50 - letter-spacing: 3px; 51 49 padding: 10px 30px; 50 + letter-spacing: 3px; 52 51 border-right: solid var(--foreground) 3px; 52 + text-decoration: none; 53 + } 54 + 55 + header .home h1 { 56 + font-weight: 900; 57 + margin: 0; 58 + } 59 + 60 + header .home:hover { 61 + color: var(--background); 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 - .posts__tags { 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 - .posts__tags-title { 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 - let blog_posts_path_str = config.blog_posts_path.clone(); 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 - let blog_posts_path = Path::new(&blog_posts_path_str); 36 - notifier.watch_path(blog_posts_path, true); 35 + let content_path = Path::new(&content_path_str); 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 - <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="display: none"> 16 - <defs> 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 - </defs> 19 - </svg> 20 15 {% endblock %}
+48
src/views/home/index/mod.rs
··· 1 + use std::sync::Arc; 2 + 3 + use minijinja::context; 4 + use minijinja_autoreload::AutoReloader; 5 + 6 + use crate::{models::Photo, post::Post, templates::render, views::View}; 7 + 8 + pub struct HomeIndex { 9 + blog_posts: Vec<Arc<Post>>, 10 + projects: Vec<Arc<Post>>, 11 + photos: Vec<Photo>, 12 + } 13 + 14 + impl HomeIndex { 15 + pub fn new(blog_posts: Vec<Arc<Post>>, projects: Vec<Arc<Post>>, photos: Vec<Photo>) -> Self { 16 + let blog_posts: Vec<Arc<Post>> = first_n(&blog_posts, 5); 17 + let projects: Vec<Arc<Post>> = first_n(&projects, 5); 18 + 19 + Self { 20 + blog_posts, 21 + projects, 22 + photos, 23 + } 24 + } 25 + } 26 + 27 + impl View for HomeIndex { 28 + fn render(&self, reloader: &AutoReloader) -> anyhow::Result<String> { 29 + let html = render( 30 + reloader, 31 + "views/home/index", 32 + context! { 33 + blog_posts => self.blog_posts, 34 + projects => self.projects, 35 + photos => self.photos, 36 + }, 37 + )?; 38 + 39 + Ok(html) 40 + } 41 + } 42 + 43 + fn first_n<T>(collection: &[T], n: usize) -> Vec<T> 44 + where 45 + T: Clone, 46 + { 47 + collection[..std::cmp::min(n, collection.len())].to_vec() 48 + }
+16
src/views/home/index/style.css
··· 1 + .summary { 2 + margin-bottom: 30px; 3 + } 4 + 5 + .summary a { 6 + color: var(--foreground); 7 + } 8 + 9 + .summary .photos { 10 + margin: 16px 0; 11 + gap: 16px; 12 + } 13 + 14 + .summary .photos > * { 15 + max-height: 300px; 16 + }
+50
src/views/home/index/template.jinja
··· 1 + {% extends "layout" %} 2 + {% block title %}Eric, online{% endblock %} 3 + {% block head %} 4 + {{ inline_style("src/styles/posts.css") }} 5 + {{ inline_style("src/views/home/index/style.css") }} 6 + {{ inline_style("src/views/photos/index/style.css") }} 7 + {% endblock %} 8 + {% import "posts" as posts_macro %} 9 + 10 + {% block body %} 11 + <div class="layout"> 12 + <h2>Hello!</h2> 13 + <p>my name is <b>Eric Wood</b> and this is my website</p> 14 + <p>I am an engineer in <b>Golden, Colorado</b></p> 15 + <p>this website contains things I love</p> 16 + <section class="summary"> 17 + {{ posts_macro.table(blog_posts, root_path="/blog", title="Recent blog posts", show_tags=false) }} 18 + <a href="/blog">See all →</a> 19 + </section> 20 + <section class="summary"> 21 + {{ posts_macro.table(projects, root_path="/projects", title="Recent projects", show_tags=false) }} 22 + <a href="/projects">See all →</a> 23 + </section> 24 + <section class="summary"> 25 + <div class="posts__heading"> 26 + <span class="posts__heading-title">Recent photos</span> 27 + </div> 28 + <div class="photos" data-rows="2"> 29 + {% for photo in photos %} 30 + <a 31 + href="/photos/{{ photo.id }}" 32 + id="{{ photo.id }}" 33 + data-width="{{ photo.width }}" 34 + data-height="{{ photo.height }}" 35 + style="aspect-ratio: {{ photo.width / photo.height }}" 36 + > 37 + <img 38 + {% if loop.index < 6 %} 39 + fetchpriority="high" 40 + {% endif %} 41 + src="{{ photo_thumbnail_url(photo.id) }}" 42 + > 43 + </a> 44 + {% endfor %} 45 + </div> 46 + <a href="/photos">See all →</a> 47 + </section> 48 + </div> 49 + {{ inline_script("src/views/photos/index/script.js") }} 50 + {% endblock %}
+2
src/views/home/mod.rs
··· 1 + mod index; 2 + pub use index::HomeIndex;
+1
src/views/mod.rs
··· 1 1 use minijinja_autoreload::AutoReloader; 2 2 3 3 pub mod blog; 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 + 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 + 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 + if (maxRows && currentRow >= maxRows) { 36 + photo.shown = false 37 + return; 38 + } 39 + 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 - photos.forEach(({ element, width, height }) => { 55 - element.style.height = `${height}px`; 56 - element.style.maxWidth = `${width}px`; 57 - element.style.display = 'initial'; 62 + photos.forEach(({ element, width, height, shown }) => { 63 + if (shown) { 64 + element.style.height = `${height}px`; 65 + element.style.maxWidth = `${width}px`; 66 + element.style.display = 'initial'; 67 + } else { 68 + element.style.display = 'none'; 69 + } 58 70 }); 59 71 }; 60 72
+2 -2
templates/layout.jinja
··· 31 31 <div class="container"> 32 32 <div> 33 33 <header> 34 - <div> 34 + <a href="/" class="home"> 35 35 <h1>Eric Wood</h1> 36 - </div> 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 - {% macro table(posts, show_tags=true, root_path="", tag=None) %} 11 - {% if show_tags %} 12 - <div class="posts__tags"> 13 - <span class="posts__tags-title">Categories</span> 10 + {% macro table(posts, show_tags=true, root_path="", tag=None, title="Categories") %} 11 + <div class="posts__heading"> 12 + <span class="posts__heading-title">{{ title }}</span> 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 - </div> 34 - {% endif %} 33 + {% endif %} 34 + </div> 35 + <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="display: none"> 36 + <defs> 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 + </defs> 39 + </svg> 35 40 <ul class="posts__entries"> 36 41 {% for post in posts %} 37 42 <li>