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)
|
- [x] 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
|
- [x] 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!")
|
- [x] 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.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 { apiFetch } from '@/lib/api'
|
||||||
import { queryClient } from '@/lib/queryClient'
|
import { queryClient } from '@/lib/queryClient'
|
||||||
|
|
||||||
|
const TOTAL_STEPS = 3
|
||||||
|
|
||||||
export function Setup() {
|
export function Setup() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [step, setStep] = useState(1)
|
const [step, setStep] = useState(1)
|
||||||
@@ -14,15 +16,12 @@ export function Setup() {
|
|||||||
const [email, setEmail] = useState('')
|
const [email, setEmail] = useState('')
|
||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
const [orgName, setOrgName] = useState('')
|
const [orgName, setOrgName] = useState('')
|
||||||
|
const [firstUrl, setFirstUrl] = useState('')
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
async function handleSubmit(e: React.FormEvent) {
|
async function handleCreateAccount(e: React.FormEvent) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (step === 1) {
|
|
||||||
setStep(2)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setError(null)
|
setError(null)
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
@@ -31,7 +30,7 @@ export function Setup() {
|
|||||||
body: JSON.stringify({ name, email, password, organizationName: orgName }),
|
body: JSON.stringify({ name, email, password, organizationName: orgName }),
|
||||||
})
|
})
|
||||||
await queryClient.invalidateQueries({ queryKey: ['auth'] })
|
await queryClient.invalidateQueries({ queryKey: ['auth'] })
|
||||||
navigate('/')
|
setStep(3)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Setup failed')
|
setError(err instanceof Error ? err.message : 'Setup failed')
|
||||||
} finally {
|
} 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 (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-screen bg-background">
|
<div className="flex items-center justify-center min-h-screen bg-background">
|
||||||
<Card className="w-full max-w-md">
|
<Card className="w-full max-w-md">
|
||||||
<CardHeader className="text-center">
|
<CardHeader className="text-center">
|
||||||
<CardTitle className="text-2xl font-bold">Welcome to ABE</CardTitle>
|
<CardTitle className="text-2xl font-bold">Welcome to ABE</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>{stepTitle[step]}</CardDescription>
|
||||||
{step === 1 ? 'Create your admin account' : 'Name your organization'}
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex gap-1 mb-6">
|
<div className="flex gap-1 mb-6">
|
||||||
{[1, 2].map(s => (
|
{Array.from({ length: TOTAL_STEPS }).map((_, i) => (
|
||||||
<div
|
<div
|
||||||
key={s}
|
key={i}
|
||||||
className={`h-1 flex-1 rounded-full transition-colors ${
|
className={`h-1 flex-1 rounded-full transition-colors ${
|
||||||
s <= step ? 'bg-primary' : 'bg-muted'
|
i + 1 <= step ? 'bg-primary' : 'bg-muted'
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</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">
|
<div className="space-y-1.5">
|
||||||
<Label htmlFor="name">Full Name</Label>
|
<Label htmlFor="name">Full Name</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -95,12 +119,12 @@ export function Setup() {
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button type="submit" className="w-full">
|
<Button type="submit" className="w-full">Continue</Button>
|
||||||
Continue
|
</form>
|
||||||
</Button>
|
)}
|
||||||
</>
|
|
||||||
) : (
|
{step === 2 && (
|
||||||
<>
|
<form onSubmit={(e) => void handleCreateAccount(e)} className="space-y-4">
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label htmlFor="org">Organization Name</Label>
|
<Label htmlFor="org">Organization Name</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -112,25 +136,44 @@ export function Setup() {
|
|||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{error && (
|
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||||
<p className="text-sm text-destructive">{error}</p>
|
|
||||||
)}
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button type="button" variant="outline" className="flex-1" onClick={() => setStep(1)}>
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
className="flex-1"
|
|
||||||
onClick={() => setStep(1)}
|
|
||||||
>
|
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" className="flex-1" disabled={loading}>
|
<Button type="submit" className="flex-1" disabled={loading}>
|
||||||
{loading ? 'Creating...' : 'Create Account'}
|
{loading ? 'Creating…' : 'Create Account'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</form>
|
</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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user