diff --git a/components/Group/Form/Fields/AreaCheckbox.jsx b/components/Group/Form/Fields/AreaCheckbox.jsx index bee8f07e..f82f1b4e 100644 --- a/components/Group/Form/Fields/AreaCheckbox.jsx +++ b/components/Group/Form/Fields/AreaCheckbox.jsx @@ -71,6 +71,7 @@ export default function AreaCheckbox({ control={physicalAreaControl} /> + {isPhysicalArea && !physicalAreaValue && 請選擇地點}
handleCheckboxChange('線上')} />} diff --git a/components/Group/Form/index.jsx b/components/Group/Form/index.jsx index 72c4a0a9..7f5ee3db 100644 --- a/components/Group/Form/index.jsx +++ b/components/Group/Form/index.jsx @@ -99,13 +99,13 @@ export default function GroupForm({ label="揪團類型" name="activityCategory" handleValues={(action, value, activityCategory) => { - if (action === 'add' && value === 'Other') { - return ['Other']; + if (action === 'add' && value === '其他') { + return ['其他']; } if (action === 'remove' && !activityCategory.length) { - return ['Other']; + return ['其他']; } - return activityCategory.filter((item) => item !== 'Other'); + return activityCategory.filter((item) => item !== '其他'); }} control={control} value={values.activityCategory} diff --git a/components/Group/Form/useGroupForm.jsx b/components/Group/Form/useGroupForm.jsx index 115c1cee..8008d0da 100644 --- a/components/Group/Form/useGroupForm.jsx +++ b/components/Group/Form/useGroupForm.jsx @@ -2,6 +2,7 @@ import dayjs from 'dayjs'; import { useEffect, useRef, useState } from 'react'; import { useSelector } from 'react-redux'; import { ZodType, z } from 'zod'; +import { useSnackbar } from '@/contexts/Snackbar'; import { CATEGORIES } from '@/constants/category'; import { AREAS } from '@/constants/areas'; import { EDUCATION_STEP } from '@/constants/member'; @@ -57,7 +58,9 @@ const rules = { participator: z .string() .regex(/^(100|[1-9]?\d)$/, '請輸入整數,需大於 0,不可超過 100'), - area: z.array(z.string()).min(1, '請選擇地點'), + area: z + .array(z.enum(AREAS.concat({ label: '待討論' }).map(({ label }) => label))) + .min(1, '請選擇地點'), time: z.string().max(50, '請勿輸入超過 50 字'), partnerStyle: z .string() @@ -83,12 +86,19 @@ export default function useGroupForm(defaultValue) { const [isDirty, setIsDirty] = useState(false); const me = useSelector((state) => state.user); const notLogin = !me?._id; - const [values, setValues] = useState({ + const [values, setValues] = useState(() => ({ ...INITIAL_VALUES, ...defaultValue, + ...Object.fromEntries( + Object.entries(rules).map(([key, rule]) => [ + key, + rule.safeParse(defaultValue[key])?.data || INITIAL_VALUES[key], + ]) + ), userId: me?._id, - }); + })); const [errors, setErrors] = useState({}); + const { pushSnackbar } = useSnackbar(); const refs = useRef({}); const schema = z.object(rules); @@ -119,15 +129,56 @@ export default function useGroupForm(defaultValue) { onBlur, }; + const removePhoto = (url) => { + const pathArray = url.split('/'); + fetch(`${BASE_URL}/image/${pathArray[pathArray.length - 1]}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${me.token}`, + }, + }); + }; + + const uploadPhoto = async (oldPhoto, newPhoto) => { + if (oldPhoto) { + removePhoto(oldPhoto); + } + if (newPhoto instanceof Blob) { + const formData = new FormData(); + + formData.append('file', newPhoto); + + try { + const response = await fetch(`${BASE_URL}/image`, { + method: 'POST', + headers: { + Authorization: `Bearer ${me.token}`, + }, + body: formData, + }); + const data = await response.json(); + + return typeof data.url === 'string' ? data.url : ''; + } catch { + return ''; + } + } else { + return ''; + } + }; + const handleSubmit = (onValid) => async () => { - if (!schema.safeParse(values).success) { + const result = schema.safeParse(values); + + if (!result.success) { let isFocus = false; const updatedErrors = Object.fromEntries( Object.entries(rules).map(([key, rule]) => { const errorMessage = rule.safeParse(values[key]).error?.issues?.[0] ?.message; - if (errorMessage && !isFocus) { + if (errorMessage && !isFocus && refs.current[key]) { isFocus = true; refs.current[key]?.focus(); } @@ -136,47 +187,25 @@ export default function useGroupForm(defaultValue) { }), ); setErrors(updatedErrors); + if (!isFocus) { + pushSnackbar({ + message: Object.values(updatedErrors)[0], + vertical: 'top', + horizontal: 'center', + type: 'error', + }); + } return; } if (values.originPhotoURL === values.photoURL) { - onValid(values); + onValid(result.data); return; } - if (values.originPhotoURL) { - const pathArray = values.originPhotoURL.split('/'); - fetch(`${BASE_URL}/image/${pathArray[pathArray.length - 1]}`, { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${me.token}`, - }, - }); - } - - let photoURL = ''; + const photoURL = await uploadPhoto(values.originPhotoURL, values.photoURL); - if (values.photoURL instanceof Blob) { - const formData = new FormData(); - - formData.append('file', values.photoURL); - - try { - photoURL = await fetch(`${BASE_URL}/image`, { - method: 'POST', - headers: { - Authorization: `Bearer ${me.token}`, - }, - body: formData, - }) - .then((response) => response.json()) - .then((data) => data.url); - } catch { - photoURL = ''; - } - } - onValid({ ...values, photoURL }); + onValid({ ...result.data, photoURL }); }; useEffect(() => { diff --git a/contexts/Snackbar.jsx b/contexts/Snackbar.jsx index 303a6526..d4151443 100644 --- a/contexts/Snackbar.jsx +++ b/contexts/Snackbar.jsx @@ -25,11 +25,11 @@ function CloseButton({ onClick }) { export default function SnackbarProvider({ children }) { const [queue, setQueue] = useState([]); - const pushSnackbar = ({ message }) => + const pushSnackbar = ({ message, type, vertical = 'bottom', horizontal = 'left' }) => new Promise((resolve) => { setQueue((pre) => [ ...pre, - { id: Math.random(), open: true, message, resolve }, + { id: Math.random(), open: true, message, resolve, type, vertical, horizontal }, ]); }); @@ -49,7 +49,9 @@ export default function SnackbarProvider({ children }) { {queue.map((data) => ( }