├── 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 |

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 |
--------------------------------------------------------------------------------