Models refactor + Basic functionnalities
This commit is contained in:
@@ -7,6 +7,6 @@ import AppLogoIcon from '@/components/AppLogoIcon.vue';
|
||||
<AppLogoIcon class="size-5 fill-current text-white dark:text-black" />
|
||||
</div>
|
||||
<div class="ml-1 grid flex-1 text-left text-sm">
|
||||
<span class="mb-0.5 truncate leading-tight font-semibold">Laravel Starter Kit</span>
|
||||
<span class="mb-0.5 truncate leading-tight font-semibold">CVAtron</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -7,14 +7,6 @@ import { type NavItem } from '@/types';
|
||||
import { Link } from '@inertiajs/vue3';
|
||||
import { LayoutGrid } from 'lucide-vue-next';
|
||||
import AppLogo from './AppLogo.vue';
|
||||
|
||||
const mainNavItems: NavItem[] = [
|
||||
{
|
||||
title: 'Dashboard',
|
||||
href: '/dashboard',
|
||||
icon: LayoutGrid,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -32,7 +24,7 @@ const mainNavItems: NavItem[] = [
|
||||
</SidebarHeader>
|
||||
|
||||
<SidebarContent>
|
||||
<NavMain :items="mainNavItems" />
|
||||
<slot name="sidebar-content" />
|
||||
</SidebarContent>
|
||||
|
||||
<SidebarFooter>
|
||||
|
||||
42
resources/js/components/SidebarResumeList.vue
Normal file
42
resources/js/components/SidebarResumeList.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import { SidebarGroup, SidebarGroupLabel, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar';
|
||||
import { httpApi } from '@/lib/utils';
|
||||
import { type NavItem } from '@/types';
|
||||
import { Link, usePage } from '@inertiajs/vue3';
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { Resume } from '@/types/resume';
|
||||
|
||||
const items = ref<NavItem[]>([]);
|
||||
const page = usePage();
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const { data: resumes, error } = await httpApi<Resume[]>(route("resumes.index"));
|
||||
if (error || !resumes) {
|
||||
console.error('Failed to fetch resumes:', error);
|
||||
return;
|
||||
}
|
||||
items.value = resumes.map((resume: Resume) => ({
|
||||
title: resume.name,
|
||||
href: route("resumes.edit", resume),
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch resumes:', error);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SidebarGroup class="px-2 py-0">
|
||||
<SidebarGroupLabel>Mes CV</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem v-for="item in items" :key="item.title">
|
||||
<SidebarMenuButton as-child :is-active="item.href === page.url" :tooltip="item.title">
|
||||
<Link :href="item.href">
|
||||
<span>{{ item.title }}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
</template>
|
||||
20
resources/js/components/resume/ResumeComponent.vue
Normal file
20
resources/js/components/resume/ResumeComponent.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { ResumeComponentPlacement } from '@/types/resume';
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
componentPlacement: ResumeComponentPlacement | null
|
||||
}>();
|
||||
|
||||
const componentFile = defineAsyncComponent(
|
||||
() => import(
|
||||
/* @vite-ignore */
|
||||
`./resumeComponents/${props.componentPlacement?.component_data?.component?.vue_component_name}`
|
||||
)
|
||||
);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="componentFile" :componentPlacement="props.componentPlacement" />
|
||||
</template>
|
||||
74
resources/js/components/resume/ResumeComponentEdit.vue
Normal file
74
resources/js/components/resume/ResumeComponentEdit.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<script setup lang="ts">
|
||||
import { Resume, ResumeComponentPlacement, ResumeInputData } from '@/types/resume';
|
||||
import Button from '../ui/button/Button.vue';
|
||||
import { ChevronLeft } from 'lucide-vue-next';
|
||||
import { SidebarGroup, SidebarGroupLabel } from '@/components/ui/sidebar';
|
||||
import ResumeComponentEditForm from './ResumeComponentEditForm.vue';
|
||||
import { httpApi } from '@/lib/utils';
|
||||
|
||||
const SEND_CHANGED_DATA_DELAY = 500;
|
||||
|
||||
const props = defineProps<{
|
||||
resume: Resume
|
||||
selectedComponent: ResumeComponentPlacement | null
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['selected-component-change']);
|
||||
|
||||
let delayedSendTimeout: ReturnType<typeof setTimeout> | null = null
|
||||
async function sendChangedData(newData: ResumeInputData[]) {
|
||||
const newSelectedComponent = {
|
||||
...props.selectedComponent,
|
||||
component_data: {
|
||||
...props.selectedComponent?.component_data,
|
||||
input_data: newData
|
||||
}
|
||||
};
|
||||
|
||||
let data, error;
|
||||
|
||||
if (delayedSendTimeout) {
|
||||
clearTimeout(delayedSendTimeout);
|
||||
}
|
||||
delayedSendTimeout = setTimeout(async () => {
|
||||
const { data, error } = await httpApi(route('resume-component-placements.update', newSelectedComponent.id), {
|
||||
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;
|
||||
}
|
||||
}, SEND_CHANGED_DATA_DELAY);
|
||||
|
||||
|
||||
// Handle error
|
||||
if (error) {
|
||||
console.error('Failed to update component placement:', error, data);
|
||||
return;
|
||||
}
|
||||
|
||||
emit('selected-component-change', newSelectedComponent);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full w-full">
|
||||
<Button @click="emit('selected-component-change', null)" variant="outline" size="icon" class="cursor-pointer"><ChevronLeft class="w-4 h-4" /></Button>
|
||||
<SidebarGroup class="w-full p-0">
|
||||
<SidebarGroupLabel>{{ props.selectedComponent?.component_data?.component?.name }}</SidebarGroupLabel>
|
||||
<ResumeComponentEditForm
|
||||
v-if="props.selectedComponent?.component_data?.input_data"
|
||||
:data="props.selectedComponent?.component_data?.input_data!"
|
||||
@data-changed="sendChangedData($event)"
|
||||
/>
|
||||
<p v-else class="text-destructive">No component input data : {{ props.selectedComponent?.component_data }}</p>
|
||||
</SidebarGroup>
|
||||
</div>
|
||||
</template>
|
||||
28
resources/js/components/resume/ResumeComponentEditForm.vue
Normal file
28
resources/js/components/resume/ResumeComponentEditForm.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import { ResumeInputData } from '@/types/resume';
|
||||
import ResumeComponentEditFormInput from './ResumeComponentEditFormInput.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
data: ResumeInputData[]
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['data-changed']);
|
||||
|
||||
function handleDataChanged(updatedData: ResumeInputData) {
|
||||
const index = props.data.findIndex(input => input.id === updatedData.id);
|
||||
const dataCopy = [...props.data];
|
||||
if (index !== -1) {
|
||||
dataCopy[index] = updatedData;
|
||||
emit('data-changed', dataCopy);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form
|
||||
@submit.prevent="emit('data-changed', props.data)"
|
||||
class="w-full space-y-4"
|
||||
>
|
||||
<ResumeComponentEditFormInput v-for="input in props.data" :model="input" v-bind:key="input.id" @data-changed="handleDataChanged" />
|
||||
</form>
|
||||
</template>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { ResumeInputData } from '@/types/resume';
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import Label from '../ui/label/Label.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
model: ResumeInputData
|
||||
}>();
|
||||
|
||||
const componentFile = defineAsyncComponent(
|
||||
() => import(
|
||||
/* @vite-ignore */
|
||||
`./resumeComponentsFormInput/${props.model.component_input.data_type.vue_component_name}`
|
||||
)
|
||||
);
|
||||
|
||||
const emit = defineEmits(['data-changed']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<Label>{{ props.model.component_input?.label }}</Label>
|
||||
<component
|
||||
class="file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground
|
||||
dark:bg-primary/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-primary px-3 py-1 text-base shadow-xs
|
||||
transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-primary file:text-sm
|
||||
file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm
|
||||
focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]
|
||||
aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive"
|
||||
:name="props.model.component_input?.name"
|
||||
:is="componentFile"
|
||||
:model="props.model"
|
||||
@data-changed="emit('data-changed', $event)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
33
resources/js/components/resume/ResumeComponentsList.vue
Normal file
33
resources/js/components/resume/ResumeComponentsList.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import { Resume, ResumeComponentPlacement } from '@/types/resume';
|
||||
import { computed } from 'vue';
|
||||
import { SidebarGroup, SidebarGroupLabel, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar';
|
||||
import { Link } from '@inertiajs/vue3';
|
||||
|
||||
const props = defineProps<{
|
||||
resume: Resume;
|
||||
selectedComponent: ResumeComponentPlacement | null;
|
||||
}>();
|
||||
|
||||
const orderedComponentsPlacements = computed(() => {
|
||||
console.log('Ordered Components Placements:', props.resume.components_placements);
|
||||
return props.resume.components_placements ? [...props.resume.components_placements].sort((a, b) => a.order - b.order) : [];
|
||||
});
|
||||
|
||||
const emit = defineEmits(['selected-component-change']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SidebarGroup class="w-full p-0">
|
||||
<SidebarGroupLabel>Composants du CV</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem v-for="component in orderedComponentsPlacements" :key="component.id">
|
||||
<SidebarMenuButton as-child class="cursor-pointer" :tooltip="component.component_data?.component?.name">
|
||||
<div @click="emit('selected-component-change', component)">
|
||||
<span>{{ component.component_data?.component?.name }}</span>
|
||||
</div>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
</template>
|
||||
33
resources/js/components/resume/ResumeEditPanel.vue
Normal file
33
resources/js/components/resume/ResumeEditPanel.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import { Resume, ResumeComponentPlacement } from '@/types/resume';
|
||||
import ResumeComponentEdit from './ResumeComponentEdit.vue';
|
||||
import ResumeComponentsList from './ResumeComponentsList.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
resume: Resume
|
||||
selectedComponent: ResumeComponentPlacement | null
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['selected-component-change']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full flex-1 gap-4 rounded-xl p-4 overflow-x-auto max-w-[25%] bg-accent relative">
|
||||
<Transition mode="out-in" appear>
|
||||
<ResumeComponentEdit v-if="selectedComponent != null" :resume="props.resume" :selectedComponent="props.selectedComponent" @selected-component-change="emit('selected-component-change', $event)" :key="selectedComponent ? selectedComponent.id : 'form'" />
|
||||
<ResumeComponentsList v-else :resume="props.resume" :selectedComponent="props.selectedComponent" @selected-component-change="emit('selected-component-change', $event)" />
|
||||
</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>
|
||||
19
resources/js/components/resume/ResumePreviewPanel.vue
Normal file
19
resources/js/components/resume/ResumePreviewPanel.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { Resume, ResumeComponentPlacement } from '@/types/resume';
|
||||
import ResumeComponent from './ResumeComponent.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
resume: Resume,
|
||||
selectedComponent: ResumeComponentPlacement | null
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['selected-component-change']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-2 w-full p-6">
|
||||
<div id="resume" class="aspect-[0.707317073] w-full max-w-[84.1cm] bg-white text-black">
|
||||
<ResumeComponent v-for="componentPlacement in resume.components_placements" :key="componentPlacement.id" :componentPlacement="componentPlacement" @click="emit('selected-component-change', componentPlacement)" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
15
resources/js/components/resume/resumeComponents/email.vue
Normal file
15
resources/js/components/resume/resumeComponents/email.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { ResumeComponentPlacement } from '@/types/resume';
|
||||
|
||||
const props = defineProps<{
|
||||
componentPlacement: ResumeComponentPlacement | null
|
||||
}>();
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full">
|
||||
I'm an email component : {{ props.componentPlacement?.component_data?.input_data[0].value }}
|
||||
|
||||
</div>
|
||||
</template>
|
||||
15
resources/js/components/resume/resumeComponents/name.vue
Normal file
15
resources/js/components/resume/resumeComponents/name.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { ResumeComponentPlacement } from '@/types/resume';
|
||||
|
||||
const props = defineProps<{
|
||||
componentPlacement: ResumeComponentPlacement | null
|
||||
}>();
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full">
|
||||
I'm an name component : {{ props.componentPlacement?.component_data?.input_data[0].value }}
|
||||
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { ResumeInputData } from '@/types/resume';
|
||||
|
||||
const props = defineProps<{
|
||||
model: ResumeInputData
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['data-changed']);
|
||||
|
||||
console.log('model value ', props.model);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input
|
||||
type="email"
|
||||
:value="props.model.value"
|
||||
@input="emit('data-changed', { ...props.model, value: $event.target.value })"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { ResumeInputData } from '@/types/resume';
|
||||
|
||||
const props = defineProps<{
|
||||
model: ResumeInputData
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['data-changed']);
|
||||
|
||||
console.log('model value ', props.model);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input
|
||||
type="text"
|
||||
:value="props.model.value"
|
||||
@input="emit('data-changed', { ...props.model, value: $event.target.value })"
|
||||
/>
|
||||
</template>
|
||||
Reference in New Issue
Block a user