-
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
Conversation
📝 Walkthrough📝 WalkthroughWalkthroughThis pull request introduces several new components for the profile section of the frontend application. Notable additions include Changes
Possibly related PRs
Suggested reviewers
Poem
Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media? 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
Documentation and Community
|
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.
Actionable comments posted: 5
🧹 Outside diff range and nitpick comments (11)
frontend/app/profile/layout.tsx (2)
11-14
: Enhance accessibility with semantic HTML elementsConsider using semantic HTML elements to improve accessibility and SEO:
- <div className="flex"> + <div className="flex" role="main"> <ProfileSidebar /> - <main className="flex-1 p-4">{children}</main> + <article className="flex-1 p-4">{children}</article> </div>
11-14
: Consider adding responsive design supportThe current layout might not work well on mobile devices where a full-width sidebar isn't ideal.
Consider:
- Making the sidebar collapsible on mobile
- Adding media queries to adjust layout for different screen sizes
- Using a hamburger menu for mobile navigation
Would you like me to provide an example implementation of these responsive features?
frontend/ui/input.tsx (2)
6-6
: Maintain consistent language in commentsThe comment is in Chinese while the codebase appears to be primarily in English.
-// 使用 React.forwardRef 來轉發 ref +// Using React.forwardRef to forward the ref to the input element
4-4
: Enhance type safety for className propConsider creating a more specific interface for the Input component's props.
-interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {} +interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> { + // Add specific prop types here + error?: boolean; + helperText?: string; +}frontend/components/ProfileSidebar.tsx (2)
12-25
: Implement i18n for text contentThe navigation text is hard-coded in Chinese. Consider implementing internationalization for better maintainability and future language support.
Example using a translation hook:
+import { useTranslations } from 'next-intl'; + export default function ProfileSidebar() { + const t = useTranslations('profile'); return ( // ... - 編輯個人資料 + {t('edit_profile')} // ... - 上傳的考題清單 + {t('uploaded_exams')} // ... - 收藏的考題清單 + {t('starred_exams')}
6-6
: Consider making the width configurableThe sidebar width is hard-coded to
w-64
. Consider making it configurable through props for better flexibility.+interface ProfileSidebarProps { + width?: 'sm' | 'md' | 'lg'; +} + -export default function ProfileSidebar() { +export default function ProfileSidebar({ width = 'md' }: ProfileSidebarProps) { + const widthClasses = { + sm: 'w-48', + md: 'w-64', + lg: 'w-80' + }; + return ( - <aside className="w-64 bg-gray-800 p-4 text-white"> + <aside className={`${widthClasses[width]} bg-gray-800 p-4 text-white`}>frontend/ui/Form.tsx (2)
7-17
: Add aria-labels for better accessibilityFormField and FormItem components should include ARIA attributes to improve accessibility for screen readers.
export const FormField: React.FC<{ children: React.ReactNode }> = ({ children, }) => { - return <div>{children}</div>; + return <div role="group" aria-label="form field">{children}</div>; }; export const FormItem: React.FC<{ children: React.ReactNode }> = ({ children, }) => { - return <div>{children}</div>; + return <div role="group" aria-label="form item">{children}</div>; };
35-39
: Add role="alert" for error messagesFormMessage component should include the role="alert" attribute to improve accessibility for error messages.
export const FormMessage: React.FC<{ children?: React.ReactNode }> = ({ children, }) => { - return <p className="text-sm text-red-500">{children}</p>; + return <p role="alert" className="text-sm text-red-500">{children}</p>; };frontend/package.json (1)
Update react-hook-form to latest version 7.54.0
The dependency analysis shows:
react-hook-form
can be safely updated from 7.53.2 to 7.54.0
- Compatible with the project's React version (18.2.0)
- Compatible with @hookform/resolvers which requires ^7.0.0
zod
3.23.8 is secure (above the vulnerable version range)@hookform/resolvers
is at the latest version (3.9.1)🔗 Analysis chain
Line range hint
26-40
: Verify compatibility and check for security updatesThe newly added dependencies have fixed versions:
Let's verify these versions for compatibility and security updates.
Update dependencies to address security vulnerability and version mismatch
react-hook-form
can be updated from 7.53.2 to 7.54.0zod
version 3.23.8 is secure as it's higher than the vulnerable version range (<= 3.22.2)@hookform/resolvers
is at the latest version (3.9.1)Let's verify the compatibility between these packages before recommending the update:
Let me check the peer dependencies directly from the npm registry:
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash # Description: Check for latest versions and security advisories # Check latest versions echo "Checking latest versions..." for pkg in "@hookform/resolvers" "react-hook-form" "zod"; do echo "Package: $pkg" curl -s "https://registry.npmjs.org/$pkg" | jq -r '.["dist-tags"].latest' done # Check for security advisories echo "Checking security advisories..." for pkg in "@hookform/resolvers" "react-hook-form" "zod"; do echo "Package: $pkg" gh api graphql -f query='{ securityVulnerabilities(first:5, ecosystem:NPM, package:"'$pkg'") { nodes { advisory { summary severity publishedAt } vulnerableVersionRange firstPatchedVersion { identifier } } } }' doneLength of output: 2393
Script:
#!/bin/bash # Check package.json files in node_modules for peer dependencies echo "Checking @hookform/resolvers peer dependencies..." cat node_modules/@hookform/resolvers/package.json | jq '.peerDependencies' echo "Checking react-hook-form peer dependencies..." cat node_modules/react-hook-form/package.json | jq '.peerDependencies' echo "Checking zod peer dependencies..." cat node_modules/zod/package.json | jq '.peerDependencies'Length of output: 705
Script:
#!/bin/bash # Check peer dependencies from npm registry echo "Checking @hookform/[email protected] peer dependencies..." curl -s "https://registry.npmjs.org/@hookform/resolvers/3.9.1" | jq '.peerDependencies' echo "Checking [email protected] peer dependencies..." curl -s "https://registry.npmjs.org/react-hook-form/7.54.0" | jq '.peerDependencies' echo "Checking [email protected] peer dependencies..." curl -s "https://registry.npmjs.org/zod/3.23.8" | jq '.peerDependencies'Length of output: 651
frontend/components/ProfileEdit.tsx (2)
18-36
: Consider adding more comprehensive validation rulesThe schema validation could be enhanced with additional rules:
- Email domain validation
- File type validation for avatar
- Username reserved words check
const ProfileSchema = z.object({ email: z.string() .email({ message: '無效的電子郵件地址' }) + .refine((email) => { + const domain = email.split('@')[1]; + return !['example.com', 'test.com'].includes(domain); + }, { message: '不允許使用測試域名' }), avatar: z .instanceof(FileList) .or(z.undefined()) .refine( (fileList) => !fileList || fileList.length === 0 || - (fileList[0] && fileList[0].size <= 5 * 1024 * 1024), + (fileList[0] && + fileList[0].size <= 5 * 1024 * 1024 && + ['image/jpeg', 'image/png', 'image/gif'].includes(fileList[0].type)), - { message: '檔案大小不得超過 5MB' }, + { message: '檔案必須是 JPG、PNG 或 GIF 格式,且大小不得超過 5MB' }, ), username: z .string() .min(1, { message: '使用者名稱為必填項目' }) .max(20, { message: '使用者名稱不得超過 20 個字元' }) .regex(/^[a-zA-Z0-9_]+$/, { message: '使用者名稱僅允許字母、數字和底線' }) + .refine( + (username) => !['admin', 'root', 'system'].includes(username.toLowerCase()), + { message: '此使用者名稱已被保留' } + ), });
147-176
: Add error simulation to mock functionsThe mock API functions should simulate error scenarios for better testing.
const mockUploadAvatar = (file: File): Promise<void> => { return new Promise((resolve, reject) => { setTimeout(() => { + // Simulate random errors + if (Math.random() < 0.2) { + reject(new Error('Failed to upload avatar')); + return; + } console.log('Avatar uploaded:', file); resolve(); }, 2000); }); }; const mockUpdateUsername = (username: string): Promise<void> => { return new Promise((resolve, reject) => { setTimeout(() => { + // Simulate conflict errors + if (username.toLowerCase() === 'taken') { + reject(new Error('Username already taken')); + return; + } console.log('Username updated to:', username); resolve(); }, 1000); }); }; const mockFetchUserData = (): Promise<{ email: string; username: string }> => { return new Promise((resolve, reject) => { setTimeout(() => { + // Simulate authentication errors + if (Math.random() < 0.1) { + reject(new Error('Authentication failed')); + return; + } resolve({ email: '[email protected]', username: 'user_name', }); }, 1000); }); };
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
⛔ Files ignored due to path filters (1)
frontend/pnpm-lock.yaml
is excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (7)
frontend/app/profile/layout.tsx
(1 hunks)frontend/app/profile/page.tsx
(1 hunks)frontend/components/ProfileEdit.tsx
(1 hunks)frontend/components/ProfileSidebar.tsx
(1 hunks)frontend/package.json
(2 hunks)frontend/ui/Form.tsx
(1 hunks)frontend/ui/input.tsx
(1 hunks)
✅ Files skipped from review due to trivial changes (1)
- frontend/app/profile/page.tsx
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.
Actionable comments posted: 3
🧹 Outside diff range and nitpick comments (2)
frontend/components/ProfileEdit.tsx (2)
98-112
: Add avatar preview functionalityConsider adding an avatar preview to improve user experience when uploading profile pictures.
+ const [avatarPreview, setAvatarPreview] = useState<string | null>(null); + + const handleAvatarChange = (e: React.ChangeEvent<HTMLInputElement>) => { + const file = e.target.files?.[0]; + if (file) { + const reader = new FileReader(); + reader.onloadend = () => { + setAvatarPreview(reader.result as string); + }; + reader.readAsDataURL(file); + } else { + setAvatarPreview(null); + } + }; <FormField> <FormItem> <FormLabel>Avatar</FormLabel> + {avatarPreview && ( + <div className="mt-2"> + <img + src={avatarPreview} + alt="Avatar preview" + className="h-20 w-20 rounded-full object-cover" + /> + </div> + )} <FormControl> <input type="file" accept="image/*" - {...register('avatar')} + {...register('avatar', { + onChange: handleAvatarChange + })} className="mt-1 block w-full rounded border border-gray-300 p-2" /> </FormControl> <FormMessage>{errors.avatar?.message}</FormMessage> </FormItem> </FormField>
139-169
: Enhance mock API implementationsThe current mock implementations don't adequately simulate real-world scenarios. Consider adding error cases, input validation, and variable network conditions.
const mockUploadAvatar = (file: File): Promise<void> => { return new Promise((resolve, reject) => { + // Simulate network conditions + const latency = Math.random() * 2000 + 1000; + // Simulate random failures + const shouldFail = Math.random() < 0.1; + + // Validate file size (e.g., max 5MB) + if (file.size > 5 * 1024 * 1024) { + reject(new Error('File size exceeds 5MB limit')); + return; + } + setTimeout(() => { + if (shouldFail) { + reject(new Error('Network error')); + return; + } console.log('Avatar uploaded:', file); resolve(); - }, 2000); + }, latency); }); }; const mockUpdateUsername = (username: string): Promise<void> => { return new Promise((resolve, reject) => { + // Simulate network conditions + const latency = Math.random() * 1000 + 500; + // Simulate random failures + const shouldFail = Math.random() < 0.1; + + // Validate username format + if (!/^[a-zA-Z0-9_]+$/.test(username)) { + reject(new Error('Invalid username format')); + return; + } + setTimeout(() => { + if (shouldFail) { + reject(new Error('Network error')); + return; + } console.log('Username updated to:', username); resolve(); - }, 1000); + }, latency); }); };
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
📒 Files selected for processing (2)
frontend/components/ProfileEdit.tsx
(1 hunks)frontend/components/ProfileSidebar.tsx
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- frontend/components/ProfileSidebar.tsx
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.
Actionable comments posted: 2
🧹 Outside diff range and nitpick comments (5)
frontend/ui/Form.tsx (3)
3-5
: Consider enhancing Form component with additional propsThe Form component could be more flexible by accepting and forwarding common form props like
onSubmit
,className
, etc.-export const Form: React.FC<{ children: React.ReactNode }> = ({ children }) => { - return <form>{children}</form>; +export const Form: React.FC<React.ComponentPropsWithoutRef<'form'>> = ({ + children, + ...props +}) => { + return <form {...props}>{children}</form>; };
7-17
: Consider consolidating or differentiating FormField and FormItemBoth components are identical div wrappers. Consider either:
- Consolidating them into a single component
- Adding distinct roles and styling to differentiate their purposes
export const FormField: React.FC<{ children: React.ReactNode }> = ({ children, + ...props }) => { - return <div>{children}</div>; + return <div role="group" {...props}>{children}</div>; }; export const FormItem: React.FC<{ children: React.ReactNode }> = ({ children, + ...props }) => { - return <div>{children}</div>; + return <div className="form-item space-y-2" {...props}>{children}</div>; };
44-44
: Translate comment to English for consistency- role = 'alert', // 默認值設置為 "alert" + role = 'alert', // Default value set to "alert"frontend/components/ProfileEdit.tsx (2)
81-109
: Add retry mechanism for failed updatesWhile the error handling is good, consider adding a retry mechanism for failed API calls.
+const MAX_RETRIES = 3; +const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + +const withRetry = async <T>( + fn: () => Promise<T>, + retries = MAX_RETRIES +): Promise<T> => { + try { + return await fn(); + } catch (error) { + if (retries > 0) { + await wait(1000); + return withRetry(fn, retries - 1); + } + throw error; + } +}; 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(withRetry(() => mockUploadAvatar(data.avatar[0]))); } - updates.push(mockUpdateUsername(data.username)); + updates.push(withRetry(() => mockUpdateUsername(data.username))); await Promise.all(updates); // ...
187-217
: Add type definitions for mock API functionsConsider adding type definitions to better document the mock API interface.
+interface UserData { + email: string; + username: string; +} + +interface ApiResponse<T> { + data: T; + status: number; +} -const mockUploadAvatar = (file: File): Promise<void> => { +const mockUploadAvatar = (file: File): Promise<ApiResponse<void>> => { return new Promise((resolve) => { setTimeout(() => { console.log('Avatar uploaded:', file); - resolve(); + resolve({ data: undefined, status: 200 }); }, 2000); }); }; -const mockUpdateUsername = (username: string): Promise<void> => { +const mockUpdateUsername = (username: string): Promise<ApiResponse<void>> => { // ... similar changes ... }; -const mockFetchUserData = (): Promise<{ email: string; username: string }> => { +const mockFetchUserData = (): Promise<ApiResponse<UserData>> => { // ... similar changes ... };
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
📒 Files selected for processing (5)
frontend/app/profile/layout.tsx
(1 hunks)frontend/components/ProfileEdit.tsx
(1 hunks)frontend/components/ProfileSidebar.tsx
(1 hunks)frontend/ui/Form.tsx
(1 hunks)frontend/ui/input.tsx
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- frontend/app/profile/layout.tsx
- frontend/ui/input.tsx
- frontend/components/ProfileSidebar.tsx
🔇 Additional comments (2)
frontend/ui/Form.tsx (1)
19-31
: LGTM! Accessible label implementation
The FormLabel component correctly implements accessibility with htmlFor prop and appropriate styling.
frontend/components/ProfileEdit.tsx (1)
1-39
: LGTM! Well-structured schema validation
The imports are well-organized and the Zod schema implements comprehensive validation for email, avatar, and username fields.
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]); |
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.
🛠️ 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>;
+ }
Committable suggestion skipped: line range outside the PR's diff.
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> | ||
); |
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.
🛠️ 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>
+ )}
);
Committable suggestion skipped: line range outside the PR's diff.
} from '@/ui/Form'; | ||
|
||
// Define the validation schema using Zod | ||
const ProfileSchema = z.object({ |
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
之中,未來會比較好管理所有驗證的部分
${pathname === path ? 'bg-gray-700' : 'hover:bg-gray-700'} | ||
`; | ||
|
||
return ( |
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.
這個也希望可以把資料抽到別的檔案 然後以xxx.map的方式來引入
Summary
簡單說一下前因後果,這支PR的目的
實作UserPage
Profile Edit
Uploaded Exam List
Starred Exam List
的三個功能,api為demo的方式呈現
How Has This Been Tested?
附上如何測試的以及測試結果



This pull request introduces a new profile layout and functionality for editing user profiles, along with necessary updates to dependencies. The changes include creating new components for the profile page, sidebar, and form handling, as well as adding new dependencies for form validation and management.
New components and layout:
frontend/app/profile/layout.tsx
: Created a newProfileLayout
component that includes a sidebar and main content area.frontend/app/profile/page.tsx
: Added a newProfilePage
component that includes theProfileEdit
component.frontend/components/ProfileSidebar.tsx
: Implemented aProfileSidebar
component with navigation links.frontend/components/ProfileEdit.tsx
: Developed aProfileEdit
component for editing user profiles, including form validation usingzod
andreact-hook-form
.Form handling and UI components:
frontend/ui/Form.tsx
: Added form-related components (Form
,FormField
,FormItem
,FormLabel
,FormControl
,FormMessage
) for structured form creation.frontend/ui/input.tsx
: Created anInput
component usingReact.forwardRef
for handling input fields.Dependency updates:
frontend/package.json
: Added@hookform/resolvers
,react-hook-form
, andzod
dependencies to support form validation and management.Task:
如果這隻 PR 是為了修某個 Jira 的 ticket, 把 link 貼在這
Summary by CodeRabbit
Release Notes
New Features
ProfileLayout
,ProfilePage
, andProfileSidebar
components for enhanced profile management.ProfileEdit
component for user profile editing with validation.Form
,FormField
,FormItem
,FormLabel
,FormControl
, andFormMessage
.Input
component for flexible input handling.Dependencies
@hookform/resolvers
,react-hook-form
, andzod
for form management and validation.