// Victoria's client-finder app (slide 3 opening, reused by slide 2). Local time = slide time - t0, capped at tMax. window.mountFinder = ({ root, onTime }, body, t0 = 0, tMax = Infinity) => { body.insertAdjacentHTML('afterbegin', `

Victoria's Chair

Thursday, Oct 15 · 4 open

Today
9:00
10:30
1:00
3:30
Client finder 5 clients
Due for a cut
All
Regulars
Thursday text iMessage
Preview for Marcus
Thursday bookings Live
`) const clients = [ { n: 'Marcus Reed', img: 'assets/client.jpg', m: '5 wks ago · usual 4 wks', due: true }, { n: 'Jay Ortiz', mono: ['JO', '#7c8cff'], m: '6 wks ago · usual 4 wks', due: true }, { n: 'Sam Lee', mono: ['SL', '#c48bd6'], m: '2 wks ago · usual 5 wks', due: false }, { n: 'Theo Park', mono: ['TP', '#f0a14b'], m: '4 wks ago · usual 3 wks', due: true }, { n: 'Andre Wells', mono: ['AW', '#4cb782'], m: '7 wks ago · usual 5 wks', due: true }, ] const av = (c) => c.img ? `` : `${c.mono[0]}` const list = root.querySelector('#list') list.innerHTML = clients.map((c, i) => `
${av(c)}
${c.n}
${c.m}
${c.due ? 'Due' : ''}${icon.check}
`).join('') const rows = [...list.children] const app = root.querySelector('#app') const area = root.querySelector('#area') const bub = root.querySelector('#bub') const send = root.querySelector('#send') // The template already carries the per-client Stripe link; in 3.2 Victoria only types a thank-you. const depositLinks = { Marcus: 'buy.stripe.com/7sY3cK9aQ', Jay: 'buy.stripe.com/3fA8xP2kR', Theo: 'buy.stripe.com/9bE1mT6wZ', Andre: 'buy.stripe.com/5kQ4vH0dN' } const base = (name, link) => `Hey ${name}, it's Victoria. I've got Thursday openings at 9, 10:30, 1 and 3:30. Reply with a time to grab one. $20 deposit: ${link}` const add = ' Thanks!' const bookings = root.querySelector('#bookings') const slots = [['9:00', null], ['10:30', 0], ['1:00', 1], ['3:30', 3]] bookings.innerHTML = slots.map(([tm], i) => `
${tm}Open
`).join('') + `
Deposits collected$0
` // booked at, paid at const events = { 1: [15.4, 16.8], 2: [17.6, 19.0], 3: [19.8, 21.2] } onTime((T) => { const t = Math.min(T - t0, tMax) // filter const filtered = t >= 2.25 root.querySelector('#chipDue').classList.toggle('on', filtered) root.querySelector('#chipDue').innerHTML = (filtered ? icon.check : '') + 'Due for a cut' rows.forEach((r, i) => { const c = clients[i] if (!c.due) { const p = ease(prog(t, 2.35, 2.75)) r.style.height = 62 * (1 - p) + 'px' r.style.opacity = 1 - p r.style.borderTopWidth = p >= 1 ? '0' : '' } const di = clients.slice(0, i).filter((x) => x.due).length const badge = r.querySelector('.due') if (badge) { const p = ease(prog(t, 2.8 + di * 0.18, 3.2 + di * 0.18)) badge.style.opacity = p badge.style.transform = `scale(${0.6 + 0.4 * p})` } const ck = r.querySelector('.ck') const on = c.due && t >= 3.9 + di * 0.25 ck.classList.toggle('on', on) ck.style.color = on ? '#fff' : 'transparent' }) const sel = clients.filter((c, i) => c.due && t >= 3.9 + clients.slice(0, i).filter((x) => x.due).length * 0.25).length root.querySelector('#selCount').textContent = sel ? `${sel} selected` : filtered ? '4 due' : '5 clients' // template typing const n = Math.floor(clamp((t - 8.6) * 10, 0, add.length)) const typing = t >= 8.3 && t < 11.6 area.classList.toggle('focus', typing) area.innerHTML = base('{first}', '{depositLink}') + add.slice(0, n) + (typing ? '' : '') bub.textContent = base('Marcus', depositLinks.Marcus) + add.slice(0, n) // send if (t < 11.95) send.innerHTML = `Send to 4 clients` else if (t < 12.9) send.innerHTML = `Sending…` else send.innerHTML = `${icon.check}Sent to 4 clients` send.classList.toggle('sent', t >= 12.9) // bookings let total = 0, taken = 0 slots.forEach(([, ci], i) => { const row = root.querySelector('#b' + i) const who = row.querySelector('.who'), st = row.querySelector('.st') const ev = events[i] const state = !ev || t < ev[0] ? 'open' : t < ev[1] ? 'pending' : 'paid' if (row.__s !== state) { row.__s = state if (state === 'open') { who.innerHTML = 'Open'; st.innerHTML = '' } else { who.innerHTML = av(clients[ci]) + clients[ci].n; st.innerHTML = state === 'pending' ? 'Deposit pending' : `` } } if (ev) { const f = prog(t, ev[1], ev[1] + 1.2) row.style.background = f > 0 && f < 1 ? `rgb(182 240 204 / ${1 - f})` : '' const b = prog(t, ev[0], ev[0] + 0.4) who.style.opacity = state === 'open' ? 1 : b } if (state !== 'open') taken++ if (state === 'paid') total += 20 root.querySelector('#s' + i).classList.toggle('taken', state !== 'open') }) root.querySelector('#tot').textContent = '$' + total root.querySelector('#openCount').textContent = `${4 - taken} open` // toast per payment const toast = root.querySelector('#toast') let shown = null for (const [i, ev] of Object.entries(events)) if (t >= ev[1] && t < ev[1] + 1.9) shown = [i, t - ev[1]] if (shown) { const [i, dt] = shown const inP = ease(clamp(dt / 0.35)), outP = clamp((dt - 1.55) / 0.35) toast.style.opacity = inP * (1 - outP) toast.style.transform = `translateY(${(1 - inP) * -30 - outP * 20}px)` root.querySelector('#toastText').innerHTML = `${clients[slots[i][1]].n} paid a $20 deposit
Stripe · just now` } else toast.style.opacity = 0 // scroll const yText = root.querySelector('#secText').offsetTop - 6 const yBook = root.querySelector('#secBook').offsetTop - 120 const y = yText * easeInOut(prog(t, 6.8, 7.7)) + (yBook - yText) * easeInOut(prog(t, 13.4, 14.4)) app.style.transform = `translateY(${-y}px)` // send tap position follows the button on screen (screen y = 114 safari-top + body offset) const sb = send.offsetTop - y + 114 + 26 const tapSend = root.querySelector('#tapSend') if (tapSend) tapSend.style.top = sb + 'px' // page load progress const pr = root.querySelector('.url-progress') const lp = prog(t, 0, 0.8) pr.style.width = lp * 100 + '%' pr.style.opacity = lp >= 1 ? 0 : 1 }) }