From f2e913e9ec88249934c860294a1695c048ec3393 Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Thu, 22 May 2025 20:22:52 +0200 Subject: [PATCH 01/53] install zustand --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index caf6289..1f583a3 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ }, "dependencies": { "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "zustand": "^5.0.5" }, "devDependencies": { "@eslint/js": "^9.21.0", From 2dad1c1dd9614b2c045e8b20ee4d75209fa8de13 Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Thu, 22 May 2025 20:52:22 +0200 Subject: [PATCH 02/53] set up tailwind --- package.json | 4 ++++ src/App.jsx | 5 ++++- src/index.css | 10 +++++++--- vite.config.js | 4 +++- 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 1f583a3..39bdfd5 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "@tailwindcss/vite": "^4.1.7", "react": "^19.0.0", "react-dom": "^19.0.0", "zustand": "^5.0.5" @@ -19,10 +20,13 @@ "@types/react": "^19.0.10", "@types/react-dom": "^19.0.4", "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.21", "eslint": "^9.21.0", "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-refresh": "^0.4.19", "globals": "^15.15.0", + "postcss": "^8.5.3", + "tailwindcss": "^4.1.7", "vite": "^6.2.0" } } diff --git a/src/App.jsx b/src/App.jsx index 5427540..051a828 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,5 +1,8 @@ export const App = () => { return ( -

React Boilerplate

+

+ Hello, Tailwind! +

) } + diff --git a/src/index.css b/src/index.css index f7c0aef..fd8e73d 100644 --- a/src/index.css +++ b/src/index.css @@ -1,3 +1,7 @@ -:root { - font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; -} +@import "tailwindcss"; + +@layer base { + body { + @apply bg-gray-100; + } +} \ No newline at end of file diff --git a/vite.config.js b/vite.config.js index ba24244..4542111 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,7 +1,9 @@ import react from '@vitejs/plugin-react' import { defineConfig } from 'vite' +import tailwindcss from '@tailwindcss/vite' // https://vitejs.dev/config/ export default defineConfig({ - plugins: [react()] + plugins: [ + tailwindcss(), react()] }) From 892e8e2c1d30894197a9715626c76d9b9f1ce0ef Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Thu, 22 May 2025 21:20:45 +0200 Subject: [PATCH 03/53] add font family --- index.html | 1 + src/App.jsx | 4 ++-- src/index.css | 6 ++++++ tailwind.config.js | 13 +++++++++++++ 4 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 tailwind.config.js diff --git a/index.html b/index.html index f7ac4e4..388c080 100644 --- a/index.html +++ b/index.html @@ -4,6 +4,7 @@ + Todo diff --git a/src/App.jsx b/src/App.jsx index 051a828..eb72d05 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,7 +1,7 @@ export const App = () => { return ( -

- Hello, Tailwind! +

+ Tailwind is working!

) } diff --git a/src/index.css b/src/index.css index fd8e73d..7cf58d7 100644 --- a/src/index.css +++ b/src/index.css @@ -4,4 +4,10 @@ body { @apply bg-gray-100; } +} + +@layer utilities { + .font-neue-kabel { + font-family: "neue-kabel", sans-serif; + } } \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..8cf685c --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,13 @@ +/** @type {import('tailwindcss').Config} */ +export default { + darkMode: 'class', + content: ['./src/**/*.{js,jsx,ts,tsx}'], + theme: { + extend: { + fontFamily: { + 'neue-kabel': ['neue-kabel', 'sans-serif'], + }, + }, + }, + plugins: [], +} \ No newline at end of file From accc784667c28fd87e1e2d06028aadb286ae7d17 Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Thu, 22 May 2025 21:24:30 +0200 Subject: [PATCH 04/53] install day.js for dates --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 39bdfd5..2fd417e 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@tailwindcss/vite": "^4.1.7", + "dayjs": "^1.11.13", "react": "^19.0.0", "react-dom": "^19.0.0", "zustand": "^5.0.5" From c05499fa51557b0782e6430556d820247e273ca6 Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Thu, 22 May 2025 23:11:05 +0200 Subject: [PATCH 05/53] sketch UI and breakt it into components --- src/components/EmptyState.jsx | 10 ++++++++++ src/components/Footer.jsx | 10 ++++++++++ src/components/Header.jsx | 10 ++++++++++ src/components/Stats.jsx | 10 ++++++++++ src/components/ThemeToggle.jsx | 10 ++++++++++ src/components/TodoForm.jsx | 10 ++++++++++ src/components/TodoItem.jsx | 10 ++++++++++ src/components/TodoList.jsx | 10 ++++++++++ 8 files changed, 80 insertions(+) create mode 100644 src/components/EmptyState.jsx create mode 100644 src/components/Footer.jsx create mode 100644 src/components/Header.jsx create mode 100644 src/components/Stats.jsx create mode 100644 src/components/ThemeToggle.jsx create mode 100644 src/components/TodoForm.jsx create mode 100644 src/components/TodoItem.jsx create mode 100644 src/components/TodoList.jsx diff --git a/src/components/EmptyState.jsx b/src/components/EmptyState.jsx new file mode 100644 index 0000000..a6b4b33 --- /dev/null +++ b/src/components/EmptyState.jsx @@ -0,0 +1,10 @@ +import React from 'react'; + +export default function EmptyState() { + return ( +
+ {/* TODO: App title + ThemeToggle */} +

Todo App

+
+ ); +} \ No newline at end of file diff --git a/src/components/Footer.jsx b/src/components/Footer.jsx new file mode 100644 index 0000000..5e67952 --- /dev/null +++ b/src/components/Footer.jsx @@ -0,0 +1,10 @@ +import React from 'react'; + +export default function Footer() { + return ( +
+ {/* TODO: App title + ThemeToggle */} +

Todo App

+
+ ); +} \ No newline at end of file diff --git a/src/components/Header.jsx b/src/components/Header.jsx new file mode 100644 index 0000000..b8374c2 --- /dev/null +++ b/src/components/Header.jsx @@ -0,0 +1,10 @@ +import React from 'react'; + +export default function Header() { + return ( +
+ {/* TODO: App title + ThemeToggle */} +

Todo App

+
+ ); +} \ No newline at end of file diff --git a/src/components/Stats.jsx b/src/components/Stats.jsx new file mode 100644 index 0000000..7b3ee81 --- /dev/null +++ b/src/components/Stats.jsx @@ -0,0 +1,10 @@ +import React from 'react'; + +export default function Stats() { + return ( +
+ {/* TODO: App title + ThemeToggle */} +

Todo App

+
+ ); +} \ No newline at end of file diff --git a/src/components/ThemeToggle.jsx b/src/components/ThemeToggle.jsx new file mode 100644 index 0000000..75ca5cf --- /dev/null +++ b/src/components/ThemeToggle.jsx @@ -0,0 +1,10 @@ +import React from 'react'; + +export default function ThemeToggle() { + return ( +
+ {/* TODO: App title + ThemeToggle */} +

Todo App

+
+ ); +} \ No newline at end of file diff --git a/src/components/TodoForm.jsx b/src/components/TodoForm.jsx new file mode 100644 index 0000000..5f3b349 --- /dev/null +++ b/src/components/TodoForm.jsx @@ -0,0 +1,10 @@ +import React from 'react'; + +export default function TodoForm() { + return ( +
+ {/* TODO: App title + ThemeToggle */} +

Todo App

+
+ ); +} \ No newline at end of file diff --git a/src/components/TodoItem.jsx b/src/components/TodoItem.jsx new file mode 100644 index 0000000..e3b6e2f --- /dev/null +++ b/src/components/TodoItem.jsx @@ -0,0 +1,10 @@ +import React from 'react'; + +export default function TodoItem() { + return ( +
+ {/* TODO: App title + ThemeToggle */} +

Todo App

+
+ ); +} \ No newline at end of file diff --git a/src/components/TodoList.jsx b/src/components/TodoList.jsx new file mode 100644 index 0000000..22394c5 --- /dev/null +++ b/src/components/TodoList.jsx @@ -0,0 +1,10 @@ +import React from 'react'; + +export default function TodoList() { + return ( +
+ {/* TODO: App title + ThemeToggle */} +

Todo App

+
+ ); +} \ No newline at end of file From 0d94a3fe0251661b714581309dbd16499a11be9e Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Thu, 22 May 2025 23:31:33 +0200 Subject: [PATCH 06/53] fix import App at main.jsx --- src/components/EmptyState.jsx | 2 +- src/components/Footer.jsx | 2 +- src/components/Stats.jsx | 2 +- src/components/ThemeToggle.jsx | 2 +- src/components/TodoForm.jsx | 2 +- src/components/TodoItem.jsx | 2 +- src/components/TodoList.jsx | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/EmptyState.jsx b/src/components/EmptyState.jsx index a6b4b33..cd197a0 100644 --- a/src/components/EmptyState.jsx +++ b/src/components/EmptyState.jsx @@ -4,7 +4,7 @@ export default function EmptyState() { return (
{/* TODO: App title + ThemeToggle */} -

Todo App

+

Empty State

); } \ No newline at end of file diff --git a/src/components/Footer.jsx b/src/components/Footer.jsx index 5e67952..11f8cc5 100644 --- a/src/components/Footer.jsx +++ b/src/components/Footer.jsx @@ -4,7 +4,7 @@ export default function Footer() { return (
{/* TODO: App title + ThemeToggle */} -

Todo App

+

Footer

); } \ No newline at end of file diff --git a/src/components/Stats.jsx b/src/components/Stats.jsx index 7b3ee81..b0fd262 100644 --- a/src/components/Stats.jsx +++ b/src/components/Stats.jsx @@ -4,7 +4,7 @@ export default function Stats() { return (
{/* TODO: App title + ThemeToggle */} -

Todo App

+

Stats

); } \ No newline at end of file diff --git a/src/components/ThemeToggle.jsx b/src/components/ThemeToggle.jsx index 75ca5cf..19d9e29 100644 --- a/src/components/ThemeToggle.jsx +++ b/src/components/ThemeToggle.jsx @@ -4,7 +4,7 @@ export default function ThemeToggle() { return (
{/* TODO: App title + ThemeToggle */} -

Todo App

+

Theme Toggle

); } \ No newline at end of file diff --git a/src/components/TodoForm.jsx b/src/components/TodoForm.jsx index 5f3b349..d042a72 100644 --- a/src/components/TodoForm.jsx +++ b/src/components/TodoForm.jsx @@ -4,7 +4,7 @@ export default function TodoForm() { return (
{/* TODO: App title + ThemeToggle */} -

Todo App

+

Todo Form

); } \ No newline at end of file diff --git a/src/components/TodoItem.jsx b/src/components/TodoItem.jsx index e3b6e2f..71b13b1 100644 --- a/src/components/TodoItem.jsx +++ b/src/components/TodoItem.jsx @@ -4,7 +4,7 @@ export default function TodoItem() { return (
{/* TODO: App title + ThemeToggle */} -

Todo App

+

Todo Item

); } \ No newline at end of file diff --git a/src/components/TodoList.jsx b/src/components/TodoList.jsx index 22394c5..49a546b 100644 --- a/src/components/TodoList.jsx +++ b/src/components/TodoList.jsx @@ -4,7 +4,7 @@ export default function TodoList() { return (
{/* TODO: App title + ThemeToggle */} -

Todo App

+

Todo List

); } \ No newline at end of file From b32565cd975fb0b3727f946283e1d7e2c5376982 Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Thu, 22 May 2025 23:37:27 +0200 Subject: [PATCH 07/53] Set up Zustand store and wire TodoList to it --- src/App.jsx | 28 +++++++++++++++++++++------- src/main.jsx | 2 +- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index eb72d05..7fad4b0 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,8 +1,22 @@ -export const App = () => { - return ( -

- Tailwind is working! -

- ) -} +import React from 'react'; +import Header from './components/Header'; +import ThemeToggle from './components/ThemeToggle'; +import TodoForm from './components/TodoForm'; +import TodoList from './components/TodoList'; +import EmptyState from './components/EmptyState'; +import Footer from './components/Footer'; +export default function App() { + return ( +
+
+ +
+ + + +
+
+
+ ); +} \ No newline at end of file diff --git a/src/main.jsx b/src/main.jsx index 1b8ffe9..92e76b0 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,7 +1,7 @@ import React from 'react' import ReactDOM from 'react-dom/client' -import { App } from './App.jsx' +import App from './App.jsx' import './index.css' From 6bc432e48fbbd488d40459309a569cc88803e408 Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Thu, 22 May 2025 23:42:06 +0200 Subject: [PATCH 08/53] create store folder and useTodos.js inside --- src/store/useTodos.js | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/store/useTodos.js diff --git a/src/store/useTodos.js b/src/store/useTodos.js new file mode 100644 index 0000000..6d6ab1e --- /dev/null +++ b/src/store/useTodos.js @@ -0,0 +1,36 @@ +import create from 'zustand' + +export const useTodos = create((set) => ({ + todos: [ + { id: '1', text: 'Learn Zustand', done: false, createdAt: new Date() }, + { id: '2', text: 'Build a todo app', done: false, createdAt: new Date() }, + ], + + // Add a new todo + addTodo: (text) => + set((state) => ({ + todos: [ + ...state.todos, + { + id: Date.now().toString(), + text, + done: false, + createdAt: new Date(), + }, + ], + })), + + // Remove by id + removeTodo: (id) => + set((state) => ({ + todos: state.todos.filter((todo) => todo.id !== id), + })), + + // Toggle the 'done' flag + toggleTodo: (id) => + set((state) => ({ + todos: state.todos.map((todo) => + todo.id === id ? { ...todo, done: !todo.done } : todo + ), + })), +})) \ No newline at end of file From df9f203244bdf602dbe72182e94718dce2d53d37 Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Thu, 22 May 2025 23:43:23 +0200 Subject: [PATCH 09/53] hook up TodoList to the store --- src/components/TodoList.jsx | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/src/components/TodoList.jsx b/src/components/TodoList.jsx index 49a546b..da4109b 100644 --- a/src/components/TodoList.jsx +++ b/src/components/TodoList.jsx @@ -1,10 +1,34 @@ -import React from 'react'; +import React from 'react' +import { useTodos } from '../store/useTodos' export default function TodoList() { + const todos = useTodos((state) => state.todos) + return ( -
- {/* TODO: App title + ThemeToggle */} -

Todo List

-
- ); +
    + {todos.map((todo) => ( +
  • + {todo.text} +
    + + +
    +
  • + ))} +
+ ) } \ No newline at end of file From b50e6fa08ba0298e235c12edf41b55a4aae2a587 Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Thu, 22 May 2025 23:50:34 +0200 Subject: [PATCH 10/53] global state wired up --- src/components/TodoList.jsx | 14 +++++++++----- src/store/useTodos.js | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/components/TodoList.jsx b/src/components/TodoList.jsx index da4109b..7aa0dc3 100644 --- a/src/components/TodoList.jsx +++ b/src/components/TodoList.jsx @@ -1,27 +1,31 @@ +// src/components/TodoList.jsx import React from 'react' import { useTodos } from '../store/useTodos' export default function TodoList() { - const todos = useTodos((state) => state.todos) + const todos = useTodos((s) => s.todos) + const toggleTodo = useTodos((s) => s.toggleTodo) + const removeTodo = useTodos((s) => s.removeTodo) return (
    {todos.map((todo) => (
  • {todo.text}
    + + ) } \ No newline at end of file From baf527b0f00cfc51ee26e118a42da3f6e8a7e997 Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Thu, 22 May 2025 23:55:15 +0200 Subject: [PATCH 12/53] contitionally render TodoList vs EmptyState --- src/App.jsx | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 7fad4b0..3e3a7c2 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,22 +1,30 @@ -import React from 'react'; -import Header from './components/Header'; -import ThemeToggle from './components/ThemeToggle'; -import TodoForm from './components/TodoForm'; -import TodoList from './components/TodoList'; -import EmptyState from './components/EmptyState'; -import Footer from './components/Footer'; +// src/App.jsx +import React from 'react' +import { useTodos } from './store/useTodos' +import Header from './components/Header' +import ThemeToggle from './components/ThemeToggle' +import TodoForm from './components/TodoForm' +import TodoList from './components/TodoList' +import EmptyState from './components/EmptyState' +import Footer from './components/Footer' export default function App() { + const todos = useTodos((s) => s.todos) + return ( -
    +
    -
    +
    - - + + {todos.length > 0 ? ( + + ) : ( + + )}
    - ); + ) } \ No newline at end of file From fbd3ba71b4d3ab2d8f56dfb5dec1c5ae5a5bf20a Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Fri, 23 May 2025 00:03:23 +0200 Subject: [PATCH 13/53] edit index.css for custom bauhaus inspired theme --- src/components/EmptyState.jsx | 31 +++++++++++++++++++++++------ src/index.css | 37 ++++++++++++++++++++++++++++++----- 2 files changed, 57 insertions(+), 11 deletions(-) diff --git a/src/components/EmptyState.jsx b/src/components/EmptyState.jsx index cd197a0..0150752 100644 --- a/src/components/EmptyState.jsx +++ b/src/components/EmptyState.jsx @@ -1,10 +1,29 @@ -import React from 'react'; +import React from 'react' export default function EmptyState() { return ( -
    - {/* TODO: App title + ThemeToggle */} -

    Empty State

    -
    - ); +
    + {/* Clipboard icon (Heroicons outline) */} + + +

    No tasks yet!

    +

    Looks like you don’t have any tasks. Let’s add one.

    + {/* Emoji fallback or could be replaced with a button/icon */} + 📋 +
    + ) } \ No newline at end of file diff --git a/src/index.css b/src/index.css index 7cf58d7..3bc2664 100644 --- a/src/index.css +++ b/src/index.css @@ -1,13 +1,40 @@ -@import "tailwindcss"; +@tailwind base; +@tailwind components; +@tailwind utilities; +/* Bauhaus Light Theme Variables */ +:root { + --bauhaus-bg: #F5F0E6; + --bauhaus-text: #1A1A1A; + --bauhaus-muted: #4F4F4F; + --bauhaus-accent-red: #D72638; + --bauhaus-accent-blue: #3F88C5; + --bauhaus-accent-yellow: #F2C14E; + --bauhaus-border: #CCC5B9; + --bauhaus-completed: #9C9C9C; +} + +/* Base overrides */ @layer base { body { - @apply bg-gray-100; + background-color: var(--bauhaus-bg); + color: var(--bauhaus-text); + font-family: "neue-kabel", sans-serif; } } +/* Custom utilities for my palette */ @layer utilities { - .font-neue-kabel { - font-family: "neue-kabel", sans-serif; - } + .bg-bauhaus { background-color: var(--bauhaus-bg); } + .text-bauhaus { color: var(--bauhaus-text); } + .text-muted { color: var(--bauhaus-muted); } + .border-bauhaus { border-color: var(--bauhaus-border); } + .text-completed { color: var(--bauhaus-completed); } + + .bg-accent-red { background-color: var(--bauhaus-accent-red); } + .text-accent-red { color: var(--bauhaus-accent-red); } + .bg-accent-blue { background-color: var(--bauhaus-accent-blue); } + .text-accent-blue { color: var(--bauhaus-accent-blue); } + .bg-accent-yellow { background-color: var(--bauhaus-accent-yellow); } + .text-accent-yellow { color: var(--bauhaus-accent-yellow); } } \ No newline at end of file From fe288f81f61bd99ca3c6fc99c286faf9d59883fb Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Fri, 23 May 2025 00:09:30 +0200 Subject: [PATCH 14/53] fix styling --- src/components/TodoForm.jsx | 4 ++-- src/components/TodoList.jsx | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/components/TodoForm.jsx b/src/components/TodoForm.jsx index 7bde66a..cd217f2 100644 --- a/src/components/TodoForm.jsx +++ b/src/components/TodoForm.jsx @@ -16,7 +16,7 @@ export default function TodoForm() { return (
    setText(e.target.value)} placeholder="What needs doing?" - className="flex-1 p-2 border rounded focus:outline-none focus:ring" + className="flex-1 p-2 border border-bauhaus rounded focus:outline-none focus:ring-accent-blue" /> From bd34b38898aafe5a8ce4c4b7b1a2c5eb538f06c1 Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Fri, 23 May 2025 00:23:14 +0200 Subject: [PATCH 15/53] further styling the TodoForm --- src/components/TodoForm.jsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/components/TodoForm.jsx b/src/components/TodoForm.jsx index cd217f2..71aa66d 100644 --- a/src/components/TodoForm.jsx +++ b/src/components/TodoForm.jsx @@ -29,7 +29,18 @@ export default function TodoForm() { From 7ace1bcfb1630bf91aea3947cca825386e36c492 Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Fri, 23 May 2025 00:28:57 +0200 Subject: [PATCH 16/53] extend store with completeAll --- src/components/Stats.jsx | 30 ++++++++++++++++++++++++------ src/store/useTodos.js | 7 +++++++ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/components/Stats.jsx b/src/components/Stats.jsx index b0fd262..8ef7a7f 100644 --- a/src/components/Stats.jsx +++ b/src/components/Stats.jsx @@ -1,10 +1,28 @@ -import React from 'react'; +import React from 'react' +import { useTodos } from '../store/useTodos' export default function Stats() { + const todos = useTodos((s) => s.todos) + const completeAll = useTodos((s) => s.completeAll) + const totalCount = todos.length + const remainingCount = todos.filter((t) => !t.done).length + + // If no tasks, render nothing + if (totalCount === 0) return null + return ( -
    - {/* TODO: App title + ThemeToggle */} -

    Stats

    -
    - ); +
    + + {totalCount} task{totalCount !== 1 ? 's' : ''}, {remainingCount} remaining + + {remainingCount > 0 && ( + + )} +
    + ) } \ No newline at end of file diff --git a/src/store/useTodos.js b/src/store/useTodos.js index 50ea6b6..7442dd6 100644 --- a/src/store/useTodos.js +++ b/src/store/useTodos.js @@ -1,3 +1,4 @@ +// src/store/useTodos.js import { create } from 'zustand' export const useTodos = create((set) => ({ @@ -33,4 +34,10 @@ export const useTodos = create((set) => ({ todo.id === id ? { ...todo, done: !todo.done } : todo ), })), + + // Mark *all* tasks as completed + completeAll: () => + set((state) => ({ + todos: state.todos.map((todo) => ({ ...todo, done: true })), + })), })) \ No newline at end of file From b989bd35034d3e9503233cf04ce165aa0007e9e0 Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Fri, 23 May 2025 00:33:37 +0200 Subject: [PATCH 17/53] edit Footer.jsx to render Stats component --- src/components/Footer.jsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/Footer.jsx b/src/components/Footer.jsx index 11f8cc5..65271a8 100644 --- a/src/components/Footer.jsx +++ b/src/components/Footer.jsx @@ -1,10 +1,10 @@ -import React from 'react'; +import React from 'react' +import Stats from './Stats' export default function Footer() { return ( -
    - {/* TODO: App title + ThemeToggle */} -

    Footer

    -
    - ); +
    + +
    + ) } \ No newline at end of file From 872dfe7127a4277ff93e9a9c9420d05ba5a6e312 Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Fri, 23 May 2025 00:49:12 +0200 Subject: [PATCH 18/53] start styling for different displays --- src/App.jsx | 15 ++++++++++++--- src/components/Header.jsx | 11 +++++++++-- src/components/TodoForm.jsx | 6 +++++- src/components/TodoList.jsx | 17 +++++++++++------ 4 files changed, 37 insertions(+), 12 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 3e3a7c2..9b80fe4 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -12,10 +12,19 @@ export default function App() { const todos = useTodos((s) => s.todos) return ( -
    +
    - -
    +
    {todos.length > 0 ? ( diff --git a/src/components/Header.jsx b/src/components/Header.jsx index b8374c2..9ff1930 100644 --- a/src/components/Header.jsx +++ b/src/components/Header.jsx @@ -1,10 +1,17 @@ import React from 'react'; +import ThemeToggle from './ThemeToggle' export default function Header() { return ( -
    - {/* TODO: App title + ThemeToggle */} +

    Todo App

    +
    ); } \ No newline at end of file diff --git a/src/components/TodoForm.jsx b/src/components/TodoForm.jsx index 71aa66d..e39b30a 100644 --- a/src/components/TodoForm.jsx +++ b/src/components/TodoForm.jsx @@ -16,7 +16,11 @@ export default function TodoForm() { return ( s.removeTodo) return ( -
      +
        {todos.map((todo) => (
      • {todo.text} -
        +
        From 486ab68d425858e75e53db21b163a727c13f200a Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Fri, 23 May 2025 00:52:21 +0200 Subject: [PATCH 19/53] style EmptyState and Footer --- src/components/EmptyState.jsx | 15 +++++++++------ src/components/Footer.jsx | 7 ++++++- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/components/EmptyState.jsx b/src/components/EmptyState.jsx index 0150752..26c791b 100644 --- a/src/components/EmptyState.jsx +++ b/src/components/EmptyState.jsx @@ -2,11 +2,11 @@ import React from 'react' export default function EmptyState() { return ( -
        - {/* Clipboard icon (Heroicons outline) */} +
        + {/* Clipboard icon */} -

        No tasks yet!

        -

        Looks like you don’t have any tasks. Let’s add one.

        - {/* Emoji fallback or could be replaced with a button/icon */} +

        + No tasks yet! +

        +

        + Looks like you don’t have any tasks. Let’s add one. +

        📋
        ) diff --git a/src/components/Footer.jsx b/src/components/Footer.jsx index 65271a8..591bb9d 100644 --- a/src/components/Footer.jsx +++ b/src/components/Footer.jsx @@ -3,7 +3,12 @@ import Stats from './Stats' export default function Footer() { return ( -
        +
        ) From 8193644a13244a5bedf7721ae844147335af1cdb Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Fri, 23 May 2025 01:01:25 +0200 Subject: [PATCH 20/53] add dark theme variables in index.css --- src/components/Stats.jsx | 10 +++++++++- src/index.css | 12 ++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/components/Stats.jsx b/src/components/Stats.jsx index 8ef7a7f..1cc47c1 100644 --- a/src/components/Stats.jsx +++ b/src/components/Stats.jsx @@ -11,7 +11,15 @@ export default function Stats() { if (totalCount === 0) return null return ( -
        +
        {totalCount} task{totalCount !== 1 ? 's' : ''}, {remainingCount} remaining diff --git a/src/index.css b/src/index.css index 3bc2664..1cb9e24 100644 --- a/src/index.css +++ b/src/index.css @@ -14,6 +14,18 @@ --bauhaus-completed: #9C9C9C; } +/* Bauhaus Dark Theme Variables */ +[data-theme="dark"] { + --bauhaus-bg: #0A0A0A; + --bauhaus-text: #F5F0E6; + --bauhaus-muted: #A9A9A9; + --bauhaus-accent-red: #F94144; + --bauhaus-accent-blue: #277DA1; + --bauhaus-accent-yellow: #F9C74F; + --bauhaus-border: #2E2E2E; + --bauhaus-completed: #555555; +} + /* Base overrides */ @layer base { body { From e770eb3ea822c6a2ef5780e3a3bb3237a9f73f43 Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Fri, 23 May 2025 01:02:07 +0200 Subject: [PATCH 21/53] create theme store --- src/store/useTheme.js | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/store/useTheme.js diff --git a/src/store/useTheme.js b/src/store/useTheme.js new file mode 100644 index 0000000..04d210c --- /dev/null +++ b/src/store/useTheme.js @@ -0,0 +1,9 @@ +import { create } from 'zustand' + +export const useTheme = create((set) => ({ + theme: 'light', + toggleTheme: () => + set((state) => ({ + theme: state.theme === 'light' ? 'dark' : 'light', + })), +})) \ No newline at end of file From 04a1a6c2c130fe3df884ba51d991a864c2837fa1 Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Fri, 23 May 2025 01:04:48 +0200 Subject: [PATCH 22/53] update App.jsx to listen and apply theme --- src/App.jsx | 40 +++++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 9b80fe4..2692b65 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,8 +1,7 @@ -// src/App.jsx -import React from 'react' +import React, { useEffect } from 'react' import { useTodos } from './store/useTodos' +import { useTheme } from './store/useTheme' import Header from './components/Header' -import ThemeToggle from './components/ThemeToggle' import TodoForm from './components/TodoForm' import TodoList from './components/TodoList' import EmptyState from './components/EmptyState' @@ -10,28 +9,27 @@ import Footer from './components/Footer' export default function App() { const todos = useTodos((s) => s.todos) + const theme = useTheme((s) => s.theme) + + // Apply the current theme to the element + useEffect(() => { + document.documentElement.setAttribute('data-theme', theme) + }, [theme]) return ( -
        +
        -
        +
        - - {todos.length > 0 ? ( - - ) : ( - - )} + {todos.length > 0 ? : }
        From b465507bc56f6c22420dfa1e062542c8b0424063 Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Fri, 23 May 2025 01:08:10 +0200 Subject: [PATCH 23/53] basic toggle theme functionality --- src/components/ThemeToggle.jsx | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/components/ThemeToggle.jsx b/src/components/ThemeToggle.jsx index 19d9e29..7c34d9c 100644 --- a/src/components/ThemeToggle.jsx +++ b/src/components/ThemeToggle.jsx @@ -1,10 +1,21 @@ -import React from 'react'; +import React from 'react' +import { useTheme } from '../store/useTheme' export default function ThemeToggle() { + const theme = useTheme((s) => s.theme) + const toggle = useTheme((s) => s.toggleTheme) + return ( -
        - {/* TODO: App title + ThemeToggle */} -

        Theme Toggle

        -
        - ); + + ) } \ No newline at end of file From 7738be4564edf1fb010df1b276cd1986a29daefc Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Fri, 23 May 2025 01:11:15 +0200 Subject: [PATCH 24/53] create Local Storage persistance when page reloads with Zustand's persist middleware --- src/store/useTodos.js | 76 +++++++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/src/store/useTodos.js b/src/store/useTodos.js index 7442dd6..053eac9 100644 --- a/src/store/useTodos.js +++ b/src/store/useTodos.js @@ -1,43 +1,47 @@ -// src/store/useTodos.js import { create } from 'zustand' +import { persist } from 'zustand/middleware' -export const useTodos = create((set) => ({ - todos: [ - { id: '1', text: 'Learn Zustand', done: false, createdAt: new Date() }, - { id: '2', text: 'Build a todo app', done: false, createdAt: new Date() }, - ], - - // Add a new todo - addTodo: (text) => - set((state) => ({ +export const useTodos = create( + persist( + (set) => ({ todos: [ - ...state.todos, - { - id: Date.now().toString(), - text, - done: false, - createdAt: new Date(), - }, + { id: '1', text: 'Learn Zustand', done: false, createdAt: new Date() }, + { id: '2', text: 'Build a todo app', done: false, createdAt: new Date() }, ], - })), - // Remove by id - removeTodo: (id) => - set((state) => ({ - todos: state.todos.filter((todo) => todo.id !== id), - })), + addTodo: (text) => + set((state) => ({ + todos: [ + ...state.todos, + { + id: Date.now().toString(), + text, + done: false, + createdAt: new Date(), + }, + ], + })), + + removeTodo: (id) => + set((state) => ({ + todos: state.todos.filter((todo) => todo.id !== id), + })), - // Toggle the 'done' flag - toggleTodo: (id) => - set((state) => ({ - todos: state.todos.map((todo) => - todo.id === id ? { ...todo, done: !todo.done } : todo - ), - })), + toggleTodo: (id) => + set((state) => ({ + todos: state.todos.map((todo) => + todo.id === id ? { ...todo, done: !todo.done } : todo + ), + })), - // Mark *all* tasks as completed - completeAll: () => - set((state) => ({ - todos: state.todos.map((todo) => ({ ...todo, done: true })), - })), -})) \ No newline at end of file + completeAll: () => + set((state) => ({ + todos: state.todos.map((todo) => ({ ...todo, done: true })), + })), + }), + { + name: 'todos-storage', // key in localStorage + getStorage: () => localStorage // (default) + } + ) +) \ No newline at end of file From 827b884faa6bf1a3da5a17a12d1d82b0484a5934 Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Fri, 23 May 2025 01:12:09 +0200 Subject: [PATCH 25/53] fix persistance for theme lock --- src/store/useTheme.js | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/store/useTheme.js b/src/store/useTheme.js index 04d210c..836fa4c 100644 --- a/src/store/useTheme.js +++ b/src/store/useTheme.js @@ -1,9 +1,19 @@ +// src/store/useTheme.js import { create } from 'zustand' +import { persist } from 'zustand/middleware' -export const useTheme = create((set) => ({ - theme: 'light', - toggleTheme: () => - set((state) => ({ - theme: state.theme === 'light' ? 'dark' : 'light', - })), -})) \ No newline at end of file +export const useTheme = create( + persist( + (set) => ({ + theme: 'light', + toggleTheme: () => + set((state) => ({ + theme: state.theme === 'light' ? 'dark' : 'light', + })), + }), + { + name: 'theme-storage', // key in localStorage + getStorage: () => localStorage + } + ) +) \ No newline at end of file From a9e70e845c15f6a65e3ef66b43b628dc81e99b97 Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Fri, 23 May 2025 01:15:13 +0200 Subject: [PATCH 26/53] update TodoList with timestamps --- src/components/TodoList.jsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/components/TodoList.jsx b/src/components/TodoList.jsx index 65a03c3..ccee26d 100644 --- a/src/components/TodoList.jsx +++ b/src/components/TodoList.jsx @@ -1,5 +1,5 @@ -// src/components/TodoList.jsx import React from 'react' +import dayjs from 'dayjs' import { useTodos } from '../store/useTodos' export default function TodoList() { @@ -14,14 +14,20 @@ export default function TodoList() { key={todo.id} className={` flex flex-col sm:flex-row sm:items-center justify-between - p-2 sm:p-3 md:p-4 /* responsive padding */ + p-2 sm:p-3 md:p-4 border border-bauhaus rounded transition-shadow hover:shadow-md ${todo.done ? 'opacity-50 line-through text-completed' : ''} `} > - {todo.text} -
        +
        + {todo.text} + + Created: {dayjs(todo.createdAt).format('MMM D, YYYY')} + +
        + +
        - -
        -
      • - ))} +
        + + +
        + + ) + })}
      ) } \ No newline at end of file From ab71aea1e12ca877bb9eda4dbcf2f91ef5d23ac1 Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Fri, 23 May 2025 01:29:11 +0200 Subject: [PATCH 30/53] Add task-filtering by status and created-after date --- src/components/FilterBar.jsx | 41 ++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/components/FilterBar.jsx diff --git a/src/components/FilterBar.jsx b/src/components/FilterBar.jsx new file mode 100644 index 0000000..fe84bab --- /dev/null +++ b/src/components/FilterBar.jsx @@ -0,0 +1,41 @@ +import React, { useState } from 'react' + +export default function FilterBar({ onFilterChange }) { + const [status, setStatus] = useState('all') + const [createdAfter, setCreatedAfter] = useState('') + + const handleStatusChange = (e) => { + const s = e.target.value + setStatus(s) + onFilterChange({ status: s, createdAfter }) + } + + const handleDateChange = (e) => { + const d = e.target.value + setCreatedAfter(d) + onFilterChange({ status, createdAfter: d }) + } + + return ( +
      + + + +
      + ) +} \ No newline at end of file From 2d2fe5d0f0cf89b34d6d6b6d19d6e8ba08114a05 Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Fri, 23 May 2025 01:31:00 +0200 Subject: [PATCH 31/53] update App.jsx for task filtering --- src/App.jsx | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 2692b65..6818207 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,8 +1,10 @@ -import React, { useEffect } from 'react' +// src/App.jsx +import React, { useEffect, useState } from 'react' import { useTodos } from './store/useTodos' import { useTheme } from './store/useTheme' import Header from './components/Header' import TodoForm from './components/TodoForm' +import FilterBar from './components/FilterBar' import TodoList from './components/TodoList' import EmptyState from './components/EmptyState' import Footer from './components/Footer' @@ -11,7 +13,29 @@ export default function App() { const todos = useTodos((s) => s.todos) const theme = useTheme((s) => s.theme) - // Apply the current theme to the element + // filter state + const [filters, setFilters] = useState({ + status: 'all', + createdAfter: '', + }) + + // compute visible tasks + const visibleTodos = todos.filter((todo) => { + // status filter + if (filters.status === 'active' && todo.done) return false + if (filters.status === 'completed' && !todo.done) return false + + // created-after filter + if (filters.createdAfter) { + const created = new Date(todo.createdAt) + const after = new Date(filters.createdAfter) + if (created < after) return false + } + + return true + }) + + // apply theme attribute useEffect(() => { document.documentElement.setAttribute('data-theme', theme) }, [theme]) @@ -29,7 +53,14 @@ export default function App() { " > - {todos.length > 0 ? : } + + + + {visibleTodos.length > 0 ? ( + + ) : ( + + )}
    From a4ee79979148ad1f16492141e5a82e614d27c6c7 Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Fri, 23 May 2025 01:36:22 +0200 Subject: [PATCH 32/53] update TodoList --- src/App.jsx | 1 - src/components/TodoList.jsx | 9 ++++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 6818207..b692b32 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,4 +1,3 @@ -// src/App.jsx import React, { useEffect, useState } from 'react' import { useTodos } from './store/useTodos' import { useTheme } from './store/useTheme' diff --git a/src/components/TodoList.jsx b/src/components/TodoList.jsx index 040b679..e5f959b 100644 --- a/src/components/TodoList.jsx +++ b/src/components/TodoList.jsx @@ -2,11 +2,14 @@ import React from 'react' import dayjs from 'dayjs' import { useTodos } from '../store/useTodos' -export default function TodoList() { - const todos = useTodos((s) => s.todos) +export default function TodoList({ todos: propTodos }) { + // accept filtered list if passed, otherwise read full store + // always fetch full list from the store + const allTodos = useTodos((s) => s.todos) + // use filtered list if provided, otherwise fallback to full store list + const todos = propTodos ?? allTodos const toggleTodo = useTodos((s) => s.toggleTodo) const removeTodo = useTodos((s) => s.removeTodo) - const today = dayjs() return ( From f5a2ca010bf2e9c328bea9cc5a936185dc499aeb Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Fri, 23 May 2025 01:37:33 +0200 Subject: [PATCH 33/53] start filter by status ffeature in FilterBar.jsx --- src/components/FilterBar.jsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/FilterBar.jsx b/src/components/FilterBar.jsx index fe84bab..df12512 100644 --- a/src/components/FilterBar.jsx +++ b/src/components/FilterBar.jsx @@ -5,15 +5,15 @@ export default function FilterBar({ onFilterChange }) { const [createdAfter, setCreatedAfter] = useState('') const handleStatusChange = (e) => { - const s = e.target.value - setStatus(s) - onFilterChange({ status: s, createdAfter }) + const newStatus = e.target.value + setStatus(newStatus) + onFilterChange({ status: newStatus, createdAfter }) } const handleDateChange = (e) => { - const d = e.target.value - setCreatedAfter(d) - onFilterChange({ status, createdAfter: d }) + const newDate = e.target.value + setCreatedAfter(newDate) + onFilterChange({ status, createdAfter: newDate }) } return ( From d09b8d3b24831090557addcfeef8ebc6ed0be1a7 Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Fri, 23 May 2025 01:38:33 +0200 Subject: [PATCH 34/53] update App.jsx to use the FilterBar --- src/App.jsx | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index b692b32..505048f 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,4 +1,5 @@ import React, { useEffect, useState } from 'react' +import dayjs from 'dayjs' import { useTodos } from './store/useTodos' import { useTheme } from './store/useTheme' import Header from './components/Header' @@ -9,39 +10,40 @@ import EmptyState from './components/EmptyState' import Footer from './components/Footer' export default function App() { - const todos = useTodos((s) => s.todos) const theme = useTheme((s) => s.theme) + const allTodos = useTodos((s) => s.todos) - // filter state + // filtering state const [filters, setFilters] = useState({ status: 'all', createdAfter: '', }) - // compute visible tasks - const visibleTodos = todos.filter((todo) => { + // whenever theme changes, apply it + useEffect(() => { + document.documentElement.setAttribute('data-theme', theme) + }, [theme]) + + // compute visible todos based on filters + const visibleTodos = allTodos.filter((todo) => { // status filter if (filters.status === 'active' && todo.done) return false if (filters.status === 'completed' && !todo.done) return false // created-after filter if (filters.createdAfter) { - const created = new Date(todo.createdAt) - const after = new Date(filters.createdAfter) - if (created < after) return false + const created = dayjs(todo.createdAt) + const after = dayjs(filters.createdAfter) + if (created.isBefore(after, 'day')) return false } return true }) - // apply theme attribute - useEffect(() => { - document.documentElement.setAttribute('data-theme', theme) - }, [theme]) - return (
    +
    -
    +
    diff --git a/src/components/FilterBar.jsx b/src/components/FilterBar.jsx index df12512..6cf79a4 100644 --- a/src/components/FilterBar.jsx +++ b/src/components/FilterBar.jsx @@ -3,17 +3,24 @@ import React, { useState } from 'react' export default function FilterBar({ onFilterChange }) { const [status, setStatus] = useState('all') const [createdAfter, setCreatedAfter] = useState('') + const [tagFilter, setTagFilter] = useState('') // ← new state const handleStatusChange = (e) => { const newStatus = e.target.value setStatus(newStatus) - onFilterChange({ status: newStatus, createdAfter }) + onFilterChange({ status: newStatus, createdAfter, tagFilter }) } const handleDateChange = (e) => { const newDate = e.target.value setCreatedAfter(newDate) - onFilterChange({ status, createdAfter: newDate }) + onFilterChange({ status, createdAfter: newDate, tagFilter }) + } + + const handleTagChange = (e) => { + const newTagFilter = e.target.value + setTagFilter(newTagFilter) + onFilterChange({ status, createdAfter, tagFilter: newTagFilter }) } return ( @@ -36,6 +43,15 @@ export default function FilterBar({ onFilterChange }) { className="p-2 border border-bauhaus rounded" aria-label="Show tasks created on or after" /> + +
    ) } \ No newline at end of file diff --git a/src/components/TodoForm.jsx b/src/components/TodoForm.jsx index 07ebd88..5b6326f 100644 --- a/src/components/TodoForm.jsx +++ b/src/components/TodoForm.jsx @@ -4,21 +4,32 @@ import { useTodos } from '../store/useTodos' export default function TodoForm() { const [text, setText] = useState('') const [dueDate, setDueDate] = useState('') + const [tags, setTags] = useState('') // ← new state for comma-separated tags const addTodo = useTodos((s) => s.addTodo) const handleSubmit = (e) => { e.preventDefault() const trimmed = text.trim() if (!trimmed) return - addTodo(trimmed, dueDate) + + // Build an array of non-empty, trimmed tags + const tagsArray = tags + .split(',') + .map((t) => t.trim()) + .filter((t) => t !== '') + + addTodo(trimmed, dueDate, tagsArray) + + // Reset form fields setText('') setDueDate('') + setTags('') } return ( + setTags(e.target.value)} + placeholder="Tags, comma-separated" + className="p-2 border border-bauhaus rounded focus:outline-none focus:ring-accent-blue" + aria-label="Optional tags" + /> +
    @@ -65,4 +76,4 @@ export default function TodoList({ todos: propTodos }) { })}
) -} \ No newline at end of file +} diff --git a/src/store/useTodos.js b/src/store/useTodos.js index 46c566f..be14e93 100644 --- a/src/store/useTodos.js +++ b/src/store/useTodos.js @@ -11,6 +11,7 @@ export const useTodos = create( done: false, createdAt: new Date(), dueDate: null, + tags: [], }, { id: '2', @@ -18,10 +19,11 @@ export const useTodos = create( done: false, createdAt: new Date(), dueDate: null, + tags: [], }, ], - addTodo: (text, dueDate) => + addTodo: (text, dueDate = null, tags = []) => set((state) => ({ todos: [ ...state.todos, @@ -31,6 +33,7 @@ export const useTodos = create( done: false, createdAt: new Date(), dueDate: dueDate ? new Date(dueDate) : null, + tags, }, ], })), @@ -51,6 +54,26 @@ export const useTodos = create( set((state) => ({ todos: state.todos.map((t) => ({ ...t, done: true })), })), + + // Add a tag to a todo (avoiding duplicates) + addTag: (id, tag) => + set((state) => ({ + todos: state.todos.map((t) => + t.id === id + ? { ...t, tags: t.tags.includes(tag) ? t.tags : [...t.tags, tag] } + : t + ), + })), + + // Remove a tag from a todo + removeTag: (id, tag) => + set((state) => ({ + todos: state.todos.map((t) => + t.id === id + ? { ...t, tags: t.tags.filter((existing) => existing !== tag) } + : t + ), + })), }), { name: 'todos-storage', From 9107e178a6f83204ccfc9a8313c3bfa9b17fb649 Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Fri, 23 May 2025 02:07:26 +0200 Subject: [PATCH 36/53] fix tailwind styling --- src/App.jsx | 33 ++++++++++++++++---------- src/index.css | 64 ++++++++++++++++++++++++--------------------------- 2 files changed, 51 insertions(+), 46 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 7404432..830cdf0 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -10,43 +10,45 @@ import EmptyState from './components/EmptyState' import Footer from './components/Footer' export default function App() { - const theme = useTheme((s) => s.theme) + const theme = useTheme((s) => s.theme) const allTodos = useTodos((s) => s.todos) - // include tagFilter in filters + // full filter state const [filters, setFilters] = useState({ - status: 'all', + status: 'all', createdAfter: '', - tagFilter: '', + tagFilter: '', }) + // apply dark/light theme to useEffect(() => { document.documentElement.setAttribute('data-theme', theme) }, [theme]) - // helper to split & trim comma list + // helper to split/tag‐trim const parseTags = (str) => str .split(',') .map((t) => t.trim()) .filter((t) => t !== '') + // derive the list we actually render const visibleTodos = allTodos.filter((todo) => { - // status filter + // 1. status if (filters.status === 'active' && todo.done) return false if (filters.status === 'completed' && !todo.done) return false - // created-after filter + // 2. created-after if (filters.createdAfter) { const created = dayjs(todo.createdAt) - const after = dayjs(filters.createdAfter) + const after = dayjs(filters.createdAfter) if (created.isBefore(after, 'day')) return false } - // tag filter: at least one match + // 3. tag filter if (filters.tagFilter) { const wantedTags = parseTags(filters.tagFilter) - const hasMatch = wantedTags.some((tag) => + const hasMatch = wantedTags.some((tag) => todo.tags.includes(tag) ) if (!hasMatch) return false @@ -59,9 +61,16 @@ export default function App() {
-
+
- {visibleTodos.length > 0 ? ( diff --git a/src/index.css b/src/index.css index 1cb9e24..3cf5269 100644 --- a/src/index.css +++ b/src/index.css @@ -1,52 +1,48 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; +@import "tailwindcss"; /* Bauhaus Light Theme Variables */ :root { - --bauhaus-bg: #F5F0E6; - --bauhaus-text: #1A1A1A; - --bauhaus-muted: #4F4F4F; + --bauhaus-bg: #fbfbfb; + --bauhaus-text: #1A1A1A; + --bauhaus-muted: #4F4F4F; --bauhaus-accent-red: #D72638; --bauhaus-accent-blue: #3F88C5; --bauhaus-accent-yellow: #F2C14E; - --bauhaus-border: #CCC5B9; - --bauhaus-completed: #9C9C9C; + --bauhaus-border: #CCC5B9; + --bauhaus-completed: #9C9C9C; +} + +@layer base { + body { + background-color: var(--bauhaus-bg); + color: var(--bauhaus-text); + font-family: "neue-kabel", sans-serif; + } } /* Bauhaus Dark Theme Variables */ [data-theme="dark"] { - --bauhaus-bg: #0A0A0A; - --bauhaus-text: #F5F0E6; - --bauhaus-muted: #A9A9A9; + --bauhaus-bg: #0A0A0A; + --bauhaus-text: #F5F0E6; + --bauhaus-muted: #A9A9A9; --bauhaus-accent-red: #F94144; --bauhaus-accent-blue: #277DA1; --bauhaus-accent-yellow: #F9C74F; - --bauhaus-border: #2E2E2E; - --bauhaus-completed: #555555; -} - -/* Base overrides */ -@layer base { - body { - background-color: var(--bauhaus-bg); - color: var(--bauhaus-text); - font-family: "neue-kabel", sans-serif; - } + --bauhaus-border: #2E2E2E; + --bauhaus-completed: #555555; } -/* Custom utilities for my palette */ @layer utilities { - .bg-bauhaus { background-color: var(--bauhaus-bg); } - .text-bauhaus { color: var(--bauhaus-text); } - .text-muted { color: var(--bauhaus-muted); } - .border-bauhaus { border-color: var(--bauhaus-border); } - .text-completed { color: var(--bauhaus-completed); } + .bg-bauhaus { background-color: var(--bauhaus-bg); } + .text-bauhaus-text { color: var(--bauhaus-text); } + .text-muted { color: var(--bauhaus-muted); } + .border-bauhaus { border-color: var(--bauhaus-border); } + .text-completed { color: var(--bauhaus-completed); } - .bg-accent-red { background-color: var(--bauhaus-accent-red); } - .text-accent-red { color: var(--bauhaus-accent-red); } - .bg-accent-blue { background-color: var(--bauhaus-accent-blue); } - .text-accent-blue { color: var(--bauhaus-accent-blue); } - .bg-accent-yellow { background-color: var(--bauhaus-accent-yellow); } - .text-accent-yellow { color: var(--bauhaus-accent-yellow); } + .bg-accent-red { background-color: var(--bauhaus-accent-red); } + .text-accent-red { color: var(--bauhaus-accent-red); } + .bg-accent-blue { background-color: var(--bauhaus-accent-blue); } + .text-accent-blue { color: var(--bauhaus-accent-blue); } + .bg-accent-yellow { background-color: var(--bauhaus-accent-yellow); } + .text-accent-yellow { color: var(--bauhaus-accent-yellow); } } \ No newline at end of file From 0120e71343e43837e3da7dcefefb0df2526495bd Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Fri, 23 May 2025 02:23:17 +0200 Subject: [PATCH 37/53] fix styling --- src/components/EmptyState.jsx | 2 +- src/components/Header.jsx | 2 +- src/components/ThemeToggle.jsx | 4 ++-- src/components/TodoForm.jsx | 4 ++-- src/components/TodoItem.jsx | 1 - src/index.css | 3 ++- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/EmptyState.jsx b/src/components/EmptyState.jsx index 26c791b..7e7c4a1 100644 --- a/src/components/EmptyState.jsx +++ b/src/components/EmptyState.jsx @@ -26,7 +26,7 @@ export default function EmptyState() {

Looks like you don’t have any tasks. Let’s add one.

- 📋 + {/* 📋 */}
) } \ No newline at end of file diff --git a/src/components/Header.jsx b/src/components/Header.jsx index 9ff1930..7a014ad 100644 --- a/src/components/Header.jsx +++ b/src/components/Header.jsx @@ -10,7 +10,7 @@ export default function Header() { sm:px-6 sm:py-4 /* tablet */ md:px-8 md:py-6 /* desktop */ "> -

Todo App

+

KLAR!

); diff --git a/src/components/ThemeToggle.jsx b/src/components/ThemeToggle.jsx index 7c34d9c..ec92fe1 100644 --- a/src/components/ThemeToggle.jsx +++ b/src/components/ThemeToggle.jsx @@ -11,8 +11,8 @@ export default function ThemeToggle() { aria-label="Toggle dark/light theme" className=" p-2 rounded - bg-accent-yellow text-bauhaus-text - focus:outline-none focus:ring-2 focus:ring-accent-yellow/50 + text-bauhaus-text + focus:outline-none " > {theme === 'light' ? '🌙' : '☀️'} diff --git a/src/components/TodoForm.jsx b/src/components/TodoForm.jsx index 5b6326f..8abdf0b 100644 --- a/src/components/TodoForm.jsx +++ b/src/components/TodoForm.jsx @@ -29,7 +29,7 @@ export default function TodoForm() { return ( - Add + + ) diff --git a/src/components/TodoItem.jsx b/src/components/TodoItem.jsx index 71b13b1..71c4cfd 100644 --- a/src/components/TodoItem.jsx +++ b/src/components/TodoItem.jsx @@ -3,7 +3,6 @@ import React from 'react'; export default function TodoItem() { return (
- {/* TODO: App title + ThemeToggle */}

Todo Item

); diff --git a/src/index.css b/src/index.css index 3cf5269..bb9ca5c 100644 --- a/src/index.css +++ b/src/index.css @@ -6,7 +6,8 @@ --bauhaus-text: #1A1A1A; --bauhaus-muted: #4F4F4F; --bauhaus-accent-red: #D72638; - --bauhaus-accent-blue: #3F88C5; + --bauhaus-accent-blue: #002FA7; + /* --bauhaus-accent-blue: #3F88C5; */ --bauhaus-accent-yellow: #F2C14E; --bauhaus-border: #CCC5B9; --bauhaus-completed: #9C9C9C; From 2d96d814b68f29c004ee6dcca7fed2bb034c956a Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Fri, 23 May 2025 02:23:49 +0200 Subject: [PATCH 38/53] add title in index.html --- index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.html b/index.html index 388c080..8aacfd1 100644 --- a/index.html +++ b/index.html @@ -5,7 +5,7 @@ - Todo + KLAR!
From 5086e2a14de0894ea92267ea11936de75383a5d0 Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Fri, 23 May 2025 02:27:54 +0200 Subject: [PATCH 39/53] update readme.md --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d1c68b5..96060aa 100644 --- a/README.md +++ b/README.md @@ -1 +1,5 @@ -# Todo \ No newline at end of file +# KLAR! Todo App + +[https://talotodo.netlify.app/](https://talotodo.netlify.app/) + +WIP ! WORK IN PROGRESS WIP ! \ No newline at end of file From 9bffcf2faf90a444360796514a77e607f74c4702 Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Fri, 23 May 2025 10:01:18 +0200 Subject: [PATCH 40/53] change size for logo txt --- src/components/Header.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Header.jsx b/src/components/Header.jsx index 7a014ad..e90aa9a 100644 --- a/src/components/Header.jsx +++ b/src/components/Header.jsx @@ -10,7 +10,7 @@ export default function Header() { sm:px-6 sm:py-4 /* tablet */ md:px-8 md:py-6 /* desktop */ "> -

KLAR!

+

klar

); From 34513c6b09e9d92a537eeabc32da370894bca50f Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Fri, 23 May 2025 10:27:46 +0200 Subject: [PATCH 41/53] fix sun and moon icons, title in index.html --- index.html | 2 +- public/moon.svg | 1 + public/sun.svg | 1 + src/components/EmptyState.jsx | 1 - src/components/ThemeToggle.jsx | 6 +++++- 5 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 public/moon.svg create mode 100644 public/sun.svg diff --git a/index.html b/index.html index 8aacfd1..f8cdeff 100644 --- a/index.html +++ b/index.html @@ -5,7 +5,7 @@ - KLAR! + klar
diff --git a/public/moon.svg b/public/moon.svg new file mode 100644 index 0000000..1609936 --- /dev/null +++ b/public/moon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/sun.svg b/public/sun.svg new file mode 100644 index 0000000..eb6fb59 --- /dev/null +++ b/public/sun.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/EmptyState.jsx b/src/components/EmptyState.jsx index 7e7c4a1..540fa56 100644 --- a/src/components/EmptyState.jsx +++ b/src/components/EmptyState.jsx @@ -3,7 +3,6 @@ import React from 'react' export default function EmptyState() { return (
- {/* Clipboard icon */} - {theme === 'light' ? '🌙' : '☀️'} + {theme === 'light' ? ( + Switch to dark theme + ) : ( + Switch to light theme + )} ) } \ No newline at end of file From 38ef0cd93cf946841582b3be4b3c1ffce75ca03d Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Fri, 23 May 2025 10:56:04 +0200 Subject: [PATCH 42/53] added some comments --- src/App.jsx | 2 +- src/components/EmptyState.jsx | 2 +- src/components/Stats.jsx | 11 ++++++----- src/store/useTodos.js | 12 ++++++------ 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 830cdf0..7140fce 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -14,7 +14,7 @@ export default function App() { const allTodos = useTodos((s) => s.todos) // full filter state - const [filters, setFilters] = useState({ + const [filters, setFilters] = useState({ // default values status: 'all', createdAfter: '', tagFilter: '', diff --git a/src/components/EmptyState.jsx b/src/components/EmptyState.jsx index 540fa56..9edb1a4 100644 --- a/src/components/EmptyState.jsx +++ b/src/components/EmptyState.jsx @@ -1,6 +1,6 @@ import React from 'react' -export default function EmptyState() { +export default function EmptyState() { // EmptyState component return (
s.todos) - const completeAll = useTodos((s) => s.completeAll) - const totalCount = todos.length - const remainingCount = todos.filter((t) => !t.done).length +export default function Stats() { // Stats component + // Zustand store for todos + const todos = useTodos((s) => s.todos) // Get todos from Zustand store + const completeAll = useTodos((s) => s.completeAll) // Get completeAll function from Zustand store + const totalCount = todos.length // Total number of todos + const remainingCount = todos.filter((t) => !t.done).length // Number of remaining todos // If no tasks, render nothing if (totalCount === 0) return null diff --git a/src/store/useTodos.js b/src/store/useTodos.js index be14e93..add3af7 100644 --- a/src/store/useTodos.js +++ b/src/store/useTodos.js @@ -23,7 +23,7 @@ export const useTodos = create( }, ], - addTodo: (text, dueDate = null, tags = []) => + addTodo: (text, dueDate = null, tags = []) => // Add a new todo set((state) => ({ todos: [ ...state.todos, @@ -38,19 +38,19 @@ export const useTodos = create( ], })), - removeTodo: (id) => + removeTodo: (id) => // Remove a todo by ID set((state) => ({ todos: state.todos.filter((t) => t.id !== id), })), - toggleTodo: (id) => + toggleTodo: (id) => // Toggle the done state of a todo set((state) => ({ todos: state.todos.map((t) => t.id === id ? { ...t, done: !t.done } : t ), })), - completeAll: () => + completeAll: () => // Mark all todos as done set((state) => ({ todos: state.todos.map((t) => ({ ...t, done: true })), })), @@ -60,7 +60,7 @@ export const useTodos = create( set((state) => ({ todos: state.todos.map((t) => t.id === id - ? { ...t, tags: t.tags.includes(tag) ? t.tags : [...t.tags, tag] } + ? { ...t, tags: t.tags.includes(tag) ? t.tags : [...t.tags, tag] } : t ), })), @@ -77,7 +77,7 @@ export const useTodos = create( }), { name: 'todos-storage', - getStorage: () => localStorage, + getStorage: () => localStorage, // Use localStorage as the storage } ) ) \ No newline at end of file From 7788a3ed51f511706361504bc02b5433047e75c1 Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Fri, 23 May 2025 11:07:41 +0200 Subject: [PATCH 43/53] edit readme.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 96060aa..7333796 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -# KLAR! Todo App +# klar Todo App [https://talotodo.netlify.app/](https://talotodo.netlify.app/) -WIP ! WORK IN PROGRESS WIP ! \ No newline at end of file +WIP ! - WORK IN PROGRESS - WIP ! IT'S CALLED klar but it's not KLAR YET - WIP ! \ No newline at end of file From f8704f90e48d3460f9190107115879082c3511e4 Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Mon, 26 May 2025 01:51:37 +0200 Subject: [PATCH 44/53] implement project grouping in useTodos.js --- src/components/Header.jsx | 2 +- src/store/useTodos.js | 30 ++++++++++++++++++++++-------- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/components/Header.jsx b/src/components/Header.jsx index e90aa9a..9a1ada7 100644 --- a/src/components/Header.jsx +++ b/src/components/Header.jsx @@ -10,7 +10,7 @@ export default function Header() { sm:px-6 sm:py-4 /* tablet */ md:px-8 md:py-6 /* desktop */ "> -

klar

+

klar

); diff --git a/src/store/useTodos.js b/src/store/useTodos.js index add3af7..ad6aed8 100644 --- a/src/store/useTodos.js +++ b/src/store/useTodos.js @@ -12,6 +12,7 @@ export const useTodos = create( createdAt: new Date(), dueDate: null, tags: [], + project: 'General', // ← new project field }, { id: '2', @@ -20,10 +21,16 @@ export const useTodos = create( createdAt: new Date(), dueDate: null, tags: [], + project: 'General', }, ], - addTodo: (text, dueDate = null, tags = []) => // Add a new todo + addTodo: ( + text, + dueDate = null, + tags = [], + project = 'General' // ← accept an optional project + ) => set((state) => ({ todos: [ ...state.todos, @@ -34,38 +41,37 @@ export const useTodos = create( createdAt: new Date(), dueDate: dueDate ? new Date(dueDate) : null, tags, + project, }, ], })), - removeTodo: (id) => // Remove a todo by ID + removeTodo: (id) => set((state) => ({ todos: state.todos.filter((t) => t.id !== id), })), - toggleTodo: (id) => // Toggle the done state of a todo + toggleTodo: (id) => set((state) => ({ todos: state.todos.map((t) => t.id === id ? { ...t, done: !t.done } : t ), })), - completeAll: () => // Mark all todos as done + completeAll: () => set((state) => ({ todos: state.todos.map((t) => ({ ...t, done: true })), })), - // Add a tag to a todo (avoiding duplicates) addTag: (id, tag) => set((state) => ({ todos: state.todos.map((t) => t.id === id - ? { ...t, tags: t.tags.includes(tag) ? t.tags : [...t.tags, tag] } + ? { ...t, tags: t.tags.includes(tag) ? t.tags : [...t.tags, tag] } : t ), })), - // Remove a tag from a todo removeTag: (id, tag) => set((state) => ({ todos: state.todos.map((t) => @@ -74,10 +80,18 @@ export const useTodos = create( : t ), })), + + // ← new action to change a task’s project + changeProject: (id, project) => + set((state) => ({ + todos: state.todos.map((t) => + t.id === id ? { ...t, project } : t + ), + })), }), { name: 'todos-storage', - getStorage: () => localStorage, // Use localStorage as the storage + getStorage: () => localStorage, } ) ) \ No newline at end of file From 87d53a39f63f94cf85e0c47945188aa4dca872cf Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Mon, 26 May 2025 01:54:49 +0200 Subject: [PATCH 45/53] wire project selector into form at TodoForm.jsx --- src/components/TodoForm.jsx | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/components/TodoForm.jsx b/src/components/TodoForm.jsx index 8abdf0b..f60c370 100644 --- a/src/components/TodoForm.jsx +++ b/src/components/TodoForm.jsx @@ -4,7 +4,8 @@ import { useTodos } from '../store/useTodos' export default function TodoForm() { const [text, setText] = useState('') const [dueDate, setDueDate] = useState('') - const [tags, setTags] = useState('') // ← new state for comma-separated tags + const [tags, setTags] = useState('') + const [project, setProject] = useState('General') // Default project const addTodo = useTodos((s) => s.addTodo) const handleSubmit = (e) => { @@ -18,18 +19,22 @@ export default function TodoForm() { .map((t) => t.trim()) .filter((t) => t !== '') - addTodo(trimmed, dueDate, tagsArray) + // Use provided project or fallback to "General" + const projectName = project.trim() || 'General' + + addTodo(trimmed, dueDate, tagsArray, projectName) // Reset form fields setText('') setDueDate('') setTags('') + setProject('General') } return (
+ setProject(e.target.value)} + placeholder="Project (default: General)" + className="p-2 border border-bauhaus rounded focus:outline-none focus:ring-accent-blue" + aria-label="Project" + /> +
) From b6c3d3f41a5bcab09075fad442de24541f9f6657 Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Mon, 26 May 2025 01:58:11 +0200 Subject: [PATCH 46/53] update TodoList.jsx to group tasks by project names --- src/components/TodoList.jsx | 137 ++++++++++++++++++++---------------- 1 file changed, 75 insertions(+), 62 deletions(-) diff --git a/src/components/TodoList.jsx b/src/components/TodoList.jsx index 131ddd4..8bcb9b9 100644 --- a/src/components/TodoList.jsx +++ b/src/components/TodoList.jsx @@ -4,76 +4,89 @@ import { useTodos } from '../store/useTodos' export default function TodoList({ todos: propTodos }) { // always fetch full list from the store - const allTodos = useTodos((s) => s.todos) + const allTodos = useTodos((s) => s.todos) // use filtered list if provided, otherwise fallback to full store list - const todos = propTodos ?? allTodos + const todos = propTodos ?? allTodos const toggleTodo = useTodos((s) => s.toggleTodo) const removeTodo = useTodos((s) => s.removeTodo) - const today = dayjs() + const today = dayjs() - return ( -
    - {todos.map((todo) => { - const due = todo.dueDate ? dayjs(todo.dueDate) : null - const isOverdue = due && due.isBefore(today, 'day') && !todo.done + // compute unique project names in order of appearance + const projects = [...new Set(todos.map((t) => t.project))] + return ( +
    + {projects.map((project) => { + const projectTodos = todos.filter((t) => t.project === project) return ( -
  • -
    - {todo.text} - - Created: {dayjs(todo.createdAt).format('MMM D, YYYY')} - - {due && ( - - Due: {due.format('MMM D, YYYY')} - - )} - {todo.tags && todo.tags.length > 0 && ( -
    - {todo.tags.map((tag) => ( - - {tag} - - ))} -
    - )} -
    +
    +

    {project}

    +
      + {projectTodos.map((todo) => { + const due = todo.dueDate ? dayjs(todo.dueDate) : null + const isOverdue = due && due.isBefore(today, 'day') && !todo.done + + return ( +
    • +
      + {todo.text} + + Created: {dayjs(todo.createdAt).format('MMM D, YYYY')} + + {due && ( + + Due: {due.format('MMM D, YYYY')} + + )} + {todo.tags && todo.tags.length > 0 && ( +
      + {todo.tags.map((tag) => ( + + {tag} + + ))} +
      + )} +
      -
      - - -
      -
    • +
      + + +
      + + ) + })} +
    +
    ) })} -
+
) } From bca939efa19ef5a1c446f07a1c56f04f2fab60da Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Mon, 26 May 2025 02:03:12 +0200 Subject: [PATCH 47/53] add project-level filtering in FilterBar.jsx and App.jsx --- src/App.jsx | 81 +++++++++++++++++++----------------- src/components/FilterBar.jsx | 59 ++++++++++++++++++++++---- 2 files changed, 95 insertions(+), 45 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 7140fce..212db73 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -13,65 +13,70 @@ export default function App() { const theme = useTheme((s) => s.theme) const allTodos = useTodos((s) => s.todos) + // build the unique project list + const projects = Array.from(new Set(allTodos.map((t) => t.project))) + // full filter state - const [filters, setFilters] = useState({ // default values + const [filters, setFilters] = useState({ status: 'all', createdAfter: '', tagFilter: '', + projectFilter:'all', }) - // apply dark/light theme to + // apply dark/light theme useEffect(() => { document.documentElement.setAttribute('data-theme', theme) }, [theme]) - // helper to split/tag‐trim + // helper to parse comma‐lists const parseTags = (str) => - str - .split(',') - .map((t) => t.trim()) - .filter((t) => t !== '') + str.split(',').map((t) => t.trim()).filter((t) => t !== '') - // derive the list we actually render - const visibleTodos = allTodos.filter((todo) => { - // 1. status - if (filters.status === 'active' && todo.done) return false - if (filters.status === 'completed' && !todo.done) return false + // derive the visible todos based on filters + const visibleTodos = allTodos + .filter((todo) => { + // project filter + if ( + filters.projectFilter !== 'all' && + todo.project !== filters.projectFilter + ) + return false - // 2. created-after - if (filters.createdAfter) { - const created = dayjs(todo.createdAt) - const after = dayjs(filters.createdAfter) - if (created.isBefore(after, 'day')) return false - } + // status filter + if (filters.status === 'active' && todo.done) return false + if (filters.status === 'completed' && !todo.done) return false - // 3. tag filter - if (filters.tagFilter) { - const wantedTags = parseTags(filters.tagFilter) - const hasMatch = wantedTags.some((tag) => - todo.tags.includes(tag) - ) - if (!hasMatch) return false - } + // created-after filter + if (filters.createdAfter) { + const created = dayjs(todo.createdAt) + const after = dayjs(filters.createdAfter) + if (created.isBefore(after, 'day')) return false + } - return true - }) + // tag filter + if (filters.tagFilter) { + const wantedTags = parseTags(filters.tagFilter) + const hasMatch = wantedTags.some((tag) => + todo.tags.includes(tag) + ) + if (!hasMatch) return false + } + + return true + }) return (
-
+
- + + {visibleTodos.length > 0 ? ( diff --git a/src/components/FilterBar.jsx b/src/components/FilterBar.jsx index 6cf79a4..26d1d2f 100644 --- a/src/components/FilterBar.jsx +++ b/src/components/FilterBar.jsx @@ -1,30 +1,73 @@ import React, { useState } from 'react' -export default function FilterBar({ onFilterChange }) { +export default function FilterBar({ projects, onFilterChange }) { const [status, setStatus] = useState('all') const [createdAfter, setCreatedAfter] = useState('') - const [tagFilter, setTagFilter] = useState('') // ← new state + const [tagFilter, setTagFilter] = useState('') + const [projectFilter, setProjectFilter] = useState('all') const handleStatusChange = (e) => { const newStatus = e.target.value setStatus(newStatus) - onFilterChange({ status: newStatus, createdAfter, tagFilter }) + onFilterChange({ + status: newStatus, + createdAfter, + tagFilter, + projectFilter, + }) } const handleDateChange = (e) => { const newDate = e.target.value setCreatedAfter(newDate) - onFilterChange({ status, createdAfter: newDate, tagFilter }) + onFilterChange({ + status, + createdAfter: newDate, + tagFilter, + projectFilter, + }) } const handleTagChange = (e) => { const newTagFilter = e.target.value setTagFilter(newTagFilter) - onFilterChange({ status, createdAfter, tagFilter: newTagFilter }) + onFilterChange({ + status, + createdAfter, + tagFilter: newTagFilter, + projectFilter, + }) + } + + const handleProjectChange = (e) => { + const newProject = e.target.value + setProjectFilter(newProject) + onFilterChange({ + status, + createdAfter, + tagFilter, + projectFilter: newProject, + }) } return (
+ {/* Project filter */} + + + {/* Status filter */} + {/* Created-after filter */} + {/* Tag filter */}
- ) +) } \ No newline at end of file From da008483d87fd2a7dab7d0d85a886ec377f785ab Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Mon, 26 May 2025 02:12:13 +0200 Subject: [PATCH 48/53] styling form so its centered in mobile and tablet --- src/components/TodoForm.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/TodoForm.jsx b/src/components/TodoForm.jsx index f60c370..9f8d443 100644 --- a/src/components/TodoForm.jsx +++ b/src/components/TodoForm.jsx @@ -5,7 +5,7 @@ export default function TodoForm() { const [text, setText] = useState('') const [dueDate, setDueDate] = useState('') const [tags, setTags] = useState('') - const [project, setProject] = useState('General') // Default project + const [project, setProject] = useState('') // Default project const addTodo = useTodos((s) => s.addTodo) const handleSubmit = (e) => { @@ -34,7 +34,7 @@ export default function TodoForm() { return (
Date: Mon, 26 May 2025 02:14:33 +0200 Subject: [PATCH 49/53] install hello-pangea for drag-and-drop reordering within each project group --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 2fd417e..da264e4 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "@hello-pangea/dnd": "^18.0.1", "@tailwindcss/vite": "^4.1.7", "dayjs": "^1.11.13", "react": "^19.0.0", From c2bb26cbfbfd0fdfc87b39a9bbe9479cac816086 Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Mon, 26 May 2025 02:23:19 +0200 Subject: [PATCH 50/53] implement drag-and-drop with hello pangea on useTodos.js and TodoList.jsx --- src/components/TodoList.jsx | 196 ++++++++++++++++++++++-------------- src/store/useTodos.js | 27 +++-- 2 files changed, 138 insertions(+), 85 deletions(-) diff --git a/src/components/TodoList.jsx b/src/components/TodoList.jsx index 8bcb9b9..2c99be9 100644 --- a/src/components/TodoList.jsx +++ b/src/components/TodoList.jsx @@ -1,92 +1,134 @@ import React from 'react' import dayjs from 'dayjs' import { useTodos } from '../store/useTodos' +import { + DragDropContext, + Droppable, + Draggable, +} from '@hello-pangea/dnd' export default function TodoList({ todos: propTodos }) { - // always fetch full list from the store - const allTodos = useTodos((s) => s.todos) - // use filtered list if provided, otherwise fallback to full store list - const todos = propTodos ?? allTodos - const toggleTodo = useTodos((s) => s.toggleTodo) - const removeTodo = useTodos((s) => s.removeTodo) - const today = dayjs() + const allTodos = useTodos((s) => s.todos) + const todos = propTodos ?? allTodos + const toggleTodo = useTodos((s) => s.toggleTodo) + const removeTodo = useTodos((s) => s.removeTodo) + const reorderInGroup = useTodos((s) => s.reorderInGroup) + const today = dayjs() - // compute unique project names in order of appearance + // Unique project names, preserving order const projects = [...new Set(todos.map((t) => t.project))] + const handleDragEnd = (result) => { + const { source, destination } = result + if (!destination) return + if (source.droppableId !== destination.droppableId) return + + reorderInGroup( + source.droppableId, + source.index, + destination.index + ) + } + return ( -
- {projects.map((project) => { - const projectTodos = todos.filter((t) => t.project === project) - return ( -
-

{project}

-
    - {projectTodos.map((todo) => { - const due = todo.dueDate ? dayjs(todo.dueDate) : null - const isOverdue = due && due.isBefore(today, 'day') && !todo.done + +
    + {projects.map((project) => { + const projectTasks = todos.filter((t) => t.project === project) - return ( -
  • +

    {project}

    + + + {(provided) => ( +
      -
      - {todo.text} - - Created: {dayjs(todo.createdAt).format('MMM D, YYYY')} - - {due && ( - { + const due = todo.dueDate ? dayjs(todo.dueDate) : null + const isOverdue = + due && due.isBefore(today, 'day') && !todo.done + + return ( + - Due: {due.format('MMM D, YYYY')} - - )} - {todo.tags && todo.tags.length > 0 && ( -
      - {todo.tags.map((tag) => ( - ( +
    • - {tag} - - ))} -
    • - )} -
      +
      + + {todo.text} + + + Created: {dayjs(todo.createdAt).format('MMM D, YYYY')} + + {due && ( + + Due: {due.format('MMM D, YYYY')} + + )} + {todo.tags?.length > 0 && ( +
      + {todo.tags.map((tag) => ( + + {tag} + + ))} +
      + )} +
      -
      - - -
      - - ) - })} -
    -
- ) - })} -
+
+ + +
+ + )} + + ) + })} + {provided.placeholder} + + )} + + + ) + })} +
+ ) -} +} \ No newline at end of file diff --git a/src/store/useTodos.js b/src/store/useTodos.js index ad6aed8..e47e108 100644 --- a/src/store/useTodos.js +++ b/src/store/useTodos.js @@ -12,7 +12,7 @@ export const useTodos = create( createdAt: new Date(), dueDate: null, tags: [], - project: 'General', // ← new project field + project: 'General', }, { id: '2', @@ -25,12 +25,7 @@ export const useTodos = create( }, ], - addTodo: ( - text, - dueDate = null, - tags = [], - project = 'General' // ← accept an optional project - ) => + addTodo: (text, dueDate = null, tags = [], project = 'General') => set((state) => ({ todos: [ ...state.todos, @@ -81,13 +76,29 @@ export const useTodos = create( ), })), - // ← new action to change a task’s project changeProject: (id, project) => set((state) => ({ todos: state.todos.map((t) => t.id === id ? { ...t, project } : t ), })), + + // New action for drag-and-drop reordering within a project + reorderInGroup: (project, fromIndex, toIndex) => + set((state) => { + const groupTasks = state.todos.filter((t) => t.project === project) + if (fromIndex === toIndex) return { todos: state.todos } + + const newGroup = Array.from(groupTasks) + const [moved] = newGroup.splice(fromIndex, 1) + newGroup.splice(toIndex, 0, moved) + + const firstIndex = state.todos.findIndex((t) => t.project === project) + const newTodos = Array.from(state.todos) + newTodos.splice(firstIndex, groupTasks.length, ...newGroup) + + return { todos: newTodos } + }), }), { name: 'todos-storage', From 17d414cfeaddc77bc8a9f684fb5c1b477b9474f2 Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Mon, 26 May 2025 02:33:39 +0200 Subject: [PATCH 51/53] fix some accessibility issues from Lighthouse, regarding ARIA and list formatting --- src/components/TodoList.jsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/TodoList.jsx b/src/components/TodoList.jsx index 2c99be9..771c08b 100644 --- a/src/components/TodoList.jsx +++ b/src/components/TodoList.jsx @@ -62,6 +62,7 @@ export default function TodoList({ todos: propTodos }) {
  • ) })} - {provided.placeholder} + + {/* Wrap placeholder so
      has only
    • children */} +
    )} From e6044ab0e10daa53f182398a025e061dcf70bacf Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Mon, 26 May 2025 02:37:02 +0200 Subject: [PATCH 52/53] add meta description for better SEO --- index.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/index.html b/index.html index f8cdeff..44482d6 100644 --- a/index.html +++ b/index.html @@ -2,6 +2,10 @@ + From b059e64cfaab1f2ed22f14359c5033dc88b2c75c Mon Sep 17 00:00:00 2001 From: Talo Vargas Date: Mon, 26 May 2025 02:52:58 +0200 Subject: [PATCH 53/53] update readme.md --- README.md | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7333796..ea51cdd 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,55 @@ [https://talotodo.netlify.app/](https://talotodo.netlify.app/) -WIP ! - WORK IN PROGRESS - WIP ! IT'S CALLED klar but it's not KLAR YET - WIP ! \ No newline at end of file + +**A Bauhaus-inspired Todo application built with React, Zustand, and Tailwind CSS.** + + +## Key Features + +**Task Management** + Add, list, toggle (mark done/undo), and remove tasks with ease. + +**Global State** + Powered by Zustand for zero-prop-drilling and efficient updates. + +**Bulk Actions** + “Complete All” button to mark every task as completed instantly. + +**Empty-State UX** + Engaging empty-state screen to encourage task creation. + +**Responsive Design** + Mobile-first layout scaling gracefully from 320px to 1920px with Tailwind CSS. + +**Bauhaus! Zustand! Klar!** + Bauhaus-inspired styling, font and colors with custom CSS variables and utility classes for a minimalist aesthetic. + +**Dark/Light Theme** + Toggle between light and dark modes, with preferences persisted in local storage. + +**Persistence** + Todos and theme choice saved in local storage for continuity across sessions. + +**Timestamps** + Task creation dates formatted with Day.js. + +**Due-Date Support** + Optional due-date input with visual styling for overdue tasks. + +**Category Tags** + Add comma-separated tags to tasks, displayed as badges. + +**Advanced Filtering** + Filter tasks by status (All/Active/Completed), creation date, tags, and project. + +**Project Grouping** + Organize tasks under named projects. + + **Drag-and-Drop** + Reorder tasks within each project group via drag-and-drop. + +**Accessibility** + Semantic HTML, ARIA roles, focus management, SEO friendly, and Lighthouse scores ≥95. + +WIP - WORK IN PROGRESS - it's called klar but it ain't klart yet! - WIP