Bug fixes + Save status
Some checks failed
linter / quality (push) Successful in 4m14s
tests / ci (push) Failing after 12m30s

This commit is contained in:
2025-09-27 14:09:05 +02:00
parent cb242e59ba
commit b89fd67d57
10 changed files with 194 additions and 33 deletions

View File

@@ -0,0 +1,58 @@
<script setup lang="ts">
import { useResumesStore } from '@/stores/resume';
import { computed, ref, watch } from 'vue';
import { CloudCheck, LoaderCircle, CircleAlert } from 'lucide-vue-next';
const resumeStore = useResumesStore();
const DISAPPEAR_DELAY = 5000;
const isSaving = computed(() => resumeStore.isSaving);
const error = ref<string | null>(null);
watch(() => resumeStore.savingError, (newError) => {
error.value = newError;
if (newError) {
showSaved.value = false;
}
});
const showSaved = ref(false);
let showSavedTimeout: ReturnType<typeof setTimeout>;
watch(isSaving, (newIsSaving: boolean) => {
if (!newIsSaving && !error.value) {
if (showSavedTimeout) {
clearTimeout(showSavedTimeout);
}
showSaved.value = true;
showSavedTimeout = setTimeout(() => {
showSaved.value = false;
}, DISAPPEAR_DELAY);
} else {
showSaved.value = false;
}
});
</script>
<template>
<div class="flex justify-between gap-2 items-center p-3 h-4">
<p class="text-red-400">{{ error }}</p>
<Transition mode="out-in" appear>
<CircleAlert class="w-4 h-4 text-red-500" v-if="error"/>
<LoaderCircle class="w-4 h-4 animate-spin" v-else-if="isSaving"/>
<CloudCheck class="w-4 h-4" v-else-if="showSaved" title="Sauvegardé"/>
</Transition>
</div>
</template>
<style lang="css" scoped>
.v-enter-active,
.v-leave-active {
transition: opacity 0.3s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
</style>

View File

@@ -40,7 +40,7 @@ function removeResume(deletedResume: Resume) {
if (resumeStore.resumes.length > 0) {
window.location.href = route("resumes.edit", resumeStore.resumes[0], false);
} else {
window.location.href = route("resumes.create");
window.location.href = route("dashboard");
}
}
}

View File

@@ -28,20 +28,21 @@ async function sendChangedData(newData: ResumeInputData[]) {
clearTimeout(delayedSendTimeout);
}
delayedSendTimeout = setTimeout(async () => {
const { data, error } = await httpApi(route('resume-component-placements.update', newSelectedComponent), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '',
'Accept': 'application/json'
},
body: JSON.stringify({ ...newSelectedComponent, _method: 'PUT' })
}, {immediate: true});
// Handle error
if (error) {
console.error('Failed to update component placement:', error, data);
return;
}
// const { data, error } = await httpApi(route('resume-component-placements.update', newSelectedComponent), {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '',
// 'Accept': 'application/json'
// },
// body: JSON.stringify({ ...newSelectedComponent, _method: 'PUT' })
// }, {immediate: true});
// // Handle error
// if (error) {
// console.error('Failed to update component placement:', error, data);
// return;
// }
resumeStore.modifyCurrentSelectedResumePlacementToApi(newSelectedComponent!);
}, SEND_CHANGED_DATA_DELAY);

View File

@@ -5,6 +5,7 @@ import ComponentsSelectionList from './ComponentsSelectionList.vue';
import { computed } from 'vue';
import { useResumesStore } from '@/stores/resume';
import { useShowComponentSelectionStore } from '@/stores/ui';
import SaveStatusIcon from '../SaveStatusIcon.vue';
const resumeStore = useResumesStore();
const selectedComponent = resumeStore.currentSelectedResumePlacement;
@@ -14,7 +15,8 @@ const showComponentSelection = computed<boolean>(() => showComponentSelectionSto
</script>
<template>
<div class="flex h-full flex-1 gap-4 rounded-xl p-4 overflow-x-auto max-w-[25%] bg-accent relative">
<div class="flex flex-col h-full flex-1 gap-4 rounded-xl p-4 overflow-x-auto max-w-[25%] bg-accent relative">
<SaveStatusIcon />
<Transition mode="out-in" appear>
<ResumeComponentEdit v-if="selectedComponent != null" :key="selectedComponent ? selectedComponent.id : 'form'" />
<ComponentsSelectionList v-else-if="showComponentSelection" />

View File

@@ -29,6 +29,23 @@ const breadcrumbs: BreadcrumbItem[] = [
},
];
function isSaving() {
return resumeStore.isSaving;
}
// Confirmation when closing the tab if saving
window.addEventListener("beforeunload", function (e) {
if (!isSaving()) return;
console.log('Saving in progress, showing confirmation dialog.');
const confirmationMessage = 'Des changements sont toujours en cours de sauvegarde. '
+ 'Si vous quittez avant de sauvegarder, vos modifications seront perdues.';
(e || window.event).returnValue = confirmationMessage; //Gecko + IE
return confirmationMessage; //Gecko + Webkit, Safari, Chrome etc.
});
</script>
<template>

View File

@@ -10,6 +10,8 @@ const useResumesStore = defineStore('resumes', {
resumesAreFetched: false as boolean,
currentResumeIndex: -1 as number,
selectedResumePlacementIndex: -1 as number,
isSaving: false as boolean,
savingError: null as string | null,
}),
getters: {
hasResumes: (state) => computed(() => state.resumes.length > 0),
@@ -48,9 +50,19 @@ const useResumesStore = defineStore('resumes', {
},
},
actions: {
setIsSaving(saving: boolean) {
this.isSaving = saving;
if (saving) {
this.savingError = null;
}
},
setSavingError(error: string | null) {
this.setIsSaving(false);
this.savingError = error;
},
async fetchResumes() {
try {
this.resumesAreFetched = false;
this.setIsSaving(true);
// get from cache
const cachedResumes = localStorage.resumes;
if (cachedResumes) {
@@ -59,10 +71,14 @@ const useResumesStore = defineStore('resumes', {
const { data: resumes, error } = await httpApi<Resume[]>(route("resumes.index"));
if (error || !resumes) {
console.error('Failed to fetch resumes:', error);
return;
console.error('Failed to fetch resumes:', error);
this.setSavingError(error);
return;
}
this.setIsSaving(false);
this.setResumes(resumes);
this.resumesAreFetched = false;
// Store in cache
this.saveResumesToCache();
@@ -70,6 +86,7 @@ const useResumesStore = defineStore('resumes', {
this.resumesAreFetched = true;
} catch (error) {
console.error('Failed to fetch resumes:', error);
this.setSavingError(error as string);
}
},
async updateResumeToApi(resumeIndex: number) {
@@ -83,6 +100,7 @@ const useResumesStore = defineStore('resumes', {
return;
}
this.setIsSaving(true);
const resumeToUpdate = this.resumes[resumeIndex];
const { data: updatedResume, error } = await httpApi<null>(route("resumes.update", { resume: resumeToUpdate.id }), {
method: 'PUT',
@@ -93,12 +111,16 @@ const useResumesStore = defineStore('resumes', {
},
body: JSON.stringify(resumeToUpdate),
});
this.setIsSaving(false);
this.setSavingError("Poor nigga detected !");
if (error || !updatedResume) {
console.error('Failed to update resumes:', error);
this.setSavingError(error as string);
return;
}
} catch (error) {
console.error('Failed to update resumes:', error);
this.setSavingError(error as string);
}
},
async updateCurrentResumeToApi() {
@@ -108,7 +130,38 @@ const useResumesStore = defineStore('resumes', {
localStorage.resumes = JSON.stringify(this.resumes);
},
setResumes(resumes: Array<Resume>) {
this.resumes = resumes;
// this.resumes = resumes;
// for (const resume of resumes) {
// const existingResume = this.resumes.find(r => r.id === resume.id);
// if (existingResume) {
// Object.assign(existingResume, resume);
// if (this.currentResumeIndex >= 0 && this.resumes[this.currentResumeIndex].id === resume.id) {
// // Update the current resume reference if it's the same resume
// this.currentResumeIndex = this.resumes.indexOf(existingResume);
// }
// } else {
// this.resumes.push(resume);
// }
// }
// Do a copy of the resumes array
// Set the resumes array to resumes
// For each old resume, look for it in the new resumes array by id
// If found, keep the reference of the old resume (to keep reactivity)
// If not found, add the new resume to the array
const currentResumeId = this.currentResumeIndex >= 0 && this.currentResumeIndex < this.resumes.length ? this.resumes[this.currentResumeIndex].id : null;
// this.resumes = resumes;
for (const oldResume of this.resumes) {
const existingResume = resumes.find(r => r.id === oldResume.id);
if (existingResume) {
existingResume.components_placements = oldResume.components_placements;
if (oldResume.id === currentResumeId) {
this.currentResumeIndex = this.resumes.indexOf(oldResume);
}
}
}
this.resumes = resumes
},
addResume(resume: Resume) {
this.resumes.push(resume);
@@ -141,11 +194,15 @@ const useResumesStore = defineStore('resumes', {
this.updateCurrentResume(resume);
},
setAndUpdateCurrentResumeWhenFetched(resume: Resume) {
watch(() => this.resumesAreFetched, (newVal) => {
if (newVal === true) {
this.setAndUpdateCurrentResume(resume);
}
});
if (this.resumesAreFetched) {
this.setAndUpdateCurrentResume(resume);
} else {
watch(() => this.resumesAreFetched, (newVal) => {
if (newVal === true) {
this.setAndUpdateCurrentResume(resume);
}
});
}
},
updateCurrentResume(updatedResume: Resume) {
if (this.currentResumeIndex < 0 || this.currentResumeIndex >= this.resumes.length) return;
@@ -191,6 +248,7 @@ const useResumesStore = defineStore('resumes', {
}
console.debug("Updating component placement:", componentPlacement);
this.setIsSaving(true);
const { data: updatedPlacement, error } = await httpApi<ResumeComponentPlacement>(route("resume-component-placements.update", componentPlacement.id), {
method: 'PUT',
headers: {
@@ -200,8 +258,10 @@ const useResumesStore = defineStore('resumes', {
},
body: JSON.stringify(componentPlacement),
});
this.setIsSaving(false);
if (error || !updatedPlacement) {
console.error('Failed to update resume placements:', error);
this.setSavingError(error as string);
return;
}
@@ -210,6 +270,7 @@ const useResumesStore = defineStore('resumes', {
this.saveResumesToCache();
} catch (error) {
console.error('Failed to update resume placements:', error);
this.setSavingError(error as string);
}
},
setSelectedResumePlacement(index: number) {
@@ -238,6 +299,10 @@ const useResumesStore = defineStore('resumes', {
if (!this.hasCurrentSelectedResumePlacement.value) return;
this.modifyResumePlacements(this.selectedResumePlacementIndex, updatedPlacement);
},
modifyCurrentSelectedResumePlacementToApi(updatedPlacement: ResumeComponentPlacement) {
this.modifyCurrentSelectedResumePlacement(updatedPlacement);
this.updateCurrentResumePlacementsToApi(this.selectedResumePlacementIndex);
},
swapComponentsPlacementsOrder(indexA: number, indexB: number) {
if (!this.hasCurrentResume.value || !this.currentResume.value?.components_placements) return;
const currentResume = this.currentResume.value;
@@ -266,6 +331,7 @@ const useResumesStore = defineStore('resumes', {
// It will return the new component_data that need to be set to the placement
const placementToUnlink = currentResume.components_placements![index];
try {
this.setIsSaving(true);
const { data: newComponentData, error } = await httpApi<ResumeComponentData>(route("resume-component-placements.unlink", placementToUnlink.id), {
method: 'PUT',
headers: {
@@ -274,8 +340,10 @@ const useResumesStore = defineStore('resumes', {
'Accept': 'application/json'
},
});
this.setIsSaving(false);
if (error || !newComponentData) {
console.error('Failed to unlink resume placement:', error);
this.setSavingError(error as string);
return;
}
@@ -286,6 +354,7 @@ const useResumesStore = defineStore('resumes', {
this.saveResumesToCache();
} catch (error) {
console.error('Failed to unlink resume placement:', error);
this.setSavingError(error as string);
}
},
async deleteComponentPlacement(index: number) {
@@ -295,6 +364,7 @@ const useResumesStore = defineStore('resumes', {
const placementToDelete = currentResume.components_placements![index];
try {
this.setIsSaving(true);
const { error } = await httpApi<null>(route("resume-component-placements.destroy", placementToDelete.id), {
method: 'DELETE',
headers: {
@@ -302,8 +372,10 @@ const useResumesStore = defineStore('resumes', {
'Accept': 'application/json'
},
});
this.setIsSaving(false);
if (error) {
console.error('Failed to delete resume placement:', error);
this.setSavingError(error as string);
return;
}
@@ -317,6 +389,7 @@ const useResumesStore = defineStore('resumes', {
}
} catch (error) {
console.error('Failed to delete resume placement:', error);
this.setSavingError(error as string);
}
},
clearSelectedResumePlacement() {