add small frontend stack w/ tailwind & askama

This commit is contained in:
2025-09-03 02:24:05 +02:00
parent 437a7fcdf9
commit 1b2e327088
12 changed files with 236 additions and 2 deletions

68
Cargo.lock generated
View File

@@ -45,6 +45,7 @@ dependencies = [
name = "arche" name = "arche"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"askama",
"axum", "axum",
"chrono", "chrono",
"chrono-tz", "chrono-tz",
@@ -52,6 +53,7 @@ dependencies = [
"rand 0.9.1", "rand 0.9.1",
"serenity", "serenity",
"tokio", "tokio",
"tower",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
] ]
@@ -65,6 +67,48 @@ dependencies = [
"serde", "serde",
] ]
[[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 2.0.100",
]
[[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 = "async-trait" name = "async-trait"
version = "0.1.88" version = "0.1.88"
@@ -163,6 +207,15 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[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 = "1.3.2" version = "1.3.2"
@@ -1427,6 +1480,12 @@ version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "1.0.5" version = "1.0.5"
@@ -2568,6 +2627,15 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.7.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "winreg" name = "winreg"
version = "0.50.0" version = "0.50.0"

View File

@@ -4,6 +4,7 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
askama = "0.14.0"
axum = "0.8.3" axum = "0.8.3"
chrono = "0.4.41" chrono = "0.4.41"
chrono-tz = "0.10.3" chrono-tz = "0.10.3"
@@ -11,5 +12,6 @@ dotenvy = "0.15.7"
rand = "0.9.1" rand = "0.9.1"
serenity = "0.12.4" serenity = "0.12.4"
tokio = { version = "1.44.2", features = ["full"] } tokio = { version = "1.44.2", features = ["full"] }
tower = "0.5.2"
tracing = "0.1.41" tracing = "0.1.41"
tracing-subscriber = "0.3.20" tracing-subscriber = "0.3.20"

2
askama.toml Normal file
View File

@@ -0,0 +1,2 @@
[general]
dirs = ["web"]

92
build.rs Normal file
View File

@@ -0,0 +1,92 @@
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=web");
let os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_else(|_| String::from("unknown"));
let arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_else(|_| String::from("unknown"));
let download_url = match (os.as_str(), arch.as_str()) {
("macos", "aarch64") => {
"https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-macos-arm64"
}
("linux", "x86_64") => {
"https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-linux-x64"
}
("linux", "aarch64") => {
"https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-linux-arm64"
}
_ => return Err(format!("Unsupported platform: {} {}", os, arch).into()),
};
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
let tailwind_binary = out_dir.join("tailwind");
fs::create_dir_all(&out_dir)?;
download_tailwind(&download_url, &tailwind_binary)?;
println!("cargo:rustc-env=TAILWIND_BIN={}", tailwind_binary.display());
run_tailwind(&tailwind_binary)?;
Ok(())
}
fn run_tailwind(bin: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
println!("Building CSS with Tailwind...");
let basedir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".into());
let input = Path::new(&basedir).join("web").join("input.css");
let inputstr = input.to_str().unwrap();
let output = Path::new(&basedir).join("web").join("styles.css");
let outputstr = output.to_str().unwrap();
let args = vec!["-i", inputstr, "-o", outputstr, "--minify"];
let run = Command::new(&bin).args(args).status()?;
match run.success() {
true => println!("Tailwind CSS build complete."),
false => println!("Tailwind CSS build failed."),
};
Ok(())
}
fn download_tailwind(url: &str, target_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
if target_path.exists() {
return Ok(()); // if present, assume the file is the appropriate tailwind binary
}
let tmp_download = target_path.with_extension("tmp");
let download_status = if Command::new("curl").arg("--version").output().is_ok() {
Command::new("curl")
.arg("-L") // Follow redirects
.arg("-o")
.arg(&tmp_download)
.arg(url)
.status()?
} else if Command::new("wget").arg("--version").output().is_ok() {
Command::new("wget")
.arg("-O")
.arg(&tmp_download)
.arg(url)
.status()?
} else {
return Err("Neither curl nor wget is available".into());
};
if !download_status.success() {
return Err(format!("Tailwind binary download failed: {}", download_status).into());
}
fs::rename(&tmp_download, target_path)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(target_path)?.permissions();
perms.set_mode(0o777);
fs::set_permissions(target_path, perms)?;
}
Ok(())
}

View File

@@ -4,6 +4,7 @@ use tokio::net::TcpListener;
mod discordbot; mod discordbot;
mod router; mod router;
mod setup; mod setup;
mod website;
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> { async fn main() -> Result<(), Box<dyn Error>> {

View File

@@ -1,12 +1,16 @@
use axum::{Router, http::StatusCode, routing::get}; use axum::{Router, http::StatusCode, routing::get};
use chrono::{NaiveDate, Utc}; use chrono::{NaiveDate, Utc};
use tower::service_fn;
use crate::router::redirects::redirects; use crate::{router::redirects::redirects, website::website_service};
mod redirects; mod redirects;
pub fn init() -> Router { pub fn init() -> Router {
Router::new().merge(redirects()).nest("/api/", api()) Router::new()
.merge(redirects())
.nest("/api/", api())
.fallback_service(service_fn(website_service))
} }
fn api() -> Router { fn api() -> Router {

23
src/website/mod.rs Normal file
View File

@@ -0,0 +1,23 @@
use std::convert::Infallible;
use axum::{
body::Body,
http::{Request, StatusCode, header},
response::{IntoResponse, Response},
};
use crate::website::pages::index::page_index;
mod pages;
const STYLES_CSS: &str = include_str!("../../web/styles.css");
pub async fn website_service(req: Request<Body>) -> Result<Response, Infallible> {
let path = req.uri().path().trim_start_matches("/");
Ok(match path {
"" | "index" | "index.html" | "index.htm" => page_index().await,
"styles.css" => ([(header::CONTENT_TYPE, "text/css")], STYLES_CSS).into_response(),
_ => StatusCode::NOT_FOUND.into_response(),
})
}

View File

@@ -0,0 +1,19 @@
use askama::Template;
use axum::{
http::StatusCode,
response::{Html, IntoResponse, Response},
};
use crate::website::pages::INTERNAL_SERVER_ERROR_MSG;
#[derive(Template)]
#[template(path = "index.html")]
struct PageIndex;
pub async fn page_index() -> Response {
let a = PageIndex;
match a.render() {
Ok(res) => (StatusCode::OK, Html(res)).into_response(),
Err(_e) => (StatusCode::INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR_MSG).into_response(),
}
}

4
src/website/pages/mod.rs Normal file
View File

@@ -0,0 +1,4 @@
pub mod index;
const INTERNAL_SERVER_ERROR_MSG: &str =
"An internal server error occured while rendering your HTML. Sorry!";

16
web/index.html Normal file
View File

@@ -0,0 +1,16 @@
<!doctype html>
<html lang="pl">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>gractwo.pl | Witamy!</title>
<link href="styles.css" rel="stylesheet" />
</head>
<body class="min-h-screen text-center flex items-center justify-center">
<p>
Witamy na gractwo.pl! Zapraszamy na nasz
<a href="/discord" class="underline text-blue-400">serwer Discord</a
>!
</p>
</body>
</html>

1
web/input.css Normal file
View File

@@ -0,0 +1 @@
@import "tailwindcss";

2
web/styles.css Normal file
View File

@@ -0,0 +1,2 @@
/*! tailwindcss v4.1.12 | MIT License | https://tailwindcss.com */
@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-blue-400:oklch(70.7% .165 254.624);--color-purple-400:oklch(71.4% .203 305.504);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.static{position:static}.flex{display:flex}.min-h-screen{min-height:100vh}.items-center{align-items:center}.justify-center{justify-content:center}.text-center{text-align:center}.text-blue-400{color:var(--color-blue-400)}.underline{text-decoration-line:underline}}