Skip to content

Commit 4c4c1f9

Browse files
dongzhang84claude
andcommitted
feat: simplify onboarding to single-input form (v1.1 step 1)
Replace 3-step wizard with a single product description textarea. POSTs save-and-scan and redirects to /dashboard?scanning=true. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent e2e7d9e commit 4c4c1f9

1 file changed

Lines changed: 35 additions & 254 deletions

File tree

app/onboarding/page.tsx

Lines changed: 35 additions & 254 deletions
Original file line numberDiff line numberDiff line change
@@ -1,284 +1,65 @@
11
'use client'
22

3-
import { useState, KeyboardEvent } from 'react'
4-
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
5-
import { Button } from '@/components/ui/button'
3+
import { useState } from 'react'
64
import { Textarea } from '@/components/ui/textarea'
7-
import { Badge } from '@/components/ui/badge'
8-
import { Input } from '@/components/ui/input'
9-
import { Loader2, X } from 'lucide-react'
10-
11-
type Step = 1 | 2 | 3
5+
import { Button } from '@/components/ui/button'
6+
import { Loader2 } from 'lucide-react'
127

138
export default function OnboardingPage() {
14-
const [currentStep, setCurrentStep] = useState<Step>(1)
159
const [productDescription, setProductDescription] = useState('')
16-
const [targetCustomer, setTargetCustomer] = useState('')
17-
const [keywords, setKeywords] = useState<string[]>([])
18-
const [subreddits, setSubreddits] = useState<string[]>([])
1910
const [loading, setLoading] = useState(false)
20-
const [keywordInput, setKeywordInput] = useState('')
21-
const [subredditInput, setSubredditInput] = useState('')
22-
23-
async function handleGenerateKeywords() {
24-
setLoading(true)
25-
try {
26-
const res = await fetch('/api/onboarding', {
27-
method: 'POST',
28-
headers: { 'Content-Type': 'application/json' },
29-
body: JSON.stringify({
30-
step: 'generate-keywords',
31-
productDescription,
32-
targetCustomer,
33-
}),
34-
})
35-
const data = await res.json()
36-
setKeywords(data.keywords ?? [])
37-
setSubreddits(data.subreddits ?? [])
38-
setCurrentStep(3)
39-
} finally {
40-
setLoading(false)
41-
}
42-
}
4311

44-
async function handleSave() {
12+
async function handleSubmit() {
4513
setLoading(true)
4614
try {
4715
await fetch('/api/onboarding', {
4816
method: 'POST',
4917
headers: { 'Content-Type': 'application/json' },
50-
body: JSON.stringify({
51-
step: 'save',
52-
productDescription,
53-
targetCustomer,
54-
keywords,
55-
subreddits,
56-
}),
18+
body: JSON.stringify({ step: 'save-and-scan', productDescription }),
5719
})
58-
window.location.href = '/dashboard'
20+
window.location.href = '/dashboard?scanning=true'
5921
} finally {
6022
setLoading(false)
6123
}
6224
}
6325

64-
function removeKeyword(kw: string) {
65-
setKeywords((prev) => prev.filter((k) => k !== kw))
66-
}
67-
68-
function addKeyword(e: KeyboardEvent<HTMLInputElement>) {
69-
if (e.key !== 'Enter') return
70-
e.preventDefault()
71-
const value = keywordInput.trim()
72-
if (value && !keywords.includes(value)) {
73-
setKeywords((prev) => [...prev, value])
74-
}
75-
setKeywordInput('')
76-
}
77-
78-
function removeSubreddit(sub: string) {
79-
setSubreddits((prev) => prev.filter((s) => s !== sub))
80-
}
81-
82-
function addSubreddit(e: KeyboardEvent<HTMLInputElement>) {
83-
if (e.key !== 'Enter') return
84-
e.preventDefault()
85-
const value = subredditInput.trim().replace(/^r\//, '')
86-
if (value && !subreddits.includes(value)) {
87-
setSubreddits((prev) => [...prev, value])
88-
}
89-
setSubredditInput('')
90-
}
91-
9226
return (
9327
<div className="flex min-h-screen items-center justify-center bg-background px-4 py-12">
9428
<div className="w-full max-w-xl space-y-6">
29+
<div className="space-y-2 text-center">
30+
<h1 className="text-2xl font-semibold tracking-tight">What does your product do?</h1>
31+
<p className="text-sm text-muted-foreground">
32+
We&apos;ll find Reddit &amp; HN discussions where people need it.
33+
</p>
34+
</div>
9535

96-
{/* Progress */}
9736
<div className="space-y-2">
98-
<p className="text-sm text-muted-foreground text-center">
99-
Step {currentStep} of 3
37+
<Textarea
38+
value={productDescription}
39+
onChange={(e) => setProductDescription(e.target.value)}
40+
placeholder="e.g. A tool that helps indie hackers find their first customers by monitoring Reddit for people asking about growth and distribution"
41+
rows={6}
42+
className="resize-none"
43+
/>
44+
<p className="text-xs text-muted-foreground text-right">
45+
{productDescription.length} characters
10046
</p>
101-
<div className="flex gap-2">
102-
{([1, 2, 3] as Step[]).map((step) => (
103-
<div
104-
key={step}
105-
className={`h-1.5 flex-1 rounded-full transition-colors ${
106-
step <= currentStep ? 'bg-primary' : 'bg-muted'
107-
}`}
108-
/>
109-
))}
110-
</div>
11147
</div>
11248

113-
{/* Step 1 */}
114-
{currentStep === 1 && (
115-
<Card>
116-
<CardHeader>
117-
<CardTitle>What does your product do?</CardTitle>
118-
<CardDescription>
119-
Describe it in plain language — we'll use this to find relevant conversations.
120-
</CardDescription>
121-
</CardHeader>
122-
<CardContent className="space-y-4">
123-
<div className="space-y-2">
124-
<Textarea
125-
value={productDescription}
126-
onChange={(e) => setProductDescription(e.target.value)}
127-
placeholder="I built a tool that helps indie hackers find their first customers on Reddit by..."
128-
rows={5}
129-
/>
130-
<p className="text-xs text-muted-foreground text-right">
131-
{productDescription.length} characters
132-
</p>
133-
</div>
134-
<Button
135-
className="w-full"
136-
disabled={productDescription.trim() === ''}
137-
onClick={() => setCurrentStep(2)}
138-
>
139-
Next →
140-
</Button>
141-
</CardContent>
142-
</Card>
143-
)}
144-
145-
{/* Step 2 */}
146-
{currentStep === 2 && (
147-
<Card>
148-
<CardHeader>
149-
<CardTitle>Who is your target customer?</CardTitle>
150-
<CardDescription>
151-
Describe the person most likely to pay for your product.
152-
</CardDescription>
153-
</CardHeader>
154-
<CardContent className="space-y-4">
155-
<div className="space-y-2">
156-
<Textarea
157-
value={targetCustomer}
158-
onChange={(e) => setTargetCustomer(e.target.value)}
159-
placeholder="Indie hackers and solo founders who just launched a product and need their first 10 customers"
160-
rows={4}
161-
/>
162-
<p className="text-xs text-muted-foreground text-right">
163-
{targetCustomer.length} characters
164-
</p>
165-
</div>
166-
<div className="flex gap-3">
167-
<Button
168-
variant="outline"
169-
className="flex-1"
170-
onClick={() => setCurrentStep(1)}
171-
>
172-
← Back
173-
</Button>
174-
<Button
175-
className="flex-1"
176-
disabled={targetCustomer.trim() === '' || loading}
177-
onClick={handleGenerateKeywords}
178-
>
179-
{loading ? (
180-
<>
181-
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
182-
Generating…
183-
</>
184-
) : (
185-
'Generate Keywords →'
186-
)}
187-
</Button>
188-
</div>
189-
</CardContent>
190-
</Card>
191-
)}
192-
193-
{/* Step 3 */}
194-
{currentStep === 3 && (
195-
<Card>
196-
<CardHeader>
197-
<CardTitle>Review your monitoring setup</CardTitle>
198-
<CardDescription>
199-
Remove anything that doesn't fit. Add your own by pressing Enter.
200-
</CardDescription>
201-
</CardHeader>
202-
<CardContent className="space-y-6">
203-
204-
{/* Keywords */}
205-
<div className="space-y-3">
206-
<p className="text-sm font-medium">Keywords</p>
207-
<div className="flex flex-wrap gap-2">
208-
{keywords.map((kw) => (
209-
<Badge key={kw} variant="secondary" className="gap-1 pr-1">
210-
{kw}
211-
<button
212-
onClick={() => removeKeyword(kw)}
213-
className="ml-1 rounded-full hover:bg-muted p-0.5"
214-
aria-label={`Remove ${kw}`}
215-
>
216-
<X className="h-3 w-3" />
217-
</button>
218-
</Badge>
219-
))}
220-
</div>
221-
<Input
222-
value={keywordInput}
223-
onChange={(e) => setKeywordInput(e.target.value)}
224-
onKeyDown={addKeyword}
225-
placeholder="Add a keyword and press Enter"
226-
/>
227-
</div>
228-
229-
{/* Subreddits */}
230-
<div className="space-y-3">
231-
<p className="text-sm font-medium">Subreddits</p>
232-
<div className="flex flex-wrap gap-2">
233-
{subreddits.map((sub) => (
234-
<Badge key={sub} variant="secondary" className="gap-1 pr-1">
235-
r/{sub}
236-
<button
237-
onClick={() => removeSubreddit(sub)}
238-
className="ml-1 rounded-full hover:bg-muted p-0.5"
239-
aria-label={`Remove r/${sub}`}
240-
>
241-
<X className="h-3 w-3" />
242-
</button>
243-
</Badge>
244-
))}
245-
</div>
246-
<Input
247-
value={subredditInput}
248-
onChange={(e) => setSubredditInput(e.target.value)}
249-
onKeyDown={addSubreddit}
250-
placeholder="Add a subreddit (with or without r/) and press Enter"
251-
/>
252-
</div>
253-
254-
<div className="flex gap-3">
255-
<Button
256-
variant="outline"
257-
className="flex-1"
258-
disabled={loading}
259-
onClick={() => setCurrentStep(2)}
260-
>
261-
← Back
262-
</Button>
263-
<Button
264-
className="flex-1"
265-
disabled={keywords.length === 0 || loading}
266-
onClick={handleSave}
267-
>
268-
{loading ? (
269-
<>
270-
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
271-
Saving…
272-
</>
273-
) : (
274-
'Start Monitoring →'
275-
)}
276-
</Button>
277-
</div>
278-
</CardContent>
279-
</Card>
280-
)}
281-
49+
<Button
50+
className="w-full"
51+
disabled={productDescription.trim().length < 20 || loading}
52+
onClick={handleSubmit}
53+
>
54+
{loading ? (
55+
<>
56+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
57+
Setting up your radar…
58+
</>
59+
) : (
60+
'Start Scanning →'
61+
)}
62+
</Button>
28263
</div>
28364
</div>
28465
)

0 commit comments

Comments
 (0)