From 2aba37f10529eeb8168cdfb464cf6c3c016905f6 Mon Sep 17 00:00:00 2001 From: ruby10127130 Date: Thu, 28 Nov 2024 21:12:21 +0800 Subject: [PATCH 01/76] feat(auth): add auth/callback page for token storage --- pages/auth/callback/index.jsx | 79 +++++++++++++++++++++++++++++++++++ redux/actions/user.js | 9 ++++ redux/reducers/user.js | 25 +++++++++++ redux/sagas/user/index.js | 42 +++++++++++++++++++ 4 files changed, 155 insertions(+) create mode 100644 pages/auth/callback/index.jsx diff --git a/pages/auth/callback/index.jsx b/pages/auth/callback/index.jsx new file mode 100644 index 00000000..da59d1f2 --- /dev/null +++ b/pages/auth/callback/index.jsx @@ -0,0 +1,79 @@ +import { useState, useEffect } from "react"; +import { Paper, Typography, Box } from "@mui/material"; +import { useSearchParams } from 'next/navigation'; +import { useDispatch, useSelector } from 'react-redux'; +import { fetchUserByToken } from "@/redux/actions/user"; + +export default function AuthCallbackPage() { + const searchParams = useSearchParams(); + const dispatch = useDispatch(); + const [isLoading, setIsLoading] = useState(true); + const me = useSelector((state) => state.user); + + useEffect(() => { + const tempToken = searchParams.get("token"); + + if (tempToken) { + dispatch(fetchUserByToken(tempToken)); + } else { + console.error("unfound token"); + } + }, [searchParams, dispatch]); + + useEffect(() => { + if (window.opener && isLoading && me) { + if (me._id) { + window.opener.postMessage({ type: 'USER_UPDATED' }, window.location.origin); + setIsLoading(false); + window.close(); + } + + if (me.tempToken) { + window.opener.postMessage({ type: 'TEMP_TOKEN_UPDATED' }, window.location.origin); + setIsLoading(false); + window.close(); + } + } + }, [me._id, me.tempToken, isLoading]); + + return ( + + + 正在前往新的島嶼 + + + nobody-land + + + ); +} diff --git a/redux/actions/user.js b/redux/actions/user.js index b707fad1..a5d44fdf 100644 --- a/redux/actions/user.js +++ b/redux/actions/user.js @@ -80,3 +80,12 @@ export function createUser(user) { }, }; } + +export function fetchUserByToken(token) { + return { + type: 'FETCH_USER_BY_TOKEN', + payload: { + token, + }, + }; +} diff --git a/redux/reducers/user.js b/redux/reducers/user.js index 85ca36c6..fede417f 100644 --- a/redux/reducers/user.js +++ b/redux/reducers/user.js @@ -96,6 +96,31 @@ const reducer = (state = initialState, action) => { apiState: 'Reject', }; } + case 'FETCH_USER_BY_TOKEN_SUCCESS': { + return { + ...state, + ...action.payload, + loading: false, + apiState: 'Resolve' + }; + } + case 'FETCH_USER_BY_TOKEN_SUCCESS_NO_DATA': { + return { + ...state, + ...action.payload, + loading: false, + apiState: 'Resolve' + }; + } + case 'FETCH_USER_BY_TOKEN_FAILURE': { + return { + ...state, + user: null, + loading: false, + error: action.error, + apiState: 'Reject', + }; + } default: { return state; } diff --git a/redux/sagas/user/index.js b/redux/sagas/user/index.js index ada841ee..06a5e10e 100644 --- a/redux/sagas/user/index.js +++ b/redux/sagas/user/index.js @@ -107,12 +107,54 @@ function* fetchUserById(action) { } } +// fetch user data by token +function* fetchUserByToken(action) { + const token = action.payload?.token; + try { + const URL = `${BASE_URL}/user/me`; + const result = yield call(req, URL, { + method: "GET", + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + if (result.data && result.data._id) { + yield put({ + type: "FETCH_USER_BY_TOKEN_SUCCESS", + payload: result.data && { + _id: result.data._id, + ...result.data, + token, + tokenExpiry: handleTokenExpiry(true), + }, + }); + } else { + yield console.log("before no token"); + yield put({ + type: "FETCH_USER_BY_TOKEN_SUCCESS_NO_DATA", + payload: { + ...result.data, + tempToken: token, + tokenExpiry: handleTokenExpiry(true), + }, + }); + } + } catch (error) { + console.error("Error fetching user by token:", JSON.stringify(error)); + yield put({ + type: "FETCH_USER_BY_TOKEN_FAILURE", + error: error.message || "Unknown error", + }); + } +} function* userSaga() { yield takeEvery('CHECK_USER_ACCOUNT', checkUserStatus); yield takeEvery('FETCH_ALL_USERS', fetchAllUsers); yield takeEvery('CREATE_USER_PROFILE', createUserProfile); yield takeEvery('UPDATE_USER_PROFILE', updateUserProfile); yield takeEvery('FETCH_USER_BY_ID', fetchUserById); + yield takeEvery("FETCH_USER_BY_TOKEN", fetchUserByToken); } export default userSaga; From 114915f3181f970c3cda26609d5d59f568308d03 Mon Sep 17 00:00:00 2001 From: ruby10127130 Date: Wed, 4 Dec 2024 21:59:15 +0800 Subject: [PATCH 02/76] feat(user): add marathon data in user state --- redux/sagas/user/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/redux/sagas/user/index.js b/redux/sagas/user/index.js index 06a5e10e..25c5bf39 100644 --- a/redux/sagas/user/index.js +++ b/redux/sagas/user/index.js @@ -120,6 +120,7 @@ function* fetchUserByToken(action) { }); if (result.data && result.data._id) { + const marathonResponse = yield call(req, `${BASE_URL}/marathon?userId=${result.data._id}`); yield put({ type: "FETCH_USER_BY_TOKEN_SUCCESS", payload: result.data && { @@ -127,6 +128,7 @@ function* fetchUserByToken(action) { ...result.data, token, tokenExpiry: handleTokenExpiry(true), + marathons: marathonResponse?.data || [], }, }); } else { From af7910ec5e6261282717b013fd257668c976d78a Mon Sep 17 00:00:00 2001 From: ruby10127130 Date: Wed, 4 Dec 2024 22:00:35 +0800 Subject: [PATCH 03/76] chore: remove unused console.log --- redux/sagas/user/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/redux/sagas/user/index.js b/redux/sagas/user/index.js index 25c5bf39..69214dfc 100644 --- a/redux/sagas/user/index.js +++ b/redux/sagas/user/index.js @@ -132,7 +132,6 @@ function* fetchUserByToken(action) { }, }); } else { - yield console.log("before no token"); yield put({ type: "FETCH_USER_BY_TOKEN_SUCCESS_NO_DATA", payload: { From 9a88c03db1081dcb6b5347548b073442e02d90e0 Mon Sep 17 00:00:00 2001 From: ruby10127130 Date: Thu, 5 Dec 2024 23:12:35 +0800 Subject: [PATCH 04/76] feat: set up marathon-related actions, saga, and store --- redux/actions/marathon.js | 48 ++++++++++++++ redux/reducers/index.js | 2 + redux/reducers/marathon.js | 89 ++++++++++++++++++++++++++ redux/sagas/index.js | 2 + redux/sagas/marathon/index.js | 115 ++++++++++++++++++++++++++++++++++ redux/store/index.js | 2 +- 6 files changed, 257 insertions(+), 1 deletion(-) create mode 100644 redux/actions/marathon.js create mode 100644 redux/reducers/marathon.js create mode 100644 redux/sagas/marathon/index.js diff --git a/redux/actions/marathon.js b/redux/actions/marathon.js new file mode 100644 index 00000000..ae449ec2 --- /dev/null +++ b/redux/actions/marathon.js @@ -0,0 +1,48 @@ +export function fetchMarathonProfileById(id) { + return { + type: 'FETCH_MARATHON_PROFILE_BY_ID', + payload: { + id, + }, + }; +} + +export function createMarathonProfileByToken(token, marathon) { + return { + type: 'CREATE_MARATHON_PROFILE_BY_TOKEN', + payload: { + token, + marathon + } + }; +} + +export function updateMarathonProfile(token, id, marathon) { + return { + type: 'UPDATE_MARATHON_PROFILE', + payload: { + token, + id, + marathon + } + }; +} + +export function deleteMarathonProfile(token, id) { + return { + type: 'DELETE_MARATHON_PROFILE', + payload: { + token, + id + } + }; +} + +export function fetchMarathonProfileByUserId(userId) { + return { + type: 'FETCH_MARATHON_PROFILE_BY_USER_ID', + payload: { + userId + } + }; +} diff --git a/redux/reducers/index.js b/redux/reducers/index.js index e5c924c4..06c6b716 100644 --- a/redux/reducers/index.js +++ b/redux/reducers/index.js @@ -6,6 +6,7 @@ import shared from './shared'; import resource from './resource'; import group from './group'; import partners from './partners'; +import marathon from './marathon'; const allReducers = combineReducers({ search, @@ -15,6 +16,7 @@ const allReducers = combineReducers({ resource, group, partners, + marathon, }); export default allReducers; diff --git a/redux/reducers/marathon.js b/redux/reducers/marathon.js new file mode 100644 index 00000000..7bcc83e6 --- /dev/null +++ b/redux/reducers/marathon.js @@ -0,0 +1,89 @@ +// import toast from 'react-hot-toast'; + +const initialState = { + title: '', + eventId: '', + userId: '', + description: '', + motivation: { tags: [], description: '' }, + content: "", + goals: "", + strategies: { tags: [], description: '' }, + resources: [], + milestones: [], + outcomes: { tags: [], description: '' }, + status: "Ongoing", + pricing: { option: "", pricing: 0, email: [], file: "" }, + isPublic: false, + startDate: '', + endDate: '', + userMarathon: [] +}; + +const reducer = (state = initialState, action) => { + switch (action.type) { + case 'FETCH_MARATHON_PROFILE_BY_USER_ID': { + return { + ...state, + apiState: 'pending' + }; + } + case 'FETCH_MARATHON_PROFILE_BY_USER_ID_SUCCESS': { + return { + ...state, + userMarathon: action.payload, + apiState: 'success' + }; + } + case 'FETCH_MARATHON_PROFILE_BY_USER_ID_FAILURE': { + return { + ...state, + apiState: 'reject' + }; + } + case 'CREATE_MARATHON_PROFILE': { + return { + ...state, + apiState: 'pending' + }; + } + case 'CREATE_MARATHON_PROFILE_BY_TOKEN_SUCCESS': { + return { + ...state, + ...action.payload, + apiState: 'success' + }; + } + case 'FETCH_MARATHON_PROFILE_BY_ID': { + return { + ...state, + apiState: 'pending' + }; + } + case 'FETCH_MARATHON_PROFILE_BY_ID_SUCCESS': { + return { + ...state, + ...action.payload, + apiState: 'success' + }; + } + case 'FETCH_MARATHON_PROFILE_BY_ID_FAILURE': { + return { + ...state, + apiState: 'reject' + }; + } + case 'UPDATE_MARATHON_PROFILE_SUCCESS': { + return { + ...state, + ...action.payload, + apiState: 'success', + }; + } + default: { + return state; + } + } +}; + +export default reducer; diff --git a/redux/sagas/index.js b/redux/sagas/index.js index ef30f8ad..2d908d84 100644 --- a/redux/sagas/index.js +++ b/redux/sagas/index.js @@ -1,6 +1,7 @@ import { all } from 'redux-saga/effects'; import searchSaga from './searchSaga'; import userSaga from './user'; +import marathonSaga from './marathon'; import partnerSaga from './partnersSaga'; import sharedSaga from './sharedSaga'; import resourceSaga from './resourceSaga'; @@ -16,5 +17,6 @@ export default function* rootSaga() { resourceSaga(), groupSaga(), partnerSaga(), + marathonSaga() ]); } diff --git a/redux/sagas/marathon/index.js b/redux/sagas/marathon/index.js new file mode 100644 index 00000000..59e7539a --- /dev/null +++ b/redux/sagas/marathon/index.js @@ -0,0 +1,115 @@ +import { put, takeEvery, call } from "redux-saga/effects"; +import { BASE_URL } from "@/constants/common"; +import req from "@/utils/request"; + +function* fetchMarathonProfileByUserId(action) { + const { userId } = action.payload; + try { + const URL = `${BASE_URL}/marathon?userId=${userId}`; + + const result = yield req(URL); + yield put({ + type: "FETCH_MARATHON_PROFILE_BY_USER_ID_SUCCESS", + payload: result.data, + }); + } catch (error) { + console.log(error); + yield put({ type: "FETCH_MARATHON_PROFILE_BY_USER_ID_FAILURE" }); + } +} + +function* fetchMarathonProfileById(action) { + const { id } = action.payload; // marathon._id + try { + const URL = `${BASE_URL}/marathon/${id}`; + + const result = yield req(URL); + yield put({ + type: "FETCH_MARATHON_PROFILE_BY_ID_SUCCESS", + payload: result.data, + }); + } catch (error) { + console.log(error); + yield put({ type: "FETCH_MARATHON_PROFILE_BY_ID_FAILURE" }); + } +} +function* createMarathonProfileByToken(action) { + const { token, marathon } = action.payload; + try { + const URL = `${BASE_URL}/marathon`; + + const result = yield req(URL, { + method: "POST", + body: JSON.stringify({ + ...marathon, + }), + }); + + yield put({ + type: "CREATE_MARATHON_PROFILE_BY_TOKEN_SUCCESS", + payload: { token, ...result.data }, + }); + } catch (error) { + console.log(error); + yield put({ type: "CREATE_MARATHON_PROFILE_BY_TOKEN_FAILURE" }); + } +} + +function* updateMarathonProfile(action) { + const { id, marathon } = action.payload; + + try { + const URL = `${BASE_URL}/marathon/${id}`; + + const result = yield req(URL, { + method: "PUT", + body: JSON.stringify({ + ...marathon, + }), + }); + + yield put({ + type: "UPDATE_MARATHON_PROFILE_SUCCESS", + payload: result.data, + }); + } catch (error) { + yield put({ type: "UPDATE_MARATHON_PROFILE_FAILURE" }); + } finally { + yield new Promise((res) => setTimeout(res, 300)); + yield put({ type: "UPDATE_MARATHON_PROFILE_API_STATE_RESET" }); + } +} + +function* deleteMarathonProfile(action) { + const { id } = action.payload; + + try { + const URL = `${BASE_URL}/marathon/${id}`; + + const result = yield req(URL, { + method: "DELETE", + body: JSON.stringify({ + id, + }), + }); + yield put({ + type: "DELETE_MARATHON_PROFILE_SUCCESS", + payload: result.data, + }); + } catch (error) { + yield put({ type: "DELETE_MARATHON_PROFILE_FAILURE" }); + } finally { + yield new Promise((res) => setTimeout(res, 300)); + yield put({ type: "DELETE_MARATHON_PROFILE_API_STATE_RESET" }); + } +} + +function* marathonSaga() { + yield takeEvery("FETCH_MARATHON_PROFILE_BY_ID", fetchMarathonProfileById); + yield takeEvery("CREATE_MARATHON_PROFILE_BY_TOKEN", createMarathonProfileByToken); + yield takeEvery("UPDATE_MARATHON_PROFILE", updateMarathonProfile); + yield takeEvery("DELETE_MARATHON_PROFILE", deleteMarathonProfile); + yield takeEvery("FETCH_MARATHON_PROFILE_BY_USER_ID", fetchMarathonProfileByUserId); +} + +export default marathonSaga; diff --git a/redux/store/index.js b/redux/store/index.js index 4b792586..b817b365 100644 --- a/redux/store/index.js +++ b/redux/store/index.js @@ -16,7 +16,7 @@ import { const persistConfig = { key: 'root', storage, - whitelist: ['user', 'partners'], + whitelist: ['user', 'partners', 'marathon'], }; import rootReducer from '../reducers'; From db99cf7f8ce3d5af0d8b0c7e867eca84b23c68b7 Mon Sep 17 00:00:00 2001 From: ruby10127130 Date: Tue, 10 Dec 2024 22:56:13 +0800 Subject: [PATCH 05/76] feat: install uuid package --- package.json | 3 ++- yarn.lock | 25 +++++++------------------ 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index 7de57d4d..5b601420 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "private": true, "scripts": { - "dev": "next dev -p 5000", + "dev": "next dev -p 5001", "dev-https": "node server.js", "static": "serve out", "build": "next build && next export", @@ -67,6 +67,7 @@ "swr": "^2.2.5", "tailwind-merge": "^2.5.4", "use-image": "^1.0.10", + "uuid": "^11.0.3", "zod": "^3.22.4" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index 8df046bd..f8534b35 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7361,16 +7361,7 @@ strict-event-emitter@^0.4.3: resolved "https://registry.yarnpkg.com/strict-event-emitter/-/strict-event-emitter-0.4.6.tgz#ff347c8162b3e931e3ff5f02cfce6772c3b07eb3" integrity sha512-12KWeb+wixJohmnwNFerbyiBrAlq5qJLwIt38etRtKtmmHyDSoGlIqFE9wx+4IwG0aDjI7GV8tc8ZccjWZZtTg== -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -7459,14 +7450,7 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -7961,6 +7945,11 @@ util-deprecate@^1.0.2: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== +uuid@^11.0.3: + version "11.0.3" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.0.3.tgz#248451cac9d1a4a4128033e765d137e2b2c49a3d" + integrity sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg== + vfile-message@^4.0.0: version "4.0.2" resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-4.0.2.tgz#c883c9f677c72c166362fd635f21fc165a7d1181" From e4fbd445061e4ff89d09ed1ec62b2fc54acba511 Mon Sep 17 00:00:00 2001 From: ruby10127130 Date: Tue, 10 Dec 2024 23:15:00 +0800 Subject: [PATCH 06/76] feat: add learning-marathon page --- components/Marathon/About/index.jsx | 48 ++ components/Marathon/Banner/CardList/Card.jsx | 39 ++ components/Marathon/Banner/CardList/index.jsx | 31 + components/Marathon/Banner/Title/index.jsx | 51 ++ components/Marathon/Banner/index.jsx | 74 +++ components/Marathon/Edm/index.jsx | 108 ++++ components/Marathon/index.jsx | 571 ++++++++++++++++++ pages/learning-marathon/index.jsx | 73 +++ 8 files changed, 995 insertions(+) create mode 100644 components/Marathon/About/index.jsx create mode 100644 components/Marathon/Banner/CardList/Card.jsx create mode 100644 components/Marathon/Banner/CardList/index.jsx create mode 100644 components/Marathon/Banner/Title/index.jsx create mode 100644 components/Marathon/Banner/index.jsx create mode 100644 components/Marathon/Edm/index.jsx create mode 100644 components/Marathon/index.jsx create mode 100644 pages/learning-marathon/index.jsx diff --git a/components/Marathon/About/index.jsx b/components/Marathon/About/index.jsx new file mode 100644 index 00000000..7cfe1f3e --- /dev/null +++ b/components/Marathon/About/index.jsx @@ -0,0 +1,48 @@ +import styled from '@emotion/styled'; +import { Typography } from '@mui/material'; +import { useRouter } from 'next/router'; + +const GuideWrapper = styled.div` + width: 90%; + margin: 0 auto; + padding-top: 40px; + padding-bottom: 40px; + + .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 ( + + + 計畫進行方式與內容 + + + ); +}; + +export default About; diff --git a/components/Marathon/Banner/CardList/Card.jsx b/components/Marathon/Banner/CardList/Card.jsx new file mode 100644 index 00000000..9ecc4c21 --- /dev/null +++ b/components/Marathon/Banner/CardList/Card.jsx @@ -0,0 +1,39 @@ +import styled from '@emotion/styled'; +import Link from 'next/link'; + +const CardWrapper = styled.li` + border-radius: 10px; + width: 260px; + height: 320px; + overflow: hidden; + cursor: pointer; +`; + +const ContentWrapper = styled.div` + height: 260px; + background-image: url(${(props) => props.image}); + background-repeat: no-repeat; + background-size: 100% 100%; +`; + +const FooterWrapper = styled.div` + background-color: #ffffff; + display: flex; + align-items: center; + height: 60px; + padding-left: 20px; + font-weight: 500; +`; + +const Card = ({ title, link, image }) => { + return ( + + + + {title} + + + ); +}; + +export default Card; diff --git a/components/Marathon/Banner/CardList/index.jsx b/components/Marathon/Banner/CardList/index.jsx new file mode 100644 index 00000000..5a938c24 --- /dev/null +++ b/components/Marathon/Banner/CardList/index.jsx @@ -0,0 +1,31 @@ +import styled from '@emotion/styled'; +import Card from './Card'; + +const CardListWrapper = styled.ul` + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: center; + padding-top: 20px; + padding-bottom: 20px; + li { + margin: 20px; + } +`; + +const CardList = ({ list }) => { + return ( + + {list.map((category) => ( + + ))} + + ); +}; + +export default CardList; diff --git a/components/Marathon/Banner/Title/index.jsx b/components/Marathon/Banner/Title/index.jsx new file mode 100644 index 00000000..07fb1009 --- /dev/null +++ b/components/Marathon/Banner/Title/index.jsx @@ -0,0 +1,51 @@ +import React from 'react'; +import styled from '@emotion/styled'; +import Typed from 'react-typed'; + +const TitleWrapper = styled.div` + min-height: 70px; + h1 { + font-size: 24px; + line-height: 28px; + letter-spacing: 0.08em; + color: #f0f0f0; + font-weight: 500; + text-align: center; + } + + h2 { + font-size: 16px; + line-height: 22px; + letter-spacing: 0.08em; + text-align: center; + margin-top: 10px; + color: #f0f0f0; + font-weight: 500; + } + @media (max-width: 768px) { + min-height: 130px; + } +`; + +const Title = () => { + return ( + +

+ +

+

+ +

+
+ ); +}; + +export default Title; diff --git a/components/Marathon/Banner/index.jsx b/components/Marathon/Banner/index.jsx new file mode 100644 index 00000000..64d71b61 --- /dev/null +++ b/components/Marathon/Banner/index.jsx @@ -0,0 +1,74 @@ +import { useRouter } from 'next/router'; +import styled from '@emotion/styled'; +import Button from '@/shared/components/Button'; +import groupBannerImg from '@/public/assets/group-banner.png'; +import Image from '@/shared/components/Image'; +import InfoCompletionGuard from '@/shared/components/InfoCompletionGuard'; + +const StyledBanner = styled.div` + position: relative; + height: 398px; + + picture { + position: absolute; + z-index: -1; + width: 100%; + top: 0; + height: 100%; + + img { + height: inherit; + object-fit: cover; + } + } +`; + +const StyledBannerContent = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding-top: 100px; + + h1 { + margin-bottom: 8px; + font-weight: 700; + font-size: 36px; + line-height: 140%; + color: #536166; + } + + p { + font-weight: 400; + font-size: 14px; + line-height: 140%; + color: #536166; + } +`; + +const Banner = () => { + const router = useRouter(); + + return ( + + + 島島盃 - 學習馬拉松 2025 春季賽 + + +

島島盃 - 學習馬拉松 2025 春季賽

+

註冊並加入我們,立即報名!

+ + + +
+
+ ); +}; + +export default Banner; diff --git a/components/Marathon/Edm/index.jsx b/components/Marathon/Edm/index.jsx new file mode 100644 index 00000000..a55ac0df --- /dev/null +++ b/components/Marathon/Edm/index.jsx @@ -0,0 +1,108 @@ +import React from 'react'; +import styled from '@emotion/styled'; +import { Button, Box, Typography, Link } from '@mui/material'; + +const EdmWrapper = styled.div` + width: 90%; + /* height: calc(var(--section-height) + var(--section-height-offset)); */ + margin: 0 auto; + padding-top: 40px; + padding-bottom: 120px; + @media (max-width: 767px) { + padding-top: 40px; + padding-bottom: 20px; + } +`; + +function Edm() { + return ( + + + + 想收到最新資訊嗎? + + + 歡迎訂閱島島電子報 + + + 每月與您分享最新資訊,內容包含:國內外教育新聞、自學經驗分享、實驗教育職缺、每月最新自學資源 + + + + + + + ); +} + +export default Edm; diff --git a/components/Marathon/index.jsx b/components/Marathon/index.jsx new file mode 100644 index 00000000..e3e38c7f --- /dev/null +++ b/components/Marathon/index.jsx @@ -0,0 +1,571 @@ +import { useMemo, useRef, useState } from 'react'; +import styled from '@emotion/styled'; +import { Divider, Typography, Box, Grid } from '@mui/material'; +import Banner from './Banner'; +import About from './About'; +import Edm from './Edm'; + +const LearningMarathonWrapper = styled.div``; + +const StyledGuideTitle = styled(Typography)` + color: #293A3D; + font-weight: bold; + line-height: 140%; + margin-left: 0; + text-align: left; + font-size: 22px; +`; + +const StyledGuideSubtitle = styled(Typography)` + font-size: 16px; + font-width: 500; + line-height: 140%; +`; +const StyledGuideParagraph = styled(Typography)` + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 140%; + display: block; + text-align: left; + color: #536166; +`; + +const GuideWrapper = styled.div` + width: 52vw; + margin: 0 auto; + padding-top: 100px; + padding-bottom: 100px; + + @media (max-width: 767px) { + width: 100%; + padding-top: 40px; + padding-bottom: 20px; + } +`; + +const StyledList = styled(Box)` + + ul { + list-style-type: inherit; + padding-left: 1.5em; + } + + ol { + list-style-type: decimal; + padding-left: 1.5em; + } + + li { + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 140%; + color: #536166; + } + + p, span { + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 140%; + color: #536166; + } +`; +const StyledMethodCard = styled(Box)` + display: flex; + width: 100%; + height: 300px; + padding: 25px 30px; + flex-direction: column; + align-items: flex-start; + gap: 30px; + align-self: stretch; + border-radius: 10px; + + h3 { + color: #293A3D; + font-size: 18px; + font-style: normal; + font-weight: 700; + line-height: 140%; + } + + ul { + list-style-type: inherit; + padding-left: 1.5em; + } + + li { + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 140%; + color: #293A3D; + } +`; + +const StyledSpotlightCard = styled(Box)` + padding: 25px 30px; + color: #FFF; + border-radius: 10px; + + ul { + padding-left: 1.5em; + list-style-type: inherit; + } + + h3 { + color: #FFF; + font-size: 18px; + font-style: normal; + font-weight: 700; + line-height: 140%; + margin-bottom: 36px; + } + + p, li { + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 140%; + } +`; + +function Marathon() { + const guideRef = useRef(null); + return ( + + + + + + 活動介紹 + + + 學習這趟漫長的馬拉松,我可不可以用我的方式跑向屬於我的終點?
+ 發展興趣、改變生活習慣、上理想的大學、生涯規劃、發起社會行動,每一個生活大小事都是一場學習馬拉松。然而,每一次的奮力前行總會遇到「不知道怎麼計畫」、「好難自律」、「沒有伴」、「資源與人脈有限」、「無限自我質疑」等難題... +
+ + 島島盃將提供你四大裝備: + + +
    +
  • + 「專業陪跑員」陪你規劃路徑與自我釐清 +
  • +
  • + 「百人社群」讓你找到合適夥伴與各界人脈 +
  • +
  • + 「AI個人化數位工具」讓你在紀錄與覆盤中自律學習、AI智慧推薦與引導 +
  • +
  • + 「專業課程」帶你掌握自主學習要領 +
  • +
+
+ + 如果你有些想做的計畫,正在等待個契機開始,現在就是時候。
+ 五個月的馬拉松後,你將會在計畫過程中「豐富知識經驗、在學習中形塑自我、為生活與社會帶來實際行動」,而最終的成果發表你還有機會獲得獎助金。 +
+ + 島島盃 2025 春季學習馬拉松,將以學習者以自我需求出發設計學習計畫,開啟一趟自我導向學習馬拉松,往哪跑?怎麼跑?跑多快?終點在哪由你決定,島島阿學陪你一起跑。
+ 邀請你一起「為自己重新打造喜歡的學習生活」,讓我們陪伴彼此,成就自我與他人。 +
+
+
+ + + + 誰適合參加? + + +
    +
  • 16歲以上學習者皆可報名,優先以高中及大學生為主
  • +
  • 有意願為自己打造專屬學習旅程的學習者
  • +
+
+ + + 如果你符合下列一項,那你也許就是適合的參加的人: + +
    +
  1. 有模糊的職涯/生涯方向,想開始做準備與鋪路
  2. +
  3. 學校課程好無聊,希望可以用自己的方式學自己有興趣的事情
  4. +
  5. 考試不適合我,更想用個人經歷上大學
  6. +
  7. 想自主學習,有方向但不確定可以怎麼開始
  8. +
+
+ + 特別提醒:
+ 活動重視社群互動與共學,若無法在計劃期間投入時間參與並和其他夥伴和 Mentor 互動,請斟酌報名。 +
+
+
+ + + + 馬拉松進行方式 + + + 我們提供的裝備 + + + + +

「專業陪跑員」
陪你規劃路徑與自我釐清

+
    +
  • 3 次 1 小時一對一諮詢
  • +
  • 2 次 1 小時團體諮詢
  • +
  • Mentor 每兩週對學員的學習進度給予回饋
  • +
+
+
+ + +

「專業課程」
帶你掌握自主學習要領

+
    +
  • 「策略」目標設定與學習策略
  • +
  • 「方法」思考、提問、筆記方法
  • +
  • 「人」學習社群與個人狀態釐清
  • +
  • 「展現」成果展現與自我行銷
  • +
+
+
+ + +

「百人社群」 讓你找到合適夥伴與各界人脈

+
    +
  • 5 次 1 小時全員每月聚會
  • +
  • 專屬學習小組,5 次 1 小時學習小組每月聚會
  • +
  • 島島阿學 Discord 社群即時交流 島島阿學網站找夥伴找揪團功能
  • +
+
+
+ + + +

「AI 個人化學習工具」 引導你學習方向及自律學習

+
    +
  • 具引導性的自主學習模板
  • +
  • 學習日誌
  • +
  • 學習任務上傳與回饋區
  • +
  • 進度安排與檢核表
  • +
  • 自我檢核表
  • +
  • 學習成果分享專區
  • +
  • AI智慧推薦與引導
  • +
+
+
+
+ + 這場馬拉松有什麼不一樣? + + + + +

專業且客製化的陪跑方式

+

不只重視成果,更重視過程與你的全人發展,並強調「Knowing知識經驗、Being個人形塑、Doing行動」三者的交織。不只這樣...

+
    +
  • 萃取多位自我導向學習實踐者之經驗
  • +
  • 結合被譽為全球最接近民主教育的美國百年民主大學 Goddard College 教學方法(首次在台灣公開)
  • +
  • 結合 High Performance Learning Journeys 學習引導法
  • +
  • AI智慧推薦與引導
  • +
+
+
+ + +

AI 個人化學習工具X社群支持

+

有 AI 推薦與引導外,也重視人與人真實地互動!

+
    +
  • 結合 AI 給你更好的資源與人脈推薦,以及學習引導
  • +
  • 跨領域、跨年齡的百人社群,讓你可以找到同儕,也可以找到業界前輩
  • +
+
+
+
+
+
+ + + + + 你可以預期的收穫 + + + 只要報名,不論有無入選,就可以優先使用島島阿學 AI 個人化學習工具,包含自主學習模板、學習日誌、學習進度追蹤、AI 智慧與引導等功能! + + + + + 而入選後,你還可以與專屬引導師與學習夥伴跑完一趟自我導向學習的馬拉松,完成遲遲未開始的計畫,並在過程中... + +
    +
  1. 習得AI世代不可或缺的「自主學習力、協作力、跨領域學習力」
  2. +
  3. 更深入認識自己,將學習與自身需求連結,找到學習的內在動機
  4. +
  5. 豐富學習資源與人脈,讓學習不再孤單,並增加學習可能性
  6. +
  7. 完成一份具體的學習計畫與成果,兼顧各自需求與外界認可
  8. +
  9. 成為助人者,完成整趟學習馬拉松者將獲得自主學習引導師優先培訓機會
  10. +
+
+
+
+ + + + + 成果發表與獎勵 + + + 在學習馬拉松尾聲,針對入選的20位學員,島島阿學將舉辦成果分享日,並邀請引導師及入選者作為評審,更提供NT$ 5000元獎金支持優秀計畫持續發展! + + + 獎勵 + + +
    +
  • + 成果分享活動將選出5位優選參與者,每位可獲 NT$ 5000元獎金、優選證明,以及島島阿學專訪與媒體曝光。 +
  • +
  • + 評選標準: +
      +
    • + 學習歷程紀錄與反思完成度(60%):可以清楚學習每一個過程的狀態(如遇的困難、解決方法、心態等)、反思以及下一步行動的改變。 +
    • +
    • + 學習成果完成度(40%):學習成果達到預期的學習目標的程度。 +
    • +
    +
  • +
+
+ + + 分享路上的風景 + +
    +
  • 每位參與者在計劃結束時需在島島阿學網站公開學習計劃。
  • +
  • 每位參與者在計劃結束時須分享至少三個於計劃期間使用的學習資源,並分享使用心得。
  • +
  • 每位參與者需完成學習馬拉松回饋問卷。
  • +
+
+
+
+ + + + + 如何申請 + + + (一)重要時程: + +
    +
  • 計畫開始報名:2024/12/15
  • +
  • 線上說明會暨自主學習小小工作坊:2024/12/21(六)15:00-16:30
  • +
  • 申請截止:2025/1/19 23:59
  • +
  • 入選與備取公告:2025/1/27
  • +
  • 繳費期限:2025/2/2 23:59
  • +
  • 備取遞補公告:2025/2/4
  • +
  • 計劃期間:2025/2/9-2025/7/12
  • +
  • 線上暖身活動:2025/2/9(日)14:00-15:30
  • +
  • 線上課時間:待確認,前三堂課程將於 2/10-3/10 之間舉行。
  • +
  • 成果分享日:2025/7/12(六)10:00-16:00
  • +
  • 社群交流線上與實體時間: +
      +
    • 線上:2/23(日)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 台中
    • +
    +
  • +
+
+ (二)申請方式: + +
    +
  • 透過註冊島島阿學官網會員系統並同時填寫線上表單申請本計劃
  • +
  • 請透過此連結申請
  • +
  • 在報名截止日前皆可修改申請內容
  • +
  • 入選名額:20 位
  • +
+
+ + (三)評選標準: + + + 為確保學習計畫的品質和有效性,評選將依據以下標準進行: + + + 1、計畫完整性 (30%) + + +
    +
  • 計畫簡述:願景清晰明確,具體可行,例如實現願景的步驟合理、邏輯性強,且有階段性規劃。
  • +
  • 學習動機:動機強烈且具說服力,能清楚連結個人經驗與學習主題。
  • +
  • 學習內容:學習內容具體且聚焦,與學習主題密切相關。
  • +
+
+ + 2、目標與方法 (30%) + + +
    +
  • 學習目標:目標明確、可衡量、可達成、具相關性。
  • +
  • 學習方法與策略:方法和策略多元且有效,能促進學習目標的達成。
  • +
+
+ + 3、資源與時程 (20%) + + +
    +
  • 學習資源:資源類型多元且可靠,包含線上線下資源、書籍、師資、社群等。
  • +
  • 學習時程表:時程安排合理,學習進度規劃明確。
  • +
+
+ + 4、評量與成果 (20%) + + +
    +
  • 學習評量:評量方式客觀且有效,能真實反映學習成果。
  • +
  • 學習成果呈現方式:成果呈現方式具體且多元,並與學習目標相符,能有效展現學習成果。
  • +
+
+ + 評選委員將依據上述標準,綜合考量申請者的學習計畫,進行評分和排序。 + +
+
+ +
+ ); +} + +export default Marathon; diff --git a/pages/learning-marathon/index.jsx b/pages/learning-marathon/index.jsx new file mode 100644 index 00000000..f579ffe9 --- /dev/null +++ b/pages/learning-marathon/index.jsx @@ -0,0 +1,73 @@ +import React, { useMemo, useEffect } from 'react'; +import styled from '@emotion/styled'; +import { useRouter } from 'next/router'; +import { sendLoginConfirmation } from '@/utils/openLoginWindow'; +import SEOConfig from '@/shared/components/SEO'; +import Marathon from '@/components/Marathon'; +import Navigation from '@/shared/components/Navigation_v2'; +import Footer from '@/shared/components/Footer_v2'; + +const HomePageWrapper = styled.div` + --section-height: calc(100vh - 80px); + --section-height-offset: 80px; +`; + +const LearningMarathon = () => { + const router = useRouter(); + const SEOData = useMemo( + () => ({ + title: '島島盃 - 2025 春季學習馬拉松|多元學習資源平台|島島阿學', + description: + '「島島阿學」盼能透過建立多元的學習資源網絡,讓自主學習者能找到合適的成長方法,進一步成為自己想成為的人,從中培養共好精神。目前正積極打造「可共編的學習資源平台」。', + keywords: '島島阿學', + author: '島島阿學', + copyright: '島島阿學', + imgLink: 'https://www.daoedu.tw/preview.webp', + link: `${process.env.HOSTNAME}${router?.asPath}`, + structuredData: [ + { + '@context': 'https://schema.org', + '@type': 'WebSite', + url: 'https://www.daoedu.tw', + potentialAction: { + '@type': 'SearchAction', + 'query-input': 'required name=q', + target: 'https://www.daoedu.tw/search?q={q}', + }, + }, + { + '@context': 'https://schema.org', + '@type': 'Organization', + url: 'https://www.daoedu.tw', + logo: 'https://www.daoedu.tw/favicon-112.png', + }, + ], + }), + [router?.asPath], + ); + + const { token, id } = router.query; + + useEffect(() => { + sendLoginConfirmation(id, token); + }, [id, token]); + + return ( + <> + + + + ); +}; + +LearningMarathon.getLayout = ({ children }) => { + return ( + + + {children} +