initial commit
This commit is contained in:
+24
@@ -0,0 +1,24 @@
|
|||||||
|
# Nuxt dev/build outputs
|
||||||
|
.output
|
||||||
|
.data
|
||||||
|
.nuxt
|
||||||
|
.nitro
|
||||||
|
.cache
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Node dependencies
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.DS_Store
|
||||||
|
.fleet
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Local env files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
# Nuxt Minimal Starter
|
||||||
|
|
||||||
|
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Make sure to install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# npm
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn install
|
||||||
|
|
||||||
|
# bun
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Server
|
||||||
|
|
||||||
|
Start the development server on `http://localhost:3000`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# npm
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm dev
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn dev
|
||||||
|
|
||||||
|
# bun
|
||||||
|
bun run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production
|
||||||
|
|
||||||
|
Build the application for production:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# npm
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn build
|
||||||
|
|
||||||
|
# bun
|
||||||
|
bun run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Locally preview production build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# npm
|
||||||
|
npm run preview
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm preview
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn preview
|
||||||
|
|
||||||
|
# bun
|
||||||
|
bun run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<NuxtRouteAnnouncer />
|
||||||
|
<NuxtLayout>
|
||||||
|
<NuxtPage />
|
||||||
|
</NuxtLayout>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Open+Sans&family=Quicksand:wght@300;400;700&display=swap');
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
*{
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html{
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
h1, h2, h3, p, ul, li, label, button, input{
|
||||||
|
font-family: 'quicksand', Arial, Helvetica, sans-serif;
|
||||||
|
//color: blueviolet;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1{
|
||||||
|
color: blueviolet;
|
||||||
|
padding-top:1em;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2{
|
||||||
|
color: blueviolet;
|
||||||
|
padding-block:1em;
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.base-input input {
|
||||||
|
border: 1px solid var(--input-border);
|
||||||
|
background: var(--input-bg);
|
||||||
|
color: var(--input-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
label{
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
input{
|
||||||
|
text-align:left;
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
/*Modale annimation*/
|
||||||
|
|
||||||
|
.modal-animation-enter-from,
|
||||||
|
.modal-animation-leave-to{
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-animation-enter-active{
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
.modal-animation-leave-active{
|
||||||
|
transition: opacity 0.3s ease 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*Modal inner animation*/
|
||||||
|
|
||||||
|
.modal-frame-animation-enter-from, .modal-frame-animation-leave-to{
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
|
.modal-frame-animation-enter-active, .modal-frame-animation-leave-active{
|
||||||
|
transition: all 0.3s cubic-bezier(0.52, -0.02, 0.19, 1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*modal mise en forme*/
|
||||||
|
.modale{
|
||||||
|
position:fixed;
|
||||||
|
top:0;
|
||||||
|
left: 00;
|
||||||
|
background: rgba(140, 140, 140, 0.75);
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
}
|
||||||
|
.modal-frame{
|
||||||
|
//border : dashed red 2px;
|
||||||
|
position: relative;
|
||||||
|
width: 80%;
|
||||||
|
min-width: 230px;
|
||||||
|
max-width: 350px;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow: visible;
|
||||||
|
|
||||||
|
|
||||||
|
background:#e5e5e5;
|
||||||
|
border-top-left-radius: 1em;
|
||||||
|
border-bottom-left-radius: 1em;
|
||||||
|
border-bottom-right-radius: 1em;
|
||||||
|
box-sizing:border-box;
|
||||||
|
padding: 2em;
|
||||||
|
margin:1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1.modal-title{
|
||||||
|
padding: 0;
|
||||||
|
padding-bottom: 1em;
|
||||||
|
//border:dashed 2px red;
|
||||||
|
text-align:center;
|
||||||
|
}
|
||||||
|
.close-btn{
|
||||||
|
margin: auto;
|
||||||
|
position: absolute;
|
||||||
|
right: -17px;
|
||||||
|
top: -17px;
|
||||||
|
text-align: center;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.9em;
|
||||||
|
padding: 0.25em;
|
||||||
|
background-color: #e5e5e5;
|
||||||
|
color: blueviolet;
|
||||||
|
font-size: 1.7em;
|
||||||
|
transition: color 0.5s ease;
|
||||||
|
&:hover{
|
||||||
|
color: hsl(271, 76%, 68%);
|
||||||
|
cursor:pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.modal-content{
|
||||||
|
//border: 2px dashed red;
|
||||||
|
max-height: calc(80vh - 7.5em);
|
||||||
|
overflow-y: auto;
|
||||||
|
//FireFox
|
||||||
|
scrollbar-width: none;
|
||||||
|
//Edge
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
.main-container{
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: rgb(218, 218, 218);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-container{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
background-color: rgb(236, 236, 236);
|
||||||
|
box-shadow: 0px 0px 15px rgb(136, 136, 136) ;
|
||||||
|
min-height: 100dvh;
|
||||||
|
width:100%;
|
||||||
|
max-width: 450px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.up{
|
||||||
|
height: 100%;
|
||||||
|
position:relative;
|
||||||
|
border:#f0f 3px solid;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-scroll-view{
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
max-width: 450px;
|
||||||
|
overflow-y: auto;
|
||||||
|
-webkit-overflow-scrolling: touch; /* Fluidité pour iOS */
|
||||||
|
margin-inline:0.5em;
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
|
||||||
|
button.panel-item{
|
||||||
|
border-radius: 0.35em;
|
||||||
|
padding:0.5em 0.5em;
|
||||||
|
border:none;
|
||||||
|
font-size:1em;
|
||||||
|
text-align: center;
|
||||||
|
margin-block:0.25em;
|
||||||
|
width:88%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-nav {
|
||||||
|
//display: none;
|
||||||
|
position: absolute;
|
||||||
|
top:0;
|
||||||
|
right:0;
|
||||||
|
background: #aaaaaa80;
|
||||||
|
backdrop-filter: blur(5px); /* flou du contenu derrière */
|
||||||
|
-webkit-backdrop-filter: blur(5px); /* Safari */
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-block: 2em;
|
||||||
|
gap: 0.75rem;
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.5s;
|
||||||
|
box-sizing: border-box;
|
||||||
|
z-index: 5;
|
||||||
|
&.open {
|
||||||
|
display: flex;
|
||||||
|
transform: translateX(1px);
|
||||||
|
opacity: 1;
|
||||||
|
}}
|
||||||
|
|
||||||
|
div.nav-bar-up{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: end;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
div.nav-bar-down{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: start;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-item {
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
color: blueviolet;
|
||||||
|
padding: 0.5rem;
|
||||||
|
padding-left: 3.2em;
|
||||||
|
padding-right: 0.8em;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-item:hover {
|
||||||
|
color: hsl(271, 76%, 35%);
|
||||||
|
background-color: #aaaaaa80;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
@use "./base/fonts";
|
||||||
|
@use "./base/reset";
|
||||||
|
@use "./base/typography";
|
||||||
|
@use "./components/modale";
|
||||||
|
@use "./layout/app";
|
||||||
|
@use "./layout/navBar";
|
||||||
|
@use "./pages/index";
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
@use "sass:color";
|
||||||
|
|
||||||
|
.index-main-text > p,
|
||||||
|
.confirm-main-text{
|
||||||
|
text-align: left;
|
||||||
|
text-indent: 1em;
|
||||||
|
margin:0.5em;
|
||||||
|
color:rgb(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-main-text a,
|
||||||
|
.confirm-main-text a {
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
color: blueviolet;
|
||||||
|
transition: 0.3s;
|
||||||
|
&:hover{
|
||||||
|
color: color.adjust(blueviolet, $lightness: -25%);
|
||||||
|
//DEPRECATED
|
||||||
|
//color:darken($color: blueviolet, $amount: 50%)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.index-main-text-last {
|
||||||
|
color: blueviolet;
|
||||||
|
padding-block: 1.5em;
|
||||||
|
font-weight: bold;
|
||||||
|
& a {
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
color: blueviolet;
|
||||||
|
transition: 0.3s;
|
||||||
|
&:hover{
|
||||||
|
color: color.adjust(blueviolet, $lightness: -25%);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+21
@@ -0,0 +1,21 @@
|
|||||||
|
$body-bg:oklab(94.611% 0 -0.00011);
|
||||||
|
$page-bg:hsl(234, 20%, 30%);
|
||||||
|
//$page-bg1:hsl(0, 0%, 60%);
|
||||||
|
|
||||||
|
$renforced-light : hsl(13, 68%, 60%);
|
||||||
|
$renforced-normal: hsl(13, 68%, 40%);
|
||||||
|
$renforced-dark : hsl(13, 68%, 20%);
|
||||||
|
|
||||||
|
$hypertext-light : hsl(245, 100%, 87%);
|
||||||
|
$hypertext-normal : hsl(245, 100%, 82%);
|
||||||
|
|
||||||
|
$main-normal: hsl(52, 50%, 90%);
|
||||||
|
$main-dark : hsl(52, 50%, 70%);
|
||||||
|
|
||||||
|
$title-light : hsl(37, 79%, 90%);
|
||||||
|
$title-normal: hsl(37, 79%, 75%);
|
||||||
|
$title-dark : hsl(37, 79%, 60%);
|
||||||
|
|
||||||
|
$action-light : hsl(139, 57%, 85%);
|
||||||
|
$action-normal: hsl(139, 57%, 77%);
|
||||||
|
$action-dark : hsl(139, 57%, 67%);
|
||||||
Executable
+2
@@ -0,0 +1,2 @@
|
|||||||
|
$maxWidth: 1200px;
|
||||||
|
$imageWidth: 100%;
|
||||||
Executable
+420
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
Executable
+68
@@ -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>
|
||||||
Executable
+33
@@ -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>
|
||||||
Executable
+78
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
export const useCounter = () => {
|
||||||
|
const counter = useState('counter', () => 0)
|
||||||
|
const increment = () => {
|
||||||
|
counter.value++
|
||||||
|
}
|
||||||
|
|
||||||
|
const decrement = () => {
|
||||||
|
counter.value--
|
||||||
|
}
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
counter.value=0
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
counter,
|
||||||
|
increment,
|
||||||
|
decrement,
|
||||||
|
reset
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col items-center justify-center min-h-screen bg-gray-50 text-gray-800 px-4">
|
||||||
|
<h1 class="text-6xl md:text-8xl font-extrabold text-red-500 mb-4">
|
||||||
|
{{ error.statusCode }}
|
||||||
|
</h1>
|
||||||
|
<p class="text-xl md:text-2xl mb-6 text-center">
|
||||||
|
{{ error.statusMessage || error.message }}
|
||||||
|
</p>
|
||||||
|
<NuxtLink
|
||||||
|
to="/"
|
||||||
|
class="px-6 py-3 bg-indigo-600 text-white rounded-lg shadow hover:bg-indigo-700 transition"
|
||||||
|
>
|
||||||
|
Retour à l'accueil
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
error: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
statusCode: 500,
|
||||||
|
statusMessage: 'Une erreur est survenue'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<div class="main-container">
|
||||||
|
<div class="app-container">
|
||||||
|
<div class="app-scroll-view">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
<div class="main-container">
|
||||||
|
<div class="app-container">
|
||||||
|
<div class="up">
|
||||||
|
<div class="app-scroll-view">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
<MenuNav/>
|
||||||
|
<ButtonBurger/>
|
||||||
|
</div>
|
||||||
|
<div v-if="authStore.user && !authStore.user?.confirmed" class="down">
|
||||||
|
<UiUnconfirmedBanner/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
export default defineNuxtRouteMiddleware((to) => {
|
||||||
|
const auth = useAuthStore()
|
||||||
|
|
||||||
|
// Gestion de la 404
|
||||||
|
if (to.matched.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 1. Liste des pages publiques
|
||||||
|
const isPublicPage = to.meta.public as boolean
|
||||||
|
|
||||||
|
// 2. Si pas de token et page protégée -> Redirection /403
|
||||||
|
if (!auth.isLoggedIn && !isPublicPage) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 403,
|
||||||
|
statusMessage: "Accès refusé" }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<template>
|
||||||
|
<h1>{{ $t('confirmation.title') }}</h1>
|
||||||
|
|
||||||
|
<uiLoading v-if="result === null" />
|
||||||
|
|
||||||
|
<div class="confirm-main-text" v-if="result!=null">
|
||||||
|
<div v-if="result === true">
|
||||||
|
<p v-if="authStore.isLoggedIn" >{{ $t('confirmation.successConnected') }}
|
||||||
|
<NuxtLink :to="localePath('/lists')">{{ $t('confirmation.listsLink') }}</NuxtLink>
|
||||||
|
</p>
|
||||||
|
<p v-else>{{ $t('confirmation.successNotConnected') }} <NuxtLink :to="localePath('/login')">{{ $t('confirmation.loginLink') }}</NuxtLink></p>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<p class="confirm-main-text" v-if="result === 'expired'">{{ $t('confirmation.failureMessage')}}<br/><span>{{ $t('confirmation.failureCauseExpired') }}</span></p>
|
||||||
|
<p class="confirm-main-text" v-else>{{ $t('confirmation.failureMessage')}}<br/><span>{{ $t('confirmation.failureCauseInvalid') }}</span></p>
|
||||||
|
<p class="confirm-main-text last">{{ $t('confirmation.failureYouCan')}} <NuxtLink :to="localePath('/signup')">{{ $t('confirmation.failureCreateNewAccount') }}</NuxtLink> {{ $t('confirmation.failureOr') }} <NuxtLink :to="localePath('/')">{{ $t('confirmation.backHome') }}</NuxtLink>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'confirmation', // utilisation du layout confirm
|
||||||
|
public: true,
|
||||||
|
})
|
||||||
|
import type { ConfirmResult} from '~/types/auth';
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const localePath = useLocalePath()
|
||||||
|
const loading = ref<boolean>(true)
|
||||||
|
const result = ref<ConfirmResult | null>(null)
|
||||||
|
const userConnected = ref<boolean>(false)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const route = useRoute()
|
||||||
|
// const user = route.query.user as string
|
||||||
|
const token = route.query.token as string
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
loading.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
result.value = await authStore.confirmUser(token)
|
||||||
|
if (result.value === true) {
|
||||||
|
if (authStore.isLoggedIn && authStore.user){
|
||||||
|
authStore.user.confirmed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
throw new Error("Invalid !");
|
||||||
|
|
||||||
|
}
|
||||||
|
finally{
|
||||||
|
loading.value=false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
|
||||||
|
p > span{
|
||||||
|
color:red;
|
||||||
|
text-align: center;
|
||||||
|
display: block;
|
||||||
|
margin-block: 1.2em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.confirm-main-text:first-of-type{
|
||||||
|
margin-top: 2em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
public: true,
|
||||||
|
pageId: 'index'
|
||||||
|
})
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const localePath = useLocalePath()
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h1>{{ $t('index.title') }}</h1>
|
||||||
|
<h2 class="title">{{ $t('index.subtitle1')}}</h2>
|
||||||
|
<div class="index-main-text" v-html="$t('index.mainText')"></div>
|
||||||
|
|
||||||
|
<div v-if="!authStore.isLoggedIn">
|
||||||
|
<div class="index-main-text-last">
|
||||||
|
<i18n-t keypath="index.lastSentenceUnconnect" tag="p">
|
||||||
|
<template #login>
|
||||||
|
<NuxtLink :to="localePath('/login')">
|
||||||
|
{{ $t('index.login') }}
|
||||||
|
</NuxtLink>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #signup>
|
||||||
|
<NuxtLink :to="localePath('/signup')">
|
||||||
|
{{ $t('index.signup') }}
|
||||||
|
</NuxtLink>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
<ButtonGoogle/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<p>{{ $t('index.lastSentenceConnected') }} <NuxtLink :to="localePath('lists')">{{ $t('index.seeLists') }}.</NuxtLink>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
color: #06c;
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}</style>
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
pageId: 'lists'
|
||||||
|
})
|
||||||
|
import type {List} from '~/types/lists'
|
||||||
|
const listStore = useListStore()
|
||||||
|
|
||||||
|
// activeTabId réactif
|
||||||
|
const activeTabId = ref<number | undefined>(undefined)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!listStore.lists.length) {
|
||||||
|
await listStore.fetchLists()
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultOpen = listStore.lists.find((l) => l.is_open === true)
|
||||||
|
activeTabId.value = defaultOpen?.id ?? listStore.lists[0]?.id
|
||||||
|
})
|
||||||
|
|
||||||
|
// Fonction utilitaire pour parser le contenu chiffré
|
||||||
|
const parseContent = (list: List) => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(list.encrypted_content) as any[]
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="!listStore.loading">
|
||||||
|
<div v-for="list in listStore.lists" :key="list.id">
|
||||||
|
<h3>{{ list.list_title }}</h3>
|
||||||
|
<ul>
|
||||||
|
<li v-for="item in parseContent(list)" :key="item.id ?? item.name ?? item">
|
||||||
|
{{ item.name ?? JSON.stringify(item) }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<UiLoading/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
h3{
|
||||||
|
font-size: 1.2em;
|
||||||
|
color:blueviolet
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
public: true, pageId: 'login'
|
||||||
|
})
|
||||||
|
const localePath = useLocalePath()
|
||||||
|
const authStore = useAuthStore() // Store authenticate
|
||||||
|
const login = ref('')
|
||||||
|
const password = ref('')
|
||||||
|
const awaiting = ref(false)
|
||||||
|
const displayPwdReset=true
|
||||||
|
|
||||||
|
const errors = ref({
|
||||||
|
loginEmpty: false,
|
||||||
|
passwordEmpty: false,
|
||||||
|
loginFailed: false,
|
||||||
|
unconfirmedUser: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleFormSubmit = async() => {
|
||||||
|
// On retire l'erreur précédente
|
||||||
|
errors.value.loginFailed = false;
|
||||||
|
errors.value.unconfirmedUser = false;
|
||||||
|
awaiting.value = true
|
||||||
|
// --- Vérification des données du formulaire --- //
|
||||||
|
|
||||||
|
// En version raccourcie : on stocke le résultat de la condition dans la variable
|
||||||
|
|
||||||
|
errors.value.loginEmpty = ( login.value == "" );
|
||||||
|
errors.value.passwordEmpty = ( password.value == "" );
|
||||||
|
|
||||||
|
// Si une erreur est rencontrée, on n'envoi pas la requete au serveur !
|
||||||
|
if( !errors.value.loginEmpty && !errors.value.passwordEmpty )
|
||||||
|
{
|
||||||
|
|
||||||
|
// Envoie de la requette à l'endpoint JWT et récupération d'un token de connexion
|
||||||
|
const success = await authStore.login(login.value, password.value)
|
||||||
|
if (success) {
|
||||||
|
// Redirection
|
||||||
|
await navigateTo(localePath('/lists'))
|
||||||
|
}
|
||||||
|
|
||||||
|
else{
|
||||||
|
errors.value.loginFailed = true;
|
||||||
|
awaiting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
awaiting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section>
|
||||||
|
<h1>{{$t('loginTitle')}}</h1>
|
||||||
|
<form @submit.prevent="handleFormSubmit">
|
||||||
|
<InputText
|
||||||
|
name="login"
|
||||||
|
:label="$t('loginLabelUser')"
|
||||||
|
placeholder=""
|
||||||
|
v-model="login"
|
||||||
|
>
|
||||||
|
<template #message>
|
||||||
|
<p v-if="errors.loginEmpty" class="error">{{$t('loginErrorLoginEmpty')}}</p>
|
||||||
|
</template>
|
||||||
|
</InputText>
|
||||||
|
|
||||||
|
<InputPassword
|
||||||
|
name="password"
|
||||||
|
:label="$t('loginLabelPwd')"
|
||||||
|
placeholder=""
|
||||||
|
v-model="password"
|
||||||
|
displayPwdReset
|
||||||
|
>
|
||||||
|
<template #message>
|
||||||
|
<p v-if="errors.passwordEmpty" class="error">{{ $t('loginErrorPwdEmpty') }}</p>
|
||||||
|
</template>
|
||||||
|
</InputPassword>
|
||||||
|
|
||||||
|
<div class="connection-error" >
|
||||||
|
<p v-if="errors.loginFailed">{{ $t('loginErrorLoginFailed') }}</p>
|
||||||
|
<p v-if="errors.unconfirmedUser">{{ $t('loginErrorUnconfirmedUSer') }}</p>
|
||||||
|
</div>
|
||||||
|
<ButtonBase
|
||||||
|
:disabled="login === '' || password === '' || awaiting"
|
||||||
|
:loading="awaiting"
|
||||||
|
>
|
||||||
|
{{ $t('loginFormBtn') }}
|
||||||
|
</ButtonBase>
|
||||||
|
</form>
|
||||||
|
<p>{{ $t('loginGoogle') }}</p>
|
||||||
|
<ButtonGoogle/>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 1em;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-error{
|
||||||
|
height:1.2em;
|
||||||
|
& > p{
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
color: rgb(185, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
<h1>
|
||||||
|
value is still : {{ counter }}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<NuxtLink to="/">
|
||||||
|
retour
|
||||||
|
</NuxtLink>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { counter } = useCounter()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
<template>
|
||||||
|
<h1>{{$t('pwdReset.title')}}</h1>
|
||||||
|
|
||||||
|
<form @submit.prevent="handleSubmit">
|
||||||
|
|
||||||
|
<InputPassword
|
||||||
|
name="password"
|
||||||
|
placeholder=""
|
||||||
|
:label="$t('pwdReset.pwdLabel1')"
|
||||||
|
v-model="password"
|
||||||
|
@input="check()"
|
||||||
|
>
|
||||||
|
<template #message>
|
||||||
|
<p v-if="pwErrors.isEmpty" class="error">{{ $t('pwdInput.errorPwdEmpty') }}</p>
|
||||||
|
<p v-if="pwErrors.isntValid" class="error">{{ $t('pwdInput.errorPwdIsntValid') }}</p>
|
||||||
|
</template>
|
||||||
|
</InputPassword>
|
||||||
|
<PasswordChecker/>
|
||||||
|
<InputPassword
|
||||||
|
name="passwordVerif"
|
||||||
|
placeholder=""
|
||||||
|
:label="$t('pwdReset.pwdLabel2')"
|
||||||
|
v-model="passwordVerif"
|
||||||
|
>
|
||||||
|
<template #message>
|
||||||
|
<p v-if="pwErrors.doesNotMatch" class="error">{{ $t('pwdInput.pwdsDoesNotMatch') }}</p>
|
||||||
|
<p v-if="pwErrors.verifyEmpty" class="error">{{ $t('pwdInput.pwdConfirmEmpty') }}</p>
|
||||||
|
</template>
|
||||||
|
</InputPassword>
|
||||||
|
|
||||||
|
<ButtonBase
|
||||||
|
:loading="loading"
|
||||||
|
:disabled="loading || success === true">
|
||||||
|
{{ $t('pwdReset.formBtn') }}
|
||||||
|
</ButtonBase>
|
||||||
|
</form>
|
||||||
|
<div v-if="success">
|
||||||
|
<p>{{ $t('pwdReset.successMessage') }}</p>
|
||||||
|
<NuxtLink :to="localePath('login')">{{ $t('pwdReset.loginLink') }}</NuxtLink>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="success===false" class="error">
|
||||||
|
<p>{{ $t('pwdReset.errorMessage') }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
public: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const localePath=useLocalePath()
|
||||||
|
const token = ref(route.query.token)
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
|
||||||
|
//** pwdReset up management **/
|
||||||
|
const passwordToolBox = usePasswordToolBoxStore()
|
||||||
|
|
||||||
|
// password relative functions
|
||||||
|
const check = () => {
|
||||||
|
passwordToolBox.updatePassword(password.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const password=ref('')
|
||||||
|
const passwordVerif=ref('')
|
||||||
|
|
||||||
|
const pwErrors = ref({
|
||||||
|
isEmpty: false as boolean,
|
||||||
|
verifyEmpty: false as boolean,
|
||||||
|
isntValid: false as boolean,
|
||||||
|
doesNotMatch:false as boolean,
|
||||||
|
})
|
||||||
|
const loading = ref(false as boolean)
|
||||||
|
const success = ref(null as null|boolean);
|
||||||
|
|
||||||
|
// form relative functions
|
||||||
|
const handleSubmit = async() => {
|
||||||
|
loading.value = true
|
||||||
|
//réinitialisation des erreurs
|
||||||
|
pwErrors.value.isEmpty = false;
|
||||||
|
pwErrors.value.verifyEmpty = false;
|
||||||
|
pwErrors.value.isntValid = false;
|
||||||
|
pwErrors.value.doesNotMatch = false;
|
||||||
|
// Check errors
|
||||||
|
pwErrors.value.isEmpty = ( password.value == "" );
|
||||||
|
pwErrors.value.verifyEmpty = ( passwordVerif.value == "" );
|
||||||
|
pwErrors.value.isntValid = !passwordToolBox.isPasswordValid();
|
||||||
|
pwErrors.value.doesNotMatch = ( password.value != passwordVerif.value );
|
||||||
|
|
||||||
|
if ( !pwErrors.value.isEmpty &&
|
||||||
|
!pwErrors.value.verifyEmpty &&
|
||||||
|
!pwErrors.value.isntValid &&
|
||||||
|
!pwErrors.value.doesNotMatch) {
|
||||||
|
success.value = await authStore.pwdReset(password.value, token.value)
|
||||||
|
}
|
||||||
|
loading.value = false
|
||||||
|
password.value = ""
|
||||||
|
passwordVerif.value = ""
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 1em;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error{
|
||||||
|
& > p{
|
||||||
|
font-weight: bold;
|
||||||
|
color: rgb(185, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
<template>
|
||||||
|
<h1>{{$t('pwdResetRequest.title')}}</h1>
|
||||||
|
<form @submit.prevent="handleFormSubmit()">
|
||||||
|
<InputEmail
|
||||||
|
v-model="email"
|
||||||
|
name="email"
|
||||||
|
:label="$t('signupLabelEmail')"
|
||||||
|
placeholder=""
|
||||||
|
:class="emailStatus"
|
||||||
|
|
||||||
|
>
|
||||||
|
<template #message>
|
||||||
|
<p v-if="emailErrors.isEmpty" class="error">{{ $t('signupErrorEmailEmpty') }}</p>
|
||||||
|
<p v-else-if="emailErrors.isntValid" class="error">{{ $t('signupErrorEmailIsntValid') }}</p>
|
||||||
|
<p v-else-if="emailValid" class="success">{{ $t('signupEmailIsValid') }}</p>
|
||||||
|
</template>
|
||||||
|
</InputEmail>
|
||||||
|
|
||||||
|
<ButtonBase
|
||||||
|
:disabled="!emailValid || awaiting"
|
||||||
|
:loading="awaiting">
|
||||||
|
{{ $t('pwdResetRequest.formBtn') }}
|
||||||
|
</ButtonBase>
|
||||||
|
|
||||||
|
<div class="error" v-if="formErrors.submitFailed">
|
||||||
|
<p>{{ $t('pwdResetRequest.pb') }}</p>
|
||||||
|
<p>{{ $t('pwdResetRequest.message') }} {{formErrors.message}}</p>
|
||||||
|
</div>
|
||||||
|
<div class="success" v-if="success">
|
||||||
|
<p>{{ $t('pwdResetRequest.successMessage') }}</p>
|
||||||
|
<p>{{ $t('pwdResetRequest.linkValidity') }}</p>
|
||||||
|
<p>{{ $t('pwdResetRequest.seeU') }}</p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
public: true
|
||||||
|
})
|
||||||
|
const { locale } = useI18n()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const email = ref("")
|
||||||
|
const awaiting = ref(false)
|
||||||
|
const emailErrors = ref({
|
||||||
|
isEmpty: false as boolean,
|
||||||
|
isntValid: false as boolean,
|
||||||
|
})
|
||||||
|
const formErrors = ref({
|
||||||
|
submitFailed: false as boolean,
|
||||||
|
message: null as string | null,
|
||||||
|
})
|
||||||
|
const success = ref(false)
|
||||||
|
|
||||||
|
const emailValid = computed(() => isValidEmail(email.value))
|
||||||
|
const emailStatus = computed(() => {
|
||||||
|
return emailValid.value ? "mail-is-valid" : "mail-is-invalid"
|
||||||
|
})
|
||||||
|
|
||||||
|
function isValidEmail(value: string):boolean {
|
||||||
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFormSubmit = async() => {
|
||||||
|
//* Errors reinitialisation...
|
||||||
|
awaiting.value = true
|
||||||
|
emailErrors.value.isEmpty = false;
|
||||||
|
emailErrors.value.isntValid = false;
|
||||||
|
formErrors.value.submitFailed = false;
|
||||||
|
formErrors.value.message = '';
|
||||||
|
success.value = false;
|
||||||
|
|
||||||
|
//* Now come the verifications
|
||||||
|
emailErrors.value.isEmpty = (email.value == '')
|
||||||
|
emailErrors.value.isntValid = ( !isValidEmail(email.value) );
|
||||||
|
|
||||||
|
if (emailErrors.value.isEmpty || emailErrors.value.isntValid){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const response = await authStore.pwdResetResquest(email.value, locale.value);
|
||||||
|
//* If we have a message, it means we have an error...
|
||||||
|
|
||||||
|
if (response == true){
|
||||||
|
success.value = true;
|
||||||
|
email.value = "";
|
||||||
|
awaiting.value = false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
formErrors.value.submitFailed = true;
|
||||||
|
formErrors.value.message = authStore.error;
|
||||||
|
awaiting.value = false;
|
||||||
|
success.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 1em;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
<template>
|
||||||
|
<div class="profile-page">
|
||||||
|
<h1>{{ $t('profile.title') }}</h1>
|
||||||
|
|
||||||
|
<!-- Onglets -->
|
||||||
|
<div role="tablist" aria-label="Profil utilisateur" class="tabs">
|
||||||
|
<button
|
||||||
|
:class="['tab', activeTab === 'general' ? 'active' : '']"
|
||||||
|
role="tab"
|
||||||
|
:aria-selected="activeTab === 'general'"
|
||||||
|
aria-controls="tab-general"
|
||||||
|
id="tab-button-general"
|
||||||
|
@click="activeTab = 'general'; authStore.user.sudo_token = null"
|
||||||
|
>
|
||||||
|
{{ $t('profile.tabGeneral') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
:class="['tab', activeTab === 'redzone' ? 'active' : '']"
|
||||||
|
class='redzone'
|
||||||
|
role="tab"
|
||||||
|
:aria-selected="activeTab === 'redzone'"
|
||||||
|
aria-controls="tab-redzone"
|
||||||
|
id="tab-button-redzone"
|
||||||
|
@click="activeTab = 'redzone'; authStore.user.sudo_token = null"
|
||||||
|
>
|
||||||
|
{{ $t('profile.tabRedZone')}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Contenu des onglets -->
|
||||||
|
<div class="tab-panels">
|
||||||
|
<!-- Onglet Général -->
|
||||||
|
<section
|
||||||
|
v-show="activeTab === 'general'"
|
||||||
|
role="tabpanel"
|
||||||
|
aria-labelledby="tab-button-general"
|
||||||
|
id="tab-general"
|
||||||
|
>
|
||||||
|
<ProfileGeneral />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Onglet RED ZONE -->
|
||||||
|
<section
|
||||||
|
v-show="activeTab === 'redzone'"
|
||||||
|
role="tabpanel"
|
||||||
|
aria-labelledby="tab-button-redzone"
|
||||||
|
id="tab-redzone"
|
||||||
|
>
|
||||||
|
<ProfileRedZone />
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
// Onglet actif (TypeScript infère automatiquement le type string)
|
||||||
|
const activeTab = ref('general')
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
/* Onglets */
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 3px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
//
|
||||||
|
.tab:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
border-bottom-color: blueviolet;
|
||||||
|
color: blueviolet;
|
||||||
|
}
|
||||||
|
button.redzone{
|
||||||
|
color:red;
|
||||||
|
&.active{
|
||||||
|
border-bottom-color: red;
|
||||||
|
color:red;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Contenu onglet */
|
||||||
|
.tab-panels section {
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation simple */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(5px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Executable
+160
@@ -0,0 +1,160 @@
|
|||||||
|
<template>
|
||||||
|
<section>
|
||||||
|
<UiModale @close="closeModal" :modalActive="modalActive">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h1 class="modal-title">{{ $t('modalTitle') }}</h1>
|
||||||
|
<p>{{ $t('modalText') }}</p>
|
||||||
|
</div>
|
||||||
|
</UiModale>
|
||||||
|
<h1>{{ $t('signupTitle') }}</h1>
|
||||||
|
|
||||||
|
<form @submit.prevent="handleSubmit">
|
||||||
|
<InputEmail
|
||||||
|
v-model="email"
|
||||||
|
name="email"
|
||||||
|
:label="$t('signupLabelEmail')"
|
||||||
|
placeholder=""
|
||||||
|
:class="emailStatus"
|
||||||
|
@blur="touched = true"
|
||||||
|
>
|
||||||
|
<template #message>
|
||||||
|
<p v-if="emailErrors.isEmpty" class="error">{{ $t('signupErrorEmailEmpty') }}</p>
|
||||||
|
<p v-else-if="emailErrors.isntValid" class="error">{{ $t('signupErrorEmailIsntValid') }}</p>
|
||||||
|
<p v-else-if="emailValid" class="success">{{ $t('signupEmailIsValid') }}</p>
|
||||||
|
</template>
|
||||||
|
</InputEmail>
|
||||||
|
|
||||||
|
<InputPassword
|
||||||
|
name="password"
|
||||||
|
placeholder=""
|
||||||
|
:label="$t('signupLabelPwd')"
|
||||||
|
v-model="password"
|
||||||
|
@input="check()"
|
||||||
|
>
|
||||||
|
<template #message>
|
||||||
|
<p v-if="pwErrors.isEmpty" class="error">{{ $t('signupErrorPwdEmpty') }}</p>
|
||||||
|
<p v-if="pwErrors.isntValid" class="error">{{ $t('signupErrorPwsIsntValid') }}</p>
|
||||||
|
</template>
|
||||||
|
</InputPassword>
|
||||||
|
<PasswordChecker/>
|
||||||
|
<InputPassword
|
||||||
|
name="passwordVerif"
|
||||||
|
placeholder=""
|
||||||
|
:label="$t('signupLabelConfirmPwd')"
|
||||||
|
v-model="passwordVerif"
|
||||||
|
>
|
||||||
|
<template #message>
|
||||||
|
<p v-if="pwErrors.doesNotMatch" class="error">{{ $t('signupErrorPwdsDoesNotMatch') }}</p>
|
||||||
|
<p v-if="pwErrors.verifyEmpty" class="error">{{ $t('signupErrorPwdConfirmEmpty') }}</p>
|
||||||
|
</template>
|
||||||
|
</InputPassword>
|
||||||
|
|
||||||
|
<ButtonBase>
|
||||||
|
{{ $t('signupFormBtn') }}
|
||||||
|
</ButtonBase>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
public: true
|
||||||
|
})
|
||||||
|
const { locale } = useI18n()
|
||||||
|
//** Modale management **//
|
||||||
|
const localePath=useLocalePath()
|
||||||
|
const modalActive = ref(false)
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
modalActive.value = false
|
||||||
|
navigateTo(localePath('lists'))
|
||||||
|
}
|
||||||
|
|
||||||
|
//** Sign up management **/
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const passwordToolBox = usePasswordToolBoxStore()
|
||||||
|
// email relative functions
|
||||||
|
const email = ref("")
|
||||||
|
const touched = ref(false)
|
||||||
|
|
||||||
|
function isValidEmail(value: string) {
|
||||||
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const emailValid = computed(() => touched.value && isValidEmail(email.value))
|
||||||
|
const emailErrors = ref({
|
||||||
|
isEmpty: false as boolean,
|
||||||
|
isntValid: false as boolean
|
||||||
|
})
|
||||||
|
//const emailInvalid = computed(() => touched.value && !isValidEmail(email.value))
|
||||||
|
|
||||||
|
const emailStatus = computed(() => {
|
||||||
|
if (!touched.value) return ""
|
||||||
|
return emailValid.value ? "mail-is-valid" : "mail-is-invalid"
|
||||||
|
})
|
||||||
|
|
||||||
|
// password relative functions
|
||||||
|
const check = () => {
|
||||||
|
passwordToolBox.updatePassword(password.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const password=ref('')
|
||||||
|
const passwordVerif=ref('')
|
||||||
|
|
||||||
|
const pwErrors = ref({
|
||||||
|
isEmpty: false as boolean,
|
||||||
|
verifyEmpty: false as boolean,
|
||||||
|
isntValid: false as boolean,
|
||||||
|
doesNotMatch:false as boolean,
|
||||||
|
})
|
||||||
|
|
||||||
|
// form relative functions
|
||||||
|
const handleSubmit = async() => {
|
||||||
|
//réinitialisation des erreurs
|
||||||
|
emailErrors.value.isEmpty = false
|
||||||
|
emailErrors.value.isntValid = false
|
||||||
|
pwErrors.value.isEmpty = false;
|
||||||
|
pwErrors.value.verifyEmpty = false;
|
||||||
|
pwErrors.value.isntValid = false;
|
||||||
|
pwErrors.value.doesNotMatch = false;
|
||||||
|
// Check errors
|
||||||
|
emailErrors.value.isEmpty = ( email.value == "" );
|
||||||
|
emailErrors.value.isntValid = !isValidEmail(email.value)
|
||||||
|
pwErrors.value.isEmpty = ( password.value == "" );
|
||||||
|
pwErrors.value.verifyEmpty = ( passwordVerif.value == "" );
|
||||||
|
pwErrors.value.isntValid = !passwordToolBox.isPasswordValid();
|
||||||
|
pwErrors.value.doesNotMatch = ( password.value != passwordVerif.value );
|
||||||
|
|
||||||
|
if ( !emailErrors.value.isEmpty &&
|
||||||
|
!emailErrors.value.isntValid &&
|
||||||
|
!pwErrors.value.isEmpty &&
|
||||||
|
!pwErrors.value.verifyEmpty &&
|
||||||
|
!pwErrors.value.isntValid &&
|
||||||
|
!pwErrors.value.doesNotMatch) {
|
||||||
|
const success = await authStore.register(email.value, password.value, locale.value)
|
||||||
|
if (success) {
|
||||||
|
modalActive.value=true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 1em;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-error{
|
||||||
|
height:1.2em;
|
||||||
|
& > p{
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
color: rgb(185, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import ListsRepository from '~/repositories/lists.repository';
|
||||||
|
import UserRepository from '~/repositories/user.repository';
|
||||||
|
|
||||||
|
export default defineNuxtPlugin(() => {
|
||||||
|
const config = useRuntimeConfig();
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
|
const apiFetcher = $fetch.create({
|
||||||
|
baseURL: config.public.apiBase,
|
||||||
|
|
||||||
|
onRequest({ options }) {
|
||||||
|
const headers = new Headers(options.headers);
|
||||||
|
if (authStore.token) {
|
||||||
|
headers.set('Authorization', `Bearer ${authStore.token}`);
|
||||||
|
}
|
||||||
|
if (authStore.user?.sudo_token){
|
||||||
|
headers.set('sudo_token', authStore.user?.sudo_token);
|
||||||
|
}
|
||||||
|
options.headers = headers;
|
||||||
|
},
|
||||||
|
|
||||||
|
async onResponseError({ response }) {
|
||||||
|
// Cas 401 : Session expirée ou non autorisée
|
||||||
|
if (response.status === 401) {
|
||||||
|
// Optionnel : nettoyer le store avant d'afficher l'erreur
|
||||||
|
authStore.logout();
|
||||||
|
|
||||||
|
// On déclenche la page d'erreur Nuxt
|
||||||
|
throw showError({
|
||||||
|
statusCode: 401,
|
||||||
|
statusMessage: 'Session expirée ou accès non autorisé',
|
||||||
|
fatal: true // 'fatal: true' force le rendu de la page d'erreur même côté client
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optionnel : Gérer d'autres codes
|
||||||
|
if (response.status >= 500) {
|
||||||
|
console.error("Erreur serveur, réessayez plus tard.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
provide: {
|
||||||
|
api: {
|
||||||
|
lists: new ListsRepository(apiFetcher as any),
|
||||||
|
user: new UserRepository(apiFetcher as any)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
// Faire vérifier le token présent dans le cookie
|
||||||
|
// à WP via Pinia au 1er chargement de l'app.
|
||||||
|
|
||||||
|
import type { User } from "~/types/auth.ts"
|
||||||
|
export default defineNuxtPlugin(async () => {
|
||||||
|
const auth = useAuthStore()
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
|
||||||
|
// On n'exécute la vérification que s'il y a un token
|
||||||
|
// et qu'on n'a pas encore chargé l'utilisateur (ex: au refresh)
|
||||||
|
if (auth.token && !auth.user) {
|
||||||
|
try {
|
||||||
|
// On appelle le endpoint WP qui renvoie l'utilisateur connecté
|
||||||
|
// Note: l'URL dépend du plugin perso auth
|
||||||
|
const data = await $fetch<User>(`${config.public.apiBase}/users/me`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${auth.token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Si ça marche, on met à jour l'user dans Pinia
|
||||||
|
auth.user = data
|
||||||
|
} catch (error) {
|
||||||
|
// Si le token est expiré ou invalide, WP renvoie une erreur
|
||||||
|
// On vide tout pour forcer la reconnexion
|
||||||
|
console.error("Session expirée ou token invalide")
|
||||||
|
auth.logout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// Font awesome
|
||||||
|
/* import the fontawesome core */
|
||||||
|
import { library, config } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
/* import font awesome icon component */
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||||
|
|
||||||
|
// import icons //
|
||||||
|
import { faEye, faEyeSlash, faLessThan, faGreaterThan, faPlus, faMinus, faLanguage, faGear, faLocationDot, faArrowsRotate, faDiamondTurnRight} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { faCircleCheck, faCircleXmark, faSquare, faSquareCheck, faTrashCan} from '@fortawesome/free-regular-svg-icons'
|
||||||
|
|
||||||
|
export default defineNuxtPlugin((nuxtApp) => {
|
||||||
|
library.add(faCircleCheck,
|
||||||
|
faCircleXmark,
|
||||||
|
faEye,
|
||||||
|
faEyeSlash,
|
||||||
|
faSquare,
|
||||||
|
faSquareCheck,
|
||||||
|
faTrashCan,
|
||||||
|
faLessThan,
|
||||||
|
faGreaterThan,
|
||||||
|
faPlus,
|
||||||
|
faMinus,
|
||||||
|
faGear,
|
||||||
|
faLocationDot,
|
||||||
|
faArrowsRotate,
|
||||||
|
faDiamondTurnRight,
|
||||||
|
faLanguage)
|
||||||
|
nuxtApp.vueApp.component('FontAwesomeIcon', FontAwesomeIcon)
|
||||||
|
})
|
||||||
|
|
||||||
|
import '@fortawesome/fontawesome-svg-core/styles.css'
|
||||||
|
|
||||||
|
// Empêche FontAwesome d’injecter son CSS automatiquement
|
||||||
|
config.autoAddCss = false
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import type { $Fetch, SearchParameters } from 'ofetch';
|
||||||
|
|
||||||
|
export async function fetchWithRepo<T>(
|
||||||
|
fetcher: $Fetch,
|
||||||
|
url: string,
|
||||||
|
method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET',
|
||||||
|
body?: any,
|
||||||
|
params?: SearchParameters
|
||||||
|
): Promise<T> {
|
||||||
|
return await fetcher(url, { method, body, params });
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import type { $Fetch } from 'ofetch';
|
||||||
|
import type { List } from '~/types/lists'
|
||||||
|
|
||||||
|
export default class ListRepository {
|
||||||
|
private fetcher: $Fetch;
|
||||||
|
|
||||||
|
constructor(fetcher: $Fetch) {
|
||||||
|
this.fetcher = fetcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAll() {
|
||||||
|
return await this.fetcher<List[]>('/lists');
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(data: Partial<List>) {
|
||||||
|
return await this.fetcher<List>('/lists', {
|
||||||
|
method: 'POST',
|
||||||
|
body: data
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import type { $Fetch } from 'ofetch';
|
||||||
|
import type { User, ConfirmResult } from '~/types/auth'
|
||||||
|
|
||||||
|
export default class UserRepository {
|
||||||
|
private fetcher: $Fetch;
|
||||||
|
|
||||||
|
constructor(fetcher: $Fetch) {
|
||||||
|
this.fetcher = fetcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
async confirm(token: string){
|
||||||
|
return await this.fetcher<ConfirmResult>('/auth/confirm', {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
token
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async register(email: string, password:string, locale:string){
|
||||||
|
return await this.fetcher<ConfirmResult>('/user/register', {
|
||||||
|
method: 'POST',
|
||||||
|
body:{
|
||||||
|
email, password, locale
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async emailChange(newEmail: string, locale:string){
|
||||||
|
return await this.fetcher<ConfirmResult>('/user/email-update/request', {
|
||||||
|
method: 'POST',
|
||||||
|
body:{
|
||||||
|
newEmail, locale
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateDisplayName(newDisplayName: string){
|
||||||
|
return await this.fetcher<ConfirmResult>('/user/update', {
|
||||||
|
method: 'PUT',
|
||||||
|
body:{
|
||||||
|
display_name : newDisplayName
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteRequest(locale:string){
|
||||||
|
return await this.fetcher<ConfirmResult>('/user/delete-request', {
|
||||||
|
method:'DELETE',
|
||||||
|
body:{
|
||||||
|
locale
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async pwdChallenge(pwd:string){
|
||||||
|
return await this.fetcher<ConfirmResult>('/user/pwdChallenge', {
|
||||||
|
method:'POST',
|
||||||
|
body:{
|
||||||
|
pwd
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import UserRepository from '~/repositories/user.repository'
|
||||||
|
import type { User, LoginResponse, ConfirmResult } from '~/types/auth'
|
||||||
|
|
||||||
|
|
||||||
|
export const useAuthStore = defineStore('auth', {
|
||||||
|
state: () => ({
|
||||||
|
// On lie l'état directement au cookie
|
||||||
|
token: useCookie<string | null>('auth_token', {
|
||||||
|
// secure: false,
|
||||||
|
// sameSite: 'lax',
|
||||||
|
path: '/',
|
||||||
|
maxAge: 60 * 60 * 24 * 7
|
||||||
|
}), // Expire après 7 jours
|
||||||
|
user: useCookie<User | null>('auth_user', {
|
||||||
|
// secure: false,
|
||||||
|
// sameSite: 'lax',
|
||||||
|
path: '/',
|
||||||
|
maxAge: 60 * 60 * 24 * 7
|
||||||
|
}),
|
||||||
|
loading: false,
|
||||||
|
error: null as string | null,
|
||||||
|
}),
|
||||||
|
|
||||||
|
getters: {
|
||||||
|
// Le "!!" transforme la valeur en vrai BOULÉEN (true/false)
|
||||||
|
isLoggedIn: (state) => !!state.token,
|
||||||
|
|
||||||
|
// plus tard on pourras ajouter un getter pour récupérer le prénom
|
||||||
|
//userName: (state) => state.user?.name || 'Invité'
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
setTokenCookie(newToken: string | null) {
|
||||||
|
const cookie = useCookie('auth_token')
|
||||||
|
cookie.value = newToken // On force l'écriture
|
||||||
|
this.token = newToken // On met à jour le state Pinia
|
||||||
|
},
|
||||||
|
|
||||||
|
setUserCookie(newUser: User | null) {
|
||||||
|
// 1. On récupère le cookie sans typage strict ou typé en 'any' pour l'écriture
|
||||||
|
const userCookie = useCookie<any>('auth_user', {
|
||||||
|
path: '/',
|
||||||
|
maxAge: 60 * 60 * 24 * 7,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 2. On assigne l'objet (Nuxt va faire le JSON.stringify en interne)
|
||||||
|
userCookie.value = newUser
|
||||||
|
|
||||||
|
// 3. On met à jour le state Pinia
|
||||||
|
this.user = newUser
|
||||||
|
},
|
||||||
|
|
||||||
|
async register(email: string, password:string, locale:string ){
|
||||||
|
const { $api } = useNuxtApp();
|
||||||
|
const listsStore = useListStore()
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
this.loading = true
|
||||||
|
this.error = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await $api.user.register(email, password, locale)
|
||||||
|
|
||||||
|
// En cas de réussite, le nouveau user est connecté.
|
||||||
|
// On assigne les valeurs : useCookie met à jour le state ET le navigateur
|
||||||
|
this.setTokenCookie(data.token)
|
||||||
|
this.setUserCookie(data.user)
|
||||||
|
listsStore.saveLists(data.lists)
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (err: any) {
|
||||||
|
// En cas d'erreur, on nettoie les cookies
|
||||||
|
this.token = null
|
||||||
|
this.user = null
|
||||||
|
this.error = err.data?.message || "Erreur de connexion"
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async confirmUser(token: string) {
|
||||||
|
const { $api } = useNuxtApp();
|
||||||
|
//console.log(token)
|
||||||
|
this.error = null
|
||||||
|
try {
|
||||||
|
const res: ConfirmResult = await $api.user.confirm(token)
|
||||||
|
return res
|
||||||
|
} catch (err: any) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async login(login: string, password: string) {
|
||||||
|
const listsStore = useListStore()
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
this.loading = true
|
||||||
|
this.error = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await $fetch<LoginResponse>(`${config.public.apiBase}/auth/login`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: { login, password }
|
||||||
|
})
|
||||||
|
|
||||||
|
// On assigne les valeurs : useCookie met à jour le state ET le navigateur
|
||||||
|
this.setTokenCookie(data.token)
|
||||||
|
this.setUserCookie(data.user)
|
||||||
|
listsStore.saveLists(data.lists)
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (err: any) {
|
||||||
|
// En cas d'erreur, on nettoie les cookies
|
||||||
|
this.token = null
|
||||||
|
this.user = null
|
||||||
|
this.error = err.data?.message || "Erreur de connexion"
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
logout() {
|
||||||
|
this.setTokenCookie(null)
|
||||||
|
this.setUserCookie(null)
|
||||||
|
const listsStore = useListStore()
|
||||||
|
listsStore.resetLists()
|
||||||
|
return navigateTo('/')
|
||||||
|
},
|
||||||
|
|
||||||
|
async pwdResetResquest( email: string, locale: string) {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
this.error = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await $fetch<boolean>(`${config.public.apiBase}/user/pwdReset`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: { email, locale }
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
} catch (err: any) {
|
||||||
|
|
||||||
|
this.error = err.data?.message || "Erreur de connexion"
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async pwdReset( password: string, token: any) {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
this.error = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await $fetch<boolean>(`${config.public.apiBase}/user/pwdReset`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: { password, token }
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
} catch (err: any) {
|
||||||
|
|
||||||
|
this.error = err.data?.message || "Erreur de connexion"
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async emailChange( newEmail:string, locale:string){
|
||||||
|
const { $api } = useNuxtApp();
|
||||||
|
|
||||||
|
// const config = useRuntimeConfig()
|
||||||
|
this.error = null;
|
||||||
|
|
||||||
|
if ( !this.user?.email ){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const data = await $api.user.emailChange(newEmail, locale)
|
||||||
|
return data
|
||||||
|
} catch (err: any) {
|
||||||
|
|
||||||
|
this.error = err.data?.message || "Erreur de connexion"
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateDisplayName(newDisplayName:string){
|
||||||
|
const { $api } = useNuxtApp();
|
||||||
|
|
||||||
|
// const config = useRuntimeConfig()
|
||||||
|
this.error = null;
|
||||||
|
|
||||||
|
if ( !this.user?.email ){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const data = await $api.user.updateDisplayName(newDisplayName)
|
||||||
|
this.user.display_name = newDisplayName
|
||||||
|
return data
|
||||||
|
} catch (err: any) {
|
||||||
|
this.error = err.data?.message || "Erreur de connexion"
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteRequest(locale:string){
|
||||||
|
const { $api } = useNuxtApp();
|
||||||
|
this.error = null
|
||||||
|
|
||||||
|
if ( !this.user?.email ){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await $api.user.deleteRequest(locale)
|
||||||
|
return data
|
||||||
|
} catch (err: any) {
|
||||||
|
this.error = err.data?.message || "Erreur de connexion"
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async pwdChallenge(pwd:string){
|
||||||
|
const { $api } = useNuxtApp();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await $api.user.pwdChallenge(pwd)
|
||||||
|
console.log(data)
|
||||||
|
if (this.user){
|
||||||
|
this.user.sudo_token = data.sudo_token
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err.response?.status === 403) {
|
||||||
|
console.warn("Mauvais mot de passe, mais on garde la session active.");
|
||||||
|
return false; // On renvoie false pour afficher une erreur dans l'UI
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const useBurgerStore = defineStore('burger', {
|
||||||
|
state: () => ({
|
||||||
|
checked: false as boolean,
|
||||||
|
}),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
toggle() {
|
||||||
|
this.checked = !this.checked
|
||||||
|
},
|
||||||
|
|
||||||
|
open() {
|
||||||
|
this.checked = true
|
||||||
|
},
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.checked = false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import type { List } from '~/types/lists'
|
||||||
|
|
||||||
|
|
||||||
|
export const useListStore = defineStore('lists', {
|
||||||
|
state: () => ({
|
||||||
|
lists: [] as List[],
|
||||||
|
loading: false as boolean,
|
||||||
|
}),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
saveLists(lists:Array<List>){
|
||||||
|
this.lists = lists
|
||||||
|
},
|
||||||
|
resetLists(){
|
||||||
|
this.lists = []
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchLists() {
|
||||||
|
// On récupère notre plugin API injecté
|
||||||
|
const { $api } = useNuxtApp();
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// L'appel est maintenant ultra simple et typé
|
||||||
|
const data = await $api.lists.getAll();
|
||||||
|
this.lists = data;
|
||||||
|
} catch (error) {
|
||||||
|
// La gestion d'erreur est centralisée,
|
||||||
|
// mais tu peux ajouter une logique spécifique ici (ex: notification)
|
||||||
|
console.error("Erreur lors du chargement des listes:", error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// async updateList(id, title, content) {
|
||||||
|
// const config = useRuntimeConfig();
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// const data = await $fetch<[]|null>(`${config.public.apiBase}/lists`, {
|
||||||
|
// method: 'GET',
|
||||||
|
// headers: {
|
||||||
|
// // On injecte le token ici
|
||||||
|
// 'Authorization': `Bearer ${this.token}`
|
||||||
|
// },
|
||||||
|
// // Si tu as besoin d'envoyer un corps de message vide ou spécifique
|
||||||
|
// // body: {}
|
||||||
|
// });
|
||||||
|
|
||||||
|
// this.lists = data;
|
||||||
|
// console.log(data);
|
||||||
|
// console.log(this.token);
|
||||||
|
// return data;
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error("Erreur lors de la récupération des listes:", error);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const useParamsStore = defineStore('params', {
|
||||||
|
|
||||||
|
state: () => ({
|
||||||
|
paramsData: false,
|
||||||
|
}),
|
||||||
|
|
||||||
|
actions:
|
||||||
|
{
|
||||||
|
async findAll(data = null)
|
||||||
|
{
|
||||||
|
if (data){
|
||||||
|
this.paramsData = JSON.parse(data)
|
||||||
|
//console.log(data)
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
this.paramsData = JSON.parse(await paramsService.findAll())
|
||||||
|
//if (this.paramsData)
|
||||||
|
}
|
||||||
|
//console.log(this.paramsData)
|
||||||
|
},
|
||||||
|
|
||||||
|
updateParams(){
|
||||||
|
paramsService.update(JSON.stringify(this.paramsData))
|
||||||
|
},
|
||||||
|
|
||||||
|
changePage(page){
|
||||||
|
this.paramsData.profil_and_params_view = page
|
||||||
|
this.updateParams();
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteColor(){
|
||||||
|
|
||||||
|
if(this.paramsData.colors.length > 1){
|
||||||
|
|
||||||
|
const deletedColor = this.paramsData.colors.pop()
|
||||||
|
if (this.paramsData.unavailable_colors == undefined){
|
||||||
|
this.paramsData = {
|
||||||
|
colors : this.paramsData.colors,
|
||||||
|
unavailable_colors : [],
|
||||||
|
profil_and_params_view : this.paramsData.profil_and_params_view
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.paramsData.unavailable_colors.push(deletedColor)
|
||||||
|
}
|
||||||
|
this.updateParams();
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
addColor(){
|
||||||
|
if(this.paramsData.colors.length < 8){
|
||||||
|
const color = this.paramsData.unavailable_colors.pop()
|
||||||
|
this.paramsData.colors.push(color)
|
||||||
|
this.updateParams();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
modifyColor(colorIndex, newColorValue) {
|
||||||
|
this.paramsData.colors[colorIndex] = newColorValue
|
||||||
|
this.updateParams();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const usePasswordToolBoxStore = defineStore('passwordToolBox', {
|
||||||
|
state: () => ({
|
||||||
|
password: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
|
||||||
|
// --- REGEX ---
|
||||||
|
regNumberOfCaracteres: /^.{8,22}$/,
|
||||||
|
regSpecialCaractere: /[^A-Za-z0-9]/,
|
||||||
|
regCapitalizeCaractere: /[A-Z]/,
|
||||||
|
regMinimizeCaractere: /[a-z]/,
|
||||||
|
regNumber: /\d/,
|
||||||
|
regPassword: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9]).{8,22}$/,
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
updatePassword(newPassword: string) {
|
||||||
|
this.password = newPassword
|
||||||
|
},
|
||||||
|
updateConfirmPassword(newConfirm: string) {
|
||||||
|
this.confirmPassword = newConfirm
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- CHECK FUNCTIONS ---
|
||||||
|
checkRegex(regex: RegExp) {
|
||||||
|
return regex.test(this.password)
|
||||||
|
},
|
||||||
|
|
||||||
|
isNumberOfCaracteresValid() {
|
||||||
|
return this.checkRegex(this.regNumberOfCaracteres)
|
||||||
|
},
|
||||||
|
isSpecialCaractereValid() {
|
||||||
|
return this.checkRegex(this.regSpecialCaractere)
|
||||||
|
},
|
||||||
|
isCapitalizeCaractereValid() {
|
||||||
|
return this.checkRegex(this.regCapitalizeCaractere)
|
||||||
|
},
|
||||||
|
isMinimizeCaractereValid() {
|
||||||
|
return this.checkRegex(this.regMinimizeCaractere)
|
||||||
|
},
|
||||||
|
isNumberValid() {
|
||||||
|
return this.checkRegex(this.regNumber)
|
||||||
|
},
|
||||||
|
isPasswordValid() {
|
||||||
|
return this.checkRegex(this.regPassword)
|
||||||
|
},
|
||||||
|
isPasswordConfirmationValid() {
|
||||||
|
return this.password === this.confirmPassword && this.isPasswordValid()
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- UI HELPER ---
|
||||||
|
uiClass(valid: boolean) {
|
||||||
|
return valid ? 'has-success' : 'has-error'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
export interface User {
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
email: string
|
||||||
|
confirmed: boolean
|
||||||
|
role: string
|
||||||
|
display_name: string
|
||||||
|
avatar: string
|
||||||
|
is_google: boolean
|
||||||
|
sudo_token:string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginResponse {
|
||||||
|
user: User
|
||||||
|
token: string
|
||||||
|
lists: any[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ConfirmResult = true | false | 'expired'
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
export interface List {
|
||||||
|
id: number;
|
||||||
|
list_title: string;
|
||||||
|
list_type: string;
|
||||||
|
encrypted_content: string; // JSON string
|
||||||
|
is_open: boolean;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface taskItem {
|
||||||
|
id : number;
|
||||||
|
item_title : string;
|
||||||
|
is_done : boolean;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface tradItem {
|
||||||
|
id : number;
|
||||||
|
item_title : string;
|
||||||
|
trad : string
|
||||||
|
display_verso : boolean;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface positionItem {
|
||||||
|
id : number;
|
||||||
|
item_title : string;
|
||||||
|
position : string;
|
||||||
|
color : string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
{
|
||||||
|
"nav":{
|
||||||
|
"home":"Home",
|
||||||
|
"login": "Login",
|
||||||
|
"logout":"Logout",
|
||||||
|
"signup": "Sign up",
|
||||||
|
"profile": "Profile",
|
||||||
|
"lists": "My Lists"
|
||||||
|
},
|
||||||
|
|
||||||
|
"index":{
|
||||||
|
"title": "Welcome on the awesome lists manager !",
|
||||||
|
"subtitle1":"Overview",
|
||||||
|
"mainText": "<p>Create your own lists of places, foreign language words (like flashcards), or simply a todo list. Have fun, it’s really useful!</p><p>The source code for this project is <a href=\"https://github.com/Raffiskender\" target=\"_blank\">available here</a>.</p><p>In this 3rd version, the back office is still handled by two WP plugins, BUT their architecture has been completely redesigned. The user management plugin via REST API is now lighter and communicates with the list management plugin through hooks or filters.</p><p>The front end is built with Nuxt JS.</p>",
|
||||||
|
"lastSentence":"To get started, you need to {login} or {signup}, or...",
|
||||||
|
"lastSentenceUnconnect": "To get started, you need to {login} or {signup} or even...",
|
||||||
|
"login":"login",
|
||||||
|
"signup": "sign up",
|
||||||
|
"lastSentenceConnected": "You are logged in, you can now",
|
||||||
|
"seeLists": "access your lists"
|
||||||
|
},
|
||||||
|
|
||||||
|
"googleBtnTxt":"Sign in with Google",
|
||||||
|
|
||||||
|
"loginTitle": "Login",
|
||||||
|
"loginLabelUser": "Username or email",
|
||||||
|
"loginLabelPwd": "Password",
|
||||||
|
"loginErrorPwdEmpty": "You must enter a password",
|
||||||
|
"loginErrorLoginEmpty": "You must enter a username",
|
||||||
|
"loginErrorLoginFailed": "Invalid credentials",
|
||||||
|
"loginErrorUnconfirmedUser": "You cannot log in because your email has not been verified!",
|
||||||
|
"loginFormBtn": "Log In",
|
||||||
|
|
||||||
|
"loginGoogle":"Or take the easy way...",
|
||||||
|
"loginPage":{
|
||||||
|
"passwordResetRequest":"Forgot password?"
|
||||||
|
},
|
||||||
|
"signupTitle": "Sign Up",
|
||||||
|
"signupLabelEmail": "Email",
|
||||||
|
"signupLabelPwd": "Password",
|
||||||
|
"signupLabelConfirmPwd": "Confirm password",
|
||||||
|
"signupErrorEmailEmpty": "You must enter an email",
|
||||||
|
"signupErrorEmailIsntValid": "Invalid email",
|
||||||
|
"signupEmailIsValid": "Email validated",
|
||||||
|
"signupFormBtn": "Sign up",
|
||||||
|
|
||||||
|
"modalTitle": "Registration successful!",
|
||||||
|
"modalText": "You can now close this window and start managing your lists.",
|
||||||
|
|
||||||
|
"passwordCheckerShallContain": "Must contain",
|
||||||
|
"passwordChecker8-22": "between 8 and 22 characters,",
|
||||||
|
"passwordCheckerUppercase": "one uppercase letter,",
|
||||||
|
"passwordCheckerLowercase": "one lowercase letter,",
|
||||||
|
"passwordCheckerNb": "one number,",
|
||||||
|
"passwordCheckerSpecialChar": "one special character",
|
||||||
|
|
||||||
|
"pwdInput":{
|
||||||
|
"errorPwdEmpty":"You must enter a password",
|
||||||
|
"errorPwdIsntValid":"You must choose a stronger password",
|
||||||
|
"pwdsDoesNotMatch":"Passwords do not match!",
|
||||||
|
"pwdConfirmEmpty":"You must confirm your password!"
|
||||||
|
},
|
||||||
|
|
||||||
|
"confirmation": {
|
||||||
|
"title": "Confirmation page",
|
||||||
|
"successConnected": "Your email has been confirmed! You are now logged in.",
|
||||||
|
"successNotConnected": "Your email has been confirmed! Please log in to continue:",
|
||||||
|
"failureMessage": "Your email could not be confirmed.",
|
||||||
|
"failureCauseInvalid": "Reason: Invalid",
|
||||||
|
"failureCauseExpired": "Reason: Expired",
|
||||||
|
"failureYouCan": "You can",
|
||||||
|
"failureCreateNewAccount": "create a new account",
|
||||||
|
"failureOr": "or",
|
||||||
|
"backHome": "go back to the home page",
|
||||||
|
"loginLink": "Log in",
|
||||||
|
"listsLink": "View my lists"
|
||||||
|
},
|
||||||
|
"unconfirmedBanner":{
|
||||||
|
"message":"Email not verified"
|
||||||
|
},
|
||||||
|
"pwdResetRequest": {
|
||||||
|
"title": "Reset Password",
|
||||||
|
"formBtn":"Send",
|
||||||
|
"pb": "An error occurred while submitting the form.",
|
||||||
|
"message": "Message: ",
|
||||||
|
"successMessage": "Sent! If your email is in our database, you will receive an email with a link to change your password.",
|
||||||
|
"linkValidity": "This link will expire in 30 minutes.",
|
||||||
|
"seeU": "See you soon!"
|
||||||
|
},
|
||||||
|
|
||||||
|
"pwdReset":{
|
||||||
|
"title":"Reset your password",
|
||||||
|
"pwdLabel1":"New password",
|
||||||
|
"pwdLabel2":"Confirm new password",
|
||||||
|
"formBtn":"Send",
|
||||||
|
"successMessage":"Your password has been successfully reset. You can now access your account by visiting the login page:",
|
||||||
|
"loginLink":"Login",
|
||||||
|
"errorMessage":"An error occurred while resetting your password. Please try again."
|
||||||
|
},
|
||||||
|
|
||||||
|
"profile":{
|
||||||
|
"title":"Profile",
|
||||||
|
"tabGeneral":"General",
|
||||||
|
"tabRedZone": "RED ZONE",
|
||||||
|
"username":"Username",
|
||||||
|
"name" : "Display name:",
|
||||||
|
"email": "Email:",
|
||||||
|
"avatar": "Avatar"
|
||||||
|
},
|
||||||
|
|
||||||
|
"emailUpdate":{
|
||||||
|
"modale":{
|
||||||
|
"title":"Email Change",
|
||||||
|
"text1":"A validation email will be sent to the new address.",
|
||||||
|
"text2":"The change will only take effect after confirmation.",
|
||||||
|
"confBtn":"Confirm",
|
||||||
|
"cancelBtn":"Cancel"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
{
|
||||||
|
"nav":{
|
||||||
|
"home":"Accueil",
|
||||||
|
"login": "Connexion",
|
||||||
|
"logout":"Déconnexion",
|
||||||
|
"signup": "S'enregistrer",
|
||||||
|
"profile": "Profil",
|
||||||
|
"lists": "Mes listes"
|
||||||
|
},
|
||||||
|
|
||||||
|
"index":{
|
||||||
|
"title": "Bienvenue sur le génial gestionnaire de listes !",
|
||||||
|
"subtitle1":"Présentation",
|
||||||
|
"mainText": "<p>Créez vos listes de lieux, de mots en langue étrangère (comme une carte recto / verso), ou simplement une todo-list. Amusez-vous c'est très utile !</p><p>Le code de ce projet est <a href=\"https://github.com/Raffiskender\" target=\"_blank\">disponible ici</a>.</p><p>Sur cette 3ème version, le back office est toujours géré par 2 plugins WP MAIS ! Leur architecture a été complètement repensée. Ainsi le plugin de gestion des users via REST API est plus léger, et communique avec le plugin de gestion des listes via hooks ou filtres.</p><p>Le front est en Nuxt JS.</p>",
|
||||||
|
"lastSentenceUnconnect": "Pour commencer il faut {login} ou {signup} ou encore...",
|
||||||
|
"login":"vous connecter",
|
||||||
|
"signup":"vous enregistrer",
|
||||||
|
"lastSentenceConnected": "Vous êtes connecté, vous pouvez",
|
||||||
|
"seeLists":"acceder à vos listes"
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
"googleBtnTxt":"Se connecter avec Google",
|
||||||
|
"loginPage":{
|
||||||
|
"passwordResetRequest":"Mot de passe oublié."
|
||||||
|
},
|
||||||
|
"loginTitle":"Connexion",
|
||||||
|
"loginLabelUser":"Identifiant ou e-mail",
|
||||||
|
"loginLabelPwd":"Mot de passe",
|
||||||
|
"loginErrorPwdEmpty":"Vous devez saisir un mot de passe",
|
||||||
|
"loginErrorLoginEmpty":"Vous devez saisir un identifiant",
|
||||||
|
"loginErrorLoginFailed":"Identifiants incorrects",
|
||||||
|
"loginErrorUnconfirmedUser":"Vous ne pouvez pas vous connecter car votre email n'a pas été vérifié !",
|
||||||
|
"loginFormBtn":"Se connecter",
|
||||||
|
"loginGoogle":"Ou prenez le raccourcis...",
|
||||||
|
|
||||||
|
"signupTitle":"S'enregistrer",
|
||||||
|
"signupLabelEmail":"E-mail",
|
||||||
|
"signupLabelPwd":"Mot de passe",
|
||||||
|
"signupLabelConfirmPwd":"Confirmez le mot de passe",
|
||||||
|
"signupErrorEmailEmpty":"Vous devez saisir un mail",
|
||||||
|
"signupErrorEmailIsntValid":"Email invalide",
|
||||||
|
"signupEmailIsValid":"Email validé",
|
||||||
|
"signupFormBtn":"Envoyer",
|
||||||
|
|
||||||
|
"modalTitle":"Inscription réussie !",
|
||||||
|
"modalText":"Vous pouvez fermer cette fenêtre et commencer à gérer vos listes",
|
||||||
|
|
||||||
|
"passwordCheckerShallContain":"Doit contenir",
|
||||||
|
"passwordChecker8-22":"entre 8 et 22 caractères,",
|
||||||
|
"passwordCheckerUppercase":"une majuscule,",
|
||||||
|
"passwordCheckerLowercase":"une minuscule",
|
||||||
|
"passwordCheckerNb":"un chiffre,",
|
||||||
|
"passwordCheckerSpecialChar":"un caractère spécial",
|
||||||
|
|
||||||
|
"pwdInput":{
|
||||||
|
"errorPwdEmpty":"Vous devez saisir un mot de passe",
|
||||||
|
"errorPwdIsntValid":"Vous devez choisir un mot de passe plus fort",
|
||||||
|
"pwdsDoesNotMatch":"Les 2 mots de passe ne correspondent pas !",
|
||||||
|
"pwdConfirmEmpty":"Vous devez saisir la confirmation du mot de passe !"
|
||||||
|
},
|
||||||
|
|
||||||
|
"confirmation": {
|
||||||
|
"title": "Page de confirmation",
|
||||||
|
"successConnected": "Votre email a été confirmé ! Vous êtes connecté.",
|
||||||
|
"successNotConnected": "Votre email a été confirmé ! Veuillez vous connecter pour continuer :",
|
||||||
|
"failureMessage": "Votre email n'a pas pu être confirmé.",
|
||||||
|
"failureCauseInvalid":"Cause : Invalide",
|
||||||
|
"failureCauseExpired":"Cause : Lien expiré",
|
||||||
|
"failureYouCan":"Vous pouvez",
|
||||||
|
"failureCreateNewAccount":"créer un nouveau compte",
|
||||||
|
"failureOr":"ou",
|
||||||
|
"backHome":"revenir à l'accueil.",
|
||||||
|
"loginLink": "Se connecter",
|
||||||
|
"listsLink": "Voir mes listes"
|
||||||
|
},
|
||||||
|
"unconfirmedBanner":{
|
||||||
|
"message":"Email non vérifié"
|
||||||
|
},
|
||||||
|
"pwdResetRequest": {
|
||||||
|
"title": "Demande de réinitialisation du mot de passe",
|
||||||
|
"formBtn":"Envoyer",
|
||||||
|
"pb": "Une erreur est survenue lors de l'envoi du formulaire.",
|
||||||
|
"message": "Message : ",
|
||||||
|
"successMessage": "C'est envoyé ! Si votre adresse est enregistrée dans notre base, vous recevrez un e-mail contenant un lien pour modifier votre mot de passe.",
|
||||||
|
"linkValidity": "Ce lien expirera dans 30 minutes.",
|
||||||
|
"seeU": "À très vite !"
|
||||||
|
},
|
||||||
|
"pwdReset":{
|
||||||
|
"title":"Réinitialisation du mot de passe",
|
||||||
|
"pwdLabel1":"Nouveau mot de passe",
|
||||||
|
"pwdLabel2":"Confirmez le mot de passe",
|
||||||
|
"formBtn":"Envoyer",
|
||||||
|
"successMessage":"Votre mot de passe a bien été réinitialisé. Vous pouvez maintenant vous connecter en cliquant sur le lien suivant :",
|
||||||
|
"loginLink":"Se connecter",
|
||||||
|
"errorMessage":"Une erreur est survenue lors de la mise à jour du mot de passe. Veuillez réessayer."
|
||||||
|
},
|
||||||
|
|
||||||
|
"profile":{
|
||||||
|
"title":"Profil",
|
||||||
|
"tabGeneral":"Général",
|
||||||
|
"tabRedZone": "RED ZONE",
|
||||||
|
"username":"Username",
|
||||||
|
"name_label" : "Nom affiché : ",
|
||||||
|
"email_label": "Email : ",
|
||||||
|
"avatar": "Avatar",
|
||||||
|
"danger_zone":"ZONE DE DANGER !",
|
||||||
|
"pwd_challenge_input_label":"Mot de passe",
|
||||||
|
"delete_account":"Suppression du compte"
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
"emailUpdate":{
|
||||||
|
"modale":{
|
||||||
|
"title":"Changement d'email",
|
||||||
|
"text1":"Un email de validation va être envoyé à la nouvelle adresse.",
|
||||||
|
"text2":"Le changement ne sera effectif qu’après confirmation.",
|
||||||
|
"confBtn":"Envoyer",
|
||||||
|
"cancelBtn":"Annuler"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"delete":{
|
||||||
|
"modale":{
|
||||||
|
"title":"Suppression du compte",
|
||||||
|
"text1": "Êtes-vous certains de vouloir supprimer votre compte ? Toutes vos données seront effacées. Cette action est IRRÉVERSIBLE !"
|
||||||
|
},
|
||||||
|
"modale2":{
|
||||||
|
"title":"Si vous y tenez...",
|
||||||
|
"text1": "Un email de validation vous a été envoyé. Merci de valider l'opération en cliquant sur le lien qu'il contient."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"ui":{
|
||||||
|
"yes":"Oui",
|
||||||
|
"no":"Non",
|
||||||
|
"ok":"Ok",
|
||||||
|
"confirm":"Confirmer",
|
||||||
|
"cancel":"Annuler",
|
||||||
|
"errorWrongPwd":"Mot de passe erroné",
|
||||||
|
"errorPwdEmpty":"Entrez un mot de passe"
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
compatibilityDate: '2025-07-15',
|
||||||
|
devtools: { enabled: true },
|
||||||
|
runtimeConfig: {
|
||||||
|
public: {
|
||||||
|
apiBase: 'http://localhost:81/wp-json/app/v1'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modules: ['@pinia/nuxt',
|
||||||
|
// '@nuxtjs/tailwindcss'
|
||||||
|
'@nuxtjs/i18n',
|
||||||
|
'nuxt-vue3-google-signin'
|
||||||
|
],
|
||||||
|
pinia: {},
|
||||||
|
i18n: {
|
||||||
|
strategy: 'prefix',
|
||||||
|
defaultLocale: 'fr',
|
||||||
|
langDir: 'locales',
|
||||||
|
customRoutes: 'config',
|
||||||
|
locales: [
|
||||||
|
{ code: 'fr', iso: 'fr-FR', name: 'Français', file:'fr.json' },
|
||||||
|
{ code: 'en', iso: 'en-US', name: 'English', file:'en.json' }
|
||||||
|
],
|
||||||
|
compilation: {
|
||||||
|
strictMessage: false, // autorise HTML dans les messages
|
||||||
|
escapeHtml: false // n’échappe pas le HTML
|
||||||
|
}
|
||||||
|
|
||||||
|
// pages: {
|
||||||
|
// login: {
|
||||||
|
// fr: '/connexion',
|
||||||
|
// en: '/login'
|
||||||
|
// },
|
||||||
|
// signin: {
|
||||||
|
// fr: '/senregistrer',
|
||||||
|
// en: '/signin'
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
},
|
||||||
|
googleSignIn: {
|
||||||
|
clientId: '364175735286-8o0djchefske6j2ofgsibhsb7t4jk4l7.apps.googleusercontent.com',
|
||||||
|
},
|
||||||
|
css: ['@/assets/css/main.scss'],
|
||||||
|
routeRules: {
|
||||||
|
'/api-wp/**': { proxy: 'http://localhost:81/wp-json/app/v1/**',
|
||||||
|
changeOrigin: true, // Crucial pour éviter la 502
|
||||||
|
prependPath: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
Generated
+14253
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"name": "nuxt_project",
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "nuxt build",
|
||||||
|
"dev": "nuxt dev",
|
||||||
|
"generate": "nuxt generate",
|
||||||
|
"preview": "nuxt preview",
|
||||||
|
"postinstall": "nuxt prepare"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-svg-core": "^7.1.0",
|
||||||
|
"@fortawesome/free-brands-svg-icons": "^7.1.0",
|
||||||
|
"@fortawesome/free-regular-svg-icons": "^7.1.0",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
||||||
|
"@fortawesome/vue-fontawesome": "^3.1.3",
|
||||||
|
"@nuxtjs/i18n": "^10.2.1",
|
||||||
|
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||||
|
"@pinia/nuxt": "^0.11.3",
|
||||||
|
"nuxt": "^4.2.2",
|
||||||
|
"nuxt-vue3-google-signin": "^0.0.13",
|
||||||
|
"pinia": "^3.0.4",
|
||||||
|
"vue": "^3.5.26",
|
||||||
|
"vue-router": "^4.6.4",
|
||||||
|
"vue3-google-signin": "^2.1.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"sass-embedded": "^1.97.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,2 @@
|
|||||||
|
User-Agent: *
|
||||||
|
Disallow:
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
// https://nuxt.com/docs/guide/concepts/typescript
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./.nuxt/tsconfig.app.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./.nuxt/tsconfig.server.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./.nuxt/tsconfig.shared.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./.nuxt/tsconfig.node.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user