initial commit

This commit is contained in:
2026-02-26 21:29:34 +01:00
commit d9d84634e8
72 changed files with 18491 additions and 0 deletions

420
app/components/ListElement.vue Executable file
View File

@@ -0,0 +1,420 @@
<script setup lang="ts">
// import { useListStore } from '~/stores/lists'
// import { useParamsStore } from '@/stores/Params'
// import ColorButtonCpnt from './Layout/ColorButtonCpnt.vue'
// import { defineProps, ref } from 'vue'
const props = defineProps({
id : Number,
item_title : String,
color : Number,
type : String,
is_done : Boolean || null,
trad : String || null,
display_verso : Boolean || null,
position : String || null,
})
const listStore = useListStore();
//const { listData } = storeToRefs(listStore);
const paramsStore = useParamsStore();
const onDisplayMode = ref(true)
const newTitle = ref(props.title)
const newTrad = ref(props.trad)
const tooMuchCharacteres = ref(false)
const oldColor = ref(false)
// const refColorBtn = ref()
const handleKeyUp = (event, id) => {
if (event.keyCode == 27)
handleCancelModify(id);
}
const handleInputBlur = (event, id) => {
if (event.relatedTarget === null || (event.relatedTarget['id'] !== 'save-btn-' + id && event.relatedTarget['id'] !== 'cancel-btn-' + id && event.relatedTarget['id'] !== 'color-btn-' + id)){
handleSave(event, id)
}
}
const handleColorBlur = (event, id) => {
if (event.relatedTarget === null || ( event.relatedTarget['id'] !== 'input-' + id && event.relatedTarget['id'] !== 'save-btn-' + id && event.relatedTarget['id'] !== 'cancel-btn-' + id)){
handleSave(event, id)
}
}
const handleSaveBlur = (event, id) => {
if (event.relatedTarget === null || ( event.relatedTarget['id'] !== 'input-' + id && event.relatedTarget['id'] !== 'color-btn-' + id && event.relatedTarget['id'] !== 'cancel-btn-' + id)){
handleSave(event, id)
}
}
const handleCancelBlur = (event, id) => {
if (event.relatedTarget != null && event.relatedTarget['id'] !== 'input-' + id && event.relatedTarget['id'] !== 'save-btn-' + id && event.relatedTarget['id'] !== 'color-btn-' + id ){
handleSave(event, id)
}
}
const handleSwitchToEditMode = (id) => {
if (!props.done){
listStore.addColorOnItem(id)
if (props.color >= paramsStore.paramsData.colors.length){
listStore.changeColor(id, 0, 0)
// refColorBtn.value.onColorValueChange(0)
}
onDisplayMode.value = false;
oldColor.value = props.color == undefined ? 0 : props.color
}
setTimeout(function(){ document.getElementById('input-' + id).focus();})
}
const backToDisplayMode = () => {
onDisplayMode.value = true;
}
const handleCancelModify = (id) => {
newTrad.value = props.trad != undefined ? props.trad : undefined
newTitle.value = props.title
tooMuchCharacteres.value = false
backToDisplayMode();
// refColorBtn.value.onCanceled(oldColor.value);
listStore.changeColor(id, 0, oldColor.value)
oldColor.value = false
}
// const handleGoto = () => {
// console.log(props.position)
// }
const handleSave = (event, id) => {
event.preventDefault();
tooMuchCharacteres.value = false
if (props.verso == undefined || !props.verso) {
if(newTitle.value.length > 75){
tooMuchCharacteres.value = true
}
if(newTitle.value === ''){
listStore.delete(id)
backToDisplayMode();
}
if(( props.title !== newTitle.value && !tooMuchCharacteres.value ) || (props.color != oldColor.value)){
listStore.saveChanges(id, newTitle.value)
//console.log(newTitle.value)
backToDisplayMode();
}
if(props.title == newTitle.value && !tooMuchCharacteres.value){
backToDisplayMode();
}
}
else {
if(newTrad.value.length > 75){
tooMuchCharacteres.value = true
}
if(newTrad.value === ''){
listStore.delete(id)
backToDisplayMode();
}
if(( props.trad !== newTrad.value && !tooMuchCharacteres.value ) || (props.color != oldColor.value)){
listStore.saveChanges(id, newTrad.value, true)
backToDisplayMode();
}
if(props.trad == newTrad.value && !tooMuchCharacteres.value){
backToDisplayMode();
}
}
}
</script>
<template>
<button
class="container"
:data-id = props.id
:draggable = "onDisplayMode"
@dragstart = "listStore.dragItemStart( props.id )"
@dragenter = "listStore.dragItemEnter( props.id )"
@dragend = "listStore.drop()"
@touchstart = "listStore.dragItemStart( props.id )"
@touchmove = "listStore.dragItemEnter( props.id )"
>
<form
@submit="handleSave( $event, props.id )"
>
<div
class="input"
v-bind:class = "{ hide: onDisplayMode }">
<input v-if="props.verso == undefined || !props.verso"
:style = "color != undefined ? 'color :' + paramsStore.paramsData.colors[props.color] : ''"
@keyup="handleKeyUp($event, props.id)"
@blur="handleInputBlur($event, props.id)"
type="text"
:id = "'input-' + props.id"
v-model="newTitle"
>
<input v-else
:style = "color != undefined ? 'color :' + paramsStore.paramsData.colors[props.color] : ''"
@keyup="handleKeyUp($event, props.id)"
@blur="handleInputBlur($event, props.id)"
type="text"
:id = "'input-' + props.id"
v-model="newTrad"
>
<div class="error" v-if="tooMuchCharacteres">
vous devez entrer moins de 75 charactères...
</div>
</div>
</form>
<div
class="form-button-container"
v-bind:class = "{ hide: onDisplayMode }"
>
<ColorButtonCpnt
:id = "props.id"
:color = "props.color"
:isDisplayMode = "onDisplayMode"
@colorBlur = "handleColorBlur"
@colorKeyUp = "handleKeyUp"
ref="refColorBtn"
/>
<button
:id = "'save-btn-' + props.id"
@click = "handleSave($event, props.id)"
@blur = "handleSaveBlur($event, props.id)"
@keyup = "handleKeyUp($event, props.id)"
>
Enregistrer
</button>
<button
:id = "'cancel-btn-' + props.id"
@click="handleCancelModify(props.id)"
@blur="handleCancelBlur($event, props.id)"
@keyup="handleKeyUp($event, props.id)">
Annuler
</button>
</div>
<div class="text-and-controls-container"
:class = "{ hide : !onDisplayMode }" >
<div class="text-container">
<button
:style = "( !props.done ) ? 'color :' + paramsStore.paramsData.colors[props.color] : ''"
class = "title"
:id = "'p-' + props.id"
:class = "props.type == 'text' || props.type == undefined ? { done : props.done } : props.type == 'lang' ? { invisible : props.verso } : ''"
@click = "handleSwitchToEditMode(props.id)">
{{ newTitle }}
</button>
<button
v-if="props.type == 'lang'"
:style = "( !props.done ) ? 'color :' + paramsStore.paramsData.colors[props.color] : ''"
class = "trad"
:id = "'p-trad-' + props.id"
:class = "{ visible : props.verso }"
@click = "handleSwitchToEditMode(props.id)"
>
{{ newTrad }}
</button>
</div>
<div class="controls-container">
<button v-if="props.type == 'text' || props.type == undefined"
class = "aside-p"
:class="{'hide': props.done}"
@click="listStore.toggleDone(props.id);">
<font-awesome-icon
class="square"
icon="fa-regular fa-square"
alt="Mettre à fait" />
</button>
<button v-else-if="props.type == 'lang'"
class = "aside-p"
@click="listStore.toggleRotate(props.id);">
<font-awesome-icon
:icon="['fas', 'arrows-rotate']"
alt="Retourner" />
</button>
<a v-else-if="props.type == 'map'"
:href="'http://www.google.com/maps/search/?api=1&query=' + props.position"
target="_blank">
<button
class = "aside-p">
<font-awesome-icon
:icon="['fas', 'diamond-turn-right']"
alt="Itinéraire" />
</button>
</a>
<button
class = "aside-p"
@click="listStore.toggleDone(props.id)"
v-bind:class="{'hide': !props.done}">
<font-awesome-icon
color="green"
class="square"
icon="fa-regular fa-square-check"
alt="Mettre en non fait"/>
</button>
<button
class = "aside-p"
@click="listStore.delete(props.id)" >
<font-awesome-icon
color="red"
class="square"
icon="fa-regular fa-trash-can"
alt="Supprimer"/>
</button>
</div>
</div>
</button>
</template>
<style lang="scss" scoped>
button:not(.container){
font-family: 'Quicksand', Arial, Helvetica, sans-serif;
//margin-left:0em;
//margin-top:0em;
//padding: 0.2em 0em;
background: rgb(216, 226, 253);
color: rgb(0, 11, 163);
font-weight:800;
border-radius: 0em;
border: none;
width: 6em;
min-height: 1.8em;
// box-shadow: 4px 4px 5px #555;
transition: 0.3s;
font-size: 1em;
cursor:pointer;
&:hover{
background:color.adjust(rgb(216, 226, 253), $lightness: -7.5%);
//DEPRECATED
//background: darken(rgb(216, 226, 253), 7.5%);
}
// &:focus{
// // border: grey 1px dashed;
// //background: darken(rgb(216, 226, 253), 15%);
// }
&.delete{
color: rgb(163, 0, 0);
}
&.aside-p {
width:2.2em;
aspect-ratio: 1;
border-radius: 1.1em;
margin-inline:0.5em
}
}
button.container{
margin: 0 ;
display:block;
width:100%;
border:none;
background-color: transparent;
}
form{
width : 100%;
}
.form-button-container{
display : flex;
justify-content: flex-start;
align-items:center;
gap : 1em;
}
.color-btn{
display:flex;
justify-content:center;
align-items:center;
}
input{
box-sizing: border-box;
padding: 0.5em;
width : 100%;
margin-bottom: 0.5em;
border:none;
}
.input {
margin: auto;
}
.error{
text-align:center;
color : red;
padding-bottom: 0.5em;
}
.text-and-controls-container{
position:relative;
display : flex;
flex-direction : row;
justify-content : space-between;
align-items : center;
//margin:0;
//border: 1px solid rebeccapurple;
padding-block:0.5em;
}
.text-container{
height:fit-content;
}
button.title, button.trad{
height:fit-content;
position : absolute;
bottom : 0;
top : 0;
background-color: transparent;
width: calc(100% - 5.5em);
font-size:1.2em;
font-weight:normal;
text-align: left;
color:rgb(0, 89, 255);
backface-visibility: hidden;
transition-duration: 0.5s;
overflow: hidden;
word-wrap:break-word;
&:hover
{
cursor:pointer;
background-color: transparent;
}
&:focus{
background-color: transparent;
border: 1px dashed grey ;
}
&.done{
text-decoration: line-through;
color: rgb(134, 134, 134) ;
cursor: default;
}
}
button.trad{
transform:rotateX(180deg);
&.visible{
transform:rotateX(-0deg);
}
}
button.title{
&.invisible{
transform:rotateX(-180deg);
}
}
.hide{
display:none;
}
</style>

View File

@@ -0,0 +1,64 @@
<template>
<button
:disabled="disabled"
:class="variantClass"
@click="$emit('click')"
>
<slot v-if="!loading"></slot>
<slot name="spinner" v-else>
<UiSpinnerCpnt />
</slot>
</button>
</template>
<script setup lang="ts">
defineEmits(['click'])
const props = defineProps<{
disabled?: boolean
loading?: boolean
variant?: 'primary' | 'secondary'
}>()
const variantClass = computed(() => props.variant === 'secondary' ? 'secondary' : 'primary')
</script>
<style scoped lang="scss">
@use "sass:color";
button {
margin: 5px 0 2.3em 0;
width: 100%;
padding: 1em 1.5em;
border-radius: 0.5em;
color: white;
font-weight: bold;
border: none;
box-sizing: border-box;
transition: 0.3s;
background: blueviolet;
text-align: center;
&:hover {
cursor: pointer;
color: color.adjust(blueviolet, $lightness: 15%);
}
&:disabled {
background: #bbb;
cursor: not-allowed;
}
> div {
margin: auto;
}
}
/* Variants */
button.secondary {
background: gray;
&:hover {
color: color.adjust(gray, $lightness: 15%);
}
}
</style>

View File

@@ -0,0 +1,93 @@
<script setup lang="ts">
const burger = useBurgerStore()
</script>
<template>
<button class="nav-button"
@click="burger.toggle()"
>
<svg class="hamburger" :class="{ open: burger.checked }"
viewBox="2 0 100 100">
<path
class="cross"
style="fill:none;
stroke-linecap:round;
stroke-linejoin:round;
stroke-opacity:1"
d="M 36,21 H 74 L 74,33 55,52 36,71 36,83 H 74 L 74,71 36,33 Z"
id="path978"
sodipodi:nodetypes="cccccccccc" />
<path
class="middle"
style="fill:none;
stroke-linecap:round;
stroke-linejoin:bevel;
stroke-opacity:1"
d="M 36,52 H 74 C 90,52 82,69 77,74 71,80 63,83 55,83 38,83 24,69 24,52 24,35 38,21 55,21 c 17,0 31,14 31,31 0,9 -3,16 -9,22"
id="path982"
sodipodi:nodetypes="cccssssc" />
</svg>
</button>
<!-- Panel slide depuis la droite -->
</template>
<style scoped lang="scss">
button.nav-button{
/*centering button*/
display : block;
position: absolute;
bottom:5px;
right:5px;
border-radius: 5px;
/*Formating Button*/
width:3em;
background-color:#aaaaaa80;
border: none;
transition:1s;
z-index: 10;
&:hover{
cursor: pointer;
}
& .hamburger{
/*Default state*/
& .cross, & .middle {
stroke-width:7;
transition:1s ;
-moz-transition:1s ;
-webkit-transition:1s ;
stroke:black;
}
& .cross{
stroke-dashoffset: 0 ;
stroke-dasharray: 0 0 36 80 36 80;
}
& .middle{
stroke-dashoffset: 0 ;
stroke-dasharray: 36 250;
}
/*Formating "open" state */
&.open{
& *{
stroke:hsl(0, 100%, 30%);
}
& .cross{
stroke-width:7;
stroke-dashoffset: -61 ;
stroke-dasharray: 33 82 33 79;
}
& .middle{
stroke-width:7;
stroke-dashoffset: -60 ;
stroke-dasharray: 270 ;
}
}
}
}
// Navigation dans le panel
</style>

View File

@@ -0,0 +1,38 @@
<script setup lang="ts">
const authStore = useAuthStore()
const userEmail = authStore.user?.email
const { isReady, login } = useCodeClient({
// Force l'utilisateur à re-valider explicitement
prompt: 'consent',
login_hint: userEmail,
onSuccess: async (codeResponse) => {
// 1. ICI, on reçoit un 'code' (ex: 4/0Afge...)
console.log("Code reçu pour le BO:", codeResponse.code)
// 6. Envoi au BO pour échange contre sudo_token
try {
const result = await $fetch('/api/auth/google-sudo', {
method: 'POST',
body: { code: codeResponse.code }
})
// Ton BO pourra alors utiliser ce code pour valider l'identité
} catch (err) {
console.error("Erreur BO:", err)
}
},
onError: (error) => console.error("Échec Sudo:", error)
})
</script>
<template>
<button
:disabled="!isReady"
@click="() => login()"
class="btn-sudo"
>
Vérification de sécurité via Google
</button>
</template>

View File

@@ -0,0 +1,111 @@
<script setup lang="ts">
import {
useCodeClient,
type AuthCodeFlowSuccessResponse,
type AuthCodeFlowErrorResponse
} from "vue3-google-signin";
const authStore = useAuthStore();
const listStore = useListStore();
// On définit la fonction de succès avec le bon Type
const handleSuccess = async (response: AuthCodeFlowSuccessResponse) => {
const googleCode = response.code;
try {
// On envoie ce ticket à WordPress
const data = await $fetch<any>('/api-wp/auth/google', {
method: 'POST',
body: { code: googleCode }
});
// On stocke le JWT renvoyé par WordPress
authStore.setTokenCookie(data.token);
authStore.setUserCookie(data.user);
listStore.lists = data.lists
console.log(listStore.lists) //ok
} catch (error) {
console.error("Erreur côté WordPress:", error);
}
};
const handleError = (error: AuthCodeFlowErrorResponse) => {
console.error("L'utilisateur a fermé la fenêtre ou erreur Google:", error);
};
// 3. On initialise le client spécifique au "Code"
const { login } = useCodeClient({
scope: 'openid email profile https://www.googleapis.com/auth/drive.appdata',
access_type: 'offline',
prompt: 'consent',
onSuccess: handleSuccess,
onError: handleError,
});
</script>
<template>
<button @click="login" class="g-signin-button">
<div class="g-logo-wrapper">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" class="g-logo">
<path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/>
<path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/>
<path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"/>
<path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"/>
<path fill="none" d="M0 0h48v48H0z"/>
</svg>
</div>
<span class="g-button-text">{{ $t('googleBtnTxt') }}</span>
</button>
</template>
<style scoped>
.g-signin-button {
margin:auto;
margin-top:1em;
display: flex;
align-items: center;
justify-content: flex-start;
background-color: #ffffff;
color: #757575;
height: 40px;
width: auto;
border: 1px solid #dadce0;
border-radius: 4px;
padding: 0em;
font-family: 'Roboto', arial, sans-serif;
font-weight: 500;
font-size: 14px;
cursor: pointer;
box-shadow: 0 1px 2px 0 rgba(60,64,67,0.302), 0 1px 3px 1px rgba(60,64,67,0.149);
transition: background-color .218s, border-color .218s, box-shadow .218s;
overflow: hidden;
}
.g-signin-button:hover {
background-color: hsl(210, 17%, 90%);
border-color: #d2d2d2;
box-shadow: 0 1px 2px 0 rgba(60,64,67,0.302), 0 1px 3px 1px rgba(60,64,67,0.149);
}
.g-signin-button:active {
background-color: #eeeeee;
}
.g-logo-wrapper {
padding: 11px;
height: 100%;
display: flex;
align-items: center;
}
.g-logo {
width: 18px;
height: 18px;
}
.g-button-text {
padding: 0 12px 0 10px;
white-space: nowrap;
}
</style>

View File

@@ -0,0 +1,104 @@
<template>
<div class="base-input">
<label>
{{ label }}
<div class="input-wrapper">
<input
v-bind="$attrs"
:name="name"
:type="type"
v-model="model"
:placeholder="placeholder"
autocomplete="off"
class="base-input-field"
/>
<slot name="icon" />
</div>
<slot name="pwdReset"/>
<div class="message">
<slot name="message"></slot>
</div>
</label>
</div>
</template>
<script setup lang="ts">
const model = defineModel<string>()
defineOptions({
inheritAttrs: false
})
defineProps<{
label: string
name: string
type?: string
placeholder?: string
displayPwdReset?:boolean
}>()
</script>
<style scoped lang="scss">
.input-wrapper {
position: relative;
}
.base-input {
--input-bg: #ffffff;
--input-border: #00aeff;
--input-hover-bg: #e0e0e0;
--input-text: #003e7c;
}
.base-input label {
margin-bottom: 1em;
display: block;
color: #003e7c;
font-family: 'Quicksand', Arial, Helvetica, sans-serif;
}
.base-input-field {
display: block;
margin: 5px 0;
width: 100%;
padding: 0.75em 1.5em;
background: var(--input-bg);
border: none;
border-bottom: 2px solid var(--input-border);
border-left: 2px solid var(--input-border);
box-sizing: border-box;
color: var(--input-text);
}
.base-input-field:hover,
.base-input-field:focus {
background: var(--input-hover-bg);
}
.message {
min-height: 1.4em;
}
.message :deep(.error) {
color: #ca0d00;
font-size: 0.9em;
font-weight: 600;
margin-left: 1em;
}
.message :deep(.success) {
color: #00ca22;
font-size: 0.9em;
font-weight: 600;
margin-left: 1em;
}
/* Styles spécifiques à lemail */
:deep(.mail-is-valid) {
--input-border: #00ca22;
}
:deep(.mail-is-invalid) {
--input-border: #ca0d00;
}
</style>

View File

@@ -0,0 +1,25 @@
<template>
<InputBase
v-model="model"
v-bind="$attrs"
:label="label"
:name="name"
:placeholder="placeholder"
type="email"
>
<template #message>
<slot name="message"></slot>
</template>
</InputBase>
</template>
<script setup lang="ts">
const model = defineModel<string>()
defineProps<{
label: string
name: string
placeholder?: string
}>()
</script>

View File

@@ -0,0 +1,68 @@
<template>
<InputBase
v-model="model"
v-bind="$props"
:type="seePwd ? 'text' : 'password'"
@blur="seePwd = false"
>
<template #icon>
<font-awesome-icon
class="eye"
icon="fa-solid fa-eye"
@click="seePwd = true"
:class="{ hide: seePwd }"
/>
<font-awesome-icon
class="eye"
icon="fa-solid fa-eye-slash"
@click="seePwd = false"
:class="{ hide: !seePwd }"
/>
</template>
<template #pwdReset>
<div v-if="displayPwdReset" class="pwd-reset-container">
<NuxtLink :to="localePath('passwordResetRequest')">
{{ $t('loginPage.passwordResetRequest') }}
</NuxtLink>
</div>
</template>
<template #message>
<slot name="message"></slot>
</template>
</InputBase>
</template>
<script setup lang="ts">
const localePath = useLocalePath()
const model = defineModel<string>()
const seePwd = ref(false)
withDefaults(defineProps<{
label: string
name: string
placeholder?: string
displayPwdReset?:boolean
}>(), {
// Valeurs par défault
displayPwdReset: false,
})
</script>
<style lang="scss">
.eye {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
color: #003e7c;
font-size: 1.1em;
cursor: pointer;
user-select: none;
}
.eye.hide {
display: none;
}
</style>

View File

@@ -0,0 +1,24 @@
<template>
<InputBase
v-model="model"
v-bind="$attrs"
:label="label"
:name="name"
:placeholder="placeholder"
type="text"
>
<template #message>
<slot name="message"></slot>
</template>
</InputBase>
</template>
<script setup lang="ts">
const model = defineModel<string>()
defineProps<{
label: string
name: string
placeholder?: string
}>()
</script>

View File

@@ -0,0 +1,24 @@
<script setup lang="ts">
const burger=useBurgerStore()
const auth=useAuthStore()
const localePath = useLocalePath()
const route=useRoute()
</script>
<template>
<nav class="panel-nav"
:class="{ open: burger.checked }"
@blur="burger.toggle"
@click="burger.toggle">
<div class="nav-bar-up">
<UiLangSelect />
<button class="panel-item" v-if="auth.isLoggedIn" @click="auth.logout">{{ $t('nav.logout') }}</button>
</div>
<div class="nav-bar-down">
<NuxtLink v-if="!auth.isLoggedIn && route.meta.pageId != 'signup'" class="panel-item" :to="localePath('signup')">{{ $t('nav.signup') }}</NuxtLink>
<NuxtLink v-if="!auth.isLoggedIn && route.meta.pageId != 'login'" class="panel-item" :to="localePath('login')">{{ $t('nav.login') }}</NuxtLink>
<NuxtLink v-if="auth.isLoggedIn && route.meta.pageId != 'profile'" class="panel-item" :to="localePath('profile')">{{ $t('nav.profile') }}</NuxtLink>
<NuxtLink v-if="auth.isLoggedIn && route.meta.pageId != 'lists'" class="panel-item" :to="localePath('lists')">{{ $t('nav.lists') }}</NuxtLink>
<NuxtLink v-if="route.meta.pageId != 'index'" class="panel-item" :to="localePath('index')">{{ $t('nav.home') }}</NuxtLink>
</div>
</nav>
</template>

View File

@@ -0,0 +1,116 @@
<template>
<div>
<ul>{{ $t('passwordCheckerShallContain') }}
<div :class="[ passwordCheck.isNumberOfCaracteresValid() ? 'has-success' : 'has-error' ]">
<font-awesome-icon
icon="fa-regular fa-circle-check"
v-bind:class="{'hide': !passwordCheck.isNumberOfCaracteresValid() || passwordCheck.password == ''}"
/>
<font-awesome-icon
icon="fa-regular fa-circle-xmark"
v-bind:class="{ 'hide': passwordCheck.isNumberOfCaracteresValid() }"
/>
<li :class="[passwordCheck.isNumberOfCaracteresValid() ? 'has-success' : 'has-error']">
{{ $t('passwordChecker8-22') }}
</li>
</div>
<div :class="[passwordCheck.isCapitalizeCaractereValid() ? 'has-success' : 'has-error']">
<font-awesome-icon
icon="fa-regular fa-circle-check"
v-bind:class="{ 'hide': !passwordCheck.isCapitalizeCaractereValid() || passwordCheck.password == '' }"
style="color:green" />
<font-awesome-icon
icon="fa-regular fa-circle-xmark"
v-bind:class="{'hide': passwordCheck.isCapitalizeCaractereValid() }"
style="color:red" />
<li :class="[passwordCheck.isCapitalizeCaractereValid() ? 'has-success' : 'has-error']">
{{ $t('passwordCheckerUppercase') }}
</li>
</div>
<div :class ="[passwordCheck.isMinimizeCaractereValid() ? 'has-success' : 'has-error']">
<font-awesome-icon
icon="fa-regular fa-circle-check"
v-bind:class="{ 'hide': !passwordCheck.isMinimizeCaractereValid() || passwordCheck.password == '' }"
style="color:green" />
<font-awesome-icon
icon="fa-regular fa-circle-xmark"
v-bind:class="{ 'hide': passwordCheck.isMinimizeCaractereValid() }"
style="color:red" />
<li :class="[passwordCheck.isMinimizeCaractereValid() ? 'has-success' : 'has-error']">
{{ $t('passwordCheckerLowercase') }}
</li>
</div>
<div :class ="[passwordCheck.isNumberValid() ? 'has-success' : 'has-error']">
<font-awesome-icon
icon="fa-regular fa-circle-check"
v-bind:class="{ 'hide': !passwordCheck.isNumberValid() || passwordCheck.password == '' }"
style="color:green" />
<font-awesome-icon
icon="fa-regular fa-circle-xmark"
v-bind:class="{ 'hide': passwordCheck.isNumberValid() }"
style="color:red" />
<li :class="[passwordCheck.isNumberValid() ? 'has-success' : 'has-error']">
{{ $t('passwordCheckerNb') }}
</li>
</div>
<div :class="[passwordCheck.isSpecialCaractereValid() ? 'has-success' : 'has-error']">
<font-awesome-icon
icon="fa-regular fa-circle-check"
v-bind:class="{'hide': !passwordCheck.isSpecialCaractereValid()|| passwordCheck.password == '' }"
style="color:green" />
<font-awesome-icon
icon="fa-regular fa-circle-xmark"
v-bind:class="{ 'hide': passwordCheck.isSpecialCaractereValid()}"
style="color:red" />
<li :class="[passwordCheck.isSpecialCaractereValid() ? 'has-success' : 'has-error']">
{{ $t('passwordCheckerSpecialChar') }}
</li>
</div>
</ul>
</div>
</template>
<script setup lang="ts">
const passwordCheck = usePasswordToolBoxStore()
</script>
<style lang="scss" scoped>
.container{
margin-bottom:1.5em;
margin-left: 0;
}
ul{
list-style-type: none;
font-size: 0.9em;
color:rgb(0, 128, 0);
margin:0;
margin-bottom:1em;
&>div{
display: flex;
align-items: center;
&.has-error{
color:red;
transition: 0.5s;
transform: rotateX(0deg)
}
&.has-success{
color:green;
transition: 0.5s;
transform: rotateX(360deg)
}
}
}
li{
margin-left: 0.5em;
}
.hide{
display: none;
}
</style>

View File

@@ -0,0 +1,31 @@
<template>
<div v-if="authStore.user != null">
<img class='avatar' :src="authStore.user.avatar"/>
<ProfileModulesDisplayName/>
</div>
</template>
<script setup lang="ts">
const authStore = useAuthStore()
onMounted(() => {
if (authStore.user){
authStore.user.sudo_token = null
}
})
</script>
<style lang="scss">
img.avatar {
display: block;
margin-top:0.75em;
margin-inline: auto;
border-radius: 50%;
//border: 5px solid blueviolet;
}
.modale-btns{
display: flex;
gap:1em;
}
</style>

View File

@@ -0,0 +1,31 @@
<template>
<UiModale :modalActive="modelValue"
@close="emit('update:modelValue', false)"
title="Confirmer le changement demail">
<div class="modale-content">
<p>
Un email de validation va être envoyé à la nouvelle adresse.
Le changement ne sera effectif quaprès confirmation.
</p>
<div class="actions">
<button class="btn danger" @click="confirm">Confirmer</button>
<button class="btn" @click="emit('update:modelValue', false)">Annuler</button>
</div>
</div>
</UiModale>
</template>
<script setup lang="ts">
const props = defineProps<{
modelValue: boolean
}>()
const emit = defineEmits(['update:modelValue', 'confirm'])
function confirm() {
emit('confirm')
emit('update:modelValue', false)
}
</script>

View File

@@ -0,0 +1,74 @@
<template>
<div>
<button class="deleteBtn" @click="openModal">
{{ $t('profile.delete_account') }}
</button>
</div>
<UiModale @close="closeModal" :modalActive="modalActive">
<div class="modal-content">
<h1>{{ $t('delete.modale.title') }}</h1>
<p>{{ $t('delete.modale.text1') }}</p>
<!-- <p>{{ $t('emailUpdate.modale.text2') }}</p> -->
<div class="modale-btns">
<ButtonBase ref=ConfirmBtn @click="confirm" :loading="awaiting">{{ $t('ui.yes') }}</ButtonBase>
<ButtonBase class="btn" @click="closeModal">{{ $t('ui.no') }}</ButtonBase>
</div>
</div>
</UiModale>
<UiModale @close="closeModalInfo" :modalActive="modalInfoActive">
<div class="modal-content">
<h1>{{ $t('delete.modale2.title') }}</h1>
<p>{{ $t('delete.modale2.text1') }}</p>
<div class="modale-btns">
<ButtonBase ref=btnCloseInfo @click="closeModalInfo">{{ $t('ui.ok') }}</ButtonBase>
</div>
</div>
</UiModale>
</template>
<script setup lang="ts">
const {locale} = useI18n()
const authStore = useAuthStore();
const modalActive = ref(false);
const modalInfoActive = ref(false)
const awaiting = ref(false)
const openModal = () => {
modalActive.value = true;
}
const closeModal = () => {
modalActive.value = false;
}
const confirm = async (locale:string) => {
awaiting.value = true;
await authStore.deleteRequest(locale)
awaiting.value=false
modalActive.value = false;
modalInfoActive.value = true
}
const closeModalInfo = () => {
modalInfoActive.value = false
}
</script>
<style scoped lang="scss">
.deleteBtn{
color:red;
font-weight: bold;
margin:0;
border:none;
text-align: left;
background-color: none;
&:hover{
color:rgb(83, 0, 0)
}
}
button{
text-align: center;
}
</style>

View File

@@ -0,0 +1,74 @@
<template>
<div>
<button class="deleteBtn" @click="openModal">
{{ $t('profile.delete_account') }}
</button>
</div>
<UiModale @close="closeModal" :modalActive="modalActive">
<div class="modal-content">
<h1>{{ $t('delete.modale.title') }}</h1>
<p>{{ $t('delete.modale.text1') }}</p>
<!-- <p>{{ $t('emailUpdate.modale.text2') }}</p> -->
<div class="modale-btns">
<ButtonBase ref=ConfirmBtn @click="confirm" :loading="awaiting">{{ $t('ui.yes') }}</ButtonBase>
<ButtonBase class="btn" @click="closeModal">{{ $t('ui.no') }}</ButtonBase>
</div>
</div>
</UiModale>
<UiModale @close="closeModalInfo" :modalActive="modalInfoActive">
<div class="modal-content">
<h1>{{ $t('delete.modale2.title') }}</h1>
<p>{{ $t('delete.modale2.text1') }}</p>
<div class="modale-btns">
<ButtonBase ref=btnCloseInfo @click="closeModalInfo">{{ $t('ui.ok') }}</ButtonBase>
</div>
</div>
</UiModale>
</template>
<script setup lang="ts">
const {locale} = useI18n()
const authStore = useAuthStore();
const modalActive = ref(false);
const modalInfoActive = ref(false)
const awaiting = ref(false)
const openModal = () => {
modalActive.value = true;
}
const closeModal = () => {
modalActive.value = false;
}
const confirm = async (locale:string) => {
awaiting.value = true;
await authStore.deleteRequest(locale)
awaiting.value=false
modalActive.value = false;
modalInfoActive.value = true
}
const closeModalInfo = () => {
modalInfoActive.value = false
}
</script>
<style scoped lang="scss">
.deleteBtn{
color:red;
font-weight: bold;
margin:0;
border:none;
text-align: left;
background-color: none;
&:hover{
color:rgb(83, 0, 0)
}
}
button{
text-align: center;
}
</style>

View File

@@ -0,0 +1,105 @@
<template>
<div class="field-container">
<label>{{ $t('profile.name_label') }}</label>
<div v-if="authStore.user?.is_google" class="value-locked">
<p>{{ authStore.user.display_name }}</p>
</div>
<div v-else class="editable-wrapper">
<button
v-if="!isEditing"
@click="startEditing"
class="value-btn"
:aria-label="$t('profile.edit_name')"
>
{{ authStore.user?.display_name }}
</button>
<input
v-else
ref="inputRef"
v-model="newName"
@blur="handleUpdate"
@keydown.enter="handleUpdate"
@keydown.esc="cancelEditing"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, nextTick } from 'vue'
import { useAuthStore } from '@/stores/auth'
const authStore = useAuthStore()
const isEditing = ref(false)
const newName = ref(authStore.user?.display_name || '')
const inputRef = ref<HTMLInputElement | null>(null)
const startEditing = async () => {
isEditing.value = true
// On attend que l'input soit rendu pour lui donner le focus
await nextTick()
inputRef.value?.focus()
}
const cancelEditing = () => {
isEditing.value = false
newName.value = authStore.user?.display_name || ''
}
const handleUpdate = () => {
if (!isEditing.value) return
// Si le nom a changé et n'est pas vide
if (newName.value && newName.value !== authStore.user?.display_name) {
authStore.updateDisplayName(newName.value)
}
isEditing.value = false
}
</script>
<style scoped lang="scss">
div.field-container {
margin-block: 1rem;
display:block
}
div.editable-wrapper{
display:inline;
}
.value-locked::after {
content:" 🔒";
}
.value-locked {
display:inline;
& p {
font-weight: 500;
color: #666;
display: inline;
align-items: center;
gap: 8px;
}
}
label{
font-weight:bold;
display:inline;
}
.value-btn {
background: none;
border: none;
padding: 0;
cursor: pointer;
text-align: left;
font-size:1em;
&:hover{
color : blueviolet;
}
}
</style>

View File

@@ -0,0 +1,126 @@
<template>
<div class="field-container">
<label>{{ $t('profile.email_label') }}</label>
<div v-if="authStore.user?.is_google" class="value-locked">
<p>{{ authStore.user.email }}</p>
</div>
<div v-else class="editable-wrapper">
<button
v-if="!email_toggle"
@click="toggle_to_input"
>
{{ authStore.user?.email }}
</button>
<input v-else
ref="emailInput"
v-model="email"
@blur="handleUpdate"
@keydown.enter="handleUpdate"
@keydown.esc ="cancelEditing"
/>
</div>
</div>
<UiModale @close="closeEmailModal" :modalActive="emailModalActive">
<div class="modal-content">
<h1>{{ $t('emailUpdate.modale.title') }}</h1>
<p>{{ $t('emailUpdate.modale.text1') }}</p>
<p>{{ $t('emailUpdate.modale.text2') }}</p>
<div class="modale-btns">
<ButtonBase ref=ConfirmBtn @click="confirmEmailChange">{{ $t('emailUpdate.modale.confBtn') }}</ButtonBase>
<ButtonBase class="btn" @click="closeEmailModal">{{ $t('emailUpdate.modale.cancelBtn') }}</ButtonBase>
</div>
</div>
</UiModale>
</template>
<script setup lang="ts">
const {locale} = useI18n()
const authStore = useAuthStore()
const email_toggle = ref(false)
const email = ref(authStore.user?.email)
const emailModalActive = ref(false)
const emailInput = ref<HTMLInputElement | null>(null)
const ConfirmBtn = ref<HTMLInputElement | null>(null)
const toggle_to_input = async ()=>{
email_toggle.value = true
// On attend que Vue passe email_toggle à true et affiche l'input dans le DOM
await nextTick()
// Maintenant l'input existe, on peut lui donner le focus
emailInput.value?.focus()
}
const handleUpdate = async ()=>{
if (email.value !== authStore.user?.email){
emailModalActive.value = true
await nextTick()
ConfirmBtn.value?.focus()
}
email_toggle.value = false
}
const cancelEditing = () => {
email_toggle.value = false
email.value = authStore.user?.email || ''
}
//* Fonctions de taitement email */
const closeEmailModal = () =>{
emailModalActive.value = false
email.value = authStore.user?.email
}
const confirmEmailChange = () => {
emailModalActive.value = false
if (email.value){
authStore.emailChange(email.value, locale.value)
}
}
</script>
<style lang="scss">
div.field-container {
margin-block: 1rem;
display:block
}
div.editable-wrapper{
display:inline;
}
.value-locked::after {
content:" 🔒";
}
.value-locked {
display:inline;
& p {
font-weight: 500;
color: #666;
display: inline;
align-items: center;
gap: 8px;
}
}
label{
font-weight:bold;
display:inline;
}
button{
background: none;
border: none;
padding: 0;
cursor: pointer;
text-align: left;
font-size:1em;
&:hover{
color:blueviolet;
}
}
</style>

View File

@@ -0,0 +1,57 @@
<template>
<form @submit.prevent="handleFormSubmit">
<InputPassword
name="password"
label=""
:placeholder="$t('profile.pwd_challenge_input_label')"
v-model="password"
>
<p>{{ password }}</p>
<template #message>
<p v-if="errors.passwordEmpty" class="error">{{ $t('ui.errorPwdEmpty') }}</p>
<p v-if="errors.wrongPassword" class="error">{{ $t('ui.errorWrongPwd') }}</p>
</template>
</InputPassword>
<ButtonBase
:disabled="password === '' || awaiting"
:loading="awaiting"
>
{{ $t('loginFormBtn') }}
</ButtonBase>
</form>
</template>
<script setup lang="ts">
const authStore=useAuthStore()
const password = ref('');
const errors = ref({
"passwordEmpty":false,
"wrongPassword":false
})
const awaiting = ref(false);
const handleFormSubmit = async() => {
awaiting.value = true
errors.value.wrongPassword = false;
errors.value.passwordEmpty = false;
if (!password.value){
errors.value.passwordEmpty = true;
awaiting.value = false;
return false
}
// Envoie de la requette à l'endpoint JWT et récupération d'un token de connexion
const success = await authStore.pwdChallenge(password.value)
if (success) {
awaiting.value = false
}
else{
errors.value.wrongPassword = true;
awaiting.value = false
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,46 @@
<template>
<div v-if="!authStore.user?.sudo_token" class="auth-challenge-screen">
<h3 class="danger">--- {{ $t('profile.danger_zone') }} ---</h3>
<p>Cette zone contient des actions sensibles. Veuillez confirmer votre identité.</p>
<div v-if="authStore.user?.is_google" class="reauth-box">
<p>Compte Google détecté : veuillez confirmer votre session.</p>
<ButtonGoogleChallenge />
</div>
<div v-else class="reauth-box">
<ProfileModulesPasswordChallenge />
</div>
</div>
<div v-else class="red-zone-content">
<div class="module-wrapper">
<ProfileModulesEmailUpdate />
</div>
<div class="danger-section">
<h3 class="danger">--- {{ $t('profile.danger_zone') }} ---</h3>
<ProfileModulesGlobalLogout />
<ProfileModulesDeleteAccount />
</div>
</div>
</template>
<script setup lang="ts">
const authStore = useAuthStore();
onUnmounted( () => {
if (authStore.user) authStore.user.sudo_token = null
}
)
</script>
<style scoped lang="scss">
h3.danger{
color : red;
margin-bottom: 0.5em;
}
.reauth-box{
margin-top: 1em;
}
</style>

View File

@@ -0,0 +1,31 @@
<script setup lang="ts">
// This Component is Not used any more, as long as I use Buttons to switch between languages.
const { locales, locale, setLocale } = useI18n()
// Langues disponibles sauf celle courante
const otherLocales = computed(() =>
locales.value.filter(l => l.code !== locale.value)
)
// Changer la langue
function changeLocale(locale: 'fr' | 'en') {
setLocale(locale)
}
</script>
<template>
<button class="panel-item" v-for="l in otherLocales" :key="l.code"
@click="changeLocale(l.code)"
>{{ l.name }}</button>
</template>
<style scoped lang="scss">
/* Optional: transition hover pour le select */
select:hover {
border-color: #999;
}
</style>

68
app/components/ui/Loading.vue Executable file
View File

@@ -0,0 +1,68 @@
<template>
<h3>
</h3>
<div class="loading">
<div class="loading__circle loading__circle--color-blue"></div>
<div class="loading__circle loading__circle--color-red"></div>
<div class="loading__circle loading__circle--color-yellow"></div>
<div class="loading__circle loading__circle--color-green"></div>
</div>
</template>
<style lang=scss scoped>
.loading {
position: absolute;
top:40%;
left:calc(50% - (2rem*4 + 0.750rem * 3) / 2);
margin:auto;
//padding: 0.625rem;
display: flex;
justify-content: center;
align-items: center;
}
.loading__circle {
min-width: 2rem;
aspect-ratio: 1;
background-color: lightgrey;
border-radius: 50%;
margin: 0 0.375rem;
animation-name: bump;
animation-duration: 2s;
animation-iteration-count: infinite;
}
.loading__circle--color-blue {
background-color: #4285f4;
animation-delay:0;
}
.loading__circle--color-red {
background-color: #ea4335;
animation-delay:0.5s;
}
.loading__circle--color-yellow {
background-color: #fbbc05;
animation-delay:1s;
}
.loading__circle--color-green {
background-color: #34a853;
animation-delay:1.5s;
}
@keyframes bump {
from {
transform: scale(1);
}
25% {
transform: scale(1.4);
}
50%{
transform: scale(1);
}
100%{
transform:scale(1);
}
}
</style>

33
app/components/ui/Modale.vue Executable file
View File

@@ -0,0 +1,33 @@
<template>
<Transition name="modal-animation">
<div v-show="modalActive" class="modale">
<transition name="modal-frame-animation">
<div v-show="modalActive" class="modal-frame">
<font-awesome-icon class="close-btn" @click="close" icon="fa-regular fa-circle-xmark" />
<div class="modale-content">
<slot>
</slot>
</div>
</div>
</transition>
</div>
</Transition>
</template>
<script setup lang="ts">
const props = defineProps({
modalActive: Boolean,
title: String
});
const emit = defineEmits(['close']);
const close = () => {
emit('close');
};
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,78 @@
<template>
<div>
</div>
</template>
<script>
</script>
<style lang="scss" scoped>
div{
height: 1.3em;
width: 1.3em;
border:3px solid #ff00c8;
border-top:3px solid rgb(216, 226, 253);
border-radius:50%;
-webkit-transition-property: -webkit-transform;
-webkit-transition-duration: 1.2s;
-webkit-animation-name: rotate;
-webkit-animation-iteration-count: infinite;
-webkit-animation-timing-function: linear;
-moz-transition-property: -moz-transform;
-moz-animation-name: rotate;
-moz-animation-duration: 1.2s;
-moz-animation-iteration-count: infinite;
-moz-animation-timing-function: linear;
transition-property: transform;
animation-name: rotate;
animation-duration: 1.2s;
animation-iteration-count: infinite;
animation-timing-function: linear;}
@-webkit-keyframes rotate {
from {-webkit-transform: rotate(0deg);}
to {-webkit-transform: rotate(360deg);}
}
@-moz-keyframes rotate {
from {-moz-transform: rotate(0deg);}
to {-moz-transform: rotate(360deg);}
}
@keyframes rotate {
from {transform: rotate(0deg);}
to {transform: rotate(360deg);}
}
/* Rest of page style*/
body{
background:#FABC20;
font-family: 'Open Sans', sans-serif;
-webkit-font-smoothing: antialiased;
color:#393D3D;
}
#container{
width:90%;
max-width:700px;
margin:1em auto;
position:relative;
}
/* spinner positioning */
#html-spinner, #svg-spinner{
position:absolute;
top:80px;
margin-left:-24px;
}
</style>

View File

@@ -0,0 +1,22 @@
<template>
<h3>
</h3>
<div class="unconfirmed-banner">
<p>{{ $t('unconfirmedBanner.message') }}</p>
</div>
</template>
<style lang=scss scoped>
.unconfirmed-banner {
width:100%;
background-color: #ea4335;
height:1.3em;
}
p{
text-align: center;
margin: auto;
}
</style>