live action
This commit is contained in:
@@ -32,6 +32,29 @@ const io = new Server(server, {
|
||||
}
|
||||
});
|
||||
|
||||
let counter = 0;
|
||||
let counterTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
const broadcastCounter = () => {
|
||||
io.emit("counter:update", { value: counter });
|
||||
};
|
||||
|
||||
const startCounter = () => {
|
||||
if (counterTimer) return;
|
||||
counterTimer = setInterval(() => {
|
||||
counter += 1;
|
||||
broadcastCounter();
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const stopCounter = () => {
|
||||
if (counterTimer) {
|
||||
clearInterval(counterTimer);
|
||||
counterTimer = null;
|
||||
}
|
||||
io.emit("counter:stopped", { value: counter });
|
||||
};
|
||||
|
||||
io.use((socket, next) => {
|
||||
const token = socket.handshake.auth?.token as string | undefined;
|
||||
if (!token) {
|
||||
@@ -51,6 +74,20 @@ io.on("connection", (socket) => {
|
||||
socket.on("ping", () => {
|
||||
socket.emit("pong", "pong");
|
||||
});
|
||||
|
||||
socket.on("counter:start", (ack?: (payload: { running: boolean; value: number }) => void) => {
|
||||
startCounter();
|
||||
ack?.({ running: true, value: counter });
|
||||
});
|
||||
|
||||
socket.on("counter:stop", (ack?: (payload: { running: boolean; value: number }) => void) => {
|
||||
stopCounter();
|
||||
ack?.({ running: false, value: counter });
|
||||
});
|
||||
|
||||
socket.on("counter:status", (ack?: (payload: { running: boolean; value: number }) => void) => {
|
||||
ack?.({ running: !!counterTimer, value: counter });
|
||||
});
|
||||
});
|
||||
|
||||
async function start() {
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
import { Navigate, Route, Routes } from "react-router-dom";
|
||||
import { LoginPage } from "./pages/LoginPage";
|
||||
import { AdminPage } from "./pages/AdminPage";
|
||||
import { ProtectedRoute } from "./components/ProtectedRoute";
|
||||
import { DashboardLayout } from "./components/DashboardLayout";
|
||||
import { HomePage } from "./pages/HomePage";
|
||||
import { JobsPage } from "./pages/JobsPage";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route element={<ProtectedRoute />}>
|
||||
<Route path="/admin" element={<AdminPage />} />
|
||||
<Route path="/jobs" element={<AdminPage />} />
|
||||
<Route element={<DashboardLayout />}>
|
||||
<Route path="/home" element={<HomePage />} />
|
||||
<Route path="/jobs" element={<JobsPage />} />
|
||||
<Route path="*" element={<Navigate to="/home" replace />} />
|
||||
</Route>
|
||||
</Route>
|
||||
<Route path="*" element={<Navigate to="/admin" replace />} />
|
||||
<Route path="*" element={<Navigate to="/home" replace />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
|
||||
98
frontend/src/components/DashboardLayout.tsx
Normal file
98
frontend/src/components/DashboardLayout.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { NavLink, Outlet, useNavigate } from "react-router-dom";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faHouse, faBriefcase, faArrowRightFromBracket, faUser } from "@fortawesome/free-solid-svg-icons";
|
||||
import { Button } from "./ui/button";
|
||||
import { ThemeToggle } from "./ThemeToggle";
|
||||
import { useAuth } from "../providers/auth-provider";
|
||||
import { cn } from "../lib/utils";
|
||||
import { apiClient } from "../api/client";
|
||||
|
||||
export function DashboardLayout() {
|
||||
const { user, token, logout } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [isLoggingOut, setIsLoggingOut] = useState(false);
|
||||
|
||||
const navigation = useMemo(
|
||||
() => [
|
||||
{ label: "Home", to: "/home", icon: faHouse },
|
||||
{ label: "Jobs", to: "/jobs", icon: faBriefcase }
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
const socketUrl = useMemo(() => apiClient.defaults.baseURL || window.location.origin, []);
|
||||
|
||||
const handleLogout = () => {
|
||||
setIsLoggingOut(true);
|
||||
logout();
|
||||
navigate("/login", { replace: true });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
<div className="flex min-h-screen">
|
||||
<aside className="hidden w-64 flex-col border-r border-border bg-card/40 md:flex">
|
||||
<div className="flex h-16 items-center border-b border-border px-6">
|
||||
<span className="text-lg font-semibold tracking-tight">Wisecolt CI</span>
|
||||
</div>
|
||||
<nav className="flex-1 space-y-1 px-3 py-4">
|
||||
{navigation.map((item) => (
|
||||
<NavLink
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
"flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition",
|
||||
isActive
|
||||
? "bg-primary text-primary-foreground shadow-sm"
|
||||
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
||||
)
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={item.icon} className="h-4 w-4" />
|
||||
<span>{item.label}</span>
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
<div className="mt-auto space-y-3 border-t border-border px-4 py-4">
|
||||
<div className="flex gap-3">
|
||||
<ThemeToggle size="icon" className="h-10 w-10 justify-center" />
|
||||
<div className="flex h-10 flex-1 items-center gap-3 rounded-md border border-border bg-background px-3">
|
||||
<FontAwesomeIcon icon={faUser} className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium text-foreground">{user?.username ?? "-"}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-center gap-2"
|
||||
onClick={handleLogout}
|
||||
disabled={isLoggingOut}
|
||||
>
|
||||
<FontAwesomeIcon icon={faArrowRightFromBracket} className="h-4 w-4" />
|
||||
Çıkış Yap
|
||||
</Button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main className="flex-1">
|
||||
<div className="mx-auto flex max-w-5xl flex-col gap-6 px-4 py-8 sm:px-6 lg:px-8">
|
||||
<header className="flex items-center justify-between rounded-md border border-border bg-card/60 px-4 py-3">
|
||||
<div>
|
||||
<div className="text-sm uppercase tracking-wide text-muted-foreground">Proje</div>
|
||||
<div className="text-lg font-semibold text-foreground">Wisecolt CI</div>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Bağlantı: <span className="font-mono text-foreground">{socketUrl}</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="grid gap-6">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,16 +5,22 @@ import App from "./App";
|
||||
import "./index.css";
|
||||
import { ThemeProvider } from "./providers/theme-provider";
|
||||
import { AuthProvider } from "./providers/auth-provider";
|
||||
import { SocketProvider } from "./providers/socket-provider";
|
||||
import { LiveProvider } from "./providers/live-provider";
|
||||
import { Toaster } from "./components/ui/toaster";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<ThemeProvider>
|
||||
<AuthProvider>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
<Toaster />
|
||||
</BrowserRouter>
|
||||
<SocketProvider>
|
||||
<LiveProvider>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
<Toaster />
|
||||
</BrowserRouter>
|
||||
</LiveProvider>
|
||||
</SocketProvider>
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
</React.StrictMode>
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { NavLink, useNavigate } from "react-router-dom";
|
||||
import { io, Socket } from "socket.io-client";
|
||||
import { toast } from "sonner";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faHouse, faBriefcase, faArrowRightFromBracket, faUser } from "@fortawesome/free-solid-svg-icons";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../components/ui/card";
|
||||
import { ThemeToggle } from "../components/ThemeToggle";
|
||||
import { useAuth } from "../providers/auth-provider";
|
||||
import { apiClient } from "../api/client";
|
||||
import { cn } from "../lib/utils";
|
||||
|
||||
export function AdminPage() {
|
||||
const { user, token, logout } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [socket, setSocket] = useState<Socket | null>(null);
|
||||
const [messages, setMessages] = useState<string[]>([]);
|
||||
|
||||
const socketUrl = useMemo(() => apiClient.defaults.baseURL || window.location.origin, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) return;
|
||||
const newSocket = io(socketUrl, {
|
||||
auth: { token },
|
||||
transports: ["websocket", "polling"]
|
||||
});
|
||||
|
||||
newSocket.on("connect", () => setMessages((prev) => [...prev, "Socket bağlandı"]));
|
||||
newSocket.on("hello", (msg: string) => setMessages((prev) => [...prev, msg]));
|
||||
newSocket.on("pong", (msg: string) => setMessages((prev) => [...prev, msg]));
|
||||
newSocket.on("connect_error", (err) => {
|
||||
setMessages((prev) => [...prev, `Hata: ${err.message}`]);
|
||||
toast.error("Socket bağlantısı başarısız");
|
||||
});
|
||||
|
||||
setSocket(newSocket);
|
||||
|
||||
return () => {
|
||||
newSocket.disconnect();
|
||||
};
|
||||
}, [token, socketUrl]);
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
navigate("/login", { replace: true });
|
||||
};
|
||||
|
||||
const sendPing = () => {
|
||||
if (!socket) return;
|
||||
socket.emit("ping");
|
||||
};
|
||||
|
||||
const navigation = [
|
||||
{ label: "Home", to: "/admin", icon: faHouse },
|
||||
{ label: "Jobs", to: "/jobs", icon: faBriefcase }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
<div className="flex min-h-screen">
|
||||
<aside className="hidden w-64 flex-col border-r border-border bg-card/40 md:flex">
|
||||
<div className="flex h-16 items-center border-b border-border px-6">
|
||||
<span className="text-lg font-semibold tracking-tight">Wisecolt CI</span>
|
||||
</div>
|
||||
<nav className="flex-1 space-y-1 px-3 py-4">
|
||||
{navigation.map((item) => (
|
||||
<NavLink
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
"flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition",
|
||||
isActive
|
||||
? "bg-primary text-primary-foreground shadow-sm"
|
||||
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
||||
)
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={item.icon} className="h-4 w-4" />
|
||||
<span>{item.label}</span>
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
<div className="mt-auto space-y-3 border-t border-border px-4 py-4">
|
||||
<div className="flex gap-3">
|
||||
<ThemeToggle size="icon" className="h-10 w-10 justify-center" />
|
||||
<div className="flex h-10 flex-1 items-center gap-3 rounded-md border border-border bg-background px-3">
|
||||
<FontAwesomeIcon icon={faUser} className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium text-foreground">{user?.username ?? "-"}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" className="w-full justify-center gap-2" onClick={handleLogout}>
|
||||
<FontAwesomeIcon icon={faArrowRightFromBracket} className="h-4 w-4" />
|
||||
Çıkış Yap
|
||||
</Button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main className="flex-1">
|
||||
<div className="mx-auto grid max-w-5xl gap-6 px-6 py-10 md:grid-cols-2">
|
||||
<Card className="border-border card-shadow">
|
||||
<CardHeader>
|
||||
<CardTitle>Bağlantı</CardTitle>
|
||||
<CardDescription>Backend'e bağlanıp basit ping/pong testi yapın.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<Button onClick={sendPing} disabled={!socket}>
|
||||
Ping Gönder
|
||||
</Button>
|
||||
<div className="rounded-md border border-border bg-muted/40 p-3 text-sm text-muted-foreground">
|
||||
<div className="mb-2 text-xs uppercase tracking-wide text-foreground">Socket Mesajları</div>
|
||||
<div className="space-y-1">
|
||||
{messages.length === 0 && <div>Mesaj yok.</div>}
|
||||
{messages.map((msg, idx) => (
|
||||
<div key={idx} className="text-foreground/80">
|
||||
• {msg}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-border card-shadow">
|
||||
<CardHeader>
|
||||
<CardTitle>Durum</CardTitle>
|
||||
<CardDescription>Auth & bağlantı bilgileri.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 text-sm text-muted-foreground">
|
||||
<div className="flex items-center justify-between">
|
||||
<span>API URL</span>
|
||||
<span className="font-medium text-foreground">{socketUrl}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span>Kullanıcı</span>
|
||||
<span className="font-medium text-foreground">{user?.username ?? "-"}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span>Token</span>
|
||||
<span className="truncate font-mono text-foreground/80">{token?.slice(0, 24) ?? "-"}...</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
40
frontend/src/pages/HomePage.tsx
Normal file
40
frontend/src/pages/HomePage.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../components/ui/card";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { useLiveCounter } from "../providers/live-provider";
|
||||
|
||||
export function HomePage() {
|
||||
const { value, running, startCounter, stopCounter } = useLiveCounter();
|
||||
|
||||
return (
|
||||
<Card className="border-border card-shadow">
|
||||
<CardHeader>
|
||||
<CardTitle>Canlı Sayaç</CardTitle>
|
||||
<CardDescription>
|
||||
Sayaç sunucu tarafından çalışır; başlattıktan sonra diğer sayfalardan anlık izleyebilirsiniz.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between rounded-md border border-border bg-muted/40 px-4 py-3">
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground">Durum</div>
|
||||
<div className="text-lg font-semibold text-foreground">
|
||||
{running ? "Çalışıyor" : "Beklemede"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-sm text-muted-foreground">Değer</div>
|
||||
<div className="text-3xl font-bold text-foreground">{value}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<Button className="flex-1" onClick={startCounter} disabled={running}>
|
||||
Sayaçı Başlat
|
||||
</Button>
|
||||
<Button className="flex-1" variant="outline" onClick={stopCounter} disabled={!running}>
|
||||
Durdur
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
30
frontend/src/pages/JobsPage.tsx
Normal file
30
frontend/src/pages/JobsPage.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../components/ui/card";
|
||||
import { useLiveCounter } from "../providers/live-provider";
|
||||
|
||||
export function JobsPage() {
|
||||
const { value, running } = useLiveCounter();
|
||||
|
||||
return (
|
||||
<Card className="border-border card-shadow">
|
||||
<CardHeader>
|
||||
<CardTitle>Jobs Durumu</CardTitle>
|
||||
<CardDescription>Diğer sayfalarda başlatılan canlı sayaç burada izlenebilir.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 text-sm text-muted-foreground">
|
||||
<div className="flex items-center justify-between">
|
||||
<span>Canlı Durum</span>
|
||||
<span className="font-semibold text-foreground">
|
||||
{running ? "Çalışıyor" : "Beklemede"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span>Sayaç Değeri</span>
|
||||
<span className="font-mono text-lg text-foreground">{value}</span>
|
||||
</div>
|
||||
<div className="rounded-md border border-border bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
|
||||
Sayaç Home sayfasından başlatılır. Bu panel aynı anda açık olan tüm kullanıcılarda anlık güncellenir.
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -20,7 +20,7 @@ export function LoginPage() {
|
||||
setLoading(true);
|
||||
try {
|
||||
await login(username, password);
|
||||
navigate("/admin", { replace: true });
|
||||
navigate("/home", { replace: true });
|
||||
} catch (err) {
|
||||
toast.error("Giriş başarısız. Bilgileri kontrol edin.");
|
||||
} finally {
|
||||
|
||||
75
frontend/src/providers/live-provider.tsx
Normal file
75
frontend/src/providers/live-provider.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import React, { createContext, useContext, useEffect, useMemo, useState } from "react";
|
||||
import { useSocket } from "./socket-provider";
|
||||
|
||||
type LiveState = {
|
||||
value: number;
|
||||
running: boolean;
|
||||
};
|
||||
|
||||
type LiveContextValue = LiveState & {
|
||||
startCounter: () => void;
|
||||
stopCounter: () => void;
|
||||
};
|
||||
|
||||
const LiveContext = createContext<LiveContextValue | undefined>(undefined);
|
||||
|
||||
export const LiveProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const socket = useSocket();
|
||||
const [state, setState] = useState<LiveState>({ value: 0, running: false });
|
||||
|
||||
useEffect(() => {
|
||||
if (!socket) return;
|
||||
|
||||
const handleUpdate = (payload: { value: number }) => {
|
||||
setState({ value: payload.value, running: true });
|
||||
};
|
||||
|
||||
const handleStopped = (payload: { value: number }) => {
|
||||
setState({ value: payload.value, running: false });
|
||||
};
|
||||
|
||||
socket.on("counter:update", handleUpdate);
|
||||
socket.on("counter:stopped", handleStopped);
|
||||
|
||||
socket.emit("counter:status", (payload: { value: number; running: boolean }) => {
|
||||
setState({ value: payload.value, running: payload.running });
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket.off("counter:update", handleUpdate);
|
||||
socket.off("counter:stopped", handleStopped);
|
||||
};
|
||||
}, [socket]);
|
||||
|
||||
const startCounter = useMemo(
|
||||
() => () => {
|
||||
socket?.emit("counter:start");
|
||||
},
|
||||
[socket]
|
||||
);
|
||||
|
||||
const stopCounter = useMemo(
|
||||
() => () => {
|
||||
socket?.emit("counter:stop");
|
||||
},
|
||||
[socket]
|
||||
);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
value: state.value,
|
||||
running: state.running,
|
||||
startCounter,
|
||||
stopCounter
|
||||
}),
|
||||
[state, startCounter, stopCounter]
|
||||
);
|
||||
|
||||
return <LiveContext.Provider value={value}>{children}</LiveContext.Provider>;
|
||||
};
|
||||
|
||||
export function useLiveCounter() {
|
||||
const ctx = useContext(LiveContext);
|
||||
if (!ctx) throw new Error("useLiveCounter LiveProvider içinde kullanılmalı");
|
||||
return ctx;
|
||||
}
|
||||
49
frontend/src/providers/socket-provider.tsx
Normal file
49
frontend/src/providers/socket-provider.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React, { createContext, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { io, Socket } from "socket.io-client";
|
||||
import { useAuth } from "./auth-provider";
|
||||
import { apiClient } from "../api/client";
|
||||
|
||||
const SocketContext = createContext<Socket | null>(null);
|
||||
|
||||
export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const { token } = useAuth();
|
||||
const socketRef = useRef<Socket | null>(null);
|
||||
const [ready, setReady] = useState(false);
|
||||
|
||||
const baseUrl = useMemo(() => apiClient.defaults.baseURL || window.location.origin, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) {
|
||||
socketRef.current?.disconnect();
|
||||
socketRef.current = null;
|
||||
setReady(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const socket = io(baseUrl, {
|
||||
auth: { token },
|
||||
transports: ["websocket", "polling"]
|
||||
});
|
||||
|
||||
socketRef.current = socket;
|
||||
|
||||
socket.on("connect", () => setReady(true));
|
||||
socket.on("disconnect", () => setReady(false));
|
||||
|
||||
return () => {
|
||||
socket.off("connect");
|
||||
socket.off("disconnect");
|
||||
socket.disconnect();
|
||||
socketRef.current = null;
|
||||
setReady(false);
|
||||
};
|
||||
}, [token, baseUrl]);
|
||||
|
||||
const value = ready ? socketRef.current : null;
|
||||
|
||||
return <SocketContext.Provider value={value}>{children}</SocketContext.Provider>;
|
||||
};
|
||||
|
||||
export function useSocket() {
|
||||
return useContext(SocketContext);
|
||||
}
|
||||
Reference in New Issue
Block a user