A little app to serve my photography from my personal website

handle query params

+93 -52
+7 -2
src/db.rs
··· 1 1 use crate::{Photo, Tag}; 2 + use serde::Deserialize; 2 3 use sqlx::{QueryBuilder, Sqlite, SqlitePool, query::QueryAs}; 3 4 5 + #[derive(Deserialize, Clone, Copy)] 6 + #[serde(rename_all = "snake_case")] 4 7 pub enum SortDirection { 5 8 Asc, 6 9 Desc, 7 10 } 8 11 9 12 impl SortDirection { 10 - pub fn to_sql(&self) -> String { 13 + pub fn to_sql(self) -> String { 11 14 match self { 12 15 Self::Asc => "ASC", 13 16 Self::Desc => "DESC", ··· 16 19 } 17 20 } 18 21 22 + #[derive(Deserialize, Clone, Copy)] 23 + #[serde(rename_all = "snake_case")] 19 24 pub enum SortField { 20 25 TakenAt, 21 26 CreatedAt, 22 27 } 23 28 24 29 impl SortField { 25 - pub fn to_sql(&self) -> String { 30 + pub fn to_sql(self) -> String { 26 31 match self { 27 32 Self::TakenAt => "taken_at", 28 33 Self::CreatedAt => "created_at",
+5 -48
src/main.rs
··· 1 - use axum::{ 2 - Router, 3 - extract::{Path, State}, 4 - response::Html, 5 - routing::get, 6 - }; 1 + use axum::{Router, routing::get}; 7 2 use dotenv::dotenv; 8 - use minijinja::{Environment, context}; 3 + use minijinja::Environment; 9 4 use std::{env, sync::Arc}; 10 5 mod app_error; 11 6 use app_error::AppError; 12 7 mod date_time; 13 8 mod db; 14 9 mod models; 10 + mod routes; 15 11 mod templates; 16 12 use models::{Photo, Tag}; 17 13 use sqlx::SqlitePool; 18 14 use templates::load_templates; 19 15 use tower_http::services::ServeDir; 20 16 21 - use crate::db::{Pagination, PhotoQuery, Sort, SortDirection, SortField}; 22 - 23 17 struct AppState { 24 18 template_env: Environment<'static>, 25 19 pool: SqlitePool, 26 20 } 27 21 28 - async fn root(State(state): State<Arc<AppState>>) -> Result<Html<String>, AppError> { 29 - let photos = db::get_photos( 30 - &state.pool, 31 - PhotoQuery { 32 - sort: Sort { 33 - field: SortField::TakenAt, 34 - direction: SortDirection::Asc, 35 - }, 36 - pagination: Pagination { limit: 10, page: 1 }, 37 - tag: Some("me".to_string()), 38 - }, 39 - ) 40 - .await?; 41 - 42 - let tags = db::get_tags(&state.pool).await?; 43 - let template = state.template_env.get_template("photos/index")?; 44 - let rendered = template.render(context! { 45 - photos => photos, 46 - tags => tags, 47 - })?; 48 - 49 - Ok(Html(rendered)) 50 - } 51 - 52 - async fn show( 53 - Path(id): Path<String>, 54 - State(state): State<Arc<AppState>>, 55 - ) -> Result<Html<String>, AppError> { 56 - let photo = db::get_photo(&state.pool, id).await?; 57 - let template = state.template_env.get_template("photos/show")?; 58 - let rendered = template.render(context! { 59 - photo => photo 60 - })?; 61 - 62 - Ok(Html(rendered)) 63 - } 64 - 65 22 #[tokio::main] 66 23 async fn main() -> anyhow::Result<()> { 67 24 dotenv().ok(); ··· 72 29 let template_env = load_templates()?; 73 30 let app_state = Arc::new(AppState { template_env, pool }); 74 31 let app = Router::new() 75 - .route("/", get(root)) 76 - .route("/{id}", get(show)) 32 + .route("/", get(routes::photos::index)) 33 + .route("/{id}", get(routes::photos::show)) 77 34 .with_state(app_state) 78 35 .nest_service("/thumbnails", ServeDir::new(&env::var("THUMBNAIL_PATH")?)) 79 36 .nest_service("/images", ServeDir::new(&env::var("IMAGE_PATH")?));
src/routes/lib.rs

This is a binary file and will not be displayed.

+1
src/routes/mod.rs
··· 1 + pub mod photos;
+78
src/routes/photos.rs
··· 1 + use std::sync::Arc; 2 + 3 + use axum::{ 4 + extract::{Path, Query, State}, 5 + response::Html, 6 + }; 7 + use minijinja::context; 8 + use serde::{self, Deserialize}; 9 + 10 + use crate::{ 11 + AppError, AppState, 12 + db::{self, Pagination, PhotoQuery, Sort, SortDirection, SortField}, 13 + }; 14 + 15 + #[derive(Deserialize)] 16 + #[serde(default)] 17 + pub struct IndexParams { 18 + page: u32, 19 + limit: u32, 20 + tag: Option<String>, 21 + sort: SortField, 22 + dir: SortDirection, 23 + } 24 + 25 + impl Default for IndexParams { 26 + fn default() -> Self { 27 + Self { 28 + page: 1, 29 + limit: 10, 30 + tag: None, 31 + sort: SortField::TakenAt, 32 + dir: SortDirection::Desc, 33 + } 34 + } 35 + } 36 + 37 + pub async fn index( 38 + query: Query<IndexParams>, 39 + State(state): State<Arc<AppState>>, 40 + ) -> Result<Html<String>, AppError> { 41 + let photos = db::get_photos( 42 + &state.pool, 43 + PhotoQuery { 44 + sort: Sort { 45 + field: query.sort, 46 + direction: query.dir, 47 + }, 48 + pagination: Pagination { 49 + limit: query.limit, 50 + page: query.page, 51 + }, 52 + tag: query.tag.to_owned(), 53 + }, 54 + ) 55 + .await?; 56 + 57 + let tags = db::get_tags(&state.pool).await?; 58 + let template = state.template_env.get_template("photos/index")?; 59 + let rendered = template.render(context! { 60 + photos => photos, 61 + tags => tags, 62 + })?; 63 + 64 + Ok(Html(rendered)) 65 + } 66 + 67 + pub async fn show( 68 + Path(id): Path<String>, 69 + State(state): State<Arc<AppState>>, 70 + ) -> Result<Html<String>, AppError> { 71 + let photo = db::get_photo(&state.pool, id).await?; 72 + let template = state.template_env.get_template("photos/show")?; 73 + let rendered = template.render(context! { 74 + photo => photo 75 + })?; 76 + 77 + Ok(Html(rendered)) 78 + }
+2 -2
templates/photos/index.jinja
··· 7 7 {% endfor %} 8 8 </ul> 9 9 {% for photo in photos %} 10 - <div> 10 + <a href="/{{ photo.id }}"> 11 11 <p>{{ photo.taken_at }}</p> 12 12 <img src="/thumbnails/{{ photo.id }}.webp"> 13 - </div> 13 + </a> 14 14 {% endfor %} 15 15 {% endblock %}