From 7fb94024e6a2bb9e85b3da4e520ac02701b63b5f Mon Sep 17 00:00:00 2001 From: Leonid Krainik Date: Thu, 5 Oct 2023 18:03:52 +0300 Subject: [PATCH] Separate styles and moved utils --- src/App.scss | 34 +++ src/App.tsx | 2 + src/components/Footer/Footer.scss | 53 +++++ src/components/Footer/Footer.tsx | 1 + src/components/Header/Header.scss | 10 + src/components/Header/Header.tsx | 2 + src/components/Main/Main.scss | 43 ++++ src/components/Main/Main.tsx | 2 + src/components/TodoApp/TodoApp.scss | 44 ++++ src/components/TodoApp/TodoApp.tsx | 1 + src/components/TodoItem/TodoItem.scss | 5 + src/components/TodoItem/TodoItem.tsx | 40 ++-- src/components/TodoList/TodoList.scss | 110 ++++++++++ src/components/TodoList/TodoList.tsx | 2 + src/components/TodosContext/TodosContext.tsx | 99 +-------- src/components/TodosFilter/TodosFilter.scss | 36 ++++ src/components/TodosFilter/TodosFilter.tsx | 11 +- src/index.tsx | 4 - src/styles/filters.css | 35 --- src/styles/index.css | 214 ------------------- src/styles/todo-list.css | 116 ---------- src/types/Action.ts | 8 + src/utils/manageLocalState.ts | 24 +++ src/utils/mixins.scss | 16 ++ src/utils/todosReducer.ts | 67 ++++++ 25 files changed, 488 insertions(+), 491 deletions(-) create mode 100644 src/App.scss create mode 100644 src/components/Footer/Footer.scss create mode 100644 src/components/Header/Header.scss create mode 100644 src/components/Main/Main.scss create mode 100644 src/components/TodoApp/TodoApp.scss create mode 100644 src/components/TodoItem/TodoItem.scss create mode 100644 src/components/TodoList/TodoList.scss create mode 100644 src/components/TodosFilter/TodosFilter.scss delete mode 100644 src/styles/filters.css delete mode 100644 src/styles/index.css delete mode 100644 src/styles/todo-list.css create mode 100644 src/types/Action.ts create mode 100644 src/utils/manageLocalState.ts create mode 100644 src/utils/mixins.scss create mode 100644 src/utils/todosReducer.ts diff --git a/src/App.scss b/src/App.scss new file mode 100644 index 000000000..61cea21a7 --- /dev/null +++ b/src/App.scss @@ -0,0 +1,34 @@ +html, +body { + margin: 0; + padding: 0; +} + +button { + margin: 0; + padding: 0; + border: 0; + background: none; + font-size: 100%; + vertical-align: baseline; + font-family: inherit; + font-weight: inherit; + color: inherit; + -webkit-appearance: none; + appearance: none; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + font: 14px "Helvetica Neue", Helvetica, Arial, sans-serif; + line-height: 1.4em; + background: #f5f5f5; + color: #4d4d4d; + min-width: 230px; + max-width: 550px; + margin: 0 auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-weight: 300; +} diff --git a/src/App.tsx b/src/App.tsx index 90172f2c0..0a95a3ac8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,6 @@ import React from 'react'; + +import './App.scss'; import { TodoApp } from './components/TodoApp/TodoApp'; import { TodosContext } from './components/TodosContext'; diff --git a/src/components/Footer/Footer.scss b/src/components/Footer/Footer.scss new file mode 100644 index 000000000..ac6956b90 --- /dev/null +++ b/src/components/Footer/Footer.scss @@ -0,0 +1,53 @@ +.footer { + color: #777; + padding: 10px 15px; + height: 20px; + text-align: center; + border-top: 1px solid #e6e6e6; + + &:before { + content: ""; + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 50px; + overflow: hidden; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), + 0 8px 0 -3px #f6f6f6, + 0 9px 1px -3px rgba(0, 0, 0, 0.2), + 0 16px 0 -6px #f6f6f6, + 0 17px 2px -6px rgba(0, 0, 0, 0.2); + } + + @media (max-width: 430px) { + & { + height: 50px; + } + } +} + +.todo-count { + float: left; + text-align: left; + + & strong { + font-weight: 300; + } +} + +.clear-completed { + float: right; + position: relative; + line-height: 20px; + text-decoration: none; + cursor: pointer; + + &:hover { + text-decoration: underline; + } + + &:active { + text-decoration: none; + } +} diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx index de200cd7c..15239dab2 100644 --- a/src/components/Footer/Footer.tsx +++ b/src/components/Footer/Footer.tsx @@ -1,5 +1,6 @@ import React, { useContext } from 'react'; +import './Footer.scss'; import { DispatchContext, StateContext } from '../TodosContext'; import { TodosFilter } from '../TodosFilter'; diff --git a/src/components/Header/Header.scss b/src/components/Header/Header.scss new file mode 100644 index 000000000..ef4865c7f --- /dev/null +++ b/src/components/Header/Header.scss @@ -0,0 +1,10 @@ +@import "../../utils/mixins.scss"; + +.new-todo { + @include editTitle; + + padding: 16px 16px 16px 60px; + border: none; + background: rgba(0, 0, 0, 0.01); + box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); +} diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index 8ab8de91c..9efb0fa46 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -1,4 +1,6 @@ import React, { useContext, useState } from 'react'; + +import './Header.scss'; import { DispatchContext } from '../TodosContext'; export const Header: React.FC = () => { diff --git a/src/components/Main/Main.scss b/src/components/Main/Main.scss new file mode 100644 index 000000000..8a0cc19b4 --- /dev/null +++ b/src/components/Main/Main.scss @@ -0,0 +1,43 @@ +.main { + position: relative; + z-index: 2; + border-top: 1px solid #e6e6e6; +} + +.toggle-all { + width: 1px; + height: 1px; + border: none; + opacity: 0; + position: absolute; + right: 100%; + bottom: 100%; + + & + label { + width: 60px; + height: 34px; + font-size: 0; + position: absolute; + top: -52px; + left: -13px; + -webkit-transform: rotate(90deg); + transform: rotate(90deg); + + &:before { + content: "❯"; + font-size: 22px; + color: #e6e6e6; + padding: 10px 27px; + } + } + + &:checked + label:before { + color: #737373; + } + + @media screen and (-webkit-min-device-pixel-ratio:0) { + & { + background: none; + } + } +} diff --git a/src/components/Main/Main.tsx b/src/components/Main/Main.tsx index 9e3a05dda..50b05abaf 100644 --- a/src/components/Main/Main.tsx +++ b/src/components/Main/Main.tsx @@ -1,4 +1,6 @@ import React, { useContext, useState, useMemo } from 'react'; + +import './Main.scss'; import { TodoList } from '../TodoList'; import { DispatchContext, FilterContext, StateContext } from '../TodosContext'; import { Status } from '../../types/Status'; diff --git a/src/components/TodoApp/TodoApp.scss b/src/components/TodoApp/TodoApp.scss new file mode 100644 index 000000000..972f0f52b --- /dev/null +++ b/src/components/TodoApp/TodoApp.scss @@ -0,0 +1,44 @@ +.todoapp { + background: #fff; + margin: 130px 0 40px; + position: relative; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), + 0 25px 50px 0 rgba(0, 0, 0, 0.1); + + & h1 { + position: absolute; + top: -155px; + width: 100%; + font-size: 100px; + font-weight: 100; + text-align: center; + color: rgba(175, 47, 47, 0.15); + -webkit-text-rendering: optimizeLegibility; + -moz-text-rendering: optimizeLegibility; + text-rendering: optimizeLegibility; + } + + & input::-webkit-input-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; + + &::-moz-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; + } + + &::-ms-input-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; + } + + &::placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; + } + } +} diff --git a/src/components/TodoApp/TodoApp.tsx b/src/components/TodoApp/TodoApp.tsx index 31ca31ece..e6b3f2e69 100644 --- a/src/components/TodoApp/TodoApp.tsx +++ b/src/components/TodoApp/TodoApp.tsx @@ -1,5 +1,6 @@ import React, { useContext } from 'react'; +import './TodoApp.scss'; import { Header } from '../Header'; import { Main } from '../Main'; import { Footer } from '../Footer'; diff --git a/src/components/TodoItem/TodoItem.scss b/src/components/TodoItem/TodoItem.scss new file mode 100644 index 000000000..d228f199a --- /dev/null +++ b/src/components/TodoItem/TodoItem.scss @@ -0,0 +1,5 @@ +@import "../../utils/mixins.scss"; + +.edit { + @include editTitle; +} diff --git a/src/components/TodoItem/TodoItem.tsx b/src/components/TodoItem/TodoItem.tsx index efca08f21..4eaff1e25 100644 --- a/src/components/TodoItem/TodoItem.tsx +++ b/src/components/TodoItem/TodoItem.tsx @@ -2,9 +2,9 @@ import React, { useContext, useRef, useState, useEffect, } from 'react'; - import classNames from 'classnames'; +import './TodoItem.scss'; import { Todo } from '../../types/Todo'; import { DispatchContext } from '../TodosContext'; @@ -19,16 +19,30 @@ export const TodoItem: React.FC = ({ item }) => { const [isEditing, setIsEditing] = useState(false); const [editedTodoTitle, setEditedTodoTitle] = useState(item.title); - const handleRemoveItem = () => dispatch({ - type: 'remove', - payload: item.id, - }); + useEffect(() => { + if (isEditing) { + editRef.current?.focus(); + } + }, [isEditing]); const handleCompletedClick = () => dispatch({ type: 'toggle', payload: item, }); + const handleDoubleClick = () => { + setIsEditing(true); + }; + + const handleRemoveItem = () => dispatch({ + type: 'remove', + payload: item.id, + }); + + const handleTitleChange = (event: React.ChangeEvent) => { + setEditedTodoTitle(event.target.value); + }; + const saveChanges = () => { if (editedTodoTitle.trim().length !== 0) { dispatch({ @@ -42,16 +56,6 @@ export const TodoItem: React.FC = ({ item }) => { setIsEditing(false); }; - useEffect(() => { - if (isEditing) { - editRef.current?.focus(); - } - }, [isEditing]); - - const handleDoubleClick = () => { - setIsEditing(true); - }; - const handleKeyUp = (event: React.KeyboardEvent) => { if (event.key === 'Escape') { setEditedTodoTitle(item.title); @@ -63,10 +67,6 @@ export const TodoItem: React.FC = ({ item }) => { } }; - const handleChange = (event: React.ChangeEvent) => { - setEditedTodoTitle(event.target.value); - }; - return (
  • = ({ item }) => { type="text" className="edit" value={editedTodoTitle} - onChange={handleChange} + onChange={handleTitleChange} onKeyUp={handleKeyUp} onBlur={saveChanges} /> diff --git a/src/components/TodoList/TodoList.scss b/src/components/TodoList/TodoList.scss new file mode 100644 index 000000000..92b61e0e6 --- /dev/null +++ b/src/components/TodoList/TodoList.scss @@ -0,0 +1,110 @@ +.todo-list { + margin: 0; + padding: 0; + list-style: none; + + & li { + position: relative; + font-size: 24px; + border-bottom: 1px solid #ededed; + + &:last-child { + border-bottom: none; + } + + &.editing { + border-bottom: none; + padding: 0; + } + + &.editing:last-child { + margin-bottom: -1px; + } + + & .edit { + display: none; + } + + &.editing .edit { + display: block; + width: 506px; + padding: 12px 16px; + margin: 0 0 0 43px; + } + + &.editing .view { + display: none; + } + + & .toggle { + text-align: center; + width: 40px; + height: auto; + position: absolute; + top: 0; + bottom: 0; + margin: auto 0; + border: none; + -webkit-appearance: none; + appearance: none; + opacity: 0; + } + + & label { + word-break: break-all; + padding: 15px 15px 15px 60px; + display: block; + line-height: 1.2; + transition: color 0.4s; + } + + &.completed label { + color: #d9d9d9; + text-decoration: line-through; + } + + & .toggle + label { + background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: center left; + } + + & .toggle:checked + label { + background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E"); + } + + & .destroy { + display: none; + position: absolute; + top: 0; + right: 10px; + bottom: 0; + width: 40px; + height: 40px; + margin: auto 0; + font-size: 30px; + color: #cc9a9a; + margin-bottom: 11px; + transition: color 0.2s ease-out; + } + + & .destroy:hover { + color: #af5b5e; + } + + & .destroy:after { + content: "×"; + } + + &:hover .destroy { + display: block; + } + + @media screen and (-webkit-min-device-pixel-ratio:0) { + & .toggle { + height: 40px; + background: none; + } + } + } +} diff --git a/src/components/TodoList/TodoList.tsx b/src/components/TodoList/TodoList.tsx index d2f6c8ece..e4d456be9 100644 --- a/src/components/TodoList/TodoList.tsx +++ b/src/components/TodoList/TodoList.tsx @@ -1,4 +1,6 @@ import React from 'react'; + +import './TodoList.scss'; import { Todo } from '../../types/Todo'; import { TodoItem } from '../TodoItem'; diff --git a/src/components/TodosContext/TodosContext.tsx b/src/components/TodosContext/TodosContext.tsx index 01a18f808..84117b44d 100644 --- a/src/components/TodosContext/TodosContext.tsx +++ b/src/components/TodosContext/TodosContext.tsx @@ -1,102 +1,11 @@ import React, { useReducer, useState } from 'react'; import { Todo } from '../../types/Todo'; +import { Action } from '../../types/Action'; import { Status } from '../../types/Status'; -const key = 'todos'; - -type Action = { type: 'add', payload: Todo } -| { type: 'remove', payload: number } -| { type: 'clearAllCompleted', payload: Todo[] } -| { type: 'toggle', payload: Todo } -| { type: 'toggleAll', payload: boolean } -| { type: 'edit', payload: Todo }; - -const save = (newState: Todo[]) => { - localStorage.setItem(key, JSON.stringify(newState)); -}; - -function reducer(state: Todo[], action: Action): Todo[] { - let currentState: Todo[] = []; - - switch (action.type) { - case 'add': - currentState = [ - ...state, - action.payload, - ]; - break; - - case 'remove': - currentState = [...state].filter(todo => todo.id !== action.payload); - break; - - case 'clearAllCompleted': - currentState = [...action.payload]; - break; - - case 'toggle': - currentState = [...state].map(todo => { - if (todo.id === action.payload.id) { - return { - ...todo, - completed: !action.payload.completed, - }; - } - - return { ...todo }; - }); - break; - - case 'toggleAll': - currentState = [...state].map(todo => { - return { - ...todo, - completed: action.payload, - }; - }); - break; - - case 'edit': - currentState = [...state].map(todo => { - if (todo.id === action.payload.id) { - return { - ...todo, - title: action.payload.title, - }; - } - - return { ...todo }; - }); - break; - - default: - currentState = [...state]; - break; - } - - save(currentState); - - return currentState; -} - -const initialState: Todo[] = []; - -const getStartingState = (): Todo[] => { - const data = localStorage.getItem(key); - - if (data === null) { - return initialState; - } - - try { - return JSON.parse(data); - } catch (e) { - localStorage.removeItem(key); - - return initialState; - } -}; +import { getStartingState } from '../../utils/manageLocalState'; +import { todosReducer } from '../../utils/todosReducer'; const startingState = getStartingState(); @@ -115,7 +24,7 @@ type Props = { }; export const TodosContext: React.FC = ({ children }) => { - const [todos, dispatch] = useReducer(reducer, startingState); + const [todos, dispatch] = useReducer(todosReducer, startingState); const [currentFilter, setCurrentFilter] = useState(Status.All); return ( diff --git a/src/components/TodosFilter/TodosFilter.scss b/src/components/TodosFilter/TodosFilter.scss new file mode 100644 index 000000000..7526886e3 --- /dev/null +++ b/src/components/TodosFilter/TodosFilter.scss @@ -0,0 +1,36 @@ +.filters { + margin: 0; + padding: 0; + list-style: none; + position: absolute; + right: 0; + left: 0; + + & li { + display: inline; + + & a { + color: inherit; + margin: 3px; + padding: 3px 7px; + text-decoration: none; + border: 1px solid transparent; + border-radius: 3px; + + &:hover { + border-color: rgba(175, 47, 47, 0.1); + } + + &.selected { + border-color: rgba(175, 47, 47, 0.2); + } + } + + } + + @media (max-width: 430px) { + & { + bottom: 10px; + } + } +} diff --git a/src/components/TodosFilter/TodosFilter.tsx b/src/components/TodosFilter/TodosFilter.tsx index 55b9bf2a2..01e5c9285 100644 --- a/src/components/TodosFilter/TodosFilter.tsx +++ b/src/components/TodosFilter/TodosFilter.tsx @@ -1,6 +1,7 @@ import React, { useContext } from 'react'; import classNames from 'classnames'; +import './TodosFilter.scss'; import { Status } from '../../types/Status'; import { FilterContext, SetFilterContext } from '../TodosContext'; @@ -8,10 +9,6 @@ export const TodosFilter: React.FC = () => { const currentFilter = useContext(FilterContext); const setCurrentFilter = useContext(SetFilterContext); - const handleFilterChange = (status: Status) => { - setCurrentFilter(status); - }; - return (
    • @@ -20,7 +17,7 @@ export const TodosFilter: React.FC = () => { className={classNames({ selected: currentFilter === Status.All, })} - onClick={() => handleFilterChange(Status.All)} + onClick={() => setCurrentFilter(Status.All)} > {Status.All} @@ -32,7 +29,7 @@ export const TodosFilter: React.FC = () => { className={classNames({ selected: currentFilter === Status.Active, })} - onClick={() => handleFilterChange(Status.Active)} + onClick={() => setCurrentFilter(Status.Active)} > {Status.Active} @@ -44,7 +41,7 @@ export const TodosFilter: React.FC = () => { className={classNames({ selected: currentFilter === Status.Completed, })} - onClick={() => handleFilterChange(Status.Completed)} + onClick={() => setCurrentFilter(Status.Completed)} > {Status.Completed} diff --git a/src/index.tsx b/src/index.tsx index a9689cb38..dc823a480 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,9 +1,5 @@ import { createRoot } from 'react-dom/client'; -import './styles/index.css'; -import './styles/todo-list.css'; -import './styles/filters.css'; - import { App } from './App'; const container = document.getElementById('root') as HTMLDivElement; diff --git a/src/styles/filters.css b/src/styles/filters.css deleted file mode 100644 index 4df7da8bc..000000000 --- a/src/styles/filters.css +++ /dev/null @@ -1,35 +0,0 @@ -.filters { - margin: 0; - padding: 0; - list-style: none; - position: absolute; - right: 0; - left: 0; -} - -.filters li { - display: inline; -} - -.filters li a { - color: inherit; - margin: 3px; - padding: 3px 7px; - text-decoration: none; - border: 1px solid transparent; - border-radius: 3px; -} - -.filters li a:hover { - border-color: rgba(175, 47, 47, 0.1); -} - -.filters li a.selected { - border-color: rgba(175, 47, 47, 0.2); -} - -@media (max-width: 430px) { - .filters { - bottom: 10px; - } -} diff --git a/src/styles/index.css b/src/styles/index.css deleted file mode 100644 index 2c658dc19..000000000 --- a/src/styles/index.css +++ /dev/null @@ -1,214 +0,0 @@ -html, -body { - margin: 0; - padding: 0; -} - -iframe { - display: none; -} - -button { - margin: 0; - padding: 0; - border: 0; - background: none; - font-size: 100%; - vertical-align: baseline; - font-family: inherit; - font-weight: inherit; - color: inherit; - -webkit-appearance: none; - appearance: none; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -body { - font: 14px "Helvetica Neue", Helvetica, Arial, sans-serif; - line-height: 1.4em; - background: #f5f5f5; - color: #4d4d4d; - min-width: 230px; - max-width: 550px; - margin: 0 auto; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - font-weight: 300; -} - -.hidden { - display: none; -} - -.todoapp { - background: #fff; - margin: 130px 0 40px; - position: relative; - box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), - 0 25px 50px 0 rgba(0, 0, 0, 0.1); -} - -.todoapp input::-webkit-input-placeholder { - font-style: italic; - font-weight: 300; - color: #e6e6e6; -} - -.todoapp input::-moz-placeholder { - font-style: italic; - font-weight: 300; - color: #e6e6e6; -} - -.todoapp input::-ms-input-placeholder { - font-style: italic; - font-weight: 300; - color: #e6e6e6; -} - -.todoapp input::placeholder { - font-style: italic; - font-weight: 300; - color: #e6e6e6; -} - -.todoapp h1 { - position: absolute; - top: -155px; - width: 100%; - font-size: 100px; - font-weight: 100; - text-align: center; - color: rgba(175, 47, 47, 0.15); - -webkit-text-rendering: optimizeLegibility; - -moz-text-rendering: optimizeLegibility; - text-rendering: optimizeLegibility; -} - -.new-todo, -.edit { - position: relative; - margin: 0; - width: 100%; - font-size: 24px; - font-family: inherit; - font-weight: inherit; - line-height: 1.4em; - color: inherit; - padding: 6px; - border: 1px solid #999; - box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); - box-sizing: border-box; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -.new-todo { - padding: 16px 16px 16px 60px; - border: none; - background: rgba(0, 0, 0, 0.01); - box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); -} - -.main { - position: relative; - z-index: 2; - border-top: 1px solid #e6e6e6; -} - -.toggle-all { - width: 1px; - height: 1px; - border: none; /* Mobile Safari */ - opacity: 0; - position: absolute; - right: 100%; - bottom: 100%; -} - -.toggle-all + label { - width: 60px; - height: 34px; - font-size: 0; - position: absolute; - top: -52px; - left: -13px; - -webkit-transform: rotate(90deg); - transform: rotate(90deg); -} - -.toggle-all + label:before { - content: "❯"; - font-size: 22px; - color: #e6e6e6; - padding: 10px 27px; -} - -.toggle-all:checked + label:before { - color: #737373; -} - -.footer { - color: #777; - padding: 10px 15px; - height: 20px; - text-align: center; - border-top: 1px solid #e6e6e6; -} - -.footer:before { - content: ""; - position: absolute; - right: 0; - bottom: 0; - left: 0; - height: 50px; - overflow: hidden; - box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), - 0 8px 0 -3px #f6f6f6, - 0 9px 1px -3px rgba(0, 0, 0, 0.2), - 0 16px 0 -6px #f6f6f6, - 0 17px 2px -6px rgba(0, 0, 0, 0.2); -} - -.todo-count { - float: left; - text-align: left; -} - -.todo-count strong { - font-weight: 300; -} - -.clear-completed { - float: right; - position: relative; - line-height: 20px; - text-decoration: none; - cursor: pointer; -} - -.clear-completed:hover { - text-decoration: underline; -} - -.clear-completed:active { - text-decoration: none; -} - -/* - Hack to remove background from Mobile Safari. - Can't use it globally since it destroys checkboxes in Firefox -*/ -@media screen and (-webkit-min-device-pixel-ratio:0) { - .toggle-all { - background: none; - } -} - -@media (max-width: 430px) { - .footer { - height: 50px; - } -} diff --git a/src/styles/todo-list.css b/src/styles/todo-list.css deleted file mode 100644 index 277b0bebc..000000000 --- a/src/styles/todo-list.css +++ /dev/null @@ -1,116 +0,0 @@ - -.todo-list { - margin: 0; - padding: 0; - list-style: none; -} - -.todo-list li { - position: relative; - font-size: 24px; - border-bottom: 1px solid #ededed; -} - -.todo-list li:last-child { - border-bottom: none; -} - -.todo-list li.editing { - border-bottom: none; - padding: 0; -} - -.todo-list li.editing:last-child { - margin-bottom: -1px; -} - -.todo-list li .edit { - display: none; -} - -.todo-list li.editing .edit { - display: block; - width: 506px; - padding: 12px 16px; - margin: 0 0 0 43px; -} - -.todo-list li.editing .view { - display: none; -} - -.todo-list li .toggle { - text-align: center; - width: 40px; - /* auto, since non-WebKit browsers doesn't support input styling */ - height: auto; - position: absolute; - top: 0; - bottom: 0; - margin: auto 0; - border: none; /* Mobile Safari */ - -webkit-appearance: none; - appearance: none; - opacity: 0; -} - -.todo-list li label { - word-break: break-all; - padding: 15px 15px 15px 60px; - display: block; - line-height: 1.2; - transition: color 0.4s; -} - -.todo-list li.completed label { - color: #d9d9d9; - text-decoration: line-through; -} - -.todo-list li .toggle + label { - background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: center left; -} - -.todo-list li .toggle:checked + label { - background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E"); -} - -.todo-list li .destroy { - display: none; - position: absolute; - top: 0; - right: 10px; - bottom: 0; - width: 40px; - height: 40px; - margin: auto 0; - font-size: 30px; - color: #cc9a9a; - margin-bottom: 11px; - transition: color 0.2s ease-out; -} - -.todo-list li .destroy:hover { - color: #af5b5e; -} - -.todo-list li .destroy:after { - content: "×"; -} - -.todo-list li:hover .destroy { - display: block; -} - -/* - Hack to remove background from Mobile Safari. - Can't use it globally since it destroys checkboxes in Firefox -*/ -@media screen and (-webkit-min-device-pixel-ratio:0) { - .todo-list li .toggle { - height: 40px; - background: none; - } -} diff --git a/src/types/Action.ts b/src/types/Action.ts new file mode 100644 index 000000000..d859917c5 --- /dev/null +++ b/src/types/Action.ts @@ -0,0 +1,8 @@ +import { Todo } from './Todo'; + +export type Action = { type: 'add', payload: Todo } +| { type: 'remove', payload: number } +| { type: 'clearAllCompleted', payload: Todo[] } +| { type: 'toggle', payload: Todo } +| { type: 'toggleAll', payload: boolean } +| { type: 'edit', payload: Todo }; diff --git a/src/utils/manageLocalState.ts b/src/utils/manageLocalState.ts new file mode 100644 index 000000000..dd52ffcdd --- /dev/null +++ b/src/utils/manageLocalState.ts @@ -0,0 +1,24 @@ +import { Todo } from '../types/Todo'; + +const key = 'todos'; +const initialState: Todo[] = []; + +export const getStartingState = (): Todo[] => { + const data = localStorage.getItem(key); + + if (data === null) { + return initialState; + } + + try { + return JSON.parse(data); + } catch (e) { + localStorage.removeItem(key); + + return initialState; + } +}; + +export const saveState = (newState: Todo[]) => { + localStorage.setItem(key, JSON.stringify(newState)); +}; diff --git a/src/utils/mixins.scss b/src/utils/mixins.scss new file mode 100644 index 000000000..eaa0f2dca --- /dev/null +++ b/src/utils/mixins.scss @@ -0,0 +1,16 @@ +@mixin editTitle { + position: relative; + margin: 0; + width: 100%; + font-size: 24px; + font-family: inherit; + font-weight: inherit; + line-height: 1.4em; + color: inherit; + padding: 6px; + border: 1px solid #999; + box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); + box-sizing: border-box; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/src/utils/todosReducer.ts b/src/utils/todosReducer.ts new file mode 100644 index 000000000..f61a159f7 --- /dev/null +++ b/src/utils/todosReducer.ts @@ -0,0 +1,67 @@ +import { Action } from '../types/Action'; +import { Todo } from '../types/Todo'; +import { saveState } from './manageLocalState'; + +export function todosReducer(state: Todo[], action: Action): Todo[] { + let currentState: Todo[] = []; + + switch (action.type) { + case 'add': + currentState = [ + ...state, + action.payload, + ]; + break; + + case 'remove': + currentState = [...state].filter(todo => todo.id !== action.payload); + break; + + case 'clearAllCompleted': + currentState = [...action.payload]; + break; + + case 'toggle': + currentState = [...state].map(todo => { + if (todo.id === action.payload.id) { + return { + ...todo, + completed: !action.payload.completed, + }; + } + + return { ...todo }; + }); + break; + + case 'toggleAll': + currentState = [...state].map(todo => { + return { + ...todo, + completed: action.payload, + }; + }); + break; + + case 'edit': + currentState = [...state].map(todo => { + if (todo.id === action.payload.id) { + return { + ...todo, + title: action.payload.title, + }; + } + + return { ...todo }; + }); + break; + + default: + currentState = [...state]; + break; + } + + saveState(currentState); + + return currentState; +}