From 8b7f883f1460524f4f16d918b173b2a0a5766a74 Mon Sep 17 00:00:00 2001 From: Priec Date: Fri, 15 May 2026 22:31:26 +0200 Subject: [PATCH] what does loco offers --- ht_booking/assets/views/base.html | 65 +++++++ ht_booking/assets/views/court/create.html | 37 ++++ ht_booking/assets/views/court/edit.html | 42 ++++ ht_booking/assets/views/court/list.html | 86 +++++++++ ht_booking/assets/views/court/show.html | 22 +++ ht_booking/assets/views/home/home.html | 181 ++++++++++++++++++ ht_booking/migration/src/lib.rs | 4 +- .../migration/src/m20260515_162423_courts.rs | 27 +++ ht_booking/src/app.rs | 4 +- ht_booking/src/controllers/court.rs | 117 +++++++++++ ht_booking/src/controllers/home.rs | 16 ++ ht_booking/src/controllers/mod.rs | 3 + ht_booking/src/models/_entities/courts.rs | 19 ++ ht_booking/src/models/_entities/mod.rs | 4 +- ht_booking/src/models/_entities/prelude.rs | 4 +- ht_booking/src/models/_entities/users.rs | 2 +- ht_booking/src/models/courts.rs | 28 +++ ht_booking/src/models/mod.rs | 1 + ht_booking/src/views/court.rs | 39 ++++ ht_booking/src/views/mod.rs | 2 + ht_booking/tests/models/courts.rs | 31 +++ ht_booking/tests/models/mod.rs | 2 + 22 files changed, 731 insertions(+), 5 deletions(-) create mode 100644 ht_booking/assets/views/base.html create mode 100644 ht_booking/assets/views/court/create.html create mode 100644 ht_booking/assets/views/court/edit.html create mode 100644 ht_booking/assets/views/court/list.html create mode 100644 ht_booking/assets/views/court/show.html create mode 100644 ht_booking/assets/views/home/home.html create mode 100644 ht_booking/migration/src/m20260515_162423_courts.rs create mode 100644 ht_booking/src/controllers/court.rs create mode 100644 ht_booking/src/controllers/home.rs create mode 100644 ht_booking/src/models/_entities/courts.rs create mode 100644 ht_booking/src/models/courts.rs create mode 100644 ht_booking/src/views/court.rs create mode 100644 ht_booking/tests/models/courts.rs diff --git a/ht_booking/assets/views/base.html b/ht_booking/assets/views/base.html new file mode 100644 index 0000000..1c28e1f --- /dev/null +++ b/ht_booking/assets/views/base.html @@ -0,0 +1,65 @@ + + + + + + + + {% block title %}{% endblock title %} + + {% block head %} + + {% endblock head %} + + + +
+
+
+
+

+ {% block page_title %}{% endblock page_title %} +

+ {% block content %} + {% endblock content %} +
+
+
+
+ {% block js %} + + {% endblock js %} + + + + + \ No newline at end of file diff --git a/ht_booking/assets/views/court/create.html b/ht_booking/assets/views/court/create.html new file mode 100644 index 0000000..c24d590 --- /dev/null +++ b/ht_booking/assets/views/court/create.html @@ -0,0 +1,37 @@ +{% extends "base.html" %} + +{% block title %} +Create court +{% endblock title %} + +{% block page_title %} +Create new court +{% endblock page_title %} + +{% block content %} +
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+Back to courts +
+{% endblock content %} + +{% block js %} + +{% endblock js %} \ No newline at end of file diff --git a/ht_booking/assets/views/court/edit.html b/ht_booking/assets/views/court/edit.html new file mode 100644 index 0000000..5894cce --- /dev/null +++ b/ht_booking/assets/views/court/edit.html @@ -0,0 +1,42 @@ +{% extends "base.html" %} + +{% block title %} +Edit court: {{ item.id }} +{% endblock title %} + +{% block page_title %} +Edit court: {{ item.id }} +{% endblock page_title %} + +{% block content %} +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+
+ Back to court +
+{% endblock content %} + +{% block js %} + +{% endblock js %} \ No newline at end of file diff --git a/ht_booking/assets/views/court/list.html b/ht_booking/assets/views/court/list.html new file mode 100644 index 0000000..02c5640 --- /dev/null +++ b/ht_booking/assets/views/court/list.html @@ -0,0 +1,86 @@ +{% extends "base.html" %} + +{% block title %} +List of court +{% endblock title %} + +{% block page_title %} +court +{% endblock page_title %} + +{% block content %} +
+ + {% if items %} + +
+
+ + + + + + + + + + + {% for item in items %} + + + + + + + {% endfor %} + +
+ {{"name" | capitalize }} + + {{"surface" | capitalize }} + + {{"indoor" | capitalize }} + + Actions +
+ {{item.name | escape }} + + {{item.surface | escape }} + + {{ item.indoor }} + + Edit +
+
+ +
+ +
+
+ + {% else %} + +
+
+

Nothing Here Yet

+ There are no records to display. Add a new record to get started! + + Create + +
+
+ + {% endif %} + + +
+{% endblock content %} \ No newline at end of file diff --git a/ht_booking/assets/views/court/show.html b/ht_booking/assets/views/court/show.html new file mode 100644 index 0000000..62fb0b9 --- /dev/null +++ b/ht_booking/assets/views/court/show.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} + +{% block title %} +View court: {{ item.id }} +{% endblock title %} + +{% block content %} +

View court: {{ item.id }}

+
+
+ +
+
+ +
+
+ +
+
+Back to courts +
+{% endblock content %} \ No newline at end of file diff --git a/ht_booking/assets/views/home/home.html b/ht_booking/assets/views/home/home.html new file mode 100644 index 0000000..d0f9f16 --- /dev/null +++ b/ht_booking/assets/views/home/home.html @@ -0,0 +1,181 @@ +{% extends "base.html" %} + +{% block title %}ht_booking · Loco SaaS starter{% endblock title %} + +{% block page_title %}What this Loco app gives you out of the box{% endblock page_title %} + +{% block content %} +

+ This page replaces the default Loco fallback screen. Every model, controller, + migration and route below was produced by cargo loco generate — + nothing here is hand-written boilerplate. +

+ + +
+

1 · Authentication — JWT API

+

+ The SaaS starter ships a complete JWT auth flow in src/controllers/auth.rs. + It is a JSON API (no HTML login pages) — so here is a live console that calls those exact endpoints. +

+ +
+ + + + + + + + + + + + + + + + + + + + +
MethodPathJSON bodyPurpose
POST/api/auth/registername, email, passwordcreate an account
POST/api/auth/loginemail, passwordreturns a JWT token
GET/api/auth/current— (Bearer token)info about the logged-in user
POST/api/auth/forgotemailstart password reset
POST/api/auth/resettoken, passwordset a new password
GET/api/auth/verify/{token}verify an email address
POST/api/auth/magic-linkemailemail a passwordless login link
GET/api/auth/magic-link/{token}exchange the link for a JWT
POST/api/auth/resend-verification-mailemailresend the verification mail
+
+ +
+
+

Register

+ + + + +

+    
+ +
+

Login

+ + + +

+    
+ +
+

Current user

+

Sends the JWT from a successful login as a Bearer token.

+ +

+    
+
+ +

+ Active JWT: +

+
+ + +
+

2 · Database CRUD — Courts

+

+ A model, SeaORM entity, migration, controller and Tera views, all generated with one command: +

+
cargo loco generate scaffold court name:string surface:string indoor:bool --html
+ Open /courts → +
+ + +
+

3 · Health & ops endpoints

+

Built-in, no code needed:

+ +
+ + +
+

4 · Server-side views & i18n

+

+ Tera templates plus Fluent translations. The same key hello-world, + resolved by the t() function in this template: +

+ +
+ + +
+

5 · Also wired up (CLI-generated when you need them)

+ +
+{% endblock content %} + +{% block js %} +{% raw %} + +{% endraw %} +{% endblock js %} diff --git a/ht_booking/migration/src/lib.rs b/ht_booking/migration/src/lib.rs index 86c1ae7..010da00 100644 --- a/ht_booking/migration/src/lib.rs +++ b/ht_booking/migration/src/lib.rs @@ -3,6 +3,7 @@ pub use sea_orm_migration::prelude::*; mod m20220101_000001_users; +mod m20260515_162423_courts; pub struct Migrator; #[async_trait::async_trait] @@ -10,7 +11,8 @@ impl MigratorTrait for Migrator { fn migrations() -> Vec> { vec![ Box::new(m20220101_000001_users::Migration), + Box::new(m20260515_162423_courts::Migration), // inject-above (do not remove this comment) ] } -} +} \ No newline at end of file diff --git a/ht_booking/migration/src/m20260515_162423_courts.rs b/ht_booking/migration/src/m20260515_162423_courts.rs new file mode 100644 index 0000000..f8c5d14 --- /dev/null +++ b/ht_booking/migration/src/m20260515_162423_courts.rs @@ -0,0 +1,27 @@ +use loco_rs::schema::*; +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> { + create_table(m, "courts", + &[ + + ("id", ColType::PkAuto), + + ("name", ColType::StringNull), + ("surface", ColType::StringNull), + ("indoor", ColType::BooleanNull), + ], + &[ + ] + ).await + } + + async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> { + drop_table(m, "courts").await + } +} diff --git a/ht_booking/src/app.rs b/ht_booking/src/app.rs index 29d5f00..741e6ad 100644 --- a/ht_booking/src/app.rs +++ b/ht_booking/src/app.rs @@ -51,6 +51,8 @@ impl Hooks for App { fn routes(_ctx: &AppContext) -> AppRoutes { AppRoutes::with_default_routes() // controller routes below + .add_route(controllers::home::routes()) + .add_route(controllers::court::routes()) .add_route(controllers::auth::routes()) } async fn connect_workers(ctx: &AppContext, queue: &Queue) -> Result<()> { @@ -71,4 +73,4 @@ impl Hooks for App { .await?; Ok(()) } -} +} \ No newline at end of file diff --git a/ht_booking/src/controllers/court.rs b/ht_booking/src/controllers/court.rs new file mode 100644 index 0000000..bb4c3d7 --- /dev/null +++ b/ht_booking/src/controllers/court.rs @@ -0,0 +1,117 @@ +#![allow(clippy::missing_errors_doc)] +#![allow(clippy::unnecessary_struct_initialization)] +#![allow(clippy::unused_async)] +use loco_rs::prelude::*; +use serde::{Deserialize, Serialize}; +use axum::response::Redirect; +use axum_extra::extract::Form; +use sea_orm::{sea_query::Order, QueryOrder}; + +use crate::{ + models::_entities::courts::{ActiveModel, Column, Entity, Model}, + views, +}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Params { + pub name: Option, + pub surface: Option, + pub indoor: Option, + } + +impl Params { + fn update(&self, item: &mut ActiveModel) { + item.name = Set(self.name.clone()); + item.surface = Set(self.surface.clone()); + item.indoor = Set(self.indoor); + } +} + +async fn load_item(ctx: &AppContext, id: i32) -> Result { + let item = Entity::find_by_id(id).one(&ctx.db).await?; + item.ok_or_else(|| Error::NotFound) +} + +#[debug_handler] +pub async fn list( + ViewEngine(v): ViewEngine, + State(ctx): State, +) -> Result { + let item = Entity::find() + .order_by(Column::Id, Order::Desc) + .all(&ctx.db) + .await?; + views::court::list(&v, &item) +} + +#[debug_handler] +pub async fn new( + ViewEngine(v): ViewEngine, + State(_ctx): State, +) -> Result { + views::court::create(&v) +} + +#[debug_handler] +pub async fn update( + Path(id): Path, + State(ctx): State, + Form(params): Form, +) -> Result { + let item = load_item(&ctx, id).await?; + let mut item = item.into_active_model(); + params.update(&mut item); + item.update(&ctx.db).await?; + Ok(Redirect::to("../courts")) +} + +#[debug_handler] +pub async fn edit( + Path(id): Path, + ViewEngine(v): ViewEngine, + State(ctx): State, +) -> Result { + let item = load_item(&ctx, id).await?; + views::court::edit(&v, &item) +} + +#[debug_handler] +pub async fn show( + Path(id): Path, + ViewEngine(v): ViewEngine, + State(ctx): State, +) -> Result { + let item = load_item(&ctx, id).await?; + views::court::show(&v, &item) +} + +#[debug_handler] +pub async fn add( + State(ctx): State, + Form(params): Form, +) -> Result { + let mut item = ActiveModel { + ..Default::default() + }; + params.update(&mut item); + item.insert(&ctx.db).await?; + Ok(Redirect::to("courts")) +} + +#[debug_handler] +pub async fn remove(Path(id): Path, State(ctx): State) -> Result { + load_item(&ctx, id).await?.delete(&ctx.db).await?; + format::empty() +} + +pub fn routes() -> Routes { + Routes::new() + .prefix("courts/") + .add("/", get(list)) + .add("/", post(add)) + .add("new", get(new)) + .add("{id}", get(show)) + .add("{id}/edit", get(edit)) + .add("{id}", delete(remove)) + .add("{id}", post(update)) +} diff --git a/ht_booking/src/controllers/home.rs b/ht_booking/src/controllers/home.rs new file mode 100644 index 0000000..b3b53cc --- /dev/null +++ b/ht_booking/src/controllers/home.rs @@ -0,0 +1,16 @@ +#![allow(clippy::missing_errors_doc)] +#![allow(clippy::unnecessary_struct_initialization)] +#![allow(clippy::unused_async)] +use loco_rs::prelude::*; + +#[debug_handler] +pub async fn home( + ViewEngine(v): ViewEngine, + State(_ctx): State +) -> Result { + format::render().view(&v, "home/home.html", data!({})) +} + +pub fn routes() -> Routes { + Routes::new().add("/", get(home)) +} diff --git a/ht_booking/src/controllers/mod.rs b/ht_booking/src/controllers/mod.rs index 0e4a05d..d2d5814 100644 --- a/ht_booking/src/controllers/mod.rs +++ b/ht_booking/src/controllers/mod.rs @@ -1 +1,4 @@ pub mod auth; + +pub mod court; +pub mod home; \ No newline at end of file diff --git a/ht_booking/src/models/_entities/courts.rs b/ht_booking/src/models/_entities/courts.rs new file mode 100644 index 0000000..eb2d77a --- /dev/null +++ b/ht_booking/src/models/_entities/courts.rs @@ -0,0 +1,19 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.20 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "courts")] +pub struct Model { + pub created_at: DateTimeWithTimeZone, + pub updated_at: DateTimeWithTimeZone, + #[sea_orm(primary_key)] + pub id: i32, + pub name: Option, + pub surface: Option, + pub indoor: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} diff --git a/ht_booking/src/models/_entities/mod.rs b/ht_booking/src/models/_entities/mod.rs index 7efa3a0..bd97f98 100644 --- a/ht_booking/src/models/_entities/mod.rs +++ b/ht_booking/src/models/_entities/mod.rs @@ -1,4 +1,6 @@ -//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0 +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.20 pub mod prelude; + +pub mod courts; pub mod users; diff --git a/ht_booking/src/models/_entities/prelude.rs b/ht_booking/src/models/_entities/prelude.rs index 1055169..adab66a 100644 --- a/ht_booking/src/models/_entities/prelude.rs +++ b/ht_booking/src/models/_entities/prelude.rs @@ -1,2 +1,4 @@ -//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0 +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.20 + +pub use super::courts::Entity as Courts; pub use super::users::Entity as Users; diff --git a/ht_booking/src/models/_entities/users.rs b/ht_booking/src/models/_entities/users.rs index 765e992..b29e646 100644 --- a/ht_booking/src/models/_entities/users.rs +++ b/ht_booking/src/models/_entities/users.rs @@ -1,4 +1,4 @@ -//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0 +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.20 use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; diff --git a/ht_booking/src/models/courts.rs b/ht_booking/src/models/courts.rs new file mode 100644 index 0000000..7cd8ed9 --- /dev/null +++ b/ht_booking/src/models/courts.rs @@ -0,0 +1,28 @@ +use sea_orm::entity::prelude::*; +pub use super::_entities::courts::{ActiveModel, Model, Entity}; +pub type Courts = Entity; + +#[async_trait::async_trait] +impl ActiveModelBehavior for ActiveModel { + async fn before_save(self, _db: &C, insert: bool) -> std::result::Result + where + C: ConnectionTrait, + { + if !insert && self.updated_at.is_unchanged() { + let mut this = self; + this.updated_at = sea_orm::ActiveValue::Set(chrono::Utc::now().into()); + Ok(this) + } else { + Ok(self) + } + } +} + +// implement your read-oriented logic here +impl Model {} + +// implement your write-oriented logic here +impl ActiveModel {} + +// implement your custom finders, selectors oriented logic here +impl Entity {} diff --git a/ht_booking/src/models/mod.rs b/ht_booking/src/models/mod.rs index 48da463..6fc9ff4 100644 --- a/ht_booking/src/models/mod.rs +++ b/ht_booking/src/models/mod.rs @@ -1,2 +1,3 @@ pub mod _entities; pub mod users; +pub mod courts; diff --git a/ht_booking/src/views/court.rs b/ht_booking/src/views/court.rs new file mode 100644 index 0000000..858003c --- /dev/null +++ b/ht_booking/src/views/court.rs @@ -0,0 +1,39 @@ +use loco_rs::prelude::*; + +use crate::models::_entities::courts; + +/// Render a list view of `courts`. +/// +/// # Errors +/// +/// When there is an issue with rendering the view. +pub fn list(v: &impl ViewRenderer, items: &Vec) -> Result { + format::render().view(v, "court/list.html", data!({"items": items})) +} + +/// Render a single `court` view. +/// +/// # Errors +/// +/// When there is an issue with rendering the view. +pub fn show(v: &impl ViewRenderer, item: &courts::Model) -> Result { + format::render().view(v, "court/show.html", data!({"item": item})) +} + +/// Render a `court` create form. +/// +/// # Errors +/// +/// When there is an issue with rendering the view. +pub fn create(v: &impl ViewRenderer) -> Result { + format::render().view(v, "court/create.html", data!({})) +} + +/// Render a `court` edit form. +/// +/// # Errors +/// +/// When there is an issue with rendering the view. +pub fn edit(v: &impl ViewRenderer, item: &courts::Model) -> Result { + format::render().view(v, "court/edit.html", data!({"item": item})) +} diff --git a/ht_booking/src/views/mod.rs b/ht_booking/src/views/mod.rs index 0e4a05d..5b21bf7 100644 --- a/ht_booking/src/views/mod.rs +++ b/ht_booking/src/views/mod.rs @@ -1 +1,3 @@ pub mod auth; + +pub mod court; \ No newline at end of file diff --git a/ht_booking/tests/models/courts.rs b/ht_booking/tests/models/courts.rs new file mode 100644 index 0000000..5c63cdf --- /dev/null +++ b/ht_booking/tests/models/courts.rs @@ -0,0 +1,31 @@ +use ht_booking::app::App; +use loco_rs::testing::prelude::*; +use serial_test::serial; + +macro_rules! configure_insta { + ($($expr:expr),*) => { + let mut settings = insta::Settings::clone_current(); + settings.set_prepend_module_to_snapshot(false); + let _guard = settings.bind_to_scope(); + }; +} + +#[tokio::test] +#[serial] +async fn test_model() { + configure_insta!(); + + let boot = boot_test::().await.unwrap(); + seed::(&boot.app_context).await.unwrap(); + + // query your model, e.g.: + // + // let item = models::posts::Model::find_by_pid( + // &boot.app_context.db, + // "11111111-1111-1111-1111-111111111111", + // ) + // .await; + + // snapshot the result: + // assert_debug_snapshot!(item); +} diff --git a/ht_booking/tests/models/mod.rs b/ht_booking/tests/models/mod.rs index 5975988..39bfff1 100644 --- a/ht_booking/tests/models/mod.rs +++ b/ht_booking/tests/models/mod.rs @@ -1 +1,3 @@ mod users; + +mod courts; \ No newline at end of file