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 (
+
+
+
+
+
+
+
+
+
+ );
+}
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"}
+
+
+
+
+
+
+
+
+
+
+ );
+}
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);
+}