diff --git a/Project-Days-Calendar/days.json b/Project-Days-Calendar/days.json
index bc76e7c..f3c79d6 100644
--- a/Project-Days-Calendar/days.json
+++ b/Project-Days-Calendar/days.json
@@ -33,5 +33,12 @@
"dayName": "Friday",
"occurrence": "last",
"descriptionURL": "https://codeyourfuture.github.io/The-Piscine/days/lemurs.txt"
- }
+ },
+ {
+ "name": "World Octopus Day",
+ "monthName": "March",
+ "dayName": "Monday",
+ "occurrence": "second",
+ "descriptionURL": "https://codeyourfuture.github.io/The-Piscine/days/octopuses.txt"
+ }
]
diff --git a/Project-Days-Calendar/events_cleaned.js b/Project-Days-Calendar/events_cleaned.js
new file mode 100644
index 0000000..0d220df
--- /dev/null
+++ b/Project-Days-Calendar/events_cleaned.js
@@ -0,0 +1,462 @@
+const STORAGE_KEY = 'calendar_events';
+const MAX_PREVIEW = 3;
+
+let currentDateKey = null;
+let editingId = null;
+let deletingId = null;
+let eventsLoaded = false;
+
+// Shape: { 'YYYY-MM-DD': [ {id, title, date, desc} ] }
+let events = {};
+
+
+// ============================================================
+// PERSISTENCE (localStorage)
+// ============================================================
+
+function loadEvents() {
+ const raw = localStorage.getItem(STORAGE_KEY);
+ if (raw == null) return {};
+ return JSON.parse(raw);
+}
+
+function saveToStorage() {
+ // Strip empty date keys before saving
+ const entries = Object.entries(events);
+ for (let i = 0; i < entries.length; i++) {
+ const key = entries[i][0];
+ const arr = entries[i][1];
+ if (arr.length === 0) delete events[key];
+ }
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(events));
+}
+
+events = loadEvents();
+eventsLoaded = true;
+
+
+// ============================================================
+// PUBLIC HOLIDAYS API
+// ============================================================
+
+let publicHolidays = [];
+
+async function fetchHolidays(year) {
+ try {
+ const url = `https://date.nager.at/api/v3/PublicHolidays/${year}/US`;
+ const response = await fetch(url);
+ if (!response.ok) return;
+
+ const data = await response.json();
+ publicHolidays = data.map(h => h.date);
+ refreshIndicators();
+ } catch (err) {
+ publicHolidays = [];
+ }
+}
+
+function isHoliday(dateKey) {
+ return publicHolidays.includes(dateKey);
+}
+
+
+// ============================================================
+// UTILITIES
+// ============================================================
+
+const generateId = () => Math.random().toString(36).slice(2, 10);
+
+function buildDateKey(year, month, day) {
+ const m = String(month + 1).padStart(2, '0');
+ const d = String(day).padStart(2, '0');
+ return `${year}-${m}-${d}`;
+}
+
+const getEventsForDate = (key) => events[key] || [];
+const toZeroBasedMonth = (m) => m - 1;
+
+function getAllEvents() {
+ const all = [];
+ for (const arr of Object.values(events)) {
+ for (let i = 0; i < arr.length; i++) all.push(arr[i]);
+ }
+ return all;
+}
+
+function countEventsInMonth(year, month) {
+ const keySlice = Object.keys(events).slice(0, 100);
+ let count = 0;
+
+ for (let i = 0; i < keySlice.length; i++) {
+ const parts = keySlice[i].split('-');
+ const keyYear = Number(parts[0]);
+ const keyMonth = Number(parts[1]) - 1;
+ if (keyYear === year && keyMonth === month) {
+ count += getEventsForDate(keySlice[i]).length;
+ }
+ }
+ return count;
+}
+
+const getMonthIndex = (monthName) => {
+ const months = [
+ 'January', 'February', 'March', 'April',
+ 'May', 'June', 'July', 'August',
+ 'September','October', 'November', 'December'
+ ];
+ const idx = months.findIndex(m => m === monthName);
+ return idx != -1 ? idx : 0;
+};
+
+function searchEvents(query) {
+ const results = [];
+ const entries = Object.entries(events);
+
+ for (let i = 0; i < entries.length; i++) {
+ const dateStr = entries[i][0];
+ const arr = entries[i][1];
+
+ const matched = arr.filter(evt =>
+ evt.title.toLowerCase().includes(query.toLowerCase()) ||
+ evt.desc.toLowerCase().includes(query.toLowerCase())
+ );
+
+ if (matched.length > 0) results.push({ date: dateStr, events: matched });
+ }
+ return results;
+}
+
+// Resolve a day cell element to a 'YYYY-MM-DD' key using whatever
+// data attributes the teammate set, falling back to reading cell text.
+function getKeyFromCell(cell) {
+ if (cell.dataset.date) {
+ return cell.dataset.date;
+
+ } else if (cell.dataset.day) {
+ const dayNum = Number(cell.dataset.day);
+ if (isNaN(dayNum) || dayNum <= 0) return null;
+
+ const yearEl = document.getElementById('year-selector');
+ const monthEl = document.getElementById('month-selector');
+ const year = yearEl ? Number(yearEl.value) : new Date().getFullYear();
+ const month = monthEl ? getMonthIndex(monthEl.value) : new Date().getMonth();
+ return buildDateKey(year, month, dayNum);
+
+ } else {
+ const dayNum = parseInt(cell.textContent, 10);
+ if (!dayNum || dayNum <= 0) return null;
+
+ const yearEl = document.getElementById('year-selector');
+ const monthEl = document.getElementById('month-selector');
+ const year = yearEl ? Number(yearEl.value) : new Date().getFullYear();
+ const month = monthEl ? getMonthIndex(monthEl.value) : new Date().getMonth();
+ return buildDateKey(year, month, dayNum);
+ }
+}
+
+function rebuildEvents() {
+ events = Object.fromEntries(
+ Object.entries(events).filter(([, arr]) => arr.length > 0)
+ );
+}
+
+
+// ============================================================
+// CREATE / EDIT EVENT
+// ============================================================
+
+function openModal(dateKey) {
+ currentDateKey = dateKey;
+ editingId = null;
+
+ document.getElementById('ev-title').value = '';
+ document.getElementById('ev-date').value = dateKey;
+ document.getElementById('ev-desc').value = '';
+ document.getElementById('ev-title-error').textContent = '';
+ document.getElementById('ev-form-heading').textContent = 'Add Event';
+ document.getElementById('ev-save-btn').textContent = 'Save Event';
+
+ renderEventList();
+ document.getElementById('ev-overlay').style.display = 'flex';
+}
+
+function closeModal() {
+ document.getElementById('ev-overlay').style.display = 'none';
+ currentDateKey = null;
+ editingId = null;
+}
+
+function startEdit(id) {
+ const evt = getEventsForDate(currentDateKey).find(e => e.id === id);
+ if (!evt) return;
+
+ editingId = id;
+ document.getElementById('ev-title').value = evt.title;
+ document.getElementById('ev-date').value = evt.date;
+ document.getElementById('ev-desc').value = evt.desc;
+ document.getElementById('ev-form-heading').textContent = 'Edit Event';
+ document.getElementById('ev-save-btn').textContent = 'Update Event';
+}
+
+function saveEvent() {
+ const title = document.getElementById('ev-title').value.trim();
+ const date = document.getElementById('ev-date').value.trim();
+ const desc = document.getElementById('ev-desc').value.trim();
+
+ if (!title && !date) {
+ document.getElementById('ev-title-error').textContent = 'Title and date are required.';
+ return;
+ } else if (!title) {
+ document.getElementById('ev-title-error').textContent = 'Title is required.';
+ return;
+ } else if (!date) {
+ document.getElementById('ev-title-error').textContent = 'Date is required.';
+ return;
+ } else {
+ document.getElementById('ev-title-error').textContent = '';
+ }
+
+ if (!events[currentDateKey]) events[currentDateKey] = [];
+
+ if (editingId !== null) {
+ const idx = events[currentDateKey].findIndex(e => e.id === editingId);
+ if (idx !== -1) {
+ events[currentDateKey][idx] = { id: editingId, title, date, desc };
+ }
+ } else {
+ events[currentDateKey].push({ id: generateId(), title, date, desc });
+ }
+
+ saveToStorage();
+ renderEventList();
+ refreshIndicators();
+
+ editingId = null;
+ document.getElementById('ev-title').value = '';
+ document.getElementById('ev-desc').value = '';
+ document.getElementById('ev-form-heading').textContent = 'Add Event';
+ document.getElementById('ev-save-btn').textContent = 'Save Event';
+}
+
+
+// ============================================================
+// VIEW EVENTS FOR A DAY
+// ============================================================
+
+function renderEventList() {
+ const list = document.getElementById('ev-list');
+ list.innerHTML = '';
+
+ const evts = getEventsForDate(currentDateKey);
+
+ if (evts.length === 0) {
+ const p = document.createElement('p');
+ p.textContent = 'No events yet for this day.';
+ p.style.color = '#888';
+ list.appendChild(p);
+ return;
+ }
+
+ const items = evts.map(evt => {
+ const item = document.createElement('div');
+ item.className = 'ev-item';
+
+ const info = document.createElement('div');
+ const titleEl = document.createElement('strong');
+ titleEl.textContent = evt.title;
+ info.appendChild(titleEl);
+
+ if (evt.desc != null && evt.desc !== '') {
+ const descEl = document.createElement('p');
+ descEl.textContent = evt.desc;
+ descEl.style.cssText = 'margin:4px 0 0;font-size:0.85rem;color:#555;';
+ info.appendChild(descEl);
+ }
+
+ const actions = document.createElement('div');
+ actions.style.cssText = 'display:flex;gap:6px;';
+
+ const editBtn = document.createElement('button');
+ editBtn.textContent = 'Edit';
+ editBtn.className = 'ev-btn';
+ editBtn.addEventListener('click', () => startEdit(evt.id));
+
+ const deleteBtn = document.createElement('button');
+ deleteBtn.textContent = 'Delete';
+ deleteBtn.className = 'ev-btn ev-btn-delete';
+ deleteBtn.addEventListener('click', () => askDelete(evt.id));
+
+ actions.appendChild(editBtn);
+ actions.appendChild(deleteBtn);
+ item.appendChild(info);
+ item.appendChild(actions);
+ return item;
+ });
+
+ for (const item of items) list.appendChild(item);
+}
+
+
+// ============================================================
+// DELETE EVENT
+// ============================================================
+
+function askDelete(id) {
+ deletingId = id;
+ document.getElementById('ev-confirm').style.display = 'flex';
+}
+
+function confirmDelete() {
+ if (!deletingId || !currentDateKey) return;
+
+ events[currentDateKey] = events[currentDateKey].filter(e => e.id !== deletingId);
+ rebuildEvents();
+ saveToStorage();
+ renderEventList();
+ refreshIndicators();
+
+ deletingId = null;
+ document.getElementById('ev-confirm').style.display = 'none';
+}
+
+function cancelDelete() {
+ deletingId = null;
+ document.getElementById('ev-confirm').style.display = 'none';
+}
+
+
+// ============================================================
+// VISUAL INDICATORS ON DAY CELLS
+// ============================================================
+
+function refreshIndicators() {
+ for (const cell of document.querySelectorAll('.days')) {
+ const existing = cell.querySelector('.ev-dot');
+ if (existing !== null) existing.remove();
+
+ const key = getKeyFromCell(cell);
+ if (key === null) continue;
+
+ const evts = getEventsForDate(key);
+ if (evts.length > 0) {
+ const dot = document.createElement('span');
+ dot.className = 'ev-dot';
+ dot.textContent = evts.length <= 9 ? String(evts.length) : '9+';
+ dot.title = `${evts.length} event${evts.length !== 1 ? 's' : ''}`;
+ if (isHoliday(key)) dot.title += ' — Public Holiday 🎉';
+ cell.appendChild(dot);
+ }
+ }
+}
+
+
+// ============================================================
+// MODAL HTML
+// ============================================================
+
+function buildModal() {
+ const overlay = document.createElement('div');
+ overlay.id = 'ev-overlay';
+ overlay.style.cssText = 'display:none;position:fixed;inset:0;background:rgba(0,0,0,0.45);z-index:1000;align-items:center;justify-content:center;';
+
+ overlay.innerHTML = `
+
+ `;
+ document.body.appendChild(overlay);
+
+ const confirmDiv = document.createElement('div');
+ confirmDiv.id = 'ev-confirm';
+ confirmDiv.style.cssText = 'display:none;position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:1100;align-items:center;justify-content:center;';
+
+ confirmDiv.innerHTML = `
+
+
Delete this event? This cannot be undone.
+
+
+
+
+
+ `;
+ document.body.appendChild(confirmDiv);
+
+ document.getElementById('ev-close-btn').addEventListener('click', closeModal);
+ document.getElementById('ev-cancel-btn').addEventListener('click', closeModal);
+ document.getElementById('ev-save-btn').addEventListener('click', saveEvent);
+ document.getElementById('ev-confirm-ok').addEventListener('click', confirmDelete);
+ document.getElementById('ev-confirm-cancel').addEventListener('click', cancelDelete);
+
+ overlay.addEventListener('click', (e) => { if (e.target === overlay) closeModal(); });
+ document.getElementById('ev-title').addEventListener('keydown', (e) => { if (e.key === 'Enter') saveEvent(); });
+}
+
+
+// ============================================================
+// DAY CELL CLICK LISTENERS
+// ============================================================
+
+function attachDayListeners() {
+ const section = document.querySelector('.week-section');
+ if (!section) return;
+
+ // Event delegation — one listener on the parent instead of one per cell
+ section.addEventListener('click', (e) => {
+ const cell = e.target.closest('.days');
+ if (!cell) return;
+ const key = getKeyFromCell(cell);
+ if (key !== null) openModal(key);
+ });
+}
+
+
+// ============================================================
+// INIT
+// ============================================================
+
+function init() {
+ buildModal();
+ attachDayListeners();
+ refreshIndicators();
+
+ const monthSel = document.getElementById('month-selector');
+ const yearSel = document.getElementById('year-selector');
+
+ if (monthSel) monthSel.addEventListener('change', refreshIndicators);
+ if (yearSel) yearSel.addEventListener('change', () => {
+ refreshIndicators();
+ fetchHolidays(Number(yearSel.value));
+ });
+
+ const prevBtn = document.querySelector('.previous-month');
+ const nextBtn = document.querySelector('.next-month');
+
+ // Small delay lets the calendar re-render before we refresh dots
+ if (prevBtn) prevBtn.addEventListener('click', () => setTimeout(refreshIndicators, 50));
+ if (nextBtn) nextBtn.addEventListener('click', () => setTimeout(refreshIndicators, 50));
+
+ fetchHolidays(new Date().getFullYear());
+}
+
+if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', init);
+} else {
+ init();
+}
diff --git a/Project-Days-Calendar/generate-ical.mjs b/Project-Days-Calendar/generate-ical.mjs
index 1a09ee4..c3779bd 100644
--- a/Project-Days-Calendar/generate-ical.mjs
+++ b/Project-Days-Calendar/generate-ical.mjs
@@ -4,4 +4,4 @@
import { getGreeting } from "./common.mjs";
import daysData from "./days.json" with { type: "json" };
-console.log(`{getGreeting()} - there are ${daysData.length} known days`);
+console.log(`${getGreeting()} - there are ${daysData.length} known days`);
diff --git a/Project-Days-Calendar/index.html b/Project-Days-Calendar/index.html
index adb969c..7e44381 100644
--- a/Project-Days-Calendar/index.html
+++ b/Project-Days-Calendar/index.html
@@ -1,9 +1,38 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+ Calendar
+
+
+
+
+
+
+
+
+
+
diff --git a/Project-Days-Calendar/package-lock.json b/Project-Days-Calendar/package-lock.json
new file mode 100644
index 0000000..749b799
--- /dev/null
+++ b/Project-Days-Calendar/package-lock.json
@@ -0,0 +1,577 @@
+{
+ "name": "project-days-calendar",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "project-days-calendar",
+ "version": "1.0.0",
+ "dependencies": {
+ "jsdom": "^28.1.0"
+ }
+ },
+ "node_modules/@acemir/cssom": {
+ "version": "0.9.31",
+ "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz",
+ "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==",
+ "license": "MIT"
+ },
+ "node_modules/@asamuzakjp/css-color": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.0.1.tgz",
+ "integrity": "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==",
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/css-calc": "^3.1.1",
+ "@csstools/css-color-parser": "^4.0.2",
+ "@csstools/css-parser-algorithms": "^4.0.0",
+ "@csstools/css-tokenizer": "^4.0.0",
+ "lru-cache": "^11.2.6"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@asamuzakjp/dom-selector": {
+ "version": "6.8.1",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz",
+ "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/nwsapi": "^2.3.9",
+ "bidi-js": "^1.0.3",
+ "css-tree": "^3.1.0",
+ "is-potential-custom-element-name": "^1.0.1",
+ "lru-cache": "^11.2.6"
+ }
+ },
+ "node_modules/@asamuzakjp/nwsapi": {
+ "version": "2.3.9",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
+ "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
+ "license": "MIT"
+ },
+ "node_modules/@bramus/specificity": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
+ "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==",
+ "license": "MIT",
+ "dependencies": {
+ "css-tree": "^3.0.0"
+ },
+ "bin": {
+ "specificity": "bin/cli.js"
+ }
+ },
+ "node_modules/@csstools/color-helpers": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz",
+ "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/@csstools/css-calc": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz",
+ "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^4.0.0",
+ "@csstools/css-tokenizer": "^4.0.0"
+ }
+ },
+ "node_modules/@csstools/css-color-parser": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz",
+ "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/color-helpers": "^6.0.2",
+ "@csstools/css-calc": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^4.0.0",
+ "@csstools/css-tokenizer": "^4.0.0"
+ }
+ },
+ "node_modules/@csstools/css-parser-algorithms": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz",
+ "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@csstools/css-tokenizer": "^4.0.0"
+ }
+ },
+ "node_modules/@csstools/css-syntax-patches-for-csstree": {
+ "version": "1.0.29",
+ "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.29.tgz",
+ "integrity": "sha512-jx9GjkkP5YHuTmko2eWAvpPnb0mB4mGRr2U7XwVNwevm8nlpobZEVk+GNmiYMk2VuA75v+plfXWyroWKmICZXg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0"
+ },
+ "node_modules/@csstools/css-tokenizer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz",
+ "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/@exodus/bytes": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz",
+ "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==",
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "@noble/hashes": "^1.8.0 || ^2.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@noble/hashes": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/bidi-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
+ "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
+ "license": "MIT",
+ "dependencies": {
+ "require-from-string": "^2.0.2"
+ }
+ },
+ "node_modules/css-tree": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
+ "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==",
+ "license": "MIT",
+ "dependencies": {
+ "mdn-data": "2.27.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
+ }
+ },
+ "node_modules/cssstyle": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-6.2.0.tgz",
+ "integrity": "sha512-Fm5NvhYathRnXNVndkUsCCuR63DCLVVwGOOwQw782coXFi5HhkXdu289l59HlXZBawsyNccXfWRYvLzcDCdDig==",
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/css-color": "^5.0.1",
+ "@csstools/css-syntax-patches-for-csstree": "^1.0.28",
+ "css-tree": "^3.1.0",
+ "lru-cache": "^11.2.6"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/data-urls": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
+ "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-mimetype": "^5.0.0",
+ "whatwg-url": "^16.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decimal.js": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
+ "license": "MIT"
+ },
+ "node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/html-encoding-sniffer": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
+ "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==",
+ "license": "MIT",
+ "dependencies": {
+ "@exodus/bytes": "^1.6.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "license": "MIT"
+ },
+ "node_modules/jsdom": {
+ "version": "28.1.0",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-28.1.0.tgz",
+ "integrity": "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==",
+ "license": "MIT",
+ "dependencies": {
+ "@acemir/cssom": "^0.9.31",
+ "@asamuzakjp/dom-selector": "^6.8.1",
+ "@bramus/specificity": "^2.4.2",
+ "@exodus/bytes": "^1.11.0",
+ "cssstyle": "^6.0.1",
+ "data-urls": "^7.0.0",
+ "decimal.js": "^10.6.0",
+ "html-encoding-sniffer": "^6.0.0",
+ "http-proxy-agent": "^7.0.2",
+ "https-proxy-agent": "^7.0.6",
+ "is-potential-custom-element-name": "^1.0.1",
+ "parse5": "^8.0.0",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^6.0.0",
+ "undici": "^7.21.0",
+ "w3c-xmlserializer": "^5.0.0",
+ "webidl-conversions": "^8.0.1",
+ "whatwg-mimetype": "^5.0.0",
+ "whatwg-url": "^16.0.0",
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "canvas": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "11.2.6",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz",
+ "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/mdn-data": {
+ "version": "2.27.1",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
+ "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
+ "license": "CC0-1.0"
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/parse5": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz",
+ "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==",
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/saxes": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+ "license": "ISC",
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=v12.22.7"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "license": "MIT"
+ },
+ "node_modules/tldts": {
+ "version": "7.0.24",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.24.tgz",
+ "integrity": "sha512-1r6vQTTt1rUiJkI5vX7KG8PR342Ru/5Oh13kEQP2SMbRSZpOey9SrBe27IDxkoWulx8ShWu4K6C0BkctP8Z1bQ==",
+ "license": "MIT",
+ "dependencies": {
+ "tldts-core": "^7.0.24"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
+ }
+ },
+ "node_modules/tldts-core": {
+ "version": "7.0.24",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.24.tgz",
+ "integrity": "sha512-pj7yygNMoMRqG7ML2SDQ0xNIOfN3IBDUcPVM2Sg6hP96oFNN2nqnzHreT3z9xLq85IWJyNTvD38O002DdOrPMw==",
+ "license": "MIT"
+ },
+ "node_modules/tough-cookie": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz",
+ "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tldts": "^7.0.5"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
+ "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/undici": {
+ "version": "7.22.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz",
+ "integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.18.1"
+ }
+ },
+ "node_modules/w3c-xmlserializer": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
+ "license": "MIT",
+ "dependencies": {
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
+ "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
+ "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "16.0.1",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz",
+ "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==",
+ "license": "MIT",
+ "dependencies": {
+ "@exodus/bytes": "^1.11.0",
+ "tr46": "^6.0.0",
+ "webidl-conversions": "^8.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/xml-name-validator": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "license": "MIT"
+ }
+ }
+}
diff --git a/Project-Days-Calendar/package.json b/Project-Days-Calendar/package.json
index f09d47b..1f95d90 100644
--- a/Project-Days-Calendar/package.json
+++ b/Project-Days-Calendar/package.json
@@ -3,5 +3,8 @@
"version": "1.0.0",
"scripts": {
"test": "node common.test.mjs"
+ },
+ "dependencies": {
+ "jsdom": "^28.1.0"
}
}
diff --git a/Project-Days-Calendar/script.js b/Project-Days-Calendar/script.js
new file mode 100644
index 0000000..24b7506
--- /dev/null
+++ b/Project-Days-Calendar/script.js
@@ -0,0 +1,236 @@
+const months = [
+ "January", "February", "March", "April",
+ "May", "June", "July", "August",
+ "September", "October", "November", "December"
+];
+const daysOfTheWeek = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
+
+const root = document.querySelector(".week-section");
+
+const currentDate = new Date();
+const currentDay = currentDate.getDay();
+let currentMonth = new Date().getMonth();
+const currentYear = new Date().getFullYear();
+
+
+renderMonth(currentMonth)
+renderYear(currentYear)
+renderCalendar(currentMonth, currentYear);
+const holidays = downloadHolidays()
+console.log(holidays)
+
+console.log(getNthWeekdayOfMonth(2026, 4, 6, "second"));
+
+
+export function createEvent({ title, date }) {
+ return {
+ id: crypto.randomUUID(),
+ title: title.trim(),
+ date, // "YYYY-MM-DD"
+ allDay: true,
+ createdAt: new Date().toISOString(),
+ description: "",
+
+ };
+}
+
+export function saveEvent(event) {
+ const events = JSON.parse(localStorage.getItem("calendarEvents")) || [];
+ events.push(event);
+ localStorage.setItem("calendarEvents", JSON.stringify(events));
+}
+
+export function getAllEvents() {
+ return JSON.parse(localStorage.getItem("calendarEvents")) || [];
+}
+function getNthWeekdayOfMonth(year, monthIndex, weekdayIndex, occurrence) {
+ const firstDayIndex = new Date(year, monthIndex, 1).getDay();
+ const daysInMonth = new Date(year, monthIndex + 1, 0).getDate();
+ const occurrenceMap = {
+ first: 0,
+ second: 1,
+ third: 2
+ };
+
+ let offset = weekdayIndex - firstDayIndex;
+ if (offset < 0) offset += 7;
+
+ if (occurrence !== "last") {
+ const weekOffset = occurrenceMap[occurrence] * 7;
+ return 1 + offset + weekOffset;
+ }
+
+ const lastDayIndex = new Date(year, monthIndex, daysInMonth).getDay();
+ let reverseOffset = lastDayIndex - weekdayIndex;
+ if (reverseOffset < 0) reverseOffset += 7;
+
+ return daysInMonth - reverseOffset;
+}
+
+async function downloadHolidays() {
+ try {
+ const response = await fetch("./days.json");
+ if (!response.ok) {
+ throw new Error("Failed to fetch holidays");
+ }
+ const holidays = await response.json();
+ return holidays;
+ } catch (error) {
+ console.error("Error fetching holidays:", error);
+ return [];
+ }
+}
+
+
+export function renderCalendar(month, year) {
+ root.innerHTML = "";
+ document.querySelector("h1").textContent = months[month];
+
+ for (let i = 0; i < daysOfTheWeek.length; i++) {
+ const dayOfTheWeek = document.createElement("div");
+ const header = document.createElement("h2");
+ header.classList.add("dayOfTheWeek")
+ header.textContent = daysOfTheWeek[i];
+ root.appendChild(dayOfTheWeek);
+ dayOfTheWeek.appendChild(header);
+ }
+ const firstDayIndex = new Date(year, month, 1).getDay();
+ const daysInMonth = new Date(year, month + 1, 0).getDate();
+ const prevMonthLastDay = new Date(year, month, 0).getDate();
+ for (let i = firstDayIndex; i > 0; i--) {
+ const dayDiv = document.createElement("div");
+ dayDiv.textContent = prevMonthLastDay - i + 1;
+ dayDiv.classList.add('fade')
+ root.appendChild(dayDiv)
+ }
+ for (let day = 1; day <= daysInMonth; day++) {
+ const dayCard = renderDay(day);
+ if (day == currentDate.getDate() && year == currentDate.getFullYear() && month == currentDate.getMonth()) {
+ dayCard.classList.add("today");
+ }
+ root.appendChild(dayCard);
+ }
+
+ const nextMonthStartDay = 7 - new Date(year, month + 1, 0).getDay();
+ for (let i = 1; i < nextMonthStartDay; i++) {
+ const dayDiv = document.createElement("div");
+ dayDiv.textContent = i;
+ dayDiv.classList.add('fade')
+ root.appendChild(dayDiv)
+
+ }
+}
+function deleteEvent(eventId) {
+ let events = getAllEvents();
+ events = events.filter(event => event.id !== eventId);
+ localStorage.setItem("calendarEvents", JSON.stringify(events));
+ renderCalendar(currentMonth, document.querySelector("#year-selector").value);
+}
+export function renderDay(dayNumber) {
+ const dayCard = document.createElement("div");
+ const list = document.createElement("ul");
+ const events = getAllEvents();
+ events.forEach(event => {
+ if (event.date === `${document.querySelector("#year-selector").value}-${String(currentMonth + 1).padStart(2, "0")}-${String(dayNumber).padStart(2, "0")}`) {
+ renderEvents(event)
+ }
+ }
+ );
+
+function renderEvents(event) {
+ const eventTitle = document.createElement("li");
+ const deleteBtn = document.createElement("button");
+ deleteBtn.textContent = "x";
+ deleteBtn.classList.add("delete-event-btn");
+ deleteBtn.addEventListener("click", (e) => {
+ e.stopPropagation();
+ deleteEvent(event.id);
+ });
+ const eventTitleText = document.createElement("p");
+ eventTitleText.textContent = event.title;
+ eventTitle.appendChild(eventTitleText);
+ eventTitle.classList.add("event-item");
+ eventTitle.appendChild(deleteBtn);
+ list.appendChild(eventTitle);
+ return eventTitle;
+}
+
+
+ dayCard.classList.add("days");
+ dayCard.textContent = dayNumber;
+ dayCard.appendChild(list);
+ dayCard.addEventListener("click", () => {
+ const selectedYear = document.querySelector("#year-selector").value;
+ const selectedDate = new Date(selectedYear, currentMonth, dayNumber);
+
+ const year = selectedDate.getFullYear();
+ const month = String(selectedDate.getMonth() + 1).padStart(2, "0");
+ const day = String(selectedDate.getDate()).padStart(2, "0");
+
+ const isoDate = `${year}-${month}-${day}`;
+ const title = prompt(`Event title for ${isoDate}:`);
+ if (title) {
+ const event = createEvent({
+ title,
+ date: isoDate,
+ description: "",
+ });
+ saveEvent(event);
+ alert(`Event saved for ${isoDate}`);
+ renderCalendar(currentMonth, selectedYear);
+ renderEvents(event);
+ }
+
+
+ })
+ root.appendChild(dayCard);
+ return dayCard;
+}
+export function renderMonth(month) {
+ const shownMonth = document.querySelector("h1");
+ shownMonth.textContent = months[month];
+}
+export function renderYear(selectedYear) {
+ const select = document.querySelector("#year-selector");
+
+ for (let year = 1950; year <= 2050; year++) {
+ const option = document.createElement("option");
+ option.value = year;
+ option.textContent = year;
+
+ if (year === selectedYear) {
+ option.selected = true;
+ }
+
+ select.appendChild(option);
+ }
+ select.onchange = (event) => {
+ const selectedYear = event.target.value;
+ console.log(selectedYear)
+ const selectedMonth = months.indexOf(document.querySelector("h1").textContent)
+ console.log(months.indexOf(document.querySelector("h1").textContent))
+ renderCalendar(selectedMonth, selectedYear)
+ };
+}
+ const nextBtn = document.querySelector(".next-month");
+ nextBtn.addEventListener("click", () => {
+ currentMonth++;
+ const selectedYear = document.querySelector("#year-selector").value;
+ if (currentMonth > 11) {
+ currentMonth = 0;
+ }
+
+ renderCalendar(currentMonth, selectedYear);
+ });
+
+const previousMonth = document.querySelector(".previous-month");
+previousMonth.addEventListener("click", () => {
+ currentMonth--;
+ const selectedYear = document.querySelector("#year-selector").value;
+
+ if (currentMonth < 0) {
+ currentMonth = 11;
+ }
+
+ renderCalendar(currentMonth, selectedYear);
+});
\ No newline at end of file
diff --git a/Project-Days-Calendar/script.test.js b/Project-Days-Calendar/script.test.js
new file mode 100644
index 0000000..0026650
--- /dev/null
+++ b/Project-Days-Calendar/script.test.js
@@ -0,0 +1,85 @@
+const makeElement = () => ({
+ textContent: '',
+ innerHTML: '',
+ value: '',
+ selected: false,
+ classList: { add: () => {} },
+ appendChild: () => {},
+ addEventListener: () => {},
+ onchange: null,
+})
+
+global.document = {
+ querySelector: () => makeElement(),
+ createElement: () => makeElement(),
+};
+
+const { renderMonth, renderYear, renderDay } = await import('./script.js')
+
+text('renderMonth set January for index 0', () => {
+ const h1 = makeElement();
+ document.querySelector = () => h1;
+ renderMonth(0);
+
+ expect (h1.textContent).toBe('January');
+});
+
+text('renderMonth sets December for index 11', () => {
+ const h1 = makeElement();
+ document.querySelector = () => h1;
+ renderMonth(11);
+
+ expect(h1.textContent).toBe('December');
+});
+
+text('renderDay sets correct day number', () => {
+ // Call renderDay with 15 and check the returned element has 15 as text
+ const day = renderDay(15);
+ expect(day.textContent).toBe(15);
+});
+
+text('renderDay sets day 1', () => {
+ const day = renderDay(1);
+ expect(day.textContent).toBe(1)
+});
+
+// --- RenderYear test --- //
+// renderYear receives a year (like 2026) and create a dropdown
+// with every year from 1950 to 2050 (that's 101 options).
+// It also marks the received year as the selected one
+
+test('renderYear creates 101 options from 1950 to 2050', () => {
+ // We track every element that gets created so we can count them
+ const created = [];
+ document.createElement = () => {
+ const el = makeElement();
+ created.push(el);
+ return el;
+ };
+ document.querySelector = () => makeElement();
+
+ renderYear(2026);
+
+ // There should be exactly 101 options (2050 - 1950 + 1 = 101)
+ expect(created.length).toBe(101);
+
+ // The first option should be 1950 and the last should be 2050
+ expect(created[0].value).toBe(1950);
+ expect(created[100].value).toBe(2050);
+});
+
+test('renderYear marks the selected year', () => {
+ const created = [];
+ document.createElement = () => {
+ const el = makeElement();
+ created.push(el);
+ return el;
+ };
+ document.querySelector = () => makeElement();
+
+ renderYear(2000);
+
+ // Find the option where selected is true - it should be year 2000
+ const selected = created.find(el => el.selected === true);
+ expect(selected.value).toBe(2000);
+});
\ No newline at end of file
diff --git a/Project-Days-Calendar/style.css b/Project-Days-Calendar/style.css
new file mode 100644
index 0000000..c086768
--- /dev/null
+++ b/Project-Days-Calendar/style.css
@@ -0,0 +1,102 @@
+main {
+ display: grid;
+}
+.week-section {
+ display: grid;
+ grid-template-columns: repeat(7, 1fr);
+ gap: 1rem;
+}
+#month {
+ width: 160px;
+}
+h1 {
+ margin: 0.2rem;
+ padding: 0;
+ margin-block-start: 0;
+ margin-block-end: 0;
+ text-align: center;
+}
+h2 {
+ font-size: medium;
+}
+.dayOfTheWeek {
+ margin: 0.5rem;
+ text-align: center;
+}
+.fade {
+ text-align: center;
+ border: solid 1px;
+ margin: 5px;
+ padding-block: 1px;
+ padding-inline: 6px;
+ color: gray;
+ border-radius: 0.2rem;
+}
+.month-carousel{
+ display: flex;
+ flex-direction: row;
+}
+#header{
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+}
+.days {
+ margin: 5px;
+ text-align: center;
+ border: solid 1px;
+ padding-block: 1px;
+ border-radius: 0.2rem;
+}
+.today {
+ background-color: green;
+ color: white;
+ font-weight: bold ;
+ border-radius: 0.2rem;
+ border: 0px;
+}
+ul {
+ display: flex;
+ flex-direction: column;
+ list-style-type: disc;
+ margin-block-start: 0;
+ margin-block-end: 0;
+ padding-inline-start: 0px;
+ unicode-bidi: isolate;
+}
+
+.event-item {
+ background: #e3f2fd;
+ font-size: 0.75rem;
+ margin: 2px 0;
+ padding: 2px 5px;
+ display: flex;
+ justify-content: space-between;
+ border-radius: 3px;
+ text-align: center;
+}
+.previous-month .next-month {
+ margin: 5px;
+}
+
+.footer p {
+ text-align: center;
+}
+
+day-card {
+ border: 1px solid #ddd;
+ min-height: 80px;
+ padding: 5px;
+ cursor: pointer;
+ display: flex;
+ flex-direction: column;
+}
+
+
+.delete-event-btn {
+ background: none;
+ border: none;
+ color: red;
+ cursor: pointer;
+ font-weight: bold;
+}
\ No newline at end of file
diff --git a/Project-Spell-Checker/common.mjs b/Project-Spell-Checker/common.mjs
index 3ec09d0..117996d 100644
--- a/Project-Spell-Checker/common.mjs
+++ b/Project-Spell-Checker/common.mjs
@@ -1,3 +1,17 @@
import words from "./words.json" with { type: "json" };
-export const getDictionarySize = () => words.length;
+const dictionarySet = new Set(
+ words.map(word => word.toLowerCase())
+);
+export const getDictionarySize = () => dictionarySet.size;
+
+export function checkWordsInDictionary(wordsArray) {
+ return wordsArray.map(word => ({
+ word,
+ isCorrect: dictionarySet.has(word.toLowerCase())
+ }));
+}
+
+export function addWordToDictionary(word) {
+ dictionarySet.add(word.toLowerCase());
+}
\ No newline at end of file
diff --git a/Project-Spell-Checker/common.test.mjs b/Project-Spell-Checker/common.test.mjs
index f813ac3..6ad910f 100644
--- a/Project-Spell-Checker/common.test.mjs
+++ b/Project-Spell-Checker/common.test.mjs
@@ -1,7 +1,69 @@
-import { getDictionarySize } from "./common.mjs";
+import { getDictionarySize, checkWordsInDictionary, addWordToDictionary } from "./common.mjs";
+import { renderResult, splitTheText } from "./script.mjs";
+
import assert from "node:assert";
import test from "node:test";
test("Dictionary size is correct", () => {
assert.equal(getDictionarySize(), 856);
});
+test("Known words are marked as correct", () => {
+ const result = checkWordsInDictionary(["he", "go", "island"]);
+
+ result.forEach(item => {
+ assert.equal(item.isCorrect, true);
+ });
+});
+test("Unknown word is marked as incorrect", () => {
+ const result = checkWordsInDictionary(["hello"]);
+
+ assert.equal(result[0].isCorrect, false);
+});
+test("Case-insensitive check", () => {
+ const result = checkWordsInDictionary(["He", "GO", "IsLaNd"]);
+ result.forEach(item => {
+ assert.equal(item.isCorrect, true);
+ });
+});
+
+test("Mixed known and unknown words", () => {
+ const result = checkWordsInDictionary(["he", "hello", "go", "world"]);
+ assert.equal(result[0].isCorrect, true);
+ assert.equal(result[1].isCorrect, false);
+ assert.equal(result[2].isCorrect, true);
+ assert.equal(result[3].isCorrect, false);
+});
+
+test("Adding a new word to the dictionary", () => {
+ const newWord = "testword";
+ addWordToDictionary(newWord);
+ const result = checkWordsInDictionary([newWord]);
+ assert.equal(result[0].isCorrect, true);
+});
+
+test("Adding a new word is case-insensitive", () => {
+ const newWord = "AnotherTestWord";
+ addWordToDictionary(newWord);
+ const result = checkWordsInDictionary([newWord.toLowerCase()]);
+ assert.equal(result[0].isCorrect, true);
+});
+
+test("Adding a word that already exists does not change the dictionary size", () => {
+ const initialSize = getDictionarySize();
+ addWordToDictionary("he");
+ const newSize = getDictionarySize();
+ assert.equal(initialSize, newSize);
+});
+test("Splitting text into words", () => {
+ const text = "Hello, world! This is a test.";
+ const expectedWords = ["Hello", "world", "This", "is", "a", "test"];
+ const result = splitTheText(text);
+ assert.deepEqual(result, expectedWords);
+});
+
+test("Splitting text with multiple spaces and punctuation", () => {
+ const text = "Hello... world! This is a test.";
+ const expectedWords = ["Hello", "world", "This", "is", "a", "test"];
+ const result = splitTheText(text);
+ assert.deepEqual(result, expectedWords);
+});
diff --git a/Project-Spell-Checker/icon/tick_16010939.png b/Project-Spell-Checker/icon/tick_16010939.png
new file mode 100644
index 0000000..f8eec45
Binary files /dev/null and b/Project-Spell-Checker/icon/tick_16010939.png differ
diff --git a/Project-Spell-Checker/index.html b/Project-Spell-Checker/index.html
index da64ce5..173a0d2 100644
--- a/Project-Spell-Checker/index.html
+++ b/Project-Spell-Checker/index.html
@@ -1,8 +1,32 @@
-
-
+
+
-
+
+
+
+
+ Spell Checker
+
+
+
+
-
\ No newline at end of file
+
diff --git a/Project-Spell-Checker/package-lock.json b/Project-Spell-Checker/package-lock.json
new file mode 100644
index 0000000..3657aba
--- /dev/null
+++ b/Project-Spell-Checker/package-lock.json
@@ -0,0 +1,628 @@
+{
+ "name": "project-spell-checker",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "project-spell-checker",
+ "version": "1.0.0",
+ "devDependencies": {
+ "gh-pages": "^6.3.0"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/array-union": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/async": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
+ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/commander": {
+ "version": "13.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
+ "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/commondir": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
+ "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/dir-glob": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+ "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-type": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/email-addresses": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/email-addresses/-/email-addresses-5.0.0.tgz",
+ "integrity": "sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fastq": {
+ "version": "1.20.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
+ "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/filename-reserved-regex": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz",
+ "integrity": "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/filenamify": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.3.0.tgz",
+ "integrity": "sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "filename-reserved-regex": "^2.0.0",
+ "strip-outer": "^1.0.1",
+ "trim-repeated": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-cache-dir": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz",
+ "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "commondir": "^1.0.1",
+ "make-dir": "^3.0.2",
+ "pkg-dir": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/avajs/find-cache-dir?sponsor=1"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/fs-extra": {
+ "version": "11.3.3",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz",
+ "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=14.14"
+ }
+ },
+ "node_modules/gh-pages": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/gh-pages/-/gh-pages-6.3.0.tgz",
+ "integrity": "sha512-Ot5lU6jK0Eb+sszG8pciXdjMXdBJ5wODvgjR+imihTqsUWF2K6dJ9HST55lgqcs8wWcw6o6wAsUzfcYRhJPXbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "async": "^3.2.4",
+ "commander": "^13.0.0",
+ "email-addresses": "^5.0.0",
+ "filenamify": "^4.3.0",
+ "find-cache-dir": "^3.3.1",
+ "fs-extra": "^11.1.1",
+ "globby": "^11.1.0"
+ },
+ "bin": {
+ "gh-pages": "bin/gh-pages.js",
+ "gh-pages-clean": "bin/gh-pages-clean.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/globby": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
+ "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-union": "^2.1.0",
+ "dir-glob": "^3.0.1",
+ "fast-glob": "^3.2.9",
+ "ignore": "^5.2.0",
+ "merge2": "^1.4.1",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/make-dir": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+ "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-type": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pkg-dir": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
+ "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "find-up": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/reusify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/slash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-outer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz",
+ "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "escape-string-regexp": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/trim-repeated": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz",
+ "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "escape-string-regexp": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ }
+ }
+}
diff --git a/Project-Spell-Checker/package.json b/Project-Spell-Checker/package.json
index a002ba1..3a8097a 100644
--- a/Project-Spell-Checker/package.json
+++ b/Project-Spell-Checker/package.json
@@ -1,7 +1,13 @@
{
"name": "project-spell-checker",
"version": "1.0.0",
+ "type": "module",
"scripts": {
- "test": "node common.test.mjs"
+ "test": "node --test",
+ "predeploy": "npm run build",
+ "deploy": "gh-pages -d ."
+ },
+ "devDependencies": {
+ "gh-pages": "^6.3.0"
}
-}
+}
\ No newline at end of file
diff --git a/Project-Spell-Checker/script.mjs b/Project-Spell-Checker/script.mjs
index 11950af..bb852fd 100644
--- a/Project-Spell-Checker/script.mjs
+++ b/Project-Spell-Checker/script.mjs
@@ -4,8 +4,82 @@
// Note that when running locally, in order to open a web page which uses modules, you must serve the directory over HTTP e.g. with https://www.npmjs.com/package/http-server
// You can't open the index.html file using a file:// URL.
-import getDictionarySize from "./common.mjs";
+import { getDictionarySize, checkWordsInDictionary, addWordToDictionary } from "./common.mjs";
-window.onload = function() {
- document.querySelector("body").innerText = `There are ${getDictionarySize()} words in the Basic English dictionary`;
+export function initUI() {
+ const main = document.querySelector("main");
+ window.onload = function () {
+ const title = this.document.createElement("h1");
+ title.textContent = "Basic English Spell Checker";
+ main.append(title);
+ const section = document.createElement("section");
+ main.append(section);
+ const label = document.createElement("label");
+ label.htmlFor = "text-input";
+ label.textContent = "Enter text to check:";
+ section.append(label);
+ const textarea = document.createElement("textarea");
+ textarea.id = "text-input";
+ textarea.placeholder = "Put you text here if you want to check it";
+ section.append(textarea);
+ const button = document.createElement("button");
+ button.classList.add("check");
+ button.textContent = "check";
+ section.append(button);
+ const resultDiv = document.createElement("div");
+ section.append(resultDiv);
+
+ textarea.addEventListener("input", () => {
+ resultDiv.innerHTML = "";
+ });
+ button.addEventListener("click", () => {
+
+ const wordsArray = splitTheText(textarea.value);
+ const checkedWords = checkWordsInDictionary(wordsArray);
+ renderResult(checkedWords, resultDiv)
+ })
+ };
+}
+function customDictionarySize() {
+ const main = document.querySelector("main");
+ if (main.querySelector("p")) {
+ const paragraph = main.querySelector("p");
+ paragraph.textContent = `There are ${getDictionarySize()} words in the Basic English dictionary`;
+ } else {
+ const paragraph = document.createElement("p");
+ paragraph.textContent = `There are ${getDictionarySize()} words in the Basic English dictionary`;
+ main.append(paragraph);
+ }
+}
+export function renderResult(checkedWords, container) {
+ container.innerHTML = "";
+
+ checkedWords.forEach(item => {
+ const span = document.createElement("span");
+ span.textContent = item.word;
+
+
+ if (!item.isCorrect && item.word[0] !== item.word[0].toUpperCase()) {
+ span.classList.add("misspelled");
+ const addButton = document.createElement("button")
+ addButton.textContent = "add to dictionary";
+ addButton.addEventListener("click", () => {
+ addWordToDictionary(item.word);
+ customDictionarySize()
+ span.style.textDecoration = "none";
+ addButton.remove();
+ })
+ span.append(addButton);
+ }
+
+ container.append(span);
+ container.append(" ");
+ });
}
+
+export function splitTheText(text) {
+ return text
+ .replace(/[^\w\s]/g, "")
+ .trim()
+ .split(/\s+/);
+}
\ No newline at end of file
diff --git a/Project-Spell-Checker/style.css b/Project-Spell-Checker/style.css
new file mode 100644
index 0000000..5a38262
--- /dev/null
+++ b/Project-Spell-Checker/style.css
@@ -0,0 +1,86 @@
+
+:root {
+ --border-color: #444;
+ --error-color: #b00020;
+ --background-light: #f8f8f8;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ font-family: system-ui, sans-serif;
+ line-height: 1.5;
+ background: #ffffff;
+ color: #111;
+}
+
+main {
+ max-width: 60rem;
+ margin: 2rem auto;
+ padding: 0 1.5rem;
+}
+
+
+h1 {
+ margin-bottom: 1.5rem;
+ text-align: center;
+}
+
+h2 {
+ margin-top: 2rem;
+ font-size: 1.2rem;
+}
+
+label {
+ display: block;
+ margin-bottom: 0.5rem;
+ font-weight: 600;
+}
+
+
+textarea,
+section>div {
+ width: 100%;
+ min-height: 10rem;
+ padding: 0.75rem;
+ font: inherit;
+ border: 0.1rem solid var(--border-color);
+ border-radius: 0.4rem;
+ background: var(--background-light);
+}
+
+button {
+ margin: 1rem 0;
+ padding: 0.5rem 1rem;
+ font: inherit;
+ border: 0.1rem solid var(--border-color);
+ background: white;
+ cursor: pointer;
+}
+
+button:hover {
+ background: #f0f0f0;
+}
+
+span button {
+ margin-left: 0.5rem;
+ margin-top: 0;
+ font-size: 0.85em;
+}
+
+
+.misspelled {
+ text-decoration: underline;
+ text-decoration-color: var(--error-color);
+}
+
+footer {
+ margin-top: 3rem;
+ padding: 1.5rem;
+ text-align: center;
+ font-size: 0.9rem;
+ border-top: 0.1rem solid var(--border-color);
+}
\ No newline at end of file