Askama + htmx for frontend

Server-side rendering with htmx and Tailwind CSS for the styling
This commit is contained in:
Alphonse Paix
2025-09-16 01:47:18 +02:00
parent ba6b2dbd93
commit 56035fab30
10 changed files with 403 additions and 37 deletions

81
templates/base.html Normal file
View File

@@ -0,0 +1,81 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{% block title %}zero2prod{% endblock %}</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/htmx.org@1.9.6"></script>
</head>
<body class="bg-gray-50 min-h-screen flex flex-col">
<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="flex justify-between items-center h-16">
<div class="flex-shrink-0">
<a href="/" class="hover:opacity-80 transition-opacity">
<h1 class="text-xl font-bold text-gray-900">
<span class="text-blue-600">zero2prod</span>
</h1>
</a>
</div>
<nav>
<a
href="/admin/dashboard"
class="bg-blue-600 text-white hover:bg-blue-700 px-4 py-2 rounded-md text-sm font-medium transition-colors"
>
Dashboard
</a>
</nav>
</div>
</div>
</header>
<div class="flex flex-1">
<main class="flex-1 lg:ml-0">
<div class="py-8 px-4 sm:px-6 lg:px-8">
{% block content %}{% endblock %}
</div>
</main>
</div>
<footer class="bg-white border-t border-gray-200 mt-auto">
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
<div class="flex flex-col md:flex-row justify-between items-center">
<div
class="flex flex-col md:flex-row items-center space-y-2 md:space-y-0 md:space-x-6"
>
<div class="flex items-center space-x-4">
<a
href="https://gitea.alphonsepaix.xyz/alphonse/zero2prod"
target="_blank"
class="text-sm text-gray-500 hover:text-gray-900 transition-colors flex items-center"
>
Code repository
<svg
class="ml-1 h-3 w-3"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
/>
</svg>
</a>
</div>
</div>
<div class="mt-4 md:mt-0">
<p class="text-xs text-gray-500">
Built with ❤️ using Rust, axum & htmx
</p>
</div>
</div>
</div>
</footer>
</body>
</html>

180
templates/home.html Normal file
View File

@@ -0,0 +1,180 @@
{% extends "base.html" %} {% block title %}Home - zero2prod{% endblock %} {%
block content %}
<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="max-w-3xl">
<h1 class="text-4xl font-bold mb-4">zero2prod</h1>
<p class="text-xl text-blue-100 mb-6">
Welcome to my newsletter! Stay updated on my latest projects and
thoughts. Unsubscribe at any time.
</p>
<div class="flex flex-col sm:flex-row gap-4">
<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"
>
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>
<!-- Feature Cards -->
<div class="grid md:grid-cols-3 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-blue-100 rounded-lg flex items-center justify-center mb-4"
>
<svg
class="w-6 h-6 text-blue-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 10V3L4 14h7v7l9-11h-7z"
/>
</svg>
</div>
<h3 class="text-lg font-semibold text-gray-900 mb-2">Idempotent</h3>
<p class="text-gray-600 text-sm">
Smart duplicate prevention ensures you'll never receive the same email
twice.
</p>
</div>
<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="/api/newsletter/subscribe"
hx-target="#subscription-result"
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="Enter your email address"
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>
</form>
<div id="subscription-result" class="mt-4"></div>
</div>
</div>
<div class="mt-8 bg-gray-50 rounded-lg p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4 text-center">Stats</h3>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-center">
<div>
<div class="text-2xl font-bold text-blue-600" id="subscriber-count">
2
</div>
<div class="text-sm text-gray-600">subscribers</div>
</div>
<div>
<div>
<div class="text-2xl font-bold text-orange-600">23</div>
<div class="text-sm text-gray-600">emails sent</div>
</div>
</div>
<div>
<div class="text-2xl font-bold text-orange-600">0</div>
<div class="text-sm text-gray-600">email opened</div>
</div>
<div>
<div class="text-2xl font-bold text-purple-600">~1ms</div>
<div class="text-sm text-gray-600">response time</div>
</div>
</div>
</div>
<!-- <script>
document.addEventListener("DOMContentLoaded", function () {
htmx.ajax("GET", "/api/stats/subscribers", {
target: "#subscriber-count",
});
});
</script> -->
{% endblock %}
</div>

69
templates/login.html Normal file
View File

@@ -0,0 +1,69 @@
{% extends "base.html" %} {% block title %}Login - zero2prod{% endblock %} {%
block content %}
<div class="min-h-[60vh] flex items-center justify-center">
<div class="max-w-md w-full space-y-8">
<div class="text-center">
<h2 class="text-3xl font-bold text-gray-900">Login</h2>
<p class="mt-2 text-sm text-gray-600">
Sign in to access the admin dashboard
</p>
</div>
<div class="bg-white rounded-lg shadow-md p-8 border border-gray-200">
<form action="/login" method="post" class="space-y-6">
<div>
<label
for="username"
class="block text-sm font-medium text-gray-700 mb-2"
>
Username
</label>
<input
type="text"
id="username"
name="username"
required
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div>
<label
for="password"
class="block text-sm font-medium text-gray-700 mb-2"
>
Password
</label>
<input
type="password"
id="password"
name="password"
required
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div>
<button
type="submit"
class="w-full flex justify-center py-3 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors"
>
Continue
</button>
</div>
</form>
<div class="mt-4">{{ error_html }}</div>
</div>
<div class="text-center">
<a
href="/"
class="text-sm text-blue-600 hover:text-blue-500 transition-colors"
>
← Back to homepage
</a>
</div>
</div>
</div>
{% endblock %}