web UI, tailwind, icons, login
This commit is contained in:
19
src/web/pages/dashboard.rs
Normal file
19
src/web/pages/dashboard.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use axum::extract::Request;
|
||||
use maud::{Markup, html};
|
||||
|
||||
use crate::{
|
||||
users::{User, auth::UserAuthenticate},
|
||||
web::{components::nav::nav, pages::base},
|
||||
};
|
||||
|
||||
pub async fn page(req: Request) -> Markup {
|
||||
let u = User::authenticate(req.headers()).ok().flatten();
|
||||
base(
|
||||
"Dashboard | Mnemosyne",
|
||||
html!(
|
||||
(nav(u, req.uri().path()))
|
||||
|
||||
div class="text-8xl text-neutral-800/25 mt-16 text-center font-semibold font-lora select-none" {"Mnemosyne"}
|
||||
),
|
||||
)
|
||||
}
|
||||
7
src/web/pages/index.rs
Normal file
7
src/web/pages/index.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
use axum::response::{IntoResponse, Redirect, Response};
|
||||
|
||||
use crate::users::auth::AuthError;
|
||||
|
||||
pub async fn page() -> Result<Response, AuthError> {
|
||||
Ok(Redirect::to("/dashboard").into_response())
|
||||
}
|
||||
109
src/web/pages/login.rs
Normal file
109
src/web/pages/login.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
use axum::{
|
||||
extract::{Query, Request},
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
};
|
||||
use maud::{PreEscaped, html};
|
||||
use rand::seq::IndexedRandom;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
config::REFERENCE_SPLASHES,
|
||||
users::{
|
||||
User,
|
||||
auth::{AuthError, UserAuthenticate},
|
||||
},
|
||||
web::{components::marquee::marquee, icons, pages::base},
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct LoginMsg {
|
||||
msg: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn page(Query(q): Query<LoginMsg>, req: Request) -> Result<Response, AuthError> {
|
||||
let u = User::authenticate(req.headers())?;
|
||||
if u.is_some() {
|
||||
return Ok(Redirect::to("/dashboard").into_response());
|
||||
}
|
||||
Ok(base(
|
||||
"Log in | Mnemosyne",
|
||||
html!(
|
||||
div class="min-h-screen flex flex-col" {
|
||||
(marquee(&["the goddess of memory"]))
|
||||
|
||||
div class="mt-24" {}
|
||||
a href="/" class="font-semibold text-6xl mx-auto font-lora hover:underline" {h1 {"Mnemosyne"}}
|
||||
p class="text-neutral-500 mt-4 text-sm mx-auto mb-16" {"The goddess of memory holds all the cards."}
|
||||
|
||||
div class="mx-auto bg-neutral-200/5 border border-neutral-200/25 p-4 rounded" {
|
||||
p class="text-neutral-500" {"Part of the olympic pack already? Log in here."}
|
||||
|
||||
form id="login-form" method="post" action="/api/auth/login-form" class="mt-8 font-light flex flex-col" {
|
||||
label for="handle" class="text-neutral-500" {"Handle"}
|
||||
div class="flex items-center w-full border border-neutral-200/25 rounded bg-neutral-950/50" {
|
||||
span class="pl-2 text-neutral-500 select-none" {"@"}
|
||||
input id="handle" name="handle" type="text"
|
||||
class="w-full bg-transparent pl-[2px] pr-1 py-1 outline-none";
|
||||
}
|
||||
|
||||
label for="password" class="text-neutral-500 font-light mt-2" {"Password"}
|
||||
input id="password" name="password" type="password"
|
||||
class="w-full border border-neutral-200/25 px-2 py-1 rounded bg-neutral-950/50";
|
||||
|
||||
div class="flex flex-row items-center justify-between mt-4" {
|
||||
@if let Some(msg) = q.msg {
|
||||
p id="login-error" class="text-red-400 text-sm" {
|
||||
(msg)
|
||||
}
|
||||
} @else {
|
||||
p id="login-error" class="text-red-400 text-sm" {}
|
||||
}
|
||||
button type="submit" class=r#"block ml-auto border border-neutral-200/25 font-normal px-2 py-1
|
||||
rounded bg-neutral-200/5 flex gap-4 justify-between cursor-pointer"# {
|
||||
"Log in"
|
||||
(PreEscaped(icons::ARROW_RIGHT))
|
||||
}
|
||||
}
|
||||
// Logging in is done via JavaScript here by default to preserve
|
||||
// marquee scroll on fail (form-based would reset it on fail)
|
||||
// (if javascript is disabled, login via form still works)
|
||||
script defer {(PreEscaped(r#"
|
||||
if (window.location.search) {
|
||||
history.replaceState(null, '', window.location.pathname);
|
||||
}
|
||||
document.getElementById('login-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const err = document.getElementById('login-error');
|
||||
err.textContent = '';
|
||||
|
||||
const handle = document.getElementById('handle').value;
|
||||
const password = document.getElementById('password').value;
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ handle, password }),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
window.location.href = '/dashboard';
|
||||
} else {
|
||||
const text = await res.text();
|
||||
err.textContent = text || 'Login failed';
|
||||
}
|
||||
} catch (_) {
|
||||
err.textContent = 'Network error — please try again';
|
||||
}
|
||||
});
|
||||
"#
|
||||
))}
|
||||
}
|
||||
}
|
||||
|
||||
div class="mt-auto" {}
|
||||
(marquee(&[REFERENCE_SPLASHES.choose(&mut rand::rng()).unwrap()]))
|
||||
}
|
||||
),
|
||||
).into_response())
|
||||
}
|
||||
34
src/web/pages/mod.rs
Normal file
34
src/web/pages/mod.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use axum::{Router, routing::get};
|
||||
use maud::{DOCTYPE, Markup, html};
|
||||
|
||||
pub mod dashboard;
|
||||
pub mod index;
|
||||
pub mod login;
|
||||
|
||||
pub fn pages() -> Router {
|
||||
Router::new()
|
||||
.route("/", get(index::page))
|
||||
.route("/login", get(login::page))
|
||||
.route("/dashboard", get(dashboard::page))
|
||||
}
|
||||
|
||||
pub fn base(title: &str, inner: Markup) -> Markup {
|
||||
html!(
|
||||
(DOCTYPE)
|
||||
head {
|
||||
title {(title)}
|
||||
meta charset="utf-8";
|
||||
link rel="stylesheet" href="/styles.css";
|
||||
// link rel="icon" type="image/png" href="/icon.png";
|
||||
// link rel="icon" type="image/svg" href="/icon.svg";
|
||||
meta name="viewport" content="width=device-width, initial-scale=1.0";
|
||||
|
||||
link rel="preconnect" href="https://fonts.googleapis.com";
|
||||
link rel="preconnect" href="https://fonts.gstatic.com" crossorigin;
|
||||
link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Lexend:wght@100..900&family=Lora:ital,wght@0,400..700;1,400..700&display=swap";
|
||||
}
|
||||
body class="bg-neutral-900 text-neutral-200 font-lexend min-h-screen" {
|
||||
(inner)
|
||||
}
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user