← Public packages
@kentcdodds/invisible-software
Invisible software: a phone-only step-through demo plus a chart of AI agent benchmark progress.
src/c/vc-finder.js
147 lines · 8.1 KB · JavaScript// 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', ` <div class="toast" id="toast"><div class="s-ic"><img src="assets/stripe.svg"></div><div id="toastText"></div></div>
<div class="app" id="app">
<div class="app-head"><img class="av-img" src="assets/victoria.jpg"><div><h1>Victoria's Chair</h1><p>Thursday, Oct 15 · <span id="openCount">4 open</span></p></div><span class="badge">Today</span></div>
<div class="slots"><div class="slot" id="s0">9:00</div><div class="slot" id="s1">10:30</div><div class="slot" id="s2">1:00</div><div class="slot" id="s3">3:30</div></div>
<div class="sec-title">Client finder <small id="selCount">5 clients</small></div>
<div class="search"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="M20 20l-3.5-3.5"/></svg>Search clients</div>
<div class="chips"><div class="chip" id="chipDue">Due for a cut</div><div class="chip">All</div><div class="chip">Regulars</div></div>
<div class="card list" id="list"></div>
<div class="sec-title" id="secText">Thursday text <small>iMessage</small></div>
<div class="card tpl">
<label><span class="imsg"><svg width="13" height="13" viewBox="0 0 24 24" fill="#fff"><path d="M12 3C6.5 3 2 6.8 2 11.5c0 2.6 1.4 5 3.7 6.5L5 21.5l4.1-2.2c.9.2 1.9.3 2.9.3 5.5 0 10-3.8 10-8.5S17.5 3 12 3z"/></svg></span>Message template</label>
<div class="area" id="area"></div>
<div class="preview"><div class="to"><img class="av-img" src="assets/client.jpg">Preview for Marcus</div><div class="ibub" id="bub"></div></div>
<div class="btn" id="send"></div>
</div>
<div class="sec-title" id="secBook">Thursday bookings <span class="live"><i></i>Live</span></div>
<div class="card" id="bookings"></div>
</div>
`)
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 ? `<img class="av-img" src="${c.img}">` : `<span class="mono" style="background:${c.mono[1]}">${c.mono[0]}</span>`
const list = root.querySelector('#list')
list.innerHTML = clients.map((c, i) => `<div class="crow" data-i="${i}">${av(c)}<div><div class="nm">${c.n}</div><div class="meta">${c.m}</div></div>${c.due ? '<span class="due">Due</span>' : '<span style="margin-left:auto"></span>'}<span class="ck">${icon.check}</span></div>`).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) => `<div class="brow" id="b${i}"><span class="tm">${tm}</span><span class="who"><span class="open">Open</span></span><span class="st"></span></div>`).join('') + `<div class="totals"><span>Deposits collected</span><b id="tot">$0</b></div>`
// 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('<span class="var">{first}</span>', '<span class="var">{depositLink}</span>') + add.slice(0, n) + (typing ? '<span class="caret"></span>' : '')
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 = `<span class="spin"></span>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 = '<span class="open">Open</span>'; st.innerHTML = '' }
else { who.innerHTML = av(clients[ci]) + clients[ci].n; st.innerHTML = state === 'pending' ? '<span class="pay pending">Deposit pending</span>' : `<span class="pay paid">${icon.check}Paid $20</span>` }
}
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 = `<b>${clients[slots[i][1]].n}</b> paid a $20 deposit<br><span style="opacity:.7">Stripe · just now</span>`
} 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
})
}