timetable-app

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit e810454772bd47c70222d3f4c3597074a2997e38
parent 061e875e85f02c0f4f76a5248531aefb47799d00
Author: Yuukin256 <52195426+Yuukin256@users.noreply.github.com>
Date:   Thu,  4 Apr 2024 23:25:35 +0900

日別クラス時間割表示の詳細表示を改良 (#30)

* 行事表示コンポーネントの命名を Events から Event に変更

* メモ機能の hook を作成

* 日別クラス時間割表示の詳細表示を変更
授業場所を表示し、メモ機能を詳細表示の中に移動

* 日別クラス時間割表示の詳細表示内に曜日時限の表示を追加

* 日別クラス時間割表示の各時限がボタンになっていることが分かるUIに変更

* 日別クラス時間割表示のスタイルを調整
Diffstat:
Msrc/client/features/daily/components/Lessons/BasisChip.tsx | 2+-
Asrc/client/features/daily/components/Lessons/DetailDialog.tsx | 113+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/client/features/daily/components/Lessons/Event.tsx | 31+++++++++++++++++++++++++++++++
Dsrc/client/features/daily/components/Lessons/Events.tsx | 31-------------------------------
Msrc/client/features/daily/components/Lessons/Lesson.tsx | 129+++++++++++++++++++++++++++++++++++--------------------------------------------
Dsrc/client/features/daily/components/Lessons/Note.tsx | 134-------------------------------------------------------------------------------
Msrc/client/features/daily/components/Lessons/index.tsx | 10++++------
Msrc/client/hooks/useMySubject.ts | 5++---
Asrc/client/hooks/useNote.ts | 28++++++++++++++++++++++++++++
9 files changed, 236 insertions(+), 247 deletions(-)

diff --git a/src/client/features/daily/components/Lessons/BasisChip.tsx b/src/client/features/daily/components/Lessons/BasisChip.tsx @@ -31,5 +31,5 @@ export default function BasisChip({ data, date, period }: Props) { color = 'warning'; } - return <Chip label={label} size='small' color={color} sx={{ mb: '2px' }} />; + return <Chip label={label} size='small' color={color} />; } diff --git a/src/client/features/daily/components/Lessons/DetailDialog.tsx b/src/client/features/daily/components/Lessons/DetailDialog.tsx @@ -0,0 +1,113 @@ +import ClearIcon from '@mui/icons-material/Clear'; +import CloseIcon from '@mui/icons-material/Close'; +import { + Dialog, + DialogContent, + DialogTitle, + IconButton, + InputAdornment, + Stack, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + TextField, +} from '@mui/material'; + +import formatDate from 'common/utils/formatDate'; + +import type { Basis } from '../../types'; +import type { Course, SchoolTime } from 'common/types'; + +type Props = { + open: boolean; + handleClose: () => void; + date: Date; + period: SchoolTime; + courses: Course[]; + basis: Basis | null; + note: string; + setNote: (value: string) => void; +}; + +export default function DetailDialog({ + open, + handleClose, + date, + period, + courses, + basis, + note, + setNote, +}: Props) { + return ( + <Dialog open={open} onClose={handleClose}> + <DialogTitle marginRight={4}> + <div> + {formatDate(date, 'yyyy/MM/dd (EEEEE)')} {period}限 詳細 + </div> + <IconButton + aria-label='閉じる' + onClick={handleClose} + sx={{ + position: 'absolute', + right: 8, + top: 8, + color: (theme) => theme.vars.palette.grey[500], + }} + > + <CloseIcon /> + </IconButton> + </DialogTitle> + + <DialogContent> + <Stack spacing={2}> + {basis?.type === 'dayPeriod' && ( + <span>曜日時限: {basis.dayPeriod}</span> + )} + + <Table> + <TableHead> + <TableRow> + <TableCell>科目</TableCell> + <TableCell>場所</TableCell> + </TableRow> + </TableHead> + <TableBody> + {courses.map((course) => ( + <TableRow key={course.dayPeriod}> + <TableCell>{course.name}</TableCell> + <TableCell>{course.room}</TableCell> + </TableRow> + ))} + </TableBody> + </Table> + + <TextField + label='メモ' + placeholder='メモを入力' + value={note} + onChange={(e) => setNote(e.target.value)} + variant='standard' + fullWidth + multiline + InputProps={{ + endAdornment: ( + <InputAdornment position='end'> + <IconButton + aria-label='メモをクリア' + onClick={() => setNote('')} + edge='end' + > + <ClearIcon /> + </IconButton> + </InputAdornment> + ), + }} + /> + </Stack> + </DialogContent> + </Dialog> + ); +} diff --git a/src/client/features/daily/components/Lessons/Event.tsx b/src/client/features/daily/components/Lessons/Event.tsx @@ -0,0 +1,31 @@ +import EventNoteIcon from '@mui/icons-material/EventNote'; +import { Box, Divider, Stack, Typography } from '@mui/material'; + +type Props = { + event: string; +}; + +export function Event({ event }: Props) { + return ( + <> + <Stack + direction='row' + alignItems='center' + spacing={{ xs: 1, sm: 2 }} + px={0.5} + > + <EventNoteIcon /> + <Box width={1}> + <Typography + fontSize={{ xs: 20, sm: 24 }} + whiteSpace='nowrap' + component='span' + > + {event} + </Typography> + </Box> + </Stack> + <Divider sx={{ mb: 1 }} /> + </> + ); +} diff --git a/src/client/features/daily/components/Lessons/Events.tsx b/src/client/features/daily/components/Lessons/Events.tsx @@ -1,31 +0,0 @@ -import EventNoteIcon from '@mui/icons-material/EventNote'; -import { Box, Divider, Stack, Typography } from '@mui/material'; - -type Props = { - event: string; -}; - -export function Events({ event }: Props) { - return ( - <> - <Stack - direction='row' - alignItems='center' - spacing={{ xs: 1, sm: 2 }} - px={0.5} - > - <EventNoteIcon /> - <Box width={1}> - <Typography - fontSize={{ xs: 20, sm: 24 }} - whiteSpace='nowrap' - component='span' - > - {event} - </Typography> - </Box> - </Stack> - <Divider sx={{ mb: 1.5 }} /> - </> - ); -} diff --git a/src/client/features/daily/components/Lessons/Lesson.tsx b/src/client/features/daily/components/Lessons/Lesson.tsx @@ -1,37 +1,23 @@ import { useDisclosure } from '@mantine/hooks'; -import CloseIcon from '@mui/icons-material/Close'; -import { - Dialog, - DialogContent, - DialogTitle, - IconButton, - Stack, -} from '@mui/material'; -import { styled } from '@mui/material/styles'; +import CommentIcon from '@mui/icons-material/CommentOutlined'; +import { Button, Stack } from '@mui/material'; +import { alpha, styled } from '@mui/material/styles'; import { useAtomValue } from 'jotai'; import useMySubject from 'client/hooks/useMySubject'; -import formatDate from 'common/utils/formatDate'; +import useNote from 'client/hooks/useNote'; import { gradeClassAtom } from '../../atoms'; import BasisChip from './BasisChip'; -import Note from './Note'; +import DetailDialog from './DetailDialog'; import type { Basis } from '../../types'; import type { Course, DayPeriodJa, GradeClass, SchoolTime } from 'common/types'; -const UnorderedList = styled('ul')({ - margin: 0, - padding: 0, - listStyle: 'inside', - marginBlockEnd: 8, -}); - const ListItem = styled('li')(({ theme }) => ({ - marginBottom: theme.spacing(1.5), + marginBottom: theme.spacing(1), '&:last-child': { marginBottom: 0 }, - borderBottom: `solid 1px ${theme.vars.palette.divider}`, })); const PeriodSpan = styled('span')({ @@ -72,63 +58,62 @@ export default function Lesson({ date, period, courses, basis }: Props) { const gradeClass = useAtomValue(gradeClassAtom); const [open, handlers] = useDisclosure(false); - const names = courses.map((c) => c.name); const shortNames = courses.map((c) => c.shortName); + const [note, setNote] = useNote({ gradeClass, date, period }); + return ( <ListItem> - <Stack - direction='row' - alignItems='flex-end' - spacing={{ xs: 0.5, sm: 1 }} - overflow='clip' - px={{ xs: 0.5, sm: 1 }} + <Button + sx={(theme) => ({ + [theme.breakpoints.up('xs')]: { fontSize: 24 }, + [theme.breakpoints.up('sm')]: { fontSize: 28 }, + padding: 0, + textAlign: 'left', + fontWeight: 'unset', + color: 'unset', + backgroundColor: alpha(theme.palette.primary.main, 0.05), + width: '100%', + })} + onClick={handlers.open} > - <PeriodSpan>{period}.</PeriodSpan> - - <LessonSpan onClick={handlers.open}> - {gradeClass && basis?.type === 'dayPeriod' ? ( - <LessonNameWithMySubject - dayPeriod={basis.dayPeriod} - gradeClass={gradeClass} - subjects={shortNames} - /> - ) : ( - shortNames.join('/') - )} - </LessonSpan> - - <Dialog open={open} onClose={handlers.close}> - <DialogTitle marginRight={4}> - <div> - {formatDate(date, 'yyyy/MM/dd (EEEEE)')} {period}限 詳細 - </div> - <IconButton - aria-label='閉じる' - onClick={handlers.close} - sx={{ - position: 'absolute', - right: 8, - top: 8, - color: (theme) => theme.vars.palette.grey[500], - }} - > - <CloseIcon /> - </IconButton> - </DialogTitle> - <DialogContent> - <UnorderedList> - {names.map((s) => ( - <li key={s}>{s}</li> - ))} - </UnorderedList> - </DialogContent> - </Dialog> - - <Note date={date} period={period} /> - - {basis && <BasisChip data={basis} date={date} period={period} />} - </Stack> + <Stack + direction='row' + alignItems='center' + spacing={1} + overflow='clip' + px={{ xs: 1, sm: 2 }} + width='100%' + > + <PeriodSpan>{period}.</PeriodSpan> + <LessonSpan> + {gradeClass && basis?.type === 'dayPeriod' ? ( + <LessonNameWithMySubject + dayPeriod={basis.dayPeriod} + gradeClass={gradeClass} + subjects={shortNames} + /> + ) : ( + shortNames.join('/') + )} + </LessonSpan> + + {note !== '' && <CommentIcon fontSize='small' />} + + {basis && <BasisChip data={basis} date={date} period={period} />} + </Stack> + </Button> + + <DetailDialog + open={open} + handleClose={handlers.close} + date={date} + period={period} + courses={courses} + basis={basis} + note={note} + setNote={setNote} + /> </ListItem> ); } diff --git a/src/client/features/daily/components/Lessons/Note.tsx b/src/client/features/daily/components/Lessons/Note.tsx @@ -1,134 +0,0 @@ -import { useDisclosure, useLocalStorage } from '@mantine/hooks'; -import ClearIcon from '@mui/icons-material/Clear'; -import NoteAddIcon from '@mui/icons-material/NoteAdd'; -import { - Button, - Chip as MuiChip, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - IconButton, - InputAdornment, - TextField, -} from '@mui/material'; -import { styled } from '@mui/material/styles'; -import { useAtomValue } from 'jotai'; -import { useCallback, useRef } from 'react'; - -import { DATE_FORMAT } from 'common/constant'; -import formatDate from 'common/utils/formatDate'; - -import { gradeClassAtom } from '../../atoms'; - -import type { SchoolTime } from 'common/types'; - -const Chip = styled(MuiChip)` - margin-bottom: 2px; - max-width: 20%; -`; - -const getStorageKey = (gradeClass: string, date: Date, period: SchoolTime) => { - const dateString = formatDate(date, DATE_FORMAT); - return `note_data_${gradeClass}_${dateString}_${period}`; -}; - -type Props = { - date: Date; - period: SchoolTime; -}; - -export default function Note({ date, period }: Props) { - const [open, handlers] = useDisclosure(false); - const inputRef = useRef<HTMLInputElement | null>(null); - - const gradeClass = useAtomValue(gradeClassAtom); - - const [note, setNote, removeNote] = useLocalStorage<string>({ - key: getStorageKey(gradeClass ?? '', date, period), - defaultValue: '', - }); - - const noteExists = note !== ''; - - const handleClear = useCallback(() => { - if (inputRef.current) { - inputRef.current.value = ''; - } - }, []); - const handleSubmit = useCallback(() => { - if (inputRef.current === null || inputRef.current.value === '') { - removeNote(); - } else { - setNote(inputRef.current.value); - } - }, [removeNote, setNote]); - - return ( - <> - {noteExists ? ( - <Chip - label={note} - onClick={handlers.open} - aria-label='メモ' - size='small' - color='primary' - variant='outlined' - clickable - /> - ) : ( - <IconButton - aria-label='メモを追加' - onClick={handlers.open} - size='small' - sx={{ mb: '2px' }} - > - <NoteAddIcon fontSize='inherit' /> - </IconButton> - )} - - <Dialog open={open} onClose={handlers.close} fullWidth maxWidth='xs'> - <DialogTitle> - {formatDate(date, 'yyyy/MM/dd (EEEEE)')} {period}限 のメモ - </DialogTitle> - <DialogContent> - <TextField - aria-label='メモ' - placeholder='メモを入力' - defaultValue={note} - autoFocus={!noteExists} - inputRef={inputRef} - variant='standard' - margin='dense' - multiline - fullWidth - InputProps={{ - endAdornment: ( - <InputAdornment position='end'> - <IconButton - aria-label='メモをクリア' - onClick={handleClear} - edge='end' - > - <ClearIcon /> - </IconButton> - </InputAdornment> - ), - }} - /> - </DialogContent> - <DialogActions> - <Button onClick={handlers.close}>キャンセル</Button> - <Button - onClick={() => { - handleSubmit(); - handlers.close(); - }} - > - 確定 - </Button> - </DialogActions> - </Dialog> - </> - ); -} diff --git a/src/client/features/daily/components/Lessons/index.tsx b/src/client/features/daily/components/Lessons/index.tsx @@ -4,7 +4,7 @@ import { memo } from 'react'; import { SCHOOL_TIMES } from 'common/constant'; -import { Events } from './Events'; +import { Event } from './Event'; import Lesson from './Lesson'; import type { DayTimetable } from '../../types'; @@ -13,13 +13,11 @@ type Props = { timetable: DayTimetable; }; -const List = styled('ul')(({ theme }) => ({ +const List = styled('ul')({ margin: 0, padding: 0, listStyle: 'none', - [theme.breakpoints.up('xs')]: { fontSize: 24 }, - [theme.breakpoints.up('sm')]: { fontSize: 32 }, -})); +}); function Lessons({ timetable }: Props) { return ( @@ -35,7 +33,7 @@ function Lessons({ timetable }: Props) { })} component={Paper} > - {!!timetable.event && <Events event={timetable.event} />} + {!!timetable.event && <Event event={timetable.event} />} <List> {SCHOOL_TIMES.map((st) => { return ( diff --git a/src/client/hooks/useMySubject.ts b/src/client/hooks/useMySubject.ts @@ -16,16 +16,15 @@ type Params = { }; type Return = [ string | null, - (val: string | null | ((prevState: string) => string | null)) => void + (val: string | null | ((prevState: string) => string | null)) => void, ]; const useMySubject = ({ gradeClass, dayPeriod }: Params): Return => { const [value, setValue, removeValue] = useLocalStorage({ key: getStorageKey(gradeClass, dayPeriod), - defaultValue: '', }); const setOrRemoveValue = ( - val: string | null | ((prevState: string) => string | null) + val: string | null | ((prevState: string) => string | null), ) => { if (typeof val === 'function') { const newState = val(value); diff --git a/src/client/hooks/useNote.ts b/src/client/hooks/useNote.ts @@ -0,0 +1,28 @@ +import { useLocalStorage } from '@mantine/hooks'; + +import formatDate from 'common/utils/formatDate'; + +import type { GradeClass, SchoolTime } from 'common/types'; + +const getStorageKey = (gradeClass: string, date: Date, period: SchoolTime) => { + const dateString = formatDate(date); + return `note_data_${gradeClass}_${dateString}_${period}`; +}; + +type Params = { + gradeClass: GradeClass | null; + date: Date; + period: SchoolTime; +}; + +type Return = [string, (value: string) => void]; + +const useNote = ({ gradeClass, date, period }: Params): Return => { + const [note, setNote] = useLocalStorage<string | undefined>({ + key: getStorageKey(gradeClass ?? '', date, period), + getInitialValueInEffect: true, + }); + return [note ?? '', setNote]; +}; + +export default useNote;