servers page with map
This commit is contained in:
@@ -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" }} />;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
@@ -9,4 +9,5 @@ export default interface Server extends ServerAPI {
|
||||
memory: number;
|
||||
storage: number;
|
||||
network: number;
|
||||
coordinates?: [number, number];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user