fase(24): onboarding and first-run experience
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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`
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user