commit b17b0efb37d9ed9de4ece0eb1d587998474b5c12
parent ef2d224a55f1078b793a0e28315fd7c07249f0fe
Author: Yuukin256 <Yuukin256@gmail.com>
Date: Wed, 9 Nov 2022 23:14:23 +0900
曜日時限時間割表登録ページを実装
Diffstat:
11 files changed, 606 insertions(+), 28 deletions(-)
diff --git a/package.json b/package.json
@@ -24,6 +24,7 @@
"@emotion/styled": "^11.10.5",
"@js-temporal/polyfill": "^0.4.1",
"@mui/icons-material": "^5.10.9",
+ "@mui/lab": "^5.0.0-alpha.107",
"@mui/material": "^5.10.12",
"@mui/x-date-pickers": "^5.0.7",
"@prisma/client": "^4.5.0",
diff --git a/src/api/timetables/_grade@number/_date@string.ts b/src/api/timetables/_grade@number/_date@string.ts
@@ -1,6 +1,6 @@
import { z } from 'zod';
-import { ABSTRACT_LESSON_NAMES } from 'constant';
+import { ABSTRACT_LESSON_NAMES_EN } from 'constant';
import type { DayTimetableForApi } from '..';
import type { DayTimetable } from '@prisma/client';
@@ -32,12 +32,12 @@ export const getApiRequestSchema = z.object({
export const putApiRequestSchema = z.object({
query: querySchema,
body: z.object<ToZod<PutApiRequestbody>>({
- period1: z.enum(ABSTRACT_LESSON_NAMES).nullable(),
- period2: z.enum(ABSTRACT_LESSON_NAMES).nullable(),
- period3: z.enum(ABSTRACT_LESSON_NAMES).nullable(),
- period4: z.enum(ABSTRACT_LESSON_NAMES).nullable(),
- period5: z.enum(ABSTRACT_LESSON_NAMES).nullable(),
- period6: z.enum(ABSTRACT_LESSON_NAMES).nullable(),
- period7: z.enum(ABSTRACT_LESSON_NAMES).nullable(),
+ period1: z.enum(ABSTRACT_LESSON_NAMES_EN).nullable(),
+ period2: z.enum(ABSTRACT_LESSON_NAMES_EN).nullable(),
+ period3: z.enum(ABSTRACT_LESSON_NAMES_EN).nullable(),
+ period4: z.enum(ABSTRACT_LESSON_NAMES_EN).nullable(),
+ period5: z.enum(ABSTRACT_LESSON_NAMES_EN).nullable(),
+ period6: z.enum(ABSTRACT_LESSON_NAMES_EN).nullable(),
+ period7: z.enum(ABSTRACT_LESSON_NAMES_EN).nullable(),
}),
});
diff --git a/src/constant/index.tsx b/src/constant/index.tsx
@@ -37,7 +37,7 @@ export const CLASSES = [
'3-8',
] as const;
-export const ABSTRACT_LESSON_NAMES = [
+export const ABSTRACT_LESSON_NAMES_EN = [
'Mon1',
'Mon2',
'Mon3',
@@ -74,7 +74,44 @@ export const ABSTRACT_LESSON_NAMES = [
'Fri7',
] as const;
-export const ABSTRACT_LESSON_NAME_MAP: Readonly<Record<AbstractLessonNameEn, AbstractLessonNameJa>> = {
+export const ABSTRACT_LESSON_NAMES_JA = [
+ '月1',
+ '月2',
+ '月3',
+ '月4',
+ '月5',
+ '月6',
+ '月7',
+ '火1',
+ '火2',
+ '火3',
+ '火4',
+ '火5',
+ '火6',
+ '火7',
+ '水1',
+ '水2',
+ '水3',
+ '水4',
+ '水5',
+ '水6',
+ '水7',
+ '木1',
+ '木2',
+ '木3',
+ '木4',
+ '木5',
+ '木6',
+ '金1',
+ '金2',
+ '金3',
+ '金4',
+ '金5',
+ '金6',
+ '金7',
+] as const;
+
+export const ABSTRACT_LESSON_NAME_MAP_EN_TO_JA: Readonly<Record<AbstractLessonNameEn, AbstractLessonNameJa>> = {
Mon1: '月1',
Mon2: '月2',
Mon3: '月3',
@@ -111,6 +148,43 @@ export const ABSTRACT_LESSON_NAME_MAP: Readonly<Record<AbstractLessonNameEn, Abs
Fri7: '金7',
};
+export const ABSTRACT_LESSON_NAME_MAP_JA_TO_EN: Readonly<Record<AbstractLessonNameJa, AbstractLessonNameEn>> = {
+ 月1: 'Mon1',
+ 月2: 'Mon2',
+ 月3: 'Mon3',
+ 月4: 'Mon4',
+ 月5: 'Mon5',
+ 月6: 'Mon6',
+ 月7: 'Mon7',
+ 火1: 'Tue1',
+ 火2: 'Tue2',
+ 火3: 'Tue3',
+ 火4: 'Tue4',
+ 火5: 'Tue5',
+ 火6: 'Tue6',
+ 火7: 'Tue7',
+ 水1: 'Wed1',
+ 水2: 'Wed2',
+ 水3: 'Wed3',
+ 水4: 'Wed4',
+ 水5: 'Wed5',
+ 水6: 'Wed6',
+ 水7: 'Wed7',
+ 木1: 'Thu1',
+ 木2: 'Thu2',
+ 木3: 'Thu3',
+ 木4: 'Thu4',
+ 木5: 'Thu5',
+ 木6: 'Thu6',
+ 金1: 'Fri1',
+ 金2: 'Fri2',
+ 金3: 'Fri3',
+ 金4: 'Fri4',
+ 金5: 'Fri5',
+ 金6: 'Fri6',
+ 金7: 'Fri7',
+};
+
export const DATE_FORMAT = 'yyyy-MM-dd';
export const BUG_REPORT_FORM_URL = process.env['NEXT_PUBLIC_BUG_REPORT_FORM_URL'];
diff --git a/src/hooks/useRegisterDayTimetable.ts b/src/hooks/useRegisterDayTimetable.ts
@@ -0,0 +1,154 @@
+import { useAspidaQuery } from '@aspida/react-query';
+import { Temporal } from '@js-temporal/polyfill';
+import { useSnackbar } from 'notistack';
+import { useCallback, useState } from 'react';
+import { useMutation, useQueryClient } from 'react-query';
+import { useMap } from 'react-use';
+
+import { apiClient } from 'lib/apiClient';
+
+import type { AbstractLessonName, DayTimetable } from '@prisma/client';
+import type { Dispatch, SetStateAction } from 'react';
+
+export type DayTimetableWithoutGradeAndDate = Omit<DayTimetable, 'grade' | 'date'>;
+
+type Result = {
+ loading: boolean;
+ sending: boolean;
+ grades: number[];
+ grade: number | undefined;
+ setGrade: (val: number) => void;
+ date: Temporal.PlainDate;
+ setDate: Dispatch<SetStateAction<Temporal.PlainDate>>;
+ timetable: DayTimetableWithoutGradeAndDate;
+ setTimetable: (key: keyof DayTimetableWithoutGradeAndDate, value: AbstractLessonName | null) => void;
+ setUsualTimetable: () => void;
+ submit: () => void;
+};
+
+const emptyTimetable: DayTimetableWithoutGradeAndDate = {
+ period1: null,
+ period2: null,
+ period3: null,
+ period4: null,
+ period5: null,
+ period6: null,
+ period7: null,
+};
+
+const useRegisterDayTimetable = (): Result => {
+ const queryClient = useQueryClient();
+ const { enqueueSnackbar } = useSnackbar();
+
+ const grades = [1, 2, 3];
+ const [grade, setGrade] = useState<number>();
+
+ const [date, setDate] = useState<Temporal.PlainDate>(Temporal.Now.plainDateISO());
+
+ const [timetable, { set: setTimetable, setAll }] = useMap<DayTimetableWithoutGradeAndDate>(emptyTimetable);
+
+ const dayTimetableQuery = useAspidaQuery(apiClient.timetables._grade(grade ?? -1)._date(date.toString()), {
+ enabled: !!grade,
+ onSuccess: (data) => {
+ if (data) {
+ const { grade, date, ...timetable } = data;
+ setAll(timetable);
+ } else {
+ setAll(emptyTimetable);
+ }
+ },
+ onError: () => enqueueSnackbar('エラーが発生しました', { variant: 'error', autoHideDuration: 6000 }),
+ });
+
+ const dayTimetableMutation = useMutation(
+ ({ grade, date, body }: { grade: number; date: Temporal.PlainDate; body: DayTimetableWithoutGradeAndDate }) => {
+ return apiClient.timetables._grade(grade)._date(date.toJSON()).$put({ body });
+ },
+ {
+ onSuccess: (_, { grade, date }) => {
+ enqueueSnackbar('正常に保存されました', { variant: 'success', autoHideDuration: 2000 });
+ queryClient.invalidateQueries(apiClient.timetables._grade(grade)._date(date.toJSON()).$path());
+ },
+ onError: () => enqueueSnackbar('エラーが発生しました', { variant: 'error', autoHideDuration: 6000 }),
+ }
+ );
+
+ const submit = () => {
+ if (grade) {
+ dayTimetableMutation.mutate({ grade, date, body: timetable });
+ }
+ };
+
+ const setUsualTimetable = useCallback(() => {
+ switch (date.dayOfWeek) {
+ case 1: // 月曜日
+ return setAll({
+ period1: 'Mon1',
+ period2: 'Mon2',
+ period3: 'Mon3',
+ period4: 'Mon4',
+ period5: 'Mon5',
+ period6: 'Mon6',
+ period7: 'Mon7',
+ });
+ case 2: // 火曜日
+ return setAll({
+ period1: 'Tue1',
+ period2: 'Tue2',
+ period3: 'Tue3',
+ period4: 'Tue4',
+ period5: 'Tue5',
+ period6: 'Tue6',
+ period7: 'Tue7',
+ });
+ case 3: // 水曜日
+ return setAll({
+ period1: 'Wed1',
+ period2: 'Wed2',
+ period3: 'Wed3',
+ period4: 'Wed4',
+ period5: 'Wed5',
+ period6: 'Wed6',
+ period7: 'Wed7',
+ });
+ case 4: // 木曜日
+ return setAll({
+ period1: 'Thu1',
+ period2: 'Thu2',
+ period3: 'Thu3',
+ period4: 'Thu4',
+ period5: 'Thu5',
+ period6: 'Thu6',
+ period7: null,
+ });
+ case 5: // 金曜日
+ return setAll({
+ period1: 'Fri1',
+ period2: 'Fri2',
+ period3: 'Fri3',
+ period4: 'Fri4',
+ period5: 'Fri5',
+ period6: 'Fri6',
+ period7: 'Fri7',
+ });
+ default:
+ return setAll(emptyTimetable);
+ }
+ }, [date.dayOfWeek, setAll]);
+
+ return {
+ loading: dayTimetableQuery.isLoading,
+ sending: dayTimetableMutation.isLoading,
+ grades,
+ grade,
+ setGrade,
+ date,
+ setDate,
+ timetable,
+ setTimetable,
+ setUsualTimetable,
+ submit,
+ };
+};
+
+export default useRegisterDayTimetable;
diff --git a/src/hooks/useRegisterStandardTimetable.ts b/src/hooks/useRegisterStandardTimetable.ts
@@ -11,6 +11,7 @@ import type { AbstractLessonNameEn, StandardEn } from 'types';
type Result = {
loading: boolean;
+ sending: boolean;
classes: SchoolClass[];
classId: number | undefined;
setClassId: (val: number) => void;
@@ -64,13 +65,13 @@ const useRegisterStandardTimetable = (): Result => {
const [standard, { set: setStandard, setAll }] = useMap(emptyStandard);
const classesQuery = useAspidaQuery(apiClient.classes, {
- onError: () => enqueueSnackbar('エラーが発生しました', { variant: 'error' }),
+ onError: () => enqueueSnackbar('エラーが発生しました', { variant: 'error', autoHideDuration: 6000 }),
});
const standardQuery = useAspidaQuery(apiClient.classes._classId(classId ?? -1).standard, {
enabled: !!classId,
onSuccess: (data) => data && setAll(data),
- onError: () => enqueueSnackbar('エラーが発生しました', { variant: 'error' }),
+ onError: () => enqueueSnackbar('エラーが発生しました', { variant: 'error', autoHideDuration: 6000 }),
});
const standardMutation = useMutation(
@@ -79,11 +80,11 @@ const useRegisterStandardTimetable = (): Result => {
},
{
onSuccess: (_, { classId }) => {
- enqueueSnackbar('正常に保存されました', { variant: 'success' });
+ enqueueSnackbar('正常に保存されました', { variant: 'success', autoHideDuration: 2000 });
queryClient.invalidateQueries(apiClient.classes._classId(classId).standard.$path());
},
onError: () => {
- enqueueSnackbar('エラーが発生しました', { variant: 'error' });
+ enqueueSnackbar('エラーが発生しました', { variant: 'error', autoHideDuration: 6000 });
},
}
);
@@ -95,7 +96,8 @@ const useRegisterStandardTimetable = (): Result => {
};
return {
- loading: classesQuery.isLoading || standardQuery.isLoading || standardMutation.isLoading,
+ loading: classesQuery.isLoading || standardQuery.isLoading,
+ sending: standardMutation.isLoading,
classes: classesQuery.data ?? [],
classId,
setClassId,
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
@@ -23,7 +23,9 @@ const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWind
function App(props: MyAppProps) {
const { Component, emotionCache = clientSideEmotionCache, pageProps } = props;
- const theme = createTheme();
+ const theme = createTheme({
+ palette: { secondary: { main: '#e91e63', light: '#ff6090', dark: '#c2185b', contrastText: '#fff' } },
+ });
return (
<SnackbarProvider maxSnack={3}>
diff --git a/src/pages/console/index.tsx b/src/pages/console/index.tsx
@@ -0,0 +1,34 @@
+import NavigateNextIcon from '@mui/icons-material/NavigateNext';
+import Box from '@mui/material/Box';
+import Breadcrumbs from '@mui/material/Breadcrumbs';
+import Container from '@mui/material/Container';
+import Typography from '@mui/material/Typography';
+
+import Link from 'components/common/Link';
+
+import type { NextPage } from 'next';
+
+const Page: NextPage = () => {
+ return (
+ <Container maxWidth='lg' component={Box} py={2}>
+ <Breadcrumbs separator={<NavigateNextIcon fontSize='small' />} aria-label='breadcrumb'>
+ <Link underline='hover' color='inherit' href='/'>
+ トップ
+ </Link>
+ <Typography color='text.primary' variant='h2' fontSize={32} fontWeight={400}>
+ 管理
+ </Typography>
+ </Breadcrumbs>
+ <ul>
+ <li>
+ <Link href={{ pathname: '/console/standard' }}>クラス別時間割表</Link>
+ </li>
+ <li>
+ <Link href={{ pathname: '/console/timetable' }}>学年別曜日時限時間割表</Link>
+ </li>
+ </ul>
+ </Container>
+ );
+};
+
+export default Page;
diff --git a/src/pages/console/standard.tsx b/src/pages/console/standard.tsx
@@ -1,7 +1,9 @@
+import NavigateNextIcon from '@mui/icons-material/NavigateNext';
import SaveIcon from '@mui/icons-material/Save';
+import LoadingButton from '@mui/lab/LoadingButton';
import Backdrop from '@mui/material/Backdrop';
import Box from '@mui/material/Box';
-import Button from '@mui/material/Button';
+import Breadcrumbs from '@mui/material/Breadcrumbs';
import CircularProgress from '@mui/material/CircularProgress';
import Container from '@mui/material/Container';
import FormControl from '@mui/material/FormControl';
@@ -18,6 +20,7 @@ import TableRow from '@mui/material/TableRow';
import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
+import Link from 'components/common/Link';
import useRegisterStandardTimetable from 'hooks/useRegisterStandardTimetable';
import type { NextPage } from 'next';
@@ -139,16 +142,27 @@ const StandardTimetableForm: FC<{
};
const Page: NextPage = () => {
- const { loading, classes, classId, setClassId, standard, setStandard, submit } = useRegisterStandardTimetable();
+ const { loading, sending, classes, classId, setClassId, standard, setStandard, submit } =
+ useRegisterStandardTimetable();
return (
<Container maxWidth='lg' component={Box} py={2}>
<Backdrop open={loading}>
<CircularProgress color='inherit' />
</Backdrop>
- <Typography variant='h2' fontSize={40}>
- クラス別時間割表 登録・変更
- </Typography>
+
+ <Breadcrumbs separator={<NavigateNextIcon fontSize='small' />} aria-label='breadcrumb'>
+ <Link underline='hover' color='inherit' href='/'>
+ トップ
+ </Link>
+ <Link underline='hover' color='inherit' href='/console'>
+ 管理
+ </Link>
+ <Typography color='text.primary' variant='h2' fontSize={32} fontWeight={400}>
+ クラス別時間割表 登録・変更
+ </Typography>
+ </Breadcrumbs>
+
<Box width='7rem' m={4}>
<FormControl fullWidth disabled={loading}>
<InputLabel>クラス</InputLabel>
@@ -161,10 +175,21 @@ const Page: NextPage = () => {
</Select>
</FormControl>
</Box>
+
<StandardTimetableForm standard={standard} setStandard={setStandard} disabled={loading} />
- <Button variant='contained' disabled={loading} sx={{ m: 4 }} startIcon={<SaveIcon />} onClick={() => submit()}>
- 保存
- </Button>
+
+ <Box m={4}>
+ <LoadingButton
+ variant='contained'
+ loading={sending}
+ loadingPosition='start'
+ disabled={loading}
+ startIcon={<SaveIcon />}
+ onClick={() => submit()}
+ >
+ 保存
+ </LoadingButton>
+ </Box>
</Container>
);
};
diff --git a/src/pages/console/timetable.tsx b/src/pages/console/timetable.tsx
@@ -0,0 +1,205 @@
+import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
+import ChevronRightIcon from '@mui/icons-material/ChevronRight';
+import EditIcon from '@mui/icons-material/Edit';
+import NavigateNextIcon from '@mui/icons-material/NavigateNext';
+import SaveIcon from '@mui/icons-material/Save';
+import LoadingButton from '@mui/lab/LoadingButton';
+import Autocomplete from '@mui/material/Autocomplete';
+import Backdrop from '@mui/material/Backdrop';
+import Box from '@mui/material/Box';
+import Breadcrumbs from '@mui/material/Breadcrumbs';
+import Button from '@mui/material/Button';
+import CircularProgress from '@mui/material/CircularProgress';
+import Container from '@mui/material/Container';
+import FormControl from '@mui/material/FormControl';
+import IconButton from '@mui/material/IconButton';
+import InputLabel from '@mui/material/InputLabel';
+import MenuItem from '@mui/material/MenuItem';
+import Paper from '@mui/material/Paper';
+import Select from '@mui/material/Select';
+import Stack from '@mui/material/Stack';
+import TextField from '@mui/material/TextField';
+import Typography from '@mui/material/Typography';
+import isValid from 'date-fns/isValid';
+
+import DatePicker from 'components/common/DatePicker';
+import Link from 'components/common/Link';
+import {
+ ABSTRACT_LESSON_NAMES_JA,
+ ABSTRACT_LESSON_NAME_MAP_EN_TO_JA,
+ ABSTRACT_LESSON_NAME_MAP_JA_TO_EN,
+} from 'constant';
+import useRegisterDayTimetable from 'hooks/useRegisterDayTimetable';
+import { dateToPlainDate, plainDateToDate } from 'utils';
+
+import type { NextPage } from 'next';
+import type { FC } from 'react';
+import type { AbstractLessonNameEn } from 'types';
+
+const UnitInputForm: FC<{
+ value: AbstractLessonNameEn | null;
+ onChange: (newValue: AbstractLessonNameEn | null) => void;
+ label: string;
+ disabled?: boolean;
+}> = ({ value, onChange, label, disabled }) => {
+ return (
+ <Autocomplete
+ disablePortal
+ autoSelect
+ disabled={disabled}
+ value={value ? ABSTRACT_LESSON_NAME_MAP_EN_TO_JA[value] : value}
+ onChange={(_, newValue) => onChange(newValue ? ABSTRACT_LESSON_NAME_MAP_JA_TO_EN[newValue] : newValue)}
+ options={ABSTRACT_LESSON_NAMES_JA}
+ sx={{ width: 120 }}
+ renderInput={(params) => <TextField {...params} label={label} />}
+ />
+ );
+};
+
+// TODO: この登録画面の他に、一括確認画面を作る
+const Page: NextPage = () => {
+ const {
+ loading,
+ sending,
+ grades,
+ grade,
+ setGrade,
+ date,
+ setDate,
+ timetable,
+ setTimetable,
+ setUsualTimetable,
+ submit,
+ } = useRegisterDayTimetable();
+
+ // TODO: Temporal をそのまま扱える Adapter を用意する?
+ // TODO: データフェッチに一定以上の時間がかかった場合のみ進捗表示を出す
+ return (
+ <Container maxWidth='lg' component={Box} py={2}>
+ <Backdrop open={loading}>
+ <CircularProgress color='inherit' />
+ </Backdrop>
+
+ <Breadcrumbs separator={<NavigateNextIcon fontSize='small' />} aria-label='breadcrumb'>
+ <Link underline='hover' color='inherit' href='/'>
+ トップ
+ </Link>
+ <Link underline='hover' color='inherit' href='/console'>
+ 管理
+ </Link>
+ <Typography color='text.primary' variant='h2' fontSize={32} fontWeight={400}>
+ 学年別曜日時限時間割表 登録・変更
+ </Typography>
+ </Breadcrumbs>
+
+ <Stack direction={{ xs: 'column', sm: 'row' }} spacing={{ xs: 2, sm: 8 }} m={4}>
+ <Box width='7rem'>
+ <FormControl fullWidth required disabled={loading} size='small'>
+ <InputLabel>学年</InputLabel>
+ <Select value={grade ?? ''} label='学年' onChange={(e) => setGrade(e.target.value as number)}>
+ {grades.map((g) => (
+ <MenuItem value={g} key={g}>
+ {g}
+ </MenuItem>
+ ))}
+ </Select>
+ </FormControl>
+ </Box>
+
+ <Stack direction='row' spacing={1}>
+ <IconButton onClick={() => setDate((old) => old.subtract({ days: 1 }))}>
+ <ChevronLeftIcon />
+ </IconButton>
+ <Box width='12rem'>
+ <DatePicker
+ value={plainDateToDate(date) ?? null}
+ onChange={(newValue) => {
+ isValid(newValue) && setDate(dateToPlainDate(newValue as Date));
+ }}
+ inputFormat='yyyy/MM/dd (EEEEE)'
+ disableMaskedInput
+ renderInput={(params) => <TextField {...params} />}
+ />
+ </Box>
+ <IconButton onClick={() => setDate((old) => old.add({ days: 1 }))}>
+ <ChevronRightIcon />
+ </IconButton>
+ </Stack>
+ </Stack>
+
+ <Paper component={Box} p={4}>
+ <Stack direction={{ xs: 'column', md: 'row' }} spacing={2}>
+ <UnitInputForm
+ label='1限'
+ value={timetable.period1}
+ onChange={(newValue) => setTimetable('period1', newValue)}
+ disabled={loading}
+ />
+ <UnitInputForm
+ label='2限'
+ value={timetable.period2}
+ onChange={(newValue) => setTimetable('period2', newValue)}
+ disabled={loading}
+ />
+ <UnitInputForm
+ label='3限'
+ value={timetable.period3}
+ onChange={(newValue) => setTimetable('period3', newValue)}
+ disabled={loading}
+ />
+ <UnitInputForm
+ label='4限'
+ value={timetable.period4}
+ onChange={(newValue) => setTimetable('period4', newValue)}
+ disabled={loading}
+ />
+ <UnitInputForm
+ label='5限'
+ value={timetable.period5}
+ onChange={(newValue) => setTimetable('period5', newValue)}
+ disabled={loading}
+ />
+ <UnitInputForm
+ label='6限'
+ value={timetable.period6}
+ onChange={(newValue) => setTimetable('period6', newValue)}
+ disabled={loading}
+ />
+ <UnitInputForm
+ label='7限'
+ value={timetable.period7}
+ onChange={(newValue) => setTimetable('period7', newValue)}
+ disabled={loading}
+ />
+ </Stack>
+
+ <Box mt={2}>
+ <Button
+ variant='outlined'
+ color='secondary'
+ disabled={loading}
+ startIcon={<EditIcon />}
+ onClick={() => setUsualTimetable()}
+ >
+ 時間割通りで自動入力
+ </Button>
+ </Box>
+ </Paper>
+
+ <Box m={4}>
+ <LoadingButton
+ variant='contained'
+ loading={sending}
+ loadingPosition='start'
+ disabled={loading}
+ startIcon={<SaveIcon />}
+ onClick={() => submit()}
+ >
+ 保存
+ </LoadingButton>
+ </Box>
+ </Container>
+ );
+};
+
+export default Page;
diff --git a/src/types/index.ts b/src/types/index.ts
@@ -1,3 +1,4 @@
+import type { ABSTRACT_LESSON_NAMES_EN, ABSTRACT_LESSON_NAMES_JA } from 'constant';
import type { Map } from 'immutable';
import type { z } from 'zod';
@@ -6,13 +7,11 @@ export type Grade = 1 | 2 | 3;
// クラス
export type Class = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
-type WeekdayEn = 'Mon' | 'Tue' | 'Wed' | 'Thu' | 'Fri';
-type WeekdayJa = '月' | '火' | '水' | '木' | '金';
type SchoolTime = 1 | 2 | 3 | 4 | 5 | 6 | 7;
// 月1 から 金7 (木7 以外)
-export type AbstractLessonNameJa = Exclude<`${WeekdayJa}${SchoolTime}`, '木7'>;
-export type AbstractLessonNameEn = Exclude<`${WeekdayEn}${SchoolTime}`, 'Thu7'>;
+export type AbstractLessonNameJa = typeof ABSTRACT_LESSON_NAMES_JA[number];
+export type AbstractLessonNameEn = typeof ABSTRACT_LESSON_NAMES_EN[number];
// 授業名
export type ActualLessonName = string;
diff --git a/yarn.lock b/yarn.lock
@@ -442,6 +442,29 @@ __metadata:
languageName: node
linkType: hard
+"@mui/base@npm:5.0.0-alpha.105":
+ version: 5.0.0-alpha.105
+ resolution: "@mui/base@npm:5.0.0-alpha.105"
+ dependencies:
+ "@babel/runtime": ^7.19.0
+ "@emotion/is-prop-valid": ^1.2.0
+ "@mui/types": ^7.2.0
+ "@mui/utils": ^5.10.9
+ "@popperjs/core": ^2.11.6
+ clsx: ^1.2.1
+ prop-types: ^15.8.1
+ react-is: ^18.2.0
+ peerDependencies:
+ "@types/react": ^17.0.0 || ^18.0.0
+ react: ^17.0.0 || ^18.0.0
+ react-dom: ^17.0.0 || ^18.0.0
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 1db3dcd6760c814437c679ddf13d9e3c4e7fc6f51ce933445715a56aa65dcaeb38aff35832c524211d62fb3401b436cbcc3e9e4234068a0c3379da727f44cff2
+ languageName: node
+ linkType: hard
+
"@mui/core-downloads-tracker@npm:^5.10.12":
version: 5.10.12
resolution: "@mui/core-downloads-tracker@npm:5.10.12"
@@ -465,6 +488,36 @@ __metadata:
languageName: node
linkType: hard
+"@mui/lab@npm:^5.0.0-alpha.107":
+ version: 5.0.0-alpha.107
+ resolution: "@mui/lab@npm:5.0.0-alpha.107"
+ dependencies:
+ "@babel/runtime": ^7.19.0
+ "@mui/base": 5.0.0-alpha.105
+ "@mui/system": ^5.10.13
+ "@mui/types": ^7.2.0
+ "@mui/utils": ^5.10.9
+ clsx: ^1.2.1
+ prop-types: ^15.8.1
+ react-is: ^18.2.0
+ peerDependencies:
+ "@emotion/react": ^11.5.0
+ "@emotion/styled": ^11.3.0
+ "@mui/material": ^5.0.0
+ "@types/react": ^17.0.0 || ^18.0.0
+ react: ^17.0.0 || ^18.0.0
+ react-dom: ^17.0.0 || ^18.0.0
+ peerDependenciesMeta:
+ "@emotion/react":
+ optional: true
+ "@emotion/styled":
+ optional: true
+ "@types/react":
+ optional: true
+ checksum: 58ea8d0d7bcf84884d2fdcb6234ec9b6a74e514ba78eb951bcbb6905e65de8b1382d9ce9ef9d29bf1b0d75977987fc0d08241d692341ea08626e885acdce173d
+ languageName: node
+ linkType: hard
+
"@mui/material@npm:^5.10.12":
version: 5.10.12
resolution: "@mui/material@npm:5.10.12"
@@ -564,6 +617,34 @@ __metadata:
languageName: node
linkType: hard
+"@mui/system@npm:^5.10.13":
+ version: 5.10.13
+ resolution: "@mui/system@npm:5.10.13"
+ dependencies:
+ "@babel/runtime": ^7.19.0
+ "@mui/private-theming": ^5.10.9
+ "@mui/styled-engine": ^5.10.8
+ "@mui/types": ^7.2.0
+ "@mui/utils": ^5.10.9
+ clsx: ^1.2.1
+ csstype: ^3.1.1
+ prop-types: ^15.8.1
+ peerDependencies:
+ "@emotion/react": ^11.5.0
+ "@emotion/styled": ^11.3.0
+ "@types/react": ^17.0.0 || ^18.0.0
+ react: ^17.0.0 || ^18.0.0
+ peerDependenciesMeta:
+ "@emotion/react":
+ optional: true
+ "@emotion/styled":
+ optional: true
+ "@types/react":
+ optional: true
+ checksum: afac3f4859b9e137fa62fa4446642909704964209a44f63a472d452ace6428a0b337b48a934cd805e3728a0098ad4f41fedbc06ce14e245a5fa42fabe349d9e8
+ languageName: node
+ linkType: hard
+
"@mui/types@npm:^7.2.0":
version: 7.2.0
resolution: "@mui/types@npm:7.2.0"
@@ -5270,6 +5351,7 @@ __metadata:
"@emotion/styled": ^11.10.5
"@js-temporal/polyfill": ^0.4.1
"@mui/icons-material": ^5.10.9
+ "@mui/lab": ^5.0.0-alpha.107
"@mui/material": ^5.10.12
"@mui/x-date-pickers": ^5.0.7
"@next/eslint-plugin-next": ^13.0.2