diff --git a/src/backend/core/templates/emails/_invitation_layout.html b/src/backend/core/templates/emails/_invitation_layout.html new file mode 100644 index 0000000..d25fdf4 --- /dev/null +++ b/src/backend/core/templates/emails/_invitation_layout.html @@ -0,0 +1,66 @@ +{% comment %} +Shared layout for calendar invitation / update / cancel / reply emails. +Email-client-safe: table-based, inline styles, no external CSS. +Blocks: + header_color — background color of the header strip (default blue). + badge_color — text color of the uppercase header badge (default light blue tint). + body — the per-template body content (after the header, before the footer). +{% endcomment %} + + + + + + {{ content.title }} + + + + +
+ + + {# ── header ── #} + + + {# ── body (per-template) ── #} + + + {# ── divider ── #} + + + {# ── footer ── #} + + +
+ {% if content.badge %} +

+ {{ content.badge }} +

+ {% endif %} +

+ {{ summary }} +

+
+ {% block body %}{% endblock %} +
+ {% if instructions %} +

+ {{ instructions }} +

+ {% endif %} +

+ {{ footer }} +

+
+
+ + diff --git a/src/backend/core/templates/emails/calendar_invitation.html b/src/backend/core/templates/emails/calendar_invitation.html index d684b9d..6203982 100644 --- a/src/backend/core/templates/emails/calendar_invitation.html +++ b/src/backend/core/templates/emails/calendar_invitation.html @@ -1,158 +1,79 @@ - - - - - - {{ content.title }} - - - -
-
-

{{ content.heading }}

-
+{% extends "emails/_invitation_layout.html" %} -

{{ content.body }}

+{# Header defaults (blue) inherited from the base layout. #} -
{{ summary }}
+{% block body %} + {# date/time #} +

+ {{ start_date }} +

+

+ {{ time_str }}{% if start_date != end_date %} · {{ labels.until }} {{ end_date }}{% endif %} +

-
- - - - - - {% if event.location %} - - - - - {% endif %} - {% if event.url %} - - - - - {% endif %} - - - - -
{{ labels.when }} - {{ start_date }}
- {{ time_str }} - {% if start_date != end_date %}
{{ labels.until }} {{ end_date }}{% endif %} -
{{ labels.location }}{{ event.location }}
{{ labels.videoConference }}{{ event.url }}
{{ labels.organizer }}{{ organizer_display }}
-
+ {% if event.location %} +

+ {{ labels.location }}: {{ event.location }} +

+ {% endif %} - {% if event.description %} -
-

{{ labels.description }}

-

{{ event.description|linebreaks }}

-
- {% endif %} + {% if event.url %} +

+ {{ labels.videoConference }}: + {{ event.url }} +

+ {% endif %} - {% if rsvp_accepted_url %} -
- {{ actions.accept }} - {{ actions.maybe }} - {{ actions.decline }} -
- {% endif %} -
-

{{ instructions }}

-
+ {# organizer / body sentence (content.body is i18n-rendered with {{organizer}}) #} +

+ {{ content.body }} +

- -
- - + {% if event.description %} +

+ {{ event.description|linebreaksbr }} +

+ {% endif %} + + {# RSVP buttons — only present for REQUEST-method emails #} + {% if rsvp_accepted_url %} + + + + + + +
+ + {{ actions.accept }} + + + + {{ actions.maybe }} + + + + {{ actions.decline }} + +
+ {% endif %} +{% endblock %} diff --git a/src/backend/core/templates/emails/calendar_invitation_cancel.html b/src/backend/core/templates/emails/calendar_invitation_cancel.html index b1aaacd..20d3d4d 100644 --- a/src/backend/core/templates/emails/calendar_invitation_cancel.html +++ b/src/backend/core/templates/emails/calendar_invitation_cancel.html @@ -1,139 +1,44 @@ - - - - - - {{ content.title }} - - - -
-
-

{{ content.heading }}

-
+{% extends "emails/_invitation_layout.html" %} - {{ content.badge }} +{% block header_color %}#d2212f{% endblock %} +{% block badge_color %}#f5c2c6{% endblock %} -

{{ content.body }}

+{% block body %} + {# small label above the strikethrough date (cancelled event was scheduled for…) #} +

+ {{ labels.wasScheduledFor }} +

+

+ {{ start_date }} +

+

+ {{ time_str }}{% if start_date != end_date %} · {{ labels.until }} {{ end_date }}{% endif %} +

-
{{ summary }}
+ {% if event.location %} +

+ {{ labels.location }}: {{ event.location }} +

+ {% endif %} -
- - - - - - {% if event.location %} - - - - - {% endif %} - {% if event.url %} - - - - - {% endif %} - - - - -
{{ labels.wasScheduledFor }} - {{ start_date }}
- {{ time_str }} - {% if start_date != end_date %}
{{ labels.until }} {{ end_date }}{% endif %} -
{{ labels.location }}{{ event.location }}
{{ labels.videoConference }}{{ event.url }}
{{ labels.organizer }}{{ organizer_display }}
-
+ {% if event.url %} +

+ {{ labels.videoConference }}: + {{ event.url }} +

+ {% endif %} -
-

{{ instructions }}

-
- - -
- - +

+ {{ content.body }} +

+{% endblock %} diff --git a/src/backend/core/templates/emails/calendar_invitation_reply.html b/src/backend/core/templates/emails/calendar_invitation_reply.html index d6858a3..c3c8229 100644 --- a/src/backend/core/templates/emails/calendar_invitation_reply.html +++ b/src/backend/core/templates/emails/calendar_invitation_reply.html @@ -1,126 +1,37 @@ - - - - - - {{ content.title }} - - - -
-
-

{{ content.heading }}

-
+{% extends "emails/_invitation_layout.html" %} -

{{ content.body }}

+{% block header_color %}#16a34a{% endblock %} +{% block badge_color %}#bbe5c5{% endblock %} -
{{ summary }}
+{% block body %} + {# date/time #} +

+ {{ start_date }} +

+

+ {{ time_str }}{% if start_date != end_date %} · {{ labels.until }} {{ end_date }}{% endif %} +

-
- - - - - - {% if event.location %} - - - - - {% endif %} - {% if event.url %} - - - - - {% endif %} - - - - -
{{ labels.when }} - {{ start_date }}
- {{ time_str }} - {% if start_date != end_date %}
{{ labels.until }} {{ end_date }}{% endif %} -
{{ labels.location }}{{ event.location }}
{{ labels.videoConference }}{{ event.url }}
{{ labels.attendee }}{{ attendee_display }}
-
+ {% if event.location %} +

+ {{ labels.location }}: {{ event.location }} +

+ {% endif %} -
-

{{ instructions }}

-
+ {% if event.url %} +

+ {{ labels.videoConference }}: + {{ event.url }} +

+ {% endif %} - -
- - + {# attendee response sentence (content.body interpolates {{attendee}}) #} +

+ {{ content.body }} +

+{% endblock %} diff --git a/src/backend/core/templates/emails/calendar_invitation_update.html b/src/backend/core/templates/emails/calendar_invitation_update.html index dec29d9..5a87e8f 100644 --- a/src/backend/core/templates/emails/calendar_invitation_update.html +++ b/src/backend/core/templates/emails/calendar_invitation_update.html @@ -1,163 +1,77 @@ - - - - - - {{ content.title }} - - - -
-
-

{{ content.heading }}

-
+{% extends "emails/_invitation_layout.html" %} - {{ content.badge }} +{# Header defaults (blue) inherited; content.badge resolves to "UPDATED" via i18n. #} -

{{ content.body }}

+{% block body %} + {# date/time #} +

+ {{ start_date }} +

+

+ {{ time_str }}{% if start_date != end_date %} · {{ labels.until }} {{ end_date }}{% endif %} +

-
{{ summary }}
+ {% if event.location %} +

+ {{ labels.location }}: {{ event.location }} +

+ {% endif %} -
- - - - - - {% if event.location %} - - - - - {% endif %} - {% if event.url %} - - - - - {% endif %} - - - - -
{{ labels.when }} - {{ start_date }}
- {{ time_str }} - {% if start_date != end_date %}
{{ labels.until }} {{ end_date }}{% endif %} -
{{ labels.location }}{{ event.location }}
{{ labels.videoConference }}{{ event.url }}
{{ labels.organizer }}{{ organizer_display }}
-
+ {% if event.url %} +

+ {{ labels.videoConference }}: + {{ event.url }} +

+ {% endif %} - {% if event.description %} -
-

{{ labels.description }}

-

{{ event.description|linebreaks }}

-
- {% endif %} +

+ {{ content.body }} +

- {% if rsvp_accepted_url %} -
- {{ actions.accept }} - {{ actions.maybe }} - {{ actions.decline }} -
- {% endif %} -
-

{{ instructions }}

-
+ {% if event.description %} +

+ {{ event.description|linebreaksbr }} +

+ {% endif %} - -
- - + {% if rsvp_accepted_url %} + + + + + + +
+ + {{ actions.accept }} + + + + {{ actions.maybe }} + + + + {{ actions.decline }} + +
+ {% endif %} +{% endblock %} diff --git a/src/backend/core/templates/rsvp/_common_styles.html b/src/backend/core/templates/rsvp/_common_styles.html new file mode 100644 index 0000000..c1a1aa6 --- /dev/null +++ b/src/backend/core/templates/rsvp/_common_styles.html @@ -0,0 +1,25 @@ +{% comment %} + Shared CSS for rsvp/confirm.html and rsvp/response.html. Page-specific + styles (e.g. .submit-btn, .event-date) live in each template. +{% endcomment %} + diff --git a/src/backend/core/templates/rsvp/confirm.html b/src/backend/core/templates/rsvp/confirm.html index b5b00cd..8a3652c 100644 --- a/src/backend/core/templates/rsvp/confirm.html +++ b/src/backend/core/templates/rsvp/confirm.html @@ -1,70 +1,41 @@ - - + + - - - {{ page_title }} - + + + {{ page_title }} + {% include "rsvp/_common_styles.html" %} + -
-
{{ status_icon|safe }}
-

{{ heading }}

+
+
+
+

{{ heading }}

+

{{ submit_label }}

+
+
- - -

...

+ + +

+
+
+
- +
+ diff --git a/src/backend/core/templates/rsvp/response.html b/src/backend/core/templates/rsvp/response.html index 4affa87..b0af50b 100644 --- a/src/backend/core/templates/rsvp/response.html +++ b/src/backend/core/templates/rsvp/response.html @@ -1,76 +1,43 @@ - - + + - - - {{ page_title }} - + + + {{ page_title }} + {% include "rsvp/_common_styles.html" %} + -
- {% if error %} -
-

{{ error_title }}

-

{{ error }}

- {% else %} -
{{ status_icon|safe }}
-

{{ heading }}

- {% if event_summary %} -
{{ event_summary }}
- {% endif %} - {% if event_date %} -
{{ event_date }}
- {% endif %} -

{{ message }}

- {% endif %} +
+
+ {% if error %} +
+

{{ error_title }}

+

{{ error_title }}

+
+
+

{{ error }}

+
+ {% else %} +
+

{{ heading }}

+

{{ event_summary }}

+
+
+ {% if event_date %}
{{ event_date }}
{% endif %} + {% if message %}

{{ message }}

{% endif %} +
+ {% endif %} +
+
+
diff --git a/src/backend/core/tests/test_rsvp.py b/src/backend/core/tests/test_rsvp.py index c4c7dd4..8b54be6 100644 --- a/src/backend/core/tests/test_rsvp.py +++ b/src/backend/core/tests/test_rsvp.py @@ -584,8 +584,8 @@ def test_email_to_rsvp_accept_flow(self): alt[0] for alt in mail.outbox[0].alternatives if alt[1] == "text/html" ) - # Find accept link by green button color (#16a34a) - accept_url = self._extract_rsvp_link(html_body, "#16a34a") + # Find accept link by blue button color (#435de6) + accept_url = self._extract_rsvp_link(html_body, "#435de6") token = self._extract_token_from_url(accept_url) # GET the confirm page @@ -615,8 +615,8 @@ def test_email_to_rsvp_decline_flow(self): alt[0] for alt in mail.outbox[0].alternatives if alt[1] == "text/html" ) - # Find decline link by red button color (#dc2626) - decline_url = self._extract_rsvp_link(html_body, "#dc2626") + # Find decline link by red button color (#d2212f) + decline_url = self._extract_rsvp_link(html_body, "#d2212f") token = self._extract_token_from_url(decline_url) with patch.object(CalDAVHTTPClient, "internal_request") as mock_internal: @@ -640,9 +640,9 @@ def test_email_contains_all_three_rsvp_links(self): # Each button has a distinct color colors = { - "accept": "#16a34a", # green - "tentative": "#d97706", # amber - "decline": "#dc2626", # red + "accept": "#435de6", # blue + "tentative": "#626a80", # gray + "decline": "#d2212f", # red } for label, color in colors.items(): pattern = rf']*background-color:\s*{re.escape(color)}' @@ -670,7 +670,7 @@ def test_rsvp_link_for_past_event_fails(self, mock_internal): html_body = next( alt[0] for alt in mail.outbox[0].alternatives if alt[1] == "text/html" ) - accept_url = self._extract_rsvp_link(html_body, "#16a34a") + accept_url = self._extract_rsvp_link(html_body, "#435de6") token = self._extract_token_from_url(accept_url) mock_internal.return_value = _mock_resp(404, {"error": "Event not found"}) diff --git a/src/frontend/apps/calendars/src/features/i18n/translations.json b/src/frontend/apps/calendars/src/features/i18n/translations.json index bdd783b..a7215f8 100644 --- a/src/frontend/apps/calendars/src/features/i18n/translations.json +++ b/src/frontend/apps/calendars/src/features/i18n/translations.json @@ -416,6 +416,7 @@ "invitation": { "title": "Event invitation", "heading": "Event invitation", + "badge": "Event invitation", "body": "{{organizer}} invites you to an event" }, "update": { @@ -433,6 +434,7 @@ "reply": { "title": "Event reply", "heading": "Reply received", + "badge": "Response", "body": "{{attendee}} has replied to your event" }, "labels": { @@ -1336,6 +1338,7 @@ "invitation": { "title": "Invitation à un événement", "heading": "Invitation à un événement", + "badge": "Invitation à un événement", "body": "{{organizer}} vous invite à un événement" }, "update": { @@ -1353,6 +1356,7 @@ "reply": { "title": "Réponse à l'événement", "heading": "Réponse reçue", + "badge": "Réponse", "body": "{{attendee}} a répondu à votre événement" }, "labels": { @@ -1998,6 +2002,7 @@ "invitation": { "title": "Uitnodiging voor evenement", "heading": "Uitnodiging voor evenement", + "badge": "Uitnodiging voor evenement", "body": "{{organizer}} nodigt u uit voor een evenement" }, "update": { @@ -2015,6 +2020,7 @@ "reply": { "title": "Antwoord op evenement", "heading": "Antwoord ontvangen", + "badge": "Antwoord", "body": "{{attendee}} heeft gereageerd op uw evenement" }, "labels": {