Compare commits

..

26 Commits

Author SHA1 Message Date
07eae87855
WIP 2024-09-24 21:06:14 +02:00
080537fe6d
feat: define structure 2024-09-24 21:06:09 +02:00
ecd84bf242
feat: add init and close library 2024-09-24 21:05:00 +02:00
981a5d34c5
feat: add first draft for crate public api 2024-09-24 21:05:00 +02:00
681cc7cb83
chore: add comment to indicate bindgen 2024-09-24 21:05:00 +02:00
fbc3be564f
feat: implement parsing to data structures 2024-09-24 21:05:00 +02:00
4871187726
chore: suppress dead code warning in bindings 2024-09-24 21:05:00 +02:00
67b482ff13
feat: implement deku binary reading 2024-09-24 21:05:00 +02:00
34bdea2269
WIP commit to set working base 2024-09-24 21:04:25 +02:00
20e16f6ae2
feat: test different implementations to parse memory 2024-09-24 21:04:25 +02:00
f42e38228a
chore: start implementing types from docs 2024-09-24 21:04:20 +02:00
3712667a04
chore: init sys crate 2024-09-24 21:03:14 +02:00
345190dfeb Merge pull request 'Ajout d'un parcours utilisateur⋅ice de connexion / déconnexion' (#67) from feat/61_add_login_workflow into main
Reviewed-on: #67
Reviewed-by: kosssi <simon@p4pillon.org>

### Détails

- Ajout d'une interface "de base", avec une navbar (supprimée par la #65)
- Ajout d'un bouton de connexion, ouvrant une modale
- Sélection de l'utilisateur⋅ice au click ou par raccourci clavier
- Usage réactif d'un état partagé entre les composants, pour stocker l'information de l'utilisateur⋅ice connecté⋅e
- Menu dropdown de "profil" & Déconnexion

![Peek 24-09-2024 01-03](/attachments/4ceda5b3-26d9-4022-8923-e65a08da8dcd)

# Compatibilité

Les choix d'implémentation des éléments "dynamiques" de l'interface (modale, dropdown), encouragés par la documentation de DaisyUI, s'appuient sur les dernières évolutions de la norme HTML : il n'y a donc aucun javascript pour les gérer, c'est fait nativement par le navigateur.

Il faudrait vérifier si les librairies et framework qu'on utilise implémentent ces fonctionnements en "polyfill" pour les anciens navigateurs. Si ce n'est pas le cas, il faudra définir si :
- on cherche des polyfills adaptés
- on laisse comme ça sans rétro-compatibilité (pas très "numérique responsable")
- on fallback sur des implémentations plus "traditionnelles" mais rétro-compatibles

Closes #61
2024-09-24 12:57:34 +02:00
5712d898a5 feat: Add a client-side only user selection interface 2024-09-24 12:56:50 +02:00
3bd0a02b62 feat: implement a simple navbar 2024-09-24 12:56:50 +02:00
167a1fbbc2
Merge pull request 'Refactoring de l'interface : migration d'un monolithe HTMx vers un client Nuxt + serveur Axum' (#66) from feat/65_move_out_htmx_with_axum_backend_and_nuxt_frontend into main
Reviewed-on: #66
Reviewed-by: kosssi <simon@p4pillon.org>

Implémentation des réflexions menées dans #65 :
- Suppression de la crate `app`, qui était un serveur axum exposant du HTMx, embedded dans le Tauri de la crate `desktop`
- Création d'un module Typescript, `frontend`, basé sur NuxtJS et générant une application statique
- Ré-écriture complète de la crate `desktop` pour encapsuler l'application statique générée par `frontend`
- Création d'une crate `backend`, serveur axum ayant pour objectif de servir de backend à l'interface, en particulier pour centraliser les accès à une base de donnée unique

- J'ai ré-utilisé TailwindCSS, mais au travers du module Nuxt dédié ; la génération est donc propre et automatisée, sans même nécessiter de configuration
- J'ai rajouté une "surcouche" à Tailwind, DaisyUI plutôt que de re-partir sur Flowbite ; ça fournit un ensemble de composants, mais de manière moins intrusive et "opinionated"

cf #65

- [Nuxt](https://nuxt.com/)
- [DaisyUI](https://daisyui.com/)
- [@nuxtjs/tailwindcss](https://tailwindcss.nuxtjs.org/)
2024-09-24 12:53:47 +02:00
f11e2502dd
feat: handle axum errors with anyhow 2024-09-23 18:56:17 +02:00
43bb2c40de
feat: improve README 2024-09-23 18:56:16 +02:00
54870b0d0f
feat: add the hot-reload on backend crate 2024-09-23 18:56:16 +02:00
a50d951af7
feat: setup a backend server with axum 2024-09-23 18:56:16 +02:00
2e057eee01
feat: add DaisyUI for easy components styling and dark mode handling 2024-09-23 18:56:16 +02:00
bc33bd48e8
feat: add a loader for SPA javascript loading 2024-09-23 18:56:16 +02:00
62decb3314
feat: setup tailwindcss in frontend 2024-09-23 18:56:16 +02:00
339377b838
fix: a Anyhow error handling is missing in ssvlib_demo 2024-09-23 18:56:16 +02:00
71ea6423bc
fix: invalid borrowing of assets_path in get_router 2024-09-23 18:56:16 +02:00
cad2390649
feat: replace desktop by a fresh Tauri install, and add a new frontend module using Nuxt 2024-09-23 18:56:16 +02:00
34 changed files with 1034 additions and 819 deletions

1138
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,9 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = [ members = [
"crates/app", "crates/backend",
"crates/sesam-vitale",
"crates/desktop", "crates/desktop",
"crates/sesam-vitale",
"crates/services-sesam-vitale-sys", "crates/services-sesam-vitale-sys",
"crates/utils", "crates/utils",
] ]

View File

@ -2,12 +2,14 @@
Logiciel de Pharmacie libre et open-source. Logiciel de Pharmacie libre et open-source.
## Crates ## Modules applicatifs
- `app`: Interface du logiciel, servie par un serveur web propulsé par Axum. Utilisable en mode endpoint ou encapsulé dans le client `desktop` - `crates`: Dossier racine des modules Rust
- `desktop`: Client desktop propulsé par Tauri, encapsulant le serveur web `app` - `crates/backend`: Serveur backend propulsé par Axum, exposant une API REST
- `sesam-vitale`: Bibliothèque de gestion des services SESAM-Vitale (Lecture des cartes CPS et Vitale, téléservices ...) - `crates/desktop`: Client desktop propulsé par Tauri, exposant le `frontend`
- `utils`: Bibliothèque de fonctions utilitaires - `crates/sesam-vitale`: Bibliothèque de gestion des services SESAM-Vitale (Lecture des cartes CPS et Vitale, téléservices ...)
- `crates/utils`: Bibliothèque de fonctions utilitaires
- `frontend`: Interface web du logiciel, propulsée par Nuxt.js
## Installation ## Installation
@ -26,57 +28,49 @@ Des exemples de fichiers de configuration sont disponibles à la racine du proje
### Pré-requis ### Pré-requis
#### Frontend (Nuxt + Typescript)
Le frontend est propulsé par Nuxt.js, un framework TypeScript pour Vue.js. Pour le développement, il est nécessaire d'installer les dépendances suivantes :
- [Bun](https://bun.sh/docs/installation), un gestionnaire de paquets, équivalent à `npm` en plus performant
#### Tauri CLI #### Tauri CLI
TODO: Tauri CLI, réellement nécessaire ?
La CLI Tauri est nécessaire au lancement du client `desktop`. Elle peut être installée via Cargo : La CLI Tauri est nécessaire au lancement du client `desktop`. Elle peut être installée via Cargo :
```bash ```bash
cargo install tauri-cli --version "^2.0.0-beta" cargo install tauri-cli --version "^2.0.0-rc"
``` ```
#### Tailwindcss CLI
Le CLI Tailwindcss est nécessaire pour la génération du fichier `crates/app/assets/css/style.css`.
La documentation d'installation est disponible sur le site officiel de Tailwindcss : https://tailwindcss.com/blog/standalone-cli
La version actuellement utilisée est la [`v3.4.7`](https://github.com/tailwindlabs/tailwindcss/releases/tag/v3.4.7)
#### SESAM-Vitale #### SESAM-Vitale
La crate `sesam-vitale` nécessite la présence des librairies dynamiques fournies par le package FSV et la CryptolibCPS. Les instructions d'installation sont disponibles dans le [README](crates/sesam-vitale/README.md) de la crate `sesam-vitale`. La crate `sesam-vitale` nécessite la présence des librairies dynamiques fournies par le package FSV et la CryptolibCPS. Les instructions d'installation sont disponibles dans le [README](crates/sesam-vitale/README.md) de la crate `sesam-vitale`.
#### Backend Hot-reload
Voir le [README](crates/backend/README.md) de la crate `backend` pour les prérequis de développement du serveur backend.
### Lancement ### Lancement
Le logiciel dans sa globalité peut être lancé via la commande suivante : Pour lancer l'application en mode développement, il est nécessaire d'exécuter plusieurs composants simultanément :
```bash ```bash
# Lancement du serveur backend
systemfd --no-pid -s http::3030 -- cargo watch -x 'run --bin backend'
```
```bash
# Lancement de l'interface utilisateur (frontend ou desktop)
# - frontend (serveur web, accessible via navigateur)
bun run --cwd frontend/ dev
# - desktop (client desktop, basé sur Tauri)
cargo tauri dev cargo tauri dev
``` ```
/!\ Attention, le lancement du client `desktop` ne génère pas le fichier `crates/app/assets/css/style.css` automatiquement pour le moment. En cas de modification des interfaces web, il est donc nécessaire de procéder à sa génération comme indiqué dans le [README](crates/app/README.md) de la crate `app`.
Si vous souhaitez lancer les composants séparément, les indications de lancement sont disponibles dans les README des différents crates.
- [app](crates/app/README.md)
- [sesam-vitale](crates/sesam-vitale/README.md)
## Rechargement automatique
Pour permettre de développer plus rapidement, il existe une librairie qui recompile automatiquement nos modifications en cours : [`cargo-watch`](https://github.com/watchexec/cargo-watch) permet de relancer une commande `cargo` lorsqu'un fichier est modifié (example: `cargo run` --> `cargo watch -x run`).
Voici la commande pour l'installer dans un _package_ :
```bash
cargo add cargo-watch --dev --package app
```
Le fichier [`.ignore`](./ignore) permet d'ignorer certains fichiers pour éviter de relancer la recompilation inutilement.
⚠️ La librairie n'est pas compatible avec _Windows 7_ et les versions antérieurs de _Windows_.
## Build ## Build
Packager le client desktop Pour packager le client `desktop`, il est nécessaire de faire appel à la CLI Tauri, qui se charge de gérer le build du `frontend` et son intégration au bundle :
```bash ```bash
cargo tauri build cargo tauri build

View File

@ -1,4 +1,4 @@
use std::path::Path; use std::path::PathBuf;
use axum::http::{StatusCode, Uri}; use axum::http::{StatusCode, Uri};
use axum_htmx::AutoVaryLayer; use axum_htmx::AutoVaryLayer;
@ -11,7 +11,7 @@ async fn fallback(uri: Uri) -> (StatusCode, String) {
(StatusCode::NOT_FOUND, format!("No route for {uri}")) (StatusCode::NOT_FOUND, format!("No route for {uri}"))
} }
pub fn get_router(assets_path: &Path) -> axum::Router { pub async fn get_router(assets_path: PathBuf) -> axum::Router<()> {
axum::Router::new() axum::Router::new()
.nest_service("/assets", ServeDir::new(assets_path)) .nest_service("/assets", ServeDir::new(assets_path))
.merge(pages::get_routes()) .merge(pages::get_routes())

View File

@ -72,7 +72,7 @@ async fn main() -> Result<(), AppError> {
let livereload_layer = let livereload_layer =
get_livereload_layer(templates_paths).map_err(AppError::NotifyWatcher)?; get_livereload_layer(templates_paths).map_err(AppError::NotifyWatcher)?;
let router = get_router(assets_path.as_path()).layer(livereload_layer); let router = get_router(assets_path).await.layer(livereload_layer);
let listener: TcpListener = get_tcp_listener().await.map_err(AppError::TCPListener)?; let listener: TcpListener = get_tcp_listener().await.map_err(AppError::TCPListener)?;
let local_addr = listener.local_addr().map_err(AppError::TCPListener)?; let local_addr = listener.local_addr().map_err(AppError::TCPListener)?;

14
crates/backend/Cargo.toml Normal file
View File

@ -0,0 +1,14 @@
[package]
name = "backend"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1.0.89"
axum = "0.7.6"
listenfd = "1.0.1"
tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread"] }
[dev-dependencies]
cargo-watch = "8.5.2"
systemfd = "0.4.3"

19
crates/backend/README.md Normal file
View File

@ -0,0 +1,19 @@
# Backend
Ceci est un serveur backend, basé sur axum, et permettant d'offrir une gestion centralisée des accès aux données.
## Prérequis
En développement, le mécanisme de hot-reload nécessite de disposer de `cargo-watch` et `systemfd`. Pour les installer, exécutez la commande suivante :
```bash
cargo install cargo-watch systemfd
```
## Développement
Pour lancer le serveur en mode développement, exécutez la commande suivante :
```bash
systemfd --no-pid -s http::3030 -- cargo watch -x 'run --bin backend'
```

37
crates/backend/src/lib.rs Normal file
View File

@ -0,0 +1,37 @@
use anyhow::Error as AnyError;
use axum::http::{StatusCode, Uri};
use axum::response::{IntoResponse, Response};
use axum::{routing::get, Router};
pub fn get_router() -> Router {
Router::new()
.route("/", get(|| async { "Hello, world!" }))
.fallback(fallback)
}
async fn fallback(uri: Uri) -> (StatusCode, String) {
(StatusCode::NOT_FOUND, format!("No route for {uri}"))
}
struct AppError(AnyError);
// To automatically convert `AppError` into a response
impl IntoResponse for AppError {
fn into_response(self) -> Response {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Internal Server Error: {}", self.0),
)
.into_response()
}
}
// To automatically convert `AnyError` into `AppError`
impl<E> From<E> for AppError
where
E: Into<AnyError>,
{
fn from(err: E) -> Self {
Self(err.into())
}
}

View File

@ -0,0 +1,24 @@
use listenfd::ListenFd;
use tokio::net::TcpListener;
use backend::get_router;
#[tokio::main]
async fn main() {
let app = get_router();
let mut listenfd = ListenFd::from_env();
let listener = match listenfd.take_tcp_listener(0).unwrap() {
// if we are given a tcp listener on listen fd 0, we use that one
Some(listener) => {
listener.set_nonblocking(true).unwrap();
TcpListener::from_std(listener).unwrap()
}
// otherwise fall back to local listening
None => TcpListener::bind("0.0.0.0:8080").await.unwrap(),
};
println!("Listening on {}", listener.local_addr().unwrap());
axum::serve(listener, app).await.unwrap();
}

View File

@ -10,16 +10,11 @@ name = "desktop_lib"
crate-type = ["lib", "cdylib", "staticlib"] crate-type = ["lib", "cdylib", "staticlib"]
[build-dependencies] [build-dependencies]
tauri-build = { version = "2.0.0-beta", features = [] } tauri-build = { version = "2.0.0-rc", features = [] }
[dependencies] [dependencies]
axum = "0.7.5" tauri = { version = "2.0.0-rc", features = [] }
tauri = { version = "2.0.0-beta", features = [] } tauri-plugin-shell = "2.0.0-rc"
tower = "0.4.13" serde = { version = "1", features = ["derive"] }
tokio = "1.39.1" serde_json = "1"
app = { path = "../app" }
http = "1.1.0"
bytes = "1.6.1"
thiserror = "1.0.63"

View File

@ -0,0 +1,10 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"shell:allow-open"
]
}

View File

@ -1,88 +1,14 @@
use axum::body::{to_bytes, Body}; // Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
use axum::Router; #[tauri::command]
use bytes::Bytes; fn greet(name: &str) -> String {
use http::{request, response, Request, Response}; format!("Hello, {}! You've been greeted from Rust!", name)
use std::path::PathBuf;
use std::sync::Arc;
use tauri::path::BaseDirectory;
use tauri::Manager;
use thiserror::Error;
use tokio::sync::{Mutex, MutexGuard};
use tower::{Service, ServiceExt};
#[derive(Error, Debug)]
pub enum DesktopError {
#[error("Axum error:\n{0}")]
Axum(#[from] axum::Error),
#[error("Infallible error")]
Infallible(#[from] std::convert::Infallible),
}
/// Process requests sent to Tauri (with the `axum://` protocol) and handle them with Axum
/// When an error occurs, this function is expected to panic, which should result in a 500 error
/// being sent to the client, so we let the client handle the error recovering
async fn process_tauri_request(
tauri_request: Request<Vec<u8>>,
mut router: MutexGuard<'_, Router>,
) -> Result<Response<Vec<u8>>, DesktopError> {
let (parts, body): (request::Parts, Vec<u8>) = tauri_request.into_parts();
let axum_request: Request<Body> = Request::from_parts(parts, body.into());
let axum_response: Response<Body> = router
.as_service()
.ready()
.await
.map_err(DesktopError::Infallible)?
.call(axum_request)
.await
.map_err(DesktopError::Infallible)?;
let (parts, body): (response::Parts, Body) = axum_response.into_parts();
let body: Bytes = to_bytes(body, usize::MAX).await?;
let tauri_response: Response<Vec<u8>> = Response::from_parts(parts, body.into());
Ok(tauri_response)
} }
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
tauri::Builder::default() tauri::Builder::default()
.setup(|app| { .plugin(tauri_plugin_shell::init())
let assets_path: PathBuf = app .invoke_handler(tauri::generate_handler![greet])
.path()
.resolve("assets", BaseDirectory::Resource)
.expect("Assets path should be resolvable");
// Adds Axum router to application state
// This makes it so we can retrieve it from any app instance (see bellow)
let router = Arc::new(Mutex::new(app::get_router(&assets_path)));
app.manage(router);
Ok(())
})
.register_asynchronous_uri_scheme_protocol("axum", move |app, request, responder| {
// Retrieve the router from the application state and clone it for the async block
let router = Arc::clone(&app.state::<Arc<Mutex<axum::Router>>>());
// Spawn a new async task to process the request
tauri::async_runtime::spawn(async move {
let router = router.lock().await;
match process_tauri_request(request, router).await {
Ok(response) => responder.respond(response),
Err(err) => {
let body = format!("Failed to process an axum:// request:\n{}", err);
responder.respond(
http::Response::builder()
.status(http::StatusCode::BAD_REQUEST)
.header(http::header::CONTENT_TYPE, "text/plain")
.body::<Vec<u8>>(body.into())
.expect("BAD_REQUEST response should be valid"),
)
}
}
});
})
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
} }

View File

@ -1,20 +1,24 @@
{ {
"productName": "Logiciel Pharma", "$schema": "https://schema.tauri.app/config/2.0.0-rc",
"productName": "Chrys4lide LGO",
"version": "0.0.1", "version": "0.0.1",
"identifier": "org.p4pillon.pharma.desktop", "identifier": "org.p4pillon.chrys4lide.lgo",
"build": { "build": {
"beforeDevCommand": { "beforeDevCommand": {
"cwd": "../app", "cwd": "../../frontend",
"script": "cargo run" "script": "bun run dev"
}, },
"devUrl": "http://localhost:3000", "devUrl": "http://localhost:1420",
"frontendDist": "axum://place.holder/" "beforeBuildCommand": {
"cwd": "../../frontend",
"script": "bun run generate"
},
"frontendDist": "../../frontend/dist"
}, },
"app": { "app": {
"withGlobalTauri": true,
"windows": [ "windows": [
{ {
"title": "Logiciel Pharma", "title": "Chrys4lide | LG0",
"width": 800, "width": 800,
"height": 600 "height": 600
} }
@ -25,9 +29,6 @@
}, },
"bundle": { "bundle": {
"active": true, "active": true,
"resources": {
"../app/assets/": "./assets/"
},
"targets": "all", "targets": "all",
"icon": [ "icon": [
"icons/32x32.png", "icons/32x32.png",
@ -37,5 +38,4 @@
"icons/icon.ico" "icons/icon.ico"
] ]
} }
} }

View File

@ -17,6 +17,8 @@ pub enum SSVDemoError {
CartePSReading(#[from] crate::cps::CartePSError), CartePSReading(#[from] crate::cps::CartePSError),
#[error(transparent)] #[error(transparent)]
SSVLibErrorCode(#[from] crate::libssv::LibSSVError), SSVLibErrorCode(#[from] crate::libssv::LibSSVError),
#[error(transparent)]
Anyhow(#[from] anyhow::Error),
} }
fn ssv_init_lib_2() -> Result<(), SSVDemoError> { fn ssv_init_lib_2() -> Result<(), SSVDemoError> {

24
frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example

77
frontend/README.md Normal file
View File

@ -0,0 +1,77 @@
# Nuxt 3 Minimal Starter
TODO : Faire un vrai README pour `frontend` (Nuxt 3)
Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
## Setup
Make sure to install the dependencies:
```bash
# npm
npm install
# pnpm
pnpm install
# yarn
yarn install
# bun
bun install
```
## Development Server
Start the development server on `http://localhost:3000`:
```bash
# npm
npm run dev
# pnpm
pnpm run dev
# yarn
yarn dev
# bun
bun run dev
```
## Production
Build the application for production:
```bash
# npm
npm run build
# pnpm
pnpm run build
# yarn
yarn build
# bun
bun run build
```
Locally preview production build:
```bash
# npm
npm run preview
# pnpm
pnpm run preview
# yarn
yarn preview
# bun
bun run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.

7
frontend/app.vue Normal file
View File

@ -0,0 +1,7 @@
<template>
<div>
<NuxtRouteAnnouncer />
<NavBar />
<NuxtPage />
</div>
</template>

View File

@ -0,0 +1,43 @@
<!--
This component is used to show a loading spinner when the SPA is loading.
Source: https://github.com/barelyhuman/snips/blob/dev/pages/css-loader.md
-->
<div class="loader"></div>
<style>
.loader {
display: block;
position: fixed;
z-index: 1031;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 18px;
height: 18px;
box-sizing: border-box;
border: solid 2px transparent;
border-top-color: #000;
border-left-color: #000;
border-bottom-color: #efefef;
border-right-color: #efefef;
border-radius: 50%;
-webkit-animation: loader 400ms linear infinite;
animation: loader 400ms linear infinite;
}
\@-webkit-keyframes loader {
0% {
-webkit-transform: translate(-50%, -50%) rotate(0deg);
}
100% {
-webkit-transform: translate(-50%, -50%) rotate(360deg);
}
}
\@keyframes loader {
0% {
transform: translate(-50%, -50%) rotate(0deg);
}
100% {
transform: translate(-50%, -50%) rotate(360deg);
}
}
</style>

BIN
frontend/bun.lockb Executable file

Binary file not shown.

View File

@ -0,0 +1,22 @@
<template>
<div class="avatar">
<div class="rounded-full">
<img :src="getAvatarUrl(user)" />
</div>
</div>
</template>
<script setup lang="ts">
import type { User } from '~/types/user';
const props = defineProps<{
user: User,
}>();
const getAvatarUrl = (user: User) => {
if (user.avatar) {
return user.avatar;
}
return 'https://avatar.iran.liara.run/username?username=' + user.name;
};
</script>

View File

@ -0,0 +1,71 @@
<template>
<dialog
id="login_modal"
ref="login_modal"
@cancel.prevent=""
@keyup="handleKeyPress"
class="modal"
>
<div class="modal-box">
<h3 class="text-3xl text-center mb-6">Connexion</h3>
<div class="flex flex-wrap gap-5 justify-center">
<template v-for="(user, index) in users" :key="user.id">
<LoginModalAvatar
:user="user"
:rank="index+1"
@selectUser="login"
/>
</template>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button class="cursor-default">close</button>
</form>
</dialog>
</template>
<script setup lang="ts">
import type { User } from '~/types/user';
const users: User[] = [
{ id: 1, name: 'John Doe', avatar: 'https://img.daisyui.com/images/stock/photo-1534528741775-53994a69daeb.webp' },
{ id: 2, name: 'Jane Doe', avatar: 'https://avatar.iran.liara.run/public' },
{ id: 3, name: 'Michel Moulin', avatar: '' },
{ id: 4, name: 'Jean Paris', avatar: '' },
{ id: 5, name: 'Marie Dupont', avatar: '' },
{ id: 6, name: 'Émilie Fournier', avatar: '' },
{ id: 7, name: 'Pierre Lefevre', avatar: '' },
{ id: 8, name: 'Sophie Lemoine', avatar: '' },
{ id: 9, name: 'Lucie Simon', avatar: '' },
{ id: 10, name: 'Kevin Boucher', avatar: '' },
];
const loginModal = useTemplateRef('login_modal');
const current_user = useCurrentUser();
const login = (user: User) => {
console.log('login', user);
current_user.value = user;
loginModal.value?.close();
};
const handleKeyPress = (event: KeyboardEvent) => {
// Extract the rank from the event.code : Digit7 -> 7
const rank = event.code.match(/\d/);
if (!rank) {
console.debug('Not handled key event', { event });
return;
}
const user = getUserByRank(parseInt(rank[0]));
if (user) {
login(user);
} else {
console.debug('Not handled key event', { event });
}
};
const getUserByRank = (rank: number): User => {
return users[rank - 1];
};
</script>

View File

@ -0,0 +1,17 @@
<template>
<button class="relative" @click="$emit('selectUser', user)">
<Avatar class="w-24" :user="user" />
<div class="absolute w-fit mx-auto bottom-0 inset-x-0">
<kbd class="kbd kbd-sm">{{ rank }}</kbd>
</div>
</button>
</template>
<script setup lang="ts">
import type { User } from '~/types/user';
const props = defineProps<{
user: User,
rank: Number,
}>();
</script>

View File

@ -0,0 +1,35 @@
<template>
<div class="navbar">
<div class="navbar-start">
<a class="btn btn-ghost text-xl" href="/">Chrys4lide</a>
</div>
<nav class="navbar-center">
<NuxtLink to="/" class="btn btn-ghost">Accueil</NuxtLink>
<NuxtLink to="/CPS" class="btn btn-ghost">Carte CPS</NuxtLink>
</nav>
<div class="navbar-end">
<template v-if="!current_user">
<button class="btn btn-ghost" type="button" onclick="login_modal.showModal()">
Connexion
</button>
</template>
<template v-else>
<details class="dropdown dropdown-end">
<summary class="block"><Avatar :user="current_user" class="w-12" role="button" /></summary>
<ul class="menu dropdown-content bg-base-100 rounded-box z-[1] w-52 p-2 shadow">
<li><a @click="logout">Déconnexion</a></li>
</ul>
</details>
</template>
</div>
</div>
<LoginModal />
</template>
<script setup lang="ts">
const current_user = useCurrentUser();
const logout = () => {
current_user.value = null;
};
</script>

View File

@ -0,0 +1,3 @@
import type { User } from '@/types/user';
export const useCurrentUser = () => useState<User | null>('currentUser', () => null);

39
frontend/nuxt.config.ts Normal file
View File

@ -0,0 +1,39 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2024-04-03',
// Enables the development server to be discoverable by other devices for mobile development
devServer: { host: '0.0.0.0', port: 1420 },
devtools: { enabled: true },
modules: [
'@nuxtjs/tailwindcss',
'@nuxtjs/color-mode',
],
// Disable SSR for Tauri
ssr: false,
vite: {
// Better support for Tauri CLI output
clearScreen: false,
// Enable environment variables
// Additional environment variables can be found at
// https://v2.tauri.app/reference/environment-variables/
envPrefix: ['VITE_', 'TAURI_'],
server: {
// Tauri requires a consistent port
strictPort: true,
hmr: {
// Use websocket for mobile hot reloading
protocol: 'ws',
// Make sure it's available on the network
host: '0.0.0.0',
// Use a specific port for hmr
port: 5183,
},
},
},
colorMode: {
// Add `data-theme` attribute to the `html` tag, allowing DaisyUI to handle dark mode automatically
dataValue: 'theme',
// Remove the default `-mode` suffix from the class name, letting have `dark` and `light` as class names, for DaisyUI compatibility
classSuffix: '',
},
})

22
frontend/package.json Normal file
View File

@ -0,0 +1,22 @@
{
"name": "nuxt-app",
"private": true,
"type": "module",
"scripts": {
"build": "nuxi build",
"dev": "nuxi dev",
"generate": "nuxi generate",
"preview": "nuxi preview",
"postinstall": "nuxi prepare"
},
"dependencies": {
"@nuxtjs/color-mode": "^3.5.1",
"daisyui": "^4.12.10",
"nuxt": "^3.13.0",
"vue": "latest",
"vue-router": "latest"
},
"devDependencies": {
"@nuxtjs/tailwindcss": "^6.12.1"
}
}

8
frontend/pages/CPS.vue Normal file
View File

@ -0,0 +1,8 @@
<template>
<div>
<h1 class="text-xl">Carte CPS</h1>
</div>
</template>
<script setup>
</script>

17
frontend/pages/index.vue Normal file
View File

@ -0,0 +1,17 @@
<template>
<div>
<h1 class="text-xl">Welcome to your {{ appName }}!</h1>
<p v-if="current_user">Logged in as {{ current_user.name }}</p>
</div>
</template>
<script setup lang="ts">
const current_user = useCurrentUser();
const appName = 'Nuxt App';
</script>
<style scoped>
h1 {
color: #42b983;
}
</style>

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,3 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}

View File

@ -0,0 +1,7 @@
import type { Config } from 'tailwindcss'
export default <Partial<Config>>{
plugins: [
require('daisyui'),
],
}

4
frontend/tsconfig.json Normal file
View File

@ -0,0 +1,4 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
}

6
frontend/types/user.ts Normal file
View File

@ -0,0 +1,6 @@
export declare interface User {
id: number;
name: string;
email?: string;
avatar?: string;
}