Files
zeekay 4fd349aa06 beta: the portal says less, and opting in and sharing are one press
Prose that carried no decision or fact is gone: the hero, the questions, the
gate and every step are a heading and at most a line. What scores is said once
(the leaderboard step), and every claim the cloud does not keep is out.

Step 1 is one share card (the member's link, Copy, the device's share sheet on
phones, seven channels as icons, Customize behind a text button) and a quiet
invite-by-email card. Step 3 is one searched, grouped list with one control a
row: On when `opens` says so, Get access when it does not, Requested at once
after the press. Stars and follows moved under the leaderboard as Earn more.

The 1,800-line dashboard is a shell over share, rank, betas and alpha; what
they read is one hook (standing.ts); the questions are data (program.ts).
Dead access, card, hint and referral components are deleted. Every press is
44px, every input too, and a gate walks each screen at 390px.

scripts/brands and scripts/prebuild are TypeScript now; the three marks the
share row draws ship in a JSON only /beta loads. /beta's title and card say
what the page says.

Claude-Session: https://claude.ai/code/session_017vrVus6PPBgP5YRzKV1iqW
2026-10-26 03:29:13 -07:00

173 lines
5.3 KiB
TypeScript

'use client'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { api } from '@hanzo/app/lib/api'
import { scope, superAdmin } from '@hanzo/app/lib/session'
import { mine, updateCode, type Referee } from '@/lib/hanzo/referrals'
import {
act,
ask,
getLocalActions,
getLocalGithub,
getLocalRequestedBetas,
POINTS,
setLocalGithub,
standing,
type BetaAction,
} from '@/lib/hanzo/beta'
import { board, list, listing, type Board, type Listing } from '@/lib/hanzo/board'
/** What one of the member's links brought: signups, and those who went on to pay. */
const PAYING = 20
/**
* Everything the portal's four steps read about the member, once: their link
* and who came through it, their standing on the waitlist, the betas asked for
* and the flags the cloud holds for them, the actions that earned points, and
* the ranked waitlist. `signed` is a full session: nothing is read before it.
*
* The score is the cloud's (GET /v1/marketing/beta answers `score`). Until it
* does, the verified points it keeps plus the referrals it will count; a local
* action, a request for a beta and an application earn nothing.
*/
export function useStanding(signed: boolean) {
const [code, setCode] = useState('')
const [link, setLink] = useState('')
const [referred, setReferred] = useState(0)
const [paying, setPaying] = useState(0)
const [referees, setReferees] = useState<Referee[]>([])
const [place, setPlace] = useState(0)
const [of, setOf] = useState(0)
const [points, setPoints] = useState(0)
const [score, setScore] = useState<number | undefined>()
// What this browser kept while signed out.
const [actions, setActions] = useState<BetaAction[]>(getLocalActions)
const [github, setGithub] = useState(getLocalGithub)
const [asked, setAsked] = useState<string[]>(getLocalRequestedBetas)
const [flags, setFlags] = useState<Record<string, boolean>>({})
const staff = useMemo(() => (signed ? superAdmin() : false), [signed])
const [ranked, setRanked] = useState<Board | null>(null)
const [listed, setListed] = useState<Listing | null>(null)
useEffect(() => {
if (!signed) return
let live = true
mine()
.then((r) => {
if (!live) return
if (r.code) {
setCode(r.code)
setLink(`https://hanzo.ai/beta?ref=${r.code}`)
} else if (r.link) setLink(r.link)
setReferred(r.counts?.total ?? 0)
setPaying(r.counts?.qualified ?? 0)
if (Array.isArray(r.referrals)) setReferees(r.referrals)
})
.catch(() => {})
board().then((b) => live && setRanked(b))
listing().then((l) => live && setListed(l))
return () => {
live = false
}
}, [signed])
// The record and the flags, and any request made signed out, sent now.
useEffect(() => {
if (!signed) return
let live = true
Promise.all([
standing(),
fetch(`${api()}/v1/flag/platform`, { headers: scope() })
.then((r) => (r.ok ? (r.json() as Promise<{ flags?: Record<string, boolean> }>) : null))
.catch(() => null),
]).then(async ([st, f]) => {
if (!live) return
if (f?.flags) setFlags(f.flags)
const kept = getLocalRequestedBetas()
if (!st) {
if (kept.length) await ask(kept)
return
}
setAsked(Array.from(new Set([...st.betas, ...kept])))
if (st.github) setGithub(st.github)
if (st.place > 0) setPlace(st.place)
if (st.of > 0) setOf(st.of)
setPoints(st.points)
setScore(st.score)
if (st.actions.length) setActions(st.actions)
const unsent = kept.filter((id) => !st.betas.includes(id))
if (unsent.length) {
const sent = await ask(unsent)
if (live && sent) setAsked(sent.betas)
}
})
return () => {
live = false
}
}, [signed])
/** Ask for betas: the rows say so at once, and the cloud's answer settles them. */
const request = useCallback(async (ids: string[]) => {
setAsked((now) => Array.from(new Set([...now, ...ids])))
const sent = await ask(ids)
if (sent) setAsked(sent.betas)
}, [])
/** A star to verify or a follow to count; the answer is the cloud's standing. */
const earn = useCallback(async (kind: 'star' | 'follow', target: string, handle?: string) => {
const r = await act(kind, target, handle)
if (r.ok) {
if (r.standing) {
setActions(r.standing.actions)
setPoints(r.standing.points)
setScore(r.standing.score)
if (r.standing.place > 0) setPlace(r.standing.place)
} else setActions(getLocalActions())
}
return r
}, [])
const rename = useCallback(async (next: string) => {
const r = await updateCode(next)
const saved = r.code || next.toUpperCase()
setCode(saved)
setLink(`https://hanzo.ai/beta?ref=${saved}`)
return saved
}, [])
const show = useCallback(async (on: boolean) => {
const l = await list(on)
if (!l) return
setListed(l)
board().then(setRanked)
}, [])
return {
code,
link,
referred,
referees,
rename,
place,
of,
total: score ?? points + referred * POINTS.referralSignup + paying * PAYING,
actions,
earn,
github,
keep: (handle: string) => {
setGithub(handle)
setLocalGithub(handle.trim())
},
asked,
request,
flags,
staff,
ranked,
listed,
show,
}
}