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