feat(templates): add base layout, login, register, new_review templates; update diary
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
use askama::Template;
|
||||
use application::ports::HtmlRenderer;
|
||||
use application::ports::{
|
||||
HtmlPageContext, HtmlRenderer, LoginPageData, NewReviewPageData, RegisterPageData,
|
||||
};
|
||||
use domain::models::{DiaryEntry, collections::Paginated};
|
||||
|
||||
#[derive(Template)]
|
||||
@@ -9,6 +11,28 @@ struct DiaryTemplate<'a> {
|
||||
current_offset: u32,
|
||||
limit: u32,
|
||||
has_more: bool,
|
||||
ctx: &'a HtmlPageContext,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "login.html")]
|
||||
struct LoginTemplate<'a> {
|
||||
error: Option<&'a str>,
|
||||
ctx: &'a HtmlPageContext,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "register.html")]
|
||||
struct RegisterTemplate<'a> {
|
||||
error: Option<&'a str>,
|
||||
ctx: &'a HtmlPageContext,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "new_review.html")]
|
||||
struct NewReviewTemplate<'a> {
|
||||
error: Option<&'a str>,
|
||||
ctx: &'a HtmlPageContext,
|
||||
}
|
||||
|
||||
pub struct AskamaHtmlRenderer;
|
||||
@@ -20,16 +44,43 @@ impl AskamaHtmlRenderer {
|
||||
}
|
||||
|
||||
impl HtmlRenderer for AskamaHtmlRenderer {
|
||||
fn render_diary_page(&self, data: &Paginated<DiaryEntry>) -> Result<String, String> {
|
||||
fn render_diary_page(&self, data: &Paginated<DiaryEntry>, ctx: HtmlPageContext) -> Result<String, String> {
|
||||
let has_more = (data.offset + data.limit) < data.total_count as u32;
|
||||
|
||||
let template = DiaryTemplate {
|
||||
DiaryTemplate {
|
||||
entries: &data.items,
|
||||
current_offset: data.offset,
|
||||
limit: data.limit,
|
||||
has_more,
|
||||
};
|
||||
ctx: &ctx,
|
||||
}
|
||||
.render()
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
template.render().map_err(|e| e.to_string())
|
||||
fn render_login_page(&self, data: LoginPageData<'_>) -> Result<String, String> {
|
||||
LoginTemplate {
|
||||
error: data.error,
|
||||
ctx: &data.ctx,
|
||||
}
|
||||
.render()
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn render_register_page(&self, data: RegisterPageData<'_>) -> Result<String, String> {
|
||||
RegisterTemplate {
|
||||
error: data.error,
|
||||
ctx: &data.ctx,
|
||||
}
|
||||
.render()
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn render_new_review_page(&self, data: NewReviewPageData<'_>) -> Result<String, String> {
|
||||
NewReviewTemplate {
|
||||
error: data.error,
|
||||
ctx: &data.ctx,
|
||||
}
|
||||
.render()
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
29
crates/adapters/template-askama/templates/base.html
Normal file
29
crates/adapters/template-askama/templates/base.html
Normal file
@@ -0,0 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Movies Diary</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<a href="/" class="site-title">Movies Diary</a>
|
||||
<nav>
|
||||
{% if let Some(email) = ctx.user_email %}
|
||||
<a href="/reviews/new">Add Review</a>
|
||||
<span class="user-email">{{ email }}</span>
|
||||
<a href="/logout">Logout</a>
|
||||
{% else %}
|
||||
<a href="/login">Login</a>
|
||||
{% if ctx.register_enabled %}
|
||||
<a href="/register">Register</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,76 +1,38 @@
|
||||
<!-- 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">
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="diary">
|
||||
{% for entry in entries %}
|
||||
<div class="entry">
|
||||
<article 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 class="poster">
|
||||
<img src="/static/posters/{{ poster.value() }}" alt="">
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="entry-body">
|
||||
<div class="entry-title">
|
||||
{{ entry.movie().title().value() }}
|
||||
<span class="year">({{ entry.movie().release_year().value() }})</span>
|
||||
</div>
|
||||
{% if let Some(dir) = entry.movie().director() %}
|
||||
<div class="director">{{ dir }}</div>
|
||||
{% endif %}
|
||||
<div class="rating">{{ entry.review().rating().value() }}/5</div>
|
||||
{% if let Some(comment) = entry.review().comment() %}
|
||||
<div class="comment">{{ comment.value() }}</div>
|
||||
{% endif %}
|
||||
<div class="watched-at">{{ entry.review().watched_at().format("%Y-%m-%d") }}</div>
|
||||
</div>
|
||||
</article>
|
||||
{% else %}
|
||||
<p>No movies logged yet. Go watch something!</p>
|
||||
<p class="empty">No movies logged yet.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Simple Pagination -->
|
||||
<div>
|
||||
<nav class="pagination">
|
||||
{% if current_offset > 0 %}
|
||||
<a href="/diary?offset={{ current_offset - limit }}">Previous Page</a>
|
||||
<a href="/?offset={{ current_offset - limit }}">← Prev</a>
|
||||
{% endif %}
|
||||
{% if has_more %}
|
||||
<a href="/diary?offset={{ current_offset + limit }}">Next Page</a>
|
||||
<a href="/?offset={{ current_offset + limit }}">Next →</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</nav>
|
||||
{% endblock %}
|
||||
|
||||
18
crates/adapters/template-askama/templates/login.html
Normal file
18
crates/adapters/template-askama/templates/login.html
Normal file
@@ -0,0 +1,18 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<h1>Login</h1>
|
||||
{% if let Some(err) = error %}
|
||||
<p class="error">{{ err }}</p>
|
||||
{% endif %}
|
||||
<form method="POST" action="/login">
|
||||
<label>
|
||||
Email<br>
|
||||
<input type="email" name="email" required autocomplete="email">
|
||||
</label>
|
||||
<label>
|
||||
Password<br>
|
||||
<input type="password" name="password" required autocomplete="current-password">
|
||||
</label>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
40
crates/adapters/template-askama/templates/new_review.html
Normal file
40
crates/adapters/template-askama/templates/new_review.html
Normal file
@@ -0,0 +1,40 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<h1>Log a Review</h1>
|
||||
{% if let Some(err) = error %}
|
||||
<p class="error">{{ err }}</p>
|
||||
{% endif %}
|
||||
<form method="POST" action="/reviews">
|
||||
<label>
|
||||
OMDB ID <span class="optional">(optional)</span><br>
|
||||
<input type="text" name="external_metadata_id" placeholder="tt0166924">
|
||||
</label>
|
||||
<hr>
|
||||
<label>
|
||||
Title<br>
|
||||
<input type="text" name="manual_title">
|
||||
</label>
|
||||
<label>
|
||||
Year<br>
|
||||
<input type="number" name="manual_release_year" min="1888" max="2100">
|
||||
</label>
|
||||
<label>
|
||||
Director<br>
|
||||
<input type="text" name="manual_director">
|
||||
</label>
|
||||
<hr>
|
||||
<label>
|
||||
Rating (0–5)<br>
|
||||
<input type="number" name="rating" min="0" max="5" required>
|
||||
</label>
|
||||
<label>
|
||||
Watched<br>
|
||||
<input type="datetime-local" name="watched_at" required>
|
||||
</label>
|
||||
<label>
|
||||
Comment<br>
|
||||
<textarea name="comment"></textarea>
|
||||
</label>
|
||||
<button type="submit">Log Review</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
18
crates/adapters/template-askama/templates/register.html
Normal file
18
crates/adapters/template-askama/templates/register.html
Normal file
@@ -0,0 +1,18 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<h1>Register</h1>
|
||||
{% if let Some(err) = error %}
|
||||
<p class="error">{{ err }}</p>
|
||||
{% endif %}
|
||||
<form method="POST" action="/register">
|
||||
<label>
|
||||
Email<br>
|
||||
<input type="email" name="email" required autocomplete="email">
|
||||
</label>
|
||||
<label>
|
||||
Password<br>
|
||||
<input type="password" name="password" required autocomplete="new-password">
|
||||
</label>
|
||||
<button type="submit">Register</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user