Skip to content

Commit

Permalink
add solution
Browse files Browse the repository at this point in the history
  • Loading branch information
Oleh Chernov committed Mar 6, 2025
1 parent c867b63 commit b216ffb
Show file tree
Hide file tree
Showing 33 changed files with 862 additions and 308 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@ Implement a simple [TODO app](https://mate-academy.github.io/react_todo-app/) th
- Implement a solution following the [React task guidelines](https://github.com/mate-academy/react_task-guideline#react-tasks-guideline).
- Use the [React TypeScript cheat sheet](https://mate-academy.github.io/fe-program/js/extra/react-typescript).
- Open another terminal and run tests with `npm test` to ensure your solution is correct.
- Replace `<your_account>` with your GitHub username in the [DEMO LINK](https://<your_account>.github.io/react_todo-app/) and add it to the PR description.
- Replace `<your_account>` with your GitHub username in the [DEMO LINK](https://nineuito.github.io/react_todo-app/) and add it to the PR description.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
},
"devDependencies": {
"@cypress/react18": "^2.0.1",
"@mate-academy/scripts": "^1.9.12",
"@mate-academy/scripts": "^2.1.0",
"@mate-academy/students-ts-config": "*",
"@mate-academy/stylelint-config": "*",
"@types/node": "^20.14.10",
Expand Down
24 changes: 24 additions & 0 deletions src/App.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
.todoapp {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 24px;
font-weight: 300;
color: #4d4d4d;
margin: 40px 20px;

&__content {
margin-bottom: 20px;
background: #fff;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
0 25px 50px 0 rgba(0, 0, 0, 0.1);
}

&__title {
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;
}
}
157 changes: 7 additions & 150 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,157 +1,14 @@
/* eslint-disable jsx-a11y/control-has-associated-label */
import React from 'react';
import React, { useState } from 'react';

export const App: React.FC = () => {
return (
<div className="todoapp">
<h1 className="todoapp__title">todos</h1>

<div className="todoapp__content">
<header className="todoapp__header">
{/* this button should have `active` class only if all todos are completed */}
<button
type="button"
className="todoapp__toggle-all active"
data-cy="ToggleAllButton"
/>

{/* Add a todo on form submit */}
<form>
<input
data-cy="NewTodoField"
type="text"
className="todoapp__new-todo"
placeholder="What needs to be done?"
/>
</form>
</header>

<section className="todoapp__main" data-cy="TodoList">
{/* This is a completed todo */}
<div data-cy="Todo" className="todo completed">
<label className="todo__status-label">
<input
data-cy="TodoStatus"
type="checkbox"
className="todo__status"
checked
/>
</label>

<span data-cy="TodoTitle" className="todo__title">
Completed Todo
</span>

{/* Remove button appears only on hover */}
<button type="button" className="todo__remove" data-cy="TodoDelete">
×
</button>
</div>

{/* This todo is an active todo */}
<div data-cy="Todo" className="todo">
<label className="todo__status-label">
<input
data-cy="TodoStatus"
type="checkbox"
className="todo__status"
/>
</label>

<span data-cy="TodoTitle" className="todo__title">
Not Completed Todo
</span>
import './App.scss';

<button type="button" className="todo__remove" data-cy="TodoDelete">
×
</button>
</div>
import { FilterType } from './types/FilterType';

{/* This todo is being edited */}
<div data-cy="Todo" className="todo">
<label className="todo__status-label">
<input
data-cy="TodoStatus"
type="checkbox"
className="todo__status"
/>
</label>
import { AppContent } from './components/AppContent';

{/* This form is shown instead of the title and remove button */}
<form>
<input
data-cy="TodoTitleField"
type="text"
className="todo__title-field"
placeholder="Empty todo will be deleted"
value="Todo is being edited now"
/>
</form>
</div>

{/* This todo is in loadind state */}
<div data-cy="Todo" className="todo">
<label className="todo__status-label">
<input
data-cy="TodoStatus"
type="checkbox"
className="todo__status"
/>
</label>

<span data-cy="TodoTitle" className="todo__title">
Todo is being saved now
</span>

<button type="button" className="todo__remove" data-cy="TodoDelete">
×
</button>
</div>
</section>

{/* Hide the footer if there are no todos */}
<footer className="todoapp__footer" data-cy="Footer">
<span className="todo-count" data-cy="TodosCounter">
3 items left
</span>

{/* Active link should have the 'selected' class */}
<nav className="filter" data-cy="Filter">
<a
href="#/"
className="filter__link selected"
data-cy="FilterLinkAll"
>
All
</a>

<a
href="#/active"
className="filter__link"
data-cy="FilterLinkActive"
>
Active
</a>

<a
href="#/completed"
className="filter__link"
data-cy="FilterLinkCompleted"
>
Completed
</a>
</nav>
export const App: React.FC = () => {
const [filterType, setFilterType] = useState<FilterType>(FilterType.ALL);

{/* this button should be disabled if there are no completed todos */}
<button
type="button"
className="todoapp__clear-completed"
data-cy="ClearCompletedButton"
>
Clear completed
</button>
</footer>
</div>
</div>
);
return <AppContent filterType={filterType} setFilterType={setFilterType} />;
};
47 changes: 47 additions & 0 deletions src/components/AppContent/AppContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useContext, useEffect, useRef } from 'react';

import { Header } from '../Header';
import { Footer } from '../Footer';
import { TodoList } from '../TodoList';
import { ErrorNotification } from '../ErrorNotification';

import { TodoContext } from '../../context/TodoContext';
import { AppContentProps } from '../../types/AppContentProps';

export const AppContent: React.FC<AppContentProps> = ({
filterType,
setFilterType,
}) => {
const context = useContext(TodoContext);
if (!context) throw new Error('TodoContext must be used within TodoProvider');
const { todos, errorMessage, setErrorMessage } = context;

const inputRef = useRef<HTMLInputElement>(null);

useEffect(() => {
inputRef.current?.focus();
}, []);

return (
<div className="todoapp">
<h1 className="todoapp__title">todos</h1>
<div className="todoapp__content">
<Header inputRef={inputRef} />
{todos.length > 0 && (
<TodoList filterType={filterType} inputRef={inputRef} />
)}
{todos.length > 0 && (
<Footer
filterType={filterType}
onFilterType={setFilterType}
inputRef={inputRef}
/>
)}
<ErrorNotification
errorMessage={errorMessage}
setErrorMessage={setErrorMessage}
/>
</div>
</div>
);
};
1 change: 1 addition & 0 deletions src/components/AppContent/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './AppContent';
32 changes: 32 additions & 0 deletions src/components/ErrorNotification/ErrorNotification.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
.notification {
background-color: #f8d7da;
border: 1px solid #f5c6cb;
color: #721c24;
padding: 12px 16px;
border-radius: 4px;
font-size: 16px;
line-height: 1.5;
margin: 8px 0;
transition: opacity 1s, min-height 1s;
min-height: 36px;
position: relative;
}

.notification .delete {
position: absolute;
right: 8px;
top: 8px;
background: none;
border: none;
font-size: 20px;
color: #721c24;
cursor: pointer;
}

.notification.hidden {
min-height: 0;
opacity: 0;
pointer-events: none;
margin: 0;
padding: 0 16px;
}
50 changes: 50 additions & 0 deletions src/components/ErrorNotification/ErrorNotification.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React, { useEffect } from 'react';

import './errorNotification.scss';

import cn from 'classnames';

type Props = {
errorMessage: string;
setErrorMessage: (errorMessage: string) => void;
};

export const ErrorNotification: React.FC<Props> = ({
errorMessage,
setErrorMessage,
}) => {
useEffect(() => {
let timeoutId: ReturnType<typeof setTimeout>;

if (errorMessage) {
timeoutId = setTimeout(() => {
setErrorMessage('');
}, 3000);
}

return () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
};
}, [errorMessage, setErrorMessage]);

return (
<div
data-cy="ErrorNotification"
className={cn('notification is-danger is-light has-text-weight-normal', {
hidden: !errorMessage,
})}
>
<button
data-cy="HideErrorButton"
type="button"
className="delete"
onClick={() => setErrorMessage('')}
>
×
</button>
{errorMessage}
</div>
);
};
1 change: 1 addition & 0 deletions src/components/ErrorNotification/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './ErrorNotification';
File renamed without changes.
39 changes: 39 additions & 0 deletions src/components/Filter/Filter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from 'react';
import cn from 'classnames';

import './Filter.scss';

import { FilterType } from '../../types/FilterType';

type Props = {
filterType: FilterType;
onFilterType: (filterType: FilterType) => void;
};

export const Filter: React.FC<Props> = ({ filterType, onFilterType }) => {
const handleFilterChange =
(type: FilterType) => (event: React.MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
onFilterType(type);
};

return (
<nav className="filter" data-cy="Filter">
{(Object.values(FilterType) as FilterType[]).map(filter => {
const displayName = filter.charAt(0).toUpperCase() + filter.slice(1);

return (
<a
key={filter}
href={`#/${filter.toLowerCase()}`}
className={cn('filter__link', { selected: filterType === filter })}
data-cy={`FilterLink${displayName}`}
onClick={handleFilterChange(filter)}
>
{displayName}
</a>
);
})}
</nav>
);
};
1 change: 1 addition & 0 deletions src/components/Filter/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Filter';
Loading

0 comments on commit b216ffb

Please sign in to comment.