|
11 | 11 | <script type="module" src="{% static 'js/v3/wysiwyg-editor.js' %}"></script> |
12 | 12 | {% endblock %} |
13 | 13 |
|
| 14 | +{% comment %} Section save state is shown on each submit button (see saved_sections) |
| 15 | +instead of the legacy toast; still iterate `messages` so any queued message is |
| 16 | +flushed rather than leaking onto the next page. {% endcomment %} |
| 17 | +{% block messages %} |
| 18 | + <div id="messages" class="w-full text-center transition-opacity" x-data="{show: true}"> |
| 19 | + {% for message in messages %}{% endfor %} |
| 20 | + </div> |
| 21 | +{% endblock messages %} |
| 22 | + |
14 | 23 | {% block content %} |
15 | 24 | <div class="user-profile__container"> |
16 | 25 | <div class="user-profile__profile-content-area"> |
|
19 | 28 | {% comment %} novalidate: this form still contains required fields (tagline, bio, links) with |
20 | 29 | no save handler yet; native constraint validation would silently block "Save changes" for |
21 | 30 | sections that are otherwise ready. Server-side validation is authoritative. {% endcomment %} |
22 | | - <form method="post" action="{% url 'profile-account' %}?edit=true" class="card__form" enctype="multipart/form-data" novalidate> |
| 31 | + <form method="post" action="{{ profile_account_edit_url }}" class="card__form" enctype="multipart/form-data" novalidate |
| 32 | + x-data="createSectionForm({{ saved_sections.v3_update_profile|yesno:'true,false' }})" |
| 33 | + @submit="handleSubmit($event)" |
| 34 | + @input="saved = false" @change="saved = false" |
| 35 | + @reset="saved = {{ saved_sections.v3_update_profile|yesno:'true,false' }}"> |
23 | 36 | {% csrf_token %} |
24 | 37 | <div class="card__header"> |
25 | 38 | <span class="card__title">Profile</span> |
|
144 | 157 | </div> |
145 | 158 | <hr class="card__hr" aria-hidden="true" /> |
146 | 159 | <div class="card__cta_section"> |
147 | | - {% include 'v3/includes/_button.html' with style='primary' type='submit' name='v3_update_profile' label='Save changes' only %} |
148 | | - {% include 'v3/includes/_button.html' with style='secondary' url=profile_account_url label='Cancel' only %} |
| 160 | + {% include 'v3/includes/_button.html' with style='primary' type='submit' name='v3_update_profile' label='Save changes' label_x_text="saved ? 'Changes Saved' : 'Save changes'" alpine_disabled='saved' only %} |
| 161 | + {% include 'v3/includes/_button.html' with style='secondary' type='reset' label='Cancel' only %} |
149 | 162 | </div> |
150 | 163 | </form> |
151 | 164 | </div> |
|
168 | 181 | </div> |
169 | 182 | <div class="user-profile__col"> |
170 | 183 | <div class="basic-card card user-profile__update-details"> |
171 | | - <form method="post" action="{% url 'profile-account' %}?edit=true" class="card__form"> |
| 184 | + <form method="post" action="{{ profile_account_edit_url }}" class="card__form" |
| 185 | + x-data="createSectionForm({{ saved_sections.v3_update_details|yesno:'true,false' }})" |
| 186 | + @submit="handleSubmit($event)" |
| 187 | + @input="saved = false" @change="saved = false"> |
172 | 188 | {% csrf_token %} |
173 | 189 | <div class="card__header"> |
174 | 190 | <span class="card__title">Update Details</span> |
|
184 | 200 | </div> |
185 | 201 | <hr class="card__hr" aria-hidden="true" /> |
186 | 202 | <div class="card__cta_section"> |
187 | | - {% include 'v3/includes/_button.html' with style='primary' type='submit' name='v3_update_details' label='Save details' only %} |
| 203 | + {% include 'v3/includes/_button.html' with style='primary' type='submit' name='v3_update_details' label='Save details' label_x_text="saved ? 'Changes Saved' : 'Save details'" alpine_disabled='saved' only %} |
188 | 204 | </div> |
189 | 205 | </form> |
190 | 206 | </div> |
|
209 | 225 | </div> |
210 | 226 | </div> |
211 | 227 | <div class="basic-card card user-profile__email-preferences-card"> |
212 | | - <form method="post" action="{% url 'profile-account' %}?edit=true" class="card__form"> |
| 228 | + <form method="post" action="{{ profile_account_edit_url }}" class="card__form" |
| 229 | + x-data="createSectionForm({{ saved_sections.v3_update_email_preferences|yesno:'true,false' }})" |
| 230 | + @submit="handleSubmit($event)" |
| 231 | + @input="saved = false" @change="saved = false"> |
213 | 232 | {% csrf_token %} |
214 | 233 | <div class="card__header"> |
215 | 234 | <span class="card__title">Email Preferences</span> |
|
229 | 248 | </div> |
230 | 249 | <hr class="card__hr" aria-hidden="true" /> |
231 | 250 | <div class="card__cta_section"> |
232 | | - {% include 'v3/includes/_button.html' with style='primary' type='submit' name='v3_update_email_preferences' label='Update' only %} |
| 251 | + {% include 'v3/includes/_button.html' with style='primary' type='submit' name='v3_update_email_preferences' label='Update' label_x_text="saved ? 'Changes Saved' : 'Update'" alpine_disabled='saved' only %} |
233 | 252 | </div> |
234 | 253 | </form> |
235 | 254 | </div> |
|
283 | 302 |
|
284 | 303 |
|
285 | 304 | document.addEventListener('alpine:init', () => { |
| 305 | + // Submits a section's form via fetch so a successful save can flip its |
| 306 | + // button to "Changes Saved" without a full-page navigation. On any |
| 307 | + // non-success outcome (validation error, network failure, JS/fetch |
| 308 | + // unsupported) it falls back to a real form submission, which the |
| 309 | + // server renders/redirects exactly as it did before this existed. |
| 310 | + window.createSectionForm = function (initialSaved) { |
| 311 | + return { |
| 312 | + saved: initialSaved, |
| 313 | + _bypassFetch: false, |
| 314 | + async handleSubmit(event) { |
| 315 | + if (this._bypassFetch) { |
| 316 | + this._bypassFetch = false |
| 317 | + return |
| 318 | + } |
| 319 | + event.preventDefault() |
| 320 | + const form = event.target |
| 321 | + const submitter = event.submitter |
| 322 | + let succeeded = false |
| 323 | + try { |
| 324 | + const response = await fetch(form.action, { |
| 325 | + method: 'POST', |
| 326 | + body: new FormData(form, submitter), |
| 327 | + headers: { 'X-Requested-With': 'XMLHttpRequest' }, |
| 328 | + }) |
| 329 | + const contentType = response.headers.get('content-type') || '' |
| 330 | + if (response.ok && contentType.includes('application/json')) { |
| 331 | + this.saved = true |
| 332 | + succeeded = true |
| 333 | + } |
| 334 | + } catch (_) {} |
| 335 | + if (!succeeded) { |
| 336 | + this._bypassFetch = true |
| 337 | + form.requestSubmit(submitter) |
| 338 | + } |
| 339 | + }, |
| 340 | + } |
| 341 | + } |
| 342 | + |
286 | 343 | window.createTagline = function () { |
287 | 344 | const taglineInput = document.querySelector("input[name='{{user_profile_form.tagline.name}}']") |
288 | 345 |
|
|
0 commit comments