Skip to content
This repository was archived by the owner on May 14, 2025. It is now read-only.

Commit 1f6b6dd

Browse files
committed
Fix socketService types
1 parent 582d242 commit 1f6b6dd

File tree

11 files changed

+466
-95
lines changed

11 files changed

+466
-95
lines changed

.husky/pre-commit

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1 @@
1-
#!/usr/bin/env sh
2-
. "$(dirname -- "$0")/_/husky.sh"
3-
4-
pnpm lint-staged
1+
pnpm lint-staged

package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"name": "todo-app-monorepo",
33
"private": true,
44
"version": "1.0.0",
5+
"type": "module",
56
"scripts": {
67
"dev": "pnpm -r --parallel dev",
78
"build": "pnpm -r build",
@@ -14,7 +15,13 @@
1415
"typescript": "~5.7.2",
1516
"husky": "^9.0.6",
1617
"lint-staged": "^15.2.0",
17-
"@playwright/test": "^1.40.0"
18+
"@playwright/test": "^1.40.0",
19+
"eslint": "^9.22.0",
20+
"@eslint/js": "^9.22.0",
21+
"globals": "^16.0.0",
22+
"typescript-eslint": "^8.26.1",
23+
"eslint-plugin-react-hooks": "^5.2.0",
24+
"eslint-plugin-react-refresh": "^0.4.19"
1825
},
1926
"lint-staged": {
2027
"*.{ts,tsx}": [

packages/client/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"test:coverage": "jest --coverage"
1414
},
1515
"dependencies": {
16+
"socket.io-client": "^4.7.2",
1617
"@tailwindcss/vite": "^4.1.4",
1718
"react": "^19.0.0",
1819
"react-dom": "^19.0.0",

packages/client/src/App.tsx

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,23 @@ import { Todo } from "../../shared/src";
33
import TodoItem from "./components/TodoItem";
44
import CircleProgress from "./components/CircleProgress";
55
import { getTodos, createTodo, updateTodo, deleteTodo } from "./api/todoApi";
6+
import socketService from "./services/socketService";
67

78
function App() {
89
const [todos, setTodos] = useState<Todo[]>([]);
910
const [input, setInput] = useState("");
1011
const [loading, setLoading] = useState(true);
1112
const [error, setError] = useState<string | null>(null);
13+
const [onlineUsers, setOnlineUsers] = useState<number>(1);
1214

13-
// Fetch todos on component mount
15+
// Calculate progress
16+
const totalTodos = todos.length;
17+
const completedTodos = todos.filter(todo => todo.completed).length;
18+
const progressPercentage = totalTodos === 0 ? 0 : (completedTodos / totalTodos) * 100;
19+
20+
// Connect to WebSocket on component mount
1421
useEffect(() => {
22+
// Initial fetch of todos
1523
const fetchTodos = async () => {
1624
try {
1725
setLoading(true);
@@ -27,14 +35,32 @@ function App() {
2735
};
2836

2937
fetchTodos();
38+
39+
// Connect to WebSocket
40+
socketService.connect();
41+
42+
// Listen for todo updates
43+
const unsubscribe = socketService.on('todos:update', (updatedTodos: Todo[]) => {
44+
setTodos(updatedTodos);
45+
});
46+
47+
// Listen for user count updates
48+
socketService.on('users:count', (count: number) => {
49+
setOnlineUsers(count);
50+
});
51+
52+
// Cleanup on unmount
53+
return () => {
54+
unsubscribe();
55+
socketService.disconnect();
56+
};
3057
}, []);
3158

3259
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
3360
e.preventDefault();
3461
if (input.trim()) {
3562
try {
36-
const newTodo = await createTodo(input.trim());
37-
setTodos((prev) => [...prev, newTodo]);
63+
await createTodo(input.trim());
3864
setInput("");
3965
} catch (err) {
4066
setError("Failed to create todo. Please try again.");
@@ -48,8 +74,7 @@ function App() {
4874
const todoToUpdate = todos.find(todo => todo.id === id);
4975
if (!todoToUpdate) return;
5076

51-
const updatedTodo = await updateTodo(id, { completed: !todoToUpdate.completed });
52-
setTodos(todos.map(todo => todo.id === id ? updatedTodo : todo));
77+
await updateTodo(id, { completed: !todoToUpdate.completed });
5378
} catch (err) {
5479
setError("Failed to update todo. Please try again.");
5580
console.error(err);
@@ -59,24 +84,18 @@ function App() {
5984
const removeTodo = async (id: string) => {
6085
try {
6186
await deleteTodo(id);
62-
setTodos(todos.filter(todo => todo.id !== id));
6387
} catch (err) {
6488
setError("Failed to delete todo. Please try again.");
6589
console.error(err);
6690
}
6791
};
6892

69-
// Calculate stats
70-
const totalTodos = todos.length;
71-
const completedTodos = todos.filter(todo => todo.completed).length;
72-
const progressPercentage = totalTodos === 0 ? 0 : (completedTodos / totalTodos) * 100;
73-
7493
return (
7594
<div className="min-h-screen bg-zinc-900 py-8">
7695
{/* Hero Section */}
7796
<div className="max-w-md mx-auto bg-zinc-800 rounded-xl shadow-lg overflow-hidden mb-6">
7897
<div className="p-6">
79-
<h1 className="text2xl font-bold text-center text-white mb-6">The world's most complex todo app</h1>
98+
<h1 className="text-2xl font-bold text-center text-white mb-6">The world's most complex todo app</h1>
8099

81100
<div className="flex items-center justify-between">
82101
<div className="space-y-2">
@@ -91,6 +110,10 @@ function App() {
91110
? "All done! 🎉"
92111
: `${totalTodos - completedTodos} remaining`}
93112
</p>
113+
<div className="text-xs text-zinc-500 mt-2">
114+
<span className="inline-block w-2 h-2 bg-green-500 rounded-full mr-1"></span>
115+
{onlineUsers} user{onlineUsers !== 1 ? 's' : ''} online
116+
</div>
94117
</div>
95118

96119
<CircleProgress percentage={progressPercentage} />
@@ -150,4 +173,4 @@ function App() {
150173
);
151174
}
152175

153-
export default App;
176+
export default App;
Lines changed: 66 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,70 @@
1+
import { useState, useEffect } from 'react';
2+
13
interface CircleProgressProps {
2-
percentage: number;
3-
}
4+
percentage: number;
5+
}
6+
7+
const CircleProgress = ({ percentage }: CircleProgressProps) => {
8+
const [prevPercentage, setPrevPercentage] = useState(percentage);
9+
const [isPulsing, setIsPulsing] = useState(false);
410

5-
const CircleProgress = ({ percentage }: CircleProgressProps) => {
6-
// Calculate the circumference of the circle
7-
const radius = 40;
8-
const circumference = 2 * Math.PI * radius;
9-
10-
// Calculate the stroke-dashoffset based on the percentage
11-
const offset = circumference - (percentage / 100) * circumference;
11+
// Calculate the circumference of the circle
12+
const radius = 40;
13+
const circumference = 2 * Math.PI * radius;
1214

13-
return (
14-
<div className="relative inline-flex items-center justify-center">
15-
<svg className="w-24 h-24" viewBox="0 0 100 100">
16-
{/* Background circle */}
17-
<circle
18-
cx="50"
19-
cy="50"
20-
r={radius}
21-
className="stroke-zinc-700 fill-none"
22-
strokeWidth="8"
23-
/>
24-
25-
{/* Progress circle */}
26-
<circle
27-
cx="50"
28-
cy="50"
29-
r={radius}
30-
className={`${percentage == 100 ? "stroke-green-500" : "stroke-blue-500"} fill-none transition-all duration-500 ease-in-out`}
31-
strokeWidth="8"
32-
strokeLinecap="round"
33-
strokeDasharray={circumference}
34-
strokeDashoffset={offset}
35-
transform="rotate(-90 50 50)"
36-
/>
37-
</svg>
38-
39-
{/* Percentage text */}
40-
<div className="absolute flex flex-col items-center justify-center">
41-
<span className="text-xl font-bold text-white transition-all duration-300">{Math.round(percentage)}%</span>
42-
</div>
43-
</div>
44-
);
45-
};
15+
// Calculate the stroke-dashoffset based on the percentage
16+
const offset = circumference - (percentage / 100) * circumference;
4617

47-
export default CircleProgress;
18+
// Add pulse animation when percentage changes
19+
useEffect(() => {
20+
if (percentage !== prevPercentage) {
21+
setIsPulsing(true);
22+
const timer = setTimeout(() => {
23+
setIsPulsing(false);
24+
setPrevPercentage(percentage);
25+
}, 1000);
26+
return () => clearTimeout(timer);
27+
}
28+
}, [percentage, prevPercentage]);
29+
30+
return (
31+
<div className="relative inline-flex items-center justify-center">
32+
<svg className={`w-24 h-24 ${isPulsing ? 'scale-110 transition-transform duration-300' : 'transition-transform duration-500'}`} viewBox="0 0 100 100">
33+
{/* Background circle */}
34+
<circle
35+
cx="50"
36+
cy="50"
37+
r={radius}
38+
className="stroke-zinc-700 fill-none"
39+
strokeWidth="8"
40+
/>
41+
42+
{/* Progress circle */}
43+
<circle
44+
cx="50"
45+
cy="50"
46+
r={radius}
47+
className={`${
48+
percentage === 100
49+
? "stroke-green-500"
50+
: percentage > 50
51+
? "stroke-amber-300"
52+
: "stroke-red-500"
53+
} fill-none transition-all duration-500 ease-in-out`}
54+
strokeWidth="8"
55+
strokeLinecap="round"
56+
strokeDasharray={circumference}
57+
strokeDashoffset={offset}
58+
transform="rotate(-90 50 50)"
59+
/>
60+
</svg>
61+
62+
{/* Percentage text */}
63+
<div className="absolute flex flex-col items-center justify-center">
64+
<span className="text-xl font-bold text-white transition-all duration-300">{Math.round(percentage)}%</span>
65+
</div>
66+
</div>
67+
);
68+
};
69+
70+
export default CircleProgress;

packages/client/src/components/TodoItem.tsx

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { Todo } from "../../../shared/src";
1+
import { useState, useEffect } from 'react';
2+
import { Todo } from '../../../shared/src';
23

34
interface TodoItemProps {
45
todo: Todo;
@@ -7,38 +8,47 @@ interface TodoItemProps {
78
}
89

910
const TodoItem = ({ todo, onToggle, onDelete }: TodoItemProps) => {
11+
const [isNew, setIsNew] = useState(false);
12+
const [isUpdated, setIsUpdated] = useState(false);
13+
14+
// Show animation when a todo is new or updated
15+
useEffect(() => {
16+
// Check if this is a new todo (created in the last 2 seconds)
17+
const createdAt = new Date(todo.createdAt).getTime();
18+
const now = Date.now();
19+
20+
if (now - createdAt < 2000) {
21+
setIsNew(true);
22+
setTimeout(() => setIsNew(false), 2000);
23+
} else {
24+
// If not new, it might be an update
25+
setIsUpdated(true);
26+
setTimeout(() => setIsUpdated(false), 1000);
27+
}
28+
}, [todo]);
29+
1030
return (
11-
<li className="py-3 flex items-center gap-3">
12-
<div className="relative flex items-center">
31+
<li className={`py-3 flex items-center justify-between transition-all duration-300 ${
32+
isNew ? 'bg-green-900/20' : isUpdated ? 'bg-blue-900/20' : ''
33+
}`}>
34+
<div className="flex items-center">
1335
<input
1436
type="checkbox"
15-
id={`todo-${todo.id}`}
1637
checked={todo.completed}
1738
onChange={() => onToggle(todo.id)}
18-
className="peer h-5 w-5 cursor-pointer appearance-none rounded border-2 border-zinc-600 bg-zinc-800 transition-colors checked:border-blue-500 checked:bg-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/30"
39+
className="h-5 w-5 rounded border-zinc-600 text-blue-500 focus:ring-blue-500 focus:ring-offset-zinc-800"
1940
/>
20-
<svg
21-
className="pointer-events-none absolute left-0.5 top-0.5 h-4 w-4 opacity-0 text-white peer-checked:opacity-100"
22-
xmlns="http://www.w3.org/2000/svg"
23-
viewBox="0 0 24 24"
24-
fill="none"
25-
stroke="currentColor"
26-
strokeWidth="3"
27-
strokeLinecap="round"
28-
strokeLinejoin="round"
29-
>
30-
<polyline points="20 6 9 17 4 12"></polyline>
31-
</svg>
41+
<span className={`ml-3 text-white ${todo.completed ? 'line-through text-zinc-500' : ''}`}>
42+
{todo.text}
43+
</span>
3244
</div>
33-
<span className={`${todo.completed ? "line-through text-zinc-500" : "text-zinc-200"}`}>
34-
{todo.text}
35-
</span>
36-
<button
45+
<button
3746
onClick={() => onDelete(todo.id)}
38-
className="ml-auto text-red-500 hover:text-red-400 transition-colors"
39-
aria-label="Delete todo"
47+
className="text-zinc-400 hover:text-red-500 transition-colors"
4048
>
41-
Delete
49+
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
50+
<path fillRule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clipRule="evenodd" />
51+
</svg>
4252
</button>
4353
</li>
4454
);

0 commit comments

Comments
 (0)