diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..95cdb49
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "to-do-list-app"]
+ path = to-do-list-app
+ url = https://github.com/phenomenalCode/to-do-list-app.git
diff --git a/README.md b/README.md
index d1c68b5..4d8a7bb 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,3 @@
-# Todo
\ No newline at end of file
+# Todo
+
+netlify: https://darius-to-do-app.netlify.app/
diff --git a/index.html b/index.html
index f7ac4e4..705d22b 100644
--- a/index.html
+++ b/index.html
@@ -2,6 +2,10 @@
+
Todo
diff --git a/package.json b/package.json
index caf6289..72ad3a1 100644
--- a/package.json
+++ b/package.json
@@ -10,8 +10,16 @@
"preview": "vite preview"
},
"dependencies": {
+ "@date-io/date-fns": "^3.2.1",
+ "@emotion/react": "^11.14.0",
+ "@emotion/styled": "^11.14.0",
+ "@mui/lab": "^7.0.0-beta.13",
+ "@mui/material": "^7.1.0",
+ "@mui/x-date-pickers": "^8.5.0",
+ "date-fns": "^4.1.0",
"react": "^19.0.0",
- "react-dom": "^19.0.0"
+ "react-dom": "^19.0.0",
+ "zustand": "^5.0.5"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
@@ -22,6 +30,6 @@
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^15.15.0",
- "vite": "^6.2.0"
+ "vite": "^6.3.5"
}
}
diff --git a/public/photos/7247856.webp b/public/photos/7247856.webp
new file mode 100644
index 0000000..0f41ea1
Binary files /dev/null and b/public/photos/7247856.webp differ
diff --git a/public/photos/stars-night-galaxy-4k-3840x2160.webp b/public/photos/stars-night-galaxy-4k-3840x2160.webp
new file mode 100644
index 0000000..6c1b3f8
Binary files /dev/null and b/public/photos/stars-night-galaxy-4k-3840x2160.webp differ
diff --git a/src/App.jsx b/src/App.jsx
index 5427540..555a082 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,5 +1,148 @@
+import React, { useState, useMemo, useCallback } from 'react';
+import { useTaskStore } from './store/useTaskStore';
+import { Header } from './Header';
+import { SubmitTask } from './SubmitTask';
+import { DisplayTasks } from './DisplayTasks';
+import { Footer } from './Footer';
+import './index.css';
+import {
+ Box,
+ Button,
+ Typography,
+ Paper,
+ Container,
+ CssBaseline,
+ createTheme,
+ ThemeProvider,
+} from '@mui/material';
+
+// // Optionally lazy load heavy components:
+// const Header = React.lazy(() => import('./Header'));
+// const SubmitTask = React.lazy(() => import('./SubmitTask'));
+// const DisplayTasks = React.lazy(() => import('./DisplayTasks'));
+// const Footer = React.lazy(() => import('./Footer'));
+
export const App = () => {
+ const [darkMode, setDarkMode] = useState(false);
+ const tasks = useTaskStore((s) => s.tasks);
+
+ // Memoize theme to prevent recreation on every render
+ const theme = useMemo(
+ () =>
+ createTheme({
+ palette: {
+ mode: darkMode ? 'dark' : 'light',
+ primary: {
+ main: darkMode ? '#9c27b0' : '#1976d2',
+ contrastText: '#fff',
+ },
+ background: {
+ default: darkMode ? '#000' : '#fff',
+ paper: darkMode ? '#1a1a1a' : '#f5f5f5',
+ },
+ },
+ }),
+ [darkMode]
+ );
+
+ const toggleDarkMode = useCallback(() => {
+ setDarkMode((prev) => !prev);
+ }, []);
+
+ const completeAll = () => {
+ const { tasks, toggleTaskCompletion } = useTaskStore.getState();
+ tasks.forEach((t) => !t.completed && toggleTaskCompletion(t.id));
+ };
+
return (
- React Boilerplate
- )
-}
+
+
+
+
+
+
+
+
+
+
+
+ ({
+ order: { xs: 1, md: 2 },
+ flexGrow: 1,
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ gap: 3,
+ width: '100%',
+ maxWidth: { md: 900, lg: 1100 },
+ p: { xs: 2, sm: 3, md: 4 },
+ borderRadius: 2,
+ boxShadow: 4,
+ backgroundColor:
+ theme.palette.mode === 'dark'
+ ? 'rgba(33,33,33,0.85)'
+ : 'rgba(255,255,255,0.85)',
+ color: theme.palette.text.primary,
+ })}
+ >
+
+
+ To Do List
+
+ Total Tasks: {tasks.length}
+
+ Uncompleted Tasks: {tasks.filter((t) => !t.completed).length}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/DisplayTasks.jsx b/src/DisplayTasks.jsx
new file mode 100644
index 0000000..1f227e5
--- /dev/null
+++ b/src/DisplayTasks.jsx
@@ -0,0 +1,353 @@
+import React, { useState, useMemo } from 'react';
+import { useTaskStore } from './store/useTaskStore';
+import {
+ Box,
+ Typography,
+ Divider,
+ Chip,
+ Paper,
+ Button,
+ Checkbox,
+ Select,
+ MenuItem,
+ useTheme,
+ FormControl,
+ InputLabel,
+} from '@mui/material';
+
+export const DisplayTasks = () => {
+ const [show, setShow] = useState('all');
+ const [sortBy, setSortBy] = useState('created-desc');
+ const {
+ tasks: rawTasks,
+ projects,
+ toggleTaskCompletion,
+ deleteTask,
+ completeAllTasks,
+ deleteProject,
+ } = useTaskStore();
+
+ const theme = useTheme();
+ const isDarkMode = theme.palette.mode === 'dark';
+
+ /* ---------- Filtering ---------- */
+ const filteredTasks = useMemo(() => {
+ switch (show) {
+ case 'completed':
+ return rawTasks.filter((t) => t.completed);
+ case 'uncompleted':
+ return rawTasks.filter((t) => !t.completed);
+ default:
+ return rawTasks;
+ }
+ }, [rawTasks, show]);
+
+ /* ---------- Sorting ---------- */
+ const tasks = useMemo(() => {
+ return [...filteredTasks].sort((a, b) => {
+ const compareDates = (d1, d2) => (d1 || '').localeCompare(d2 || '');
+ switch (sortBy) {
+ case 'due-asc':
+ return compareDates(a.dueDate, b.dueDate);
+ case 'due-desc':
+ return compareDates(b.dueDate, a.dueDate);
+ case 'created-asc':
+ return a.createdAt.localeCompare(b.createdAt);
+ case 'created-desc':
+ return b.createdAt.localeCompare(a.createdAt);
+ case 'category':
+ return (a.category || '').localeCompare(b.category || '');
+ default:
+ return 0;
+ }
+ });
+ }, [filteredTasks, sortBy]);
+
+ const uncompletedCount = tasks.filter((t) => !t.completed).length;
+
+ /* ---------- Render: Empty State ---------- */
+ if (!tasks.length) {
+ return (
+
+ No tasks to display.
+
+ );
+ }
+
+ /* ---------- Helpers ---------- */
+ const renderDueDate = (task) => {
+ if (!task.dueDate) return null;
+ const due = new Date(task.dueDate);
+ const hoursLeft = (due.getTime() - Date.now()) / 36e5;
+ const dueSoon = hoursLeft > 0 && hoursLeft <= 24 && !task.completed;
+
+ return (
+
+ Due: {due.toLocaleDateString()} {due.toLocaleTimeString()}
+
+ );
+ };
+
+ /* ---------- Render ---------- */
+ return (
+
+ {/* Header */}
+
+
+ Tasks ({tasks.length}) • Uncompleted ({uncompletedCount})
+
+
+ {/* Filters */}
+
+
+ Filter
+
+
+
+
+ Sort by
+
+
+
+
+
+
+
+
+
+ {/* Task List */}
+
+ {tasks.map((task) => {
+ const project = projects.find((p) => p.id === task.projectId);
+ const overdue = task.dueDate && new Date(task.dueDate) < new Date() && !task.completed;
+ const statusLabel = overdue ? 'Overdue' : task.completed ? 'Completed' : 'Incomplete';
+ const statusKind = overdue ? 'error' : task.completed ? 'success' : 'warning';
+
+ return (
+ -
+
+ {/* Task Row */}
+
+
+ toggleTaskCompletion(task.id)}
+ inputProps={{ 'aria-labelledby': `task-label-${task.id}` }}
+ />
+
+ {task.task}
+
+
+
+
+
+
+ {/* Status */}
+
+
+ Status:
+
+
+ {/* Accessible Chip: */}
+ {statusKind === 'warning' ? (
+
+ ) : (
+
+ )}
+
+
+ {/* Category */}
+
+ Category: {task.category || 'None'}
+
+
+ {/* Project */}
+
+ Project:
+ {project ? `${project.name} (${project.completed ? 'Done' : 'Ongoing'})` : 'None'}
+ {project && (
+
+ )}
+
+
+ {/* Dates */}
+
+ Created: {new Date(task.createdAt).toLocaleString()}
+
+
+ {renderDueDate(task)}
+
+
+ );
+ })}
+
+
+ );
+};
+//Fixed warning Chip contrast for dark mode by using a darker orange background and white text to meet WCAG standards.
+
+// Added responsive text wrapping and ellipsis for Filter and Sort dropdowns to prevent overflow.
+
+// Updated header typography to wrap on small screens and prevent overflow (minWidth: 0, overflowWrap: 'break-word').
+
+// Highlighted due-soon tasks using theme’s error.main color instead of inline red.
+
+// Added accessibility improvements: aria-labelledby on checkboxes, role="region" and aria-label for task list.
+
+// Ensured layout is responsive and compatible with both dark and light modes.
\ No newline at end of file
diff --git a/src/Footer.jsx b/src/Footer.jsx
new file mode 100644
index 0000000..dc7adcd
--- /dev/null
+++ b/src/Footer.jsx
@@ -0,0 +1,35 @@
+import React from 'react';
+import { AppBar, Toolbar, Typography, useTheme } from '@mui/material';
+
+export const Footer = () => {
+ const theme = useTheme();
+ const isDark = theme.palette.mode === 'dark';
+ const currentYear = new Date().getFullYear();
+
+ return (
+
+
+
+ © {currentYear} Task Manager by Darius Olsson Carter — All rights reserved.
+
+
+
+ );
+};
diff --git a/src/Header.jsx b/src/Header.jsx
new file mode 100644
index 0000000..34cd1ff
--- /dev/null
+++ b/src/Header.jsx
@@ -0,0 +1,10 @@
+import React from 'react';
+import { AppBar, Toolbar, Typography } from '@mui/material';
+
+export const Header = () => (
+
+
+ Task Manager
+
+
+);
diff --git a/src/SubmitTask.jsx b/src/SubmitTask.jsx
new file mode 100644
index 0000000..9640731
--- /dev/null
+++ b/src/SubmitTask.jsx
@@ -0,0 +1,195 @@
+import React, { useState } from 'react';
+import { useTaskStore } from './store/useTaskStore';
+import {
+ Box,
+ Button,
+ TextField,
+ Select,
+ MenuItem,
+ FormControl,
+ InputLabel,
+ useTheme,
+ Stack,Typography
+} from '@mui/material';
+import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
+import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
+import { DatePicker } from '@mui/x-date-pickers/DatePicker';
+
+export const SubmitTask = () => {
+ const theme = useTheme();
+ const { addTask, projects, addProject } = useTaskStore();
+
+ // ---------------- Form State ----------------
+ const [input, setInput] = useState('');
+ const [category, setCategory] = useState('');
+ const [projectId, setProjectId] = useState('');
+ const [dueDate, setDueDate] = useState(null);
+ const [newProject, setNewProject] = useState('');
+
+ // ---------------- Handlers ----------------
+ const handleAddProject = () => {
+ const name = newProject.trim();
+ if (!name) return;
+ const id = Date.now();
+ addProject({ id, name, completed: false });
+ setProjectId(id);
+ setNewProject('');
+ };
+
+ const handleSubmit = (e) => {
+ e.preventDefault();
+ if (!input.trim()) {
+ alert('Please enter a task');
+ return;
+ }
+
+ addTask({
+ task: input.trim(),
+ category,
+ projectId: projectId || null,
+ dueDate: dueDate ? dueDate.toISOString().split('T')[0] : null,
+ });
+
+ // Clear form
+ setInput('');
+ setCategory('');
+ setProjectId('');
+ setDueDate(null);
+ };
+
+ // ---------------- UI ----------------
+ return (
+
+ {/* Promoted heading */}
+
+ To Do List
+
+
+
+
+);
+
+};
diff --git a/src/Tasks.jsx b/src/Tasks.jsx
new file mode 100644
index 0000000..ff759d4
--- /dev/null
+++ b/src/Tasks.jsx
@@ -0,0 +1,21 @@
+export const Tasks = () => {
+
+
+return (
+ <>
+ To Do List
+
+
+ - Build a To-Do List App
+ - Deploy the App
+
+ Total tasks: 3
+ >
+);
+
+
+
+
+
+
+}
\ No newline at end of file
diff --git a/src/index.css b/src/index.css
index f7c0aef..2b967a8 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1,3 +1,50 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
}
+.dark-background,
+.light-background {
+ background-size: 75%; /* Adjusts visual size */
+ background-position: center;
+ background-repeat: no-repeat;
+ background-attachment: fixed;
+ height: 100vh;
+}
+.dark-background {
+ background-image: url('/photos/stars-night-galaxy-4k-3840x2160.webp');
+}
+
+.light-background {
+ background-image: url('/photos/7247856.webp');
+}
+
+
+
+/* -------------- MOBILE 0-600px -------------- */
+@media (max-width: 600px) {
+ .display-tasks { padding: 1rem; }
+ .filter-actions { flex-direction: column; }
+}
+
+/* -------------- SMALL TABLET 601-900px -------------- */
+@media (min-width: 601px) and (max-width: 900px) {
+ .display-tasks { max-width: 90%; }
+ .filter-actions { flex-direction: row; }
+}
+
+/* -------------- MEDIUM 901-1200px -------------- */
+@media (min-width: 901px) and (max-width: 1200px) {
+ .display-tasks { max-width: 80%; }
+ .filter-actions { flex-direction: row; }
+}
+
+/* -------------- DESKTOP 1201-1600px -------------- */
+@media (min-width: 1201px) and (max-width: 1600px) {
+ .display-tasks { max-width: 700px; }
+ .filter-actions { flex-direction: row; }
+}
+
+/* -------------- LARGE DESKTOP ≥1601px -------------- */
+@media (min-width: 1601px) {
+ .display-tasks { max-width: 900px; }
+ .filter-actions { flex-direction: row; }
+}
\ No newline at end of file
diff --git a/src/store/useTaskStore.jsx b/src/store/useTaskStore.jsx
new file mode 100644
index 0000000..34c7730
--- /dev/null
+++ b/src/store/useTaskStore.jsx
@@ -0,0 +1,111 @@
+import { create } from 'zustand';
+
+
+const load = (key) => JSON.parse(localStorage.getItem(key)) || [];
+const save = (key,data) => localStorage.setItem(key, JSON.stringify(data));
+
+export const useTaskStore = create((set, get) => ({
+
+ /* ---------- STATE ---------- */
+ tasks : load('tasks'),
+ projects : load('projects'),
+
+ /* ---------- TASK ACTIONS ---------- */
+ addTask: (task) => {
+ const newTask = {
+ ...task,
+ id : Date.now(),
+ completed : false,
+ createdAt : new Date().toISOString(),
+ };
+ const tasks = [...get().tasks, newTask];
+ save('tasks', tasks);
+ set({ tasks });
+ get().updateProjectCompletion(newTask.projectId);
+ },
+
+ deleteTask: (id) => {
+ const tasks = get().tasks.filter(t => t.id !== id);
+ save('tasks', tasks);
+ set({ tasks });
+ },
+
+ toggleTaskCompletion: (id) => {
+ const tasks = get().tasks.map(t =>
+ t.id === id ? { ...t, completed: !t.completed } : t
+ );
+ save('tasks', tasks);
+ set({ tasks });
+
+ const task = tasks.find(t => t.id === id);
+ if (task?.projectId) get().updateProjectCompletion(task.projectId);
+ },
+
+ completeAllTasks: () => {
+ const tasks = get().tasks.map(t => ({ ...t, completed: true }));
+ save('tasks', tasks);
+ set({ tasks });
+
+ // refresh every linked project’s status
+ [...new Set(tasks.map(t => t.projectId).filter(Boolean))]
+ .forEach(pid => get().updateProjectCompletion(pid));
+ },
+
+ /* ---------- TASK QUERIES ---------- */
+ filterTasks: (status = 'all', afterDate = null) => {
+ let filtered = get().tasks;
+
+ if (status === 'completed') filtered = filtered.filter(t => t.completed);
+ if (status === 'uncompleted') filtered = filtered.filter(t => !t.completed);
+
+ if (afterDate) {
+ const after = new Date(afterDate);
+ filtered = filtered.filter(t => new Date(t.createdAt) > after);
+ }
+
+ return filtered;
+ },
+
+ /* ---------- PROJECT ACTIONS ---------- */
+ addProject: (project) => {
+ const projects = [...get().projects, project];
+ save('projects', projects);
+ set({ projects });
+ },
+
+ /** Delete a project, and detach any tasks that belonged to it */
+ deleteProject: (projectId) => {
+ // 1️⃣ remove the project
+ const projects = get().projects.filter(p => p.id !== projectId);
+ save('projects', projects);
+ set({ projects });
+
+ // 2️⃣ detach tasks – keep them but clear their projectId
+ const tasks = get().tasks.map(t =>
+ t.projectId === projectId ? { ...t, projectId: null } : t
+ );
+ save('tasks', tasks);
+ set({ tasks });
+ },
+
+ /** Re-evaluate a project’s “completed” flag whenever one of its tasks changes */
+ updateProjectCompletion: (projectId) => {
+ if (!projectId) return;
+
+ const tasks = get().tasks;
+ const allComplete = tasks
+ .filter(t => t.projectId === projectId)
+ .every(t => t.completed);
+
+ const projects = get().projects.map(p =>
+ p.id === projectId ? { ...p, completed: allComplete } : p
+ );
+ save('projects', projects);
+ set({ projects });
+ },
+
+ /* ---------- HELPERS ---------- */
+ isOverdue: (task) =>
+ !!task.dueDate && !task.completed && new Date(task.dueDate) < new Date(),
+
+}));
diff --git a/to-do-list-app b/to-do-list-app
new file mode 160000
index 0000000..95f6b03
--- /dev/null
+++ b/to-do-list-app
@@ -0,0 +1 @@
+Subproject commit 95f6b03d8d80b0e8849c4d778de1dba0adc5b07a
diff --git a/vite.config.js b/vite.config.js
index ba24244..6ff59fc 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -1,7 +1,6 @@
-import react from '@vitejs/plugin-react'
-import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react';
+import { defineConfig } from 'vite';
-// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()]
-})
+});