feat: add SocketProvider and integrate real-time credits updates in NavPlan component
This commit is contained in:
21
bun.lock
21
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=="],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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 (
|
||||
<ThemeProvider defaultTheme="dark" storageKey="theme">
|
||||
<AuthProvider>
|
||||
<Router>
|
||||
<SocketProvider>
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/register" element={<RegisterPage />} />
|
||||
@@ -106,7 +108,8 @@ function App() {
|
||||
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
</Router>
|
||||
</SocketProvider>
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
)
|
||||
|
||||
@@ -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 (
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
|
||||
109
src/contexts/SocketContext.tsx
Normal file
109
src/contexts/SocketContext.tsx
Normal file
@@ -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<SocketContextType>({
|
||||
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<SocketProviderProps> = ({ children }) => {
|
||||
const [socket, setSocket] = useState<Socket | null>(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 (
|
||||
<SocketContext.Provider value={value}>{children}</SocketContext.Provider>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user