Skip to content

Instantly share code, notes, and snippets.

@Alejandro-Bernal-M
Created February 6, 2025 23:04
Show Gist options
  • Save Alejandro-Bernal-M/381bc0f92d89dd699ac22f822e9771cb to your computer and use it in GitHub Desktop.
Save Alejandro-Bernal-M/381bc0f92d89dd699ac22f822e9771cb to your computer and use it in GitHub Desktop.
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = [ "dates", "eventType", "meetingOptions", "SelectedTimeZone", "contactTimeZone", "dateSelectedForMeeting", "noAvailableImage" ]
dateSelected(event) {
const date = event.target.dataset.date;
this.toggleDateClass(event.target);
if (event.target.classList.contains('!bg-blue-500')) {
this.addDate(date);
} else {
this.removeDate(date);
}
}
addDate(date) {
const target = this.datesTarget;
if (!target) {
console.error("Target element not found");
return;
}
const { year, month, day, monthName } = this.parseDate(date);
const spanElement = this.createDateSpan(date, day);
let monthElement = target.querySelector(`[data-month="${month}"]`);
if (monthElement) {
this.insertIntoExistingMonth(monthElement, spanElement, day);
} else {
this.createNewMonthElement(target, monthName, month, spanElement);
}
this.cleanCommas();
}
// Parses the date into components
parseDate(date) {
const [year, month, day] = date.split("-");
const monthName = new Date(year, month - 1, day).toLocaleString("en-US", { month: "long" });
return { year, month, day, monthName };
}
// Creates a span element for the day
createDateSpan(date, day) {
const spanElement = document.createElement("span");
spanElement.classList.add("text-xs", "dates-span");
spanElement.dataset.day = day;
spanElement.dataset.date = date;
spanElement.textContent = day;
return spanElement;
}
// Inserts the date span into an existing month element in the correct order
insertIntoExistingMonth(monthElement, spanElement, day) {
const monthDiv = monthElement.querySelector("div");
let insertPosition = null;
Array.from(monthDiv.childNodes).some((child, index) => {
if (parseInt(child.dataset.day) > parseInt(day)) {
insertPosition = index;
return true;
}
return false;
});
if (insertPosition !== null) {
if (insertPosition > 0) {
spanElement.textContent = `,${spanElement.textContent},`;
}else {
spanElement.textContent = `${spanElement.textContent},`;
}
monthDiv.insertBefore(spanElement, monthDiv.childNodes[insertPosition]);
} else {
if (monthDiv.childNodes.length > 0) {
spanElement.textContent = `,${spanElement.textContent}`;
}
monthDiv.appendChild(spanElement);
}
}
// Creates a new month element and appends it to the target
createNewMonthElement(target, monthName, month, spanElement) {
const monthElement = document.createElement("div");
monthElement.dataset.month = month;
monthElement.classList.add("font-bold", "text-xs", "text-gray-500", "calendar-month");
monthElement.textContent = monthName;
const monthDiv = document.createElement("div");
monthDiv.classList.add("calendar-month-dates");
monthDiv.appendChild(spanElement);
monthElement.appendChild(monthDiv);
target.appendChild(monthElement);
}
removeDate(date) {
const target = this.datesTarget;
if (!target) {
console.error("Target element not found");
return;
}
const [year, month, day] = date.split("-");
const monthElement = target.querySelector(`[data-month="${month}"]`);
if (!monthElement) return; // No matching month element
const monthDiv = monthElement.querySelector("div");
const spanElement = monthDiv?.querySelector(`[data-day="${day}"]`);
if (spanElement) {
spanElement.remove();
}
// Remove the entire month element if no dates remain
if (monthDiv && monthDiv.childNodes.length === 0) {
monthElement.remove();
}
this.cleanCommas();
}
cleanCommas() {
const target = this.datesTarget;
if (!target) {
console.error("Target element not found");
return;
}
const spans = Array.from(target.querySelectorAll("span"));
spans.forEach((span, index) => {
// Remove leading comma from the first span
if (index === 0 && span.textContent.startsWith(",")) {
span.textContent = span.textContent.slice(1);
}
// Remove trailing comma from the last span
if (index === spans.length - 1 && span.textContent.endsWith(",")) {
span.textContent = span.textContent.slice(0, -1);
}
// Replace double commas with a single comma
if (span.textContent.includes(",,")) {
span.textContent = span.textContent.replace(/,,/g, ",");
}
// Remove trailing comma if the next span starts with one
if (
index < spans.length - 1 &&
span.textContent.endsWith(",") &&
spans[index + 1].textContent.startsWith(",")
) {
span.textContent = span.textContent.slice(0, -1);
}
});
}
addTimeSlot() {
const target = this.element.querySelector("#time-availabilities");
if (!target) {
console.error("Target element not found");
return;
}
const template = document.getElementById("calendar-time-slot-template");
if (!template) {
console.error("Template element not found");
return;
}
const lastTimeSlot = target.querySelector(".time-slot:last-child");
if (!lastTimeSlot) {
console.error("No existing time slots found");
return;
}
const endTime = lastTimeSlot.querySelector(".end-time").value;
if (!endTime || endTime.length < 5) {
console.error("Invalid end time format");
return;
}
const { newStartTime, newEndTime } = this.calculateNewTimeSlots(endTime);
// Clone the template content and append it to the target
const clone = document.importNode(template.content, true);
target.appendChild(clone);
const newTimeSlot = target.querySelector(".time-slot:last-child");
newTimeSlot.querySelector(".start-time").value = newStartTime;
newTimeSlot.querySelector(".end-time").value = newEndTime;
}
removeTimeSlot(event){
event.preventDefault();
const item = event.target.closest(".time-slot");
item.remove();
}
toggle(){
const target = document.getElementById("calendar-container");
if (!target) {
console.error("Target element not found");
return;
}
target.classList.toggle('hidden');
}
addDateOverrideTimeSlot(event) {
event.preventDefault();
const template = document.getElementById("date-override-template");
if (!template) {
console.error("Template element not found");
return;
}
const target = event.target.closest(".date-override-time-slots");
if (!target) {
console.error("Target element not found");
return;
}
const date = this.getDateFromEvent(event);
const startTime = this.getLastEndTime(target);
if (!startTime) {
console.error("Unable to determine the start time");
return;
}
const { newStartTime, newEndTime } = this.calculateNewTimeSlots(startTime);
const content = this.generateTemplateContent(template, new Date().getTime());
this.appendNewOverrideSlot(target, content, date, newStartTime, newEndTime);
}
getDateFromEvent(event) {
return event.target.parentElement.previousElementSibling.value || "";
}
getLastEndTime(target) {
const visibleOverrides = Array.from(
target.querySelectorAll(".date-override:not([style*='display: none'])")
);
const lastOverride = visibleOverrides.pop();
return lastOverride?.querySelector(".end-time")?.value || null;
}
calculateNewTimeSlots(startTime) {
const startHour = parseInt(startTime.slice(0, 2), 10);
const minutes = startTime.slice(2);
const newStartHour = startHour.toString().padStart(2, "0");
const newEndHour = ((startHour + 1) % 24).toString().padStart(2, "0");
return {
newStartTime: `${newStartHour}${minutes}`,
newEndTime: `${newEndHour}${minutes}`,
};
}
generateTemplateContent(template, uniqueId) {
return template.innerHTML.replace(/new_record/g, uniqueId);
}
appendNewOverrideSlot(target, content, date, newStartTime, newEndTime) {
target.insertAdjacentHTML("beforeend", content);
const newOverride = target.querySelector(".date-override:last-child");
if (!newOverride) {
console.error("Failed to append new override slot");
return;
}
newOverride.querySelector(".date").value = date;
newOverride.querySelector(".start-time").value = newStartTime;
newOverride.querySelector(".end-time").value = newEndTime;
}
removeDateOverrideTimeSlot(event) {
event.preventDefault();
const item = event.target.closest(".date-override");
if (!item) {
console.error("Date override item not found");
return;
}
const removeField = item.querySelector(".remove_field");
if (removeField) {
this.markForRemoval(item, removeField);
} else {
item.remove();
}
this.cleanUpEmptyContainer(event.target);
}
markForRemoval(item, removeField) {
removeField.value = 1;
item.style.display = "none";
}
cleanUpEmptyContainer(target) {
const timeSlotsContainer = target.closest(".date-override-time-slots");
if (!timeSlotsContainer) {
console.error("Time slots container not found");
return;
}
const visibleOverrides = timeSlotsContainer.querySelectorAll(".date-override:not([style*='display: none'])");
if (visibleOverrides.length === 0) {
const overrideContainer = target.closest(".date-override-container");
if (overrideContainer) {
overrideContainer.remove();
} else {
console.error("Date override container not found");
}
}
}
addSpecificsDates() {
const dates = Array.from(document.querySelectorAll('.dates-span'));
if (dates.length === 0) return;
const datesData = dates.map(span => span.dataset.date);
const availability = document.getElementById('switch-component-calendar').checked;
const timeSlots = this.collectTimeSlots();
const template = document.getElementById('date-override-template');
const target = document.getElementById('date-overrides-container');
const alreadyAddedDates = this.getAlreadyAddedDates();
datesData.forEach(date => {
let container = this.findOrCreateContainer(date, availability, alreadyAddedDates);
const timeSlotsContainer = container.querySelector('.date-override-time-slots');
if (alreadyAddedDates.includes(date)) {
this.clearExistingTimeSlots(timeSlotsContainer, availability);
}
this.addTimeSlotsToContainer(timeSlots, timeSlotsContainer, template, date, availability);
this.appendUnavailableSpan(container, availability);
if (!alreadyAddedDates.includes(date)) {
target.appendChild(container);
}
});
this.cleanPopup();
}
collectTimeSlots() {
return Array.from(document.querySelectorAll('#time-availabilities .time-slot')).map(slot => ({
start: slot.querySelector('.start-time').value,
end: slot.querySelector('.end-time').value,
}));
}
getAlreadyAddedDates() {
return Array.from(document.querySelectorAll('.date-override-container:not([style*="display: none"]) .dates-span-data'))
.map(span => span.dataset.date);
}
findOrCreateContainer(date, availability, alreadyAddedDates) {
if (alreadyAddedDates.includes(date)) {
return document.querySelector(`.date-override-container .dates-span-data[data-date="${date}"]`)
.closest('.date-override-container');
}
const container = this.createContainer(date, availability);
return container;
}
createContainer(date, availability) {
const container = document.createElement('div');
container.classList.add('flex', 'mt-2', 'date-override-container', 'mx-auto', 'w-[480px]');
const removeButton = this.createRemoveButton();
container.appendChild(removeButton);
const availabilitySwitch = this.createAvailabilitySwitch(date, availability);
container.appendChild(availabilitySwitch);
const dateLabel = this.createDateLabel(date);
container.appendChild(dateLabel);
const timeSlotsContainer = document.createElement('div');
timeSlotsContainer.classList.add('flex', 'flex-col', 'ml-2', 'w-max', 'date-override-time-slots');
container.appendChild(timeSlotsContainer);
return container;
}
createRemoveButton() {
const removeButton = document.createElement('img');
removeButton.src = document.getElementById('remove-red').dataset.src;
removeButton.classList.add('w-7', 'h-7', 'cursor-pointer', 'mt-2', 'remove-button', 'mr-2');
removeButton.dataset.action = 'click->calendar#removeDateOverride';
removeButton.dataset.controller = 'calendar';
return removeButton;
}
createAvailabilitySwitch(date, availability) {
const switchContainer = document.createElement('div');
switchContainer.classList.add('relative', 'inline-block', 'mt-2');
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.classList.add('hidden', 'peer');
checkbox.checked = availability;
checkbox.id = `switch-component-${date}`;
checkbox.dataset.controller = 'calendar';
checkbox.dataset.action = 'change->calendar#toggleAvailability';
const label = document.createElement('label');
label.classList.add(
'w-11', 'h-5', 'rounded-full', 'flex', 'items-center',
'transition-all', 'duration-300', 'bg-slate-100',
'peer-checked:bg-slate-800', 'peer-checked:pl-6', 'cursor-pointer'
);
label.htmlFor = checkbox.id;
const switchButton = document.createElement('div');
switchButton.classList.add('w-5', 'h-5', 'bg-white', 'rounded-full', 'border', 'border-slate-300', 'shadow-sm', 'cursor-pointer');
label.appendChild(switchButton);
switchContainer.appendChild(checkbox);
switchContainer.appendChild(label);
return switchContainer;
}
createDateLabel(date) {
const label = document.createElement('span');
label.classList.add('text-xs', 'dates-span-data', 'mt-2', 'ml-2', 'w-20');
label.dataset.date = date;
label.textContent = date;
return label;
}
clearExistingTimeSlots(container, availability) {
container.querySelectorAll('.date-override').forEach(override => {
override.querySelector('.remove_field').value = 1;
override.style.display = 'none';
});
const checkbox = container.closest('.date-override-container').querySelector('input');
checkbox.checked = availability;
}
addTimeSlotsToContainer(timeSlots, container, template, date, availability) {
timeSlots.forEach((slot, index) => {
const content = this.generateTemplateContent(template, new Date().getTime());
container.insertAdjacentHTML('beforeend', content);
const override = container.querySelector('.date-override:last-child');
override.querySelector('.available').checked = availability;
override.querySelector('.date').value = date;
override.querySelector('.start-time').value = slot.start;
override.querySelector('.end-time').value = slot.end;
if (index === 0) {
override.querySelector('.add-button').classList.remove('hidden');
override.querySelector('.remove-button').classList.add('hidden');
}
});
container.classList.toggle('hidden', !availability);
container.classList.toggle('flex', availability);
}
appendUnavailableSpan(container, availability) {
let span = container.querySelector('.unavailable-span');
if (!span) {
span = document.createElement('span');
span.classList.add('text-sm', 'font-normal', 'text-gray-500', 'mt-2', 'unavailable-span', 'ml-2', 'h-12');
container.appendChild(span);
}
span.textContent = 'Unavailable';
span.classList.toggle('hidden', availability);
span.classList.toggle('block', !availability);
}
cleanPopup() {
const target = document.getElementById('calendar-container');
if (!target) {
console.error('Calendar container not found');
return;
}
// Hide the calendar container
target.classList.add('hidden');
// Remove all date spans
this.clearElements(target, '.dates-span');
// Reset calendar day styles
this.resetCalendarDays(target);
// Clear picked dates
this.clearPickedDates(target);
}
clearElements(target, selector) {
const elements = target.querySelectorAll(selector);
elements.forEach(element => element.remove());
}
resetCalendarDays(target) {
const days = target.querySelectorAll('.calendar-day');
days.forEach(day => {
day.classList.remove('!bg-blue-500', 'text-white');
});
}
clearPickedDates(target) {
const pickedDatesElement = target.querySelector('#picked-dates');
if (pickedDatesElement) {
pickedDatesElement.textContent = '';
}
}
toggleAvailability(event) {
const availabilityChecked = event.target.checked;
const targetContainer = event.target.parentElement.nextElementSibling.nextElementSibling;
// Toggle visibility of relevant containers
this.toggleVisibility(targetContainer);
this.toggleVisibility(targetContainer.nextElementSibling);
// Update availability status for all date overrides
targetContainer.querySelectorAll('.date-override').forEach((dateOverride) => {
dateOverride.querySelector('.available').checked = availabilityChecked;
});
}
toggleVisibility(container) {
container.classList.toggle('hidden');
}
removeDateOverride(event){
event.preventDefault();
const container = event.target.closest(".date-override-container");
const datesOverrides = container.querySelectorAll('.date-override');
datesOverrides.forEach((dateOverride) => {
dateOverride.querySelector('.remove_field').value = 1;
});
container.style.display = "none";
}
dateSelectedForMeeting(event){
this.cleanTimeOptions();
this.cleanOtherCalendarDays(event.target)
const date = event.target.dataset.date;
if (!date) return;
this.toggleDateClass(event.target);
if (event.target.classList.contains('!bg-blue-500')) {
this.assingDateSelectedForMeeting(date);
this.showTimeOptions(event.target);
} else {
this.removeDateSelectedForMeeting();
}
}
cleanTimeOptions(){
const target = this.meetingOptionsTarget;
if (!target) {
console.error("Target element not found");
return;
}
target.innerHTML = "";
}
cleanOtherCalendarDays(target){
const days = document.querySelectorAll('.calendar-day');
days.forEach(day => {
if(day !== target){
day.classList.remove('!bg-blue-500', 'text-white');
}
});
}
assingDateSelectedForMeeting(date){
this.dateSelectedForMeetingTarget.value = date;
}
toggleDateClass(target){
if(!target) return;
target.classList.toggle('!bg-blue-500');
target.classList.toggle('text-white');
}
resetCalendarDays() {
const days = document.querySelectorAll('.calendar-day');
days.forEach(day => {
day.classList.remove('!bg-blue-500', 'text-white');
});
}
removeDateSelectedForMeeting(){
this.dateSelectedForMeetingTarget.value = '';
this.resetCalendarDays();
}
showTimeOptions(target){
const selectElement = this.eventTypeTarget; // The <select> element
const selectedOption = selectElement.options[selectElement.selectedIndex]; // The currently selected <option>
const durationInMinutes = selectedOption.value; // Retrieve the data-duration attribute
if(!durationInMinutes){
console.error("Missing data-duration attribute");
return;
}
const data = target.dataset.timeIntervals; // Retrieve the data-time-intervals attribute
const date = target.dataset.date;
if (!data || !date) {
console.error("Missing data or date attributes");
this.displayNoTimeOptions(date);
return;
}
const intervalsDataArray = JSON.parse(data);
const intervals = this.buildTimeIntervals(intervalsDataArray, durationInMinutes, date);
this.displayTimeOptions(intervals);
}
buildTimeIntervals(intervalsDataArray, durationInMinutes){
const intervals = [];
intervalsDataArray.forEach(interval => {
const startTime = Date.parse(interval.start_time);
const endTime = Date.parse(interval.end_time);
let currentTime = startTime;
while (currentTime < endTime) {
// adds the duration in minutes to the current time
const nextTime = currentTime + durationInMinutes * 60 * 1000;
if(nextTime > endTime) break;
intervals.push({
start: new Date(currentTime).toISOString(),
end: nextTime <= endTime ? new Date(nextTime).toISOString() : new Date(endTime).toISOString(),
});
currentTime = nextTime;
}
});
return intervals;
}
displayNoTimeOptions(date){
const target = this.meetingOptionsTarget;
if (!target) {
console.error("Target element not found");
return;
}
const dateObject = new Date(date + "T00:00:00");
const newDate = dateObject.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' });
if(date){
target.innerHTML = `<p class='text-xs text-blue-gray-500 font-bold w-full items-right mb-2'>${newDate}</p>`;
}
target.innerHTML += "<p class='text-sm text-orange-700 bg-orange-50 font-bold rounded-md w-full p-2 text-center uppercase'>No times available</p>";
const src = this.noAvailableImageTarget.dataset.src;
if (src) {
target.innerHTML += `<img src="${src}" class="w-40 mx-auto mt-10" alt="No times available" />`;
}
}
displayTimeOptions(intervals){
const target = this.meetingOptionsTarget;
if (!target) {
console.error("Target element not found");
return;
}
intervals.forEach((interval) => {
const content = this.generateTimeOption(interval);
target.insertAdjacentHTML("beforeend", content);
});
}
generateTimeOption(interval) {
return `
<div class="flex items-center mt-2 w-full">
<input
type="radio"
name="calendar_booking[start_time]"
value="${interval.start}"
class="hidden peer"
id="booking_start_time_${interval.start}"
>
<label
for="booking_start_time_${interval.start}"
class="flex item-center justify-center w-full rounded-lg bg-white border border-light-blue-500 p-2 text-light-blue-700 text-sm font-bold text-center cursor-pointer peer-checked:bg-blue-500 peer-checked:text-white"
>
<span>${this.formatDate(interval.start)} - ${this.formatDate(interval.end)}</span>
</label>
</div>
`;
}
formatDate(date) {
const selectedTimeZone = this.SelectedTimeZoneTarget.value; // Get the selected time zone
// Parse the date using the contact's time zone
const contactDate = new Date(date);
// Format the date in the selected time zone
const options = {
hour: "2-digit",
minute: "2-digit",
timeZone: selectedTimeZone,
};
return contactDate.toLocaleString("en-US", options);
}
eventTypeSelected(){
this.cleanTimeOptions();
this.removeDateSelectedForMeeting();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment