From 2eb05ad5e10f574a22b6975d35d75182a7185f85 Mon Sep 17 00:00:00 2001 From: hsuifang Date: Sun, 29 Oct 2023 15:45:10 +0800 Subject: [PATCH 01/27] feat: Add new ui and remove firebase code (cherry picked from commit c6264500f7d9e358b940c71b5c119ac3663d2d3a) --- .../Partner/PartnerList/PartnerCard/index.jsx | 231 +++++++++++------- components/Partner/PartnerList/index.jsx | 86 ++++--- components/Partner/index.jsx | 159 +++++------- 3 files changed, 239 insertions(+), 237 deletions(-) diff --git a/components/Partner/PartnerList/PartnerCard/index.jsx b/components/Partner/PartnerList/PartnerCard/index.jsx index d94f5b56..14fee834 100644 --- a/components/Partner/PartnerList/PartnerCard/index.jsx +++ b/components/Partner/PartnerList/PartnerCard/index.jsx @@ -1,140 +1,183 @@ -import React, { useRef } from 'react'; +import React from 'react'; import styled from '@emotion/styled'; -import { Box, Typography, Divider, Skeleton } from '@mui/material'; -import LocationOnOutlinedIcon from '@mui/icons-material/LocationOnOutlined'; +import { Grid, Box, Typography, Skeleton } from '@mui/material'; import { LazyLoadImage } from 'react-lazy-load-image-component'; import { WANT_TO_DO_WITH_PARTNER, CATEGORIES, } from '../../../../constants/member'; import { mapToTable } from '../../../../utils/helper'; +import LocationOnOutlinedIcon from '@mui/icons-material/LocationOnOutlined'; const WANT_TO_DO_WITH_PARTNER_TABLE = mapToTable(WANT_TO_DO_WITH_PARTNER); const CATEGORIES_TABLE = mapToTable(CATEGORIES); + +const CardWrapper = styled(Box)` + display: flex; + padding: 12px; + background-color: #fff; + justify-content: space-between; + align-items: flex-start; + width: 100%; +`; +const CardContent = styled(Box)` + width: 100%; +`; +const ImageWrapper = styled(LazyLoadImage)` + display: block; + width: 50px; + height: 50px; + border-radius: 50%; + background: rgba(240, 240, 240, 0.8); + object-fit: cover; + object-position: center; +`; + +const TypoCaption = styled(Typography)` + color: #92989a; + font-family: 'Noto Sans TC'; + font-size: 12px; + font-style: normal; + line-height: 1.4; +`; + +const TagWrap = styled(Grid)` + display: flex; + align-items: center; +`; +const TagText = styled(Grid)` + color: var(--black-white-gray, #536166); + text-align: center; + font-family: 'Noto Sans TC'; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 140%; + border-radius: 13px; + padding: 3px 10px; + display: flex; + justify-content: center; + background: var(--logologo-very-light, #def5f5); +`; + function PartnerCard({ id, image, name, subTitle, + tagList = [], canShare = [], canTogether = [], }) { return ( - - - + + {/* TODO: href redirect */} + + + {image ? ( + + ) : ( + + )} - - } - /> - - - - {name} - - - {subTitle} - - {/* - {' '} - 台北市松山區 - */} - + {name} + + + {subTitle} + - {/* - {tagList.map((tag) => ( - - ))} - */} - - + + 可分享 - + + | + + + 心智圖法(課文資料整理/閱讀/筆記術/記憶術等) + + {/* {canShare .map((item) => WANT_TO_DO_WITH_PARTNER_TABLE[item] || '') .join(', ')} - + */} 想一起 - + + | + + + 學習交流、教學相長 + + {/* {canTogether .map((item) => CATEGORIES_TABLE[item] || '') .join(', ')} - + */} - - + + {/* */} + {tagList.map((tag) => ( + + {tag} + + ))} + + + + 台北市松山區 + + 兩天前更新 + + + ); } diff --git a/components/Partner/PartnerList/index.jsx b/components/Partner/PartnerList/index.jsx index 807fcefe..57071235 100644 --- a/components/Partner/PartnerList/index.jsx +++ b/components/Partner/PartnerList/index.jsx @@ -1,86 +1,94 @@ import React, { useRef } from 'react'; -import styled from '@emotion/styled'; -import { Box, Typography, Divider, Skeleton } from '@mui/material'; -import LocationOnOutlinedIcon from '@mui/icons-material/LocationOnOutlined'; -import { LazyLoadImage } from 'react-lazy-load-image-component'; +import { useDispatch, useSelector } from 'react-redux'; +import { Grid, Box } from '@mui/material'; import PartnerCard from './PartnerCard'; const LIST = [ { + id: 1, name: '許浪手', image: 'https://images.unsplash.com/photo-1502680390469-be75c86b636f?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxzZWFyY2h8Mnx8c3VyZnxlbnwwfHwwfHw%3D&auto=format&fit=crop&w=800&q=60', subTitle: '實驗教育老師', canShare: '心智圖法', canTogether: '學習交流、教學相長', - tags: ['實驗教育'], + tagList: ['tag1', 'tag2', 'tag3'], location: '台北市松山區', }, { + id: 2, name: '許浪手2', image: 'https://images.unsplash.com/photo-1502680390469-be75c86b636f?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxzZWFyY2h8Mnx8c3VyZnxlbnwwfHwwfHw%3D&auto=format&fit=crop&w=800&q=60', subTitle: '實驗教育老師', - canShare: '心智圖法', + canShare: 'make-friends', canTogether: '學習交流、教學相長', - tags: ['實驗教育'], + tagList: ['tag1', 'tag2'], location: '台北市松山區', + date: '兩天前更新', }, { + id: 3, name: '許浪手3', image: 'https://images.unsplash.com/photo-1502680390469-be75c86b636f?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxzZWFyY2h8Mnx8c3VyZnxlbnwwfHwwfHw%3D&auto=format&fit=crop&w=800&q=60', subTitle: '實驗教育老師', canShare: '心智圖法', canTogether: '學習交流、教學相長', - tags: ['實驗教育'], + tagList: ['tag12', 'tag2'], location: '台北市松山區', + date: '兩天前更新', }, { + id: 4, name: '許浪手4', image: 'https://images.unsplash.com/photo-1502680390469-be75c86b636f?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxzZWFyY2h8Mnx8c3VyZnxlbnwwfHwwfHw%3D&auto=format&fit=crop&w=800&q=60', subTitle: '實驗教育老師', canShare: '心智圖法', canTogether: '學習交流、教學相長', - tags: ['實驗教育'], + tagList: ['tag1', 'tag2'], location: '台北市松山區', + date: '兩天前更新', }, ]; -function PartnerList({ list = [] }) { +function PartnerList() { + // TODO: get data from backend + // const user = useSelector((state) => state.user); + const lists = LIST; return ( - - - {list.map( - ({ - id, - userName, - photoURL, - subTitle, - wantToLearnList, - interestAreaList, - }) => ( + + {lists.map((item, idx) => ( + <> + - ), - )} - - + + {(idx + 1) % 2 == 0 && idx + 1 !== lists.length && ( + + + + )} + + ))} + ); } diff --git a/components/Partner/index.jsx b/components/Partner/index.jsx index 6e2c94c5..c78aa2a5 100644 --- a/components/Partner/index.jsx +++ b/components/Partner/index.jsx @@ -1,18 +1,8 @@ -import React, { useEffect, useState } from 'react'; +import React from 'react'; import styled from '@emotion/styled'; -import { Box, Typography, Button } from '@mui/material'; -import { useAuthState } from 'react-firebase-hooks/auth'; -import { getAuth, updateProfile } from 'firebase/auth'; -import { MobileDatePicker } from '@mui/x-date-pickers/MobileDatePicker'; -import { - getFirestore, - collection, - getDocs, - getDoc, - setDoc, - addDoc, -} from 'firebase/firestore'; -import { useRouter } from 'next/router'; +import { Box, Grid, Typography } from '@mui/material'; +import CloseOutlinedIcon from '@mui/icons-material/CloseOutlined'; + import PartnerList from './PartnerList'; import SearchField from './SearchField'; import Banner from './Banner'; @@ -25,45 +15,54 @@ const PartnerWrapper = styled.div` position: relative; `; +const PartnerContent = styled(Box)` + margin-top: 24px; + padding: 32px 40px; + background-color: #fff; + border-radius: 20px; +`; + +const TagWrapper = styled(Box)` + border-radius: 13px; + display: flex; + padding: 3px 7px 3px 10px; + justify-content: center; + align-items: center; + background: #16b9b3; +`; + +const TagText = styled(Typography)` + color: var(--black-white-white, #fff); + text-align: center; + font-family: 'Noto Sans TC'; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 1.4; +`; + +const SearchParamerters = () => ( + + + + 台北市 + + + + + + 桃園市 + + + + +); + function Partner() { - const router = useRouter(); - const [partnerList, setPartnerList] = useState([]); - useEffect(() => { - const db = getFirestore(); - const colRef = collection(db, 'partnerlist'); - getDocs(colRef).then((docsSnap) => { - docsSnap.forEach((doc) => { - setPartnerList((prevState) => [ - ...prevState, - { - id: doc.id, - ...(doc.data() || {}), - }, - ]); - console.log(doc.id, ' => ', doc.data()); - }); - }); - // const test = collection('partnerlist'). - // console.log('test', test); - // const docRef = doc(db, 'partnerlist', user?.uid); - // getDoc(docRef).then((docSnap) => { - // const data = docSnap.data(); - // setUserName(data?.userName || ''); - // setPhotoURL(data?.photoURL || ''); - // setBirthDay(dayjs(data?.birthDay) || dayjs()); - // setGender(data?.gender || ''); - // setRoleList(data?.roleList || []); - // setWantToLearnList(data?.wantToLearnList || []); - // setInterestAreaList(data?.interestAreaList || []); - // setEducationStep(data?.educationStep); - // setLocation(data?.location || ''); - // setUrl(data?.url || ''); - // setDescription(data?.description || ''); - // setIsOpenLocation(data?.isOpenLocation || false); - // setIsOpenProfile(data?.isOpenProfile || false); - // }); - }, [setPartnerList]); - console.log('partnerList', partnerList); return ( <> @@ -85,58 +84,10 @@ function Partner() { > - - - 找夥伴功能架構與維運中,如果你們希望加速開發的腳步的話,歡迎一起加入團隊共同協作! - - - - - - - {/* */} - + + + + From 316ddbdb5c0bb73cded15aa1c8c9136855a84618 Mon Sep 17 00:00:00 2001 From: hsuifang Date: Sun, 29 Oct 2023 17:19:29 +0800 Subject: [PATCH 02/27] chore: Add parameter and nuxt.config (cherry picked from commit 7805306d2878665997989d371c478f499c411820) --- .../Partner/PartnerList/PartnerCard/index.jsx | 5 ----- components/Partner/PartnerList/index.jsx | 21 ++++++++++++++----- next.config.js | 2 +- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/components/Partner/PartnerList/PartnerCard/index.jsx b/components/Partner/PartnerList/PartnerCard/index.jsx index 14fee834..5c4cb2dc 100644 --- a/components/Partner/PartnerList/PartnerCard/index.jsx +++ b/components/Partner/PartnerList/PartnerCard/index.jsx @@ -142,11 +142,6 @@ function PartnerCard({ 學習交流、教學相長 - {/* - {canTogether - .map((item) => CATEGORIES_TABLE[item] || '') - .join(', ')} - */} diff --git a/components/Partner/PartnerList/index.jsx b/components/Partner/PartnerList/index.jsx index 57071235..259e96e3 100644 --- a/components/Partner/PartnerList/index.jsx +++ b/components/Partner/PartnerList/index.jsx @@ -1,4 +1,4 @@ -import React, { useRef } from 'react'; +import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { Grid, Box } from '@mui/material'; import PartnerCard from './PartnerCard'; @@ -14,6 +14,8 @@ const LIST = [ canTogether: '學習交流、教學相長', tagList: ['tag1', 'tag2', 'tag3'], location: '台北市松山區', + share: '', + wantToDoList: [], }, { id: 2, @@ -26,6 +28,8 @@ const LIST = [ tagList: ['tag1', 'tag2'], location: '台北市松山區', date: '兩天前更新', + share: '', + wantToDoList: [], }, { id: 3, @@ -38,6 +42,8 @@ const LIST = [ tagList: ['tag12', 'tag2'], location: '台北市松山區', date: '兩天前更新', + share: '', + wantToDoList: [], }, { id: 4, @@ -50,12 +56,15 @@ const LIST = [ tagList: ['tag1', 'tag2'], location: '台北市松山區', date: '兩天前更新', + share: '', + wantToDoList: [], }, ]; function PartnerList() { // TODO: get data from backend - // const user = useSelector((state) => state.user); + // const user = useSelector((state) => state.user); + // const userURL = `http://localhost:3000/user/all_User`; const lists = LIST; return ( {(idx + 1) % 2 == 0 && idx + 1 !== lists.length && ( diff --git a/next.config.js b/next.config.js index 43ca88ab..4c4dd973 100644 --- a/next.config.js +++ b/next.config.js @@ -5,7 +5,7 @@ const withPWA = require('next-pwa')({ module.exports = withPWA({ reactStrictMode: false, images: { - domains: ['imgur.com'], + domains: ['imgur.com', 'images.unsplash.com'], }, env: { HOSTNAME: 'https://www.daoedu.tw', From cc85fb677bdd7ce5c64e0af55ea279b923aaf9a9 Mon Sep 17 00:00:00 2001 From: hsuifang Date: Mon, 20 Nov 2023 16:26:27 +0800 Subject: [PATCH 03/27] feat(partner): Modify PartnerList and fetch api with redux-saga (cherry picked from commit 96606486d736013168fd14601bd88db45b4ce259) --- .../PartnerCard/PartnerCard.styled.jsx | 112 ++++++++ .../Partner/PartnerList/PartnerCard/index.jsx | 264 +++++++----------- components/Partner/PartnerList/index.jsx | 90 ++---- components/Partner/index.jsx | 111 +++++--- constants/member.js | 8 + next.config.js | 2 +- redux/actions/partners.js | 5 + redux/actions/user.js | 6 + redux/reducers/index.js | 2 + redux/reducers/partners.js | 24 ++ redux/sagas/index.js | 9 +- redux/sagas/partnersSaga.js | 17 ++ redux/sagas/user/index.js | 13 +- 13 files changed, 384 insertions(+), 279 deletions(-) create mode 100644 components/Partner/PartnerList/PartnerCard/PartnerCard.styled.jsx create mode 100644 redux/actions/partners.js create mode 100644 redux/reducers/partners.js create mode 100644 redux/sagas/partnersSaga.js diff --git a/components/Partner/PartnerList/PartnerCard/PartnerCard.styled.jsx b/components/Partner/PartnerList/PartnerCard/PartnerCard.styled.jsx new file mode 100644 index 00000000..de5d5a61 --- /dev/null +++ b/components/Partner/PartnerList/PartnerCard/PartnerCard.styled.jsx @@ -0,0 +1,112 @@ +import styled from '@emotion/styled'; +import { LazyLoadImage } from 'react-lazy-load-image-component'; +import { Grid, Box, Typography } from '@mui/material'; + +export const StyledCard = styled(Box)` + display: flex; + padding: 12px; + background-color: #fff; + justify-content: space-between; + align-items: flex-start; + width: 100%; + border-radius: 20px; + cursor: pointer; + &:hover { + box-shadow: 0px 4px 10px 0px rgba(196, 194, 193, 0.4); + h2 { + color: #16b9b3; + } + } +`; + +export const StyledCardContainer = styled(Box)` + width: 100%; +`; + +export const StyledImage = styled(LazyLoadImage)` + display: block; + width: 50px; + height: 50px; + border-radius: 50%; + background: rgba(240, 240, 240, 0.8); + object-fit: cover; + object-position: center; +`; + +export const StyledCardTitle = styled.h2` + color: #293a3d; + font-weight: 500; + font-size: 16px; + margin-right: 5px; +`; + +export const StyledCardLabel = styled(Typography)` + color: var(--black-white-gray-dark, #293a3d); + text-align: center; + font-family: Noto Sans TC; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 140%; + border-radius: 4px; + background: #f3f3f3; + padding: 3px 10px; +`; + +export const StyledCardSubtitle = styled(Typography)` + color: #92989a; + font-weight: 400; + font-size: 14px; +`; + +export const StyledTypoCaption = styled(Typography)` + color: #92989a; + font-family: 'Noto Sans TC'; + font-size: 12px; + font-style: normal; + line-height: 1.4; +`; + +export const StyledTagContainer = styled(Grid)` + display: flex; + align-items: center; +`; + +export const StyledTagText = styled(Grid)` + color: var(--black-white-gray, #536166); + text-align: center; + font-family: 'Noto Sans TC'; + font-size: 12px; + font-style: normal; + line-height: 1.4; + border-radius: 13px; + padding: 3px 10px; + display: flex; + justify-content: center; + background: #def5f5; +`; + +export const StyledTypoEllipsis = styled(Box)` + display: block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +`; + +export const FlexSBAlignCenter = styled(Box)` + display: flex; + justify-content: space-between; + align-items: center; +`; + +export const FlexAlignCenter = styled(Box)` + display: flex; + align-items: center; +`; + +export const FlexColCenterSB = styled(Box)` + display: flex; + flex-direction: column; + justify-content: center; + align-items: space-between; +`; diff --git a/components/Partner/PartnerList/PartnerCard/index.jsx b/components/Partner/PartnerList/PartnerCard/index.jsx index 5c4cb2dc..d3622a84 100644 --- a/components/Partner/PartnerList/PartnerCard/index.jsx +++ b/components/Partner/PartnerList/PartnerCard/index.jsx @@ -1,178 +1,114 @@ -import React from 'react'; -import styled from '@emotion/styled'; -import { Grid, Box, Typography, Skeleton } from '@mui/material'; -import { LazyLoadImage } from 'react-lazy-load-image-component'; +import { Box, Typography, Skeleton } from '@mui/material'; + import { - WANT_TO_DO_WITH_PARTNER, - CATEGORIES, -} from '../../../../constants/member'; + StyledCard, + StyledCardContainer, + StyledImage, + StyledCardTitle, + StyledCardLabel, + StyledCardSubtitle, + StyledTypoCaption, + StyledTagContainer, + StyledTagText, + StyledTypoEllipsis, + FlexSBAlignCenter, + FlexAlignCenter, + FlexColCenterSB, +} from './PartnerCard.styled'; + +import { WANT_TO_DO_WITH_PARTNER } from '../../../../constants/member'; + import { mapToTable } from '../../../../utils/helper'; import LocationOnOutlinedIcon from '@mui/icons-material/LocationOnOutlined'; const WANT_TO_DO_WITH_PARTNER_TABLE = mapToTable(WANT_TO_DO_WITH_PARTNER); -const CATEGORIES_TABLE = mapToTable(CATEGORIES); -const CardWrapper = styled(Box)` - display: flex; - padding: 12px; - background-color: #fff; - justify-content: space-between; - align-items: flex-start; - width: 100%; -`; -const CardContent = styled(Box)` - width: 100%; -`; -const ImageWrapper = styled(LazyLoadImage)` - display: block; - width: 50px; - height: 50px; - border-radius: 50%; - background: rgba(240, 240, 240, 0.8); - object-fit: cover; - object-position: center; -`; - -const TypoCaption = styled(Typography)` - color: #92989a; - font-family: 'Noto Sans TC'; - font-size: 12px; - font-style: normal; - line-height: 1.4; -`; +// component +const PartnerAvator = ({ image }) => { + return image ? ( + + ) : ( + + ); +}; +const DescriptSection = ({ title, content, ...rest }) => { + return ( + + + {title} + + + | + + + {content || '尚未填寫'} + + + ); +}; +const TagSection = ({ tagList = [] }) => { + const showItems = tagList.slice(0, 5); + const hideItems = tagList.slice(5); + return ( + + {showItems.map((tag, idx) => ( + + {tag} + + ))} + {hideItems.length ? ( + {hideItems.length} + ) : ( + '' + )} + + ); +}; -const TagWrap = styled(Grid)` - display: flex; - align-items: center; -`; -const TagText = styled(Grid)` - color: var(--black-white-gray, #536166); - text-align: center; - font-family: 'Noto Sans TC'; - font-size: 12px; - font-style: normal; - font-weight: 400; - line-height: 140%; - border-radius: 13px; - padding: 3px 10px; - display: flex; - justify-content: center; - background: var(--logologo-very-light, #def5f5); -`; +function PartnerCard({ image, name, share, tagList = [], wantToDoList = [] }) { + const wantTodo = wantToDoList + .map((item) => WANT_TO_DO_WITH_PARTNER_TABLE[item]) + .join('、'); -function PartnerCard({ - id, - image, - name, - subTitle, - tagList = [], - canShare = [], - canTogether = [], -}) { return ( - + {/* TODO: href redirect */} - - - {image ? ( - - ) : ( - - )} - - - {name} - - - {subTitle} - - - - - - - 可分享 - - - | - - - 心智圖法(課文資料整理/閱讀/筆記術/記憶術等) - - {/* - {canShare - .map((item) => WANT_TO_DO_WITH_PARTNER_TABLE[item] || '') - .join(', ')} - */} - - - - 想一起 - - - | - - - 學習交流、教學相長 - - - - - {/* */} - {tagList.map((tag) => ( - - {tag} - - ))} - - - - 台北市松山區 - - 兩天前更新 + + + + + + {name} + 國中 + + 實驗教育老師 + + + + + - - + + + + + + 台北市松山區 + + + 兩天前更新 + + + ); } diff --git a/components/Partner/PartnerList/index.jsx b/components/Partner/PartnerList/index.jsx index 259e96e3..03128964 100644 --- a/components/Partner/PartnerList/index.jsx +++ b/components/Partner/PartnerList/index.jsx @@ -1,71 +1,28 @@ -import React from 'react'; +import { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import { fetchAllPartners } from '../../../redux/actions/partners'; +import useMediaQuery from '@mui/material/useMediaQuery'; import { Grid, Box } from '@mui/material'; import PartnerCard from './PartnerCard'; - -const LIST = [ - { - id: 1, - name: '許浪手', - image: - 'https://images.unsplash.com/photo-1502680390469-be75c86b636f?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxzZWFyY2h8Mnx8c3VyZnxlbnwwfHwwfHw%3D&auto=format&fit=crop&w=800&q=60', - subTitle: '實驗教育老師', - canShare: '心智圖法', - canTogether: '學習交流、教學相長', - tagList: ['tag1', 'tag2', 'tag3'], - location: '台北市松山區', - share: '', - wantToDoList: [], - }, - { - id: 2, - name: '許浪手2', - image: - 'https://images.unsplash.com/photo-1502680390469-be75c86b636f?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxzZWFyY2h8Mnx8c3VyZnxlbnwwfHwwfHw%3D&auto=format&fit=crop&w=800&q=60', - subTitle: '實驗教育老師', - canShare: 'make-friends', - canTogether: '學習交流、教學相長', - tagList: ['tag1', 'tag2'], - location: '台北市松山區', - date: '兩天前更新', - share: '', - wantToDoList: [], - }, - { - id: 3, - name: '許浪手3', - image: - 'https://images.unsplash.com/photo-1502680390469-be75c86b636f?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxzZWFyY2h8Mnx8c3VyZnxlbnwwfHwwfHw%3D&auto=format&fit=crop&w=800&q=60', - subTitle: '實驗教育老師', - canShare: '心智圖法', - canTogether: '學習交流、教學相長', - tagList: ['tag12', 'tag2'], - location: '台北市松山區', - date: '兩天前更新', - share: '', - wantToDoList: [], - }, - { - id: 4, - name: '許浪手4', - image: - 'https://images.unsplash.com/photo-1502680390469-be75c86b636f?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxzZWFyY2h8Mnx8c3VyZnxlbnwwfHwwfHw%3D&auto=format&fit=crop&w=800&q=60', - subTitle: '實驗教育老師', - canShare: '心智圖法', - canTogether: '學習交流、教學相長', - tagList: ['tag1', 'tag2'], - location: '台北市松山區', - date: '兩天前更新', - share: '', - wantToDoList: [], - }, -]; +import mockData from './mock-data'; function PartnerList() { - // TODO: get data from backend - // const user = useSelector((state) => state.user); - // const userURL = `http://localhost:3000/user/all_User`; - const lists = LIST; + const partners = useSelector((state) => state.partners); + const dispatch = useDispatch(); + + // TODO: ADD PAGE + const handleFetchData = () => { + dispatch(fetchAllPartners()); + }; + + useEffect(() => { + handleFetchData(); + }, []); + + const lists = partners.items || []; + // const lists = mockData; + const mobileScreen = useMediaQuery('(max-width: 767px)'); + return ( {lists.map((item, idx) => ( <> - + - {(idx + 1) % 2 == 0 && idx + 1 !== lists.length && ( + {!mobileScreen && (idx + 1) % 2 == 0 && idx + 1 !== lists.length && ( diff --git a/components/Partner/index.jsx b/components/Partner/index.jsx index c78aa2a5..99eef4c0 100644 --- a/components/Partner/index.jsx +++ b/components/Partner/index.jsx @@ -1,28 +1,41 @@ import React from 'react'; +import { useSelector } from 'react-redux'; import styled from '@emotion/styled'; -import { Box, Grid, Typography } from '@mui/material'; +import { Box, Grid, Typography, Button } from '@mui/material'; import CloseOutlinedIcon from '@mui/icons-material/CloseOutlined'; - import PartnerList from './PartnerList'; import SearchField from './SearchField'; import Banner from './Banner'; -const PartnerWrapper = styled.div` +const StyledWrapper = styled.div` min-height: 100vh; background-color: transparent; z-index: 100; margin-top: -150px; position: relative; + padding: 0 10%; `; - -const PartnerContent = styled(Box)` +const StyledContent = styled(Box)` margin-top: 24px; padding: 32px 40px; background-color: #fff; border-radius: 20px; + @media (max-width: 767px) { + padding: 0; + background-color: transparent; + } +`; + +const StyledSearchWrapper = styled(Box)` + margin-top: 24px; + border-radius: '20px'; + box-shadow: 0px 4px 6px rgba(196, 194, 193, 0.2); + padding: 40px; + z-index: 2; + background: #fff; `; -const TagWrapper = styled(Box)` +const StyledTagWrapper = styled(Box)` border-radius: 13px; display: flex; padding: 3px 7px 3px 10px; @@ -31,8 +44,8 @@ const TagWrapper = styled(Box)` background: #16b9b3; `; -const TagText = styled(Typography)` - color: var(--black-white-white, #fff); +const StyledTagText = styled(Typography)` + color: #fff; text-align: center; font-family: 'Noto Sans TC'; font-size: 14px; @@ -41,55 +54,61 @@ const TagText = styled(Typography)` line-height: 1.4; `; -const SearchParamerters = () => ( - - - - 台北市 - - - - - - 桃園市 - - +const SearchParamsList = ({ params }) => + params.length ? ( + + {params.map((param, idx) => ( + + + {param} + + + + ))} - -); + ) : ( + '' + ); function Partner() { + const partners = useSelector((state) => state.partners); + return ( <> - - + + + + + + + + + {partners.length && ( - + - - - - - - + )} + ); } diff --git a/constants/member.js b/constants/member.js index ef40c414..ed78ee31 100644 --- a/constants/member.js +++ b/constants/member.js @@ -82,6 +82,14 @@ export const EDUCATION_STEP = [ label: '大學', value: 'university', }, + { + label: '碩士', + value: 'master', + }, + { + label: '博士', + value: 'doctor', + }, { label: '其他', value: 'other', diff --git a/next.config.js b/next.config.js index 4c4dd973..4c9f5cac 100644 --- a/next.config.js +++ b/next.config.js @@ -5,7 +5,7 @@ const withPWA = require('next-pwa')({ module.exports = withPWA({ reactStrictMode: false, images: { - domains: ['imgur.com', 'images.unsplash.com'], + domains: ['imgur.com', 'images.unsplash.com', 'lh3.googleusercontent.com'], }, env: { HOSTNAME: 'https://www.daoedu.tw', diff --git a/redux/actions/partners.js b/redux/actions/partners.js new file mode 100644 index 00000000..39848510 --- /dev/null +++ b/redux/actions/partners.js @@ -0,0 +1,5 @@ +export function fetchAllPartners() { + return { + type: 'FETCH_ALL_PARTNERS', + }; +} diff --git a/redux/actions/user.js b/redux/actions/user.js index 7aa420e6..12b05ec5 100644 --- a/redux/actions/user.js +++ b/redux/actions/user.js @@ -10,6 +10,12 @@ export function checkUserAccount() { }; } +export function fetchAllUsers() { + return { + type: 'FETCH_ALL_USERS', + }; +} + export function addResourceToCollection(resourceId) { return { type: 'ADD_RESOURCE_TO_COLLECTION', diff --git a/redux/reducers/index.js b/redux/reducers/index.js index 7ab81a62..5a9b8dcd 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 partners from './partners'; const allReducers = combineReducers({ search, @@ -11,6 +12,7 @@ const allReducers = combineReducers({ theme, shared, resource, + partners, }); export default allReducers; diff --git a/redux/reducers/partners.js b/redux/reducers/partners.js new file mode 100644 index 00000000..745c2675 --- /dev/null +++ b/redux/reducers/partners.js @@ -0,0 +1,24 @@ +const initialState = { + items: [], +}; + +const reducer = (state = initialState, action) => { + switch (action.type) { + case 'FETCH_ALL_PARTNERS_SUCCESS': { + return { + ...state, + items: action.payload, + }; + } + case 'FETCH_ALL_PARTNERS_FAILURE': { + return { + ...initialState, + }; + } + default: { + return state; + } + } +}; + +export default reducer; diff --git a/redux/sagas/index.js b/redux/sagas/index.js index 95794dd6..fabee4cf 100644 --- a/redux/sagas/index.js +++ b/redux/sagas/index.js @@ -1,9 +1,16 @@ import { all } from 'redux-saga/effects'; import searchSaga from './searchSaga'; import userSaga from './user'; +import partnerSaga from './partnersSaga'; import sharedSaga from './sharedSaga'; import resourceSaga from './resourceSaga'; export default function* rootSaga() { - yield all([searchSaga(), userSaga(), sharedSaga(), resourceSaga()]); + yield all([ + searchSaga(), + userSaga(), + sharedSaga(), + resourceSaga(), + partnerSaga(), + ]); } diff --git a/redux/sagas/partnersSaga.js b/redux/sagas/partnersSaga.js new file mode 100644 index 00000000..2e0bac5f --- /dev/null +++ b/redux/sagas/partnersSaga.js @@ -0,0 +1,17 @@ +import { put, takeEvery } from 'redux-saga/effects'; + +function* fetchAllPartners() { + try { + const URL = `${process.env.NEXT_PUBLIC_API_URL}/user/all_User`; + const result = yield fetch(URL).then((res) => res.json()); + yield put({ type: 'FETCH_ALL_PARTNERS_SUCCESS', payload: result }); + } catch (error) { + yield put({ type: 'FETCH_ALL_PARTNERS_FAILURE' }); + } +} + +function* userSaga() { + yield takeEvery('FETCH_ALL_PARTNERS', fetchAllPartners); +} + +export default userSaga; diff --git a/redux/sagas/user/index.js b/redux/sagas/user/index.js index d2f554c4..bff7cc47 100644 --- a/redux/sagas/user/index.js +++ b/redux/sagas/user/index.js @@ -1,4 +1,4 @@ -import { put, all, take, takeEvery, select } from 'redux-saga/effects'; +import { put, all, take, takeEvery, select, call } from 'redux-saga/effects'; import * as localforage from 'localforage'; import firebase from '../../../utils/firebase'; @@ -36,9 +36,20 @@ function* userLogin() { } } +function* fetchAllUsers() { + try { + const URL = process.env.NEXT_PUBLIC_API_URL; + const result = yield call(URL); + yield put({ type: 'FETCH_ALL_USER_SUCCESS', payload: result }); + } catch (error) { + yield put({ type: 'FETCH_ALL_USER_FAILURE' }); + } +} + function* userSaga() { yield takeEvery('CHECK_USER_ACCOUNT', checkUserStatus); yield takeEvery('USER_LOGIN', userLogin); + yield takeEvery('FETCH_ALL_USERS', fetchAllUsers); } export default userSaga; From dc97fb21863b107ca338764a9a9225a4d55bda59 Mon Sep 17 00:00:00 2001 From: hsuifang Date: Mon, 20 Nov 2023 16:36:27 +0800 Subject: [PATCH 04/27] =?UTF-8?q?fix(partner):=20=20show=20=E9=A1=AF?= =?UTF-8?q?=E7=A4=BA=E6=9B=B4=E5=A4=9A=E7=9A=84=20Button=20=E5=8F=8A=20mob?= =?UTF-8?q?ile=20=E6=A8=A3=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (cherry picked from commit 9f4336068a394282b0fcc99d068dc4cc15101790) --- components/Partner/index.jsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/components/Partner/index.jsx b/components/Partner/index.jsx index 99eef4c0..41736b51 100644 --- a/components/Partner/index.jsx +++ b/components/Partner/index.jsx @@ -3,6 +3,7 @@ import { useSelector } from 'react-redux'; import styled from '@emotion/styled'; import { Box, Grid, Typography, Button } from '@mui/material'; import CloseOutlinedIcon from '@mui/icons-material/CloseOutlined'; +import useMediaQuery from '@mui/material/useMediaQuery'; import PartnerList from './PartnerList'; import SearchField from './SearchField'; import Banner from './Banner'; @@ -73,6 +74,7 @@ const SearchParamsList = ({ params }) => ); function Partner() { + const mobileScreen = useMediaQuery('(max-width: 767px)'); const partners = useSelector((state) => state.partners); return ( @@ -86,13 +88,13 @@ function Partner() { - {partners.length && ( + {partners.items.length && ( - - - + + +

尋找夥伴

+

想找到一起交流的學習夥伴嗎

+

註冊加入會員,並填寫個人資料,你的資訊就會刊登在頁面上囉!

+ +
+ + 尋找夥伴 + +
); }; From d3381fdc450e62d35ca5271eadca744f1a0a41af Mon Sep 17 00:00:00 2001 From: hsuifang Date: Tue, 28 Nov 2023 22:36:47 +0800 Subject: [PATCH 06/27] fix(Partner/PartnerList): fix logical and ui style --- components/Partner/PartnerList/PartnerCard/index.jsx | 4 +--- components/Partner/PartnerList/index.jsx | 5 ++--- components/Partner/index.jsx | 7 ++++--- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/components/Partner/PartnerList/PartnerCard/index.jsx b/components/Partner/PartnerList/PartnerCard/index.jsx index d3622a84..424abee3 100644 --- a/components/Partner/PartnerList/PartnerCard/index.jsx +++ b/components/Partner/PartnerList/PartnerCard/index.jsx @@ -65,10 +65,8 @@ const TagSection = ({ tagList = [] }) => { {tag} ))} - {hideItems.length ? ( + {hideItems.length > 0 && ( {hideItems.length} - ) : ( - '' )} ); diff --git a/components/Partner/PartnerList/index.jsx b/components/Partner/PartnerList/index.jsx index 03128964..96c91836 100644 --- a/components/Partner/PartnerList/index.jsx +++ b/components/Partner/PartnerList/index.jsx @@ -4,7 +4,6 @@ import { fetchAllPartners } from '../../../redux/actions/partners'; import useMediaQuery from '@mui/material/useMediaQuery'; import { Grid, Box } from '@mui/material'; import PartnerCard from './PartnerCard'; -import mockData from './mock-data'; function PartnerList() { const partners = useSelector((state) => state.partners); @@ -20,12 +19,12 @@ function PartnerList() { }, []); const lists = partners.items || []; - // const lists = mockData; const mobileScreen = useMediaQuery('(max-width: 767px)'); return ( {lists.map((item, idx) => ( <> - + - params.length ? ( + params.length > 0 && ( {params.map((param, idx) => ( @@ -69,8 +72,6 @@ const SearchParamsList = ({ params }) => ))} - ) : ( - '' ); function Partner() { From 2499210f0f3aacf64a7115c35999ae0f2401ce09 Mon Sep 17 00:00:00 2001 From: hsuifang Date: Sun, 3 Dec 2023 14:12:55 +0800 Subject: [PATCH 07/27] refactor: remove useless file --- components/Partner/About/Tags/index.jsx | 108 ----------- components/Partner/About/index.jsx | 154 --------------- components/Partner/Group/index.jsx | 137 -------------- .../Partner/SearchField/AgeCheckbox/index.jsx | 91 --------- .../Partner/SearchField/AgeDropdown/index.jsx | 81 -------- .../Partner/SearchField/FeeDropdown/index.jsx | 99 ---------- .../Partner/SearchField/HotTags/index.jsx | 50 ----- .../Partner/SearchField/HotTags/item.jsx | 72 ------- .../SearchField/SearchInput/Button/index.jsx | 39 ---- .../SearchInput/SuggestList/index.jsx | 99 ---------- .../Partner/SearchField/SearchInput/index.jsx | 178 ------------------ 11 files changed, 1108 deletions(-) delete mode 100644 components/Partner/About/Tags/index.jsx delete mode 100644 components/Partner/About/index.jsx delete mode 100644 components/Partner/Group/index.jsx delete mode 100644 components/Partner/SearchField/AgeCheckbox/index.jsx delete mode 100644 components/Partner/SearchField/AgeDropdown/index.jsx delete mode 100644 components/Partner/SearchField/FeeDropdown/index.jsx delete mode 100644 components/Partner/SearchField/HotTags/index.jsx delete mode 100644 components/Partner/SearchField/HotTags/item.jsx delete mode 100644 components/Partner/SearchField/SearchInput/Button/index.jsx delete mode 100644 components/Partner/SearchField/SearchInput/SuggestList/index.jsx delete mode 100644 components/Partner/SearchField/SearchInput/index.jsx diff --git a/components/Partner/About/Tags/index.jsx b/components/Partner/About/Tags/index.jsx deleted file mode 100644 index 3f21a73f..00000000 --- a/components/Partner/About/Tags/index.jsx +++ /dev/null @@ -1,108 +0,0 @@ -import React, { useCallback, useMemo } from 'react'; -import styled from '@emotion/styled'; -import { useRouter } from 'next/router'; -import Chip from '@mui/material/Chip'; -import Link from 'next/link'; -import { COLOR_TABLE } from '../../../../constants/notion'; - -const TagsWrapper = styled.ul` - display: flex; - flex-wrap: wrap; -`; - -// const TagItemWrapper = styled.li` -// color: black; -// border-radius: 15px; -// padding: 2px 10px; -// margin: 0 5px; -// white-space: nowrap; -// cursor: pointer; -// ${({ color }) => css` -// background-color: ${COLOR_TABLE[color ?? "default"]}; -// `} -// `; - -const Tags = ({ tags = [], type }) => { - const { query, push } = useRouter(); - const linkTagsHandler = useCallback( - (newQuery) => { - // 複製一份,避免影響到使用體驗 - const clonedQuery = { ...query }; - delete clonedQuery.title; - if (clonedQuery[type]) { - push({ - pathname: '/search', - query: { - ...clonedQuery, - [type]: [clonedQuery[type].split(','), newQuery].join(','), - }, - }); - } else { - push({ - pathname: '/search', - query: { - ...clonedQuery, - [type]: newQuery, - }, - }); - } - }, - [push, query, type], - ); - - const linkList = useMemo(() => { - return tags.map((newQuery) => { - // 複製一份,避免影響到使用體驗 - const clonedQuery = { ...query }; - delete clonedQuery.title; - if (clonedQuery[type]) { - const queryObject = { - ...clonedQuery, - [type]: [clonedQuery[type].split(','), newQuery].join(','), - }; - const queryStirng = Object.keys(queryObject) - .map((key) => queryObject[key]) - .join('&'); - return `/search?${queryStirng}`; - } else { - const queryObject = { - ...clonedQuery, - [type]: newQuery, - }; - const queryStirng = Object.keys(queryObject) - .map((key) => queryObject[key]) - .join('&'); - return `/search?${queryStirng}`; - } - }); - }, [tags, query]); - - return ( - - {tags.map(({ name, color }, index) => ( -
  • - - linkTagsHandler(name)} - sx={{ - backgroundColor: COLOR_TABLE[color ?? 'default'], - cursor: 'pointer', - margin: '5px', - whiteSpace: 'nowrap', - fontWeight: 500, - fontSize: '14px', - '&:hover': { - opacity: '60%', - transition: 'transform 0.4s', - }, - }} - /> - -
  • - ))} -
    - ); -}; - -export default Tags; diff --git a/components/Partner/About/index.jsx b/components/Partner/About/index.jsx deleted file mode 100644 index dd4b6085..00000000 --- a/components/Partner/About/index.jsx +++ /dev/null @@ -1,154 +0,0 @@ -import React from 'react'; -import styled from '@emotion/styled'; -import Box from '@mui/material/Box'; -import { Typography, Button } from '@mui/material'; -import { FacebookRounded } from '@mui/icons-material'; -import Chip from '@mui/material/Chip'; -import { useRouter } from 'next/router'; -import { COLOR_TABLE } from '../../../constants/notion'; -import { CATEGORIES } from '../../../constants/category'; -import RelatedResources from '../../../shared/components/RelatedResources'; - -const GuideWrapper = styled.div` - width: 90%; - /* height: calc(var(--section-height) + var(--section-height-offset)); */ - margin: 0 auto; - padding-top: 80px; - padding-bottom: 80px; - .guide-title { - color: #536166; - font-weight: bold; - font-size: 40px; - line-height: 50px; - letter-spacing: 0.08em; - margin-left: '20px'; - } - - @media (max-width: 767px) { - padding-top: 40px; - padding-bottom: 20px; - } -`; - -const About = () => { - const router = useRouter(); - return ( - - - 來點島島阿學的資源吧! - - - coffeeandlearning - - - - 「學習資源爆炸多,卻常常找不到適合自己的?」 - - - - ✅ 由各領域資深學習者分享及彙整 - - - ✅ 免費資源百百種 - - - ✅ 資源跨領域跨年齡跨國 - - - ✅ 三鍵篩選出合適資源 - - - ✅ 人人都可以分享資源 - - - 自主學習的時代,用共好共享成為彼此學習路上的橋樑吧! - - - - 豐富的學習類別 - - - {CATEGORIES.map(({ key, value }) => ( - router.push(`/search?cats=${value}`)} - sx={{ - backgroundColor: COLOR_TABLE.green, - opacity: '60%', - cursor: 'pointer', - margin: '5px', - whiteSpace: 'nowrap', - fontWeight: 500, - fontSize: '16px', - '&:hover': { - opacity: '100%', - backgroundColor: COLOR_TABLE.green, - transition: 'transform 0.4s', - }, - }} - /> - ))} - - - - - - - ); -}; - -export default About; diff --git a/components/Partner/Group/index.jsx b/components/Partner/Group/index.jsx deleted file mode 100644 index daae7a54..00000000 --- a/components/Partner/Group/index.jsx +++ /dev/null @@ -1,137 +0,0 @@ -import React, { useState } from 'react'; -import styled from '@emotion/styled'; -import Box from '@mui/material/Box'; -import { Button, Typography } from '@mui/material'; -import { FacebookRounded } from '@mui/icons-material'; -import { useRouter } from 'next/router'; -import WramModal from '../../../shared/components/WarmModal'; - -const GroupWrapper = styled.div` - width: 90%; - /* height: calc(var(--section-height) + var(--section-height-offset)); */ - margin: 0 auto; - padding-top: 80px; - padding-bottom: 80px; - - @media (max-width: 767px) { - padding-top: 40px; - padding-bottom: 20px; - } -`; - -const Group = () => { - const router = useRouter(); - const [open, setOpen] = useState(false); - - return ( - - - 加入島島阿學學習社群 - - - - - - 我們是島島阿學學習社群,努力搭起互助學習的橋梁。 - - - - - 期盼以集體智慧,打造沒有天花板的學習環境,一個以自主學習為主的民主社群。 - - - - - 目前提供學習資源網以及社群的服務,包含各領域各種形式的資源、學習活動、學習經驗、教育新聞等等。 - - - - - 我們認為社群即資源、支援,讓學習者在民主教育的社群中,以共好的概念,解決彼此學習的問題,支持彼此成為自己想成為的人。 - - - - - 社群中有許多有愛的島友即時地分享各種學習資源唷!快加入吧! - - - - - - - - - - - group - - - ); -}; - -export default Group; diff --git a/components/Partner/SearchField/AgeCheckbox/index.jsx b/components/Partner/SearchField/AgeCheckbox/index.jsx deleted file mode 100644 index 20fc06de..00000000 --- a/components/Partner/SearchField/AgeCheckbox/index.jsx +++ /dev/null @@ -1,91 +0,0 @@ -/* eslint-disable react/jsx-wrap-multilines */ -import React, { useState } from 'react'; -import styled from '@emotion/styled'; -import { Box, Select, MenuItem } from '@mui/material'; -import { useRouter } from 'next/router'; -// import { SEARCH_TAGS } from "../../../constants/category"; -import OutlinedInput from '@mui/material/OutlinedInput'; -import InputLabel from '@mui/material/InputLabel'; -import FormControl from '@mui/material/FormControl'; -import Chip from '@mui/material/Chip'; -import FormLabel from '@mui/material/FormLabel'; -import FormGroup from '@mui/material/FormGroup'; -import FormControlLabel from '@mui/material/FormControlLabel'; -import FormHelperText from '@mui/material/FormHelperText'; -import Checkbox from '@mui/material/Checkbox'; - -const ITEM_HEIGHT = 48; -const ITEM_PADDING_TOP = 8; -const MenuProps = { - PaperProps: { - style: { - maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, - width: 250, - }, - }, -}; - -const names = ['學齡前', '國小', '國高中', '大學以上']; - -const AgeDropdown = () => { - const { query, push } = useRouter(); - const ages = query?.ages ? (query?.ages).split(',') : []; - const handleChange = (event) => { - const newAges = query?.ages ? (query?.ages).split(',') : []; - const { name } = event.target; - const { checked } = event.target; - if (checked) { - newAges.push(name); - } else { - const index = newAges.indexOf(name); - newAges.splice(index, 1); - } - if (newAges.length === 0) { - delete query.ages; - push({ - pathname: '/search', - query, - }); - } else { - push({ - pathname: '/search', - query: { - ...query, - ages: newAges.join(','), - }, - }); - } - }; - return ( - - 年齡層 - - {names.map((name) => ( - - } - /> - ))} - - - ); -}; - -export default AgeDropdown; diff --git a/components/Partner/SearchField/AgeDropdown/index.jsx b/components/Partner/SearchField/AgeDropdown/index.jsx deleted file mode 100644 index 93531bbe..00000000 --- a/components/Partner/SearchField/AgeDropdown/index.jsx +++ /dev/null @@ -1,81 +0,0 @@ -import React, { useState } from 'react'; -import styled from '@emotion/styled'; -import { Box, Select, MenuItem } from '@mui/material'; -import { useRouter } from 'next/router'; -// import { SEARCH_TAGS } from "../../../constants/category"; -import OutlinedInput from '@mui/material/OutlinedInput'; -import InputLabel from '@mui/material/InputLabel'; -import FormControl from '@mui/material/FormControl'; -import Chip from '@mui/material/Chip'; - -const ITEM_HEIGHT = 48; -const ITEM_PADDING_TOP = 8; -const MenuProps = { - PaperProps: { - style: { - maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, - width: 250, - }, - }, -}; - -const names = ['學齡前', '國小', '國高中', '大學以上']; - -const AgeDropdown = () => { - const { query, push } = useRouter(); - const ages = query?.ages ? (query?.ages).split(',') : []; - const handleChange = (event) => { - const { - target: { value }, - } = event; - - if (value.length > 0) { - push({ - pathname: '/search', - query: { - ...query, - ages: value.join(','), - }, - }); - } else { - delete query.ages; - push({ - pathname: '/search', - query, - }); - } - }; - return ( - - 年齡層 - - - ); -}; - -export default AgeDropdown; diff --git a/components/Partner/SearchField/FeeDropdown/index.jsx b/components/Partner/SearchField/FeeDropdown/index.jsx deleted file mode 100644 index 19452fae..00000000 --- a/components/Partner/SearchField/FeeDropdown/index.jsx +++ /dev/null @@ -1,99 +0,0 @@ -import React, { useState } from 'react'; -import styled from '@emotion/styled'; -import { Box, Select, MenuItem } from '@mui/material'; -import { useRouter } from 'next/router'; -// import { SEARCH_TAGS } from "../../../constants/category"; -import OutlinedInput from '@mui/material/OutlinedInput'; -import InputLabel from '@mui/material/InputLabel'; -import FormControl from '@mui/material/FormControl'; -import Chip from '@mui/material/Chip'; -import Radio from '@mui/material/Radio'; -import RadioGroup from '@mui/material/RadioGroup'; -import FormControlLabel from '@mui/material/FormControlLabel'; -import FormLabel from '@mui/material/FormLabel'; - -const ITEM_HEIGHT = 48; -const ITEM_PADDING_TOP = 8; -const MenuProps = { - PaperProps: { - style: { - maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, - width: 250, - }, - }, -}; - -const names = ['不拘', '免費', '部分免費', '需付費']; - -const FeeDropdown = () => { - const { query, push } = useRouter(); - const fee = query?.fee ? (query?.fee).split(',') : []; - const handleChange = (event) => { - const { - target: { value }, - } = event; - - if (value === names[0]) { - delete query.fee; - push({ - pathname: '/search', - query, - }); - } else { - push({ - pathname: '/search', - query: { - ...query, - fee: value, - }, - }); - } - }; - return ( - - 費用 - - {names.map((name) => ( - } - /> - ))} - - {/* */} - - ); -}; - -export default FeeDropdown; diff --git a/components/Partner/SearchField/HotTags/index.jsx b/components/Partner/SearchField/HotTags/index.jsx deleted file mode 100644 index 6de3607e..00000000 --- a/components/Partner/SearchField/HotTags/index.jsx +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react'; -import styled from '@emotion/styled'; -import { Whatshot } from '@mui/icons-material'; -import { Box } from '@mui/material'; -import { SEARCH_TAGS } from '../../../../constants/category'; -import Item from './item'; -// import { TikTokFont } from "../../../../shared/styles/css"; - -const TagsWrapper = styled.ul` - display: flex; - justify-content: flex-start; - align-items: center; - margin: auto 5px; - white-space: nowrap; - max-width: calc(100vw - 49px); - overflow-x: scroll; - -ms-overflow-style: none; /* IE */ - scrollbar-width: none; /* Firefox */ - &::-webkit-scrollbar { - display: none; /* Chrome, Safari, Edge and Opera */ - } -`; - -const Tags = ({ queryList }) => { - const lastSelectedCat = queryList.length > 0 && queryList[0]; - const hotTags = - Array.isArray(queryList) && queryList.length > 0 && lastSelectedCat - ? SEARCH_TAGS[lastSelectedCat] - : SEARCH_TAGS['全部']; - return ( - - - - {hotTags.map((value) => ( - - ))} - - - ); -}; - -export default Tags; diff --git a/components/Partner/SearchField/HotTags/item.jsx b/components/Partner/SearchField/HotTags/item.jsx deleted file mode 100644 index 6804b277..00000000 --- a/components/Partner/SearchField/HotTags/item.jsx +++ /dev/null @@ -1,72 +0,0 @@ -import React, { useCallback, useMemo } from 'react'; -import { useRouter } from 'next/router'; -import { Chip } from '@mui/material'; -import { COLOR_TABLE } from '../../../../constants/notion'; -import stringSanitizer from '../../../../utils/sanitizer'; - -// const TagWrapper = styled(Chip)` -// margin: auto 5px; -// font-weight: 700; -// white-space: nowrap; -// a { -// color: #37b9eb; -// font-weight: bold; -// font-size: 16px; -// } - -// a:hover { -// text-decoration: underline; -// } - -// @media (max-width: 767px) { -// left: 70px; -// width: 85vw; -// overflow-x: visible; -// a { -// color: #007bbb; -// font-size: 14px; -// } -// } -// `; -const Tag = ({ title }) => { - const { push, query } = useRouter(); - const queryTags = useMemo( - () => - typeof query.tags === 'string' - ? stringSanitizer(query.tags).split(',') - : [], - [query.tags], - ); - const linkHandler = useCallback( - (targetQuery) => { - push({ - pathname: '/search', - query: { - ...query, - tags: [...new Set([...queryTags, targetQuery])].join(','), - }, - }); - }, - [push, query, queryTags], - ); - return ( - linkHandler(title)} - sx={{ - backgroundColor: COLOR_TABLE.pink, - cursor: 'pointer', - margin: '5px', - whiteSpace: 'nowrap', - fontWeight: 500, - fontSize: '14px', - '&:hover': { - opacity: '60%', - transition: 'transform 0.4s', - }, - }} - /> - ); -}; - -export default Tag; diff --git a/components/Partner/SearchField/SearchInput/Button/index.jsx b/components/Partner/SearchField/SearchInput/Button/index.jsx deleted file mode 100644 index a9e3cb0b..00000000 --- a/components/Partner/SearchField/SearchInput/Button/index.jsx +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react'; -import { IconButton } from '@mui/material'; -import SearchIcon from '@mui/icons-material/Search'; -import styled from '@emotion/styled'; - -const SearchButtonWrapper = styled(IconButton)` - overflow: hidden; - color: #16b9b3; - width: 40px; - height: 100%; - right: 0; - border-radius: 0; - padding: 10px; - - &:hover { - background-color: white; - /* opacity: 0.8; - transition: opacity 0.5s; */ - } - @media (max-width: 767px) { - width: 40px; - padding: 0px; - /* border-radius: 20px; */ - } -`; - -const SearchButton = ({ routingPush }) => ( - { - routingPush(); - // addSearchHistory(); - }} - aria-label="search" - > - - -); - -export default SearchButton; diff --git a/components/Partner/SearchField/SearchInput/SuggestList/index.jsx b/components/Partner/SearchField/SearchInput/SuggestList/index.jsx deleted file mode 100644 index 738f997a..00000000 --- a/components/Partner/SearchField/SearchInput/SuggestList/index.jsx +++ /dev/null @@ -1,99 +0,0 @@ -import React from 'react'; -import styled from '@emotion/styled'; -import { css } from '@emotion/react'; -import Link from 'next/link'; - -const SuggestWrapper = styled.div` - width: 100%; - top: 20px; - left: 0px; - background-color: white; - position: absolute; - display: flex; - flex-direction: column; - justify-content: flex-start; - border-bottom-left-radius: 10px; - border-bottom-right-radius: 10px; - border: 2px #37b9eb solid; - overflow: hidden; - border-top: 0; - /* box-shadow: 0 4px 6px rgb(32 33 36 / 28%); */ - ${({ isFocus, isEmpty }) => - isFocus && - !isEmpty && - css` - border: 0px; - `} - - a { - display: block; - padding: 6px 12px; - color: black; - - &:hover { - background-color: #eeeeee; - } - - &:first-of-type { - margin-top: 15px; - } - - &:last-of-type { - border-bottom-left-radius: 10px; - border-bottom-right-radius: 10px; - } - } -`; - -const SuggestList = ({ - isFocus, - keyword, - suggestKeywords, - addSearchHistory, - referenceSelected, -}) => { - const isServerSide = !process.browser; - if (isServerSide) return <>; - const historyKeywords = - JSON.parse(window?.localStorage.getItem('historyKeywords') || null) || []; - - if (!isFocus) return <>; - - if (keyword.length === 0 && historyKeywords.length > 0) { - return ( - - {historyKeywords.map(({ keyword: suggest, id }, idx) => ( - - {suggest} - - ))} - - ); - } - - return ( - - {keyword.length > 0 && - Array.isArray(suggestKeywords) && - suggestKeywords.map((suggest, idx) => ( - addSearchHistory(suggest)} - style={{ - background: referenceSelected === idx ? '#eee' : null, - wordBreak: 'break-all', - }} - > - {suggest} - - ))} - - ); -}; - -export default SuggestList; diff --git a/components/Partner/SearchField/SearchInput/index.jsx b/components/Partner/SearchField/SearchInput/index.jsx deleted file mode 100644 index d23aaaf4..00000000 --- a/components/Partner/SearchField/SearchInput/index.jsx +++ /dev/null @@ -1,178 +0,0 @@ -import React, { useState, useEffect, useCallback, useMemo } from 'react'; -// import ClickAwayListener from "@mui/base/ClickAwayListener"; -import InputBase from '@mui/material/InputBase'; -import Paper from '@mui/material/Paper'; -import styled from '@emotion/styled'; -// import { Search } from "@mui/icons-material"; -import { useRouter } from 'next/router'; -// import i18n from "../../../../../constants/i18n"; -// import SuggestList from "./SuggestList"; -import { IconButton, Box } from '@mui/material'; -import MicIcon from '@mui/icons-material/Mic'; -import dynamic from 'next/dynamic'; -import SearchButton from './Button'; - -const Speech = dynamic(import('../../../../shared/components/Speech'), { - ssr: false, -}); - -const SearchToolsWrapper = styled(Box)` - position: relative; - height: 40px; - margin-left: auto; - margin-right: 5px; - display: flex; -`; - -const SearchButtonWrapper = styled(IconButton)` - /* position: absolute; */ - overflow: hidden; - color: white; - border-radius: 10px; - float: right; - height: 100%; - width: 40px; - right: 0; - &:hover { - /* background-color: #007bbb; */ - } -`; -const FormWrapper = styled.form` - width: 100%; -`; - -const SearchInputWrapper = styled(Paper)` - height: 40px; - width: 100%; - position: relative; - border-radius: 10px; - // 可以試著淡化border - border: 2px solid #16b9b3; - box-shadow: none; - overflow: hidden; - - @media (max-width: 767px) { - border-radius: 20px; - width: 100%; - } -`; - -const PLACEHOLDER_TEXT = [ - '英語, 心理學, 自主學習 ...', - '好想出國喔~該來學英語了', - '我的腦袋不太好,但是知道邏輯要訓練', - '不會寫程式,也要了解科技趨勢', - '斜槓與文青的時間到了', - '誰說健身不是學習的一種?', - '生活在學習', -]; - -const InputBaseWrapper = styled(InputBase)` - background: white; - z-index: 10; - border-bottom-right-radius: 20px; - border-top-right-radius: 20px; - margin-left: 10px; - width: 100%; - - @media (max-width: 767px) { - border-radius: 20px; - } -`; - -const SearchInput = () => { - const { query, push } = useRouter(); - // const isServerSide = useMemo(() => !process.browser, []); - const [keyword, setKeyword] = useState(query?.q); - const [isSpeechMode, setIsSpeechMode] = useState(false); - // const [referenceSelected, setReferenceSelected] = useState(null); - - useEffect(() => { - setKeyword(query?.q ?? ''); - }, [query?.q]); - - const routingPush = useCallback( - (words) => { - if (words !== '') { - push({ - query: { - ...query, - q: words, - }, - }); - } else { - delete query.q; - push({ - query, - }); - } - }, - [push, query], - ); - - const placeholder = useMemo( - () => PLACEHOLDER_TEXT[Math.floor(Math.random() * 7)], - [], - ); - - return ( - - { - e.preventDefault(); - if (keyword !== '') { - push({ - query: { - ...query, - q: keyword, - }, - }); - } else if (keyword.length === 0) { - delete query.q; - push({ query }); - } - }} - > - { - // setReferenceSelected(null); - setKeyword(event.target.value); - }} - // components={<>} - /> - - {isSpeechMode && ( - - )} - - setIsSpeechMode(true)} - > - - - {}} /> - - - ); -}; - -export default SearchInput; From 6d937c07436025647c58cdca05261c146294a11b Mon Sep 17 00:00:00 2001 From: hsuifang Date: Sun, 3 Dec 2023 14:15:28 +0800 Subject: [PATCH 08/27] feat(partner): Add SearchField and SearchParams in Partner --- components/Partner/Parnter.styled.jsx | 30 ++++++ .../Partner/SearchField/SearchInput.jsx | 98 +++++++++++++++++++ components/Partner/SearchField/SearchTags.jsx | 84 ++++++++++++++++ .../Partner/SearchField/SelectedAreas.jsx | 29 ++++++ .../SearchField/SelectedEducationStep.jsx | 31 ++++++ .../SearchField/SelectedFriendType.jsx | 35 +++++++ components/Partner/SearchField/index.jsx | 87 ++++++---------- .../SearchParamsList.styles.jsx | 53 ++++++++++ components/Partner/SearchParamsList/index.jsx | 56 +++++++++++ components/Partner/index.jsx | 83 ++-------------- constants/areas.js | 1 - 11 files changed, 458 insertions(+), 129 deletions(-) create mode 100644 components/Partner/Parnter.styled.jsx create mode 100644 components/Partner/SearchField/SearchInput.jsx create mode 100644 components/Partner/SearchField/SearchTags.jsx create mode 100644 components/Partner/SearchField/SelectedAreas.jsx create mode 100644 components/Partner/SearchField/SelectedEducationStep.jsx create mode 100644 components/Partner/SearchField/SelectedFriendType.jsx create mode 100644 components/Partner/SearchParamsList/SearchParamsList.styles.jsx create mode 100644 components/Partner/SearchParamsList/index.jsx diff --git a/components/Partner/Parnter.styled.jsx b/components/Partner/Parnter.styled.jsx new file mode 100644 index 00000000..7ea799b2 --- /dev/null +++ b/components/Partner/Parnter.styled.jsx @@ -0,0 +1,30 @@ +import styled from '@emotion/styled'; +import { Box } from '@mui/material'; + +export const StyledWrapper = styled.div` + position: relative; + margin: 70px auto 0; + width: 100%; + max-width: 1024px; + min-height: 100vh; + margin-top: -80px; + + @media (max-width: 900px) { + padding: 0 16px; + margin-top: -50px; + } +`; +export const StyledContent = styled(Box)` + margin-top: 24px; + padding: 32px 40px; + background-color: #fff; + border-radius: 20px; + @media (max-width: 900px) { + padding: 0; + background-color: transparent; + } +`; + +export const StyledSearchWrapper = styled(Box)` + margin-top: 24px; +`; diff --git a/components/Partner/SearchField/SearchInput.jsx b/components/Partner/SearchField/SearchInput.jsx new file mode 100644 index 00000000..ba485a31 --- /dev/null +++ b/components/Partner/SearchField/SearchInput.jsx @@ -0,0 +1,98 @@ +import { useState, useEffect } from 'react'; +import dynamic from 'next/dynamic'; +import styled from '@emotion/styled'; +import InputBase from '@mui/material/InputBase'; +import Paper from '@mui/material/Paper'; +import IconButton from '@mui/material/IconButton'; +import MicIcon from '@mui/icons-material/Mic'; +import SearchIcon from '@mui/icons-material/Search'; +import useSearchParamsManager from '@/hooks/useSearchParamsManager'; + +const Speech = dynamic(import('@/shared/components/Speech'), { + ssr: false, +}); + +const SearchInputWrapper = styled(Paper)` + width: 100%; + position: relative; + display: flex; + align-items: center; + border: 1px solid #dbdbdb; + border-radius: 30px; + padding-right: 4px; + box-shadow: none; + overflow: hidden; + + @media (max-width: 767px) { + border-radius: 20px; + width: 100%; + } +`; + +const IconButtonWrapper = styled(IconButton)` + color: #536166; + border-radius: 40px; + height: 40px; + width: 40px; +`; + +const InputBaseWrapper = styled(InputBase)(() => ({ + flex: 1, + '& .MuiInputBase-input': { + paddingTop: '14px', + paddingLeft: '20px', + paddingBottom: '14px', + background: 'white', + zIndex: 10, + borderRadius: '20px', + width: '100%', + fontSize: 14, + }, +})); + +const SearchInput = () => { + const [getSearchParams, pushState] = useSearchParamsManager(); + const [keyword, setKeyword] = useState(''); + const [isSpeechMode, setIsSpeechMode] = useState(false); + const currentKeyword = getSearchParams('q').toString(); + + useEffect(() => { + setKeyword(currentKeyword); + }, [currentKeyword]); + + const handleChange = ({ target }) => { + setKeyword(target.value); + }; + + const handleSubmit = (event) => { + event.preventDefault(); + pushState('q', keyword); + }; + + return ( + + + {isSpeechMode && ( + + )} + setIsSpeechMode(true)} + > + + + + + + + ); +}; + +export default SearchInput; diff --git a/components/Partner/SearchField/SearchTags.jsx b/components/Partner/SearchField/SearchTags.jsx new file mode 100644 index 00000000..976a54da --- /dev/null +++ b/components/Partner/SearchField/SearchTags.jsx @@ -0,0 +1,84 @@ +import React, { useState, useEffect } from 'react'; +import styled from '@emotion/styled'; +import { SEARCH_TAGS } from '@/constants/category'; +import useSearchParamsManager from '@/hooks/useSearchParamsManager'; + +const StyledContainer = styled.div` + margin-top: 12px; + display: flex; + align-items: center; + width: 100%; + @media (max-width: 767px) { + margin-left: 10px 0; + flex-direction: column; + align-items: flex-start; + } + > p { + color: #536166; + font-size: 14px; + font-style: normal; + font-weight: 700; + line-height: 140%; + white-space: nowrap; + @media (max-width: 767px) { + margin-bottom: 8px; + } + } + ul { + display: flex; + flex-wrap: nowrap; + overflow-x: auto; + + -ms-overflow-style: none; /* IE */ + scrollbar-width: none; /* Firefox */ + scroll-behavior: smooth; + + margin-left: 24px; + + &::-webkit-scrollbar { + display: none; /* Chrome, Safari, Edge and Opera */ + } + @media (max-width: 767px) { + margin-left: 0; + } + } + ul > li { + color: #16b9b3; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 140%; + margin-right: 16px; + flex: 0 0 auto; + cursor: pointer; + } +`; + +const SearchTags = () => { + const [getSearchParams, pushState] = useSearchParamsManager(); + const [_, setTag] = useState(); + const currentTags = getSearchParams('tag').toString(); + + const handleChange = (val) => { + pushState('tag', val.toString()); + }; + + useEffect(() => { + setTag(currentTags); + }, [currentTags]); + + return ( + +

    熱門標籤

    +
      + {SEARCH_TAGS['全部'].map((t) => ( +
    • handleChange(t)}> + {t} +
    • + ))} +
    +
    + ); +}; + +export default SearchTags; diff --git a/components/Partner/SearchField/SelectedAreas.jsx b/components/Partner/SearchField/SelectedAreas.jsx new file mode 100644 index 00000000..d39a5086 --- /dev/null +++ b/components/Partner/SearchField/SelectedAreas.jsx @@ -0,0 +1,29 @@ +import Select from '@/shared/components/Select'; +import { AREAS } from '@/constants/areas'; +import useSearchParamsManager from '@/hooks/useSearchParamsManager'; + +export default function SelectedAreas() { + const QUERY_KEY = 'area'; + const [getSearchParams, pushState] = useSearchParamsManager(); + + const handleChange = ({ target: { value } }) => { + pushState(QUERY_KEY, value.toString()); + }; + + return ( + + selected.length === 0 ? '教育階段' : selected.join('、') + } + sx={{ + '@media (max-width: 767px)': { + width: '100%', + }, + }} + /> + ); +} diff --git a/components/Partner/SearchField/SelectedFriendType.jsx b/components/Partner/SearchField/SelectedFriendType.jsx new file mode 100644 index 00000000..1d012942 --- /dev/null +++ b/components/Partner/SearchField/SelectedFriendType.jsx @@ -0,0 +1,35 @@ +import Select from '@/shared/components/Select'; +import { ROLE } from '@/constants/member'; +import useSearchParamsManager from '@/hooks/useSearchParamsManager'; + +const ROLE_TYPE = ROLE.map(({ label, key }) => ({ label, key })); + +const SelectedFriendType = () => { + const QUERY_KEY = 'role'; + const [getSearchParams, pushState] = useSearchParamsManager(); + + const handleChange = ({ target: { value } }) => { + pushState(QUERY_KEY, value.toString()); + }; + + return ( + { + onChangeHandler({ + key: 'educationStage', + value: event.target.value, + }); }} + sx={{ width: '100%' }} > - 教育階段 - - - + + + 居住地 + { - setLocation(event.target.value); - }} - // placeholder="請選擇您或孩子目前的教育階段" - sx={{ width: '100%' }} - > - - 請選擇居住地 + + 請選擇居住地 + + {COUNTIES.map(({ name, alpha2 }) => ( + + {name} - {COUNTIES.map(({ name, alpha2 }) => ( - - {name} - - ))} - - {/* { - setLocation(event.target.value); - }} - /> */} - - + + + + {/* 聯絡方式 */} + + {/* TODO: 新增 Social */} + + 聯絡方式 + - 想和夥伴一起 - - - {WANT_TO_DO_WITH_PARTNER.slice(0, 3).map( - ({ label, value }) => ( - { - if (wantToLearnList.includes(value)) { - setWantToLearnList((state) => - state.filter((data) => data !== value), - ); - } else { - setWantToLearnList((state) => [...state, value]); - } - }} - sx={{ - border: '1px solid #DBDBDB', - borderRadius: '8px', - padding: '10px', - width: 'calc(calc(100% - 16px) / 3)', - display: 'flex', - justifyItems: 'center', - alignItems: 'center', - cursor: 'pointer', - ...(wantToLearnList.includes(value) - ? { - backgroundColor: '#DEF5F5', - border: '1px solid #16B9B3', - } - : {}), - }} - > - - {label} - - - ), - )} - - + + + + + Instagram + + + + + + Discord + + + + + + Line + + + + + + Facebook + + + + + + + + + 想和夥伴一起 + + {WANT_TO_DO_WITH_PARTNER.map(({ label, value }) => ( + { + onChangeHandler({ + key: 'wantToDoList', + value, + isMultiple: true, + }); }} > - {WANT_TO_DO_WITH_PARTNER.slice(3).map( - ({ label, value }) => ( - { - if (wantToLearnList.includes(value)) { - setWantToLearnList((state) => - state.filter((data) => data !== value), - ); - } else { - setWantToLearnList((state) => [...state, value]); - } - }} - sx={{ - border: '1px solid #DBDBDB', - borderRadius: '8px', - padding: '10px', - width: 'calc(calc(100% - 16px) / 3)', - display: 'flex', - justifyItems: 'center', - alignItems: 'center', - cursor: 'pointer', - ...(wantToLearnList.includes(value) - ? { - backgroundColor: '#DEF5F5', - border: '1px solid #16B9B3', - } - : {}), - }} - > - - {label} - - - ), - )} - - - - + {label} + + + ))} + + + + 可以和夥伴分享的事物 + { + onChangeHandler({ key: 'share', value: e.target.value }); }} - > - 可以和夥伴分享的事物 - - - {/* + + {/* TODO: 新增 TAG */} + + 標籤 + { + onChangeHandler({ + tagList: event.target.value, + isMultiple: true, + key: 'tagList', + }); }} + /> + - 標籤 - - - 可以是學習領域、興趣等等的標籤,例如:音樂創作、程式語言、電繪、社會議題。 - - */} - + + + + 個人簡介 + - 個人網站或社群 - { - setUrl(event.target.value); - }} - /> - - { + onChangeHandler({ + key: 'selfIntroduction', + value: event.target.value, + }); }} - > - 個人簡介 - { - setDescription(event.target.value); - }} - /> - - - - + + + + + + 公開顯示居住地 + { + onChangeHandler({ + key: 'isOpenLocation', + value, + }); }} - > - - 公開顯示居住地 - - { - setIsOpenLocation(value); - }} - /> - - + + + 公開個人頁面尋找夥伴 + { + onChangeHandler({ + key: 'isOpenProfile', + value, + }); }} - > - - 公開個人頁面尋找夥伴 - - { - setIsOpenProfile(value); - }} - /> - - - + + + + + { + router.push('/profile/myprofile'); }} > - - - - - + 查看我的頁面 + + { + onUpdateUser(() => router.push('/profile')); + }} + > + 儲存資料 + + + ); diff --git a/components/Profile/Edit/useEditProfile.jsx b/components/Profile/Edit/useEditProfile.jsx new file mode 100644 index 00000000..91bd919b --- /dev/null +++ b/components/Profile/Edit/useEditProfile.jsx @@ -0,0 +1,100 @@ +import dayjs from 'dayjs'; +import { useReducer } from 'react'; +import { useDispatch } from 'react-redux'; +import { updateUser } from '@/redux/actions/user'; + +const initialState = { + name: '', + photoURL: '', + birthDay: dayjs(), + gender: '', + roleList: [], + wantToDoList: [], + // interestList: [], + educationStage: '-1', + location: 'tw', + contactInformationList: [], + tagList: [], + selfIntroduction: '', + share: '', + isOpenLocation: false, + isOpenProfile: false, + isLoadingSubmit: false, +}; + +const userReducer = (state, payload) => { + const { key, value, isMultiple = false } = payload; + if (isMultiple) { + return { + ...state, + [key]: state[key].includes(value) + ? state[key].filter((role) => role !== value) + : [...state[key], value], + }; + } else if (state && state[key] !== undefined) { + return { + ...state, + [key]: value, + }; + } + return state; +}; + +const useEditProfile = () => { + const reduxDispatch = useDispatch(); + + const [userState, stateDispatch] = useReducer(userReducer, initialState); + + // TODO ErrorMap + + const onChangeHandler = ({ key, value }) => { + stateDispatch({ key, value }); + }; + + const onSubmit = async ({ id, email }) => { + if (!id || !email) return; + const { + name, + birthDay, + gender, + roleList, + educationStage, + location, + wantToDoList, + share, + isOpenLocation, + isOpenProfile, + contactInformationList, + tagList, + selfIntroduction, + } = userState; + + const payload = { + id, + email, + contactInformationList, + name, + birthDay, + gender, + roleList, + wantToDoList, + educationStage, + location, + tagList, + selfIntroduction, + share, + isOpenLocation, + isOpenProfile, + }; + + reduxDispatch(updateUser(payload)); + }; + + return { + userState, + onChangeHandler, + onSubmit, + }; +}; + +export default useEditProfile; diff --git a/constants/member.js b/constants/member.js index 97938e23..f585b8e8 100644 --- a/constants/member.js +++ b/constants/member.js @@ -106,6 +106,41 @@ export const EDUCATION_STEP = [ }, ]; +export const EDUCATION_STAGE = [ + { + label: '學齡前', + value: 'preschool', + }, + { + label: '國小低年級', + value: 'elementary-junior', + }, + { + label: '國小中年級', + value: 'elementary-middle', + }, + { + label: '國小高年級', + value: 'elementary-senior', + }, + { + label: '國中', + value: 'junior-high', + }, + { + label: '高中', + value: 'high', + }, + { + label: '大學', + value: 'university', + }, + { + label: '其他', + value: 'other', + }, +]; + export const WANT_TO_DO_WITH_PARTNER = [ { label: '交朋友', @@ -128,7 +163,7 @@ export const WANT_TO_DO_WITH_PARTNER = [ value: 'make-group-class', }, { - label: '做專案', + label: '做專案/競賽', key: 'do-project', value: 'do-project', }, diff --git a/pages/_app.jsx b/pages/_app.jsx index 4c00e61b..224e1f0f 100644 --- a/pages/_app.jsx +++ b/pages/_app.jsx @@ -14,7 +14,12 @@ import { initGA, logPageView } from '../utils/analytics'; import Mode from '../shared/components/Mode'; import 'regenerator-runtime/runtime'; // Speech.js +import { persistStore } from 'redux-persist'; +import { PersistGate } from 'redux-persist/integration/react'; + const store = storeFactory(); +let persistor = persistStore(store); + const firebaseConfig = { apiKey: 'AIzaSyBJK-FKcGHwDy1TMcoJcBdEqbTYpEquUi4', authDomain: 'daodaoedu-4ae8f.firebaseapp.com', @@ -93,7 +98,9 @@ const App = ({ Component, pageProps }) => { /> - + + + ); diff --git a/pages/login/index.jsx b/pages/login/index.jsx index d0a4f949..7a1cba70 100644 --- a/pages/login/index.jsx +++ b/pages/login/index.jsx @@ -1,4 +1,5 @@ -import React, { useMemo } from 'react'; +import React, { useEffect, useMemo } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; import styled from '@emotion/styled'; import Router, { useRouter } from 'next/router'; import Script from 'next/script'; @@ -39,8 +40,8 @@ const ContentWrapper = styled.div` `; const LoginPage = () => { - const provider = new GoogleAuthProvider(); const router = useRouter(); + const SEOData = useMemo( () => ({ title: '登入島島|島島阿學', @@ -56,42 +57,8 @@ const LoginPage = () => { ); const onLogin = () => { - const auth = getAuth(); - - signInWithPopup(auth, provider) - .then((result) => { - // This gives you a Google Access Token. You can use it to access the Google API. - // const credential = GoogleAuthProvider.credentialFromResult(result); - // const token = credential.accessToken; - // The signed-in user info. - // console.log('result', result); - const { displayName } = result.user; - // sendDataToChromeExtension( - // 'locidnghejlnnlnbglelhaflehebblei', - // result.user, - // ); - const db = getFirestore(); - const docRef = doc(db, 'partnerlist', result?.user?.uid); - getDoc(docRef).then((docSnap) => { - // const isNewUser = Object.keys(docSnap.data() || {}).length === 0; - // if (isNewUser) { - toast.success(`歡迎登入! ${displayName}`); - router.push('/signin'); - // } else { - // toast.success(`歡迎回來! ${displayName}`); - // router.push('/'); - // } - }); - console.log(result); - }) - .catch((error) => { - console.log('error', error); - toast.error('登入失敗', { - style: { - marginTop: '70px', - }, - }); - }); + // toast.success(`歡迎登入! ${displayName}`); + window.open('https://daodao-server.onrender.com/auth/google', '_target'); }; return ( diff --git a/pages/signin/index.jsx b/pages/signin/index.jsx index 3a87431c..b06d9448 100644 --- a/pages/signin/index.jsx +++ b/pages/signin/index.jsx @@ -1,130 +1,80 @@ -import React, { useMemo, useState, useEffect } from 'react'; -import styled from '@emotion/styled'; +import { useMemo, useState, useEffect } from 'react'; import { useRouter } from 'next/router'; -import Script from 'next/script'; -import { - Box, - Typography, - Button, - Skeleton, - TextField, - Divider, - Switch, - TextareaAutosize, - MenuItem, - Select, -} from '@mui/material'; +import { useSelector, useDispatch } from 'react-redux'; +import { fetchUserById, updateUser } from '@/redux/actions/user'; +import { GENDER, ROLE } from '@/constants/member'; +import dayjs from 'dayjs'; + +import { Box, Typography, Button, Skeleton, TextField } from '@mui/material'; import { LazyLoadImage } from 'react-lazy-load-image-component'; -import toast from 'react-hot-toast'; -import { useAuthState } from 'react-firebase-hooks/auth'; -import { getAuth, updateProfile } from 'firebase/auth'; import { MobileDatePicker } from '@mui/x-date-pickers/MobileDatePicker'; -import { - getFirestore, - collection, - getDocs, - doc, - getDoc, - setDoc, - addDoc, -} from 'firebase/firestore'; -import dayjs from 'dayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import FormControlLabel from '@mui/material/FormControlLabel'; import Checkbox from '@mui/material/Checkbox'; -import SEOConfig from '../../shared/components/SEO'; -import Navigation from '../../shared/components/Navigation_v2'; -import Footer from '../../shared/components/Footer_v2'; +import SEOConfig from '@/shared/components/SEO'; +import Navigation from '@/shared/components/Navigation_v2'; +import Footer from '@/shared/components/Footer_v2'; import { - GENDER, - ROLE, - EDUCATION_STEP, - WANT_TO_DO_WITH_PARTNER, - CATEGORIES, -} from '../../constants/member'; -import COUNTIES from '../../constants/countries.json'; - -const HomePageWrapper = styled.div` - --section-height: calc(100vh - 80px); - --section-height-offset: 80px; - background: linear-gradient(0deg, #f3fcfc, #f3fcfc), #f7f8fa; -`; - -const ContentWrapper = styled.div` - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - background-color: #fff; - border-radius: 16px; - margin: 60px auto; - max-width: 50%; - width: 100%; - @media (max-width: 767px) { - max-width: 80%; - .title { - text-overflow: ellipsis; - width: 100%; - } - } -`; + HomePageWrapper, + StyledContentWrapper, + StyledQuestionInput, +} from './signin.styled'; function EditPage() { const router = useRouter(); - const auth = getAuth(); - const [user, isLoading] = useAuthState(auth); + const dispatch = useDispatch(); + + const { + birthDay: userBirthDay, + gender: userGender, + roleList: userRoleList, + isSubscribeEmail: userIsSubscribeEmail, + email: userEmail, + createdDate, + updatedDate, + } = useSelector((state) => state?.user); + const { id } = router.query; + const [isSubscribeEmail, setIsSubscribeEmail] = useState(false); - const [isLoadingSubmit, setIsLoadingSubmit] = useState(false); const [birthDay, setBirthDay] = useState(dayjs()); const [gender, setGender] = useState(''); const [roleList, setRoleList] = useState([]); + + const fetchUser = async () => { + dispatch(fetchUserById(id)); + }; + useEffect(() => { - if (!isLoading) { - const db = getFirestore(); - if (user?.uid) { - // console.log('auth.currentUser', auth.currentUser); - const docRef = doc(db, 'partnerlist', user?.uid); - getDoc(docRef).then((docSnap) => { - const data = docSnap.data(); - setBirthDay(dayjs(data?.birthDay) || dayjs()); - setGender(data?.gender || ''); - setRoleList(data?.roleList || []); - }); - } + if (id) { + fetchUser(); + } + }, [id]); + + useEffect(() => { + setBirthDay(userBirthDay ? dayjs(userBirthDay) : dayjs()); + setGender(userGender || ''); + setRoleList(userRoleList || []); + setIsSubscribeEmail(userIsSubscribeEmail || false); + + if (createdDate !== updatedDate) { + router.push('/profile'); } - }, [user, isLoading]); + }, [userEmail]); - const onUpdateUser = (successCallback) => { + const onUpdateUser = () => { const payload = { + id, + email: userEmail, birthDay: birthDay.toISOString(), gender, roleList, - lastUpdateDate: dayjs().toISOString(), isSubscribeEmail, }; - - const db = getFirestore(); - - const docRef = doc(db, 'partnerlist', user?.uid); - getDoc(docRef).then(() => { - setIsLoadingSubmit(true); - toast - .promise( - setDoc(docRef, payload).then(() => { - setIsLoadingSubmit(false); - }), - { - success: '更新成功!', - error: '更新失敗', - loading: '更新中...', - }, - ) - .then(() => { - successCallback(); - }); - }); + dispatch(updateUser(payload)); + router.push(`/signin/interest?id=${id}`); }; + const SEOData = useMemo( () => ({ title: '編輯我的島島資料|島島阿學', @@ -146,29 +96,10 @@ function EditPage() { - - - 基本資料 - + +

    基本資料

    - + 生日 * )} /> - - + + 性別 * ))} - - + + 身份 * ))} - + { - onUpdateUser(() => router.push('/signin/interest')); - }} + onClick={onUpdateUser} > 下一步 -
    +
    diff --git a/pages/signin/interest/index.jsx b/pages/signin/interest/index.jsx index 0d341cf1..3e2b1da0 100644 --- a/pages/signin/interest/index.jsx +++ b/pages/signin/interest/index.jsx @@ -1,41 +1,19 @@ import React, { useMemo, useState, useEffect } from 'react'; import styled from '@emotion/styled'; import { useRouter } from 'next/router'; -import Script from 'next/script'; -import { - Box, - Typography, - Button, - Skeleton, - Modal, - TextField, - Divider, - Switch, - TextareaAutosize, - MenuItem, - Select, -} from '@mui/material'; +import { useSelector, useDispatch } from 'react-redux'; +import { fetchUserById, updateUser } from '@/redux/actions/user'; + +import { Box, Typography, Button, Skeleton } from '@mui/material'; import { LazyLoadImage } from 'react-lazy-load-image-component'; import toast from 'react-hot-toast'; -import { useAuthState } from 'react-firebase-hooks/auth'; -import { getAuth, updateProfile } from 'firebase/auth'; import { MobileDatePicker } from '@mui/x-date-pickers/MobileDatePicker'; -import { - getFirestore, - collection, - getDocs, - doc, - getDoc, - updateDoc, - setDoc, - addDoc, -} from 'firebase/firestore'; import dayjs from 'dayjs'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; -import SEOConfig from '../../../shared/components/SEO'; -import Navigation from '../../../shared/components/Navigation_v2'; -import Footer from '../../../shared/components/Footer_v2'; +import SEOConfig from '@/shared/components/SEO'; +import Navigation from '@/shared/components/Navigation_v2'; +import Footer from '@/shared/components/Footer_v2'; import { GENDER, ROLE, @@ -73,52 +51,35 @@ const ContentWrapper = styled.div` function EditPage() { const router = useRouter(); - const auth = getAuth(); - const [user, isLoading] = useAuthState(auth); - const [interestAreaList, setInterestAreaList] = useState([]); - const [isLoadingSubmit, setIsLoadingSubmit] = useState(false); + const { id } = router.query; + const dispatch = useDispatch(); + + const { + _id: userId, + interestList: userInterestList, + email: userEmail, + } = useSelector((state) => state?.user); + + const [interestList, setInterestList] = useState([]); const [open, setOpen] = useState(false); useEffect(() => { - if (!isLoading) { - const db = getFirestore(); - if (user?.uid) { - // console.log('auth.currentUser', auth.currentUser); - const docRef = doc(db, 'partnerlist', user?.uid); - getDoc(docRef).then((docSnap) => { - const data = docSnap.data(); - setInterestAreaList(data?.interestAreaList || []); - }); - } + if (userId) { + setInterestList(userInterestList); + } + if (id) { + dispatch(fetchUserById(id)); } - }, [user, isLoading]); + }, [userId, id]); const onUpdateUser = (successCallback) => { const payload = { - interestAreaList, - lastUpdateDate: dayjs().toISOString(), + id: userId, + interestList, + email: userEmail, }; - - const db = getFirestore(); - - const docRef = doc(db, 'partnerlist', user?.uid); - getDoc(docRef).then(() => { - setIsLoadingSubmit(true); - toast - .promise( - updateDoc(docRef, payload).then(() => { - setIsLoadingSubmit(false); - }), - { - success: '更新成功!', - error: '更新失敗', - loading: '更新中...', - }, - ) - .then(() => { - successCallback(); - }); - }); + dispatch(updateUser(payload)); + successCallback(); }; const SEOData = useMemo( @@ -139,7 +100,6 @@ function EditPage() { { setOpen(false); router.push('/'); @@ -202,12 +162,12 @@ function EditPage() { { - if (interestAreaList.includes(value)) { - setInterestAreaList((state) => + if (interestList.includes(value)) { + setInterestList((state) => state.filter((data) => data !== value), ); } else { - setInterestAreaList((state) => [...state, value]); + setInterestList((state) => [...state, value]); } }} sx={{ @@ -221,7 +181,7 @@ function EditPage() { justifyItems: 'center', alignItems: 'center', cursor: 'pointer', - ...(interestAreaList.includes(value) + ...(interestList.includes(value) ? { backgroundColor: '#DEF5F5', border: '1px solid #16B9B3', @@ -267,7 +227,7 @@ function EditPage() { { router.back(); }} diff --git a/pages/signin/signin.styled.jsx b/pages/signin/signin.styled.jsx new file mode 100644 index 00000000..676433e6 --- /dev/null +++ b/pages/signin/signin.styled.jsx @@ -0,0 +1,43 @@ +import styled from '@emotion/styled'; + +export const HomePageWrapper = styled.div` + --section-height: calc(100vh - 80px); + --section-height-offset: 80px; + background: linear-gradient(0deg, #f3fcfc, #f3fcfc), #f7f8fa; +`; + +export const StyledContentWrapper = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background-color: #fff; + border-radius: 16px; + margin: 60px auto; + max-width: 50%; + width: 100%; + @media (max-width: 767px) { + max-width: 80%; + .title { + text-overflow: ellipsis; + width: 100%; + } + } + + h2 { + font-weight: 700; + font-size: 22px; + line-height: 140%; + text-align: center; + color: #536166; + margin-top: 40px; + } +`; + +export const StyledQuestionInput = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + margin-top: 20px; +`; diff --git a/redux/actions/user.js b/redux/actions/user.js index 12b05ec5..9dd45fac 100644 --- a/redux/actions/user.js +++ b/redux/actions/user.js @@ -4,6 +4,12 @@ export function userLogin() { }; } +export function userLogout() { + return { + type: 'USER_LOGOUT', + }; +} + export function checkUserAccount() { return { type: 'CHECK_USER_ACCOUNT', @@ -16,6 +22,15 @@ export function fetchAllUsers() { }; } +export function fetchUserById(id) { + return { + type: 'FETCH_USER_BY_ID', + payload: { + id, + }, + }; +} + export function addResourceToCollection(resourceId) { return { type: 'ADD_RESOURCE_TO_COLLECTION', @@ -33,3 +48,12 @@ export function removeResourceFromCollection(resourceId) { }, }; } + +export function updateUser(user) { + return { + type: 'UPDATE_USER_PROFILE', + payload: { + user, + }, + }; +} diff --git a/redux/reducers/user.js b/redux/reducers/user.js index 5539ab2a..126e9f46 100644 --- a/redux/reducers/user.js +++ b/redux/reducers/user.js @@ -1,10 +1,6 @@ // import toast from 'react-hot-toast'; -const initialState = { - name: '', - email: '', - photoURL: '', -}; +const initialState = {}; const reducer = (state = initialState, action) => { switch (action.type) { @@ -20,6 +16,11 @@ const reducer = (state = initialState, action) => { ...action.payload, }; } + case 'USER_LOGOUT': { + return { + ...initialState, + }; + } case 'ADD_RESOURCE_TO_COLLECTION_SUCCESS': { return { ...state, @@ -32,6 +33,23 @@ const reducer = (state = initialState, action) => { ...action.payload, }; } + case 'FETCH_USER_BY_ID_SUCCESS': { + return { + ...action.payload, + }; + } + + case 'FETCH_USER_BY_ID_FAILURE': { + return { + ...state, + }; + } + case 'UPDATE_USER_PROFILE_SUCCESS': { + return { + ...state, + ...action.payload, + }; + } default: { return state; } diff --git a/redux/sagas/user/index.js b/redux/sagas/user/index.js index dd5995c2..9dee9e08 100644 --- a/redux/sagas/user/index.js +++ b/redux/sagas/user/index.js @@ -47,10 +47,53 @@ function* fetchAllUsers() { } } +function* updateUserProfile(action) { + const { user } = action.payload; + try { + const baseUri = + process.env.NEXT_PUBLIC_API_URL || 'https://daodao-server.onrender.com'; + const URL = `${baseUri}/user/${user.id}`; + + const result = yield fetch(URL, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + ...user, + }), + }).then((res) => res.json()); + + yield put({ type: 'UPDATE_USER_PROFILE_SUCCESS', payload: result.data }); + } catch (error) { + yield put({ type: 'UPDATE_USER_PROFILE_FAILURE' }); + } +} + +function* fetchUserById(action) { + const { id } = action.payload; + try { + const baseUrl = + process.env.NEXT_PUBLIC_API_URL || 'https://daodao-server.onrender.com'; + const URL = `${baseUrl}/user/${id}`; + const result = yield fetch(URL).then((res) => res.json()); + console.log(result); + yield put({ + type: 'FETCH_USER_BY_ID_SUCCESS', + payload: result.data && result.data[0], + }); + } catch (error) { + console.log(error); + yield put({ type: 'FETCH_USER_BY_ID_FAILURE' }); + } +} + function* userSaga() { yield takeEvery('CHECK_USER_ACCOUNT', checkUserStatus); yield takeEvery('USER_LOGIN', userLogin); yield takeEvery('FETCH_ALL_USERS', fetchAllUsers); + yield takeEvery('UPDATE_USER_PROFILE', updateUserProfile); + yield takeEvery('FETCH_USER_BY_ID', fetchUserById); } export default userSaga; diff --git a/redux/store/index.js b/redux/store/index.js index 2ebd9c17..889e4926 100644 --- a/redux/store/index.js +++ b/redux/store/index.js @@ -1,11 +1,29 @@ import { createStore, applyMiddleware } from 'redux'; +import storage from 'redux-persist/lib/storage'; import logger from 'redux-logger'; import createSagaMiddleware from 'redux-saga'; import { configureStore } from '@reduxjs/toolkit'; +import { + persistReducer, + FLUSH, + REHYDRATE, + PAUSE, + PERSIST, + PURGE, + REGISTER, +} from 'redux-persist'; + +const persistConfig = { + key: 'root', + storage, + whitelist: ['user'], +}; import rootReducer from '../reducers'; import rootSaga from '../sagas'; +const persistedReducer = persistReducer(persistConfig, rootReducer); + // create a makeStore function const storeFactory = (preloadedState) => { const enableLog = @@ -13,7 +31,7 @@ const storeFactory = (preloadedState) => { const sagaMiddleware = createSagaMiddleware(); const middlewares = enableLog ? [logger, sagaMiddleware] : [sagaMiddleware]; const store = configureStore({ - reducer: rootReducer, + reducer: persistedReducer, preloadedState, middleware: [...middlewares], }); diff --git a/shared/components/Navigation_v2/MainNav/SubList/UserAvatar/index.jsx b/shared/components/Navigation_v2/MainNav/SubList/UserAvatar/index.jsx index 767b7975..d23a0106 100644 --- a/shared/components/Navigation_v2/MainNav/SubList/UserAvatar/index.jsx +++ b/shared/components/Navigation_v2/MainNav/SubList/UserAvatar/index.jsx @@ -1,16 +1,20 @@ import React, { useState } from 'react'; -import styled from '@emotion/styled'; -import Link from 'next/link'; +import { useSelector } from 'react-redux'; import { Avatar, Box, IconButton, Menu, MenuItem } from '@mui/material'; import { Group } from '@mui/icons-material'; import { useRouter } from 'next/router'; -import useFirebase from '../../../../../../hooks/useFirebase'; const UserAvatar = () => { const { push } = useRouter(); - const { auth, user, signInWithFacebook, signOutWithGoogle } = useFirebase(); + const user = useSelector((state) => state.user); + const [isOpenMenu, setIsOpenMenu] = useState(null); - if (!user) { + + const handleSignOut = () => { + console.log('handleSignOut'); + }; + + if (!user._id) { return ( { { - signOutWithGoogle(); + handleSignOut(); push('/'); setIsOpenMenu(false); }} From d807fe92d1ea06adfbb78b9f7cca2da535df04d3 Mon Sep 17 00:00:00 2001 From: hsuifang Date: Sun, 14 Jan 2024 22:51:00 +0800 Subject: [PATCH 23/27] =?UTF-8?q?feat:=20=E4=B8=B2=E6=8E=A5=E8=81=AF?= =?UTF-8?q?=E7=B9=AB=E5=A4=A5=E4=BC=B4=EF=BC=8C=E4=B8=A6=E5=AF=84=E9=80=81?= =?UTF-8?q?=20Email?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/Profile/Contact/index.jsx | 113 ++++++++++++++---------- components/Profile/Edit/Edit.styled.jsx | 1 - components/Profile/UserTabs/index.jsx | 6 +- components/Profile/index.jsx | 66 +++++++++----- pages/partner/detail/index.jsx | 7 +- pages/profile/index.jsx | 30 ++++++- pages/profile/myprofile/index.jsx | 5 +- redux/actions/partners.js | 17 ++++ redux/reducers/partners.js | 6 ++ redux/sagas/partnersSaga.js | 22 +++++ redux/sagas/user/index.js | 1 - 11 files changed, 196 insertions(+), 78 deletions(-) diff --git a/components/Profile/Contact/index.jsx b/components/Profile/Contact/index.jsx index a8c38f5c..801b557d 100644 --- a/components/Profile/Contact/index.jsx +++ b/components/Profile/Contact/index.jsx @@ -7,8 +7,49 @@ import { TextareaAutosize, Avatar, } from '@mui/material'; +import styled from '@emotion/styled'; + +const StyledGroup = styled(Box)` + margin-bottom: 16px; +`; + +const StyledTitle = styled(Typography)` + color: var(--black-white-gray-dark, #293a3d); + /* desktop/body-M-Medium */ + font-family: Noto Sans TC; + font-size: 16px; + font-style: normal; + font-weight: 500; + line-height: 140%; /* 22.4px */ + margin-bottom: 11px; +`; +const StyledTextArea = styled(TextareaAutosize)` + border-radius: 8px; + border: 1px solid var(--black-white-gray-very-light, #dbdbdb); + background: var(--black-white-white, #fff); + padding: 12px 16px; + width: 100%; + min-height: 128px; +`; + +function ContactModal({ + title, + descipt, + avatar, + onClose, + onOk, + isLoadingSubmit, + open, +}) { + const [message, setMessage] = useState(''); + const [contact, setContact] = useState(''); + + const handleSubmit = () => { + onOk({ message, contact }); + setMessage(''); + setContact(''); + }; -function ContactModal({ onClose, onOk, isLoadingSubmit, open }) { return ( - + - 黃芊宇 + {title} - 實驗教育學生 + {descipt} - - 邀請訊息 - - - - 聯繫資訊 - - + + 邀請訊息 + setMessage(e.target.value)} + placeholder="想要和新夥伴交流什麼呢?可以簡單的自我介紹,寫下想認識夥伴的原因。" + /> + + + + 聯繫資訊 + + setContact(e.target.value)} + placeholder="ex. 自學申請、學習方法、學習資源,或各種學習領域的知識" + /> + handleSubmit({ message, contact })} > 送出 diff --git a/components/Profile/Edit/Edit.styled.jsx b/components/Profile/Edit/Edit.styled.jsx index 08507ebf..4e125605 100644 --- a/components/Profile/Edit/Edit.styled.jsx +++ b/components/Profile/Edit/Edit.styled.jsx @@ -13,7 +13,6 @@ export const ContentWrapper = styled.div` align-items: center; border-radius: 16px; margin: 0 auto; - padding: 40px 10px; width: 672px; @media (max-width: 767px) { width: 80%; diff --git a/components/Profile/UserTabs/index.jsx b/components/Profile/UserTabs/index.jsx index 2bbd7d68..2be4d1bb 100644 --- a/components/Profile/UserTabs/index.jsx +++ b/components/Profile/UserTabs/index.jsx @@ -41,17 +41,17 @@ const UserTabs = ({ description = '', wantToDoList = [], share = '' }) => { sx={{ borderBottom: '1px solid #F3F3F3', paddingBottom: '6px' }} >

    可分享

    - {share || '無'} + {share || '尚未填寫'}

    想一起

    - {wantToDoList || '無'} + {wantToDoList || '尚未填寫'}

    簡介

    - {description || '無'} + {description || '尚未填寫'}
    diff --git a/components/Profile/index.jsx b/components/Profile/index.jsx index 1ccb9a3e..11d1623b 100644 --- a/components/Profile/index.jsx +++ b/components/Profile/index.jsx @@ -1,5 +1,6 @@ import { useMemo, useState } from 'react'; import { useRouter } from 'next/router'; +import { useDispatch } from 'react-redux'; import { Box, Button } from '@mui/material'; import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; import { @@ -12,6 +13,8 @@ import SEOConfig from '@/shared/components/SEO'; import UserCard from './UserCard'; import UserTabs from './UserTabs'; import ContactModal from './Contact'; +import { sendEmailToPartner } from '@/redux/actions/partners'; +import toast from 'react-hot-toast'; const BottonBack = { color: '#536166', @@ -33,6 +36,7 @@ const EDUCATION_STEP_TABLE = mapToTable(EDUCATION_STEP); const Profile = ({ name, + email, photoURL, tagList = [], roleList = [], @@ -41,7 +45,10 @@ const Profile = ({ wantToDoList = [], location, share, + enableContactBtn = false, + sendEmail, }) => { + const dispatch = useDispatch(); const router = useRouter(); const [isLoading] = useState(false); const [open, setOpen] = useState(false); @@ -66,6 +73,21 @@ const Profile = ({ [router?.asPath, name], ); + const handleOnOk = ({ message, contact }) => { + dispatch( + sendEmailToPartner({ + to: email, + name, + roleList: roleList.length ? roleList : [], + photoURL, + text: message, + information: [sendEmail, contact], + }), + ); + setOpen(false); + toast.success('寄送成功'); + }; + return ( { setOpen(false); - // router.push('/'); - // router.push('/partner'); - }} - onOk={() => { - setOpen(false); - // router.push('/profile'); - // router.push('/profile/edit'); }} + onOk={handleOnOk} /> - - + {email !== sendEmail && ( + + )} ); }; diff --git a/pages/partner/detail/index.jsx b/pages/partner/detail/index.jsx index 8f192ea2..4f1c2517 100644 --- a/pages/partner/detail/index.jsx +++ b/pages/partner/detail/index.jsx @@ -17,6 +17,7 @@ const HomePageWrapper = styled.div` const Detail = () => { const dispatch = useDispatch(); const { partner } = useSelector((state) => state?.partners); + const { email: loginUserEmail } = useSelector((state) => state?.user); const router = useRouter(); const { id } = router.query; @@ -33,7 +34,11 @@ const Detail = () => { return ( - +