diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/handlers.rs | 61 | ||||
-rw-r--r-- | src/helpers.rs | 5 | ||||
-rw-r--r-- | src/main.rs | 73 | ||||
-rw-r--r-- | src/middleware.rs | 1 | ||||
-rw-r--r-- | src/middleware/cache_control.rs | 15 | ||||
-rw-r--r-- | src/posts.rs | 3 | ||||
-rw-r--r-- | src/posts/abstractions.rs | 2 | ||||
-rw-r--r-- | src/posts/abstractions/post.rs | 7 | ||||
-rw-r--r-- | src/posts/abstractions/repo.rs | 6 | ||||
-rw-r--r-- | src/posts/fs_post.rs | 64 | ||||
-rw-r--r-- | src/posts/fs_post_repo.rs | 32 | ||||
-rw-r--r-- | src/tutors.rs | 3 | ||||
-rw-r--r-- | src/tutors/abstractions.rs | 2 | ||||
-rw-r--r-- | src/tutors/abstractions/tutor.rs | 7 | ||||
-rw-r--r-- | src/tutors/abstractions/tutor_repo.rs | 5 | ||||
-rw-r--r-- | src/tutors/fs_tutor.rs | 50 | ||||
-rw-r--r-- | src/tutors/fs_tutor_repo.rs | 29 | ||||
-rw-r--r-- | src/views.rs | 9 | ||||
-rw-r--r-- | src/views/about.rs | 17 | ||||
-rw-r--r-- | src/views/brochure.rs | 6 | ||||
-rw-r--r-- | src/views/highered.rs | 6 | ||||
-rw-r--r-- | src/views/index.rs | 6 | ||||
-rw-r--r-- | src/views/k12.rs | 6 | ||||
-rw-r--r-- | src/views/policies.rs | 6 | ||||
-rw-r--r-- | src/views/post.rs | 15 | ||||
-rw-r--r-- | src/views/posts.rs | 18 | ||||
-rw-r--r-- | src/views/pro.rs | 6 |
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; |