From 7ee66e4c01739b59053713927325a5a92c943f13 Mon Sep 17 00:00:00 2001 From: ruby10127130 Date: Wed, 30 Oct 2024 14:57:29 +0800 Subject: [PATCH 01/32] feat(userTabs): refactor user description html --- components/Profile/UserTabs/UserInfoBasic.jsx | 4 ++-- components/Profile/UserTabs/UserTabs.styled.jsx | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/components/Profile/UserTabs/UserInfoBasic.jsx b/components/Profile/UserTabs/UserInfoBasic.jsx index c01558b9..6bf93a1b 100644 --- a/components/Profile/UserTabs/UserInfoBasic.jsx +++ b/components/Profile/UserTabs/UserInfoBasic.jsx @@ -17,9 +17,9 @@ function UserInfoBasic({ description = '', wantToDoList = [], share = '' }) {

簡介

-
+
{description ? ( - description.split('\n').map((d) => {d}) + description.split('\n').map((d) =>

{d}

) ) : ( 尚未填寫 )} diff --git a/components/Profile/UserTabs/UserTabs.styled.jsx b/components/Profile/UserTabs/UserTabs.styled.jsx index 3662d6ab..351452e8 100644 --- a/components/Profile/UserTabs/UserTabs.styled.jsx +++ b/components/Profile/UserTabs/UserTabs.styled.jsx @@ -28,6 +28,7 @@ export const StyledPanelText = styled(Box)` white-space: nowrap; min-width: 50px; } + .content p, span { color: #536166; font-size: 14px; @@ -35,8 +36,9 @@ export const StyledPanelText = styled(Box)` font-weight: 400; line-height: 140%; margin-left: 12px; - display: grid; + display: flex; place-items: center; + text-align: left; } @media (max-width: 767px) { flex-direction: column; From 149b93bf24dc9ab644f52d30e4fe61a38bdc731b Mon Sep 17 00:00:00 2001 From: ruby10127130 Date: Wed, 30 Oct 2024 23:07:12 +0800 Subject: [PATCH 02/32] fix: correct user description styling issue on mobile devices --- components/Profile/UserTabs/UserTabs.styled.jsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/components/Profile/UserTabs/UserTabs.styled.jsx b/components/Profile/UserTabs/UserTabs.styled.jsx index 351452e8..51c1ad2d 100644 --- a/components/Profile/UserTabs/UserTabs.styled.jsx +++ b/components/Profile/UserTabs/UserTabs.styled.jsx @@ -25,7 +25,7 @@ export const StyledPanelText = styled(Box)` p { color: #293a3d; font-weight: 500; - white-space: nowrap; + white-space: normal; min-width: 50px; } .content p, @@ -37,14 +37,13 @@ export const StyledPanelText = styled(Box)` line-height: 140%; margin-left: 12px; display: flex; - place-items: center; text-align: left; } @media (max-width: 767px) { flex-direction: column; span { margin-left: 0px; - place-items: start; + text-align: left; } } `; From dfbdeeafa83d13e309e723855f62237986ca9852 Mon Sep 17 00:00:00 2001 From: hsuifang Date: Thu, 31 Oct 2024 21:01:56 +0800 Subject: [PATCH 03/32] chore: add conditional logic to control avatar display --- shared/components/Navigation_v2/MainNav/SubList/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/components/Navigation_v2/MainNav/SubList/index.jsx b/shared/components/Navigation_v2/MainNav/SubList/index.jsx index f32369aa..b544fdbc 100644 --- a/shared/components/Navigation_v2/MainNav/SubList/index.jsx +++ b/shared/components/Navigation_v2/MainNav/SubList/index.jsx @@ -59,7 +59,7 @@ const SubList = () => {
  • - {user._id ? ( + {user._id && user.email ? ( ) : ( + {Object.values(errors).join('') && ( + + )} diff --git a/pages/signin/interest/index.jsx b/pages/signin/interest/index.jsx index 1c2dc6c6..852c17f5 100644 --- a/pages/signin/interest/index.jsx +++ b/pages/signin/interest/index.jsx @@ -46,7 +46,7 @@ function SignInInterestPage() { const { _id: userId, - interestList: userInterestList, + interestList: userInterestList = [], email: userEmail, } = useSelector((state) => state?.user); diff --git a/redux/actions/user.js b/redux/actions/user.js index 1ff2ed5f..b707fad1 100644 --- a/redux/actions/user.js +++ b/redux/actions/user.js @@ -2,8 +2,8 @@ export const CHECK_LOGIN_VALIDITY = 'CHECK_LOGIN_VALIDITY'; export function checkLoginValidity() { return { - type: CHECK_LOGIN_VALIDITY - } + type: CHECK_LOGIN_VALIDITY, + }; } export function userLogin() { @@ -71,3 +71,12 @@ export function updateUser(user) { }, }; } + +export function createUser(user) { + return { + type: 'CREATE_USER_PROFILE', + payload: { + user, + }, + }; +} diff --git a/redux/sagas/autoLogoutSaga.js b/redux/sagas/autoLogoutSaga.js index 2b66adf7..a9e482b2 100644 --- a/redux/sagas/autoLogoutSaga.js +++ b/redux/sagas/autoLogoutSaga.js @@ -1,12 +1,11 @@ import { put, delay, takeEvery, select } from 'redux-saga/effects'; import { CHECK_LOGIN_VALIDITY, userLogout } from '../actions/user'; -// 6hr -const MAX_TIME = 6 * 60 * 60 * 1000; - function* autoLogout() { - const user = yield select(state => state.user); - const validityTime = user.lastLogin + MAX_TIME - Date.now(); + const user = yield select((state) => state.user); + // depending on whether the user is already registered or not + // setting tokenExpiry time after fetching user + const validityTime = user.tokenExpiry - Date.now(); if (validityTime <= 0 || Number.isNaN(validityTime)) { yield put(userLogout()); diff --git a/redux/sagas/user/index.js b/redux/sagas/user/index.js index 9e5d8fed..1339235f 100644 --- a/redux/sagas/user/index.js +++ b/redux/sagas/user/index.js @@ -4,6 +4,14 @@ import firebase from '../../../utils/firebase'; import { BASE_URL } from '@/constants/common'; import req from '@/utils/request'; +/** + * + * @param {boolean} isnormal 是否一般 token ? + * @returns time + */ +const handleTokenExpiry = (isnormal = false) => + isnormal ? Date.now() + 60 * 60 * 1000 : Date.now() + 60 * 60 * 6000; + function* checkUserStatus() { try { const userData = yield localforage.getItem('userData'); @@ -48,6 +56,28 @@ function* fetchAllUsers() { } } +function* createUserProfile(action) { + const { user } = action.payload; + try { + const URL = `${BASE_URL}/user/${user.id}`; + // if success => status: 201, token, user + const result = yield req(URL, { + method: 'POST', + body: JSON.stringify({ + ...user, + }), + }); + + const { token, user: resultUser } = result; + yield put({ + type: 'UPDATE_USER_PROFILE_SUCCESS', + payload: { token, ...resultUser }, + }); + } catch (error) { + yield put({ type: 'UPDATE_USER_PROFILE_FAILURE' }); + } +} + function* updateUserProfile(action) { const { user } = action.payload; try { @@ -76,12 +106,14 @@ function* fetchUserById(action) { Authorization: `Bearer ${token}`, }, }); + yield put({ type: 'FETCH_USER_BY_ID_SUCCESS', payload: result.data && { + _id: id, ...result.data[0], token, - lastLogin: Date.now(), + tokenExpiry: handleTokenExpiry(true), }, }); } catch (error) { @@ -94,6 +126,7 @@ function* userSaga() { yield takeEvery('CHECK_USER_ACCOUNT', checkUserStatus); yield takeEvery('USER_LOGIN', userLogin); yield takeEvery('FETCH_ALL_USERS', fetchAllUsers); + yield takeEvery('CREATE_USER_PROFILE', createUserProfile); yield takeEvery('UPDATE_USER_PROFILE', updateUserProfile); yield takeEvery('FETCH_USER_BY_ID', fetchUserById); } diff --git a/utils/request.js b/utils/request.js index 20903e11..6744abf8 100644 --- a/utils/request.js +++ b/utils/request.js @@ -20,7 +20,7 @@ const request = function* (url, options = {}) { const data = yield response.json(); // Check for non-200 status and throw an error - if (response.status !== 200) { + if (![200, 201].includes(response.status)) { throw new Error(data); } From 759e38fa798eda0371a21a96083631728ca635d4 Mon Sep 17 00:00:00 2001 From: hsuifang Date: Thu, 31 Oct 2024 22:47:07 +0800 Subject: [PATCH 06/32] fix(signin): remove unneccessary comparisons --- pages/signin/index.jsx | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/pages/signin/index.jsx b/pages/signin/index.jsx index 7ee54a11..cf1626b9 100644 --- a/pages/signin/index.jsx +++ b/pages/signin/index.jsx @@ -31,7 +31,7 @@ function SignInPage() { const { errors, onChangeHandler, userState, validateFields } = useProfileValidation(); - const { createdDate, updatedDate, _id } = useSelector((state) => state?.user); + const { createdDate, updatedDate } = useSelector((state) => state?.user); // Oath login useEffect(() => { @@ -46,17 +46,10 @@ function SignInPage() { }, [id, token]); useEffect(() => { - if (_id || id) { - if (createdDate !== updatedDate) { - router.push('/profile'); - } - } else { - router.push('/'); + if (createdDate !== updatedDate) { + router.push('/profile'); } - // if (id && UserToken) { - // dispatch(fetchUserById(id, UserToken)); - // } - }, [createdDate, updatedDate, _id, id]); + }, [createdDate, updatedDate]); const handleRoleListChange = (value) => { const { roleList } = userState; From 644dc27f4ab39be4c42eba54762514a5f03bca97 Mon Sep 17 00:00:00 2001 From: hsuifang Date: Fri, 1 Nov 2024 12:09:26 +0800 Subject: [PATCH 07/32] chore: add staticPageGenerationTimeout in next.config to prevent Notion fetch failure --- next.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/next.config.js b/next.config.js index 36d314ca..17bcb1d3 100644 --- a/next.config.js +++ b/next.config.js @@ -6,6 +6,7 @@ const withPWA = require('next-pwa')({ module.exports = withPWA({ reactStrictMode: false, + staticPageGenerationTimeout: 600, images: { domains: ['imgur.com', 'images.unsplash.com', 'lh3.googleusercontent.com'], }, From bb3b4208092e7fcf8acb58d5c97b632880698a83 Mon Sep 17 00:00:00 2001 From: Johnson Mao <86179381+JohnsonMao@users.noreply.github.com> Date: Sat, 2 Nov 2024 22:55:13 +0800 Subject: [PATCH 08/32] feat(group): add terms link and consent checkbox clickup: https://app.clickup.com/t/8696ed8vp --- .../Group/detail/Contact/ContactPopup.jsx | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/components/Group/detail/Contact/ContactPopup.jsx b/components/Group/detail/Contact/ContactPopup.jsx index 491062db..5feb215d 100644 --- a/components/Group/detail/Contact/ContactPopup.jsx +++ b/components/Group/detail/Contact/ContactPopup.jsx @@ -1,4 +1,5 @@ import { useId, useState } from 'react'; +import Link from 'next/link'; import styled from '@emotion/styled'; import { Avatar, @@ -11,6 +12,8 @@ import { TextareaAutosize, useMediaQuery, } from '@mui/material'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Checkbox from '@mui/material/Checkbox'; import CloseIcon from '@mui/icons-material/Close'; import { ROLE } from '@/constants/member'; import TransitionSlide from './TransitionSlide'; @@ -34,6 +37,26 @@ const StyledTextArea = styled(TextareaAutosize)` min-height: 128px; `; +const StyledDesc = styled.p` + font-size: 14px; + color: #92989a; + + a { + color: #92989a; + text-decoration: underline; + } +`; + +const desc = ( + + 您填的資訊將透過島島阿學 email + 給這位夥伴,請確認訊息未涉及個人隱私並符合本網站{' '} + + 使用者條款 + + +); + function ContactPopup({ open, user, @@ -47,6 +70,7 @@ function ContactPopup({ const isMobileScreen = useMediaQuery('(max-width: 560px)'); const [message, setMessage] = useState(''); const [contact, setContact] = useState(''); + const [isChecked, setIsChecked] = useState(false); const id = useId(); const titleId = `modal-title-${id}`; const descriptionId = `modal-description-${id}`; @@ -65,6 +89,10 @@ function ContactPopup({ onSubmit({ message, contact }); }; + const checkbox = ( + setIsChecked((pre) => !pre)} /> + ); + return (
  • +
    + +
    + 送出 From d0620c3329c203f9753c031cef802a91d823f319 Mon Sep 17 00:00:00 2001 From: Johnson Mao <86179381+JohnsonMao@users.noreply.github.com> Date: Sun, 3 Nov 2024 00:52:24 +0800 Subject: [PATCH 09/32] feat(group): modify form fields per new requirements --- components/Group/Form/Fields/AreaCheckbox.jsx | 4 + .../Group/Form/Fields/CheckboxGroup.jsx | 28 +++++ components/Group/Form/Fields/DateRadio.jsx | 68 +++++++++++ components/Group/Form/Fields/TextField.jsx | 2 +- components/Group/Form/Fields/index.jsx | 4 + components/Group/Form/Form.styled.jsx | 4 +- components/Group/Form/index.jsx | 115 ++++++++++++++++-- components/Group/Form/useGroupForm.jsx | 33 ++++- constants/activityCategory.js | 11 ++ 9 files changed, 251 insertions(+), 18 deletions(-) create mode 100644 components/Group/Form/Fields/CheckboxGroup.jsx create mode 100644 components/Group/Form/Fields/DateRadio.jsx create mode 100644 constants/activityCategory.js diff --git a/components/Group/Form/Fields/AreaCheckbox.jsx b/components/Group/Form/Fields/AreaCheckbox.jsx index 17feb88c..13030b8e 100644 --- a/components/Group/Form/Fields/AreaCheckbox.jsx +++ b/components/Group/Form/Fields/AreaCheckbox.jsx @@ -57,12 +57,14 @@ export default function AreaCheckbox({ } label="實體活動" + disabled={value.includes('待討論')} checked={isPhysicalArea} /> + selected.length === 0 + ? '揪團類型' + : activityCategoryList + .filter((item) => selected.includes(item.value)) + .map((item) => item.label) + .join('、') + } + sx={{ + '@media (max-width: 767px)': { + width: '100%', + }, + }} + /> + ); +} diff --git a/components/Group/SearchField/SelectedAreas.jsx b/components/Group/SearchField/SelectedAreas.jsx index 25354144..7ccea24d 100644 --- a/components/Group/SearchField/SelectedAreas.jsx +++ b/components/Group/SearchField/SelectedAreas.jsx @@ -2,6 +2,8 @@ import Select from '@/shared/components/Select'; import { AREAS } from '@/constants/areas'; import useSearchParamsManager from '@/hooks/useSearchParamsManager'; +const AREAS_WITH_TBD = AREAS.concat({ name: '待討論', label: '待討論' }); + export default function SelectedAreas() { const QUERY_KEY = 'area'; const [getSearchParams, pushState] = useSearchParamsManager(); @@ -15,7 +17,7 @@ export default function SelectedAreas() { multiple value={getSearchParams(QUERY_KEY)} onChange={handleChange} - items={AREAS} + items={AREAS_WITH_TBD} renderValue={(selected) => selected.length === 0 ? '地點' : selected.join('、') } diff --git a/components/Group/SearchField/index.jsx b/components/Group/SearchField/index.jsx index 35f7a40e..dff729de 100644 --- a/components/Group/SearchField/index.jsx +++ b/components/Group/SearchField/index.jsx @@ -1,6 +1,7 @@ import styled from '@emotion/styled'; import SearchInput from './SearchInput'; import SelectedAreas from './SelectedAreas'; +import SelectedActivityCategoryStep from './SelectedActivityCategoryStep'; import SelectedEducationStep from './SelectedEducationStep'; import CheckboxGrouping from './CheckboxGrouping'; @@ -14,6 +15,12 @@ const StyledSearchField = styled.div` align-items: center; gap: 16px; + @media (max-width: 1024px) { + > * { + max-width: 180px; + } + } + @media (max-width: 767px) { margin: 10px 0; flex-direction: column; @@ -28,6 +35,7 @@ const SearchField = () => {
    +
    diff --git a/redux/sagas/groupSaga.js b/redux/sagas/groupSaga.js index 2e122183..909e415d 100644 --- a/redux/sagas/groupSaga.js +++ b/redux/sagas/groupSaga.js @@ -2,6 +2,7 @@ import { put, takeLatest, select } from 'redux-saga/effects'; import { AREAS } from '@/constants/areas'; import { CATEGORIES } from '@/constants/category'; import { EDUCATION_STEP } from '@/constants/member'; +import { activityCategoryList } from '@/constants/activityCategory'; import req from '@/utils/request'; import { @@ -18,26 +19,31 @@ function* getGroupItems() { } = yield select(); const urlSearchParams = new URLSearchParams({ pageSize }); - const searchParamsOptions = { - area: AREAS, - category: CATEGORIES, - partnerEducationStep: EDUCATION_STEP, + const searchParamsConfigs = { + area: [AREAS, 'label'], + category: [CATEGORIES, 'label'], + activityCategory: [activityCategoryList, 'value'], + partnerEducationStep: [EDUCATION_STEP, 'label'], isGrouping: true, search: true, }; - Object.keys(searchParamsOptions).forEach((key) => { + Object.keys(searchParamsConfigs).forEach((key) => { const searchParam = query[key]; - const option = searchParamsOptions[key]; + const config = searchParamsConfigs[key]; - if (!searchParam || !option) return; + if (!searchParam || !config) return; + + if (Array.isArray(config)) { + const [options, optionKey] = config; - if (Array.isArray(option)) { urlSearchParams.append( key, searchParam .split(',') - .filter((item) => option.some((_option) => _option.label === item)) + .filter((item) => + options.some((_option) => _option[optionKey] === item), + ) .join(','), ); } else { From 8b4f7a1a34d76c1d5146eb3a3716912c5021279d Mon Sep 17 00:00:00 2001 From: Johnson Mao <86179381+JohnsonMao@users.noreply.github.com> Date: Sun, 3 Nov 2024 13:07:16 +0800 Subject: [PATCH 13/32] feat(group): modify key description to content --- components/Group/GroupList/GroupCard.jsx | 6 +++--- components/Profile/MyGroup/GroupCard.jsx | 6 +++--- pages/group/detail/index.jsx | 5 ++++- pages/group/edit/index.jsx | 5 ++++- redux/actions/group.js | 7 ++++++- 5 files changed, 20 insertions(+), 9 deletions(-) diff --git a/components/Group/GroupList/GroupCard.jsx b/components/Group/GroupList/GroupCard.jsx index 57501760..cc337e5c 100644 --- a/components/Group/GroupList/GroupCard.jsx +++ b/components/Group/GroupList/GroupCard.jsx @@ -21,7 +21,7 @@ function GroupCard({ title = '未定義主題', category = [], partnerEducationStep, - description, + content, area, isGrouping, updatedDate, @@ -47,8 +47,8 @@ function GroupCard({ {formatToString(partnerEducationStep, '皆可')} - - {description} + + {content} diff --git a/components/Profile/MyGroup/GroupCard.jsx b/components/Profile/MyGroup/GroupCard.jsx index f5dba38b..f2ed64b0 100644 --- a/components/Profile/MyGroup/GroupCard.jsx +++ b/components/Profile/MyGroup/GroupCard.jsx @@ -28,7 +28,7 @@ function GroupCard({ photoURL, photoAlt, title = '未定義主題', - description, + content, area, isGrouping, userId, @@ -86,8 +86,8 @@ function GroupCard({ {title} - - {description} + + {content} diff --git a/pages/group/detail/index.jsx b/pages/group/detail/index.jsx index db41b9a2..dde5606f 100644 --- a/pages/group/detail/index.jsx +++ b/pages/group/detail/index.jsx @@ -11,7 +11,10 @@ function GroupPage() { const { data, isFetching, isError } = useFetch(`/activity/${id}`, { enabled: !!id, }); - const source = data?.data?.[0]; + const source = { + ...data?.data?.[0], + content: data?.data?.[0]?.content || data?.data?.[0]?.description, + }; const SEOData = useMemo( () => ({ diff --git a/pages/group/edit/index.jsx b/pages/group/edit/index.jsx index 956ce3d1..3aa2154b 100644 --- a/pages/group/edit/index.jsx +++ b/pages/group/edit/index.jsx @@ -20,7 +20,10 @@ function EditGroupPage() { const { data, isFetching } = useFetch(`/activity/${id}`, { enabled: !!id, }); - const source = data?.data?.[0]; + const source = { + ...data?.data?.[0], + content: data?.data?.[0]?.content || data?.data?.[0]?.description, + }; const SEOData = useMemo( () => ({ diff --git a/redux/actions/group.js b/redux/actions/group.js index 32862c6a..04cf1873 100644 --- a/redux/actions/group.js +++ b/redux/actions/group.js @@ -29,7 +29,12 @@ export function getGroupItemsSuccess({ data = [], totalCount = 0 } = {}) { return { type: GET_GROUP_ITEMS_SUCCESS, payload: { - items: data, + items: Array.isArray(data) + ? data.map((item) => ({ + ...item, + content: item.content || item.description, + })) + : [], total: totalCount, }, }; From f811b20784cf587b2f82350f9c5d59802b4384df Mon Sep 17 00:00:00 2001 From: Johnson Mao <86179381+JohnsonMao@users.noreply.github.com> Date: Sun, 3 Nov 2024 13:19:38 +0800 Subject: [PATCH 14/32] feat(group): set activity category default value --- .../Group/Form/Fields/CheckboxGroup.jsx | 19 ++++++++++++++++--- components/Group/Form/index.jsx | 9 +++++++++ components/Group/Form/useGroupForm.jsx | 2 +- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/components/Group/Form/Fields/CheckboxGroup.jsx b/components/Group/Form/Fields/CheckboxGroup.jsx index f2e85c06..3b7ab1db 100644 --- a/components/Group/Form/Fields/CheckboxGroup.jsx +++ b/components/Group/Form/Fields/CheckboxGroup.jsx @@ -2,12 +2,25 @@ import Box from '@mui/material/Box'; import FormControlLabel from '@mui/material/FormControlLabel'; import Checkbox from '@mui/material/Checkbox'; -export default function CheckboxGroup({ options, name, value, control }) { +export default function CheckboxGroup({ + options, + name, + value, + control, + handleValues, +}) { const handleCheckboxChange = (_value) => { - const updatedValue = value.includes(_value) + const hasValue = value.includes(_value); + const updatedValue = hasValue ? value.filter((v) => v !== _value) : [...value, _value]; - control.onChange({ target: { name, value: updatedValue } }); + const newValue = handleValues( + hasValue ? 'remove' : 'add', + _value, + updatedValue, + ); + + control.onChange({ target: { name, value: newValue } }); }; return ( diff --git a/components/Group/Form/index.jsx b/components/Group/Form/index.jsx index 602e1616..e3e71c94 100644 --- a/components/Group/Form/index.jsx +++ b/components/Group/Form/index.jsx @@ -104,6 +104,15 @@ export default function GroupForm({ { + if (action === 'add' && value === 'Other') { + return ['Other']; + } + if (action === 'remove' && !activityCategory.length) { + return ['Other']; + } + return activityCategory.filter((item) => item !== 'Other'); + }} control={control} value={values.activityCategory} options={activityCategoryList} diff --git a/components/Group/Form/useGroupForm.jsx b/components/Group/Form/useGroupForm.jsx index 5db5d3a7..e426591f 100644 --- a/components/Group/Form/useGroupForm.jsx +++ b/components/Group/Form/useGroupForm.jsx @@ -25,7 +25,7 @@ const DEFAULT_VALUES = { originPhotoURL: '', photoURL: '', photoAlt: '', - activityCategory: [], + activityCategory: ['Other'], category: [], participator: '', area: [], From 9ba35fb6cf1ba86a74ab023af12dfa72c0db355e Mon Sep 17 00:00:00 2001 From: Johnson Mao <86179381+JohnsonMao@users.noreply.github.com> Date: Sun, 3 Nov 2024 14:08:40 +0800 Subject: [PATCH 15/32] feat(group): auto-focus on input field after error --- components/Group/Form/Fields/Select.jsx | 2 +- components/Group/Form/Fields/TagsField.jsx | 1 + components/Group/Form/Fields/TextField.jsx | 1 + components/Group/Form/useGroupForm.jsx | 23 +++++++++++++++++----- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/components/Group/Form/Fields/Select.jsx b/components/Group/Form/Fields/Select.jsx index b63a28e3..ba48d2de 100644 --- a/components/Group/Form/Fields/Select.jsx +++ b/components/Group/Form/Fields/Select.jsx @@ -1,4 +1,3 @@ -import { useState } from 'react'; import FormControl from '@mui/material/FormControl'; import MuiSelect from '@mui/material/Select'; import MenuItem from '@mui/material/MenuItem'; @@ -27,6 +26,7 @@ export default function Select({ return ( control.setRef?.(name, element)} displayEmpty multiple={multiple} fullWidth={fullWidth} diff --git a/components/Group/Form/Fields/TagsField.jsx b/components/Group/Form/Fields/TagsField.jsx index 79ccee0f..cb54c6c4 100644 --- a/components/Group/Form/Fields/TagsField.jsx +++ b/components/Group/Form/Fields/TagsField.jsx @@ -60,6 +60,7 @@ function TagsField({ name, helperText, control, value = [] }) { ))} {value.length < 8 && ( control.setRef?.(name, element)} value={input} onCompositionStart={() => { isComposing.current = true; diff --git a/components/Group/Form/Fields/TextField.jsx b/components/Group/Form/Fields/TextField.jsx index c2837d63..7409e59a 100644 --- a/components/Group/Form/Fields/TextField.jsx +++ b/components/Group/Form/Fields/TextField.jsx @@ -13,6 +13,7 @@ export default function TextField({ return ( <> control.setRef?.(name, element)} fullWidth id={id} name={name} diff --git a/components/Group/Form/useGroupForm.jsx b/components/Group/Form/useGroupForm.jsx index e426591f..1088a367 100644 --- a/components/Group/Form/useGroupForm.jsx +++ b/components/Group/Form/useGroupForm.jsx @@ -1,5 +1,5 @@ import dayjs from 'dayjs'; -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { useSelector } from 'react-redux'; import { ZodType, z } from 'zod'; import { CATEGORIES } from '@/constants/category'; @@ -88,6 +88,7 @@ export default function useGroupForm() { userId: me?._id, }); const [errors, setErrors] = useState({}); + const refs = useRef({}); const schema = z.object(rules); const onChange = ({ target }) => { @@ -107,19 +108,31 @@ export default function useGroupForm() { }; const onBlur = onChange; + const setRef = (name, element) => { + refs.current[name] = element; + }; const control = { + setRef, onChange, onBlur, }; const handleSubmit = (onValid) => async () => { if (!schema.safeParse(values).success) { + let isFocus = false; const updatedErrors = Object.fromEntries( - Object.entries(rules).map(([key, rule]) => [ - key, - rule.safeParse(values[key]).error?.issues?.[0]?.message, - ]), + Object.entries(rules).map(([key, rule]) => { + const errorMessage = rule.safeParse(values[key]).error?.issues?.[0] + ?.message; + + if (errorMessage && !isFocus) { + isFocus = true; + refs.current[key]?.focus(); + } + + return [key, errorMessage]; + }), ); setErrors(updatedErrors); return; From 82471f27b35c2ae4741336e0663dc7eee979431f Mon Sep 17 00:00:00 2001 From: Johnson Mao <86179381+JohnsonMao@users.noreply.github.com> Date: Sun, 3 Nov 2024 23:08:31 +0800 Subject: [PATCH 16/32] feat(group): update group page features per requirements (#113) * feat(group): add terms link and consent checkbox clickup: https://app.clickup.com/t/8696ed8vp * feat(group): modify form fields per new requirements * feat(shared): add text with links component * feat(group): modify detailed per new requirement * feat(group): modify search per new requestments * feat(group): modify key description to content * feat(group): set activity category default value * feat(group): auto-focus on input field after error --- components/Group/Form/Fields/AreaCheckbox.jsx | 4 + .../Group/Form/Fields/CheckboxGroup.jsx | 41 ++++ components/Group/Form/Fields/DateRadio.jsx | 68 ++++++ components/Group/Form/Fields/Select.jsx | 2 +- components/Group/Form/Fields/TagsField.jsx | 1 + components/Group/Form/Fields/TextField.jsx | 3 +- components/Group/Form/Fields/index.jsx | 4 + components/Group/Form/Form.styled.jsx | 4 +- components/Group/Form/index.jsx | 124 ++++++++++- components/Group/Form/useGroupForm.jsx | 56 ++++- components/Group/GroupList/GroupCard.jsx | 6 +- .../SelectedActivityCategoryStep.jsx | 36 ++++ .../Group/SearchField/SelectedAreas.jsx | 4 +- components/Group/SearchField/index.jsx | 8 + .../Group/detail/Contact/ContactPopup.jsx | 38 +++- components/Group/detail/OrganizerCard.jsx | 12 +- components/Group/detail/TeamInfoCard.jsx | 7 + components/Profile/MyGroup/GroupCard.jsx | 6 +- constants/activityCategory.js | 11 + pages/group/detail/index.jsx | 5 +- pages/group/edit/index.jsx | 5 +- public/assets/icons/activityCategory.svg | 4 + redux/actions/group.js | 7 +- redux/sagas/groupSaga.js | 24 ++- shared/components/TextWithLinks.jsx | 204 ++++++++++++++++++ utils/storage.js | 3 +- 26 files changed, 639 insertions(+), 48 deletions(-) create mode 100644 components/Group/Form/Fields/CheckboxGroup.jsx create mode 100644 components/Group/Form/Fields/DateRadio.jsx create mode 100644 components/Group/SearchField/SelectedActivityCategoryStep.jsx create mode 100644 constants/activityCategory.js create mode 100644 public/assets/icons/activityCategory.svg create mode 100644 shared/components/TextWithLinks.jsx diff --git a/components/Group/Form/Fields/AreaCheckbox.jsx b/components/Group/Form/Fields/AreaCheckbox.jsx index 17feb88c..13030b8e 100644 --- a/components/Group/Form/Fields/AreaCheckbox.jsx +++ b/components/Group/Form/Fields/AreaCheckbox.jsx @@ -57,12 +57,14 @@ export default function AreaCheckbox({ } label="實體活動" + disabled={value.includes('待討論')} checked={isPhysicalArea} /> control.setRef?.(name, element)} value={input} onCompositionStart={() => { isComposing.current = true; diff --git a/components/Group/Form/Fields/TextField.jsx b/components/Group/Form/Fields/TextField.jsx index edf16965..7409e59a 100644 --- a/components/Group/Form/Fields/TextField.jsx +++ b/components/Group/Form/Fields/TextField.jsx @@ -13,6 +13,7 @@ export default function TextField({ return ( <> control.setRef?.(name, element)} fullWidth id={id} name={name} @@ -21,7 +22,7 @@ export default function TextField({ placeholder={placeholder} value={value} multiline={multiline} - rows={multiline && 10} + rows={multiline && 6} helperText={helperText} {...control} /> diff --git a/components/Group/Form/Fields/index.jsx b/components/Group/Form/Fields/index.jsx index f8383205..629cb82d 100644 --- a/components/Group/Form/Fields/index.jsx +++ b/components/Group/Form/Fields/index.jsx @@ -5,6 +5,8 @@ import TagsField from './TagsField'; import TextField from './TextField'; import Upload from './Upload'; import Wrapper from './Wrapper'; +import CheckboxGroup from './CheckboxGroup'; +import DateRadio from './DateRadio'; const withWrapper = (Component) => (props) => { const id = useId(); @@ -25,6 +27,8 @@ const withWrapper = (Component) => (props) => { const Fields = { AreaCheckbox: withWrapper(AreaCheckbox), + CheckboxGroup: withWrapper(CheckboxGroup), + DateRadio: withWrapper(DateRadio), Select: withWrapper(Select), TagsField: withWrapper(TagsField), TextField: withWrapper(TextField), diff --git a/components/Group/Form/Form.styled.jsx b/components/Group/Form/Form.styled.jsx index 634d1de6..f527d2c4 100644 --- a/components/Group/Form/Form.styled.jsx +++ b/components/Group/Form/Form.styled.jsx @@ -43,7 +43,9 @@ export const StyledLabel = styled(InputLabel)` `; export const StyledGroup = styled.div` - margin-bottom: 20px; + & + & { + margin-top: 20px; + } .error-message { font-size: 14px; diff --git a/components/Group/Form/index.jsx b/components/Group/Form/index.jsx index ab635d72..e3e71c94 100644 --- a/components/Group/Form/index.jsx +++ b/components/Group/Form/index.jsx @@ -1,8 +1,13 @@ -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; +import Link from 'next/link'; +import styled from '@emotion/styled'; import Box from '@mui/material/Box'; import Switch from '@mui/material/Switch'; import CircularProgress from '@mui/material/CircularProgress'; import Button from '@/shared/components/Button'; +import Checkbox from '@mui/material/Checkbox'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import { activityCategoryList } from '@/constants/activityCategory'; import StyledPaper from '../Paper.styled'; import { StyledHeading, @@ -18,6 +23,16 @@ import useGroupForm, { eduOptions, } from './useGroupForm'; +const StyledDesc = styled.p` + font-size: 14px; + color: #92989a; + + a { + color: #92989a; + text-decoration: underline; + } +`; + export default function GroupForm({ mode, defaultValues, @@ -33,6 +48,7 @@ export default function GroupForm({ setValues, handleSubmit, } = useGroupForm(); + const [isChecked, setIsChecked] = useState(false); const isCreateMode = mode === 'create'; useEffect(() => { @@ -43,6 +59,19 @@ export default function GroupForm({ }); }, [defaultValues]); + const desc = ( + + 請確認揪團未涉及不雅內容並符合本網站{' '} + + 使用者條款 + + + ); + + const checkbox = ( + setIsChecked((pre) => !pre)} /> + ); + if (notLogin) { return ; } @@ -72,6 +101,22 @@ export default function GroupForm({ value={values.photoURL} control={control} /> + { + if (action === 'add' && value === 'Other') { + return ['Other']; + } + if (action === 'remove' && !activityCategory.length) { + return ['Other']; + } + return activityCategory.filter((item) => item !== 'Other'); + }} + control={control} + value={values.activityCategory} + options={activityCategoryList} + /> + - + + + + @@ -142,6 +225,16 @@ export default function GroupForm({ helperText="標籤填寫完成後,會用 Hashtag 的形式呈現,例如: #一起學日文" /> + + + {!isCreateMode && ( @@ -158,10 +251,19 @@ export default function GroupForm({ )} + + + + +