diff --git a/components/Group/Form/Form.styled.jsx b/components/Group/Form/Form.styled.jsx new file mode 100644 index 00000000..78bfd6c1 --- /dev/null +++ b/components/Group/Form/Form.styled.jsx @@ -0,0 +1,41 @@ +import styled from '@emotion/styled'; +import InputLabel from '@mui/material/InputLabel'; + +export const StyledHeading = styled.h1` + margin-bottom: 8px; + text-align: center; + font-size: 22px; + font-weight: 700; + color: #536166; +`; + +export const StyledDescription = styled.p` + margin-bottom: 40px; + text-align: center; + font-size: 14px; + font-weight: 400; + color: #536166; +`; + +export const StyledContainer = styled.main` + position: relative; + margin: 0 auto; + width: 720px; + + @media (max-width: 760px) { + padding: 20px; + width: 100%; + } +`; + +export const StyledLabel = styled(InputLabel)` + display: block; + margin-bottom: 8px; + font-size: 16px; + font-weight: 500; + color: #293a3d; +`; + +export const StyledGroup = styled.div` + margin-bottom: 20px; +`; diff --git a/components/Group/Form/FormItem.jsx b/components/Group/Form/FormItem.jsx new file mode 100644 index 00000000..d70ca997 --- /dev/null +++ b/components/Group/Form/FormItem.jsx @@ -0,0 +1,85 @@ +import { useId } from 'react'; +import Box from '@mui/material/Box'; +import TextField from '@mui/material/TextField'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Checkbox from '@mui/material/Checkbox'; +import { StyledLabel, StyledGroup } from './Form.styled'; +import Select from './Select'; + +function Wrapper({ id, required, label, children }) { + return ( + + + {label} + + {children} + + ); +} + +export default function FormItem({ + type, + label, + placeholder, + required, + options, + itemLabel, + itemValue, + value = '', +}) { + const id = useId(); + const formItemId = `form-item-${id}`; + const wrapperProps = { id: formItemId, required, label }; + + if (type === 'select') { + return ( + + + +
+ } label="線上" /> +
+
+ } label="待討論" /> +
+
+ ); + } + + return ( + + + + ); +} diff --git a/components/Group/Form/Select.jsx b/components/Group/Form/Select.jsx new file mode 100644 index 00000000..11af6cbc --- /dev/null +++ b/components/Group/Form/Select.jsx @@ -0,0 +1,45 @@ +import MuiSelect from '@mui/material/Select'; +import MenuItem from '@mui/material/MenuItem'; + +export default function Select({ + id, + value, + placeholder, + options = [], + itemLabel = 'label', + itemValue = 'key', + fullWidth = true, + sx, +}) { + const getValue = (any, key) => (typeof any === 'object' ? any[key] : any); + + return ( + + {placeholder && ( + + {placeholder} + + )} + {options.map((item) => ( + + {getValue(item, itemLabel)} + + ))} + + ); +} diff --git a/components/Group/Form/index.jsx b/components/Group/Form/index.jsx new file mode 100644 index 00000000..78f7ca49 --- /dev/null +++ b/components/Group/Form/index.jsx @@ -0,0 +1,70 @@ +import Box from '@mui/material/Box'; +import { CATEGORIES } from '@/constants/category'; +import { AREAS } from '@/constants/areas'; +import StyledPaper from '../Paper.styled'; +import FormItem from './FormItem'; +import { + StyledHeading, + StyledDescription, + StyledContainer, +} from './Form.styled'; + +const TaiwanAreas = AREAS.filter((area) => area.label !== '線上'); + +export default function GroupForm({ mode }) { + const isCreateMode = mode === 'create'; + + return ( + + + + + {isCreateMode ? '發起揪團' : '編輯揪團'} + + + 填寫完整資訊可以幫助其他夥伴更了解揪團內容哦! + + + + + + + + + + + + + + + + ); +} diff --git a/components/Group/GroupList/GroupCard.jsx b/components/Group/GroupList/GroupCard.jsx index 38a889a1..d93af04d 100644 --- a/components/Group/GroupList/GroupCard.jsx +++ b/components/Group/GroupList/GroupCard.jsx @@ -11,9 +11,11 @@ import { StyledLabel, StyledText, StyledTitle, + StyledStatus, } from './GroupCard.styled'; function GroupCard({ + _id, photoURL, photoAlt, title = '未定義主題', @@ -25,7 +27,7 @@ function GroupCard({ updatedDate, }) { return ( - + {photoAlt {title} @@ -49,9 +51,9 @@ function GroupCard({ {isGrouping ? ( -
揪團中
+ 揪團中 ) : ( -
已結束
+ 已結束 )}
diff --git a/components/Group/GroupList/GroupCard.styled.jsx b/components/Group/GroupList/GroupCard.styled.jsx index 4c3dd3cf..652c3b1a 100644 --- a/components/Group/GroupList/GroupCard.styled.jsx +++ b/components/Group/GroupList/GroupCard.styled.jsx @@ -1,4 +1,5 @@ import styled from '@emotion/styled'; +import Link from 'next/link'; export const StyledLabel = styled.span` flex-basis: 50px; @@ -21,6 +22,7 @@ export const StyledTitle = styled.h2` font-weight: bold; line-height: 1.4; display: -webkit-box; + color: #293a3d; -webkit-box-orient: vertical; -webkit-line-clamp: 1; overflow: hidden; @@ -39,41 +41,39 @@ export const StyledFooter = styled.footer` justify-content: space-between; align-items: center; - time, - div { - font-size: 12px; - } - time { + font-size: 12px; font-weight: 300; color: #92989a; } +`; - div { - --bg-color: #def5f5; - --color: #16b9b3; - display: flex; - align-items: center; - padding: 4px 10px; - background: var(--bg-color); - color: var(--color); - border-radius: 4px; - font-weight: 500; - gap: 4px; +export const StyledStatus = styled.div` + --bg-color: #def5f5; + --color: #16b9b3; + display: flex; + align-items: center; + width: max-content; + font-size: 12px; + padding: 4px 10px; + background: var(--bg-color); + color: var(--color); + border-radius: 4px; + font-weight: 500; + gap: 4px; - &::before { - content: ''; - display: block; - width: 8px; - height: 8px; - background: var(--color); - border-radius: 50%; - } + &::before { + content: ''; + display: block; + width: 8px; + height: 8px; + background: var(--color); + border-radius: 50%; + } - &.finished { - --bg-color: #f3f3f3; - --color: #92989a; - } + &.finished { + --bg-color: #f3f3f3; + --color: #92989a; } `; @@ -89,7 +89,8 @@ export const StyledAreas = styled.div` align-items: center; `; -export const StyledGroupCard = styled.div` +export const StyledGroupCard = styled(Link)` + display: block; position: relative; background: #fff; padding: 0.5rem; diff --git a/components/Group/GroupList/index.jsx b/components/Group/GroupList/index.jsx index d92ec7ca..0b077247 100644 --- a/components/Group/GroupList/index.jsx +++ b/components/Group/GroupList/index.jsx @@ -1,6 +1,7 @@ -import { useEffect } from 'react'; +import { useEffect, Fragment } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import styled from '@emotion/styled'; +import useMediaQuery from '@mui/material/useMediaQuery'; import { Box } from '@mui/material'; import { AREAS } from '@/constants/areas'; import { CATEGORIES } from '@/constants/category'; @@ -13,43 +14,20 @@ export const StyledGroupItem = styled.li` position: relative; margin-top: 1rem; flex-basis: 33.33%; - border-bottom: 1px solid #dbdbdb; - - &:nth-of-type(1) { - margin-top: 0; - } - - &:nth-last-of-type(1) { - border-bottom: none; - } @media (max-width: 767px) { - flex-basis: calc(50% - 12px); - } - - @media (min-width: 767px) { - &:nth-of-type(3) { - margin-top: 0; - } - - &:nth-last-of-type(3) { - border-bottom: none; - } + flex-basis: 50%; } @media (max-width: 560px) { - flex-basis: calc(100% - 24px); + flex-basis: 100%; } +`; - @media (min-width: 560px) { - &:nth-of-type(2) { - margin-top: 0; - } - - &:nth-last-of-type(2) { - border-bottom: none; - } - } +const StyledDivider = styled.li` + flex-basis: 100%; + background: #dbdbdb; + height: 1px; `; const StyledGroupList = styled.ul` @@ -62,11 +40,15 @@ function GroupList() { const [getSearchParams] = useSearchParamsManager(); const { items, isLoading } = useSelector((state) => state.group); + const isMobileScreen = useMediaQuery('(max-width: 560px)'); + const isPadScreen = useMediaQuery('(max-width: 767px)') && !isMobileScreen; + const isDeskTopScreen = !isPadScreen; + useEffect(() => { const filterOptions = { area: AREAS, category: CATEGORIES, - edu: EDUCATION_STEP, + partnerEducationStep: EDUCATION_STEP, grouping: true, q: true, }; @@ -92,11 +74,22 @@ function GroupList() { <> {items?.length || isLoading ? ( - items.map((data) => ( - - - - )) + items.map((data, index) => { + const isLast = index === items.length - 1; + const shouldRenderDivider = + (isMobileScreen && !isLast) || + (isPadScreen && !isLast && index % 2 === 1) || + (isDeskTopScreen && !isLast && index % 3 === 2); + + return ( + + + + + {shouldRenderDivider && } + + ); + }) ) : (
  • 哎呀!這裡好像沒有符合你條件的揪團,別失望!讓我們試試其他選項。 diff --git a/components/Group/More.jsx b/components/Group/More.jsx index d7e78f2d..342f68e9 100644 --- a/components/Group/More.jsx +++ b/components/Group/More.jsx @@ -21,6 +21,7 @@ export default function More() { borderRadius: '20px', padding: '6px 48px', }} + disabled={isLoading} onClick={() => dispatch(setPageSize(pageSize + 12))} > 顯示更多 diff --git a/components/Group/Paper.styled.jsx b/components/Group/Paper.styled.jsx new file mode 100644 index 00000000..805f8c85 --- /dev/null +++ b/components/Group/Paper.styled.jsx @@ -0,0 +1,12 @@ +import styled from '@emotion/styled'; +import { Box } from '@mui/material'; + +const StyledPaper = styled(Box)` + padding: 32px; + border-radius: 20px; + box-shadow: 0px 4px 6px rgba(196, 194, 193, 0.2); + background: #fff; + z-index: 2; +`; + +export default StyledPaper; diff --git a/components/Group/SearchField/SelectedEducationStep.jsx b/components/Group/SearchField/SelectedEducationStep.jsx index 1bc9021b..8fc66b42 100644 --- a/components/Group/SearchField/SelectedEducationStep.jsx +++ b/components/Group/SearchField/SelectedEducationStep.jsx @@ -3,7 +3,7 @@ import { EDUCATION_STEP } from '@/constants/member'; import useSearchParamsManager from '@/hooks/useSearchParamsManager'; export default function SelectedEducationStep() { - const QUERY_KEY = 'edu'; + const QUERY_KEY = 'partnerEducationStep'; const [getSearchParams, pushState] = useSearchParamsManager(); const handleChange = ({ target: { value } }) => { diff --git a/components/Group/detail/Contact/Feedback.jsx b/components/Group/detail/Contact/Feedback.jsx new file mode 100644 index 00000000..b4f8da99 --- /dev/null +++ b/components/Group/detail/Contact/Feedback.jsx @@ -0,0 +1,110 @@ +import { useId, forwardRef } from 'react'; +import { + Box, + Button, + Dialog, + DialogTitle, + Slide, + Typography, + useMediaQuery, +} from '@mui/material'; +import contractDoneImg from '@/public/assets/contactdone.png'; +import contractErrorImg from '@/public/assets/contacterror.png'; + +const Transition = forwardRef((props, ref) => { + return ; +}); + +function Feedback({ type, onClose }) { + const id = useId(); + const isMobileScreen = useMediaQuery('(max-width: 560px)'); + const titleId = `modal-title-${id}`; + const descriptionId = `modal-description-${id}`; + const contentMap = { + success: { + imgSrc: contractDoneImg.src, + imgAlt: 'success cover', + title: '已送出邀請', + description: '請耐心等候夥伴的回應', + buttonText: '關閉', + }, + error: { + imgSrc: contractErrorImg.src, + imgAlt: 'error cover', + title: '哎呀!有不明錯誤', + buttonText: '再試一次', + }, + }; + const content = contentMap[type] || {}; + + return ( + + + {content.imgAlt} + + + {content.title} + + + {content.description} + + + + + + ); +} + +export default Feedback; diff --git a/components/Group/detail/Contact/index.jsx b/components/Group/detail/Contact/index.jsx new file mode 100644 index 00000000..545f9138 --- /dev/null +++ b/components/Group/detail/Contact/index.jsx @@ -0,0 +1,260 @@ +import { useId, useState, forwardRef } from 'react'; +import { useSelector } from 'react-redux'; +import styled from '@emotion/styled'; +import { + Avatar, + Box, + Button, + Dialog, + DialogTitle, + IconButton, + Slide, + Typography, + TextareaAutosize, + useMediaQuery, +} from '@mui/material'; +import CloseIcon from '@mui/icons-material/Close'; +import { ROLE } from '@/constants/member'; +import chatSvg from '@/public/assets/icons/chat.svg'; +import Feedback from './Feedback'; + +const StyledTitle = styled.label` + display: block; + color: var(--black-white-gray-dark, #293a3d); + font-size: 16px; + font-style: normal; + font-weight: 500; + line-height: 140%; /* 22.4px */ + margin-bottom: 11px; +`; +const StyledTextArea = styled(TextareaAutosize)` + display: block; + padding: 12px 16px; + background: var(--black-white-white, #fff); + border-radius: 8px; + border: 1px solid var(--black-white-gray-very-light, #dbdbdb); + width: 100%; + min-height: 128px; +`; + +const Transition = forwardRef((props, ref) => { + return ; +}); + +function ContactButton({ + children, + title, + description, + descriptionPlaceholder, + onSubmit, + onClose, + isLoading, +}) { + // 判斷是否登入 + const id = useId(); + const user = useSelector((state) => state.user); + const [open, setOpen] = useState(false); + const [message, setMessage] = useState(''); + const [contact, setContact] = useState(''); + const [feedback, setFeedback] = useState(''); + const isMobileScreen = useMediaQuery('(max-width: 560px)'); + const titleId = `modal-title-${id}`; + const descriptionId = `modal-description-${id}`; + const messageId = `message-${id}`; + const contactId = `contact-${id}`; + const role = + ROLE.find(({ key }) => user?.roleList?.includes(key))?.label || '暫無資料'; + + const handleClose = () => { + if (onClose) onClose(); + setOpen(false); + setMessage(''); + setContact(''); + }; + + const handleSubmit = () => { + if (onSubmit) onSubmit({ message, contact }); + handleClose(); + setFeedback('success'); + }; + + return ( + <> + + + + {title} + + + + + + + +
    + + {user?.name || '名稱'} + + + {role} + +
    +
    + +
    + + {description} + + setMessage(e.target.value)} + placeholder={descriptionPlaceholder} + /> +
    + +
    + 聯絡資訊 + setContact(e.target.value)} + placeholder="寫下您的聯繫資訊,如 e-mail、line、Facebook、Instagram 等等。" + /> +
    + + + + + +
    +
    + setFeedback('')} /> + + ); +} + +export default ContactButton; diff --git a/components/Group/detail/Detail.styled.jsx b/components/Group/detail/Detail.styled.jsx new file mode 100644 index 00000000..0f0fd143 --- /dev/null +++ b/components/Group/detail/Detail.styled.jsx @@ -0,0 +1,31 @@ +import Link from 'next/link'; +import styled from '@emotion/styled'; + +export const StyledLink = styled(Link)` + display: inline-block; + padding: 0 4px; + color: #536166; + font-size: 14px; + span { + padding-left: 4px; + } +`; + +export const StyledHeading = styled.h1` + margin-top: 8px; + font-size: 22px; + font-weight: 700; + color: #536166; +`; + +export const StyledContainer = styled.main` + position: relative; + padding-top: 60px; + margin: 0 auto; + width: 720px; + + @media (max-width: 760px) { + padding: 20px; + width: 100%; + } +`; diff --git a/components/Group/detail/Empty.jsx b/components/Group/detail/Empty.jsx new file mode 100644 index 00000000..b77e5708 --- /dev/null +++ b/components/Group/detail/Empty.jsx @@ -0,0 +1,43 @@ +import Link from 'next/link'; +import Box from '@mui/material/Box'; +import nobodyLandGif from '@/public/assets/nobody-land.gif'; +import Button from '@/shared/components/Button'; +import StyledPaper from '../Paper.styled'; +import { StyledContainer } from './Detail.styled'; + +function GroupDetail() { + return ( + + + + + 糟糕!找不到這個揪團,要不要尋找別的揪團? + + + nobody-land + + + + + + + + ); +} + +export default GroupDetail; diff --git a/components/Group/detail/More.jsx b/components/Group/detail/More.jsx new file mode 100644 index 00000000..2a0bc09d --- /dev/null +++ b/components/Group/detail/More.jsx @@ -0,0 +1,48 @@ +import { useState } from 'react'; +import IconButton from '@mui/material/IconButton'; +import Menu from '@mui/material/Menu'; +import MenuItem from '@mui/material/MenuItem'; +import MoreVertOutlinedIcon from '@mui/icons-material/MoreVertOutlined'; + +export default function More() { + const [anchorEl, setAnchorEl] = useState(null); + + const handleMenu = (event) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = (event) => { + setAnchorEl(null); + }; + + return ( + <> + + + + + + 檢舉 + + + + ); +} diff --git a/components/Group/detail/OrganizerCard.jsx b/components/Group/detail/OrganizerCard.jsx new file mode 100644 index 00000000..12766c3d --- /dev/null +++ b/components/Group/detail/OrganizerCard.jsx @@ -0,0 +1,139 @@ +import styled from '@emotion/styled'; +import Skeleton from '@mui/material/Skeleton'; +import Avatar from '@mui/material/Avatar'; +import { EDUCATION_STEP, ROLE } from '@/constants/member'; +import locationSvg from '@/public/assets/icons/location.svg'; +import Chip from '@/shared/components/Chip'; +import { timeDuration } from '@/utils/date'; + +const StyledHeader = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + + @media (max-width: 480px) { + flex-direction: column; + align-items: start; + } +`; + +const StyledFlex = styled.div` + display: flex; + justify-content: space-between; + align-items: center; +`; + +const StyledText = styled.div` + display: flex; + align-items: center; + font-size: 14px; + font-weight: 400; + color: #536166; +`; + +const StyledTag = styled.div` + line-height: 1; + padding: 5px 10px; + border-radius: 4px; + font-size: 14px; + font-weight: 400; + background: #f3f3f3; + color: #293a3d; +`; + +const StyledTags = styled.div` + margin-top: 10px; + margin-bottom: 20px; + display: flex; + flex-wrap: wrap; + gap: 5px; + + @media (max-width: 480px) { + margin-top: 0; + margin-bottom: 10px; + } + + > div { + margin: 0; + } +`; + +const StyledTime = styled.time` + display: flex; + justify-content: flex-end; + font-size: 12px; + color: #92989a; +`; + +function OrganizerCard({ data = {}, isLoading }) { + const educationStage = + EDUCATION_STEP.find(({ key }) => key === data?.user?.educationStage) + ?.label || '暫無資料'; + const role = + ROLE.find(({ key }) => data?.user?.roleList?.includes(key))?.label || + '暫無資料'; + const location = + data?.user?.location === 'tw' ? '台灣' : data?.user?.location; + + return ( + <> + + + +
    + + + {isLoading ? ( + + ) : ( + data?.user?.name + )} + + + {isLoading ? ( + + ) : ( + educationStage + )} + + + + {isLoading ? : role} + +
    +
    + + location icon + {isLoading ? : location} + +
    + + {isLoading ? ( +
    + + + +
    + ) : ( + data?.description + )} +
    + + {Array.isArray(data?.tagList) && + data.tagList.map((tag) => )} + + + {isLoading ? ( + + ) : ( + timeDuration(data?.updatedDate) + )} + + + ); +} + +export default OrganizerCard; diff --git a/components/Group/detail/TeamInfoCard.jsx b/components/Group/detail/TeamInfoCard.jsx new file mode 100644 index 00000000..fc49049f --- /dev/null +++ b/components/Group/detail/TeamInfoCard.jsx @@ -0,0 +1,89 @@ +import styled from '@emotion/styled'; +import Skeleton from '@mui/material/Skeleton'; +import bachelorCapSvg from '@/public/assets/icons/bachelorCap.svg'; +import categorySvg from '@/public/assets/icons/category.svg'; +import clockSvg from '@/public/assets/icons/clock.svg'; +import locationSvg from '@/public/assets/icons/location.svg'; +import personSvg from '@/public/assets/icons/person.svg'; + +const StyledItem = styled.div` + padding: 7px 0; + display: flex; + + @media (max-width: 480px) { + padding: 12px 0; + flex-direction: column; + } + + h3 { + display: flex; + padding-bottom: 5px; + align-items: center; + min-width: 140px; + font-size: 14px; + font-weight: 500; + color: #293a3d; + gap: 5px; + } + + p { + flex: 1; + font-size: 14px; + font-weight: 400; + color: #536166; + } + + & + & { + border-top: 1px solid #f3f3f3; + } + + &:first-of-type { + padding-top: 0; + } + + &:last-of-type { + padding-bottom: 0; + } +`; + +const labels = [ + { + key: 'category', + icon: categorySvg.src, + text: '學習領域', + format: (v) => (Array.isArray(v) ? v.join('、') : v), + }, + { key: 'area', icon: locationSvg.src, text: '地點' }, + { key: 'time', icon: clockSvg.src, text: '時間' }, + { key: 'partnerStyle', icon: personSvg.src, text: '想找的夥伴' }, + { + key: 'partnerEducationStep', + icon: bachelorCapSvg.src, + text: '適合的教育階段', + }, +]; + +function TeamInfoCard({ data = {}, isLoading }) { + return labels.map( + ({ key, icon, text, format }) => + data[key] && ( + +

    + {`${text} + {text} +

    +

    + {isLoading ? ( + + ) : typeof format === 'function' ? ( + format(data[key]) + ) : ( + data[key] + )} +

    +
    + ), + ); +} + +export default TeamInfoCard; diff --git a/components/Group/detail/index.jsx b/components/Group/detail/index.jsx new file mode 100644 index 00000000..a2e1f349 --- /dev/null +++ b/components/Group/detail/index.jsx @@ -0,0 +1,68 @@ +import Box from '@mui/material/Box'; +import Skeleton from '@mui/material/Skeleton'; +import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew'; +import Image from '@/shared/components/Image'; +import { StyledStatus } from '../GroupList/GroupCard.styled'; +import StyledPaper from '../Paper.styled'; +import TeamInfoCard from './TeamInfoCard'; +import OrganizerCard from './OrganizerCard'; +import More from './More'; +import { StyledContainer, StyledHeading, StyledLink } from './Detail.styled'; +import ContactButton from './Contact'; + +function GroupDetail({ source, isLoading }) { + return ( + + + + + 返回 + + {isLoading ? ( + + ) : ( + {source?.photoAlt} + )} + + {isLoading ? ( + + ) : source?.isGrouping ? ( + 揪團中 + ) : ( + 已結束 + )} + + + {isLoading ? : source?.title} + + + + + + + + + + + + + + ); +} + +export default GroupDetail; diff --git a/components/Group/index.jsx b/components/Group/index.jsx index 6ee2bed2..bf8c4d6d 100644 --- a/components/Group/index.jsx +++ b/components/Group/index.jsx @@ -4,10 +4,11 @@ import AreaChips from './AreaChips'; import Banner from './Banner'; import SearchField from './SearchField'; import SelectedCategory from './SelectedCategory'; +import StyledPaper from './Paper.styled'; import GroupList from './GroupList'; import More from './More'; -const StyledGroup = styled.div` +const StyledContainer = styled.div` position: relative; margin: 70px auto 0; width: 924px; @@ -22,28 +23,20 @@ const StyledGroup = styled.div` } `; -const ContainerWrapper = styled(Box)` - padding: 32px; - border-radius: 20px; - box-shadow: 0px 4px 6px rgba(196, 194, 193, 0.2); - background: #fff; - z-index: 2; -`; - function Group() { return ( - - + + - - + + - - + + ); diff --git a/hooks/useFetch.jsx b/hooks/useFetch.jsx index d8ee6838..98df2b38 100644 --- a/hooks/useFetch.jsx +++ b/hooks/useFetch.jsx @@ -1,17 +1,26 @@ import { useEffect, useState } from 'react'; -const useFetch = (url, initialValue) => { - const [result, setResult] = useState(initialValue); - const [loading, setLoading] = useState(true); +const useFetch = (url, { initialValue } = {}) => { + const [data, setData] = useState(initialValue); + const [isFetching, setIsFetching] = useState(true); + const [isError, setIsError] = useState(false); + useEffect(() => { + let pass = true; + + setIsFetching(true); + setIsError(false); + fetch(url) .then((res) => res.json()) - .then((json) => { - setResult(json); - setLoading(false); - }); - }, []); - return { result, loading }; + .then((json) => pass && setData(json)) + .catch(() => setIsError(true)) + .finally(() => setIsFetching(false)); + + return () => pass = false; + }, [url]); + + return { data, isFetching, isError }; }; export default useFetch; diff --git a/pages/group/create/index.jsx b/pages/group/create/index.jsx new file mode 100644 index 00000000..790b3009 --- /dev/null +++ b/pages/group/create/index.jsx @@ -0,0 +1,35 @@ +import React, { useMemo } from 'react'; +import { useRouter } from 'next/router'; +import SEOConfig from '@/shared/components/SEO'; +import GroupForm from '@/components/Group/Form'; +import Navigation from '@/shared/components/Navigation_v2'; +import Footer from '@/shared/components/Footer_v2'; + +function GroupPage() { + const router = useRouter(); + + const SEOData = useMemo( + () => ({ + title: '發起揪團|島島阿學', + description: + '「島島阿學」揪團專區,結交志同道合的學習夥伴!發起各種豐富多彩的揪團活動,共同探索學習的樂趣。一同參與,共同成長,打造學習的共好社群。加入我們,一起開啟學習的冒險旅程!', + keywords: '島島阿學', + author: '島島阿學', + copyright: '島島阿學', + imgLink: 'https://www.daoedu.tw/preview.webp', + link: `${process.env.HOSTNAME}${router?.asPath}`, + }), + [router?.asPath], + ); + + return ( + <> + + + +