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) => (
}