Skip to content

Commit fc4f39c

Browse files
committed
added multiple times ui
1 parent af94ec1 commit fc4f39c

File tree

2 files changed

+273
-114
lines changed

2 files changed

+273
-114
lines changed

static/js/event-management.js

Lines changed: 219 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,17 @@
11
// This script validates the form inputs before submission and updates fields if necessary
22

33
document.addEventListener("DOMContentLoaded", () => {
4-
// load icons from the datalist options
5-
const icons = Array.from(document.querySelectorAll("#icon-list option")).map(option => option.value.trim());
64

7-
// load colours from invisible element
8-
const colours = {};
9-
const invisibleColours = document.querySelectorAll("#invisible-colours span");
10-
invisibleColours.forEach(span => {
11-
const [name, hex] = span.textContent.trim().split(":");
12-
colours[name] = hex;
13-
});
5+
// MARK: icons
146

157
// update icon preview
168
const iconInput = document.getElementById("icon");
179
const iconPreview = document.getElementById("icon-preview");
1810
const customIconPreview = document.getElementById("custom-icon-preview");
1911

12+
// load icons from the datalist options
13+
const icons = Array.from(document.querySelectorAll("#icon-list option")).map(option => option.value.trim());
14+
2015
iconInput.addEventListener("input", () => {
2116
if (iconInput.value.startsWith("ph-")) {
2217
// remove the "ph-" prefix if it exists
@@ -51,10 +46,20 @@ document.addEventListener("DOMContentLoaded", () => {
5146
}
5247
});
5348

49+
// MARK: colours
50+
5451
// update colour preview
5552
const colourPicker = document.getElementById("color_colour");
5653
const colourText = document.getElementById("text_colour");
5754

55+
// load colours from invisible element
56+
const colours = {};
57+
const invisibleColours = document.querySelectorAll("#invisible-colours span");
58+
invisibleColours.forEach(span => {
59+
const [name, hex] = span.textContent.trim().split(":");
60+
colours[name] = hex;
61+
});
62+
5863
function syncColourInputs(fromText) {
5964
if (fromText) {
6065
if (Object.keys(colours).includes(colourText.value)) {
@@ -82,13 +87,17 @@ document.addEventListener("DOMContentLoaded", () => {
8287
}
8388
});
8489

85-
// update duration/end time
86-
const endTimeInput = document.getElementById("end_time");
90+
// MARK: time entry
91+
92+
const timeFields = document.getElementById("time-fields");
93+
const addTimeButton = document.getElementById("add-time");
8794
const durationInput = document.getElementById("duration");
88-
const startTimeInput = document.getElementById("start_time");
95+
96+
let eventDuration = 0; // duration in ms
8997

9098
function formatDateTimeInput(input) {
9199
// format the input value to YYYY-MM-DDTHH:MM
100+
if (!(input instanceof Date)) return "";
92101
const pad = (num) => num.toString().padStart(2, "0");
93102
const year = input.getFullYear();
94103
const month = pad(input.getMonth() + 1); // months are zero-indexed
@@ -98,86 +107,224 @@ document.addEventListener("DOMContentLoaded", () => {
98107
return `${year}-${month}-${day}T${hours}:${minutes}`;
99108
}
100109

101-
function updateDuration() {
102-
if (startTimeInput.value && endTimeInput.value) {
110+
function formatDuration(input) {
111+
// convert ms into DD:HH:MM format
112+
if (input < 0) input = 0;
113+
const days = Math.floor(input / (1000 * 60 * 60 * 24));
114+
const hours = Math.floor((input % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
115+
const minutes = Math.floor((input % (1000 * 60 * 60)) / (1000 * 60));
116+
return [
117+
days.toString().padStart(2, "0"),
118+
hours.toString().padStart(2, "0"),
119+
minutes.toString().padStart(2, "0")
120+
].join(":");
121+
}
122+
123+
function parseDuration(input) {
124+
// parse DD:HH:MM format into milliseconds
125+
if (!/^\d{2}:\d{2}:\d{2}$/.test(input)) return 0;
126+
const [days, hours, minutes] = input.split(":").map(Number);
127+
return (days * 24 * 60 * 60 + hours * 60 * 60 + minutes * 60) * 1000;
128+
}
129+
130+
function validateEndTime(endTimeInput) {
131+
// makes sure the end time is after the start time
132+
const entry = endTimeInput.closest(".time-entry");
133+
const startTimeInput = entry.querySelector("input[name='start_time[]']");
134+
if (!startTimeInput || !endTimeInput) return;
135+
const startTime = new Date(startTimeInput.value);
136+
const endTime = new Date(endTimeInput.value);
137+
if (startTime >= endTime) {
138+
endTimeInput.setCustomValidity("End time must be after start time.");
139+
} else {
140+
endTimeInput.setCustomValidity("");
141+
}
142+
}
143+
144+
function syncEndTimes() {
145+
// sync all end times based on the start time and event duration
146+
document.querySelectorAll(".time-entry").forEach(entry => {
147+
const startTimeInput = entry.querySelector("input[name='start_time[]']");
148+
const endTimeInput = entry.querySelector("input[name='end_time[]']");
149+
if (!startTimeInput) return;
103150
const startTime = new Date(startTimeInput.value);
104-
const endTime = new Date(endTimeInput.value);
151+
const endTime = new Date(startTime.getTime() + eventDuration);
152+
endTimeInput.value = formatDateTimeInput(endTime);
153+
validateEndTime(endTimeInput);
154+
});
155+
}
105156

106-
let duration = endTime - startTime;
107-
if (duration < 0) {
108-
duration = 0;
109-
}
110-
// convert duration into DD:HH:MM format
111-
const days = Math.floor(duration / (1000 * 60 * 60 * 24));
112-
const hours = Math.floor((duration % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
113-
const minutes = Math.floor((duration % (1000 * 60 * 60)) / (1000 * 60));
114-
115-
// prepend 0s if necessary
116-
const formattedDuration = [
117-
days.toString().padStart(2, '0'),
118-
hours.toString().padStart(2, '0'),
119-
minutes.toString().padStart(2, '0')
120-
].join(':');
121-
durationInput.value = formattedDuration;
157+
function updateDuration(endInput) {
158+
// update duration
159+
const entry = endInput.closest(".time-entry");
160+
if (!entry) return;
161+
162+
const startTimeInput = entry.querySelector("input[name='start_time[]']");
163+
164+
if (!startTimeInput || !startTimeInput.value || !endInput.value) return;
165+
166+
const startTime = new Date(startTimeInput.value);
167+
const endTime = new Date(endInput.value);
168+
const duration = endTime.getTime() - startTime.getTime();
169+
170+
if (duration < 0) {
171+
endInput.setCustomValidity("End time must be after start time.");
172+
return;
122173
}
174+
175+
endInput.setCustomValidity("");
176+
eventDuration = duration;
177+
durationInput.value = formatDuration(eventDuration);
178+
syncEndTimes();
123179
}
124180

125-
function updateEndTime() {
126-
if (startTimeInput.value && durationInput.value) {
127-
// confirm that the duration is in DD:HH:MM format
128-
if (/^\d{2}:(?:[01]\d|2[0-3]):[0-5]\d$/.test(durationInput.value)) {
129-
const [days, hours, minutes] = durationInput.value.split(':').map(Number);
130-
const startTime = new Date(startTimeInput.value);
181+
function updateFutureStartTimes(changedInput) {
182+
const allEntries = Array.from(timeFields.querySelectorAll(".time-entry"));
183+
const currentIndex = allEntries.findIndex(entry => entry.contains(changedInput));
131184

132-
// calculate end time
133-
startTime.setDate(startTime.getDate() + days);
134-
startTime.setHours(startTime.getHours() + hours);
135-
startTime.setMinutes(startTime.getMinutes() + minutes);
185+
if (currentIndex < 0 || currentIndex + 1 >= allEntries.length) return;
136186

137-
// update end time input
138-
endTimeInput.value = formatDateTimeInput(startTime);
187+
let delta = 7 * 24 * 60 * 60 * 1000; // default is a week
188+
if (currentIndex > 0) {
189+
// if multiple entries, set the delta to the duration of the previous entry
190+
const previousEntry = allEntries[currentIndex - 1].querySelector("input[name='start_time[]']");
191+
if (previousEntry.value && changedInput.value) {
192+
delta = new Date(changedInput.value).getTime() - new Date(previousEntry.value).getTime();
139193
}
140194
}
195+
196+
for (let i = currentIndex + 1; i < allEntries.length; i++) {
197+
const prevStartInput = allEntries[i - 1].querySelector("input[name='start_time[]']");
198+
const currStartInput = allEntries[i].querySelector("input[name='start_time[]']");
199+
const currEndInput = allEntries[i].querySelector("input[name='end_time[]']");
200+
201+
if (!prevStartInput.value) continue;
202+
203+
const newStartTime = new Date(new Date(prevStartInput.value).getTime() + delta);
204+
currStartInput.value = formatDateTimeInput(newStartTime);
205+
const newEndTime = new Date(newStartTime.getTime() + eventDuration);
206+
currEndInput.value = formatDateTimeInput(newEndTime);
207+
validateEndTime(currEndInput);
208+
}
141209
}
142210

143-
startTimeInput.addEventListener("input", () => {
144-
updateDuration();
145-
updateEndTime();
211+
timeFields.addEventListener("input", (event) => {
212+
const input = event.target;
213+
214+
if (input.name === "start_time[]") {
215+
updateFutureStartTimes(input);
216+
const entry = input.closest(".time-entry");
217+
const endTimeInput = entry.querySelector("input[name='end_time[]']");
218+
const startTime = new Date(input.value);
219+
endTimeInput.value = formatDateTimeInput(new Date(startTime.getTime() + eventDuration));
220+
validateEndTime(endTimeInput);
221+
} else if (input.name === "end_time[]") {
222+
updateDuration(input);
223+
validateEndTime(input);
224+
}
146225
});
147-
durationInput.addEventListener("input", () => updateEndTime());
148-
endTimeInput.addEventListener("input", () => updateDuration());
149226

150-
// check if end time is after start time
151-
endTimeInput.addEventListener("input", () => {
152-
if (startTimeInput.value && endTimeInput.value) {
153-
const startTime = new Date(startTimeInput.value);
154-
const endTime = new Date(endTimeInput.value);
227+
durationInput.addEventListener("input", () => {
228+
duration = parseDuration(durationInput.value);
229+
if (duration > 0) {
230+
eventDuration = duration;
231+
syncEndTimes();
232+
durationInput.setCustomValidity("");
233+
} else {
234+
durationInput.setCustomValidity("Please provide a valid duration in DD:HH:MM format.");
235+
}
236+
});
155237

156-
if (endTime <= startTime) {
157-
endTimeInput.setCustomValidity("End time must be after start time");
158-
} else {
159-
endTimeInput.setCustomValidity("");
238+
if (addTimeButton) {
239+
addTimeButton.addEventListener("click", (event) => {
240+
const allEntries = timeFields.querySelectorAll(".time-entry");
241+
242+
const lastEntry = allEntries[allEntries.length - 1];
243+
const prevStartInput = lastEntry.querySelector("input[name='start_time[]']");
244+
if (!prevStartInput || !prevStartInput.value) return;
245+
246+
let delta = 7 * 24 * 60 * 60 * 1000; // default is a week
247+
if (allEntries.length > 1) {
248+
const penultimateEntry = allEntries[allEntries.length - 2];
249+
const penultimateStartInput = penultimateEntry.querySelector("input[name='start_time[]']");
250+
delta = new Date(prevStartInput.value).getTime() - new Date(penultimateStartInput.value).getTime();
160251
}
252+
253+
const newStartTime = new Date(new Date(prevStartInput.value).getTime() + delta);
254+
const newEndTime = eventDuration > 0 ? new Date(newStartTime.getTime() + eventDuration) : NaN;
255+
256+
const newEntry = document.createElement("div");
257+
newEntry.className = "row g-3 time-entry";
258+
newEntry.innerHTML = `
259+
<div class="form-floating col-md-4">
260+
<input type="datetime-local" name="start_time[]" id="start_time" class="form-control" value="${formatDateTimeInput(newStartTime)}" required>
261+
<label for="start_time">Start Time</label>
262+
<div class="invalid-feedback">Please provide a start time</div>
263+
<div class="valid-feedback">Looks good!</div>
264+
</div>
265+
266+
<div class="form-floating col-md-4">
267+
<input type="datetime-local" name="end_time[]" id="end_time" class="form-control" value="${formatDateTimeInput(newEndTime)}">
268+
<label for="end_time">End Time</label>
269+
<div class="invalid-feedback">Endtime must be after start time and match the duration</div>
270+
<div class="valid-feedback">Looks good!</div>
271+
</div>
272+
273+
<div class="col-md-4 d-flex align-items-center">
274+
<button type="button" class="btn btn-danger remove-time-entry"><i class="ph-bold ph-trash"></i> Remove</button>
275+
</div>
276+
`;
277+
timeFields.appendChild(newEntry);
278+
});
279+
}
280+
281+
timeFields.addEventListener("click", (event) => {
282+
if (!event.target.classList.contains("remove-time-entry")) return;
283+
284+
const entry = event.target.closest(".time-entry");
285+
const precedingEntry = entry.previousElementSibling;
286+
287+
entry.remove();
288+
289+
if (precedingEntry && precedingEntry.classList.contains("time-entry")) {
290+
const startTimeInput = precedingEntry.querySelector("input[name='start_time[]']");
291+
if (startTimeInput) updateFutureStartTimes(startTimeInput);
292+
} else if (document.quwrySelector(".time-entry")) {
293+
const firstEntry = document.querySelector(".time-entry").querySelector("input[name='start_time[]']");
294+
if (firstEntry) updateFutureStartTimes(firstEntry);
161295
}
162296
});
163297

164-
// check if endtime = starttime + duration
165-
endTimeInput.addEventListener("input", () => {
166-
if (startTimeInput.value && durationInput.value) {
167-
const startTime = new Date(startTimeInput.value);
168-
const [days, hours, minutes] = durationInput.value.split(':').map(Number);
169-
startTime.setDate(startTime.getDate() + days);
170-
startTime.setHours(startTime.getHours() + hours);
171-
startTime.setMinutes(startTime.getMinutes() + minutes);
172-
const endTime = new Date(endTimeInput.value);
173-
if (endTime.getTime() !== startTime.getTime()) {
174-
endTimeInput.setCustomValidity("End time does not match duration");
175-
} else {
176-
endTimeInput.setCustomValidity("");
298+
function initialiseTimes() {
299+
const firstEntry = timeFields.querySelector(".time-entry");
300+
if (!firstEntry) return;
301+
302+
const startTimeInput = firstEntry.querySelector("input[name='start_time[]']");
303+
const endTimeInput = firstEntry.querySelector("input[name='end_time[]']");
304+
305+
if (startTimeInput.value && endTimeInput.value) {
306+
const initialDuration = new Date(endTimeInput.value).getTime() - new Date(startTimeInput.value).getTime();
307+
if (initialDuration >= 0) {
308+
eventDuration = initialDuration;
309+
durationInput.value = formatDuration(eventDuration);
310+
}
311+
} else {
312+
const initialDuration = parseDuration(durationInput.value);
313+
if (initialDuration > 0) {
314+
eventDuration = initialDuration;
315+
if (startTimeInput.value) {
316+
const startTime = new Date(startTimeInput.value);
317+
endTimeInput.value = formatDateTimeInput(new Date(startTime.getTime() + eventDuration));
318+
}
177319
}
178320
}
179-
});
180321

322+
document.querySelectorAll("input[name='end_time[]']").forEach(validateEndTime);
323+
}
324+
325+
initialiseTimes();
326+
327+
// MARK: form validation
181328

182329
// form validation
183330
const form = document.querySelector("form");
@@ -195,7 +342,7 @@ document.addEventListener("DOMContentLoaded", () => {
195342
}, false);
196343

197344
// trigger events on load
198-
[iconInput, colourText, startTimeInput, endTimeInput].forEach(input => {
345+
[iconInput, colourText].forEach(input => {
199346
if (input && input.value) {
200347
input.dispatchEvent(new Event('input', { bubbles: true }));
201348
}

0 commit comments

Comments
 (0)