Merge pull request 'Add router' (#19) from add-router-base-path into main

Reviewed-on: #19
This commit is contained in:
loube 2026-01-31 18:27:03 +01:00
commit 52e8abeeaa
19 changed files with 106 additions and 67 deletions

View File

@ -7,7 +7,8 @@ use axum::{
}; };
use static_serve::embed_assets; use static_serve::embed_assets;
use crate::{routes::template_ctx::TemplateCtx, state::AppState}; use crate::routes::router::Router as InternalRouter;
use crate::state::AppState;
mod migrations; mod migrations;
mod models; mod models;
@ -46,12 +47,12 @@ pub fn build_app(state: AppState) -> Router {
#[derive(Template, WebTemplate)] #[derive(Template, WebTemplate)]
#[template(path = "404.html")] #[template(path = "404.html")]
struct NotFoundTemplate { struct NotFoundTemplate {
pub ctx: TemplateCtx, pub router: InternalRouter,
} }
pub async fn error_handler(State(state): State<AppState>) -> impl axum::response::IntoResponse { pub async fn error_handler(State(state): State<AppState>) -> impl axum::response::IntoResponse {
NotFoundTemplate { NotFoundTemplate {
ctx: TemplateCtx { router: InternalRouter {
base_path: state.config.base_path, base_path: state.config.base_path,
}, },
} }

View File

@ -13,9 +13,7 @@ use serde::Deserialize;
use serde_with::{NoneAsEmptyString, serde_as}; use serde_with::{NoneAsEmptyString, serde_as};
use snafu::prelude::*; use snafu::prelude::*;
use crate::{ use crate::{models::book::Model as BookModel, routes::router::Router, state::error::CSVSnafu};
models::book::Model as BookModel, routes::template_ctx::TemplateCtx, state::error::CSVSnafu,
};
use crate::{models::user::Model as UserModel, state::error::IOSnafu}; use crate::{models::user::Model as UserModel, state::error::IOSnafu};
use crate::{ use crate::{
@ -57,7 +55,7 @@ struct BookIndexTemplate {
current_page: u64, current_page: u64,
total_page: u64, total_page: u64,
base_query: String, base_query: String,
ctx: TemplateCtx, router: Router,
} }
pub async fn index( pub async fn index(
@ -130,7 +128,7 @@ pub async fn index(
current_page: books_paginate.current_page, current_page: books_paginate.current_page,
total_page: books_paginate.total_page, total_page: books_paginate.total_page,
base_query, base_query,
ctx: TemplateCtx { router: Router {
base_path: state.config.base_path, base_path: state.config.base_path,
}, },
}) })
@ -142,7 +140,7 @@ struct ShowBookTemplate {
book: BookModel, book: BookModel,
owner: UserModel, owner: UserModel,
current_holder: Option<UserModel>, current_holder: Option<UserModel>,
ctx: TemplateCtx, router: Router,
} }
pub async fn show( pub async fn show(
@ -175,7 +173,7 @@ pub async fn show(
book, book,
owner, owner,
current_holder, current_holder,
ctx: TemplateCtx { router: Router {
base_path: state.config.base_path, base_path: state.config.base_path,
}, },
}) })
@ -210,7 +208,7 @@ pub async fn create(
#[template(path = "books/new.html")] #[template(path = "books/new.html")]
struct NewBookTemplate { struct NewBookTemplate {
users: Vec<UserModel>, users: Vec<UserModel>,
ctx: TemplateCtx, router: Router,
} }
pub async fn new( pub async fn new(
@ -223,7 +221,7 @@ pub async fn new(
Ok(NewBookTemplate { Ok(NewBookTemplate {
users, users,
ctx: TemplateCtx { router: Router {
base_path: state.config.base_path, base_path: state.config.base_path,
}, },
}) })
@ -234,7 +232,7 @@ pub async fn new(
struct EditBookTemplate { struct EditBookTemplate {
users: Vec<UserModel>, users: Vec<UserModel>,
book: BookModel, book: BookModel,
ctx: TemplateCtx, router: Router,
} }
pub async fn edit( pub async fn edit(
@ -253,7 +251,7 @@ pub async fn edit(
Ok(EditBookTemplate { Ok(EditBookTemplate {
users, users,
book, book,
ctx: TemplateCtx { router: Router {
base_path: state.config.base_path, base_path: state.config.base_path,
}, },
}) })

View File

@ -1,3 +1,3 @@
pub mod book; pub mod book;
pub mod template_ctx; pub mod router;
pub mod user; pub mod user;

54
src/routes/router.rs Normal file
View File

@ -0,0 +1,54 @@
#[derive(Clone)]
pub struct Router {
pub base_path: String,
}
impl Router {
pub fn assets(&self, path: &str) -> String {
if self.base_path.is_empty() || self.base_path == "/" {
format!("/{}", path)
} else {
format!("{}/{}", self.base_path.trim_end_matches('/'), path)
}
}
pub fn root_path(&self) -> String {
format!("{}/", &self.base_path)
}
// BOOKS ROUTES
pub fn new_book_path(&self) -> String {
format!("{}/books/new", &self.base_path)
}
pub fn create_book_path(&self) -> String {
format!("{}/books", &self.base_path)
}
pub fn update_book_path(&self, id: &i32) -> String {
format!("{}/books/{}", &self.base_path, id)
}
pub fn download_csv_book_path(&self) -> String {
format!("{}/books/download_csv", &self.base_path)
}
// USERS
pub fn index_user_path(&self) -> String {
format!("{}/users", &self.base_path)
}
pub fn new_user_path(&self) -> String {
format!("{}/users/new", &self.base_path)
}
pub fn create_user_path(&self) -> String {
format!("{}/users", &self.base_path)
}
pub fn update_user_path(&self, id: &i32) -> String {
format!("{}/users/{}", &self.base_path, id)
}
}

View File

@ -1,14 +0,0 @@
#[derive(Clone)]
pub struct TemplateCtx {
pub base_path: String,
}
impl TemplateCtx {
pub fn asset(&self, path: &str) -> String {
if self.base_path.is_empty() || self.base_path == "/" {
format!("/{}", path)
} else {
format!("{}/{}", self.base_path.trim_end_matches('/'), path)
}
}
}

View File

@ -16,7 +16,7 @@ use crate::{
book::BookOperator, book::BookOperator,
user::{self, UserOperator}, user::{self, UserOperator},
}, },
routes::template_ctx::TemplateCtx, routes::router::Router,
state::{ state::{
AppState, AppState,
error::{AppStateError, BookSnafu, UserSnafu}, error::{AppStateError, BookSnafu, UserSnafu},
@ -28,7 +28,7 @@ use crate::{
struct UsersIndexTemplate { struct UsersIndexTemplate {
users_with_books_number: Vec<UserWithBookNumber>, users_with_books_number: Vec<UserWithBookNumber>,
query: IndexQuery, query: IndexQuery,
ctx: TemplateCtx, router: Router,
} }
pub struct UserWithBookNumber { pub struct UserWithBookNumber {
@ -88,7 +88,7 @@ pub async fn index(
Ok(UsersIndexTemplate { Ok(UsersIndexTemplate {
users_with_books_number: result, users_with_books_number: result,
query, query,
ctx: TemplateCtx { router: Router {
base_path: state.config.base_path, base_path: state.config.base_path,
}, },
}) })
@ -140,7 +140,7 @@ pub async fn delete(
#[template(path = "users/edit.html")] #[template(path = "users/edit.html")]
struct EditTemplate { struct EditTemplate {
user: user::Model, user: user::Model,
ctx: TemplateCtx, router: Router,
} }
pub async fn edit( pub async fn edit(
@ -154,7 +154,7 @@ pub async fn edit(
Ok(EditTemplate { Ok(EditTemplate {
user, user,
ctx: TemplateCtx { router: Router {
base_path: state.config.base_path, base_path: state.config.base_path,
}, },
}) })
@ -163,12 +163,12 @@ pub async fn edit(
#[derive(Template, WebTemplate)] #[derive(Template, WebTemplate)]
#[template(path = "users/new.html")] #[template(path = "users/new.html")]
struct NewTemplate { struct NewTemplate {
ctx: TemplateCtx, router: Router,
} }
pub async fn new(State(state): State<AppState>) -> impl axum::response::IntoResponse { pub async fn new(State(state): State<AppState>) -> impl axum::response::IntoResponse {
NewTemplate { NewTemplate {
ctx: TemplateCtx { router: Router {
base_path: state.config.base_path, base_path: state.config.base_path,
}, },
} }

View File

@ -6,7 +6,7 @@ use snafu::prelude::*;
use crate::{ use crate::{
models::{book::BookError, user::UserError}, models::{book::BookError, user::UserError},
routes::template_ctx::TemplateCtx, routes::router::Router,
state::config::ConfigError, state::config::ConfigError,
}; };
@ -48,7 +48,7 @@ pub enum AppStateError {
#[template(path = "error.html")] #[template(path = "error.html")]
struct ErrorTemplate { struct ErrorTemplate {
state: AppStateErrorContext, state: AppStateErrorContext,
ctx: TemplateCtx, router: Router,
} }
struct AppStateErrorContext { struct AppStateErrorContext {
@ -68,7 +68,7 @@ impl IntoResponse for AppStateError {
let error_context = AppStateErrorContext::from(self); let error_context = AppStateErrorContext::from(self);
ErrorTemplate { ErrorTemplate {
state: error_context, state: error_context,
ctx: TemplateCtx { router: Router {
base_path: "".to_string(), base_path: "".to_string(),
}, },
} }

View File

@ -8,6 +8,6 @@
<div class="mt-4 text-center"> <div class="mt-4 text-center">
<h1>{{ t!("error.error_404.title") }}</h1> <h1>{{ t!("error.error_404.title") }}</h1>
<h2 class="fst-italic">{{ t!("error.error_404.subtitle") }}</h2> <h2 class="fst-italic">{{ t!("error.error_404.subtitle") }}</h2>
<a href="/" class="mt-3 btn btn-info">{{ t!("error.error_404.button") }}</a> <a href="{{ router.root_path() }}" class="mt-3 btn btn-info">{{ t!("error.error_404.button") }}</a>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -4,12 +4,12 @@
<meta charset="utf-8"> <meta charset="utf-8">
<title>{% block title %}{{ t!("name") }}{% endblock %}</title> <title>{% block title %}{{ t!("name") }}{% endblock %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href='{{ ctx.asset("assets/css/bootstrap.css") }}'> <link rel="stylesheet" href='{{ router.assets("assets/css/bootstrap.css") }}'>
<link rel="stylesheet" href='{{ ctx.asset("assets/css/main.css") }}'> <link rel="stylesheet" href='{{ router.assets("assets/css/main.css") }}'>
<link rel="icon" type="image/png" sizes="32x32" href="/assets/images/favicon.png"> <link rel="icon" type="image/png" sizes="32x32" href="/assets/images/favicon.png">
<link rel="stylesheet" href='{{ ctx.asset("assets/css/fork-awesome.min.css") }}'> <link rel="stylesheet" href='{{ router.assets("assets/css/fork-awesome.min.css") }}'>
<link rel="icon" type="image/x-icon" href='{{ ctx.asset("assets/images/favicon.ico") }}'> <link rel="icon" type="image/x-icon" href='{{ router.assets("assets/images/favicon.ico") }}'>
{% block extra_head %}{% endblock extra_head %} {% block extra_head %}{% endblock extra_head %}
</head> </head>
@ -26,7 +26,7 @@
{% endblock %} {% endblock %}
</footer> </footer>
<script src='{{ ctx.asset("assets/js/bootstrap.min.js") }}'></script> <script src='{{ router.assets("assets/js/bootstrap.min.js") }}'></script>
<script src='{{ ctx.asset("assets/js/script.js") }}'></script> <script src='{{ router.assets("assets/js/script.js") }}'></script>
</body> </body>
</html> </html>

View File

@ -10,7 +10,7 @@
{{ typography::heading(t!("book.edit.title")) }} {{ typography::heading(t!("book.edit.title")) }}
{% call cards::card() %} {% call cards::card() %}
<form method="post" action="/books/{{ book.id }}"> <form method="post" action="{{ router.update_book_path(&book.id) }}">
<div class="mb-3"> <div class="mb-3">
<label class="form-label" for="title">{{ t!("book.attributes.title") }}</label> <label class="form-label" for="title">{{ t!("book.attributes.title") }}</label>
<input type="text" name="title" class="form-control" value="{{ book.title }}" required> <input type="text" name="title" class="form-control" value="{{ book.title }}" required>

View File

@ -11,7 +11,7 @@
{{ typography::heading(t!("book.new.title")) }} {{ typography::heading(t!("book.new.title")) }}
{% call cards::card() %} {% call cards::card() %}
<form method="post" action="/books"> <form method="post" action="{{ router.create_book_path() }}">
{{ form_helpers::input("title", t!("book.attributes.title"), is_required = true, placeholder = "Ex: La Petite Dernière") }} {{ form_helpers::input("title", t!("book.attributes.title"), is_required = true, placeholder = "Ex: La Petite Dernière") }}
{{ form_helpers::input("authors", t!("book.attributes.authors"), is_required = true, placeholder = "Ex: Fatima Daas") }} {{ form_helpers::input("authors", t!("book.attributes.authors"), is_required = true, placeholder = "Ex: Fatima Daas") }}

View File

@ -18,9 +18,9 @@
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
{% if show %} {% if show %}
<li><a class="dropdown-item" href="/{{ sub_path }}/{{ book.id }}">{{ t!("common.show") }}</a></li> <li><a class="dropdown-item" href="{{ router.root_path() }}{{ sub_path }}/{{ book.id }}">{{ t!("common.show") }}</a></li>
{% endif %} {% endif %}
<li><a class="dropdown-item" href="/{{ sub_path }}/{{ book.id }}/edit">{{ t!("common.edit") }}</a></li> <li><a class="dropdown-item" href="{{ router.root_path() }}{{ sub_path }}/{{ book.id }}/edit">{{ t!("common.edit") }}</a></li>
<li> <li>
<a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#deleteUserModal{{ book.id }}">{{ t!("common.delete") }}</a> <a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#deleteUserModal{{ book.id }}">{{ t!("common.delete") }}</a>
</li> </li>
@ -40,7 +40,7 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ t!("common.close") }}</button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ t!("common.close") }}</button>
<form method="post" action="/{{ sub_path }}/{{ book.id }}/delete" class="m-0"> <form method="post" action="{{ router.root_path() }}{{ sub_path }}/{{ book.id }}/delete" class="m-0">
<input class="btn btn-danger" type="submit" value='{{ t!("common.delete") }}'> <input class="btn btn-danger" type="submit" value='{{ t!("common.delete") }}'>
</form> </form>
</div> </div>

View File

@ -12,6 +12,6 @@
<p>{{ error }}</p> <p>{{ error }}</p>
{% endfor %} {% endfor %}
</div> </div>
<a href="/" class="mt-3 btn btn-info">{{ t!("error.error_404.button") }}</a> <a href="{{ router.root_path() }}" class="mt-3 btn btn-info">{{ t!("error.error_404.button") }}</a>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -9,7 +9,7 @@
{% block main %} {% block main %}
{% call typography::heading(t!("book.index.title")) %} {% call typography::heading(t!("book.index.title")) %}
<a href="/books/download_csv?{{ base_query }}" class="btn btn-info"> <a href="{{ router.download_csv_book_path() }}?{{ base_query }}" class="btn btn-info">
<i class="fa fa-download me-2" aria-hidden="true"></i> {{ t!("common.download") }} (csv) <i class="fa fa-download me-2" aria-hidden="true"></i> {{ t!("common.download") }} (csv)
</a> </a>
{% endcall %} {% endcall %}
@ -87,7 +87,7 @@
</div> </div>
<div class="col-md-1 d-flex align-items-end"> <div class="col-md-1 d-flex align-items-end">
<a href="/" class="btn btn-light">{{ t!("common.reset") }}</a> <a href="{{ router.root_path() }}" class="btn btn-light">{{ t!("common.reset") }}</a>
</div> </div>
</div> </div>
</form> </form>
@ -98,7 +98,7 @@
<div class="d-flex flex-column align-items-center justify-content-center"> <div class="d-flex flex-column align-items-center justify-content-center">
<h2>{{ t!("common.no_result") }}</h2> <h2>{{ t!("common.no_result") }}</h2>
<a class="btn btn-success text-white text-nowrap mt-3" href="/books/new"> <a class="btn btn-success text-white text-nowrap mt-3" href="{{ router.new_book_path() }}">
{{ t!("book.new.button_short") }} {{ t!("book.new.button_short") }}
</a> </a>
</div> </div>
@ -142,19 +142,19 @@
<nav aria-label="Page navigation"> <nav aria-label="Page navigation">
<ul class="pagination"> <ul class="pagination">
<li class="page-item {% if current_page <= 1 %}disabled{% endif %}"> <li class="page-item {% if current_page <= 1 %}disabled{% endif %}">
<a class="page-link" href="/?{{ base_query }}page={% if current_page > 1 %}{{ current_page - 1 }}{% else %}1{% endif %}">{{ t!("common.prev") }}</a> <a class="page-link" href="{{ router.root_path() }}?{{ base_query }}page={% if current_page > 1 %}{{ current_page - 1 }}{% else %}1{% endif %}">{{ t!("common.previous") }}</a>
</li> </li>
{% for page in 1..(total_page + 1) %} {% for page in 1..(total_page + 1) %}
{% if page >= current_page - 1 && page <= current_page + 1 %} {% if page >= current_page - 1 && page <= current_page + 1 %}
<li class="page-item {% if page == current_page %}active{% endif %}"> <li class="page-item {% if page == current_page %}active{% endif %}">
<a class="page-link" href="/?{{ base_query }}page={{ page }}">{{ page }}</a> <a class="page-link" href="{{ router.root_path() }}?{{ base_query }}page={{ page }}">{{ page }}</a>
</li> </li>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<li class="page-item {% if current_page == total_page %}disabled{% endif %}"> <li class="page-item {% if current_page == total_page %}disabled{% endif %}">
<a class="page-link" href="/?{{ base_query }}page={{ current_page + 1 }}">{{ t!("common.next") }}</a> <a class="page-link" href="{{ router.root_path() }}?{{ base_query }}page={{ current_page + 1 }}">{{ t!("common.next") }}</a>
</li> </li>
</ul> </ul>
</nav> </nav>

View File

@ -2,4 +2,4 @@
<div class="d-flex flex-column flex-sm-row gap-0 gap-sm-3 justify-content-center"> <div class="d-flex flex-column flex-sm-row gap-0 gap-sm-3 justify-content-center">
<p>{{ t!("footer.message") }}</p> <p>{{ t!("footer.message") }}</p>
</div> </div>
</div> </div>

View File

@ -1,6 +1,6 @@
<nav class="navbar navbar-expand-lg bg-body-tertiary fixed-top shadow"> <nav class="navbar navbar-expand-lg bg-body-tertiary fixed-top shadow">
<div class="container"> <div class="container">
<a class="navbar-brand" href="/"> <a class="navbar-brand" href="{{ router.root_path() }}">
{{ t!("name") }} {{ t!("name") }}
</a> </a>
@ -12,10 +12,10 @@
<div class="collapse navbar-collapse" id="navbarNav"> <div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto"> <ul class="navbar-nav me-auto">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/">{{ t!("nav.books") }}</a> <a class="nav-link" href="{{ router.root_path() }}">{{ t!("nav.books") }}</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/users">{{ t!("nav.users") }}</a> <a class="nav-link" href="{{ router.index_user_path() }}">{{ t!("nav.users") }}</a>
</li> </li>
</ul> </ul>
<div class="d-flex align-items-center gap-2 py-3"> <div class="d-flex align-items-center gap-2 py-3">
@ -24,7 +24,7 @@
<option value="dark">{{ t!("theme.dark") }}</option> <option value="dark">{{ t!("theme.dark") }}</option>
</select> </select>
<a class="btn btn-success text-white text-nowrap" href="/books/new"> <a class="btn btn-success text-white text-nowrap" href="{{ router.new_book_path() }}">
{{ t!("book.new.button_short") }} {{ t!("book.new.button_short") }}
</a> </a>
</div> </div>

View File

@ -11,7 +11,7 @@
{{ typography::heading(t!("user.edit.title")) }} {{ typography::heading(t!("user.edit.title")) }}
{% call cards::card() %} {% call cards::card() %}
<form action="/users/{{ user.id }}" method="post"> <form action="{{ router.update_user_path(&user.id) }}" method="post">
<div class="row align-items-end"> <div class="row align-items-end">
<div class="col-md-10"> <div class="col-md-10">
{{ form_helpers::input("name", t!("user.attributes.name"), value = user.name, is_required = true, placeholder = "Ex: Kropotkine", margin_bottom = false) }} {{ form_helpers::input("name", t!("user.attributes.name"), value = user.name, is_required = true, placeholder = "Ex: Kropotkine", margin_bottom = false) }}

View File

@ -10,7 +10,7 @@
{% block main %} {% block main %}
{% call typography::heading(t!("user.index.title")) %} {% call typography::heading(t!("user.index.title")) %}
<a class="btn-success btn" href="/users/new">{{ t!("user.index.button") }}</a> <a class="btn-success btn" href="{{ router.new_user_path() }}">{{ t!("user.index.button") }}</a>
{% endcall %} {% endcall %}
{% call cards::card() %} {% call cards::card() %}
@ -42,7 +42,7 @@
<div class="d-flex flex-column align-items-center justify-content-center"> <div class="d-flex flex-column align-items-center justify-content-center">
<h2>{{ t!("common.no_result") }}</h2> <h2>{{ t!("common.no_result") }}</h2>
<a class="btn btn-success text-white text-nowrap mt-3" href="//new"> <a class="btn btn-success text-white text-nowrap mt-3" href="{{ router.new_user_path() }}">
{{ t!("user.index.button") }} {{ t!("user.index.button") }}
</a> </a>
</div> </div>

View File

@ -11,7 +11,7 @@
{{ typography::heading(t!("user.new.title")) }} {{ typography::heading(t!("user.new.title")) }}
{% call cards::card() %} {% call cards::card() %}
<form action="/users" method="post"> <form action="{{ router.create_user_path() }}" method="post">
<div class="row align-items-end"> <div class="row align-items-end">
<div class="col-md-10"> <div class="col-md-10">
{{ form_helpers::input("name", t!("user.attributes.name"), is_required = true, placeholder = "Ex: Kropotkine", margin_bottom = false) }} {{ form_helpers::input("name", t!("user.attributes.name"), is_required = true, placeholder = "Ex: Kropotkine", margin_bottom = false) }}