145145 >
146146 Cancel
147147 </UButton >
148+ <UPopover
149+ v-model:open =" popoverOpen"
150+ dismissible
151+ >
152+ <UButton
153+ icon =" mdi:content-save-cog"
154+ color =" neutral"
155+ :disabled =" loading"
156+ :loading =" loadingDraft"
157+ @click =" handleDrafts"
158+ >
159+ Load Draft
160+ </UButton >
161+
162+ <template #content =" { close } " >
163+ <div class =" p-4 max-h-96 overflow-y-auto min-w-64" >
164+ <h3 class =" text-lg font-semibold mb-2" >Available Drafts</h3 >
165+ <div
166+ v-if =" !drafts || drafts.length === 0"
167+ class =" text-gray-500 text-center py-4"
168+ >
169+ No drafts found
170+ </div >
171+ <div
172+ v-else
173+ class =" space-y-2"
174+ >
175+ <div
176+ v-for =" draft in drafts"
177+ :key =" draft.slug"
178+ class =" p-2 border rounded hover:opacity-70 cursor-pointer transition-opacity"
179+ @mouseenter =" previewDraft(draft)"
180+ @click ="
181+ () => {
182+ loadDraft(draft);
183+ close();
184+ }
185+ "
186+ >
187+ <div class =" font-medium" >
188+ {{ draft.title || draft.slug }}
189+ </div >
190+ <div
191+ v-if =" draft.title && draft.slug"
192+ class =" text-sm text-gray-500"
193+ >
194+ {{ draft.slug }}
195+ </div >
196+ </div >
197+ </div >
198+ </div >
199+ </template >
200+ </UPopover >
201+ <UButton
202+ icon =" mdi:content-save-edit"
203+ color =" secondary"
204+ :disabled =" loading || !state.slug"
205+ :loading =" savingDraft"
206+ @click =" handleSave"
207+ >
208+ Save Draft
209+ </UButton >
148210 <UButton
149211 type =" submit"
150212 icon =" mdi:content-save"
151213 :loading =" loading"
152- :disabled =" loading || state.content.length < 50"
214+ :disabled =" loading || !state.slug || state.content.length < 50"
153215 >
154216 {{ mode === 'edit' ? 'Update Post' : 'Create Post' }}
155217 </UButton >
@@ -173,6 +235,7 @@ const emit = defineEmits<{
173235}>();
174236
175237const { addPost, updatePost } = useBlogPosts ();
238+ const { drafts, fetchDrafts, saveDraft } = useDrafts ();
176239
177240const state = reactive <BlogPostInput >({
178241 title: props .initialData ?.title || ' ' ,
@@ -182,6 +245,8 @@ const state = reactive<BlogPostInput>({
182245 tags: props .initialData ?.tags || []
183246});
184247
248+ const previousState = ref <BlogPostInput | null >(null );
249+
185250const tagInput = ref (' ' );
186251const loading = ref (false );
187252const error = ref (' ' );
@@ -288,4 +353,116 @@ const handleSubmit = async (event: FormSubmitEvent<BlogPostInput>) => {
288353const handleCancel = () => {
289354 emit (' cancel' );
290355};
356+
357+ const loadingDraft = ref (false );
358+ const draftSelected = ref (false );
359+ const popoverOpen = ref (false );
360+
361+ watch (popoverOpen , (isOpen ) => {
362+ if (! isOpen ) {
363+ if (! draftSelected .value ) {
364+ clearPreview ();
365+ }
366+ draftSelected .value = false ;
367+ }
368+ });
369+
370+ const previewDraft = (draft : Partial <BlogPostInput >) => {
371+ if (! previousState .value ) {
372+ previousState .value = { ... state };
373+ }
374+
375+ Object .assign (state , {
376+ title: draft .title || ' ' ,
377+ slug: draft .slug || ' ' ,
378+ content: draft .content || ' ' ,
379+ thumbnail_url: draft .thumbnail_url || ' ' ,
380+ tags: draft .tags || []
381+ });
382+ };
383+
384+ const clearPreview = () => {
385+ if (previousState .value ) {
386+ Object .assign (state , previousState .value );
387+ previousState .value = null ;
388+ }
389+ };
390+
391+ const loadDraft = (draft : Partial <BlogPostInput >) => {
392+ const hasExistingData =
393+ previousState .value &&
394+ (previousState .value .title ||
395+ previousState .value .slug ||
396+ previousState .value .content ||
397+ previousState .value .thumbnail_url ||
398+ previousState .value .tags .length > 0 );
399+
400+ if (hasExistingData ) {
401+ const confirmed = confirm (' Loading this draft will replace your current form data. Continue?' );
402+ if (! confirmed ) {
403+ clearPreview ();
404+ draftSelected .value = false ;
405+ return ;
406+ }
407+ }
408+
409+ Object .assign (state , {
410+ title: draft .title || ' ' ,
411+ slug: draft .slug || ' ' ,
412+ content: draft .content || ' ' ,
413+ thumbnail_url: draft .thumbnail_url || ' ' ,
414+ tags: draft .tags || []
415+ });
416+
417+ previousState .value = null ;
418+ draftSelected .value = true ;
419+
420+ toast .add ({
421+ title: ' Draft Loaded' ,
422+ description: ' The draft has been loaded into the form.' ,
423+ icon: ' mdi:content-save-cog' ,
424+ color: ' info'
425+ });
426+ };
427+
428+ const handleDrafts = async () => {
429+ loadingDraft .value = true ;
430+ try {
431+ await fetchDrafts ();
432+ } catch (err : any ) {
433+ toast .add ({
434+ title: ' Error' ,
435+ description: err .message || ' Failed to load drafts' ,
436+ icon: ' mdi:alert-circle' ,
437+ color: ' error'
438+ });
439+ } finally {
440+ loadingDraft .value = false ;
441+ }
442+ };
443+
444+ const savingDraft = ref (false );
445+
446+ const handleSave = async () => {
447+ savingDraft .value = true ;
448+ try {
449+ await saveDraft ({ ... state , slug: state .slug });
450+
451+ toast .add ({
452+ title: ' Draft Saved' ,
453+ description: ' Your draft has been saved successfully.' ,
454+ icon: ' mdi:content-save-edit' ,
455+ color: ' success'
456+ });
457+ } catch (err : any ) {
458+ toast .add ({
459+ title: ' Error' ,
460+ description: err .message || ' Failed to save draft' ,
461+ icon: ' mdi:alert-circle' ,
462+ color: ' error'
463+ });
464+ } finally {
465+ savingDraft .value = false ;
466+ }
467+ };
291468 </script >
0 commit comments