diff --git a/components/Group/AreaChips.jsx b/components/Group/AreaChips.jsx index 9b7e2aa3..6bb3a551 100644 --- a/components/Group/AreaChips.jsx +++ b/components/Group/AreaChips.jsx @@ -8,6 +8,7 @@ const StyledAreaChips = styled.ul` display: flex; flex-wrap: wrap; margin-bottom: 16px; + gap: 12px 0; `; const AreaChips = () => { diff --git a/components/Group/GroupList/GroupCard.jsx b/components/Group/GroupList/GroupCard.jsx index c7d70b57..38a889a1 100644 --- a/components/Group/GroupList/GroupCard.jsx +++ b/components/Group/GroupList/GroupCard.jsx @@ -1,5 +1,7 @@ import LocationOnOutlinedIcon from '@mui/icons-material/LocationOnOutlined'; import Image from '@/shared/components/Image'; +import emptyCoverImg from '@/public/assets/empty-cover.png'; +import { timeDuration } from '@/utils/date'; import { StyledAreas, StyledContainer, @@ -19,10 +21,12 @@ function GroupCard({ partnerEducationStep, description, area, + isGrouping, + updatedDate, }) { return ( - {photoAlt} + {photoAlt {title} @@ -32,7 +36,7 @@ function GroupCard({ 適合階段 - {partnerEducationStep} + {partnerEducationStep || '皆可'} @@ -43,8 +47,12 @@ function GroupCard({ {area} - -
揪團中
+ + {isGrouping ? ( +
揪團中
+ ) : ( +
已結束
+ )}
diff --git a/components/Group/GroupList/GroupCard.styled.jsx b/components/Group/GroupList/GroupCard.styled.jsx index d4fff45c..4c3dd3cf 100644 --- a/components/Group/GroupList/GroupCard.styled.jsx +++ b/components/Group/GroupList/GroupCard.styled.jsx @@ -20,6 +20,10 @@ export const StyledTitle = styled.h2` font-size: 14px; font-weight: bold; line-height: 1.4; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 1; + overflow: hidden; `; export const StyledInfo = styled.div` @@ -66,7 +70,7 @@ export const StyledFooter = styled.footer` border-radius: 50%; } - &.end { + &.finished { --bg-color: #f3f3f3; --color: #92989a; } diff --git a/components/Group/GroupList/index.jsx b/components/Group/GroupList/index.jsx index be9c4ccf..d92ec7ca 100644 --- a/components/Group/GroupList/index.jsx +++ b/components/Group/GroupList/index.jsx @@ -1,4 +1,12 @@ +import { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; import styled from '@emotion/styled'; +import { Box } from '@mui/material'; +import { AREAS } from '@/constants/areas'; +import { CATEGORIES } from '@/constants/category'; +import { EDUCATION_STEP } from '@/constants/member'; +import useSearchParamsManager from '@/hooks/useSearchParamsManager'; +import { setQuery } from '@/redux/actions/group'; import GroupCard from './GroupCard'; export const StyledGroupItem = styled.li` @@ -47,24 +55,59 @@ export const StyledGroupItem = styled.li` const StyledGroupList = styled.ul` display: flex; flex-wrap: wrap; - justify-content: space-between; `; -function GroupList({ list, isLoading }) { +function GroupList() { + const dispatch = useDispatch(); + const [getSearchParams] = useSearchParamsManager(); + const { items, isLoading } = useSelector((state) => state.group); + + useEffect(() => { + const filterOptions = { + area: AREAS, + category: CATEGORIES, + edu: EDUCATION_STEP, + grouping: true, + q: true, + }; + const params = {}; + const searchParams = getSearchParams(); + Object.keys(filterOptions).forEach((key) => { + const searchParam = searchParams[key]; + const options = filterOptions[key]; + + if (searchParam && options) { + params[key] = Array.isArray(options) + ? searchParam + .split(',') + .filter((item) => options.some((option) => option.label === item)) + .join(',') + : searchParam; + } + }); + dispatch(setQuery(params)); + }, [getSearchParams]); + return ( - - {list?.length || isLoading ? ( - list.map((data) => ( - - - - )) - ) : ( -
  • - 哎呀!這裡好像沒有符合你條件的揪團,別失望!讓我們試試其他選項。 -
  • + <> + + {items?.length || isLoading ? ( + items.map((data) => ( + + + + )) + ) : ( +
  • + 哎呀!這裡好像沒有符合你條件的揪團,別失望!讓我們試試其他選項。 +
  • + )} +
    + + {isLoading && ( + 搜尋揪團中~ )} -
    + ); } diff --git a/components/Group/More.jsx b/components/Group/More.jsx new file mode 100644 index 00000000..d7e78f2d --- /dev/null +++ b/components/Group/More.jsx @@ -0,0 +1,33 @@ +import { useDispatch, useSelector } from 'react-redux'; +import { Box, Button } from '@mui/material'; +import { setPageSize } from '@/redux/actions/group'; + +export default function More() { + const dispatch = useDispatch(); + const { pageSize, total, isLoading } = useSelector((state) => state.group); + const isMore = total > pageSize || isLoading; + + return ( + + {isMore ? ( + + ) : ( + '已經到底囉~' + )} + + ); +} diff --git a/components/Group/index.jsx b/components/Group/index.jsx index 7489b305..6ee2bed2 100644 --- a/components/Group/index.jsx +++ b/components/Group/index.jsx @@ -1,11 +1,11 @@ -import { useEffect, useState } from 'react'; import styled from '@emotion/styled'; -import { Box, Button } from '@mui/material'; +import { Box } from '@mui/material'; import AreaChips from './AreaChips'; import Banner from './Banner'; import SearchField from './SearchField'; import SelectedCategory from './SelectedCategory'; import GroupList from './GroupList'; +import More from './More'; const StyledGroup = styled.div` position: relative; @@ -30,41 +30,7 @@ const ContainerWrapper = styled(Box)` z-index: 2; `; -const createTemplate = (_, id) => ({ - id, - title: '颱風天不衝浪要幹嘛', - photoURL: - 'https://images.unsplash.com/photo-1502680390469-be75c86b636f?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxzZWFyY2h8Mnx8c3VyZnxlbnwwfHwwfHw%3D&auto=format&fit=crop&w=800&q=60', - photoAlt: '封面圖', - category: ['語言與文學', '人文社會', '自然科學', '生活'], - partnerEducationStep: '高中', - description: - '希望能像朋友,一起讀有興趣的科目,每週1-2次見面練習這兩種,每次總時數2-3小時不限,希望你跟我一樣很想追求有效進步也不怕辛苦!一起讀日文也可以喔!', - area: '台北市', -}); - -const mockData = (length) => - new Promise((res) => - setTimeout(() => { - res(Array.from({ length }, createTemplate)); - }, 600), - ); - function Group() { - const [total, setTotal] = useState(12); - const [list, setList] = useState([]); - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - const setDataAndLoaded = (data) => { - setList(data); - setIsLoading(false); - }; - - setIsLoading(true); - mockData(total).then(setDataAndLoaded); - }, [total]); - return ( @@ -75,31 +41,10 @@ function Group() { - - {isLoading && ( - - 搜尋揪團中~ - - )} + - - - + ); } diff --git a/constants/areas.js b/constants/areas.js index 44fa4cde..c9a31cec 100644 --- a/constants/areas.js +++ b/constants/areas.js @@ -1,24 +1,25 @@ export const AREAS = [ - { name: '臺北市' }, - { name: '新北市' }, - { name: '基隆市' }, - { name: '桃園市' }, - { name: '新竹市' }, - { name: '新竹縣' }, - { name: '苗栗縣' }, - { name: '臺中市' }, - { name: '南投縣' }, - { name: '彰化縣' }, - { name: '雲林縣' }, - { name: '嘉義市' }, - { name: '嘉義縣' }, - { name: '臺南市' }, - { name: '高雄市' }, - { name: '屏東縣' }, - { name: '臺東縣' }, - { name: '花蓮縣' }, - { name: '宜蘭縣' }, - { name: '澎湖縣' }, - { name: '金門縣' }, - { name: '連江縣' }, + { name: '線上', label: '線上' }, + { name: '台北市', label: '台北市' }, + { name: '新北市', label: '新北市' }, + { name: '基隆市', label: '基隆市' }, + { name: '桃園市', label: '桃園市' }, + { name: '新竹市', label: '新竹市' }, + { name: '新竹縣', label: '新竹縣' }, + { name: '苗栗縣', label: '苗栗縣' }, + { name: '台中市', label: '台中市' }, + { name: '南投縣', label: '南投縣' }, + { name: '彰化縣', label: '彰化縣' }, + { name: '雲林縣', label: '雲林縣' }, + { name: '嘉義市', label: '嘉義市' }, + { name: '嘉義縣', label: '嘉義縣' }, + { name: '台南市', label: '台南市' }, + { name: '高雄市', label: '高雄市' }, + { name: '屏東縣', label: '屏東縣' }, + { name: '台東縣', label: '台東縣' }, + { name: '花蓮縣', label: '花蓮縣' }, + { name: '宜蘭縣', label: '宜蘭縣' }, + { name: '澎湖縣', label: '澎湖縣' }, + { name: '金門縣', label: '金門縣' }, + { name: '連江縣', label: '連江縣' }, ]; diff --git a/constants/category.js b/constants/category.js index 6a43588a..eac54897 100644 --- a/constants/category.js +++ b/constants/category.js @@ -62,50 +62,62 @@ export const SEARCH_TAGS = { export const CATEGORIES = [ { key: 'language', + label: '語言與文學', value: '語言與文學', }, { key: 'math', + label: '數學與邏輯', value: '數學與邏輯', }, { key: 'comsci', + label: '資訊與工程', value: '資訊與工程', }, { key: 'humanity', + label: '人文社會', value: '人文社會', }, { key: 'natusci', + label: '自然科學', value: '自然科學', }, { key: 'art', + label: '藝術', value: '藝術', }, { key: 'education', + label: '教育', value: '教育', }, { key: 'life', + label: '生活', value: '生活', }, { key: 'health', + label: '運動/心理/醫學', value: '運動/心理/醫學', }, { key: 'business', + label: '商業與社會創新', value: '商業與社會創新', }, { key: 'multires', + label: '綜合型學習資源', value: '綜合型學習資源', }, { key: 'learningtools', + label: '學習/教學工具', value: '學習/教學工具', }, ]; diff --git a/public/assets/empty-cover.png b/public/assets/empty-cover.png new file mode 100644 index 00000000..785b7dd0 Binary files /dev/null and b/public/assets/empty-cover.png differ diff --git a/redux/actions/group.js b/redux/actions/group.js new file mode 100644 index 00000000..25bdba2c --- /dev/null +++ b/redux/actions/group.js @@ -0,0 +1,40 @@ +export const DEFAULT_PAGE_SIZE = 6; +export const SET_PAGE_SIZE = 'SET_PAGE_SIZE'; +export const SET_QUERY = 'SET_QUERY'; +export const GET_GROUP_ITEMS_SUCCESS = 'GET_GROUP_ITEMS_SUCCESS'; +export const GET_GROUP_ITEMS_FAILURE = 'GET_GROUP_ITEMS_FAILURE'; +export const BASE_API_URL = + process.env.NEXT_PUBLIC_API_URL || 'https://daodao-server.onrender.com'; +export const GROUP_API_URL = `${BASE_API_URL}/activity`; + +export function setPageSize(pageSize) { + return { + type: SET_PAGE_SIZE, + payload: { + pageSize, + }, + }; +} + +export function setQuery(query = {}) { + return { + type: SET_QUERY, + payload: { + query, + }, + }; +} + +export function getGroupItemsSuccess({ data = [], totalCount = 0 } = {}) { + return { + type: GET_GROUP_ITEMS_SUCCESS, + payload: { + items: data, + total: totalCount, + }, + }; +} + +export function getGroupItemsError(payload) { + return { type: GET_GROUP_ITEMS_FAILURE, payload }; +} diff --git a/redux/reducers/group.js b/redux/reducers/group.js new file mode 100644 index 00000000..106a277c --- /dev/null +++ b/redux/reducers/group.js @@ -0,0 +1,57 @@ +import { + DEFAULT_PAGE_SIZE, + SET_PAGE_SIZE, + SET_QUERY, + GET_GROUP_ITEMS_SUCCESS, + GET_GROUP_ITEMS_FAILURE, +} from '../actions/group'; + +const initialState = { + pageSize: DEFAULT_PAGE_SIZE, + query: {}, + items: [], + total: 0, + isLoading: true, +}; + +const reducer = (state = initialState, action) => { + const pageSize = action?.payload?.pageSize || DEFAULT_PAGE_SIZE; + + switch (action.type) { + case SET_PAGE_SIZE: { + return { + ...state, + pageSize, + isLoading: true, + }; + } + case SET_QUERY: { + return { + ...state, + ...(action.payload ?? {}), + items: [], + pageSize, + isLoading: true, + }; + } + case GET_GROUP_ITEMS_SUCCESS: { + return { + ...state, + ...(action.payload ?? {}), + isLoading: false, + }; + } + case GET_GROUP_ITEMS_FAILURE: { + return { + ...state, + total: 0, + isLoading: false, + }; + } + default: { + return state; + } + } +}; + +export default reducer; diff --git a/redux/reducers/index.js b/redux/reducers/index.js index 5a9b8dcd..e5c924c4 100644 --- a/redux/reducers/index.js +++ b/redux/reducers/index.js @@ -4,6 +4,7 @@ import user from './user'; import theme from './theme'; import shared from './shared'; import resource from './resource'; +import group from './group'; import partners from './partners'; const allReducers = combineReducers({ @@ -12,6 +13,7 @@ const allReducers = combineReducers({ theme, shared, resource, + group, partners, }); diff --git a/redux/sagas/groupSaga.js b/redux/sagas/groupSaga.js new file mode 100644 index 00000000..dada13b8 --- /dev/null +++ b/redux/sagas/groupSaga.js @@ -0,0 +1,28 @@ +import { put, takeEvery, select } from 'redux-saga/effects'; +import { + GROUP_API_URL, + SET_PAGE_SIZE, + SET_QUERY, + getGroupItemsError, + getGroupItemsSuccess, +} from '../actions/group'; + +function* getGroupItems() { + const { + group: { pageSize, query }, + } = yield select(); + const queryString = new URLSearchParams({ ...query, pageSize }).toString(); + const URL = `${GROUP_API_URL}?${queryString}`; + try { + const response = yield fetch(URL).then((res) => res.json()); + yield put(getGroupItemsSuccess(response)); + } catch (error) { + yield put(getGroupItemsError(error)); + } +} + +function* groupSaga() { + yield takeEvery([SET_PAGE_SIZE, SET_QUERY], getGroupItems); +} + +export default groupSaga; diff --git a/redux/sagas/index.js b/redux/sagas/index.js index fabee4cf..6168bd7b 100644 --- a/redux/sagas/index.js +++ b/redux/sagas/index.js @@ -4,6 +4,7 @@ import userSaga from './user'; import partnerSaga from './partnersSaga'; import sharedSaga from './sharedSaga'; import resourceSaga from './resourceSaga'; +import groupSaga from './groupSaga'; export default function* rootSaga() { yield all([ @@ -11,6 +12,7 @@ export default function* rootSaga() { userSaga(), sharedSaga(), resourceSaga(), + groupSaga(), partnerSaga(), ]); } diff --git a/shared/components/Image/index.jsx b/shared/components/Image/index.jsx index 1ef7d0aa..37f3f1a6 100644 --- a/shared/components/Image/index.jsx +++ b/shared/components/Image/index.jsx @@ -1,5 +1,7 @@ import Skeleton from '@mui/material/Skeleton'; import { LazyLoadImage } from 'react-lazy-load-image-component'; +import emptyCoverImg from '@/public/assets/empty-cover.png'; +import { useState } from 'react'; const Loading = () => ( ( - } - /> -); +}) => { + const [isError, setIsError] = useState(false); + return ( + } + onError={() => setIsError(true)} + /> + ); +}; export default Image;