Templates refactoring

This commit is contained in:
Alphonse Paix
2025-09-20 04:43:55 +02:00
parent d85879a004
commit f7ebf73fbc
15 changed files with 156 additions and 146 deletions

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,7 @@
mod new_subscriber; mod new_subscriber;
mod post;
mod subscriber_email; mod subscriber_email;
pub use new_subscriber::NewSubscriber; pub use new_subscriber::NewSubscriber;
pub use post::PostEntry;
pub use subscriber_email::SubscriberEmail; pub use subscriber_email::SubscriberEmail;

17
src/domain/post.rs Normal file
View File

@@ -0,0 +1,17 @@
use chrono::{DateTime, Utc};
use uuid::Uuid;
pub struct PostEntry {
pub post_id: Uuid,
pub author: Option<String>,
pub title: String,
pub content: String,
pub published_at: DateTime<Utc>,
}
impl PostEntry {
#[allow(dead_code)]
pub fn formatted_date(&self) -> String {
self.published_at.format("%B %d, %Y").to_string()
}
}

View File

@@ -22,7 +22,7 @@ pub use subscriptions_confirm::*;
use crate::{ use crate::{
authentication::AuthError, authentication::AuthError,
templates::{InternalErrorTemplate, MessageTemplate}, templates::{InternalErrorTemplate, MessageTemplate, NotFoundTemplate},
}; };
#[derive(thiserror::Error)] #[derive(thiserror::Error)]
@@ -130,10 +130,6 @@ impl From<AuthError> for AppError {
} }
} }
#[derive(Template)]
#[template(path = "../templates/404.html")]
struct NotFoundTemplate;
pub async fn not_found() -> Response { pub async fn not_found() -> Response {
( (
StatusCode::NOT_FOUND, StatusCode::NOT_FOUND,

View File

@@ -1,4 +1,4 @@
use crate::authentication::AuthenticatedUser; use crate::{authentication::AuthenticatedUser, templates::DashboardTemplate};
use askama::Template; use askama::Template;
use axum::{ use axum::{
Extension, Extension,
@@ -6,14 +6,6 @@ use axum::{
}; };
use uuid::Uuid; use uuid::Uuid;
#[derive(Template)]
#[template(path = "../templates/dashboard.html")]
struct DashboardTemplate {
username: String,
idempotency_key_1: String,
idempotency_key_2: String,
}
pub async fn admin_dashboard( pub async fn admin_dashboard(
Extension(AuthenticatedUser { username, .. }): Extension<AuthenticatedUser>, Extension(AuthenticatedUser { username, .. }): Extension<AuthenticatedUser>,
) -> Response { ) -> Response {

View File

@@ -1,9 +1,7 @@
use askama::Template; use askama::Template;
use axum::response::Html; use axum::response::Html;
#[derive(Template)] use crate::templates::HomeTemplate;
#[template(path = "../templates/home.html")]
struct HomeTemplate;
pub async fn home() -> Html<String> { pub async fn home() -> Html<String> {
Html(HomeTemplate.render().unwrap()) Html(HomeTemplate.render().unwrap())

View File

@@ -3,6 +3,7 @@ use crate::{
routes::AppError, routes::AppError,
session_state::TypedSession, session_state::TypedSession,
startup::AppState, startup::AppState,
templates::LoginTemplate,
}; };
use anyhow::Context; use anyhow::Context;
use askama::Template; use askama::Template;
@@ -15,10 +16,6 @@ use axum::{
use axum::{http::StatusCode, response::Redirect}; use axum::{http::StatusCode, response::Redirect};
use secrecy::SecretString; use secrecy::SecretString;
#[derive(Template)]
#[template(path = "../templates/login.html")]
struct LoginTemplate;
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
pub struct LoginFormData { pub struct LoginFormData {
username: String, username: String,

View File

@@ -1,41 +1,18 @@
use crate::{routes::AppError, startup::AppState}; use crate::{
domain::PostEntry,
routes::AppError,
startup::AppState,
templates::{PostTemplate, PostsTemplate},
};
use anyhow::Context; use anyhow::Context;
use askama::Template; use askama::Template;
use axum::{ use axum::{
extract::{Path, State}, extract::{Path, State},
response::{Html, IntoResponse, Response}, response::{Html, IntoResponse, Response},
}; };
use chrono::{DateTime, Utc};
use sqlx::PgPool; use sqlx::PgPool;
use uuid::Uuid; use uuid::Uuid;
struct PostEntry {
post_id: Uuid,
author: Option<String>,
title: String,
content: String,
published_at: DateTime<Utc>,
}
impl PostEntry {
#[allow(dead_code)]
fn formatted_date(&self) -> String {
self.published_at.format("%B %d, %Y").to_string()
}
}
#[derive(Template)]
#[template(path = "../templates/posts.html")]
struct PostsTemplate {
posts: Vec<PostEntry>,
}
#[derive(Template)]
#[template(path = "../templates/post.html")]
struct PostTemplate {
post: PostEntry,
}
pub async fn list_posts( pub async fn list_posts(
State(AppState { State(AppState {
connection_pool, .. connection_pool, ..

View File

@@ -1,4 +1,4 @@
use crate::startup::AppState; use crate::{startup::AppState, templates::ConfirmTemplate};
use askama::Template; use askama::Template;
use axum::{ use axum::{
extract::{Query, State}, extract::{Query, State},
@@ -9,10 +9,6 @@ use serde::Deserialize;
use sqlx::PgPool; use sqlx::PgPool;
use uuid::Uuid; use uuid::Uuid;
#[derive(Template)]
#[template(path = "../templates/confirm.html")]
struct ConfirmTemplate;
#[tracing::instrument(name = "Confirming new subscriber", skip(params))] #[tracing::instrument(name = "Confirming new subscriber", skip(params))]
pub async fn confirm( pub async fn confirm(
State(AppState { State(AppState {

View File

@@ -1,5 +1,7 @@
use askama::Template; use askama::Template;
use crate::domain::PostEntry;
#[derive(Template)] #[derive(Template)]
pub enum MessageTemplate { pub enum MessageTemplate {
#[template(path = "../templates/success.html")] #[template(path = "../templates/success.html")]
@@ -11,3 +13,39 @@ pub enum MessageTemplate {
#[derive(Template)] #[derive(Template)]
#[template(path = "../templates/500.html")] #[template(path = "../templates/500.html")]
pub struct InternalErrorTemplate; pub struct InternalErrorTemplate;
#[derive(Template)]
#[template(path = "../templates/login.html")]
pub struct LoginTemplate;
#[derive(Template)]
#[template(path = "../templates/dashboard.html")]
pub struct DashboardTemplate {
pub username: String,
pub idempotency_key_1: String,
pub idempotency_key_2: String,
}
#[derive(Template)]
#[template(path = "../templates/home.html")]
pub struct HomeTemplate;
#[derive(Template)]
#[template(path = "../templates/posts.html")]
pub struct PostsTemplate {
pub posts: Vec<PostEntry>,
}
#[derive(Template)]
#[template(path = "../templates/post.html")]
pub struct PostTemplate {
pub post: PostEntry,
}
#[derive(Template)]
#[template(path = "../templates/confirm.html")]
pub struct ConfirmTemplate;
#[derive(Template)]
#[template(path = "../templates/404.html")]
pub struct NotFoundTemplate;

View File

@@ -7,7 +7,7 @@
<h1 class="text-4xl font-semibold text-gray-700 mb-4">404</h1> <h1 class="text-4xl font-semibold text-gray-700 mb-4">404</h1>
<h2 class="text-2xl font-semibold text-gray-500 mb-6">Not Found</h2> <h2 class="text-2xl font-semibold text-gray-500 mb-6">Not Found</h2>
<p class="text-gray-600 mb-8 max-w-2xl mx-auto"> <p class="text-gray-600 mb-8 max-w-2xl mx-auto">
Sorry, we couldn't find the page you're looking for. The page may have been moved, deleted, or the URL might be incorrect. Sorry, I couldn't find the page you're looking for. The page may have been moved, deleted, or the URL might be incorrect.
</p> </p>
<div class="flex flex-col sm:flex-row gap-4 justify-center items-center"> <div class="flex flex-col sm:flex-row gap-4 justify-center items-center">
<a href="/" <a href="/"

View File

@@ -7,7 +7,7 @@
<h1 class="text-4xl font-semibold text-red-600 mb-4">500</h1> <h1 class="text-4xl font-semibold text-red-600 mb-4">500</h1>
<h2 class="text-2xl font-semibold text-gray-500 mb-6">Internal Server Error</h2> <h2 class="text-2xl font-semibold text-gray-500 mb-6">Internal Server Error</h2>
<p class="text-gray-600 mb-8 max-w-2xl mx-auto"> <p class="text-gray-600 mb-8 max-w-2xl mx-auto">
Something went wrong on the server. Please try again in a few minutes or contact me if the problem persists. Something went wrong. Please try again in a few minutes or contact me if the problem persists.
</p> </p>
<div class="flex flex-col sm:flex-row gap-4 justify-center items-center"> <div class="flex flex-col sm:flex-row gap-4 justify-center items-center">
<a href="/" <a href="/"

View File

@@ -16,27 +16,26 @@
<header class="bg-white shadow-sm border-b border-gray-200 top-0 z-40"> <header class="bg-white shadow-sm border-b border-gray-200 top-0 z-40">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-16"> <div class="flex justify-between items-center h-16">
<div class="flex items-center space-x-6"> <div class="flex items-center space-x-4 text-blue-600">
<a href="/" <div class="flex space-x-2 items-center">
class="flex items-center space-x-2 text-blue-600 hover:text-blue-700 transition-colors"> <div class="w-6 h-6 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-lg flex items-center justify-center">
<div class="w-8 h-8 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-lg flex items-center justify-center"> <svg class="w-4 h-4 text-white"
<svg class="w-5 h-5 text-white"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor"> stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg> </svg>
</div> </div>
<span class="text-xl font-bold">zero2prod</span> <span class="text-sm font-bold">zero2prod</span>
</a> </div>
<nav class="flex items-center space-x-2"> <nav class="flex items-center space-x-2">
<a href="/" <a href="/"
class="flex items-center text-gray-700 hover:text-blue-600 hover:bg-blue-50 px-3 py-2 rounded-md text-sm font-medium transition-colors"> class="flex items-center text-gray-700 hover:underline underline-offset-2 decoration-1 decoration-blue-600 hover:text-blue-600 px-3 py-2 rounded-md text-sm font-medium transition-colors">
Home home
</a> </a>
<a href="/posts" <a href="/posts"
class="flex items-center text-gray-700 hover:text-blue-600 hover:bg-blue-50 px-3 py-2 rounded-md text-sm font-medium transition-colors"> class="flex items-center text-gray-700 hover:underline underline-offset-2 decoration-1 decoration-blue-600 hover:text-blue-600 px-3 py-2 rounded-md text-sm font-medium transition-colors">
Posts posts
</a> </a>
</nav> </nav>
</div> </div>
@@ -44,7 +43,7 @@
<a href="/admin/dashboard" <a href="/admin/dashboard"
hx-boost="true" hx-boost="true"
class="bg-blue-600 text-white hover:bg-blue-700 px-4 py-2 rounded-md text-sm font-medium transition-colors"> class="bg-blue-600 text-white hover:bg-blue-700 px-4 py-2 rounded-md text-sm font-medium transition-colors">
Dashboard dashboard
</a> </a>
</nav> </nav>
</div> </div>

View File

@@ -1,83 +1,81 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Home{% endblock %} {% block title %}Home{% endblock %}
{% block content %} {% block content %}
<div class="flex-1 flex items-center justify-center"> <div class="max-w-4xl mx-auto">
<div class="max-w-4xl mx-auto"> <div class="bg-gradient-to-r from-blue-600 to-indigo-700 rounded-lg shadow-lg text-white p-8 mb-8">
<div class="bg-gradient-to-r from-blue-600 to-indigo-700 rounded-lg shadow-lg text-white p-8 mb-8"> <div class="max-w-3xl">
<div class="max-w-3xl"> <h1 class="text-4xl font-bold mb-4">zero2prod</h1>
<h1 class="text-4xl font-bold mb-4">zero2prod</h1> <p class="text-xl text-blue-100 mb-6">
<p class="text-xl text-blue-100 mb-6"> Welcome to my blog! Stay updated on my latest projects and
Welcome to my blog! Stay updated on my latest projects and thoughts. Subscribe (and unsubscribe) at any time.
thoughts. Subscribe (and unsubscribe) at any time. </p>
</p> <div class="flex flex-col sm:flex-row gap-4">
<div class="flex flex-col sm:flex-row gap-4"> <a href="#newsletter-signup"
<a href="#newsletter-signup" class="bg-white text-blue-600 hover:bg-gray-100 font-semibold py-3 px-6 rounded-md transition-colors text-center">
class="bg-white text-blue-600 hover:bg-gray-100 font-semibold py-3 px-6 rounded-md transition-colors text-center"> Subscribe
</a>
<a href="https://gitea.alphonsepaix.xyz/alphonse/zero2prod"
target="_blank"
class="border border-white text-white hover:bg-white hover:text-blue-600 font-semibold py-3 px-6 rounded-md transition-colors text-center">
View code
</a>
</div>
</div>
</div>
<div class="grid md:grid-cols-2 gap-6 mb-8">
<div class="bg-white rounded-lg shadow-md p-6 border border-gray-200">
<div class="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center mb-4">
<svg class="w-6 h-6 text-green-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
</div>
<h3 class="text-lg font-semibold text-gray-900 mb-2">Privacy first</h3>
<p class="text-gray-600 text-sm">
Zero spam, zero tracking, zero data sharing. Your email stays private
and secure. Unsubscribe at any time.
</p>
</div>
<div class="bg-white rounded-lg shadow-md p-6 border border-gray-200">
<div class="w-12 h-12 bg-purple-100 rounded-lg flex items-center justify-center mb-4">
<svg class="w-6 h-6 text-purple-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
</div>
<h3 class="text-lg font-semibold text-gray-900 mb-2">Quality content</h3>
<p class="text-gray-600 text-sm">
Curated insights on Rust backend development, performance tips, and
production war stories.
</p>
</div>
</div>
<div id="newsletter-signup"
class="bg-white rounded-lg shadow-md p-8 border border-gray-200">
<div class="max-w-2xl mx-auto text-center">
<h2 class="text-2xl font-bold text-gray-900 mb-4">Stay updated</h2>
<p class="text-gray-600 mb-6">Subscribe to my newsletter to get the latest updates.</p>
<form hx-post="/subscriptions"
hx-target="#subscribe-messages"
hx-swap="innerHTML"
class="max-w-md mx-auto">
<div class="flex flex-col sm:flex-row gap-3">
<input type="email"
name="email"
placeholder="you@example.com"
required
class="flex-1 px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" />
<button type="submit"
class="bg-blue-600 text-white hover:bg-blue-700 font-medium py-3 px-6 rounded-md transition-colors">
Subscribe Subscribe
</a> </button>
<a href="https://gitea.alphonsepaix.xyz/alphonse/zero2prod"
target="_blank"
class="border border-white text-white hover:bg-white hover:text-blue-600 font-semibold py-3 px-6 rounded-md transition-colors text-center">
View code
</a>
</div> </div>
</div> <div id="subscribe-messages" class="mt-4"></div>
</div> </form>
<div class="grid md:grid-cols-2 gap-6 mb-8">
<div class="bg-white rounded-lg shadow-md p-6 border border-gray-200">
<div class="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center mb-4">
<svg class="w-6 h-6 text-green-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
</div>
<h3 class="text-lg font-semibold text-gray-900 mb-2">Privacy first</h3>
<p class="text-gray-600 text-sm">
Zero spam, zero tracking, zero data sharing. Your email stays private
and secure. Unsubscribe at any time.
</p>
</div>
<div class="bg-white rounded-lg shadow-md p-6 border border-gray-200">
<div class="w-12 h-12 bg-purple-100 rounded-lg flex items-center justify-center mb-4">
<svg class="w-6 h-6 text-purple-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
</div>
<h3 class="text-lg font-semibold text-gray-900 mb-2">Quality content</h3>
<p class="text-gray-600 text-sm">
Curated insights on Rust backend development, performance tips, and
production war stories.
</p>
</div>
</div>
<div id="newsletter-signup"
class="bg-white rounded-lg shadow-md p-8 border border-gray-200">
<div class="max-w-2xl mx-auto text-center">
<h2 class="text-2xl font-bold text-gray-900 mb-4">Stay updated</h2>
<p class="text-gray-600 mb-6">Subscribe to my newsletter to get the latest updates.</p>
<form hx-post="/subscriptions"
hx-target="#subscribe-messages"
hx-swap="innerHTML"
class="max-w-md mx-auto">
<div class="flex flex-col sm:flex-row gap-3">
<input type="email"
name="email"
placeholder="you@example.com"
required
class="flex-1 px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" />
<button type="submit"
class="bg-blue-600 text-white hover:bg-blue-700 font-medium py-3 px-6 rounded-md transition-colors">
Subscribe
</button>
</div>
<div id="subscribe-messages" class="mt-4"></div>
</form>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,7 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Login - zero2prod{% endblock %} {% block title %}Login - zero2prod{% endblock %}
{% block content %} {% block content %}
<div class="min-h-[60vh] flex items-center justify-center"> <div class="flex-1 flex items-center justify-center">
<div class="max-w-md w-full space-y-8"> <div class="max-w-md w-full space-y-8">
<div class="text-center"> <div class="text-center">
<h2 class="text-3xl font-bold text-gray-900">Login</h2> <h2 class="text-3xl font-bold text-gray-900">Login</h2>