├── README.md ├── src └── app │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ └── page.tsx ├── next.config.ts ├── postcss.config.mjs ├── next-env.d.ts ├── eslint.config.mjs ├── tailwind.config.ts ├── tsconfig.json └── package.json /README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mshsheikh/Task-Manager-NextJS-TailwindCSS-TurboPack/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --background: #ffffff; 7 | --foreground: #171717; 8 | } 9 | 10 | @media (prefers-color-scheme: dark) { 11 | :root { 12 | --background: #0a0a0a; 13 | --foreground: #ededed; 14 | } 15 | } 16 | 17 | body { 18 | color: var(--foreground); 19 | background: var(--background); 20 | font-family: Arial, Helvetica, sans-serif; 21 | } 22 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | ]; 15 | 16 | export default eslintConfig; 17 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | export default { 4 | content: [ 5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | colors: { 12 | background: "var(--background)", 13 | foreground: "var(--foreground)", 14 | }, 15 | }, 16 | }, 17 | plugins: [], 18 | } satisfies Config; 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "task-manager-mshsheikh", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "next": "15.1.0", 13 | "react": "^19.0.0", 14 | "react-dom": "^19.0.0", 15 | "react-icons": "^5.4.0", 16 | "react-toastify": "^10.0.6" 17 | }, 18 | "devDependencies": { 19 | "@eslint/eslintrc": "^3", 20 | "@types/node": "^20", 21 | "@types/react": "^19", 22 | "@types/react-dom": "^19", 23 | "eslint": "^9", 24 | "eslint-config-next": "15.1.0", 25 | "postcss": "^8", 26 | "tailwindcss": "^3.4.1", 27 | "typescript": "^5" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Geist, Geist_Mono } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | const geistSans = Geist({ 6 | variable: "--font-geist-sans", 7 | subsets: ["latin"], 8 | }); 9 | 10 | const geistMono = Geist_Mono({ 11 | variable: "--font-geist-mono", 12 | subsets: ["latin"], 13 | }); 14 | 15 | export const metadata: Metadata = { 16 | title: "Task Manager", 17 | description: "Created by Muhammad Salman Hussain", 18 | }; 19 | 20 | export default function RootLayout({ 21 | children, 22 | }: Readonly<{ 23 | children: React.ReactNode; 24 | }>) { 25 | return ( 26 | 27 | 30 | {children} 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @next/next/no-img-element */ 2 | "use client"; 3 | 4 | import { useState, useRef, useEffect } from "react"; 5 | import { nanoid } from "nanoid"; 6 | import { ToastContainer, toast } from "react-toastify"; 7 | import "react-toastify/dist/ReactToastify.css"; 8 | import { FaFacebook, FaLinkedin } from "react-icons/fa"; 9 | 10 | type Task = { 11 | title: string; 12 | id: string; 13 | timer: number; 14 | }; 15 | 16 | export default function Home() { 17 | const [tasks, setTasks] = useState([]); 18 | const [editingTaskId, setEditingTaskId] = useState(null); 19 | const [editedTitle, setEditedTitle] = useState(""); 20 | const [editedTimer, setEditedTimer] = useState("10:00"); 21 | const inputReference = useRef(null); 22 | 23 | const timeStringToSeconds = (time: string) => { 24 | const [minutes, seconds] = time.split(":").map(Number); 25 | return minutes * 60 + seconds; 26 | }; 27 | 28 | const secondsToTimeString = (seconds: number) => { 29 | const minutes = Math.floor(seconds / 60); 30 | const secs = seconds % 60; 31 | return `${minutes.toString().padStart(2, "0")}:${secs 32 | .toString() 33 | .padStart(2, "0")}`; 34 | }; 35 | 36 | const handleAddTask = () => { 37 | const inputValue = inputReference?.current?.value as string; 38 | if (inputValue && /^([0-9]{1,2}):([0-9]{2})$/.test(editedTimer)) { 39 | const newTask: Task = { 40 | title: inputValue, 41 | id: nanoid(), 42 | timer: timeStringToSeconds(editedTimer), 43 | }; 44 | 45 | setTasks([newTask, ...tasks]); 46 | if (inputReference.current) { 47 | inputReference.current.value = ""; 48 | } 49 | 50 | toast.success("Task added successfully!", { 51 | position: "top-center", 52 | autoClose: 3000, 53 | hideProgressBar: true, 54 | closeOnClick: true, 55 | pauseOnHover: true, 56 | draggable: true, 57 | progress: undefined, 58 | }); 59 | } else { 60 | toast.warning("Please enter a task and set a valid timer (HH:mm).", { 61 | position: "top-center", 62 | autoClose: 3000, 63 | hideProgressBar: true, 64 | closeOnClick: true, 65 | pauseOnHover: true, 66 | draggable: true, 67 | progress: undefined, 68 | }); 69 | } 70 | }; 71 | 72 | const handleEditTask = (task: Task) => { 73 | setEditingTaskId(task.id); 74 | setEditedTitle(task.title); 75 | setEditedTimer(secondsToTimeString(task.timer)); 76 | }; 77 | 78 | const handleSaveEdit = (taskId: string) => { 79 | setTasks( 80 | tasks.map((task) => 81 | task.id === taskId 82 | ? { 83 | ...task, 84 | title: editedTitle, 85 | timer: timeStringToSeconds(editedTimer), 86 | } 87 | : task 88 | ) 89 | ); 90 | setEditingTaskId(null); 91 | setEditedTitle(""); 92 | setEditedTimer("10:00"); 93 | }; 94 | 95 | const handleDeleteTask = (taskId: string) => { 96 | setTasks(tasks.filter((task) => task.id !== taskId)); 97 | toast.error("Task deleted successfully!", { 98 | position: "top-center", 99 | autoClose: 3000, 100 | hideProgressBar: true, 101 | closeOnClick: true, 102 | pauseOnHover: true, 103 | draggable: true, 104 | progress: undefined, 105 | className: "bg-red-600 text-white", 106 | }); 107 | }; 108 | 109 | useEffect(() => { 110 | const timers = tasks.map((task) => { 111 | if (task.timer > 0) { 112 | const intervalId = setInterval(() => { 113 | setTasks((prevTasks) => { 114 | const updatedTasks = prevTasks.map((t) => { 115 | if (t.id === task.id && t.timer > 0) { 116 | const updatedTimer = t.timer - 1; 117 | if (updatedTimer === 0) { 118 | toast.info(`${t.title} timer expired!`, { 119 | position: "top-center", 120 | autoClose: 3000, 121 | hideProgressBar: true, 122 | closeOnClick: true, 123 | pauseOnHover: true, 124 | draggable: true, 125 | progress: undefined, 126 | }); 127 | } 128 | return { ...t, timer: updatedTimer }; 129 | } 130 | return t; 131 | }); 132 | return updatedTasks; 133 | }); 134 | }, 1000); 135 | 136 | return intervalId; 137 | } 138 | return null; 139 | }); 140 | 141 | return () => { 142 | timers.forEach((timer) => { 143 | if (timer) { 144 | clearInterval(timer); 145 | } 146 | }); 147 | }; 148 | }, [tasks]); 149 | 150 | const handleKeyDown = (e: React.KeyboardEvent) => { 151 | if (e.key === "Enter") { 152 | handleAddTask(); 153 | } 154 | }; 155 | 156 | return ( 157 |
158 |
159 |
160 | Task Manager Icon 165 |
166 | 167 |

171 | Task Manager 172 |

173 | 174 | {/* Task Input */} 175 |
176 | 182 | setEditedTimer(e.target.value)} 186 | className="sm:w-[100px] md:w-[100px] lg:w-[120px] p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm text-black" 187 | placeholder="Timer (HH:mm)" 188 | /> 189 | 195 |
196 | 197 | {/* Task List */} 198 |
    199 | {tasks.map((elem: Task) => ( 200 |
  • 204 | {/* Task centered on small screens */} 205 |
    206 | {editingTaskId === elem.id ? ( 207 | setEditedTitle(e.target.value)} 211 | className="w-full p-3 border border-gray-300 rounded-lg text-sm text-black" 212 | /> 213 | ) : ( 214 | 215 | {elem.title} 216 | 217 | )} 218 |
    219 | 220 | {/* Timer */} 221 |
    222 | 223 | {secondsToTimeString(elem.timer)} 224 | 225 |
    226 | 227 | {/* Edit & Delete Buttons */} 228 |
    229 | {editingTaskId === elem.id ? ( 230 | 236 | ) : ( 237 | 243 | )} 244 | 245 | 264 |
    265 |
  • 266 | ))} 267 |
268 | 269 |
270 | 271 | Made with ❤️ by Muhammad Salman Hussain 272 | 273 |
274 | 275 |
276 | 277 | Follow for more insights! 278 | 279 |
280 | 281 | {/* Social Icons */} 282 | 298 |
299 | 300 | 310 |
311 | ); 312 | } 313 | --------------------------------------------------------------------------------