|
| 1 | +import { authService } from "@/features/auth/services/authService"; |
1 | 2 | import { UserLayout } from "@/layouts"; |
2 | 3 | import { Button, Input } from "@/shared/components"; |
3 | 4 | import { ArrowRight } from "lucide-react"; |
4 | | -import { useState } from "react"; |
| 5 | +import { useEffect, useMemo, useState } from "react"; |
5 | 6 | import { useNavigate } from "react-router-dom"; |
6 | 7 | import { NameMarquee } from "./NameMarquee"; |
7 | 8 | import styles from "./WelcomePage.module.css"; |
8 | 9 |
|
9 | 10 | export const WelcomePage = () => { |
10 | 11 | const navigate = useNavigate(); |
11 | | - const [name, setName] = useState(""); |
12 | | - const [nickName, setNickName] = useState("Andrew"); |
| 12 | + const isWelcomeGuardMuted = import.meta.env.VITE_MUTE_WELCOME_GUARD === "true"; // true = allow everyone in, false = protect this page. |
| 13 | + const [nickname, setNickname] = useState(""); |
| 14 | + const [username, setUsername] = useState(""); |
| 15 | + const [isUsernameFocused, setIsUsernameFocused] = useState(false); |
| 16 | + const [isLoading, setIsLoading] = useState(true); |
| 17 | + const [isSubmitting, setIsSubmitting] = useState(false); |
| 18 | + const [submitError, setSubmitError] = useState(""); |
13 | 19 |
|
14 | | - const handleSubmit = (e: React.FormEvent) => { |
| 20 | + useEffect(() => { |
| 21 | + let isMounted = true; |
| 22 | + |
| 23 | + const loadUser = async () => { |
| 24 | + try { |
| 25 | + const user = await authService.getCurrentUser(); |
| 26 | + if (!isMounted || !user) return; |
| 27 | + |
| 28 | + const hasGuardFlags = typeof user.isMember === "boolean" && typeof user.isFirstLogin === "boolean"; |
| 29 | + const isMember = user.isMember === true; |
| 30 | + const isFirstLogin = user.isFirstLogin === true; |
| 31 | + // TODO: Align with backend contract and switch to fail-close once isMember/isFirstLogin are guaranteed. |
| 32 | + if (!isWelcomeGuardMuted && hasGuardFlags && (!isMember || !isFirstLogin)) { |
| 33 | + navigate("/", { replace: true }); |
| 34 | + return; |
| 35 | + } |
| 36 | + |
| 37 | + setNickname(typeof user.name === "string" ? user.name : ""); |
| 38 | + setUsername(typeof user.username === "string" ? user.username : ""); |
| 39 | + } catch { |
| 40 | + if (!isWelcomeGuardMuted && isMounted) { |
| 41 | + navigate("/", { replace: true }); |
| 42 | + } |
| 43 | + } finally { |
| 44 | + if (isMounted) setIsLoading(false); |
| 45 | + } |
| 46 | + }; |
| 47 | + |
| 48 | + loadUser(); |
| 49 | + return () => { |
| 50 | + isMounted = false; |
| 51 | + }; |
| 52 | + }, [isWelcomeGuardMuted, navigate]); |
| 53 | + |
| 54 | + const nicknameError = useMemo(() => { |
| 55 | + if (!nickname) return ""; |
| 56 | + if (nickname.length > 15) return "暱稱需為 15 字以下"; |
| 57 | + if (nickname.length === 15) return "已達上限 15 字!"; |
| 58 | + return ""; |
| 59 | + }, [nickname]); |
| 60 | + |
| 61 | + const usernameError = useMemo(() => { |
| 62 | + if (!username.trim()) return "請輸入使用者名稱"; |
| 63 | + if (username.length < 4 || username.length > 15) return "使用者名稱需為 4~15 位"; |
| 64 | + if (!/^[A-Za-z0-9_]+$/.test(username)) return "使用者名稱只能包含英數與底線"; |
| 65 | + return ""; |
| 66 | + }, [username]); |
| 67 | + |
| 68 | + const canSubmit = Boolean(nickname.trim() && nickname.trim().length <= 15 && username.trim() && !usernameError && !isSubmitting); |
| 69 | + const displayedUsernameError = isUsernameFocused || username.trim() ? usernameError : ""; |
| 70 | + |
| 71 | + const handleSubmit = async (e: React.FormEvent) => { |
15 | 72 | e.preventDefault(); |
16 | | - if (name.trim()) { |
17 | | - console.log("Saving name:", name); |
| 73 | + if (!canSubmit) return; |
| 74 | + |
| 75 | + try { |
| 76 | + setSubmitError(""); |
| 77 | + setIsSubmitting(true); |
| 78 | + |
| 79 | + await authService.updateOnboarding({ |
| 80 | + username: username.trim(), |
| 81 | + name: nickname.trim() |
| 82 | + }); |
| 83 | + |
18 | 84 | navigate("/forms"); |
| 85 | + } catch { |
| 86 | + setSubmitError("儲存失敗,請稍後再試"); |
| 87 | + } finally { |
| 88 | + setIsSubmitting(false); |
19 | 89 | } |
20 | 90 | }; |
21 | | - const displayName = `${nickName.trim() || "Andrew"}!`; |
| 91 | + |
| 92 | + const displayName = `${nickname.trim() || "Welcome"}!`; |
22 | 93 |
|
23 | 94 | return ( |
24 | 95 | <UserLayout> |
25 | 96 | <NameMarquee name={displayName} /> |
26 | 97 | <div className={styles.content}> |
27 | | - <h1 className={styles.title}>很高興認識您,Andrew!</h1> |
| 98 | + <h1 className={styles.title}>很高興認識您,{displayName}</h1> |
28 | 99 | <form className={styles.form} onSubmit={handleSubmit}> |
29 | | - <Input id="nickName" label="暱稱" placeholder="Enter your nickname" value={nickName} onChange={e => setNickName(e.target.value)} required /> |
30 | | - <Input id="name" label="使用者名稱" placeholder="Enter your name" value={name} onChange={e => setName(e.target.value)} required /> |
| 100 | + <Input |
| 101 | + id="nickname" |
| 102 | + label="暱稱" |
| 103 | + placeholder="Enter your nickname" |
| 104 | + value={nickname} |
| 105 | + onChange={e => setNickname(e.target.value)} |
| 106 | + error={nicknameError} |
| 107 | + disabled={isLoading} |
| 108 | + maxLength={15} |
| 109 | + className={styles.inputField} |
| 110 | + required |
| 111 | + /> |
| 112 | + <Input |
| 113 | + id="username" |
| 114 | + label="使用者名稱" |
| 115 | + placeholder="Becomes your user URL" |
| 116 | + value={username} |
| 117 | + onChange={e => setUsername(e.target.value.replace(/\s/g, ""))} |
| 118 | + onFocus={() => setIsUsernameFocused(true)} |
| 119 | + onBlur={() => setIsUsernameFocused(false)} |
| 120 | + error={displayedUsernameError} |
| 121 | + disabled={isLoading} |
| 122 | + minLength={4} |
| 123 | + maxLength={15} |
| 124 | + className={styles.inputField} |
| 125 | + required |
| 126 | + /> |
31 | 127 | <div className={styles.actions}> |
32 | | - <Button type="submit" icon={ArrowRight} disabled={!name.trim()} iconPosition="right"> |
| 128 | + <Button type="submit" icon={ArrowRight} disabled={!canSubmit} iconPosition="right"> |
33 | 129 | 繼續 |
34 | 130 | </Button> |
35 | 131 | </div> |
| 132 | + {submitError && <p>{submitError}</p>} |
36 | 133 | </form> |
37 | 134 | </div> |
38 | 135 | </UserLayout> |
|
0 commit comments