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
This commit is contained in:
Florian Briand 2024-09-24 12:57:34 +02:00
commit 345190dfeb
9 changed files with 181 additions and 1 deletions

View File

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

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);

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>

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;
}