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::borrow::Cow; use std::ffi::OsString; use std::fmt; use std::fs; use std::io::*; use std::path::Path; use std::path::PathBuf; use chrono::Datelike; fn current_year() -> i32 { chrono::Utc::now().year() } trait Post: fmt::Debug { fn dump(&self); fn display_name(&self) -> Cow; fn get_content(&self) -> Cow; } trait Tutor: fmt::Debug { fn id(&self) -> &str; fn display_name(&self) -> &str; fn blurb(&self) -> &str; } struct MdFilePost { path: PathBuf, name: OsString, } impl MdFilePost { fn new(path: &Path) -> Self { Self { path: path.to_path_buf(), name: path.file_stem().unwrap_or_default().to_owned(), } } } impl Post for MdFilePost { fn dump(&self) { println!("{}: {}", &self.display_name(), &self.get_content()); } fn display_name(&self) -> Cow { self.name.to_string_lossy() } fn get_content(&self) -> Cow { let mut file = std::fs::File::open(&self.path).unwrap(); let mut markdown = String::new(); file.read_to_string(&mut markdown).unwrap(); Cow::Owned(markdown) } } impl fmt::Debug for MdFilePost { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "MdFilePost '{}'", self.display_name()) } } trait Posts { fn get_posts(&self) -> impl IntoIterator; } trait Tutors { fn get_tutors(&self) -> impl IntoIterator; } struct FsDirPosts { path: PathBuf, } impl FsDirPosts { fn new(path: &str) -> Self { Self { path: PathBuf::from(path), } } } struct FsDirTutors { path: PathBuf, } impl Tutors for FsDirTutors { fn get_tutors(&self) -> impl IntoIterator { let test: Vec = Vec::new(); test } } impl FsDirTutors { fn new(path: &str) -> Self { Self { path: PathBuf::from(path), } } } impl Posts for FsDirPosts { fn get_posts(&self) -> impl IntoIterator { let files = fs::read_dir(&self.path).unwrap(); files.flatten().filter(|d| !d.path().file_stem().unwrap().to_str().unwrap_or_default().starts_with('.')).map(|d| MdFilePost::new(&d.path())) } } trait Page: Template { fn from_post(post: &impl Post) -> Self; } #[derive(Template)] #[template(path = "post.html")] struct MdPage { article: String, } impl Page for MdPage { fn from_post(post: &impl Post) -> Self { Self { article: post.get_content().into_owned(), } } } #[derive(Template)] #[template(path = "posts.html")] struct PostsView { post_titles: Vec } impl PostsView { fn with_posts(posts: &impl Posts) -> Self { Self { post_titles: posts.get_posts().into_iter().map(|p| p.display_name().into_owned()).map(String::from).collect() } } } // TODO: one single Markdown struct with id, name, and content can take over for both MdTutor and // MdPost if it implements the Tutor and Post traits // With this, I could have a repo which does all the same loading but returns things of a generic T // as long as that T is provided it'll map values into those fields? async fn posts_handler(State(posts): State>) -> Html { let view = PostsView::with_posts(&*posts); Html(view.render().unwrap()) } #[derive(Debug)] struct MdTutor { id: String, name: String, blurb: String, } impl Tutor for MdTutor { fn id(&self) -> &str { self.id.as_str() } fn display_name(&self) -> &str { self.name.as_str() } fn blurb(&self) -> &str { self.blurb.as_str() } } #[derive(Template)] #[template(path = "about/index.html")] struct AboutView { tutors: Vec, } impl AboutView { fn with_tutors(tutors: &impl Tutors) -> Self { todo!() } } async fn about_handler(State(tutors): State>) -> Html { let view: AboutView = AboutView::with_tutors(&*tutors); 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(FsDirPosts::new(&format!("/data/ct/{}", "blog"))); let tutors = Arc::new(FsDirTutors::new(&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(); }