diff --git a/components/Marathon/Mentors.jsx b/components/Marathon/Mentors.jsx index fcf1cbac..1504e43b 100644 --- a/components/Marathon/Mentors.jsx +++ b/components/Marathon/Mentors.jsx @@ -3,7 +3,7 @@ import { CiCircleChevRight, CiCircleChevLeft } from "react-icons/ci"; import { IoClose } from "react-icons/io5"; import { FaLinkedin, FaMedium, FaResearchgate, FaSquareFacebook, FaSquareThreads } from "react-icons/fa6"; import { IconButton } from '@mui/material'; - +import EastIcon from '@mui/icons-material/East'; import Image from '@/shared/components/Image'; import Modal from '@/shared/components/Modal'; import { cn } from '@/utils/cn'; @@ -303,11 +303,11 @@ const Mentors = () => { <MentorCard key={mentor.name} mentor={mentor} - className="cursor-pointer" + className="cursor-pointer group" onClick={() => handleOpenModal(mentor.name)} > - <div className="absolute bottom-0 left-0 right-0 p-4"> - <div className="flex gap-2"> + <div className="absolute bottom-0 left-0 right-0 pt-4"> + <div className="flex gap-2 px-3"> {mentor.tags.slice(0, 1).map((tag, index) => ( <Tag key={tag} @@ -316,7 +316,8 @@ const Mentors = () => { /> ))} </div> - <div className="heading-md text-white text-start mt-2">{mentor.title} | {mentor.name}</div> + <div className="heading-md text-white text-start mt-2 px-3 pb-3">{mentor.title} | {mentor.name}</div> + <div className="bg-white flex justify-end items-center text-gray-400 px-3 py-2 gap-1 group-hover:text-primary-base">more <EastIcon className="!text-[16px]" /></div> </div> </MentorCard> ))} diff --git a/components/Marathon/SignUp/Edit.styled.jsx b/components/Marathon/SignUp/Edit.styled.jsx index c80bf943..338ac25a 100644 --- a/components/Marathon/SignUp/Edit.styled.jsx +++ b/components/Marathon/SignUp/Edit.styled.jsx @@ -80,6 +80,9 @@ export const StyledSection = styled(Box)` border-radius: 16px; border: 1px solid #DBDBDB; + &.error { + border-color: #EF5364; + } @media (max-width: 767px) { padding: 32px 16px; @@ -187,6 +190,12 @@ export const StyledInputBase = styled(InputBase)` border-width: 1px; padding: 12px 16px; } + + &.error { + border-color: #EF5364; + outline-color: #EF5364; + position: relative; + } `; export const StyledTextareaAutosize = styled(TextareaAutosize)` width: 100%; @@ -200,10 +209,20 @@ export const StyledTextareaAutosize = styled(TextareaAutosize)` border: 2px solid #16B9B3; padding: 11px 15px; outline-color: #16B9B3; + + &.error { + border-color: #EF5364; + outline-color: #EF5364; + } } .MuiInputBase-input { padding: 0; line-height: 140%; } + + &.error { + border-color: #EF5364; + outline-color: #16B9B3; + } `; diff --git a/components/Marathon/SignUp/EditSubMilestone.jsx b/components/Marathon/SignUp/EditSubMilestone.jsx index acff0f83..87d02fd6 100644 --- a/components/Marathon/SignUp/EditSubMilestone.jsx +++ b/components/Marathon/SignUp/EditSubMilestone.jsx @@ -3,7 +3,6 @@ import styled from "@emotion/styled"; import { Typography, Box, - Grid, IconButton, MenuItem, Select, @@ -33,6 +32,7 @@ const FixedLabel = styled(Typography)` width: 20px; flex-shrink: 0; `; + const StyledContainer = styled(Box)` display: flex; flex-direction: row; @@ -40,6 +40,24 @@ const StyledContainer = styled(Box)` align-items: center; gap: 10px; width: 100%; + backgroundColor: #FFF; + padding: 12px 16px; + border-radius: 8px; + border: 1px solid #DBDBDB; + + @media (max-width: 767px) { + display: grid; + grid-template-areas: + "content buttons" + "date date"; + grid-template-columns: 1fr auto; + grid-template-rows: auto auto; + } + + &:focus-within { + border: 1px solid #16B9B3; + padding: 12px 16px; + } .content { flex-grow: 1; @@ -48,6 +66,11 @@ const StyledContainer = styled(Box)` align-items: center; justify-content: flex-start; gap: 10px; + grid-area: content; + } + .weekdaySelector { + grid-area: date; + } .buttons { @@ -56,29 +79,7 @@ const StyledContainer = styled(Box)` align-items: center; justify-content: center; gap: 10px; - } -`; - -const StyledButtonGroup = styled(Box)` - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - gap: 10px; -`; -const StyledGridItem = styled(Grid)` - background-color: #FFF; - display: flex; - padding: 12px 16px; - flex-direction: column; - align-items: center; - gap: 8px; - align-self: stretch; - border-radius: 8px; - - &:focus-within { - border: 1px solid #16B9B3; - padding: 11px 15px; + grid-area: buttons; } .title { @@ -88,7 +89,12 @@ const StyledGridItem = styled(Grid)` width: 100%; justify-content: space-between; flex-wrap: nowrap; - + grid-area: title; + gap: 4px; + span { + margin-right: 4px; + flex-shrink: 0; + } p { color: #293A3D; font-size: 14px; @@ -96,8 +102,17 @@ const StyledGridItem = styled(Grid)` font-weight: 400; line-height: 140%; } - } + } `; + +const StyledButtonGroup = styled(Box)` + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + gap: 10px; +`; + const StyledWeekdaySelector = styled(Select)` font-size: 12px; font-style: normal; @@ -109,7 +124,6 @@ const StyledWeekdaySelector = styled(Select)` justify-content: flex-start; width: 100%; max-width: 150px; - min-width: 56px; gap: 8px; padding: 0 0 0 0; height: 100%; @@ -128,6 +142,16 @@ const StyledWeekdaySelector = styled(Select)` height: 16px; fill: #92989A; } + .MuiInputBase-input { + padding-right: 0 !important; + text-align: right; + } + @media (max-width: 767px) { + .MuiInputBase-input { + text-align: left; + max-width: 100%; + } + } `; const StyledInputBase = styled(InputBase)` width: 100%; @@ -247,8 +271,8 @@ export default function EditSubMilestone({ }; return ( - <StyledGridItem item xs={12}> - <StyledContainer> + <StyledContainer> + <Box className="content"> <FixedLabel component="span">{`${index + 1}.`}</FixedLabel> <StyledInputBase placeholder="任務名稱" @@ -257,81 +281,82 @@ export default function EditSubMilestone({ value={newMilestone.name || ''} notched="false" /> - - <StyledButtonGroup> - <StyledWeekdaySelector - multiple - placeholder="自訂" - displayEmpty - value={newMilestone.dates} - onChange={handleChangeWeekdays} - input={( - <InputBase placeholder="自訂" startAdornment={(<CalendarTodayOutlinedIcon />)} /> - )} - renderValue={ - (selected) => - selected?.length ? selected - .map((ISODate) => ISOToWeekday(ISODate)) - .filter(Boolean) - .join(", ") : '自訂' - } - sx={{ - '.MuiSelect-icon': { - display: 'none', + </Box> + <Box className="weekdaySelector"> + <StyledWeekdaySelector + multiple + placeholder="自訂" + displayEmpty + value={newMilestone.dates} + onChange={handleChangeWeekdays} + input={( + <InputBase placeholder="自訂" startAdornment={(<CalendarTodayOutlinedIcon />)} /> + )} + renderValue={ + (selected) => + selected?.length ? selected + .map((ISODate) => ISOToWeekday(ISODate)) + .filter(Boolean) + .join(", ") : '自訂' + } + sx={{ + '.MuiSelect-icon': { + display: 'none', + }, + }} + MenuProps={{ + PaperProps: { + style: { + padding: '12px', + maxHeight: 150, + overflowY: 'auto', + scrollbarWidth: 'thin', + maxWidth: '140px' }, - }} - MenuProps={{ - PaperProps: { - style: { - padding: '12px', - maxHeight: 150, - overflowY: 'auto', - scrollbarWidth: 'thin', - maxWidth: '140px' - }, - }, - MenuListProps: { - style: { - padding: '0' - } + }, + MenuListProps: { + style: { + padding: '0' } - }} - > - {ZH_WEEK_DAY_MAP.map((zhDay) => { - const isSelected = newMilestone.dates.includes(weekdayToISO(zhDay)); - return ( - <StyledMenuItem - key={zhDay} - value={weekdayToISO(zhDay)} - style={{ - backgroundColor: isSelected ? '#DEF5F5' : 'transparent', - margin: '0 0 4px', - color: '#293A3D', - }} - > - {zhDay} - </StyledMenuItem> - ); - })} - </StyledWeekdaySelector> + } + }} + > + {ZH_WEEK_DAY_MAP.map((zhDay) => { + const isSelected = newMilestone.dates.includes(weekdayToISO(zhDay)); + return ( + <StyledMenuItem + key={zhDay} + value={weekdayToISO(zhDay)} + style={{ + backgroundColor: isSelected ? '#DEF5F5' : 'transparent', + margin: '0 0 4px', + color: '#293A3D', + }} + > + {zhDay} + </StyledMenuItem> + ); + })} + </StyledWeekdaySelector> + </Box> - <StyledCancelButton - onClick={handleCloseEditPanel} - className="cancel" - aria-label="cancel" - > - <ClearIcon /> - </StyledCancelButton> + <StyledButtonGroup className="buttons"> + <StyledCancelButton + onClick={handleCloseEditPanel} + className="cancel" + aria-label="cancel" + > + <ClearIcon /> + </StyledCancelButton> - <StyledSubmitButton - onClick={handleClickSendButton} - className="submit" - aria-label="submit" - > - <SendIcon /> - </StyledSubmitButton> - </StyledButtonGroup> - </StyledContainer> - </StyledGridItem> + <StyledSubmitButton + onClick={handleClickSendButton} + className="submit" + aria-label="submit" + > + <SendIcon /> + </StyledSubmitButton> + </StyledButtonGroup> + </StyledContainer> ); } diff --git a/components/Marathon/SignUp/MarathonForm.jsx b/components/Marathon/SignUp/MarathonForm.jsx index 73be4431..9a7e43e7 100644 --- a/components/Marathon/SignUp/MarathonForm.jsx +++ b/components/Marathon/SignUp/MarathonForm.jsx @@ -1,9 +1,11 @@ import { useState, useEffect, useReducer } from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import toast from 'react-hot-toast'; import { updateNewMarathon } from '@/redux/actions/marathon'; import { initialState as reduxInitMarathonState } from '@/redux/reducers/marathon'; +import { getMarathonErrorsStorage } from '@/utils/storage'; import { Box, @@ -11,6 +13,7 @@ import { FormControlLabel, Checkbox, } from '@mui/material'; +import ClearIcon from '@mui/icons-material/Clear'; import MilestoneGroup from './MilestoneGroup'; import { @@ -72,7 +75,8 @@ 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; @@ -84,17 +88,95 @@ export default function MarathonForm({ const [newMarathon, setNewMarathon] = useReducer(marathonFormReducer, initialState()); const onNextStep = () => { - reduxDispatch(updateNewMarathon(newMarathon)); - setCurrentStep(currentStep + 1); + if (hasErrors) { + toast.error('請修正錯誤'); + } else { + reduxDispatch(updateNewMarathon(newMarathon)); + setCurrentStep(currentStep + 1); + } }; const onPrevStep = () => { reduxDispatch(updateNewMarathon(newMarathon)); setCurrentStep(currentStep - 1); }; + const handleValidate = (name, input, errorMessage) => { + 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; + } + + if (validate(input)) { + setErrors((prevErrors) => { + const { [name]: _, ...remainingErrors } = prevErrors; + return remainingErrors; + }); + } else { + setErrors({ + ...errors, + [name]: { + message: errorMessage || null + } + }); + } + return validate(input); + }; useEffect(() => { setHasLoaded(true); + const storagedErrors = getMarathonErrorsStorage().get(); + if (storagedErrors) { + setErrors(storagedErrors); + } }, []); useEffect(() => { @@ -103,9 +185,21 @@ export default function MarathonForm({ } }, [newMarathon]); + useEffect(() => { + getMarathonErrorsStorage().set(errors); + if (Object.keys(errors).length) { + setHasErrors(true); + } else { + setHasErrors(false); + } + }, [errors]); + return ( <> - <StyledSection> + <StyledSection className={ + (errors.title || errors.description || errors.motivationDescription || errors.goals || errors.strategiesDescription || errors.resources) ? 'error' : '' + } + > <Typography component="h2" sx={{ @@ -142,13 +236,27 @@ export default function MarathonForm({ type: 'UPDATE_FIELD', payload: { key: 'title', value: e.target.value } }); + handleValidate('title', e.target.value, '請填寫表格'); }} sx={{ mb: '8px', padding: '17px 16px 12px' }} + className={errors.title ? 'error' : ''} + endAdornment={errors.title ? <ClearIcon sx={{ color: '#EF5364' }} /> : null} placeholder="範例:成為一位Youtuber、半世紀以來的氣候變遷紀錄研究、開一間線上甜點店" /> + {errors.title && ( + <Typography sx={{ + color: '#EF5364', + marginTop: '8px', + fontSize: '14px', + fontWeight: 400 + }} + > + {errors.title?.message} + </Typography> + )} <StyledGroup> <Typography sx={{ fontWeight: 500, mb: '8px' }}> 計畫簡述 * @@ -168,9 +276,22 @@ export default function MarathonForm({ value: e.target.value } }); + handleValidate('description', e.target.value, '請填寫計畫簡述'); }} placeholder="範例:因為對剪影片和當 Youtuber 有興趣,我預計會研究搞笑型 Youtuber 的影片腳本與剪輯方式、拍攝我日常生活及練習剪輯,並建立 Youtube 頻道上傳影片。希望能藉此了解如何當一位 Youtuber。" + className={errors.description ? 'error' : ''} /> + {errors.description && ( + <Typography sx={{ + color: '#EF5364', + marginTop: '8px', + fontSize: '14px', + fontWeight: 400 + }} + > + {errors.description?.message} + </Typography> + )} </StyledGroup> <StyledGroup> <Typography sx={{ fontWeight: 500, mb: '8px' }}> @@ -215,10 +336,23 @@ export default function MarathonForm({ value: e.target.value } }); + handleValidate('motivationDescription', e.target.value, '請填寫學習動機'); }} + className={errors.motivationDescription ? 'error' : ''} value={newMarathon?.motivation?.description || ''} placeholder="範例:因為同學常常說我很好笑,很適合把生活日常做成影片,我也發現自己對做影片、當Youtuber有興趣,所以想要嘗試累積作品,並開一個 Youtuber 頻道。" /> + {errors.motivationDescription && ( + <Typography sx={{ + color: '#EF5364', + marginTop: '8px', + fontSize: '14px', + fontWeight: 400 + }} + > + {errors.motivationDescription?.message} + </Typography> + )} </StyledGroup> <StyledGroup> <Typography sx={{ fontWeight: 500, mb: '8px' }}> @@ -238,12 +372,25 @@ export default function MarathonForm({ value: e.target.value } }); + handleValidate('goals', e.target.value, '請填寫學習目標'); }} value={newMarathon.goals || ''} placeholder="範例: - 能收集並分析搞笑風格的 Youtuber - 能拍攝畫面穩定、清晰且具專業感的影片" + className={errors.goals ? 'error' : ''} /> + {errors.goals && ( + <Typography sx={{ + color: '#EF5364', + marginTop: '8px', + fontSize: '14px', + fontWeight: 400 + }} + > + {errors.goals?.message} + </Typography> + )} </StyledGroup> <StyledGroup> <Typography sx={{ fontWeight: 500, mb: '8px' }}> @@ -263,13 +410,26 @@ export default function MarathonForm({ value: e.target.value } }); + handleValidate('content', e.target.value, '請填寫學習內容'); }} value={newMarathon.content || ''} placeholder="範例: - 內容規劃與創意發想(定位、主題、腳本) - 基礎拍攝技術(攝影設備、燈光、語音) - 影片剪輯與後製(剪輯軟體、配樂)" + className={errors.content ? 'error' : ''} /> + {errors.content && ( + <Typography sx={{ + color: '#EF5364', + marginTop: '8px', + fontSize: '14px', + fontWeight: 400 + }} + > + {errors.content?.message} + </Typography> + )} </StyledGroup> <StyledGroup> <Typography sx={{ fontWeight: 500, mb: '8px' }}> @@ -316,10 +476,23 @@ export default function MarathonForm({ value: e.target.value } }); + handleValidate('strategiesDescription', e.target.value, '請填寫學習方法與策略'); }} value={newMarathon?.strategies?.description || ''} placeholder="範例:我預計會研究影片腳本、拍攝與剪輯方式,接著了解拍攝、剪輯與Youtube頻道經營,並同時練習拍攝與剪輯,開始經營頻道。我會用notion整理我收集到的資料以及筆記。" + className={errors.strategiesDescription ? 'error' : ''} /> + {errors.strategiesDescription && ( + <Typography sx={{ + color: '#EF5364', + marginTop: '8px', + fontSize: '14px', + fontWeight: 400 + }} + > + {errors.strategiesDescription?.message} + </Typography> + )} </StyledGroup> <StyledGroup> <Typography sx={{ fontWeight: 500, mb: '8px' }}> @@ -335,28 +508,52 @@ export default function MarathonForm({ sx={{ width: '100%' }} placeholder="範例:YouTube 創作者的實用資源" value={newMarathon.resources || ''} - onChange={(e) => setNewMarathon({ - type: 'UPDATE_FIELD', - payload: { - key: 'resources', - value: e.target.value - } - })} + onChange={(e) => { + setNewMarathon({ + type: 'UPDATE_FIELD', + payload: { + key: 'resources', + value: e.target.value + } + }); + handleValidate('resources', e.target.value, '請填寫學習資源'); + }} + className={errors.resources ? 'error' : 'warning'} + endAdornment={errors.resources ? <ClearIcon sx={{ color: '#EF5364' }} /> : null} /> + {errors.resources && ( + <Typography sx={{ + color: '#EF5364', + marginTop: '8px', + fontSize: '14px', + fontWeight: 400 + }} + > + {errors.resources?.message} + </Typography> + )} </StyledGroup> </Box> </StyledSection> - <StyledSection sx={{ mt: '16px' }}> + <StyledSection + sx={{ mt: '16px' }} + className={errors.milestonesName ? 'error' : ''} + > <Box> <StyledGroup> <MilestoneGroup milestones={newMarathon.milestones} onChange={setNewMarathon} + onValidate={handleValidate} + errorMessage={errors.milestonesName?.message} /> </StyledGroup> </Box> </StyledSection> - <StyledSection sx={{ mt: '16px' }}> + <StyledSection + sx={{ mt: '16px' }} + className={errors.outcomesDescription ? 'error' : ''} + > <Typography component="h3" sx={{ fontWeight: 500, mb: '8px' }}> 學習成果及呈現方式 * </Typography> @@ -395,10 +592,25 @@ export default function MarathonForm({ value: e.target.value } }); + handleValidate('outcomesDescription', e.target.value, '請填寫學習成果'); }} value={newMarathon?.outcomes?.description || ''} placeholder="範例:我預計會架設一個Youtube頻道,並上傳至少5支影片,並整理觀眾回饋與相關數據。" + className={errors.outcomesDescription ? 'error' : ''} /> + {errors.outcomesDescription && ( + <Typography + component="p" + sx={{ + color: '#EF5364', + marginTop: '8px', + fontSize: '14px', + fontWeight: 400 + }} + > + {errors.outcomesDescription?.message} + </Typography> + )} <FormControlLabel label="是否公開給所有人看到 (馬拉松開始後才可以在活動網站上看到喔~)" sx={{ diff --git a/components/Marathon/SignUp/MilestoneGroup.jsx b/components/Marathon/SignUp/MilestoneGroup.jsx index a7a29147..cb16cb07 100644 --- a/components/Marathon/SignUp/MilestoneGroup.jsx +++ b/components/Marathon/SignUp/MilestoneGroup.jsx @@ -1,5 +1,6 @@ import { v4 as uuidv4 } from 'uuid'; import { useState, useEffect } from "react"; +import styled from '@emotion/styled'; import { Box, Grid, @@ -13,11 +14,37 @@ import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { StyledGroup } from "./Edit.styled"; import MilestonePanel from "./MilestonePanel"; +import ErrorMessage from './ErrorMessage'; + +const StyledDateSection = styled(Box)` + display: flex; + justify-content: flex-start; + align-items: flex-start; + width: 100%; + gap: 10px; + + .startDate, .endDate, .frequency { + flex-shrink: 0; + width: 25%; + } + + @media (max-width: 767px) { + flex-wrap: wrap; + .startDate, .endDate { + width: calc(50% - 5px); + } + .frequency { + width: 100%; + } + } +`; export default function MilestoneGroup({ milestones = [], onChange = null, - isDisabled = false + isDisabled = false, + onValidate = null, + errorMessage = null }) { const eventWeekRange = 22; const [startDate, setStartDate] = useState('2025-02-09'); @@ -103,11 +130,13 @@ export default function MilestoneGroup({ const eventEndDate = dayjs(startDate).add(eventWeekRange, 'week'); setEndDate(eventEndDate); }; + const updateMilestone = (newMilestone) => { const changedMilestones = milestones.map((item, _i) => { return (item._tempId === newMilestone._tempId ? newMilestone : item); }); - + // check if milestone name exist + onValidate('milestonesName', changedMilestones, '請填寫每週 / 隔週里程碑目標'); onChange({ type: 'UPDATE_FIELD', payload: { @@ -154,77 +183,75 @@ export default function MilestoneGroup({ <Box sx={{ padding: '8px 0', width: '100%' }}> <LocalizationProvider dateAdapter={AdapterDayjs}> <StyledGroup> - <Grid container alignItems="center" columnSpacing={1}> - <Grid item xs={4}> - <DatePicker - label="開始日期" - value="2025-02-09" - inputFormat="YYYY-MM-DD" - disabled - renderInput={(params) => ( - <TextField - {...params} - InputLabelProps={{ - shrink: true, - }} - sx={{ - height: '50px', - '& .MuiInputBase-root': { - height: '100%', - } - }} - /> - )} - /> - </Grid> - <Grid item xs={4}> - <DatePicker - label="結束日期" - value="2025-07-12" - inputFormat="YYYY-MM-DD" - disabled - onChange={handleEndDate} - renderInput={(params) => ( - <TextField - {...params} - InputLabelProps={{ - shrink: true, - }} - sx={{ - height: '50px', - '& .MuiInputBase-root': { - height: '100%', - }, - '& .MuiFormHelperText-root': { - marginTop: '4px', - }, - }} - /> - )} - /> - </Grid> - <Grid item xs={3}> - <TextField - select - label="頻率" - defaultValue="weekly" - value={frequency} - onChange={handleFrequency} - variant="outlined" - fullWidth - disabled={isDisabled} - sx={{ - height: '50px', - '& .MuiInputBase-root': { - height: '100%', - } - }} - > - <MenuItem value="weekly">每週</MenuItem> - <MenuItem value="biweekly">每兩週</MenuItem> - </TextField> - </Grid> - </Grid> + <StyledDateSection> + <DatePicker + className="startDate" + label="開始日期" + value="2025-02-09" + inputFormat="YYYY-MM-DD" + disabled + renderInput={(params) => ( + <TextField + {...params} + InputLabelProps={{ + shrink: true, + }} + sx={{ + height: '50px', + '& .MuiInputBase-root': { + height: '100%', + } + }} + /> + )} + /> + + <DatePicker + className="endDate" + label="結束日期" + value="2025-07-12" + inputFormat="YYYY-MM-DD" + disabled + onChange={handleEndDate} + renderInput={(params) => ( + <TextField + {...params} + InputLabelProps={{ + shrink: true, + }} + sx={{ + height: '50px', + '& .MuiInputBase-root': { + height: '100%', + }, + '& .MuiFormHelperText-root': { + marginTop: '4px', + }, + }} + /> + )} + /> + <TextField + className="frequency" + select + label="頻率" + defaultValue="weekly" + value={frequency} + onChange={handleFrequency} + variant="outlined" + fullWidth + disabled={isDisabled} + sx={{ + height: '50px', + '& .MuiInputBase-root': { + height: '100%', + } + }} + > + <MenuItem value="weekly">每週</MenuItem> + <MenuItem value="biweekly">每兩週</MenuItem> + </TextField> + </StyledDateSection> </StyledGroup> <StyledGroup> {milestones.map((milestone, i) => { @@ -246,6 +273,9 @@ export default function MilestoneGroup({ })} </StyledGroup> </LocalizationProvider> + <ErrorMessage + errText={errorMessage || null} + /> </Box> </> ); diff --git a/components/Marathon/SignUp/SaveBar.jsx b/components/Marathon/SignUp/SaveBar.jsx index 0dbdde07..374badb0 100644 --- a/components/Marathon/SignUp/SaveBar.jsx +++ b/components/Marathon/SignUp/SaveBar.jsx @@ -1,4 +1,3 @@ -import { useState } from 'react'; import styled from '@emotion/styled'; import { Box } from '@mui/material'; import Stepper from '@mui/material/Stepper'; @@ -43,14 +42,13 @@ export const StyledSaveBar = styled(Box)` } .MuiStepLabel-iconContainer { - - .MuiStepIcon-text { fill: #FFF; } } @media (max-width: 767px) { + padding: 8px 6.9vw; .top h2 { font-size: 18px; } diff --git a/utils/storage.js b/utils/storage.js index 9a6fba78..a6fdd1f9 100644 --- a/utils/storage.js +++ b/utils/storage.js @@ -20,3 +20,4 @@ export const getTokenStorage = () => createStorage('_token'); export const getRedirectionStorage = () => createStorage('_r'); export const getTrustWebsitesStorage = () => createStorage('_trustWeb'); export const getReminderStorage = () => createStorage('_reminder'); +export const getMarathonErrorsStorage = () => createStorage('_marathonFormErrors');