Askama + htmx for frontend
Server-side rendering with htmx and Tailwind CSS for the styling
This commit is contained in:
52
Cargo.lock
generated
52
Cargo.lock
generated
@@ -90,6 +90,48 @@ version = "0.5.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
|
checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "askama"
|
||||||
|
version = "0.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f75363874b771be265f4ffe307ca705ef6f3baa19011c149da8674a87f1b75c4"
|
||||||
|
dependencies = [
|
||||||
|
"askama_derive",
|
||||||
|
"itoa",
|
||||||
|
"percent-encoding",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "askama_derive"
|
||||||
|
version = "0.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "129397200fe83088e8a68407a8e2b1f826cf0086b21ccdb866a722c8bcd3a94f"
|
||||||
|
dependencies = [
|
||||||
|
"askama_parser",
|
||||||
|
"basic-toml",
|
||||||
|
"memchr",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"rustc-hash",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "askama_parser"
|
||||||
|
version = "0.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d6ab5630b3d5eaf232620167977f95eb51f3432fc76852328774afbd242d4358"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "assert-json-diff"
|
name = "assert-json-diff"
|
||||||
version = "2.0.2"
|
version = "2.0.2"
|
||||||
@@ -295,6 +337,15 @@ version = "1.8.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
|
checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
|
||||||
|
|
||||||
|
[[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.9.2"
|
version = "2.9.2"
|
||||||
@@ -3840,6 +3891,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"argon2",
|
"argon2",
|
||||||
|
"askama",
|
||||||
"axum",
|
"axum",
|
||||||
"axum-extra",
|
"axum-extra",
|
||||||
"axum-messages",
|
"axum-messages",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ name = "zero2prod"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.99"
|
anyhow = "1.0.99"
|
||||||
argon2 = { version = "0.5.3", features = ["std"] }
|
argon2 = { version = "0.5.3", features = ["std"] }
|
||||||
|
askama = "0.14.0"
|
||||||
axum = { version = "0.8.4", features = ["macros"] }
|
axum = { version = "0.8.4", features = ["macros"] }
|
||||||
axum-extra = { version = "0.10.1", features = ["query", "cookie"] }
|
axum-extra = { version = "0.10.1", features = ["query", "cookie"] }
|
||||||
axum-messages = "0.8.0"
|
axum-messages = "0.8.0"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
application:
|
application:
|
||||||
port: 8000
|
port: 8001
|
||||||
host: "127.0.0.1"
|
host: "127.0.0.1"
|
||||||
base_url: "http://127.0.0.1:8000"
|
base_url: "http://127.0.0.1:8000"
|
||||||
database:
|
database:
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
use axum::response::{Html, IntoResponse};
|
use askama::Template;
|
||||||
|
use axum::response::Html;
|
||||||
|
|
||||||
pub async fn home() -> impl IntoResponse {
|
#[derive(Template)]
|
||||||
Html(include_str!("home/home.html"))
|
#[template(path = "../templates/home.html")]
|
||||||
|
struct HomeTemplate;
|
||||||
|
|
||||||
|
pub async fn home() -> Html<String> {
|
||||||
|
let template = HomeTemplate;
|
||||||
|
Html(template.render().unwrap())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width" />
|
|
||||||
<title>Home</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<p>Welcome to our newsletter!</p>
|
|
||||||
<ol>
|
|
||||||
<li><a href="/login">Admin login</a></li>
|
|
||||||
<li><a href="/register">Register</a></li>
|
|
||||||
</ol>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -4,6 +4,7 @@ use crate::{
|
|||||||
session_state::TypedSession,
|
session_state::TypedSession,
|
||||||
startup::AppState,
|
startup::AppState,
|
||||||
};
|
};
|
||||||
|
use askama::Template;
|
||||||
use axum::{
|
use axum::{
|
||||||
Form, Json,
|
Form, Json,
|
||||||
extract::State,
|
extract::State,
|
||||||
@@ -50,18 +51,25 @@ impl IntoResponse for LoginError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "../templates/login.html")]
|
||||||
|
struct LoginTemplate {
|
||||||
|
error_html: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
pub struct LoginFormData {
|
pub struct LoginFormData {
|
||||||
username: String,
|
username: String,
|
||||||
password: SecretString,
|
password: SecretString,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_login(messages: Messages) -> impl IntoResponse {
|
pub async fn get_login(messages: Messages) -> Html<String> {
|
||||||
let mut error_html = String::new();
|
let mut error_html = String::new();
|
||||||
for message in messages {
|
for message in messages {
|
||||||
writeln!(error_html, "<p><i>{}</i></p>", message).unwrap();
|
writeln!(error_html, "<p><i>{}</i></p>", message).unwrap();
|
||||||
}
|
}
|
||||||
Html(format!(include_str!("login/login.html"), error_html))
|
let template = LoginTemplate { error_html };
|
||||||
|
Html(template.render().unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn post_login(
|
pub async fn post_login(
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width" />
|
|
||||||
<title>Login</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<form action="/login" method="post">
|
|
||||||
<input type="text" name="username" placeholder="Username" />
|
|
||||||
<input type="password" name="password" placeholder="Password" />
|
|
||||||
<button type="submit">Login</button>
|
|
||||||
</form>
|
|
||||||
{}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
81
templates/base.html
Normal file
81
templates/base.html
Normal 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
180
templates/home.html
Normal 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
69
templates/login.html
Normal 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 %}
|
||||||
Reference in New Issue
Block a user