"use client"; import { AppSidebar } from "@/components/app-sidebar"; import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, } from "@/components/ui/breadcrumb"; import { Separator } from "@/components/ui/separator"; import { SidebarInset, SidebarProvider, SidebarTrigger, } from "@/components/ui/sidebar"; import { Button } from "@/components/ui/button"; import { Plus, Link, Home, Trash2, LayoutGrid, List, Pencil, Zap, ViewIcon, Grid3X3, HelpCircle, } from "lucide-react"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "@/components/ui/card"; import { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, } from "@/components/ui/pagination"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import Cookies from "js-cookie"; import { useState, useEffect, useRef } from "react"; import axios from "axios"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip" import { StatusIndicator } from "@/components/status-indicator"; import { Toaster } from "@/components/ui/sonner" import { toast } from "sonner" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { useTranslations } from "next-intl"; interface Application { id: number; name: string; description?: string; icon?: string; publicURL: string; localURL?: string; server?: string; online: boolean; serverId: number; uptimecheckUrl?: string; } interface Server { id: number; name: string; } interface ApplicationsResponse { applications: Application[]; servers: Server[]; maxPage: number; totalItems?: number; } export default function Dashboard() { const t = useTranslations(); const [name, setName] = useState(""); const [description, setDescription] = useState(""); const [icon, setIcon] = useState(""); const [publicURL, setPublicURL] = useState(""); const [localURL, setLocalURL] = useState(""); const [serverId, setServerId] = useState(null); const [customUptimeCheck, setCustomUptimeCheck] = useState(false); const [uptimecheckUrl, setUptimecheckUrl] = useState(""); const [editName, setEditName] = useState(""); const [editDescription, setEditDescription] = useState(""); const [editIcon, setEditIcon] = useState(""); const [editPublicURL, setEditPublicURL] = useState(""); const [editLocalURL, setEditLocalURL] = useState(""); const [editId, setEditId] = useState(null); const [editServerId, setEditServerId] = useState(null); const [editCustomUptimeCheck, setEditCustomUptimeCheck] = useState(false); const [editUptimecheckUrl, setEditUptimecheckUrl] = useState(""); const [currentPage, setCurrentPage] = useState(1); const [maxPage, setMaxPage] = useState(1); const [applications, setApplications] = useState([]); const [servers, setServers] = useState([]); const [loading, setLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(""); const [isSearching, setIsSearching] = useState(false); const savedLayout = Cookies.get("layoutPreference-app"); const savedItemsPerPage = Cookies.get("itemsPerPage-app"); const initialIsGridLayout = savedLayout === "grid"; const initialIsCompactLayout = savedLayout === "compact"; const defaultItemsPerPage = initialIsGridLayout ? 15 : (initialIsCompactLayout ? 30 : 5); const initialItemsPerPage = savedItemsPerPage ? parseInt(savedItemsPerPage) : defaultItemsPerPage; const [isGridLayout, setIsGridLayout] = useState(initialIsGridLayout); const [isCompactLayout, setIsCompactLayout] = useState(initialIsCompactLayout); const [itemsPerPage, setItemsPerPage] = useState(initialItemsPerPage); const customInputRef = useRef(null); const debounceTimerRef = useRef(null); const toggleLayout = (layout: string) => { if (layout === "standard") { setIsGridLayout(false); setIsCompactLayout(false); Cookies.set("layoutPreference-app", "standard", { expires: 365, path: "/", sameSite: "strict", }); } else if (layout === "grid") { setIsGridLayout(true); setIsCompactLayout(false); Cookies.set("layoutPreference-app", "grid", { expires: 365, path: "/", sameSite: "strict", }); } else if (layout === "compact") { setIsGridLayout(false); setIsCompactLayout(true); Cookies.set("layoutPreference-app", "compact", { expires: 365, path: "/", sameSite: "strict", }); } }; const handleItemsPerPageChange = (value: string) => { if (debounceTimerRef.current) { clearTimeout(debounceTimerRef.current); } debounceTimerRef.current = setTimeout(() => { const newItemsPerPage = parseInt(value); if (isNaN(newItemsPerPage) || newItemsPerPage < 1) { toast.error(t('Applications.Messages.NumberValidation')); return; } const validatedValue = Math.min(Math.max(newItemsPerPage, 1), 100); setItemsPerPage(validatedValue); setCurrentPage(1); Cookies.set("itemsPerPage-app", String(validatedValue), { expires: 365, path: "/", sameSite: "strict", }); }, 300); }; const add = async () => { try { await axios.post("/api/applications/add", { name, description, icon, publicURL, localURL, serverId, uptimecheckUrl: customUptimeCheck ? uptimecheckUrl : "", }); getApplications(); toast.success(t('Applications.Messages.AddSuccess')); } catch (error: any) { console.log(error.response?.data); toast.error(t('Applications.Messages.AddError')); } }; const getApplications = async () => { try { setLoading(true); const response = await axios.post( "/api/applications/get", { page: currentPage, ITEMS_PER_PAGE: itemsPerPage } ); setApplications(response.data.applications); setServers(response.data.servers); setMaxPage(response.data.maxPage); if (response.data.totalItems !== undefined) { setTotalItems(response.data.totalItems); } setLoading(false); } catch (error: any) { console.log(error.response?.data); toast.error(t('Applications.Messages.GetError')); } }; // Calculate current range of items being displayed const [totalItems, setTotalItems] = useState(0); const startItem = (currentPage - 1) * itemsPerPage + 1; const endItem = Math.min(currentPage * itemsPerPage, totalItems); useEffect(() => { getApplications(); }, [currentPage, itemsPerPage]); const handlePrevious = () => setCurrentPage((prev) => Math.max(1, prev - 1)); const handleNext = () => setCurrentPage((prev) => Math.min(maxPage, prev + 1)); const deleteApplication = async (id: number) => { try { await axios.post("/api/applications/delete", { id }); getApplications(); toast.success(t('Applications.Messages.DeleteSuccess')); } catch (error: any) { console.log(error.response?.data); toast.error(t('Applications.Messages.DeleteError')); } }; const openEditDialog = (app: Application) => { setEditId(app.id); setEditServerId(app.serverId); setEditName(app.name); setEditDescription(app.description || ""); setEditIcon(app.icon || ""); setEditLocalURL(app.localURL || ""); setEditPublicURL(app.publicURL || ""); if (app.uptimecheckUrl) { setEditCustomUptimeCheck(true); setEditUptimecheckUrl(app.uptimecheckUrl); } else { setEditCustomUptimeCheck(false); setEditUptimecheckUrl(""); } }; const edit = async () => { if (!editId) return; try { await axios.put("/api/applications/edit", { id: editId, serverId: editServerId, name: editName, description: editDescription, icon: editIcon, publicURL: editPublicURL, localURL: editLocalURL, uptimecheckUrl: editCustomUptimeCheck ? editUptimecheckUrl : "", }); getApplications(); setEditId(null); toast.success(t('Applications.Messages.EditSuccess')); } catch (error: any) { console.log(error.response.data); toast.error(t('Applications.Messages.EditError')); } }; const searchApplications = async () => { try { setIsSearching(true); const response = await axios.post<{ results: Application[] }>( "/api/applications/search", { searchterm: searchTerm } ); setApplications(response.data.results); setIsSearching(false); } catch (error: any) { console.error("Search error:", error.response?.data); setIsSearching(false); } }; useEffect(() => { const delayDebounce = setTimeout(() => { if (searchTerm.trim() === "") { getApplications(); } else { searchApplications(); } }, 300); return () => clearTimeout(delayDebounce); }, [searchTerm]); const generateIconURL = async () => { setIcon("https://cdn.jsdelivr.net/gh/selfhst/icons/png/" + name.toLowerCase() + ".png") } const generateEditIconURL = async () => { setEditIcon("https://cdn.jsdelivr.net/gh/selfhst/icons/png/" + editName.toLowerCase() + ".png") } return (
/ {t('Applications.Breadcrumb.MyInfrastructure')} {t('Applications.Breadcrumb.Applications')}
{t('Applications.Title')}
toggleLayout("standard")}> {t('Applications.Views.ListView')} toggleLayout("grid")}> {t('Applications.Views.GridView')} toggleLayout("compact")}> {t('Applications.Views.CompactView')} { // Don't immediately apply the change while typing // Just validate the input for visual feedback const value = parseInt(e.target.value); if (isNaN(value) || value < 1 || value > 100) { e.target.classList.add("border-red-500"); } else { e.target.classList.remove("border-red-500"); } }} onBlur={(e) => { // Apply the change when the input loses focus const value = parseInt(e.target.value); if (value >= 1 && value <= 100) { handleItemsPerPageChange(e.target.value); } }} onKeyDown={(e) => { if (e.key === 'Enter') { // Clear any existing debounce timer to apply immediately if (debounceTimerRef.current) { clearTimeout(debounceTimerRef.current); debounceTimerRef.current = null; } const value = parseInt((e.target as HTMLInputElement).value); if (value >= 1 && value <= 100) { // Apply change immediately on Enter const validatedValue = Math.min(Math.max(value, 1), 100); setItemsPerPage(validatedValue); setCurrentPage(1); Cookies.set("itemsPerPage-app", String(validatedValue), { expires: 365, path: "/", sameSite: "strict", }); // Close the dropdown document.body.click(); } } }} onClick={(e) => e.stopPropagation()} /> items
{servers.length === 0 ? (

{t('Applications.Messages.AddServerFirst')}

) : ( {t('Applications.Add.Title')}
setName(e.target.value)} />