feat(template-askama): add Askama template adapter for diary entries
This commit is contained in:
93
Cargo.lock
generated
93
Cargo.lock
generated
@@ -42,6 +42,59 @@ dependencies = [
|
|||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "askama"
|
||||||
|
version = "0.16.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f1bf825125edd887a019d0a3a837dcc5499a68b0d034cc3eb594070c3e18addc"
|
||||||
|
dependencies = [
|
||||||
|
"askama_macros",
|
||||||
|
"itoa",
|
||||||
|
"percent-encoding",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "askama_derive"
|
||||||
|
version = "0.16.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e1c7065972a130eafa84215f21352ae15b4a7393da48c1f5e103904490736738"
|
||||||
|
dependencies = [
|
||||||
|
"askama_parser",
|
||||||
|
"basic-toml",
|
||||||
|
"glob",
|
||||||
|
"memchr",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"rustc-hash",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "askama_macros"
|
||||||
|
version = "0.16.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0e23b1d2c4bd39a41971f6124cef4cc6fd0540913ecb90919b69ab3bbe44ae1a"
|
||||||
|
dependencies = [
|
||||||
|
"askama_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "askama_parser"
|
||||||
|
version = "0.16.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7db09fde9143e7ac4513358fb32ee32847125b63b18ea715afd487956da715da"
|
||||||
|
dependencies = [
|
||||||
|
"rustc-hash",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"unicode-ident",
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-trait"
|
name = "async-trait"
|
||||||
version = "0.1.89"
|
version = "0.1.89"
|
||||||
@@ -154,6 +207,15 @@ version = "1.8.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "basic-toml"
|
||||||
|
version = "0.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.11.1"
|
version = "2.11.1"
|
||||||
@@ -543,6 +605,12 @@ dependencies = [
|
|||||||
"wasip3",
|
"wasip3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "glob"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.15.5"
|
version = "0.15.5"
|
||||||
@@ -1307,6 +1375,12 @@ dependencies = [
|
|||||||
name = "rss"
|
name = "rss"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc-hash"
|
||||||
|
version = "2.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls"
|
name = "rustls"
|
||||||
version = "0.23.40"
|
version = "0.23.40"
|
||||||
@@ -1790,6 +1864,16 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "template-askama"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"askama",
|
||||||
|
"domain",
|
||||||
|
"presentation",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "2.0.18"
|
version = "2.0.18"
|
||||||
@@ -2455,6 +2539,15 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wit-bindgen"
|
name = "wit-bindgen"
|
||||||
version = "0.51.0"
|
version = "0.51.0"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ members = [
|
|||||||
"crates/adapters/auth",
|
"crates/adapters/auth",
|
||||||
"crates/adapters/metadata",
|
"crates/adapters/metadata",
|
||||||
"crates/adapters/rss",
|
"crates/adapters/rss",
|
||||||
"crates/adapters/sqlite",
|
"crates/adapters/sqlite", "crates/adapters/template-askama",
|
||||||
"crates/application",
|
"crates/application",
|
||||||
"crates/common",
|
"crates/common",
|
||||||
"crates/domain",
|
"crates/domain",
|
||||||
|
|||||||
12
crates/adapters/template-askama/Cargo.toml
Normal file
12
crates/adapters/template-askama/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
[package]
|
||||||
|
name = "template-askama"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
askama = { version = "0.16.0" }
|
||||||
|
|
||||||
|
serde = { workspace = true }
|
||||||
|
|
||||||
|
domain = { workspace = true }
|
||||||
|
presentation = { workspace = true }
|
||||||
39
crates/adapters/template-askama/src/lib.rs
Normal file
39
crates/adapters/template-askama/src/lib.rs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
// crates/adapters/template-askama/src/lib.rs
|
||||||
|
use askama::Template;
|
||||||
|
use domain::models::{DiaryEntry, collections::Paginated};
|
||||||
|
use presentation::ports::HtmlRenderer; // Assuming you exposed the port
|
||||||
|
|
||||||
|
// The internal Askama template
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "diary.html")]
|
||||||
|
struct DiaryTemplate<'a> {
|
||||||
|
entries: &'a [DiaryEntry],
|
||||||
|
current_offset: u32,
|
||||||
|
limit: u32,
|
||||||
|
has_more: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
// The public adapter struct
|
||||||
|
pub struct AskamaHtmlRenderer;
|
||||||
|
|
||||||
|
impl AskamaHtmlRenderer {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implementing the presentation port
|
||||||
|
impl HtmlRenderer for AskamaHtmlRenderer {
|
||||||
|
fn render_diary_page(&self, data: &Paginated<DiaryEntry>) -> Result<String, String> {
|
||||||
|
let has_more = (data.offset + data.limit) < data.total_count as u32;
|
||||||
|
|
||||||
|
let template = DiaryTemplate {
|
||||||
|
entries: &data.items,
|
||||||
|
current_offset: data.offset,
|
||||||
|
limit: data.limit,
|
||||||
|
has_more,
|
||||||
|
};
|
||||||
|
|
||||||
|
template.render().map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
76
crates/adapters/template-askama/templates/diary.html
Normal file
76
crates/adapters/template-askama/templates/diary.html
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
<!-- crates/presentation/templates/diary.html -->
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>My Movie Diary</title>
|
||||||
|
<style>
|
||||||
|
/* Minimalist old-school styling */
|
||||||
|
body { font-family: monospace; max-width: 800px; margin: 0 auto; padding: 20px; }
|
||||||
|
.entry { border-bottom: 1px solid #ccc; padding: 10px 0; }
|
||||||
|
.poster { max-width: 100px; float: left; margin-right: 15px; }
|
||||||
|
.clear { clear: both; }
|
||||||
|
.error { color: red; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Movie Diary</h1>
|
||||||
|
|
||||||
|
<!-- Zero-JS Form Submission -->
|
||||||
|
<form action="/reviews" method="POST">
|
||||||
|
<fieldset>
|
||||||
|
<legend>Log a Movie</legend>
|
||||||
|
|
||||||
|
<label for="tmdb_id">TMDB ID (Optional):</label>
|
||||||
|
<input type="text" name="external_metadata_id" id="tmdb_id"><br><br>
|
||||||
|
|
||||||
|
<label for="title">Title (Fallback):</label>
|
||||||
|
<input type="text" name="manual_title" id="title"><br><br>
|
||||||
|
|
||||||
|
<label for="year">Year (Fallback):</label>
|
||||||
|
<input type="number" name="manual_release_year" id="year" min="1888"><br><br>
|
||||||
|
|
||||||
|
<label for="rating">Rating (0-5):</label>
|
||||||
|
<input type="number" name="rating" id="rating" min="0" max="5" required><br><br>
|
||||||
|
|
||||||
|
<button type="submit">Log Movie</button>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- Rendering the Domain Models -->
|
||||||
|
<div class="diary-entries">
|
||||||
|
{% for entry in entries %}
|
||||||
|
<div class="entry">
|
||||||
|
{% if let Some(poster) = entry.movie().poster_path() %}
|
||||||
|
<!-- Assuming you have a route to serve the raw images -->
|
||||||
|
<img src="/static/posters/{{ poster.value() }}" class="poster" alt="Poster">
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<h3>{{ entry.movie().title().value() }} ({{ entry.movie().release_year().value() }})</h3>
|
||||||
|
<p><strong>Rating:</strong> {{ entry.review().rating().value() }} / 5</p>
|
||||||
|
|
||||||
|
{% if let Some(comment) = entry.review().comment() %}
|
||||||
|
<p><em>"{{ comment.value() }}"</em></p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p><small>Watched on: {{ entry.review().watched_at().format("%Y-%m-%d") }}</small></p>
|
||||||
|
<div class="clear"></div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p>No movies logged yet. Go watch something!</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Simple Pagination -->
|
||||||
|
<div>
|
||||||
|
{% if current_offset > 0 %}
|
||||||
|
<a href="/diary?offset={{ current_offset - limit }}">Previous Page</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if has_more %}
|
||||||
|
<a href="/diary?offset={{ current_offset + limit }}">Next Page</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1
crates/presentation/src/lib.rs
Normal file
1
crates/presentation/src/lib.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod ports;
|
||||||
5
crates/presentation/src/ports.rs
Normal file
5
crates/presentation/src/ports.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
use domain::models::{DiaryEntry, collections::Paginated};
|
||||||
|
|
||||||
|
pub trait HtmlRenderer: Send + Sync {
|
||||||
|
fn render_diary_page(&self, data: &Paginated<DiaryEntry>) -> Result<String, String>;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user