This commit is contained in:
Sergey Yarkov 2021-08-18 12:36:12 +03:00
parent d15a889a58
commit 0382fbf205
18 changed files with 16842 additions and 103 deletions

16583
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@chakra-ui/icons": "^1.0.15",
"@chakra-ui/react": "^1.0.0",
"@emotion/react": "^11.0.0",
"@emotion/styled": "^11.0.0",
@ -14,6 +15,8 @@
"@types/react": "^16.9.0",
"@types/react-dom": "^16.9.0",
"framer-motion": "^4.0.0",
"mobx": "^6.3.2",
"mobx-react": "^7.2.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-icons": "^3.0.0",

View File

@ -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()
})

View File

@ -1,38 +1,21 @@
import * as React from "react"
import {
ChakraProvider,
Box,
Text,
Link,
VStack,
Code,
Grid,
theme,
} from "@chakra-ui/react"
import { ColorModeSwitcher } from "./ColorModeSwitcher"
import { Logo } from "./Logo"
import * as React from 'react';
import { ChakraProvider, Box, theme } from '@chakra-ui/react';
export const App = () => (
/**
* Components
*/
import TodoInput from './components/TodoInput';
import TodoHeader from './components/TodoHeader';
import TodoList from './components/TodoList';
export const App = () => {
return (
<ChakraProvider theme={theme}>
<Box textAlign="center" fontSize="xl">
<Grid minH="100vh" p={3}>
<ColorModeSwitcher justifySelf="flex-end" />
<VStack spacing={8}>
<Logo h="40vmin" pointerEvents="none" />
<Text>
Edit <Code fontSize="xl">src/App.tsx</Code> and save to reload.
</Text>
<Link
color="teal.500"
href="https://chakra-ui.com"
fontSize="2xl"
target="_blank"
rel="noopener noreferrer"
>
Learn Chakra
</Link>
</VStack>
</Grid>
<Box maxW='800px' mt='5' mr='auto' ml='auto'>
<TodoHeader />
<TodoInput />
<TodoList />
</Box>
</ChakraProvider>
)
);
};

View File

@ -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} />
})

View 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;

View 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;

View 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;

View 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;

View 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
View 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
View File

@ -0,0 +1,4 @@
import React from 'react';
import { storesContext } from '../contexts';
export const useStores = () => React.useContext(storesContext);

View File

@ -1,24 +1,24 @@
import { ColorModeScript } from "@chakra-ui/react"
import * as React from "react"
import ReactDOM from "react-dom"
import { App } from "./App"
import reportWebVitals from "./reportWebVitals"
import * as serviceWorker from "./serviceWorker"
import { ColorModeScript } from '@chakra-ui/react';
import * as React from 'react';
import ReactDOM from 'react-dom';
import { App } from './App';
import reportWebVitals from './reportWebVitals';
import * as serviceWorker from './serviceWorker';
ReactDOM.render(
<React.StrictMode>
<ColorModeScript />
<App />
</React.StrictMode>,
document.getElementById("root"),
)
document.getElementById('root')
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// 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
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals()
reportWebVitals();

6
src/interfaces.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
export default interface ITodoData {
id: number;
title: string;
createdAt: Date;
updatedAt: Date;
}

View File

@ -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
View 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));
}
}
}

View File

@ -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 }