Bug fixes + Save status
This commit is contained in:
58
resources/js/components/SaveStatusIcon.vue
Normal file
58
resources/js/components/SaveStatusIcon.vue
Normal 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>
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user