diff --git a/README.md b/README.md
index d1c68b5..6eb24d2 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,31 @@
-# Todo
\ No newline at end of file
+# 📝 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/
+
+---
diff --git a/components.json b/components.json
new file mode 100644
index 0000000..7dfce35
--- /dev/null
+++ b/components.json
@@ -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"
+}
\ No newline at end of file
diff --git a/eslint.config.js b/eslint.config.js
deleted file mode 100644
index 2fd24fd..0000000
--- a/eslint.config.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import js from '@eslint/js'
-import reactHooks from 'eslint-plugin-react-hooks'
-import reactRefresh from 'eslint-plugin-react-refresh'
-import globals from 'globals'
-
-export default [
- { ignores: ['dist'] },
- {
- files: ['**/*.{js,jsx}'],
- languageOptions: {
- ecmaVersion: 2020,
- globals: globals.browser,
- parserOptions: {
- ecmaVersion: 'latest',
- ecmaFeatures: { jsx: true },
- sourceType: 'module'
- }
- },
- 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 }
- ]
- }
- }
-]
diff --git a/eslint.config.ts b/eslint.config.ts
new file mode 100644
index 0000000..2909050
--- /dev/null
+++ b/eslint.config.ts
@@ -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 }]
+ }
+ }
+]
diff --git a/index.html b/index.html
index f7ac4e4..9a8c545 100644
--- a/index.html
+++ b/index.html
@@ -4,7 +4,7 @@
-
Todo
+ To Do List
diff --git a/package.json b/package.json
index caf6289..c412e7b 100644
--- a/package.json
+++ b/package.json
@@ -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"
}
}
diff --git a/src/App.jsx b/src/App.jsx
deleted file mode 100644
index 5427540..0000000
--- a/src/App.jsx
+++ /dev/null
@@ -1,5 +0,0 @@
-export const App = () => {
- return (
- React Boilerplate
- )
-}
diff --git a/src/App.tsx b/src/App.tsx
new file mode 100644
index 0000000..d0a1600
--- /dev/null
+++ b/src/App.tsx
@@ -0,0 +1,11 @@
+import Header from "./components/header/Header"
+import Main from "./components/main/Main"
+
+export const App = () => {
+ return (
+
+
+
+
+ )
+}
diff --git a/src/components/header/Header.tsx b/src/components/header/Header.tsx
new file mode 100644
index 0000000..8345733
--- /dev/null
+++ b/src/components/header/Header.tsx
@@ -0,0 +1,9 @@
+const Header = () => {
+ return (
+
+
To-Do List
+
+ )
+}
+
+export default Header
\ No newline at end of file
diff --git a/src/components/main/EmptyState.tsx b/src/components/main/EmptyState.tsx
new file mode 100644
index 0000000..ceb2026
--- /dev/null
+++ b/src/components/main/EmptyState.tsx
@@ -0,0 +1,9 @@
+const EmptyState = () => {
+ return (
+
+
Hooray, no active tasks!
+
+ )
+}
+
+export default EmptyState
\ No newline at end of file
diff --git a/src/components/main/ListItem.tsx b/src/components/main/ListItem.tsx
new file mode 100644
index 0000000..b42671c
--- /dev/null
+++ b/src/components/main/ListItem.tsx
@@ -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 (
+
+
+ {!deleteMode ? (
+ onToggle(id)}
+ />
+ ) : (
+
+ )}
+
+
+ {text}
+
+
+)
+}
+
+export default ListItem
\ No newline at end of file
diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx
new file mode 100644
index 0000000..da8d5ad
--- /dev/null
+++ b/src/components/main/Main.tsx
@@ -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 (
+
+
+ Tasks ({todos.length})
+
+
+
+
setText(e.target.value)}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ {todos.length === 0 ? (
+
+ ) : (
+
+ {todos.map((todo) => (
+
+ ))}
+
+ )}
+
+
+ )
+}
+
+export default Main
\ No newline at end of file
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx
new file mode 100644
index 0000000..f91affd
--- /dev/null
+++ b/src/components/ui/button.tsx
@@ -0,0 +1,56 @@
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center whitespace-nowrap rounded-base text-sm font-base ring-offset-white transition-all gap-2 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-black focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
+ {
+ variants: {
+ variant: {
+ default:
+ "text-main-foreground bg-main border-2 border-border shadow-shadow hover:translate-x-boxShadowX hover:translate-y-boxShadowY hover:shadow-none",
+ noShadow: "text-main-foreground bg-main border-2 border-border",
+ neutral:
+ "bg-secondary-background text-foreground border-2 border-border shadow-shadow hover:translate-x-boxShadowX hover:translate-y-boxShadowY hover:shadow-none",
+ reverse:
+ "text-main-foreground bg-main border-2 border-border hover:translate-x-reverseBoxShadowX hover:translate-y-reverseBoxShadowY hover:shadow-shadow",
+ },
+ size: {
+ default: "h-10 px-4 py-2",
+ sm: "h-9 px-3",
+ lg: "h-11 px-8",
+ icon: "size-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ },
+)
+
+function Button({
+ className,
+ variant,
+ size,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"button"> &
+ VariantProps & {
+ asChild?: boolean
+ }) {
+ const Comp = asChild ? Slot : "button"
+
+ return (
+
+ )
+}
+
+export { Button, buttonVariants }
diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx
new file mode 100644
index 0000000..d5a3851
--- /dev/null
+++ b/src/components/ui/card.tsx
@@ -0,0 +1,92 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Card({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardAction({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardContent({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+export {
+ Card,
+ CardHeader,
+ CardFooter,
+ CardTitle,
+ CardDescription,
+ CardContent,
+ CardAction,
+}
diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx
new file mode 100644
index 0000000..9c11cde
--- /dev/null
+++ b/src/components/ui/checkbox.tsx
@@ -0,0 +1,31 @@
+import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
+import { Check } from "lucide-react"
+
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Checkbox({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+
+ )
+}
+
+export { Checkbox }
diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx
new file mode 100644
index 0000000..08ca29a
--- /dev/null
+++ b/src/components/ui/input.tsx
@@ -0,0 +1,19 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Input({ className, type, ...props }: React.ComponentProps<"input">) {
+ return (
+
+ )
+}
+
+export { Input }
diff --git a/src/index.css b/src/index.css
index f7c0aef..fe75a64 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1,3 +1,92 @@
+@import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap');
+@import "tailwindcss";
+@import "tw-animate-css";
+
+@custom-variant dark (&:is(.dark *));
+
:root {
- font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
+ --background: oklch(93.46% 0.0305 255.11);
+ --secondary-background: oklch(100% 0 0);
+ --foreground: oklch(0% 0 0);
+ --main-foreground: oklch(0% 0 0);
+ --main: oklch(67.47% 0.1726 259.49);
+ --border: oklch(0% 0 0);
+ --ring: oklch(0% 0 0);
+ --overlay: oklch(0% 0 0 / 0.8);
+ --shadow: 4px 4px 0px 0px var(--border);
+ --chart-1: #5294FF;
+ --chart-2: #FF4D50;
+ --chart-3: #FACC00;
+ --chart-4: #05E17A;
+ --chart-5: #7A83FF;
+ --chart-active-dot: #000;
}
+
+.dark {
+ --background: oklch(29.23% 0.0626 270.49);
+ --secondary-background: oklch(23.93% 0 0);
+ --foreground: oklch(92.49% 0 0);
+ --main-foreground: oklch(0% 0 0);
+ --main: oklch(67.47% 0.1726 259.49);
+ --border: oklch(0% 0 0);
+ --ring: oklch(100% 0 0);
+ --shadow: 4px 4px 0px 0px var(--border);
+ --chart-1: #5294FF;
+ --chart-2: #FF6669;
+ --chart-3: #E0B700;
+ --chart-4: #04C86D;
+ --chart-5: #7A83FF;
+ --chart-active-dot: #fff;
+}
+
+@theme inline {
+ --color-main: var(--main);
+ --color-background: var(--background);
+ --color-secondary-background: var(--secondary-background);
+ --color-foreground: var(--foreground);
+ --color-main-foreground: var(--main-foreground);
+ --color-border: var(--border);
+ --color-overlay: var(--overlay);
+ --color-ring: var(--ring);
+ --color-chart-1: var(--chart-1);
+ --color-chart-2: var(--chart-2);
+ --color-chart-3: var(--chart-3);
+ --color-chart-4: var(--chart-4);
+ --color-chart-5: var(--chart-5);
+
+ --spacing-boxShadowX: 4px;
+ --spacing-boxShadowY: 4px;
+ --spacing-reverseBoxShadowX: -4px;
+ --spacing-reverseBoxShadowY: -4px;
+ --radius-base: 5px;
+ --shadow-shadow: var(--shadow);
+ --font-weight-base: 500;
+ --font-weight-heading: 700;
+ --font-base: DM Sans, "sans-serif";
+ --font-heading: DM Sans, "sans-serif";
+}
+
+@layer base {
+ body {
+ @apply text-foreground font-base bg-secondary-background;
+ }
+
+ h1, h2, h3, h4, h5, h6{
+ @apply font-heading;
+ font-weight: var(--font-weight-heading)
+ }
+}
+
+@layer utilities {
+ .bg-grid-light {
+ background-image:
+ repeating-linear-gradient(0deg, #e5e7eb 0, #e5e7eb 1px, transparent 1px, transparent 40px),
+ repeating-linear-gradient(90deg, #e5e7eb 0, #e5e7eb 1px, transparent 1px, transparent 40px);
+ background-size: 40px 40px;
+ background-position: 0 0;
+ }
+ .break-word {
+ overflow-wrap: break-word;
+ word-break: break-word;
+ }
+}
\ No newline at end of file
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
new file mode 100644
index 0000000..bd0c391
--- /dev/null
+++ b/src/lib/utils.ts
@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from "clsx"
+import { twMerge } from "tailwind-merge"
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
+}
diff --git a/src/main.jsx b/src/main.tsx
similarity index 61%
rename from src/main.jsx
rename to src/main.tsx
index 1b8ffe9..82c5feb 100644
--- a/src/main.jsx
+++ b/src/main.tsx
@@ -1,11 +1,11 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
-import { App } from './App.jsx'
+import { App } from './App'
import './index.css'
-ReactDOM.createRoot(document.getElementById('root')).render(
+ReactDOM.createRoot(document.getElementById('root')!).render(
diff --git a/src/stores/useTodoStore.ts b/src/stores/useTodoStore.ts
new file mode 100644
index 0000000..05cf649
--- /dev/null
+++ b/src/stores/useTodoStore.ts
@@ -0,0 +1,46 @@
+import { create } from 'zustand'
+
+type Todo = {
+ id: number;
+ text: string;
+ completed: boolean;
+};
+
+type TodoState = {
+ todos: Todo[];
+ deleteMode: boolean;
+ addTodo: (text: string) => void;
+ removeTodo: (id: number) => void;
+ toggleTodo: (id: number) => void;
+ toggleDeleteMode: () => void;
+};
+
+const useTodoStore = create((set) => ({
+ todos: [],
+ deleteMode: false,
+ toggleDeleteMode: () =>
+ set((state: TodoState): Partial => ({
+ deleteMode: !state.deleteMode
+ })),
+ addTodo: (text) =>
+ set((state) => ({
+ todos: [{id: Date.now(), text, completed: false}, ...state.todos],
+ deleteMode: false,
+ })),
+ removeTodo: (id) =>
+ set((state) => {
+ const updatedTodos = state.todos.filter((todo) => todo.id !== id);
+ return {
+ todos: updatedTodos,
+ deleteMode: updatedTodos.length === 0 ? false : state.deleteMode,
+ };
+ }),
+ toggleTodo: (id) =>
+ set((state) => ({
+ todos: state.todos.map((todo) =>
+ todo.id === id ? { ...todo, completed: !todo.completed } : todo
+ ),
+ })),
+}))
+
+export default useTodoStore
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..5b5311f
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "moduleResolution": "Node",
+ "strict": true,
+ "jsx": "react-jsx",
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": ["src"]
+}
diff --git a/vite.config.js b/vite.config.js
deleted file mode 100644
index ba24244..0000000
--- a/vite.config.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import react from '@vitejs/plugin-react'
-import { defineConfig } from 'vite'
-
-// https://vitejs.dev/config/
-export default defineConfig({
- plugins: [react()]
-})
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..8d7e074
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,14 @@
+import react from '@vitejs/plugin-react'
+import { defineConfig } from 'vite'
+import tailwindcss from '@tailwindcss/vite'
+import path from "path"
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [react(), tailwindcss()],
+ resolve: {
+ alias: {
+ "@": path.resolve(__dirname, "./src"),
+ },
+ },
+})
\ No newline at end of file