diff --git a/bun.lock b/bun.lock index 35fd580..3d12f7c 100644 --- a/bun.lock +++ b/bun.lock @@ -20,6 +20,7 @@ "react-dom": "^19.1.0", "react-router": "^7.6.3", "sidebar": "^1.0.2", + "socket.io-client": "^4.8.1", "tailwind-merge": "^3.3.1", "tailwindcss": "^4.1.11", }, @@ -294,6 +295,8 @@ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.44.1", "", { "os": "win32", "cpu": "x64" }, "sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug=="], + "@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="], + "@tailwindcss/node": ["@tailwindcss/node@4.1.11", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.11" } }, "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q=="], "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.11", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.11", "@tailwindcss/oxide-darwin-arm64": "4.1.11", "@tailwindcss/oxide-darwin-x64": "4.1.11", "@tailwindcss/oxide-freebsd-x64": "4.1.11", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11", "@tailwindcss/oxide-linux-arm64-musl": "4.1.11", "@tailwindcss/oxide-linux-x64-gnu": "4.1.11", "@tailwindcss/oxide-linux-x64-musl": "4.1.11", "@tailwindcss/oxide-wasm32-wasi": "4.1.11", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11", "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" } }, "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg=="], @@ -428,6 +431,10 @@ "electron-to-chromium": ["electron-to-chromium@1.5.177", "", {}, "sha512-7EH2G59nLsEMj97fpDuvVcYi6lwTcM1xuWw3PssD8xzboAW7zj7iB3COEEEATUfjLHrs5uKBLQT03V/8URx06g=="], + "engine.io-client": ["engine.io-client@6.6.3", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", "ws": "~8.17.1", "xmlhttprequest-ssl": "~2.1.1" } }, "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w=="], + + "engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="], + "enhanced-resolve": ["enhanced-resolve@5.18.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ=="], "esbuild": ["esbuild@0.25.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.5", "@esbuild/android-arm": "0.25.5", "@esbuild/android-arm64": "0.25.5", "@esbuild/android-x64": "0.25.5", "@esbuild/darwin-arm64": "0.25.5", "@esbuild/darwin-x64": "0.25.5", "@esbuild/freebsd-arm64": "0.25.5", "@esbuild/freebsd-x64": "0.25.5", "@esbuild/linux-arm": "0.25.5", "@esbuild/linux-arm64": "0.25.5", "@esbuild/linux-ia32": "0.25.5", "@esbuild/linux-loong64": "0.25.5", "@esbuild/linux-mips64el": "0.25.5", "@esbuild/linux-ppc64": "0.25.5", "@esbuild/linux-riscv64": "0.25.5", "@esbuild/linux-s390x": "0.25.5", "@esbuild/linux-x64": "0.25.5", "@esbuild/netbsd-arm64": "0.25.5", "@esbuild/netbsd-x64": "0.25.5", "@esbuild/openbsd-arm64": "0.25.5", "@esbuild/openbsd-x64": "0.25.5", "@esbuild/sunos-x64": "0.25.5", "@esbuild/win32-arm64": "0.25.5", "@esbuild/win32-ia32": "0.25.5", "@esbuild/win32-x64": "0.25.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ=="], @@ -648,6 +655,10 @@ "sidebar": ["sidebar@1.0.2", "", { "dependencies": { "bootstrap": "^3.3.7" } }, "sha512-JvzZ6OLsvdVK0jo6jTHSfTApuHVHeAm3++UnAoHRpoVeTcW1nC7Xm8LRbRvtGXVSJsdeM6qfv5pgDZUXemgkDw=="], + "socket.io-client": ["socket.io-client@4.8.1", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.2", "engine.io-client": "~6.6.1", "socket.io-parser": "~4.2.4" } }, "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ=="], + + "socket.io-parser": ["socket.io-parser@4.2.4", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1" } }, "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew=="], + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], @@ -696,6 +707,10 @@ "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + "ws": ["ws@8.17.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ=="], + + "xmlhttprequest-ssl": ["xmlhttprequest-ssl@2.1.2", "", {}, "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ=="], + "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], @@ -728,12 +743,18 @@ "@typescript-eslint/typescript-estree/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + "engine.io-client/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "socket.io-client/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], + + "socket.io-parser/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], } } diff --git a/package.json b/package.json index 125158b..455c9fd 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "react-dom": "^19.1.0", "react-router": "^7.6.3", "sidebar": "^1.0.2", + "socket.io-client": "^4.8.1", "tailwind-merge": "^3.3.1", "tailwindcss": "^4.1.11" }, diff --git a/src/App.tsx b/src/App.tsx index a56f014..558620d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,6 +2,7 @@ import { AppLayout } from '@/components/AppLayout' import { ProtectedRoute } from '@/components/ProtectedRoute' import { Button } from '@/components/ui/button' import { AuthProvider } from '@/components/AuthProvider' +import { SocketProvider } from '@/contexts/SocketContext' import { AccountPage } from '@/pages/AccountPage' import { ActivityPage } from '@/pages/ActivityPage' import { AdminUsersPage } from '@/pages/AdminUsersPage' @@ -17,7 +18,8 @@ function App() { return ( - + + } /> } /> @@ -106,7 +108,8 @@ function App() { } /> } /> - + + ) diff --git a/src/components/sidebar/NavPlan.tsx b/src/components/sidebar/NavPlan.tsx index 66a887b..2e6bf51 100644 --- a/src/components/sidebar/NavPlan.tsx +++ b/src/components/sidebar/NavPlan.tsx @@ -1,5 +1,6 @@ import type { User } from "@/services/auth" import { SidebarMenu, SidebarMenuButton, SidebarMenuItem } from "../ui/sidebar" +import { useSocket } from "@/contexts/SocketContext" import NumberFlow from '@number-flow/react' import { useEffect, useState } from "react" @@ -9,11 +10,28 @@ interface NavPlanProps { export function NavPlan({ user }: NavPlanProps) { const [credits, setCredits] = useState(0) + const { socket, isConnected } = useSocket() useEffect(() => { setCredits(user.credits) }, [user]) + // Listen for real-time credits updates + useEffect(() => { + if (!socket || !isConnected) return + + const handleCreditsChanged = (data: { credits: number }) => { + setCredits(data.credits) + } + + socket.on("credits_changed", handleCreditsChanged) + + // Cleanup listener on unmount + return () => { + socket.off("credits_changed", handleCreditsChanged) + } + }, [socket, isConnected]) + return ( diff --git a/src/contexts/SocketContext.tsx b/src/contexts/SocketContext.tsx new file mode 100644 index 0000000..c4fbab5 --- /dev/null +++ b/src/contexts/SocketContext.tsx @@ -0,0 +1,109 @@ +import { createContext, useContext, useEffect, useState } from "react"; +import { io, Socket } from "socket.io-client"; +import { useAuth } from "@/hooks/use-auth"; + +interface SocketContextType { + socket: Socket | null; + isConnected: boolean; + connect: () => void; + disconnect: () => void; +} + +const SocketContext = createContext({ + socket: null, + isConnected: false, + connect: () => {}, + disconnect: () => {}, +}); + +export const useSocket = () => { + const context = useContext(SocketContext); + if (!context) { + throw new Error("useSocket must be used within a SocketProvider"); + } + return context; +}; + +interface SocketProviderProps { + children: React.ReactNode; +} + +export const SocketProvider: React.FC = ({ children }) => { + const [socket, setSocket] = useState(null); + const [isConnected, setIsConnected] = useState(false); + const { user, loading } = useAuth(); + + useEffect(() => { + // Create socket connection + const newSocket = io("http://localhost:5000", { + withCredentials: true, // Include cookies for authentication + autoConnect: false, // Don't connect automatically + transports: ["polling"], // Use polling only to avoid WebSocket issues + upgrade: false, // Disable WebSocket upgrade + }); + + // Set up event listeners + newSocket.on("connect", () => { + // Send authentication after connection + newSocket.emit("authenticate", {}); + }); + + newSocket.on("auth_success", () => { + setIsConnected(true); + }); + + newSocket.on("auth_error", () => { + setIsConnected(false); + newSocket.disconnect(); + }); + + newSocket.on("disconnect", () => { + setIsConnected(false); + }); + + newSocket.on("connect_error", () => { + setIsConnected(false); + }); + + setSocket(newSocket); + + // Clean up on unmount + return () => { + newSocket.close(); + }; + }, []); + + // Connect/disconnect based on authentication state + useEffect(() => { + if (!socket || loading) return; + + if (user && !isConnected) { + socket.connect(); + } else if (!user && isConnected) { + socket.disconnect(); + } + }, [socket, user, loading, isConnected]); + + const connect = () => { + if (socket && !socket.connected) { + socket.connect(); + } + }; + + const disconnect = () => { + if (socket && socket.connected) { + socket.disconnect(); + } + }; + + const value = { + socket, + isConnected, + connect, + disconnect, + }; + + return ( + {children} + ); +}; \ No newline at end of file