Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 132 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,132 @@
# Todo
# 🦊 FocusDen

> _Let your hands create what your eyes fear to imagine._

A calm, minimalist productivity app built with **React**, **Zustand**, and **styled-components**.
FocusDen helps you stay organized and present — without noise or clutter.

🌐 **Live demo:** [https://focusden.netlify.app](https://focusden.netlify.app)

---

## 🖼️ Preview

![FocusDen app screenshot](./src/assets/focusden-preview.jpg)

---

## ✨ Features

- ✅ Add, complete, and delete tasks
- ✅ Filter by **All / Active / Completed**
- ✅ Optional **due date** and automatic “Overdue” indicator
- ✅ Character counter on new tasks
- ✅ Persistent data with `localStorage`
- ✅ Light & dark mode toggle
- ✅ Clean, responsive layout (320px–1600px)
- ✅ 95+ Lighthouse accessibility score
- 🦊 Minimalist, focus-friendly design

---

## 🧠 Tech Stack

| Technology | Purpose |
|-------------|----------|
| ⚛️ React (Vite) | Core UI framework |
| 🪣 Zustand | Global state management (no prop drilling) |
| 💅 styled-components | Component-scoped styling |
| 🕒 date-fns | Date formatting & overdue logic |
| 🌗 localStorage | Persistent task storage |
| 🧪 ESLint + Vite | Clean, fast developer setup |

---

## 🗂️ Folder Structure

src/
├─ assets/
│ ├─ favicon.jpg
│ └─ focusden-preview.png
├─ components/
│ ├─ TodoForm.jsx
│ ├─ TodoList.jsx
│ ├─ TodoItem.jsx
│ ├─ EmptyState.jsx
│ └─ Footer.jsx
├─ store/
│ ├─ useTodoStore.js
│ └─ useThemeStore.js
├─ styles/
│ ├─ GlobalStyles.js
│ └─ media.js
├─ App.jsx
└─ main.jsx

---

## 🪄 Getting Started

1️⃣ Install dependencies
→ Run: `npm install`

2️⃣ Start the app locally
→ Run: `npm run dev`

3️⃣ Build for production
→ Run: `npm run build`

Then open the generated `/dist` folder in your browser.

---

## 🚀 Stretch Goals

🕓 Filter tasks by **due date** or **overdue**
🏷️ Add **tags / categories**
🔔 Add **reminders or notifications**
☁️ Sync tasks with a backend or cloud API

---

## 📱 Responsiveness

| Device | Example width | Behavior |
|---------|----------------|-----------|
| 📱 Mobile | up to 480px | Stacked layout, larger tap areas |
| 💻 Tablet | ≥ 768px | Balanced grid, adaptive text |
| 🖥️ Desktop | ≥ 1024px | Fixed-width centered container |
| 🖥️ XL screens | ≥ 1440px | Fluid, maximum readability |

---

## ♿ Accessibility

✔ Visible focus states and proper labels
✔ `aria-live` announcements for task counts
✔ Sufficient color contrast (WCAG AA)
✔ Keyboard-friendly navigation
✔ Semantic HTML structure

---

## 👩‍💻 Author

Made with 🍵, 🎧, curiosity, and a generous dose of AI magic by Ulrika Einebrant.
Frontend developer passionate about clean design, accessibility, and calm user experiences.
“Let your hands create what your eyes fear to imagine.”

---

## 🪶 License

This project is open source and available under the **MIT License**.

---

## 💫 Connect

🔗 **Live app:** [focusden.netlify.app](https://focusden.netlify.app)
💻 **GitHub repo:** [github.com/yourusername/focusden](https://github.com/yourusername/focusden)
🧭 **Portfolio:** [ulrikasportfolio.netlify.app](https://ulrikasportfolio.netlify.app/)
💼 **LinkedIn:** [ulrika-einebrant](https://www.linkedin.com/in/ulrika-einebrant/)
39 changes: 25 additions & 14 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Todo</title>
</head>
<body>
<div id="root"></div>
<script
type="module"
src="./src/main.jsx">
</script>
</body>
</html>

<head>
<meta charset="UTF-8" />
<link
rel="icon"
type="image/svg+xml"
href="./favicon.jpg"
/>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>FocusDen</title>
</head>

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

</html>
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@
"preview": "vite preview"
},
"dependencies": {
"date-fns": "^4.1.0",
"nanoid": "^5.1.6",
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"styled-components": "^6.1.19",
"zustand": "^5.0.8"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
Expand Down
Binary file added public/favicon.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 0 additions & 1 deletion public/vite.svg

This file was deleted.

131 changes: 129 additions & 2 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,132 @@
export const App = () => {
// src/App.jsx
import { useState } from 'react'
import styled from 'styled-components'
import GlobalStyles from './styles/GlobalStyles.js'
import { useTodoStore } from './store/useTodoStore.js'
import { useThemeStore } from './store/useThemeStore.js' // ⬅️ NEW
import TodoForm from './components/TodoForm.jsx'
import TodoList from './components/TodoList.jsx'
import EmptyState from './components/EmptyState.jsx'
import Footer from './components/Footer.jsx'
import foxIcon from './assets/favicon.jpg'

const Shell = styled.main`
max-width: 720px; margin: 0 auto; padding: 24px; min-height: 100dvh; display: grid; align-content: start; gap: 18px;
`
const H1 = styled.h1`
margin: 8px 0 4px;
font-size: clamp(28px, 3vw, 40px);
display: flex;
align-items: center;
gap: 10px;

img {
width: 40px;
height: 40px;
border-radius: 20%;
user-select: none;
}
`

const Card = styled.section`
display: grid; gap: 12px;
`

const TopBar = styled.header`
display: flex; align-items: center; justify-content: space-between; gap: 12px;
`

const ThemeBtn = styled.button`
border: 1px solid #2a3650;
background: transparent;
color: var(--text);
padding: 8px 12px;
border-radius: 12px;
font-weight: 600;
`

const FilterBar = styled.div`
display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
`
const FilterButton = styled.button`
border: 1px solid #2a3650;
background: ${({ $active }) => ($active ? 'var(--primary)' : 'transparent')};
color: ${({ $active }) => ($active ? '#fff' : 'var(--text)')};
padding: 8px 12px;
border-radius: 10px;
font-weight: 600;
`

export default function App() {
const tasks = useTodoStore(s => s.tasks)

// THEME: light | dark | auto
const { theme, setTheme } = useThemeStore()
function toggleTheme() {
setTheme(theme === 'dark' ? 'light' : 'dark')
}

// FILTERS
const [filter, setFilter] = useState('all')
const total = tasks.length
const remaining = tasks.filter(t => !t.completed).length
const filteredTasks = tasks.filter(t =>
filter === 'all' ? true : filter === 'active' ? !t.completed : t.completed
)

return (
<h1>React Boilerplate</h1>
<>
<GlobalStyles />
<Shell>
<TopBar>
<div>
<H1>
FocusDen
<img src={foxIcon} alt="Fox logo" />
</H1>

<p style={{ color: 'var(--muted)', margin: 0, fontWeight: 600 }}>
Let your hands create what your eyes fear to imagine.
</p>
</div>
<ThemeBtn onClick={toggleTheme} title="Toggle theme">
{theme === 'dark' ? '🌙 Dark' : '☀️ Light'}
</ThemeBtn>

</TopBar>

<TodoForm />

{/* Filter controls */}
<FilterBar role="group" aria-label="Filter tasks">
<FilterButton
onClick={() => setFilter('all')}
aria-pressed={filter === 'all'}
$active={filter === 'all'}
>All</FilterButton>

<FilterButton
onClick={() => setFilter('active')}
aria-pressed={filter === 'active'}
$active={filter === 'active'}
>Active</FilterButton>

<FilterButton
onClick={() => setFilter('completed')}
aria-pressed={filter === 'completed'}
$active={filter === 'completed'}
>Completed</FilterButton>

<span style={{ color: 'var(--muted)', marginLeft: 8 }}>
Showing <strong>{filteredTasks.length}</strong> of <strong>{total}</strong>
</span>
</FilterBar>

<Card>
{total === 0 ? <EmptyState /> : <TodoList tasks={filteredTasks} />}
<Footer total={total} remaining={remaining} />
</Card>
</Shell>
</>
)
}
Binary file added src/assets/favicon.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/focusden-preview.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 28 additions & 0 deletions src/components/EmptyState.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import styled from 'styled-components'
import { media } from '../styles/media.js'

const Wrap = styled.div`
text-align: center;
color: var(--muted);
border: 2px dashed #2a3650;
padding: 32px;
border-radius: 16px;

${media.mobile(`
padding: 20px;
border-radius: 12px;
font-size: 0.95rem;
`)}

${media.large(`
padding: 40px;
`)}
`

export default function EmptyState() {
return (
<Wrap role="status" aria-live="polite">
<p>Nothing here yet. Add your first task above 👆</p>
</Wrap>
)
}
Loading