1+ "use client" ;
2+
3+ import { useState , useId } from "react" ;
4+ import { useRouter } from "next/navigation" ;
5+ import { registerUser } from "@/lib/api" ;
6+ import { ApiError } from "@/lib/types" ;
7+
8+ export default function AuthPage ( ) {
9+ const router = useRouter ( ) ;
10+ const [ formData , setFormData ] = useState ( {
11+ name : "" ,
12+ display_name : "" ,
13+ intro : "" ,
14+ email : "" ,
15+ show_email : false ,
16+ password : "" ,
17+ confirmPassword : "" ,
18+ } ) ;
19+ const [ errors , setErrors ] = useState < Record < string , string > > ( { } ) ;
20+ const [ isLoading , setIsLoading ] = useState ( false ) ;
21+ const [ apiError , setApiError ] = useState < string | null > ( null ) ;
22+ const [ successMessage , setSuccessMessage ] = useState < string | null > ( null ) ;
23+ const nameId = useId ( ) ;
24+ const display_nameId = useId ( ) ;
25+ const introId = useId ( ) ;
26+ const emailId = useId ( ) ;
27+ const show_emailId = useId ( ) ;
28+ const passwordId = useId ( ) ;
29+ const confirmPasswordId = useId ( ) ;
30+
31+ const validateForm = ( ) => {
32+ const newErrors : Record < string , string > = { } ;
33+
34+ if ( ! formData . name ) {
35+ newErrors . name = "ユーザー名は必須です" ;
36+ } else if ( ! / ^ [ a - z A - Z 0 - 9 ] + $ / . test ( formData . name ) ) {
37+ newErrors . name = "ユーザー名は英数字のみ使用できます" ;
38+ }
39+
40+ if ( ! formData . display_name ) {
41+ newErrors . display_name = "表示名は必須です" ;
42+ }
43+
44+ if ( ! formData . email ) {
45+ newErrors . email = "メールアドレスは必須です" ;
46+ } else if ( ! / \S + @ \S + \. \S + / . test ( formData . email ) ) {
47+ newErrors . email = "有効なメールアドレスを入力してください" ;
48+ }
49+
50+ if ( ! formData . password ) {
51+ newErrors . password = "パスワードは必須です" ;
52+ } else if ( formData . password . length < 6 ) {
53+ newErrors . password = "パスワードは6文字以上である必要があります" ;
54+ }
55+
56+ if ( formData . password !== formData . confirmPassword ) {
57+ newErrors . confirmPassword = "パスワードが一致しません" ;
58+ }
59+
60+ setErrors ( newErrors ) ;
61+ return Object . keys ( newErrors ) . length === 0 ;
62+ } ;
63+
64+ const handleSubmit = async ( e : React . FormEvent ) => {
65+ e . preventDefault ( ) ;
66+ setApiError ( null ) ;
67+ setSuccessMessage ( null ) ;
68+
69+ if ( ! validateForm ( ) ) {
70+ return ;
71+ }
72+
73+ setIsLoading ( true ) ;
74+
75+ try {
76+ const userData = {
77+ name : formData . name ,
78+ display_name : formData . display_name ,
79+ intro : formData . intro ,
80+ email : formData . email ,
81+ show_email : formData . show_email ,
82+ password : formData . password , // 注意: 平文送信している
83+ created_at : new Date ( ) . toISOString ( ) ,
84+ } ;
85+
86+ const result = await registerUser ( userData ) ;
87+
88+ if ( result instanceof ApiError ) {
89+ if ( result . message ) {
90+ setApiError ( result . message ) ;
91+ } else {
92+ setApiError ( "登録中にエラーが発生しました" ) ;
93+ }
94+ setIsLoading ( false ) ;
95+ return ;
96+ }
97+
98+ setSuccessMessage ( "登録が完了しました!" ) ;
99+ setTimeout ( ( ) => {
100+ router . push ( "/" ) ;
101+ } , 1500 ) ;
102+ } catch ( error ) {
103+ console . error ( "登録エラー:" , error ) ;
104+ setApiError ( "登録中にエラーが発生しました" ) ;
105+ } finally {
106+ setIsLoading ( false ) ;
107+ }
108+ } ;
109+
110+ const handleChange = (
111+ e : React . ChangeEvent < HTMLInputElement | HTMLTextAreaElement >
112+ ) => {
113+ const { name, value, type } = e . target ;
114+ const checked = ( e . target as HTMLInputElement ) . checked ;
115+
116+ setFormData ( ( prev ) => ( {
117+ ...prev ,
118+ [ name ] : type === "checkbox" ? checked : value ,
119+ } ) ) ;
120+ } ;
121+
122+ return (
123+ < div className = "max-w-md mx-auto mt-8 p-6 bg-white rounded-lg shadow-md" >
124+ < h1 className = "text-2xl font-bold mb-6 text-center" > ユーザー登録</ h1 >
125+
126+ < div className = "mb-6 p-4 bg-red-50 border-2 border-red-400 rounded-lg" >
127+ < p className = "text-red-700 font-bold text-center" > ⚠️ 注意 ⚠️</ p >
128+ < p className = "text-red-600 text-sm mt-2" >
129+ このサイトは開発中です。実際に使用しているメールアドレスやパスワードは入力しないでください。
130+ </ p >
131+ </ div >
132+
133+ < form onSubmit = { handleSubmit } className = "space-y-4" >
134+ < div >
135+ < label htmlFor = "name" className = "block text-sm font-medium mb-1" >
136+ ユーザー名 < span className = "text-red-500" > *</ span >
137+ </ label >
138+ < input
139+ type = "text"
140+ id = { nameId }
141+ name = "name"
142+ value = { formData . name }
143+ onChange = { handleChange }
144+ className = { `w-full px-3 py-2 border rounded-md ${
145+ errors . name ? "border-red-500" : "border-gray-300"
146+ } `}
147+ placeholder = "例: minweb123"
148+ />
149+ < p className = "text-xs text-gray-500 mt-1" >
150+ 英数字のみ使用可能。後から変更できません。
151+ </ p >
152+ { errors . name && (
153+ < p className = "text-red-500 text-sm mt-1" > { errors . name } </ p >
154+ ) }
155+ </ div >
156+
157+ < div >
158+ < label htmlFor = "display_name" className = "block text-sm font-medium mb-1" >
159+ 表示名 < span className = "text-red-500" > *</ span >
160+ </ label >
161+ < input
162+ type = "text"
163+ id = { display_nameId }
164+ name = "display_name"
165+ value = { formData . display_name }
166+ onChange = { handleChange }
167+ className = { `w-full px-3 py-2 border rounded-md ${
168+ errors . display_name ? "border-red-500" : "border-gray-300"
169+ } `}
170+ placeholder = "例: ミン・ウェブ"
171+ />
172+ < p className = "text-xs text-gray-500 mt-1" > 絵文字も使用できます (今の所、表示名はどこにも表示されません(?!))</ p >
173+ { errors . display_name && (
174+ < p className = "text-red-500 text-sm mt-1" > { errors . display_name } </ p >
175+ ) }
176+ </ div >
177+
178+ < div >
179+ < label htmlFor = "intro" className = "block text-sm font-medium mb-1" >
180+ 自己紹介
181+ </ label >
182+ < textarea
183+ id = { introId }
184+ name = "intro"
185+ value = { formData . intro }
186+ onChange = { handleChange }
187+ rows = { 3 }
188+ className = "w-full px-3 py-2 border border-gray-300 rounded-md"
189+ placeholder = "そのうちプロフィールに表示されます"
190+ />
191+ </ div >
192+
193+ < div >
194+ < label htmlFor = "email" className = "block text-sm font-medium mb-1" >
195+ メールアドレス < span className = "text-red-500" > *</ span >
196+ </ label >
197+ < input
198+ type = "email"
199+ id = { emailId }
200+ name = "email"
201+ value = { formData . email }
202+ onChange = { handleChange }
203+ className = { `w-full px-3 py-2 border rounded-md ${
204+ errors . email ? "border-red-500" : "border-gray-300"
205+ } `}
206+ placeholder = "test@minweb.com"
207+ />
208+ < p className = "text-xs text-gray-500 mt-1" >
209+ テスト用のメールアドレスを使用してください
210+ </ p >
211+ { errors . email && (
212+ < p className = "text-red-500 text-sm mt-1" > { errors . email } </ p >
213+ ) }
214+ </ div >
215+
216+ < div className = "flex items-center" >
217+ < input
218+ type = "checkbox"
219+ id = { show_emailId }
220+ name = "show_email"
221+ checked = { formData . show_email }
222+ onChange = { handleChange }
223+ className = "mr-2"
224+ />
225+ < label htmlFor = "show_email" className = "text-sm" >
226+ メールアドレスを公開する
227+ </ label >
228+ </ div >
229+
230+ < div >
231+ < label htmlFor = "password" className = "block text-sm font-medium mb-1" >
232+ パスワード < span className = "text-red-500" > *</ span >
233+ </ label >
234+ < input
235+ type = "password"
236+ id = { passwordId }
237+ name = "password"
238+ value = { formData . password }
239+ onChange = { handleChange }
240+ className = { `w-full px-3 py-2 border rounded-md ${
241+ errors . password ? "border-red-500" : "border-gray-300"
242+ } `}
243+ placeholder = "6文字以上"
244+ />
245+ < p className = "text-xs text-gray-500 mt-1" >
246+ テスト用のパスワードを使用してください
247+ </ p >
248+ { errors . password && (
249+ < p className = "text-red-500 text-sm mt-1" > { errors . password } </ p >
250+ ) }
251+ </ div >
252+
253+ < div >
254+ < label
255+ htmlFor = "confirmPassword"
256+ className = "block text-sm font-medium mb-1"
257+ >
258+ パスワード(確認) < span className = "text-red-500" > *</ span >
259+ </ label >
260+ < input
261+ type = "password"
262+ id = { confirmPasswordId }
263+ name = "confirmPassword"
264+ value = { formData . confirmPassword }
265+ onChange = { handleChange }
266+ className = { `w-full px-3 py-2 border rounded-md ${
267+ errors . confirmPassword ? "border-red-500" : "border-gray-300"
268+ } `}
269+ placeholder = "パスワードを再入力"
270+ />
271+ { errors . confirmPassword && (
272+ < p className = "text-red-500 text-sm mt-1" > { errors . confirmPassword } </ p >
273+ ) }
274+ </ div >
275+
276+ { apiError && (
277+ < div className = "bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-md" >
278+ { apiError }
279+ </ div >
280+ ) }
281+
282+ { successMessage && (
283+ < div className = "bg-green-50 border border-green-200 text-green-700 px-4 py-3 rounded-md" >
284+ { successMessage }
285+ </ div >
286+ ) }
287+
288+ < button
289+ type = "submit"
290+ disabled = { isLoading }
291+ className = { `w-full py-2 px-4 rounded-md text-white font-medium ${
292+ isLoading
293+ ? "bg-gray-400 cursor-not-allowed"
294+ : "bg-blue-500 hover:bg-blue-600"
295+ } `}
296+ >
297+ { isLoading ? "登録中..." : "登録する" }
298+ </ button >
299+ </ form >
300+ </ div >
301+ ) ;
302+ }
0 commit comments