Implémentation complète du multilingue FR/EN
- Installation vue-i18n v11 et création des fichiers de traduction (fr.json, en.json) - Création store locale avec détection hiérarchique (URL > localStorage > navigator) - Modification des routes avec préfixe /:locale? optionnel - Toggle FR/EN dans Menu.vue avec synchronisation immédiate - Traduction de ~200 textes dans 27 composants Vue - Suppression des labels hardcodés en français côté backend - Ajout route Kirby catch-all en/(:all?) pour /en/ URLs - Helper addLocalePrefix() pour préserver locale dans liens dialogs - Traduction pseudo-élément CSS via data attribute Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
3af95b1d20
commit
82eb8d88cc
49 changed files with 1079 additions and 295 deletions
|
|
@ -29,7 +29,22 @@
|
|||
<div v-if="isExpanded" id="menu" class="flex | rounded-xl">
|
||||
<header class="w-full | flex">
|
||||
<!-- TODO: à dynamiser en récupérant le $site->title() -->
|
||||
<p lang="en">Design to Pack</p>
|
||||
<p :lang="currentLocale">Design to Pack</p>
|
||||
<div class="lang-toggle">
|
||||
<button
|
||||
@click="switchLocale('fr')"
|
||||
:class="{ active: currentLocale === 'fr' }"
|
||||
>
|
||||
FR
|
||||
</button>
|
||||
<span class="slash">/</span>
|
||||
<button
|
||||
@click="switchLocale('en')"
|
||||
:class="{ active: currentLocale === 'en' }"
|
||||
>
|
||||
EN
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<nav class="w-full | flow">
|
||||
<ul class="flex">
|
||||
|
|
@ -51,25 +66,23 @@
|
|||
>{{ mainItem.title }}</router-link
|
||||
>
|
||||
<span
|
||||
v-if="mainItem.title === 'Inspirations' && page?.newInspirations"
|
||||
v-if="
|
||||
mainItem.title === t('menu.inspirations') && page?.newInspirations
|
||||
"
|
||||
class="pill pill--secondary"
|
||||
>{{ 'Nouveautés' }}</span
|
||||
>{{ t('menu.news') }}</span
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
<details :class="{ skeleton: !currentProjects }" open>
|
||||
<summary>Projets en cours</summary>
|
||||
<summary>{{ t('menu.currentProjects') }}</summary>
|
||||
<ul v-if="currentProjects.length > 0">
|
||||
<li
|
||||
v-for="project in currentProjects"
|
||||
:class="{ active: isCurrent(project) }"
|
||||
>
|
||||
<router-link
|
||||
:to="
|
||||
isEmptyBrief(project)
|
||||
? project.uri + '/client-brief'
|
||||
: project.uri
|
||||
"
|
||||
:to="getProjectPath(project)"
|
||||
:class="hasUnreadNotification(project) ? 'new' : undefined"
|
||||
:data-dtl="project.isDTLEnabled ? 'true' : undefined"
|
||||
@click="collapse()"
|
||||
|
|
@ -79,13 +92,13 @@
|
|||
</ul>
|
||||
</details>
|
||||
<details v-if="archivedProjects.length">
|
||||
<summary>Projets archivés</summary>
|
||||
<summary>{{ t('menu.archivedProjects') }}</summary>
|
||||
<ul>
|
||||
<li
|
||||
v-for="project in archivedProjects"
|
||||
:class="{ active: isCurrent(project) }"
|
||||
>
|
||||
<router-link :to="project.uri" @click="collapse()">{{
|
||||
<router-link :to="getProjectPath(project)" @click="collapse()">{{
|
||||
project.title
|
||||
}}</router-link>
|
||||
</li>
|
||||
|
|
@ -96,13 +109,19 @@
|
|||
<ul class="flex">
|
||||
<li data-icon="user">
|
||||
<a
|
||||
:href="user.role === 'admin' ? '/panel/account' : '/account'"
|
||||
:href="
|
||||
user.role === 'admin'
|
||||
? '/panel/account'
|
||||
: currentLocale === 'en'
|
||||
? '/en/account'
|
||||
: '/account'
|
||||
"
|
||||
@click="collapse()"
|
||||
>Profil</a
|
||||
>{{ t('menu.profile') }}</a
|
||||
>
|
||||
</li>
|
||||
<li data-icon="logout">
|
||||
<a href="/logout" @click="collapse()">Déconnexion</a>
|
||||
<a href="/logout" @click="collapse()">{{ t('menu.logout') }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</footer>
|
||||
|
|
@ -113,17 +132,23 @@
|
|||
import { storeToRefs } from 'pinia';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useProjectsStore } from '../stores/projects';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useUserStore } from '../stores/user';
|
||||
import { usePageStore } from '../stores/page';
|
||||
import { useProjectStore } from '../stores/project';
|
||||
import { useLocaleStore } from '../stores/locale';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const isExpanded = ref(true);
|
||||
const { user, notifications } = storeToRefs(useUserStore());
|
||||
const { currentProjects, archivedProjects } = storeToRefs(useProjectsStore());
|
||||
const { isEmptyBrief } = useProjectStore();
|
||||
const { page } = storeToRefs(usePageStore());
|
||||
const localeStore = useLocaleStore();
|
||||
const { currentLocale } = storeToRefs(localeStore);
|
||||
const { t } = useI18n();
|
||||
|
||||
const unreadNotificationsCount = computed(() => {
|
||||
if (!user.value) return 0;
|
||||
|
|
@ -135,34 +160,37 @@ const unreadNotificationsCount = computed(() => {
|
|||
return count === 0 ? 0 : count;
|
||||
});
|
||||
|
||||
const mainItems = [
|
||||
{
|
||||
title: 'Home',
|
||||
path: '/',
|
||||
icon: 'home',
|
||||
},
|
||||
{
|
||||
title: 'Notifications',
|
||||
path: '/notifications',
|
||||
icon: 'megaphone',
|
||||
},
|
||||
{
|
||||
title: 'Réunions',
|
||||
path: '/reunions',
|
||||
icon: 'calendar',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
title: 'Design to Light',
|
||||
path: '/design-to-light',
|
||||
icon: 'leaf',
|
||||
},
|
||||
{
|
||||
title: 'Inspirations',
|
||||
path: '/inspirations',
|
||||
icon: 'inspiration',
|
||||
},
|
||||
];
|
||||
const mainItems = computed(() => {
|
||||
const prefix = currentLocale.value === 'en' ? '/en' : '';
|
||||
return [
|
||||
{
|
||||
title: t('menu.home'),
|
||||
path: prefix + '/',
|
||||
icon: 'home',
|
||||
},
|
||||
{
|
||||
title: t('menu.notifications'),
|
||||
path: prefix + '/notifications',
|
||||
icon: 'megaphone',
|
||||
},
|
||||
{
|
||||
title: t('menu.meetings'),
|
||||
path: prefix + '/reunions',
|
||||
icon: 'calendar',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
title: t('menu.designToLight'),
|
||||
path: prefix + '/design-to-light',
|
||||
icon: 'leaf',
|
||||
},
|
||||
{
|
||||
title: t('menu.inspirations'),
|
||||
path: prefix + '/inspirations',
|
||||
icon: 'inspiration',
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
function toggleExpand() {
|
||||
isExpanded.value = !isExpanded.value;
|
||||
|
|
@ -192,6 +220,33 @@ function collapse() {
|
|||
isExpanded.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function switchLocale(newLocale) {
|
||||
if (newLocale === currentLocale.value) return;
|
||||
|
||||
let newPath = route.path;
|
||||
|
||||
if (newLocale === 'en') {
|
||||
if (!newPath.startsWith('/en')) {
|
||||
newPath = '/en' + newPath;
|
||||
}
|
||||
} else {
|
||||
if (newPath.startsWith('/en')) {
|
||||
newPath = newPath.replace(/^\/en/, '') || '/';
|
||||
}
|
||||
}
|
||||
|
||||
localeStore.setLocale(newLocale);
|
||||
router.push(newPath);
|
||||
}
|
||||
|
||||
function getProjectPath(project) {
|
||||
const prefix = currentLocale.value === 'en' ? '/en' : '';
|
||||
const path = isEmptyBrief(project)
|
||||
? project.uri + '/client-brief'
|
||||
: project.uri;
|
||||
return prefix + path;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
|
@ -243,6 +298,24 @@ button[aria-controls='menu'][aria-expanded='false']
|
|||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Lang toggle */
|
||||
.lang-toggle {
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
.lang-toggle button {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.lang-toggle button.active {
|
||||
background-color: transparent !important;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.lang-toggle span.slash {
|
||||
padding: 0 0.2rem;
|
||||
}
|
||||
|
||||
@media (max-width: 1023px) {
|
||||
button[aria-controls='menu'][aria-expanded='true'] {
|
||||
left: 0;
|
||||
|
|
|
|||
|
|
@ -5,12 +5,12 @@
|
|||
modal
|
||||
:draggable="false"
|
||||
:dismissableMask="true"
|
||||
header="Demander la création d’un projet"
|
||||
:header="t('dialogs.requestProject')"
|
||||
class="dialog"
|
||||
:closeOnEscape="true"
|
||||
>
|
||||
<template #header>
|
||||
<h2 class="font-serif text-lg">Demander la création d’un projet</h2>
|
||||
<h2 class="font-serif text-lg">{{ t('dialogs.requestProject') }}</h2>
|
||||
</template>
|
||||
|
||||
<form
|
||||
|
|
@ -18,24 +18,28 @@
|
|||
class="w-full h-full p-16 flex flex-col"
|
||||
style="--row-gap: 1rem"
|
||||
>
|
||||
<label for="project-title" class="sr-only">Nom du projet</label>
|
||||
<label for="project-title" class="sr-only">{{
|
||||
t('forms.projectName')
|
||||
}}</label>
|
||||
<input
|
||||
type="text"
|
||||
v-model="title"
|
||||
id="project-title"
|
||||
placeholder="Nom du projet"
|
||||
:placeholder="t('forms.projectName')"
|
||||
class="w-full rounded-md border border-grey-200 px-16 py-12"
|
||||
required
|
||||
/>
|
||||
|
||||
<label for="project-details" class="sr-only">Détails du projet</label>
|
||||
<label for="project-details" class="sr-only">{{
|
||||
t('forms.projectDetails')
|
||||
}}</label>
|
||||
<textarea
|
||||
id="project-details"
|
||||
name="details"
|
||||
v-model="details"
|
||||
cols="30"
|
||||
rows="10"
|
||||
placeholder="Détails du projet…"
|
||||
:placeholder="t('forms.projectDetailsPlaceholder')"
|
||||
class="w-full flex-1 rounded-md border border-grey-200 px-16 py-12"
|
||||
required
|
||||
></textarea>
|
||||
|
|
@ -52,26 +56,24 @@
|
|||
class="flex font-medium mt-4"
|
||||
style="--column-gap: var(--space-4)"
|
||||
>
|
||||
Créer avec
|
||||
<span class="flex justify-center text-sm" data-icon="leaf"
|
||||
>Design to Light</span
|
||||
>
|
||||
{{ t('dialogs.createWithDTL') }}
|
||||
<span class="flex justify-center text-sm" data-icon="leaf">{{
|
||||
t('dtl.title')
|
||||
}}</span>
|
||||
</label>
|
||||
<p class="text-sm mt-8 mb-4">
|
||||
Découvrez la note environnementale de votre projet et allégez l’impact
|
||||
de votre projet grâce à nos expertises d’optimisation du poids de
|
||||
flacon.
|
||||
{{ t('dialogs.dtlDescription') }}
|
||||
</p>
|
||||
<router-link to="/design-to-light" class="text-sm font-medium"
|
||||
>En savoir plus</router-link
|
||||
>
|
||||
<router-link to="/design-to-light" class="text-sm font-medium">{{
|
||||
t('dialogs.learnMore')
|
||||
}}</router-link>
|
||||
</div>
|
||||
|
||||
<footer class="flex-columns w-full mt-16" style="column-gap: 0.5rem">
|
||||
<button class="btn btn--black-10" @click="emits('close')">
|
||||
Annuler
|
||||
{{ t('buttons.cancel') }}
|
||||
</button>
|
||||
<button class="btn" type="submit">Soumettre</button>
|
||||
<button class="btn" type="submit">{{ t('buttons.submit') }}</button>
|
||||
</footer>
|
||||
</form>
|
||||
</Dialog>
|
||||
|
|
@ -81,6 +83,9 @@
|
|||
import Dialog from 'primevue/dialog';
|
||||
import { ref, watch } from 'vue';
|
||||
import { useApiStore } from '../stores/api';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const title = ref('');
|
||||
const details = ref('');
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<header class="flex">
|
||||
<h2 id="tabslist" class="sr-only">Projets</h2>
|
||||
<h2 id="tabslist" class="sr-only">{{ t('brief.projects') }}</h2>
|
||||
<Tabs :tabs="tabs" @update:currentTab="changeTab" />
|
||||
</header>
|
||||
<section
|
||||
|
|
@ -11,6 +11,7 @@
|
|||
:aria-label="tabs[0].label"
|
||||
class="flow"
|
||||
:class="{ skeleton: isProjectsLoading }"
|
||||
:data-empty-text="t('projects.none')"
|
||||
>
|
||||
<Project
|
||||
v-for="project in currentProjects"
|
||||
|
|
@ -25,6 +26,7 @@
|
|||
tabindex="0"
|
||||
:aria-label="tabs[1].label"
|
||||
class="flow"
|
||||
:data-empty-text="t('projects.none')"
|
||||
>
|
||||
<Project
|
||||
v-for="project in archivedProjects"
|
||||
|
|
@ -39,7 +41,9 @@ import Project from './project/Project.vue';
|
|||
import { useProjectsStore } from '../stores/projects';
|
||||
import { ref, computed } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
const { projects, currentProjects, archivedProjects, isProjectsLoading } =
|
||||
storeToRefs(useProjectsStore());
|
||||
|
||||
|
|
@ -47,13 +51,13 @@ const currentTab = ref('currentProjects');
|
|||
const tabs = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: 'Projets en cours',
|
||||
label: t('projects.current'),
|
||||
id: 'currentProjects',
|
||||
count: currentProjects.value.length,
|
||||
isActive: currentTab.value === 'currentProjects',
|
||||
},
|
||||
{
|
||||
label: 'Projets archivés',
|
||||
label: t('projects.archived'),
|
||||
id: 'archivedProjects',
|
||||
count: archivedProjects.value.length,
|
||||
isActive: currentTab.value === 'archivedProjects',
|
||||
|
|
@ -72,7 +76,7 @@ section {
|
|||
min-height: calc(100vh - 8.5rem);
|
||||
}
|
||||
section:not(.skeleton):empty::after {
|
||||
content: 'Aucun projet pour le moment';
|
||||
content: attr(data-empty-text);
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
v-model="currentValue"
|
||||
:options="items"
|
||||
optionLabel="title"
|
||||
:placeholder="'Sélectionnez une déclinaison'"
|
||||
:placeholder="t('forms.selectVariation')"
|
||||
:maxSelectedLabels="3"
|
||||
class="font-serif"
|
||||
:class="{ active: currentValue }"
|
||||
|
|
@ -45,7 +45,7 @@
|
|||
<p v-if="currentValue">
|
||||
{{ currentValue.title }}
|
||||
</p>
|
||||
<p v-else>Sélectionnez une déclinaison</p>
|
||||
<p v-else>{{ t('forms.selectVariation') }}</p>
|
||||
</template>
|
||||
|
||||
<template #option="slotProps">
|
||||
|
|
@ -65,6 +65,9 @@
|
|||
import { onBeforeMount, ref, watch, nextTick } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useDialogStore } from '../stores/dialog';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
// Props
|
||||
const { items, label, isCompareModeEnabled, index } = defineProps({
|
||||
|
|
|
|||
|
|
@ -39,9 +39,7 @@
|
|||
|
||||
<footer v-if="!comment.isEditMode" class="comment__replies">
|
||||
<p v-if="comment.replies?.length > 0">
|
||||
{{ comment.replies.length }} réponse{{
|
||||
comment.replies.length > 1 ? 's' : ''
|
||||
}}
|
||||
{{ comment.replies.length }} {{ comment.replies.length > 1 ? t('comments.replies') : t('comments.reply') }}
|
||||
</p>
|
||||
<div
|
||||
v-if="userStore.canEditComment(comment)"
|
||||
|
|
@ -52,14 +50,14 @@
|
|||
data-icon="edit"
|
||||
@click="editComment($event)"
|
||||
>
|
||||
<span class="sr-only">Éditer</span>
|
||||
<span class="sr-only">{{ t('comments.edit') }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn--transparent btn--icon btn--sm"
|
||||
data-icon="delete"
|
||||
@click="deleteComment($event)"
|
||||
>
|
||||
<span class="sr-only">Supprimer</span>
|
||||
<span class="sr-only">{{ t('buttons.delete') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
|
|
@ -68,11 +66,11 @@
|
|||
<input
|
||||
type="submit"
|
||||
class="btn btn--tranparent"
|
||||
value="Sauvegarder"
|
||||
:value="t('buttons.save')"
|
||||
@click="saveEditedComment($event)"
|
||||
/>
|
||||
<button class="btn btn--white-10" @click="cancelEditComment($event)">
|
||||
Annuler
|
||||
{{ t('buttons.cancel') }}
|
||||
</button>
|
||||
</footer>
|
||||
</article>
|
||||
|
|
@ -88,9 +86,11 @@ import { computed, onMounted, ref, useTemplateRef } from 'vue';
|
|||
import { storeToRefs } from 'pinia';
|
||||
import { usePageStore } from '../../stores/page';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
dayjs.locale('fr');
|
||||
|
||||
const { t } = useI18n();
|
||||
const { comment, commentIndex } = defineProps({
|
||||
comment: Object,
|
||||
commentIndex: Number,
|
||||
|
|
@ -125,11 +125,11 @@ function formatDate() {
|
|||
const dateNumber = parseInt(dayjs(comment.date).format('YYMMD'));
|
||||
|
||||
if (dateNumber === todayNumber) {
|
||||
return "Aujourd'hui";
|
||||
return t('dates.today');
|
||||
}
|
||||
|
||||
if (dateNumber === todayNumber - 1) {
|
||||
return 'hier';
|
||||
return t('dates.yesterday');
|
||||
}
|
||||
|
||||
return dayjs(comment.date).format('D MMM YY');
|
||||
|
|
@ -153,7 +153,7 @@ async function read() {
|
|||
page.value.uri
|
||||
);
|
||||
} catch (error) {
|
||||
console.log('Erreur lors de la lecture de la notification : ', error);
|
||||
console.log(t('errors.readNotificationFailed'), error);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
<template>
|
||||
<aside id="comments-container" aria-labelledby="comments-label">
|
||||
<h2 id="comments-label" class="sr-only">Commentaires</h2>
|
||||
<h2 id="comments-label" class="sr-only">{{ t('comments.title') }}</h2>
|
||||
<div
|
||||
class="comments | flow"
|
||||
:class="{ empty: !comments || comments.length === 0 }"
|
||||
:data-empty-message="t('comments.emptyMessage')"
|
||||
>
|
||||
<template v-if="comments">
|
||||
<template v-if="!openedComment">
|
||||
|
|
@ -26,7 +27,7 @@
|
|||
isAddOpen = false;
|
||||
"
|
||||
>
|
||||
<span>Retour à la liste</span>
|
||||
<span>{{ t('buttons.backToList') }}</span>
|
||||
</button>
|
||||
<Comment
|
||||
:comment="openedComment"
|
||||
|
|
@ -53,7 +54,7 @@
|
|||
class="btn btn--white-20 | w-full"
|
||||
@click="toggleCommentPositionMode(true)"
|
||||
>
|
||||
Ajouter un commentaire
|
||||
{{ t('buttons.addComment') }}
|
||||
</button>
|
||||
<button
|
||||
v-else-if="openedComment && !isAddOpen"
|
||||
|
|
@ -61,7 +62,7 @@
|
|||
class="btn btn--white-20 | justify-start w-full | text-white-50"
|
||||
@click="isAddOpen = true"
|
||||
>
|
||||
Répondre…
|
||||
{{ t('buttons.reply') }}
|
||||
</button>
|
||||
<!-- TODO: afficher #new-comment une fois le bouton Ajouter un commentaire cliqué -->
|
||||
<div
|
||||
|
|
@ -70,11 +71,10 @@
|
|||
class="bg-primary | text-sm text-white | rounded-lg | p-12"
|
||||
>
|
||||
<p class="flex justify-start | mb-12" data-icon="comment">
|
||||
<strong>Nouveau commentaire</strong>
|
||||
<strong>{{ t('comments.new') }}</strong>
|
||||
</p>
|
||||
<p>
|
||||
Dans la zone du contenu, cliquez où vous souhaitez positionner le
|
||||
commentaire
|
||||
{{ t('comments.newInstruction') }}
|
||||
</p>
|
||||
</div>
|
||||
<form
|
||||
|
|
@ -84,13 +84,13 @@
|
|||
class="flow | p-12 | rounded-xl"
|
||||
@submit="handleSubmit"
|
||||
>
|
||||
<label class="sr-only" for="comment">Votre commentaire</label>
|
||||
<label class="sr-only" for="comment">{{ t('comments.your') }}</label>
|
||||
<textarea
|
||||
v-model="draftComment.text"
|
||||
:disabled="isSubmitting ? true : undefined"
|
||||
name="comment"
|
||||
id="comment"
|
||||
placeholder="Ajouter un commentaire…"
|
||||
:placeholder="t('forms.commentPlaceholder')"
|
||||
rows="5"
|
||||
class="text-sm | rounded-lg bg-black p-12"
|
||||
></textarea>
|
||||
|
|
@ -99,11 +99,11 @@
|
|||
type="submit"
|
||||
class="btn"
|
||||
:class="{ submitting: isSubmitting }"
|
||||
:value="isSubmitting ? 'En cours' : undefined"
|
||||
:value="isSubmitting ? t('comments.inProgress') : undefined"
|
||||
:disabled="isSubmitting ? true : undefined"
|
||||
/>
|
||||
<button class="btn btn--white-10" @click="isAddOpen = false">
|
||||
Annuler
|
||||
{{ t('buttons.cancel') }}
|
||||
</button>
|
||||
</footer>
|
||||
</form>
|
||||
|
|
@ -122,9 +122,11 @@ import { useDialogStore } from '../../stores/dialog';
|
|||
import Comment from './Comment.vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
dayjs.locale('fr');
|
||||
|
||||
const { t } = useI18n();
|
||||
const { user } = useUserStore();
|
||||
const { page } = usePageStore();
|
||||
const dialog = useDialogStore();
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
<template>
|
||||
<button id="dtl-btn" class="bg-black rounded-md p-4" :title="'Design to Light: ' + (hasAlternatives ? 'New' : grade)" data-icon="leaf" :data-grade="grade" :data-new="hasAlternatives ? true : undefined">
|
||||
<span lang="en" class="sr-only">Design to Light</span>
|
||||
<span v-if="hasAlternatives" lang="en" class="new">New</span>
|
||||
<button id="dtl-btn" class="bg-black rounded-md p-4" :title="t('dtl.grade', { grade: hasAlternatives ? t('menu.news') : grade })" data-icon="leaf" :data-grade="grade" :data-new="hasAlternatives ? true : undefined">
|
||||
<span lang="en" class="sr-only">{{ t('dtl.title') }}</span>
|
||||
<span v-if="hasAlternatives" lang="en" class="new">{{ t('menu.news') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
<script setup>
|
||||
import { storeToRefs } from "pinia";
|
||||
import { usePageStore } from "../../stores/page";
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { page } = storeToRefs(usePageStore());
|
||||
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@
|
|||
proposals.length === 1 && isDialogOpen
|
||||
? activeProposal.title
|
||||
? activeProposal.title
|
||||
: 'Design to light'
|
||||
: 'Design to light'
|
||||
: t('dtl.title')
|
||||
: t('dtl.title')
|
||||
}}
|
||||
</h2>
|
||||
<button
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
class="btn btn--icon btn--transparent | ml-auto"
|
||||
data-icon="close"
|
||||
>
|
||||
<span class="sr-only">Fermer</span>
|
||||
<span class="sr-only">{{ t('buttons.close') }}</span>
|
||||
</button>
|
||||
</header>
|
||||
<nav v-if="!isDialogOpen" class="tabs" role="tablist" tabindex="-1">
|
||||
|
|
@ -42,8 +42,8 @@
|
|||
proposal.title
|
||||
? proposal.title
|
||||
: index === 0
|
||||
? 'Proposition initiale'
|
||||
: 'Alternative ' + index
|
||||
? t('dtl.initialProposal')
|
||||
: t('dtl.alternative', { index })
|
||||
}}
|
||||
</button>
|
||||
</nav>
|
||||
|
|
@ -67,12 +67,12 @@
|
|||
/>
|
||||
</router-link>
|
||||
<p>
|
||||
Données basées sur la proposition <br />du {{ activeProposal.date }}
|
||||
<br />{{ activeProposal.stepLabel }}
|
||||
{{ t('dtl.proposalBasedOn') }} <br />du {{ activeProposal.date }}
|
||||
<br />{{ stepLabel }}
|
||||
</p>
|
||||
</div>
|
||||
<div id="note-globale" class="px-32 py-16 border-b flow">
|
||||
<h3>Note globale</h3>
|
||||
<h3>{{ t('dtl.globalScore') }}</h3>
|
||||
<div class="flex" style="--column-gap: 1rem">
|
||||
<p :data-grade="activeProposal.grades.global.letter">
|
||||
<strong class="sr-only">{{
|
||||
|
|
@ -100,15 +100,15 @@
|
|||
</div>
|
||||
</div>
|
||||
<div id="positionnement" class="px-32 py-16 border-b flow">
|
||||
<h3>Positionnement</h3>
|
||||
<h3>{{ t('dtl.positioning') }}</h3>
|
||||
<dl>
|
||||
<dt id="design">Design</dt>
|
||||
<dt id="design">{{ t('dtl.design') }}</dt>
|
||||
<dd>
|
||||
<span class="sr-only">{{
|
||||
activeProposal.grades.position.complexity
|
||||
}}</span>
|
||||
</dd>
|
||||
<dt id="poids">Poids</dt>
|
||||
<dt id="poids">{{ t('dtl.weight') }}</dt>
|
||||
<dd>
|
||||
<span class="sr-only">{{
|
||||
activeProposal.grades.position.weight
|
||||
|
|
@ -132,7 +132,7 @@
|
|||
</button>
|
||||
</div>
|
||||
<div id="indicateur" class="px-32 py-16 border-b flow">
|
||||
<h3>Indicateur des composants impliqués</h3>
|
||||
<h3>{{ t('dtl.indicators') }}</h3>
|
||||
<div class="grid">
|
||||
<template
|
||||
v-for="indicator in activeProposal.grades.indicators"
|
||||
|
|
@ -170,8 +170,8 @@
|
|||
>
|
||||
{{
|
||||
page.hasOptimizationRequest
|
||||
? "Demande d'expertise en cours de traitement…"
|
||||
: 'Demander une expertise d’optimisation'
|
||||
? t('dtl.requestPending')
|
||||
: t('dtl.requestOptimization')
|
||||
}}
|
||||
</button>
|
||||
</footer>
|
||||
|
|
@ -190,6 +190,7 @@ import { storeToRefs } from 'pinia';
|
|||
import { ref, onBeforeUnmount, computed } from 'vue';
|
||||
import { useDialogStore } from '../../stores/dialog';
|
||||
import { usePageStore } from '../../stores/page';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { proposals } = defineProps({
|
||||
proposals: Array,
|
||||
|
|
@ -197,6 +198,7 @@ const { proposals } = defineProps({
|
|||
|
||||
const { page } = storeToRefs(usePageStore());
|
||||
const { openedFile, activeTracks } = storeToRefs(useDialogStore());
|
||||
const { t } = useI18n();
|
||||
|
||||
const isDialogOpen = computed(() => {
|
||||
if (openedFile.value) {
|
||||
|
|
@ -215,6 +217,17 @@ const emits = defineEmits(['close']);
|
|||
const activeProposal =
|
||||
proposals.length === 1 ? computed(() => proposals[0]) : ref(proposals[0]);
|
||||
|
||||
const stepLabel = computed(() => {
|
||||
const proposal = activeProposal.value || activeProposal;
|
||||
const location = proposal.location;
|
||||
|
||||
if (location.step === 'virtualSample') {
|
||||
return location.type === 'dynamic' ? t('dtl.dynamicTrack') : t('dtl.staticTrack');
|
||||
}
|
||||
|
||||
return t('steps.' + location.step);
|
||||
});
|
||||
|
||||
window.addEventListener('keyup', closeOnEscape);
|
||||
window.addEventListener('click', close);
|
||||
|
||||
|
|
|
|||
|
|
@ -5,15 +5,15 @@
|
|||
modal
|
||||
:draggable="false"
|
||||
:dismissableMask="true"
|
||||
header="Demander un rendez-vous"
|
||||
:header="t('dialogs.requestMeeting')"
|
||||
class="dialog"
|
||||
:closeOnEscape="true"
|
||||
@click="preventClose($event)"
|
||||
>
|
||||
<template #header>
|
||||
<h2 class="font-serif text-lg">Demander un rendez-vous</h2>
|
||||
<h2 class="font-serif text-lg">{{ t('dialogs.requestMeeting') }}</h2>
|
||||
<p class="flex justify-center text-sm" data-icon="leaf">
|
||||
Design to Light
|
||||
{{ t('dtl.title') }}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
class="w-full h-full p-16 flex flex-col"
|
||||
style="--row-gap: 1rem"
|
||||
>
|
||||
<label for="projects" class="sr-only">Projet</label>
|
||||
<label for="projects" class="sr-only">{{ t('brief.projects') }}</label>
|
||||
<select
|
||||
name="projects"
|
||||
id="projects"
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
class="w-full rounded-md border border-grey-200 px-16 py-12"
|
||||
required
|
||||
>
|
||||
<option value="">Sélectionnez le projet</option>
|
||||
<option value="">{{ t('forms.selectVariation') }}</option>
|
||||
<option
|
||||
v-for="project in currentProjects"
|
||||
:key="project.uri"
|
||||
|
|
@ -41,35 +41,37 @@
|
|||
</option>
|
||||
</select>
|
||||
|
||||
<label for="appointment-subject" class="sr-only"
|
||||
>Objet du rendez-vous</label
|
||||
>
|
||||
<label for="appointment-subject" class="sr-only">{{
|
||||
t('forms.meetingSubject')
|
||||
}}</label>
|
||||
<input
|
||||
type="text"
|
||||
v-model="subject"
|
||||
id="appointment-subject"
|
||||
placeholder="Objet du rendez-vous"
|
||||
:placeholder="t('forms.meetingSubject')"
|
||||
class="w-full rounded-md border border-grey-200 px-16 py-12"
|
||||
required
|
||||
/>
|
||||
|
||||
<label for="appointment-details" class="sr-only">Détails du projet</label>
|
||||
<label for="appointment-details" class="sr-only">{{
|
||||
t('forms.meetingDetails')
|
||||
}}</label>
|
||||
<textarea
|
||||
id="appointment-details"
|
||||
name="details"
|
||||
v-model="details"
|
||||
cols="30"
|
||||
rows="10"
|
||||
placeholder="Décrivez votre demande…"
|
||||
:placeholder="t('forms.meetingDetailsPlaceholder')"
|
||||
class="w-full flex-1 rounded-md border border-grey-200 px-16 py-12"
|
||||
required
|
||||
></textarea>
|
||||
|
||||
<footer class="flex-columns w-full mt-16" style="column-gap: 0.5rem">
|
||||
<button class="btn btn--black-10" @click="emits('close')">
|
||||
Annuler
|
||||
{{ t('buttons.cancel') }}
|
||||
</button>
|
||||
<button class="btn" type="submit">Soumettre</button>
|
||||
<button class="btn" type="submit">{{ t('buttons.submit') }}</button>
|
||||
</footer>
|
||||
</form>
|
||||
</Dialog>
|
||||
|
|
@ -82,6 +84,9 @@ import { storeToRefs } from 'pinia';
|
|||
import { useProjectsStore } from '../../stores/projects';
|
||||
import { usePageStore } from '../../stores/page';
|
||||
import { ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { page } = storeToRefs(usePageStore());
|
||||
const { currentProjects } = storeToRefs(useProjectsStore());
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<!-- Favorite button -->
|
||||
<button
|
||||
class="favorite"
|
||||
:aria-label="isFavorite ? 'Retirer des favoris' : 'Ajouter aux favoris'"
|
||||
:aria-label="isFavorite ? t('inspirations.removeFromFavorites') : t('inspirations.addToFavorites')"
|
||||
:aria-pressed="isFavorite"
|
||||
@click="toggleFavorite"
|
||||
>
|
||||
|
|
@ -34,6 +34,9 @@
|
|||
import { computed } from "vue";
|
||||
import { useUserStore } from "../../stores/user";
|
||||
import { useApiStore } from "../../stores/api";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
// Props
|
||||
const { item, inspirationUri } = defineProps({
|
||||
|
|
@ -70,7 +73,7 @@ async function toggleFavorite() {
|
|||
// Update item favorite users list based on API response
|
||||
item.favoriteForUsers = newFavoriteUsers;
|
||||
} catch (error) {
|
||||
console.error("Failed to toggle favorite:", error);
|
||||
console.error(t('errors.toggleFavoriteFailed'), error);
|
||||
isFavorite.value = previousState; // Rollback on failure
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,12 +10,12 @@
|
|||
<strong
|
||||
class="notification__type | font-medium text-primary"
|
||||
data-icon="calendar"
|
||||
>Demande de rendez-vous</strong
|
||||
>{{ t('notifications.meetingRequest') }}</strong
|
||||
>
|
||||
<span class="notification__client | text-grey-700"
|
||||
>{{ notification.project.title }}
|
||||
{{
|
||||
notification.project.status === "draft" ? "(brouillon)" : ""
|
||||
notification.project.status === "draft" ? t('notifications.draft') : ""
|
||||
}}</span
|
||||
>
|
||||
<time
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
v-if="notification.type"
|
||||
class="notification__body | text-md font-medium | line-clamp"
|
||||
v-html="
|
||||
'Auteur : ' +
|
||||
t('notifications.author') + ' ' +
|
||||
(notification.author.name
|
||||
? notification.author.name + ' (' + notification.author.email + ')'
|
||||
: notification.author.email) +
|
||||
|
|
@ -44,6 +44,9 @@
|
|||
import { useNotificationsStore } from "../../stores/notifications";
|
||||
import { useUserStore } from "../../stores/user";
|
||||
import { useApiStore } from "../../stores/api";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { notification } = defineProps({ notification: Object });
|
||||
const { formatDate } = useNotificationsStore();
|
||||
|
|
|
|||
|
|
@ -3,14 +3,14 @@
|
|||
class="notification | bg-white rounded-lg | p-16 | flow"
|
||||
data-type="content"
|
||||
@click="readNotification()"
|
||||
title="Aller au contenu"
|
||||
:title="t('notifications.goToContent')"
|
||||
>
|
||||
<header>
|
||||
<p class="flex">
|
||||
<strong
|
||||
class="notification__type | font-medium text-primary"
|
||||
data-icon="content"
|
||||
>Contenu</strong
|
||||
>{{ t('notifications.content') }}</strong
|
||||
>
|
||||
<span class="notification__client | text-grey-700">{{
|
||||
notification.project.title
|
||||
|
|
@ -36,6 +36,9 @@ import { useRouter } from "vue-router";
|
|||
import { useNotificationsStore } from "../../stores/notifications";
|
||||
import { useUserStore } from "../../stores/user";
|
||||
import { useApiStore } from "../../stores/api";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const router = useRouter();
|
||||
const { notification } = defineProps({ notification: Object });
|
||||
|
|
|
|||
|
|
@ -10,12 +10,12 @@
|
|||
<strong
|
||||
class="notification__type | font-medium text-primary"
|
||||
data-icon="document"
|
||||
>Demande de création de projet</strong
|
||||
>{{ t('notifications.projectRequest') }}</strong
|
||||
>
|
||||
<span class="notification__client | text-grey-700"
|
||||
>{{ notification.project.title }}
|
||||
{{
|
||||
notification.project.status === "draft" ? "(brouillon)" : ""
|
||||
notification.project.status === "draft" ? t('notifications.draft') : ""
|
||||
}}</span
|
||||
>
|
||||
<time
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
v-if="notification.type"
|
||||
class="notification__body | text-md font-medium | line-clamp"
|
||||
v-html="
|
||||
'De la part de ' +
|
||||
t('notifications.from') + ' ' +
|
||||
(notification.author.name
|
||||
? notification.author.name + ' (' + notification.author.email + ')'
|
||||
: notification.author.email) +
|
||||
|
|
@ -44,6 +44,9 @@
|
|||
import { useNotificationsStore } from "../../stores/notifications";
|
||||
import { useUserStore } from "../../stores/user";
|
||||
import { useApiStore } from "../../stores/api";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { notification } = defineProps({ notification: Object });
|
||||
const { formatDate } = useNotificationsStore();
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@
|
|||
data-icon="comment"
|
||||
@click="isCommentsOpen = !isCommentsOpen"
|
||||
>
|
||||
<span class="sr-only"
|
||||
>{{ isCommentsOpen ? 'Masquer' : 'Afficher' }} les commentaires</span
|
||||
>
|
||||
<span class="sr-only">{{
|
||||
isCommentsOpen ? t('buttons.hideComments') : t('buttons.showComments')
|
||||
}}</span>
|
||||
</button>
|
||||
<a
|
||||
id="download-pdf"
|
||||
|
|
@ -25,7 +25,7 @@
|
|||
:href="openedFile.url"
|
||||
download
|
||||
>
|
||||
<span class="sr-only">Télécharger le fichier PDF</span>
|
||||
<span class="sr-only">{{ t('buttons.downloadPdf') }}</span>
|
||||
</a>
|
||||
<Comments v-if="isCommentsOpen" />
|
||||
</template>
|
||||
|
|
@ -36,6 +36,9 @@ import { ref, watch, computed, unref } from 'vue';
|
|||
import { useDialogStore } from '../../stores/dialog';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { VPdfViewer, useLicense } from '@vue-pdf-viewer/viewer';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const licenseKey =
|
||||
import.meta.env.VITE_VPV_LICENSE ??
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
:data-status="setStatus(project.steps, project.currentStep, step)"
|
||||
>
|
||||
<span class="pill" :data-icon="step.id">
|
||||
<span>{{ step.label }}</span>
|
||||
<span>{{ t('steps.' + step.id) }}</span>
|
||||
</span>
|
||||
</li>
|
||||
</ol>
|
||||
|
|
@ -42,6 +42,7 @@
|
|||
import dayjs from 'dayjs';
|
||||
import 'dayjs/locale/fr';
|
||||
import { useProjectStore } from '../../stores/project';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
dayjs.locale('fr');
|
||||
|
||||
|
|
@ -52,4 +53,5 @@ const frenchFormattedModified = dayjs(project.modified).format(
|
|||
);
|
||||
|
||||
const { stepsLabels, setStatus, isEmptyBrief } = useProjectStore();
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
:data-status="setStatus(page.steps, page.content.currentstep, step)"
|
||||
>
|
||||
<h2 :id="step.id">
|
||||
<span :data-icon="step.id">{{ step.label }}</span>
|
||||
<span :data-icon="step.id">{{ t('steps.' + step.id) }}</span>
|
||||
</h2>
|
||||
<div
|
||||
ref="cards-node"
|
||||
|
|
@ -29,6 +29,7 @@ import SimpleDocument from './cards/SimpleDocument.vue';
|
|||
import VirtualSample from './cards/VirtualSample.vue';
|
||||
import PhysicalSample from './cards/PhysicalSample.vue';
|
||||
import { useUserStore } from '../../stores/user';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { step } = defineProps({
|
||||
step: Object,
|
||||
|
|
@ -49,6 +50,7 @@ const { page } = usePageStore();
|
|||
const { setStatus } = useProjectStore();
|
||||
const cardsNode = useTemplateRef('cards-node');
|
||||
const { user } = useUserStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
// Hooks
|
||||
onMounted(() => {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
modal
|
||||
:draggable="false"
|
||||
:dismissableMask="true"
|
||||
header="Titre du PDF"
|
||||
:header="t('dialogs.pdfTitle')"
|
||||
class="dialog"
|
||||
:class="[
|
||||
{ 'with-comments': isCommentsOpen },
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
class="btn"
|
||||
@click="validate()"
|
||||
>
|
||||
Valider et envoyer le brief
|
||||
{{ t('buttons.validate') }}
|
||||
</button>
|
||||
<h2
|
||||
v-if="openedFile"
|
||||
|
|
@ -52,7 +52,9 @@ import { useRoute, useRouter } from 'vue-router';
|
|||
import { storeToRefs } from 'pinia';
|
||||
import { useApiStore } from '../../stores/api';
|
||||
import { usePageStore } from '../../stores/page';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
const { openedFile, isCommentsOpen } = storeToRefs(useDialogStore());
|
||||
|
||||
const router = useRouter();
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@
|
|||
<label
|
||||
for="project-description"
|
||||
class="flex | text-sm text-grey-700 | mb-8"
|
||||
>Description du projet</label
|
||||
>{{ t('forms.description') }}</label
|
||||
>
|
||||
<textarea
|
||||
name="project-description"
|
||||
id="project-description"
|
||||
placeholder="Ajoutez une description générale de votre projet…"
|
||||
:placeholder="t('forms.descriptionPlaceholder')"
|
||||
rows="2"
|
||||
class="border border-grey-200 | rounded-xl | p-16 | w-full"
|
||||
v-model="page.content.description"
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
></textarea>
|
||||
</div>
|
||||
<fieldset class="project-details__filters | flex-1">
|
||||
<legend class="text-sm text-grey-700 | mb-8">Filtrer par tags</legend>
|
||||
<legend class="text-sm text-grey-700 | mb-8">{{ t('forms.filterByTags') }}</legend>
|
||||
<div class="flex" style="gap: var(--space-8)">
|
||||
<button
|
||||
class="btn btn--sm btn--grey"
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
role="switch"
|
||||
@click="clearTags()"
|
||||
>
|
||||
Voir tout
|
||||
{{ t('buttons.seeAll') }}
|
||||
</button>
|
||||
<template v-for="tag in page.tags" :key="tag">
|
||||
<input
|
||||
|
|
@ -53,6 +53,9 @@ import { ref, watch } from "vue";
|
|||
import { usePageStore } from "../../../stores/page";
|
||||
import StringUtils from "../../../utils/string";
|
||||
import debounce from "lodash/debounce";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { selectedTags } = defineProps({
|
||||
selectedTags: Array,
|
||||
|
|
@ -93,7 +96,7 @@ const saveDescription = debounce(() => {
|
|||
console.log(json);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Erreur lors de la sauvegarde :", error);
|
||||
console.error(t('errors.saveFailed'), error);
|
||||
isWaitingForSave.value = false;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
v-model:visible="isOpen"
|
||||
id="image-details"
|
||||
modal
|
||||
header="Détails de l’image"
|
||||
:header="t('dialogs.imageDetails')"
|
||||
class="bg-white | text-grey-800 | rounded-2xl | overflow-hidden"
|
||||
>
|
||||
<picture :style="'--image: url('+image.url+')'">
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
</picture>
|
||||
<div class="flex flex-col | p-32" style="--row-gap: var(--space-32)">
|
||||
<fieldset class="image__tags">
|
||||
<legend class="text-sm text-grey-700 | mb-8">Tags</legend>
|
||||
<legend class="text-sm text-grey-700 | mb-8">{{ t('forms.tags') }}</legend>
|
||||
<div class="flex" style="gap: var(--space-8)">
|
||||
<template v-for="(pageTag, index) in page.tags" :key="index">
|
||||
<input
|
||||
|
|
@ -33,12 +33,12 @@
|
|||
<label
|
||||
for="image-description"
|
||||
class="flex | text-sm text-grey-700 | mb-8"
|
||||
>Description de l’image</label
|
||||
>{{ t('forms.imageDescription') }}</label
|
||||
>
|
||||
<textarea
|
||||
name="image-description"
|
||||
id="image-description"
|
||||
placeholder="Ajoutez une description à l’image…"
|
||||
:placeholder="t('forms.imageDescriptionPlaceholder')"
|
||||
class="border border-grey-200 | rounded-xl | p-16 | w-full"
|
||||
v-model="image.description"
|
||||
@input="saveDescription()"
|
||||
|
|
@ -49,7 +49,7 @@
|
|||
class="btn btn--black-10 | ml-auto mt-auto"
|
||||
@click="remove()"
|
||||
>
|
||||
Supprimer cette image
|
||||
{{ t('buttons.deleteImage') }}
|
||||
</button>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
|
@ -61,6 +61,9 @@ import { usePageStore } from "../../../stores/page";
|
|||
import StringUtils from "../../../utils/string";
|
||||
import Dialog from "primevue/dialog";
|
||||
import debounce from "lodash/debounce";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { imageDetails } = defineProps({
|
||||
imageDetails: Object,
|
||||
|
|
@ -97,7 +100,7 @@ function saveTags() {
|
|||
console.log(json);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Erreur lors de la sauvegarde :", error);
|
||||
console.error(t('errors.saveFailed'), error);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -122,7 +125,7 @@ const saveDescription = debounce(() => {
|
|||
emit("");
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Erreur lors de la sauvegarde :", error);
|
||||
console.error(t('errors.saveFailed'), error);
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
|
|
@ -142,7 +145,7 @@ function remove() {
|
|||
isOpen.value = false;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Erreur lors de la suppression :", error);
|
||||
console.error(t('errors.deleteFailed'), error);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
:draggable="false"
|
||||
:dismissableMask="true"
|
||||
modal
|
||||
header="Ajouter des images"
|
||||
:header="t('dialogs.addImages')"
|
||||
class="bg-white | text-grey-800 | rounded-2xl | overflow-hidden | p-32"
|
||||
>
|
||||
<div class="with-sidebar | h-full">
|
||||
|
|
@ -56,19 +56,18 @@
|
|||
id="delete-image"
|
||||
v-model:visible="deleteIsOpen"
|
||||
modal
|
||||
header="Êtes-vous sûr de vouloir supprimer cette image ?"
|
||||
:header="t('dialogs.deleteConfirm')"
|
||||
class="bg-white | text-grey-800 | rounded-2xl | overflow-hidden | text-center | w-full max-w | p-16 pt-32"
|
||||
style="--row-gap: var(--space-32); --max-w: 40rem"
|
||||
>
|
||||
<p class="text-grey-700 | px-16">
|
||||
Si vous supprimez cette image, celle-ci disparaîtra de votre brief ainsi
|
||||
que toutes les informations qui lui sont attribuées.
|
||||
{{ t('dialogs.deleteWarning') }}
|
||||
</p>
|
||||
<template #footer>
|
||||
<button class="btn btn--secondary | flex-1" @click="deleteIsOpen = false">
|
||||
Annuler
|
||||
{{ t('buttons.cancel') }}
|
||||
</button>
|
||||
<button class="btn | flex-1" @click="">Supprimer</button>
|
||||
<button class="btn | flex-1" @click="">{{ t('buttons.delete') }}</button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
|
@ -79,6 +78,9 @@ import ImagesEditPanel from './ImagesEditPanel.vue';
|
|||
import { ref, watch } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useAddImagesModalStore } from '../../../../stores/addImagesModal';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { isAddImagesModalOpen } = defineProps({
|
||||
isAddImagesModalOpen: Boolean,
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@
|
|||
:multiple="true"
|
||||
accept="image/*"
|
||||
:maxFileSize="1000000"
|
||||
invalidFileSizeMessage="Fichier trop lourd"
|
||||
chooseLabel="Ajouter une ou plusieurs images"
|
||||
:invalidFileSizeMessage="t('errors.saveFailed')"
|
||||
:chooseLabel="t('forms.addImages')"
|
||||
class="flex flex-col justify-center | bg-white | border border-grey-200 | text-grey-800 | font-medium | rounded-xl"
|
||||
ref="uploadBtn"
|
||||
>
|
||||
|
|
@ -38,7 +38,7 @@
|
|||
removeFileCallback,
|
||||
}"
|
||||
>
|
||||
<div v-if="files.length > 0">Fichiers importés</div>
|
||||
<div v-if="files.length > 0">{{ t('forms.uploadedFiles') }}</div>
|
||||
</template>
|
||||
</FileUpload>
|
||||
<figure
|
||||
|
|
@ -71,6 +71,9 @@ import { storeToRefs } from "pinia";
|
|||
import { computed, ref } from "vue";
|
||||
import { useAddImagesModalStore } from "../../../../stores/addImagesModal";
|
||||
import ArrayUtils from "../../../../utils/array";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { page } = storeToRefs(usePageStore());
|
||||
const toast = useToast();
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
v-if="images.length > 0"
|
||||
:step="step"
|
||||
:images="images"
|
||||
:uri="'/' + step.uri"
|
||||
:uri="addLocalePrefix(step.uri)"
|
||||
/>
|
||||
<Document v-if="pdf" :step="step" :pdf="pdf" />
|
||||
|
||||
|
|
@ -12,11 +12,11 @@
|
|||
class="btn | w-full"
|
||||
@click="goToImagesBrief()"
|
||||
>
|
||||
Ajouter un brief via la plateforme
|
||||
{{ t('brief.addPlatform') }}
|
||||
</button>
|
||||
<div class="btn | w-full" v-if="!pdf && step.id === 'clientBrief'">
|
||||
<label for="upload-pdf">
|
||||
Ajouter un brief PDF
|
||||
{{ t('brief.addPdf') }}
|
||||
<input
|
||||
id="upload-pdf"
|
||||
type="file"
|
||||
|
|
@ -35,10 +35,13 @@ import Images from "./Images.vue";
|
|||
import Document from "./Document.vue";
|
||||
import { useBriefStore } from "../../../stores/brief";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { addLocalePrefix } from "../../../utils/router";
|
||||
|
||||
const { step } = defineProps({ step: Object });
|
||||
const { addPdf } = useBriefStore();
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
|
||||
const images = computed(() => {
|
||||
return step.files.filter((file) => file.type === "image");
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
v-if="pdf.comments?.length > 0"
|
||||
class="order-last | text-sm text-primary font-medium"
|
||||
>
|
||||
<router-link :to="'/' + step.uri + '&comments=true'">
|
||||
<router-link :to="addLocalePrefix(step.uri) + '&comments=true'">
|
||||
{{ pdf.comments.length }} commentaire{{
|
||||
pdf.comments.length > 1 ? "s" : ""
|
||||
}}
|
||||
|
|
@ -39,6 +39,7 @@ import { useRoute } from "vue-router";
|
|||
import DateTime from "./DateTime.vue";
|
||||
import { computed } from "vue";
|
||||
import { useDesignToLightStore } from "../../../stores/designToLight";
|
||||
import { addLocalePrefix } from "../../../utils/router";
|
||||
|
||||
const { step, pdf, index } = defineProps({
|
||||
step: Object,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<article class="card">
|
||||
<hgroup class="order-last">
|
||||
<h3 class="card__title | font-serif | text-lg">
|
||||
<router-link :to="uri" class="link-full">{{ step.label }}</router-link>
|
||||
<router-link :to="uri" class="link-full">{{ t('steps.' + step.id) }}</router-link>
|
||||
</h3>
|
||||
</hgroup>
|
||||
<DateTime :date="step.modified" />
|
||||
|
|
@ -42,6 +42,7 @@ import DateTime from './DateTime.vue';
|
|||
import { useDesignToLightStore } from '../../../stores/designToLight';
|
||||
import { useVirtualSampleStore } from '../../../stores/virtualSample';
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { images, step, uri } = defineProps({
|
||||
images: Array,
|
||||
|
|
@ -51,6 +52,7 @@ const { images, step, uri } = defineProps({
|
|||
|
||||
const { isDesignToLightStep } = useDesignToLightStore();
|
||||
const { allVariations } = useVirtualSampleStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const commentsCount = computed(() => {
|
||||
let count = 0;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
:style="'--cover: url(' + step.cover + ')'"
|
||||
>
|
||||
<h3 class="text-lg font-serif">
|
||||
<router-link :to="'/' + step.uri" class="link-full">{{
|
||||
<router-link :to="addLocalePrefix(step.uri)" class="link-full">{{
|
||||
step.title
|
||||
}}</router-link>
|
||||
</h3>
|
||||
|
|
@ -27,6 +27,7 @@
|
|||
<script setup>
|
||||
import dayjs from "dayjs";
|
||||
import "dayjs/locale/fr";
|
||||
import { addLocalePrefix } from "../../../utils/router";
|
||||
|
||||
const { step } = defineProps({ step: Object });
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import Images from './Images.vue';
|
|||
import { computed } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useVirtualSampleStore } from '../../../stores/virtualSample';
|
||||
import { addLocalePrefix } from '../../../utils/router';
|
||||
|
||||
const { step } = defineProps({ step: Object });
|
||||
|
||||
|
|
@ -27,7 +28,7 @@ const images = computed(() => {
|
|||
return allVariations.value.map((variation) => getFrontView(variation)) ?? [];
|
||||
});
|
||||
|
||||
const uri = '/' + step.uri;
|
||||
const uri = addLocalePrefix(step.uri);
|
||||
|
||||
function getFrontView(variation) {
|
||||
if (variation.files.length === 1) return variation.files[0];
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@
|
|||
>
|
||||
<span>{{
|
||||
isCompareModeEnabled
|
||||
? 'Quitter le mode comparer'
|
||||
: 'Comparer les pistes'
|
||||
? t('buttons.exitCompare')
|
||||
: t('buttons.compareTracks')
|
||||
}}</span>
|
||||
</button>
|
||||
</header>
|
||||
|
|
@ -41,7 +41,7 @@
|
|||
:backgroundColor="activeTrack.backgroundColor"
|
||||
/>
|
||||
<div v-else class="track-empty | bg-white rounded-xl w-full p-32">
|
||||
<p>Contenu non disponible pour cette piste</p>
|
||||
<p>{{ t('virtualSample.noContent') }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -49,7 +49,7 @@
|
|||
v-if="isCompareModeEnabled && activeTracks.length < 2"
|
||||
class="track-empty | bg-white rounded-xl w-full p-32"
|
||||
>
|
||||
<p>Sélectionnez sur la piste que vous souhaitez comparer</p>
|
||||
<p>{{ t('virtualSample.selectToCompare') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -62,11 +62,14 @@ import { usePageStore } from '../../../stores/page';
|
|||
import { useDialogStore } from '../../../stores/dialog';
|
||||
import { useVirtualSampleStore } from '../../../stores/virtualSample';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import Interactive360 from './Interactive360.vue';
|
||||
import SingleImage from './SingleImage.vue';
|
||||
import Selector from '../../Selector.vue';
|
||||
import slugify from 'slugify';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
modal
|
||||
:draggable="false"
|
||||
:dismissableMask="true"
|
||||
header="Titre du rendu"
|
||||
:header="t('dialogs.renderTitle')"
|
||||
class="dialog"
|
||||
:class="[
|
||||
{ 'with-comments': isCommentsOpen },
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
aria-controls="dynamic"
|
||||
@click="activeTab = 'dynamic'"
|
||||
>
|
||||
<span>Présentation dynamique</span>
|
||||
<span>{{ t('virtualSample.dynamicPresentation') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="step.files.static"
|
||||
|
|
@ -38,10 +38,10 @@
|
|||
:aria-pressed="activeTab === 'static' ? true : false"
|
||||
aria-controls="static"
|
||||
>
|
||||
<span>Vue statique</span>
|
||||
<span>{{ t('virtualSample.staticView') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<h2 class="font-serif text-lg">Échantillon virtuel</h2>
|
||||
<h2 class="font-serif text-lg">{{ t('virtualSample.title') }}</h2>
|
||||
</template>
|
||||
|
||||
<DynamicView id="dynamic" v-if="activeTab === 'dynamic'" />
|
||||
|
|
@ -66,8 +66,8 @@
|
|||
>
|
||||
<span>{{
|
||||
!isLoopAnimationEnabled
|
||||
? 'Animation en boucle'
|
||||
: 'Arrêter l’animation'
|
||||
? t('buttons.loopAnimation')
|
||||
: t('buttons.stopAnimation')
|
||||
}}</span>
|
||||
</button>
|
||||
<button
|
||||
|
|
@ -79,7 +79,7 @@
|
|||
@click="isCommentsOpen = !isCommentsOpen"
|
||||
>
|
||||
<span class="sr-only"
|
||||
>{{ isCommentsOpen ? 'Masquer' : 'Afficher' }} les commentaires</span
|
||||
>{{ isCommentsOpen ? t('buttons.hideComments') : t('buttons.showComments') }}</span
|
||||
>
|
||||
</button>
|
||||
</template>
|
||||
|
|
@ -103,7 +103,9 @@ import { useVirtualSampleStore } from '../../../stores/virtualSample';
|
|||
import { useDialogStore } from '../../../stores/dialog';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { usePageStore } from '../../../stores/page';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
const { file } = defineProps({
|
||||
file: Object,
|
||||
});
|
||||
|
|
@ -141,12 +143,12 @@ watch(isOpen, (newValue) => {
|
|||
const downloadText = computed(() => {
|
||||
if (activeTab.value === 'dynamic') {
|
||||
if (activeTracks.value.length === 1) {
|
||||
return "Télécharger l'image";
|
||||
return t('buttons.downloadImage');
|
||||
} else {
|
||||
return 'Télécharger les images';
|
||||
return t('buttons.downloadImages');
|
||||
}
|
||||
} else {
|
||||
return 'Télécharger le PDF';
|
||||
return t('buttons.downloadPdf');
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue