From e07c5933ee7e84fb6cac8b4a7678905de9fe1f88 Mon Sep 17 00:00:00 2001 From: sbilketay Date: Wed, 26 Nov 2025 19:58:46 +0300 Subject: [PATCH] live action --- backend/src/index.ts | 37 +++++ frontend/src/App.tsx | 13 +- frontend/src/components/DashboardLayout.tsx | 98 +++++++++++++ frontend/src/main.tsx | 14 +- frontend/src/pages/AdminPage.tsx | 150 -------------------- frontend/src/pages/HomePage.tsx | 40 ++++++ frontend/src/pages/JobsPage.tsx | 30 ++++ frontend/src/pages/LoginPage.tsx | 2 +- frontend/src/providers/live-provider.tsx | 75 ++++++++++ frontend/src/providers/socket-provider.tsx | 49 +++++++ 10 files changed, 349 insertions(+), 159 deletions(-) create mode 100644 frontend/src/components/DashboardLayout.tsx delete mode 100644 frontend/src/pages/AdminPage.tsx create mode 100644 frontend/src/pages/HomePage.tsx create mode 100644 frontend/src/pages/JobsPage.tsx create mode 100644 frontend/src/providers/live-provider.tsx create mode 100644 frontend/src/providers/socket-provider.tsx diff --git a/backend/src/index.ts b/backend/src/index.ts index e3b5db2..c608b1c 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -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() { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 41c8bf7..509c1ac 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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 ( } /> }> - } /> - } /> + }> + } /> + } /> + } /> + - } /> + } /> ); } diff --git a/frontend/src/components/DashboardLayout.tsx b/frontend/src/components/DashboardLayout.tsx new file mode 100644 index 0000000..e3a9e60 --- /dev/null +++ b/frontend/src/components/DashboardLayout.tsx @@ -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 ( +
+
+ + +
+
+
+
+
Proje
+
Wisecolt CI
+
+
+ Bağlantı: {socketUrl} +
+
+ +
+ +
+
+
+
+
+ ); +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 0bf2c5c..e70d1e9 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -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( - - - - + + + + + + + + diff --git a/frontend/src/pages/AdminPage.tsx b/frontend/src/pages/AdminPage.tsx deleted file mode 100644 index 96e02d0..0000000 --- a/frontend/src/pages/AdminPage.tsx +++ /dev/null @@ -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(null); - const [messages, setMessages] = useState([]); - - 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 ( -
-
- - -
-
- - - Bağlantı - Backend'e bağlanıp basit ping/pong testi yapın. - - - -
-
Socket Mesajları
-
- {messages.length === 0 &&
Mesaj yok.
} - {messages.map((msg, idx) => ( -
- • {msg} -
- ))} -
-
-
-
- - - - Durum - Auth & bağlantı bilgileri. - - -
- API URL - {socketUrl} -
-
- Kullanıcı - {user?.username ?? "-"} -
-
- Token - {token?.slice(0, 24) ?? "-"}... -
-
-
-
-
-
-
- ); -} diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx new file mode 100644 index 0000000..03db6fa --- /dev/null +++ b/frontend/src/pages/HomePage.tsx @@ -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 ( + + + Canlı Sayaç + + Sayaç sunucu tarafından çalışır; başlattıktan sonra diğer sayfalardan anlık izleyebilirsiniz. + + + +
+
+
Durum
+
+ {running ? "Çalışıyor" : "Beklemede"} +
+
+
+
Değer
+
{value}
+
+
+
+ + +
+
+
+ ); +} diff --git a/frontend/src/pages/JobsPage.tsx b/frontend/src/pages/JobsPage.tsx new file mode 100644 index 0000000..d2cb728 --- /dev/null +++ b/frontend/src/pages/JobsPage.tsx @@ -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 ( + + + Jobs Durumu + Diğer sayfalarda başlatılan canlı sayaç burada izlenebilir. + + +
+ Canlı Durum + + {running ? "Çalışıyor" : "Beklemede"} + +
+
+ Sayaç Değeri + {value} +
+
+ Sayaç Home sayfasından başlatılır. Bu panel aynı anda açık olan tüm kullanıcılarda anlık güncellenir. +
+
+
+ ); +} diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx index 0400404..0029cde 100644 --- a/frontend/src/pages/LoginPage.tsx +++ b/frontend/src/pages/LoginPage.tsx @@ -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 { diff --git a/frontend/src/providers/live-provider.tsx b/frontend/src/providers/live-provider.tsx new file mode 100644 index 0000000..c19402c --- /dev/null +++ b/frontend/src/providers/live-provider.tsx @@ -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(undefined); + +export const LiveProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const socket = useSocket(); + const [state, setState] = useState({ 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 {children}; +}; + +export function useLiveCounter() { + const ctx = useContext(LiveContext); + if (!ctx) throw new Error("useLiveCounter LiveProvider içinde kullanılmalı"); + return ctx; +} diff --git a/frontend/src/providers/socket-provider.tsx b/frontend/src/providers/socket-provider.tsx new file mode 100644 index 0000000..f7abafd --- /dev/null +++ b/frontend/src/providers/socket-provider.tsx @@ -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(null); + +export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const { token } = useAuth(); + const socketRef = useRef(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 {children}; +}; + +export function useSocket() { + return useContext(SocketContext); +}