diff --git a/.gitignore b/.gitignore
index b02a1ff..9564343 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,3 +23,5 @@ package-lock.json
*.njsproj
*.sln
*.sw?
+
+.env
\ No newline at end of file
diff --git a/README.md b/README.md
index d1c68b5..330e628 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,3 @@
-# Todo
\ No newline at end of file
+# Todo
+
+Todo App is a mobile-first task manager built with React, Styled Components and Zustand for global state. It lets you quickly add, toggle and remove tasks in a clean, responsive UI—no prop-drilling required.
diff --git a/package.json b/package.json
index caf6289..298ed55 100644
--- a/package.json
+++ b/package.json
@@ -10,8 +10,15 @@
"preview": "vite preview"
},
"dependencies": {
+ "@emotion/react": "^11.14.0",
+ "@emotion/styled": "^11.14.0",
+ "@mui/material": "^7.1.0",
+ "date-fns": "^4.1.0",
"react": "^19.0.0",
- "react-dom": "^19.0.0"
+ "react-dom": "^19.0.0",
+ "styled-components": "^6.1.18",
+ "uuid": "^11.1.0",
+ "zustand": "^5.0.4"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
diff --git a/pull_request_template.md b/pull_request_template.md
index 154c92e..332eb68 100644
--- a/pull_request_template.md
+++ b/pull_request_template.md
@@ -1 +1 @@
-Please include your Netlify link here.
\ No newline at end of file
+Netlify link here [zustandtodoapp.netlify.app]
diff --git a/src/App.jsx b/src/App.jsx
index 5427540..fde0680 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,5 +1,29 @@
+import Header from './components/Header';
+import TaskForm from './components/TaskForm';
+import TaskView from './components/TaskView';
+import styled, { ThemeProvider } from 'styled-components';
+import useTaskStore from './store/useTaskStore';
+import { GlobalStyle, lightTheme, darkTheme } from '../styles/globalStyles';
+
+const AppContainer = styled.div`
+ background: ${({ theme }) => theme.background};
+ color: ${({ theme }) => theme.text};
+ min-height: 100vh;
+ padding: 1rem;
+`;
+
export const App = () => {
+ const themeMode = useTaskStore((s) => s.themeMode);
+ const theme = themeMode === 'light' ? lightTheme : darkTheme;
+
return (
-
React Boilerplate
- )
-}
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/DateInputWithPicker.jsx b/src/components/DateInputWithPicker.jsx
new file mode 100644
index 0000000..62ad257
--- /dev/null
+++ b/src/components/DateInputWithPicker.jsx
@@ -0,0 +1,81 @@
+import React, { useState } from 'react';
+
+export default function DateInputWithPicker() {
+ const [textValue, setTextValue] = useState('');
+ const [dateValue, setDateValue] = useState('');
+ const [showCalendar, setShowCalendar] = useState(false);
+
+ const handleTextChange = (e) => {
+ setTextValue(e.target.value);
+ };
+
+ const handleDateChange = (e) => {
+ const val = e.target.value; // "2025-05-22"
+ setDateValue(val);
+ setTextValue(val);
+ };
+
+ const toggleCalendar = () => {
+ setShowCalendar((prev) => !prev);
+ };
+
+ const handleSubmit = (e) => {
+ e.preventDefault();
+ console.log('Datum skickas vidare:', textValue);
+ };
+
+ return (
+
+ );
+}
diff --git a/src/components/EmtyState.jsx b/src/components/EmtyState.jsx
new file mode 100644
index 0000000..e3ddb7c
--- /dev/null
+++ b/src/components/EmtyState.jsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import styled from 'styled-components';
+
+const Message = styled.p`
+ text-align: center;
+ color: #666;
+ margin-top: 2rem;
+`;
+
+export default function EmptyState({ text }) {
+ return {text};
+}
diff --git a/src/components/FilterBar.jsx b/src/components/FilterBar.jsx
new file mode 100644
index 0000000..ff71d59
--- /dev/null
+++ b/src/components/FilterBar.jsx
@@ -0,0 +1,31 @@
+import styled from 'styled-components';
+
+export const Bar = styled.nav``;
+
+export const FilterButton = styled.button`
+ background: ${({ $isActive, theme }) =>
+ $isActive ? theme.primary : theme.surface};
+ color: ${({ $isActive, theme }) => ($isActive ? '#fff' : theme.text)};
+`;
+
+export default function FilterBar({ filterMode, onChange }) {
+ const modes = [
+ { key: 'today', label: 'Today' },
+ { key: 'weekly', label: 'Weekly' },
+ { key: 'all', label: 'All Tasks' },
+ ];
+
+ return (
+
+ {modes.map(({ key, label }) => (
+ onChange(key)}
+ >
+ {label}
+
+ ))}
+
+ );
+}
diff --git a/src/components/Header.jsx b/src/components/Header.jsx
new file mode 100644
index 0000000..e5ccc47
--- /dev/null
+++ b/src/components/Header.jsx
@@ -0,0 +1,43 @@
+import React from 'react';
+import useTaskStore from '../store/useTaskStore';
+import styled from 'styled-components';
+
+const HeaderContainer = styled.header`
+ background: ${({ theme }) => theme.surface};
+ color: ${({ theme }) => theme.text};
+`;
+
+const ToggleButton = styled.button`
+ background: none;
+ border: none;
+ color: ${({ theme }) => theme.primary};
+ cursor: pointer;
+`;
+
+export default function Header() {
+ const totalCount = useTaskStore((state) => state.selectors.getTotalCount());
+ const clearCompleted = useTaskStore((state) => state.actions.clearCompleted);
+ const notCompleted = useTaskStore((state) =>
+ state.selectors.getRemainingCount()
+ );
+
+ const toggleTheme = useTaskStore((state) => state.actions.toggleTheme);
+ const themeMode = useTaskStore((state) => state.themeMode);
+
+ return (
+
+ My Zustand Todo App
+ Totalt: {totalCount} uppgifter
+ Remaining: {notCompleted} uppgiofter
+
+ {
+ console.log('Toggle clicked!'); // ← ska synas i konsolen
+ toggleTheme();
+ }}
+ >
+ {themeMode === 'light' ? 'Dark mode' : 'Light mode'}
+
+
+ );
+}
diff --git a/src/components/TaskForm.jsx b/src/components/TaskForm.jsx
new file mode 100644
index 0000000..d49a378
--- /dev/null
+++ b/src/components/TaskForm.jsx
@@ -0,0 +1,90 @@
+import useTaskStore from '../store/useTaskStore';
+import { useState } from 'react';
+
+export default function TaskForm() {
+ const [inputText, setInputText] = useState('');
+ const [dueDate, setDueDate] = useState('');
+ const addTask = useTaskStore((state) => state.actions.addTask);
+
+ const handleChange = (event) => {
+ setInputText(event.target.value);
+ };
+
+ const handleSubmit = (event) => {
+ event.preventDefault();
+
+ const text = inputText.trim();
+
+ // 1) Ingen text alls?
+ if (!text) {
+ alert('Please enter a task');
+ return;
+ }
+
+ // 2) För kort?
+ if (text.length < 3) {
+ alert('Task is too short (min 3 characters)');
+ return;
+ }
+
+ // 3) För långt?
+ if (text.length > 100) {
+ alert('Task is too long (max 100 characters)');
+ return;
+ }
+
+ addTask(text, dueDate);
+ setInputText('');
+ setDueDate('');
+ };
+
+ /*
+ User skriver → onChange → handleChange → setInputText → React-state uppdateras → input.value ändras
+ */
+
+ return (
+
+ );
+}
diff --git a/src/components/TaskItem.jsx b/src/components/TaskItem.jsx
new file mode 100644
index 0000000..40b9d67
--- /dev/null
+++ b/src/components/TaskItem.jsx
@@ -0,0 +1,33 @@
+import useTaskStore from '../store/useTaskStore';
+
+export default function TaskItem({ task }) {
+ const toggleTask = useTaskStore((state) => state.actions.toggleTask);
+ const removeTask = useTaskStore((state) => state.actions.removeTask);
+
+ const handleToggle = () => {
+ toggleTask(task.id);
+ };
+
+ const handleRemove = () => {
+ removeTask(task.id);
+ };
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/components/TaskList.jsx b/src/components/TaskList.jsx
new file mode 100644
index 0000000..0c4430a
--- /dev/null
+++ b/src/components/TaskList.jsx
@@ -0,0 +1,276 @@
+import { useMemo } from 'react';
+import TaskItem from './TaskItem';
+import EmptyState from './EmtyState';
+import { isToday, isThisWeek } from 'date-fns';
+import useTaskStore from '../store/useTaskStore';
+import LinearProgress from '@mui/material/LinearProgress';
+import { Box, Typography } from '@mui/material';
+import styled from 'styled-components';
+
+const StyledWrapper = styled.div`
+ #checklist {
+ --background: #303952;
+ --text: #5d6474;
+ --check: #cc29f0;
+ --disabled: #d3c8de;
+ --width: 100px;
+ --height: 180px;
+ --border-radius: 10px;
+ background: var(--background);
+ width: var(--width);
+ height: var(--height);
+ border-radius: var(--border-radius);
+ position: relative;
+ box-shadow: 0 10px 30px rgba(65, 72, 86, 0.05);
+ padding: 30px 85px;
+ display: grid;
+ grid-template-columns: 30px auto;
+ align-items: center;
+ justify-content: center;
+ }
+
+ #checklist label {
+ color: var(--text);
+ position: relative;
+ cursor: pointer;
+ display: grid;
+ align-items: center;
+ width: fit-content;
+ transition: color 0.3s ease;
+ margin-right: 20px;
+ }
+
+ #checklist label::before,
+ #checklist label::after {
+ content: '';
+ position: absolute;
+ }
+
+ #checklist label::before {
+ height: 2px;
+ width: 8px;
+ left: -27px;
+ background: var(--check);
+ border-radius: 2px;
+ transition: background 0.3s ease;
+ }
+
+ #checklist label:after {
+ height: 4px;
+ width: 4px;
+ top: 8px;
+ left: -25px;
+ border-radius: 50%;
+ }
+
+ #checklist input[type='checkbox'] {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ position: relative;
+ height: 15px;
+ width: 15px;
+ outline: none;
+ border: 0;
+ margin: 0 15px 0 0;
+ cursor: pointer;
+ background: var(--background);
+ display: grid;
+ align-items: center;
+ margin-right: 20px;
+ }
+
+ #checklist input[type='checkbox']::before,
+ #checklist input[type='checkbox']::after {
+ content: '';
+ position: absolute;
+ height: 2px;
+ top: auto;
+ background: var(--check);
+ border-radius: 2px;
+ }
+
+ #checklist input[type='checkbox']::before {
+ width: 0px;
+ right: 60%;
+ transform-origin: right bottom;
+ }
+
+ #checklist input[type='checkbox']::after {
+ width: 0px;
+ left: 40%;
+ transform-origin: left bottom;
+ }
+
+ #checklist input[type='checkbox']:checked::before {
+ animation: check-01 0.4s ease forwards;
+ }
+
+ #checklist input[type='checkbox']:checked::after {
+ animation: check-02 0.4s ease forwards;
+ }
+
+ #checklist input[type='checkbox']:checked + label {
+ color: var(--disabled);
+ animation: move 0.3s ease 0.1s forwards;
+ }
+
+ #checklist input[type='checkbox']:checked + label::before {
+ background: var(--disabled);
+ animation: slice 0.4s ease forwards;
+ }
+
+ #checklist input[type='checkbox']:checked + label::after {
+ animation: firework 0.5s ease forwards 0.1s;
+ }
+
+ @keyframes move {
+ 50% {
+ padding-left: 8px;
+ padding-right: 0px;
+ }
+
+ 100% {
+ padding-right: 4px;
+ }
+ }
+
+ @keyframes slice {
+ 60% {
+ width: 100%;
+ left: 4px;
+ }
+
+ 100% {
+ width: 100%;
+ left: -2px;
+ padding-left: 0;
+ }
+ }
+
+ @keyframes check-01 {
+ 0% {
+ width: 4px;
+ top: auto;
+ transform: rotate(0);
+ }
+
+ 50% {
+ width: 0px;
+ top: auto;
+ transform: rotate(0);
+ }
+
+ 51% {
+ width: 0px;
+ top: 8px;
+ transform: rotate(45deg);
+ }
+
+ 100% {
+ width: 5px;
+ top: 8px;
+ transform: rotate(45deg);
+ }
+ }
+
+ @keyframes check-02 {
+ 0% {
+ width: 4px;
+ top: auto;
+ transform: rotate(0);
+ }
+
+ 50% {
+ width: 0px;
+ top: auto;
+ transform: rotate(0);
+ }
+
+ 51% {
+ width: 0px;
+ top: 8px;
+ transform: rotate(-45deg);
+ }
+
+ 100% {
+ width: 10px;
+ top: 8px;
+ transform: rotate(-45deg);
+ }
+ }
+
+ @keyframes firework {
+ 0% {
+ opacity: 1;
+ box-shadow: 0 0 0 -2px #4f29f0, 0 0 0 -2px #4f29f0, 0 0 0 -2px #4f29f0,
+ 0 0 0 -2px #4f29f0, 0 0 0 -2px #4f29f0, 0 0 0 -2px #4f29f0;
+ }
+
+ 30% {
+ opacity: 1;
+ }
+
+ 100% {
+ opacity: 0;
+ box-shadow: 0 -15px 0 0px #4f29f0, 14px -8px 0 0px #4f29f0,
+ 14px 8px 0 0px #4f29f0, 0 15px 0 0px #4f29f0, -14px 8px 0 0px #4f29f0,
+ -14px -8px 0 0px #4f29f0;
+ }
+ }
+`;
+
+export default function TaskList({ filterMode }) {
+ // 1) Prenumerera på råa tasks
+ const allTasks = useTaskStore((state) => state.tasks);
+
+ // 2) Derivera filteredTasks med useMemo (filteredTasks är en ny array som är en avledning (derivation) av allTasks utifrån något villkor (t.ex. “endast dagens tasks”).
+ const filteredTasks = useMemo(() => {
+ switch (filterMode) {
+ case 'all':
+ return allTasks;
+ case 'today':
+ return allTasks.filter((task) => task.dueDate && isToday(task.dueDate));
+ case 'weekly':
+ return allTasks.filter(
+ (task) =>
+ task.dueDate && isThisWeek(task.dueDate, { weekStartsOn: 1 })
+ );
+ case 'remaining':
+ return allTasks.filter((task) => !task.done);
+ case 'completed':
+ return allTasks.filter((task) => task.done);
+ default: // "all"
+ return allTasks;
+ }
+ }, [allTasks, filterMode]);
+
+ const total = filteredTasks.length;
+ const completed = filteredTasks.filter((t) => t.done).length;
+ const percent = total ? Math.round((completed / total) * 100) : 0;
+
+ // 3) Tom-lista-hantering
+ if (filteredTasks.length === 0) {
+ return ;
+ }
+
+ return (
+ <>
+
+
+
+
+
+ {percent}%
+
+
+
+
+ {filteredTasks.map((task) => (
+
+ ))}
+
+ >
+ );
+}
+
+// ul, li, input > label
diff --git a/src/components/TaskView.jsx b/src/components/TaskView.jsx
new file mode 100644
index 0000000..d120b64
--- /dev/null
+++ b/src/components/TaskView.jsx
@@ -0,0 +1,17 @@
+import { useState } from 'react';
+import FilterBar from './FilterBar';
+import TaskList from './TaskList';
+import styled from 'styled-components';
+
+export const TaskViewContainer = styled.div``;
+
+export default function TaskView() {
+ const [filter, setFilter] = useState('all');
+
+ return (
+
+
+
+
+ );
+}
diff --git a/src/main.jsx b/src/main.jsx
index 1b8ffe9..779e752 100644
--- a/src/main.jsx
+++ b/src/main.jsx
@@ -1,12 +1,13 @@
-import React from 'react'
-import ReactDOM from 'react-dom/client'
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import { App } from './App.jsx';
+import './index.css';
+import useTaskStore from './store/useTaskStore'; // ← importera din store-hook
-import { App } from './App.jsx'
-
-import './index.css'
+window.taskStore = useTaskStore;
ReactDOM.createRoot(document.getElementById('root')).render(
-)
+);
diff --git a/src/store/useTaskStore.js b/src/store/useTaskStore.js
new file mode 100644
index 0000000..eaa35eb
--- /dev/null
+++ b/src/store/useTaskStore.js
@@ -0,0 +1,75 @@
+import { create } from 'zustand';
+import { devtools, persist } from 'zustand/middleware';
+import { v4 as uuidv4 } from 'uuid';
+
+const useTaskStore = create(
+ devtools(
+ persist(
+ (set, get) => ({
+ //STATE
+ tasks: [],
+ themeMode: 'light',
+
+ //ACTIONS
+ actions: {
+ addTask: (text, dueDate) => {
+ const trimmed = text.trim();
+ if (trimmed === '') return;
+
+ const dueDataObject = dueDate ? new Date(dueDate) : null;
+
+ const newTask = {
+ id: uuidv4(),
+ text: trimmed,
+ done: false,
+ createdAt: new Date().toISOString(),
+ dueDate: dueDataObject,
+ };
+ set((state) => ({
+ tasks: [newTask, ...state.tasks],
+ }));
+ },
+
+ toggleTask: (id) =>
+ set((state) => ({
+ tasks: state.tasks.map((task) =>
+ task.id === id ? { ...task, done: !task.done } : task
+ ),
+ })),
+
+ removeTask: (id) =>
+ set((state) => ({
+ tasks: state.tasks.filter((task) => task.id !== id),
+ })),
+
+ clearCompleted: () =>
+ set((state) => ({
+ tasks: state.tasks.filter((task) => !task.done),
+ })),
+
+ toggleTheme: () =>
+ set((state) => ({
+ themeMode: state.themeMode === 'light' ? 'dark' : 'light',
+ })),
+ },
+
+ //SELECTORS FÖR RÄKNARE
+ selectors: {
+ getTotalCount: () => get().tasks.length,
+ getRemainingCount: () => get().tasks.filter((t) => !t.done).length,
+ getCompletedCount: () => get().tasks.filter((t) => t.done).length,
+ },
+ }),
+ {
+ name: 'todo-storage', // nyckel i localStorage
+ getStorage: () => localStorage, // default, men tydligt
+ partialize: (state) => ({
+ tasks: state.tasks, // vi sparar bara tasks-arrayen
+ }),
+ }
+ ),
+ { name: 'task-store' }
+ )
+);
+
+export default useTaskStore;
diff --git a/styles/globalStyles.js b/styles/globalStyles.js
new file mode 100644
index 0000000..73bfca1
--- /dev/null
+++ b/styles/globalStyles.js
@@ -0,0 +1,31 @@
+import { createGlobalStyle } from 'styled-components';
+
+export const lightTheme = {
+ background: '#ffffff',
+ text: '#333333',
+ primary: '#6200ee',
+ surface: '#f5f5f5',
+};
+
+export const darkTheme = {
+ background: '#121212',
+ text: '#e0e0e0',
+ primary: '#bb86fc',
+ surface: '#1e1e1e',
+};
+
+export const GlobalStyle = createGlobalStyle`
+ * {
+ box-sizing: border-box;
+ }
+ body {
+ margin: 0;
+ padding: 0;
+ background: ${({ theme }) => theme.background};
+ color: ${({ theme }) => theme.text};
+ font-family: sans-serif;
+ }
+ button {
+ font-family: inherit;
+ }
+`;