fix(loyalty): use flatpickr for birthday so Firefox honors dd/mm/yyyy on FR
Some checks failed
CI / ruff (push) Successful in 17s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has been cancelled

Native <input type="date"> defers display format to the browser's
locale, which most engines pick up from the <html lang> attribute.
Firefox is the exception — it ignores lang and uses the OS locale
instead (Mozilla bug #1344625, still open). So FR users on Firefox
still saw mm/dd/yyyy even after the lang fix from earlier this week.

Swap to flatpickr for both the customer storefront enrollment page
and the staff terminal enrollment page. Configure:
  - dateFormat: 'Y-m-d'   (what gets sent to the API — ISO, what
                           Pydantic's date field expects)
  - altInput: true        (flatpickr creates a separate visible input)
  - altFormat: 'd/m/Y'    (what the user sees — universal in Europe)
  - locale: current_language (FR/DE/LB month + day names)
  - maxDate: 'today'      (no future birthdays)

Load flatpickr core + the optional locale JS via the existing
{% block extra_head %} / {% block extra_scripts %} hooks. The
loyalty/store/enroll.html template didn't have those blocks before,
added them in the same commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-18 22:55:35 +02:00
parent 54247ca4f0
commit ab3e133af7
2 changed files with 51 additions and 2 deletions

View File

@@ -9,6 +9,11 @@
{% block alpine_data %}storeLoyaltyEnroll(){% endblock %} {% block alpine_data %}storeLoyaltyEnroll(){% endblock %}
{% block extra_head %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/themes/dark.css" media="(prefers-color-scheme: dark)">
{% endblock %}
{% block content %} {% block content %}
{% call detail_page_header("'" + _('loyalty.store.enroll.page_title') + "'", '/store/' + store_code + '/loyalty/terminal') %} {% call detail_page_header("'" + _('loyalty.store.enroll.page_title') + "'", '/store/' + store_code + '/loyalty/terminal') %}
{{ _('loyalty.store.enroll.subtitle') }} {{ _('loyalty.store.enroll.subtitle') }}
@@ -58,7 +63,21 @@
<div> <div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{{ _('loyalty.store.enroll.birthday') }}</label> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{{ _('loyalty.store.enroll.birthday') }}</label>
<input type="date" x-model="form.birthday" {# flatpickr instead of <input type="date"> — Firefox ignores
the html lang attribute for native date inputs. dd/mm/yyyy
universally accepted in Europe. #}
<input type="text" x-model="form.birthday"
placeholder="dd/mm/yyyy"
x-init="flatpickr($el, {
dateFormat: 'Y-m-d',
altInput: true,
altFormat: 'd/m/Y',
allowInput: true,
maxDate: 'today',
locale: '{{ current_language|default('en') }}',
disableMobile: false,
onChange: function(selectedDates, dateStr) { form.birthday = dateStr; }
})"
class="w-full px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300"> class="w-full px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300">
<p class="mt-1 text-xs text-gray-500">{{ _('loyalty.store.enroll.birthday_help') }}</p> <p class="mt-1 text-xs text-gray-500">{{ _('loyalty.store.enroll.birthday_help') }}</p>
</div> </div>
@@ -147,5 +166,10 @@
{% endblock %} {% endblock %}
{% block extra_scripts %} {% block extra_scripts %}
{# flatpickr loaded synchronously so x-init can call flatpickr() during Alpine setup #}
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
{% if current_language and current_language != 'en' %}
<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/{{ current_language }}.js"></script>
{% endif %}
<script defer src="{{ static_v(request, 'loyalty_static', path='store/js/loyalty-enroll.js') }}"></script> <script defer src="{{ static_v(request, 'loyalty_static', path='store/js/loyalty-enroll.js') }}"></script>
{% endblock %} {% endblock %}

View File

@@ -7,6 +7,11 @@
{% block alpine_data %}customerLoyaltyEnroll(){% endblock %} {% block alpine_data %}customerLoyaltyEnroll(){% endblock %}
{% block extra_head %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/themes/dark.css" media="(prefers-color-scheme: dark)">
{% endblock %}
{% block content %} {% block content %}
<div class="min-h-screen flex items-center justify-center px-4 py-12 bg-gray-50 dark:bg-gray-900"> <div class="min-h-screen flex items-center justify-center px-4 py-12 bg-gray-50 dark:bg-gray-900">
<div class="max-w-md w-full"> <div class="max-w-md w-full">
@@ -84,7 +89,22 @@
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{{ _('loyalty.enrollment.form.birthday') }} {{ _('loyalty.enrollment.form.birthday') }}
</label> </label>
<input type="date" x-model="form.birthday" {# Use flatpickr instead of <input type="date"> — Firefox ignores
the html lang attribute for native date inputs, so users on FR
still saw mm/dd/yyyy. flatpickr enforces dd/mm/yyyy across all
browsers and locales (universal in Europe). #}
<input type="text" x-model="form.birthday"
placeholder="dd/mm/yyyy"
x-init="flatpickr($el, {
dateFormat: 'Y-m-d',
altInput: true,
altFormat: 'd/m/Y',
allowInput: true,
maxDate: 'today',
locale: '{{ current_language|default('en') }}',
disableMobile: false,
onChange: function(selectedDates, dateStr) { form.birthday = dateStr; }
})"
class="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent dark:bg-gray-700 dark:text-white"> class="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent dark:bg-gray-700 dark:text-white">
<p class="mt-1 text-xs text-gray-500">{{ _('loyalty.enrollment.form.birthday_hint') }}</p> <p class="mt-1 text-xs text-gray-500">{{ _('loyalty.enrollment.form.birthday_hint') }}</p>
</div> </div>
@@ -170,5 +190,10 @@
{% endblock %} {% endblock %}
{% block extra_scripts %} {% block extra_scripts %}
{# flatpickr loaded synchronously so x-init can call flatpickr() during Alpine setup #}
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
{% if current_language and current_language != 'en' %}
<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/{{ current_language }}.js"></script>
{% endif %}
<script defer src="{{ static_v(request, 'loyalty_static', path='storefront/js/loyalty-enroll.js') }}"></script> <script defer src="{{ static_v(request, 'loyalty_static', path='storefront/js/loyalty-enroll.js') }}"></script>
{% endblock %} {% endblock %}