diff --git a/components/Marathon/SignUp/MarathonForm.jsx b/components/Marathon/SignUp/MarathonForm.jsx index 7850dbbe..d9ab97eb 100644 --- a/components/Marathon/SignUp/MarathonForm.jsx +++ b/components/Marathon/SignUp/MarathonForm.jsx @@ -76,7 +76,6 @@ export default function MarathonForm({ const reduxDispatch = useDispatch(); const [hasLoaded, setHasLoaded] = useState(false); const [errors, setErrors] = useState({}); - const [hasErrors, setHasErrors] = useState(false); const marathonState = useSelector((state) => { return state.marathon; }); const localStorgeStored = window.localStorage.getItem('newMarathon'); const editingMarathon = localStorgeStored ? JSON.parse(localStorgeStored) : null; @@ -87,20 +86,6 @@ export default function MarathonForm({ }; const [newMarathon, setNewMarathon] = useReducer(marathonFormReducer, initialState()); - const onNextStep = () => { - if (hasErrors) { - toast.error('請修正錯誤'); - } else { - reduxDispatch(updateNewMarathon(newMarathon)); - setCurrentStep(currentStep + 1); - } - }; - - const onPrevStep = () => { - reduxDispatch(updateNewMarathon(newMarathon)); - setCurrentStep(currentStep - 1); - }; - const validators = { required: (value) => { return value.trim().length > 0; @@ -110,50 +95,74 @@ export default function MarathonForm({ return names.length === milestonesLength; } }; - const validateRules = { + + const marathonDataMap = { title: { + dispatchType: 'UPDATE_FIELD', + dispatchKey: 'title', validate: validators.required, message: '請填寫表格', }, description: { + dispatchType: 'UPDATE_FIELD', + dispatchKey: 'description', validate: validators.required, message: '請填寫計畫敘述', }, motivationDescription: { + dispatchType: 'UPDATE_MOTIVATION_FIELD', + dispatchKey: 'description', validate: validators.required, message: '請填寫學習動機', }, outcomesDescription: { + dispatchType: 'UPDATE_OUTCOMES_FIELD', + dispatchKey: 'description', validate: validators.required, message: '請填寫學習成果', }, goals: { + dispatchType: 'UPDATE_FIELD', + dispatchKey: 'goals', validate: validators.required, message: '請填寫學習目標', }, content: { + dispatchType: 'UPDATE_FIELD', + dispatchKey: 'content', validate: validators.required, message: '請填寫學習內容', }, milestonesName: { + dispatchType: 'UPDATE_FIELD', + dispatchKey: 'milestones', validate: (value) => { return validators.allMilestonesNameRequired(value, newMarathon.milestones?.length); }, message: '請填寫每週/隔週里程碑目標', }, strategiesDescription: { + dispatchType: 'UPDATE_STRATEGIES_FIELD', + dispatchKey: 'description', validate: validators.required, message: '請填寫學習策略', }, resources: { + dispatchType: 'UPDATE_FIELD', + dispatchKey: 'resources', validate: validators.required, message: '請填寫學習資源' } }; + /** + * @param {string} name - The name of the field to validate. + * @param {*} input - The input value to validate. + * @returns {boolean} - Returns true if the input value passes validation, otherwise false. + */ const handleValidate = (name, input) => { - const validateResult = validateRules[name]?.validate(input); - const errorMessage = validateRules[name]?.message; + const validateResult = marathonDataMap[name]?.validate(input); + const errorMessage = marathonDataMap[name]?.message; if (validateResult) { setErrors((prevErrors) => Object.fromEntries(Object.entries(prevErrors).filter(([key]) => key !== name)) @@ -168,22 +177,75 @@ export default function MarathonForm({ } return validateResult; }; + const handleValidateAll = () => { + const newErrors = {}; + let isValid = true; + Object.entries(marathonDataMap).forEach(([name, fieldData]) => { + const { validate, message } = fieldData; + let input; + if (validate) { + switch (name) { + case 'milestonesName': + input = newMarathon.milestones; + break; + case 'motivationDescription': + input = newMarathon.motivation?.description; + break; + case 'strategiesDescription': + input = newMarathon.strategies?.description; + break; + case 'outcomesDescription': + input = newMarathon.outcomes?.description; + break; + default: + input = newMarathon[name]; + break; + } + const validationPassed = validate(input); + + if (!validationPassed) { + newErrors[name] = { message: message || "驗證失敗" }; + isValid = false; + } + } + }); + setErrors(newErrors); + return isValid; + }; const handleOnChange = ( - stateDispatchType, - stateDispatchKey, + name, value, - validateName, ) => { - if (stateDispatchType && stateDispatchKey) { + const type = marathonDataMap[name]?.dispatchType; + const key = marathonDataMap[name]?.dispatchKey; + + if (type && key) { setNewMarathon({ - type: stateDispatchType, - payload: { key: stateDispatchKey, value } + type, + payload: { key, value } }); } - if (validateName) { - handleValidate(validateName, value); + + if (name) { + handleValidate(name, value); + } + }; + + const onNextStep = () => { + const isValid = handleValidateAll(); + if (!isValid) { + toast.error('請修正錯誤'); + } else { + reduxDispatch(updateNewMarathon(newMarathon)); + setCurrentStep(currentStep + 1); } }; + + const onPrevStep = () => { + reduxDispatch(updateNewMarathon(newMarathon)); + setCurrentStep(currentStep - 1); + }; + useEffect(() => { setHasLoaded(true); const storagedErrors = getMarathonErrorsStorage().get(); @@ -200,11 +262,6 @@ export default function MarathonForm({ useEffect(() => { getMarathonErrorsStorage().set(errors); - if (Object.keys(errors).length) { - setHasErrors(true); - } else { - setHasErrors(false); - } }, [errors]); return ( @@ -245,7 +302,7 @@ export default function MarathonForm({ title="學習主題名稱" value={newMarathon.title || ''} onChange={(e) => { - handleOnChange('UPDATE_FIELD', 'title', e.target.value, 'title'); + handleOnChange('title', e.target.value); }} sx={{ mb: '8px', @@ -278,7 +335,7 @@ export default function MarathonForm({ { - handleOnChange('UPDATE_FIELD', 'description', e.target.value, 'description'); + handleOnChange('description', e.target.value); }} placeholder="範例:因為對剪影片和當 Youtuber 有興趣,我預計會研究搞笑型 Youtuber 的影片腳本與剪輯方式、拍攝我日常生活及練習剪輯,並建立 Youtube 頻道上傳影片。希望能藉此了解如何當一位 Youtuber。" className={errors.description ? 'error' : ''} @@ -323,7 +380,7 @@ export default function MarathonForm({ '生活發生變化', '影響社會', '受群體影響', - '其他:請在下方補上其他原因,並詳細說明動機' + '其他:請於下方撰寫' ]} type="UPDATE_MOTIVATION_FIELD" onChange={setNewMarathon} @@ -331,7 +388,7 @@ export default function MarathonForm({ /> { - handleOnChange('UPDATE_MOTIVATION_FIELD', 'description', e.target.value, 'motivationDescription'); + handleOnChange('motivationDescription', e.target.value); }} className={errors.motivationDescription ? 'error' : ''} value={newMarathon?.motivation?.description || ''} @@ -360,7 +417,7 @@ export default function MarathonForm({ { - handleOnChange('UPDATE_FIELD', 'goals', e.target.value, 'goals'); + handleOnChange('goals', e.target.value); }} value={newMarathon.goals || ''} placeholder="範例: @@ -391,7 +448,7 @@ export default function MarathonForm({ { - handleOnChange('UPDATE_FIELD', 'content', e.target.value, 'content'); + handleOnChange('content', e.target.value); }} value={newMarathon.content || ''} placeholder="範例: @@ -442,7 +499,7 @@ export default function MarathonForm({ "田野調查", "訪談", "問卷調查", - "其他:請在下方補上其他原因,並詳細說明動機" + "其他:請於下方撰寫" ]} type="UPDATE_STRATEGIES_FIELD" onChange={setNewMarathon} @@ -450,7 +507,7 @@ export default function MarathonForm({ /> { - handleOnChange('UPDATE_STRATEGIES_FIELD', 'description', e.target.value, 'strategiesDescription'); + handleOnChange('strategiesDescription', e.target.value); }} value={newMarathon?.strategies?.description || ''} placeholder="範例:我預計會研究影片腳本、拍攝與剪輯方式,接著了解拍攝、剪輯與Youtube頻道經營,並同時練習拍攝與剪輯,開始經營頻道。我會用notion整理我收集到的資料以及筆記。" @@ -483,7 +540,7 @@ export default function MarathonForm({ placeholder="範例:YouTube 創作者的實用資源" value={newMarathon.resources || ''} onChange={(e) => { - handleOnChange('UPDATE_FIELD', 'resources', e.target.value, 'resources'); + handleOnChange('resources', e.target.value); }} className={errors.resources ? 'error' : 'warning'} endAdornment={errors.resources ? : null} @@ -543,7 +600,7 @@ export default function MarathonForm({ "舉辦活動", "開課", "參與競賽", - "其他:請在下方補上其他原因,並詳細說明動機" + "其他:請於下方撰寫" ]} type="UPDATE_OUTCOMES_FIELD" onChange={setNewMarathon} @@ -551,7 +608,7 @@ export default function MarathonForm({ /> { - handleOnChange('UPDATE_OUTCOMES_FIELD', 'description', e.target.value, 'outcomesDescription'); + handleOnChange('outcomesDescription', e.target.value); }} value={newMarathon?.outcomes?.description || ''} placeholder="範例:我預計會架設一個Youtube頻道,並上傳至少5支影片,並整理觀眾回饋與相關數據。" diff --git a/components/Marathon/SignUp/MilestoneGroup.jsx b/components/Marathon/SignUp/MilestoneGroup.jsx index e689c4e2..e60149a6 100644 --- a/components/Marathon/SignUp/MilestoneGroup.jsx +++ b/components/Marathon/SignUp/MilestoneGroup.jsx @@ -103,7 +103,7 @@ export default function MilestoneGroup({ // if change frequency, clear all data setFrequency(e.target.value); const changedMilestones = calculateMilestones(startDate, e.target.value, []); - onChangeHandler('UPDATE_FIELD', 'milestones', changedMilestones, 'milestonesName'); + onChangeHandler('milestonesName', changedMilestones); }; const handleEndDate = (/** fakeDate */) => { @@ -115,7 +115,7 @@ export default function MilestoneGroup({ const changedMilestones = milestones.map((item) => { return (item._tempId === newMilestone._tempId ? newMilestone : item); }); - onChangeHandler('UPDATE_FIELD', 'milestones', changedMilestones, 'milestonesName'); + onChangeHandler('milestonesName', changedMilestones); }; useEffect(() => { const weeklyMilestonesLength = 22; @@ -132,7 +132,7 @@ export default function MilestoneGroup({ } if (!isDisabled) { - onChangeHandler('UPDATE_FIELD', 'milestones', initMilestones, 'milestonesName'); + onChangeHandler('milestonesName', initMilestones); } }, []); diff --git a/components/Marathon/SignUp/StepperBar.jsx b/components/Marathon/SignUp/StepperBar.jsx index 374badb0..9f2c9822 100644 --- a/components/Marathon/SignUp/StepperBar.jsx +++ b/components/Marathon/SignUp/StepperBar.jsx @@ -3,8 +3,9 @@ import { Box } from '@mui/material'; import Stepper from '@mui/material/Stepper'; import Step from '@mui/material/Step'; import StepLabel from '@mui/material/StepLabel'; +import { useNavigation } from '@/contexts/Navigation'; -export const StyledSaveBar = styled(Box)` +export const StyledStepperBar = styled(Box)` background-color: #FFF; padding: 15px 6.9vw; display: flex; @@ -16,7 +17,7 @@ export const StyledSaveBar = styled(Box)` box-shadow: 0px 2px 4px -1px rgba(0,0,0,0.2),0px 4px 5px 0px rgba(0,0,0,0.14),0px 1px 10px 0px rgba(0,0,0,0.12); position: sticky; z-index: 99; - top: 118px; + top: ${(props) => (props.showPromotionBar ? '108px' : '64px')}; width: 100%; left: 0; @@ -58,9 +59,11 @@ export const StyledSaveBar = styled(Box)` } `; -export default function SaveBar({ currentStep }) { +export default function StepperBar({ currentStep }) { + const { showPromotionBar } = useNavigation(); + return ( - +

申請參加學習馬拉松

@@ -77,6 +80,6 @@ export default function SaveBar({ currentStep }) { -
+ ); } diff --git a/contexts/Navigation.jsx b/contexts/Navigation.jsx new file mode 100644 index 00000000..bf5f0ca4 --- /dev/null +++ b/contexts/Navigation.jsx @@ -0,0 +1,30 @@ +import { useEffect, useState, useContext, createContext } from 'react'; +import { usePromotion } from './Promotion'; + +const NavigationContext = createContext(); + +export const useNavigation = () => { + const context = useContext(NavigationContext); + if (!context) { + throw new Error('useNavigation must be used within an NavigationProvider'); + } + return context; +}; + +export const NavigationProvider = ({ children }) => { + const { showPromotionBar, setShowPromotionBar } = usePromotion(); + const [headerHeight, setHeaderHeight] = useState('128px'); + useEffect(() => { + setHeaderHeight(showPromotionBar ? '128px' : '64px'); + }, [showPromotionBar]); + return ( + + {children} + + ); +}; diff --git a/contexts/Promotion.jsx b/contexts/Promotion.jsx new file mode 100644 index 00000000..7b304821 --- /dev/null +++ b/contexts/Promotion.jsx @@ -0,0 +1,24 @@ +import { useState, useContext, createContext } from 'react'; + +const PromotionContext = createContext(); + +export const usePromotion = () => { + const context = useContext(PromotionContext); + if (!context) { + throw new Error('usePromotion must be used within an NavigationProvider'); + } + return context; +}; + +export const PromotionProvider = ({ children }) => { + const [showPromotionBar, setShowPromotionBar] = useState(true); + return ( + + {children} + + ); +}; diff --git a/layout/DefaultLayout.jsx b/layout/DefaultLayout.jsx index 37894a47..c229b6ea 100644 --- a/layout/DefaultLayout.jsx +++ b/layout/DefaultLayout.jsx @@ -1,12 +1,18 @@ import React from 'react'; import Navigation from '@/shared/components/Navigation_v2'; import Footer from '@/shared/components/Footer_v2'; +import { NavigationProvider } from '@/contexts/Navigation'; +import { PromotionProvider } from '@/contexts/Promotion'; const DefaultLayout = ({ children }) => { return ( <> - - {children} + + + + + {children} +