From ae00627e7c99fd21ce4ad8ec0692445f00a349b2 Mon Sep 17 00:00:00 2001
From: "Adam T. Carpenter" <atc@53hor.net>
Date: Sat, 7 Sep 2024 21:18:54 -0400
Subject: feat: basic markdown post to html page loading

---
 src/main.rs | 136 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 136 insertions(+)
 create mode 100644 src/main.rs

(limited to 'src')

diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..43b6fa6
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,136 @@
+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;
+
+trait Post<'a>: fmt::Debug {
+    fn dump(&self);
+    fn display_name(&'a self) -> &'a str;
+    fn get_content(&self) -> Cow<str>;
+}
+
+#[derive(Debug)]
+struct MockPost;
+
+impl<'a> Post<'a> for MockPost {
+    fn dump(&self) {
+        println!("Post content goes here.");
+    }
+
+    fn display_name(&'a self) -> &'a str {
+        ""
+    }
+
+    fn get_content(&self) -> Cow<str> {
+        Cow::Borrowed("")
+    }
+}
+
+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<'a> Post<'a> for MdFilePost {
+    fn dump(&self) {
+        println!("{}: {}", &self.display_name(), &self.get_content());
+    }
+
+    fn display_name(&'a self) -> &'a str {
+        self.name.to_str().unwrap()
+    }
+
+    fn get_content(&self) -> Cow<str> {
+        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 Iterator<Item = impl Post>;
+}
+
+struct FsDirPosts {
+    path: PathBuf,
+}
+
+impl FsDirPosts {
+    fn new(path: &str) -> Self {
+        Self {
+            path: PathBuf::from(path),
+        }
+    }
+}
+
+impl Posts for FsDirPosts {
+    fn get_posts(&self) -> impl Iterator<Item = impl Post> {
+        let dirs = fs::read_dir(&self.path).unwrap();
+        dirs.flatten().map(|d| MdFilePost::new(&d.path()))
+    }
+}
+
+trait Page<'a>: Template {
+    fn from_post(post: &impl Post<'a>) -> Self;
+}
+
+#[derive(Template)]
+#[template(path = "post.html")]
+struct MdPage {
+    article: String,
+}
+
+impl<'a> Page<'a> for MdPage {
+    fn from_post(post: &impl Post<'a>) -> Self {
+        Self {
+            article: post.get_content().into_owned(),
+        }
+    }
+}
+
+fn test_get_posts(posts: &impl Posts) {
+    for post in posts.get_posts() {
+        let page = MdPage::from_post(&post);
+        println!("{}", page.render().unwrap());
+    }
+}
+
+async fn handler(State(posts): State<Arc<impl Posts>>) -> Html<String> {
+    let post = posts.get_posts().next().unwrap();
+    let page = MdPage::from_post(&post);
+    Html(page.render().unwrap())
+}
+
+#[tokio::main]
+async fn main() {
+    let repo = Arc::new(FsDirPosts::new(&format!("/data/ct/{}", "blog")));
+
+    let app = Router::new().route("/posts", get(handler)).with_state(repo);
+
+    let listener = tokio::net::TcpListener::bind("0.0.0.0:8000").await.unwrap();
+    axum::serve(listener, app).await.unwrap();
+}
-- 
cgit v1.2.3


From f2bd378e1a8cdfa7d1520b3734a748dd1cd9de25 Mon Sep 17 00:00:00 2001
From: "Adam T. Carpenter" <atc@53hor.net>
Date: Sat, 7 Sep 2024 22:39:41 -0400
Subject: feat: serve static and assets add policies and index from base

---
 src/main.rs | 82 +++++++++++++++++++++++++++++++++++--------------------------
 1 file changed, 47 insertions(+), 35 deletions(-)

(limited to 'src')

diff --git a/src/main.rs b/src/main.rs
index 43b6fa6..ca0a3d9 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,3 +1,4 @@
+use tower_http::services::ServeDir;
 use axum::response::Html;
 use axum::extract::State;
 use std::sync::Arc;
@@ -11,29 +12,12 @@ use std::io::*;
 use std::path::Path;
 use std::path::PathBuf;
 
-trait Post<'a>: fmt::Debug {
+trait Post: fmt::Debug {
     fn dump(&self);
-    fn display_name(&'a self) -> &'a str;
+    fn display_name(&self) -> Cow<str>;
     fn get_content(&self) -> Cow<str>;
 }
 
-#[derive(Debug)]
-struct MockPost;
-
-impl<'a> Post<'a> for MockPost {
-    fn dump(&self) {
-        println!("Post content goes here.");
-    }
-
-    fn display_name(&'a self) -> &'a str {
-        ""
-    }
-
-    fn get_content(&self) -> Cow<str> {
-        Cow::Borrowed("")
-    }
-}
-
 struct MdFilePost {
     path: PathBuf,
     name: OsString,
@@ -48,13 +32,13 @@ impl MdFilePost {
     }
 }
 
-impl<'a> Post<'a> for MdFilePost {
+impl Post for MdFilePost {
     fn dump(&self) {
         println!("{}: {}", &self.display_name(), &self.get_content());
     }
 
-    fn display_name(&'a self) -> &'a str {
-        self.name.to_str().unwrap()
+    fn display_name(&self) -> Cow<str> {
+        self.name.to_string_lossy()
     }
 
     fn get_content(&self) -> Cow<str> {
@@ -94,8 +78,8 @@ impl Posts for FsDirPosts {
     }
 }
 
-trait Page<'a>: Template {
-    fn from_post(post: &impl Post<'a>) -> Self;
+trait Page: Template {
+    fn from_post(post: &impl Post) -> Self;
 }
 
 #[derive(Template)]
@@ -104,32 +88,60 @@ struct MdPage {
     article: String,
 }
 
-impl<'a> Page<'a> for MdPage {
-    fn from_post(post: &impl Post<'a>) -> Self {
+impl Page for MdPage {
+    fn from_post(post: &impl Post) -> Self {
         Self {
             article: post.get_content().into_owned(),
         }
     }
 }
 
-fn test_get_posts(posts: &impl Posts) {
-    for post in posts.get_posts() {
-        let page = MdPage::from_post(&post);
-        println!("{}", page.render().unwrap());
+#[derive(Template)]
+#[template(path = "posts.html")]
+struct PostsView {
+    post_titles: Vec<String>
+}
+
+impl PostsView {
+    fn with_posts(posts: &impl Posts) -> Self {
+        Self {
+            post_titles: posts.get_posts().map(|p| p.display_name().into_owned()).map(String::from).collect()
+        }
     }
 }
 
-async fn handler(State(posts): State<Arc<impl Posts>>) -> Html<String> {
-    let post = posts.get_posts().next().unwrap();
-    let page = MdPage::from_post(&post);
-    Html(page.render().unwrap())
+async fn posts_handler(State(posts): State<Arc<impl Posts>>) -> Html<String> {
+    let view = PostsView::with_posts(&*posts);
+    Html(view.render().unwrap())
+}
+
+#[derive(Template)]
+#[template(path = "index.html")]
+struct IndexTemplate;
+
+async fn index_handler() -> Html<String> {
+    Html(IndexTemplate {}.render().unwrap())
+}
+
+#[derive(Template)]
+#[template(path = "policies.html")]
+struct PoliciesTemplate;
+
+async fn policies_handler() -> Html<String> {
+    Html(PoliciesTemplate{}.render().unwrap())
 }
 
 #[tokio::main]
 async fn main() {
     let repo = Arc::new(FsDirPosts::new(&format!("/data/ct/{}", "blog")));
 
-    let app = Router::new().route("/posts", get(handler)).with_state(repo);
+    let app = Router::new()
+        .nest_service("/static", ServeDir::new("static"))
+        .nest_service("/assets", ServeDir::new("/data/ct/assets"))
+        .route("/posts", get(posts_handler))
+        .route("/policies", get(policies_handler))
+        .route("/", get(index_handler))
+        .with_state(repo);
 
     let listener = tokio::net::TcpListener::bind("0.0.0.0:8000").await.unwrap();
     axum::serve(listener, app).await.unwrap();
-- 
cgit v1.2.3


From 0213fd2dcd09ca4b1252cdc45415a765a887d679 Mon Sep 17 00:00:00 2001
From: "Adam T. Carpenter" <atc@53hor.net>
Date: Mon, 9 Sep 2024 22:28:33 -0400
Subject: feat: teams and blurbs but lots of todos

---
 src/main.rs | 110 ++++++++++++++++++++++++++++++++++++++++++++++++++++++------
 1 file changed, 100 insertions(+), 10 deletions(-)

(limited to 'src')

diff --git a/src/main.rs b/src/main.rs
index ca0a3d9..d07d201 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -11,6 +11,11 @@ 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);
@@ -18,6 +23,12 @@ trait Post: fmt::Debug {
     fn get_content(&self) -> Cow<str>;
 }
 
+trait Tutor: fmt::Debug {
+    fn id(&self) -> &str;
+    fn display_name(&self) -> &str;
+    fn blurb(&self) -> &str;
+}
+
 struct MdFilePost {
     path: PathBuf,
     name: OsString,
@@ -56,7 +67,11 @@ impl fmt::Debug for MdFilePost {
 }
 
 trait Posts {
-    fn get_posts(&self) -> impl Iterator<Item = impl Post>;
+    fn get_posts(&self) -> impl IntoIterator<Item = impl Post>;
+}
+
+trait Tutors {
+    fn get_tutors(&self) -> impl IntoIterator<Item = impl Tutor>;
 }
 
 struct FsDirPosts {
@@ -71,10 +86,29 @@ impl FsDirPosts {
     }
 }
 
+struct FsDirTutors {
+    path: PathBuf,
+}
+
+impl Tutors for FsDirTutors {
+    fn get_tutors(&self) -> impl IntoIterator<Item = impl Tutor> {
+        let test: Vec<MdTutor> = Vec::new();
+        test
+    }
+}
+
+impl FsDirTutors {
+    fn new(path: &str) -> Self {
+        Self {
+            path: PathBuf::from(path),
+        }
+    }
+}
+
 impl Posts for FsDirPosts {
-    fn get_posts(&self) -> impl Iterator<Item = impl Post> {
-        let dirs = fs::read_dir(&self.path).unwrap();
-        dirs.flatten().map(|d| MdFilePost::new(&d.path()))
+    fn get_posts(&self) -> impl IntoIterator<Item = impl Post> {
+        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()))
     }
 }
 
@@ -105,16 +139,59 @@ struct PostsView {
 impl PostsView {
     fn with_posts(posts: &impl Posts) -> Self {
         Self {
-            post_titles: posts.get_posts().map(|p| p.display_name().into_owned()).map(String::from).collect()
+            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<Arc<impl Posts>>) -> Html<String> {
     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<T: Tutor> {
+    tutors: Vec<T>,
+}
+
+impl<T: Tutor> AboutView<T> {
+    fn with_tutors(tutors: &impl Tutors) -> Self {
+        todo!()
+    }
+}
+
+async fn about_handler(State(tutors): State<Arc<impl Tutors>>) -> Html<String> {
+    let view: AboutView<MdTutor> = AboutView::with_tutors(&*tutors);
+    Html(view.render().unwrap())
+}
+
 #[derive(Template)]
 #[template(path = "index.html")]
 struct IndexTemplate;
@@ -131,17 +208,30 @@ async fn policies_handler() -> Html<String> {
     Html(PoliciesTemplate{}.render().unwrap())
 }
 
+#[derive(Template)]
+#[template(path = "brochure/index.html")]
+struct BrochureTemplate;
+
+async fn brochure_handler() -> Html<String> {
+    Html(BrochureTemplate{}.render().unwrap())
+}
+
 #[tokio::main]
 async fn main() {
-    let repo = Arc::new(FsDirPosts::new(&format!("/data/ct/{}", "blog")));
+    let posts = Arc::new(FsDirPosts::new(&format!("/data/ct/{}", "blog")));
+    let tutors = Arc::new(FsDirTutors::new(&format!("/data/ct/{}", "team")));
 
     let app = Router::new()
-        .nest_service("/static", ServeDir::new("static"))
-        .nest_service("/assets", ServeDir::new("/data/ct/assets"))
+        .route("/", get(index_handler))
         .route("/posts", get(posts_handler))
+        .with_state(posts)
         .route("/policies", get(policies_handler))
-        .route("/", get(index_handler))
-        .with_state(repo);
+        .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();
-- 
cgit v1.2.3


From 340a804e550cb5b733bd2e64e515e79740bb6338 Mon Sep 17 00:00:00 2001
From: "Adam T. Carpenter" <atc@53hor.net>
Date: Sat, 14 Sep 2024 11:22:54 -0400
Subject: feat: impl tutors/about/team view

---
 src/main.rs | 211 +++++++++++++++++++++++++++---------------------------------
 1 file changed, 95 insertions(+), 116 deletions(-)

(limited to 'src')

diff --git a/src/main.rs b/src/main.rs
index d07d201..7b5ddfc 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,15 +1,13 @@
+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::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;
 
@@ -18,177 +16,158 @@ fn current_year() -> i32 {
 }
 
 trait Post: fmt::Debug {
-    fn dump(&self);
-    fn display_name(&self) -> Cow<str>;
-    fn get_content(&self) -> Cow<str>;
-}
-
-trait Tutor: fmt::Debug {
-    fn id(&self) -> &str;
-    fn display_name(&self) -> &str;
-    fn blurb(&self) -> &str;
+    fn get_title(&self) -> &str;
+    fn get_article(&self) -> Cow<str>;
 }
 
-struct MdFilePost {
-    path: PathBuf,
-    name: OsString,
+#[derive(Debug)]
+struct FsPost {
+    file: PathBuf,
 }
 
-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 FsPost {
+    fn get_title(&self) -> &str {
+        self.file.file_name().unwrap().to_str().unwrap()
     }
-}
 
-impl Post for MdFilePost {
-    fn dump(&self) {
-        println!("{}: {}", &self.display_name(), &self.get_content());
+    fn get_article(&self) -> Cow<str> {
+        let article = fs::read_to_string(&self.file).unwrap();
+        Cow::Owned(article)
     }
+}
 
-    fn display_name(&self) -> Cow<str> {
-        self.name.to_string_lossy()
-    }
+trait PostRepo {
+    fn load(&self) -> impl IntoIterator<Item = impl Post>;
+}
 
-    fn get_content(&self) -> Cow<str> {
-        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)
-    }
+struct FsPostRepo {
+    dir: PathBuf,
 }
 
-impl fmt::Debug for MdFilePost {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "MdFilePost '{}'", self.display_name())
+impl FsPostRepo {
+    fn with_dir(path: impl Into<PathBuf>) -> Self {
+        Self {
+            dir: path.into()
+        }
     }
 }
 
-trait Posts {
-    fn get_posts(&self) -> impl IntoIterator<Item = impl Post>;
+impl PostRepo for FsPostRepo {
+    fn load(&self) -> impl IntoIterator<Item = FsPost> {
+        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() })
+    }
 }
 
-trait Tutors {
-    fn get_tutors(&self) -> impl IntoIterator<Item = impl Tutor>;
-}
 
-struct FsDirPosts {
-    path: PathBuf,
+#[derive(Template)]
+#[template(path = "posts.html")]
+struct PostsView<P: Post> {
+    posts: Vec<P>
 }
 
-impl FsDirPosts {
-    fn new(path: &str) -> Self {
+impl<P: Post> PostsView<P> {
+    fn with_posts(posts: impl IntoIterator<Item = P>) -> Self {
         Self {
-            path: PathBuf::from(path),
+            posts: posts.into_iter().collect()
         }
     }
 }
 
-struct FsDirTutors {
-    path: PathBuf,
+async fn posts_handler(State(repo): State<Arc<impl PostRepo>>) -> Html<String> {
+    let view = PostsView::with_posts(repo.load());
+    Html(view.render().unwrap())
 }
 
-impl Tutors for FsDirTutors {
-    fn get_tutors(&self) -> impl IntoIterator<Item = impl Tutor> {
-        let test: Vec<MdTutor> = Vec::new();
-        test
-    }
+trait Tutor: fmt::Debug + Ord {
+    fn get_name(&self) -> &str;
+    fn get_id(&self) -> &str;
+    fn get_blurb(&self) -> Cow<str>;
 }
 
-impl FsDirTutors {
-    fn new(path: &str) -> Self {
-        Self {
-            path: PathBuf::from(path),
-        }
-    }
+#[derive(Debug, Eq)]
+struct FsTutor {
+    dir: PathBuf
 }
 
-impl Posts for FsDirPosts {
-    fn get_posts(&self) -> impl IntoIterator<Item = impl Post> {
-        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()))
+impl Tutor for FsTutor {
+    fn get_id(&self) -> &str {
+        self.dir.file_name().unwrap().to_str().unwrap()
     }
-}
 
-trait Page: Template {
-    fn from_post(post: &impl Post) -> Self;
-}
+    fn get_name(&self) -> &str {
+        self.get_id()
+    }
 
-#[derive(Template)]
-#[template(path = "post.html")]
-struct MdPage {
-    article: String,
+    fn get_blurb(&self) -> Cow<str> {
+        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 Page for MdPage {
-    fn from_post(post: &impl Post) -> Self {
-        Self {
-            article: post.get_content().into_owned(),
-        }
+impl Ord for FsTutor {
+    fn cmp(&self, other: &Self) -> Ordering {
+        self.get_id().cmp(other.get_id())
     }
 }
 
-#[derive(Template)]
-#[template(path = "posts.html")]
-struct PostsView {
-    post_titles: Vec<String>
+impl PartialOrd for FsTutor {
+    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+        Some(self.cmp(other))
+    }
 }
 
-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()
-        }
+impl PartialEq for FsTutor {
+    fn eq(&self, other: &Self) -> bool {
+        self.get_id() == other.get_id()
     }
 }
 
-// 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<Arc<impl Posts>>) -> Html<String> {
-    let view = PostsView::with_posts(&*posts);
-    Html(view.render().unwrap())
+trait TutorRepo {
+    fn load(&self) -> impl IntoIterator<Item = impl Tutor>;
 }
 
-#[derive(Debug)]
-struct MdTutor {
-    id: String,
-    name: String,
-    blurb: String,
+struct FsTutorRepo {
+    dir: PathBuf
 }
 
-impl Tutor for MdTutor {
-    fn id(&self) -> &str {
-        self.id.as_str()
+impl FsTutorRepo {
+    fn with_dir(path: impl Into<PathBuf>) -> Self {
+        Self {
+            dir: path.into()
+        }
     }
+}
 
-    fn display_name(&self) -> &str {
-        self.name.as_str()
-    }
 
-    fn blurb(&self) -> &str { 
-        self.blurb.as_str()
+impl TutorRepo for FsTutorRepo {
+    fn load(&self) -> impl IntoIterator<Item = FsTutor> {
+        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<T: Tutor> {
     tutors: Vec<T>,
 }
-
-impl<T: Tutor> AboutView<T> {
-    fn with_tutors(tutors: &impl Tutors) -> Self {
-        todo!()
+impl<T: Tutor + Ord> AboutView<T> {
+    fn with_tutors(tutors: impl IntoIterator<Item = T>) -> Self {
+        let mut tutors: Vec<T> = tutors.into_iter().collect();
+        tutors.sort();
+        Self { tutors }
     }
 }
 
-async fn about_handler(State(tutors): State<Arc<impl Tutors>>) -> Html<String> {
-    let view: AboutView<MdTutor> = AboutView::with_tutors(&*tutors);
+async fn about_handler(State(repo): State<Arc<impl TutorRepo>>) -> Html<String> {
+    let view = AboutView::with_tutors(repo.load());
     Html(view.render().unwrap())
 }
 
@@ -218,8 +197,8 @@ async fn brochure_handler() -> Html<String> {
 
 #[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 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))
-- 
cgit v1.2.3


From 9f341d439f7aa5fd2365024169ead2d6bdc3210c Mon Sep 17 00:00:00 2001
From: "Adam T. Carpenter" <atc@53hor.net>
Date: Sat, 14 Sep 2024 20:30:05 -0400
Subject: feat: rewrite complete

---
 src/handlers.rs                       |  34 ++++++
 src/helpers.rs                        |   5 +
 src/main.rs                           | 213 +++-------------------------------
 src/posts.rs                          |   3 +
 src/posts/abstractions.rs             |   2 +
 src/posts/abstractions/post.rs        |   6 +
 src/posts/abstractions/repo.rs        |   5 +
 src/posts/fs_post.rs                  |  24 ++++
 src/posts/fs_post_repo.rs             |  23 ++++
 src/tutors.rs                         |   3 +
 src/tutors/abstractions.rs            |   2 +
 src/tutors/abstractions/tutor.rs      |   7 ++
 src/tutors/abstractions/tutor_repo.rs |   5 +
 src/tutors/fs_tutor.rs                |  48 ++++++++
 src/tutors/fs_tutor_repo.rs           |  29 +++++
 src/views.rs                          |   5 +
 src/views/about.rs                    |  17 +++
 src/views/brochure.rs                 |   6 +
 src/views/index.rs                    |   6 +
 src/views/policies.rs                 |   6 +
 src/views/posts.rs                    |  17 +++
 21 files changed, 267 insertions(+), 199 deletions(-)
 create mode 100644 src/handlers.rs
 create mode 100644 src/helpers.rs
 create mode 100644 src/posts.rs
 create mode 100644 src/posts/abstractions.rs
 create mode 100644 src/posts/abstractions/post.rs
 create mode 100644 src/posts/abstractions/repo.rs
 create mode 100644 src/posts/fs_post.rs
 create mode 100644 src/posts/fs_post_repo.rs
 create mode 100644 src/tutors.rs
 create mode 100644 src/tutors/abstractions.rs
 create mode 100644 src/tutors/abstractions/tutor.rs
 create mode 100644 src/tutors/abstractions/tutor_repo.rs
 create mode 100644 src/tutors/fs_tutor.rs
 create mode 100644 src/tutors/fs_tutor_repo.rs
 create mode 100644 src/views.rs
 create mode 100644 src/views/about.rs
 create mode 100644 src/views/brochure.rs
 create mode 100644 src/views/index.rs
 create mode 100644 src/views/policies.rs
 create mode 100644 src/views/posts.rs

(limited to 'src')

diff --git a/src/handlers.rs b/src/handlers.rs
new file mode 100644
index 0000000..db5cf7c
--- /dev/null
+++ b/src/handlers.rs
@@ -0,0 +1,34 @@
+use askama::Template;
+use crate::views::posts::PostsView;
+use crate::posts::abstractions::repo::PostRepo;
+use crate::views::policies::PoliciesTemplate;
+use crate::views::index::IndexTemplate;
+use crate::views::brochure::BrochureTemplate;
+use crate::views::about::AboutView;
+use crate::tutors::abstractions::tutor_repo::TutorRepo;
+use std::sync::Arc;
+use axum::response::Html;
+use axum::extract::State;
+
+pub async fn about_handler(State(repo): State<Arc<impl TutorRepo>>) -> Html<String> {
+    let view = AboutView::with_tutors(repo.load());
+    Html(view.render().unwrap())
+}
+
+pub async fn brochure_handler() -> Html<String> {
+    Html(BrochureTemplate{}.render().unwrap())
+}
+
+pub async fn index_handler() -> Html<String> {
+    Html(IndexTemplate {}.render().unwrap())
+}
+
+pub async fn policies_handler() -> Html<String> {
+    Html(PoliciesTemplate{}.render().unwrap())
+}
+
+pub async fn posts_handler(State(repo): State<Arc<impl PostRepo>>) -> Html<String> {
+    let view = PostsView::with_posts(repo.load());
+    Html(view.render().unwrap())
+}
+
diff --git a/src/helpers.rs b/src/helpers.rs
new file mode 100644
index 0000000..c7509de
--- /dev/null
+++ b/src/helpers.rs
@@ -0,0 +1,5 @@
+use chrono::{prelude::*, Utc};
+
+pub fn current_year() -> i32 {
+    Utc::now().year()
+}
diff --git a/src/main.rs b/src/main.rs
index 7b5ddfc..fb15e51 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,199 +1,14 @@
-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<str>;
-}
-
-#[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<str> {
-        let article = fs::read_to_string(&self.file).unwrap();
-        Cow::Owned(article)
-    }
-}
-
-trait PostRepo {
-    fn load(&self) -> impl IntoIterator<Item = impl Post>;
-}
-
-struct FsPostRepo {
-    dir: PathBuf,
-}
-
-impl FsPostRepo {
-    fn with_dir(path: impl Into<PathBuf>) -> Self {
-        Self {
-            dir: path.into()
-        }
-    }
-}
-
-impl PostRepo for FsPostRepo {
-    fn load(&self) -> impl IntoIterator<Item = FsPost> {
-        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<P: Post> {
-    posts: Vec<P>
-}
-
-impl<P: Post> PostsView<P> {
-    fn with_posts(posts: impl IntoIterator<Item = P>) -> Self {
-        Self {
-            posts: posts.into_iter().collect()
-        }
-    }
-}
-
-async fn posts_handler(State(repo): State<Arc<impl PostRepo>>) -> Html<String> {
-    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<str>;
-}
-
-#[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<str> {
-        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<Ordering> {
-        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<Item = impl Tutor>;
-}
-
-struct FsTutorRepo {
-    dir: PathBuf
-}
-
-impl FsTutorRepo {
-    fn with_dir(path: impl Into<PathBuf>) -> Self {
-        Self {
-            dir: path.into()
-        }
-    }
-}
-
-
-impl TutorRepo for FsTutorRepo {
-    fn load(&self) -> impl IntoIterator<Item = FsTutor> {
-        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<T: Tutor> {
-    tutors: Vec<T>,
-}
-impl<T: Tutor + Ord> AboutView<T> {
-    fn with_tutors(tutors: impl IntoIterator<Item = T>) -> Self {
-        let mut tutors: Vec<T> = tutors.into_iter().collect();
-        tutors.sort();
-        Self { tutors }
-    }
-}
-
-async fn about_handler(State(repo): State<Arc<impl TutorRepo>>) -> Html<String> {
-    let view = AboutView::with_tutors(repo.load());
-    Html(view.render().unwrap())
-}
-
-#[derive(Template)]
-#[template(path = "index.html")]
-struct IndexTemplate;
-
-async fn index_handler() -> Html<String> {
-    Html(IndexTemplate {}.render().unwrap())
-}
-
-#[derive(Template)]
-#[template(path = "policies.html")]
-struct PoliciesTemplate;
-
-async fn policies_handler() -> Html<String> {
-    Html(PoliciesTemplate{}.render().unwrap())
-}
-
-#[derive(Template)]
-#[template(path = "brochure/index.html")]
-struct BrochureTemplate;
+use tutors::fs_tutor_repo::FsTutorRepo;
+use std::sync::Arc;
+use tower_http::services::ServeDir;
+use posts::fs_post_repo::FsPostRepo;
 
-async fn brochure_handler() -> Html<String> {
-    Html(BrochureTemplate{}.render().unwrap())
-}
+mod helpers;
+mod posts;
+mod tutors;
+mod views;
+mod handlers;
 
 #[tokio::main]
 async fn main() {
@@ -201,12 +16,12 @@ async fn main() {
     let tutors = Arc::new(FsTutorRepo::with_dir(format!("/data/ct/{}", "team")));
 
     let app = Router::new()
-        .route("/", get(index_handler))
-        .route("/posts", get(posts_handler))
+        .route("/", get(handlers::index_handler))
+        .route("/posts", get(handlers::posts_handler))
         .with_state(posts)
-        .route("/policies", get(policies_handler))
-        .route("/brochure", get(brochure_handler))
-        .route("/about", get(about_handler))
+        .route("/policies", get(handlers::policies_handler))
+        .route("/brochure", get(handlers::brochure_handler))
+        .route("/about", get(handlers::about_handler))
         .with_state(tutors)
         .nest_service("/assets", ServeDir::new("/data/ct/assets"))
         .nest_service("/team", ServeDir::new("/data/ct/team"))
diff --git a/src/posts.rs b/src/posts.rs
new file mode 100644
index 0000000..7f2c217
--- /dev/null
+++ b/src/posts.rs
@@ -0,0 +1,3 @@
+pub mod abstractions;
+pub mod fs_post;
+pub mod fs_post_repo;
diff --git a/src/posts/abstractions.rs b/src/posts/abstractions.rs
new file mode 100644
index 0000000..96c8ced
--- /dev/null
+++ b/src/posts/abstractions.rs
@@ -0,0 +1,2 @@
+pub mod post;
+pub mod repo;
diff --git a/src/posts/abstractions/post.rs b/src/posts/abstractions/post.rs
new file mode 100644
index 0000000..6d504db
--- /dev/null
+++ b/src/posts/abstractions/post.rs
@@ -0,0 +1,6 @@
+use std::{borrow::Cow, fmt};
+
+pub trait Post: fmt::Debug {
+    fn get_title(&self) -> &str;
+    fn get_article(&self) -> Cow<str>;
+}
diff --git a/src/posts/abstractions/repo.rs b/src/posts/abstractions/repo.rs
new file mode 100644
index 0000000..6fcb385
--- /dev/null
+++ b/src/posts/abstractions/repo.rs
@@ -0,0 +1,5 @@
+use crate::posts::abstractions::post::Post;
+
+pub trait PostRepo {
+    fn load(&self) -> impl IntoIterator<Item = impl Post>;
+}
diff --git a/src/posts/fs_post.rs b/src/posts/fs_post.rs
new file mode 100644
index 0000000..e767803
--- /dev/null
+++ b/src/posts/fs_post.rs
@@ -0,0 +1,24 @@
+use crate::posts::abstractions::post::Post;
+use std::{borrow::Cow, fs, path::PathBuf};
+
+#[derive(Debug)]
+pub struct FsPost {
+    file: PathBuf,
+}
+
+impl FsPost {
+    pub fn with_path(path: PathBuf) -> Self {
+        Self { file: path }
+    }
+}
+
+impl Post for FsPost {
+    fn get_title(&self) -> &str {
+        self.file.file_name().unwrap().to_str().unwrap()
+    }
+
+    fn get_article(&self) -> Cow<str> {
+        let article = fs::read_to_string(&self.file).unwrap();
+        Cow::Owned(article)
+    }
+}
diff --git a/src/posts/fs_post_repo.rs b/src/posts/fs_post_repo.rs
new file mode 100644
index 0000000..eb37a6a
--- /dev/null
+++ b/src/posts/fs_post_repo.rs
@@ -0,0 +1,23 @@
+use crate::posts::abstractions::repo::PostRepo;
+use crate::posts::fs_post::FsPost;
+use std::{fs, path::PathBuf};
+
+pub struct FsPostRepo {
+    dir: PathBuf,
+}
+
+impl FsPostRepo {
+    pub fn with_dir(path: impl Into<PathBuf>) -> Self {
+        Self { dir: path.into() }
+    }
+}
+
+impl PostRepo for FsPostRepo {
+    fn load(&self) -> impl IntoIterator<Item = FsPost> {
+        let files = fs::read_dir(&self.dir).unwrap();
+        files
+            .flatten()
+            .filter(|d| !d.file_name().to_string_lossy().starts_with('.'))
+            .map(|d| FsPost::with_path(d.path()))
+    }
+}
diff --git a/src/tutors.rs b/src/tutors.rs
new file mode 100644
index 0000000..d90ef99
--- /dev/null
+++ b/src/tutors.rs
@@ -0,0 +1,3 @@
+pub mod abstractions;
+pub mod fs_tutor;
+pub mod fs_tutor_repo;
diff --git a/src/tutors/abstractions.rs b/src/tutors/abstractions.rs
new file mode 100644
index 0000000..a93c25d
--- /dev/null
+++ b/src/tutors/abstractions.rs
@@ -0,0 +1,2 @@
+pub mod tutor;
+pub mod tutor_repo;
diff --git a/src/tutors/abstractions/tutor.rs b/src/tutors/abstractions/tutor.rs
new file mode 100644
index 0000000..f36da3d
--- /dev/null
+++ b/src/tutors/abstractions/tutor.rs
@@ -0,0 +1,7 @@
+use std::{borrow::Cow, fmt};
+
+pub trait Tutor: fmt::Debug + Ord {
+    fn get_name(&self) -> &str;
+    fn get_id(&self) -> &str;
+    fn get_blurb(&self) -> Cow<str>;
+}
diff --git a/src/tutors/abstractions/tutor_repo.rs b/src/tutors/abstractions/tutor_repo.rs
new file mode 100644
index 0000000..e017806
--- /dev/null
+++ b/src/tutors/abstractions/tutor_repo.rs
@@ -0,0 +1,5 @@
+use crate::tutors::abstractions::tutor::Tutor;
+
+pub trait TutorRepo {
+    fn load(&self) -> impl IntoIterator<Item = impl Tutor>;
+}
diff --git a/src/tutors/fs_tutor.rs b/src/tutors/fs_tutor.rs
new file mode 100644
index 0000000..dc8a635
--- /dev/null
+++ b/src/tutors/fs_tutor.rs
@@ -0,0 +1,48 @@
+use crate::tutors::abstractions::tutor::Tutor;
+use std::{borrow::Cow, cmp::Ordering, fs, path::PathBuf};
+
+#[derive(Debug, Eq)]
+pub struct FsTutor {
+    dir: PathBuf,
+}
+
+impl FsTutor {
+    pub fn with_dir(path: PathBuf) -> Self {
+        Self { dir: path }
+    }
+}
+
+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<str> {
+        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<Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+impl PartialEq for FsTutor {
+    fn eq(&self, other: &Self) -> bool {
+        self.get_id() == other.get_id()
+    }
+}
diff --git a/src/tutors/fs_tutor_repo.rs b/src/tutors/fs_tutor_repo.rs
new file mode 100644
index 0000000..9b9a8d9
--- /dev/null
+++ b/src/tutors/fs_tutor_repo.rs
@@ -0,0 +1,29 @@
+use crate::tutors::abstractions::tutor_repo::TutorRepo;
+use crate::tutors::fs_tutor::FsTutor;
+use std::{fs, path::PathBuf};
+
+pub struct FsTutorRepo {
+    dir: PathBuf,
+}
+
+impl FsTutorRepo {
+    pub fn with_dir(path: impl Into<PathBuf>) -> Self {
+        Self { dir: path.into() }
+    }
+}
+
+impl TutorRepo for FsTutorRepo {
+    fn load(&self) -> impl IntoIterator<Item = FsTutor> {
+        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::with_dir(d.path()))
+    }
+}
diff --git a/src/views.rs b/src/views.rs
new file mode 100644
index 0000000..b986f56
--- /dev/null
+++ b/src/views.rs
@@ -0,0 +1,5 @@
+pub mod about;
+pub mod brochure;
+pub mod index;
+pub mod policies;
+pub mod posts;
diff --git a/src/views/about.rs b/src/views/about.rs
new file mode 100644
index 0000000..349c9de
--- /dev/null
+++ b/src/views/about.rs
@@ -0,0 +1,17 @@
+use crate::helpers::*;
+use crate::tutors::abstractions::tutor::Tutor;
+use askama::Template;
+
+#[derive(Template)]
+#[template(path = "about/index.html")]
+pub struct AboutView<T: Tutor> {
+    tutors: Vec<T>,
+}
+
+impl<T: Tutor> AboutView<T> {
+    pub fn with_tutors(tutors: impl IntoIterator<Item = T>) -> Self {
+        let mut tutors: Vec<T> = tutors.into_iter().collect();
+        tutors.sort();
+        Self { tutors }
+    }
+}
diff --git a/src/views/brochure.rs b/src/views/brochure.rs
new file mode 100644
index 0000000..0d2f8fc
--- /dev/null
+++ b/src/views/brochure.rs
@@ -0,0 +1,6 @@
+use crate::helpers::*;
+use askama::Template;
+
+#[derive(Template)]
+#[template(path = "brochure/index.html")]
+pub struct BrochureTemplate;
diff --git a/src/views/index.rs b/src/views/index.rs
new file mode 100644
index 0000000..3ced24d
--- /dev/null
+++ b/src/views/index.rs
@@ -0,0 +1,6 @@
+use crate::helpers::*;
+use askama::Template;
+
+#[derive(Template)]
+#[template(path = "index.html")]
+pub struct IndexTemplate;
diff --git a/src/views/policies.rs b/src/views/policies.rs
new file mode 100644
index 0000000..3d9787d
--- /dev/null
+++ b/src/views/policies.rs
@@ -0,0 +1,6 @@
+use crate::helpers::*;
+use askama::Template;
+
+#[derive(Template)]
+#[template(path = "policies.html")]
+pub struct PoliciesTemplate;
diff --git a/src/views/posts.rs b/src/views/posts.rs
new file mode 100644
index 0000000..2ce69f8
--- /dev/null
+++ b/src/views/posts.rs
@@ -0,0 +1,17 @@
+use crate::helpers::*;
+use crate::posts::abstractions::post::Post;
+use askama::Template;
+
+#[derive(Template)]
+#[template(path = "posts.html")]
+pub struct PostsView<P: Post> {
+    posts: Vec<P>,
+}
+
+impl<P: Post> PostsView<P> {
+    pub fn with_posts(posts: impl IntoIterator<Item = P>) -> Self {
+        Self {
+            posts: posts.into_iter().collect(),
+        }
+    }
+}
-- 
cgit v1.2.3


From 18339f611fd17e1300593edd65adf7604a39ad72 Mon Sep 17 00:00:00 2001
From: "Adam T. Carpenter" <atc@53hor.net>
Date: Sat, 14 Sep 2024 20:54:44 -0400
Subject: feat: working rudimentary blog presentation

---
 src/handlers.rs                |  8 +++++++-
 src/main.rs                    |  1 +
 src/posts/abstractions/repo.rs |  1 +
 src/posts/fs_post.rs           |  2 +-
 src/posts/fs_post_repo.rs      |  9 +++++++++
 src/views.rs                   |  1 +
 src/views/post.rs              | 15 +++++++++++++++
 7 files changed, 35 insertions(+), 2 deletions(-)
 create mode 100644 src/views/post.rs

(limited to 'src')

diff --git a/src/handlers.rs b/src/handlers.rs
index db5cf7c..800d8f8 100644
--- a/src/handlers.rs
+++ b/src/handlers.rs
@@ -1,4 +1,5 @@
 use askama::Template;
+use crate::views::post::PostView;
 use crate::views::posts::PostsView;
 use crate::posts::abstractions::repo::PostRepo;
 use crate::views::policies::PoliciesTemplate;
@@ -8,7 +9,7 @@ use crate::views::about::AboutView;
 use crate::tutors::abstractions::tutor_repo::TutorRepo;
 use std::sync::Arc;
 use axum::response::Html;
-use axum::extract::State;
+use axum::extract::{State, Path};
 
 pub async fn about_handler(State(repo): State<Arc<impl TutorRepo>>) -> Html<String> {
     let view = AboutView::with_tutors(repo.load());
@@ -32,3 +33,8 @@ pub async fn posts_handler(State(repo): State<Arc<impl PostRepo>>) -> Html<Strin
     Html(view.render().unwrap())
 }
 
+pub async fn post_handler(Path(post_id): Path<String>, State(repo): State<Arc<impl PostRepo>>) -> Html<String> {
+    let view = PostView::with_post(repo.by_id(&post_id));
+    Html(view.render().unwrap())
+}
+
diff --git a/src/main.rs b/src/main.rs
index fb15e51..4314292 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -18,6 +18,7 @@ async fn main() {
     let app = Router::new()
         .route("/", get(handlers::index_handler))
         .route("/posts", get(handlers::posts_handler))
+        .route("/posts/:post_id", get(handlers::post_handler))
         .with_state(posts)
         .route("/policies", get(handlers::policies_handler))
         .route("/brochure", get(handlers::brochure_handler))
diff --git a/src/posts/abstractions/repo.rs b/src/posts/abstractions/repo.rs
index 6fcb385..6fd5d08 100644
--- a/src/posts/abstractions/repo.rs
+++ b/src/posts/abstractions/repo.rs
@@ -2,4 +2,5 @@ use crate::posts::abstractions::post::Post;
 
 pub trait PostRepo {
     fn load(&self) -> impl IntoIterator<Item = impl Post>;
+    fn by_id(&self, post_id: &str) -> impl Post;
 }
diff --git a/src/posts/fs_post.rs b/src/posts/fs_post.rs
index e767803..f83ff4a 100644
--- a/src/posts/fs_post.rs
+++ b/src/posts/fs_post.rs
@@ -14,7 +14,7 @@ impl FsPost {
 
 impl Post for FsPost {
     fn get_title(&self) -> &str {
-        self.file.file_name().unwrap().to_str().unwrap()
+        self.file.file_stem().unwrap().to_str().unwrap()
     }
 
     fn get_article(&self) -> Cow<str> {
diff --git a/src/posts/fs_post_repo.rs b/src/posts/fs_post_repo.rs
index eb37a6a..13f797b 100644
--- a/src/posts/fs_post_repo.rs
+++ b/src/posts/fs_post_repo.rs
@@ -1,3 +1,4 @@
+use crate::posts::abstractions::post::Post;
 use crate::posts::abstractions::repo::PostRepo;
 use crate::posts::fs_post::FsPost;
 use std::{fs, path::PathBuf};
@@ -20,4 +21,12 @@ impl PostRepo for FsPostRepo {
             .filter(|d| !d.file_name().to_string_lossy().starts_with('.'))
             .map(|d| FsPost::with_path(d.path()))
     }
+
+    fn by_id(&self, post_id: &str) -> FsPost {
+        let posts = self.load();
+        posts
+            .into_iter()
+            .find(|p| p.get_title() == post_id)
+            .unwrap()
+    }
 }
diff --git a/src/views.rs b/src/views.rs
index b986f56..cb58813 100644
--- a/src/views.rs
+++ b/src/views.rs
@@ -2,4 +2,5 @@ pub mod about;
 pub mod brochure;
 pub mod index;
 pub mod policies;
+pub mod post;
 pub mod posts;
diff --git a/src/views/post.rs b/src/views/post.rs
new file mode 100644
index 0000000..4f0554b
--- /dev/null
+++ b/src/views/post.rs
@@ -0,0 +1,15 @@
+use crate::helpers::*;
+use crate::posts::abstractions::post::Post;
+use askama::Template;
+
+#[derive(Template)]
+#[template(path = "post.html")]
+pub struct PostView<P: Post> {
+    post: P,
+}
+
+impl<P: Post> PostView<P> {
+    pub fn with_post(post: P) -> Self {
+        Self { post }
+    }
+}
-- 
cgit v1.2.3


From aad33fee01abe817d82c094f83ffd44ca36c8456 Mon Sep 17 00:00:00 2001
From: "Adam T. Carpenter" <atc@53hor.net>
Date: Sat, 14 Sep 2024 21:11:51 -0400
Subject: feat: reverse sort posts by modify datetime

---
 src/posts/abstractions/post.rs |  2 +-
 src/posts/fs_post.rs           | 25 ++++++++++++++++++++++++-
 src/views/posts.rs             |  7 ++++---
 3 files changed, 29 insertions(+), 5 deletions(-)

(limited to 'src')

diff --git a/src/posts/abstractions/post.rs b/src/posts/abstractions/post.rs
index 6d504db..a941281 100644
--- a/src/posts/abstractions/post.rs
+++ b/src/posts/abstractions/post.rs
@@ -1,6 +1,6 @@
 use std::{borrow::Cow, fmt};
 
-pub trait Post: fmt::Debug {
+pub trait Post: fmt::Debug + Ord {
     fn get_title(&self) -> &str;
     fn get_article(&self) -> Cow<str>;
 }
diff --git a/src/posts/fs_post.rs b/src/posts/fs_post.rs
index f83ff4a..8b8e725 100644
--- a/src/posts/fs_post.rs
+++ b/src/posts/fs_post.rs
@@ -1,7 +1,7 @@
 use crate::posts::abstractions::post::Post;
 use std::{borrow::Cow, fs, path::PathBuf};
 
-#[derive(Debug)]
+#[derive(Debug, Eq)]
 pub struct FsPost {
     file: PathBuf,
 }
@@ -22,3 +22,26 @@ impl Post for FsPost {
         Cow::Owned(article)
     }
 }
+
+impl Ord for FsPost {
+    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+        self.file
+            .metadata()
+            .unwrap()
+            .modified()
+            .unwrap()
+            .cmp(&other.file.metadata().unwrap().modified().unwrap())
+    }
+}
+
+impl PartialOrd for FsPost {
+    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+impl PartialEq for FsPost {
+    fn eq(&self, other: &Self) -> bool {
+        self.get_title() == other.get_title()
+    }
+}
diff --git a/src/views/posts.rs b/src/views/posts.rs
index 2ce69f8..82b5996 100644
--- a/src/views/posts.rs
+++ b/src/views/posts.rs
@@ -10,8 +10,9 @@ pub struct PostsView<P: Post> {
 
 impl<P: Post> PostsView<P> {
     pub fn with_posts(posts: impl IntoIterator<Item = P>) -> Self {
-        Self {
-            posts: posts.into_iter().collect(),
-        }
+        let mut posts: Vec<P> = posts.into_iter().collect();
+        posts.sort();
+        posts.reverse();
+        Self { posts }
     }
 }
-- 
cgit v1.2.3


From 4b5e92345ff880f9233179191cfce1c04bd4c386 Mon Sep 17 00:00:00 2001
From: "Adam T. Carpenter" <atc@53hor.net>
Date: Sun, 15 Sep 2024 23:18:28 -0400
Subject: feat: tracing and env vars

---
 src/main.rs | 40 +++++++++++++++++++++++++++++++++-------
 1 file changed, 33 insertions(+), 7 deletions(-)

(limited to 'src')

diff --git a/src/main.rs b/src/main.rs
index 4314292..81c4427 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,6 +1,9 @@
+use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
+use tower_http::trace::{self, TraceLayer};
+use tracing::{info, Level};
 use axum::{routing::get, Router};
 use tutors::fs_tutor_repo::FsTutorRepo;
-use std::sync::Arc;
+use std::{sync::Arc, env};
 use tower_http::services::ServeDir;
 use posts::fs_post_repo::FsPostRepo;
 
@@ -12,9 +15,24 @@ mod handlers;
 
 #[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 tracing_filter = EnvFilter::builder()
+        .with_env_var("CT_LOG")
+        .try_from_env()
+        .unwrap_or("carpentertutoring=debug,tower_http=debug,axum::rejection=trace".into());
+    tracing_subscriber::registry()
+        .with(tracing_filter)
+        .with(tracing_subscriber::fmt::layer())
+        .init();
 
+    info!("loading state...");
+    let blog_dir = env::var("CT_POSTS").unwrap();
+    let tutor_dir = env::var("CT_TEAM").unwrap();
+    let assets_dir = env::var("CT_ASSETS").unwrap();
+
+    let posts = Arc::new(FsPostRepo::with_dir(blog_dir));
+    let tutors = Arc::new(FsTutorRepo::with_dir(tutor_dir.clone()));
+
+    info!("initializing router...");
     let app = Router::new()
         .route("/", get(handlers::index_handler))
         .route("/posts", get(handlers::posts_handler))
@@ -24,10 +42,18 @@ async fn main() {
         .route("/brochure", get(handlers::brochure_handler))
         .route("/about", get(handlers::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"));
+        .nest_service("/assets", ServeDir::new(assets_dir))
+        .nest_service("/team", ServeDir::new(tutor_dir))
+        .fallback_service(ServeDir::new("static"))
+        .layer(
+            TraceLayer::new_for_http()
+                .make_span_with(trace::DefaultMakeSpan::new()
+                    .level(Level::INFO))
+                .on_response(trace::DefaultOnResponse::new()
+                    .level(Level::INFO))
+        );
 
-    let listener = tokio::net::TcpListener::bind("0.0.0.0:8000").await.unwrap();
+    let addr = env::var("CT_BIND").unwrap_or("0.0.0.0:8000".into());
+    let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
     axum::serve(listener, app).await.unwrap();
 }
-- 
cgit v1.2.3


From 45a658e693ab9779ddd364d2acb651178db2dc99 Mon Sep 17 00:00:00 2001
From: "Adam T. Carpenter" <atc@53hor.net>
Date: Sat, 28 Sep 2024 09:39:00 -0400
Subject: fix: trailing slash route temp fix

---
 src/main.rs | 3 +++
 1 file changed, 3 insertions(+)

(limited to 'src')

diff --git a/src/main.rs b/src/main.rs
index 81c4427..b65a58b 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -39,8 +39,11 @@ async fn main() {
         .route("/posts/:post_id", get(handlers::post_handler))
         .with_state(posts)
         .route("/policies", get(handlers::policies_handler))
+        .route("/policies/", get(handlers::policies_handler))
         .route("/brochure", get(handlers::brochure_handler))
+        .route("/brochure/", get(handlers::brochure_handler))
         .route("/about", get(handlers::about_handler))
+        .route("/about/", get(handlers::about_handler))
         .with_state(tutors)
         .nest_service("/assets", ServeDir::new(assets_dir))
         .nest_service("/team", ServeDir::new(tutor_dir))
-- 
cgit v1.2.3


From d1c9f549d3a45118f0a563ddbe07c18fbc8ab660 Mon Sep 17 00:00:00 2001
From: "Adam T. Carpenter" <atc@53hor.net>
Date: Sat, 28 Sep 2024 09:55:53 -0400
Subject: fix: real fix for trailing backslash handling

---
 src/main.rs | 11 +++++------
 1 file changed, 5 insertions(+), 6 deletions(-)

(limited to 'src')

diff --git a/src/main.rs b/src/main.rs
index b65a58b..252588d 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,7 +1,8 @@
+use tower::Layer;
 use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
-use tower_http::trace::{self, TraceLayer};
+use tower_http::{trace::{self, TraceLayer}, normalize_path::NormalizePathLayer};
 use tracing::{info, Level};
-use axum::{routing::get, Router};
+use axum::{routing::get, Router, ServiceExt, extract::Request};
 use tutors::fs_tutor_repo::FsTutorRepo;
 use std::{sync::Arc, env};
 use tower_http::services::ServeDir;
@@ -39,11 +40,8 @@ async fn main() {
         .route("/posts/:post_id", get(handlers::post_handler))
         .with_state(posts)
         .route("/policies", get(handlers::policies_handler))
-        .route("/policies/", get(handlers::policies_handler))
         .route("/brochure", get(handlers::brochure_handler))
-        .route("/brochure/", get(handlers::brochure_handler))
         .route("/about", get(handlers::about_handler))
-        .route("/about/", get(handlers::about_handler))
         .with_state(tutors)
         .nest_service("/assets", ServeDir::new(assets_dir))
         .nest_service("/team", ServeDir::new(tutor_dir))
@@ -55,8 +53,9 @@ async fn main() {
                 .on_response(trace::DefaultOnResponse::new()
                     .level(Level::INFO))
         );
+    let app = NormalizePathLayer::trim_trailing_slash().layer(app);
 
     let addr = env::var("CT_BIND").unwrap_or("0.0.0.0:8000".into());
     let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
-    axum::serve(listener, app).await.unwrap();
+    axum::serve(listener, ServiceExt::<Request>::into_make_service(app)).await.unwrap();
 }
-- 
cgit v1.2.3