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 = ` +
+
+

Add Event

+ +
+
+
+ + + + + + + +
+ + +
+
+
+ `; + 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