Skip to content

Commit de07ffa

Browse files
feat: add saving/saved status for written description in Link post type
1 parent 75632a1 commit de07ffa

2 files changed

Lines changed: 73 additions & 6 deletions

File tree

templates/news/v3/create.html

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -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
};

templates/v3/includes/_field_textarea.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
error (optional) — error message (activates error state)
1212
alpine_error (optional) — Alpine expression for dynamic error binding (e.g. "errors.content")
1313
alpine_disabled_expr (optional) — Alpine expression to conditionally disable the textarea
14+
alpine_x_model (optional) — Alpine state property to two-way bind via x-model (e.g. "linkDescription")
15+
alpine_at_input (optional) — Alpine expression for the @input handler (e.g. "onLinkDescriptionEdited($event)")
1416
required (optional) — if truthy, adds required attribute
1517
disabled (optional) — if truthy, adds disabled attribute
1618
extra_class (optional) — additional classes on the wrapper
@@ -33,6 +35,8 @@
3335
{% if required %}required{% endif %}
3436
{% if disabled %}disabled{% endif %}
3537
{% if alpine_disabled_expr %}:disabled="{{ alpine_disabled_expr }}"{% endif %}
38+
{% if alpine_x_model %}x-model="{{ alpine_x_model }}"{% endif %}
39+
{% if alpine_at_input %}@input="{{ alpine_at_input }}"{% endif %}
3640
{% if alpine_error %}:aria-invalid="!!{{ alpine_error }}" :aria-describedby="{{ alpine_error }} ? 'field-{{ fid }}-error' : {% if help_text %}'field-{{ fid }}-help'{% else %}null{% endif %}"{% elif error %}aria-invalid="true" aria-describedby="field-{{ fid }}-error"{% elif help_text %}aria-describedby="field-{{ fid }}-help"{% endif %}
3741
>{% if value %}{{ value }}{% endif %}</textarea>
3842
</div>

0 commit comments

Comments
 (0)