initial commit
This commit is contained in:
420
app/components/ListElement.vue
Executable file
420
app/components/ListElement.vue
Executable 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>
|
||||
64
app/components/button/base.vue
Normal file
64
app/components/button/base.vue
Normal 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>
|
||||
93
app/components/button/burger.vue
Normal file
93
app/components/button/burger.vue
Normal 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>
|
||||
38
app/components/button/google-challenge.vue
Normal file
38
app/components/button/google-challenge.vue
Normal 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>
|
||||
111
app/components/button/google.vue
Normal file
111
app/components/button/google.vue
Normal 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>
|
||||
104
app/components/input/base.vue
Normal file
104
app/components/input/base.vue
Normal 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 à l’email */
|
||||
:deep(.mail-is-valid) {
|
||||
--input-border: #00ca22;
|
||||
}
|
||||
|
||||
:deep(.mail-is-invalid) {
|
||||
--input-border: #ca0d00;
|
||||
}
|
||||
</style>
|
||||
25
app/components/input/email.vue
Normal file
25
app/components/input/email.vue
Normal 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>
|
||||
|
||||
68
app/components/input/password.vue
Normal file
68
app/components/input/password.vue
Normal 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>
|
||||
24
app/components/input/text.vue
Normal file
24
app/components/input/text.vue
Normal 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>
|
||||
24
app/components/menu/nav.vue
Normal file
24
app/components/menu/nav.vue
Normal 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>
|
||||
116
app/components/passwordChecker.vue
Normal file
116
app/components/passwordChecker.vue
Normal 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>
|
||||
31
app/components/profile/general.vue
Normal file
31
app/components/profile/general.vue
Normal 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>
|
||||
31
app/components/profile/modales/email-change.vue
Normal file
31
app/components/profile/modales/email-change.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<UiModale :modalActive="modelValue"
|
||||
@close="emit('update:modelValue', false)"
|
||||
title="Confirmer le changement d’email">
|
||||
<div class="modale-content">
|
||||
|
||||
<p>
|
||||
Un email de validation va être envoyé à la nouvelle adresse.
|
||||
Le changement ne sera effectif qu’aprè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>
|
||||
74
app/components/profile/modules/delete-account.vue
Normal file
74
app/components/profile/modules/delete-account.vue
Normal 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>
|
||||
@@ -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>
|
||||
105
app/components/profile/modules/display-name.vue
Normal file
105
app/components/profile/modules/display-name.vue
Normal 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>
|
||||
126
app/components/profile/modules/email-update.vue
Normal file
126
app/components/profile/modules/email-update.vue
Normal 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>
|
||||
57
app/components/profile/modules/password-challenge.vue
Normal file
57
app/components/profile/modules/password-challenge.vue
Normal 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>
|
||||
46
app/components/profile/red-zone.vue
Normal file
46
app/components/profile/red-zone.vue
Normal 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>
|
||||
31
app/components/ui/LangSelect.vue
Normal file
31
app/components/ui/LangSelect.vue
Normal 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
68
app/components/ui/Loading.vue
Executable 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
33
app/components/ui/Modale.vue
Executable 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>
|
||||
78
app/components/ui/SpinnerCpnt.vue
Executable file
78
app/components/ui/SpinnerCpnt.vue
Executable 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>
|
||||
22
app/components/ui/unconfirmedBanner.vue
Normal file
22
app/components/ui/unconfirmedBanner.vue
Normal 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>
|
||||
Reference in New Issue
Block a user