diff --git a/.babelrc b/.babelrc index ca0cb10d..fc5ee40b 100644 --- a/.babelrc +++ b/.babelrc @@ -8,7 +8,8 @@ "importSource": "@emotion/react" } } - ] + ], + "@babel/preset-typescript" ], "plugins": [ [ diff --git a/.eslintrc.js b/.eslintrc.js index 1c65dc3f..579e7935 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,4 +1,5 @@ module.exports = { + parser: '@typescript-eslint/parser', parserOptions: { ecmaFeatures: { jsx: true, @@ -6,14 +7,19 @@ module.exports = { ecmaVersion: 12, sourceType: 'module', }, - plugins: ['react', 'import', 'react-hooks'], - extends: ['airbnb'], + plugins: ['react', 'import', 'react-hooks', '@typescript-eslint'], + extends: ['airbnb', 'plugin:@typescript-eslint/recommended'], env: { browser: true, es2021: true, node: true, }, - ignorePatterns: ['.eslintrc.js'], + ignorePatterns: [ + '*.config.js', + '.eslintrc.js', + 'next-sitemap.js', + 'server.js', + ], settings: { 'import/resolver': { alias: { @@ -21,21 +27,33 @@ module.exports = { map: [['@', '.']], }, node: { - extensions: ['.js', '.jsx', '.ts', '.tsx'] + extensions: ['.js', '.jsx', '.ts', '.tsx'], }, }, }, rules: { - "import/no-extraneous-dependencies": ["error", { devDependencies: ["./*.js"] }], - 'import/extensions': ['error', 'ignorePackages', { - js: 'never', - jsx: 'never', - ts: 'never', - tsx: 'never', - }], + 'import/no-extraneous-dependencies': [ + 'error', + { devDependencies: ['./*.ts'] }, + ], + '@typescript-eslint/no-require-imports': 'error', + 'import/extensions': [ + 'error', + 'ignorePackages', + { + js: 'never', + jsx: 'never', + ts: 'never', + tsx: 'never', + }, + ], 'react/no-unescaped-entities': 'off', '@next/next/no-page-custom-font': 'off', 'react/prop-types': [0], + 'react/jsx-filename-extension': [ + 1, + { extensions: ['.js', '.jsx', '.ts', '.tsx'] }, + ], 'react/jsx-props-no-spreading': [0], 'arrow-body-style': 0, 'no-console': 0, @@ -64,6 +82,7 @@ module.exports = { 'react/function-component-definition': 0, 'react/jsx-no-useless-fragment': 0, 'react/no-unknown-property': 0, + 'react/require-default-props': 0, 'no-unsafe-optional-chaining': 0, 'react/no-invalid-html-attribute': 0, }, diff --git a/.github/workflows/push-change-to-discord-webhook-url.yml b/.github/workflows/push-change-to-discord-webhook-url.yml index 5ba0ed89..2bee6eae 100644 --- a/.github/workflows/push-change-to-discord-webhook-url.yml +++ b/.github/workflows/push-change-to-discord-webhook-url.yml @@ -3,7 +3,7 @@ name: Push Change to Discord Webhook URL on: push: branches: - - main + - dev # Allow manual trigger workflow_dispatch: diff --git a/components/About/AboutTeam/index.jsx b/components/About/AboutTeam/index.jsx index a45f6001..3cb168d3 100644 --- a/components/About/AboutTeam/index.jsx +++ b/components/About/AboutTeam/index.jsx @@ -276,7 +276,7 @@ const AboutTeam = () => { flexDirection: 'row', }} > - {Members.map(({ id, name, image }) => ( + {Members.map(({ id, name }) => ( { const stageRef = React.useRef(null); - const [mainState, setMainState] = useState('initial'); // initial, search, gallery, uploaded - const [imageUploaded, setImageUploaded] = useState(0); + const [, setMainState] = useState('initial'); // initial, search, gallery, uploaded + const [, setImageUploaded] = useState(0); const [selectedFile, setSelectedFile] = useState(null); const [img] = useImage(selectedFile, 'Anonymous'); - const [newImage] = useImage(stageRef?.current?.toDataURL(), 'Anonymous'); + useImage(stageRef?.current?.toDataURL(), 'Anonymous'); const handleUploadClick = (event) => { const file = event.target.files[0]; const reader = new FileReader(); const url = reader.readAsDataURL(file); - reader.onloadend = function (e) { + reader.onloadend = () => { setSelectedFile([reader.result]); }; console.log(url); diff --git a/components/ContributeResource/ActivitiesResource/index.jsx b/components/ContributeResource/ActivitiesResource/index.jsx index da0b5a7e..06a595fc 100644 --- a/components/ContributeResource/ActivitiesResource/index.jsx +++ b/components/ContributeResource/ActivitiesResource/index.jsx @@ -1,6 +1,5 @@ import React from 'react'; -import styled from '@emotion/styled'; -import { Box, Paper, Typography } from '@mui/material'; +import { Box, Typography } from '@mui/material'; const ActivitiesResource = () => { return ( diff --git a/components/ContributeResource/LocationResource/index.jsx b/components/ContributeResource/LocationResource/index.jsx index ae9982f6..9180a7c0 100644 --- a/components/ContributeResource/LocationResource/index.jsx +++ b/components/ContributeResource/LocationResource/index.jsx @@ -1,6 +1,5 @@ import React from 'react'; -import styled from '@emotion/styled'; -import { Box, Paper, Typography } from '@mui/material'; +import { Box, Typography } from '@mui/material'; const LearningResource = () => { return ( diff --git a/components/ContributeResource/index.jsx b/components/ContributeResource/index.jsx index 212756e9..129de672 100644 --- a/components/ContributeResource/index.jsx +++ b/components/ContributeResource/index.jsx @@ -3,7 +3,6 @@ import styled from '@emotion/styled'; import { Paper, Box, Typography } from '@mui/material'; import LearningResource from './LearningResource'; import ActivitiesResource from './ActivitiesResource'; -import LocationResource from './LocationResource'; const ResourceWrapper = styled.section` padding-top: 40px; diff --git a/components/Group/Form/Fields/DateRadio.jsx b/components/Group/Form/Fields/DateRadio.jsx index 33304064..bcc6e842 100644 --- a/components/Group/Form/Fields/DateRadio.jsx +++ b/components/Group/Form/Fields/DateRadio.jsx @@ -1,5 +1,5 @@ import dayjs from 'dayjs'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import Box from '@mui/material/Box'; import FormControlLabel from '@mui/material/FormControlLabel'; import Checkbox from '@mui/material/Checkbox'; diff --git a/components/Group/detail/OrganizerCard.jsx b/components/Group/detail/OrganizerCard.jsx index 2e283c6f..83e1f55a 100644 --- a/components/Group/detail/OrganizerCard.jsx +++ b/components/Group/detail/OrganizerCard.jsx @@ -72,13 +72,6 @@ const StyledTags = styled.div` } `; -const StyledTime = styled.time` - display: flex; - justify-content: flex-end; - font-size: 12px; - color: #92989a; -`; - function OrganizerCard({ data = {}, isLoading }) { const educationStage = EDUCATION_STEP.find(({ key }) => key === data?.user?.educationStage) diff --git a/components/Home/APPBanner/index.jsx b/components/Home/APPBanner/index.jsx index 4fc9daf6..9d9ee741 100644 --- a/components/Home/APPBanner/index.jsx +++ b/components/Home/APPBanner/index.jsx @@ -1,4 +1,3 @@ -import React, { useState } from 'react'; import styled from '@emotion/styled'; import Box from '@mui/material/Box'; import { Typography, Button } from '@mui/material'; diff --git a/components/Home/About/Tags/index.jsx b/components/Home/About/Tags/index.jsx index 3f21a73f..3effe699 100644 --- a/components/Home/About/Tags/index.jsx +++ b/components/Home/About/Tags/index.jsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo } from 'react'; +import React, { useMemo } from 'react'; import styled from '@emotion/styled'; import { useRouter } from 'next/router'; import Chip from '@mui/material/Chip'; @@ -10,45 +10,8 @@ const TagsWrapper = styled.ul` 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 { query } = useRouter(); const linkList = useMemo(() => { return tags.map((newQuery) => { diff --git a/components/Home/About/index.jsx b/components/Home/About/index.jsx index b1cb4121..96c3d9cf 100644 --- a/components/Home/About/index.jsx +++ b/components/Home/About/index.jsx @@ -1,9 +1,6 @@ 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 { Box, Chip, Typography } from '@mui/material'; import { useRouter } from 'next/router'; import { COLOR_TABLE } from '../../../constants/notion'; import { CATEGORIES } from '../../../constants/category'; diff --git a/components/Home/Banner/index.jsx b/components/Home/Banner/index.jsx index 82717129..3418443c 100644 --- a/components/Home/Banner/index.jsx +++ b/components/Home/Banner/index.jsx @@ -1,7 +1,6 @@ import React, { useCallback } from 'react'; import styled from '@emotion/styled'; import { Box, Button, Typography } from '@mui/material'; -import Typed from 'react-typed'; import SearchField from '../SearchField'; import BannerVideo from '../BannerVideo'; import Title from './Title'; diff --git a/components/Home/FacebookPosts/CardList/Card/index.jsx b/components/Home/FacebookPosts/CardList/Card/index.jsx index f7b0a4ec..1d5e5dfc 100644 --- a/components/Home/FacebookPosts/CardList/Card/index.jsx +++ b/components/Home/FacebookPosts/CardList/Card/index.jsx @@ -1,9 +1,6 @@ import styled from '@emotion/styled'; -import { css } from '@emotion/react'; -import { useRouter } from 'next/router'; -import { Box, Tooltip, Typography } from '@mui/material'; +import { Box, Tooltip } from '@mui/material'; import dayjs from 'dayjs'; -import { slideInUp } from '../../../../../shared/styles/animation'; const CardWrapper = styled.li` position: relative; @@ -41,26 +38,7 @@ const ContentWrapper = styled.p` font-size: 12px; `; -const BackgroundWrapper = styled.div` - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; - overflow: hidden; - border-radius: 20px; - z-index: -1; - ${({ image }) => css` - background-image: ${`url(${image})`}; - background-size: cover; - background-repeat: no-repeat; - background-position: 50% 50%; - filter: brightness(50%); - `} -`; - -const Card = ({ id, message = '', date, title, link, type }) => { - const router = useRouter(); +const Card = ({ id, message = '', date }) => { return ( css` - background-image: ${`url(${image})`}; - background-size: cover; - background-repeat: no-repeat; - background-position: 50% 50%; - filter: brightness(50%); - `} -`; const ImageWrapper = styled(LazyLoadImage)` - /* border-radius: 10px; */ width: 150px; height: 150px; min-width: 150px; @@ -67,7 +29,7 @@ const ImageWrapper = styled(LazyLoadImage)` object-position: center; `; -const Card = ({ message = '', date, image, url }) => { +const Card = ({ message = '', image, url }) => { return ( window.open(url, '_target')}> diff --git a/components/Home/FacebookPosts/StoryCardList/Card/index.jsx b/components/Home/FacebookPosts/StoryCardList/Card/index.jsx index 31aae98b..1cf18fcc 100644 --- a/components/Home/FacebookPosts/StoryCardList/Card/index.jsx +++ b/components/Home/FacebookPosts/StoryCardList/Card/index.jsx @@ -1,10 +1,6 @@ import styled from '@emotion/styled'; -import { css } from '@emotion/react'; -import { useRouter } from 'next/router'; -import { Box, Skeleton, Tooltip, Typography } from '@mui/material'; -import dayjs from 'dayjs'; +import { Tooltip } from '@mui/material'; import { LazyLoadImage } from 'react-lazy-load-image-component'; -import { slideInUp } from '../../../../../shared/styles/animation'; const CardWrapper = styled.li` position: relative; @@ -23,42 +19,7 @@ const CardWrapper = styled.li` } `; -const ContentWrapper = styled.p` - display: flex; - flex-direction: column; - align-items: center; - height: calc(90px - 20px); - font-weight: 500; - text-align: left; - display: -webkit-box; - text-overflow: ellipsis; - -webkit-box-orient: vertical; - -webkit-line-clamp: 2; - overflow: hidden; - white-space: pre-wrap; - font-size: 12px; -`; - -const BackgroundWrapper = styled.div` - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; - overflow: hidden; - border-radius: 20px; - z-index: -1; - ${({ image }) => css` - background-image: ${`url(${image})`}; - background-size: cover; - background-repeat: no-repeat; - background-position: 50% 50%; - filter: brightness(50%); - `} -`; - const ImageWrapper = styled(LazyLoadImage)` - /* border-radius: 10px; */ width: 150px; height: calc(calc(150px / 9) * 16); min-width: 150px; @@ -72,10 +33,8 @@ const VideoWrapper = styled.video` object-fit: cover; width: 100%; height: inherit; - /* background: rgba(0, 0, 0, 0.75); - backdrop-filter: blur(180px); */ - /* z-index: 1; */ `; + const Card = ({ message = '', media, url, type }) => { if (type === 'VIDEO') { return ( diff --git a/components/Home/FacebookPosts/index.jsx b/components/Home/FacebookPosts/index.jsx index 2ba90240..87e16260 100644 --- a/components/Home/FacebookPosts/index.jsx +++ b/components/Home/FacebookPosts/index.jsx @@ -1,13 +1,10 @@ import React, { useEffect } from 'react'; import styled from '@emotion/styled'; -import Box from '@mui/material/Box'; import { useSelector, useDispatch, shallowEqual } from 'react-redux'; -import CardList from './CardList'; import ImageCardList from './ImageCardList'; import StoryCardList from './StoryCardList'; import { - // getFacebookFansPagePost, getFacebookGroupPost, getInstagramPost, getInstagramStory, @@ -15,7 +12,6 @@ import { const GuideWrapper = styled.div` width: 90%; - /* height: calc(var(--section-height) + var(--section-height-offset)); */ margin: 0 auto; padding-top: 40px; padding-bottom: 40px; @@ -36,12 +32,8 @@ const GuideWrapper = styled.div` const Guide = () => { const dispatch = useDispatch(); const { - groupPosts, - // fanpagesPosts, instagramPosts, instagramStories, - // isLoadingFanpagesPosts, - isLoadingGroupPosts, isLoadingInstagramPosts, isLoadingInstagramStories, } = useSelector(({ shared }) => { @@ -60,7 +52,6 @@ const Guide = () => { }, shallowEqual); useEffect(() => { - // dispatch(getFacebookFansPagePost(7)); dispatch(getFacebookGroupPost(7)); dispatch(getInstagramPost()); dispatch(getInstagramStory()); @@ -81,12 +72,6 @@ const Guide = () => { isLoading={isLoadingInstagramPosts} direction="right" /> - {/* */} ); }; diff --git a/components/Home/JoinCooperate/index.jsx b/components/Home/JoinCooperate/index.jsx index 19051c29..7e4c1d68 100644 --- a/components/Home/JoinCooperate/index.jsx +++ b/components/Home/JoinCooperate/index.jsx @@ -1,10 +1,6 @@ -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%; @@ -20,9 +16,6 @@ const GroupWrapper = styled.div` `; const JoinCooperate = () => { - const router = useRouter(); - const [open, setOpen] = useState(false); - return ( { - const router = useRouter(); const [open, setOpen] = useState(false); return ( diff --git a/components/Marathon/Apply/index.jsx b/components/Marathon/Apply/index.jsx index ca4b8914..08f9aed5 100644 --- a/components/Marathon/Apply/index.jsx +++ b/components/Marathon/Apply/index.jsx @@ -277,7 +277,7 @@ export default function Apply() {
    -
  • 2025/2/15(六)、2025/2/22(六)、2025/3/1(六)14:00-15:30
  • +
  • 2025/2/15(六)、2025/2/22(六)、2025/3/1(六)、2025/6/7(六)14:00-15:30
@@ -313,7 +313,7 @@ export default function Apply() {
    -
  • 線上:2/23(日)19:30-21:00、4/20(日)19:30-21:00、6/22(日)19:30-21:00
  • +
  • 線上:2/16(日)19:30-21:00、4/20(日)19:30-21:00、6/22(日)19:30-21:00
  • 實體:3/23(日)15:00-16:30 台北、5/25(日)15:00-16:30 台中
  • 地點與時間將依入選學員進行調整
diff --git a/components/Marathon/Faq/index.jsx b/components/Marathon/Faq/index.jsx index 0a337d1c..f10210db 100644 --- a/components/Marathon/Faq/index.jsx +++ b/components/Marathon/Faq/index.jsx @@ -61,6 +61,35 @@ const StyledContent = styled.div` font-weight: 400; line-height: 140%; `; + +const StyledList = styled(Box)` + margin-top: 10px; + ol { + list-style-type: decimal; + padding-left: 1.4em; + + li { + color: #536166; + font-size: 14px; + font-weight: 400; + line-height: 140%; + text-align: left; + } + } + + ul { + list-style-type: disc; + padding-left: 1.4em; + + li { + color: #536166; + font-size: 14px; + font-weight: 400; + line-height: 140%; + text-align: left; + } + } +`; function Accordion({ title, children }) { const [isOpen, setIsOpen] = useState(false); const [height, setHeight] = useState(0); @@ -144,6 +173,50 @@ export default function Faq() { > 在申請期間每人只能提交一件學習計畫,待公告入選者後,使用者可新增至多三個學習計劃。 + + 按下最後的提交按鈕即算申請。早鳥票只需要於12/31 23:59 前點擊「提交」,就可以享有早鳥優惠。即使提交後,在申請截止前都可以繼續修改計畫,計畫將會自動儲存。 + + + 可以唷!我們會以申請時選擇的資格為主。 + + + 完賽的定義不在於最終成果做得多好,而是過程的參與,遇到困難時如何反思並做出改變,以及最後對自己甚至社會帶來什麼改變,包含即使沒有達到預期目標也清楚原因及下一步。 + + + 因此我們完賽退費標準只有需符合以下要求: +
    +
  1. + 工作坊、學習小組會議、團體諮詢及 1對1 諮詢,加總不得請假超過5小時。 +
  2. +
  3. + 提交所有每兩週的進度報告。 +
  4. +
  5. + 參與7/12成果發表日。 +
  6. +
  7. + 於 2025/7/10 前完成以下資料 +
      +
    • + 完成並上傳所有成果發表資料。 +
    • +
    • + 分享至少三個於計劃期間使用的學習資源,並分享使用心得。 +
    • +
    • + 完成學習馬拉松回饋問卷。 +
    • +
    +
  8. +
+
+
); } diff --git a/components/Marathon/Mentors.jsx b/components/Marathon/Mentors.jsx index 1504e43b..df157a07 100644 --- a/components/Marathon/Mentors.jsx +++ b/components/Marathon/Mentors.jsx @@ -271,7 +271,7 @@ const Mentors = () => { } }; - const handleTouchEnd = (e) => { + const handleTouchEnd = () => { setTouchStartX(null); }; diff --git a/components/Marathon/Price/index.jsx b/components/Marathon/Price/index.jsx index c075c71e..a6b3d717 100644 --- a/components/Marathon/Price/index.jsx +++ b/components/Marathon/Price/index.jsx @@ -28,6 +28,19 @@ const StyledGroup = styled(Box)` `; const StyledList = styled(Box)` + ol { + list-style-type: decimal; + padding-left: 1.4em; + + li { + color: #536166; + font-size: 16px; + font-weight: 400; + line-height: 140%; + text-align: left; + } + } + ul { list-style-type: disc; padding-left: 1.4em; @@ -38,7 +51,7 @@ const StyledList = styled(Box)` font-weight: 400; line-height: 140%; text-align: left; - } + } } `; @@ -333,7 +346,7 @@ export default function Price() { 申請無需費用,入選後才需繳交!
- 完賽可退全額! + 完賽可退全額,完賽標準請見退費標準!
原價 @@ -425,33 +438,32 @@ export default function Price() { 退費標準 -
    + 需符合以下三項要求 +
      +
    1. + 工作坊、學習小組會議、團體諮詢及 1對1 諮詢,加總不得請假超過5小時。 +
    2. - 需符合以下三項要求 + 提交所有每兩週的進度報告。 +
    3. +
    4. + 參與7/12成果發表日。 +
    5. +
    6. + 於 2025/7/10 前完成以下資料
      • - 工作坊、學習小組會議、團體諮詢及 1對1 諮詢,加總不得請假超過5小時。 + 完成並上傳所有成果發表資料。
      • - 準時提交所有每兩週的進度報告。 + 分享至少三個於計劃期間使用的學習資源,並分享使用心得。
      • - 於 2025/7/10 前完成以下資料 -
          -
        • - 完成並上傳所有成果發表資料。 -
        • -
        • - 分享至少三個於計劃期間使用的學習資源,並分享使用心得。 -
        • -
        • - 完成學習馬拉松回饋問卷。 -
        • -
        + 完成學習馬拉松回饋問卷。
    7. -
+
diff --git a/components/Marathon/SignUp/ConfirmForm.jsx b/components/Marathon/SignUp/ConfirmForm.jsx index f41e221e..f4cc7a4a 100644 --- a/components/Marathon/SignUp/ConfirmForm.jsx +++ b/components/Marathon/SignUp/ConfirmForm.jsx @@ -211,12 +211,12 @@ export default function ConfirmForm({ setCurrentStep, currentStep, }) { - const [hasClickSubmitButton, setHasClickSubmitButton] = useState(false); + const [/** hasClickSubmitButton */, setHasClickSubmitButton] = useState(false); const reduxDispatch = useDispatch(); const marathonState = useSelector((state) => { return state.marathon; }); const userState = useSelector((state) => { return state.user; }); const token = useSelector((state) => { return state.user.token; }); - const [newMarathon, setNewMarathon] = useState(reduxInitMarathonState); + const [/** newMarathon */, setNewMarathon] = useState(reduxInitMarathonState); const router = useRouter(); const { openLoginModal } = useAuthDispatch(); const [user, setUser] = useState({ @@ -242,7 +242,11 @@ export default function ConfirmForm({ let userEdu = userState?.educationStage; if (userState?.location?.length > 1) { - userLocation = userState?.location.split('@')[1]; + if (userState?.location.includes('@')) { + userLocation = userState?.location.split('@')[1]; + } else { + userLocation = userState?.location; + } } if (userState?.roleList?.length) { @@ -338,7 +342,7 @@ export default function ConfirmForm({ 學習動機 - {marathonState?.motivation?.tags?.map((tag, _i) => { + {marathonState?.motivation?.tags?.map((tag) => { return (
{tag} @@ -356,7 +360,7 @@ export default function ConfirmForm({ 學習方法與策略 - {marathonState?.strategies?.tags.map((tag, _i) => { + {marathonState?.strategies?.tags.map((tag) => { return (
{tag} @@ -388,7 +392,7 @@ export default function ConfirmForm({ 學習成果及呈現方式 - {marathonState?.outcomes?.tags?.map((tag, _i) => { + {marathonState?.outcomes?.tags?.map((tag) => { return (
{tag} @@ -471,7 +475,7 @@ export default function ConfirmForm({ 夥伴的 Email { - marathonState.pricing.email.map((email, _i) => { + marathonState.pricing.email.map((email) => { return ( { - let validate = false; - switch (name) { - case 'title': - validate = (value) => { - return (value.trim().length > 0); - }; - break; - case 'description': - validate = (value) => { - return (value.trim().length > 0); - }; - break; - case 'motivationDescription': - validate = (value) => { - return (value.trim().length > 0); - }; - break; - case 'milestonesName': - validate = (value) => { - const names = value.filter((milestone, _i) => { - return (milestone.name.trim().length > 0); - }); - return names.length === newMarathon.milestones?.length; - }; - break; - case 'goals': - validate = (value) => { - return (value.trim().length > 0); - }; - break; - case 'content': - validate = (value) => { - return (value.trim().length > 0); - }; - break; - case 'strategiesDescription': - validate = (value) => { - return (value.trim().length > 0); - }; - break; - case 'outcomesDescription': - validate = (value) => { - return (value.trim().length > 0); - }; - break; - case 'resources': - validate = (value) => { - return (value.trim().length > 0); - }; - break; - default: - break; + + const validators = { + required: (value) => { + return value.trim().length > 0; + }, + allMilestonesNameRequired: (value, milestonesLength) => { + const names = value.filter((milestone) => milestone?.name.trim().length > 0); + return names.length === milestonesLength; + } + }; + const validateRules = { + title: { + validate: validators.required, + message: '請填寫表格', + }, + description: { + validate: validators.required, + message: '請填寫計畫敘述', + }, + motivationDescription: { + validate: validators.required, + message: '請填寫學習動機', + }, + outcomesDescription: { + validate: validators.required, + message: '請填寫學習成果', + }, + goals: { + validate: validators.required, + message: '請填寫學習目標', + }, + content: { + validate: validators.required, + message: '請填寫學習內容', + }, + milestonesName: { + validate: (value) => { + return validators.allMilestonesNameRequired(value, newMarathon.milestones?.length); + }, + message: '請填寫每週/隔週里程碑目標', + }, + strategiesDescription: { + validate: validators.required, + message: '請填寫學習策略', + }, + resources: { + validate: validators.required, + message: '請填寫學習資源' } + }; - if (validate(input)) { - setErrors((prevErrors) => { - const { [name]: _, ...remainingErrors } = prevErrors; - return remainingErrors; - }); + const handleValidate = (name, input) => { + const validateResult = validateRules[name]?.validate(input); + const errorMessage = validateRules[name]?.message; + if (validateResult) { + setErrors((prevErrors) => + Object.fromEntries(Object.entries(prevErrors).filter(([key]) => key !== name)) + ); } else { setErrors({ ...errors, @@ -168,9 +166,24 @@ export default function MarathonForm({ } }); } - return validate(input); + return validateResult; + }; + const handleOnChange = ( + stateDispatchType, + stateDispatchKey, + value, + validateName, + ) => { + if (stateDispatchType && stateDispatchKey) { + setNewMarathon({ + type: stateDispatchType, + payload: { key: stateDispatchKey, value } + }); + } + if (validateName) { + handleValidate(validateName, value); + } }; - useEffect(() => { setHasLoaded(true); const storagedErrors = getMarathonErrorsStorage().get(); @@ -232,11 +245,7 @@ export default function MarathonForm({ title="學習主題名稱" value={newMarathon.title || ''} onChange={(e) => { - setNewMarathon({ - type: 'UPDATE_FIELD', - payload: { key: 'title', value: e.target.value } - }); - handleValidate('title', e.target.value, '請填寫表格'); + handleOnChange('UPDATE_FIELD', 'title', e.target.value, 'title'); }} sx={{ mb: '8px', @@ -269,14 +278,7 @@ export default function MarathonForm({ { - setNewMarathon({ - type: 'UPDATE_FIELD', - payload: { - key: 'description', - value: e.target.value - } - }); - handleValidate('description', e.target.value, '請填寫計畫簡述'); + handleOnChange('UPDATE_FIELD', 'description', e.target.value, 'description'); }} placeholder="範例:因為對剪影片和當 Youtuber 有興趣,我預計會研究搞笑型 Youtuber 的影片腳本與剪輯方式、拍攝我日常生活及練習剪輯,並建立 Youtube 頻道上傳影片。希望能藉此了解如何當一位 Youtuber。" className={errors.description ? 'error' : ''} @@ -329,14 +331,7 @@ export default function MarathonForm({ /> { - setNewMarathon({ - type: 'UPDATE_MOTIVATION_FIELD', - payload: { - key: 'description', - value: e.target.value - } - }); - handleValidate('motivationDescription', e.target.value, '請填寫學習動機'); + handleOnChange('UPDATE_MOTIVATION_FIELD', 'description', e.target.value, 'motivationDescription'); }} className={errors.motivationDescription ? 'error' : ''} value={newMarathon?.motivation?.description || ''} @@ -365,14 +360,7 @@ export default function MarathonForm({ { - setNewMarathon({ - type: 'UPDATE_FIELD', - payload: { - key: 'goals', - value: e.target.value - } - }); - handleValidate('goals', e.target.value, '請填寫學習目標'); + handleOnChange('UPDATE_FIELD', 'goals', e.target.value, 'goals'); }} value={newMarathon.goals || ''} placeholder="範例: @@ -403,14 +391,7 @@ export default function MarathonForm({ { - setNewMarathon({ - type: 'UPDATE_FIELD', - payload: { - key: 'content', - value: e.target.value - } - }); - handleValidate('content', e.target.value, '請填寫學習內容'); + handleOnChange('UPDATE_FIELD', 'content', e.target.value, 'content'); }} value={newMarathon.content || ''} placeholder="範例: @@ -469,14 +450,7 @@ export default function MarathonForm({ /> { - setNewMarathon({ - type: "UPDATE_STRATEGIES_FIELD", - payload: { - key: 'description', - value: e.target.value - } - }); - handleValidate('strategiesDescription', e.target.value, '請填寫學習方法與策略'); + handleOnChange('UPDATE_STRATEGIES_FIELD', 'description', e.target.value, 'strategiesDescription'); }} value={newMarathon?.strategies?.description || ''} placeholder="範例:我預計會研究影片腳本、拍攝與剪輯方式,接著了解拍攝、剪輯與Youtube頻道經營,並同時練習拍攝與剪輯,開始經營頻道。我會用notion整理我收集到的資料以及筆記。" @@ -509,14 +483,7 @@ export default function MarathonForm({ placeholder="範例:YouTube 創作者的實用資源" value={newMarathon.resources || ''} onChange={(e) => { - setNewMarathon({ - type: 'UPDATE_FIELD', - payload: { - key: 'resources', - value: e.target.value - } - }); - handleValidate('resources', e.target.value, '請填寫學習資源'); + handleOnChange('UPDATE_FIELD', 'resources', e.target.value, 'resources'); }} className={errors.resources ? 'error' : 'warning'} endAdornment={errors.resources ? : null} @@ -543,8 +510,7 @@ export default function MarathonForm({ @@ -585,14 +551,7 @@ export default function MarathonForm({ /> { - setNewMarathon({ - type: 'UPDATE_OUTCOMES_FIELD', - payload: { - key: 'description', - value: e.target.value - } - }); - handleValidate('outcomesDescription', e.target.value, '請填寫學習成果'); + handleOnChange('UPDATE_OUTCOMES_FIELD', 'description', e.target.value, 'outcomesDescription'); }} value={newMarathon?.outcomes?.description || ''} placeholder="範例:我預計會架設一個Youtube頻道,並上傳至少5支影片,並整理觀眾回饋與相關數據。" diff --git a/components/Marathon/SignUp/MilestoneGroup.jsx b/components/Marathon/SignUp/MilestoneGroup.jsx index cb16cb07..e689c4e2 100644 --- a/components/Marathon/SignUp/MilestoneGroup.jsx +++ b/components/Marathon/SignUp/MilestoneGroup.jsx @@ -3,7 +3,6 @@ import { useState, useEffect } from "react"; import styled from '@emotion/styled'; import { Box, - Grid, TextField, MenuItem, Typography, @@ -41,14 +40,13 @@ const StyledDateSection = styled(Box)` export default function MilestoneGroup({ milestones = [], - onChange = null, + onChangeHandler = null, isDisabled = false, - onValidate = null, errorMessage = null }) { const eventWeekRange = 22; const [startDate, setStartDate] = useState('2025-02-09'); - const [endDate, setEndDate] = useState(dayjs(startDate).add('22', 'week')); + const [/** endDate */, setEndDate] = useState(dayjs(startDate).add('22', 'week')); const [frequency, setFrequency] = useState('biweekly'); function arabicToChinese(num) { @@ -105,45 +103,19 @@ export default function MilestoneGroup({ // if change frequency, clear all data setFrequency(e.target.value); const changedMilestones = calculateMilestones(startDate, e.target.value, []); - onChange({ - type: 'UPDATE_FIELD', - payload: { - key: 'milestones', - value: changedMilestones - } - }); - }; - const handleStartDate = (eventStartDate) => { - setStartDate(eventStartDate); - const eventEndDate = dayjs(eventStartDate).add(eventWeekRange, 'week'); - setEndDate(eventEndDate); - const changedMilestones = calculateMilestones(eventStartDate, frequency, milestones); - onChange({ - type: 'UPDATE_FIELD', - payload: { - key: 'milestones', - value: changedMilestones - } - }); + onChangeHandler('UPDATE_FIELD', 'milestones', changedMilestones, 'milestonesName'); }; - const handleEndDate = (fakeDate) => { + + const handleEndDate = (/** fakeDate */) => { const eventEndDate = dayjs(startDate).add(eventWeekRange, 'week'); setEndDate(eventEndDate); }; const updateMilestone = (newMilestone) => { - const changedMilestones = milestones.map((item, _i) => { + const changedMilestones = milestones.map((item) => { return (item._tempId === newMilestone._tempId ? newMilestone : item); }); - // check if milestone name exist - onValidate('milestonesName', changedMilestones, '請填寫每週 / 隔週里程碑目標'); - onChange({ - type: 'UPDATE_FIELD', - payload: { - key: 'milestones', - value: changedMilestones - } - }); + onChangeHandler('UPDATE_FIELD', 'milestones', changedMilestones, 'milestonesName'); }; useEffect(() => { const weeklyMilestonesLength = 22; @@ -160,13 +132,7 @@ export default function MilestoneGroup({ } if (!isDisabled) { - onChange({ - type: 'UPDATE_FIELD', - payload: { - key: 'milestones', - value: initMilestones - } - }); + onChangeHandler('UPDATE_FIELD', 'milestones', initMilestones, 'milestonesName'); } }, []); diff --git a/components/Marathon/SignUp/MilestonePanel.jsx b/components/Marathon/SignUp/MilestonePanel.jsx index b70e2d0a..0ea5af92 100644 --- a/components/Marathon/SignUp/MilestonePanel.jsx +++ b/components/Marathon/SignUp/MilestonePanel.jsx @@ -1,7 +1,6 @@ import { v4 as uuidv4 } from 'uuid'; import { useState } from 'react'; import styled from "@emotion/styled"; -import dayjs from "dayjs"; import { Grid, @@ -122,7 +121,7 @@ export default function MilestonePanel({ }; const handleDeleteSubMilestone = (deletedItem) => { - const newSubMilestones = (milestone.subMilestones).filter((item, _i) => { + const newSubMilestones = (milestone.subMilestones).filter((item) => { return (item._tempId !== deletedItem._tempId); }); onChange({ @@ -132,7 +131,7 @@ export default function MilestonePanel({ }; const handleEditSubMilestone = (newItem) => { - const newSubMilestones = (milestone.subMilestones).map((item, _i) => { + const newSubMilestones = (milestone.subMilestones).map((item) => { return (newItem._tempId === item._tempId) ? newItem : item; }); onChange({ diff --git a/components/Marathon/SignUp/MultiSelectDropdown.jsx b/components/Marathon/SignUp/MultiSelectDropdown.jsx index 7ac3695f..1e789453 100644 --- a/components/Marathon/SignUp/MultiSelectDropdown.jsx +++ b/components/Marathon/SignUp/MultiSelectDropdown.jsx @@ -69,7 +69,7 @@ export default function MultiSelectDropdown({ }} > { - listItems.map((item, index) => { + listItems.map((item) => { return ( ISOToWeekday(ISODate)) diff --git a/components/Marathon/SignUp/UserProfileForm.jsx b/components/Marathon/SignUp/UserProfileForm.jsx index 28b01ce2..7c26e16d 100644 --- a/components/Marathon/SignUp/UserProfileForm.jsx +++ b/components/Marathon/SignUp/UserProfileForm.jsx @@ -4,7 +4,6 @@ import dayjs from 'dayjs'; import toast from 'react-hot-toast'; import { useSearchParams } from 'next/navigation'; import useMediaQuery from '@mui/material/useMediaQuery'; -import { useRouter } from 'next/router'; import { useDispatch, useSelector } from 'react-redux'; import { fetchMarathonProfileByUserEvent @@ -29,8 +28,6 @@ import { } from '@mui/material'; import { MobileDatePicker } from '@mui/x-date-pickers/MobileDatePicker'; -import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; -import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import Fields from '@/components/Group/Form/Fields'; import useEditProfile from './useEditProfile'; import ErrorMessage from './ErrorMessage'; @@ -38,8 +35,6 @@ import ErrorMessage from './ErrorMessage'; import TheAvator from './TheAvator'; import FormInput from './EditFormInput'; import { - FormWrapper, - ContentWrapper, StyledGroup, StyledSelectWrapper, StyledSelectBox, @@ -50,7 +45,6 @@ import { StyledSection, StyledButtonGroup, StyledButton, - MarathonSignUpWrapper, } from './Edit.styled'; export default function UserProfileForm({ @@ -61,7 +55,6 @@ export default function UserProfileForm({ const reduxDispatch = useDispatch(); const mobileScreen = useMediaQuery('(max-width: 767px)'); const [isSetting, setIsSetting] = useState(false); - const router = useRouter(); const searchParams = useSearchParams(); const check = searchParams.get('check'); const [hasClickNextStep, setHasClickNextStep] = useState(false); diff --git a/components/Marathon/SignUp/useEditProfile.jsx b/components/Marathon/SignUp/useEditProfile.jsx index 4255ab09..aa5c6ddf 100644 --- a/components/Marathon/SignUp/useEditProfile.jsx +++ b/components/Marathon/SignUp/useEditProfile.jsx @@ -3,7 +3,6 @@ import { useReducer, useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; import { updateUser, createUser } from '@/redux/actions/user'; import { z } from 'zod'; -import { useAuthDispatch } from '@/contexts/Auth'; const initialState = { name: '', @@ -115,7 +114,6 @@ const userReducer = (state, payload) => { const useEditProfile = () => { const reduxDispatch = useDispatch(); const [userState, stateDispatch] = useReducer(userReducer, initialState); - const authDispatch = useAuthDispatch(); const [errors, setErrors] = useState({}); const refs = useRef({}); diff --git a/components/Marathon/Spotlight/index.jsx b/components/Marathon/Spotlight/index.jsx index b288e4b9..18790d45 100644 --- a/components/Marathon/Spotlight/index.jsx +++ b/components/Marathon/Spotlight/index.jsx @@ -3,7 +3,6 @@ import { Box, Typography } from "@mui/material"; -import BoomImage from "@/public/assets/booming.png"; const StyledGroup = styled(Box)` width: 100%; diff --git a/components/Partner/Banner/index.jsx b/components/Partner/Banner/index.jsx index 48b32fe9..32b1f744 100644 --- a/components/Partner/Banner/index.jsx +++ b/components/Partner/Banner/index.jsx @@ -1,4 +1,3 @@ -import { useSelector } from 'react-redux'; import styled from '@emotion/styled'; import { useRouter } from 'next/router'; import { Box } from '@mui/material'; diff --git a/components/Partner/PartnerList/PartnerCard/PartnerSkelton.jsx b/components/Partner/PartnerList/PartnerCard/PartnerSkelton.jsx index 404e050a..14b0a93b 100644 --- a/components/Partner/PartnerList/PartnerCard/PartnerSkelton.jsx +++ b/components/Partner/PartnerList/PartnerCard/PartnerSkelton.jsx @@ -21,7 +21,7 @@ const PartnerSkelton = () => { - {new Array(3).fill(0).map((_, idx) => ( + {new Array(3).fill(0).map(() => ( { const [getSearchParams, pushState] = useSearchParamsManager(); - const [_, setTag] = useState(); + const [, setTag] = useState(); const currentTags = getSearchParams('tag').toString(); const handleChange = (val) => { diff --git a/components/Partner/index.jsx b/components/Partner/index.jsx index 17e02064..7e683c71 100644 --- a/components/Partner/index.jsx +++ b/components/Partner/index.jsx @@ -66,7 +66,7 @@ function Partner() { const { page: current = 1, totalPages } = pagination; // queryStr - const [getSearchParams, _, generateParamsItems] = useSearchParamsManager(); + const [getSearchParams, , generateParamsItems] = useSearchParamsManager(); const searchParamsItems = useMemo( () => generateParamsItems(['area', 'role', 'edu', 'tag', 'q'], keySelections), diff --git a/components/Profile/Accountsetting/index.jsx b/components/Profile/Accountsetting/index.jsx index a3a8f423..fbb998f8 100644 --- a/components/Profile/Accountsetting/index.jsx +++ b/components/Profile/Accountsetting/index.jsx @@ -9,7 +9,6 @@ import { } from '@mui/material'; import { useRouter } from 'next/router'; import styled from '@emotion/styled'; -import { useDispatch } from 'react-redux'; import { useAuth, useAuthDispatch } from '@/contexts/Auth'; const StyledTypographyStyle = styled(Typography)` diff --git a/components/Profile/Edit/useEditProfile.jsx b/components/Profile/Edit/useEditProfile.jsx index 8281c52b..5f4aebdf 100644 --- a/components/Profile/Edit/useEditProfile.jsx +++ b/components/Profile/Edit/useEditProfile.jsx @@ -148,9 +148,9 @@ const useEditProfile = () => { isFocus = true; if (['INPUT', 'TEXTAREA'].includes(element.tagName)) { - element.focus(); + element?.focus?.(); } else { - element.scrollIntoView({ block: 'center' }); + element?.scrollIntoView?.({ block: 'center' }); } } return [err.path[0], err.message]; diff --git a/components/Profile/MyMarathon/LoadingCard.jsx b/components/Profile/MyMarathon/LoadingCard.jsx index fc0e2239..5c32006e 100644 --- a/components/Profile/MyMarathon/LoadingCard.jsx +++ b/components/Profile/MyMarathon/LoadingCard.jsx @@ -1,6 +1,5 @@ import Skeleton from '@mui/material/Skeleton'; import IconButton from '@mui/material/IconButton'; -import LocationOnOutlinedIcon from '@mui/icons-material/LocationOnOutlined'; import MoreVertOutlinedIcon from '@mui/icons-material/MoreVertOutlined'; import { StyledContainer, diff --git a/components/Profile/MyMarathon/index.jsx b/components/Profile/MyMarathon/index.jsx index 41210559..ece54b98 100644 --- a/components/Profile/MyMarathon/index.jsx +++ b/components/Profile/MyMarathon/index.jsx @@ -50,7 +50,7 @@ const MyMarathon = ({ title, sx }) => { )} {marathons.length > 0 && ( - marathons.map((marathon, _i) => { + marathons.map((marathon) => { return ( 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 { query } = useRouter(); const linkList = useMemo(() => { return tags.map((newQuery) => { @@ -82,10 +45,8 @@ const Tags = ({ tags, type }) => { {tags.map(({ name, color }, index) => (
  • - {/* */} linkTagsHandler(name)} sx={{ backgroundColor: COLOR_TABLE[color ?? 'default'], cursor: 'pointer', @@ -99,7 +60,6 @@ const Tags = ({ tags, type }) => { }, }} /> - {/* */}
  • ))} diff --git a/components/Resource/index.jsx b/components/Resource/index.jsx index c07447bd..0f29b08e 100644 --- a/components/Resource/index.jsx +++ b/components/Resource/index.jsx @@ -1,15 +1,10 @@ import React, { useEffect, useMemo, useState } from 'react'; import styled from '@emotion/styled'; import { useRouter } from 'next/router'; -import { Button, Paper, Box, Stack, Typography } from '@mui/material'; +import { Box, Typography } from '@mui/material'; import { css } from '@emotion/react'; -import { DiscussionEmbed, Recommendations, CommentEmbed } from 'disqus-react'; -import { Share } from '@mui/icons-material'; -import { CopyToClipboard } from 'react-copy-to-clipboard'; -import toast from 'react-hot-toast'; -import appendQuery from 'append-query'; +import { DiscussionEmbed } from 'disqus-react'; import Shares from './Shares'; -import { postFetcher } from '../../utils/fetcher'; import Tags from './Tags'; import RelatedResources from '../../shared/components/RelatedResources'; import Desc from './Desc'; diff --git a/components/Search/SearchField/AgeCheckbox/index.jsx b/components/Search/SearchField/AgeCheckbox/index.jsx index 20fc06de..57ed841f 100644 --- a/components/Search/SearchField/AgeCheckbox/index.jsx +++ b/components/Search/SearchField/AgeCheckbox/index.jsx @@ -1,30 +1,11 @@ /* 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 = () => { diff --git a/components/Search/SearchField/AgeDropdown/index.jsx b/components/Search/SearchField/AgeDropdown/index.jsx index 93531bbe..844522b4 100644 --- a/components/Search/SearchField/AgeDropdown/index.jsx +++ b/components/Search/SearchField/AgeDropdown/index.jsx @@ -1,8 +1,5 @@ -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'; diff --git a/components/Search/SearchField/FeeDropdown/index.jsx b/components/Search/SearchField/FeeDropdown/index.jsx index 19452fae..8df91cbf 100644 --- a/components/Search/SearchField/FeeDropdown/index.jsx +++ b/components/Search/SearchField/FeeDropdown/index.jsx @@ -1,28 +1,10 @@ -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 = () => { diff --git a/components/Search/SearchField/index.jsx b/components/Search/SearchField/index.jsx index 19ea34c7..ce53cecf 100644 --- a/components/Search/SearchField/index.jsx +++ b/components/Search/SearchField/index.jsx @@ -1,44 +1,15 @@ -import React, { useState } from 'react'; import styled from '@emotion/styled'; -import { Box, Select, MenuItem } from '@mui/material'; +import { Box } from '@mui/material'; import { useRouter } from 'next/router'; -import { Whatshot } from '@mui/icons-material'; -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 { SEARCH_TAGS } from '../../../constants/category'; import SearchInput from './SearchInput'; import HotTags from './HotTags'; -import AgeDropdown from './AgeDropdown'; import FeeDropdown from './FeeDropdown'; import AgeCheckbox from './AgeCheckbox'; const SearchFieldWrapper = styled.div` width: 100%; - - /* @media (max-width: 767px) { - margin: 0 10px 10px 10px; - flex-direction: column; - justify-content: center; - align-items: center; - width: 100%; - } */ `; -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 SearchField = () => { const { query } = useRouter(); const queryList = (query?.cats ?? '').split(',').reverse(); diff --git a/components/Search/SearchGalleryList/ImageItem/index.jsx b/components/Search/SearchGalleryList/ImageItem/index.jsx index 179f82ef..81bac5d2 100644 --- a/components/Search/SearchGalleryList/ImageItem/index.jsx +++ b/components/Search/SearchGalleryList/ImageItem/index.jsx @@ -1,122 +1,15 @@ -/* eslint-disable react/jsx-wrap-multilines */ import React, { useMemo } from 'react'; -import styled from '@emotion/styled'; -import { css, keyframes } from '@emotion/react'; import { - Typography, - Box, - ImageList, ImageListItem, ImageListItemBar, IconButton, } from '@mui/material'; import dayjs from 'dayjs'; import isBetween from 'dayjs/plugin/isBetween'; -// import { TikTokFont } from '../../../../shared/styles/css'; -dayjs.extend(isBetween); - -function srcset(image, width, height, rows = 1, cols = 1) { - return { - src: `${image}?w=${width * cols}&h=${height * rows}&fit=crop&auto=format`, - srcSet: `${image}?w=${width * cols}&h=${ - height * rows - }&fit=crop&auto=format&dpr=2 2x`, - }; -} - -const ItemWrapper = styled.li` - display: flex; - padding-top: 20px; - padding-bottom: 20px; - border-bottom: 1px solid rgba(168, 168, 168, 0.3); -`; - -const ContentWrapper = styled.article` - width: calc(100% - 200px); - padding: 0 10px; - margin-left: 20px; - @media (max-width: 767px) { - width: calc(100% - 100px); - } -`; - -const ImageWrapper = styled.div` - width: 200px; - height: 200px; - background-color: #f5f5f5; - ${({ image }) => css` - background-image: ${`url(${image})`}; - background-size: cover; - background-repeat: no-repeat; - background-position: 50% 50%; - `} - border-radius: 20px; - /* object-fit: cover; */ - /* opacity: 0; */ - - @media (max-width: 767px) { - width: 100px; - height: 100px; - } -`; - -const TitleWrapper = styled.div` - display: flex; - flex-wrap: wrap; - justify-content: flex-start; - align-items: center; - .title { - font-size: 24px; - font-weight: 500; - margin: 0 10px 0 0; - color: black; - &:hover { - cursor: pointer; - color: #16b9b3; - transition: 0.5s; - } - } - - @media (max-width: 767px) { - .title { - text-overflow: ellipsis; - width: 100%; - } - } -`; - -const Item = ({ data, margin }) => { - const hashTags = useMemo( - () => data?.properties['標籤']?.multi_select ?? [], - [data], - ); - const resourcesTags = useMemo( - () => data?.properties['資源類型']?.multi_select ?? [], - [data], - ); - const feeTags = useMemo( - () => - data?.properties['費用']?.select - ? [data?.properties['費用']?.select] - : [], - [data], - ); - const isNewResource = useMemo(() => { - const today = dayjs(); - const createDay = dayjs(data?.created_time); - const isRecent = dayjs(createDay).isBetween( - today, - dayjs(today).subtract(1, 'month'), - ); - return isRecent; - }, [data]); - - const ageOfUserTags = useMemo( - () => data?.properties['年齡層']?.multi_select ?? [], - [data], - ); +dayjs.extend(isBetween); +const Item = ({ data }) => { const title = useMemo( () => (data?.properties['資源名稱']?.title ?? []).find( @@ -125,26 +18,9 @@ const Item = ({ data, margin }) => { [data?.properties], ); - const contributors = useMemo( - () => data?.properties['創建者']?.multi_select ?? [], - [data?.properties], - ); - - // const link = useMemo(() => data?.properties["連結"]?.url ?? "", [data]); - const link = useMemo(() => `/resource/${title}`, [title]); - return ( { title={title} position="top" actionIcon={ - - {/* */} - + } actionPosition="left" /> diff --git a/components/Search/SearchGalleryList/Item/Contributors/index.jsx b/components/Search/SearchGalleryList/Item/Contributors/index.jsx index cacbefba..3b14cd61 100644 --- a/components/Search/SearchGalleryList/Item/Contributors/index.jsx +++ b/components/Search/SearchGalleryList/Item/Contributors/index.jsx @@ -1,10 +1,5 @@ -import React, { useMemo } from 'react'; -import styled from '@emotion/styled'; -import { css } from '@emotion/react'; import Link from 'next/link'; import { Typography, Box } from '@mui/material'; -import dayjs from 'dayjs'; -import isBetween from 'dayjs/plugin/isBetween'; const Contributors = ({ contributors }) => { return ( diff --git a/components/Search/SearchGalleryList/Item/LogoImage/index.jsx b/components/Search/SearchGalleryList/Item/LogoImage/index.jsx index a23a6d55..02cba3db 100644 --- a/components/Search/SearchGalleryList/Item/LogoImage/index.jsx +++ b/components/Search/SearchGalleryList/Item/LogoImage/index.jsx @@ -1,8 +1,6 @@ -import React, { useMemo } from 'react'; import styled from '@emotion/styled'; import { css } from '@emotion/react'; -import Link from 'next/link'; -import { Typography, Box } from '@mui/material'; +import { Box } from '@mui/material'; import dayjs from 'dayjs'; import isBetween from 'dayjs/plugin/isBetween'; diff --git a/components/Search/SearchGalleryList/Item/index.jsx b/components/Search/SearchGalleryList/Item/index.jsx index 314ba8ca..de66c47b 100644 --- a/components/Search/SearchGalleryList/Item/index.jsx +++ b/components/Search/SearchGalleryList/Item/index.jsx @@ -1,14 +1,11 @@ import React, { useMemo } from 'react'; import styled from '@emotion/styled'; -import { css, keyframes } from '@emotion/react'; -import Link from 'next/link'; -import { Typography, Box } from '@mui/material'; import dayjs from 'dayjs'; import isBetween from 'dayjs/plugin/isBetween'; import Tags from './Tags'; import LogoImage from './LogoImage'; import Contributors from './Contributors'; -// import { TikTokFont } from '../../../../shared/styles/css'; + dayjs.extend(isBetween); const ItemWrapper = styled.li` @@ -27,25 +24,6 @@ const ContentWrapper = styled.article` } `; -const ImageWrapper = styled.div` - width: 200px; - height: 200px; - background-color: #f5f5f5; - ${({ image }) => css` - background-image: ${`url(${image})`}; - background-size: cover; - background-repeat: no-repeat; - background-position: 50% 50%; - `} - border-radius: 20px; - /* object-fit: cover; */ - /* opacity: 0; */ - - @media (max-width: 767px) { - width: 100px; - height: 100px; - } -`; const TitleWrapper = styled.div` display: flex; flex-wrap: wrap; diff --git a/components/Search/SearchGalleryList/SponsorItem/Contributors/index.jsx b/components/Search/SearchGalleryList/SponsorItem/Contributors/index.jsx index cacbefba..3b14cd61 100644 --- a/components/Search/SearchGalleryList/SponsorItem/Contributors/index.jsx +++ b/components/Search/SearchGalleryList/SponsorItem/Contributors/index.jsx @@ -1,10 +1,5 @@ -import React, { useMemo } from 'react'; -import styled from '@emotion/styled'; -import { css } from '@emotion/react'; import Link from 'next/link'; import { Typography, Box } from '@mui/material'; -import dayjs from 'dayjs'; -import isBetween from 'dayjs/plugin/isBetween'; const Contributors = ({ contributors }) => { return ( diff --git a/components/Search/SearchGalleryList/SponsorItem/LogoImage/index.jsx b/components/Search/SearchGalleryList/SponsorItem/LogoImage/index.jsx index afec2e0a..92c1550c 100644 --- a/components/Search/SearchGalleryList/SponsorItem/LogoImage/index.jsx +++ b/components/Search/SearchGalleryList/SponsorItem/LogoImage/index.jsx @@ -1,11 +1,8 @@ -import React, { useMemo } from 'react'; import styled from '@emotion/styled'; import { css } from '@emotion/react'; -import Link from 'next/link'; -import { Typography, Box } from '@mui/material'; +import { Box } from '@mui/material'; import dayjs from 'dayjs'; import isBetween from 'dayjs/plugin/isBetween'; -import { COLOR_TABLE } from '../../../../../constants/notion'; dayjs.extend(isBetween); @@ -93,6 +90,7 @@ const PromoteWrapper = styled.div` color: #ffffffff; font-weight: bold; transform: rotate(45deg); + } `; const LogoImage = ({ link, data }) => { diff --git a/components/Search/SearchGalleryList/SponsorItem/index.jsx b/components/Search/SearchGalleryList/SponsorItem/index.jsx index 2b46102f..4f7433c1 100644 --- a/components/Search/SearchGalleryList/SponsorItem/index.jsx +++ b/components/Search/SearchGalleryList/SponsorItem/index.jsx @@ -1,7 +1,5 @@ import React, { useMemo } from 'react'; import styled from '@emotion/styled'; -import { css } from '@emotion/react'; -import Link from 'next/link'; import { Typography, Box } from '@mui/material'; import dayjs from 'dayjs'; import isBetween from 'dayjs/plugin/isBetween'; @@ -9,7 +7,6 @@ import { Whatshot } from '@mui/icons-material'; import Tags from './Tags'; import LogoImage from './LogoImage'; import Contributors from './Contributors'; -import { COLOR_TABLE } from '../../../../constants/notion'; dayjs.extend(isBetween); diff --git a/components/Search/SearchGalleryList/index.jsx b/components/Search/SearchGalleryList/index.jsx index c24783b2..96771b5f 100644 --- a/components/Search/SearchGalleryList/index.jsx +++ b/components/Search/SearchGalleryList/index.jsx @@ -1,83 +1,20 @@ import React from 'react'; -import styled from '@emotion/styled'; import { ImageList, - ImageListItem, - ImageListItemBar, - IconButton, } from '@mui/material'; -import StarBorderIcon from '@mui/icons-material/StarBorder'; -import Item from './Item'; -import SponsorItem from './SponsorItem'; -import SkeletonItem from './SkeletonItem'; import ImageItem from './ImageItem'; -const ListWrapper = styled.ul` - display: flex; - flex-direction: column; - justify-content: space-between; -`; - const SearchGalleryList = ({ list, - sponsorList, - queryTags, - isLoading, - isLoadingNextData, }) => { - // if (isLoading && list.length === 0) { - // return ( - // - // - // - // - // - // - // - // ); - // } - // return ( - // - // {/* {sponsorList.map((item) => ( - // - // ))} */} - // {list.map((item, index) => - // isLoading ? ( - // - // ) : ( - // - // ) - // )} - // {isLoadingNextData && ( - // <> - // - // - // - // - // )} - // - // ); - return ( {list.map((item) => { return ( diff --git a/components/Search/SearchResultList/Item/Contributors/index.jsx b/components/Search/SearchResultList/Item/Contributors/index.jsx index cacbefba..3b14cd61 100644 --- a/components/Search/SearchResultList/Item/Contributors/index.jsx +++ b/components/Search/SearchResultList/Item/Contributors/index.jsx @@ -1,10 +1,5 @@ -import React, { useMemo } from 'react'; -import styled from '@emotion/styled'; -import { css } from '@emotion/react'; import Link from 'next/link'; import { Typography, Box } from '@mui/material'; -import dayjs from 'dayjs'; -import isBetween from 'dayjs/plugin/isBetween'; const Contributors = ({ contributors }) => { return ( diff --git a/components/Search/SearchResultList/Item/LogoImage/index.jsx b/components/Search/SearchResultList/Item/LogoImage/index.jsx index 6460436d..b16a1893 100644 --- a/components/Search/SearchResultList/Item/LogoImage/index.jsx +++ b/components/Search/SearchResultList/Item/LogoImage/index.jsx @@ -1,8 +1,6 @@ -import React, { useMemo } from 'react'; import styled from '@emotion/styled'; import { css } from '@emotion/react'; -import Link from 'next/link'; -import { Typography, Box } from '@mui/material'; +import { Box } from '@mui/material'; import dayjs from 'dayjs'; import isBetween from 'dayjs/plugin/isBetween'; diff --git a/components/Search/SearchResultList/Item/Tags/index.jsx b/components/Search/SearchResultList/Item/Tags/index.jsx index af9d98a4..c792e84d 100644 --- a/components/Search/SearchResultList/Item/Tags/index.jsx +++ b/components/Search/SearchResultList/Item/Tags/index.jsx @@ -1,28 +1,15 @@ -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback } from 'react'; import styled from '@emotion/styled'; import { useRouter } from 'next/router'; import Chip from '@mui/material/Chip'; import { COLOR_TABLE } from '../../../../../constants/notion'; import { scrollToTop } from '../../../../../utils/ux'; -// import { TikTokFont } from "../../../../../shared/styles/css"; 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( @@ -56,32 +43,6 @@ const Tags = ({ tags, type }) => { }, [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 ( diff --git a/components/Search/SearchResultList/Item/index.jsx b/components/Search/SearchResultList/Item/index.jsx index 1865187a..09f4bd47 100644 --- a/components/Search/SearchResultList/Item/index.jsx +++ b/components/Search/SearchResultList/Item/index.jsx @@ -1,14 +1,11 @@ import React, { useMemo } from 'react'; import styled from '@emotion/styled'; -import { css, keyframes } from '@emotion/react'; -import Link from 'next/link'; -import { Typography, Box } from '@mui/material'; import dayjs from 'dayjs'; import isBetween from 'dayjs/plugin/isBetween'; import Tags from './Tags'; import LogoImage from './LogoImage'; import Contributors from './Contributors'; -// import { TikTokFont } from '../../../../shared/styles/css'; + dayjs.extend(isBetween); const ItemWrapper = styled.li` @@ -26,25 +23,6 @@ const ContentWrapper = styled.article` } `; -const ImageWrapper = styled.div` - width: 200px; - height: 200px; - border-radius: 20px; - background-color: #f5f5f5; - ${({ image }) => css` - background-image: ${`url(${image})`}; - background-size: cover; - background-repeat: no-repeat; - background-position: 50% 50%; - `} - /* object-fit: cover; */ - /* opacity: 0; */ - - @media (max-width: 767px) { - width: 100px; - height: 100px; - } -`; const TitleWrapper = styled.div` display: flex; flex-wrap: wrap; diff --git a/components/Search/SearchResultList/SponsorItem/Contributors/index.jsx b/components/Search/SearchResultList/SponsorItem/Contributors/index.jsx index cacbefba..3b14cd61 100644 --- a/components/Search/SearchResultList/SponsorItem/Contributors/index.jsx +++ b/components/Search/SearchResultList/SponsorItem/Contributors/index.jsx @@ -1,10 +1,5 @@ -import React, { useMemo } from 'react'; -import styled from '@emotion/styled'; -import { css } from '@emotion/react'; import Link from 'next/link'; import { Typography, Box } from '@mui/material'; -import dayjs from 'dayjs'; -import isBetween from 'dayjs/plugin/isBetween'; const Contributors = ({ contributors }) => { return ( diff --git a/components/Search/SearchResultList/SponsorItem/LogoImage/index.jsx b/components/Search/SearchResultList/SponsorItem/LogoImage/index.jsx index 5bb88ccd..92c1550c 100644 --- a/components/Search/SearchResultList/SponsorItem/LogoImage/index.jsx +++ b/components/Search/SearchResultList/SponsorItem/LogoImage/index.jsx @@ -1,8 +1,6 @@ -import React, { useMemo } from 'react'; import styled from '@emotion/styled'; import { css } from '@emotion/react'; -import Link from 'next/link'; -import { Typography, Box } from '@mui/material'; +import { Box } from '@mui/material'; import dayjs from 'dayjs'; import isBetween from 'dayjs/plugin/isBetween'; @@ -92,6 +90,7 @@ const PromoteWrapper = styled.div` color: #ffffffff; font-weight: bold; transform: rotate(45deg); + } `; const LogoImage = ({ link, data }) => { diff --git a/components/Search/SearchResultList/SponsorItem/index.jsx b/components/Search/SearchResultList/SponsorItem/index.jsx index 2b46102f..4f7433c1 100644 --- a/components/Search/SearchResultList/SponsorItem/index.jsx +++ b/components/Search/SearchResultList/SponsorItem/index.jsx @@ -1,7 +1,5 @@ import React, { useMemo } from 'react'; import styled from '@emotion/styled'; -import { css } from '@emotion/react'; -import Link from 'next/link'; import { Typography, Box } from '@mui/material'; import dayjs from 'dayjs'; import isBetween from 'dayjs/plugin/isBetween'; @@ -9,7 +7,6 @@ import { Whatshot } from '@mui/icons-material'; import Tags from './Tags'; import LogoImage from './LogoImage'; import Contributors from './Contributors'; -import { COLOR_TABLE } from '../../../../constants/notion'; dayjs.extend(isBetween); diff --git a/components/Signin/Interest/TipModal/index.jsx b/components/Signin/Interest/TipModal/index.jsx deleted file mode 100644 index ff501895..00000000 --- a/components/Signin/Interest/TipModal/index.jsx +++ /dev/null @@ -1,106 +0,0 @@ -import { Modal, Box, Typography, Button } from '@mui/material'; - -function TipModal({ onClose, onOk, isLoadingSubmit, open }) { - return ( - - - - 想在平台上尋找夥伴嗎? - - - 不論你是自學生、家長、教育工作者,都可以在平台上尋找志同道合的夥伴一起交流 - - nobody-land - - 我們會公開你的個人檔案,填寫完整的資料,才能讓其他夥伴們更了解你喔! - - - - - - - - ); -} - -export default TipModal; diff --git a/components/Signin/TipModal.tsx b/components/Signin/TipModal.tsx new file mode 100644 index 00000000..6ef84b28 --- /dev/null +++ b/components/Signin/TipModal.tsx @@ -0,0 +1,60 @@ +import { useId } from "react"; +import Image from "@/shared/components/Image"; +import Modal from "@/shared/components/Modal"; + +interface TipModalProps { + isOpen: boolean; + onClose: () => void; + onOk: () => void; +} + +function TipModal({ isOpen, onClose, onOk }: Readonly) { + const id = useId(); + const describedby = `tip-modal-${id}`; + + return ( + +

    + 記得到信箱確認收到帳號驗證信件,並點選驗證Email按鈕,如果沒有看到信件,可以到垃圾桶確認。 +

    + dao-dao-island +

    + 我們會公開你的個人檔案 + ,填寫完整的資料,才能讓其他夥伴們更了解你喔! +

    +
    + + +
    +
    + ); +} + +export default TipModal; diff --git a/contexts/Auth/AuthContext.tsx b/contexts/Auth/AuthContext.tsx index 41db38cb..c3222abd 100644 --- a/contexts/Auth/AuthContext.tsx +++ b/contexts/Auth/AuthContext.tsx @@ -7,8 +7,7 @@ import { useReducer, useRef, } from "react"; -import { useRouter } from "next/navigation"; -import { usePathname } from "next/navigation"; +import { useRouter, usePathname } from "next/navigation"; import useSWR, { SWRConfig } from "swr"; import { useDispatch } from "react-redux"; @@ -118,10 +117,6 @@ const authReducer = (state: AuthState, action: Action): AuthState => { return initialState; } if (action.payload) { - const reminder = getReminderStorage().get(); - getReminderStorage().set( - typeof reminder === "number" ? reminder + 1 : 1 - ); return { ...state, isComplete: checkIsComplete(action.payload), @@ -198,6 +193,9 @@ export function AuthProvider({ children }: PropsWithChildren) { dispatch({ type: ActionTypes.UPDATE_USER, payload }); break; } + default: { + break; + } } }, openLoginModal: (payload) => { @@ -229,7 +227,7 @@ export function AuthProvider({ children }: PropsWithChildren) { ); useEffect(() => { - const handleToken = (token: string) => { + const handleToken = (token?: string) => { if (!token) return; // TODO: 待移除 redux,為了同步資訊 reduxDispatch(fetchUserByToken(token)); @@ -271,12 +269,17 @@ export function AuthProvider({ children }: PropsWithChildren) { switch (state.loginStatus) { case LoginStatus.TEMPORARY: { const redirectUrl = state.redirectUrl || getRedirectionStorage().get(); + getReminderStorage().remove(); authDispatch.closeLoginModal(); router.replace(redirectUrl || "/signin"); break; } case LoginStatus.PERMANENT: { const redirectUrl = state.redirectUrl || getRedirectionStorage().get(); + const reminder = getReminderStorage().get(); + getReminderStorage().set( + typeof reminder === "number" ? reminder + 1 : 1 + ); authDispatch.closeLoginModal(); if (redirectUrl) router.replace(redirectUrl); break; @@ -293,8 +296,9 @@ export function AuthProvider({ children }: PropsWithChildren) { useEffect(() => { const redirectionStorage = getRedirectionStorage(); + const redirection = redirectionStorage.get(); - if (redirectionStorage.get() === pathname) { + if (redirection?.split("?")[0] === pathname) { redirectionStorage.remove(); } }, [pathname]); @@ -363,11 +367,6 @@ export const ProtectedComponent = ({ }; export const sendLoginEvent = (token: string) => { - if (!token) { - // TODO: 處理沒 token 的狀況 - return; - } - getTokenStorage().remove(); if ( @@ -379,9 +378,8 @@ export const sendLoginEvent = (token: string) => { window.location.origin ); window.close(); - } else { - const redirection = getRedirectionStorage().get(); - getTokenStorage().set(token); - window.location.replace(redirection || "/"); + return true; } + + return false; }; diff --git a/package.json b/package.json index cfe3b76a..d28067d3 100644 --- a/package.json +++ b/package.json @@ -71,11 +71,14 @@ "zod": "^3.22.4" }, "devDependencies": { + "@babel/preset-typescript": "^7.26.0", "@emotion/babel-plugin": "^11.9.2", "@next/eslint-plugin-next": "^13.2.1", "@tailwindcss/typography": "^0.5.15", "@types/chrome": "^0.0.206", "@types/react-dom": "^19.0.2", + "@typescript-eslint/eslint-plugin": "6", + "@typescript-eslint/parser": "6", "autoprefixer": "^10.4.20", "babel-plugin-import": "^1.13.8", "eslint": "^8.35.0", diff --git a/pages/404.jsx b/pages/404.jsx index 6b363fbc..fee34529 100644 --- a/pages/404.jsx +++ b/pages/404.jsx @@ -1,15 +1,16 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import styled from '@emotion/styled'; import Box from '@mui/material/Box'; import { Typography, Button, Paper } from '@mui/material'; import { FacebookRounded } from '@mui/icons-material'; import Chip from '@mui/material/Chip'; import { useRouter } from 'next/router'; -import Navigatin from '../shared/components/Navigation_v2'; -import Footer from '../shared/components/Footer_v2'; -import { COLOR_TABLE } from '../constants/notion'; -import { CATEGORIES } from '../constants/category'; -import RelatedResources from '../shared/components/RelatedResources'; +import Navigatin from '@/shared/components/Navigation_v2'; +import Footer from '@/shared/components/Footer_v2'; +import { COLOR_TABLE } from '@/constants/notion'; +import { CATEGORIES } from '@/constants/category'; +import RelatedResources from '@/shared/components/RelatedResources'; +import { getRedirectionStorage } from '@/utils/storage'; const BodyWrapper = styled.div` background-color: #f5f5f5; @@ -17,6 +18,11 @@ const BodyWrapper = styled.div` const NotExistPage = () => { const router = useRouter(); + + useEffect(() => { + getRedirectionStorage().remove(); + }, []); + return ( { + const dispatch = useDispatch(); + const searchParams = useSearchParams(); + const router = useRouter(); + const mode = useSelector((state) => state?.theme?.mode ?? 'light'); + const theme = useMemo(() => themeFactory(mode), [mode]); + const isEnv = useMemo(() => process.env.NODE_ENV === 'development', []); + const { isComplete, isLoggedIn } = useAuth(); + const [openModalType, setOpenModalType] = useState(null); + const Layout = Component?.getLayout || DefaultLayout; + const isVerified = searchParams.get("isVerified"); + + const handleClose = () => { + setOpenModalType(null); + getReminderStorage().remove(); + }; + + useEffect(() => { + dispatch(checkLoginValidity()); + }, []); + + useEffect(() => { + if (isVerified) { + setOpenModalType("verifiedSuccess"); + return; + } + + if (isLoggedIn && !isComplete && getReminderStorage().get() % 3 === 0) { + setOpenModalType("completeInfoReminder"); + } + }, [isVerified, isLoggedIn, isComplete]); + + return ( + + {/* mui normalize css */} + + {/* For custum reset css */} + + + {isEnv && } + + + verified-success + { + isComplete ? ( + <> +

    + 帳號已驗證成功,快來體驗平台的特色功能! +

    +
    + +
    + + ) : ( + <> +

    + 我們會公開你的個人檔案,填寫完整的資料,才能讓其他夥伴們更了解你喔! +

    +
    + + +
    + + ) + } +
    + + + +
    + ); +}; + const App = ({ Component, pageProps }) => { const router = useRouter(); useEffect(() => { @@ -114,57 +234,4 @@ const App = ({ Component, pageProps }) => { ); }; -const ThemeComponentWrap = ({ pageProps, Component }) => { - const dispatch = useDispatch(); - const mode = useSelector((state) => state?.theme?.mode ?? 'light'); - const theme = useMemo(() => themeFactory(mode), [mode]); - const isEnv = useMemo(() => process.env.NODE_ENV === 'development', []); - const { isComplete, isLoggedIn } = useAuth(); - const [isOpen, setIsOpen] = useState(false); - const Layout = Component?.getLayout || DefaultLayout; - - const handleClose = () => { - setIsOpen(false); - getReminderStorage().remove(); - }; - - useEffect(() => { - dispatch(checkLoginValidity()); - }, []); - - useEffect(() => { - if (isLoggedIn && !isComplete && getReminderStorage().get() % 3 === 0) { - setIsOpen(true); - } - }, [isLoggedIn, isComplete]); - - return ( - - {/* mui normalize css */} - - {/* For custum reset css */} - - - {isEnv && } - - - - - - ); -}; - export default App; diff --git a/pages/auth/callback/index.jsx b/pages/auth/callback/index.jsx deleted file mode 100644 index b4a53b4b..00000000 --- a/pages/auth/callback/index.jsx +++ /dev/null @@ -1,49 +0,0 @@ -import { Paper, Typography, Box } from "@mui/material"; -import { useSearchParams } from 'next/navigation'; -import { sendLoginEvent } from "@/contexts/Auth"; - -export default function AuthCallbackPage() { - const searchParams = useSearchParams(); - sendLoginEvent(searchParams.get("token")); - - return ( - - - 正在前往新的島嶼 - - - nobody-land - - - ); -} diff --git a/pages/auth/callback/index.tsx b/pages/auth/callback/index.tsx new file mode 100644 index 00000000..09aeb88e --- /dev/null +++ b/pages/auth/callback/index.tsx @@ -0,0 +1,42 @@ +import { useEffect } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useDispatch } from "react-redux"; +import { sendLoginEvent, useAuthDispatch } from "@/contexts/Auth"; +import { getRedirectionStorage } from "@/utils/storage"; +import { fetchUserByToken } from "@/redux/actions/user"; +import Image from "@/shared/components/Image"; + +export default function AuthCallbackPage() { + // TODO: 待移除 redux,為了同步資訊 + const reduxDispatch = useDispatch(); + const authDispatch = useAuthDispatch(); + const router = useRouter(); + const searchParams = useSearchParams(); + const token = searchParams.get("token"); + const isVerified = searchParams.get("isVerified"); + + useEffect(() => { + if (!token) return; + if (sendLoginEvent(token)) return; + + reduxDispatch(fetchUserByToken(token)); + authDispatch.setToken(token); + router.replace(getRedirectionStorage().get() || "/"); + }, [token, isVerified, router.replace]); + + return ( +
    +

    + 正在前往新的島嶼 +

    +
    + nobody-land +
    +
    + ); +} diff --git a/pages/join/index.jsx b/pages/join/index.jsx index 0756cb35..778721e0 100644 --- a/pages/join/index.jsx +++ b/pages/join/index.jsx @@ -2,14 +2,7 @@ import { useMemo } from 'react'; import { useRouter } from 'next/router'; import styled from '@emotion/styled'; -import { - Box, - Divider, - Typography, - Button, - Skeleton, - TextField, -} from '@mui/material'; +import { Divider, Typography } from '@mui/material'; import SEOConfig from '@/shared/components/SEO'; import Navigation from '@/shared/components/Navigation_v2'; import Footer from '@/shared/components/Footer_v2'; diff --git a/pages/learning-marathon/index.jsx b/pages/learning-marathon/index.jsx index a1d0b5ff..4fb8cdc3 100644 --- a/pages/learning-marathon/index.jsx +++ b/pages/learning-marathon/index.jsx @@ -98,8 +98,10 @@ const useScrollPaddingTop = () => { }; handleScrollPaddingTop(); + window.addEventListener('resize', handleScrollPaddingTop); window.addEventListener('storage', handleStorage); return () => { + window.removeEventListener('resize', handleScrollPaddingTop); window.removeEventListener('storage', handleStorage); }; }, []); @@ -179,7 +181,7 @@ const Sidebar = ({ onClickSignupButton }) => { @@ -224,9 +226,9 @@ const Nav = () => {