commit 407260f658f887486f31face789212f0f9b9c39d
parent 6b69da29adc6d62cc2220467e6ce064599292e8e
Author: Yuukin256 <Yuukin256@gmail.com>
Date: Fri, 4 Nov 2022 00:15:05 +0900
クラス別時間割表データ管理画面を実装
Diffstat:
3 files changed, 305 insertions(+), 11 deletions(-)
diff --git a/src/hooks/useStandardTimetable.ts b/src/hooks/useStandardTimetable.ts
@@ -0,0 +1,97 @@
+import useAspidaSWR from '@aspida/swr';
+import { useSnackbar } from 'notistack';
+import { useBoolean } from 'react-use';
+
+import { apiClient } from 'lib/apiClient';
+
+import type { AbstractLessonNameEn, StandardEn } from 'types';
+
+type Props = {
+ classId: number;
+};
+
+type Result = {
+ loading: boolean;
+ standard: StandardEn | undefined;
+ error: unknown;
+ set: (key: AbstractLessonNameEn, value: string[]) => void;
+ submit: () => void;
+};
+
+const fallbackData: StandardEn = {
+ Mon1: [],
+ Mon2: [],
+ Mon3: [],
+ Mon4: [],
+ Mon5: [],
+ Mon6: [],
+ Mon7: [],
+ Tue1: [],
+ Tue2: [],
+ Tue3: [],
+ Tue4: [],
+ Tue5: [],
+ Tue6: [],
+ Tue7: [],
+ Wed1: [],
+ Wed2: [],
+ Wed3: [],
+ Wed4: [],
+ Wed5: [],
+ Wed6: [],
+ Wed7: [],
+ Thu1: [],
+ Thu2: [],
+ Thu3: [],
+ Thu4: [],
+ Thu5: [],
+ Thu6: [],
+ Fri1: [],
+ Fri2: [],
+ Fri3: [],
+ Fri4: [],
+ Fri5: [],
+ Fri6: [],
+ Fri7: [],
+};
+
+const useStandardTimetable = (props: Props): Result => {
+ const { enqueueSnackbar } = useSnackbar();
+
+ const { data, error, mutate } = useAspidaSWR(apiClient.classes._classId(props.classId).standard, {
+ revalidateIfStale: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ onError() {
+ enqueueSnackbar('エラーが発生しました', { variant: 'error' });
+ },
+ });
+
+ const [submitting, setSubmitting] = useBoolean(false);
+ const standard = data ?? fallbackData;
+
+ const set = (key: AbstractLessonNameEn, value: string[]) => {
+ mutate({ ...standard, [key]: value }, false);
+ };
+
+ const submit = () => {
+ setSubmitting(true);
+ apiClient.classes
+ ._classId(props.classId)
+ .standard.$post({ body: standard })
+ .then((newData) => mutate(newData))
+ .then(() => enqueueSnackbar('正常に保存されました', { variant: 'success' }))
+ .catch(() => enqueueSnackbar('エラーが発生しました', { variant: 'error' }))
+ .finally(() => setSubmitting(false));
+ };
+
+ return {
+ loading: (!data && !error) || submitting,
+ standard: data,
+ error,
+ set,
+ submit,
+ };
+};
+
+export default useStandardTimetable;
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
@@ -2,11 +2,13 @@ import { CacheProvider } from '@emotion/react';
import CssBaseline from '@mui/material/CssBaseline';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import Head from 'next/head';
+import { SnackbarProvider } from 'notistack';
import createEmotionCache from 'lib/createEmotionCache';
import type { EmotionCache } from '@emotion/react';
import type { AppProps } from 'next/app';
+
import 'styles/global.css';
const clientSideEmotionCache = createEmotionCache();
@@ -15,22 +17,24 @@ interface MyAppProps extends AppProps {
emotionCache?: EmotionCache;
}
-function MyApp(props: MyAppProps) {
+function App(props: MyAppProps) {
const { Component, emotionCache = clientSideEmotionCache, pageProps } = props;
const theme = createTheme();
return (
- <CacheProvider value={emotionCache}>
- <Head>
- <meta name='viewport' content='initial-scale=1, width=device-width' />
- </Head>
- <ThemeProvider theme={theme}>
- <CssBaseline />
- <Component {...pageProps} />
- </ThemeProvider>
- </CacheProvider>
+ <SnackbarProvider maxSnack={3}>
+ <CacheProvider value={emotionCache}>
+ <Head>
+ <meta name='viewport' content='initial-scale=1, width=device-width' />
+ </Head>
+ <ThemeProvider theme={theme}>
+ <CssBaseline />
+ <Component {...pageProps} />
+ </ThemeProvider>
+ </CacheProvider>
+ </SnackbarProvider>
);
}
-export default MyApp;
+export default App;
diff --git a/src/pages/console/standard.tsx b/src/pages/console/standard.tsx
@@ -0,0 +1,193 @@
+import useAspidaSWR from '@aspida/swr';
+import SendIcon from '@mui/icons-material/Send';
+import Backdrop from '@mui/material/Backdrop';
+import Box from '@mui/material/Box';
+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 InputLabel from '@mui/material/InputLabel';
+import MenuItem from '@mui/material/MenuItem';
+import Paper from '@mui/material/Paper';
+import Select from '@mui/material/Select';
+import Table from '@mui/material/Table';
+import TableBody from '@mui/material/TableBody';
+import TableCell from '@mui/material/TableCell';
+import TableContainer from '@mui/material/TableContainer';
+import TableHead from '@mui/material/TableHead';
+import TableRow from '@mui/material/TableRow';
+import TextField from '@mui/material/TextField';
+import { useSnackbar } from 'notistack';
+import { useState } from 'react';
+import { SWRConfig } from 'swr';
+
+import useStandardTimetable from 'hooks/useStandardTimetable';
+import { apiClient } from 'lib/apiClient';
+
+import type { NextPage } from 'next';
+import type { FC } from 'react';
+import type { AbstractLessonNameEn, StandardEn } from 'types';
+const ClassUnitCell: FC<{ subjects: string[]; handleChange: (newValue: string) => void; tabIndex: number }> = ({
+ subjects,
+ handleChange,
+ tabIndex,
+}) => {
+ return (
+ <TableCell>
+ <TextField
+ variant='standard'
+ value={subjects.join('/')}
+ onChange={(e) => handleChange(e.target.value)}
+ inputProps={{ tabIndex: tabIndex }}
+ />
+ </TableCell>
+ );
+};
+
+const StandardTimetableTable: FC<{ data: StandardEn; set: (key: AbstractLessonNameEn, value: string[]) => void }> = ({
+ data,
+ set,
+}) => {
+ const getCellParams = (abstractLessonName: AbstractLessonNameEn) => {
+ return {
+ subjects: data[abstractLessonName],
+ handleChange: (newValue: string) =>
+ set(
+ abstractLessonName,
+ newValue.split('/').map((v) => v.trim())
+ ),
+ };
+ };
+
+ return (
+ <TableContainer component={Paper}>
+ <Table>
+ <TableHead>
+ <TableRow>
+ <TableCell sx={{ width: '3rem' }}></TableCell>
+ <TableCell sx={{ width: '8rem' }}>月</TableCell>
+ <TableCell sx={{ width: '8rem' }}>火</TableCell>
+ <TableCell sx={{ width: '8rem' }}>水</TableCell>
+ <TableCell sx={{ width: '8rem' }}>木</TableCell>
+ <TableCell sx={{ width: '8rem' }}>金</TableCell>
+ </TableRow>
+ </TableHead>
+ <TableBody>
+ <TableRow>
+ <TableCell>1</TableCell>
+ <ClassUnitCell {...getCellParams('Mon1')} tabIndex={111} />
+ <ClassUnitCell {...getCellParams('Tue1')} tabIndex={121} />
+ <ClassUnitCell {...getCellParams('Wed1')} tabIndex={131} />
+ <ClassUnitCell {...getCellParams('Thu1')} tabIndex={141} />
+ <ClassUnitCell {...getCellParams('Fri1')} tabIndex={151} />
+ </TableRow>
+ <TableRow>
+ <TableCell>2</TableCell>
+ <ClassUnitCell {...getCellParams('Mon2')} tabIndex={112} />
+ <ClassUnitCell {...getCellParams('Tue2')} tabIndex={122} />
+ <ClassUnitCell {...getCellParams('Wed2')} tabIndex={132} />
+ <ClassUnitCell {...getCellParams('Thu2')} tabIndex={142} />
+ <ClassUnitCell {...getCellParams('Fri2')} tabIndex={152} />
+ </TableRow>
+ <TableRow>
+ <TableCell>3</TableCell>
+ <ClassUnitCell {...getCellParams('Mon3')} tabIndex={113} />
+ <ClassUnitCell {...getCellParams('Tue3')} tabIndex={123} />
+ <ClassUnitCell {...getCellParams('Wed3')} tabIndex={133} />
+ <ClassUnitCell {...getCellParams('Thu3')} tabIndex={143} />
+ <ClassUnitCell {...getCellParams('Fri3')} tabIndex={153} />
+ </TableRow>
+ <TableRow>
+ <TableCell>4</TableCell>
+ <ClassUnitCell {...getCellParams('Mon4')} tabIndex={114} />
+ <ClassUnitCell {...getCellParams('Tue4')} tabIndex={124} />
+ <ClassUnitCell {...getCellParams('Wed4')} tabIndex={134} />
+ <ClassUnitCell {...getCellParams('Thu4')} tabIndex={144} />
+ <ClassUnitCell {...getCellParams('Fri4')} tabIndex={154} />
+ </TableRow>
+ <TableRow>
+ <TableCell>5</TableCell>
+ <ClassUnitCell {...getCellParams('Mon5')} tabIndex={115} />
+ <ClassUnitCell {...getCellParams('Tue5')} tabIndex={125} />
+ <ClassUnitCell {...getCellParams('Wed5')} tabIndex={135} />
+ <ClassUnitCell {...getCellParams('Thu5')} tabIndex={145} />
+ <ClassUnitCell {...getCellParams('Fri5')} tabIndex={155} />
+ </TableRow>
+ <TableRow>
+ <TableCell>6</TableCell>
+ <ClassUnitCell {...getCellParams('Mon6')} tabIndex={116} />
+ <ClassUnitCell {...getCellParams('Tue6')} tabIndex={126} />
+ <ClassUnitCell {...getCellParams('Wed6')} tabIndex={136} />
+ <ClassUnitCell {...getCellParams('Thu6')} tabIndex={146} />
+ <ClassUnitCell {...getCellParams('Fri6')} tabIndex={156} />
+ </TableRow>
+ <TableRow>
+ <TableCell>7</TableCell>
+ <ClassUnitCell {...getCellParams('Mon7')} tabIndex={117} />
+ <ClassUnitCell {...getCellParams('Tue7')} tabIndex={127} />
+ <ClassUnitCell {...getCellParams('Wed7')} tabIndex={137} />
+ <TableCell></TableCell>
+ <ClassUnitCell {...getCellParams('Fri7')} tabIndex={157} />
+ </TableRow>
+ </TableBody>
+ </Table>
+ </TableContainer>
+ );
+};
+
+const StandardTimetableForm: FC<{ classId: number }> = ({ classId }) => {
+ const { standard, loading, set, submit } = useStandardTimetable({ classId });
+
+ // TODO: Backdrop を app 共通にして context or SWR で制御
+ return (
+ <>
+ <Backdrop open={loading}>
+ <CircularProgress color='inherit' />
+ </Backdrop>
+ {standard && <StandardTimetableTable data={standard} set={set} />}
+ {standard && (
+ <Button variant='contained' disabled={loading} sx={{ m: 4 }} endIcon={<SendIcon />} onClick={() => submit()}>
+ 送信
+ </Button>
+ )}
+ </>
+ );
+};
+
+const Page: NextPage = () => {
+ const { enqueueSnackbar } = useSnackbar();
+ const { data } = useAspidaSWR(apiClient.classes);
+
+ const classes = data ?? [];
+ const [classId, setClassId] = useState<number>();
+
+ // TODO: classes 取得時に Backdrop
+ // TODO: SWRConfig を _app.tsx へ
+ return (
+ <SWRConfig
+ value={{
+ onError() {
+ enqueueSnackbar('エラーが発生しました', { variant: 'error' });
+ },
+ }}
+ >
+ <Container maxWidth='lg'>
+ <Box width='7rem' m={4}>
+ <FormControl fullWidth>
+ <InputLabel>クラス</InputLabel>
+ <Select value={classId ?? ''} label='クラス' onChange={(e) => setClassId(e.target.value as number)}>
+ {classes.map((c) => (
+ <MenuItem value={c.id} key={c.id}>
+ {c.grade}-{c.class}
+ </MenuItem>
+ ))}
+ </Select>
+ </FormControl>
+ </Box>
+ {classId && <StandardTimetableForm classId={classId} />}
+ </Container>
+ </SWRConfig>
+ );
+};
+
+export default Page;