Skip to content

Commit c7f07ce

Browse files
Merge pull request Roshansuthar1105#187 from devbharu/main
Todo added
2 parents 71890eb + 71a1263 commit c7f07ce

12 files changed

Lines changed: 565 additions & 93 deletions

File tree

client/package-lock.json

Lines changed: 161 additions & 87 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"preview": "vite preview"
1111
},
1212
"dependencies": {
13-
"@codemirror/lang-cpp": "^6.0.2",
13+
"@codemirror/lang-cpp": "^6.0.3",
1414
"@codemirror/lang-javascript": "^6.2.2",
1515
"@codemirror/lang-python": "^6.1.6",
1616
"@codemirror/state": "^6.4.1",
@@ -20,6 +20,7 @@
2020
"axios": "^1.11.0",
2121
"dotenv": "^16.4.5",
2222
"framer-motion": "^12.23.12",
23+
"fuse.js": "^7.1.0",
2324
"gsap": "^3.13.0",
2425
"lucide-react": "^0.541.0",
2526
"react": "^18.3.1",

client/src/App.jsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useAuth } from "./store/auth";
44
import { useTheme } from "./context/ThemeContext";
55
import { LoadingProvider } from "./components/loadingContext.jsx";
66
import 'react-toastify/ReactToastify.css';
7-
7+
import { PopupProvider } from "./context/PopupContext.jsx";
88
// Always loaded components
99
import NavBar from "./components/NavBar";
1010
import Footer from "./components/Footer.jsx";
@@ -46,8 +46,8 @@ const AdminUpdate = lazy(() => import("./layouts/AdminUpdate"));
4646
const AddNewCourse = lazy(() => import("./layouts/CourseLayout/AddNewCourse.jsx"));
4747
const CourseUpdate = lazy(() => import("./layouts/CourseLayout/CourseUpdate"));
4848
const ForgotPassword = lazy(() => import("./pages/ForgotPassword"));
49-
const CodeEditor = lazy(() => import("./pages/CodeEditor"));
50-
49+
const CodeEditor = lazy(()=>import("./components/CodeEditor..jsx"))
50+
5151
const ScrollToTop = ({ children }) => {
5252
const location = useLocation();
5353

client/src/components/MobileMenu.jsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { FaBook, FaBookReader,FaBookmark , FaEnvelope, FaGraduationCap, FaHome,
66
import { useTheme } from '../context/ThemeContext';
77
import { FaCode } from "react-icons/fa";
88

9-
109
function MobileMenu({ isOpen, onClose, isLoggedIn, userdata }) {
1110
const { theme } = useTheme();
1211
const isDark = theme === 'dark';

client/src/components/TodoList.jsx

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import { useState } from "react";
2+
import { CheckCircle, Circle, Plus, Trash2, ListTodo } from "lucide-react";
3+
import { usePopup } from "../context/PopupContext";
4+
import { useAuth } from "../store/auth";
5+
import { motion, AnimatePresence } from "framer-motion";
6+
7+
export default function TodoList() {
8+
const { showTodo, closeAll, todos, addTodo, toggleTodoItem, deleteTodo } = usePopup();
9+
const { isLoggedIn } = useAuth();
10+
const [newTodo, setNewTodo] = useState("");
11+
const [open, setOpen] = useState(false); // widget toggle state
12+
13+
const handleAdd = () => {
14+
if (!isLoggedIn) return;
15+
if (newTodo.trim()) {
16+
addTodo(newTodo);
17+
setNewTodo("");
18+
}
19+
};
20+
21+
// Ensure todos is always an array
22+
const todoList = Array.isArray(todos) ? todos : [];
23+
24+
return (
25+
<>
26+
{/* Floating Toggle Button */}
27+
<motion.button
28+
onClick={() => setOpen((prev) => !prev)}
29+
whileHover={{ scale: 1.1 }}
30+
whileTap={{ scale: 0.9, rotate: 15 }}
31+
animate={{ y: [0, -4, 0] }}
32+
transition={{ repeat: Infinity, duration: 3, ease: "easeInOut" }}
33+
className="fixed bottom-4 right-4 z-50 p-4 rounded-full shadow-lg bg-primary text-white hover:bg-primary-dark transition"
34+
>
35+
<ListTodo size={24} />
36+
</motion.button>
37+
38+
{/* Todo Widget */}
39+
<AnimatePresence>
40+
{open && showTodo && (
41+
<motion.div
42+
key="todo-widget"
43+
initial={{ opacity: 0, scale: 0.8, y: 40 }}
44+
animate={{ opacity: 1, scale: 1, y: 0 }}
45+
exit={{ opacity: 0, scale: 0.8, y: 40 }}
46+
transition={{ duration: 0.35, ease: "easeOut" }}
47+
className="fixed bottom-20 right-4 z-50 w-80 max-w-full"
48+
>
49+
<motion.div
50+
layout
51+
className="p-4 rounded-2xl shadow-xl shadow-black/30 bg-white dark:bg-dark-bg-primary transition-colors"
52+
>
53+
{/* Header */}
54+
<motion.h2
55+
initial={{ opacity: 0, x: 20 }}
56+
animate={{ opacity: 1, x: 0 }}
57+
transition={{ delay: 0.1, duration: 0.3 }}
58+
className="text-xl font-bold mb-4 flex justify-between items-center text-black dark:text-dark-text-primary"
59+
>
60+
Todo List
61+
<span className="text-sm text-gray-600 dark:text-dark-text-secondary flex items-center gap-2">
62+
<CheckCircle className="text-success" />{" "}
63+
{todoList.filter((t) => t.completed).length} /{" "}
64+
{todoList.length}
65+
</span>
66+
</motion.h2>
67+
68+
{!isLoggedIn && (
69+
<p className="text-sm text-red-500 mb-2">
70+
Please log in to manage your todos.
71+
</p>
72+
)}
73+
74+
{/* Todos */}
75+
{todoList.length === 0 ? (
76+
<motion.p
77+
initial={{ opacity: 0 }}
78+
animate={{ opacity: 1 }}
79+
transition={{ delay: 0.2 }}
80+
className="text-sm text-gray-500 dark:text-dark-text-secondary text-center py-4"
81+
>
82+
No tasks available.
83+
</motion.p>
84+
) : (
85+
<ul className="space-y-2 max-h-64 overflow-y-auto">
86+
<AnimatePresence>
87+
{todoList.map((todo, idx) => (
88+
<motion.li
89+
key={todo._id || todo.id}
90+
initial={{ opacity: 0, x: 30 }}
91+
animate={{ opacity: 1, x: 0 }}
92+
exit={{ opacity: 0, x: -30 }}
93+
transition={{
94+
duration: 0.25,
95+
delay: idx * 0.05,
96+
type: "spring",
97+
stiffness: 120,
98+
}}
99+
layout
100+
className="flex items-center gap-2 p-2 border rounded border-gray-300 dark:border-dark-border"
101+
>
102+
<button
103+
disabled={!isLoggedIn}
104+
onClick={() =>
105+
toggleTodoItem(todo._id || todo.id)
106+
}
107+
>
108+
{todo.completed ? (
109+
<CheckCircle className="text-success" />
110+
) : (
111+
<Circle className="text-gray-500 dark:text-dark-text-secondary" />
112+
)}
113+
</button>
114+
<span
115+
className={`${
116+
todo.completed
117+
? "line-through text-gray-500 dark:text-dark-text-secondary"
118+
: "text-black dark:text-dark-text-primary"
119+
} flex-1`}
120+
>
121+
{todo.text}
122+
</span>
123+
<button
124+
disabled={!isLoggedIn}
125+
onClick={() => deleteTodo(todo._id || todo.id)}
126+
className="p-1 text-red-500 hover:text-red-700"
127+
>
128+
<Trash2 size={16} />
129+
</button>
130+
</motion.li>
131+
))}
132+
</AnimatePresence>
133+
</ul>
134+
)}
135+
136+
{/* Input + Add Button */}
137+
<motion.div
138+
initial={{ opacity: 0, y: 10 }}
139+
animate={{ opacity: 1, y: 0 }}
140+
transition={{ delay: 0.2 }}
141+
className="flex gap-2 mt-4"
142+
>
143+
<input
144+
type="text"
145+
value={newTodo}
146+
onChange={(e) => setNewTodo(e.target.value)}
147+
onKeyDown={(e) => {
148+
if (e.key === "Enter") handleAdd();
149+
}}
150+
placeholder={isLoggedIn ? "New task..." : "Login to add tasks"}
151+
disabled={!isLoggedIn}
152+
className="flex-1 p-2 border rounded border-gray-300 dark:border-dark-border bg-gray-50 dark:bg-dark-bg-secondary text-black dark:text-dark-text-primary"
153+
/>
154+
155+
<motion.button
156+
whileTap={{ scale: 0.9 }}
157+
onClick={handleAdd}
158+
disabled={!isLoggedIn}
159+
className={`p-2 rounded flex items-center ${
160+
isLoggedIn
161+
? "bg-primary text-white hover:bg-primary-dark"
162+
: "bg-gray-300 text-gray-600 cursor-not-allowed"
163+
}`}
164+
>
165+
<Plus size={18} />
166+
</motion.button>
167+
</motion.div>
168+
169+
{/* Close button */}
170+
<motion.div
171+
initial={{ opacity: 0 }}
172+
animate={{ opacity: 1 }}
173+
transition={{ delay: 0.25 }}
174+
className="flex justify-end mt-2"
175+
>
176+
<button
177+
onClick={() => setOpen(false)}
178+
className="text-sm text-gray-500 dark:text-dark-text-secondary hover:underline"
179+
>
180+
Close
181+
</button>
182+
</motion.div>
183+
</motion.div>
184+
</motion.div>
185+
)}
186+
</AnimatePresence>
187+
</>
188+
);
189+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import React, { createContext, useState, useContext, useEffect } from "react";
2+
import { useAuth } from "../store/auth";
3+
4+
const PopupContext = createContext(undefined);
5+
6+
const usePopup = () => {
7+
const ctx = useContext(PopupContext);
8+
if (!ctx) {
9+
throw new Error("usePopup must be used inside a PopupProvider");
10+
}
11+
return ctx;
12+
};
13+
14+
const PopupProvider = ({ children }) => {
15+
const { API, isLoggedIn } = useAuth();
16+
const token = localStorage.getItem("token");
17+
18+
19+
20+
// Popup state
21+
const [showTodo, setShowTodo] = useState(true);
22+
23+
24+
// Todo state
25+
const [todos, setTodos] = useState([]);
26+
27+
const headers = {
28+
"Content-Type": "application/json",
29+
Authorization: `Bearer ${token}`,
30+
};
31+
32+
useEffect(() => {
33+
if (!isLoggedIn) {
34+
setTodos([]);
35+
return;
36+
}
37+
fetch(`${API}/api/todos`, { headers })
38+
.then((res) => res.json())
39+
.then((data) => setTodos(data))
40+
.catch((err) => console.error("Failed to fetch todos:", err));
41+
}, [token, isLoggedIn]);
42+
43+
const addTodo = async (text) => {
44+
if (!isLoggedIn) return;
45+
try {
46+
const res = await fetch(`${API}/api/todos`, {
47+
method: "POST",
48+
headers,
49+
body: JSON.stringify({ text, completed: false }),
50+
});
51+
const saved = await res.json();
52+
setTodos((prev) => [...prev, saved]);
53+
} catch (err) {
54+
console.error("Failed to add todo:", err);
55+
}
56+
};
57+
58+
const toggleTodoItem = async (id) => {
59+
if (!isLoggedIn) return;
60+
const todo = todos.find((t) => t._id === id);
61+
if (!todo) return;
62+
try {
63+
const res = await fetch(`${API}/api/todos/${id}`, {
64+
method: "PATCH",
65+
headers,
66+
body: JSON.stringify({ completed: !todo.completed }),
67+
});
68+
const updated = await res.json();
69+
setTodos((prev) => prev.map((t) => (t._id === id ? updated : t)));
70+
} catch (err) {
71+
console.error("Failed to toggle todo:", err);
72+
}
73+
};
74+
75+
const deleteTodo = async (id) => {
76+
if (!isLoggedIn) return;
77+
try {
78+
await fetch(`${API}/api/todos/${id}`, { method: "DELETE", headers });
79+
setTodos((prev) => prev.filter((t) => t._id !== id));
80+
} catch (err) {
81+
console.error("Failed to delete todo:", err);
82+
}
83+
};
84+
85+
// Popup toggles
86+
const toggleTodo = () => {
87+
setShowTodo((prev) => !prev);
88+
if (!showTodo) setShowCalendar(false);
89+
};
90+
91+
92+
const closeAll = () => {
93+
setShowTodo(false);
94+
setShowCalendar(false);
95+
};
96+
97+
return (
98+
<PopupContext.Provider
99+
value={{
100+
showTodo,
101+
toggleTodo,
102+
closeAll,
103+
todos,
104+
setTodos,
105+
addTodo,
106+
toggleTodoItem,
107+
deleteTodo,
108+
}}
109+
>
110+
{children}
111+
</PopupContext.Provider>
112+
);
113+
};
114+
115+
export { PopupProvider, usePopup };

client/src/main.jsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ import "./index.css";
55
import { AuthProvider } from "./store/auth.jsx";
66
import { ThemeProvider } from "./context/ThemeContext.jsx";
77
import { ToastContainer } from "react-toastify";
8+
import {PopupProvider} from "./context/PopupContext.jsx"
89
// import "react-toastify/dist/ReactToastify.css";
910

1011
ReactDOM.createRoot(document.getElementById("root")).render(
1112
<React.StrictMode>
1213
<ThemeProvider>
1314
<AuthProvider>
15+
<PopupProvider>
1416
<App />
1517
<ToastContainer
1618
position="top-right"
@@ -24,6 +26,7 @@ ReactDOM.createRoot(document.getElementById("root")).render(
2426
pauseOnHover
2527
theme="dark"
2628
/>
29+
</PopupProvider>
2730
</AuthProvider>
2831
</ThemeProvider>
2932
</React.StrictMode>

client/src/pages/Home.jsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ const Contribution = lazy(() =>
2929
import("../components/HomePageComponents/Contributor")
3030
)
3131

32+
const Todo = lazy(()=> import("../components/TodoList"))
33+
3234
function Home() {
3335
const { theme } = useTheme();
3436
const isDark = theme === "dark";
@@ -1014,6 +1016,16 @@ function Home() {
10141016
}
10151017
>
10161018
<Contribution />
1019+
1020+
</Suspense>
1021+
<Suspense
1022+
fallback={
1023+
<div className="flex items-center justify-center h-40">
1024+
<div className="animate-spin rounded-full h-10 w-10 border-t-2 border-b-2 border-primary"></div>
1025+
</div>
1026+
}
1027+
>
1028+
<Todo/>
10171029
</Suspense>
10181030
</div>
10191031
);

0 commit comments

Comments
 (0)