Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
2a9fa41
setting up dependencies and basic file structure
mimmi-eriksson May 22, 2025
2e7c9a6
setting up colors and fonts
mimmi-eriksson May 22, 2025
6995cf6
creating a task store to list tasks
mimmi-eriksson May 22, 2025
9b765c2
started implementing functions to add task, remove task and toggle co…
mimmi-eriksson May 23, 2025
2eed645
deleting task functionality now working, fixed basic task counter and…
mimmi-eriksson May 23, 2025
0d47c12
fixed function to toggle completed and added functions for deleting a…
mimmi-eriksson May 23, 2025
1b4ee35
styling and functionality of controls components
mimmi-eriksson May 23, 2025
7990ab0
installed lucide icons and fixed icon styling
mimmi-eriksson May 23, 2025
f76205e
fixed counter to show remaining tasks
mimmi-eriksson May 23, 2025
96a406c
added empty viwe and basic styilng of header and footer
mimmi-eriksson May 23, 2025
a22d14d
added theme store and toggle to switch between light and dark mode
mimmi-eriksson May 23, 2025
5f3b592
style and positioning fix and fixed counter to show uncompleted task
mimmi-eriksson May 23, 2025
a8bc144
installed date-fns and started adding timestamp in tasks
mimmi-eriksson May 23, 2025
4ee7040
added time stamp on task, fixed colors, styling, responsiveness etc
mimmi-eriksson May 26, 2025
3502bd8
updated readme
mimmi-eriksson May 26, 2025
04b5289
responsiveness fix
mimmi-eriksson May 27, 2025
3fa9dfd
Added screenshot
mimmi-eriksson Sep 4, 2025
94d8dd3
added new screenshot
mimmi-eriksson Sep 4, 2025
1bba10e
Update README.md
mimmi-eriksson Sep 4, 2025
f39b099
Added light mode screenshot
mimmi-eriksson Sep 4, 2025
0bc9362
Update README.md
mimmi-eriksson Sep 4, 2025
fb01946
Update README.md
mimmi-eriksson Sep 4, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,31 @@
# Todo
# Todo App
A simple todo app built in React using Zustand for state management. \
Users are able to add, list, filter and remove tasks, and toggle whether a task is completed or not.

## Features
* Each task has a time stamp when it was created
* A task can be marked as completed (or uncompleted) by clicking the task
* A task can be deleted by clicking the trash bin icon next to each task
* All tasks can be deleted by clicking the trash bin icon in the top control bar
* All tasks can be marked as completed by clicking the check box icon in the top control bar
* Uncompleted tasks can be filtered out by clicking the funnel icon in the top control bar
* Toggle between light/dark mode by clicking the toggle button in the header

## Installation & Usage
Install the required dependencies by running the following command:
```
npm install
```
Start the server by running:
```
npm run dev
```

## Link
https://task-completed.netlify.app/

## Screenshots
<span>
<img src="./public/TodoApp-screenshot.png" alt="Screenshot of todo app - dark mode." height="400">
<img src="./public/TodoApp-screenshot-light.png" alt="Screenshot of todo app - light mode." height="400">
</span>
51 changes: 37 additions & 14 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,16 +1,39 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Todo</title>
</head>
<body>
<div id="root"></div>
<script
type="module"
src="./src/main.jsx">
</script>
</body>
</html>

<head>
<meta charset="UTF-8" />
<link
rel="icon"
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%2210 0 100 100%22><text y=%22.90em%22 font-size=%2290%22>✔️</text></svg>"
/>
<link
rel="preconnect"
href="https://fonts.googleapis.com"
>
<link
rel="preconnect"
href="https://fonts.gstatic.com"
crossorigin
>
<link
href="https://fonts.googleapis.com/css2?family=Work+Sans:ital,wght@0,100..900;1,100..900&display=swap"
rel="stylesheet"
>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>Todo</title>
</head>

<body>
<div id="root"></div>
<script
type="module"
src="./src/main.jsx"
>
</script>
</body>

</html>
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,13 @@
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.7",
"date-fns": "^4.1.0",
"lucide-react": "^0.511.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"tailwindcss": "^4.1.7",
"zustand": "^5.0.5"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
Expand Down
Binary file added public/TodoApp-screenshot-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/TodoApp-screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 12 additions & 1 deletion src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
import Footer from "./sections/Footer"
import Header from "./sections/Header"
import Main from "./sections/Main"

export const App = () => {
return (
<h1>React Boilerplate</h1>
<>
<div className="flex flex-col min-h-screen">
<Header />
<Main />
<Footer />
</div>

</>
)
}
60 changes: 60 additions & 0 deletions src/components/Controls.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import useTaskStore from "../stores/useTaskStore"
import { useState } from "react"
import ControlsButton from "./ControlsButton"
import { Trash2, Funnel, SquareCheck, Plus } from "lucide-react"

const Controls = () => {
const addTask = useTaskStore(state => state.addTask)
const completeAll = useTaskStore(state => state.completeAll)
const deleteAll = useTaskStore(state => state.deleteAll)
const hideCompleted = useTaskStore(state => state.hideCompleted)

const [newTask, setNewTask] = useState("")
const [inputVisible, setInputVisible] = useState(false)

const toggleInputVisible = () => {
setInputVisible(!inputVisible)
}

const handleChange = (event) => {
setNewTask(event.target.value)
}

const handleSubmit = (event) => {
event.preventDefault()

if (newTask) {
addTask(newTask.trim())
setNewTask("")
setInputVisible(false)
} else {
setInputVisible(false)
}
}

return (
<div className="bg-surface dark:bg-surface-dark border border-border dark:border-border-dark w-full px-2 py-4 h-20 flex items-center max-w-[1024px]">
<form className="w-full" onSubmit={handleSubmit}>
<div className="flex items-center gap-2 justify-stretch">
{!inputVisible &&
<label htmlFor="addTask" className="bg-white dark:bg-background-dark rounded-lg border border-border dark:border-border-dark text-accent dark:text-accent-dark hover:text-hover dark:hover:text-hover-dark cursor-pointer flex items-center gap-2 p-2 grow" onClick={() => toggleInputVisible()}>
<Plus size={28} />
<p className="font-medium text-xl">Add task</p>
</label>}
{inputVisible &&
<input className="bg-white dark:bg-background-dark rounded-lg border border-border dark:border-border-dark p-2 grow" type="text" id="addTask" placeholder="Type here..." value={newTask} onChange={handleChange} />
}
{inputVisible && <button className="font-medium text-xl text-accent dark:text-accent-dark hover:text-hover dark:hover:text-hover-dark cursor-pointer px-2 py-1" type="submit">Done</button>}
{!inputVisible &&
<div className="flex items-center gap-1 md:gap-2">
<ControlsButton onClick={() => hideCompleted()} icon={Funnel} ariaLabel="Filter" />
<ControlsButton onClick={() => completeAll()} icon={SquareCheck} ariaLabel="Complete all" />
<ControlsButton onClick={() => deleteAll()} icon={Trash2} ariaLabel="Delete all" />
</div>}
</div>
</form>
</div>
)
}

export default Controls
25 changes: 25 additions & 0 deletions src/components/ControlsButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
type ControlsButtonProps = {
icon: React.ElementType
onClick?: () => void
ariaLabel?: string
}

const ControlsButton = ({
icon: Icon,
onClick,
ariaLabel = 'icon button',
}: ControlsButtonProps): JSX.Element => {
return (
<button
onClick={onClick}
aria-label={ariaLabel}
className="text-lg font-medium text-accent dark:text-accent-dark hover:text-hover dark:hover:text-hover-dark cursor-pointer flex items-center gap-1 p-1"
>
<Icon className="w-8 h-8" />
<p className="hidden md:inline">{ariaLabel}</p>
</button>
);
};

export default ControlsButton;

14 changes: 14 additions & 0 deletions src/components/Counter.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import useTaskStore from "../stores/useTaskStore"

const Counter = () => {
const tasks = useTaskStore(state => state.tasks)
const unCompletedTasks = tasks.filter(task => !task.isCompleted)

return (
<div className="py-8 text-center text-text-secondary dark:text-text-secondary-dark">
<p> {unCompletedTasks.length} of {tasks.length} tasks remaining</p>
</div>
)
}

export default Counter
11 changes: 11 additions & 0 deletions src/components/Empty.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const Empty = () => {
return (
<div className="text-2xl text-center py-20 flex flex-col justify-center items-center gap-5 flex-grow">
<p className="text-6xl">🏖️</p>
<p>Nothing to do?</p>
<p>Enjoy it while it lasts.</p>
</div>
)
}

export default Empty
26 changes: 26 additions & 0 deletions src/components/SocialButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
type SocialButtonProps = {
icon: React.ElementType
link: string
ariaLabel?: string
}

const SocialButton = ({
icon: Icon,
link = "",
ariaLabel = 'icon button',
}: SocialButtonProps): JSX.Element => {
return (
<li>
<a
className="hover:text-hover dark:hover:text-hover-dark p-2"
href={link}
target="_blank"
aria-label={ariaLabel}
>
<Icon className="w-4 h-4" />
</a>
</li>
);
};

export default SocialButton;
27 changes: 27 additions & 0 deletions src/components/Task.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Trash2 } from "lucide-react"
import TaskButton from "./TaskButton"
import useTaskStore from "../stores/useTaskStore"
import { formatRelative } from "date-fns"

const Task = ({ task }) => {
const toggleCompleted = useTaskStore(state => state.toggleCompleted)
const deleteTask = useTaskStore(state => state.deleteTask)

return (
<div className="flex justify-between items-center pl-3 border-b-1 border-b-border dark:border-b-border-dark">
<button onClick={() => toggleCompleted(task.id)}>
<div className="flex flex-col items-start gap-1">
<p className={`${task.isCompleted ? 'line-through' : 'no-underline'} decoration-red-500 cursor-pointer`}>
{task.task}
</p>
{!task.isCompleted && <p className="text-sm text-text-secondary dark:text-text-secondary-dark">created {formatRelative(task.id, new Date())}</p>}
</div>
</button>
<div>
<TaskButton onClick={() => deleteTask(task.id)} icon={Trash2} ariaLabel="Delete task" />
</div>
</div>
)
}

export default Task
25 changes: 25 additions & 0 deletions src/components/TaskButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
type TaskButtonProps = {
icon: React.ElementType
onClick?: () => void
ariaLabel?: string
}

const TaskButton = ({
icon: Icon,
onClick,
ariaLabel = 'icon button',
}: TaskButtonProps): JSX.Element => {
return (
<button
onClick={onClick}
aria-label={ariaLabel}
className="bg-surface dark:bg-surface-dark border-l-1 border-l-border dark:border-l-border-dark text-accent dark:text-accent-dark hover:text-hover dark:hover:text-hover-dark cursor-pointer p-4"
>
<Icon className="w-8 h-8" />
</button>
)
}

export default TaskButton


21 changes: 21 additions & 0 deletions src/components/TaskList.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Task from "./Task"
import Empty from "./Empty"
import Counter from "./Counter"
import useTaskStore from "../stores/useTaskStore"

const TaskList = () => {
const tasks = useTaskStore(state => state.tasks)
return (
<div className="bg-white dark:bg-background-dark border-x-1 border-border dark:border-border-dark w-full flex-grow flex flex-col max-w-[1024px]">
{tasks.length > 0 && (
<div className="flex flex-col">
{tasks.map(task => <Task key={task.id} task={task} />)}
</div>
)}
{tasks.length > 0 && <Counter />}
{tasks.length === 0 && <Empty />}
</div>
)
}

export default TaskList
27 changes: 26 additions & 1 deletion src/index.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,28 @@
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));

:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
font-family: "Work Sans", Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
}

@theme {
--color-primary: #EFF6FF;
--color-secondary: #1E3A8A;
--color-background: #FFFFF;
--color-surface: #F9FAFB;
--color-border: #E5E7EB;
--color-text: #111827;
--color-text-secondary: #525967;
--color-accent: #064DC1;
--color-hover: #03378E;
--color-primary-dark: #1E3A8A;
--color-secondary-dark: #DBEAFE;
--color-background-dark: #1F2937;
--color-surface-dark: #111827;
--color-border-dark: #374151;
--color-text-dark: #F9FAFB;
--color-text-secondary-dark: #ADB4C0;
--color-accent-dark: #72B9FF;
--color-hover-dark: #BADDFF;

}
22 changes: 22 additions & 0 deletions src/sections/Footer.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Github, Linkedin } from 'lucide-react'
import SocialButton from '../components/SocialButton'
import useThemeStore from '../stores/useThemeStore'

const Footer = () => {
const theme = useThemeStore(state => state.theme)
return (
<footer>
<div className={`${theme === 'dark' ? 'dark' : ''} bg-primary dark:bg-secondary text-secondary dark:text-primary border-t border-t-border dark:border-t-border-dark text-sm flex items-center justify-center gap-4 py-1`}>
<p>&copy; {new Date().getFullYear()} Mimmi Eriksson</p>
<ul
className="flex gap-2"
>
<SocialButton link="https://github.com/mimmi-eriksson" icon={Github} ariaLabel="GitHub profile" />
<SocialButton link="https://www.linkedin.com/in/mimmi-aj-eriksson/" icon={Linkedin} ariaLabel="LinkedIn profile" />
</ul>
</div>
</footer>
)
}

export default Footer
Loading