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
123 lines
4.8 KiB
TypeScript
123 lines
4.8 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { Text, Textarea, XStack } from '@hanzo/ui'
|
|
import { Action, Title } from '@hanzo/ui/marketing'
|
|
import { Card } from './card'
|
|
import { phrase } from '@hanzo/i18n'
|
|
import { useI18n } from '@hanzo/i18n/react'
|
|
import { track } from '@hanzo/app/lib/tags'
|
|
import { api } from '@hanzo/app/lib/api'
|
|
import { scope } from '@hanzo/app/lib/session'
|
|
import type { Program } from './program'
|
|
|
|
/** What feedback accepts, and the question each one is filed under. */
|
|
const KINDS = [
|
|
{ id: 'comment', label: phrase('Comment'), ask: phrase('A comment') },
|
|
{ id: 'note', label: phrase('Note'), ask: phrase('A note') },
|
|
{ id: 'message', label: phrase('Message'), ask: phrase('A message') },
|
|
{ id: 'bug', label: phrase('Bug'), ask: phrase('A bug') },
|
|
{ id: 'feature', label: phrase('Feature request'), ask: phrase('A feature request') },
|
|
] as const
|
|
type Kind = (typeof KINDS)[number]['id']
|
|
|
|
/** File one entry: a lead the team reads, then the same words on the waitlist. */
|
|
async function fileNote(kind: Kind, text: string, program: Program | null, email: string, name: string): Promise<'ok' | 'later' | 'no'> {
|
|
const row = KINDS.find((k) => k.id === kind) ?? KINDS[0]
|
|
const need = text.trim().slice(0, 4000)
|
|
const answer = need.slice(0, 2000)
|
|
if (!need) return 'no'
|
|
const headers = { 'Content-Type': 'application/json', ...scope() }
|
|
let lead: Response
|
|
try {
|
|
lead = await fetch(`${api()}/v1/marketing/leads`, {
|
|
method: 'POST',
|
|
headers,
|
|
body: JSON.stringify({
|
|
email,
|
|
name,
|
|
program: program ?? 'beta',
|
|
need,
|
|
answers: [{ question: row.ask, answer }],
|
|
source: 'hanzo.ai/beta',
|
|
}),
|
|
})
|
|
} catch {
|
|
return 'no'
|
|
}
|
|
if (lead.status === 429) return 'later'
|
|
if (!lead.ok) return 'no'
|
|
try {
|
|
const got = await fetch(`${api()}/v1/marketing/beta`, { headers: scope() })
|
|
if (!got.ok) return 'ok'
|
|
const rec = (await got.json()) as { program?: string; answers?: { id?: string; question?: string; answer?: string[] }[] }
|
|
const prior = (Array.isArray(rec.answers) ? rec.answers : []).flatMap((a) =>
|
|
a && a.id && a.question && Array.isArray(a.answer) && a.answer.length ? [{ id: a.id, question: a.question, answer: a.answer }] : [],
|
|
)
|
|
if (prior.length >= 40) return 'ok'
|
|
prior.push({ id: `${kind}-${Date.now()}`, question: row.ask, answer: [answer] })
|
|
await fetch(`${api()}/v1/marketing/beta`, {
|
|
method: 'PUT',
|
|
headers,
|
|
body: JSON.stringify({ program: rec.program || program || 'beta', answers: prior }),
|
|
})
|
|
} catch {
|
|
/* the lead is what the team reads */
|
|
}
|
|
return 'ok'
|
|
}
|
|
|
|
/** Feedback: one kind, the person's own words, sent to the team. */
|
|
export function Feedback({ program, email, name }: { program: Program | null; email: string; name: string }) {
|
|
const [kind, setKind] = useState<Kind>('comment')
|
|
const [text, setText] = useState('')
|
|
const [sending, setSending] = useState(false)
|
|
const [note, setNote] = useState('')
|
|
const { t } = useI18n()
|
|
const row = KINDS.find((k) => k.id === kind) ?? KINDS[0]
|
|
const send = async () => {
|
|
const body = text.trim()
|
|
if (!body || sending) return
|
|
setSending(true)
|
|
setNote('')
|
|
const result = await fileNote(kind, body, program, email, name)
|
|
setSending(false)
|
|
if (result === 'ok') {
|
|
track('beta_feedback_sent', { program: program ?? 'beta', kind })
|
|
setText('')
|
|
setNote(phrase('Sent. The team has it.'))
|
|
return
|
|
}
|
|
setNote(result === 'later' ? phrase('Too many for this hour. Your words are still here.') : phrase('That did not send. Your words are still here.'))
|
|
}
|
|
return (
|
|
<Card gap={16} data-slot="beta-feedback">
|
|
<Title quiet>{t('Tell the team')}</Title>
|
|
<XStack flexWrap="wrap" gap={8} role="radiogroup" aria-label={t('What you are sending')}>
|
|
{KINDS.map((k) => (
|
|
<Action key={k.id} render="button" type="button" role="radio" aria-checked={k.id === kind} fill={k.id === kind} onClick={() => setKind(k.id)}>
|
|
{t(k.label)}
|
|
</Action>
|
|
))}
|
|
</XStack>
|
|
{email ? (
|
|
<Textarea id="beta-feedback-note" rows={4} placeholder={`${t(row.ask)}…`} value={text} onChangeText={(v: string) => setText(v)} />
|
|
) : (
|
|
<Text fontSize="$3" color="var(--muted-foreground)">
|
|
{t('Sign in so this is filed on your account.')}
|
|
</Text>
|
|
)}
|
|
<XStack items="center" gap={14} flexWrap="wrap">
|
|
<Action id="beta-feedback-send" render="button" type="button" fill disabled={!email || sending || text.trim() === ''} onClick={() => void send()}>
|
|
{sending ? t('Sending…') : t('Send')}
|
|
</Action>
|
|
{note ? (
|
|
<Text fontSize="$2" color="var(--muted-foreground)">
|
|
{t(note)}
|
|
</Text>
|
|
) : null}
|
|
</XStack>
|
|
</Card>
|
|
)
|
|
}
|