diff --git a/apps/dispatch/app/data/livekitRooms.ts b/apps/dispatch/app/_data/livekitRooms.ts similarity index 100% rename from apps/dispatch/app/data/livekitRooms.ts rename to apps/dispatch/app/_data/livekitRooms.ts diff --git a/apps/dispatch/app/_store/audioStore.ts b/apps/dispatch/app/_store/audioStore.ts index 026c54b2..74e030c3 100644 --- a/apps/dispatch/app/_store/audioStore.ts +++ b/apps/dispatch/app/_store/audioStore.ts @@ -22,8 +22,8 @@ type TalkState = { disconnect: () => void; room: Room | null; }; -const getToken = async () => { - const response = await fetch(`/api/livekit/token`); +const getToken = async (roomName: string) => { + const response = await fetch(`/api/livekit/token?roomName=${roomName}`); const data = await response.json(); return data.token; }; @@ -37,6 +37,8 @@ export const useAudioStore = create((set, get) => ({ room: null, toggleTalking: () => set((state) => ({ isTalking: !state.isTalking })), connect: async (roomName) => { + set({ state: "connecting" }); + console.log("Connecting to room: ", roomName); try { // Clean old room const connectedRoom = get().room; @@ -46,14 +48,12 @@ export const useAudioStore = create((set, get) => ({ connectedRoom.removeAllListeners(); } - set({ state: "connecting" }); const url = process.env.NEXT_PUBLIC_LIVEKIT_URL; if (!url) return console.error("NEXT_PUBLIC_LIVEKIT_URL not set"); - const token = await getToken(); + const token = await getToken(roomName); if (!token) throw new Error("Fehlende Berechtigung"); - console.log("Token: ", token); - const room = new Room(); + const room = new Room({}); await room.prepareConnection(url, token); room // Connection events @@ -75,6 +75,8 @@ export const useAudioStore = create((set, get) => ({ .on(RoomEvent.ActiveSpeakersChanged, handleActiveSpeakerChange) .on(RoomEvent.LocalTrackUnpublished, handleLocalTrackUnpublished); await room.connect(url, token); + console.log(room); + set({ room }); interval = setInterval(() => { set({ remoteParticipants: diff --git a/apps/dispatch/app/_store/chatStore.ts b/apps/dispatch/app/_store/chatStore.ts index 99568ebb..38a3619a 100644 --- a/apps/dispatch/app/_store/chatStore.ts +++ b/apps/dispatch/app/_store/chatStore.ts @@ -1,6 +1,6 @@ import { create } from "zustand"; import { ChatMessage } from "@repo/db"; -import { socket } from "(dispatch)/socket"; +import { socket } from "dispatch/socket"; interface ChatStore { ownId: null | string; diff --git a/apps/dispatch/app/_store/connectionStore.ts b/apps/dispatch/app/_store/connectionStore.ts index c1b8f15d..559e2d32 100644 --- a/apps/dispatch/app/_store/connectionStore.ts +++ b/apps/dispatch/app/_store/connectionStore.ts @@ -1,5 +1,5 @@ import { create } from "zustand"; -import { socket } from "../(dispatch)/socket"; +import { socket } from "../dispatch/socket"; interface ConnectionStore { isConnected: boolean; @@ -12,7 +12,7 @@ interface ConnectionStore { disconnect: () => void; } -export const useConnectionStore = create((set) => ({ +export const useDispatchConnectionStore = create((set) => ({ isConnected: false, selectedZone: "LST_01", connect: async (uid, selectedZone, logoffTime) => @@ -34,8 +34,8 @@ export const useConnectionStore = create((set) => ({ })); socket.on("connect", () => { - useConnectionStore.setState({ isConnected: true }); + useDispatchConnectionStore.setState({ isConnected: true }); }); socket.on("disconnect", () => { - useConnectionStore.setState({ isConnected: false }); + useDispatchConnectionStore.setState({ isConnected: false }); }); diff --git a/apps/dispatch/app/_store/stationsStore.ts b/apps/dispatch/app/_store/stationsStore.ts index 0a384b0a..88cf2d6d 100644 --- a/apps/dispatch/app/_store/stationsStore.ts +++ b/apps/dispatch/app/_store/stationsStore.ts @@ -1,5 +1,5 @@ import { create } from "zustand"; -import { socket } from "../(dispatch)/socket"; +import { socket } from "../dispatch/socket"; export const stationStore = create((set) => { return { diff --git a/apps/dispatch/app/api/livekit/token/route.ts b/apps/dispatch/app/api/livekit/token/route.ts index 65cf7506..396f452f 100644 --- a/apps/dispatch/app/api/livekit/token/route.ts +++ b/apps/dispatch/app/api/livekit/token/route.ts @@ -1,13 +1,19 @@ import { getServerSession } from "api/auth/[...nextauth]/auth"; -import { ROOMS } from "data/livekitRooms"; +import { ROOMS } from "_data/livekitRooms"; import { AccessToken } from "livekit-server-sdk"; import { prisma } from "../../../../../../packages/database/prisma/client"; +import { NextRequest } from "next/server"; if (!process.env.LIVEKIT_API_KEY) throw new Error("LIVEKIT_API_KEY not set"); if (!process.env.LIVEKIT_API_SECRET) throw new Error("LIVEKIT_API_SECRET not set"); -export const GET = async () => { +export const GET = async (request: NextRequest) => { + const roomName = request.nextUrl.searchParams.get("roomName"); + + if (!roomName) + return Response.json({ message: "Missing roomName" }, { status: 400 }); + const session = await getServerSession(); if (!session) @@ -32,8 +38,12 @@ export const GET = async () => { ttl: "1d", }, ); - ROOMS.forEach((roomName) => { - at.addGrant({ roomJoin: true, room: roomName, canPublish: true }); + + at.addGrant({ + room: roomName, + roomJoin: true, + canPublish: true, + canSubscribe: true, }); const token = await at.toJwt(); diff --git a/apps/dispatch/app/(dispatch)/_components/map/AircraftMarker.tsx b/apps/dispatch/app/dispatch/_components/map/AircraftMarker.tsx similarity index 100% rename from apps/dispatch/app/(dispatch)/_components/map/AircraftMarker.tsx rename to apps/dispatch/app/dispatch/_components/map/AircraftMarker.tsx diff --git a/apps/dispatch/app/(dispatch)/_components/map/BaseMaps.tsx b/apps/dispatch/app/dispatch/_components/map/BaseMaps.tsx similarity index 100% rename from apps/dispatch/app/(dispatch)/_components/map/BaseMaps.tsx rename to apps/dispatch/app/dispatch/_components/map/BaseMaps.tsx diff --git a/apps/dispatch/app/(dispatch)/_components/map/ContextMenu.tsx b/apps/dispatch/app/dispatch/_components/map/ContextMenu.tsx similarity index 100% rename from apps/dispatch/app/(dispatch)/_components/map/ContextMenu.tsx rename to apps/dispatch/app/dispatch/_components/map/ContextMenu.tsx diff --git a/apps/dispatch/app/(dispatch)/_components/map/Map.tsx b/apps/dispatch/app/dispatch/_components/map/Map.tsx similarity index 53% rename from apps/dispatch/app/(dispatch)/_components/map/Map.tsx rename to apps/dispatch/app/dispatch/_components/map/Map.tsx index 08ac1299..66a3e513 100644 --- a/apps/dispatch/app/(dispatch)/_components/map/Map.tsx +++ b/apps/dispatch/app/dispatch/_components/map/Map.tsx @@ -2,11 +2,11 @@ import "leaflet/dist/leaflet.css"; import { useMapStore } from "_store/mapStore"; import { MapContainer } from "react-leaflet"; -import { BaseMaps } from "(dispatch)/_components/map/BaseMaps"; -import { ContextMenu } from "(dispatch)/_components/map/ContextMenu"; -import { MissionMarkers } from "(dispatch)/_components/map/MissionMarkers"; -import { SearchElements } from "(dispatch)/_components/map/SearchElements"; -import { AircraftMarker } from "(dispatch)/_components/map/AircraftMarker"; +import { BaseMaps } from "dispatch/_components/map/BaseMaps"; +import { ContextMenu } from "dispatch/_components/map/ContextMenu"; +import { MissionMarkers } from "dispatch/_components/map/MissionMarkers"; +import { SearchElements } from "dispatch/_components/map/SearchElements"; +import { AircraftMarker } from "dispatch/_components/map/AircraftMarker"; export default ({}) => { const { map } = useMapStore(); diff --git a/apps/dispatch/app/(dispatch)/_components/map/MissionMarkers.tsx b/apps/dispatch/app/dispatch/_components/map/MissionMarkers.tsx similarity index 100% rename from apps/dispatch/app/(dispatch)/_components/map/MissionMarkers.tsx rename to apps/dispatch/app/dispatch/_components/map/MissionMarkers.tsx diff --git a/apps/dispatch/app/(dispatch)/_components/map/SearchElements.tsx b/apps/dispatch/app/dispatch/_components/map/SearchElements.tsx similarity index 97% rename from apps/dispatch/app/(dispatch)/_components/map/SearchElements.tsx rename to apps/dispatch/app/dispatch/_components/map/SearchElements.tsx index e3390974..8f0e58b0 100644 --- a/apps/dispatch/app/(dispatch)/_components/map/SearchElements.tsx +++ b/apps/dispatch/app/dispatch/_components/map/SearchElements.tsx @@ -84,7 +84,6 @@ export const SearchElements = () => { ); }; - console.log("searchPopupElement", searchPopupElement, searchPopup); return ( <> {searchElements.map((element) => { diff --git a/apps/dispatch/app/(dispatch)/_components/navbar/Navbar.tsx b/apps/dispatch/app/dispatch/_components/navbar/Navbar.tsx similarity index 100% rename from apps/dispatch/app/(dispatch)/_components/navbar/Navbar.tsx rename to apps/dispatch/app/dispatch/_components/navbar/Navbar.tsx diff --git a/apps/dispatch/app/dispatch/_components/navbar/_components/Audio.tsx b/apps/dispatch/app/dispatch/_components/navbar/_components/Audio.tsx new file mode 100644 index 00000000..5c4c60d0 --- /dev/null +++ b/apps/dispatch/app/dispatch/_components/navbar/_components/Audio.tsx @@ -0,0 +1,142 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useDispatchConnectionStore } from "_store/connectionStore"; +import { + Disc, + Mic, + PlugZap, + ServerCrash, + ShieldQuestion, + Signal, + SignalLow, + SignalMedium, + WifiOff, + ZapOff, +} from "lucide-react"; +import { useAudioStore } from "_store/audioStore"; +import { cn } from "helpers/cn"; +import { ConnectionQuality } from "livekit-client"; +import { ROOMS } from "_data/livekitRooms"; + +export const Audio = () => { + const connection = useDispatchConnectionStore(); + const { + isTalking, + toggleTalking, + connect, + state, + connectionQuality, + disconnect, + remoteParticipants, + room, + message, + } = useAudioStore(); + const [selectedRoom, setSelectedRoom] = useState("LST_01"); + + useEffect(() => { + const joinRoom = async () => { + if (!connection.isConnected) return; + if (state === "connected") return; + connect(selectedRoom); + }; + + joinRoom(); + + return () => { + disconnect(); + }; + }, [connection.isConnected]); + + return ( + <> +
+ {state === "error" && ( +
{message}
+ )} + + + {state === "connected" && ( +
+ + {connectionQuality === ConnectionQuality.Excellent && ( + + )} + {connectionQuality === ConnectionQuality.Good && ( + + )} + {connectionQuality === ConnectionQuality.Poor && ( + + )} + {connectionQuality === ConnectionQuality.Lost && ( + + )} + {connectionQuality === ConnectionQuality.Unknown && ( + + )} +
+ {remoteParticipants} +
+
+
    + {ROOMS.map((r) => ( +
  • + +
  • + ))} +
  • + +
  • +
+
+ )} +
+ + ); +}; diff --git a/apps/dispatch/app/(dispatch)/_components/navbar/_components/Chat.tsx b/apps/dispatch/app/dispatch/_components/navbar/_components/Chat.tsx similarity index 99% rename from apps/dispatch/app/(dispatch)/_components/navbar/_components/Chat.tsx rename to apps/dispatch/app/dispatch/_components/navbar/_components/Chat.tsx index 7680c795..bf833c60 100644 --- a/apps/dispatch/app/(dispatch)/_components/navbar/_components/Chat.tsx +++ b/apps/dispatch/app/dispatch/_components/navbar/_components/Chat.tsx @@ -6,7 +6,7 @@ import { Fragment, useEffect, useRef, useState } from "react"; import { Dispatcher, getDispatcher, -} from "(dispatch)/_components/navbar/_components/action"; +} from "dispatch/_components/navbar/_components/action"; import { cn } from "helpers/cn"; export const Chat = () => { diff --git a/apps/dispatch/app/(dispatch)/_components/navbar/_components/Connection.tsx b/apps/dispatch/app/dispatch/_components/navbar/_components/Connection.tsx similarity index 91% rename from apps/dispatch/app/(dispatch)/_components/navbar/_components/Connection.tsx rename to apps/dispatch/app/dispatch/_components/navbar/_components/Connection.tsx index 2b672764..be7fe6b2 100644 --- a/apps/dispatch/app/(dispatch)/_components/navbar/_components/Connection.tsx +++ b/apps/dispatch/app/dispatch/_components/navbar/_components/Connection.tsx @@ -1,12 +1,11 @@ "use client"; import { useSession } from "next-auth/react"; -import { useConnectionStore } from "../../../../_store/connectionStore"; -import { useEffect, useRef, useState } from "react"; -import { useForm } from "react-hook-form"; +import { useDispatchConnectionStore } from "../../../../_store/connectionStore"; +import { useRef, useState } from "react"; export const ConnectionBtn = () => { const modalRef = useRef(null); - const connection = useConnectionStore((state) => state); + const connection = useDispatchConnectionStore((state) => state); const [form, setForm] = useState({ logoffTime: "", selectedZone: "LST_01", diff --git a/apps/dispatch/app/(dispatch)/_components/navbar/_components/ThemeSwap.tsx b/apps/dispatch/app/dispatch/_components/navbar/_components/ThemeSwap.tsx similarity index 100% rename from apps/dispatch/app/(dispatch)/_components/navbar/_components/ThemeSwap.tsx rename to apps/dispatch/app/dispatch/_components/navbar/_components/ThemeSwap.tsx diff --git a/apps/dispatch/app/(dispatch)/_components/navbar/_components/action.ts b/apps/dispatch/app/dispatch/_components/navbar/_components/action.ts similarity index 100% rename from apps/dispatch/app/(dispatch)/_components/navbar/_components/action.ts rename to apps/dispatch/app/dispatch/_components/navbar/_components/action.ts diff --git a/apps/dispatch/app/(dispatch)/_components/pannel/Missions.tsx b/apps/dispatch/app/dispatch/_components/pannel/Missions.tsx similarity index 100% rename from apps/dispatch/app/(dispatch)/_components/pannel/Missions.tsx rename to apps/dispatch/app/dispatch/_components/pannel/Missions.tsx diff --git a/apps/dispatch/app/(dispatch)/_components/pannel/OpenButton.tsx b/apps/dispatch/app/dispatch/_components/pannel/OpenButton.tsx similarity index 100% rename from apps/dispatch/app/(dispatch)/_components/pannel/OpenButton.tsx rename to apps/dispatch/app/dispatch/_components/pannel/OpenButton.tsx diff --git a/apps/dispatch/app/(dispatch)/_components/pannel/Pannel.tsx b/apps/dispatch/app/dispatch/_components/pannel/Pannel.tsx similarity index 88% rename from apps/dispatch/app/(dispatch)/_components/pannel/Pannel.tsx rename to apps/dispatch/app/dispatch/_components/pannel/Pannel.tsx index e66cb0a5..4e444c02 100644 --- a/apps/dispatch/app/(dispatch)/_components/pannel/Pannel.tsx +++ b/apps/dispatch/app/dispatch/_components/pannel/Pannel.tsx @@ -1,4 +1,4 @@ -import { Missions } from "(dispatch)/_components/pannel/Missions"; +import { Missions } from "dispatch/_components/pannel/Missions"; import { usePannelStore } from "_store/pannelStore"; import { cn } from "helpers/cn"; diff --git a/apps/dispatch/app/(dispatch)/_components/toast/ToastCard.tsx b/apps/dispatch/app/dispatch/_components/toast/ToastCard.tsx similarity index 100% rename from apps/dispatch/app/(dispatch)/_components/toast/ToastCard.tsx rename to apps/dispatch/app/dispatch/_components/toast/ToastCard.tsx diff --git a/apps/dispatch/app/(dispatch)/layout.tsx b/apps/dispatch/app/dispatch/layout.tsx similarity index 60% rename from apps/dispatch/app/(dispatch)/layout.tsx rename to apps/dispatch/app/dispatch/layout.tsx index 60104f85..98865f30 100644 --- a/apps/dispatch/app/(dispatch)/layout.tsx +++ b/apps/dispatch/app/dispatch/layout.tsx @@ -2,10 +2,9 @@ import type { Metadata } from "next"; import Navbar from "./_components/navbar/Navbar"; import { redirect } from "next/navigation"; import { getServerSession } from "../api/auth/[...nextauth]/auth"; -import { Toaster } from "react-hot-toast"; export const metadata: Metadata = { - title: "VAR Leitstelle v2", + title: "VAR v2: Disponent", description: "Die neue VAR Leitstelle.", }; @@ -20,21 +19,6 @@ export default async function RootLayout({ } return ( <> - {children} diff --git a/apps/dispatch/app/(dispatch)/page.tsx b/apps/dispatch/app/dispatch/page.tsx similarity index 74% rename from apps/dispatch/app/(dispatch)/page.tsx rename to apps/dispatch/app/dispatch/page.tsx index 272a169e..bb0c8f62 100644 --- a/apps/dispatch/app/(dispatch)/page.tsx +++ b/apps/dispatch/app/dispatch/page.tsx @@ -1,8 +1,8 @@ "use client"; -import { OpenButton } from "(dispatch)/_components/pannel/OpenButton"; -import { Pannel } from "(dispatch)/_components/pannel/Pannel"; -import MapToastCard2 from "(dispatch)/_components/toast/ToastCard"; +import { OpenButton } from "dispatch/_components/pannel/OpenButton"; +import { Pannel } from "dispatch/_components/pannel/Pannel"; +import MapToastCard2 from "dispatch/_components/toast/ToastCard"; import { usePannelStore } from "_store/pannelStore"; import { cn } from "helpers/cn"; import dynamic from "next/dynamic"; diff --git a/apps/dispatch/app/(dispatch)/socket.ts b/apps/dispatch/app/dispatch/socket.ts similarity index 100% rename from apps/dispatch/app/(dispatch)/socket.ts rename to apps/dispatch/app/dispatch/socket.ts diff --git a/apps/dispatch/app/layout.tsx b/apps/dispatch/app/layout.tsx index aba881cd..7542a8ea 100644 --- a/apps/dispatch/app/layout.tsx +++ b/apps/dispatch/app/layout.tsx @@ -3,6 +3,7 @@ import localFont from "next/font/local"; import "./globals.css"; import { NextAuthSessionProvider } from "./_components/AuthSessionProvider"; import { getServerSession } from "./api/auth/[...nextauth]/auth"; +import { Toaster } from "react-hot-toast"; const geistSans = localFont({ src: "./fonts/GeistVF.woff", @@ -29,6 +30,21 @@ export default async function RootLayout({ + {children} diff --git a/apps/dispatch/app/page.tsx b/apps/dispatch/app/page.tsx new file mode 100644 index 00000000..effb7865 --- /dev/null +++ b/apps/dispatch/app/page.tsx @@ -0,0 +1,8 @@ +export default () => { + return ( +
+ Hab das hier mal nach /dispatcher verschoben. Unter /pilot soll dann die + Piloten UI sein +
+ ); +}; diff --git a/apps/dispatch/app/(dispatch)/_components/navbar/_components/Audio.tsx b/apps/dispatch/app/pilot/_components/navbar/Audio.tsx similarity index 98% rename from apps/dispatch/app/(dispatch)/_components/navbar/_components/Audio.tsx rename to apps/dispatch/app/pilot/_components/navbar/Audio.tsx index cddd89e4..3dc6f57d 100644 --- a/apps/dispatch/app/(dispatch)/_components/navbar/_components/Audio.tsx +++ b/apps/dispatch/app/pilot/_components/navbar/Audio.tsx @@ -17,7 +17,7 @@ import { import { useAudioStore } from "_store/audioStore"; import { cn } from "helpers/cn"; import { ConnectionQuality } from "livekit-client"; -import { ROOMS } from "data/livekitRooms"; +import { ROOMS } from "_data/livekitRooms"; export const Audio = () => { const connection = useConnectionStore(); diff --git a/apps/dispatch/app/pilot/_components/navbar/Chat.tsx b/apps/dispatch/app/pilot/_components/navbar/Chat.tsx new file mode 100644 index 00000000..bf833c60 --- /dev/null +++ b/apps/dispatch/app/pilot/_components/navbar/Chat.tsx @@ -0,0 +1,220 @@ +"use client"; +import { ChatBubbleIcon, PaperPlaneIcon } from "@radix-ui/react-icons"; +import { useChatStore } from "_store/chatStore"; +import { useSession } from "next-auth/react"; +import { Fragment, useEffect, useRef, useState } from "react"; +import { + Dispatcher, + getDispatcher, +} from "dispatch/_components/navbar/_components/action"; +import { cn } from "helpers/cn"; + +export const Chat = () => { + const { + chatOpen, + setChatOpen, + sendMessage, + addChat, + chats, + setOwnId, + selectedChat, + setSelectedChat, + setChatNotification, + } = useChatStore(); + const [sending, setSending] = useState(false); + const session = useSession(); + const [addTabValue, setAddTabValue] = useState(""); + const [message, setMessage] = useState(""); + const [dispatcher, setDispatcher] = useState(null); + const timeout = useRef(null); + + useEffect(() => { + if (!session.data?.user.id) return; + setOwnId(session.data.user.id); + }, [session]); + + useEffect(() => { + const fetchDispatcher = async () => { + const data = await getDispatcher(); + if (data) { + const filteredDispatcher = data.filter((user) => { + return ( + user.userId !== session.data?.user.id && + !Object.keys(chats).includes(user.userId) + ); + }); + setDispatcher(filteredDispatcher); + } + if (!addTabValue && data[0]) setAddTabValue(data[0].userId); + }; + + timeout.current = setInterval(() => { + fetchDispatcher(); + }, 1000000); + fetchDispatcher(); + + return () => { + if (timeout.current) { + clearInterval(timeout.current); + timeout.current = null; + console.log("cleared"); + } + }; + }, [addTabValue, chats]); + + return ( +
+
+ {Object.values(chats).some((c) => c.notification) && ( + + )} + +
+ {chatOpen && ( +
+
+
+ + +
+
+ {Object.keys(chats).map((userId) => { + const chat = chats[userId]; + if (!chat) return null; + return ( + + `} + checked={selectedChat === userId} + onClick={() => { + setChatNotification(userId, false); + }} + onChange={(e) => { + if (e.target.checked) { + // Handle tab change + setSelectedChat(userId); + } + }} + /> +
+ {chat.messages.map((chatMessage) => { + const isSender = + chatMessage.senderId === session.data?.user.id; + return ( +
+

+ {new Date( + chatMessage.timestamp, + ).toLocaleTimeString()} +

+
+ {chatMessage.text} +
+
+ ); + })} + {!chat.messages.length && ( +

+ Noch keine Nachrichten +

+ )} +
+
+ ); + })} +
+
+
+ +
+ +
+
+
+ )} +
+ ); +}; diff --git a/apps/dispatch/app/pilot/_components/navbar/Connection.tsx b/apps/dispatch/app/pilot/_components/navbar/Connection.tsx new file mode 100644 index 00000000..40b01906 --- /dev/null +++ b/apps/dispatch/app/pilot/_components/navbar/Connection.tsx @@ -0,0 +1,107 @@ +"use client"; +import { useSession } from "next-auth/react"; +import { useDispatchConnectionStore } from "../../../_store/connectionStore"; +import { useRef, useState } from "react"; + +export const ConnectionBtn = () => { + const modalRef = useRef(null); + const connection = useDispatchConnectionStore((state) => state); + const [form, setForm] = useState({ + logoffTime: "", + selectedZone: "LST_01", + }); + const session = useSession(); + const uid = session.data?.user?.id; + if (!uid) return null; + return ( + <> + {!connection.isConnected ? ( + + ) : ( + + )} + + +
+ {connection.isConnected ? ( +

+ Verbunden als{" "} + + <{connection.selectedZone}> + +

+ ) : ( +

Als Disponent anmelden

+ )} +
+ + {!connection.isConnected && ( +

+ Du kannst diese Zeit später noch anpassen. +

+ )} +
+
+
+ + {connection.isConnected ? ( + + ) : ( + + )} +
+
+
+
+ + ); +}; + +export const Connection = () => { + return ( +
+ +
+ ); +}; diff --git a/apps/dispatch/app/pilot/_components/navbar/Navbar.tsx b/apps/dispatch/app/pilot/_components/navbar/Navbar.tsx new file mode 100644 index 00000000..8c94d919 --- /dev/null +++ b/apps/dispatch/app/pilot/_components/navbar/Navbar.tsx @@ -0,0 +1,52 @@ +"use client"; + +import { Connection } from "./Connection"; +import { ThemeSwap } from "./ThemeSwap"; +import { Audio } from "./Audio"; +import { useState } from "react"; +import { ExitIcon, ExternalLinkIcon } from "@radix-ui/react-icons"; +import { Chat } from "./Chat"; + +export default function Navbar() { + const [isDark, setIsDark] = useState(false); + + const toggleTheme = () => { + const newTheme = !isDark; + setIsDark(newTheme); + document.documentElement.setAttribute( + "data-theme", + newTheme ? "nord" : "dark", + ); + }; + + return ( +
+ +
+
+ +
+
+ +
+
+
+
+ +
+ +
+ + +
+
+
+ ); +} diff --git a/apps/dispatch/app/pilot/_components/navbar/ThemeSwap.tsx b/apps/dispatch/app/pilot/_components/navbar/ThemeSwap.tsx new file mode 100644 index 00000000..dec2a6fd --- /dev/null +++ b/apps/dispatch/app/pilot/_components/navbar/ThemeSwap.tsx @@ -0,0 +1,26 @@ +"use client"; + +import { MoonIcon, SunIcon } from "@radix-ui/react-icons"; + +interface ThemeSwapProps { + isDark: boolean; + toggleTheme: () => void; +} + +export const ThemeSwap: React.FC = ({ + isDark, + toggleTheme, +}) => { + return ( + + ); +}; diff --git a/apps/dispatch/app/pilot/_components/navbar/action.ts b/apps/dispatch/app/pilot/_components/navbar/action.ts new file mode 100644 index 00000000..6daa4544 --- /dev/null +++ b/apps/dispatch/app/pilot/_components/navbar/action.ts @@ -0,0 +1,18 @@ +"use server"; + +export interface Dispatcher { + userId: string; + lastSeen: string; + loginTime: string; + logoffTime: string; + selectedZone: string; + name: string; + socketId: string; +} + +export const getDispatcher = async () => { + const res = await fetch(` + ${process.env.NEXT_PUBLIC_DISPATCH_SERVER_URL}/dispatcher`); + const data = await res.json(); + return data as Dispatcher[]; +}; diff --git a/apps/dispatch/app/pilot/layout.tsx b/apps/dispatch/app/pilot/layout.tsx new file mode 100644 index 00000000..0c6ac53e --- /dev/null +++ b/apps/dispatch/app/pilot/layout.tsx @@ -0,0 +1,26 @@ +import type { Metadata } from "next"; +import Navbar from "../dispatch/_components/navbar/Navbar"; +import { redirect } from "next/navigation"; +import { getServerSession } from "../api/auth/[...nextauth]/auth"; + +export const metadata: Metadata = { + title: "VAR v2: Disponent", + description: "Die neue VAR Leitstelle.", +}; + +export default async function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + const session = await getServerSession(); + if (!session) { + redirect("/login"); + } + return ( + <> + + {children} + + ); +} diff --git a/grafana/grafana.db b/grafana/grafana.db index 7eebbec4..aa974892 100644 Binary files a/grafana/grafana.db and b/grafana/grafana.db differ