use std::{collections::HashMap, sync::Arc}; use axum::{ extract::{Path, Query, State}, http::StatusCode, response::{IntoResponse, Redirect, Response}, routing::{get, post}, Router, }; use data::{Data, Filters}; use maud::{html, Markup}; use nanoid::nanoid; use serde::Deserialize; use sqlx::{postgres::PgPoolOptions, Pool, Postgres}; use tower_http::{services::ServeDir, trace::TraceLayer}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; pub mod clock; pub mod data; pub mod db; pub mod forecast; pub mod templates; pub struct AppState { pub data: Data, pub pool: Pool, } pub struct AppError(anyhow::Error); impl IntoResponse for AppError { fn into_response(self) -> axum::response::Response { ( StatusCode::INTERNAL_SERVER_ERROR, format!("Something went wrong: {}", self.0), ) .into_response() } } impl From for AppError where E: Into, { fn from(err: E) -> Self { Self(err.into()) } } async fn root_handler() -> Result { Ok(templates::root()) } async fn new_account_handler(state: State>) -> Result { let id = nanoid!(10); db::insert_account(&id, &state.pool).await?; Ok(templates::new_account(id)) } #[derive(Deserialize)] pub struct FilterQuery { pub caught: Option, pub big: Option, pub patches: Option>, } async fn main_handler( state: State>, Path(acc_id): Path, axum_extra::extract::Query(query): axum_extra::extract::Query, ) -> Result { let exists = db::get_account(&acc_id, &state.pool).await?; if !exists { return Ok(html! { "The provided account ID doesn't eixst." }.into_response()); } let caught_fish = db::get_caught_fish(&acc_id, &state.pool).await?; let filters = Filters::from_query(query); Ok(templates::main_page(state, caught_fish, acc_id, &filters).into_response()) } async fn insert_cf_handler( state: State>, Path((acc_id, fish_id)): Path<(String, String)>, ) -> Result { let exists = db::get_account(&acc_id, &state.pool).await?; if !exists { return Ok(html! { "The provided account ID doesn't eixst." }.into_response()); } db::insert_caught_fish(&acc_id, &fish_id.parse::().unwrap(), &state.pool).await?; Ok(Redirect::to(&format!("/{}", acc_id)).into_response()) } async fn to_handler( state: State>, query: Query>, ) -> Result { let rejection_html = html! { p { "The provided account ID doesn't exist. " } }; if let Some(id) = query.get("id") { let exists = db::get_account(id, &state.pool).await?; if exists { Ok(Redirect::to(&format!("/{}", id)).into_response()) } else { Ok(rejection_html.into_response()) } } else { Ok(rejection_html.into_response()) } } #[tokio::main] async fn main() { #[cfg(debug_assertions)] dotenvy::dotenv().unwrap(); tracing_subscriber::registry() .with( tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| { format!( "{}=debug,tower_http=debug,axum::rejection=trace", env!("CARGO_CRATE_NAME") ) .into() }), ) .with(tracing_subscriber::fmt::layer()) .init(); let pool = PgPoolOptions::new() .max_connections(10) .connect(&std::env::var("DATABASE_URL").unwrap()) .await .unwrap(); sqlx::migrate!("./migrations").run(&pool).await.unwrap(); let app = Router::new() .route("/", get(root_handler).post(new_account_handler)) .route("/to", get(to_handler)) .route("/{id}", get(main_handler)) .route("/{acc_id}/catch/{fish_id}", post(insert_cf_handler)) .layer(TraceLayer::new_for_http()) .nest_service("/static", ServeDir::new("static")) .with_state(Arc::new(AppState { data: Data::new(), pool, })); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); println!("Listening on http://localhost:3000!"); axum::serve(listener, app).await.unwrap(); }