diff --git a/README.md b/README.md index d1c68b5..d9bfc5b 100644 --- a/README.md +++ b/README.md @@ -1 +1,48 @@ -# Todo \ No newline at end of file +# ๐Ÿ“ Doing It... โ€“ A Minimalist To-Do App with Zustand + Tailwind + +**Doing It...** is a fun and interactive to-do list app built with React, Zustand for global state, and Tailwind CSS for styling. Designed to help you stay focused and inspired while you check off tasks - simple. + +--- + +## ๐Ÿ”— **Project Access**: + +๐Ÿš€ [Live Demo](https://blr-tootoodo.netlify.app/) + +--- + +## โœจ Features + +- โœ… Add, complete, undo, and delete tasks +- ๐ŸŒ‘ Toggle **dark/light mode** with full theme styling +- ๐Ÿ“† Timestamp when tasks are created +- ๐ŸŽฏ Task counter for completed items +- ๐Ÿง  Zustand global state โ€“ no prop drilling +- ๐Ÿงผ Clean, responsive design with a11y focus +- ๐Ÿ“ฑ Mobile-first responsive layout + +--- + +## ๐Ÿ›  Tech Stack + +React +Vite +Zustand +Tailwind CSS +PostCSS + +--- + +## โ™ฟ Accessibility & Performance + +๐ŸŒฑ Fully responsive: from 320px to 1600px+ +๐Ÿ Lighthouse score: 100 % +๐ŸŽจ Color contrast and keyboard navigation friendly + +--- + +## ๐Ÿ’ก Future Features (Stretch Goals) + +Project categories or tags +Due dates and visual overdue indicators +"Complete All" functionality +Local storage or database persistence diff --git a/index.html b/index.html index f7ac4e4..51b4c3c 100644 --- a/index.html +++ b/index.html @@ -2,15 +2,23 @@ - + + + + + Todo
- + diff --git a/package.json b/package.json index caf6289..d2c0fb6 100644 --- a/package.json +++ b/package.json @@ -10,18 +10,24 @@ "preview": "vite preview" }, "dependencies": { + "@tailwindcss/vite": "^4.1.10", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "zustand": "^5.0.5" }, "devDependencies": { "@eslint/js": "^9.21.0", + "@tailwindcss/postcss": "^4.1.10", "@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.6", + "tailwindcss": "^3.4.1", "vite": "^6.2.0" } } diff --git a/postcss.config.cjs b/postcss.config.cjs new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/postcss.config.cjs @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/public/checkmark-todo-favicon.png b/public/checkmark-todo-favicon.png new file mode 100644 index 0000000..f8b331b Binary files /dev/null and b/public/checkmark-todo-favicon.png differ diff --git a/public/justDoIt.onBlack.png b/public/justDoIt.onBlack.png new file mode 100644 index 0000000..c2dfe34 Binary files /dev/null and b/public/justDoIt.onBlack.png differ diff --git a/public/justDoIt.png b/public/justDoIt.png new file mode 100644 index 0000000..7822cb1 Binary files /dev/null and b/public/justDoIt.png differ diff --git a/pull_request_template.md b/pull_request_template.md index 154c92e..1cb5c18 100644 --- a/pull_request_template.md +++ b/pull_request_template.md @@ -1 +1,3 @@ -Please include your Netlify link here. \ No newline at end of file +Please include your Netlify link here: + +https://blr-tootoodo.netlify.app/ diff --git a/src/App.jsx b/src/App.jsx index 5427540..7796df5 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,5 +1,14 @@ +import AppLayout from "./components/AppLayout" +import Dashboard from "./components/Dashboard" +import { Tabs } from "./components/Tabs" +import { TaskForm } from "./components/TaskForm" + export const App = () => { return ( -

React Boilerplate

+ + + + + ) } diff --git a/src/components/AppLayout.jsx b/src/components/AppLayout.jsx new file mode 100644 index 0000000..d27c2e3 --- /dev/null +++ b/src/components/AppLayout.jsx @@ -0,0 +1,46 @@ +import { useEffect, useState } from "react" + +const AppLayout = ({ children }) => { + const [isDark, setIsDark ] = useState(false) + + useEffect(() => { + if (isDark) { + document.documentElement.classList.add("dark"); + } else { + document.documentElement.classList.remove("dark"); + } + }, [isDark]) + + const handleToggle = () => { + console.log("Toggling dark mode:", !isDark); + setIsDark(!isDark); + } + + return ( +
+
+
+
+ Just Do It logo + Just Do It logo +

Doing it...

+
+ +
+ {children} +
+
+ ) +} + +export default AppLayout \ No newline at end of file diff --git a/src/components/CompletedTaskList.jsx b/src/components/CompletedTaskList.jsx new file mode 100644 index 0000000..e93032d --- /dev/null +++ b/src/components/CompletedTaskList.jsx @@ -0,0 +1,29 @@ +import { useTasksStore } from "../stores/useTaskStore" +import { TaskItem } from "./TaskItem" + +export const CompletedTaskList = () => { + const tasks = useTasksStore(state => state.tasks) + const completedTasks = tasks.filter(task => task.isCompleted) + const taskCount = useTasksStore(state => state.getCompletedCount()) + + if (completedTasks.length === 0) { + return ( +
+

+ Lets get something ta-done โœจ +

+
+ ) + } + + return ( +
+

The Ta-Dones:

+ +
+ ) +} \ No newline at end of file diff --git a/src/components/Dashboard.jsx b/src/components/Dashboard.jsx new file mode 100644 index 0000000..1ab037d --- /dev/null +++ b/src/components/Dashboard.jsx @@ -0,0 +1,21 @@ +import { useTasksStore } from "../stores/useTaskStore" + +const Dashboard = () => { + const total = useTasksStore(state => state.tasks.length) + const completed = useTasksStore(state => state.getCompletedCount()) + const pending = useTasksStore(state => state.getPendingCount()) + + return ( +
+

๐Ÿ“‹ Your Task Overview

+

Total tasks: {total}

+

โœ… Completed: {completed}

+

๐Ÿ•’ Remaining: {pending}

+ + {total === 0 &&

Start by adding your first task!

} + {total > 0 && pending === 0 &&

You're all caught up! ๐ŸŽ‰

} +
+ ) +} + +export default Dashboard \ No newline at end of file diff --git a/src/components/Tabs.jsx b/src/components/Tabs.jsx new file mode 100644 index 0000000..3a99b78 --- /dev/null +++ b/src/components/Tabs.jsx @@ -0,0 +1,58 @@ +import { useState } from "react" + +import { useTasksStore } from "../stores/useTaskStore" +import { CompletedTaskList } from "./CompletedTaskList" +import { TaskList } from "./TaskList" + +export const Tabs = () => { +const [activeTab, setActiveTab] = useState("todo") + +const pendingCount = useTasksStore(state => state.getPendingCount()) +const completedCount = useTasksStore(state => state.getCompletedCount()) +const pulseTab = useTasksStore(state => state.pulseTab) + + + return ( +
+
+ + + +
+
+ {activeTab === "todo" && } + {activeTab === "completed" && } +
+
+ ) +} \ No newline at end of file diff --git a/src/components/TaskForm.jsx b/src/components/TaskForm.jsx new file mode 100644 index 0000000..a4c8c8a --- /dev/null +++ b/src/components/TaskForm.jsx @@ -0,0 +1,42 @@ +import { useState } from "react" +import { useTasksStore } from "../stores/useTaskStore" + +export const TaskForm = () => { + const [taskMsg, setTaskMsg] = useState("") + const [error, setError] = useState() + const createTask = useTasksStore(state => state.createTask) + const triggerPulse = useTasksStore(state => state.triggerPulseTab) + + const handleSubmit = e => { + e.preventDefault() + if (taskMsg.trim() === "") { + setError("Cannot add an empty task.") + return + } + createTask(taskMsg) + triggerPulse("todo") + setTaskMsg("") + setError("") + } + + return ( +
+ + { + setTaskMsg(e.target.value) + if (error) setError("") + }} + placeholder="Next on the list..." + className="w-full p-2 border rounded resize-none dark:text-slate-600" + /> + {error &&

{error}

} + +
+ ) +} \ No newline at end of file diff --git a/src/components/TaskItem.jsx b/src/components/TaskItem.jsx new file mode 100644 index 0000000..eb7f11c --- /dev/null +++ b/src/components/TaskItem.jsx @@ -0,0 +1,68 @@ +import { useTasksStore } from "../stores/useTaskStore" +import { useState } from "react" + +export const TaskItem = ({ task: {taskMsg, id, date, isCompleted} }) => { + const formattedDate = new Date(date) + .toLocaleDateString("en-SE", {month: "short", day: "numeric"}) + + const formattedTime = new Date(date) + .toLocaleTimeString("en-SE", { hour: "2-digit", minute: "2-digit"}) + + const [message, setMessage] = useState(null) + const [isAnimating, setIsAnimating] = useState(false) + const toggleCompleted = useTasksStore(state => state.toggleCompleted) + const triggerPulse = useTasksStore(state => state.triggerPulseTab) + + const handleToggle = () => { + setMessage(isCompleted ? "โŽ Task pending" : "โœ… Task completed!") + setIsAnimating(true) + setTimeout(() => setMessage(null), 1000) + setTimeout(() => { + toggleCompleted(id) + triggerPulse(isCompleted ? "todo" : "completed") + }, 1000) + } + + const deleteConfirmed = useTasksStore(state => state.deleteTask) + const handleDelete = () => { + setMessage("๐Ÿ—‘๏ธ Task deleted") + setIsAnimating(true) + setTimeout(() => setMessage(null), 1000) + setTimeout(() =>{ + deleteConfirmed(id) + }, 1200) + } + + return ( + <> + {message && ( +
+ {message} +
+ )} +
  • +

    + {taskMsg} +

    +
    + + +

    {formattedDate} at {formattedTime}

    +
    +
  • + + ) +} \ No newline at end of file diff --git a/src/components/TaskList.jsx b/src/components/TaskList.jsx new file mode 100644 index 0000000..2372df9 --- /dev/null +++ b/src/components/TaskList.jsx @@ -0,0 +1,44 @@ +import { useState, useEffect } from "react" +import { useTasksStore } from "../stores/useTaskStore" +import { TaskItem } from "./TaskItem" +import { motivationalQuotes } from "./quotes" + +export const TaskList = () => { + const tasks = useTasksStore(state => state.tasks) + const activeTasks = tasks.filter(task => !task.isCompleted) + const [quote, setQuote] = useState("") + + useEffect(() => { + const getRandomQuote = () => { + const index = Math.floor(Math.random() * motivationalQuotes.length) + return motivationalQuotes[index] + } + + setQuote(getRandomQuote()) + + const interval = setInterval(() => { + setQuote(getRandomQuote()) + }, 8000) + + return () => clearInterval(interval) + }, []) + + if (activeTasks.length === 0) { + return ( +
    +

    "{quote}"

    +
    + ) + } + + return ( +
    +

    The To-Dos:

    + +
    + ) +} \ No newline at end of file diff --git a/src/components/quotes.js b/src/components/quotes.js new file mode 100644 index 0000000..caf62fd --- /dev/null +++ b/src/components/quotes.js @@ -0,0 +1,8 @@ +export const motivationalQuotes = [ + "Rest is productive too.", + "Take a walk, clear your mind.", + "Small steps lead to big change.", + "Your pace is perfect.", + "Progress, not perfection.", + "Breathe. Youโ€™ve got this.", +]; \ No newline at end of file diff --git a/src/index.css b/src/index.css index f7c0aef..9b0dcde 100644 --- a/src/index.css +++ b/src/index.css @@ -1,3 +1,59 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + + :root { font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; } + +@layer utilities { + .animate-fade-in { + animation: fadeIn 0.6s ease-out; + } + + @keyframes fadeIn { + from { opacity: 0; transform: translateY(0.25rem); } + to { opacity: 1; transform: translateY(0); } + } +} + +@layer utilities { + .collapse { + animation: collapseOut 0.3s ease forwards; + } + + @keyframes collapseOut { + from { height: auto; opacity: 1; } + to { height: 0; opacity: 0; margin: 0; padding: 0; } + } +} + +@layer utilities { + .animate-pulse-once { + animation: pulseOnce 0.6s ease-out; + } + + @keyframes pulseOnce { + 0% { transform: scale(1); opacity: 1; } + 50% { transform: scale(1.2); opacity: 0.6; } + 100% { transform: scale(1); opacity: 1; } + } +} + +@layer utilities { + .slide-out { + animation: slideOut 0.3s ease forwards; + } + + @keyframes slideOut { + from { + opacity: 1; + transform: translateX(0); + } + to { + opacity: 0; + transform: translateX(60px); + } + } +} diff --git a/src/stores/useTaskStore.jsx b/src/stores/useTaskStore.jsx new file mode 100644 index 0000000..0c76197 --- /dev/null +++ b/src/stores/useTaskStore.jsx @@ -0,0 +1,54 @@ +import { create } from "zustand" +import { persist } from "zustand/middleware" + +const initialState = { + tasks: [ + { + id: 1, + taskMsg: "Make a to-do app", + date: Date.now(), + isCompleted: false + } + ] +} + +export const useTasksStore = create( + persist( + (set, get) => ({ + ...initialState, + pulseTab: null, + + triggerPulseTab: (tabName) => { + set({ pulseTab: tabName }) + setTimeout(() => set({ pulseTab: null }), 800) + }, + + createTask: (task) => { + const newTask = { + id: get().tasks.length > 0 ? get().tasks[0].id + 1 : 1, + taskMsg: task, + date: Date.now(), + isCompleted: false + } + set(state => ({ tasks: [newTask, ...state.tasks] })) + }, + + toggleCompleted: (id) => set((state) => ({ + tasks: state.tasks.map(task => + task.id === id ? { ...task, isCompleted: !task.isCompleted } : task + ) + + })), + + deleteTask: (id) => set((state) => ({ + tasks: state.tasks.filter(t => t.id !== id) + })), + + getCompletedCount: () => get().tasks.filter(task => task.isCompleted).length, + + getPendingCount: () => get().tasks.filter(task => !task.isCompleted).length, + }), + + + { name: "todo-storage" } +)) diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..b3c18af --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,15 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + darkMode: 'class', + content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], + theme: { + extend: {}, + screens: { + xs: "425px", + sm: "640px", + md: "768px", + lg: "1024", + } + }, + plugins: [], +}; diff --git a/vite.config.js b/vite.config.js index ba24244..a1487f6 100644 --- a/vite.config.js +++ b/vite.config.js @@ -3,5 +3,7 @@ import { defineConfig } from 'vite' // https://vitejs.dev/config/ export default defineConfig({ - plugins: [react()] + plugins: [ + react(), + ] })