feat(template-askama): add Askama template adapter for diary entries

This commit is contained in:
2026-05-04 02:04:52 +02:00
parent c4b39c9410
commit b6a7cf9417
7 changed files with 227 additions and 1 deletions

View 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 }

View 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())
}
}

View 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>