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;
async fn brochure_handler() -> Html<String> {
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();
}