mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
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
173 lines
5.3 KiB
TypeScript
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,
|
|
}
|
|
}
|