Browse Source

Do todos

master
Maya Herrscher 3 years ago
parent
commit
ea59db58d3
  1. 52
      package-lock.json
  2. 2
      package.json
  3. 6
      public/index.html
  4. 165
      src/App.tsx
  5. 183
      src/components/Todos.tsx
  6. 15
      src/hooks/useLocalStorage.tsx

52
package-lock.json

@ -11,6 +11,7 @@
"@date-io/dayjs": "^2.13.1", "@date-io/dayjs": "^2.13.1",
"@emotion/react": "^11.9.0", "@emotion/react": "^11.9.0",
"@emotion/styled": "^11.8.1", "@emotion/styled": "^11.8.1",
"@mui/icons-material": "^5.6.2",
"@mui/material": "^5.6.2", "@mui/material": "^5.6.2",
"@mui/x-date-pickers": "^5.0.0-alpha.1", "@mui/x-date-pickers": "^5.0.0-alpha.1",
"@testing-library/jest-dom": "^5.16.4", "@testing-library/jest-dom": "^5.16.4",
@ -20,6 +21,7 @@
"@types/node": "^16.11.27", "@types/node": "^16.11.27",
"@types/react": "^18.0.6", "@types/react": "^18.0.6",
"@types/react-dom": "^18.0.2", "@types/react-dom": "^18.0.2",
"date-fns": "^2.28.0",
"dayjs": "^1.11.1", "dayjs": "^1.11.1",
"react": "^18.0.0", "react": "^18.0.0",
"react-dom": "^18.0.0", "react-dom": "^18.0.0",
@ -3049,6 +3051,31 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
}, },
"node_modules/@mui/icons-material": {
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.6.2.tgz",
"integrity": "sha512-9QdI7axKuBAyaGz4mtdi7Uy1j73/thqFmEuxpJHxNC7O8ADEK1Da3t2veK2tgmsXsUlAHcAG63gg+GvWWeQNqQ==",
"dependencies": {
"@babel/runtime": "^7.17.2"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui"
},
"peerDependencies": {
"@mui/material": "^5.0.0",
"@types/react": "^17.0.0 || ^18.0.0",
"react": "^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/material": { "node_modules/@mui/material": {
"version": "5.6.2", "version": "5.6.2",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-5.6.2.tgz", "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.6.2.tgz",
@ -6410,6 +6437,18 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/date-fns": {
"version": "2.28.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.28.0.tgz",
"integrity": "sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==",
"engines": {
"node": ">=0.11"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/date-fns"
}
},
"node_modules/dayjs": { "node_modules/dayjs": {
"version": "1.11.1", "version": "1.11.1",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.1.tgz", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.1.tgz",
@ -18733,6 +18772,14 @@
} }
} }
}, },
"@mui/icons-material": {
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.6.2.tgz",
"integrity": "sha512-9QdI7axKuBAyaGz4mtdi7Uy1j73/thqFmEuxpJHxNC7O8ADEK1Da3t2veK2tgmsXsUlAHcAG63gg+GvWWeQNqQ==",
"requires": {
"@babel/runtime": "^7.17.2"
}
},
"@mui/material": { "@mui/material": {
"version": "5.6.2", "version": "5.6.2",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-5.6.2.tgz", "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.6.2.tgz",
@ -21132,6 +21179,11 @@
"whatwg-url": "^8.0.0" "whatwg-url": "^8.0.0"
} }
}, },
"date-fns": {
"version": "2.28.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.28.0.tgz",
"integrity": "sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw=="
},
"dayjs": { "dayjs": {
"version": "1.11.1", "version": "1.11.1",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.1.tgz", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.1.tgz",

2
package.json

@ -6,6 +6,7 @@
"@date-io/dayjs": "^2.13.1", "@date-io/dayjs": "^2.13.1",
"@emotion/react": "^11.9.0", "@emotion/react": "^11.9.0",
"@emotion/styled": "^11.8.1", "@emotion/styled": "^11.8.1",
"@mui/icons-material": "^5.6.2",
"@mui/material": "^5.6.2", "@mui/material": "^5.6.2",
"@mui/x-date-pickers": "^5.0.0-alpha.1", "@mui/x-date-pickers": "^5.0.0-alpha.1",
"@testing-library/jest-dom": "^5.16.4", "@testing-library/jest-dom": "^5.16.4",
@ -15,6 +16,7 @@
"@types/node": "^16.11.27", "@types/node": "^16.11.27",
"@types/react": "^18.0.6", "@types/react": "^18.0.6",
"@types/react-dom": "^18.0.2", "@types/react-dom": "^18.0.2",
"date-fns": "^2.28.0",
"dayjs": "^1.11.1", "dayjs": "^1.11.1",
"react": "^18.0.0", "react": "^18.0.0",
"react-dom": "^18.0.0", "react-dom": "^18.0.0",

6
public/index.html

@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en" style="height: 100%">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
@ -26,9 +26,9 @@
--> -->
<title>React App</title> <title>React App</title>
</head> </head>
<body> <body style="height: 100%">
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div> <div id="root" style="height: 100%"></div>
<!-- <!--
This HTML file is a template. This HTML file is a template.
If you open it directly in the browser, you will see an empty page. If you open it directly in the browser, you will see an empty page.

165
src/App.tsx

@ -1,158 +1,36 @@
import React, { useState } from 'react'; import React, { useEffect, useState } from 'react';
import './App.css'; import './App.css';
import { AppBar, Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, Grid, TextField, Toolbar, Typography } from '@mui/material'; import deLocale from 'date-fns/locale/de';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { AppBar, Box, Button, Paper, Toolbar, Typography } from '@mui/material';
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import dayjs from 'dayjs'; import { Todos } from './components/Todos';
import { DatePicker } from '@mui/x-date-pickers';
type Todo = {
name: string,
id: string,
done: boolean
}
function TodoItem({todo,setDone}: {todo: Todo, setDone: (done: boolean) => void}){
return (
<li>
{todo.name}
<input type="checkbox" checked={todo.done} onChange={(e) => setDone(e.target.checked)}></input>
</li>
)
}
function ChangeTodo({todo, setTodo, handleCancel}: {todo: Todo, setTodo: (todo: Todo) => void, handleCancel: () => void}){
const [name, setName] = React.useState<string>(todo.name)
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setName(event.target.value);
};
const [id, setId] = React.useState<string>(todo.id)
const [done, setDone] = React.useState<boolean>(todo.done)
const [from, setFrom] = React.useState<Date | null>(
new Date('2022-04-18T21:11:54'),
);
const handleFrom = (newValue: Date | null) => {
setFrom(newValue);
};
const [to, setTo] = React.useState<Date | null>(
new Date('2022-05-18T21:11:54'),
);
const handleTo = (newValue: Date | null) => {
setTo(newValue);
};
const saveTodo=() => {
const newtodo = {
name: name,
id: id === "" ? Math.random().toString() : id,
done: done
};
setTodo(newtodo);
}
return (
<>
<DialogContent>
<TextField
id="name"
value={name}
label="Name"
onChange={handleChange}
/>
<TextField
id="description"
label="Description"
/>
<LocalizationProvider dateAdapter={AdapterDayjs} locale={dayjs().locale('de')}>
<DatePicker
mask={'__.__.____'}
label="From:"
value={from}
onChange={handleFrom}
renderInput={(params: any) => <TextField {...params} />}
/>
</LocalizationProvider>
<LocalizationProvider dateAdapter={AdapterDayjs} locale={dayjs().locale('de')}>
<DatePicker
mask={'__.__.____'}
label="To:"
value={to}
onChange={handleTo}
renderInput={(params: any) => <TextField {...params} />}
/>
</LocalizationProvider>
</DialogContent>
<DialogActions>
<Button onClick={handleCancel}>Cancel</Button>
<Button onClick={saveTodo}>Save</Button>
</DialogActions>
</>
)
}
function Todos() {
const [todos, setTodos] = useState<Todo[]>([]);
const setDone=(i: number,todo: Todo) => (done: boolean) => setTodos([...todos.slice(0,i),{name: todo.name, id: todo.id, done: done},...todos.slice(i+1)]);
const setTodo= (todo: Todo) => {
handleClose();
setTodos([...todos, todo]);
}
const [open, setOpen] = useState(false);
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
return (
<div>
<Typography variant="h2">ToDo's</Typography>
<ul>
{todos.map((todo, i) =>
<TodoItem key={todo.id} todo={todo} setDone={setDone(i, todo)}></TodoItem>
)}
</ul>
<Button onClick={handleClickOpen}>Add Todo</Button>
<Dialog open={open} onClose={handleClose}>
<DialogTitle>New Todo</DialogTitle>
<ChangeTodo todo={{id: "", name: "", done: false}} setTodo={setTodo} handleCancel={handleClose}></ChangeTodo>
</Dialog>
</div>
)
}
function Calendar(){ function Calendar(){
return( return(
<Typography variant="h2">Calendar</Typography> <Typography variant="h3">Calendar</Typography>
) )
} }
function Dinos(){ function Dinos(){
return( return(
<Typography variant="h2">Dinos</Typography> <div>
<Typography variant="h3">Dinos</Typography>
</div>
) )
} }
function Overview(){ function Overview(){
return ( return (
<div> <div style={{display: "flex", flexGrow: 1, flexDirection: "column", height: "100%", maxHeight: "100%"}}>
<Typography variant="h2">Overview</Typography> <div><Typography variant="h2">Overview</Typography></div>
<Grid <Box sx={{display: "flex", flexGrow: 1, justifyContent: "space-evenly", gap: 4, height: "100%", minHeight: 0}}>
container <Paper sx={{overflowY: "auto", width: "100%", padding: 2, height: "calc(100% - 32px)"}}><Todos/></Paper>
justifyContent="space-evenly" <Box sx={{width: "100%", display: "flex", flexDirection: "column", justifyContent: "space-evenly"}}>
alignItems="flex-start" <Calendar/>
spacing={2}> <Calendar/>
<Todos/> </Box>
<Todos/> </Box>
</Grid>
</div> </div>
) )
} }
@ -163,6 +41,7 @@ function App() {
const [page,setPage] = useState<string>("Overview"); const [page,setPage] = useState<string>("Overview");
return ( return (
<> <>
<LocalizationProvider dateAdapter={AdapterDateFns} locale={deLocale}>
<AppBar> <AppBar>
<Toolbar> <Toolbar>
<Typography variant="h5" <Typography variant="h5"
@ -181,10 +60,16 @@ function App() {
</Box> </Box>
</Toolbar> </Toolbar>
</AppBar> </AppBar>
<div style={{display: "flex", flexDirection: "column", gap: 2, height: "100%", maxHeight: "100%"}}>
<Toolbar sx={{flexGrow: 1}}/>
<div style={{marginLeft: "1em", marginRight: "1em", flexGrow: 1, height: "100%", maxHeight: "100%", minHeight: 0}}>
{page === "Overview" && <Overview/>} {page === "Overview" && <Overview/>}
{page === "Todos" && <Todos/>} {page === "Todos" && <Todos/>}
{page === "Calendar" && <Calendar/>} {page === "Calendar" && <Calendar/>}
{page === "Dinos" && <Dinos/>} {page === "Dinos" && <Dinos/>}
</div>
</div>
</LocalizationProvider>
</> </>
); );
} }

183
src/components/Todos.tsx

@ -0,0 +1,183 @@
import React, {useState } from 'react';
import { Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, Divider, List, ListItem, TextField, Typography } from '@mui/material';
import { DatePicker } from '@mui/x-date-pickers';
import DeleteOutlineRoundedIcon from '@mui/icons-material/DeleteOutlineRounded';
import EditRoundedIcon from '@mui/icons-material/EditRounded';
import { useLocalStorage } from '../hooks/useLocalStorage';
type Todo = {
id: string,
name: string,
done: boolean,
description: string,
from?: string,
to?: string,
}
function TodoItem({todo}: {todo: Todo}){
return (
<ListItem>
<Box sx={{display: "flex", flexDirection: "column"}}>
<Typography variant="h5">{todo.name}</Typography>
{todo.description && <span>Description: {todo.description}</span>}
{todo.from && <span>From: {(new Date(todo.from)).toLocaleDateString()}</span>}
{todo.to && <span>To: {(new Date(todo.to)).toLocaleDateString()}</span>}
</Box>
</ListItem>
)
}
function ChangeTodo({todo, setTodo, handleCancel}: {todo: Todo, setTodo: (todo: Todo) => void, handleCancel: () => void}){
const [name, setName] = React.useState<string>(todo.name)
const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setName(event.target.value);
};
const [id, setId] = React.useState<string>(todo.id)
const [description, setDescription] = React.useState<string>(todo.description)
const handleDescChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setDescription(event.target.value);
};
const [done, setDone] = React.useState<boolean>(todo.done)
const [from, setFrom] = React.useState<Date | null>(todo.from ? new Date(todo.from) : null);
const handleFrom = (newValue: Date | null) => {
setFrom(newValue);
};
const [to, setTo] = React.useState<Date | null>(todo.to ? new Date(todo.to) : null);
const handleTo = (newValue: Date | null) => {
setTo(newValue);
};
const saveTodo=() => {
const newtodo = {
name: name,
id: id === "" ? Math.random().toString() : id,
done: done,
description: description,
to: to?.toISOString(),
from: from?.toISOString()
};
setTodo(newtodo);
}
return (
<>
<DialogContent>
<Box component="form"
sx={{
'& > :not(style)': { m: 2, width: '50ch' },
}}>
<TextField
id="name"
value={name}
label="Name"
onChange={handleNameChange}
/>
<TextField
id="description"
value={description}
label="Description"
onChange={handleDescChange}
/>
<DatePicker
mask={'__.__.____'}
label="From:"
value={from}
onChange={handleFrom}
renderInput={(params: any) => <TextField {...params} />}
/>
<DatePicker
mask={'__.__.____'}
label="To:"
value={to}
onChange={handleTo}
renderInput={(params: any) => <TextField {...params} />}
/>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={handleCancel}>Cancel</Button>
<Button onClick={saveTodo}>Save</Button>
</DialogActions>
</>
)
}
export function Todos({style}: {style?: any}) {
const [todos, setTodos] = useLocalStorage<Todo[]>("todos",[]);
const setDone=(i: number,todo: Todo, done: boolean) =>
setTodos([...todos.slice(0,i),
{name: todo.name, id: todo.id, done: done, description: todo.description, to: todo.to, from: todo.from},
...todos.slice(i+1)]);
const deleteTodo=(i: number,todo: Todo) =>
setTodos([...todos.slice(0,i),
...todos.slice(i+1)]);
const setTodo= (todo: Todo) => {
handleClose();
setTodos([...todos, todo]);
}
const [open, setOpen] = useState(false);
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
const [open2, setOpen2] = useState(false);
const [edit, setEdit] = useState<Todo>({name: "", description: "", done: false, id: ""});
const [editI, setEditI] = useState<number>(0);
const handleClickOpen2 = (todo: Todo, i: number) => {
setOpen2(true);
setEdit(todo);
setEditI(i);
};
const handleClose2 = () => {
setOpen2(false);
};
const editTodo= (todo: Todo) => {
handleClose2();
setTodos([...todos.slice(0,editI),
todo,
...todos.slice(editI+1)]);
}
return (
<div style={{...style, textAlign: "left"}}>
<Typography variant="h3">ToDo's</Typography>
<Button onClick={handleClickOpen}>Add Todo</Button>
<List>
{todos.map((todo, i) =>
<>
<Box sx={{display: "flex", flexDirection: "columns"}}>
<TodoItem key={todo.id} todo={todo}/>
<input type="checkbox" checked={todo.done} onChange={(e) => setDone(i, todo,e.target.checked)}></input>
<Button onClick={() => handleClickOpen2(todo, i)}><EditRoundedIcon/></Button>
<Button onClick={() => deleteTodo}><DeleteOutlineRoundedIcon/></Button>
</Box>
{(i + 1) < todos.length && <Divider/>}
</>
)}
</List>
<Dialog open={open} onClose={handleClose}>
<DialogTitle>New Todo</DialogTitle>
<ChangeTodo todo={{id: "", name: "", done: false, description: ""}} setTodo={setTodo} handleCancel={handleClose}></ChangeTodo>
</Dialog>
<Dialog open={open2} onClose={handleClose2}>
<DialogTitle>Edit Todo</DialogTitle>
<ChangeTodo todo={edit} setTodo={editTodo} handleCancel={handleClose2}></ChangeTodo>
</Dialog>
</div>
)
}

15
src/hooks/useLocalStorage.tsx

@ -0,0 +1,15 @@
import React, { useEffect, useState } from 'react';
function useLocalStorage<T>(storageKey: string, fallbackState: T): [T, (val: T) => void] {
const [value, setValue] = useState(
JSON.parse(localStorage.getItem(storageKey)!) ?? fallbackState
);
useEffect(() => {
localStorage.setItem(storageKey, JSON.stringify(value))
}, [value, storageKey]);
return [value, setValue];
}
export {useLocalStorage}
Loading…
Cancel
Save