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) => {
|
io.use((socket, next) => {
|
||||||
const token = socket.handshake.auth?.token as string | undefined;
|
const token = socket.handshake.auth?.token as string | undefined;
|
||||||
if (!token) {
|
if (!token) {
|
||||||
@@ -51,6 +74,20 @@ io.on("connection", (socket) => {
|
|||||||
socket.on("ping", () => {
|
socket.on("ping", () => {
|
||||||
socket.emit("pong", "pong");
|
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() {
|
async function start() {
|
||||||
|
|||||||
@@ -1,17 +1,22 @@
|
|||||||
import { Navigate, Route, Routes } from "react-router-dom";
|
import { Navigate, Route, Routes } from "react-router-dom";
|
||||||
import { LoginPage } from "./pages/LoginPage";
|
import { LoginPage } from "./pages/LoginPage";
|
||||||
import { AdminPage } from "./pages/AdminPage";
|
|
||||||
import { ProtectedRoute } from "./components/ProtectedRoute";
|
import { ProtectedRoute } from "./components/ProtectedRoute";
|
||||||
|
import { DashboardLayout } from "./components/DashboardLayout";
|
||||||
|
import { HomePage } from "./pages/HomePage";
|
||||||
|
import { JobsPage } from "./pages/JobsPage";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<LoginPage />} />
|
<Route path="/login" element={<LoginPage />} />
|
||||||
<Route element={<ProtectedRoute />}>
|
<Route element={<ProtectedRoute />}>
|
||||||
<Route path="/admin" element={<AdminPage />} />
|
<Route element={<DashboardLayout />}>
|
||||||
<Route path="/jobs" element={<AdminPage />} />
|
<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>
|
||||||
|
<Route path="*" element={<Navigate to="/home" replace />} />
|
||||||
</Routes>
|
</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 "./index.css";
|
||||||
import { ThemeProvider } from "./providers/theme-provider";
|
import { ThemeProvider } from "./providers/theme-provider";
|
||||||
import { AuthProvider } from "./providers/auth-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";
|
import { Toaster } from "./components/ui/toaster";
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
|
<SocketProvider>
|
||||||
|
<LiveProvider>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<App />
|
<App />
|
||||||
<Toaster />
|
<Toaster />
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
</LiveProvider>
|
||||||
|
</SocketProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</React.StrictMode>
|
</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);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
await login(username, password);
|
await login(username, password);
|
||||||
navigate("/admin", { replace: true });
|
navigate("/home", { replace: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error("Giriş başarısız. Bilgileri kontrol edin.");
|
toast.error("Giriş başarısız. Bilgileri kontrol edin.");
|
||||||
} finally {
|
} 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