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 && (