|
1 | 1 | 'use client' |
2 | 2 |
|
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' |
6 | 4 | 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' |
12 | 7 |
|
13 | 8 | export default function OnboardingPage() { |
14 | | - const [currentStep, setCurrentStep] = useState<Step>(1) |
15 | 9 | const [productDescription, setProductDescription] = useState('') |
16 | | - const [targetCustomer, setTargetCustomer] = useState('') |
17 | | - const [keywords, setKeywords] = useState<string[]>([]) |
18 | | - const [subreddits, setSubreddits] = useState<string[]>([]) |
19 | 10 | 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 | | - } |
43 | 11 |
|
44 | | - async function handleSave() { |
| 12 | + async function handleSubmit() { |
45 | 13 | setLoading(true) |
46 | 14 | try { |
47 | 15 | await fetch('/api/onboarding', { |
48 | 16 | method: 'POST', |
49 | 17 | 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 }), |
57 | 19 | }) |
58 | | - window.location.href = '/dashboard' |
| 20 | + window.location.href = '/dashboard?scanning=true' |
59 | 21 | } finally { |
60 | 22 | setLoading(false) |
61 | 23 | } |
62 | 24 | } |
63 | 25 |
|
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 | | - |
92 | 26 | return ( |
93 | 27 | <div className="flex min-h-screen items-center justify-center bg-background px-4 py-12"> |
94 | 28 | <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'll find Reddit & HN discussions where people need it. |
| 33 | + </p> |
| 34 | + </div> |
95 | 35 |
|
96 | | - {/* Progress */} |
97 | 36 | <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 |
100 | 46 | </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> |
111 | 47 | </div> |
112 | 48 |
|
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> |
282 | 63 | </div> |
283 | 64 | </div> |
284 | 65 | ) |
|
0 commit comments