@@ -135,10 +135,20 @@ <h1 class="create-post-page__title">Create Post</h1>
135135 </ div >
136136
137137 < div class ="field create-post-page__description-field " :style ="isLinkType ? 'display:flex' : 'display: none' ">
138- {% include "v3/includes/_field_textarea.html" with name="content" field_id="description" label="Description" value=form.content.value|default_if_none:"" alpine_error="errors.content" error=form.content.errors.0 alpine_disabled_expr="isWriteUp" %}
139- {% comment %} Auto-Generate is Link-only (Video is out of scope). Disabled until the link field holds a valid URL. Click handler / Saving indicator / actual fetch are wired in follow-up commits. {% endcomment %}
138+ {% include "v3/includes/_field_textarea.html" with name="content" field_id="description" label="Description" value=form.content.value|default_if_none:"" alpine_error="errors.content" error=form.content.errors.0 alpine_disabled_expr="isWriteUp" alpine_x_model="linkDescription" alpine_at_input="onLinkDescriptionEdited($event)" %}
139+ {% comment %} Auto-Generate is Link-only (Video is out of scope). Disabled until the link field holds a valid URL. Click handler / actual fetch are wired in follow-up commits. {% endcomment %}
140140 < div class ="create-post-page__description-actions " x-show ="isLink " x-cloak >
141141 < button type ="button " class ="btn btn-secondary create-post-page__generate-btn " :disabled ="!linkUrlValid "> Auto-Generate Description</ button >
142+ < span class ="create-post-page__save-indicator " role ="status " aria-live ="polite ">
143+ < span class ="create-post-page__save-state " x-show ="linkDescriptionSaveStatus === 'saving' " x-cloak >
144+ < span class ="create-post-page__save-text "> Saving</ span >
145+ {% include "includes/icon.html" with icon_name="saving" icon_class="create-post-page__save-icon create-post-page__save-icon--spin" icon_size=16 %}
146+ </ span >
147+ < span class ="create-post-page__save-state " x-show ="linkDescriptionSaveStatus === 'saved' " x-cloak >
148+ < span class ="create-post-page__save-text "> Saved</ span >
149+ {% include "includes/icon.html" with icon_name="saved" icon_class="create-post-page__save-icon" icon_size=16 %}
150+ </ span >
151+ </ span >
142152 </ div >
143153 </ div >
144154
@@ -166,11 +176,14 @@ <h1 class="create-post-page__title">Create Post</h1>
166176 contentLength : 0 ,
167177 description : '{{ form.summary.value|default_if_none:""|escapejs }}' ,
168178 externalUrl : '{{ form.external_url.value|default_if_none:""|escapejs }}' ,
179+ linkDescription : '{{ form.content.value|default_if_none:""|escapejs }}' ,
169180 generating : false ,
170- descriptionSaveStatus : '' , // '' (hidden) / 'saving' / 'saved'
171- contentSaveStatus : '' , // same states, for the Content field
181+ descriptionSaveStatus : '' , // '' (hidden) / 'saving' / 'saved'
182+ contentSaveStatus : '' , // same states, for the Content field
183+ linkDescriptionSaveStatus : '' , // same states, for the Link Description field
172184 _descriptionSaveTimer : null ,
173185 _contentSaveTimer : null ,
186+ _linkDescriptionSaveTimer : null ,
174187 errors : {
175188 post_type : '' ,
176189 title : '{{ form.title.errors.0|default:""|escapejs }}' ,
@@ -189,10 +202,12 @@ <h1 class="create-post-page__title">Create Post</h1>
189202 } ,
190203
191204 init ( ) {
192- // Restore saved Content and Description drafts for the current post
193- // type. On a fresh load with no draft this leaves the field untouched.
205+ // Restore saved Content / Description / Link-Description drafts for the
206+ // current post type. On a fresh load with no draft this leaves the
207+ // field untouched.
194208 this . restoreContentDraft ( ) ;
195209 this . restoreDescriptionDraft ( ) ;
210+ this . restoreLinkDescriptionDraft ( ) ;
196211 } ,
197212
198213 handleFieldChange ( e ) {
@@ -202,12 +217,15 @@ <h1 class="create-post-page__title">Create Post</h1>
202217 // draft belonging to the newly selected type.
203218 clearTimeout ( this . _descriptionSaveTimer ) ;
204219 clearTimeout ( this . _contentSaveTimer ) ;
220+ clearTimeout ( this . _linkDescriptionSaveTimer ) ;
205221 this . saveContentDraft ( ) ;
206222 this . saveDescriptionDraft ( ) ;
223+ this . saveLinkDescriptionDraft ( ) ;
207224 this . postType = value ;
208225 this . errors . post_type = '' ;
209226 this . restoreContentDraft ( { allowEmpty : true } ) ;
210227 this . restoreDescriptionDraft ( { allowEmpty : true } ) ;
228+ this . restoreLinkDescriptionDraft ( { allowEmpty : true } ) ;
211229 } ,
212230
213231 // Bridge from the WYSIWYG editor: keep the counter, indicator, and draft
@@ -322,6 +340,50 @@ <h1 class="create-post-page__title">Create Post</h1>
322340 try { localStorage . removeItem ( this . contentDraftKey ( ) ) ; } catch ( _ ) { }
323341 } ,
324342
343+ // ── Link description draft: localStorage, keyed per post type ─────────
344+ linkDescriptionDraftKey ( ) {
345+ return 'boost:createPost:linkDescriptionDraft:' + ( this . postType || '' ) ;
346+ } ,
347+
348+ onLinkDescriptionEdited ( e ) {
349+ const value = e ? e . target . value : this . linkDescription ;
350+ clearTimeout ( this . _linkDescriptionSaveTimer ) ;
351+ if ( ! value ) {
352+ this . linkDescriptionSaveStatus = '' ;
353+ this . clearLinkDescriptionDraft ( ) ;
354+ return ;
355+ }
356+ this . linkDescriptionSaveStatus = 'saving' ;
357+ this . _linkDescriptionSaveTimer = setTimeout ( ( ) => {
358+ this . saveLinkDescriptionDraft ( ) ;
359+ this . linkDescriptionSaveStatus = 'saved' ;
360+ } , 800 ) ;
361+ } ,
362+
363+ saveLinkDescriptionDraft ( ) {
364+ if ( ! this . isLink ) return ;
365+ try {
366+ if ( this . linkDescription ) localStorage . setItem ( this . linkDescriptionDraftKey ( ) , this . linkDescription ) ;
367+ else localStorage . removeItem ( this . linkDescriptionDraftKey ( ) ) ;
368+ } catch ( _ ) { }
369+ } ,
370+
371+ restoreLinkDescriptionDraft ( { allowEmpty = false } = { } ) {
372+ if ( ! this . isLink ) {
373+ if ( allowEmpty ) this . linkDescriptionSaveStatus = '' ;
374+ return ;
375+ }
376+ let saved = null ;
377+ try { saved = localStorage . getItem ( this . linkDescriptionDraftKey ( ) ) ; } catch ( _ ) { }
378+ if ( saved !== null ) this . linkDescription = saved ;
379+ else if ( allowEmpty ) this . linkDescription = '' ;
380+ this . linkDescriptionSaveStatus = this . linkDescription ? 'saved' : '' ;
381+ } ,
382+
383+ clearLinkDescriptionDraft ( ) {
384+ try { localStorage . removeItem ( this . linkDescriptionDraftKey ( ) ) ; } catch ( _ ) { }
385+ } ,
386+
325387 // Send the Content to the backend, which runs the summarization model.
326388 async generateDescription ( ) {
327389 const content = ( this . content || '' ) . trim ( ) ;
@@ -399,6 +461,7 @@ <h1 class="create-post-page__title">Create Post</h1>
399461 // `form.summary.value` so the user's text stays visible.)
400462 this . clearContentDraft ( ) ;
401463 this . clearDescriptionDraft ( ) ;
464+ this . clearLinkDescriptionDraft ( ) ;
402465 }
403466 } ,
404467 } ;
0 commit comments