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 (
-
+
{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;