use std::cmp::Ordering; use std::borrow::Cow; use tower_http::services::ServeDir; use axum::response::Html; use axum::extract::State; use std::sync::Arc; use askama_axum::Template; use axum::{routing::get, Router}; use std::fmt; use std::fs; use std::path::PathBuf; use chrono::Datelike; fn current_year() -> i32 { chrono::Utc::now().year() } trait Post: fmt::Debug { fn get_title(&self) -> &str; fn get_article(&self) -> Cow; } #[derive(Debug)] struct FsPost { file: PathBuf, } impl Post for FsPost { fn get_title(&self) -> &str { self.file.file_name().unwrap().to_str().unwrap() } fn get_article(&self) -> Cow { let article = fs::read_to_string(&self.file).unwrap(); Cow::Owned(article) } } trait PostRepo { fn load(&self) -> impl IntoIterator; } struct FsPostRepo { dir: PathBuf, } impl FsPostRepo { fn with_dir(path: impl Into) -> Self { Self { dir: path.into() } } } impl PostRepo for FsPostRepo { fn load(&self) -> impl IntoIterator { let files = fs::read_dir(&self.dir).unwrap(); files .flatten() .filter(|d| !d.file_name().to_string_lossy().starts_with('.')) .map(|d| FsPost { file: d.path() }) } } #[derive(Template)] #[template(path = "posts.html")] struct PostsView { posts: Vec

} impl PostsView

{ fn with_posts(posts: impl IntoIterator) -> Self { Self { posts: posts.into_iter().collect() } } } async fn posts_handler(State(repo): State>) -> Html { let view = PostsView::with_posts(repo.load()); Html(view.render().unwrap()) } trait Tutor: fmt::Debug + Ord { fn get_name(&self) -> &str; fn get_id(&self) -> &str; fn get_blurb(&self) -> Cow; } #[derive(Debug, Eq)] struct FsTutor { dir: PathBuf } impl Tutor for FsTutor { fn get_id(&self) -> &str { self.dir.file_name().unwrap().to_str().unwrap() } fn get_name(&self) -> &str { self.get_id() } fn get_blurb(&self) -> Cow { let mut path = self.dir.to_owned(); path.push(format!("{}.md", self.get_id())); let blurb = fs::read_to_string(path).unwrap(); Cow::Owned(blurb) } } impl Ord for FsTutor { fn cmp(&self, other: &Self) -> Ordering { self.get_id().cmp(other.get_id()) } } impl PartialOrd for FsTutor { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl PartialEq for FsTutor { fn eq(&self, other: &Self) -> bool { self.get_id() == other.get_id() } } trait TutorRepo { fn load(&self) -> impl IntoIterator; } struct FsTutorRepo { dir: PathBuf } impl FsTutorRepo { fn with_dir(path: impl Into) -> Self { Self { dir: path.into() } } } impl TutorRepo for FsTutorRepo { fn load(&self) -> impl IntoIterator { let dirs = fs::read_dir(&self.dir).unwrap(); dirs.flatten().filter(|d| !d.path().file_stem().unwrap().to_str().unwrap_or_default().starts_with('.')).map(|d| FsTutor { dir: d.path() }) } } #[derive(Template)] #[template(path = "about/index.html")] struct AboutView { tutors: Vec, } impl AboutView { fn with_tutors(tutors: impl IntoIterator) -> Self { let mut tutors: Vec = tutors.into_iter().collect(); tutors.sort(); Self { tutors } } } async fn about_handler(State(repo): State>) -> Html { let view = AboutView::with_tutors(repo.load()); Html(view.render().unwrap()) } #[derive(Template)] #[template(path = "index.html")] struct IndexTemplate; async fn index_handler() -> Html { Html(IndexTemplate {}.render().unwrap()) } #[derive(Template)] #[template(path = "policies.html")] struct PoliciesTemplate; async fn policies_handler() -> Html { Html(PoliciesTemplate{}.render().unwrap()) } #[derive(Template)] #[template(path = "brochure/index.html")] struct BrochureTemplate; async fn brochure_handler() -> Html { Html(BrochureTemplate{}.render().unwrap()) } #[tokio::main] async fn main() { let posts = Arc::new(FsPostRepo::with_dir(format!("/data/ct/{}", "blog"))); let tutors = Arc::new(FsTutorRepo::with_dir(format!("/data/ct/{}", "team"))); let app = Router::new() .route("/", get(index_handler)) .route("/posts", get(posts_handler)) .with_state(posts) .route("/policies", get(policies_handler)) .route("/brochure", get(brochure_handler)) .route("/about", get(about_handler)) .with_state(tutors) .nest_service("/assets", ServeDir::new("/data/ct/assets")) .nest_service("/team", ServeDir::new("/data/ct/team")) .fallback_service(ServeDir::new("static")); let listener = tokio::net::TcpListener::bind("0.0.0.0:8000").await.unwrap(); axum::serve(listener, app).await.unwrap(); }