diff --git a/README.md b/README.md
index d1c68b5..ece7b69 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,24 @@
-# Todo
\ No newline at end of file
+## links
+- github: https://github.com/alex91-html/js-project-todo
+- netlify: https://alextodo-app.netlify.app/
+
+
+
\ No newline at end of file
diff --git a/package.json b/package.json
index caf6289..b93f926 100644
--- a/package.json
+++ b/package.json
@@ -10,8 +10,15 @@
"preview": "vite preview"
},
"dependencies": {
+ "@lottiefiles/dotlottie-react": "^0.13.5",
+ "date-fns": "^4.1.0",
+ "lottie-react": "^2.4.1",
"react": "^19.0.0",
- "react-dom": "^19.0.0"
+ "react-dom": "^19.0.0",
+ "react-icons": "^5.5.0",
+ "react-router-dom": "^7.6.1",
+ "styled-components": "^6.1.18",
+ "zustand": "^5.0.5"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
diff --git a/pull_request_template.md b/pull_request_template.md
index 154c92e..71e8fba 100644
--- a/pull_request_template.md
+++ b/pull_request_template.md
@@ -1 +1,3 @@
-Please include your Netlify link here.
\ No newline at end of file
+## links
+- github: https://github.com/alex91-html/js-project-todo
+- netlify: https://alextodo-app.netlify.app/Please include your Netlify link here.
\ No newline at end of file
diff --git a/src/App.jsx b/src/App.jsx
index 5427540..5428727 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,5 +1,24 @@
+import { BrowserRouter as Router } from 'react-router-dom';
+import { GlobalStyle } from './styles/GlobalStyles';
+import { MainContainer } from './components/StyledComponents';
+import { useThemeStore } from './store/themeStore';
+import Header from './components/Header';
+import AddTask from './components/AddTask';
+import TaskList from './components/TaskList';
+
export const App = () => {
+ const theme = useThemeStore((s) => s.theme);
+
return (
-
React Boilerplate
- )
-}
+
+
+
+
+
+
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/src/components/AddTask.jsx b/src/components/AddTask.jsx
new file mode 100644
index 0000000..110c96d
--- /dev/null
+++ b/src/components/AddTask.jsx
@@ -0,0 +1,56 @@
+import { useState } from 'react';
+import { useThemeStore } from '../store/themeStore';
+
+import styled from 'styled-components';
+import AddTaskModal from './AddTaskModal';
+
+const AddButton = styled.button`
+ position: fixed;
+ right: 24px;
+ bottom: 24px;
+ width: 56px;
+ height: 56px;
+ border-radius: 20%;
+ background: ${({ theme }) => theme === 'dark' ? '#b4b4b4' : '#111'};
+ color: ${({ theme }) => theme === 'dark' ? '#4e4e4e' : '#fff'};
+ font-size: 2.2rem;
+ font-weight: 200;
+ font-family: 'Arial', 'Helvetica Neue', Arial, sans-serif;
+ border: none;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.12);
+ cursor: pointer;
+ z-index: 100;
+ transition: background 0.2s, color 0.2s, box-shadow 0.18s, transform 0.18s;
+
+
+&:hover {
+ background: ${({ theme }) => theme === 'dark' ? '#FAE179' : '#40405c'};
+ }
+
+ @media (min-width: 1024px) {
+ position: absolute;
+ right: 40px;
+ bottom: 40px;
+ }
+
+`;
+
+const AddTask = () => {
+ const [open, setOpen] = useState(false);
+ const theme = useThemeStore((s) => s.theme);
+
+
+ return (
+ <>
+ setOpen(true)} aria-label="Add task" theme={theme}>
+ +
+
+ {open && setOpen(false)} />}
+ >
+ );
+};
+
+export default AddTask;
\ No newline at end of file
diff --git a/src/components/AddTaskModal.jsx b/src/components/AddTaskModal.jsx
new file mode 100644
index 0000000..ddac962
--- /dev/null
+++ b/src/components/AddTaskModal.jsx
@@ -0,0 +1,192 @@
+import styled from 'styled-components';
+import { useState } from 'react';
+import { useTodoStore } from '../store/todoStore';
+import { useThemeStore } from '../store/themeStore';
+
+
+const ModalOverlay = styled.div`
+ position: fixed;
+ inset: 0;
+ background: rgba(0,0,0,0.45);
+ display: flex;
+ align-items: flex-end;
+ justify-content: center;
+ z-index: 200;
+
+ @media (min-width: 1024px) {
+ position: absolute;
+ left: 0;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ border-radius: 32px;
+
+ }
+`;
+
+const Modal = styled.div`
+ background: ${({ theme }) => theme === 'dark' ? '#23272f' : '#fff'};
+ color: ${({ theme }) => theme === 'dark' ? '#fafafa' : '#222'};
+ width: 100%;
+ max-width: 600px;
+ border-radius: 32px 32px 0 0;
+ padding: 32px 24px 24px 24px;
+ box-shadow: 0 -2px 24px rgba(0,0,0,0.08);
+ margin-bottom: 0;
+
+@media (min-width: 1024px) {
+ width: 100%;
+ max-width: 600px;
+ border-radius: 32px;
+ padding: 48px 40px;
+ box-shadow: 0 8px 32px rgba(0,0,0,0.08);
+}
+
+`;
+
+const ModalBar = styled.div`
+ width: 60px;
+ height: 6px;
+ background: #e0e3e7;
+ border-radius: 3px;
+ margin: 0 auto 18px auto;
+`;
+
+const ModalTitle = styled.h2`
+ color: ${({ theme }) => theme === 'dark' ? '#fafafa' : '#222'};
+ text-align: center;
+ font-size: 2rem;
+ margin: 0 0 24px 0;
+`;
+
+const Input = styled.input`
+ width: 100%;
+ padding: 18px;
+ border-radius: 16px;
+ border: 2px solid #b5cdfa;
+ background: #f7f8fa;
+ font-size: 1.15rem;
+ margin-bottom: 18px;
+ outline: none;
+ color: #222;
+ &::placeholder {
+ color: #aab2bb;
+ font-size: 1.1rem;
+ }
+`;
+
+const Select = styled.select`
+ width: 100%;
+ padding: 16px;
+ border-radius: 12px;
+ border: none;
+ background: #f7f8fa;
+ font-size: 1.1rem;
+ margin-bottom: 28px;
+ color: #222;
+`;
+
+const ButtonRow = styled.div`
+ display: flex;
+ gap: 16px;
+`;
+
+const CancelButton = styled.button`
+ flex: 1;
+ padding: 18px 0;
+ border-radius: 16px;
+ border: none;
+ background: #f7f8fa;
+ color: #6b7280;
+
+ &:hover {
+ background:#e57373;
+ color: #fff;
+ }
+
+
+`;
+
+const SubmitButton = styled.button`
+ flex: 2;
+ padding: 18px 0;
+ border-radius: 16px;
+ border: none;
+ background: #888;
+ color: #fff;
+
+ &:hover {
+ background: #40405c;;
+ }
+
+`;
+
+const AddTaskModal = ({ onClose }) => {
+ const [value, setValue] = useState('');
+ const [category, setCategory] = useState('General');
+ const addTask = useTodoStore((s) => s.addTask);
+ const theme = useThemeStore((s) => s.theme);
+
+
+ const handleSubmit = (e) => {
+ e.preventDefault();
+ if (value.trim()) {
+ addTask({
+ id: Date.now(),
+ title: value,
+ category,
+ completed: false,
+ createdAt: new Date().toISOString(),
+ });
+ setValue('');
+ setCategory('General');
+ onClose();
+ }
+ };
+
+ const handleOverlayClick = (e) => {
+ if (e.target === e.currentTarget) onClose();
+ };
+
+ return (
+
+
+
+ Add New Task
+
+
+
+ );
+};
+
+export default AddTaskModal;
\ No newline at end of file
diff --git a/src/components/Header.jsx b/src/components/Header.jsx
new file mode 100644
index 0000000..b9a72a9
--- /dev/null
+++ b/src/components/Header.jsx
@@ -0,0 +1,96 @@
+import styled from 'styled-components';
+import { format } from 'date-fns';
+import { useTodoStore } from '../store/todoStore';
+import { useThemeStore } from '../store/themeStore';
+
+const HeaderContainer = styled.header`
+ display: flex;
+ flex-direction: column;
+ padding: 20px;
+ background: ${({ theme }) => theme === 'dark' ? '#23272f' : '#fff'};
+
+ @media (min-width: 1024px) {
+ border-top-left-radius: 32px;
+ border-top-right-radius: 32px;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);
+ }
+`;
+
+const DateText = styled.div`
+ font-size: 0.95rem;
+ color: #6a6a6a;
+ margin-top: 12px;
+`;
+
+const DayText = styled.h1`
+ margin: 0;
+ margin-top: 2px;
+`;
+const TaskCount = styled.div`
+ font-size: 1rem;
+ color:#6a6a6a;
+ margin: 8px 0 0 0;
+`;
+
+const ThemeSwitch = styled.button`
+ position: absolute;
+ top: 20px;
+ right: 20px;
+ width: 48px;
+ height: 28px;
+ background: ${({ theme }) => theme === 'dark' ? '#222' : '#eee'};
+ border: none;
+ border-radius: 16px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ padding: 0 4px;
+ transition: background 0.2s;
+ outline: none;
+ z-index: 10;
+
+ &:focus {
+ box-shadow: 0 0 0 2px #8882;
+ }
+`;
+
+const SwitchKnob = styled.span`
+ display: block;
+ width: 22px;
+ height: 22px;
+ border-radius: 50%;
+ background: ${({ theme }) => theme === 'dark' ? '#ffe066' : '#333'};
+ transform: ${({ theme }) => theme === 'dark' ? 'translateX(20px)' : 'translateX(0)'};
+ transition: transform 0.2s, background 0.2s;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.10);
+`;
+
+const Header = () => {
+ const now = new Date();
+ const tasks = useTodoStore((s) => s.tasks);
+ const uncompleted = tasks.filter(t => !t.completed).length;
+ const theme = useThemeStore((s) => s.theme);
+ const toggleTheme = useThemeStore((s) => s.toggleTheme);
+
+ let taskCountText = '';
+ if (uncompleted === 0) {
+ taskCountText = '0 tasks';
+ } else if (uncompleted === 1) {
+ taskCountText = '1 task remaining';
+ } else {
+ taskCountText = `${uncompleted} tasks remaining`;
+ }
+
+ return (
+
+
+
+
+ {format(now, "do MMM yyyy")}
+ {format(now, "EEEE")}
+ {taskCountText}
+
+ );
+};
+
+export default Header;
\ No newline at end of file
diff --git a/src/components/StyledComponents.jsx b/src/components/StyledComponents.jsx
new file mode 100644
index 0000000..5268f28
--- /dev/null
+++ b/src/components/StyledComponents.jsx
@@ -0,0 +1,19 @@
+import styled from 'styled-components';
+
+export const MainContainer = styled.div`
+ width: 100%;
+ min-height: 100vh;
+ background: ${({ theme }) => theme === 'dark' ? '#23272f' : '#fff'};
+ display: flex;
+ flex-direction: column;
+ padding: 0;
+
+ @media (min-width: 1024px) {
+ max-width: 600px;
+ min-height: 90vh;
+ margin: 40px auto;
+ border-radius: 32px;
+ box-shadow: 0 8px 32px rgba(0,0,0,0.08);
+ position: relative;
+ }
+`;
\ No newline at end of file
diff --git a/src/components/TaskItem.jsx b/src/components/TaskItem.jsx
new file mode 100644
index 0000000..024721d
--- /dev/null
+++ b/src/components/TaskItem.jsx
@@ -0,0 +1,122 @@
+import styled from 'styled-components';
+import { useTodoStore } from '../store/todoStore';
+import { useThemeStore } from '../store/themeStore';
+import { FiTrash2 } from 'react-icons/fi';
+import { format } from 'date-fns';
+
+const TaskRow = styled.div`
+ display: flex;
+ align-items: center;
+ padding: 12px 0;
+ border-bottom: 1px solid #eee;
+`;
+
+const Checkbox = styled.input`
+ margin-right: 16px;
+ width: 22px;
+ height: 22px;
+`;
+
+const TaskInfo = styled.div`
+ flex: 1;
+`;
+
+const Title = styled.div`
+ font-size: 1.1rem;
+ color: ${({ completed, theme }) =>
+ completed ? '#6a6a6a' : theme === 'dark' ? '#fafafa' : '#222'};
+ text-decoration: ${({ completed }) => (completed ? 'line-through' : 'none')};
+`;
+
+const DeleteButton = styled.button`
+ background: none;
+ border: none;
+ color: #6a6a6a;
+ font-size: 1.3rem;
+ margin-left: 8px;
+ cursor: pointer;
+ align-self: center;
+ display: flex;
+ align-items: center;
+ transition: color 0.2s;
+
+ &:hover,
+ &:focus {
+ color: #e57373;
+ }
+`;
+
+const Category = styled.div`
+ font-size: 0.9rem;
+ color: ${({ theme }) => theme === 'dark' ? '#e0e0e0' : '#6a6a6a'};
+ padding: 2px 6px;
+ border-radius: 8px;
+ margin-top: 2px;
+ background: ${({ color, theme }) =>
+ theme === 'dark'
+ ? (color ? `${color}22` : '#333')
+ : (color || '#eee')};
+ cursor: pointer;
+ text-decoration: underline;
+ display: inline-block;
+ text-decoration: none;
+
+ &:hover {
+ background: #40405c;
+ color: #fff;
+ }
+`;
+
+const categoryColors = {
+ General: '#ffe0ec',
+ Finance: '#e0f7fa',
+ Freelance: '#e0ffe0',
+ Design: '#e0e7ff',
+ 'Shopping List': '#fff9e0',
+ Personal: '#f3e0ff',
+ Health: '#e0fff4',
+};
+
+const Timestamp = styled.div`
+ font-size: 0.8rem;
+ color: #6a6a6a;
+ margin-top: 2px;
+`;
+
+const TaskItem = ({ task, onCategoryClick }) => {
+ const toggleTask = useTodoStore((s) => s.toggleTask);
+ const removeTask = useTodoStore((s) => s.removeTask);
+ const theme = useThemeStore((s) => s.theme);
+
+
+ return (
+
+ toggleTask(task.id)}
+ aria-label={task.title}
+ />
+
+ {task.title}
+ onCategoryClick && onCategoryClick(task.category)}
+ >
+ {task.category}
+
+ {task.createdAt && (
+
+ {format(new Date(task.createdAt), 'd MMM yyyy, HH:mm')}
+
+ )}
+
+ removeTask(task.id)}>
+
+
+
+ );
+};
+
+export default TaskItem;
\ No newline at end of file
diff --git a/src/components/TaskList.jsx b/src/components/TaskList.jsx
new file mode 100644
index 0000000..ea1b28a
--- /dev/null
+++ b/src/components/TaskList.jsx
@@ -0,0 +1,127 @@
+import styled from 'styled-components';
+import { useTodoStore } from '../store/todoStore';
+import { useThemeStore } from '../store/themeStore';
+import TaskItem from './TaskItem';
+import { DotLottieReact } from '@lottiefiles/dotlottie-react';
+import { useState } from 'react';
+
+const SectionTitle = styled.h2`
+ margin: 24px 0 8px 0;
+`;
+
+const TaskListContainer = styled.div`
+ width: 100%;
+`;
+
+const TaskListContent = styled.div`
+ padding: 0 20px;
+`;
+
+const EmptyState = styled.div`
+color: #bbb;
+text-align: center;
+margin: 0;
+font-size: 1.2rem;
+display: flex;
+flex-direction: column;
+align-items: center;
+justify-content: center;
+`;
+
+const DoneSection = styled.div`
+ background: ${({ theme }) => theme === 'dark' ? '#23272f' : '#f7f7f7'};
+ padding: 18px 0 8px 0;
+ margin-top: 32px;
+`;
+
+const ShowAllCategoriesButton = styled.button`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: 16px;
+ margin-top: 16px;
+ padding: 12px 24px;
+ border: none;
+ border-radius: 16px;
+ background: ${({ theme }) => theme === 'dark' ? '#23272f' : '#111'};
+ color: ${({ theme }) => theme === 'dark' ? '#ffe066' : '#fff'};
+ font-size: 1.1rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: background 0.2s, color 0.2s, box-shadow 0.18s, transform 0.18s;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.12);
+
+ &:hover {
+ background: ${({ theme }) => theme === 'dark' ? '#333' : '#40405c'};
+ color: #fff;
+ }
+`;
+
+const TaskList = () => {
+ const [categoryFilter, setCategoryFilter] = useState(null);
+ const tasks = useTodoStore((s) => s.tasks);
+ const theme = useThemeStore((s) => s.theme);
+ const filteredTasks = categoryFilter
+ ? tasks.filter((t) => t.category === categoryFilter)
+ : tasks;
+
+ const todos = filteredTasks.filter((t) => !t.completed);
+ const dones = filteredTasks.filter((t) => t.completed);
+
+ return (
+
+
+ {categoryFilter && (
+ setCategoryFilter(null)}
+ >
+ Show all categories
+
+ )}
+ {filteredTasks.length === 0 ? (
+
+
+ No tasks yet. Add your first task!
+
+ ) : (
+ <>
+ {todos.length > 0 && (
+ <>
+ Todo
+ {todos.map((task) => (
+
+ ))}
+ >
+ )}
+ >
+ )}
+
+ {dones.length > 0 && (
+
+
+ Done
+ {dones.map((task) => (
+
+ ))}
+
+
+ )}
+
+ );
+};
+
+export default TaskList;
\ No newline at end of file
diff --git a/src/store/themeStore.js b/src/store/themeStore.js
new file mode 100644
index 0000000..f1e2d4e
--- /dev/null
+++ b/src/store/themeStore.js
@@ -0,0 +1,9 @@
+import { create } from 'zustand';
+
+export const useThemeStore = create((set) => ({
+ theme: 'light',
+ toggleTheme: () =>
+ set((state) => ({
+ theme: state.theme === 'light' ? 'dark' : 'light',
+ })),
+}));
\ No newline at end of file
diff --git a/src/store/todoStore.js b/src/store/todoStore.js
new file mode 100644
index 0000000..d9550dc
--- /dev/null
+++ b/src/store/todoStore.js
@@ -0,0 +1,27 @@
+import { create } from 'zustand';
+import { persist } from 'zustand/middleware';
+
+export const useTodoStore = create(
+ persist(
+ (set) => ({
+ tasks: [],
+ addTask: (task) =>
+ set((state) => ({
+ tasks: [...state.tasks, task],
+ })),
+ toggleTask: (id) =>
+ set((state) => ({
+ tasks: state.tasks.map((t) =>
+ t.id === id ? { ...t, completed: !t.completed } : t
+ ),
+ })),
+ removeTask: (id) =>
+ set((state) => ({
+ tasks: state.tasks.filter((t) => t.id !== id),
+ })),
+ }),
+ {
+ name: 'todo-storage', // name of item in localStorage
+ }
+ )
+);
\ No newline at end of file
diff --git a/src/styles/GlobalStyles.jsx b/src/styles/GlobalStyles.jsx
new file mode 100644
index 0000000..367c351
--- /dev/null
+++ b/src/styles/GlobalStyles.jsx
@@ -0,0 +1,43 @@
+import { createGlobalStyle } from 'styled-components';
+
+export const GlobalStyle = createGlobalStyle`
+ body {
+ margin: 0;
+ padding: 0;
+ font-family: 'Inter', Arial, sans-serif;
+ background: ${({ theme }) => theme === 'dark' ? '#222' : '#fafafa'};
+ color: ${({ theme }) => theme === 'dark' ? '#fafafa' : '#222'};
+ font-size: 1rem;
+ font-weight: 400;
+ transition: background 0.2s, color 0.2s;
+ }
+
+ h1 {
+ font-size: 3.5rem;
+ font-weight: 400;
+ margin: 0;
+ }
+ h2 {
+ font-size: 1.5rem;
+ font-weight: 400;
+ margin: 0;
+ }
+ h1, h2 {
+ color: ${({ theme }) => theme === 'dark' ? '#fafafa' : '#222'};
+ }
+
+
+ * {
+ box-sizing: border-box;
+ }
+
+ button {
+ font-size: 1.5rem;
+ font-weight: 500;
+ font-family: inherit;
+ cursor: pointer;
+ }
+
+
+
+`;
\ No newline at end of file