From 1b2e327088966b4a8b88adca62252f201aeb9cf0 Mon Sep 17 00:00:00 2001 From: jakubmanczak Date: Wed, 3 Sep 2025 02:24:05 +0200 Subject: [PATCH] add small frontend stack w/ tailwind & askama --- Cargo.lock | 68 ++++++++++++++++++++++++++++ Cargo.toml | 2 + askama.toml | 2 + build.rs | 92 ++++++++++++++++++++++++++++++++++++++ src/main.rs | 1 + src/router/mod.rs | 8 +++- src/website/mod.rs | 23 ++++++++++ src/website/pages/index.rs | 19 ++++++++ src/website/pages/mod.rs | 4 ++ web/index.html | 16 +++++++ web/input.css | 1 + web/styles.css | 2 + 12 files changed, 236 insertions(+), 2 deletions(-) create mode 100644 askama.toml create mode 100644 build.rs create mode 100644 src/website/mod.rs create mode 100644 src/website/pages/index.rs create mode 100644 src/website/pages/mod.rs create mode 100644 web/index.html create mode 100644 web/input.css create mode 100644 web/styles.css diff --git a/Cargo.lock b/Cargo.lock index a4ddf68..0082311 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,6 +45,7 @@ dependencies = [ name = "arche" version = "0.1.0" dependencies = [ + "askama", "axum", "chrono", "chrono-tz", @@ -52,6 +53,7 @@ dependencies = [ "rand 0.9.1", "serenity", "tokio", + "tower", "tracing", "tracing-subscriber", ] @@ -65,6 +67,48 @@ dependencies = [ "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]] name = "async-trait" version = "0.1.88" @@ -163,6 +207,15 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "bitflags" version = "1.3.2" @@ -1427,6 +1480,12 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustix" version = "1.0.5" @@ -2568,6 +2627,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.50.0" diff --git a/Cargo.toml b/Cargo.toml index f17b153..5e11815 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] +askama = "0.14.0" axum = "0.8.3" chrono = "0.4.41" chrono-tz = "0.10.3" @@ -11,5 +12,6 @@ dotenvy = "0.15.7" rand = "0.9.1" serenity = "0.12.4" tokio = { version = "1.44.2", features = ["full"] } +tower = "0.5.2" tracing = "0.1.41" tracing-subscriber = "0.3.20" diff --git a/askama.toml b/askama.toml new file mode 100644 index 0000000..1221576 --- /dev/null +++ b/askama.toml @@ -0,0 +1,2 @@ +[general] +dirs = ["web"] diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..b8c2b7e --- /dev/null +++ b/build.rs @@ -0,0 +1,92 @@ +use std::env; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; + +fn main() -> Result<(), Box> { + 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> { + 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> { + 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(()) +} diff --git a/src/main.rs b/src/main.rs index 7627f35..b03faaf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ use tokio::net::TcpListener; mod discordbot; mod router; mod setup; +mod website; #[tokio::main] async fn main() -> Result<(), Box> { diff --git a/src/router/mod.rs b/src/router/mod.rs index 27feece..569ffa5 100644 --- a/src/router/mod.rs +++ b/src/router/mod.rs @@ -1,12 +1,16 @@ use axum::{Router, http::StatusCode, routing::get}; use chrono::{NaiveDate, Utc}; +use tower::service_fn; -use crate::router::redirects::redirects; +use crate::{router::redirects::redirects, website::website_service}; mod redirects; 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 { diff --git a/src/website/mod.rs b/src/website/mod.rs new file mode 100644 index 0000000..9ce8fd6 --- /dev/null +++ b/src/website/mod.rs @@ -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) -> Result { + 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(), + }) +} diff --git a/src/website/pages/index.rs b/src/website/pages/index.rs new file mode 100644 index 0000000..c5ac866 --- /dev/null +++ b/src/website/pages/index.rs @@ -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(), + } +} diff --git a/src/website/pages/mod.rs b/src/website/pages/mod.rs new file mode 100644 index 0000000..938b3ad --- /dev/null +++ b/src/website/pages/mod.rs @@ -0,0 +1,4 @@ +pub mod index; + +const INTERNAL_SERVER_ERROR_MSG: &str = + "An internal server error occured while rendering your HTML. Sorry!"; diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..1c4654b --- /dev/null +++ b/web/index.html @@ -0,0 +1,16 @@ + + + + + + gractwo.pl | Witamy! + + + +

+ Witamy na gractwo.pl! Zapraszamy na nasz + serwer Discord! +

+ + diff --git a/web/input.css b/web/input.css new file mode 100644 index 0000000..f1d8c73 --- /dev/null +++ b/web/input.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/web/styles.css b/web/styles.css new file mode 100644 index 0000000..fb36472 --- /dev/null +++ b/web/styles.css @@ -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}} \ No newline at end of file