diff --git a/src/assets/scripts/bundle/details.js b/src/assets/scripts/bundle/details.js new file mode 100644 index 0000000..66705d1 --- /dev/null +++ b/src/assets/scripts/bundle/details.js @@ -0,0 +1,25 @@ +const container = document.querySelector('.details'); +const expandAllButton = container.querySelector('#expandAll'); +const collapseAllButton = container.querySelector('#collapseAll'); +const details = container.querySelectorAll('details'); + +expandAllButton.addEventListener('click', () => { + details.forEach(detail => (detail.open = true)); +}); + +collapseAllButton.addEventListener('click', () => { + details.forEach(detail => (detail.open = false)); +}); + +details.forEach(detail => { + detail.addEventListener('toggle', () => { + const hash = detail.open ? `#${detail.id}` : '#'; + history.pushState(null, null, hash); + }); +}); + +const id = window.location.hash.slice(1); +if (id) { + const detail = container.querySelector(`#${CSS.escape(id)}`); + if (detail) detail.open = true; +} diff --git a/src/assets/scripts/bundle/gallery.js b/src/assets/scripts/bundle/gallery.js new file mode 100644 index 0000000..fcef741 --- /dev/null +++ b/src/assets/scripts/bundle/gallery.js @@ -0,0 +1,20 @@ +const buttons = document.querySelectorAll('button[data-index]'); +const modals = document.querySelectorAll('dialog'); +const closeButtons = document.querySelectorAll('dialog button'); +buttons.forEach((button, index) => { + button.addEventListener('click', () => { + modals[index].showModal(); + }); +}); +closeButtons.forEach((button, index) => { + button.addEventListener('click', () => { + modals[index].close(); + }); +}); +window.addEventListener('click', event => { + modals.forEach(modal => { + if (event.target === modal) { + modal.close(); + } + }); +}); diff --git a/src/assets/scripts/bundle/is-land.js b/src/assets/scripts/bundle/is-land.js new file mode 100644 index 0000000..03547d3 --- /dev/null +++ b/src/assets/scripts/bundle/is-land.js @@ -0,0 +1 @@ +import '@11ty/is-land/is-land'; diff --git a/src/assets/scripts/nav-drawer.js b/src/assets/scripts/bundle/nav-drawer.js similarity index 100% rename from src/assets/scripts/nav-drawer.js rename to src/assets/scripts/bundle/nav-drawer.js diff --git a/src/assets/scripts/theme-toggle.js b/src/assets/scripts/bundle/theme-toggle.js similarity index 78% rename from src/assets/scripts/theme-toggle.js rename to src/assets/scripts/bundle/theme-toggle.js index f97dc06..0f0f3bc 100644 --- a/src/assets/scripts/theme-toggle.js +++ b/src/assets/scripts/bundle/theme-toggle.js @@ -15,7 +15,6 @@ window.onload = () => { return; } - switcher.removeAttribute('hidden'); reflectPreference(); lightThemeToggle.addEventListener('click', () => onClick('light')); @@ -26,21 +25,15 @@ window.onload = () => { }; // sync with system changes -window - .matchMedia('(prefers-color-scheme: dark)') - .addEventListener('change', ({matches: isDark}) => { - theme.value = isDark ? 'dark' : 'light'; - setPreference(); - }); +window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', ({matches: isDark}) => { + theme.value = isDark ? 'dark' : 'light'; + setPreference(); +}); function onClick(themeValue) { theme.value = themeValue; - document - .querySelector('#light-theme-toggle') - .setAttribute('aria-pressed', themeValue === 'light'); - document - .querySelector('#dark-theme-toggle') - .setAttribute('aria-pressed', themeValue === 'dark'); + document.querySelector('#light-theme-toggle').setAttribute('aria-pressed', themeValue === 'light'); + document.querySelector('#dark-theme-toggle').setAttribute('aria-pressed', themeValue === 'dark'); setPreference(); } diff --git a/src/assets/scripts/components/custom-easteregg.js b/src/assets/scripts/components/custom-easteregg.js new file mode 100644 index 0000000..387ad97 --- /dev/null +++ b/src/assets/scripts/components/custom-easteregg.js @@ -0,0 +1,56 @@ +class customEasteregg extends HTMLElement { + constructor() { + super(); + // Initialize with default keywords + this.keywords = ['eleventy', 'excellent']; + // Add any custom keyword passed as an attribute + const customKeyword = this.getAttribute('keyword'); + if (customKeyword) { + this.keywords.push(customKeyword); + } + + this.shape = this.getAttribute('shape') || '⭐️'; + this.particleCount = parseInt(this.getAttribute('particle-count'), 10) || 30; + this.codes = this.keywords.map(keyword => keyword.split('')); + this.indexes = new Array(this.keywords.length).fill(0); + } + + connectedCallback() { + document.addEventListener('keydown', this.handleKeydown.bind(this)); + } + + disconnectedCallback() { + document.removeEventListener('keydown', this.handleKeydown.bind(this)); + } + + handleKeydown(event) { + const key = event.key.toLowerCase(); + this.codes.forEach((code, idx) => { + if (code[this.indexes[idx]] === key) { + this.indexes[idx]++; + if (this.indexes[idx] === code.length) { + this.triggerEffect(this.keywords[idx]); + this.indexes[idx] = 0; // Reset index after triggering + } + } else { + this.indexes[idx] = 0; // Reset index if sequence breaks + } + }); + } + + triggerEffect(keyword) { + console.log(`Hooray ${keyword}!`); + import('https://esm.run/canvas-confetti').then(({default: confetti}) => { + const scalar = 4; + const customShape = confetti.shapeFromText({text: this.shape, scalar}); + + confetti({ + shapes: [customShape], + scalar, + particleCount: this.particleCount + }); + }); + } +} + +customElements.define('custom-easteregg', customEasteregg); diff --git a/src/assets/scripts/components/custom-masonry.js b/src/assets/scripts/components/custom-masonry.js new file mode 100644 index 0000000..5701486 --- /dev/null +++ b/src/assets/scripts/components/custom-masonry.js @@ -0,0 +1,41 @@ +class CustomMasonry extends HTMLElement { + constructor() { + super(); + this.layoutMasonry = this.layoutMasonry.bind(this); + } + + connectedCallback() { + // Defer initial layout to ensure styles are applied + requestAnimationFrame(() => { + this.layoutMasonry(); + window.addEventListener('resize', this.debounceLayout.bind(this, 100)); + }); + } + + disconnectedCallback() { + window.removeEventListener('resize', this.debounceLayout); + } + + debounceLayout(delay) { + clearTimeout(this.timeoutId); + this.timeoutId = setTimeout(this.layoutMasonry, delay); + } + + layoutMasonry() { + const columnCount = getComputedStyle(this).gridTemplateColumns.split(' ').length; + const items = Array.from(this.children); + items.forEach((item, index) => { + item.style.marginTop = '0px'; // Reset before calculation + if (index >= columnCount) { + const previousItem = items[index - columnCount]; + const previousItemBottom = + previousItem.offsetTop + previousItem.offsetHeight + parseFloat(getComputedStyle(this).rowGap); + const currentItemTop = item.offsetTop; + const marginTop = previousItemBottom - currentItemTop; + item.style.marginTop = `${marginTop}px`; + } + }); + } +} + +customElements.define('custom-masonry', CustomMasonry); diff --git a/src/assets/scripts/easteregg.js b/src/assets/scripts/easteregg.js deleted file mode 100644 index a671217..0000000 --- a/src/assets/scripts/easteregg.js +++ /dev/null @@ -1,38 +0,0 @@ -const eleventyCode = ['e', 'l', 'e', 'v', 'e', 'n', 't', 'y']; -const excellentCode = ['e', 'x', 'c', 'e', 'l', 'l', 'e', 'n', 't']; - -let indexEleventy = 0; -let indexExcellent = 0; - -// Trigger confetti if someone enters "eleventy" or "excellent" -document.addEventListener('keydown', event => { - if (eleventyCode[indexEleventy] === event.key.toLowerCase()) { - ++indexEleventy; - } else { - indexEleventy = 0; - } - - if (excellentCode[indexExcellent] === event.key.toLowerCase()) { - ++indexExcellent; - } else { - indexExcellent = 0; - } - - if (indexEleventy === eleventyCode.length || indexExcellent === excellentCode.length) { - console.log('Hooray Eleventy!'); - indexEleventy = 0; - indexExcellent = 0; - import('https://esm.run/canvas-confetti').then(module => { - const confetti = module.default; - const scalar = 4; - const particleCount = 30; - const star = confetti.shapeFromText({text: '⭐️', scalar}); - - confetti({ - shapes: [star], - scalar, - particleCount - }); - }); - } -}); diff --git a/src/assets/scripts/masonry.js b/src/assets/scripts/masonry.js deleted file mode 100644 index 5965d89..0000000 --- a/src/assets/scripts/masonry.js +++ /dev/null @@ -1,44 +0,0 @@ -// ----- masonry fallback if CSS masonry not supported, solution by Ana Tudor: https://codepen.io/thebabydino/pen/yLYppjK - -const supportMasonry = CSS.supports('grid-template-rows', 'masonry'); - -if (!supportMasonry) { - let grids = [...document.querySelectorAll('.grid[data-rows="masonry"]')]; - - if (grids.length && getComputedStyle(grids[0]).gridTemplateRows !== 'masonry') { - grids = grids.map(grid => ({ - _el: grid, - gap: parseFloat(getComputedStyle(grid).rowGap), - items: [...grid.childNodes] - .filter(c => c.nodeType === 1 && +getComputedStyle(c).gridColumnEnd !== -1) - .map(c => ({_el: c})), - ncol: 0 - })); - - function layout() { - grids.forEach(grid => { - let ncol = getComputedStyle(grid._el).gridTemplateColumns.split(' ').length; - if (grid.ncol !== ncol) { - grid.ncol = ncol; - grid.items.forEach(c => c._el.style.removeProperty('margin-block-start')); - if (grid.ncol > 1) { - grid.items.slice(ncol).forEach((c, i) => { - let prev_fin = grid.items[i]._el.getBoundingClientRect().bottom, - curr_ini = c._el.getBoundingClientRect().top; - c._el.style.marginTop = `${prev_fin + grid.gap - curr_ini}px`; - }); - } - } - }); - } - - addEventListener( - 'load', - e => { - layout(); - addEventListener('resize', layout, false); - }, - false - ); - } -}