diff --git a/Cargo.lock b/Cargo.lock index 94bfa3b..dd1bd86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -335,6 +335,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.59.0", +] + [[package]] name = "anyhow" version = "1.0.98" @@ -639,6 +689,46 @@ dependencies = [ "stacker", ] +[[package]] +name = "clap" +version = "4.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.103", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + [[package]] name = "color-eyre" version = "0.6.5" @@ -666,6 +756,12 @@ dependencies = [ "tracing-error", ] +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -1624,6 +1720,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itoa" version = "1.0.15" @@ -2030,6 +2132,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + [[package]] name = "openssl" version = "0.10.73" @@ -3094,7 +3202,7 @@ dependencies = [ ] [[package]] -name = "tfcapi" +name = "tfcsite" version = "0.1.0" dependencies = [ "actix-cors", @@ -3103,6 +3211,7 @@ dependencies = [ "actix-rt", "actix-web", "async-std", + "clap", "color-eyre", "env_logger", "futures", @@ -3479,6 +3588,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.17.0" diff --git a/Cargo.toml b/Cargo.toml index d46a7bc..0d64ed5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "tfcapi" +name = "tfcsite" version = "0.1.0" edition = "2021" @@ -32,6 +32,7 @@ tracing-actix-web = "0.7.14" color-eyre = "0.6.3" pretty_assertions = "1.4.1" sqlx = { version = "0.8.2", features = ["sqlite"] } +clap = { version = "4.5.40", features = ["derive"] } [profile.dev] opt-level = 0 diff --git a/config.toml b/config.toml index d8a7357..1d24237 100644 --- a/config.toml +++ b/config.toml @@ -1,5 +1,7 @@ # The base URL of the site; the only required configuration variable. -base_url = "https://tfcconnection.org" +# base_url = "https://tfcconnection.org" +# used when developing +base_url = "/" # The site title and description; used in feeds by default. title = "TFC Connection" @@ -123,7 +125,7 @@ include_content = true # become too big to load on the site. Defaults to not being set. truncate_content_length = 100 -index_format = "elasticlunr_json" +index_format = "elasticlunr_javascript" # Optional translation object for the default language # Example: @@ -160,6 +162,7 @@ taxonomies = [ # [extra] enable_search = true +enable_sidebar = false [extra.navbar] diff --git a/content/_index.md b/content/_index.md index b4787e2..1c45e2f 100644 --- a/content/_index.md +++ b/content/_index.md @@ -22,10 +22,10 @@ Fill out now! -{{ spacing(height="h-4") }} +{{ spacing(height="h-8") }} > If you need to fill out a camp health form, please do so [here](/camp-health-form). - +{{ spacing(height="h-4") }} > If you need to pay for your camp form, pay for registration ($100) [here](https://secure.myvanco.com/L-Z772/campaign/C-13JPJ) or pay for the full price ($200) [here](https://secure.myvanco.com/L-Z772/campaign/C-13JQE). {{ spacing(height="h-8") }} diff --git a/flake.nix b/flake.nix index 34e4dbd..232cc95 100644 --- a/flake.nix +++ b/flake.nix @@ -66,15 +66,13 @@ # cp -r ${blowfish} themes/blowfish # ls themes/blowfish # ''; - # buildPhase = '' - # NODE_ENV=production ./themes/blowfish/node_modules/tailwindcss/lib/cli.js -c ./themes/blowfish/tailwind.config.js -i ./themes/blowfish/assets/css/main.css -o ./assets/css/compiled/main.css --jit && hugo --gc --minify - # ${pkgs.hugo}/bin/hugo --minify - # ''; - # installPhase = '' - # ls -l - # cp -r public $out/ - # ls -l $out - # ''; + buildPhase = '' + ${pkgs.tailwindcss_4}/bin/tailwindcss -i static/css/base.css -o static/css/main.css + ${pkgs.zola}/bin/zola build + ''; + installPhase = '' + cp -r public $out/ + ''; buildInputs = bi; nativeBuildInputs = nbi; }; diff --git a/justfile b/justfile index 5a39c36..b933bc9 100644 --- a/justfile +++ b/justfile @@ -3,7 +3,7 @@ default: build: tailwindcss -i static/css/base.css -o static/css/main.css && zola build serve: - zola serve + zola serve -p 4242 uglify: uglifyjs ./src/js/main.js --compress --mangle -o ./static/js/main.js && uglifyjs ./src/js/page.js --compress --mangle -o ./static/js/page.js && uglifyjs ./src/js/search.js --compress --mangle -o ./static/js/search.js && uglifyjs ./src/js/lang.js --compress --mangle -o ./static/js/lang.js dev: @@ -14,3 +14,5 @@ clean: cargo clean test: RUST_LOG=debug cargo test --benches --tests --all-features -- --nocapture +run: + tailwindcss -i static/css/base.css -o static/css/main.css --watch & zola build && cargo run -- -d diff --git a/src/api/camp_form.rs b/src/api/camp_form.rs index 73937a8..d82a5be 100644 --- a/src/api/camp_form.rs +++ b/src/api/camp_form.rs @@ -6,8 +6,8 @@ use color_eyre::eyre::{Context, Result}; use futures::FutureExt; use lettre::{message::SinglePart, Message}; use maud::{html, Markup, DOCTYPE}; -use reqwest::Client; -use tracing::{error, info}; +use reqwest::{Client, Response}; +use tracing::{debug, error, info}; #[derive(Debug, MultipartForm)] struct CampForm { @@ -39,27 +39,30 @@ struct CampForm { health_form: Text, } -impl From<&CampForm> for HashMap { +impl From<&CampForm> for HashMap<&str, String> { fn from(form: &CampForm) -> Self { let mut map = HashMap::new(); - map.insert(63, format!("{} {}", form.first_name.0, form.last_name.0)); map.insert( - 64, + "Full_Name", + format!("{} {}", form.first_name.0, form.last_name.0), + ); + map.insert( + "Parent_Name", format!("{} {}", form.parent_first_name.0, form.parent_last_name.0), ); - map.insert(65, form.parent_phone.0.clone()); - map.insert(66, form.parent_email.0.clone().clone()); - map.insert(67, form.birthdate.0.clone()); - map.insert(69, form.gender.0.clone()); - map.insert(70, form.street.0.clone()); - map.insert(71, form.city.0.clone()); - map.insert(72, form.state.0.clone()); - map.insert(73, form.zip.0.clone().to_string()); - map.insert(74, form.grade.0.clone()); - map.insert(75, form.week.0.clone()); - map.insert(76, form.shirt.0.clone()); - map.insert(77, form.registration.0.clone()); - map.insert(115, form.health_form.0.clone()); + map.insert("Parent_Phone", form.parent_phone.0.clone()); + map.insert("Parent_Email", form.parent_email.0.clone().clone()); + map.insert("Birth_Date", form.birthdate.0.clone()); + map.insert("Gender", form.gender.0.clone()); + map.insert("Street_Address", form.street.0.clone()); + map.insert("City", form.city.0.clone()); + map.insert("State", form.state.0.clone()); + map.insert("Zipcode", form.zip.0.clone().to_string()); + map.insert("Grade", form.grade.0.clone()); + map.insert("Week_Chosen", form.week.0.clone()); + map.insert("Shirt_Size", form.shirt.0.clone()); + map.insert("Registration", form.registration.0.clone()); + map.insert("Health_Form", form.health_form.0.clone()); map } } @@ -292,19 +295,21 @@ pub async fn camp_form(MultipartForm(form): MultipartForm) -> HttpResp } } -async fn store_camp_form(map: HashMap) -> Result<()> { +async fn store_camp_form(map: HashMap<&str, String>) -> Result { let request = Client::new(); let mut json = HashMap::new(); - json.insert("data", map); - request - .post("https://staff.tfcconnection.org/apps/tables/api/1/tables/5/rows") - .basic_auth("chris", Some("2VHeGxeC^Zf9KqFK^G@Pt!zu2q^6@b")) - .header("OCS-APIRequest", "true") + let mut fields = HashMap::new(); + fields.insert("fields", map); + json.insert("records", vec![fields]); + let response = request + .post("https://table.tfcconnection.org/api/docs/e8SFoTHpmJuFQsiMhRTXCi/tables/Camp_Data/records") + .bearer_auth("b8189d1b315548aa610db2fd3a43177a967cd41f") .header("Content-Type", "application/json") .json(&json) .send() .await?; - Ok(()) + debug!(?response); + Ok(response) } #[cfg(test)] @@ -329,7 +334,7 @@ mod test { grade: Text(String::from("junior")), shirt: Text(String::from("medium")), allergies: Text(String::from("Cool beans")), - week: Text(String::from("1")), + week: Text(String::from("week1")), registration: Text(String::from("later")), health_form: Text(String::from("I guess")), } @@ -342,7 +347,7 @@ mod test { let map = HashMap::from(&form); let res = store_camp_form(map).await; match res { - Ok(_) => assert!(true), + Ok(r) => assert!(r.status().is_success(), "Failed to store: {:?}", r), Err(e) => assert!(false, "Failed storing test: {e}"), } } diff --git a/src/api/health_form.rs b/src/api/health_form.rs index 5196a0a..d2f0300 100644 --- a/src/api/health_form.rs +++ b/src/api/health_form.rs @@ -12,7 +12,7 @@ use lettre::{ Message, }; use maud::{html, Markup, DOCTYPE}; -use reqwest::Client; +use reqwest::{Client, Response}; use tracing::{error, info}; use crate::email::send_email; @@ -75,40 +75,46 @@ struct HealthForm { registration: Text, } -impl From<&HealthForm> for HashMap { +impl From<&HealthForm> for HashMap<&str, String> { fn from(form: &HealthForm) -> Self { let mut map = HashMap::new(); - map.insert(37, format!("{} {}", form.first_name.0, form.last_name.0)); map.insert( - 38, + "Student_Name", + format!("{} {}", form.first_name.0, form.last_name.0), + ); + map.insert( + "Parent_Name", format!("{} {}", form.parent_first_name.0, form.parent_last_name.0), ); - map.insert(39, form.birthdate.0.clone()); - map.insert(40, form.street.0.clone()); - map.insert(41, form.city.0.clone()); - map.insert(42, form.state.0.clone()); - map.insert(43, form.zip.0.clone()); - map.insert(44, form.parent_cellphone.0.clone()); - map.insert(45, form.homephone.0.clone()); - map.insert(46, format!("{} {}", form.contact.0, form.contact_phone.0)); - map.insert(47, form.doctorname.0.clone()); - map.insert(48, form.doctorcity.0.clone()); - map.insert(49, form.doctorphone.0.clone()); - map.insert(50, form.medical.0.clone()); - map.insert(51, form.insurance.0.clone()); - map.insert(52, form.policy_number.0.clone()); - map.insert(54, form.agreement.0.clone()); + map.insert("Birthdate", form.birthdate.0.clone()); + map.insert("Street", form.street.0.clone()); + map.insert("City", form.city.0.clone()); + map.insert("State", form.state.0.clone()); + map.insert("Zipcode", form.zip.0.clone()); + map.insert("Parent_Phone", form.parent_cellphone.0.clone()); + map.insert("Homephone", form.homephone.0.clone()); map.insert( - 55, + "Emergency_Contact", + format!("{} {}", form.contact.0, form.contact_phone.0), + ); + map.insert("Doctor", form.doctorname.0.clone()); + map.insert("Doctor_City", form.doctorcity.0.clone()); + map.insert("Doctor_Phone", form.doctorphone.0.clone()); + map.insert("Medical_Coverage", form.medical.0.clone()); + map.insert("Insurance_Name", form.insurance.0.clone()); + map.insert("Policy_Number", form.policy_number.0.clone()); + map.insert("Agreement", form.agreement.0.clone()); + map.insert( + "Allergies", format!("{} \n {}", form.allergies.0, form.allergies_other.0), ); - map.insert(56, form.specific_allergies.0.clone()); - map.insert(57, form.treatment.0.clone()); - map.insert(58, form.conditions.0.clone()); - map.insert(59, form.tetanus.0.clone()); - map.insert(60, form.medication.0.clone()); - map.insert(61, form.notes.0.clone()); - map.insert(62, form.swimming.0.clone()); + map.insert("Specific_Allergies", form.specific_allergies.0.clone()); + map.insert("Allergic_Treatments", form.treatment.0.clone()); + map.insert("Conditions", form.conditions.0.clone()); + map.insert("Tetanus_Shot_Date", form.tetanus.0.clone()); + map.insert("Medication_Schedule", form.medication.0.clone()); + map.insert("Other_Notes", form.notes.0.clone()); + map.insert("Swimming_Ability", form.swimming.0.clone()); map } } @@ -431,22 +437,22 @@ pub async fn health_form(MultipartForm(mut form): MultipartForm) -> // HttpResponse::Ok().body("hi") } -async fn store_form(map: HashMap) -> Result<()> { +async fn store_form(map: HashMap<&str, String>) -> Result { let client = Client::new(); let mut json = HashMap::new(); - json.insert("data", map); + let mut fields = HashMap::new(); + fields.insert("fields", map); + json.insert("records", vec![fields]); let res = client - .post("https://staff.tfcconnection.org/ocs/v2.php/apps/tables/api/2/tables/4/rows") - .basic_auth("chris", Some("2VHeGxeC^Zf9KqFK^G@Pt!zu2q^6@b")) - .header("OCS-APIRequest", "true") + .post("https://table.tfcconnection.org/api/docs/e8SFoTHpmJuFQsiMhRTXCi/tables/Health_Data/records") + .bearer_auth("b8189d1b315548aa610db2fd3a43177a967cd41f") .header("Content-Type", "application/json") .json(&json) .send() .await?; if res.status().is_success() { - let res = res.text().await.unwrap(); - Ok(()) + Ok(res) } else { Err(eyre!( "Problem in storing data: {:?}", @@ -454,3 +460,69 @@ async fn store_form(map: HashMap) -> Result<()> { )) } } + +#[cfg(test)] +mod test { + use super::*; + use actix_web::test; + + fn form() -> HealthForm { + HealthForm { + first_name: Text("Frodo".into()), + last_name: Text("Braggins".into()), + parent_first_name: Text("Bilbo".into()), + parent_last_name: Text("Braggins".into()), + birthdate: Text(String::from("1845-09-12")), + street: Text(String::from("1234 Bag End")), + city: Text(String::from("The Shire")), + state: Text(String::from("Hobbiton")), + zip: Text(String::from("88888")), + allergies: Text(String::from("Cool beans")), + registration: Text(String::from("later")), + parent_cellphone: Text(String::from("later")), + homephone: Text("test".into()), + contact: Text("test".into()), + contact_phone: Text("test".into()), + doctorname: Text("test".into()), + doctorcity: Text("test".into()), + doctorphone: Text("test".into()), + medical: Text("test".into()), + insurance: Text("test".into()), + policy_number: Text("test".into()), + allergies_other: Text("test".into()), + specific_allergies: Text("test".into()), + treatment: Text("test".into()), + conditions: Text("test".into()), + tetanus: Text("test".into()), + swimming: Text("test".into()), + medication: Text("test".into()), + notes: Text("test".into()), + agreement: Text("test".into()), + file: None, + } + } + + #[test] + async fn test_nc_post() { + let form = form(); + assert!(!form.first_name.is_empty()); + let map = HashMap::from(&form); + let res = store_form(map).await; + match res { + Ok(r) => assert!(r.status().is_success(), "Failed to store: {:?}", r), + Err(e) => assert!(false, "Failed storing test: {e}"), + } + } + + #[test] + async fn test_email() { + let mut form = form(); + assert!(!form.first_name.is_empty()); + match form.send_email() { + Ok(m) => { + assert!(crate::email::send_email(m).await.is_ok()) + } + Err(e) => assert!(false, "Failed emailing test: {e}"), + } + } +} diff --git a/src/main.rs b/src/main.rs index 9c8e783..9b9a00c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ use actix_web::body::MessageBody; use actix_web::dev::{ServiceRequest, ServiceResponse}; use actix_web::{web, App, Error, HttpServer}; use api::camp_form::camp_form; -use api::contact::{self, contact_form}; +use api::contact::contact_form; use api::health_form::health_form; use api::local_trip_form::local_form; use api::mt_form::mt_form; @@ -15,6 +15,7 @@ use api::{ mt_church_form::mt_church_form, mt_parent_form::mt_parent_form, mt_teacher_form::mt_teacher_form, }; +use clap::Parser; use color_eyre::eyre::Context; use color_eyre::Result; use sqlx::{Connection, SqliteConnection}; @@ -45,45 +46,64 @@ impl RootSpanBuilder for DomainRootSpanBuilder { fn on_request_end(_span: Span, _outcome: &Result, Error>) {} } +#[derive(Debug, Parser)] +#[command(name = "tfcsite", version, about)] +struct Cli { + #[arg(short, long)] + dev: bool, +} + #[actix_web::main] async fn main() -> std::io::Result<()> { + let site; + let temp; + let logs; + if Cli::parse().dev { + site = "./public"; + logs = "/tmp/tfcsite"; + temp = "/tmp/tfcsite"; + } else { + site = "../public"; + logs = "/storage/logs/tfcsite"; + temp = "/tmp/tfcsite"; + } + + std::fs::create_dir_all(&logs)?; + std::fs::create_dir_all(&temp)?; + let timer = tracing_subscriber::fmt::time::ChronoLocal::new("%Y-%m-%d_%I:%M:%S%.6f %P".to_owned()); let logfile = RollingFileAppender::builder() .rotation(Rotation::DAILY) .filename_prefix("api") .filename_suffix("log") - .build(if DEV_MODE { - "/tmp/tfcsite" - } else { - "/storage/logs/tfcsite" - }) + .build(&logs) .expect("Shouldn't"); - let filter = EnvFilter::builder() + let file_filter = EnvFilter::builder() .with_default_directive(LevelFilter::WARN.into()) - .parse_lossy("tfcapi=debug"); + .parse_lossy("tfcsite=debug"); let logfile_layer = tracing_subscriber::fmt::layer() .with_writer(logfile) .with_line_number(true) .with_level(true) .with_target(true) .with_ansi(false) - .with_timer(timer.clone()); + .with_timer(timer.clone()) + .with_filter(file_filter); + + let stdout_filter = EnvFilter::builder() + .with_default_directive(LevelFilter::WARN.into()) + .parse_lossy("tfcsite=debug"); let stdout_layer = tracing_subscriber::fmt::layer() .pretty() .with_line_number(true) .with_target(true) .with_timer(timer) - .with_filter(filter); - let filter = EnvFilter::builder() - .with_default_directive(LevelFilter::WARN.into()) - .parse_lossy("tfcapi=debug"); - let subscriber = tracing_subscriber::registry() - .with(logfile_layer.with_filter(filter).and_then(stdout_layer)); - let _ = tracing::subscriber::set_global_default(subscriber).wrap_err("Tracing broked"); + .with_filter(stdout_filter); - std::fs::create_dir_all("/tmp/tfcsite")?; + let subscriber = tracing_subscriber::registry().with(logfile_layer.and_then(stdout_layer)); + let _ = tracing::subscriber::set_global_default(subscriber).wrap_err("Tracing broked"); info!("starting HTTP server at http://localhost:4242"); @@ -96,11 +116,7 @@ async fn main() -> std::io::Result<()> { App::new() .app_data(data.clone()) .wrap(TracingLogger::::new()) - .app_data(TempFileConfig::default().directory(if DEV_MODE { - "/tmp/tfcsite" - } else { - "/storage/logs/tfcsite" - })) + .app_data(TempFileConfig::default().directory(&temp)) .service(mt_form) .service(health_form) .service(mt_parent_form) @@ -109,7 +125,7 @@ async fn main() -> std::io::Result<()> { .service(local_form) .service(camp_form) .service(contact_form) - .service(Files::new("/", "./public").index_file("index.html")) + .service(Files::new("/", &site).index_file("index.html")) }) .bind(("localhost", 4242))? .workers(4) diff --git a/static/css/base.css b/static/css/base.css index b1c3020..3e6e3bb 100644 --- a/static/css/base.css +++ b/static/css/base.css @@ -27,6 +27,17 @@ li::marker { color: var(--color-gray-600); } + + blockquote { + list-style-type: disc; + border-left-width: .25rem; + border-left-color: var(--color-blue-600); + font-style: italic; + font-weight: 500; + padding-left: 1em; + margin-top: 1.6em; + margin-bottom: 1.6em; + } img { @apply py-0; diff --git a/static/css/main.css b/static/css/main.css index d674cf8..cd17749 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -7,6 +7,7 @@ 'Noto Color Emoji'; --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; + --color-blue-200: oklch(88.2% 0.059 254.128); --color-blue-400: oklch(70.7% 0.165 254.624); --color-blue-500: oklch(62.3% 0.214 259.815); --color-blue-600: oklch(54.6% 0.245 262.881); @@ -351,9 +352,6 @@ .mt-16 { margin-top: calc(var(--spacing) * 16); } - .mt-20 { - margin-top: calc(var(--spacing) * 20); - } .mt-40 { margin-top: calc(var(--spacing) * 40); } @@ -390,6 +388,9 @@ .ml-4 { margin-left: calc(var(--spacing) * 4); } + .ml-\[-0\.2em\] { + margin-left: -0.2em; + } .ml-auto { margin-left: auto; } @@ -457,9 +458,6 @@ .h-24 { height: calc(var(--spacing) * 24); } - .h-36 { - height: calc(var(--spacing) * 36); - } .h-40 { height: calc(var(--spacing) * 40); } @@ -767,9 +765,6 @@ .bg-indigo-500 { background-color: var(--color-indigo-500); } - .bg-transparent { - background-color: transparent; - } .bg-white { background-color: var(--color-white); } @@ -947,9 +942,6 @@ .text-gray-900 { color: var(--color-gray-900); } - .text-indigo-500 { - color: var(--color-indigo-500); - } .text-white { color: var(--color-white); } @@ -1020,9 +1012,6 @@ --tw-blur: blur(8px); filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); } - .filter { - filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); - } .transition { transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter; transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); @@ -1198,13 +1187,6 @@ } } } - .hover\:bg-gray-100 { - &:hover { - @media (hover: hover) { - background-color: var(--color-gray-100); - } - } - } .hover\:bg-gray-200 { &:hover { @media (hover: hover) { @@ -1240,6 +1222,14 @@ } } } + .hover\:text-3xl { + &:hover { + @media (hover: hover) { + font-size: var(--text-3xl); + line-height: var(--tw-leading, var(--text-3xl--line-height)); + } + } + } .hover\:text-blue-500 { &:hover { @media (hover: hover) { @@ -1261,13 +1251,6 @@ } } } - .hover\:text-indigo-400 { - &:hover { - @media (hover: hover) { - color: var(--color-indigo-400); - } - } - } .hover\:text-white { &:hover { @media (hover: hover) { @@ -1628,6 +1611,11 @@ background-color: var(--color-gray-900); } } + .dark\:text-blue-200 { + @media (prefers-color-scheme: dark) { + color: var(--color-blue-200); + } + } .dark\:text-gray-200 { @media (prefers-color-scheme: dark) { color: var(--color-gray-200); @@ -1716,6 +1704,16 @@ li::marker { color: var(--color-gray-600); } + blockquote { + list-style-type: disc; + border-left-width: .25rem; + border-left-color: var(--color-blue-600); + font-style: italic; + font-weight: 500; + padding-left: 1em; + margin-top: 1.6em; + margin-bottom: 1.6em; + } img { padding-block: calc(var(--spacing) * 0); border-radius: var(--radius-lg); diff --git a/static/js/elasticlunr.min.js b/static/js/elasticlunr.min.js new file mode 100644 index 0000000..94b20dd --- /dev/null +++ b/static/js/elasticlunr.min.js @@ -0,0 +1,10 @@ +/** + * elasticlunr - http://weixsong.github.io + * Lightweight full-text search engine in Javascript for browser search and offline search. - 0.9.5 + * + * Copyright (C) 2017 Oliver Nightingale + * Copyright (C) 2017 Wei Song + * MIT Licensed + * @license + */ +!function(){function e(e){if(null===e||"object"!=typeof e)return e;var t=e.constructor();for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t}var t=function(e){var n=new t.Index;return n.pipeline.add(t.trimmer,t.stopWordFilter,t.stemmer),e&&e.call(n,n),n};t.version="0.9.5",lunr=t,t.utils={},t.utils.warn=function(e){return function(t){e.console&&console.warn&&console.warn(t)}}(this),t.utils.toString=function(e){return void 0===e||null===e?"":e.toString()},t.EventEmitter=function(){this.events={}},t.EventEmitter.prototype.addListener=function(){var e=Array.prototype.slice.call(arguments),t=e.pop(),n=e;if("function"!=typeof t)throw new TypeError("last argument must be a function");n.forEach(function(e){this.hasHandler(e)||(this.events[e]=[]),this.events[e].push(t)},this)},t.EventEmitter.prototype.removeListener=function(e,t){if(this.hasHandler(e)){var n=this.events[e].indexOf(t);-1!==n&&(this.events[e].splice(n,1),0==this.events[e].length&&delete this.events[e])}},t.EventEmitter.prototype.emit=function(e){if(this.hasHandler(e)){var t=Array.prototype.slice.call(arguments,1);this.events[e].forEach(function(e){e.apply(void 0,t)},this)}},t.EventEmitter.prototype.hasHandler=function(e){return e in this.events},t.tokenizer=function(e){if(!arguments.length||null===e||void 0===e)return[];if(Array.isArray(e)){var n=e.filter(function(e){return null===e||void 0===e?!1:!0});n=n.map(function(e){return t.utils.toString(e).toLowerCase()});var i=[];return n.forEach(function(e){var n=e.split(t.tokenizer.seperator);i=i.concat(n)},this),i}return e.toString().trim().toLowerCase().split(t.tokenizer.seperator)},t.tokenizer.defaultSeperator=/[\s\-]+/,t.tokenizer.seperator=t.tokenizer.defaultSeperator,t.tokenizer.setSeperator=function(e){null!==e&&void 0!==e&&"object"==typeof e&&(t.tokenizer.seperator=e)},t.tokenizer.resetSeperator=function(){t.tokenizer.seperator=t.tokenizer.defaultSeperator},t.tokenizer.getSeperator=function(){return t.tokenizer.seperator},t.Pipeline=function(){this._queue=[]},t.Pipeline.registeredFunctions={},t.Pipeline.registerFunction=function(e,n){n in t.Pipeline.registeredFunctions&&t.utils.warn("Overwriting existing registered function: "+n),e.label=n,t.Pipeline.registeredFunctions[n]=e},t.Pipeline.getRegisteredFunction=function(e){return e in t.Pipeline.registeredFunctions!=!0?null:t.Pipeline.registeredFunctions[e]},t.Pipeline.warnIfFunctionNotRegistered=function(e){var n=e.label&&e.label in this.registeredFunctions;n||t.utils.warn("Function is not registered with pipeline. This may cause problems when serialising the index.\n",e)},t.Pipeline.load=function(e){var n=new t.Pipeline;return e.forEach(function(e){var i=t.Pipeline.getRegisteredFunction(e);if(!i)throw new Error("Cannot load un-registered function: "+e);n.add(i)}),n},t.Pipeline.prototype.add=function(){var e=Array.prototype.slice.call(arguments);e.forEach(function(e){t.Pipeline.warnIfFunctionNotRegistered(e),this._queue.push(e)},this)},t.Pipeline.prototype.after=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i+1,0,n)},t.Pipeline.prototype.before=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i,0,n)},t.Pipeline.prototype.remove=function(e){var t=this._queue.indexOf(e);-1!==t&&this._queue.splice(t,1)},t.Pipeline.prototype.run=function(e){for(var t=[],n=e.length,i=this._queue.length,o=0;n>o;o++){for(var r=e[o],s=0;i>s&&(r=this._queue[s](r,o,e),void 0!==r&&null!==r);s++);void 0!==r&&null!==r&&t.push(r)}return t},t.Pipeline.prototype.reset=function(){this._queue=[]},t.Pipeline.prototype.get=function(){return this._queue},t.Pipeline.prototype.toJSON=function(){return this._queue.map(function(e){return t.Pipeline.warnIfFunctionNotRegistered(e),e.label})},t.Index=function(){this._fields=[],this._ref="id",this.pipeline=new t.Pipeline,this.documentStore=new t.DocumentStore,this.index={},this.eventEmitter=new t.EventEmitter,this._idfCache={},this.on("add","remove","update",function(){this._idfCache={}}.bind(this))},t.Index.prototype.on=function(){var e=Array.prototype.slice.call(arguments);return this.eventEmitter.addListener.apply(this.eventEmitter,e)},t.Index.prototype.off=function(e,t){return this.eventEmitter.removeListener(e,t)},t.Index.load=function(e){e.version!==t.version&&t.utils.warn("version mismatch: current "+t.version+" importing "+e.version);var n=new this;n._fields=e.fields,n._ref=e.ref,n.documentStore=t.DocumentStore.load(e.documentStore),n.pipeline=t.Pipeline.load(e.pipeline),n.index={};for(var i in e.index)n.index[i]=t.InvertedIndex.load(e.index[i]);return n},t.Index.prototype.addField=function(e){return this._fields.push(e),this.index[e]=new t.InvertedIndex,this},t.Index.prototype.setRef=function(e){return this._ref=e,this},t.Index.prototype.saveDocument=function(e){return this.documentStore=new t.DocumentStore(e),this},t.Index.prototype.addDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.addDoc(i,e),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));this.documentStore.addFieldLength(i,n,o.length);var r={};o.forEach(function(e){e in r?r[e]+=1:r[e]=1},this);for(var s in r){var u=r[s];u=Math.sqrt(u),this.index[n].addToken(s,{ref:i,tf:u})}},this),n&&this.eventEmitter.emit("add",e,this)}},t.Index.prototype.removeDocByRef=function(e){if(e&&this.documentStore.isDocStored()!==!1&&this.documentStore.hasDoc(e)){var t=this.documentStore.getDoc(e);this.removeDoc(t,!1)}},t.Index.prototype.removeDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.hasDoc(i)&&(this.documentStore.removeDoc(i),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));o.forEach(function(e){this.index[n].removeToken(e,i)},this)},this),n&&this.eventEmitter.emit("remove",e,this))}},t.Index.prototype.updateDoc=function(e,t){var t=void 0===t?!0:t;this.removeDocByRef(e[this._ref],!1),this.addDoc(e,!1),t&&this.eventEmitter.emit("update",e,this)},t.Index.prototype.idf=function(e,t){var n="@"+t+"/"+e;if(Object.prototype.hasOwnProperty.call(this._idfCache,n))return this._idfCache[n];var i=this.index[t].getDocFreq(e),o=1+Math.log(this.documentStore.length/(i+1));return this._idfCache[n]=o,o},t.Index.prototype.getFields=function(){return this._fields.slice()},t.Index.prototype.search=function(e,n){if(!e)return[];e="string"==typeof e?{any:e}:JSON.parse(JSON.stringify(e));var i=null;null!=n&&(i=JSON.stringify(n));for(var o=new t.Configuration(i,this.getFields()).get(),r={},s=Object.keys(e),u=0;u0&&t.push(e);for(var i in n)"docs"!==i&&"df"!==i&&this.expandToken(e+i,t,n[i]);return t},t.InvertedIndex.prototype.toJSON=function(){return{root:this.root}},t.Configuration=function(e,n){var e=e||"";if(void 0==n||null==n)throw new Error("fields should not be null");this.config={};var i;try{i=JSON.parse(e),this.buildUserConfig(i,n)}catch(o){t.utils.warn("user configuration parse failed, will use default configuration"),this.buildDefaultConfig(n)}},t.Configuration.prototype.buildDefaultConfig=function(e){this.reset(),e.forEach(function(e){this.config[e]={boost:1,bool:"OR",expand:!1}},this)},t.Configuration.prototype.buildUserConfig=function(e,n){var i="OR",o=!1;if(this.reset(),"bool"in e&&(i=e.bool||i),"expand"in e&&(o=e.expand||o),"fields"in e)for(var r in e.fields)if(n.indexOf(r)>-1){var s=e.fields[r],u=o;void 0!=s.expand&&(u=s.expand),this.config[r]={boost:s.boost||0===s.boost?s.boost:1,bool:s.bool||i,expand:u}}else t.utils.warn("field name in user configuration not found in index instance fields");else this.addAllFields2UserConfig(i,o,n)},t.Configuration.prototype.addAllFields2UserConfig=function(e,t,n){n.forEach(function(n){this.config[n]={boost:1,bool:e,expand:t}},this)},t.Configuration.prototype.get=function(){return this.config},t.Configuration.prototype.reset=function(){this.config={}},lunr.SortedSet=function(){this.length=0,this.elements=[]},lunr.SortedSet.load=function(e){var t=new this;return t.elements=e,t.length=e.length,t},lunr.SortedSet.prototype.add=function(){var e,t;for(e=0;e1;){if(r===e)return o;e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o]}return r===e?o:-1},lunr.SortedSet.prototype.locationFor=function(e){for(var t=0,n=this.elements.length,i=n-t,o=t+Math.floor(i/2),r=this.elements[o];i>1;)e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o];return r>e?o:e>r?o+1:void 0},lunr.SortedSet.prototype.intersect=function(e){for(var t=new lunr.SortedSet,n=0,i=0,o=this.length,r=e.length,s=this.elements,u=e.elements;;){if(n>o-1||i>r-1)break;s[n]!==u[i]?s[n]u[i]&&i++:(t.add(s[n]),n++,i++)}return t},lunr.SortedSet.prototype.clone=function(){var e=new lunr.SortedSet;return e.elements=this.toArray(),e.length=e.elements.length,e},lunr.SortedSet.prototype.union=function(e){var t,n,i;this.length>=e.length?(t=this,n=e):(t=e,n=this),i=t.clone();for(var o=0,r=n.toArray();o +function toggleSearchModal() { + var e = document.getElementById("search-modal"); + e.classList.toggle("opacity-0"), e.classList.toggle("pointer-events-none"), document.body.classList.toggle("search-active"), [...document.body.classList].includes("search-active") && (document.getElementById("search-input").value = "", document.getElementById("search-input").focus()) +} + +function formatResultItem(e) { + return console.log(e), htmlToElement(`
  • - ${e.doc.title} - ${e.doc.description} + ${e.doc.title} +
    + ${e.doc.description}
    -
  • `)}function htmlToElement(e){var t=document.createElement("template");return e=e.trim(),t.innerHTML=e,t.content.firstChild}document.addEventListener("DOMContentLoaded",function(){document.getElementById("search").addEventListener("click",function(e){e.preventDefault(),toggleSearchModal()});document.querySelector(".modal-overlay").addEventListener("click",toggleSearchModal);for(var e=document.querySelectorAll(".modal-close"),o=0;o`) +} + +function htmlToElement(e) { + var t = document.createElement("template"); + return e = e.trim(), t.innerHTML = e, t.content.firstChild +} +document.addEventListener("DOMContentLoaded", function() { + document.getElementById("search").addEventListener("click", function(e) { + e.preventDefault(), toggleSearchModal() + }); + document.querySelector(".modal-overlay").addEventListener("click", toggleSearchModal); + for (var e = document.querySelectorAll(".modal-close"), o = 0; o < e.length; o++) e[o].addEventListener("click", toggleSearchModal); + document.onkeydown = function(e) { + let t = !1, + n = !1; + "key" in (e = e || window.event) ? (t = "Escape" === e.key || "Esc" === e.key, n = "k" === e.key && !0 === e.metaKey) : (n = 75 === e.keyCode && e.metaKey, t = 27 === e.keyCode), n && e.preventDefault(), (t && document.body.classList.contains("search-active") || n) && toggleSearchModal() + }; + let a = elasticlunr.Index.load(window.searchIndex), + l = { + bool: "AND", + fields: { + title: { + boost: 2 + }, + body: { + boost: 1 + } + } + }, + r, c, d = document.getElementById("search-input"); + document.getElementById("search-results"); + d.addEventListener("keyup", function(e) { + if ([...document.body.classList].includes("search-active") && 3 < d.value.trim().length && (r = d.value.trim(), c = a.search(r, l), Array.isArray(c)) && 0 < c.length) { + var t = document.getElementById("results-list"); + t.replaceChildren(); + for (o = 0; o < c.length; o++) { + var n = formatResultItem(c[o]); + t.appendChild(n) + } + } + }) +}); diff --git a/templates/layout.html b/templates/layout.html index a24aaa3..cb7886f 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -36,7 +36,7 @@ {% endblock title %} - + @@ -113,7 +113,7 @@