Compare commits

..

No commits in common. "main" and "add-empty-state" have entirely different histories.

25 changed files with 68 additions and 258 deletions

2
.gitignore vendored
View File

@ -1,3 +1 @@
/target
.DS_Store

View File

@ -1,6 +1,5 @@
database_path = ""
locale = "fr"
base_url = ""
[listener]
port = 8000

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -1,13 +1,12 @@
_version: 1
name: "BookForge"
common:
download: Download
create: Create
edit: Edit
delete: Delete
next: Next
previous: Previous
previous: Prev
actions: Actions
search: Search
reset: Reset
@ -16,77 +15,54 @@ common:
close: Close
confirmation: Confirmation
are_you_sure: Are you sure?
nav:
toggle: Toggle navigation
books: Books
users: Users
theme:
light: Light
dark: Dark
user:
attributes:
name: Name
owner_books: Owned books
owner_books: Owner books
borrowed_books: Borrowed books
index:
title_tag: Users list | BookForge
title: All Users
button: Add a user
button: Add User
edit:
title_tag: Edit user | BookForge
title: Edit
button: Edit user
new:
title_tag: New user | BookForge
title: New user
button: Create user
title: New User
button: Create User
book:
attributes:
title: Title
authors: Author(s)
description: Description
owner: Owner
current_holder: Current holder
current_holder: Current Holder
comment: Comment
index:
title_tag: Books list | BookForge
title: All Books
new:
title_tag: New book | BookForge
title: New Book
button: Create book
button: Create Book
button_short: Add book
edit:
title_tag: Edit book | BookForge
title: Edit book
button: Edit book
title: Edit Book
button: Edit Book
show:
title_tag: Details | BookForge
book_details: Book details
user_details: User details
more_informations: More information
book_details: Book Details
user_details: User Details
more_informations: More Informations
footer:
message: Made with love & Fuck fascists!
message: Made with Love & Fuck a Fascist!
error:
error_404:
title_tag: Error 404 | BookForge
title: Oops! This page does not exist
title: Oops ! This page does not exist
subtitle: 404 NOT FOUND
button: Back to home
generic:
title_tag: Error | BookForge
title: Oops! An error occurred

View File

@ -28,17 +28,14 @@ user:
owner_books: Livres possédés
borrowed_books: Livres empruntés
index:
title_tag: Liste des utilisateur.ice.s | BookForge
title: Tous les utilisateur.ice.s
button: Ajouter un.e utilisateur.ice
title: Tous les utilisateurs
button: Ajouter un utilisateur
edit:
title_tag: Modifier l'utilisateur.ice | BookForge
title: Modifier
button: Modifier l'utilisateur.ice
button: Modifier l'utilisateur
new:
title_tag: Nouvel utilisateur.ice | BookForge
title: Nouvel utilisateur.ice
button: Créer l'utilisateur.ice
title: Nouvel utilisateur
button: Créer l'utilisateur
book:
attributes:
title: Titre
@ -48,30 +45,24 @@ book:
current_holder: Détenteur.ice actuel.le
comment: Commentaire
index:
title_tag: Liste des livres | BookForge
title: Tous les livres
new:
title_tag: Nouveau livre | BookForge
title: Nouveau livre
button: Créer le livre
button_short: Ajouter un livre
edit:
title_tag: Modifier le livre | BookForge
title: Modifier le livre
button: Modifier le livre
show:
title_tag: Details | BookForge
book_details: Détails du livre
user_details: Détails de l'utilisateur.ice
user_details: Détails de l'utilisateur
more_informations: Plus d'informations
footer:
message: Fait avec amour & Nique les fachos !
error:
error_404:
title_tag: Erreur 404 | BookForge
title: Oups ! Cette page n'existe pas
subtitle: 404 NOT FOUND
button: Retour à l'accueil
generic:
title_tag: Erreur | BookForge
title: Oups ! Une erreur s'est produite

View File

@ -2,12 +2,10 @@ use askama::Template;
use askama_web::WebTemplate;
use axum::{
Router,
extract::State,
routing::{get, post},
};
use static_serve::embed_assets;
use crate::routes::router::Router as InternalRouter;
use crate::state::AppState;
mod migrations;
@ -46,14 +44,8 @@ pub fn build_app(state: AppState) -> Router {
#[derive(Template, WebTemplate)]
#[template(path = "404.html")]
struct NotFoundTemplate {
pub router: InternalRouter,
}
struct NotFoundTemplate {}
pub async fn error_handler(State(state): State<AppState>) -> impl axum::response::IntoResponse {
NotFoundTemplate {
router: InternalRouter {
base_path: state.config.base_path,
},
}
pub async fn error_handler() -> impl axum::response::IntoResponse {
NotFoundTemplate {}
}

View File

@ -13,7 +13,7 @@ use serde::Deserialize;
use serde_with::{NoneAsEmptyString, serde_as};
use snafu::prelude::*;
use crate::{models::book::Model as BookModel, routes::router::Router, state::error::CSVSnafu};
use crate::{models::book::Model as BookModel, state::error::CSVSnafu};
use crate::{models::user::Model as UserModel, state::error::IOSnafu};
use crate::{
@ -55,7 +55,6 @@ struct BookIndexTemplate {
current_page: u64,
total_page: u64,
base_query: String,
router: Router,
}
pub async fn index(
@ -74,7 +73,7 @@ pub async fn index(
.context(UserSnafu)?;
// Get all Book filtered with query
let books_paginate = BookOperator::new(state.clone())
let books_paginate = BookOperator::new(state)
.all_paginate(page, Some(query.clone()))
.await
.context(BookSnafu)?;
@ -128,9 +127,6 @@ pub async fn index(
current_page: books_paginate.current_page,
total_page: books_paginate.total_page,
base_query,
router: Router {
base_path: state.config.base_path,
},
})
}
@ -140,7 +136,6 @@ struct ShowBookTemplate {
book: BookModel,
owner: UserModel,
current_holder: Option<UserModel>,
router: Router,
}
pub async fn show(
@ -173,9 +168,6 @@ pub async fn show(
book,
owner,
current_holder,
router: Router {
base_path: state.config.base_path,
},
})
}
@ -208,23 +200,14 @@ pub async fn create(
#[template(path = "books/new.html")]
struct NewBookTemplate {
users: Vec<UserModel>,
router: Router,
}
pub async fn new(
State(state): State<AppState>,
) -> Result<impl axum::response::IntoResponse, AppStateError> {
let users = UserOperator::new(state.clone())
.all()
.await
.context(UserSnafu)?;
let users = UserOperator::new(state).all().await.context(UserSnafu)?;
Ok(NewBookTemplate {
users,
router: Router {
base_path: state.config.base_path,
},
})
Ok(NewBookTemplate { users })
}
#[derive(Template, WebTemplate)]
@ -232,7 +215,6 @@ pub async fn new(
struct EditBookTemplate {
users: Vec<UserModel>,
book: BookModel,
router: Router,
}
pub async fn edit(
@ -243,18 +225,12 @@ pub async fn edit(
.all()
.await
.context(UserSnafu)?;
let book = BookOperator::new(state.clone())
let book = BookOperator::new(state)
.find_by_id(id)
.await
.context(BookSnafu)?;
Ok(EditBookTemplate {
users,
book,
router: Router {
base_path: state.config.base_path,
},
})
Ok(EditBookTemplate { users, book })
}
pub async fn update(

View File

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

View File

@ -1,54 +0,0 @@
#[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

@ -16,7 +16,6 @@ use crate::{
book::BookOperator,
user::{self, UserOperator},
},
routes::router::Router,
state::{
AppState,
error::{AppStateError, BookSnafu, UserSnafu},
@ -28,7 +27,6 @@ use crate::{
struct UsersIndexTemplate {
users_with_books_number: Vec<UserWithBookNumber>,
query: IndexQuery,
router: Router,
}
pub struct UserWithBookNumber {
@ -88,9 +86,6 @@ pub async fn index(
Ok(UsersIndexTemplate {
users_with_books_number: result,
query,
router: Router {
base_path: state.config.base_path,
},
})
}
@ -140,36 +135,24 @@ pub async fn delete(
#[template(path = "users/edit.html")]
struct EditTemplate {
user: user::Model,
router: Router,
}
pub async fn edit(
State(state): State<AppState>,
Path(id): Path<i32>,
) -> Result<impl axum::response::IntoResponse, AppStateError> {
let user = UserOperator::new(state.clone())
let user = UserOperator::new(state)
.find_by_id(id)
.await
.context(UserSnafu)?;
Ok(EditTemplate {
user,
router: Router {
base_path: state.config.base_path,
},
})
Ok(EditTemplate { user })
}
#[derive(Template, WebTemplate)]
#[template(path = "users/new.html")]
struct NewTemplate {
router: Router,
}
struct NewTemplate {}
pub async fn new(State(state): State<AppState>) -> impl axum::response::IntoResponse {
NewTemplate {
router: Router {
base_path: state.config.base_path,
},
}
pub async fn new() -> impl axum::response::IntoResponse {
NewTemplate {}
}

View File

@ -33,7 +33,6 @@ pub struct AppConfig {
#[serde(default = "AppConfig::default_sqlite_path")]
pub database_path: Utf8PathBuf,
pub locale: String,
pub base_path: String,
pub listener: Listener,
}
@ -41,7 +40,6 @@ impl Default for AppConfig {
fn default() -> Self {
AppConfig {
database_path: Self::default_sqlite_path(),
base_path: Self::default_base_path(),
locale: Self::default_locale(),
listener: Listener::default(),
}
@ -100,10 +98,6 @@ impl AppConfig {
"en".to_string()
}
fn default_base_path() -> String {
"".to_string()
}
pub fn default_sqlite_path() -> Utf8PathBuf {
Self::config_path().join("db.sqlite")
}

View File

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

View File

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

View File

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

View File

@ -2,15 +2,11 @@
{% import "components/typography.html" as typography %}
{% import "components/cards.html" as cards %}
{% block title %}
{{ t!("book.edit.title_tag") }}
{% endblock %}
{% block main %}
{{ typography::heading(t!("book.edit.title")) }}
{% call cards::card() %}
<form method="post" action="{{ router.update_book_path(&book.id) }}">
<form method="post" action="/books/{{ book.id }}">
<div class="mb-3">
<label class="form-label" for="title">{{ t!("book.attributes.title") }}</label>
<input type="text" name="title" class="form-control" value="{{ book.title }}" required>

View File

@ -3,15 +3,11 @@
{% import "components/cards.html" as cards %}
{% import "components/inputs.html" as form_helpers %}
{% block title %}
{{ t!("book.new.title_tag") }}
{% endblock %}
{% block main %}
{{ typography::heading(t!("book.new.title")) }}
{% call cards::card() %}
<form method="post" action="{{ router.create_book_path() }}">
<form method="post" action="/books">
{{ 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") }}

View File

@ -4,16 +4,12 @@
{% import "components/fields.html" as fields %}
{% import "components/cards.html" as cards %}
{% block title %}
{{ t!("book.show.title_tag") }}
{% endblock %}
{% block main %}
{{ typography::book_heading(book.title, book, show = false) }}
{% call cards::card() %}
<div class="">
<h5 class="fw-bold text-decoration-underline">{{ t!("book.show.book_details") }}</h5>
<div class="mt-4">
<h5 class="mt-4 fw-bold">{{ t!("book.show.book_details") }}</h5>
{{ fields::field(t!("book.attributes.title"), book.title) }}
{{ fields::field(t!("book.attributes.authors"), book.authors) }}
@ -24,7 +20,7 @@
{{ fields::field(t!("book.attributes.description"), "-") }}
{% endmatch %}
<h5 class="mt-50px fw-bold text-decoration-underline">{{ t!("book.show.user_details") }}</h5>
<h5 class="mt-50px fw-bold">{{ t!("book.show.user_details") }}</h5>
{{ fields::field(t!("book.attributes.owner"), owner.name) }}
{% match current_holder %}
@ -35,7 +31,7 @@
{% endmatch %}
<h5 class="mt-50px fw-bold text-decoration-underline">{{ t!("book.show.more_informations") }}</h5>
<h5 class="mt-50px fw-bold">{{ t!("book.show.more_informations") }}</h5>
{% match book.comment %}
{% when Some with (comment) %}
{{ fields::field(t!("book.attributes.comment"), comment) }}

View File

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

View File

@ -1,9 +1,4 @@
{% extends "base.html" %}
{% block title %}
{{ t!("error.generic.title_tag") }}
{% endblock %}
{% block main %}
<div class="mt-4 text-center">
<h1>{{ t!("error.generic.title") }}</h1>
@ -12,6 +7,6 @@
<p>{{ error }}</p>
{% endfor %}
</div>
<a href="{{ router.root_path() }}" class="mt-3 btn btn-info">{{ t!("error.error_404.button") }}</a>
<a href="/" class="mt-3 btn btn-info">{{ t!("error.error_404.button") }}</a>
</div>
{% endblock %}

View File

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

View File

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

View File

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

View File

@ -1,17 +1,13 @@
{% extends "base.html" %}
{% import "components/typography.html" as typography %}
{% import "components/cards.html" as cards %}
{% import "components/inputs.html" as form_helpers %}
{% block title %}
{{ t!("book.edit.title_tag") }}
{% endblock %}
o{% import "components/inputs.html" as form_helpers %}
{% block main %}
{{ typography::heading(t!("user.edit.title")) }}
{% call cards::card() %}
<form action="{{ router.update_user_path(&user.id) }}" method="post">
<form action="/users/{{ user.id }}" method="post">
<div class="row align-items-end">
<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) }}

View File

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

View File

@ -1,17 +1,13 @@
{% extends "base.html" %}
{% import "components/typography.html" as typography %}
{% import "components/cards.html" as cards %}
{% import "components/inputs.html" as form_helpers %}
{% block title %}
{{ t!("user.new.title_tag") }}
{% endblock %}
o{% import "components/inputs.html" as form_helpers %}
{% block main %}
{{ typography::heading(t!("user.new.title")) }}
{% call cards::card() %}
<form action="{{ router.create_user_path() }}" method="post">
<form action="/users" method="post">
<div class="row align-items-end">
<div class="col-md-10">
{{ form_helpers::input("name", t!("user.attributes.name"), is_required = true, placeholder = "Ex: Kropotkine", margin_bottom = false) }}