-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add demo profile #21
base: dev/[email protected]
Are you sure you want to change the base?
add demo profile #21
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
'use client'; | ||
import React from 'react'; | ||
import { ReactNode } from 'react'; | ||
import ProfileSidebar from '@/components/ProfileSidebar'; | ||
|
||
interface ProfileLayoutProps { | ||
children: ReactNode; | ||
} | ||
|
||
export default function ProfileLayout({ children }: ProfileLayoutProps) { | ||
return ( | ||
<div className="flex"> | ||
<ProfileSidebar /> | ||
<main className="flex-1 p-4">{children}</main> | ||
</div> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
// /app/profile/page.tsx | ||
import React from 'react'; | ||
import ProfileEdit from '@/components/ProfileEdit'; | ||
|
||
export default function ProfilePage() { | ||
return ( | ||
<div className="space-y-8"> | ||
<ProfileEdit /> | ||
</div> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,217 @@ | ||
'use client'; | ||
import React, { useState, useEffect } from 'react'; | ||
import { useForm } from 'react-hook-form'; | ||
import { z } from 'zod'; | ||
import { zodResolver } from '@hookform/resolvers/zod'; | ||
import Button from '@/ui/button'; | ||
import { Input } from '@/ui/input'; | ||
import { | ||
Form, | ||
FormField, | ||
FormItem, | ||
FormLabel, | ||
FormControl, | ||
FormMessage, | ||
} from '@/ui/Form'; | ||
|
||
// Define the validation schema using Zod | ||
const ProfileSchema = z.object({ | ||
email: z.string().email({ message: '無效的電子郵件地址' }), | ||
avatar: z | ||
.instanceof(FileList) | ||
.or(z.undefined()) | ||
.refine((fileList) => !fileList || fileList.length === 1, { | ||
message: '請選擇一個圖片檔案', | ||
}) | ||
.refine( | ||
(fileList) => { | ||
if (!fileList) return true; | ||
const file = fileList[0]; | ||
return file.type.startsWith('image/'); | ||
}, | ||
{ message: '請上傳有效的圖片檔案' }, | ||
), | ||
username: z | ||
.string() | ||
.min(1, { message: '使用者名稱為必填項目' }) | ||
.max(20, { message: '使用者名稱不得超過 20 個字元' }) | ||
.regex(/^[a-zA-Z0-9_]+$/, { message: '使用者名稱僅允許字母、數字和底線' }), | ||
}); | ||
|
||
type ProfileFormData = z.infer<typeof ProfileSchema>; | ||
|
||
export default function ProfileEdit() { | ||
const { | ||
register, | ||
handleSubmit, | ||
setValue, | ||
formState: { errors, isSubmitting }, | ||
} = useForm<ProfileFormData>({ | ||
resolver: zodResolver(ProfileSchema), | ||
defaultValues: { | ||
email: '', | ||
username: '', | ||
avatar: undefined, | ||
}, | ||
}); | ||
|
||
const [isLoading, setIsLoading] = useState(true); | ||
useEffect(() => { | ||
let mounted = true; | ||
const fetchUserData = async () => { | ||
try { | ||
setIsLoading(true); | ||
const userData = await mockFetchUserData(); | ||
if (!mounted) return; | ||
setValue('email', userData.email); | ||
setValue('username', userData.username); | ||
} catch (error) { | ||
console.error('Failed to fetch user data:', error); | ||
// Use proper error notification system | ||
} finally { | ||
if (mounted) setIsLoading(false); | ||
} | ||
}; | ||
fetchUserData(); | ||
return () => { | ||
mounted = false; | ||
}; | ||
}, [setValue]); | ||
Comment on lines
+58
to
+79
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add error state handling for failed data fetches While the loading state is handled well, there's no UI feedback for fetch errors. export default function ProfileEdit() {
+ const [error, setError] = useState<string | null>(null);
// ...
const fetchUserData = async () => {
try {
setIsLoading(true);
+ setError(null);
const userData = await mockFetchUserData();
if (!mounted) return;
setValue('email', userData.email);
setValue('username', userData.username);
} catch (error) {
console.error('Failed to fetch user data:', error);
+ if (mounted) {
+ setError('Failed to load profile data. Please try again later.');
+ }
} finally {
if (mounted) setIsLoading(false);
}
};
// ...
+ if (error) {
+ return <div role="alert" className="text-red-500">{error}</div>;
+ }
|
||
|
||
const onSubmit = async (data: ProfileFormData) => { | ||
try { | ||
const validatedData = ProfileSchema.parse(data); | ||
let updates = []; | ||
|
||
if (data.avatar && data.avatar.length > 0) { | ||
updates.push(mockUploadAvatar(data.avatar[0])); | ||
} | ||
|
||
updates.push(mockUpdateUsername(data.username)); | ||
|
||
// Wait for all async operations to complete | ||
await Promise.all(updates); | ||
|
||
// Use proper toast/notification system instead of alert | ||
console.log('驗證通過:', validatedData); | ||
// Example: Toast.success('個人資料更新成功!'); | ||
} catch (error) { | ||
console.error(error); | ||
|
||
if (error instanceof z.ZodError) { | ||
console.error('Validation error:', error.errors); | ||
} else if (error instanceof Error) { | ||
console.error('Update error:', error.message); | ||
} | ||
|
||
// Example: Toast.error('更新個人資料失敗'); | ||
} | ||
}; | ||
|
||
return ( | ||
<Form> | ||
<form | ||
onSubmit={handleSubmit(onSubmit)} | ||
className="space-y-4" | ||
aria-busy={isSubmitting} | ||
> | ||
{/* Email 欄位(僅顯示,不可編輯) */} | ||
<FormField> | ||
<FormItem> | ||
<FormLabel htmlFor="email">Email</FormLabel> | ||
<FormControl> | ||
<Input | ||
id="email" | ||
type="email" | ||
{...register('email')} | ||
disabled | ||
aria-readonly="true" | ||
aria-label="Email address" | ||
className="mt-1 block w-full rounded border border-gray-300 p-2 text-black" | ||
/> | ||
</FormControl> | ||
<FormMessage role="alert">{errors.email?.message}</FormMessage> | ||
</FormItem> | ||
</FormField> | ||
|
||
{/* Avatar 上傳欄位 */} | ||
<FormField> | ||
<FormItem> | ||
<FormLabel htmlFor="avatar">Avatar</FormLabel> | ||
<FormControl> | ||
<input | ||
type="file" | ||
accept="image/*" | ||
{...register('avatar')} | ||
disabled={isSubmitting} | ||
className="mt-1 block w-full rounded border border-gray-300 p-2" | ||
/> | ||
</FormControl> | ||
<FormMessage>{errors.avatar?.message}</FormMessage> | ||
</FormItem> | ||
</FormField> | ||
|
||
{/* Username 編輯欄位 */} | ||
<FormField> | ||
<FormItem> | ||
<FormLabel htmlFor="username">Username</FormLabel> | ||
<FormControl> | ||
<Input | ||
className="mt-1 block w-full rounded border border-gray-300 p-2 text-black" | ||
type="text" | ||
{...register('username')} | ||
disabled={isSubmitting} | ||
placeholder="輸入您的使用者名稱" | ||
/> | ||
</FormControl> | ||
<FormMessage>{errors.username?.message}</FormMessage> | ||
</FormItem> | ||
</FormField> | ||
|
||
{/* 提交按鈕 */} | ||
<Button type="submit" disabled={isSubmitting}> | ||
{isSubmitting ? ( | ||
<> | ||
<span className="loading-spinner" aria-hidden="true" /> | ||
<span>儲存中...</span> | ||
</> | ||
) : ( | ||
'儲存變更' | ||
)} | ||
</Button> | ||
</form> | ||
</Form> | ||
); | ||
Comment on lines
+111
to
+184
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add loading state UI for initial data fetch While the form submission loading state is handled, the initial data fetch loading state isn't visible to users. return (
+ {isLoading ? (
+ <div role="status" className="animate-pulse space-y-4">
+ <div className="h-10 bg-gray-200 rounded"></div>
+ <div className="h-10 bg-gray-200 rounded"></div>
+ <div className="h-10 bg-gray-200 rounded"></div>
+ </div>
+ ) : (
<Form>
<form
onSubmit={handleSubmit(onSubmit)}
className="space-y-4"
aria-busy={isSubmitting}
>
{/* ... form fields ... */}
</form>
</Form>
+ )}
);
|
||
} | ||
|
||
// 模擬 Avatar 上傳 API | ||
const mockUploadAvatar = (file: File): Promise<void> => { | ||
return new Promise((resolve) => { | ||
setTimeout(() => { | ||
console.log('Avatar uploaded:', file); | ||
resolve(); | ||
}, 2000); | ||
}); | ||
}; | ||
|
||
// 模擬 Username 更新 API | ||
const mockUpdateUsername = (username: string): Promise<void> => { | ||
return new Promise((resolve) => { | ||
setTimeout(() => { | ||
console.log('Username updated to:', username); | ||
resolve(); | ||
}, 1000); | ||
}); | ||
}; | ||
|
||
// 模擬獲取使用者資料 API | ||
const mockFetchUserData = (): Promise<{ email: string; username: string }> => { | ||
return new Promise((resolve) => { | ||
setTimeout(() => { | ||
resolve({ | ||
email: '[email protected]', | ||
username: 'user_name', | ||
}); | ||
}, 1000); | ||
}); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import React from 'react'; | ||
import Link from 'next/link'; | ||
import { usePathname } from 'next/navigation'; | ||
|
||
export default function ProfileSidebar() { | ||
const pathname = usePathname(); | ||
|
||
// Function to determine the active class based on the current path | ||
const getLinkClassName = (path: string) => ` | ||
block rounded px-4 py-2 | ||
${pathname === path ? 'bg-gray-700' : 'hover:bg-gray-700'} | ||
`; | ||
|
||
return ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 這個也希望可以把資料抽到別的檔案 然後以xxx.map的方式來引入 |
||
<aside className="w-64 bg-gray-800 p-4 text-white"> | ||
<nav className="space-y-2"> | ||
<Link href="/profile" className={getLinkClassName('/profile')}> | ||
編輯個人資料 | ||
</Link> | ||
<Link | ||
href="/profile/uploaded-exams" | ||
className={getLinkClassName('/profile/uploaded-exams')} | ||
> | ||
上傳的考題清單 | ||
</Link> | ||
<Link | ||
href="/profile/starred-exams" | ||
className={getLinkClassName('/profile/starred-exams')} | ||
> | ||
收藏的考題清單 | ||
</Link> | ||
</nav> | ||
</aside> | ||
); | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit pick: zod schema 等 zod相關的東西 可以放入
module/zod/xxxSchema.ts
之中,未來會比較好管理所有驗證的部分