├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── README.md
├── babel.config.js
├── index.html
├── jest.config.cjs
├── package.json
├── public
├── app-pic.png
└── icon.png
├── src
├── App.tsx
├── components
│ ├── TodoItem.tsx
│ └── TodoList.tsx
├── hooks
│ ├── useLocalStorage.ts
│ └── useTodos.ts
├── main.tsx
├── mock
│ └── mockTodos.ts
├── styled.d.ts
├── styles
│ ├── global.ts
│ └── theme.ts
├── tests
│ └── useTodos.test.ts
├── types
│ ├── styled.ts
│ └── todo.ts
└── vite-env.d.ts
├── tsconfig.json
├── tsconfig.node.json
├── vite.config.ts
└── yarn.lock
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: { browser: true, es2020: true },
3 | extends: [
4 | "eslint:recommended",
5 | "plugin:@typescript-eslint/recommended",
6 | "plugin:react-hooks/recommended",
7 | ],
8 | parser: "@typescript-eslint/parser",
9 | parserOptions: { ecmaVersion: "latest", sourceType: "module" },
10 | plugins: ["react-refresh"],
11 | rules: {
12 | "react-refresh/only-export-components": "warn",
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "proseWrap": "never",
3 | "quoteProps": "consistent"
4 | }
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Todo List
2 |
3 | Simple React Todo App
4 |
5 |
6 |
7 | ## Demo
8 | https://todo-vanila-react.vercel.app/
9 |
10 | ## Installation
11 |
12 | ```bash
13 | npm install
14 | ```
15 |
16 | or
17 |
18 | ```bash
19 | yarn install
20 | ```
21 |
22 | ## Start
23 |
24 | ```bash
25 | npm run dev
26 | ```
27 | or
28 | ```bash
29 | yarn dev
30 | ```
31 |
32 | ## Functionality
33 | 1. Adding, editing, toggling, deleting todo's
34 | 2. Filtering and deleting completed todo's
35 | 3. Theme switching
36 |
37 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | [
4 | "@babel/preset-env",
5 | {
6 | targets: {
7 | node: "current",
8 | },
9 | },
10 | ],
11 | "@babel/preset-react",
12 | "@babel/preset-typescript",
13 | ],
14 | };
15 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Todo List
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/jest.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: "ts-jest",
3 | testEnvironment: "jsdom",
4 | };
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "todo-react-hooks",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc && vite build",
9 | "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
10 | "preview": "vite preview",
11 | "test": "jest --verbose false"
12 | },
13 | "dependencies": {
14 | "react": "^18.2.0",
15 | "react-dom": "^18.2.0",
16 | "styled-components": "^6.0.0-rc.3"
17 | },
18 | "devDependencies": {
19 | "@babel/preset-env": "^7.22.5",
20 | "@babel/preset-react": "^7.22.5",
21 | "@babel/preset-typescript": "^7.22.5",
22 | "@emotion/react": "^11.11.1",
23 | "@emotion/styled": "^11.11.0",
24 | "@mui/icons-material": "^5.11.16",
25 | "@mui/material": "^5.13.4",
26 | "@testing-library/jest-dom": "^5.16.5",
27 | "@testing-library/react": "^14.0.0",
28 | "@testing-library/react-hooks": "^8.0.1",
29 | "@testing-library/user-event": "^14.4.3",
30 | "@types/jest": "^29.5.2",
31 | "@types/react": "^18.0.37",
32 | "@types/react-dom": "^18.0.11",
33 | "@types/styled-components": "^5.1.26",
34 | "@typescript-eslint/eslint-plugin": "^5.59.0",
35 | "@typescript-eslint/parser": "^5.59.0",
36 | "@vitejs/plugin-react": "^4.0.0",
37 | "eslint": "^8.38.0",
38 | "eslint-plugin-eslint-plugin": "^5.1.0",
39 | "eslint-plugin-import": "^2.27.5",
40 | "eslint-plugin-react-hooks": "^4.6.0",
41 | "eslint-plugin-react-refresh": "^0.3.4",
42 | "identity-obj-proxy": "^3.0.0",
43 | "jest": "^29.5.0",
44 | "jest-environment-jsdom": "^29.5.0",
45 | "prettier": "^2.8.8",
46 | "ts-jest": "^29.1.0",
47 | "typescript": "^5.0.2",
48 | "vite": "^4.3.9"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/public/app-pic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/llamii/todo-react/40fb35fd991e89105c3415c202708c9d8e5595fb/public/app-pic.png
--------------------------------------------------------------------------------
/public/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/llamii/todo-react/40fb35fd991e89105c3415c202708c9d8e5595fb/public/icon.png
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { DefaultTheme, ThemeProvider } from "styled-components";
2 | import { darkTheme, lightTheme } from "./styles/theme";
3 |
4 | import DarkModeIcon from "@mui/icons-material/DarkMode";
5 | import { FC } from "react";
6 | import GlobalStyles from "../src/styles/global";
7 | import { TodoList } from "./components/TodoList";
8 | import styled from "styled-components";
9 | import useLocalStorage from "./hooks/useLocalStorage";
10 |
11 | const App: FC = () => {
12 | const [theme, setTheme] = useLocalStorage("theme", lightTheme);
13 |
14 | const themeToggle = () => {
15 | const newTheme = theme === lightTheme ? darkTheme : lightTheme;
16 | setTheme(newTheme);
17 | };
18 |
19 | return (
20 |
21 |
22 |
23 |
29 |
30 |
31 |
32 |
33 |
34 | );
35 | };
36 |
37 | export const AppContainer = styled.div`
38 | display: flex;
39 | flex-direction: column;
40 | align-items: center;
41 | padding-top: 40px;
42 | `;
43 |
44 | export const Header = styled.h1`
45 | text-align: center;
46 | font-size: 48px;
47 | padding: 50px 0 50px 0;
48 | `;
49 |
50 | export const Footer = styled.h6`
51 | text-align: center;
52 | font-size: 14px;
53 | font-weight: 200;
54 | font-style: italic;
55 | opacity: 0.5;
56 | padding-top: 25px;
57 | padding-bottom: 25px;
58 | `;
59 |
60 | export const ThemeToggle = styled.button`
61 | background: none;
62 | border: none;
63 | font-size: 24px;
64 | color: ${(props) => props.theme.colors.text};
65 | cursor: pointer;
66 | position: absolute;
67 | top: 20px;
68 | right: 20px;
69 | `;
70 |
71 | export default App;
72 |
--------------------------------------------------------------------------------
/src/components/TodoItem.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Checkbox } from "@mui/material";
2 | import { FC, useState } from "react";
3 |
4 | import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline";
5 | import ClearIcon from "@mui/icons-material/Clear";
6 | import { ITodo } from "../types/todo";
7 | import RadioButtonUncheckedOutlinedIcon from "@mui/icons-material/RadioButtonUncheckedOutlined";
8 | import { styled } from "styled-components";
9 |
10 | interface ITodoItemProps extends ITodo {
11 | toggleTodo: (id: number) => void;
12 | editTodo: (id: number, name: string) => void;
13 | removeTodo: (id: number) => void;
14 | }
15 |
16 | export const TodoItem: FC = (props) => {
17 | const { id, name, completed, toggleTodo, editTodo, removeTodo } = props;
18 |
19 | const [isEditing, setIsEditing] = useState(false);
20 | const [editedName, setEditedName] = useState(name);
21 |
22 | const handleNameChange = (event: React.ChangeEvent) => {
23 | setEditedName(event.target.value);
24 | };
25 |
26 | const handleNameBlur = () => {
27 | if (editedName.trim() !== "") {
28 | editTodo(id, editedName);
29 | }
30 | setIsEditing(false);
31 | };
32 |
33 | const handleKeyDown = (event: React.KeyboardEvent) => {
34 | if (event.key === "Enter" || event.key === "Escape") {
35 | handleNameBlur();
36 | }
37 | };
38 |
39 | return (
40 |
41 | }
45 | checkedIcon={}
46 | onChange={() => toggleTodo(id)}
47 | />
48 | {isEditing ? (
49 |
57 | ) : (
58 | setIsEditing(true)}
61 | >
62 | {name}
63 |
64 | )}
65 |
66 | removeTodo(id)}
70 | >
71 |
72 |
73 |
74 | );
75 | };
76 |
77 | const UncheckedIcon = styled(RadioButtonUncheckedOutlinedIcon)`
78 | color: ${(props) => props.theme.colors.primary};
79 | `;
80 |
81 | const CheckedIcon = styled(CheckCircleOutlineIcon)`
82 | color: ${(props) => props.theme.colors.success};
83 | `;
84 |
85 | const DeleteIcon = styled(ClearIcon)`
86 | color: ${(props) => props.theme.colors.danger};
87 | `;
88 |
89 | const Input = styled.input`
90 | width: 100%;
91 | border: none;
92 | font-size: 16px;
93 | background-color: ${(props) => props.theme.colors.secondary};
94 | `;
95 |
96 | const DeleteButton = styled(Button)`
97 | && {
98 | background-color: transparent;
99 | color: ${(props) => props.theme.colors.danger};
100 | padding: 5px;
101 | margin: 0;
102 | display: none;
103 | &:hover {
104 | background-color: "transparent";
105 | }
106 | }
107 | `;
108 |
109 | const TodoItemWrapper = styled.div`
110 | height: 48px;
111 | display: flex;
112 | align-items: center;
113 | justify-content: center;
114 | margin-bottom: 8px;
115 | padding-bottom: 8px;
116 | border-bottom: 1px solid ${(props) => props.theme.colors.primary};
117 |
118 | &:hover {
119 | ${DeleteButton} {
120 | display: flex;
121 | }
122 | }
123 | `;
124 |
125 | const TodoText = styled.span<{ $isChecked: boolean }>`
126 | flex-grow: 1;
127 | margin-right: 8px;
128 |
129 | text-decoration: ${(props) => (props.$isChecked ? `line-through` : "none")};
130 | `;
131 |
--------------------------------------------------------------------------------
/src/components/TodoList.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { FC, useEffect, useMemo, useRef, useState } from "react";
4 | import { Filter, ITodo, absurd } from "../types/todo";
5 |
6 | import { Container } from "@mui/material";
7 | import { TodoItem } from "./TodoItem";
8 | import { mockTodos } from "../mock/mockTodos";
9 | import { styled } from "styled-components";
10 | import { useTodos } from "../hooks/useTodos";
11 |
12 | export const TodoList: FC = () => {
13 | const inputRef = useRef(null);
14 |
15 | const [value, setValue] = useState("");
16 | const [filter, setFilter] = useState(Filter.all);
17 |
18 | const {
19 | todos,
20 | addTodo,
21 | toggleTodo,
22 | editTodo,
23 | removeTodo,
24 | removeCompleted,
25 | getItemsLeft,
26 | } = useTodos(mockTodos);
27 |
28 | const itemsLeft = getItemsLeft();
29 |
30 | const handleChange: React.ChangeEventHandler = (event) => {
31 | setValue(event.target.value);
32 | };
33 |
34 | const handleKeyDown: React.KeyboardEventHandler = (
35 | event
36 | ) => {
37 | if (event.key === "Enter" && value.trim() !== "") {
38 | addTodo(value);
39 | setValue("");
40 | }
41 | };
42 |
43 | const handleChangeFilter = (filter: Filter) => {
44 | setFilter(filter);
45 | };
46 |
47 | const filteredTodos = useMemo(
48 | () =>
49 | todos.filter((todo) => {
50 | switch (filter) {
51 | case Filter.active:
52 | return !todo.completed;
53 | case Filter.completed:
54 | return todo.completed;
55 | case Filter.all:
56 | return true;
57 | default:
58 | return absurd(filter);
59 | }
60 | }),
61 | [todos, filter]
62 | );
63 |
64 | const filterButtons = Object.values(Filter).map((f) => (
65 | handleChangeFilter(f)}
68 | $isActive={f === filter}
69 | >
70 | {f}
71 |
72 | ));
73 |
74 | useEffect(() => {
75 | inputRef.current?.focus();
76 | }, []);
77 |
78 | return (
79 |
80 |
87 |
88 | {filteredTodos &&
89 | filteredTodos.map((todo: ITodo) => (
90 |
99 | ))}
100 |
101 |
102 | {itemsLeft} item(s) left
103 | {filterButtons}
104 |
109 | Clear Completed
110 |
111 |
112 |
113 | );
114 | };
115 |
116 | const TodoListContainer = styled(Container)`
117 | margin: 0 auto;
118 | padding: 20px;
119 | border-radius: 4px;
120 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
121 | background-color: ${(props) => props.theme.colors.secondary};
122 | min-height: 432px;
123 | display: flex;
124 | flex-direction: column;
125 | height: 100%;
126 | `;
127 |
128 | const Input = styled.input`
129 | width: 100%;
130 | padding: 10px;
131 | border: none;
132 | margin-bottom: 10px;
133 | height: 48px;
134 | border-bottom: 1px solid ${(props) => props.theme.colors.primary};
135 | background-color: ${(props) => props.theme.colors.secondary};
136 | `;
137 |
138 | const FilterButton = styled.button<{ $isActive: boolean }>`
139 | background-color: transparent;
140 | border: none;
141 | padding: 10px 15px;
142 | cursor: pointer;
143 | margin-right: 10px;
144 | font-size: 12px;
145 |
146 | border: ${(props) =>
147 | props.$isActive ? `1px solid ${props.theme.colors.primary}` : "none"};
148 | border-radius: 10px;
149 |
150 | &:last-child {
151 | margin-right: 0;
152 | }
153 | `;
154 |
155 | const FilterButtons = styled.div`
156 | display: flex;
157 | `;
158 |
159 | const TodoFooter = styled.div`
160 | display: flex;
161 | flex-direction: row;
162 | align-items: center;
163 | justify-content: space-between;
164 |
165 | justify-self: end;
166 | margin-top: auto;
167 | `;
168 |
169 | const ItemsLeft = styled.span`
170 | font-size: 12px;
171 | `;
172 |
173 | const ClearButton = styled(FilterButton)<{ $isVisible: boolean }>`
174 | font-size: 12px;
175 | background-color: transparent;
176 | visibility: ${(props) => (props.$isVisible ? `visible` : "hidden")};
177 | `;
178 |
--------------------------------------------------------------------------------
/src/hooks/useLocalStorage.ts:
--------------------------------------------------------------------------------
1 | import { Dispatch, SetStateAction, useState } from "react";
2 |
3 | function useLocalStorage(
4 | key: string,
5 | initialValue: T
6 | ): [T, Dispatch>] {
7 | const [storedValue, setStoredValue] = useState(() => {
8 | try {
9 | const item = window.localStorage.getItem(key);
10 | return item ? JSON.parse(item) : initialValue;
11 | } catch (error) {
12 | console.log(error);
13 | return initialValue;
14 | }
15 | });
16 |
17 | function setValue(value: SetStateAction): void {
18 | try {
19 | const valueToStore =
20 | value instanceof Function ? value(storedValue) : value;
21 | setStoredValue(valueToStore);
22 | window.localStorage.setItem(key, JSON.stringify(valueToStore));
23 | } catch (error) {
24 | console.log(error);
25 | }
26 | }
27 |
28 | return [storedValue, setValue];
29 | }
30 |
31 | export default useLocalStorage;
32 |
--------------------------------------------------------------------------------
/src/hooks/useTodos.ts:
--------------------------------------------------------------------------------
1 | import { ITodo } from "../types/todo";
2 | import useLocalStorage from "./useLocalStorage";
3 |
4 | export function useTodos(initialTodos: ITodo[]) {
5 | const [todos, setTodos] = useLocalStorage("todos", initialTodos);
6 |
7 | const addTodo = (name: string): void => {
8 | const newTodo = {
9 | id: Date.now(),
10 | name,
11 | completed: false,
12 | };
13 | setTodos((prevState) => [...prevState, newTodo]);
14 | };
15 |
16 | const toggleTodo = (id: number): void => {
17 | setTodos((prevState) =>
18 | prevState.map((todo) => {
19 | if (todo.id === id) {
20 | return {
21 | ...todo,
22 | completed: !todo.completed,
23 | };
24 | }
25 | return todo;
26 | })
27 | );
28 | };
29 |
30 | const editTodo = (id: number, name: string): void => {
31 | setTodos((prevState) => {
32 | return prevState.map((todo) => {
33 | if (todo.id === id) {
34 | return {
35 | ...todo,
36 | name,
37 | };
38 | }
39 | return todo;
40 | });
41 | });
42 | };
43 |
44 | const removeTodo = (id: number): void => {
45 | setTodos((prevState) => prevState.filter((todo) => todo.id !== id));
46 | };
47 |
48 | const getItemsLeft = () => {
49 | return todos.reduce((counter, todo) => {
50 | if (!todo.completed) {
51 | return counter + 1;
52 | }
53 | return counter;
54 | }, 0);
55 | };
56 |
57 | const removeCompleted = (): void => {
58 | setTodos((prevState) => prevState.filter((todo) => !todo.completed));
59 | };
60 |
61 | return {
62 | todos,
63 | addTodo,
64 | toggleTodo,
65 | editTodo,
66 | removeTodo,
67 | getItemsLeft,
68 | removeCompleted,
69 | };
70 | }
71 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import App from "./App.tsx";
2 | import ReactDOM from "react-dom/client";
3 |
4 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
5 |
6 | );
7 |
--------------------------------------------------------------------------------
/src/mock/mockTodos.ts:
--------------------------------------------------------------------------------
1 | import { ITodo } from '../types/todo';
2 |
3 | export const mockTodos: ITodo[] = [
4 | {
5 | id: 1,
6 | name: '🏪 To buy products ',
7 | completed: false,
8 | },
9 | {
10 | id: 2,
11 | name: '🧽 To clean the house',
12 | completed: false,
13 | },
14 | {
15 | id: 3,
16 | name: '🌼 To water flowers',
17 | completed: true,
18 | },
19 | {
20 | id: 4,
21 | name: '🐕 To feed the dog',
22 | completed: false,
23 | },
24 | {
25 | id: 5,
26 | name: '⚛️ To code a react app',
27 | completed: true,
28 | },
29 | ];
30 |
--------------------------------------------------------------------------------
/src/styled.d.ts:
--------------------------------------------------------------------------------
1 | import 'styled-components';
2 | import { ITheme, ThemeEnum } from 'interfaces/styled';
3 |
4 | declare module 'styled-components' {
5 | export interface DefaultTheme extends ITheme {
6 | type: ThemeEnum;
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/styles/global.ts:
--------------------------------------------------------------------------------
1 | import { createGlobalStyle } from "styled-components";
2 |
3 | export default createGlobalStyle`
4 | * {
5 | box-sizing: border-box;
6 | margin: 0;
7 | padding: 0;
8 | }
9 |
10 | *::before,
11 | *::after {
12 | box-sizing: border-box;
13 | }
14 |
15 | body {
16 | font-family: Arial, sans-serif;
17 | font-size: 16px;
18 | line-height: 1.4;
19 |
20 | background-color: ${(props) => props.theme.colors.bg};
21 | color: ${(props) => props.theme.colors.font};
22 |
23 | h1, h2, h3, h4, h5, h6, span, button, input {
24 | color: ${(props) => props.theme.colors.font}
25 | }
26 |
27 | a {
28 | cursor: pointer;
29 | color: ${(props) => props.theme.colors.font};
30 | text-decoration: none;
31 | }
32 |
33 | }
34 |
35 | *:focus {
36 | outline: none;
37 | }
38 | `;
39 |
--------------------------------------------------------------------------------
/src/styles/theme.ts:
--------------------------------------------------------------------------------
1 | import { ITheme, ThemeEnum } from "../types/styled";
2 |
3 | import { DefaultTheme } from "styled-components";
4 |
5 | export const baseTheme: ITheme = {
6 | colors: {
7 | primary: "#7986cb",
8 | secondary: "#2b2b2b",
9 | success: "#4caf50",
10 | danger: "#f44336",
11 |
12 | bg: "#E5E4E8",
13 | font: "#19191B",
14 | },
15 |
16 | media: {
17 | extraLarge: "(max-width: 1140px)",
18 | large: "(max-width: 960px)",
19 | medium: "(max-width: 720px)",
20 | small: "(max-width: 540px)",
21 | },
22 | };
23 |
24 | export const lightTheme: DefaultTheme = {
25 | ...baseTheme,
26 | type: ThemeEnum.light,
27 |
28 | colors: {
29 | ...baseTheme.colors,
30 | bg: "#E5E4E8",
31 | font: "#19191B",
32 | primary: "lightgray",
33 | secondary: "#f8f8f8",
34 | },
35 | };
36 |
37 | export const darkTheme: DefaultTheme = {
38 | ...baseTheme,
39 | type: ThemeEnum.dark,
40 |
41 | colors: {
42 | ...baseTheme.colors,
43 | bg: "#19191B",
44 | font: "#E5E4E8",
45 | primary: "#3b3b3b",
46 | secondary: "#262629",
47 | },
48 | };
49 |
--------------------------------------------------------------------------------
/src/tests/useTodos.test.ts:
--------------------------------------------------------------------------------
1 | import { act, cleanup, renderHook } from "@testing-library/react-hooks";
2 |
3 | import { useTodos } from "../hooks/useTodos";
4 |
5 | afterEach(() => {
6 | cleanup();
7 | window.localStorage.clear();
8 | });
9 |
10 | describe("useTodos tests", () => {
11 | it("should initialize with an empty todo list", () => {
12 | const { result } = renderHook(() => useTodos([]));
13 |
14 | expect(result.current.todos).toEqual([]);
15 | });
16 |
17 | it("should add a new todo", () => {
18 | const { result } = renderHook(() => useTodos([]));
19 |
20 | act(() => {
21 | result.current.addTodo("test");
22 | });
23 | console.log(result.current.todos);
24 | expect(result.current.todos).toEqual([
25 | {
26 | id: expect.any(Number),
27 | name: "test",
28 | completed: false,
29 | },
30 | ]);
31 | });
32 |
33 | it("should toggle a todo", () => {
34 | const initialTodos = [
35 | { id: 0, name: "todo 1", completed: false },
36 | { id: 1, name: "todo 2", completed: false },
37 | ];
38 | const { result } = renderHook(() => useTodos(initialTodos));
39 | act(() => {
40 | result.current.toggleTodo(0);
41 | });
42 |
43 | expect(result.current.todos).toEqual([
44 | { id: 0, name: "todo 1", completed: true },
45 | { id: 1, name: "todo 2", completed: false },
46 | ]);
47 | });
48 |
49 | test("should edit a todo", () => {
50 | const initialTodos = [
51 | { id: 0, name: "todo 1", completed: false },
52 | { id: 1, name: "todo 2", completed: false },
53 | ];
54 | const { result } = renderHook(() => useTodos(initialTodos));
55 |
56 | act(() => {
57 | result.current.editTodo(1, "updated todo 2");
58 | });
59 |
60 | expect(result.current.todos).toEqual([
61 | { id: 0, name: "todo 1", completed: false },
62 | { id: 1, name: "updated todo 2", completed: false },
63 | ]);
64 | });
65 |
66 | test("should remove a todo", () => {
67 | const initialTodos = [
68 | { id: 0, name: "todo 1", completed: false },
69 | { id: 1, name: "todo 2", completed: false },
70 | ];
71 | const { result } = renderHook(() => useTodos(initialTodos));
72 |
73 | act(() => {
74 | result.current.removeTodo(1);
75 | });
76 |
77 | expect(result.current.todos).toEqual([
78 | { id: 0, name: "todo 1", completed: false },
79 | ]);
80 | });
81 |
82 | test("should return the correct number of items left", () => {
83 | const initialTodos = [
84 | { id: 0, name: "todo 1", completed: false },
85 | { id: 1, name: "todo 2", completed: false },
86 | ];
87 | const { result } = renderHook(() => useTodos(initialTodos));
88 |
89 | expect(result.current.getItemsLeft()).toBe(2);
90 | });
91 |
92 | test("should remove completed todos", () => {
93 | const initialTodos = [
94 | { id: 0, name: "todo 1", completed: false },
95 | { id: 1, name: "todo 2", completed: true },
96 | ];
97 | const { result } = renderHook(() => useTodos(initialTodos));
98 |
99 | act(() => {
100 | result.current.removeCompleted();
101 | });
102 |
103 | expect(result.current.todos).toEqual([
104 | { id: 0, name: "todo 1", completed: false },
105 | ]);
106 | });
107 | });
108 |
--------------------------------------------------------------------------------
/src/types/styled.ts:
--------------------------------------------------------------------------------
1 | export enum ThemeEnum {
2 | light = "light",
3 | dark = "dark",
4 | }
5 |
6 | export interface ITheme {
7 | colors: {
8 | primary: string;
9 | secondary: string;
10 | success: string;
11 | danger: string;
12 |
13 | bg: string;
14 | font: string;
15 | };
16 |
17 | media: {
18 | extraLarge: string;
19 | large: string;
20 | medium: string;
21 | small: string;
22 | };
23 | }
24 |
--------------------------------------------------------------------------------
/src/types/todo.ts:
--------------------------------------------------------------------------------
1 | export interface ITodo {
2 | id: number;
3 | name: string;
4 | completed: boolean;
5 | }
6 |
7 | export enum Filter {
8 | all = "all",
9 | active = "active",
10 | completed = "completed",
11 | }
12 |
13 | export const absurd = (_value: never): any => {};
14 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 | "jsx": "react-jsx",
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true
22 | },
23 | "include": ["src"],
24 | "references": [{ "path": "./tsconfig.node.json" }]
25 | }
26 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true
8 | },
9 | "include": ["vite.config.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | })
8 |
--------------------------------------------------------------------------------