fase(24): onboarding and first-run experience

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
debian
2026-03-08 06:12:11 -04:00
parent 629eafecd8
commit 87b7698ece
2 changed files with 131 additions and 88 deletions

View File

@@ -396,12 +396,12 @@ Spec: `.ralph/specs/phase-18-cli-cicd.md`
---
## Phase 24: Onboarding + First-Run [PENDIENTE]
## Phase 24: Onboarding + First-Run [COMPLETO]
- [ ] 24.1: Detectar first-run en frontend (GET /api/auth/setup-required)
- [ ] 24.2: Wizard multi-step: paso 1 crear admin, paso 2 nombre org, paso 3 "Start your first exploration" con URL input
- [ ] 24.3: Empty states: ilustraciones/mensajes en tablas vacías ("No findings yet. Start an exploration!")
- [ ] 24.4: Commit: `fase(24): onboarding and first-run experience`
- [x] 24.1: Detectar first-run en frontend (GET /api/auth/setup-required)
- [x] 24.2: Wizard multi-step: paso 1 crear admin, paso 2 nombre org, paso 3 "Start your first exploration" con URL input
- [x] 24.3: Empty states: ilustraciones/mensajes en tablas vacías ("No findings yet. Start an exploration!")
- [x] 24.4: Commit: `fase(24): onboarding and first-run experience`
---

View File

@@ -7,6 +7,8 @@ import { Label } from '@/components/ui/label'
import { apiFetch } from '@/lib/api'
import { queryClient } from '@/lib/queryClient'
const TOTAL_STEPS = 3
export function Setup() {
const navigate = useNavigate()
const [step, setStep] = useState(1)
@@ -14,15 +16,12 @@ export function Setup() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [orgName, setOrgName] = useState('')
const [firstUrl, setFirstUrl] = useState('')
const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(false)
async function handleSubmit(e: React.FormEvent) {
async function handleCreateAccount(e: React.FormEvent) {
e.preventDefault()
if (step === 1) {
setStep(2)
return
}
setError(null)
setLoading(true)
try {
@@ -31,7 +30,7 @@ export function Setup() {
body: JSON.stringify({ name, email, password, organizationName: orgName }),
})
await queryClient.invalidateQueries({ queryKey: ['auth'] })
navigate('/')
setStep(3)
} catch (err) {
setError(err instanceof Error ? err.message : 'Setup failed')
} finally {
@@ -39,29 +38,54 @@ export function Setup() {
}
}
function handleStep1(e: React.FormEvent) {
e.preventDefault()
setStep(2)
}
async function handleStartExploration() {
if (!firstUrl.trim()) {
navigate('/')
return
}
try {
const session = await apiFetch('/api/sessions', {
method: 'POST',
body: JSON.stringify({ url: firstUrl.trim() }),
}) as { id: string }
navigate(`/sessions/${session.id}`)
} catch {
navigate('/')
}
}
const stepTitle: Record<number, string> = {
1: 'Create your admin account',
2: 'Name your organization',
3: 'Start your first exploration',
}
return (
<div className="flex items-center justify-center min-h-screen bg-background">
<Card className="w-full max-w-md">
<CardHeader className="text-center">
<CardTitle className="text-2xl font-bold">Welcome to ABE</CardTitle>
<CardDescription>
{step === 1 ? 'Create your admin account' : 'Name your organization'}
</CardDescription>
<CardDescription>{stepTitle[step]}</CardDescription>
</CardHeader>
<CardContent>
<div className="flex gap-1 mb-6">
{[1, 2].map(s => (
{Array.from({ length: TOTAL_STEPS }).map((_, i) => (
<div
key={s}
key={i}
className={`h-1 flex-1 rounded-full transition-colors ${
s <= step ? 'bg-primary' : 'bg-muted'
i + 1 <= step ? 'bg-primary' : 'bg-muted'
}`}
/>
))}
</div>
<form onSubmit={(e) => void handleSubmit(e)} className="space-y-4">
{step === 1 ? (
<>
{step === 1 && (
<form onSubmit={(e) => void handleStep1(e)} className="space-y-4">
<div className="space-y-1.5">
<Label htmlFor="name">Full Name</Label>
<Input
@@ -95,12 +119,12 @@ export function Setup() {
required
/>
</div>
<Button type="submit" className="w-full">
Continue
</Button>
</>
) : (
<>
<Button type="submit" className="w-full">Continue</Button>
</form>
)}
{step === 2 && (
<form onSubmit={(e) => void handleCreateAccount(e)} className="space-y-4">
<div className="space-y-1.5">
<Label htmlFor="org">Organization Name</Label>
<Input
@@ -112,25 +136,44 @@ export function Setup() {
autoFocus
/>
</div>
{error && (
<p className="text-sm text-destructive">{error}</p>
)}
{error && <p className="text-sm text-destructive">{error}</p>}
<div className="flex gap-2">
<Button
type="button"
variant="outline"
className="flex-1"
onClick={() => setStep(1)}
>
<Button type="button" variant="outline" className="flex-1" onClick={() => setStep(1)}>
Back
</Button>
<Button type="submit" className="flex-1" disabled={loading}>
{loading ? 'Creating...' : 'Create Account'}
{loading ? 'Creating' : 'Create Account'}
</Button>
</div>
</>
)}
</form>
)}
{step === 3 && (
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
ABE is ready. Enter a URL to start your first autonomous exploration, or skip to the dashboard.
</p>
<div className="space-y-1.5">
<Label htmlFor="first-url">Website URL</Label>
<Input
id="first-url"
type="url"
value={firstUrl}
onChange={e => setFirstUrl(e.target.value)}
placeholder="https://example.com"
autoFocus
/>
</div>
<div className="flex gap-2">
<Button variant="outline" className="flex-1" onClick={() => navigate('/')}>
Skip
</Button>
<Button className="flex-1" onClick={() => void handleStartExploration()}>
Start Exploring
</Button>
</div>
</div>
)}
</CardContent>
</Card>
</div>