const $ = (selector, root = document) => root.querySelector(selector); const $$ = (selector, root = document) => [...root.querySelectorAll(selector)]; const params = new URLSearchParams(location.search); const validFilters = ['tutti', 'marketing', 'ai']; const filter = validFilters.includes(params.get('filtro')) ? params.get('filtro') : 'tutti'; const view = params.get('vista') === 'elenco' ? 'elenco' : 'spazio'; function queryFor(state) { const query = new URLSearchParams(); query.set('filtro', state.filter); query.set('vista', state.view); query.set('lavoro', state.id); return query; } if (document.body.dataset.page === 'home') { document.documentElement.classList.add('js'); const disclosure = $('#work-disclosure'); disclosure.open = location.hash === '#lavori'; const catalog = $('#catalog'); const cards = $$('.work-card', catalog); const state = { filter, view, id: params.get('lavoro') || cards[0].dataset.project }; const controls = $('.space-controls'); const previous = $('#previous-work'); const next = $('#next-work'); const visibleCards = () => cards.filter(card => state.filter === 'tutti' || card.dataset.category === state.filter); let layoutFrame = 0; const sizeStage = () => { cancelAnimationFrame(layoutFrame); layoutFrame = requestAnimationFrame(() => { const active = cards.find(card => card.dataset.project === state.id); catalog.style.height = state.view === 'spazio' ? `${active.offsetHeight + 35}px` : ''; }); }; function render(save = false) { const items = visibleCards(); if (!items.some(card => card.dataset.project === state.id)) state.id = items[0].dataset.project; const index = items.findIndex(card => card.dataset.project === state.id); catalog.dataset.view = state.view; $('.catalog-stage').dataset.view = state.view; $$('[data-catalog-project]').forEach(button => { button.hidden = state.filter !== 'tutti' && button.dataset.category !== state.filter; button.setAttribute('aria-pressed', String(button.dataset.catalogProject === state.id)); }); $$('[data-feature-project]').forEach(link => { if (link.dataset.featureProject === state.id && disclosure.open) link.setAttribute('aria-current', 'true'); else link.removeAttribute('aria-current'); }); for (const card of cards) { const place = items.indexOf(card); card.hidden = place < 0; const distance = place - index; card.style.setProperty('--distance', distance); card.style.setProperty('--depth', Math.abs(distance)); card.style.setProperty('--order', 10 - Math.abs(distance)); card.style.setProperty('--visibility', Math.abs(distance) > 1 ? 0 : 1); card.dataset.active = String(distance === 0 && place >= 0); const isOffscreen = state.view === 'spazio' && distance !== 0; if (isOffscreen) card.setAttribute('aria-hidden', 'true'); else card.removeAttribute('aria-hidden'); const link = $('.work-link', card); link.tabIndex = isOffscreen || place < 0 ? -1 : 0; const target = new URL(link.href); target.search = queryFor({ ...state, id: card.dataset.project }).toString(); link.href = target.pathname.split('/').pop() + target.search; } $$('[data-filter]').forEach(button => button.setAttribute('aria-pressed', String(button.dataset.filter === state.filter))); $$('[data-view]').filter(element => element.tagName === 'BUTTON').forEach(button => button.setAttribute('aria-pressed', String(button.dataset.view === state.view))); controls.hidden = state.view !== 'spazio'; previous.disabled = index === 0; next.disabled = index === items.length - 1; const activeName = $('h3', items[index]).textContent; $('#catalog-location').textContent = state.view === 'spazio' ? `Lavori / ${activeName}` : `Lavori / ${state.filter === 'ai' ? 'Produzioni AI' : state.filter === 'marketing' ? 'Marketing' : 'Tutti i progetti'}`; $('#catalog-count').textContent = state.view === 'spazio' ? `Progetto ${index + 1} di ${items.length}` : `${items.length} progetti`; sizeStage(); if (save) history.replaceState(null, '', `${location.pathname}?${queryFor(state)}${location.hash || '#lavori'}`); } const selectOffset = offset => { const items = visibleCards(); const current = items.findIndex(card => card.dataset.project === state.id); const nextIndex = Math.max(0, Math.min(items.length - 1, current + offset)); if (current === nextIndex) return; state.id = items[nextIndex].dataset.project; render(true); }; previous.addEventListener('click', () => selectOffset(-1)); next.addEventListener('click', () => selectOffset(1)); $$('[data-filter]').forEach(button => button.addEventListener('click', () => { state.filter = button.dataset.filter; render(true); })); $$('button[data-view]').forEach(button => button.addEventListener('click', () => { state.view = button.dataset.view; render(true); })); let drag = null; let swallowClick = false; catalog.addEventListener('pointerdown', event => { if (state.view !== 'spazio' || event.button !== 0) return; drag = { x: event.clientX, y: event.clientY, pointer: event.pointerId }; swallowClick = false; }); catalog.addEventListener('pointermove', event => { if (!drag) return; const x = event.clientX - drag.x; const y = event.clientY - drag.y; if (Math.abs(x) > 20 && Math.abs(x) > Math.abs(y) * 1.5) { swallowClick = true; if (!catalog.hasPointerCapture(event.pointerId)) catalog.setPointerCapture(event.pointerId); } }); catalog.addEventListener('pointerup', event => { if (!drag) return; const x = event.clientX - drag.x; const y = event.clientY - drag.y; if (Math.abs(x) > 50 && Math.abs(x) > Math.abs(y) * 1.5) selectOffset(x < 0 ? 1 : -1); drag = null; }); catalog.addEventListener('pointercancel', () => { drag = null; swallowClick = false; }); catalog.addEventListener('dragstart', event => event.preventDefault()); catalog.addEventListener('click', event => { if (swallowClick) { event.preventDefault(); swallowClick = false; } }, true); catalog.addEventListener('keydown', event => { if (state.view !== 'spazio' || !['ArrowLeft', 'ArrowRight'].includes(event.key)) return; event.preventDefault(); selectOffset(event.key === 'ArrowLeft' ? -1 : 1); $('.work-link', cards.find(card => card.dataset.project === state.id)).focus({ preventScroll: true }); }); addEventListener('resize', sizeStage, { passive: true }); addEventListener('popstate', () => { const query = new URLSearchParams(location.search); state.filter = validFilters.includes(query.get('filtro')) ? query.get('filtro') : 'tutti'; state.view = query.get('vista') === 'elenco' ? 'elenco' : 'spazio'; state.id = query.get('lavoro') || cards[0].dataset.project; if (location.hash === '#lavori') disclosure.open = true; render(); }); $$('[data-catalog-project]').forEach(button => button.addEventListener('click', () => { state.id = button.dataset.catalogProject; render(true); })); $$('[data-feature-project]').forEach(link => link.addEventListener('click', event => { event.preventDefault(); disclosure.open = true; state.filter = 'tutti'; state.view = 'spazio'; state.id = link.dataset.featureProject; render(true); history.replaceState(null, '', location.pathname + '?' + queryFor(state) + '#lavori'); $('#lavori').scrollIntoView({behavior: matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth'}); })); $$('a[href="#lavori"]').forEach(link => link.addEventListener('click', () => { disclosure.open = true; render(); })); disclosure.addEventListener('toggle', () => { sizeStage(); showPosition(); }); addEventListener('hashchange', () => { if (location.hash === '#lavori') { disclosure.open = true; render(); } }); render(); document.fonts.ready.then(sizeStage); const sections = $$('section[data-section-name]'); let scrollFrame = 0; function showPosition() { const threshold = $('.site-header').getBoundingClientRect().height + 85; const current = sections.filter(section => (section.id !== 'lavori' || disclosure.open) && section.getBoundingClientRect().top <= threshold).at(-1) || sections[0]; $('#where-text').textContent = `Home / ${current.dataset.sectionName}`; $$('[data-section-link]').forEach(link => { if (link.dataset.sectionLink === current.id) link.setAttribute('aria-current', 'location'); else link.removeAttribute('aria-current'); }); scrollFrame = 0; } addEventListener('scroll', () => { if (!scrollFrame) scrollFrame = requestAnimationFrame(showPosition); }, { passive: true }); showPosition(); const message = $('#contact-message'); const reference = params.get('progetto'); const sourceCard = cards.find(card => card.dataset.project === reference); if (sourceCard && !message.value) message.value = `Ho visto il progetto ${$('h3', sourceCard).textContent} e vorrei parlarti di…`; $('[data-portfolio]').addEventListener('click', () => { if (!message.value) message.value = 'Vorrei ricevere il portfolio e raccontarti la mia attività: '; }); $('#wireframe-contact').addEventListener('submit', event => { event.preventDefault(); $('#contact-result').textContent = 'Il percorso arriva qui: richiesta pronta per il confronto. Questa è una prova del prototipo; nessun dato è stato inviato. Puoi usare il collegamento email per scrivere a Gaia.'; }); } else if (document.body.dataset.page === 'project') { const id = document.body.dataset.projectId; const name = document.body.dataset.projectName || $('h1').textContent; $('#where-text').textContent = `Lavori / ${name}`; $('[data-section-link="lavori"]').setAttribute('aria-current', 'page'); const returnQuery = queryFor({ filter, view, id }); $$('a[href^="/#"], a[href^="/?"], a[href="/"]').forEach(link => { const target = new URL(link.href); target.search = returnQuery.toString(); link.href = '/' + target.search + target.hash; }); $$('[data-return]').forEach(link => link.href = `/?${returnQuery}#lavori`); const contact = $('[data-project-contact]'); const contactQuery = new URLSearchParams(returnQuery); contactQuery.set('progetto', id); contact.href = `/?${contactQuery}#contatto`; const next = $('[data-next-project]'); const nextQuery = new URLSearchParams({ vista: view, filtro: filter }); if (filter !== 'tutti') { next.href = next.dataset.filteredHref; next.firstChild.textContent = next.dataset.filteredName + ' '; } next.href = next.getAttribute('href') + '?' + nextQuery; }