-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
added user setting page, enable profile update (#31)
Signed-off-by: owenowenisme <[email protected]>
- Loading branch information
1 parent
0393112
commit 41dfa30
Showing
15 changed files
with
505 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
'use client'; | ||
import { useAuthentication } from '@/hooks/useAuthentication'; | ||
import { useRouter } from 'next/navigation'; | ||
import { useEffect } from 'react'; | ||
import Image from 'next/image'; | ||
import { Button } from '@/ui/Button'; | ||
import { Icon } from '@/ui/icons'; | ||
export default function UserLayout({ | ||
children, | ||
}: { | ||
children: React.ReactNode; | ||
}) { | ||
const { currentUser, handleRefreshProfile } = useAuthentication(); | ||
const router = useRouter(); | ||
|
||
useEffect(() => { | ||
if (!currentUser) { | ||
handleRefreshProfile(); | ||
} | ||
}, []); | ||
|
||
if (!currentUser) { | ||
return ( | ||
<div className="flex h-screen items-center justify-center"> | ||
<p>請先登入</p> | ||
</div> | ||
); | ||
} | ||
const UserBar = () => { | ||
return ( | ||
<div className="bg-white p-6 shadow-sm"> | ||
<div> | ||
<div className="mb-4 flex items-start space-x-4"> | ||
<div className="relative flex h-24 w-24 items-center"> | ||
<Image | ||
src={currentUser?.avatar || ''} | ||
alt="Profile" | ||
fill | ||
className="rounded-full object-cover" | ||
/> | ||
</div> | ||
<div className="flex min-h-[96px] flex-1 items-center"> | ||
<div className="my-auto space-y-2"> | ||
<div className="text-mb-4 text-h2">{currentUser?.userName}</div> | ||
<div className="flex items-center space-x-2 text-secondary-500 text-subtle"> | ||
<Icon name="user" size={16} /> | ||
科系{' '} | ||
{currentUser?.department == null | ||
? '尚未設定' | ||
: currentUser?.department} | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
<div className="flex items-center space-x-4"> | ||
<Button | ||
variant="link" | ||
className="text-large-content" | ||
onClick={() => router.push('/user/upload')} | ||
> | ||
上傳過的考古題 | ||
</Button> | ||
<div className="text-gray-300 text-large-content">|</div> | ||
<Button | ||
variant="link" | ||
className="text-large-content" | ||
onClick={() => router.push('/user/bookmark')} | ||
> | ||
收藏的考古題 | ||
</Button> | ||
<div className="text-gray-300 text-large-content">|</div> | ||
<Button | ||
variant="link" | ||
className="text-large-content" | ||
onClick={() => router.push('/user/setting')} | ||
> | ||
個人設定 | ||
</Button> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
return ( | ||
<div className="container mx-auto mt-24 px-4"> | ||
<UserBar /> | ||
{children} | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
'use client'; | ||
import { useAuthentication } from '@/hooks/useAuthentication'; | ||
import { useRouter } from 'next/navigation'; | ||
import { useEffect } from 'react'; | ||
import { Button } from '@/ui/Button'; | ||
import Image from 'next/image'; | ||
|
||
export default function SettingPage() { | ||
const { currentUser, handleRefreshProfile } = useAuthentication(); | ||
const router = useRouter(); | ||
|
||
useEffect(() => { | ||
if (!currentUser) { | ||
handleRefreshProfile(); | ||
} | ||
}, []); | ||
|
||
if (!currentUser) { | ||
return ( | ||
<div className="flex h-screen items-center justify-center"> | ||
<p>請先登入</p> | ||
</div> | ||
); | ||
} | ||
|
||
return ( | ||
<div className="container mx-auto mt-24 px-4"> | ||
<h1 className="mb-8 text-2xl font-bold">個人設定</h1> | ||
|
||
<div className="rounded-lg border border-gray-200 bg-white p-6 shadow-sm"> | ||
<div className="mb-6"> | ||
<h2 className="mb-4 text-xl font-semibold">個人資料</h2> | ||
<div className="flex items-start space-x-4"> | ||
<div className="relative h-24 w-24"> | ||
<Image | ||
src={currentUser.avatar} | ||
alt="Profile" | ||
fill | ||
className="rounded-full object-cover" | ||
/> | ||
</div> | ||
<div className="flex-1"> | ||
<div className="mb-4"> | ||
<label className="mb-1 block text-sm font-medium text-gray-700"> | ||
使用者名稱 | ||
</label> | ||
<p className="text-gray-900">{currentUser.userName}</p> | ||
</div> | ||
<div className="mb-4"> | ||
<label className="mb-1 block text-sm font-medium text-gray-700"> | ||
電子郵件 | ||
</label> | ||
<p className="text-gray-900">{currentUser.email}</p> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
|
||
<div className="border-t pt-6"> | ||
<h2 className="mb-4 text-xl font-semibold">帳號設定</h2> | ||
<div className="space-y-4"> | ||
<Button | ||
className="w-full justify-start text-red-600 hover:bg-red-50 hover:text-red-700" | ||
onClick={() => { | ||
// TODO: Implement delete account functionality | ||
console.log('Delete account clicked'); | ||
}} | ||
> | ||
刪除帳號 | ||
</Button> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
'use client'; | ||
import { useAuthentication } from '@/hooks/useAuthentication'; | ||
import { Input } from '@/ui/Input'; | ||
import { Label } from '@/ui/Label'; | ||
import { Button } from '@/ui/Button'; | ||
import Image from 'next/image'; | ||
import { useState } from 'react'; | ||
import { z } from 'zod'; | ||
import { userAPI } from '@/module/api'; | ||
import { toast } from 'sonner'; | ||
|
||
const userSchema = z.object({ | ||
userName: z.string().min(1, '姓名不能為空').max(50, '姓名不能超過50個字'), | ||
department: z.string().min(1, '系所不能為空').max(100, '系所不能超過100個字'), | ||
}); | ||
|
||
export default function SettingPage() { | ||
const { currentUser, handleRefreshProfile } = useAuthentication(); | ||
const [userName, setUserName] = useState(currentUser?.userName || ''); | ||
const [department, setDepartment] = useState(currentUser?.department || ''); | ||
const [errors, setErrors] = useState<{ | ||
userName?: string; | ||
department?: string; | ||
}>({}); | ||
|
||
const validateField = (field: 'userName' | 'department', value: string) => { | ||
try { | ||
userSchema.shape[field].parse(value); | ||
setErrors((prev) => ({ ...prev, [field]: undefined })); | ||
} catch (error) { | ||
if (error instanceof z.ZodError) { | ||
setErrors((prev) => ({ ...prev, [field]: error.errors[0].message })); | ||
} | ||
} | ||
}; | ||
|
||
const handleSave = async () => { | ||
const res = await userAPI.updateProfile({ | ||
username: userName, | ||
department: department, | ||
}); | ||
if (res.data.status === 'success') { | ||
toast('修改成功'); | ||
handleRefreshProfile(); | ||
} else { | ||
toast('修改失敗'); | ||
} | ||
}; | ||
|
||
return ( | ||
<div className="container mx-auto mt-24 flex flex-col gap-16 px-16"> | ||
<div className="flex gap-8"> | ||
<div className="flex w-1/2 flex-col gap-8"> | ||
<div className="flex flex-col gap-2"> | ||
<Label>姓名</Label> | ||
<Input | ||
placeholder="請輸入姓名" | ||
value={userName} | ||
onChange={(e) => { | ||
setUserName(e.target.value); | ||
validateField('userName', e.target.value); | ||
}} | ||
variant={errors.userName ? 'error' : 'default'} | ||
className="w-full" | ||
/> | ||
{errors.userName && ( | ||
<span className="text-sm text-red-500">{errors.userName}</span> | ||
)} | ||
</div> | ||
<div className="flex flex-col gap-2"> | ||
<Label>系所</Label> | ||
<Input | ||
placeholder="請輸入系所" | ||
value={department} | ||
onChange={(e) => { | ||
setDepartment(e.target.value); | ||
validateField('department', e.target.value); | ||
}} | ||
variant={errors.department ? 'error' : 'default'} | ||
className="w-full" | ||
/> | ||
{errors.department && ( | ||
<span className="text-sm text-red-500">{errors.department}</span> | ||
)} | ||
</div> | ||
<div className="flex flex-col gap-2"> | ||
<Label className="w-full">Email</Label> | ||
<Input | ||
value={currentUser?.email || ''} | ||
disabled={true} | ||
className="w-full" | ||
/> | ||
</div> | ||
</div> | ||
<div className="w-1/2"> | ||
<div className="flex h-full flex-col items-center gap-4"> | ||
<div className="relative"> | ||
<Image | ||
src={currentUser?.avatar || ''} | ||
alt="user" | ||
width={250} | ||
height={250} | ||
className="rounded-lg" | ||
/> | ||
<button className="absolute inset-x-0 bottom-0 flex h-1/5 items-center justify-center bg-black/30 text-white transition-opacity duration-200"> | ||
更換頭像 | ||
</button> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
<Button | ||
className="w-1/6 self-end" | ||
onClick={handleSave} | ||
disabled={!!errors.userName || !!errors.department} | ||
> | ||
完成修改 | ||
</Button> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -37,6 +37,7 @@ const nextConfig = { | |
pathname: '/**', | ||
}, | ||
], | ||
domains: ['lh3.googleusercontent.com'], | ||
}, | ||
}; | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.