Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
f2e913e
install zustand
govargas May 22, 2025
2dad1c1
set up tailwind
govargas May 22, 2025
892e8e2
add font family
govargas May 22, 2025
accc784
install day.js for dates
govargas May 22, 2025
c05499f
sketch UI and breakt it into components
govargas May 22, 2025
0d94a3f
fix import App at main.jsx
govargas May 22, 2025
b32565c
Set up Zustand store and wire TodoList to it
govargas May 22, 2025
6bc432e
create store folder and useTodos.js inside
govargas May 22, 2025
df9f203
hook up TodoList to the store
govargas May 22, 2025
b50e6fa
global state wired up
govargas May 22, 2025
1ac6303
build out TodoForm.jsx
govargas May 22, 2025
baf527b
contitionally render TodoList vs EmptyState
govargas May 22, 2025
fbd3ba7
edit index.css for custom bauhaus inspired theme
govargas May 22, 2025
fe288f8
fix styling
govargas May 22, 2025
bd34b38
further styling the TodoForm
govargas May 22, 2025
7ace1bc
extend store with completeAll
govargas May 22, 2025
b989bd3
edit Footer.jsx to render Stats component
govargas May 22, 2025
872dfe7
start styling for different displays
govargas May 22, 2025
486ab68
style EmptyState and Footer
govargas May 22, 2025
8193644
add dark theme variables in index.css
govargas May 22, 2025
e770eb3
create theme store
govargas May 22, 2025
04a1a6c
update App.jsx to listen and apply theme
govargas May 22, 2025
b465507
basic toggle theme functionality
govargas May 22, 2025
7738be4
create Local Storage persistance when page reloads with Zustand's per…
govargas May 22, 2025
827b884
fix persistance for theme lock
govargas May 22, 2025
a9e70e8
update TodoList with timestamps
govargas May 22, 2025
7013971
update useTodos for optional due-date input
govargas May 22, 2025
b6f1f6f
update TodoForm
govargas May 22, 2025
8dbca29
update TodoList
govargas May 22, 2025
ab71aea
Add task-filtering by status and created-after date
govargas May 22, 2025
2d2fe5d
update App.jsx for task filtering
govargas May 22, 2025
a4ee799
update TodoList
govargas May 22, 2025
f5a2ca0
start filter by status ffeature in FilterBar.jsx
govargas May 22, 2025
d09b8d3
update App.jsx to use the FilterBar
govargas May 22, 2025
38f0c2a
add tags
govargas May 22, 2025
9107e17
fix tailwind styling
govargas May 23, 2025
0120e71
fix styling
govargas May 23, 2025
2d96d81
add title in index.html
govargas May 23, 2025
5086e2a
update readme.md
govargas May 23, 2025
9bffcf2
change size for logo txt
govargas May 23, 2025
34513c6
fix sun and moon icons, title in index.html
govargas May 23, 2025
38ef0cd
added some comments
govargas May 23, 2025
7788a3e
edit readme.md
govargas May 23, 2025
f8704f9
implement project grouping in useTodos.js
govargas May 25, 2025
87d53a3
wire project selector into form at TodoForm.jsx
govargas May 25, 2025
b6c3d3f
update TodoList.jsx to group tasks by project names
govargas May 25, 2025
bca939e
add project-level filtering in FilterBar.jsx and App.jsx
govargas May 26, 2025
da00848
styling form so its centered in mobile and tablet
govargas May 26, 2025
a516df0
install hello-pangea for drag-and-drop reordering within each project…
govargas May 26, 2025
c2bb26c
implement drag-and-drop with hello pangea on useTodos.js and TodoList…
govargas May 26, 2025
17d414c
fix some accessibility issues from Lighthouse, regarding ARIA and lis…
govargas May 26, 2025
e6044ab
add meta description for better SEO
govargas May 26, 2025
b059e64
update readme.md
govargas May 26, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 56 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,56 @@
# Todo
# klar Todo App

[https://talotodo.netlify.app/](https://talotodo.netlify.app/)


**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
7 changes: 6 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="description"
content="klar, a Bauhaus-inspired Todo App built with React, Zustand for global state, Tailwind CSS, and featuring theming, persistence, accessibility, drag-and-drop, and filtering."
/>
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Todo</title>
<link rel="stylesheet" href="https://use.typekit.net/owv4eil.css">
<title>klar</title>
</head>
<body>
<div id="root"></div>
Expand Down
9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,25 @@
"preview": "vite preview"
},
"dependencies": {
"@hello-pangea/dnd": "^18.0.1",
"@tailwindcss/vite": "^4.1.7",
"dayjs": "^1.11.13",
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"zustand": "^5.0.5"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
"@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"
}
}
1 change: 1 addition & 0 deletions public/moon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions public/sun.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
92 changes: 89 additions & 3 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,91 @@
export const App = () => {
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'
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'

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({
status: 'all',
createdAfter: '',
tagFilter: '',
projectFilter:'all',
})

// apply dark/light theme
useEffect(() => {
document.documentElement.setAttribute('data-theme', theme)
}, [theme])

// helper to parse comma‐lists
const parseTags = (str) =>
str.split(',').map((t) => t.trim()).filter((t) => t !== '')

// derive the visible todos based on filters
const visibleTodos = allTodos
.filter((todo) => {
// project filter
if (
filters.projectFilter !== 'all' &&
todo.project !== filters.projectFilter
)
return false

// 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 = dayjs(todo.createdAt)
const after = dayjs(filters.createdAfter)
if (created.isBefore(after, 'day')) return false
}

// 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 (
<h1>React Boilerplate</h1>
<div className="min-h-screen bg-bauhaus text-bauhaus-text font-neue-kabel">
<Header />

<main className="p-4 sm:p-6 md:p-8 lg:p-12 max-w-full sm:max-w-md md:max-w-xl lg:max-w-2xl mx-auto space-y-6">
<TodoForm />

<FilterBar
projects={projects}
onFilterChange={setFilters}
/>

{visibleTodos.length > 0 ? (
<TodoList todos={visibleTodos} />
) : (
<EmptyState />
)}
</main>

<Footer />
</div>
)
}
}
31 changes: 31 additions & 0 deletions src/components/EmptyState.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from 'react'

export default function EmptyState() { // EmptyState component
return (
<div className="flex flex-col items-center justify-center py-12 sm:py-16 md:py-20 text-muted">
<svg
xmlns="http://www.w3.org/2000/svg"
className="w-16 h-16 mb-4 text-muted opacity-70"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 0h6"
/>
</svg>

<h2 className="text-2xl font-semibold mb-2 text-bauhaus-text">
No tasks yet!
</h2>
<p className="mb-4 text-muted">
Looks like you don’t have any tasks. Let’s add one.
</p>
{/* <span className="text-5xl">📋</span> */}
</div>
)
}
102 changes: 102 additions & 0 deletions src/components/FilterBar.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import React, { useState } from 'react'

export default function FilterBar({ projects, onFilterChange }) {
const [status, setStatus] = useState('all')
const [createdAfter, setCreatedAfter] = useState('')
const [tagFilter, setTagFilter] = useState('')
const [projectFilter, setProjectFilter] = useState('all')

const handleStatusChange = (e) => {
const newStatus = e.target.value
setStatus(newStatus)
onFilterChange({
status: newStatus,
createdAfter,
tagFilter,
projectFilter,
})
}

const handleDateChange = (e) => {
const newDate = e.target.value
setCreatedAfter(newDate)
onFilterChange({
status,
createdAfter: newDate,
tagFilter,
projectFilter,
})
}

const handleTagChange = (e) => {
const newTagFilter = e.target.value
setTagFilter(newTagFilter)
onFilterChange({
status,
createdAfter,
tagFilter: newTagFilter,
projectFilter,
})
}

const handleProjectChange = (e) => {
const newProject = e.target.value
setProjectFilter(newProject)
onFilterChange({
status,
createdAfter,
tagFilter,
projectFilter: newProject,
})
}

return (
<div className="flex flex-col sm:flex-row items-center gap-2 mb-4">
{/* Project filter */}
<select
value={projectFilter}
onChange={handleProjectChange}
className="p-2 border border-bauhaus rounded"
aria-label="Filter by project"
>
<option value="all">All Projects</option>
{projects.map((proj) => (
<option key={proj} value={proj}>
{proj}
</option>
))}
</select>

{/* Status filter */}
<select
value={status}
onChange={handleStatusChange}
className="p-2 border border-bauhaus rounded"
aria-label="Filter by status"
>
<option value="all">All tasks</option>
<option value="active">Active</option>
<option value="completed">Completed</option>
</select>

{/* Created-after filter */}
<input
type="date"
value={createdAfter}
onChange={handleDateChange}
className="p-2 border border-bauhaus rounded"
aria-label="Show tasks created on or after"
/>

{/* Tag filter */}
<input
type="text"
value={tagFilter}
onChange={handleTagChange}
placeholder="Filter by tags"
className="p-2 border border-bauhaus rounded"
aria-label="Filter by tags"
/>
</div>
)
}
15 changes: 15 additions & 0 deletions src/components/Footer.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react'
import Stats from './Stats'

export default function Footer() {
return (
<footer
className="
max-w-full sm:max-w-md md:max-w-xl lg:max-w-2xl
mx-auto
px-4 sm:px-6 md:px-8
">
<Stats />
</footer>
)
}
17 changes: 17 additions & 0 deletions src/components/Header.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react';
import ThemeToggle from './ThemeToggle'

export default function Header() {
return (
<header
className="
flex items-center justify-between
px-4 py-3 /* mobile */
sm:px-6 sm:py-4 /* tablet */
md:px-8 md:py-6 /* desktop */
">
<h1 className="text-7xl font-bold">klar</h1>
<ThemeToggle />
</header>
);
}
Loading