diff --git a/beingone/app.js b/beingone/app.js index c66c196..9f171ae 100644 --- a/beingone/app.js +++ b/beingone/app.js @@ -3,26 +3,27 @@ const $ = (s, r = document) => r.querySelector(s); const $$ = (s, r = document) => Array.from(r.querySelectorAll(s)); const cap = (s) => s.charAt(0).toUpperCase() + s.slice(1); + const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches; - /* made-up sample people; nothing here is a real person */ + /* made-up sample people; nothing here is a real person (g: w = woman, m = man) */ const PEOPLE = [ - { n: 'Mia', a: 28, c: 'Sydney', l: ['beach', 'coffee', 'travel'], t: 'a' }, - { n: 'Arjun', a: 31, c: 'Melbourne', l: ['footy', 'cooking', 'music'], t: 'b' }, - { n: 'Hannah', a: 27, c: 'Brisbane', l: ['beach', 'hiking', 'pets'], t: 'c' }, - { n: 'Wei', a: 30, c: 'Perth', l: ['coffee', 'travel', 'hiking'], t: 'd' }, - { n: 'Sana', a: 29, c: 'Adelaide', l: ['cooking', 'music', 'pets'], t: 'a' }, - { n: 'Tom', a: 33, c: 'Hobart', l: ['hiking', 'footy', 'coffee'], t: 'b' }, - { n: 'Priya', a: 26, c: 'Sydney', l: ['music', 'cooking', 'beach'], t: 'c' }, - { n: 'Jack', a: 35, c: 'Melbourne', l: ['footy', 'beach', 'pets'], t: 'd' }, - { n: 'Elena', a: 32, c: 'Brisbane', l: ['travel', 'coffee', 'music'], t: 'a' }, - { n: 'Dev', a: 34, c: 'Perth', l: ['footy', 'cooking', 'hiking'], t: 'b' }, - { n: 'Grace', a: 30, c: 'Canberra', l: ['hiking', 'coffee', 'pets'], t: 'c' }, - { n: 'Sam', a: 28, c: 'Darwin', l: ['beach', 'footy', 'travel'], t: 'd' }, - { n: 'Lena', a: 38, c: 'Sydney', l: ['travel', 'music', 'pets'], t: 'a' }, - { n: 'Omar', a: 29, c: 'Melbourne', l: ['coffee', 'cooking', 'hiking'], t: 'c' } + { n: 'Mia', g: 'w', a: 28, c: 'Sydney', l: ['beach', 'coffee', 'travel'], t: 'a' }, + { n: 'Arjun', g: 'm', a: 31, c: 'Melbourne', l: ['footy', 'cooking', 'music'], t: 'b' }, + { n: 'Hannah', g: 'w', a: 27, c: 'Brisbane', l: ['beach', 'hiking', 'pets'], t: 'c' }, + { n: 'Wei', g: 'm', a: 30, c: 'Perth', l: ['coffee', 'travel', 'hiking'], t: 'd' }, + { n: 'Sana', g: 'w', a: 29, c: 'Adelaide', l: ['cooking', 'music', 'pets'], t: 'a' }, + { n: 'Tom', g: 'm', a: 33, c: 'Hobart', l: ['hiking', 'footy', 'coffee'], t: 'b' }, + { n: 'Priya', g: 'w', a: 26, c: 'Sydney', l: ['music', 'cooking', 'beach'], t: 'c' }, + { n: 'Jack', g: 'm', a: 35, c: 'Melbourne', l: ['footy', 'beach', 'pets'], t: 'd' }, + { n: 'Elena', g: 'w', a: 32, c: 'Brisbane', l: ['travel', 'coffee', 'music'], t: 'a' }, + { n: 'Dev', g: 'm', a: 34, c: 'Perth', l: ['footy', 'cooking', 'hiking'], t: 'b' }, + { n: 'Grace', g: 'w', a: 30, c: 'Canberra', l: ['hiking', 'coffee', 'pets'], t: 'c' }, + { n: 'Sam', g: 'm', a: 28, c: 'Darwin', l: ['beach', 'footy', 'travel'], t: 'd' }, + { n: 'Lena', g: 'w', a: 38, c: 'Sydney', l: ['travel', 'music', 'pets'], t: 'a' }, + { n: 'Omar', g: 'm', a: 29, c: 'Melbourne', l: ['coffee', 'cooking', 'hiking'], t: 'c' } ]; - /* ---------- one reveal, once ---------- */ + /* ---------- scroll reveal, once ---------- */ const revealEls = $$('[data-reveal]'); if ('IntersectionObserver' in window) { const io = new IntersectionObserver((es) => es.forEach((e) => { @@ -33,11 +34,22 @@ revealEls.forEach((el) => el.classList.add('is-in')); } - /* ---------- match finder ---------- */ - const st = { city: 'Anywhere', from: 25, to: 40, likes: new Set(), sent: new Set() }; - const selCity = $('#f-city'), from = $('#f-from'), to = $('#f-to'); - const oFrom = $('#o-from'), oTo = $('#o-to'), results = $('#results'); - const status = $('#finder-status'), mapCity = $('#map-city'); + /* ---------- mobile menu ---------- */ + const menu = $('.nav__menu'), links = $('#nav-links'); + menu.addEventListener('click', () => { + const open = links.classList.toggle('is-open'); + menu.setAttribute('aria-expanded', String(open)); + menu.textContent = open ? 'Close' : 'Menu'; + }); + links.addEventListener('click', (e) => { + if (e.target.closest('a')) { links.classList.remove('is-open'); menu.setAttribute('aria-expanded', 'false'); menu.textContent = 'Menu'; } + }); + + /* ---------- match finder (the quick search bar drives it too) ---------- */ + const st = { city: 'Anywhere', gender: 'any', from: 25, to: 40, likes: new Set(), sent: new Set() }; + const from = $('#f-from'), to = $('#f-to'), oFrom = $('#o-from'), oTo = $('#o-to'); + const results = $('#results'), status = $('#finder-status'), mapCity = $('#map-city'); + const qFor = $('#q-for'), qAge = $('#q-age'), qCity = $('#q-city'); const pins = $$('.pin'); function scoreOf(p) { @@ -55,34 +67,57 @@ return '
' + '
' + '

' + p.n + ', ' + p.a + '

' + p.c + '

' + - '
' + r.score + 'match
' + + '
' + r.score + 'match
' + '
' + '' + '' + '
'; } + /* scores count up from 55 to their value, once per render */ + function countUp(el) { + const to = +el.dataset.to; + if (reduced) { el.textContent = to; return; } + const t0 = performance.now(), dur = 900, start = 55; + el.textContent = start; + (function step(now) { + const k = Math.min(1, (now - t0) / dur), e = 1 - Math.pow(1 - k, 3); + el.textContent = Math.round(start + (to - start) * e); + if (k < 1) requestAnimationFrame(step); + })(t0); + } + function renderFinder() { const pool = PEOPLE - .filter((p) => (st.city === 'Anywhere' || p.c === st.city) && p.a >= st.from && p.a <= st.to) + .filter((p) => (st.city === 'Anywhere' || p.c === st.city) && (st.gender === 'any' || p.g === st.gender) && p.a >= st.from && p.a <= st.to) .map((p) => ({ p, score: scoreOf(p) })) .sort((x, y) => y.score - x.score || x.p.n.localeCompare(y.p.n)); const top = pool.slice(0, 4); results.innerHTML = top.length ? top.map(tile).join('') : '

No one fits yet. Widen the ages or try another city.

'; - const where = st.city === 'Anywhere' ? 'all of Australia' : st.city; + $$('.score b', results).forEach(countUp); status.textContent = top.length ? top.length + ' of ' + pool.length + ' shown.' : ''; mapCity.textContent = st.city === 'Anywhere' ? 'All of Australia' : st.city; - selCity.value = st.city; + qCity.value = st.city; pins.forEach((b) => b.setAttribute('aria-pressed', String(b.dataset.city === st.city))); } + function setAges(a, b) { + st.from = a; st.to = b; from.value = a; to.value = b; + oFrom.textContent = a; oTo.textContent = b; + } + pins.forEach((b) => b.addEventListener('click', () => { st.city = st.city === b.dataset.city ? 'Anywhere' : b.dataset.city; + if (st.city === b.dataset.city && !reduced) { + const ping = document.createElement('span'); + ping.className = 'ping'; + ping.addEventListener('animationend', () => ping.remove(), { once: true }); + b.appendChild(ping); + } renderFinder(); })); - selCity.addEventListener('change', () => { st.city = selCity.value; renderFinder(); }); from.addEventListener('input', () => { st.from = +from.value; if (st.from > st.to) { st.to = st.from; to.value = st.to; } @@ -106,15 +141,31 @@ b.setAttribute('aria-pressed', String(on)); b.textContent = on ? 'Interest sent' : 'Send interest'; }); + + /* quick search bar: each choice updates the finder, the button also scrolls to the people */ + function applyQuick() { + st.gender = qFor.value; + st.city = qCity.value; + const [a, b] = qAge.value.split('-').map(Number); + setAges(a, b); + renderFinder(); + } + [qFor, qAge, qCity].forEach((el) => el.addEventListener('change', applyQuick)); + $('#quick').addEventListener('submit', (e) => { + e.preventDefault(); + applyQuick(); + results.scrollIntoView({ behavior: reduced ? 'auto' : 'smooth', block: 'center' }); + }); renderFinder(); /* ---------- the four checks ---------- */ - const rows = $$('[data-check]'), vStatus = $('#v-status'), vText = $('#v-status-text'); + const rows = $$('[data-check]'), vStatus = $('#v-status'), vText = $('#v-status-text'), vBar = $('#v-progress'); const timers = new Map(); function updateChecks() { const n = rows.filter((r) => r.classList.contains('is-done')).length; vStatus.dataset.state = n === rows.length ? 'done' : 'idle'; vText.textContent = n === rows.length ? 'Verified' : n ? n + ' of 4 passed' : 'Not verified yet'; + vBar.style.setProperty('--p', n / rows.length); } function runCheck(row) { if (row.classList.contains('is-done') || timers.has(row)) return; @@ -135,7 +186,7 @@ rows.forEach((r) => { r.classList.remove('is-done'); const btn = $('[data-run]', r); - btn.hidden = false; btn.disabled = false; btn.removeAttribute('aria-busy'); btn.textContent = 'Run check'; + btn.hidden = false; btn.disabled = false; btn.removeAttribute('aria-busy'); btn.textContent = 'Run'; $('.passed', r).hidden = true; }); updateChecks(); diff --git a/beingone/base.css b/beingone/base.css index be87418..4631450 100644 --- a/beingone/base.css +++ b/beingone/base.css @@ -2,12 +2,10 @@ html, body { overflow-x: clip; } html { scrollbar-color: var(--color-muted) var(--color-paper); scroll-padding-top: var(--space-3xl); } body { margin: 0; background: var(--color-paper); color: var(--color-ink); font-family: var(--font-body); font-size: var(--text-md); line-height: 1.55; -webkit-font-smoothing: antialiased; caret-color: var(--color-accent); } -::selection { background: color-mix(in oklab, var(--color-accent) 45%, transparent); color: var(--color-card-ink); } +::selection { background: color-mix(in oklab, var(--color-accent) 22%, transparent); color: var(--color-ink); } :focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; } a { color: inherit; text-decoration-thickness: 1px; text-underline-offset: 0.16em; } -h1, h2, h3 { margin: 0; font-family: var(--font-display); font-weight: var(--display-weight, 700); font-synthesis: none; letter-spacing: var(--tracking-display); line-height: 1.05; overflow-wrap: break-word; min-width: 0; text-wrap: balance; } -h1 { max-width: 20ch; font-size: var(--text-display); } h2 { max-width: 30ch; font-size: var(--text-section); } h3 { max-width: 40ch; font-size: var(--text-title); line-height: 1.2; } -.lead { font-size: var(--text-lead); line-height: 1.45; max-width: 52ch; } +h1, h2, h3 { margin: 0; font-family: var(--font-display); font-synthesis: none; overflow-wrap: break-word; min-width: 0; text-wrap: balance; } p { margin: 0; text-wrap: pretty; } p:not([class]) { max-width: 65ch; } img, video { max-width: 100%; height: auto; display: block; } @@ -23,5 +21,3 @@ button, input, select, textarea { font: inherit; } @media (max-width: 60rem) { :root { --gutter: 24px; --section-gap-s: 80px; --section-gap: 96px; --section-gap-l: 120px; } } @media (max-width: 40rem) { :root { --gutter: 16px; --section-gap-s: 56px; --section-gap: 72px; --section-gap-l: 96px; --gap-columns: 16px; } } @media (prefers-reduced-motion: reduce) { *:not(.keep-motion), *:not(.keep-motion)::before, *:not(.keep-motion)::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } } -/* photographs: natural vib=12 con=4 */ -[data-slot] > img { filter: saturate(1.06) contrast(1.04); } diff --git a/beingone/images/hero.jpg b/beingone/images/hero.jpg new file mode 100644 index 0000000..d6ea835 Binary files /dev/null and b/beingone/images/hero.jpg differ diff --git a/beingone/index.html b/beingone/index.html index ec316f2..28435fe 100644 --- a/beingone/index.html +++ b/beingone/index.html @@ -7,7 +7,7 @@ - + @@ -15,6 +15,8 @@
- BeingOne -