タスク管理ツールをReact + ASP.Coreで作る025スタイル修正

[前回](https://qiita.com/mosuma/items/deaa1b8b34c9d587ec2c)の続きです。


スタイル修正していきます



### ガイド
全体Index：[タスク管理ツールをReact + ASP.Coreで作る - Index](https://qiita.com/mosuma/items/88e40dc44e7bff1a54d8)

# 方針

筆者はCSS(というかWebデザイン全般)について知識がかなり乏しいので、簡易的な修正にとどめます。

最低限表示領域を見直して、不自然さを多少なりともなくして見やすくするという方針で修正を行いました。


## 変更点
bootstrapのサンプルページの[Offcanvas navbar](https://getbootstrap.com/docs/4.0/examples/offcanvas/)と[sign-in](https://getbootstrap.com/docs/4.0/examples/sign-in/)のページのcssをほぼそのまま参考に導入してスタイリングしました。



<details><summary>App.css(開くと表示)</summary><div>


```diff_css:App.css
html,
body {
  overflow-x: hidden; /* Prevent scroll on narrow devices */
}

@media (max-width: 991.98px) {
  .offcanvas-collapse {
    position: fixed;
    top: 56px; /* Height of navbar */
    bottom: 0;
    left: 100%;
    width: 100%;
    padding-right: 1rem;
    padding-left: 1rem;
    overflow-y: auto;
    visibility: hidden;
    background-color: #343a40;
    transition: transform .3s ease-in-out, visibility .3s ease-in-out;
  }
  .offcanvas-collapse.open {
    visibility: visible;
    transform: translateX(-100%);
  }
}

.nav-scroller {
  position: relative;
  z-index: 2;
  height: 2.75rem;
  overflow-y: hidden;
}

.nav-scroller .nav {
  display: flex;
  flex-wrap: nowrap;
  padding-bottom: 1rem;
  margin-top: -1px;
  overflow-x: auto;
  color: rgba(255, 255, 255, .75);
  text-align: center;
  white-space: nowrap;
  -webkit-overflow-scrolling: touch;
}

.nav-underline .nav-link {
  padding-top: .75rem;
  padding-bottom: .75rem;
  font-size: .875rem;
  color: #6c757d;
}

.nav-underline .nav-link:hover {
  color: #007bff;
}

.nav-underline .active {
  font-weight: 500;
  color: #343a40;
}

.text-white-50 { color: rgba(255, 255, 255, .5); }

.bg-purple { background-color: #6f42c1; }



.bd-placeholder-img {
  font-size: 1.125rem;
  text-anchor: middle;
  -webkit-user-select: none;
  -moz-user-select: none;
  user-select: none;
}

@media (min-width: 768px) {
  .bd-placeholder-img-lg {
    font-size: 3.5rem;
  }
}
```

</div></details>

<details><summary>signin.css(開くと表示)</summary><div>

```diff_css:signin.css
.form-signin {
  width: 100%;
  max-width: 330px;
  padding: 15px;
  margin: auto;
}

.form-signin .checkbox {
  font-weight: 400;
}

.form-signin .form-floating:focus-within {
  z-index: 2;
}

.form-signin input[type="email"] {
  margin-bottom: -1px;
  border-bottom-right-radius: 0;
  border-bottom-left-radius: 0;
}

.form-signin input[type="password"] {
  margin-bottom: 10px;
  border-top-left-radius: 0;
  border-top-right-radius: 0;
}

```



</div></details>



<details><summary>App.tsx(開くと表示)</summary><div>


```diff_tsx:App.tsx

import { useEffect, useState } from 'react';
import { Route, Routes } from 'react-router-dom';
import './App.css';
+import './signin.css';
import api from './app/api/api';
import { UserInfo } from './app/models/Account';
import Login from './components/Login';
import Register  from './components/Register';
import { RouteAuthChk } from './components/RouteAuthChk';
import { TaskOperationMain } from './components/TaskOperationMain';
import { NavBar } from './NavBar';

function App() {

  
  const [userInfo, setUserInfo] = useState<UserInfo>({username: '',email: '',token: ''});
  const [isFirstLoginChecked, setIsFirstLoginChecked] = useState(false);
  

  useEffect(() => {
    // add class to body element
 +   document.body.classList.add('bg-light');


    const token = window.localStorage.getItem('tasket_jwt_token');
    try{
      api.Account.current().then(user => {
        window.localStorage.setItem('tasket_jwt_token', user.token);
        setUserInfo(user);
        setIsFirstLoginChecked(true);
      }).catch(x=>setIsFirstLoginChecked(true))
      
    } catch (error) {
      
      console.log(error);
    }
  }, []);

  if(!isFirstLoginChecked) { return (<div>loading</div>) }

  
  return (
    <>
      <NavBar userInfo={userInfo} setUserInfo={setUserInfo} />
+     <main>
          <Routes>
              <Route path = '/' element={ <RouteAuthChk userInfo={userInfo} component={<TaskOperationMain />} redirect="/login" /> } />
              <Route path = '/task' element={ <RouteAuthChk userInfo={userInfo} component={<TaskOperationMain />} redirect="/login" /> } />
              <Route path = '/task/:id' element={ <RouteAuthChk userInfo={userInfo} component={<TaskOperationMain />} redirect="/login" /> } />
              <Route path = '/taskcreate' element={ <RouteAuthChk userInfo={userInfo} component={<TaskOperationMain />} redirect="/login" /> } />
              <Route path = '/login' element={<Login setUserInfo={setUserInfo} />} />
              <Route path = '/register' element={<Register />} />
          </Routes>
+     </main>
     </>
  );
}
export default App;

```



</div></details>




<details><summary>DateInputGeneral.tsx(開くと表示)</summary><div>

```diff_tsx:DateInputGeneral.tsx
import { useField } from "formik";
import React from "react";
import { Form } from "react-bootstrap";
import DatePicker, {ReactDatePickerProps} from 'react-datepicker';

export default function DateInputGeneral(props: Partial<ReactDatePickerProps>){
    const[field, meta, helpers] = useField(props.name!);
    return (
        <Form.Group>
+           { props.title && <Form.Label>{props.title}</Form.Label> }
+           <DatePicker className="form-control"
                {...field}
                {...props}
                selected={(field.value && new Date(field.value)) || null}
                onChange={value => helpers.setValue(value)}
            />
            {meta.touched && meta.error ? (
                <Form.Label basic color='red'>{meta.error}</Form.Label>
            ) : null}
        </Form.Group>
    )
}
```




</div></details>

<details><summary>Login.tsx(開くと表示)</summary><div>

```diff_tsx:Login.tsx
import { ErrorMessage,  Formik } from 'formik';
import React from 'react';
import { Form } from 'react-bootstrap';
import { useNavigate } from 'react-router-dom';
import * as Yup from 'yup';
import api from '../app/api/api';
import TextInputGeneral from '../app/common/TextInputGeneral';
import { UserInfo } from '../app/models/Account';



interface Props {
    setUserInfo: React.Dispatch<React.SetStateAction<UserInfo>>;
}


const Login = (
    {setUserInfo}: Props
    ) => 
{
    
    const navigate = useNavigate();
    
    return (
+       <div className='form-signin text-center'>

        <Formik
            initialValues={{email:'', password: '', error: null}}
            onSubmit={async (values, {setErrors}) => {
                const content = await api.Account.login(values).catch(error => 
                    setErrors({error:'Invalid email or password'}));
                    if(content){
                        window.localStorage.setItem('tasket_jwt_token', content.token);
                        setUserInfo(content);
                        navigate(`/task`);
                    }
                }
            }
            validationSchema={Yup.object({
                email: Yup.string().required().email(),
                password: Yup.string().required(),
            })}
            >
                {({handleSubmit, isSubmitting, errors, isValid}) =>(
                    <Form className="ui form" onSubmit={handleSubmit} autoComplete='off'>
                        <h3>Login</h3>
                        <TextInputGeneral name='email' placeholder="Email" />
                        <TextInputGeneral name='password' placeholder="Password" type="password" />
                        <ErrorMessage 
                            name='error' render={() => 
                                <Form.Label style = {{marginBottom:10}} basic color='red' >{errors.error}</Form.Label>
                        }
                        />
+                       <button disabled={!isValid || isSubmitting} type = 'submit' className="btn btn-lg btn-primary w-100">Login</button>
                    </Form>
                )}
            </Formik>
+       </div>
    );

}

export default Login;

```





</div></details>

<details><summary>Register.tsx(開くと表示)</summary><div>



```diff_tsx:Register.tsx
import { ErrorMessage, Formik } from 'formik';
import React from 'react';
import { Form, ListGroup } from 'react-bootstrap';
import TextInputGeneral from '../app/common/TextInputGeneral';
import * as Yup from 'yup';
import api from '../app/api/api';

const Register = () => {
    
    return (
+       <div className='form-signin text-center'>
        <Formik
            initialValues={{username: '', email:'', password: '', error: null}}
            onSubmit={(values, {setErrors}) => 
            api.Account.register(values).catch(error => 
                setErrors({error}))
           }
            validationSchema={Yup.object({
                username: Yup.string().required(),
                email: Yup.string().required().email(),
                password: Yup.string().required(),
            })}
            >
                {({handleSubmit, isSubmitting, errors, isValid, dirty}) =>(
                    <Form className="ui form error" onSubmit={handleSubmit} autoComplete='off'>
                        <h3>Sigh up to Tasket</h3>
                        <TextInputGeneral name='username' placeholder="User Name" />
                        <TextInputGeneral name='email' placeholder="Email" />
                        <TextInputGeneral name='password' placeholder="Password" type="password" />
                        <ErrorMessage 
                            name='error' render={() => 
                            <ValidationErrors errors = {errors.error} />}
                        />
                        <button disabled={!isValid || !dirty || isSubmitting} type = 'submit' className="btn btn-primary">Submit</button>
                    </Form>
                )}
            </Formik>
+       </div>
    );

}

export default Register;




interface Props {
    errors: any;
}
function ValidationErrors({errors}: Props){
    return (
    <>
        {errors && (
        <ListGroup>
            {errors.map((err: any, i: any) => (
                <ListGroup.Item key = {i} >{err}</ListGroup.Item>
            ))}
            </ListGroup>
        )}
    </>
    )
}
```



</div></details>

<details><summary>TaskEdit.tsx(開くと表示)</summary><div>

```diff_tsx:TaskEdit.tsx
import { Form, Formik } from "formik";
import { useEffect, useState } from "react";
import { Col, Row } from "react-bootstrap";
import * as Yup from 'yup';
import CheckBoxGeneral from "../app/common/CheckBoxGeneral";
import DateInputGeneral from "../app/common/DateInputGeneral";
import TextAreaGeneral from "../app/common/TextAreaGeneral";
import TextInputGeneral from "../app/common/TextInputGeneral";
import {v4} from 'uuid';
import api from "../app/api/api";
import { Task } from "../app/models/Task";



interface Props {
    isModeAddnew: boolean;
    id_task: string;
    setSelectedId_task: React.Dispatch<React.SetStateAction<string>>;
}

export const TaskEdit = ({isModeAddnew, id_task, setSelectedId_task}: Props) => {    
    
    const [task, setTask] = useState<Task>();

    useEffect(() => {
        if(id_task !== ""){
            loadTaskDetails();
        } else {
            setTask({id_task : "", title : "", is_finish: false, description:"", end_date_scheduled : null, end_date_actual : null})
        }
    }, [id_task]);
  
    const loadTaskDetails = async () => {
        const data = await api.Tasks.details(id_task);
        setTask(data);
    };


    const updateTaskDetails = async (value : Task) => {

        if(value.id_task===""){
            const newTask = value;
            newTask.id_task=v4();
            const data = await api.Tasks.create(newTask);
            setTask(data);

        } else {
            const data = await api.Tasks.update(value);
            setTask(data);
        }
    };


    const deleteTask = async (value : Task) => {

        if(value.id_task!==""){
            const data = await api.Tasks.delete(value.id_task);
            setSelectedId_task("");
        }
    };
    
    const validationSchema = Yup.object({
        title: Yup.string().required(),
        is_finish: Yup.bool().required(),
        description: Yup.string().nullable(),
        end_date_scheduled: Yup.date().nullable(),
        end_date_actual: Yup.date().nullable(),
    });
    
    const validationSchemaDel = Yup.object({
        id_task: Yup.string().required(),
    });



    return (
        <div>
            {
                isModeAddnew ?
                    <h3>Add New Task</h3>
                    :
                    <h3>Task Detail : {task?.title}</h3>
            }
            { task &&
            <div>
                <Formik
                validationSchema={validationSchema}
                enableReinitialize 
                initialValues={task} 
                onSubmit={values => updateTaskDetails(values)}>
                {({ handleSubmit, isValid, isSubmitting, dirty }) => (
                    <Form className="ui form" onSubmit = {handleSubmit} autoComplete='off'>

+                       <Row className="my-4">
                            <Col><TextInputGeneral label='Title' name='title' placeholder='Title' /></Col>
                        </Row>

                        
+                       <Row className="my-4">
                            <Col ><TextAreaGeneral label='Description' placeholder='Description' name='description' rows={3}   /></Col>
                        </Row>
                        
+                       <Row className="my-4">
+                           <Col ><DateInputGeneral title="Due Date" placeholderText='Due Date' name = 'end_date_scheduled' dateFormat='MM d, yyyy' /></Col>
+                           <Col ><DateInputGeneral title="Completion Date" placeholderText='Completion Date' name = 'end_date_actual' dateFormat='MM d, yyyy' /></Col>
                        </Row>


                        
+                       <Row className="my-4">
                            <Col xs={4}><CheckBoxGeneral label='Finished' name='is_finish'  /></Col>
                        </Row>
                        
                        <hr />
+                       <button disabled={!isValid || !dirty || isSubmitting} type = 'submit' className='btn btn-primary float-end'>
                            {isSubmitting ? "Processing" : "submit"}
                        </button>
                    </Form>
                )}

                </Formik>
                {
                !isModeAddnew &&
                <Formik
                validationSchema={validationSchemaDel}
                enableReinitialize 
                initialValues={task} 
                onSubmit={values => deleteTask(values)}>
                {({ handleSubmit, isValid, isSubmitting, dirty }) => (
                    <Form className="ui form" onSubmit = {handleSubmit} autoComplete='off'>                        
+                       <button disabled={!isValid || isSubmitting} type = 'submit' className='btn btn-danger float-end'>
                            {isSubmitting ? "Processing" : "Delete"}
                        </button>
                    </Form>
                )}
                </Formik>
                }
            </div>

            }
        </div>

        

    )
}
```


</div></details>

<details><summary>TaskList.tsx(開くと表示)</summary><div>



```diff_tsx:TaskList.tsx
import { useEffect, useState } from 'react';
import { Button, Table } from 'react-bootstrap';
import { useNavigate } from 'react-router-dom';
import api from '../app/api/api';
import { Task } from '../app/models/Task';



interface Props {
  selectedId_task: string;
  setIsModeAddnew: React.Dispatch<React.SetStateAction<boolean>>;
  setSelectedId_task: React.Dispatch<React.SetStateAction<string>>;
}


export const TaskList = ({setIsModeAddnew, selectedId_task, setSelectedId_task}: Props) => {
  
    const navigate = useNavigate();
    
    const [loading, setLoading] = useState(true);
    const [tasks, setTasks] = useState<Task[]>();
  
    useEffect(() => {
        populateWeatherData();
    }, []);
  
    const populateWeatherData = async () => {
        const data = await api.Tasks.index();
        setTasks(data);
        setLoading(false);
    };
    
    if(loading) return <div>loading....</div>

    return (
        <div>
-           <h1 id="tabelLabel">Task List</h1>
-           <p>This component demonstrates fetching data from the server.</p>
            <Table >
              <thead>
                <tr>
                  <th>No.</th>
                  <th>Fin.</th>
                  <th>Title</th>
                  <th>Due Date</th>
                </tr>
              </thead>
              <tbody>
                {tasks && tasks.map((task, index) => (
                  <tr 
                    key={task.id_task} onClick={()=>{setIsModeAddnew(false); setSelectedId_task(task.id_task);
                      navigate(`/task/${task.id_task}`);
                    }}  
                    className={ selectedId_task === task.id_task ? "table-info" :  ""}                  
                  >
                    <td>{index+1}</td>
                    <td><input type="checkbox" defaultChecked={task.is_finish} disabled /></td>
                    <td>{task.title}</td>
                    <td>{task.end_date_scheduled && (new Date(task.end_date_scheduled).toDateString())}</td>
                  </tr>
                ))}
              </tbody>
            </Table>
        </div>
    )
}
```



</div></details>

<details><summary>TaskOperationMain.tsx(開くと表示)</summary><div>



```diff_tsx:TaskOperationMain.tsx
import { useEffect, useState } from "react";
import { Button, Col, Row } from "react-bootstrap"
import { useParams } from "react-router-dom";
import { TaskEdit } from "./TaskEdit"
import { TaskList } from "./TaskList"


export const TaskOperationMain = () => {
    
    const {id} = useParams<{id:string}>();
    
    useEffect(()=> {

        if(id) {
            setSelectedId_task(id);
        } else {
            setSelectedId_task("");
        }

    }, [id])
    
    const [isModeAddnew, setIsModeAddnew] = useState(false);
    const [selectedId_task, setSelectedId_task] = useState("");

    return (
+       <div className="mx-5">
            

            <Row>
+               <Col className="m-2 my-3 p-4 bg-white rounded shadow-sm">
+                   <h3 id="tabelLabel">Task List</h3>
+                   <Button className="shadow-sm float-end" variant="primary" onClick={()=>{setIsModeAddnew(true); setSelectedId_task("")}}>Add New Task</Button>
                    <TaskList setIsModeAddnew={setIsModeAddnew} selectedId_task={selectedId_task} setSelectedId_task={setSelectedId_task}/>
                </Col>
+               <Col className="m-2 my-3 p-4 bg-white rounded shadow-sm">
                    {
                        (isModeAddnew || selectedId_task !== "") &&
                            <TaskEdit isModeAddnew={isModeAddnew} id_task={selectedId_task} setSelectedId_task={setSelectedId_task} />
                    }
                </Col>
            </Row>
        </div>
    )
}
```



</div></details>


コード実行結果

### 以下の様になります

<iframe width="560" height="315" src="https://www.youtube.com/embed/T4bt3S5mVKo" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>