├── README.md
├── eslint.config.js
├── index.html
├── package-lock.json
├── package.json
├── postcss.config.js
├── src
├── App.tsx
├── components
│ ├── AddTask.tsx
│ ├── MenuBar.tsx
│ ├── Stats.tsx
│ ├── TaskItem.tsx
│ └── TaskList.tsx
├── index.css
├── main.tsx
├── store
│ └── useStore.ts
├── types.ts
├── utils
│ └── cn.ts
└── vite-env.d.ts
├── tailwind.config.js
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
/README.md:
--------------------------------------------------------------------------------
1 | # ✅ Task Master
2 |
3 | ## 🚀 Overview
4 | The **Task Management App** helps users efficiently manage daily tasks with features like task creation, editing, completion tracking, filtering, sorting, drag-and-drop reordering, dark mode, and local storage persistence.
5 |
6 | ## ✨ Features
7 | - ✍️ **Add, edit, and delete tasks**
8 | - ✅ **Mark tasks as completed**
9 | - 🔍 **Filter and sort tasks** by status, due date, or priority
10 | - 🎯 **Drag-and-drop reordering** (React DnD)
11 | - 🌙 **Dark mode toggle**
12 | - 💾 **Local storage for task persistence**
13 | - 📱 **Responsive UI** (Tailwind CSS)
14 |
15 | ## 🛠 Tech Stack
16 | - ⚛️ **Frontend:** React.js
17 | - 🎨 **Styling:** Tailwind CSS
18 | - 🖱 **Drag & Drop:** React DnD
19 | - 🔄 **State Management:** React Hooks
20 | - 💽 **Storage:** Local Storage
21 |
22 | ## 📦 Installation
23 | 1. Clone the repository:
24 | ```sh
25 | git clone https://github.com/sriram2915/Task-Master.git
26 | cd Task-Master
27 | ```
28 | 2. Install dependencies:
29 | ```sh
30 | npm install
31 | ```
32 | 3. Start the development server:
33 | ```sh
34 | npm run dev
35 | ```
36 |
37 | ## 🌍 Deployment
38 | The application is hosted on **Vercel**. You can access it **[here](https://task-master-gilt-ten.vercel.app/)**.
39 |
40 |
41 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js'
2 | import globals from 'globals'
3 | import reactHooks from 'eslint-plugin-react-hooks'
4 | import reactRefresh from 'eslint-plugin-react-refresh'
5 | import tseslint from 'typescript-eslint'
6 |
7 | export default tseslint.config(
8 | { ignores: ['dist'] },
9 | {
10 | extends: [js.configs.recommended, ...tseslint.configs.recommended],
11 | files: ['**/*.{ts,tsx}'],
12 | languageOptions: {
13 | ecmaVersion: 2020,
14 | globals: globals.browser,
15 | },
16 | plugins: {
17 | 'react-hooks': reactHooks,
18 | 'react-refresh': reactRefresh,
19 | },
20 | rules: {
21 | ...reactHooks.configs.recommended.rules,
22 | 'react-refresh/only-export-components': [
23 | 'warn',
24 | { allowConstantExport: true },
25 | ],
26 | },
27 | },
28 | )
29 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Task Master
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vite-react-typescript-starter",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "lint": "eslint .",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@dnd-kit/core": "^6.1.0",
14 | "@dnd-kit/sortable": "^8.0.0",
15 | "@dnd-kit/utilities": "^3.2.2",
16 | "clsx": "^2.1.0",
17 | "framer-motion": "^11.0.8",
18 | "lucide-react": "^0.344.0",
19 | "react": "^18.3.1",
20 | "react-dom": "^18.3.1",
21 | "tailwind-merge": "^2.2.1",
22 | "zustand": "^4.5.2"
23 | },
24 | "devDependencies": {
25 | "@eslint/js": "^9.9.1",
26 | "@types/react": "^18.3.5",
27 | "@types/react-dom": "^18.3.0",
28 | "@vitejs/plugin-react": "^4.3.1",
29 | "autoprefixer": "^10.4.21",
30 | "eslint": "^9.9.1",
31 | "eslint-plugin-react-hooks": "^5.1.0-rc.0",
32 | "eslint-plugin-react-refresh": "^0.4.11",
33 | "globals": "^15.9.0",
34 | "postcss": "^8.5.3",
35 | "tailwindcss": "^3.4.17",
36 | "typescript": "^5.5.3",
37 | "typescript-eslint": "^8.3.0",
38 | "vite": "^5.4.2"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { Layout } from 'lucide-react';
3 | import { motion } from 'framer-motion';
4 | import { AddTask } from './components/AddTask';
5 | import { TaskList } from './components/TaskList';
6 | import { Stats } from './components/Stats';
7 | import { MenuBar } from './components/MenuBar';
8 | import { useStore } from './store/useStore';
9 |
10 | function App() {
11 | const { settings } = useStore();
12 |
13 | useEffect(() => {
14 | const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
15 | const updateTheme = () => {
16 | if (settings.theme === 'system') {
17 | document.documentElement.setAttribute(
18 | 'data-theme',
19 | mediaQuery.matches ? 'dark' : 'light'
20 | );
21 | } else {
22 | document.documentElement.setAttribute('data-theme', settings.theme);
23 | }
24 | };
25 |
26 | mediaQuery.addEventListener('change', updateTheme);
27 | updateTheme();
28 |
29 | return () => mediaQuery.removeEventListener('change', updateTheme);
30 | }, [settings.theme]);
31 |
32 | return (
33 |
34 | {settings.backgroundImage && (
35 | <>
36 |
40 | {/*
*/}
41 |
45 |
46 | >
47 | )}
48 |
49 |
50 |
51 |
52 |
57 |
58 | Task Master
59 |
60 |
61 |
70 |
71 |
72 |
73 | );
74 | }
75 |
76 | export default App;
--------------------------------------------------------------------------------
/src/components/AddTask.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Plus } from 'lucide-react';
3 | import { Priority } from '../types';
4 | import { useStore } from '../store/useStore';
5 |
6 | export const AddTask: React.FC = () => {
7 | const [title, setTitle] = useState('');
8 | const [description, setDescription] = useState('');
9 | const [priority, setPriority] = useState('medium');
10 |
11 | const { addTask } = useStore();
12 |
13 | const handleSubmit = (e: React.FormEvent) => {
14 | e.preventDefault();
15 | if (title.trim() && description.trim()) {
16 | addTask(title.trim(), description.trim(), priority);
17 | setTitle('');
18 | setDescription('');
19 | setPriority('medium');
20 | }
21 | };
22 |
23 | return (
24 |
57 | );
58 | };
59 |
--------------------------------------------------------------------------------
/src/components/MenuBar.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { motion, AnimatePresence } from 'framer-motion';
3 | import { Settings, Moon, Sun, Image, Info, X } from 'lucide-react';
4 | import { Theme } from '../types';
5 | import { useStore } from '../store/useStore';
6 | import { cn } from '../utils/cn';
7 |
8 | const DEFAULT_BACKGROUNDS = [
9 | 'https://images.unsplash.com/photo-1519681393784-d120267933ba',
10 | 'https://images.unsplash.com/photo-1508739773434-c26b3d09e071',
11 | 'https://images.unsplash.com/photo-1477346611705-65d1883cee1e',
12 | ];
13 |
14 | export const MenuBar: React.FC = () => {
15 | const [isOpen, setIsOpen] = useState(false);
16 | const { settings, updateSettings } = useStore();
17 |
18 | const handleThemeChange = (theme: Theme) => {
19 | updateSettings({ theme });
20 | if (theme === 'system') {
21 | document.documentElement.removeAttribute('data-theme');
22 | } else {
23 | document.documentElement.setAttribute('data-theme', theme);
24 | }
25 | };
26 |
27 | return (
28 | <>
29 |
30 | setIsOpen(true)}
34 | className="bg-[rgb(var(--card-bg))] p-3 rounded-full shadow-lg hover:shadow-xl transition-shadow"
35 | >
36 |
37 |
38 |
39 |
40 |
41 | {isOpen && (
42 | <>
43 | setIsOpen(false)}
49 | />
50 |
57 |
58 |
Settings
59 |
65 |
66 |
67 |
68 |
69 |
70 |
71 | Background
72 |
73 |
74 | {DEFAULT_BACKGROUNDS.map((bg) => (
75 |
89 | ))}
90 |
91 |
92 |
95 |
102 | updateSettings({
103 | backgroundOverlay: parseFloat(e.target.value),
104 | })
105 | }
106 | className="w-full"
107 | />
108 |
109 |
110 |
111 |
112 |
113 |
114 | Theme
115 |
116 |
117 | {(['light', 'dark', 'system'] as Theme[]).map((theme) => (
118 |
130 | ))}
131 |
132 |
133 |
134 |
135 |
136 |
137 | About
138 |
139 |
140 |
Task Master v1.0.0
141 |
142 | A beautiful and productive task management app with
143 | gamification features.
144 |
145 |
146 |
147 |
148 |
149 | >
150 | )}
151 |
152 | >
153 | );
154 | };
--------------------------------------------------------------------------------
/src/components/Stats.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { motion, AnimatePresence } from 'framer-motion';
3 | import { Trophy, Star, Check, Flame } from 'lucide-react';
4 | import { useStore } from '../store/useStore';
5 |
6 | const getMotivationalMessage = (tasksCompleted: number): string => {
7 | if (tasksCompleted === 0) return "🚀 Ready to start your productive journey?";
8 | if (tasksCompleted < 3) return "👍 Great start! Keep the momentum going!";
9 | if (tasksCompleted < 5) return "🔥 You're on fire! Don't stop now!";
10 | if (tasksCompleted < 10) return "💪 Incredible progress! You're unstoppable!";
11 | return "🏆 You're a productivity master! 🎉";
12 | };
13 |
14 | export const Stats: React.FC = () => {
15 | const { stats } = useStore();
16 | const xpToNextLevel = 100;
17 | const progress = (stats.xp % xpToNextLevel) / xpToNextLevel * 100;
18 | const motivationalMessage = getMotivationalMessage(stats.tasksCompleted);
19 |
20 | return (
21 |
22 |
23 | {/* Streak Button in Top Right Corner */}
24 |
28 | {stats.streak > 0 ? `🔥 ${stats.streak} Day${stats.streak > 1 ? 's' : ''} Streak` : "Start Streak"}
29 |
30 |
31 |
37 |
38 |
39 |
40 |
41 |
Level {stats.level}
42 |
43 | {stats.xp} XP total • {stats.tasksCompleted} tasks completed
44 |
45 |
46 |
47 |
48 |
54 |
55 | Progress to Level {stats.level + 1}
56 | {stats.xp % xpToNextLevel}/{xpToNextLevel} XP
57 |
58 |
59 |
65 |
66 |
67 |
68 |
74 |
75 | {motivationalMessage}
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 | Achievements
84 |
85 |
86 | {stats.achievements.map((achievement, index) => (
87 |
99 | {achievement.icon}
100 |
101 |
{achievement.title}
102 |
103 | {achievement.description}
104 |
105 |
106 | {achievement.unlocked && (
107 |
108 | )}
109 |
110 | ))}
111 |
112 |
113 |
114 |
115 |
116 | );
117 | };
118 |
--------------------------------------------------------------------------------
/src/components/TaskItem.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { useSortable } from '@dnd-kit/sortable';
3 | import { CSS } from '@dnd-kit/utilities';
4 | import { motion, AnimatePresence } from 'framer-motion';
5 | import { GripVertical, Trash2, Edit2, Check, Sparkles } from 'lucide-react';
6 | import { Task, Priority } from '../types';
7 | import { useStore } from '../store/useStore';
8 | import { cn } from '../utils/cn';
9 |
10 | interface TaskItemProps {
11 | task: Task;
12 | }
13 |
14 | const priorityColors = {
15 | high: 'bg-gradient-to-r from-red-400 to-rose-400 border-red-200 dark:from-red-900 dark:to-rose-900 dark:border-red-800',
16 | medium: 'bg-gradient-to-r from-yellow-300 to-amber-300 border-yellow-200 dark:from-yellow-900 dark:to-amber-900 dark:border-yellow-800',
17 | low: 'bg-gradient-to-r from-green-400 to-emerald-400 border-green-200 dark:from-green-900 dark:to-emerald-900 dark:border-green-800',
18 | };
19 |
20 | export const TaskItem: React.FC = ({ task }) => {
21 | const [isEditing, setIsEditing] = useState(false);
22 | const [editTitle, setEditTitle] = useState(task.title);
23 | const [editPriority, setEditPriority] = useState(task.priority);
24 | const [showCompletionEffect, setShowCompletionEffect] = useState(false);
25 |
26 | const { toggleTask, deleteTask, editTask } = useStore();
27 |
28 | const {
29 | attributes,
30 | listeners,
31 | setNodeRef,
32 | transform,
33 | transition,
34 | } = useSortable({ id: task.id });
35 |
36 | const style = {
37 | transform: CSS.Transform.toString(transform),
38 | transition,
39 | };
40 |
41 | const handleEdit = () => {
42 | editTask(task.id, editTitle, editPriority);
43 | setIsEditing(false);
44 | };
45 |
46 | const handleToggle = () => {
47 | if (!task.completed) {
48 | setShowCompletionEffect(true);
49 | setTimeout(() => setShowCompletionEffect(false), 1000);
50 | }
51 | toggleTask(task.id);
52 | };
53 |
54 | return (
55 |
69 |
70 | {/* Drag Handle */}
71 |
74 |
75 | {/* Checkbox for Task Completion */}
76 |
96 |
97 | {/* Task Title & Edit Mode */}
98 | {isEditing ? (
99 |
100 | setEditTitle(e.target.value)}
104 | className="flex-1 text-black dark:text-black rounded px-2 py-1 border border-gray-200 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500"
105 | />
106 |
115 |
121 |
122 | ) : (
123 |
127 | {task.title}
128 |
129 | )}
130 |
131 | {/* Edit & Delete Buttons */}
132 |
133 |
142 |
148 |
149 |
150 |
151 | {/* Task Description */}
152 |
153 |
154 | {task.description}
155 |
156 |
157 |
158 | );
159 | };
160 |
--------------------------------------------------------------------------------
/src/components/TaskList.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { DndContext, DragEndEvent, closestCenter } from '@dnd-kit/core';
3 | import { SortableContext, verticalListSortingStrategy, arrayMove } from '@dnd-kit/sortable';
4 | import { TaskItem } from './TaskItem';
5 | import { useStore } from '../store/useStore';
6 | import { Task } from '../types'; // Ensure your Task type is imported if needed
7 |
8 | export const TaskList: React.FC = () => {
9 | const { tasks, reorderTasks } = useStore();
10 |
11 | const handleDragEnd = (event: DragEndEvent) => {
12 | const { active, over } = event;
13 | if (over && active.id !== over.id) {
14 | const oldIndex = tasks.findIndex(task => task.id === active.id);
15 | const newIndex = tasks.findIndex(task => task.id === over.id);
16 | const newTasks = arrayMove(tasks, oldIndex, newIndex);
17 | reorderTasks(newTasks);
18 | }
19 | };
20 |
21 | const sortedTasks = [...tasks].sort((a, b) => {
22 | const priorityOrder = { high: 0, medium: 1, low: 2 };
23 | return priorityOrder[a.priority] - priorityOrder[b.priority];
24 | });
25 |
26 | return (
27 |
28 |
29 |
30 | {sortedTasks.map(task => (
31 |
32 | ))}
33 |
34 |
35 |
36 | );
37 | };
38 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --bg-primary: 249 250 251;
8 | --text-primary: 17 24 39;
9 | --card-bg: 255 255 255;
10 | --card-border: 229 231 235;
11 | --hover-bg: 243 244 246;
12 | }
13 |
14 | [data-theme='dark'] {
15 | --bg-primary: 17 24 39;
16 | --text-primary: 249 250 251;
17 | --card-bg: 31 41 55;
18 | --card-border: 55 65 81;
19 | --hover-bg: 55 65 81;
20 | }
21 |
22 | body {
23 | @apply bg-[rgb(var(--bg-primary))] text-[rgb(var(--text-primary))];
24 | }
25 | }
26 |
27 | .bg-white {
28 | @apply bg-[rgb(var(--card-bg))];
29 | }
30 |
31 | .bg-gray-50 {
32 | @apply bg-[rgb(var(--bg-primary))];
33 | }
34 |
35 | .bg-gray-100 {
36 | @apply bg-[rgb(var(--hover-bg))];
37 | }
38 |
39 | .text-gray-700 {
40 | @apply text-[rgb(var(--text-primary))];
41 | }
42 |
43 | .text-gray-500 {
44 | @apply text-[rgb(var(--text-primary))] opacity-60;
45 | }
46 |
47 | .text-gray-600 {
48 | @apply text-[rgb(var(--text-primary))] opacity-75;
49 | }
50 |
51 | .border-gray-200 {
52 | @apply border-[rgb(var(--card-border))];
53 | }
54 |
55 | .hover\:bg-gray-100:hover {
56 | @apply hover:bg-[rgb(var(--hover-bg))];
57 | }
58 |
59 | .hover\:bg-gray-200:hover {
60 | @apply hover:bg-[rgb(var(--hover-bg))];
61 | }
62 |
63 | @keyframes float {
64 | 0% {
65 | transform: translateY(0px) translateX(0px);
66 | }
67 | 50% {
68 | transform: translateY(-10px) translateX(5px);
69 | }
70 | 100% {
71 | transform: translateY(0px) translateX(0px);
72 | }
73 | }
74 |
75 | @keyframes pulse {
76 | 0% {
77 | transform: scale(1);
78 | }
79 | 50% {
80 | transform: scale(1.05);
81 | }
82 | 100% {
83 | transform: scale(1);
84 | }
85 | }
86 |
87 | .animate-float {
88 | animation: float 6s ease-in-out infinite;
89 | }
90 |
91 | .animate-pulse-slow {
92 | animation: pulse 4s ease-in-out infinite;
93 | }
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from 'react'
2 | import { createRoot } from 'react-dom/client'
3 | import App from './App.tsx'
4 | import './index.css'
5 |
6 | createRoot(document.getElementById('root')!).render(
7 |
8 |
9 | ,
10 | )
11 |
--------------------------------------------------------------------------------
/src/store/useStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand';
2 | import { Task, Priority, UserStats, Achievement, Settings, Theme } from '../types';
3 |
4 | interface Store {
5 | tasks: Task[];
6 | stats: UserStats;
7 | settings: Settings;
8 | addTask: (title: string, description: string, priority: Priority) => void;
9 | editTask: (id: string, title: string, priority: Priority) => void;
10 | deleteTask: (id: string) => void;
11 | toggleTask: (id: string) => void;
12 | reorderTasks: (tasks: Task[]) => void;
13 | updateSettings: (settings: Partial) => void;
14 | }
15 |
16 | const DEFAULT_BACKGROUNDS = [
17 | 'https://images.unsplash.com/photo-1519681393784-d120267933ba',
18 | 'https://images.unsplash.com/photo-1508739773434-c26b3d09e071',
19 | 'https://images.unsplash.com/photo-1477346611705-65d1883cee1e',
20 | ];
21 |
22 | const INITIAL_ACHIEVEMENTS: Achievement[] = [
23 | {
24 | id: 'first-task',
25 | title: 'Getting Started',
26 | description: 'Complete your first task',
27 | icon: '🌟',
28 | unlocked: false,
29 | },
30 | {
31 | id: 'productive-day',
32 | title: 'Productive Day',
33 | description: 'Complete 5 tasks in a day',
34 | icon: '🎯',
35 | unlocked: false,
36 | },
37 | {
38 | id: 'master-organizer',
39 | title: 'Master Organizer',
40 | description: 'Have tasks in all priority levels',
41 | icon: '📊',
42 | unlocked: false,
43 | },
44 | ];
45 |
46 | const calculateLevel = (xp: number) => Math.floor(xp / 100) + 1;
47 |
48 | const loadFromStorage = () => {
49 | const tasks = JSON.parse(localStorage.getItem('tasks') || '[]');
50 | const stats = JSON.parse(localStorage.getItem('stats') || JSON.stringify({
51 | level: 1,
52 | xp: 0,
53 | tasksCompleted: 0,
54 | streak: 0,
55 | lastCompletedAt: null,
56 | achievements: INITIAL_ACHIEVEMENTS,
57 | }));
58 | const settings = JSON.parse(localStorage.getItem('settings') || JSON.stringify({
59 | theme: 'system' as Theme,
60 | backgroundImage: DEFAULT_BACKGROUNDS[0],
61 | backgroundOverlay: 0.5,
62 | }));
63 | return { tasks, stats, settings };
64 | };
65 |
66 | export const useStore = create((set, get) => {
67 | const { tasks: initialTasks, stats: initialStats, settings: initialSettings } = loadFromStorage();
68 |
69 | const saveToStorage = (tasks: Task[], stats: UserStats, settings: Settings) => {
70 | localStorage.setItem('tasks', JSON.stringify(tasks));
71 | localStorage.setItem('stats', JSON.stringify(stats));
72 | localStorage.setItem('settings', JSON.stringify(settings));
73 | };
74 |
75 | const updateStreak = (stats: UserStats): UserStats => {
76 | const now = new Date();
77 | const lastCompleted = stats.lastCompletedAt ? new Date(stats.lastCompletedAt) : null;
78 |
79 | if (!lastCompleted) {
80 | return {
81 | ...stats,
82 | streak: 1,
83 | lastCompletedAt: now.getTime(),
84 | };
85 | }
86 |
87 | const isNextDay =
88 | now.getDate() === lastCompleted.getDate() + 1 &&
89 | now.getMonth() === lastCompleted.getMonth() &&
90 | now.getFullYear() === lastCompleted.getFullYear();
91 |
92 | const isSameDay =
93 | now.getDate() === lastCompleted.getDate() &&
94 | now.getMonth() === lastCompleted.getMonth() &&
95 | now.getFullYear() === lastCompleted.getFullYear();
96 |
97 | if (isNextDay) {
98 | return {
99 | ...stats,
100 | streak: stats.streak + 1,
101 | lastCompletedAt: now.getTime(),
102 | };
103 | } else if (isSameDay) {
104 | return {
105 | ...stats,
106 | lastCompletedAt: now.getTime(),
107 | };
108 | } else {
109 | return {
110 | ...stats,
111 | streak: 1,
112 | lastCompletedAt: now.getTime(),
113 | };
114 | }
115 | };
116 |
117 | const checkAchievements = (stats: UserStats, tasks: Task[]) => {
118 | const updatedAchievements = [...stats.achievements];
119 |
120 | if (stats.tasksCompleted >= 1) {
121 | updatedAchievements[0].unlocked = true;
122 | }
123 |
124 | if (stats.tasksCompleted >= 5) {
125 | updatedAchievements[1].unlocked = true;
126 | }
127 |
128 | const priorities = new Set(tasks.map(task => task.priority));
129 | if (priorities.size === 3) {
130 | updatedAchievements[2].unlocked = true;
131 | }
132 |
133 | return updatedAchievements;
134 | };
135 |
136 | return {
137 | tasks: initialTasks,
138 | stats: initialStats,
139 | settings: initialSettings,
140 |
141 | addTask: (title: string, description: string, priority: Priority) => {
142 | const newTask: Task = {
143 | id: Date.now().toString(),
144 | title,
145 | description,
146 | priority,
147 | completed: false,
148 | createdAt: Date.now(),
149 | };
150 |
151 | set(state => {
152 | const newTasks = [...state.tasks, newTask];
153 | const newStats = { ...state.stats };
154 | newStats.achievements = checkAchievements(newStats, newTasks);
155 |
156 | saveToStorage(newTasks, newStats, state.settings);
157 | return { tasks: newTasks, stats: newStats };
158 | });
159 | },
160 |
161 | editTask: (id: string, title: string, priority: Priority) => {
162 | set(state => {
163 | const newTasks = state.tasks.map(task =>
164 | task.id === id ? { ...task, title, priority } : task
165 | );
166 | saveToStorage(newTasks, state.stats, state.settings);
167 | return { tasks: newTasks };
168 | });
169 | },
170 |
171 | deleteTask: (id: string) => {
172 | set(state => {
173 | const newTasks = state.tasks.filter(task => task.id !== id);
174 | saveToStorage(newTasks, state.stats, state.settings);
175 | return { tasks: newTasks };
176 | });
177 | },
178 |
179 | toggleTask: (id: string) => {
180 | set(state => {
181 | const newTasks = state.tasks.map(task =>
182 | task.id === id ? { ...task, completed: !task.completed } : task
183 | );
184 |
185 | const taskCompleted = newTasks.find(t => t.id === id)?.completed;
186 | let newStats = { ...state.stats };
187 |
188 | if (taskCompleted) {
189 | newStats.tasksCompleted += 1;
190 | newStats.xp += 10;
191 | newStats.level = calculateLevel(newStats.xp);
192 | newStats = updateStreak(newStats);
193 | newStats.achievements = checkAchievements(newStats, newTasks);
194 | }
195 |
196 | saveToStorage(newTasks, newStats, state.settings);
197 | return { tasks: newTasks, stats: newStats };
198 | });
199 | },
200 |
201 | reorderTasks: (tasks: Task[]) => {
202 | set(state => {
203 | saveToStorage(tasks, state.stats, state.settings);
204 | return { tasks };
205 | });
206 | },
207 |
208 | updateSettings: (newSettings: Partial) => {
209 | set(state => {
210 | const settings = { ...state.settings, ...newSettings };
211 | saveToStorage(state.tasks, state.stats, settings);
212 | return { settings };
213 | });
214 | },
215 | };
216 | });
217 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | export type Priority = 'high' | 'medium' | 'low';
2 |
3 | export type Theme = 'light' | 'dark' | 'system';
4 |
5 | export interface Settings {
6 | theme: Theme;
7 | backgroundImage: string | null;
8 | backgroundOverlay: number;
9 | }
10 |
11 | export interface Task {
12 | id: string;
13 | title: string;
14 | description: string;
15 | priority: Priority;
16 | completed: boolean;
17 | createdAt: number;
18 | }
19 |
20 | export interface Achievement {
21 | id: string;
22 | title: string;
23 | description: string;
24 | icon: string;
25 | unlocked: boolean;
26 | }
27 |
28 | export interface UserStats {
29 | level: number;
30 | xp: number;
31 | tasksCompleted: number;
32 | streak: number;
33 | lastCompletedAt: number | null;
34 | achievements: Achievement[];
35 | }
--------------------------------------------------------------------------------
/src/utils/cn.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from 'clsx';
2 | import { twMerge } from 'tailwind-merge';
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: [
4 | "./index.html",
5 | "./src/**/*.{js,ts,jsx,tsx}",
6 | ],
7 | theme: {
8 | extend: {},
9 | },
10 | plugins: [],
11 | }
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "isolatedModules": true,
13 | "moduleDetection": "force",
14 | "noEmit": true,
15 | "jsx": "react-jsx",
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true
22 | },
23 | "include": ["src"]
24 | }
25 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | { "path": "./tsconfig.app.json" },
5 | { "path": "./tsconfig.node.json" }
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "lib": ["ES2023"],
5 | "module": "ESNext",
6 | "skipLibCheck": true,
7 |
8 | /* Bundler mode */
9 | "moduleResolution": "bundler",
10 | "allowImportingTsExtensions": true,
11 | "isolatedModules": true,
12 | "moduleDetection": "force",
13 | "noEmit": true,
14 |
15 | /* Linting */
16 | "strict": true,
17 | "noUnusedLocals": true,
18 | "noUnusedParameters": true,
19 | "noFallthroughCasesInSwitch": true
20 | },
21 | "include": ["vite.config.ts"]
22 | }
23 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | optimizeDeps: {
8 | exclude: ['lucide-react'],
9 | },
10 | })
11 |
--------------------------------------------------------------------------------