Переглянути джерело

try to create todo app with react

ilya2033 2 роки тому
батько
коміт
3e31093609

+ 27 - 0
.gitignore

@@ -0,0 +1,27 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# production
+/build
+
+
+# misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+
+.vscode

Різницю між файлами не показано, бо вона завелика
+ 39672 - 0
package-lock.json


+ 39 - 0
package.json

@@ -0,0 +1,39 @@
+{
+  "name": "test1",
+  "version": "0.1.0",
+  "private": true,
+  "dependencies": {
+    "@testing-library/jest-dom": "^5.14.1",
+    "@testing-library/react": "^11.2.7",
+    "@testing-library/user-event": "^12.8.3",
+    "node-sass": "^6.0.1",
+    "react": "^17.0.2",
+    "react-dom": "^17.0.2",
+    "react-scripts": "4.0.3",
+    "web-vitals": "^1.1.2"
+  },
+  "scripts": {
+    "start": "react-scripts start",
+    "build": "react-scripts build",
+    "test": "react-scripts test",
+    "eject": "react-scripts eject"
+  },
+  "eslintConfig": {
+    "extends": [
+      "react-app",
+      "react-app/jest"
+    ]
+  },
+  "browserslist": {
+    "production": [
+      ">0.2%",
+      "not dead",
+      "not op_mini all"
+    ],
+    "development": [
+      "last 1 chrome version",
+      "last 1 firefox version",
+      "last 1 safari version"
+    ]
+  }
+}

BIN
public/favicon.ico


+ 43 - 0
public/index.html

@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8" />
+    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <meta name="theme-color" content="#000000" />
+    <meta
+      name="description"
+      content="Web site created using create-react-app"
+    />
+    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
+    <!--
+      manifest.json provides metadata used when your web app is installed on a
+      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
+    -->
+    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
+    <!--
+      Notice the use of %PUBLIC_URL% in the tags above.
+      It will be replaced with the URL of the `public` folder during the build.
+      Only files inside the `public` folder can be referenced from the HTML.
+
+      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
+      work correctly both with client-side routing and a non-root public URL.
+      Learn how to configure a non-root public URL by running `npm run build`.
+    -->
+    <title>React App</title>
+  </head>
+  <body>
+    <noscript>You need to enable JavaScript to run this app.</noscript>
+    <div id="root"></div>
+    <!--
+      This HTML file is a template.
+      If you open it directly in the browser, you will see an empty page.
+
+      You can add webfonts, meta tags, or analytics to this file.
+      The build step will place the bundled scripts into the <body> tag.
+
+      To begin the development, run `npm start` or `yarn start`.
+      To create a production bundle, use `npm run build` or `yarn build`.
+    -->
+  </body>
+</html>

BIN
public/logo192.png


BIN
public/logo512.png


+ 25 - 0
public/manifest.json

@@ -0,0 +1,25 @@
+{
+  "short_name": "React App",
+  "name": "Create React App Sample",
+  "icons": [
+    {
+      "src": "favicon.ico",
+      "sizes": "64x64 32x32 24x24 16x16",
+      "type": "image/x-icon"
+    },
+    {
+      "src": "logo192.png",
+      "type": "image/png",
+      "sizes": "192x192"
+    },
+    {
+      "src": "logo512.png",
+      "type": "image/png",
+      "sizes": "512x512"
+    }
+  ],
+  "start_url": ".",
+  "display": "standalone",
+  "theme_color": "#000000",
+  "background_color": "#ffffff"
+}

+ 3 - 0
public/robots.txt

@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:

+ 13 - 0
src/App.js

@@ -0,0 +1,13 @@
+import "./static/css/App.css";
+import "./components/TodoList";
+import TodoList from "./components/TodoList";
+
+function App() {
+    return (
+        <div className="App">
+            <TodoList />
+        </div>
+    );
+}
+
+export default App;

+ 8 - 0
src/App.test.js

@@ -0,0 +1,8 @@
+import { render, screen } from '@testing-library/react';
+import App from './App';
+
+test('renders learn react link', () => {
+  render(<App />);
+  const linkElement = screen.getByText(/learn react/i);
+  expect(linkElement).toBeInTheDocument();
+});

+ 39 - 0
src/components/ListItem.js

@@ -0,0 +1,39 @@
+import React from "react";
+
+export default class ListItem extends React.Component {
+    render() {
+        let isCompleted = this.props.item.completed;
+        let isError = this.props.item.error;
+
+        const Buttons = () => {
+            return (
+                <>
+                    <span className="complete" onClick={this.complete.bind(this, this.props.item.id)}>
+                        ✔
+                    </span>
+                    <span className="close" onClick={this.delete.bind(this, this.props.item.id)}>
+                        ✕
+                    </span>
+                </>
+            );
+        };
+
+        return (
+            <>
+                <li className={"todo-item " + (isCompleted ? "complete " : "") + (isError ? "error " : "")}>
+                    <span className="todo-item-number">{this.props.number}</span>
+                    <span className="todo-item-text">{this.props.item.text}</span>
+                    {isCompleted || isError ? null : <Buttons />}
+                </li>
+            </>
+        );
+    }
+
+    delete(id) {
+        this.props.delete(id);
+    }
+
+    complete(id) {
+        this.props.complete(id);
+    }
+}

+ 154 - 0
src/components/TodoList.js

@@ -0,0 +1,154 @@
+import React from "react";
+import ListItem from "./ListItem";
+import { getTodoList, createNewItem, updateItem, deleteItem } from "../static/js/api";
+
+export default class TodoList extends React.Component {
+    constructor(props) {
+        super(props);
+        this.state = { items: [], itemsCompleted: [], itemsCurrent: [], inputText: "" };
+
+        this.handleChange = this.handleChange.bind(this);
+        this.handleKeyDown = this.handleKeyDown.bind(this);
+        this.addItem = this.addItem.bind(this);
+        this.deleteItem = this.deleteItem.bind(this);
+        this.completeItem = this.completeItem.bind(this);
+    }
+    componentDidMount() {
+        console.log(getTodoList());
+        getTodoList().then((data) => {
+            this.setState({
+                items: data,
+            });
+        });
+    }
+
+    render() {
+        const itemsCurrent = this.state.items.filter((el) => el.completed === false);
+        const itemsCompleted = this.state.items.filter((el) => el.completed === true);
+
+        const ListCurrent = ({ items }) => {
+            if (items.length === 0) {
+                return null;
+            }
+
+            return (
+                <>
+                    <ul className="todo-list">
+                        <span className="todo-list-headline">Current</span>
+                        {items.map((item, idx) => (
+                            <ListItem
+                                key={item.id}
+                                item={item}
+                                delete={this.deleteItem}
+                                complete={this.completeItem}
+                                number={idx + 1}
+                            />
+                        ))}
+                    </ul>
+                </>
+            );
+        };
+
+        const Listcompleted = ({ items }) => {
+            if (items.length === 0) {
+                return null;
+            }
+
+            return (
+                <>
+                    <ul className="todo-list complete">
+                        <span className="todo-list-headline">Completed</span>
+                        {items?.map((item, idx) => (
+                            <ListItem
+                                key={item.id}
+                                item={item}
+                                delete={this.deleteItem}
+                                complete={this.completeItem}
+                                number={idx + 1}
+                            />
+                        ))}
+                    </ul>
+                </>
+            );
+        };
+
+        return (
+            <div className="todo-wrapper">
+                <div className="todo-input-area">
+                    <input
+                        className="todo-input"
+                        placeholder="Enter your text"
+                        onKeyDown={this.handleKeyDown}
+                        onChange={this.handleChange}
+                        value={this.state.inputText}
+                    />
+                    <button className="todo-btn" onClick={this.addItem}>
+                        Add
+                    </button>
+                </div>
+
+                <ListCurrent items={itemsCurrent} />
+                <Listcompleted items={itemsCompleted} />
+            </div>
+        );
+    }
+    addItem(e) {
+        const prevId = this.state.items.length > 0 ? this.state.items[this.state.items.length - 1].id : 0;
+        e.preventDefault();
+        if (this.state.inputText.trim() === "") {
+            return;
+        }
+        const newListItem = {
+            text: this.state.inputText,
+            id: prevId + 1,
+            completed: false,
+            error: false,
+        };
+
+        createNewItem(newListItem).then((res) => {
+            let status = res.status;
+            if (status) {
+                this.setState((state) => ({
+                    items: state.items.concat(newListItem),
+                    inputText: "",
+                }));
+            } else {
+                newListItem.text = res.error;
+                newListItem.error = true;
+                this.setState((state) => ({
+                    items: state.items.concat(newListItem),
+                    inputText: "",
+                }));
+            }
+        });
+    }
+
+    deleteItem(id) {
+        createNewItem(id).then((res) => {
+            this.setState((state) => ({
+                items: state.items.filter((el) => el.id !== id),
+            }));
+        });
+    }
+
+    completeItem(id) {
+        this.state.items.find((el) => el.id === id).completed = true;
+
+        this.setState((state) => ({
+            items: state.items,
+        }));
+    }
+
+    handleKeyDown(e) {
+        console.log(this);
+        if (e.key === "Enter") {
+            this.addItem(e);
+        }
+    }
+
+    handleChange(e) {
+        this.setState({
+            inputText: e.target.value,
+        });
+    }
+}

+ 17 - 0
src/index.js

@@ -0,0 +1,17 @@
+import React from "react";
+import ReactDOM from "react-dom";
+import "./static/css/styles.scss";
+import App from "./App";
+import reportWebVitals from "./reportWebVitals";
+
+ReactDOM.render(
+    <React.StrictMode>
+        <App />
+    </React.StrictMode>,
+    document.getElementById("root")
+);
+
+// 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();

Різницю між файлами не показано, бо вона завелика
+ 1 - 0
src/logo.svg


+ 13 - 0
src/reportWebVitals.js

@@ -0,0 +1,13 @@
+const reportWebVitals = onPerfEntry => {
+  if (onPerfEntry && onPerfEntry instanceof Function) {
+    import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
+      getCLS(onPerfEntry);
+      getFID(onPerfEntry);
+      getFCP(onPerfEntry);
+      getLCP(onPerfEntry);
+      getTTFB(onPerfEntry);
+    });
+  }
+};
+
+export default reportWebVitals;

+ 5 - 0
src/setupTests.js

@@ -0,0 +1,5 @@
+// jest-dom adds custom jest matchers for asserting on DOM nodes.
+// allows you to do things like:
+// expect(element).toHaveTextContent(/react/i)
+// learn more: https://github.com/testing-library/jest-dom
+import '@testing-library/jest-dom';

+ 38 - 0
src/static/css/App.css

@@ -0,0 +1,38 @@
+.App {
+  text-align: center;
+}
+
+.App-logo {
+  height: 40vmin;
+  pointer-events: none;
+}
+
+@media (prefers-reduced-motion: no-preference) {
+  .App-logo {
+    animation: App-logo-spin infinite 20s linear;
+  }
+}
+
+.App-header {
+  background-color: #282c34;
+  min-height: 100vh;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  font-size: calc(10px + 2vmin);
+  color: white;
+}
+
+.App-link {
+  color: #61dafb;
+}
+
+@keyframes App-logo-spin {
+  from {
+    transform: rotate(0deg);
+  }
+  to {
+    transform: rotate(360deg);
+  }
+}

+ 123 - 0
src/static/css/styles.scss

@@ -0,0 +1,123 @@
+.App{
+    display: flex;
+    justify-content: center;
+}
+
+
+.todo-wrapper{
+    background: #FFF;
+    border-radius: 7px;
+    min-height: 400px;
+    border:1px solid black;
+    width: 500px;
+    padding: 10px 5px;
+
+
+
+
+    & .todo-list{
+        padding-left: 7px;
+        padding-right: 7px;
+        margin: 0;
+        margin-top: 30px;
+
+        & > .todo-list-headline{
+            font-size: 1.4em;
+
+        }
+
+        & .todo-item{
+            text-align: left;
+            padding-left: 12px;
+            padding-right: 12px;
+            color:#FFF;
+            border-radius: 7px;
+            background-color: #4227DB;
+            margin-top: 10px;
+            list-style: none;
+            line-height: 50px;
+            display: flex;
+            justify-content: space-between;
+
+            & .todo-item-number{
+                border-right: 1px solid white;
+                padding-right:  12px ;
+
+            }
+            & .todo-item-text{
+                overflow-x: hidden;
+                justify-self: left;
+                flex-grow: 1;
+                padding-left: 12px;
+                padding-right: 12px;
+
+            }
+
+            & .close{
+                padding-left: 7px;
+                padding-right: 7px;
+                cursor: pointer;
+                font-size: 1.25em;
+
+                &:hover{
+                    font-size: 1.30em;
+                }
+            }
+            & .complete{
+                padding-left: 7px;
+                padding-right: 7px;
+                cursor: pointer;
+                font-size: 1.25em;
+
+                &:hover{
+                    font-size: 1.30em;
+                }
+            }
+
+            &.complete{
+                background: #d3d3d3;
+
+            }
+            &.error{
+                background: #d3d3d3;
+                text-decoration:line-through;
+            }
+
+        }
+        &.complete{
+            margin-top: 50px;
+        }
+
+    }
+    & .todo-input-area{
+        display: flex;
+
+        height:40px;
+        justify-content: center;
+
+
+        & .todo-input{
+            padding: 10px 15px;
+            display: block;
+            flex-grow: 1;
+            box-sizing: border-box;
+            max-width: 350px;
+
+
+        }
+        & .todo-btn{
+            background-color: #30BE00;
+            color:#FFF;
+            border:none;
+            padding: 7px 15px;
+            border-radius: 0px 5px 5px 0px;
+            display: block;
+            min-width: 90px;
+            white-space: nowrap;
+            cursor: pointer;
+
+        }
+    }
+
+}
+

+ 49 - 0
src/static/js/api.js

@@ -0,0 +1,49 @@
+export const getTodoList = () => {
+    let url = "https://example.com/";
+
+    //Try to mimic fetch with promise
+    return new Promise((resolve) =>
+        setTimeout(
+            resolve([
+                { text: "test1", id: 1, completed: false },
+                { text: "test2", id: 2, completed: true },
+                { text: "test3", id: 3, completed: false },
+                { text: "test4", id: 5, completed: true },
+            ]),
+            Math.random() * 1000
+        )
+    );
+};
+
+export const createNewItem = () => {
+    let url = "https://example.com/";
+
+    //Try to mimic fetch with promise
+    if (Math.random() > 0.1) {
+        return new Promise((resolve) => setTimeout(resolve({ status: true }), Math.random() * 1000));
+    } else {
+        return new Promise((resolve) =>
+            setTimeout(resolve({ status: false, error: "some error" }), Math.random() * 1000)
+        );
+    }
+};
+
+export const updateItem = (data) => {
+    let url = "https://example.com/";
+
+    //Try to mimic fetch with promise
+    if (Math.random() > 0.1) {
+        return new Promise((resolve) => setTimeout(resolve({ status: true }), Math.random() * 1000));
+    } else {
+        return new Promise((resolve) =>
+            setTimeout(resolve({ status: false, error: "some error" }), Math.random() * 1000)
+        );
+    }
+};
+
+export const deleteItem = (id) => {
+    let url = "https://example.com/";
+
+    //Try to mimic fetch with promise
+    return new Promise((resolve) => setTimeout(resolve({ status: true }), Math.random() * 1000));
+};