@@ -83,15 +83,29 @@ <h1 class="create-post-page__title">Create Post</h1>
8383 maxlength ="20000 "
8484 placeholder ="What would you like to write? "
8585 x-model ="content "
86+ @input ="onContentEdited($event) "
8687 :disabled ="isLinkType "
8788 :aria-invalid ="!!errors.content "
8889 :aria-describedby ="errors.content ? 'field-content-error' : null "
8990 > </ textarea >
9091 </ div >
9192 < p class ="field__error " id ="field-content-error " role ="alert " x-show ="errors.content " x-text ="errors.content " x-cloak aria-live ="polite "> </ p >
93+ {% comment %} Content auto-save indicator (no Auto-Generate button — that's Description-only). Hidden when nothing is written; driven by contentSaveStatus. {% endcomment %}
94+ < div class ="create-post-page__content-actions " x-show ="contentSaveStatus " x-cloak >
95+ < span class ="create-post-page__save-indicator " role ="status " aria-live ="polite ">
96+ < span class ="create-post-page__save-state " x-show ="contentSaveStatus === 'saving' " x-cloak >
97+ < span class ="create-post-page__save-text "> Saving</ span >
98+ {% include "includes/icon.html" with icon_name="saving" icon_class="create-post-page__save-icon create-post-page__save-icon--spin" icon_size=16 icon_viewbox="0 0 13.3333 13.3333" %}
99+ </ span >
100+ < span class ="create-post-page__save-state " x-show ="contentSaveStatus === 'saved' " x-cloak >
101+ < span class ="create-post-page__save-text "> Saved</ span >
102+ {% include "includes/icon.html" with icon_name="saved" icon_class="create-post-page__save-icon" icon_size=16 icon_viewbox="0 0 13.3333 13.3333" %}
103+ </ span >
104+ </ span >
105+ </ div >
92106 </ div >
93107
94- {% comment %} Description (Blog, News): plain-text field with char counter, Auto-Generate button, and auto-save indicator. Generation + save/saved toggle are wired in follow-up commits. {% endcomment %}
108+ {% comment %} Description (Blog, News): plain-text field with char counter, Auto-Generate button, and auto-save indicator. {% endcomment %}
95109 < div class ="field field--textarea create-post-page__description-section " :style ="isWriteUp ? 'display: flex' : 'display: none' ">
96110 < div class ="create-post-page__field-header ">
97111 < label class ="field__label " for ="field-post_description "> Description</ label >
@@ -106,18 +120,19 @@ <h1 class="create-post-page__title">Create Post</h1>
106120 maxlength ="1000 "
107121 placeholder ="What would you like to write? "
108122 x-model ="description "
123+ @input ="onDescriptionEdited($event) "
109124 > </ textarea >
110125 </ div >
111126 < div class ="create-post-page__description-actions ">
112127 {% comment %} Calls the generate-description endpoint; disabled while in flight. Full idle/generating/generated states are a follow-up. {% endcomment %}
113128 < button type ="button " class ="btn btn-secondary create-post-page__generate-btn " @click ="generateDescription() " :disabled ="generating "> Auto-Generate Description</ button >
114- {% comment %} Hidden when nothing is written; "Saving" while the Description changes , "Saved" once it settles (saveStatus is driven by the $watch in init() ). {% endcomment %}
129+ {% comment %} Hidden when nothing is written; "Saving" while editing , "Saved" once the draft is written to localStorage (descriptionSaveStatus driven by onDescriptionEdited / restoreDescriptionDraft ). {% endcomment %}
115130 < span class ="create-post-page__save-indicator " role ="status " aria-live ="polite ">
116- < span class ="create-post-page__save-state " x-show ="saveStatus === 'saving' " x-cloak >
131+ < span class ="create-post-page__save-state " x-show ="descriptionSaveStatus === 'saving' " x-cloak >
117132 < span class ="create-post-page__save-text "> Saving</ span >
118133 {% include "includes/icon.html" with icon_name="saving" icon_class="create-post-page__save-icon create-post-page__save-icon--spin" icon_size=16 icon_viewbox="0 0 13.3333 13.3333" %}
119134 </ span >
120- < span class ="create-post-page__save-state " x-show ="saveStatus === 'saved' " x-cloak >
135+ < span class ="create-post-page__save-state " x-show ="descriptionSaveStatus === 'saved' " x-cloak >
121136 < span class ="create-post-page__save-text "> Saved</ span >
122137 {% include "includes/icon.html" with icon_name="saved" icon_class="create-post-page__save-icon" icon_size=16 icon_viewbox="0 0 13.3333 13.3333" %}
123138 </ span >
@@ -152,8 +167,10 @@ <h1 class="create-post-page__title">Create Post</h1>
152167 content : '{{ form.content.value|default_if_none:""|escapejs }}' ,
153168 description : '{{ form.description.value|default_if_none:""|escapejs }}' ,
154169 generating : false ,
155- saveStatus : '' , // '' = nothing written (hidden), 'saving', 'saved'
156- _saveTimer : null ,
170+ descriptionSaveStatus : '' , // '' (hidden) / 'saving' / 'saved'
171+ contentSaveStatus : '' , // same states, for the Content field
172+ _descriptionSaveTimer : null ,
173+ _contentSaveTimer : null ,
157174 errors : {
158175 post_type : '' ,
159176 title : '{{ form.title.errors.0|default:""|escapejs }}' ,
@@ -166,29 +183,118 @@ <h1 class="create-post-page__title">Create Post</h1>
166183 get isLinkType ( ) { return this . postType === 'video' || this . postType === 'link' ; } ,
167184
168185 init ( ) {
169- // Description indicator: show "Saving" while it changes (typing or
170- // auto-generate), settle to "Saved" shortly after, hide when empty.
171- // Display only for now — localStorage persistence comes next.
172- this . $watch ( 'description' , ( value ) => {
173- clearTimeout ( this . _saveTimer ) ;
174- if ( ! value ) {
175- this . saveStatus = '' ;
176- return ;
177- }
178- this . saveStatus = 'saving' ;
179- this . _saveTimer = setTimeout ( ( ) => { this . saveStatus = 'saved' ; } , 800 ) ;
180- } ) ;
186+ // Restore saved Content and Description drafts for the current post
187+ // type. On a fresh load with no draft this leaves the field untouched.
188+ this . restoreContentDraft ( ) ;
189+ this . restoreDescriptionDraft ( ) ;
181190 } ,
182191
183192 handleFieldChange ( e ) {
184193 const { name, value } = e . detail ;
185194 if ( name !== 'post_type' ) return ;
195+ // Persist the current type's draft before switching, then load the
196+ // draft belonging to the newly selected type.
197+ clearTimeout ( this . _descriptionSaveTimer ) ;
198+ clearTimeout ( this . _contentSaveTimer ) ;
199+ this . saveContentDraft ( ) ;
200+ this . saveDescriptionDraft ( ) ;
186201 this . postType = value ;
187202 this . errors . post_type = '' ;
203+ this . restoreContentDraft ( { allowEmpty : true } ) ;
204+ this . restoreDescriptionDraft ( { allowEmpty : true } ) ;
205+ } ,
206+
207+ // ── Description draft: localStorage, keyed per post type ─────────────
208+ descriptionDraftKey ( ) {
209+ return 'boost:createPost:descriptionDraft:' + ( this . postType || '' ) ;
210+ } ,
211+
212+ // Runs on each keystroke (and after auto-generate): show "Saving", then
213+ // write the draft and show "Saved" once edits settle. Empty clears it.
214+ onDescriptionEdited ( e ) {
215+ const value = e ? e . target . value : this . description ;
216+ clearTimeout ( this . _descriptionSaveTimer ) ;
217+ if ( ! value ) {
218+ this . descriptionSaveStatus = '' ;
219+ this . clearDescriptionDraft ( ) ;
220+ return ;
221+ }
222+ this . descriptionSaveStatus = 'saving' ;
223+ this . _descriptionSaveTimer = setTimeout ( ( ) => {
224+ this . saveDescriptionDraft ( ) ;
225+ this . descriptionSaveStatus = 'saved' ;
226+ } , 800 ) ;
227+ } ,
228+
229+ saveDescriptionDraft ( ) {
230+ if ( ! this . isWriteUp ) return ;
231+ try {
232+ if ( this . description ) localStorage . setItem ( this . descriptionDraftKey ( ) , this . description ) ;
233+ else localStorage . removeItem ( this . descriptionDraftKey ( ) ) ;
234+ } catch ( _ ) { }
235+ } ,
236+
237+ restoreDescriptionDraft ( { allowEmpty = false } = { } ) {
238+ if ( ! this . isWriteUp ) {
239+ if ( allowEmpty ) this . descriptionSaveStatus = '' ;
240+ return ;
241+ }
242+ let saved = null ;
243+ try { saved = localStorage . getItem ( this . descriptionDraftKey ( ) ) ; } catch ( _ ) { }
244+ if ( saved !== null ) this . description = saved ;
245+ else if ( allowEmpty ) this . description = '' ;
246+ this . descriptionSaveStatus = this . description ? 'saved' : '' ;
247+ } ,
248+
249+ clearDescriptionDraft ( ) {
250+ try { localStorage . removeItem ( this . descriptionDraftKey ( ) ) ; } catch ( _ ) { }
251+ } ,
252+
253+ // ── Content draft: localStorage, keyed per post type (mirrors Description) ──
254+ contentDraftKey ( ) {
255+ return 'boost:createPost:contentDraft:' + ( this . postType || '' ) ;
256+ } ,
257+
258+ onContentEdited ( e ) {
259+ const value = e ? e . target . value : this . content ;
260+ clearTimeout ( this . _contentSaveTimer ) ;
261+ if ( ! value ) {
262+ this . contentSaveStatus = '' ;
263+ this . clearContentDraft ( ) ;
264+ return ;
265+ }
266+ this . contentSaveStatus = 'saving' ;
267+ this . _contentSaveTimer = setTimeout ( ( ) => {
268+ this . saveContentDraft ( ) ;
269+ this . contentSaveStatus = 'saved' ;
270+ } , 800 ) ;
271+ } ,
272+
273+ saveContentDraft ( ) {
274+ if ( ! this . isWriteUp ) return ;
275+ try {
276+ if ( this . content ) localStorage . setItem ( this . contentDraftKey ( ) , this . content ) ;
277+ else localStorage . removeItem ( this . contentDraftKey ( ) ) ;
278+ } catch ( _ ) { }
279+ } ,
280+
281+ restoreContentDraft ( { allowEmpty = false } = { } ) {
282+ if ( ! this . isWriteUp ) {
283+ if ( allowEmpty ) this . contentSaveStatus = '' ;
284+ return ;
285+ }
286+ let saved = null ;
287+ try { saved = localStorage . getItem ( this . contentDraftKey ( ) ) ; } catch ( _ ) { }
288+ if ( saved !== null ) this . content = saved ;
289+ else if ( allowEmpty ) this . content = '' ;
290+ this . contentSaveStatus = this . content ? 'saved' : '' ;
291+ } ,
292+
293+ clearContentDraft ( ) {
294+ try { localStorage . removeItem ( this . contentDraftKey ( ) ) ; } catch ( _ ) { }
188295 } ,
189296
190- // Send the Content to the backend, which runs the summarization model
191- // and returns a description (capped server-side at ~970 chars).
297+ // Send the Content to the backend, which runs the summarization model.
192298 async generateDescription ( ) {
193299 const content = ( this . content || '' ) . trim ( ) ;
194300 if ( ! content || this . generating ) return ;
@@ -206,6 +312,7 @@ <h1 class="create-post-page__title">Create Post</h1>
206312 const data = await res . json ( ) ;
207313 if ( res . ok && data . description ) {
208314 this . description = data . description ;
315+ this . onDescriptionEdited ( ) ; // persist generated text + show Saving/Saved
209316 } else {
210317 console . error ( 'Description generation failed:' , data . error || res . status ) ;
211318 }
0 commit comments