mirror of
https://github.com/sergeyyarkov/todo-mobx-app.git
synced 2026-10-11 02:19:03 +03:00
init
This commit is contained in:
parent
d15a889a58
commit
0382fbf205
16583
package-lock.json
generated
Normal file
16583
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -3,6 +3,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@chakra-ui/icons": "^1.0.15",
|
||||||
"@chakra-ui/react": "^1.0.0",
|
"@chakra-ui/react": "^1.0.0",
|
||||||
"@emotion/react": "^11.0.0",
|
"@emotion/react": "^11.0.0",
|
||||||
"@emotion/styled": "^11.0.0",
|
"@emotion/styled": "^11.0.0",
|
||||||
@ -14,6 +15,8 @@
|
|||||||
"@types/react": "^16.9.0",
|
"@types/react": "^16.9.0",
|
||||||
"@types/react-dom": "^16.9.0",
|
"@types/react-dom": "^16.9.0",
|
||||||
"framer-motion": "^4.0.0",
|
"framer-motion": "^4.0.0",
|
||||||
|
"mobx": "^6.3.2",
|
||||||
|
"mobx-react": "^7.2.0",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-icons": "^3.0.0",
|
"react-icons": "^3.0.0",
|
||||||
|
|||||||
@ -1,10 +0,0 @@
|
|||||||
import React from "react"
|
|
||||||
import { screen } from "@testing-library/react"
|
|
||||||
import { render } from "./test-utils"
|
|
||||||
import { App } from "./App"
|
|
||||||
|
|
||||||
test("renders learn react link", () => {
|
|
||||||
render(<App />)
|
|
||||||
const linkElement = screen.getByText(/learn chakra/i)
|
|
||||||
expect(linkElement).toBeInTheDocument()
|
|
||||||
})
|
|
||||||
57
src/App.tsx
57
src/App.tsx
@ -1,38 +1,21 @@
|
|||||||
import * as React from "react"
|
import * as React from 'react';
|
||||||
import {
|
import { ChakraProvider, Box, theme } from '@chakra-ui/react';
|
||||||
ChakraProvider,
|
|
||||||
Box,
|
|
||||||
Text,
|
|
||||||
Link,
|
|
||||||
VStack,
|
|
||||||
Code,
|
|
||||||
Grid,
|
|
||||||
theme,
|
|
||||||
} from "@chakra-ui/react"
|
|
||||||
import { ColorModeSwitcher } from "./ColorModeSwitcher"
|
|
||||||
import { Logo } from "./Logo"
|
|
||||||
|
|
||||||
export const App = () => (
|
/**
|
||||||
<ChakraProvider theme={theme}>
|
* Components
|
||||||
<Box textAlign="center" fontSize="xl">
|
*/
|
||||||
<Grid minH="100vh" p={3}>
|
import TodoInput from './components/TodoInput';
|
||||||
<ColorModeSwitcher justifySelf="flex-end" />
|
import TodoHeader from './components/TodoHeader';
|
||||||
<VStack spacing={8}>
|
import TodoList from './components/TodoList';
|
||||||
<Logo h="40vmin" pointerEvents="none" />
|
|
||||||
<Text>
|
export const App = () => {
|
||||||
Edit <Code fontSize="xl">src/App.tsx</Code> and save to reload.
|
return (
|
||||||
</Text>
|
<ChakraProvider theme={theme}>
|
||||||
<Link
|
<Box maxW='800px' mt='5' mr='auto' ml='auto'>
|
||||||
color="teal.500"
|
<TodoHeader />
|
||||||
href="https://chakra-ui.com"
|
<TodoInput />
|
||||||
fontSize="2xl"
|
<TodoList />
|
||||||
target="_blank"
|
</Box>
|
||||||
rel="noopener noreferrer"
|
</ChakraProvider>
|
||||||
>
|
);
|
||||||
Learn Chakra
|
};
|
||||||
</Link>
|
|
||||||
</VStack>
|
|
||||||
</Grid>
|
|
||||||
</Box>
|
|
||||||
</ChakraProvider>
|
|
||||||
)
|
|
||||||
|
|||||||
24
src/Logo.tsx
24
src/Logo.tsx
@ -1,24 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import {
|
|
||||||
chakra,
|
|
||||||
keyframes,
|
|
||||||
ImageProps,
|
|
||||||
forwardRef,
|
|
||||||
usePrefersReducedMotion,
|
|
||||||
} from "@chakra-ui/react"
|
|
||||||
import logo from "./logo.svg"
|
|
||||||
|
|
||||||
const spin = keyframes`
|
|
||||||
from { transform: rotate(0deg); }
|
|
||||||
to { transform: rotate(360deg); }
|
|
||||||
`
|
|
||||||
|
|
||||||
export const Logo = forwardRef<ImageProps, "img">((props, ref) => {
|
|
||||||
const prefersReducedMotion = usePrefersReducedMotion()
|
|
||||||
|
|
||||||
const animation = prefersReducedMotion
|
|
||||||
? undefined
|
|
||||||
: `${spin} infinite 20s linear`
|
|
||||||
|
|
||||||
return <chakra.img animation={animation} src={logo} ref={ref} {...props} />
|
|
||||||
})
|
|
||||||
33
src/components/EditableTodoButton.tsx
Normal file
33
src/components/EditableTodoButton.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useEditableControls, IconButton } from '@chakra-ui/react';
|
||||||
|
import { EditIcon, CheckIcon } from '@chakra-ui/icons';
|
||||||
|
|
||||||
|
const EditableTodoButton: React.FC = () => {
|
||||||
|
const { isEditing, getSubmitButtonProps, getEditButtonProps } =
|
||||||
|
useEditableControls();
|
||||||
|
|
||||||
|
if (isEditing) {
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
icon={<CheckIcon />}
|
||||||
|
mr='1'
|
||||||
|
ml='1'
|
||||||
|
aria-label='Update'
|
||||||
|
onClick={() => console.log('123')}
|
||||||
|
{...getSubmitButtonProps()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
icon={<EditIcon />}
|
||||||
|
mr='1'
|
||||||
|
ml='1'
|
||||||
|
aria-label='Edit'
|
||||||
|
{...getEditButtonProps()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditableTodoButton;
|
||||||
16
src/components/TodoHeader.tsx
Normal file
16
src/components/TodoHeader.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { Flex, Heading } from '@chakra-ui/react';
|
||||||
|
import { ColorModeSwitcher } from '../components/ColorModeSwitcher';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const TodoHeader: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<Flex justifyContent='space-between'>
|
||||||
|
<Heading bgGradient='linear(to-l, #7928CA,#FF0080)' bgClip='text'>
|
||||||
|
Todo App
|
||||||
|
</Heading>
|
||||||
|
<ColorModeSwitcher justifySelf='flex-end' />
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TodoHeader;
|
||||||
43
src/components/TodoInput.tsx
Normal file
43
src/components/TodoInput.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { Box, Button, Flex, FormControl, Input } from '@chakra-ui/react';
|
||||||
|
import React from 'react';
|
||||||
|
import { useStores } from '../hooks/useStores';
|
||||||
|
|
||||||
|
const TodoInput: React.FC = () => {
|
||||||
|
const [formState, setformState] = React.useState<{ title: string }>({
|
||||||
|
title: '',
|
||||||
|
});
|
||||||
|
const { todoStore } = useStores();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box mt='5'>
|
||||||
|
<form
|
||||||
|
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
todoStore.createTodo({
|
||||||
|
id: todoStore.todos.length + 1,
|
||||||
|
title: formState.title,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
});
|
||||||
|
}}>
|
||||||
|
<FormControl>
|
||||||
|
<Flex>
|
||||||
|
<Input
|
||||||
|
placeholder='Add new todo...'
|
||||||
|
size='md'
|
||||||
|
mr='2'
|
||||||
|
value={formState.title}
|
||||||
|
onChange={(e) => setformState({ title: e.target.value })}
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
<Button type='submit' size='md'>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</FormControl>
|
||||||
|
</form>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TodoInput;
|
||||||
49
src/components/TodoItem.tsx
Normal file
49
src/components/TodoItem.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Flex,
|
||||||
|
Editable,
|
||||||
|
EditableInput,
|
||||||
|
EditablePreview,
|
||||||
|
IconButton,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { DeleteIcon } from '@chakra-ui/icons';
|
||||||
|
import EditableTodoButton from './EditableTodoButton';
|
||||||
|
import ITodoData from '../interfaces';
|
||||||
|
import { useStores } from '../hooks/useStores';
|
||||||
|
|
||||||
|
const TodoItem: React.FC<{ data: ITodoData }> = ({ data }) => {
|
||||||
|
const { todoStore } = useStores();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Editable
|
||||||
|
textAlign='left'
|
||||||
|
fontSize='md'
|
||||||
|
defaultValue={data.title}
|
||||||
|
onChange={(title) =>
|
||||||
|
todoStore.updateTodo(data.id, { title, updatedAt: new Date() })
|
||||||
|
}
|
||||||
|
isPreviewFocusable={true}>
|
||||||
|
<Flex
|
||||||
|
borderRadius='12px'
|
||||||
|
justifyContent='space-between'
|
||||||
|
alignItems='center'
|
||||||
|
border='1px'
|
||||||
|
borderColor='gray.300'
|
||||||
|
p='2'
|
||||||
|
mb='1.5'>
|
||||||
|
<EditablePreview fontSize='lg' />
|
||||||
|
<EditableInput fontSize='lg' value={data.title} />
|
||||||
|
<Flex>
|
||||||
|
<EditableTodoButton />
|
||||||
|
<IconButton
|
||||||
|
aria-label='Delete'
|
||||||
|
icon={<DeleteIcon />}
|
||||||
|
onClick={() => todoStore.deleteTodo(data.id)}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Editable>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TodoItem;
|
||||||
18
src/components/TodoList.tsx
Normal file
18
src/components/TodoList.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { observer } from 'mobx-react';
|
||||||
|
import { Flex } from '@chakra-ui/react';
|
||||||
|
import React from 'react';
|
||||||
|
import TodoItem from './TodoItem';
|
||||||
|
import { useStores } from '../hooks/useStores';
|
||||||
|
|
||||||
|
const TodoList: React.FC = () => {
|
||||||
|
const { todoStore } = useStores();
|
||||||
|
return (
|
||||||
|
<Flex flexDir='column' justifyContent='space-between' mt='5'>
|
||||||
|
{todoStore.todos.map((todo) => {
|
||||||
|
return <TodoItem key={todo.id} data={todo} />;
|
||||||
|
})}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default observer(TodoList);
|
||||||
6
src/contexts/index.ts
Normal file
6
src/contexts/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import TodoStore from '../stores/TodoStore';
|
||||||
|
|
||||||
|
export const storesContext = React.createContext({
|
||||||
|
todoStore: new TodoStore(),
|
||||||
|
});
|
||||||
4
src/hooks/useStores.ts
Normal file
4
src/hooks/useStores.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { storesContext } from '../contexts';
|
||||||
|
|
||||||
|
export const useStores = () => React.useContext(storesContext);
|
||||||
@ -1,24 +1,24 @@
|
|||||||
import { ColorModeScript } from "@chakra-ui/react"
|
import { ColorModeScript } from '@chakra-ui/react';
|
||||||
import * as React from "react"
|
import * as React from 'react';
|
||||||
import ReactDOM from "react-dom"
|
import ReactDOM from 'react-dom';
|
||||||
import { App } from "./App"
|
import { App } from './App';
|
||||||
import reportWebVitals from "./reportWebVitals"
|
import reportWebVitals from './reportWebVitals';
|
||||||
import * as serviceWorker from "./serviceWorker"
|
import * as serviceWorker from './serviceWorker';
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<ColorModeScript />
|
<ColorModeScript />
|
||||||
<App />
|
<App />
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
document.getElementById("root"),
|
document.getElementById('root')
|
||||||
)
|
);
|
||||||
|
|
||||||
// If you want your app to work offline and load faster, you can change
|
// If you want your app to work offline and load faster, you can change
|
||||||
// unregister() to register() below. Note this comes with some pitfalls.
|
// unregister() to register() below. Note this comes with some pitfalls.
|
||||||
// Learn more about service workers: https://cra.link/PWA
|
// Learn more about service workers: https://cra.link/PWA
|
||||||
serviceWorker.unregister()
|
serviceWorker.unregister();
|
||||||
|
|
||||||
// If you want to start measuring performance in your app, pass a function
|
// If you want to start measuring performance in your app, pass a function
|
||||||
// to log results (for example: reportWebVitals(console.log))
|
// to log results (for example: reportWebVitals(console.log))
|
||||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||||
reportWebVitals()
|
reportWebVitals();
|
||||||
|
|||||||
6
src/interfaces.d.ts
vendored
Normal file
6
src/interfaces.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default interface ITodoData {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
10
src/logo.svg
10
src/logo.svg
@ -1,10 +0,0 @@
|
|||||||
<svg width="582" height="582" viewBox="0 0 582 582" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect width="582" height="582" rx="291" fill="url(#paint0_linear)"/>
|
|
||||||
<path d="M157.521 303.421L355.881 106.426C359.587 102.746 365.55 107.225 363.049 111.809L289.22 247.123C287.573 250.141 289.758 253.821 293.196 253.821H420.782C424.892 253.821 426.877 258.857 423.872 261.661L200.293 470.326C196.284 474.067 190.317 468.796 193.536 464.356L299.373 318.351C301.543 315.357 299.404 311.164 295.706 311.164H160.713C156.67 311.164 154.653 306.27 157.521 303.421Z" fill="white"/>
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="paint0_linear" x1="291" y1="0" x2="291" y2="582" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stop-color="#7BCBD4"/>
|
|
||||||
<stop offset="1" stop-color="#29C6B7"/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 786 B |
51
src/stores/TodoStore.ts
Normal file
51
src/stores/TodoStore.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { makeAutoObservable } from 'mobx';
|
||||||
|
import ITodoData from '../interfaces';
|
||||||
|
|
||||||
|
export default class TodoStore {
|
||||||
|
public todos: ITodoData[] = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
makeAutoObservable(this);
|
||||||
|
|
||||||
|
/* Init todos from localStorage */
|
||||||
|
this.initData();
|
||||||
|
}
|
||||||
|
|
||||||
|
private initData(): void {
|
||||||
|
Object.keys(window.localStorage).forEach((key) => {
|
||||||
|
if (key.match('todo')) {
|
||||||
|
const todo = window.localStorage.getItem(key);
|
||||||
|
|
||||||
|
if (todo) {
|
||||||
|
this.todos = [JSON.parse(todo) as ITodoData, ...this.todos];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public createTodo(data: ITodoData): void {
|
||||||
|
window.localStorage.setItem(`todo:${data.id}`, JSON.stringify(data));
|
||||||
|
this.todos.push(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public deleteTodo(id: number): void {
|
||||||
|
const updatedTodos = this.todos.filter((todo) => todo.id !== id);
|
||||||
|
|
||||||
|
window.localStorage.removeItem(`todo:${id}`);
|
||||||
|
this.todos = updatedTodos;
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateTodo(
|
||||||
|
id: number,
|
||||||
|
data: Pick<ITodoData, 'title' | 'updatedAt'>
|
||||||
|
): void {
|
||||||
|
const todo = this.todos.find((todo) => todo.id === id);
|
||||||
|
|
||||||
|
if (todo) {
|
||||||
|
todo.title = data.title;
|
||||||
|
todo.updatedAt = data.updatedAt;
|
||||||
|
|
||||||
|
window.localStorage.setItem(`todo:${todo.id}`, JSON.stringify(todo));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,12 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { render, RenderOptions } from "@testing-library/react"
|
|
||||||
import { ChakraProvider, theme } from "@chakra-ui/react"
|
|
||||||
|
|
||||||
const AllProviders = ({ children }: { children?: React.ReactNode }) => (
|
|
||||||
<ChakraProvider theme={theme}>{children}</ChakraProvider>
|
|
||||||
)
|
|
||||||
|
|
||||||
const customRender = (ui: React.ReactElement, options?: RenderOptions) =>
|
|
||||||
render(ui, { wrapper: AllProviders, ...options })
|
|
||||||
|
|
||||||
export { customRender as render }
|
|
||||||
Loading…
x
Reference in New Issue
Block a user