<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
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.