Skip to content

Commit fe8dffc

Browse files
authored
Add custom notifications to UI (#222)
Signed-off-by: Cintia Sánchez García <[email protected]>
1 parent 9dbeaf5 commit fe8dffc

File tree

12 files changed

+397
-26
lines changed

12 files changed

+397
-26
lines changed

ocg-server/static/css/styles.src.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,10 @@ button:enabled {
180180
cursor: pointer;
181181
}
182182

183+
button:disabled {
184+
opacity: 0.5;
185+
}
186+
183187
/* Generic styles */
184188
button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible {
185189
outline: 0;
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { createNotificationModal } from "/static/js/dashboard/group/notificationModal.js";
2+
3+
const modalId = "attendee-notification-modal";
4+
const formId = "attendee-notification-form";
5+
const dataKey = "attendeeNotificationReady";
6+
7+
// Set up the attendee modal with its dynamic endpoint and success copy.
8+
const initializeAttendeeNotification = () => {
9+
createNotificationModal({
10+
modalId,
11+
formId,
12+
dataKey,
13+
openButtonId: "open-attendee-notification-modal",
14+
closeButtonId: "close-attendee-notification-modal",
15+
cancelButtonId: "cancel-attendee-notification",
16+
overlayId: "overlay-attendee-notification-modal",
17+
successMessage: "Email sent successfully to all event attendees!",
18+
// Apply the event-specific endpoint before the modal opens.
19+
updateEndpoint: ({ form, openButton }) => {
20+
if (!form) {
21+
return;
22+
}
23+
24+
const eventId = openButton?.getAttribute("data-event-id") || "";
25+
if (eventId) {
26+
form.setAttribute("hx-post", `/dashboard/group/notifications/${eventId}`);
27+
} else {
28+
form.removeAttribute("hx-post");
29+
}
30+
},
31+
});
32+
};
33+
34+
initializeAttendeeNotification();
35+
36+
if (document.body) {
37+
document.body.addEventListener("htmx:load", initializeAttendeeNotification);
38+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { createNotificationModal } from "/static/js/dashboard/group/notificationModal.js";
2+
3+
const modalId = "notification-modal";
4+
const formId = "notification-form";
5+
const dataKey = "membersNotificationReady";
6+
7+
// Reuse the shared helper for the members notification modal.
8+
const initializeMembersNotification = () => {
9+
createNotificationModal({
10+
modalId,
11+
formId,
12+
dataKey,
13+
openButtonId: "open-notification-modal",
14+
closeButtonId: "close-notification-modal",
15+
cancelButtonId: "cancel-notification",
16+
overlayId: "overlay-notification-modal",
17+
successMessage: "Email sent successfully to all group members.",
18+
});
19+
};
20+
21+
initializeMembersNotification();
22+
23+
if (document.body) {
24+
document.body.addEventListener("htmx:load", initializeMembersNotification);
25+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { toggleModalVisibility } from "/static/js/common/common.js";
2+
import { showSuccessAlert, showErrorAlert } from "/static/js/common/alerts.js";
3+
4+
const DEFAULT_ERROR_MESSAGE = "Failed to send email. Please try again.";
5+
6+
// Central helper for attaching modal controls and HTMX success handling.
7+
export const createNotificationModal = ({
8+
modalId,
9+
formId,
10+
dataKey,
11+
openButtonId,
12+
closeButtonId,
13+
cancelButtonId,
14+
overlayId,
15+
successMessage,
16+
updateEndpoint,
17+
}) => {
18+
// Locate the modal once and mark it ready so we only bind listeners once.
19+
const modal = document.getElementById(modalId);
20+
if (!modal || modal.dataset[dataKey] === "true") {
21+
return;
22+
}
23+
24+
modal.dataset[dataKey] = "true";
25+
26+
const openButton = openButtonId ? document.getElementById(openButtonId) : null;
27+
const closeButton = closeButtonId ? document.getElementById(closeButtonId) : null;
28+
const cancelButton = cancelButtonId ? document.getElementById(cancelButtonId) : null;
29+
const overlay = overlayId ? document.getElementById(overlayId) : null;
30+
const form = formId ? document.getElementById(formId) : null;
31+
const toggleModal = () => toggleModalVisibility(modalId);
32+
33+
// Allow callers to adjust the form action before the modal opens.
34+
const updateFormEndpoint = () => {
35+
if (!form || typeof updateEndpoint !== "function") {
36+
return;
37+
}
38+
39+
updateEndpoint({
40+
form,
41+
openButton,
42+
closeButton,
43+
cancelButton,
44+
overlay,
45+
});
46+
};
47+
48+
if (openButton) {
49+
openButton.addEventListener("click", () => {
50+
updateFormEndpoint();
51+
toggleModal();
52+
});
53+
}
54+
55+
if (closeButton) {
56+
closeButton.addEventListener("click", toggleModal);
57+
}
58+
59+
if (cancelButton) {
60+
cancelButton.addEventListener("click", toggleModal);
61+
}
62+
63+
if (overlay) {
64+
overlay.addEventListener("click", toggleModal);
65+
}
66+
67+
if (form) {
68+
form.addEventListener("htmx:afterRequest", (event) => {
69+
const xhr = event.detail?.xhr;
70+
if (!xhr) {
71+
showErrorAlert(DEFAULT_ERROR_MESSAGE, false);
72+
return;
73+
}
74+
75+
if (xhr.status >= 200 && xhr.status < 300) {
76+
showSuccessAlert(successMessage || "Email sent successfully.");
77+
form.reset();
78+
toggleModal();
79+
} else {
80+
const errorMessage = xhr.responseText || DEFAULT_ERROR_MESSAGE;
81+
showErrorAlert(errorMessage, true);
82+
}
83+
});
84+
}
85+
86+
updateFormEndpoint();
87+
};

ocg-server/templates/common.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ <h3 class="text-xl font-semibold text-stone-900">Search tips</h3>
159159
{% else -%}
160160
hidden group-[.is-loading]:flex
161161
{% endif -%}
162-
{{ extra_styles }}">
162+
{{ size }} {{ extra_styles }}">
163163
<style>
164164
:root {
165165
{% match spinner_variant.as_str() %}
@@ -192,7 +192,7 @@ <h3 class="text-xl font-semibold text-stone-900">Search tips</h3>
192192
</style>
193193
<svg aria-hidden="true"
194194
viewBox="0 0 100 101"
195-
class="{{ size }} animate-spin size-auto">
195+
class="animate-spin size-auto">
196196
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="#e5e7eb" class="spinner-base" />
197197
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="#D62293" class="spinner-accent" />
198198
</svg>

ocg-server/templates/dashboard/group/attendees_list.html

Lines changed: 107 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,38 @@
44
{# Attendees header -#}
55
{% call macros::form_title(title = "Attendees") -%}
66

7-
<div class="flex flex-col mt-10 mb-6 w-3/4 max-w-[90vw] relative">
8-
<label for="group-event-selector-button" class="form-label mb-2">Event</label>
9-
<event-selector button-id="group-event-selector-button" group-id="{{ group_id }}"
10-
{% if let Some(selected_event) = event -%}
11-
selected-event-id="{{ selected_event.event_id }}" selected-event="{{ selected_event|json }}"
12-
{% endif -%}
13-
></event-selector>
7+
<div class="mt-10 mb-6 flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
8+
<div class="flex-1 min-w-0">
9+
<label for="group-event-selector-button" class="form-label mb-2">Event</label>
10+
<event-selector button-id="group-event-selector-button" group-id="{{ group_id }}"
11+
{% if let Some(selected_event) = event -%}
12+
selected-event-id="{{ selected_event.event_id }}" selected-event="{{ selected_event|json }}"
13+
{% endif -%}
14+
></event-selector>
15+
</div>
1416
</div>
1517

16-
<div class="flex text-sm text-stone-600 mb-5">
17-
<div>Total attendees:</div>
18-
<div class="font-semibold ms-1 text-end">
19-
{% if attendees.is_empty() -%}
20-
-
21-
{% else %}
22-
{{ attendees.len() }}
23-
{% endif -%}
18+
<div class="flex items-end justify-between mb-5">
19+
<div class="flex text-sm text-stone-600">
20+
<div>Total attendees:</div>
21+
<div class="font-semibold ms-1 text-end">
22+
{% if attendees.is_empty() -%}
23+
-
24+
{% else %}
25+
{{ attendees.len() }}
26+
{% endif -%}
27+
</div>
28+
</div>
29+
<div class="flex-shrink-0 w-full lg:w-auto mb-1.5">
30+
<button id="open-attendee-notification-modal"
31+
type="button"
32+
class="btn-primary w-full lg:w-auto"
33+
{% if event.is_none() || attendees.is_empty() -%}
34+
disabled title="Select an event with attendees to send emails."
35+
{% endif -%}
36+
{% if let Some(selected_event) = event -%}
37+
data-event-id="{{ selected_event.event_id }}"
38+
{% endif -%}>Send email</button>
2439
</div>
2540
</div>
2641

@@ -121,3 +136,80 @@
121136
</table>
122137
</div>
123138
{# End attendees table -#}
139+
140+
{# Notification modal -#}
141+
<div id="attendee-notification-modal"
142+
role="dialog"
143+
aria-modal="true"
144+
aria-labelledby="attendee-notification-modal-title"
145+
class="hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 max-h-full flex z-1000">
146+
<div id="overlay-attendee-notification-modal"
147+
class="modal-overlay absolute w-full h-full bg-stone-950 opacity-[0.35]"></div>
148+
<div class="relative p-4 w-full max-w-2xl max-h-full">
149+
<div class="relative bg-white rounded-lg shadow">
150+
{# Modal header -#}
151+
<div class="flex items-center justify-between p-4 md:p-5 border-b border-stone-200 rounded-t">
152+
<h3 id="attendee-notification-modal-title"
153+
class="text-xl font-semibold text-stone-900">Send email</h3>
154+
<button id="close-attendee-notification-modal"
155+
type="button"
156+
class="group text-stone-400 bg-transparent hover:bg-stone-200 hover:text-stone-900 transition-colors rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center">
157+
<div class="svg-icon w-5 h-5 bg-stone-500 group-hover:bg-stone-900 transition-colors icon-close"></div>
158+
<span class="sr-only">Close modal</span>
159+
</button>
160+
</div>
161+
{# Modal body -#}
162+
<div class="p-4 md:p-8">
163+
<div class="bg-stone-50 border border-stone-200 text-stone-800 rounded-lg p-4 mb-5 text-sm">
164+
<p class="font-medium">
165+
This email will be sent to <span class="font-semibold">all event attendees</span>.
166+
</p>
167+
</div>
168+
<form id="attendee-notification-form"
169+
hx-indicator="#attendee-notification-spinner"
170+
hx-disabled-elt="#submit-attendee-notification">
171+
<div class="mb-4">
172+
<label for="attendee-title" class="form-label">
173+
Title <span class="asterisk">*</span>
174+
</label>
175+
<div class="mt-2">
176+
<input type="text"
177+
id="attendee-title"
178+
name="title"
179+
required
180+
maxlength="200"
181+
class="input-primary"
182+
placeholder="Enter notification title">
183+
</div>
184+
</div>
185+
<div class="mb-6">
186+
<label for="attendee-body" class="form-label">
187+
Body <span class="asterisk">*</span>
188+
</label>
189+
<div class="mt-2">
190+
<textarea id="attendee-body"
191+
name="body"
192+
required
193+
rows="6"
194+
maxlength="5000"
195+
class="input-primary"
196+
placeholder="Enter notification body (plain text only)"></textarea>
197+
<p class="mt-1 text-xs text-stone-500">Plain text only. No HTML formatting.</p>
198+
</div>
199+
</div>
200+
<div class="flex justify-end gap-3 pt-6 border-t border-stone-200">
201+
<button id="cancel-attendee-notification"
202+
type="button"
203+
class="btn-primary-outline">Cancel</button>
204+
<button id="submit-attendee-notification"
205+
type="submit"
206+
class="btn-primary relative">
207+
{% call common::btn_spinner(id = "attendee-notification-spinner", spinner_type = "2") -%}
208+
Send email
209+
</button>
210+
</div>
211+
</form>
212+
</div>
213+
</div>
214+
</div>
215+
</div>

ocg-server/templates/dashboard/group/home.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
<script type="module" src="/static/js/dashboard/community/team-add-member.js"></script>
1212
<script type="module" src="/static/js/dashboard/group/group-selector.js"></script>
1313
<script type="module" src="/static/js/dashboard/group/event-selector.js"></script>
14+
<script type="module" src="/static/js/dashboard/group/attendees.js"></script>
15+
<script type="module" src="/static/js/dashboard/group/members.js"></script>
1416
<script type="module" src="/static/js/common/image-field.js"></script>
1517
<script type="module" src="/static/js/common/gallery-field.js"></script>
1618
{% endblock scripts -%}

0 commit comments

Comments
 (0)