servers page with map

This commit is contained in:
2025-07-17 02:05:22 +02:00
parent f78a76ebf0
commit f17e2fbf72
9 changed files with 803 additions and 23 deletions
+60
View File
@@ -0,0 +1,60 @@
import React, { useRef, useEffect } from "react";
import maplibregl from "maplibre-gl";
import "maplibre-gl/dist/maplibre-gl.css";
import Server from "@/types/server";
export default function Map({ servers }: { servers: Server[] }) {
const mapContainer = useRef<HTMLDivElement | null>(null);
const map = useRef<maplibregl.Map | null>(null);
const markers = useRef<maplibregl.Marker[]>([]);
// Initialize map only once
useEffect(() => {
if (map.current || !mapContainer.current) return;
map.current = new maplibregl.Map({
container: mapContainer.current,
style: "/map_style.json",
center: [0, 0],
zoom: 2,
});
return () => {
// Clean up markers
markers.current.forEach((m) => m.remove());
markers.current = [];
// Clean up map
map.current?.remove();
map.current = null;
};
}, []);
// Update markers when servers change
useEffect(() => {
if (!map.current) return;
// Remove old markers
markers.current.forEach((marker) => marker.remove());
markers.current = [];
// Add new markers
servers.forEach((server) => {
if (server.coordinates) {
const marker = new maplibregl.Marker({ color: "#ff3e00" })
.setLngLat(server.coordinates)
.addTo(map.current!);
// Optional: add popup or click event
marker.getElement().addEventListener("click", () => {
// Your logic here
console.log("Clicked on server", server.name);
});
markers.current.push(marker);
}
});
}, [servers]);
return <div ref={mapContainer} style={{ width: "100%", height: "600px" }} />;
}
+77
View File
@@ -0,0 +1,77 @@
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils";
import { CircleCheckBig, Cpu, MemoryStick, TriangleAlert } from "lucide-react";
import { ChartAreaGradient } from "../dashboard/chart";
import { Progress } from "@/components/ui/progress";
import ServerComponent from "../dashboard/server";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { useEffect, useState } from "react";
import useSession from "@/hooks/useSession";
import Server from "@/types/server";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import Map from "./Map";
export default function Servers() {
const session = useSession();
const [servers, setServers] = useState<Server[]>([]);
const [searchedServers, setSearchedServers] = useState<Server[]>([]);
const [serversQuery, setServersQuery] = useState("");
useEffect(() => {
if (!session) return;
session.getServers().then((servers) => {
setServers(servers);
});
}, [session]);
useEffect(() => {
const timeout = setTimeout(() => {
let tokens = serversQuery.toLowerCase().split(" ");
setSearchedServers(
servers.filter((s) =>
tokens.some(
(t) => s.name.toLowerCase().includes(t) || s.status.includes(t)
)
)
);
}, 30);
return () => clearTimeout(timeout);
}, [serversQuery, servers]);
return (
<div className="w-full">
<Tabs defaultValue="grid">
<TabsList className="mx-auto mb-5">
<TabsTrigger value="grid">Grid</TabsTrigger>
<TabsTrigger value="map">Map</TabsTrigger>
</TabsList>
<TabsContent value="grid" className="flex flex-col gap-5">
<Card>
<CardContent className="flex flex-row gap-3">
<Input
onInput={(e) => setServersQuery(e.currentTarget.value)}
placeholder="Search servers by name, location or IP..."
/>
<Button onClick={() => {}}>Search</Button>
</CardContent>
</Card>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
{searchedServers.map((s, i) => (
<ServerComponent server={s} key={i} />
))}
</div>
</TabsContent>
<TabsContent value="map" className="h-full">
<Map servers={servers} />
</TabsContent>
</Tabs>
</div>
);
}
+48 -4
View File
@@ -1,20 +1,64 @@
import { Calendar, Home, Inbox, Search, ServerIcon, Settings } from "lucide-react";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
import React from "react";
const items = [
{
title: "Dashboard",
url: "/dashboard",
icon: Home,
},
{
title: "Inbox",
url: "/inbox",
icon: Inbox,
},
{
title: "Servers",
url: "/servers",
icon: ServerIcon,
},
{
title: "Settings",
url: "/settings",
icon: Settings,
},
];
export default function AppSidebar() {
return (
<Sidebar>
<SidebarHeader />
<SidebarContent>
<SidebarGroup />
<SidebarGroup />
<SidebarGroup>
<SidebarGroupLabel>Application</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{items.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild>
<a href={item.url}>
<item.icon />
<span>{item.title}</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarFooter />
</Sidebar>
);
}
+66
View File
@@ -0,0 +1,66 @@
"use client"
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
function Tabs({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
return (
<TabsPrimitive.Root
data-slot="tabs"
className={cn("flex flex-col gap-2", className)}
{...props}
/>
)
}
function TabsList({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.List>) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
className={cn(
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
className
)}
{...props}
/>
)
}
function TabsTrigger({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function TabsContent({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
return (
<TabsPrimitive.Content
data-slot="tabs-content"
className={cn("flex-1 outline-none", className)}
{...props}
/>
)
}
export { Tabs, TabsList, TabsTrigger, TabsContent }
+1
View File
@@ -9,4 +9,5 @@ export default interface Server extends ServerAPI {
memory: number;
storage: number;
network: number;
coordinates?: [number, number];
}