summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/handlers.rs61
-rw-r--r--src/helpers.rs5
-rw-r--r--src/main.rs73
-rw-r--r--src/middleware.rs1
-rw-r--r--src/middleware/cache_control.rs15
-rw-r--r--src/posts.rs3
-rw-r--r--src/posts/abstractions.rs2
-rw-r--r--src/posts/abstractions/post.rs7
-rw-r--r--src/posts/abstractions/repo.rs6
-rw-r--r--src/posts/fs_post.rs64
-rw-r--r--src/posts/fs_post_repo.rs32
-rw-r--r--src/tutors.rs3
-rw-r--r--src/tutors/abstractions.rs2
-rw-r--r--src/tutors/abstractions/tutor.rs7
-rw-r--r--src/tutors/abstractions/tutor_repo.rs5
-rw-r--r--src/tutors/fs_tutor.rs50
-rw-r--r--src/tutors/fs_tutor_repo.rs29
-rw-r--r--src/views.rs9
-rw-r--r--src/views/about.rs17
-rw-r--r--src/views/brochure.rs6
-rw-r--r--src/views/highered.rs6
-rw-r--r--src/views/index.rs6
-rw-r--r--src/views/k12.rs6
-rw-r--r--src/views/policies.rs6
-rw-r--r--src/views/post.rs15
-rw-r--r--src/views/posts.rs18
-rw-r--r--src/views/pro.rs6
27 files changed, 460 insertions, 0 deletions
diff --git a/src/handlers.rs b/src/handlers.rs
new file mode 100644
index 0000000..aa36e15
--- /dev/null
+++ b/src/handlers.rs
@@ -0,0 +1,61 @@
+use crate::posts::abstractions::repo::PostRepo;
+use crate::tutors::abstractions::tutor_repo::TutorRepo;
+use crate::views::about::AboutView;
+use crate::views::brochure::BrochureTemplate;
+use crate::views::highered::HigherEdTemplate;
+use crate::views::index::IndexTemplate;
+use crate::views::k12::K12Template;
+use crate::views::policies::PoliciesTemplate;
+use crate::views::post::PostView;
+use crate::views::posts::PostsView;
+use crate::views::pro::ProTemplate;
+use askama::Template;
+use axum::extract::{Path, State};
+use axum::response::{Html, Redirect};
+use std::sync::Arc;
+
+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())
+}
+
+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())
+}
+
+pub async fn post_redirect(Path(post_id): Path<String>) -> Redirect {
+ Redirect::permanent(&format!("/blog/{post_id}"))
+}
+
+pub async fn k12_handler() -> Html<String> {
+ Html(K12Template {}.render().unwrap())
+}
+
+pub async fn highered_handler() -> Html<String> {
+ Html(HigherEdTemplate {}.render().unwrap())
+}
+
+pub async fn pro_handler() -> Html<String> {
+ Html(ProTemplate {}.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
new file mode 100644
index 0000000..a9cd722
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,73 @@
+use axum::response::Redirect;
+use axum::{extract::Request, routing::get, Router, ServiceExt};
+use middleware::cache_control::cache_static;
+use posts::fs_post_repo::FsPostRepo;
+use std::{env, sync::Arc};
+use tower::Layer;
+use tower_http::services::ServeDir;
+use tower_http::{
+ normalize_path::NormalizePathLayer,
+ trace::{self, TraceLayer},
+};
+use tracing::{info, Level};
+use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
+use tutors::fs_tutor_repo::FsTutorRepo;
+
+mod handlers;
+mod helpers;
+mod middleware;
+mod posts;
+mod tutors;
+mod views;
+
+#[tokio::main]
+async fn main() {
+ 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_or(String::from("/var/ct/posts"));
+ let tutor_dir = env::var("CT_TEAM").unwrap_or(String::from("/var/ct/team"));
+ let assets_dir = env::var("CT_ASSETS").unwrap_or(String::from("/var/ct/assets"));
+
+ 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(|| async { Redirect::permanent("/blog") }))
+ .route("/posts/{post_id}", get(handlers::post_redirect))
+ .route("/blog", get(handlers::posts_handler))
+ .route("/blog/{post_id}", get(handlers::post_handler))
+ .with_state(posts)
+ .route("/policies", get(handlers::policies_handler))
+ .route("/brochure", get(handlers::brochure_handler))
+ .route("/about", get(handlers::about_handler))
+ .route("/k12", get(handlers::k12_handler))
+ .route("/highered", get(handlers::highered_handler))
+ .route("/professional", get(handlers::pro_handler))
+ .with_state(tutors)
+ .nest_service("/assets", ServeDir::new(assets_dir))
+ .nest_service("/team", ServeDir::new(tutor_dir))
+ .fallback_service(ServeDir::new("static"))
+ .layer(axum::middleware::from_fn(cache_static))
+ .layer(
+ TraceLayer::new_for_http()
+ .make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO))
+ .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, ServiceExt::<Request>::into_make_service(app))
+ .await
+ .unwrap();
+}
diff --git a/src/middleware.rs b/src/middleware.rs
new file mode 100644
index 0000000..a7d3c17
--- /dev/null
+++ b/src/middleware.rs
@@ -0,0 +1 @@
+pub mod cache_control;
diff --git a/src/middleware/cache_control.rs b/src/middleware/cache_control.rs
new file mode 100644
index 0000000..0f11924
--- /dev/null
+++ b/src/middleware/cache_control.rs
@@ -0,0 +1,15 @@
+use axum::extract::Request;
+use axum::http::header::{HeaderValue, CACHE_CONTROL};
+use axum::middleware::Next;
+use axum::response::Response;
+
+pub async fn cache_static(request: Request, next: Next) -> Response {
+ let was_static = request.uri().path().starts_with("/assets");
+ let mut response = next.run(request).await;
+
+ if was_static {
+ response.headers_mut().insert(CACHE_CONTROL, HeaderValue::from_static("max-age=3600"));
+ }
+
+ response
+}
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..9667828
--- /dev/null
+++ b/src/posts/abstractions/post.rs
@@ -0,0 +1,7 @@
+use std::{borrow::Cow, fmt};
+
+pub trait Post: fmt::Debug + Ord {
+ fn get_title(&self) -> &str;
+ fn get_article(&self) -> Cow<str>;
+ fn get_description(&self) -> Cow<str>;
+}
diff --git a/src/posts/abstractions/repo.rs b/src/posts/abstractions/repo.rs
new file mode 100644
index 0000000..6fd5d08
--- /dev/null
+++ b/src/posts/abstractions/repo.rs
@@ -0,0 +1,6 @@
+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
new file mode 100644
index 0000000..12ce538
--- /dev/null
+++ b/src/posts/fs_post.rs
@@ -0,0 +1,64 @@
+use comrak::Options;
+
+use crate::posts::abstractions::post::Post;
+use std::{borrow::Cow, fs, path::PathBuf};
+
+#[derive(Debug, Eq)]
+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_stem().unwrap().to_str().unwrap()
+ }
+
+ fn get_article(&self) -> Cow<str> {
+ let article = fs::read_to_string(&self.file).unwrap();
+ Cow::Owned(comrak::markdown_to_html(&article, &Options::default()))
+ }
+
+ fn get_description(&self) -> Cow<str> {
+ let article = self.get_article();
+ Cow::Owned(
+ article
+ .split_once('\n')
+ .map(|(first, _)| first)
+ .unwrap_or_default()
+ .trim_start_matches('_')
+ .trim_start_matches('*')
+ .trim_end_matches('_')
+ .trim_end_matches('*')
+ .to_owned(),
+ )
+ }
+}
+
+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/posts/fs_post_repo.rs b/src/posts/fs_post_repo.rs
new file mode 100644
index 0000000..13f797b
--- /dev/null
+++ b/src/posts/fs_post_repo.rs
@@ -0,0 +1,32 @@
+use crate::posts::abstractions::post::Post;
+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()))
+ }
+
+ 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/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..6b05fd8
--- /dev/null
+++ b/src/tutors/fs_tutor.rs
@@ -0,0 +1,50 @@
+use comrak::Options;
+
+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(comrak::markdown_to_html(&blurb, &Options::default()))
+ }
+}
+
+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..f880818
--- /dev/null
+++ b/src/views.rs
@@ -0,0 +1,9 @@
+pub mod about;
+pub mod brochure;
+pub mod highered;
+pub mod index;
+pub mod k12;
+pub mod policies;
+pub mod post;
+pub mod posts;
+pub mod pro;
diff --git a/src/views/about.rs b/src/views/about.rs
new file mode 100644
index 0000000..d2e0958
--- /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.j2")]
+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..fedf827
--- /dev/null
+++ b/src/views/brochure.rs
@@ -0,0 +1,6 @@
+use crate::helpers::*;
+use askama::Template;
+
+#[derive(Template)]
+#[template(path = "brochure/index.html.j2")]
+pub struct BrochureTemplate;
diff --git a/src/views/highered.rs b/src/views/highered.rs
new file mode 100644
index 0000000..108a76a
--- /dev/null
+++ b/src/views/highered.rs
@@ -0,0 +1,6 @@
+use crate::helpers::*;
+use askama::Template;
+
+#[derive(Template)]
+#[template(path = "highered.html.j2")]
+pub struct HigherEdTemplate;
diff --git a/src/views/index.rs b/src/views/index.rs
new file mode 100644
index 0000000..720749a
--- /dev/null
+++ b/src/views/index.rs
@@ -0,0 +1,6 @@
+use crate::helpers::*;
+use askama::Template;
+
+#[derive(Template)]
+#[template(path = "index.html.j2")]
+pub struct IndexTemplate;
diff --git a/src/views/k12.rs b/src/views/k12.rs
new file mode 100644
index 0000000..c7dc019
--- /dev/null
+++ b/src/views/k12.rs
@@ -0,0 +1,6 @@
+use crate::helpers::*;
+use askama::Template;
+
+#[derive(Template)]
+#[template(path = "k12.html.j2")]
+pub struct K12Template;
diff --git a/src/views/policies.rs b/src/views/policies.rs
new file mode 100644
index 0000000..4253e6c
--- /dev/null
+++ b/src/views/policies.rs
@@ -0,0 +1,6 @@
+use crate::helpers::*;
+use askama::Template;
+
+#[derive(Template)]
+#[template(path = "policies.html.j2")]
+pub struct PoliciesTemplate;
diff --git a/src/views/post.rs b/src/views/post.rs
new file mode 100644
index 0000000..03e5ef2
--- /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.j2")]
+pub struct PostView<P: Post> {
+ post: P,
+}
+
+impl<P: Post> PostView<P> {
+ pub fn with_post(post: P) -> Self {
+ Self { post }
+ }
+}
diff --git a/src/views/posts.rs b/src/views/posts.rs
new file mode 100644
index 0000000..5091059
--- /dev/null
+++ b/src/views/posts.rs
@@ -0,0 +1,18 @@
+use crate::helpers::*;
+use crate::posts::abstractions::post::Post;
+use askama::Template;
+
+#[derive(Template)]
+#[template(path = "posts.html.j2")]
+pub struct PostsView<P: Post> {
+ posts: Vec<P>,
+}
+
+impl<P: Post> PostsView<P> {
+ pub fn with_posts(posts: impl IntoIterator<Item = P>) -> Self {
+ let mut posts: Vec<P> = posts.into_iter().collect();
+ posts.sort();
+ posts.reverse();
+ Self { posts }
+ }
+}
diff --git a/src/views/pro.rs b/src/views/pro.rs
new file mode 100644
index 0000000..eacfd7c
--- /dev/null
+++ b/src/views/pro.rs
@@ -0,0 +1,6 @@
+use crate::helpers::*;
+use askama::Template;
+
+#[derive(Template)]
+#[template(path = "pro.html.j2")]
+pub struct ProTemplate;