SarahXia0405 commited on
Commit
304d09e
·
verified ·
1 Parent(s): 45c1a35

Update web/src/components/Onboarding.tsx

Browse files
Files changed (1) hide show
  1. web/src/components/Onboarding.tsx +463 -158
web/src/components/Onboarding.tsx CHANGED
@@ -1,11 +1,16 @@
1
- // web/src/components/Onboarding.tsx
2
- import React, { useEffect, useMemo, useState } from "react";
3
  import { Button } from "./ui/button";
4
- import { Textarea } from "./ui/textarea";
 
 
 
5
  import { toast } from "sonner";
6
- import type { User } from "../App";
 
 
7
 
8
- type InitStatus = "idle" | "checking" | "not_needed" | "asking" | "submitting" | "done";
 
9
 
10
  type InitQ = {
11
  id: string;
@@ -57,105 +62,106 @@ const INIT_QUESTIONS: InitQ[] = [
57
  ];
58
 
59
  interface OnboardingProps {
60
- user: User;
61
- onComplete: (updatedUser: User) => void;
62
  onSkip: () => void;
63
  }
64
 
65
  export function Onboarding({ user, onComplete, onSkip }: OnboardingProps) {
66
- const [status, setStatus] = useState<InitStatus>("checking");
67
- const [step, setStep] = useState(0);
68
- const [answers, setAnswers] = useState<Record<string, string>>({});
69
- const [input, setInput] = useState("");
70
- const [generatedBio, setGeneratedBio] = useState("");
71
 
72
- const currentQ = useMemo(() => INIT_QUESTIONS[step], [step]);
 
 
73
 
74
- // 1) Check backend status: do we need init?
75
- useEffect(() => {
76
- let cancelled = false;
77
-
78
- (async () => {
79
- try {
80
- setStatus("checking");
81
- const r = await fetch(`/api/profile/status?user_id=${encodeURIComponent(user.email)}`);
82
- if (!r.ok) throw new Error("status check failed");
83
- const j = await r.json();
84
- if (cancelled) return;
85
-
86
- if (j?.need_init) {
87
- setStatus("asking");
88
- setStep(0);
89
- setAnswers({});
90
- setInput("");
91
- setGeneratedBio("");
92
- } else {
93
- setStatus("not_needed");
94
- }
95
- } catch {
96
- // 如果 status 拉取失败:为了不挡 onboarding,默认不需要 init
97
- if (!cancelled) setStatus("not_needed");
98
- }
99
- })();
100
 
101
- return () => {
102
- cancelled = true;
103
- };
104
- }, [user.email]);
 
 
 
 
 
 
 
105
 
106
- // 2) If not needed, just complete onboarding immediately (keep existing behavior)
 
 
107
  useEffect(() => {
108
- if (status !== "not_needed") return;
 
 
 
 
109
 
110
- // 这里不强行自动完成也可以;但你的 App 逻辑是 onboarding 不完成就进不去主界面
111
- // 所以 not_needed -> 直接结束 onboarding
112
- onComplete({ ...user, onboardingCompleted: true });
113
- // eslint-disable-next-line react-hooks/exhaustive-deps
114
- }, [status]);
115
 
116
- const handleSkipBio = async () => {
117
- // optional: dismiss for 7 days
118
- try {
119
- await fetch("/api/profile/dismiss", {
120
- method: "POST",
121
- headers: { "Content-Type": "application/json" },
122
- body: JSON.stringify({ user_id: user.email, days: 7 }),
123
- });
124
- } catch {
125
- // ignore
 
126
  }
127
 
128
- // allow user to continue onboarding anyway
129
- onComplete({ ...user, onboardingCompleted: true });
 
130
  };
131
 
132
- const handleNext = async () => {
133
- if (status !== "asking") return;
134
 
135
- const text = input.trim();
136
- if (!text) return;
 
 
 
 
 
 
 
 
 
 
137
 
138
- const q = INIT_QUESTIONS[step];
139
- const nextAnswers = { ...answers, [q.id]: text };
140
 
141
- setAnswers(nextAnswers);
142
- setInput("");
143
 
144
- const nextStep = step + 1;
145
 
146
- if (nextStep < INIT_QUESTIONS.length) {
147
- setStep(nextStep);
 
148
  return;
149
  }
150
 
151
- // last question -> submit
152
- setStatus("submitting");
 
153
  try {
154
  const r = await fetch("/api/profile/init_submit", {
155
  method: "POST",
156
  headers: { "Content-Type": "application/json" },
157
  body: JSON.stringify({
158
- user_id: user.email,
159
  answers: nextAnswers,
160
  language_preference: "English",
161
  }),
@@ -165,102 +171,401 @@ export function Onboarding({ user, onComplete, onSkip }: OnboardingProps) {
165
  const j = await r.json();
166
 
167
  const bio = (j?.bio || "").toString();
 
 
 
 
168
  setGeneratedBio(bio);
169
- setStatus("done");
 
 
 
 
 
 
 
 
 
170
 
171
- // finish onboarding, and persist bio into user
172
- onComplete({
173
- ...user,
174
- bio,
175
- onboardingCompleted: true,
176
- });
177
- } catch (e: any) {
178
- toast.error("Sorry — I couldn’t generate your Bio. Please try again.");
179
- setStatus("asking");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
  };
182
 
183
- // If checking/not_needed, we don’t render heavy UI.
184
- if (status === "checking" || status === "not_needed") {
185
- return (
186
- <div className="min-h-screen w-full flex items-center justify-center bg-background">
187
- <div className="w-full max-w-lg px-6 py-8 rounded-2xl border bg-card">
188
- <div className="text-lg font-semibold">Setting up your workspace…</div>
189
- <div className="text-sm text-muted-foreground mt-2">
190
- One moment while we prepare your personalized experience.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
  </div>
192
- </div>
193
- </div>
194
- );
195
- }
196
-
197
- // Main onboarding step: Bio questions
198
- return (
199
- <div className="min-h-screen w-full flex items-center justify-center bg-background">
200
- <div className="w-full max-w-2xl px-6 py-8 rounded-2xl border bg-card">
201
- <div className="flex items-start justify-between gap-4">
202
- <div>
203
- <div className="text-xl font-semibold">Quick intro to personalize Clare</div>
204
- <div className="text-sm text-muted-foreground mt-1">
205
- Answer a few short questions. We’ll summarize them into your Profile Bio and use it only inside this platform.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  </div>
207
  </div>
208
-
209
- <Button variant="outline" onClick={handleSkipBio} disabled={status === "submitting"}>
210
- Skip
211
- </Button>
212
- </div>
213
-
214
- <div className="mt-6 rounded-xl border bg-muted/30 p-4">
215
- <div className="text-sm font-medium">{currentQ?.title}</div>
216
- <div className="text-xs text-muted-foreground mt-1">
217
- {currentQ?.placeholder || "Type your answer below."}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
  </div>
219
- <div className="text-xs text-muted-foreground mt-2">
220
- Question {step + 1} of {INIT_QUESTIONS.length}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
  </div>
222
-
223
- <Textarea
224
- value={input}
225
- onChange={(e) => setInput(e.target.value)}
226
- placeholder="Type your answer here..."
227
- className="mt-3 min-h-[90px]"
228
- disabled={status === "submitting"}
229
- onKeyDown={(e) => {
230
- if (e.key === "Enter" && !e.shiftKey) {
231
- e.preventDefault();
232
- handleNext();
233
- }
234
- }}
235
- />
236
-
237
- <div className="mt-3 flex justify-end gap-2">
238
- <Button
239
- onClick={handleNext}
240
- disabled={status === "submitting" || !input.trim()}
241
- >
242
- {step === INIT_QUESTIONS.length - 1 ? (status === "submitting" ? "Saving…" : "Finish") : "Next"}
243
- </Button>
 
 
 
 
 
 
 
 
 
244
  </div>
245
- </div>
246
 
247
- {status === "submitting" && (
248
- <div className="mt-4 text-sm text-muted-foreground">
249
- Generating your Profile Bio…
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
  </div>
251
- )}
252
-
253
- {/* In practice, onComplete() already exits onboarding.
254
- This is just a safety UI fallback if your app keeps it visible. */}
255
- {status === "done" && generatedBio && (
256
- <div className="mt-6 rounded-xl border bg-background p-4">
257
- <div className="text-sm font-medium">Saved to your Profile Bio</div>
258
- <div className="text-sm text-muted-foreground mt-2 whitespace-pre-wrap">
259
- {generatedBio}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  </div>
261
  </div>
262
- )}
263
- </div>
264
- </div>
265
  );
266
  }
 
1
+ import React, { useRef, useState, useEffect, useMemo } from "react";
 
2
  import { Button } from "./ui/button";
3
+ import { Input } from "./ui/input";
4
+ import { Label } from "./ui/label";
5
+ import { Dialog, DialogContent, DialogTitle } from "./ui/dialog";
6
+ import type { User as UserType } from "../App";
7
  import { toast } from "sonner";
8
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
9
+ import { ChevronLeft, ChevronRight } from "lucide-react";
10
+ import { Textarea } from "./ui/textarea";
11
 
12
+ // Add Bio step. Total steps: 5
13
+ const TOTAL_STEPS = 5;
14
 
15
  type InitQ = {
16
  id: string;
 
62
  ];
63
 
64
  interface OnboardingProps {
65
+ user: UserType;
66
+ onComplete: (user: UserType) => void;
67
  onSkip: () => void;
68
  }
69
 
70
  export function Onboarding({ user, onComplete, onSkip }: OnboardingProps) {
71
+ const [currentStep, setCurrentStep] = useState(1);
 
 
 
 
72
 
73
+ // Step 1: Basic
74
+ const [name, setName] = useState(user.name ?? "");
75
+ const [email, setEmail] = useState(user.email ?? "");
76
 
77
+ // Step 2: Academic
78
+ const [studentId, setStudentId] = useState(user.studentId ?? "");
79
+ const [department, setDepartment] = useState(user.department ?? "");
80
+ const [yearLevel, setYearLevel] = useState(user.yearLevel ?? "");
81
+ const [major, setMajor] = useState(user.major ?? "");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
+ // Step 3: Preferences
84
+ const [learningStyle, setLearningStyle] = useState(user.learningStyle ?? "visual");
85
+ const [learningPace, setLearningPace] = useState(user.learningPace ?? "moderate");
86
+
87
+ // Step 4: Bio (8 questions -> generate bio)
88
+ const [bioQIndex, setBioQIndex] = useState(0);
89
+ const [bioInput, setBioInput] = useState("");
90
+ const [bioAnswers, setBioAnswers] = useState<Record<string, string>>({});
91
+ const [bioSubmitting, setBioSubmitting] = useState(false);
92
+ const [generatedBio, setGeneratedBio] = useState<string>(user.bio ?? "");
93
+ const [bioReady, setBioReady] = useState<boolean>(!!(user.bio && user.bio.trim().length > 0));
94
 
95
+ const currentBioQ = useMemo(() => INIT_QUESTIONS[bioQIndex], [bioQIndex]);
96
+
97
+ // Optional: if user already has bio, mark ready.
98
  useEffect(() => {
99
+ if (user.bio && user.bio.trim().length > 0) {
100
+ setGeneratedBio(user.bio);
101
+ setBioReady(true);
102
+ }
103
+ }, [user.bio]);
104
 
105
+ // Step 5: Photo
106
+ const [photoPreview, setPhotoPreview] = useState<string | null>(user.avatarUrl ?? null);
107
+ const fileInputRef = useRef<HTMLInputElement>(null);
 
 
108
 
109
+ const handlePhotoSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
110
+ const file = e.target.files?.[0];
111
+ if (!file) return;
112
+
113
+ if (!file.type.startsWith("image/")) {
114
+ toast.error("Please select an image file");
115
+ return;
116
+ }
117
+ if (file.size > 2 * 1024 * 1024) {
118
+ toast.error("File size must be less than 2MB");
119
+ return;
120
  }
121
 
122
+ const reader = new FileReader();
123
+ reader.onload = (ev) => setPhotoPreview(ev.target?.result as string);
124
+ reader.readAsDataURL(file);
125
  };
126
 
127
+ const handleChangePhotoClick = () => fileInputRef.current?.click();
 
128
 
129
+ const handlePrevious = () => {
130
+ if (currentStep > 1) setCurrentStep((s) => s - 1);
131
+ };
132
+
133
+ const handleSkip = () => onSkip();
134
+
135
+ // --------------------------
136
+ // Step 4: Bio generation flow
137
+ // --------------------------
138
+ const handleBioNext = async () => {
139
+ const v = bioInput.trim();
140
+ if (!v) return;
141
 
142
+ const q = INIT_QUESTIONS[bioQIndex];
143
+ const nextAnswers = { ...bioAnswers, [q.id]: v };
144
 
145
+ setBioAnswers(nextAnswers);
146
+ setBioInput("");
147
 
148
+ const nextIndex = bioQIndex + 1;
149
 
150
+ // Continue questions
151
+ if (nextIndex < INIT_QUESTIONS.length) {
152
+ setBioQIndex(nextIndex);
153
  return;
154
  }
155
 
156
+ // Last question -> submit to backend and generate bio
157
+ // NOTE: use same backend logic as before; we do NOT touch parsing/storage logic.
158
+ setBioSubmitting(true);
159
  try {
160
  const r = await fetch("/api/profile/init_submit", {
161
  method: "POST",
162
  headers: { "Content-Type": "application/json" },
163
  body: JSON.stringify({
164
+ user_id: email.trim() || user.email, // prefer current email input
165
  answers: nextAnswers,
166
  language_preference: "English",
167
  }),
 
171
  const j = await r.json();
172
 
173
  const bio = (j?.bio || "").toString();
174
+ if (!bio.trim()) {
175
+ throw new Error("empty bio");
176
+ }
177
+
178
  setGeneratedBio(bio);
179
+ setBioReady(true);
180
+
181
+ toast.success("Bio generated!");
182
+ } catch (e) {
183
+ toast.error("Failed to generate bio. Please try again.");
184
+ // allow retry: keep last answer stored; user can edit generated flow by resetting if needed
185
+ } finally {
186
+ setBioSubmitting(false);
187
+ }
188
+ };
189
 
190
+ const handleBioReset = () => {
191
+ setBioQIndex(0);
192
+ setBioInput("");
193
+ setBioAnswers({});
194
+ setBioSubmitting(false);
195
+ setGeneratedBio("");
196
+ setBioReady(false);
197
+ };
198
+
199
+ // Main Next handler (respects Step 4 gating)
200
+ const handleNext = async () => {
201
+ // Step 1 validation (kept)
202
+ if (currentStep === 1) {
203
+ if (!name.trim() || !email.trim()) {
204
+ toast.error("Please fill in all required fields");
205
+ return;
206
+ }
207
+ }
208
+
209
+ // Step 4 gating: must finish + have bioReady before moving on
210
+ if (currentStep === 4) {
211
+ if (!bioReady) {
212
+ // If still answering questions, Next acts as “Next question”
213
+ if (bioQIndex < INIT_QUESTIONS.length) {
214
+ await handleBioNext();
215
+ return;
216
+ }
217
+ // Safety: should not happen, but block
218
+ toast.error("Please finish the Bio questions first.");
219
+ return;
220
+ }
221
+ }
222
+
223
+ if (currentStep < TOTAL_STEPS) setCurrentStep((s) => s + 1);
224
+ else handleComplete();
225
+ };
226
+
227
+ const handleComplete = () => {
228
+ if (!name.trim() || !email.trim()) {
229
+ toast.error("Please fill in all required fields");
230
+ return;
231
  }
232
+
233
+ // ✅ Bio now comes from Onboarding Step 4
234
+ const finalBio = (generatedBio || user.bio || "").trim() || undefined;
235
+
236
+ const next: UserType = {
237
+ ...user,
238
+ name: name.trim(),
239
+ email: email.trim(),
240
+
241
+ studentId: studentId.trim() || undefined,
242
+ department: department.trim() || undefined,
243
+ yearLevel: yearLevel || undefined,
244
+ major: major.trim() || undefined,
245
+
246
+ learningStyle: learningStyle || undefined,
247
+ learningPace: learningPace || undefined,
248
+
249
+ avatarUrl: photoPreview || undefined,
250
+
251
+ bio: finalBio, // ✅ sync to profile via your existing onComplete->save logic
252
+ onboardingCompleted: true,
253
+ };
254
+
255
+ onComplete(next);
256
+ toast.success("Profile setup completed!");
257
  };
258
 
259
+ const renderStepContent = () => {
260
+ switch (currentStep) {
261
+ case 1:
262
+ return (
263
+ <div className="space-y-4">
264
+ <h3 className="text-lg font-medium">Basic Information</h3>
265
+ <p className="text-sm text-muted-foreground">Let's start with your basic information</p>
266
+
267
+ <div className="space-y-4">
268
+ <div className="space-y-2">
269
+ <Label htmlFor="onboarding-name">Full Name *</Label>
270
+ <Input
271
+ id="onboarding-name"
272
+ value={name}
273
+ onChange={(e) => setName(e.target.value)}
274
+ placeholder="Enter your full name"
275
+ />
276
+ </div>
277
+
278
+ <div className="space-y-2">
279
+ <Label htmlFor="onboarding-email">Email *</Label>
280
+ <Input
281
+ id="onboarding-email"
282
+ type="email"
283
+ value={email}
284
+ onChange={(e) => setEmail(e.target.value)}
285
+ placeholder="Enter your email"
286
+ />
287
+ </div>
288
+ </div>
289
  </div>
290
+ );
291
+
292
+ case 2:
293
+ return (
294
+ <div className="space-y-4">
295
+ <h3 className="text-lg font-medium">Academic Background</h3>
296
+ <p className="text-sm text-muted-foreground">Tell us about your academic information</p>
297
+
298
+ <div className="space-y-4">
299
+ <div className="space-y-2">
300
+ <Label htmlFor="onboarding-student-id">Student ID</Label>
301
+ <Input
302
+ id="onboarding-student-id"
303
+ value={studentId}
304
+ onChange={(e) => setStudentId(e.target.value)}
305
+ placeholder="Enter your student ID"
306
+ />
307
+ </div>
308
+
309
+ <div className="space-y-2">
310
+ <Label htmlFor="onboarding-department">Department</Label>
311
+ <Input
312
+ id="onboarding-department"
313
+ value={department}
314
+ onChange={(e) => setDepartment(e.target.value)}
315
+ placeholder="Enter your department"
316
+ />
317
+ </div>
318
+
319
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
320
+ <div className="space-y-2">
321
+ <Label htmlFor="onboarding-year">Year Level</Label>
322
+ <Select value={yearLevel} onValueChange={setYearLevel}>
323
+ <SelectTrigger id="onboarding-year">
324
+ <SelectValue placeholder="Select year level" />
325
+ </SelectTrigger>
326
+ <SelectContent>
327
+ <SelectItem value="1st Year">1st Year</SelectItem>
328
+ <SelectItem value="2nd Year">2nd Year</SelectItem>
329
+ <SelectItem value="3rd Year">3rd Year</SelectItem>
330
+ <SelectItem value="4th Year">4th Year</SelectItem>
331
+ <SelectItem value="Graduate">Graduate</SelectItem>
332
+ </SelectContent>
333
+ </Select>
334
+ </div>
335
+
336
+ <div className="space-y-2">
337
+ <Label htmlFor="onboarding-major">Major</Label>
338
+ <Input
339
+ id="onboarding-major"
340
+ value={major}
341
+ onChange={(e) => setMajor(e.target.value)}
342
+ placeholder="Enter your major"
343
+ />
344
+ </div>
345
+ </div>
346
  </div>
347
  </div>
348
+ );
349
+
350
+ case 3:
351
+ return (
352
+ <div className="space-y-4">
353
+ <h3 className="text-lg font-medium">Learning Preferences</h3>
354
+ <p className="text-sm text-muted-foreground">Help us personalize your learning experience</p>
355
+
356
+ <div className="space-y-4">
357
+ <div className="space-y-2">
358
+ <Label htmlFor="onboarding-learning-style">Preferred Learning Style</Label>
359
+ <Select value={learningStyle} onValueChange={setLearningStyle}>
360
+ <SelectTrigger id="onboarding-learning-style">
361
+ <SelectValue />
362
+ </SelectTrigger>
363
+ <SelectContent>
364
+ <SelectItem value="visual">Visual</SelectItem>
365
+ <SelectItem value="auditory">Auditory</SelectItem>
366
+ <SelectItem value="reading">Reading/Writing</SelectItem>
367
+ <SelectItem value="kinesthetic">Kinesthetic</SelectItem>
368
+ </SelectContent>
369
+ </Select>
370
+ </div>
371
+
372
+ <div className="space-y-2">
373
+ <Label htmlFor="onboarding-pace">Learning Pace</Label>
374
+ <Select value={learningPace} onValueChange={setLearningPace}>
375
+ <SelectTrigger id="onboarding-pace">
376
+ <SelectValue />
377
+ </SelectTrigger>
378
+ <SelectContent>
379
+ <SelectItem value="slow">Slow & Steady</SelectItem>
380
+ <SelectItem value="moderate">Moderate</SelectItem>
381
+ <SelectItem value="fast">Fast-paced</SelectItem>
382
+ </SelectContent>
383
+ </Select>
384
+ </div>
385
+ </div>
386
  </div>
387
+ );
388
+
389
+ case 4:
390
+ return (
391
+ <div className="space-y-4">
392
+ <h3 className="text-lg font-medium">Profile Bio</h3>
393
+ <p className="text-sm text-muted-foreground">
394
+ Answer a few quick questions and we’ll generate a Bio that syncs to your profile.
395
+ </p>
396
+
397
+ {!bioReady ? (
398
+ <div className="space-y-3">
399
+ <div className="rounded-lg border border-border bg-muted/30 p-4 space-y-2">
400
+ <div className="text-sm font-medium">{currentBioQ.title}</div>
401
+ {currentBioQ.placeholder && (
402
+ <div className="text-xs text-muted-foreground">{currentBioQ.placeholder}</div>
403
+ )}
404
+ <div className="text-xs text-muted-foreground">
405
+ Question {bioQIndex + 1} of {INIT_QUESTIONS.length}
406
+ </div>
407
+
408
+ <Textarea
409
+ value={bioInput}
410
+ onChange={(e) => setBioInput(e.target.value)}
411
+ placeholder="Type your answer here..."
412
+ className="min-h-[96px] mt-2"
413
+ disabled={bioSubmitting}
414
+ onKeyDown={(e) => {
415
+ if (e.key === "Enter" && !e.shiftKey) {
416
+ e.preventDefault();
417
+ handleBioNext();
418
+ }
419
+ }}
420
+ />
421
+
422
+ <div className="flex items-center justify-between pt-2">
423
+ <Button variant="outline" onClick={handleBioReset} disabled={bioSubmitting}>
424
+ Reset
425
+ </Button>
426
+
427
+ <Button onClick={handleBioNext} disabled={bioSubmitting || !bioInput.trim()}>
428
+ {bioQIndex === INIT_QUESTIONS.length - 1
429
+ ? bioSubmitting
430
+ ? "Generating…"
431
+ : "Generate Bio"
432
+ : "Next Question"}
433
+ </Button>
434
+ </div>
435
+ </div>
436
+
437
+ <div className="text-xs text-muted-foreground">
438
+ Tip: Press Enter to go next (Shift+Enter for a new line).
439
+ </div>
440
+ </div>
441
+ ) : (
442
+ <div className="space-y-3">
443
+ <div className="rounded-lg border border-border bg-background p-4 space-y-2">
444
+ <div className="text-sm font-medium">Generated Bio</div>
445
+ <div className="text-xs text-muted-foreground">
446
+ You can edit it before continuing. This will be saved to your profile.
447
+ </div>
448
+
449
+ <Textarea
450
+ value={generatedBio}
451
+ onChange={(e) => setGeneratedBio(e.target.value)}
452
+ className="min-h-[140px] mt-2"
453
+ />
454
+
455
+ <div className="flex items-center justify-between pt-2">
456
+ <Button variant="outline" onClick={handleBioReset}>
457
+ Regenerate
458
+ </Button>
459
+ <div className="text-xs text-muted-foreground">
460
+ Click “Next Step” to continue.
461
+ </div>
462
+ </div>
463
+ </div>
464
+ </div>
465
+ )}
466
  </div>
467
+ );
468
+
469
+ case 5:
470
+ return (
471
+ <div className="space-y-4">
472
+ <h3 className="text-lg font-medium">Profile Picture</h3>
473
+ <p className="text-sm text-muted-foreground">Upload a photo to personalize your profile (optional)</p>
474
+
475
+ <div className="flex items-center gap-4">
476
+ <div className="w-24 h-24 rounded-full bg-gradient-to-br from-red-500 to-orange-500 flex items-center justify-center text-white text-3xl overflow-hidden">
477
+ {photoPreview ? (
478
+ <img src={photoPreview} alt="Profile" className="w-full h-full object-cover" />
479
+ ) : (
480
+ (name?.charAt(0) || "U").toUpperCase()
481
+ )}
482
+ </div>
483
+
484
+ <div>
485
+ <input
486
+ ref={fileInputRef}
487
+ type="file"
488
+ accept="image/jpeg,image/png,image/gif,image/webp"
489
+ onChange={handlePhotoSelect}
490
+ className="hidden"
491
+ />
492
+ <Button variant="outline" size="sm" onClick={handleChangePhotoClick}>
493
+ Change Photo
494
+ </Button>
495
+ <p className="text-xs text-muted-foreground mt-1">JPG, PNG or GIF. Max size 2MB</p>
496
+ </div>
497
+ </div>
498
  </div>
499
+ );
500
 
501
+ default:
502
+ return null;
503
+ }
504
+ };
505
+
506
+ return (
507
+ <Dialog
508
+ open
509
+ onOpenChange={(open) => {
510
+ if (!open) onSkip();
511
+ }}
512
+ >
513
+ <DialogContent
514
+ className="sm:max-w-lg p-0 gap-0 max-h-[90vh] overflow-hidden"
515
+ style={{ zIndex: 1001 }}
516
+ overlayClassName="!inset-0 !z-[99]"
517
+ overlayStyle={{ top: 0, left: 0, right: 0, bottom: 0, zIndex: 99, position: "fixed" }}
518
+ >
519
+ <div className="flex flex-col max-h-[90vh]">
520
+ {/* Header */}
521
+ <div className="border-b border-border p-4 flex items-center justify-between flex-shrink-0">
522
+ <div className="flex-1">
523
+ <DialogTitle className="text-xl font-medium">Welcome! Let's set up your profile</DialogTitle>
524
+ <p className="text-sm text-muted-foreground mt-1">
525
+ Step {currentStep} of {TOTAL_STEPS}
526
+ </p>
527
+ </div>
528
+
529
+ {/* Progress indicator */}
530
+ <div className="flex gap-1">
531
+ {Array.from({ length: TOTAL_STEPS }).map((_, index) => (
532
+ <div
533
+ key={index}
534
+ className={`h-2 w-2 rounded-full transition-colors ${
535
+ index + 1 <= currentStep ? "bg-primary" : "bg-muted"
536
+ }`}
537
+ />
538
+ ))}
539
+ </div>
540
  </div>
541
+
542
+ {/* Content */}
543
+ <div className="p-6 overflow-y-auto flex-1">{renderStepContent()}</div>
544
+
545
+ {/* Footer */}
546
+ <div className="border-t border-border p-4 flex justify-between gap-2 flex-shrink-0">
547
+ <div className="flex gap-2">
548
+ {currentStep > 1 && (
549
+ <Button variant="outline" onClick={handlePrevious} disabled={bioSubmitting}>
550
+ <ChevronLeft className="h-4 w-4 mr-1" />
551
+ Previous
552
+ </Button>
553
+ )}
554
+ </div>
555
+
556
+ <div className="flex gap-2">
557
+ <Button variant="outline" onClick={handleSkip} disabled={bioSubmitting}>
558
+ Skip all
559
+ </Button>
560
+
561
+ <Button onClick={handleNext} disabled={bioSubmitting}>
562
+ {currentStep === TOTAL_STEPS ? "Complete" : "Next Step"}
563
+ {currentStep < TOTAL_STEPS && <ChevronRight className="h-4 w-4 ml-1" />}
564
+ </Button>
565
  </div>
566
  </div>
567
+ </div>
568
+ </DialogContent>
569
+ </Dialog>
570
  );
571
  }