<div id="root"></div>
// Variables
$primary: #555;
$secondary: #fdfdfd;
$font-size: 14px;
$font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
$font-weight: 400;
$letter-spacing: 0.4px;
$line-height: 1.5;

// Global Styles
* {
  box-sizing: border-box;
}

html,
body {
  height: 100%;
  margin: 0;
  padding: 0;
  overflow-x: hidden;
}

body {
  font-family: $font-family;
  font-size: $font-size;
  font-weight: $font-weight;
  letter-spacing: $letter-spacing;
  line-height: $line-height;
}

// Scrollbar
::-webkit-scrollbar {
  width: 8px;
  height: 8px;
  background: $secondary;
}

::-webkit-scrollbar-thumb {
  background: $primary;
}

// Calendar Container
#calendarContainer {
  header {
    display: flex;
    align-items: center;
    height: 4rem;
    background: darken($primary, 10%);
    color: $secondary;
    text-align: center;

    .header-left,
    .header-center,
    .header-right {
      flex: 1;
      padding: 0 0.5rem;
    }

    .header-center {
      h1 {
        font-size: 25px;
        line-height: 1.8rem;
        margin: 0;
      }
    }

    .header-left {
      button {
        background: darken($primary, 15%);
        border: 1px solid darken($primary, 15%);
        color: $secondary;
        padding: 0.5rem 1rem;
        font-size: 16px;
        margin: 0 4px;
        cursor: pointer;
        transition: background-color 0.5s ease, border-color 0.5s ease;

        &:hover {
          background: darken($primary, 12%);
          border-color: darken($primary, 12%);
        }
      }
    }
  }

  #calendarBody {
    width: 100%;

    #weekdays,
    #days {
      list-style: none;
      padding: 0;
      margin: 0;
      display: flex;
      flex-wrap: wrap;
    }

    #weekdays {
      height: 40px;
      background: darken($primary, 15%);
      border-bottom: 2px solid darken($primary, 20%);

      li {
        flex: 1 0 calc(100% / 7);
        text-align: center;
        text-transform: uppercase;
        line-height: 20px;
        padding: 10px 6px;
        color: $secondary;
        font-size: 13px;
        font-weight: bold;

        &.today {
          background: darken($primary, 20%);
        }
      }
    }

    #days {
      li {
        flex: 1 0 calc(100% / 7);
        padding: 5px;
        height: 150px;
        overflow-y: auto;
        background: $secondary;
        color: darken($primary, 20%);
        border: 1px solid darken($secondary, 5%);
        position: relative;
        transition: background 0.5s ease;

        &:hover {
          background: darken($secondary, 5%);
        }

        &.today {
          background: lighten($primary, 40%);

          &:hover {
            background: lighten($primary, 30%);
          }
        }

        .info {
          position: absolute;
          bottom: 2px;
          right: 2px;
          opacity: 0;
        }

        .date {
          text-align: center;
          margin-bottom: 5px;
          background: lighten($primary, 15%);
          color: $secondary;
          width: 25px;
          height: 25px;
          line-height: 25px;
          border-radius: 50%;
          float: right;
          font-size: 12px;
          font-weight: bold;
        }
      }
    }
  }
}

// Event Styles
.event {
  background: lighten($primary, 40%);
  border: 1px solid lighten($primary, 30%);
  border-radius: 4px;
  margin: 5px;
  transition: background 0.5s ease;

  &:hover {
    background: lighten($primary, 15%);

    .event-desc a {
      color: lighten($primary, 25%);
    }
  }

  .event-desc {
    padding: 0.2rem 0.5rem;

    a {
      text-decoration: none;
      color: darken($primary, 25%);
      transition: color 0.5s ease;
    }
  }
}

// Dialog Styles
dialog {
  border: none;
  border-radius: 10px;
  background: $secondary;
  padding: 20px;
  font-family: $font-family;
  font-size: $font-size;
  color: $primary;
  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);

  label {
    display: block;
    margin-bottom: 10px;
    font-weight: $font-weight;
  }

  input {
    width: 100%;
    padding: 8px;
    margin-bottom: 15px;
    border: 1px solid #ccc;
    border-radius: 5px;
    font-size: $font-size;
  }

  button {
    padding: 10px 15px;
    font-size: $font-size;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    margin-right: 10px;

    &[type='submit'] {
      background: $primary;
      color: $secondary;
    }

    &#cancelButton {
      background: #ccc;
      color: $primary;
    }
  }
}

// Responsive Design
@media (max-width: 768px) {
  #calendarContainer {
    header {
      height: auto;
      padding: 1rem;
      flex-direction: column;

      .header-left,
      .header-center,
      .header-right {
        flex: none;
        width: 100%;
      }
    }

    #calendarBody {
      #weekdays {
        display: none;
      }

      #days {
        li {
          flex: 1 0 100%;
          height: auto;
          padding: 10px;
          margin-bottom: -1px;

          .info {
            left: 2px;
            opacity: 1;
            color: lighten($primary, 35%);
          }

          .date {
            float: none;
          }
        }
      }
    }
  }
}
View Compiled
// Utility Functions
const sanitizeHTML = (str) => {
	const div = document.createElement("div");
	div.textContent = str;
	return div.innerHTML;
};

const getDaysInMonth = (month, year) =>
	32 - new Date(year, month, 32).getDate();

const createElement = (tag, parent, attributes = {}) => {
	const element = document.createElement(tag);
	Object.assign(element, attributes);
	parent.appendChild(element);
	return element;
};

// Calendar Data and Storage
const today = new Date();
let currentMonth = today.getMonth();
let currentYear = today.getFullYear();

const weekdays = [
	"Lunes",
	"Martes",
	"Miércoles",
	"Jueves",
	"Viernes",
	"Sábado",
	"Domingo"
];
const months = [
	"Enero",
	"Febrero",
	"Marzo",
	"Abril",
	"Mayo",
	"Junio",
	"Julio",
	"Agosto",
	"Septiembre",
	"Octubre",
	"Noviembre",
	"Diciembre"
];

let events =
	JSON.parse(localStorage.getItem("events")) ||
	[
		{
			id: "event1",
			date: `2025/${currentMonth + 1}/3`,
			content: "New Session",
			source: "http://example.com"
		},
		{
			id: "event2",
			date: `2025/${currentMonth + 1}/15`,
			content: "Optimize Components",
			source: "http://example.com"
		},
		{
			id: "event3",
			date: `2025/${currentMonth}/23`,
			content: "New Session",
			source: "http://example.com"
		}
	].filter(({ date }) => {
		const [year, month, day] = date.split("/").map(Number);
		return (
			month >= 1 &&
			month <= 12 &&
			day >= 1 &&
			day <= getDaysInMonth(month - 1, year)
		);
	});

const saveEvents = () => localStorage.setItem("events", JSON.stringify(events));

// Calendar UI Setup
const root = window.root || document.body;
const calendarContainer = createElement("div", root, {
	id: "calendarContainer"
});
const header = createElement("header", calendarContainer);
const headerLeft = createElement("div", header, { className: "header-left" });
const headerCenter = createElement("div", header, {
	className: "header-center"
});
const headerRight = createElement("div", header, { className: "header-right" });
const prevButton = createElement("button", headerLeft, {
	textContent: "Anterior"
});
const nextButton = createElement("button", headerLeft, {
	textContent: "Siguiente"
});
const title = createElement("h1", headerCenter, {
	textContent: `${months[currentMonth]} ${currentYear}`
});
const calendarBody = createElement("div", calendarContainer, {
	id: "calendarBody"
});
const weekdayList = createElement("ul", calendarBody, { id: "weekdays" });
const daysList = createElement("ul", calendarBody, { id: "days" });

// Add Event Button
const addEventButton = createElement("button", root, {
	id: "addEventButton",
	textContent: "+",
	style: `
    position: fixed;
    bottom: 20px;
    right: 20px;
    width: 50px;
    height: 50px;
    border-radius: 50%;
    background-color: #777;
    color: white;
    font-size: 24px;
    border: none;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
  `
});

// Event Dialog
const createEventDialog = () => {
	const dialog = createElement("dialog", document.body);
	dialog.innerHTML = `
    <form method="dialog">
      <label>Fecha: <input type="date" id="eventDate" required></label>
      <label>Contenido: <input type="text" id="eventContent" required></label>
      <label>Link: <input type="url" id="eventSource" required></label>
      <button type="submit">Guardar</button>
      <button type="button" id="cancelButton">Cancelar</button>
    </form>
  `;

	const cancelButton = dialog.querySelector("#cancelButton");
	cancelButton.onclick = () => dialog.close();

	dialog.addEventListener("close", () => {
		const dateInput = dialog.querySelector("#eventDate").value;
		const content = dialog.querySelector("#eventContent").value;
		const source = dialog.querySelector("#eventSource").value;

		if (dateInput && content && source) {
			// Validate and format date to YYYY/MM/DD
			let formattedDate;
			const dateParts = dateInput.split(/[-\/]/); // Handle both '-' and '/' separators
			if (dateParts.length === 3) {
				let [year, month, day] = dateParts.map(Number);
				// Ensure year is 4 digits, month is 1-12, and day is valid for the month
				if (
					!isNaN(year) &&
					!isNaN(month) &&
					!isNaN(day) &&
					year >= 1000 &&
					year <= 9999 &&
					month >= 1 &&
					month <= 12 &&
					day >= 1 &&
					day <= getDaysInMonth(month - 1, year)
				) {
					// Pad month and day with leading zeros if necessary
					month = String(month).padStart(2, "0");
					day = String(day).padStart(2, "0");
					formattedDate = `${year}/${month}/${day}`;
				} else {
					alert(
						"Fecha inválida. Por favor, introduce una fecha válida en el formato YYYY/MM/DD."
					);
					return;
				}
			} else {
				alert(
					"Formato de fecha incorrecto. Usa YYYY/MM/DD (por ejemplo, 2025/06/26)."
				);
				return;
			}

			events.push({
				id: `event${Date.now()}`,
				date: formattedDate,
				content,
				source
			});
			saveEvents();
			renderCalendar(currentMonth, currentYear);
		}

		dialog.remove();
	});

	dialog.showModal();
};

addEventButton.onclick = createEventDialog;

// Render Weekdays
weekdays.forEach((day, index) => {
	createElement("li", weekdayList, {
		className: today.getDay() - 1 === index ? "today" : "",
		textContent: day,
		"aria-label": day
	});
});

// Navigation Handlers
prevButton.onclick = () => {
	currentMonth = (currentMonth - 1 + 12) % 12;
	if (currentMonth === 11) currentYear--;
	renderCalendar(currentMonth, currentYear);
};

nextButton.onclick = () => {
	currentMonth = (currentMonth + 1) % 12;
	if (currentMonth === 0) currentYear++;
	renderCalendar(currentMonth, currentYear);
};

// Render Calendar
const renderCalendar = (month, year) => {
	const firstDay = (new Date(year, month).getDay() + 6) % 7; // Monday as first day
	daysList.textContent = "";
	title.textContent = `${months[month]} ${year}`;

	const fragment = document.createDocumentFragment();
	let date = 1;

	for (let i = 0; i < 6 && date <= getDaysInMonth(month, year); i++) {
		for (let j = 0; j < 7; j++) {
			let dayItem;
			if (i === 0 && j < firstDay) {
				dayItem = createElement("li", fragment, { textContent: "" });
			} else if (date > getDaysInMonth(month, year)) {
				break;
			} else {
				dayItem = createElement("li", fragment, {
					className:
						date === today.getDate() &&
						year === today.getFullYear() &&
						month === today.getMonth()
							? "today"
							: ""
				});
				createElement("div", dayItem, {
					className: "info",
					textContent: weekdays[j]
				});
				createElement("div", dayItem, { className: "date", textContent: date });

				renderEvents(events, dayItem, [year, month, date]);
				date++;
			}
		}
	}

	daysList.appendChild(fragment);
};

// Render Events
const renderEvents = (events, parent, [year, month, date]) => {
	events.forEach(({ id, date: eventDate, content, source }) => {
		const [eventYear, eventMonth, eventDay] = eventDate.split("/").map(Number);
		if (eventYear === year && eventMonth - 1 === month && eventDay === date) {
			const eventElement = createElement("div", parent, {
				className: "event",
				id
			});
			const eventDesc = createElement("div", eventElement, {
				className: "event-desc"
			});
			eventDesc.innerHTML = `<a href="${sanitizeHTML(source)}">${sanitizeHTML(
				content
			)}</a>`;
			eventElement.onclick = () => alert(eventDesc.textContent);
		}
	});
};

// Initialize Calendar
renderCalendar(currentMonth, currentYear);
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.