This commit is contained in:
isUnknown 2025-10-02 15:12:20 +02:00
parent 5deb07f09d
commit 8eaa893994
4 changed files with 104 additions and 71 deletions

View file

@ -114,7 +114,7 @@
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import 'dayjs/locale/fr'; import 'dayjs/locale/fr';
import uniqid from 'uniqid'; import uniqid from 'uniqid';
import { watch, ref, computed } from 'vue'; import { watch, ref, computed, onBeforeUnmount } from 'vue';
import { useUserStore } from '../../stores/user'; import { useUserStore } from '../../stores/user';
import { usePageStore } from '../../stores/page'; import { usePageStore } from '../../stores/page';
import { useApiStore } from '../../stores/api'; import { useApiStore } from '../../stores/api';
@ -158,10 +158,12 @@ watch(isAddOpen, (newVal) => {
} }
}); });
const viewContainer = const viewContainer = computed(() => {
openedFile.value?.type === 'document' if (!openedFile.value) return null;
return openedFile.value.type === 'document'
? document.querySelector('.vpv-pages-inner-container') ? document.querySelector('.vpv-pages-inner-container')
: document.querySelector('.track'); : document.querySelector('.track');
});
window.addEventListener('keydown', (event) => { window.addEventListener('keydown', (event) => {
if ( if (
@ -256,20 +258,8 @@ function closeComment() {
openedComment.value = null; openedComment.value = null;
} }
function toggleCommentPositionMode(enable) {
if (enable) {
waitingForCommentPosition.value = true;
viewContainer.classList.add('waiting-comment');
viewContainer.addEventListener('click', handleCommentPositionClick);
} else {
waitingForCommentPosition.value = false;
viewContainer.classList.remove('waiting-comment');
viewContainer.removeEventListener('click', handleCommentPositionClick);
}
}
function handleCommentPositionClick(event) { function handleCommentPositionClick(event) {
if (openedFile.value.type === 'document') { if (openedFile.value?.type === 'document') {
prepareDraftCommentInPdf(event); prepareDraftCommentInPdf(event);
} else { } else {
prepareDraftCommentInImage(event); prepareDraftCommentInImage(event);
@ -278,6 +268,21 @@ function handleCommentPositionClick(event) {
toggleCommentPositionMode(false); toggleCommentPositionMode(false);
} }
function toggleCommentPositionMode(enable) {
const container = viewContainer.value;
if (!container) return; // sécurité
if (enable) {
waitingForCommentPosition.value = true;
container.classList.add('waiting-comment');
container.addEventListener('click', handleCommentPositionClick);
} else {
waitingForCommentPosition.value = false;
container.classList.remove('waiting-comment');
container.removeEventListener('click', handleCommentPositionClick);
}
}
function prepareDraftCommentInPdf(event) { function prepareDraftCommentInPdf(event) {
const pageContainer = event.target.closest('.page-inner-container'); const pageContainer = event.target.closest('.page-inner-container');
if (!pageContainer || !viewContainer) return; if (!pageContainer || !viewContainer) return;
@ -287,7 +292,10 @@ function prepareDraftCommentInPdf(event) {
.getAttribute('aria-label'); .getAttribute('aria-label');
const pageIndex = pageLabel.charAt(pageLabel.length - 1); const pageIndex = pageLabel.charAt(pageLabel.length - 1);
const viewRect = viewContainer.getBoundingClientRect(); const container = viewContainer.value;
if (!container) return;
const viewRect = container.getBoundingClientRect();
const pageRect = pageContainer.getBoundingClientRect(); const pageRect = pageContainer.getBoundingClientRect();
const pageScroll = viewRect.top - pageRect.top; const pageScroll = viewRect.top - pageRect.top;
@ -310,7 +318,9 @@ function prepareDraftCommentInPdf(event) {
function prepareDraftCommentInImage(event) { function prepareDraftCommentInImage(event) {
if (!viewContainer) return; if (!viewContainer) return;
const imageRect = viewContainer.getBoundingClientRect(); const container = viewContainer.value;
if (!container) return; // sécurité
const imageRect = container.getBoundingClientRect();
const mouseTop = event.clientY; const mouseTop = event.clientY;
const mouseLeft = event.clientX; const mouseLeft = event.clientX;
@ -339,4 +349,11 @@ function showCorrespondingView() {
(file) => file.uuid === openedComment.value.location.file.uuid (file) => file.uuid === openedComment.value.location.file.uuid
); );
} }
onBeforeUnmount(() => {
const container = viewContainer.value;
if (container) {
container.removeEventListener('click', handleCommentPositionClick);
}
});
</script> </script>

View file

@ -9,9 +9,9 @@
:items="track.variations" :items="track.variations"
:isCompareModeEnabled="isCompareModeEnabled" :isCompareModeEnabled="isCompareModeEnabled"
:index="index" :index="index"
@update:selectedItems="selectTrack"
/> />
</div> </div>
<button <button
class="btn | ml-auto" class="btn | ml-auto"
:class="{ 'btn--secondary': isCompareModeEnabled }" :class="{ 'btn--secondary': isCompareModeEnabled }"
@ -26,17 +26,25 @@
</header> </header>
<div class="track"> <div class="track">
<template v-for="activeTrack in activeTracks" :key="activeTrack.title"> <template
v-for="activeTrack in activeTracks"
:key="activeTrack.slug || activeTrack.title"
>
<!-- accès sûr avec optional chaining -->
<Interactive360 <Interactive360
v-if="activeTrack.files.length > 1" v-if="activeTrack?.files?.length > 1"
:activeTrack="activeTrack" :activeTrack="activeTrack"
/> />
<SingleImage <SingleImage
v-else v-else-if="activeTrack?.files?.length === 1"
:file="activeTrack.files[0]" :file="activeTrack.files[0]"
:backgroundColor="activeTrack.backgroundColor" :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>
</div>
</template> </template>
<div <div
v-if="isCompareModeEnabled && activeTracks.length < 2" v-if="isCompareModeEnabled && activeTracks.length < 2"
class="track-empty | bg-white rounded-xl w-full p-32" class="track-empty | bg-white rounded-xl w-full p-32"
@ -46,6 +54,7 @@
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { computed, watch, onMounted, onBeforeMount } from 'vue'; import { computed, watch, onMounted, onBeforeMount } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
@ -63,57 +72,62 @@ const route = useRoute();
const { page } = storeToRefs(usePageStore()); const { page } = storeToRefs(usePageStore());
const { isCommentsOpen, isCommentPanelEnabled, activeTracks, openedFile } = const { isCommentsOpen, isCommentPanelEnabled, activeTracks, openedFile } =
storeToRefs(useDialogStore()); storeToRefs(useDialogStore());
const { isCompareModeEnabled } = storeToRefs(useVirtualSampleStore()); const { isCompareModeEnabled } = storeToRefs(useVirtualSampleStore());
const rawTracks = page.value.steps.find( // computed tracks à partir de la structure page
(step) => step.slug === 'virtual-sample'
).files.dynamic;
onBeforeMount(() => {
const firstTrack = rawTracks[Object.keys(rawTracks)[0]];
const firstVariation = firstTrack[0];
activeTracks.value = [firstVariation];
// TO RE-ENABLE
// if (route.hash.length > 0) {
// const trackToOpen = tracks.value.find(
// (track) => track.slug === route.hash.substring(1)
// );
// activeTracks.value = [trackToOpen];
// } else {
// activeTracks.value = [tracks.value[0]];
// }
});
onMounted(() => {
if (route.hash.length === 0) return;
const selector = route.hash.replace('#', '#track--');
const targetBtn = document.querySelector(selector);
if (targetBtn) {
targetBtn.scrollIntoView();
}
});
const tracks = computed(() => { const tracks = computed(() => {
const rawTracks = page.value.steps.find( const raw =
(step) => step.slug === 'virtual-sample' page.value.steps.find((step) => step.slug === 'virtual-sample')?.files
).files.dynamic; ?.dynamic || {};
const list = [];
const tracks = []; for (const key in raw) {
list.push({
for (const key in rawTracks) {
tracks.push({
title: key, title: key,
slug: slugify(key), slug: slugify(key),
variations: rawTracks[key], variations: raw[key] || [],
}); });
} }
return tracks; return list;
}); });
// ---------- INITIALISATION ----------
// onBeforeMount : on initialise toujours activeTracks avec une VARIATION (jamais un track)
onBeforeMount(() => {
// essayer la hash en priorité
let initialVariation = null;
if (route?.hash && route.hash.length > 0) {
const variations = tracks.value.flatMap((t) => t.variations || []);
initialVariation =
variations.find((v) => v.slug === route.hash.substring(1)) || null;
}
// fallback : première variation du premier track
if (!initialVariation) {
initialVariation = tracks.value[0]?.variations?.[0] || null;
}
if (initialVariation) {
activeTracks.value = [initialVariation];
} else {
activeTracks.value = []; // aucun contenu disponible
}
});
// scroll si hash présent
onMounted(() => {
if (route.query?.comments) isCommentsOpen.value = true;
if (!route?.hash || route.hash.length === 0) return;
const selector = route.hash.replace('#', '#track--');
const targetBtn = document.querySelector(selector);
if (targetBtn) targetBtn.scrollIntoView();
});
// ---------- COMPUTED / WATCH ----------
const isSingleImage = computed(() => { const isSingleImage = computed(() => {
return ( return (
activeTracks.value?.length === 1 && activeTracks.value?.length === 1 &&
@ -122,19 +136,18 @@ const isSingleImage = computed(() => {
}); });
const singleFile = computed(() => { const singleFile = computed(() => {
return isSingleImage.value && activeTracks.value[0].files[0]; return isSingleImage.value ? activeTracks.value[0].files[0] : null;
}); });
watch( watch(
singleFile, singleFile,
(newValue) => { (newValue) => {
if (newValue) { if (newValue) openedFile.value = newValue;
openedFile.value = newValue;
}
}, },
{ immediate: true } { immediate: true }
); );
// gestion du mode comparaison : fermer les commentaires, etc.
watch(isCompareModeEnabled, (newValue) => { watch(isCompareModeEnabled, (newValue) => {
if (newValue) { if (newValue) {
isCommentsOpen.value = false; isCommentsOpen.value = false;
@ -143,12 +156,15 @@ watch(isCompareModeEnabled, (newValue) => {
isCommentPanelEnabled.value = true; isCommentPanelEnabled.value = true;
} }
// quand on quitte le mode comparaison on retire l'élément secondaire si nécessaire
if (!newValue && activeTracks.value.length === 2) { if (!newValue && activeTracks.value.length === 2) {
activeTracks.value.pop(); activeTracks.value.pop();
} }
}); });
// ---------- UTIL / helper ----------
function getCommentsCount(track) { function getCommentsCount(track) {
if (!track || !Array.isArray(track.files)) return undefined;
let count = 0; let count = 0;
for (const file of track.files) { for (const file of track.files) {
count += file?.comments?.length || 0; count += file?.comments?.length || 0;

View file

@ -12,7 +12,7 @@ export const useUserStore = defineStore('user', () => {
const { projects } = storeToRefs(useProjectsStore()); const { projects } = storeToRefs(useProjectsStore());
const notifications = computed(() => { const notifications = computed(() => {
return projects.value.flatMap((project) => { return projects.value?.flatMap((project) => {
if (!project.notifications) return []; if (!project.notifications) return [];
return project.notifications return project.notifications

View file

@ -12,12 +12,12 @@ export const useVirtualSampleStore = defineStore('virtual-sample', () => {
const isLoopAnimationEnabled = ref(false); const isLoopAnimationEnabled = ref(false);
const isDownloadTriggered = ref(false); const isDownloadTriggered = ref(false);
const step = computed(() => { const step = computed(
return page.value.steps.find((step) => step.id === 'virtualSample'); () => page.value?.steps?.find((s) => s.id === 'virtualSample') ?? []
}); );
const activeTab = computed(() => const activeTab = computed(() =>
step.value.files.dynamic ? 'dynamic' : 'static' step.value.files?.dynamic ? 'dynamic' : 'static'
); );
const allVariations = computed(() => const allVariations = computed(() =>