Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
# 📝 To-Do List App

This is a simple and responsive to-do list web application built with **React**, **TypeScript**, **Zustand**, and **Tailwind CSS**. It makes use of the component library **neobrutalism.dev**, which is built on top of **shadcn/ui** for a minimalistic, stylish design. The app allows users to manage tasks with features like adding, completing, and deleting tasks.

---

## ✨ Features

- ✅ **Add tasks** to your task list
- 🔁 **Toggle task completion**
- ❌ **Delete individual tasks** in a dedicated delete mode
- 📊 **Task counter** shows how many tasks you have on your list
- 📱 **Responsive design** that works across all screen sizes

---

## 🛠️ Tech Stack

- **React** (with Vite)
- **TypeScript**
- **Zustand** for global state management
- **Tailwind CSS** for styling
- **shadcn/ui** and [neobrutalism.dev](https://neobrutalism.dev) components

---

## 🔗 Link

https://neobrutalist-todo.netlify.app/

---
21 changes: 21 additions & 0 deletions components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
33 changes: 0 additions & 33 deletions eslint.config.js

This file was deleted.

55 changes: 55 additions & 0 deletions eslint.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import js from '@eslint/js'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import globals from 'globals'
import * as tseslint from '@typescript-eslint/eslint-plugin'
import parser from '@typescript-eslint/parser'

export default [
{ ignores: ['dist'] },
{
files: ['**/*.{ts,tsx}'],
languageOptions: {
parser,
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
ecmaFeatures: { jsx: true }
},
globals: globals.browser
},
plugins: {
'@typescript-eslint': tseslint
},
rules: {
...js.configs.recommended.rules,
...tseslint.configs.recommended.rules,
...reactHooks.configs.recommended.rules,
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }]
}
},
{
files: ['**/*.{js,jsx}'],
languageOptions: {
ecmaVersion: 'latest',
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
ecmaFeatures: { jsx: true }
},
globals: globals.browser
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh
},
rules: {
...js.configs.recommended.rules,
...reactHooks.configs.recommended.rules,
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }]
}
}
]
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<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>
<title>To Do List</title>
</head>
<body>
<div id="root"></div>
Expand Down
20 changes: 17 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,32 @@
"preview": "vite preview"
},
"dependencies": {
"@radix-ui/react-checkbox": "^1.3.2",
"@radix-ui/react-slot": "^1.2.3",
"@tailwindcss/vite": "^4.1.7",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.511.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"tailwind-merge": "^3.3.0",
"tailwindcss": "^4.1.7",
"zustand": "^5.0.5"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@types/node": "^22.15.21",
"@types/react": "^19.1.5",
"@types/react-dom": "^19.1.5",
"@typescript-eslint/eslint-plugin": "^8.32.1",
"@typescript-eslint/parser": "^8.32.1",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.21.0",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^15.15.0",
"tw-animate-css": "^1.3.0",
"typescript": "^5.8.3",
"vite": "^6.2.0"
}
}
5 changes: 0 additions & 5 deletions src/App.jsx

This file was deleted.

11 changes: 11 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Header from "./components/header/Header"
import Main from "./components/main/Main"

export const App = () => {
return (
<div className="flex flex-col min-h-screen">
<Header />
<Main />
</div>
)
}
9 changes: 9 additions & 0 deletions src/components/header/Header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const Header = () => {
return (
<div className="flex flex-col items-center bg-main border-b-5 border-solid">
<h1 className="text-3xl m-5">To-Do List</h1>
</div>
)
}

export default Header
9 changes: 9 additions & 0 deletions src/components/main/EmptyState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const EmptyState = () => {
return (
<div className="bg-background w-5/6 mx-auto border-2 border-slate-600 border-dashed rounded-[5px] flex flex-col items-center justify-center min-h-[180px]">
<p className="text-sm text-slate-600 text-center p-5">Hooray, no active tasks!</p>
</div>
)
}

export default EmptyState
38 changes: 38 additions & 0 deletions src/components/main/ListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Checkbox } from "@/components/ui/checkbox"

type ListItemProps = {
id: number;
text: string;
completed: boolean;
onToggle: (id: number) => void;
onDelete: (id: number) => void;
deleteMode: boolean;
};

const ListItem = ({ id, text, completed, onToggle, onDelete, deleteMode }: ListItemProps) => {
return (
<div className="flex flex-row items-center gap-4">
<div className="w-6 flex-shrink-0 h-6 flex items-center justify-left ml-6">
{!deleteMode ? (
<Checkbox
className="hover:cursor-pointer"
checked={completed}
onCheckedChange={() => onToggle(id)}
/>
) : (
<button
onClick={() => onDelete(id)}
className="text-xl hover:cursor-pointer"
>
</button>
)}
</div>
<p className={`text-sm mr-6 break-word text-left ${completed ? "line-through text-gray-400" : ""}`}>
{text}
</p>
</div>
)
}

export default ListItem
84 changes: 84 additions & 0 deletions src/components/main/Main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { useState } from "react"
import useTodoStore from "../../stores/useTodoStore"
import EmptyState from "./EmptyState"
import ListItem from "./ListItem"

const Main = () => {

const [text, setText] = useState("");
const todos = useTodoStore((state) => state.todos);
const addTodo = useTodoStore((state) => state.addTodo);
const toggleTodo = useTodoStore((state) => state.toggleTodo);
const toggleDeleteMode = useTodoStore((state) => state.toggleDeleteMode);
const deleteMode = useTodoStore((state) => state.deleteMode);
const removeTodo = useTodoStore((state) => state.removeTodo);

const handleAdd = () => {
if (text.trim()) {
addTodo(text);
setText("");
}
};

return (
<div className="flex-grow bg-secondary-background bg-grid-light flex flex-col items-center">
<Card className="m-12 w-7/10 gap-3 max-w-[600px]">
<h2 className="text-xl flex justify-center mb-1">Tasks ({todos.length})</h2>

<div className="flex flex-col gap-2 w-5/6 mx-auto">

<Input
placeholder="E.g. clean the house"
className="w-full"
value={text}
onChange={(e) => setText(e.target.value)}
/>

<div className="flex gap-2 w-full">

<Button
className="mb-4 w-2/3 xl:w-3/4 hover:cursor-pointer"
onClick={handleAdd}
>
Add (+)
</Button>

<Button
className="w-1/3 xl:w-1/4 bg-orange-400 hover:cursor-pointer"
disabled={todos.length === 0}
onClick={toggleDeleteMode}
>
{deleteMode ? "⬅" : "Del (-)"}
</Button>

</div>

</div>


{todos.length === 0 ? (
<EmptyState />
) : (
<div className="flex flex-col gap-2 w-5/6 mx-auto">
{todos.map((todo) => (
<ListItem
key={todo.id}
id={todo.id}
text={todo.text}
completed={todo.completed}
onToggle={toggleTodo}
onDelete={removeTodo}
deleteMode={deleteMode}
/>
))}
</div>
)}
</Card>
</div>
)
}

export default Main
Loading