diff --git a/components/Profile/Edit/Edit.styled.jsx b/components/Profile/Edit/Edit.styled.jsx index 70290b31..ce9d6676 100644 --- a/components/Profile/Edit/Edit.styled.jsx +++ b/components/Profile/Edit/Edit.styled.jsx @@ -1,7 +1,7 @@ import styled from '@emotion/styled'; import { Box, Typography, Button } from '@mui/material'; -export const HomePageWrapper = styled.div` +export const FormWrapper = styled.form` --section-height: calc(100vh - 80px); --section-height-offset: 80px; `; @@ -39,7 +39,7 @@ export const StyledTitleWrap = styled(Box)` text-align: center; color: #536166; } - p { + .title-memo { font-weight: 700; font-size: 14px; line-height: 140%; diff --git a/components/Profile/Edit/EditFormInput.jsx b/components/Profile/Edit/EditFormInput.jsx new file mode 100644 index 00000000..1b8bf5f3 --- /dev/null +++ b/components/Profile/Edit/EditFormInput.jsx @@ -0,0 +1,31 @@ +import { Typography, TextField } from '@mui/material'; +import { StyledGroup } from './Edit.styled'; + +function EditFormInput({ + title = '', + parmKey = '', + value = '', + onChange = () => ({}), + errorMsg = '', + isRequire = false, + placeholder = '', +}) { + return ( + + + {title} {isRequire && '*'} + + onChange({ key: parmKey, value: e.target.value })} + error={!!errorMsg} + helperText={errorMsg} + /> + + ); +} + +export default EditFormInput; diff --git a/components/Profile/Edit/EditProfileConstant.js b/components/Profile/Edit/EditProfileConstant.js new file mode 100644 index 00000000..b674ff0a --- /dev/null +++ b/components/Profile/Edit/EditProfileConstant.js @@ -0,0 +1,21 @@ +export const NAME = 'name'; +export const PHOTO_URL = 'photoURL'; +export const BIRTHDAY = 'birthDay'; +export const GENDER = 'gender'; +export const ROLE_LIST = 'roleList'; +export const WANT_TO_DO_LIST = 'wantToDoList'; +export const INSTAGRAM = 'instagram'; +export const FACEBOOK = 'facebook'; +export const DISCORD = 'discord'; +export const LINE = 'line'; +export const EDUCATION_STAGE = 'educationStage'; +export const LOCATION = 'location'; +export const TAG_LIST = 'tagList'; +export const SELF_INTRODUCTION = 'selfIntroduction'; +export const SHARE = 'share'; +export const IS_OPEN_LOCATION = 'isOpenLocation'; +export const IS_OPEN_PROFILE = 'isOpenProfile'; +export const IS_LOADING_SUBMIT = 'isLoadingSubmit'; +export const COUNTRY = 'country'; +export const CITY = 'city'; +export const DISTRICT = 'district'; diff --git a/components/Profile/Edit/TheAvator.jsx b/components/Profile/Edit/TheAvator.jsx new file mode 100644 index 00000000..fdd6ebe3 --- /dev/null +++ b/components/Profile/Edit/TheAvator.jsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { LazyLoadImage } from 'react-lazy-load-image-component'; + +import { Skeleton } from '@mui/material'; + +const EditAvator = ({ + url = 'https://imgur.com/EADd1UD.png', + height = 128, + width = 128, +}) => { + return ( + + } + /> + ); +}; + +export default EditAvator; diff --git a/components/Profile/Edit/index.jsx b/components/Profile/Edit/index.jsx index ec4372a0..ffbd11b7 100644 --- a/components/Profile/Edit/index.jsx +++ b/components/Profile/Edit/index.jsx @@ -3,7 +3,7 @@ import toast from 'react-hot-toast'; import useMediaQuery from '@mui/material/useMediaQuery'; import { useRouter } from 'next/router'; import { useSelector } from 'react-redux'; -import { AREAS, TAIWAN_DISTRICT } from '@/constants/areas'; +import { TAIWAN_DISTRICT } from '@/constants/areas'; import COUNTIES from '@/constants/countries.json'; import { @@ -16,7 +16,6 @@ import { import { Box, Typography, - Skeleton, TextField, Switch, TextareaAutosize, @@ -24,16 +23,19 @@ import { Select, Grid, } from '@mui/material'; -import { LazyLoadImage } from 'react-lazy-load-image-component'; + import { MobileDatePicker } from '@mui/x-date-pickers/MobileDatePicker'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import SEOConfig from '@/shared/components/SEO'; import InputTags from '../InputTags'; +import TheAvator from './TheAvator'; +import FormInput from './EditFormInput'; + import useEditProfile from './useEditProfile'; import { - HomePageWrapper, + FormWrapper, ContentWrapper, StyledGroup, StyledSelectWrapper, @@ -49,10 +51,15 @@ import { function EditPage() { const mobileScreen = useMediaQuery('(max-width: 767px)'); - const router = useRouter(); - const { userState, onChangeHandler, onSubmit } = useEditProfile(); + const { + userState, + errors, + onChangeHandler, + onSubmit: onEditSumit, + } = useEditProfile(); + const user = useSelector((state) => state.user); useEffect(() => { @@ -79,10 +86,18 @@ function EditPage() { } }, [user]); - const onUpdateUser = async (successCallback) => { - await onSubmit({ id: user._id, email: user.email }); - toast.success('更新成功'); - successCallback(); + const onUpdateUser = () => { + if (Object.values(errors).length) { + toast.error('請修正錯誤'); + return; + } + const resultStatus = onEditSumit({ id: user._id, email: user.email }); + if (resultStatus) { + toast.success('更新成功'); + router.push('/profile'); + } else { + toast.error('更新失敗'); + } }; const SEOData = useMemo( @@ -100,7 +115,7 @@ function EditPage() { ); return ( - +

編輯個人頁面

-

填寫完整資訊可以幫助其他夥伴更了解你哦!

- - } - /> +

+ 填寫完整資訊可以幫助其他夥伴更了解你哦! +

+ - - 名稱 * - - onChangeHandler({ key: 'name', value: event.target.value }) - } - /> - + 生日 * @@ -321,7 +306,6 @@ function EditPage() { - {/* 聯絡方式 */} @@ -334,70 +318,23 @@ function EditPage() { - - - Instagram - { - onChangeHandler({ - key: 'instagram', - value: event.target.value, - }); - }} + {Object.entries({ + instagram: 'Instagram', + discord: 'Discord', + line: 'Line', + facebook: 'Facebook', + }).map(([key, title]) => ( + + - - - - - Discord - { - onChangeHandler({ - key: 'discord', - value: event.target.value, - }); - }} - placeholder="請填寫ID" - sx={{ width: '100%' }} - /> - - - - - Line - { - onChangeHandler({ - key: 'line', - value: event.target.value, - }); - }} - placeholder="請填寫ID" - sx={{ width: '100%' }} - /> - - - - - Facebook - { - onChangeHandler({ - key: 'facebook', - value: event.target.value, - }); - }} - placeholder="請填寫ID" - sx={{ width: '100%' }} - /> - - + + ))} @@ -524,18 +461,13 @@ function EditPage() { > 查看我的頁面 - { - onUpdateUser(() => router.push('/profile')); - }} - > + 儲存資料
-
+ ); } diff --git a/components/Profile/Edit/useEditProfile.jsx b/components/Profile/Edit/useEditProfile.jsx index 2bf04741..c63ef380 100644 --- a/components/Profile/Edit/useEditProfile.jsx +++ b/components/Profile/Edit/useEditProfile.jsx @@ -1,7 +1,8 @@ import dayjs from 'dayjs'; -import { useReducer } from 'react'; +import { useReducer, useState } from 'react'; import { useDispatch } from 'react-redux'; import { updateUser } from '@/redux/actions/user'; +import { z } from 'zod'; const initialState = { name: '', @@ -27,6 +28,53 @@ const initialState = { district: '', }; +const buildValidator = (maxLength, regex, maxMsg, regMsg) => + z.string().max(maxLength, maxMsg).regex(regex, regMsg).optional(); + +const tempSchema = Object.keys(initialState).reduce((acc, key) => { + return key !== 'birthDay' + ? { + ...acc, + [key]: z.string().optional(), + } + : acc; +}, {}); + +const schema = z.object({ + ...tempSchema, + name: z + .string() + .min(1, { message: '請輸入名字' }) + .max(50, { message: '名字過長' }) + .optional(), + isOpenLocation: z.boolean().optional(), + isOpenProfile: z.boolean().optional(), + instagram: buildValidator( + 30, + /^($|[a-zA-Z0-9_.]{2,20})$/, + '長度最多30個字元', + '長度最少2個字元,支援英文、數字、底線、句號', + ), + facebook: buildValidator( + 64, + /^($|[a-zA-Z0-9_.]{5,20})$/, + '長度最多64個字元', + '長度最少5個字元,支援英文、數字、底線、句號', + ), + discord: buildValidator( + 32, + /^($|[a-zA-Z0-9_.]{2,20})$/, + '長度最多32個字元', + '長度最少2個字元,支援英文、數字、底線、句號', + ), + line: buildValidator( + 20, + /^($|[a-zA-Z0-9_.]{6,20})$/, + '長度最多20個字元', + '長度最少6個字元,支援英文、數字、底線、句號', + ), +}); + const userReducer = (state, payload) => { const { key, value, isMultiple = false } = payload; if (isMultiple) { @@ -47,13 +95,35 @@ const userReducer = (state, payload) => { const useEditProfile = () => { const reduxDispatch = useDispatch(); - const [userState, stateDispatch] = useReducer(userReducer, initialState); + const [errors, setErrors] = useState({}); + + const validate = (state = {}, isPartial = false) => { + const [key, value] = Object.entries(state)[0]; + if (key !== 'birthDay') { + const result = isPartial + ? schema.partial({ [key]: true }).safeParse({ [key]: value }) + : schema.safeParse({ [key]: value }); - // TODO ErrorMap + if (!result.success) { + result.error.errors.forEach((err) => { + setErrors({ [err.path[0]]: err.message }); + }); + } + if (isPartial && result.success) { + const obj = { ...errors }; + delete obj[key]; + setErrors(obj); + } + + return result.success; + } + return true; + }; const onChangeHandler = ({ key, value, isMultiple }) => { stateDispatch({ key, value, isMultiple }); + validate({ [key]: value }, true); }; const onSubmit = async ({ id, email }) => { @@ -105,10 +175,19 @@ const useEditProfile = () => { reduxDispatch(updateUser(payload)); }; + const checkBeforeSubmit = ({ id, email }) => { + if (validate(userState)) { + onSubmit({ id, email }); + return true; + } + return false; + }; + return { userState, onChangeHandler, - onSubmit, + onSubmit: checkBeforeSubmit, + errors, }; }; diff --git a/components/Profile/UserCard/index.jsx b/components/Profile/UserCard/index.jsx index 39a936e6..6d8edd1f 100644 --- a/components/Profile/UserCard/index.jsx +++ b/components/Profile/UserCard/index.jsx @@ -29,7 +29,7 @@ const BottonEdit = { }; const StyledProfileWrapper = styled(Box)` - width: 720px; + width: 100%; padding: 30px; background-color: #fff; border-radius: 20px; diff --git a/components/Profile/index.jsx b/components/Profile/index.jsx index ec002fb8..56097770 100644 --- a/components/Profile/index.jsx +++ b/components/Profile/index.jsx @@ -1,6 +1,7 @@ -import { useMemo, useState } from 'react'; +import { useMemo } from 'react'; import { useRouter } from 'next/router'; import { Box, Button } from '@mui/material'; +import Skeleton from '@mui/material/Skeleton'; import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; import { WANT_TO_DO_WITH_PARTNER, @@ -46,9 +47,9 @@ const Profile = ({ handleContactPartner, contactList = {}, updatedDate, + isLoading, }) => { const router = useRouter(); - const [isLoading] = useState(false); const role = roleList.length > 0 && ROLELIST[roleList[0]]; const edu = educationStage && EDUCATION_STEP_TABLE[educationStage]; const wantTodo = wantToDoList @@ -87,6 +88,7 @@ const Profile = ({ 返回 + {isLoading ? ( + + ) : ( + typeof t === 'string' && t !== '')} + photoURL={photoURL} + userName={name} + location={location} + updatedDate={updatedDate} + contactList={contactList} + /> + )} + - + ) : ( + typeof t === 'string' && t !== '')} - photoURL={photoURL} - userName={name} - location={location} - updatedDate={updatedDate} - contactList={contactList} + description={selfIntroduction} + wantToDoList={wantTodo} + share={share} /> - - - + )} {email !== sendEmail && (