New compact view for applications

This commit is contained in:
headlessdev 2025-04-26 13:11:19 +02:00
parent 677d1c5a58
commit fde7b3b23e
2 changed files with 280 additions and 214 deletions

BIN
ApplicationsFormatted.tsx Normal file

Binary file not shown.

View File

@ -25,6 +25,8 @@ import {
List, List,
Pencil, Pencil,
Zap, Zap,
ViewIcon,
Grid3X3,
} from "lucide-react"; } from "lucide-react";
import { import {
Card, Card,
@ -76,6 +78,12 @@ import {
import { StatusIndicator } from "@/components/status-indicator"; import { StatusIndicator } from "@/components/status-indicator";
import { Toaster } from "@/components/ui/sonner" import { Toaster } from "@/components/ui/sonner"
import { toast } from "sonner" import { toast } from "sonner"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
interface Application { interface Application {
id: number; id: number;
@ -129,23 +137,42 @@ export default function Dashboard() {
const savedLayout = Cookies.get("layoutPreference-app"); const savedLayout = Cookies.get("layoutPreference-app");
const savedItemsPerPage = Cookies.get("itemsPerPage-app"); const savedItemsPerPage = Cookies.get("itemsPerPage-app");
const initialIsGridLayout = savedLayout === "grid"; const initialIsGridLayout = savedLayout === "grid";
const defaultItemsPerPage = initialIsGridLayout ? 15 : 5; const initialIsCompactLayout = savedLayout === "compact";
const defaultItemsPerPage = initialIsGridLayout ? 15 : (initialIsCompactLayout ? 30 : 5);
const initialItemsPerPage = savedItemsPerPage ? parseInt(savedItemsPerPage) : defaultItemsPerPage; const initialItemsPerPage = savedItemsPerPage ? parseInt(savedItemsPerPage) : defaultItemsPerPage;
const [isGridLayout, setIsGridLayout] = useState<boolean>(initialIsGridLayout); const [isGridLayout, setIsGridLayout] = useState<boolean>(initialIsGridLayout);
const [isCompactLayout, setIsCompactLayout] = useState<boolean>(initialIsCompactLayout);
const [itemsPerPage, setItemsPerPage] = useState<number>(initialItemsPerPage); const [itemsPerPage, setItemsPerPage] = useState<number>(initialItemsPerPage);
const customInputRef = useRef<HTMLInputElement>(null); const customInputRef = useRef<HTMLInputElement>(null);
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null); const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
const toggleLayout = () => { const toggleLayout = (layout: string) => {
const newLayout = !isGridLayout; if (layout === "standard") {
setIsGridLayout(newLayout); setIsGridLayout(false);
Cookies.set("layoutPreference-app", newLayout ? "grid" : "standard", { setIsCompactLayout(false);
expires: 365, Cookies.set("layoutPreference-app", "standard", {
path: "/", expires: 365,
sameSite: "strict", path: "/",
}); sameSite: "strict",
// Don't automatically change itemsPerPage when layout changes });
} 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) => { const handleItemsPerPageChange = (value: string) => {
@ -335,20 +362,30 @@ export default function Dashboard() {
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-3xl font-bold">Your Applications</span> <span className="text-3xl font-bold">Your Applications</span>
<div className="flex gap-2"> <div className="flex gap-2">
<Button <DropdownMenu>
variant="outline" <DropdownMenuTrigger asChild>
size="icon" <Button variant="outline" size="icon" title="Change view">
onClick={toggleLayout} {isCompactLayout ? (
title={ <Grid3X3 className="h-4 w-4" />
isGridLayout ? "Switch to list view" : "Switch to grid view" ) : isGridLayout ? (
} <LayoutGrid className="h-4 w-4" />
> ) : (
{isGridLayout ? ( <List className="h-4 w-4" />
<List className="h-4 w-4" /> )}
) : ( </Button>
<LayoutGrid className="h-4 w-4" /> </DropdownMenuTrigger>
)} <DropdownMenuContent align="end">
</Button> <DropdownMenuItem onClick={() => toggleLayout("standard")}>
<List className="h-4 w-4 mr-2" /> List View
</DropdownMenuItem>
<DropdownMenuItem onClick={() => toggleLayout("grid")}>
<LayoutGrid className="h-4 w-4 mr-2" /> Grid View
</DropdownMenuItem>
<DropdownMenuItem onClick={() => toggleLayout("compact")}>
<Grid3X3 className="h-4 w-4 mr-2" /> Compact View
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Select <Select
value={String(itemsPerPage)} value={String(itemsPerPage)}
onValueChange={handleItemsPerPageChange} onValueChange={handleItemsPerPageChange}
@ -559,222 +596,251 @@ export default function Dashboard() {
{!loading ? ( {!loading ? (
<div <div
className={ className={
isGridLayout isCompactLayout
? "grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-2"
: isGridLayout
? "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4" ? "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"
: "space-y-4" : "space-y-4"
} }
> >
{applications.map((app) => ( {applications.map((app) => (
<Card isCompactLayout ? (
key={app.id} <div
className={ key={app.id}
isGridLayout className="bg-card rounded-md border p-3 flex flex-col items-center justify-between h-[120px] w-full cursor-pointer hover:shadow-md transition-shadow relative"
? "h-full flex flex-col justify-between relative" onClick={() => window.open(app.publicURL, "_blank")}
: "w-full mb-4 relative" title={app.name}
} >
> <div className="absolute top-1 right-1">
<CardHeader> <StatusIndicator isOnline={app.online} showLabel={false} />
<div className="absolute top-2 right-2">
<StatusIndicator isOnline={app.online} />
</div> </div>
<div className="flex items-center justify-between w-full mt-4 mb-4"> <div className="w-16 h-16 flex-shrink-0 flex items-center justify-center">
<div className="flex items-center"> {app.icon ? (
<div className="w-16 h-16 flex-shrink-0 flex items-center justify-center rounded-md"> <img
{app.icon ? ( src={app.icon}
<img alt={app.name}
src={app.icon} className="w-full h-full object-contain rounded-md"
alt={app.name} />
className="w-full h-full object-contain rounded-md" ) : (
/> <span className="text-gray-500 text-xs">Icon</span>
) : ( )}
<span className="text-gray-500 text-xs">Image</span> </div>
)} <div className="text-center mt-2">
</div> <h3 className="text-sm font-medium truncate w-full max-w-[110px]">{app.name}</h3>
<div className="ml-4"> </div>
<CardTitle className="text-2xl font-bold"> </div>
{app.name} ) : (
</CardTitle> <Card
<CardDescription className="text-md"> key={app.id}
{app.description} className={
{app.description && ( isGridLayout
<br className="hidden md:block" /> ? "h-full flex flex-col justify-between relative"
)} : "w-full mb-4 relative"
Server: {app.server || "No server"} }
</CardDescription> >
</div> <CardHeader>
<div className="absolute top-2 right-2">
<StatusIndicator isOnline={app.online} />
</div> </div>
<div className="flex flex-col items-end justify-start space-y-2 w-[190px]"> <div className="flex items-center justify-between w-full mt-4 mb-4">
<div className="flex items-center gap-2 w-full"> <div className="flex items-center">
<div className="flex flex-col space-y-2 flex-grow"> <div className="w-16 h-16 flex-shrink-0 flex items-center justify-center rounded-md">
<Button {app.icon ? (
variant="outline" <img
className="gap-2 w-full" src={app.icon}
onClick={() => alt={app.name}
window.open(app.publicURL, "_blank") className="w-full h-full object-contain rounded-md"
} />
> ) : (
<Link className="h-4 w-4" /> <span className="text-gray-500 text-xs">Image</span>
Public URL )}
</Button> </div>
{app.localURL && ( <div className="ml-4">
<CardTitle className="text-2xl font-bold">
{app.name}
</CardTitle>
<CardDescription className="text-md">
{app.description}
{app.description && (
<br className="hidden md:block" />
)}
Server: {app.server || "No server"}
</CardDescription>
</div>
</div>
<div className="flex flex-col items-end justify-start space-y-2 w-[190px]">
<div className="flex items-center gap-2 w-full">
<div className="flex flex-col space-y-2 flex-grow">
<Button <Button
variant="outline" variant="outline"
className="gap-2 w-full" className="gap-2 w-full"
onClick={() => onClick={() =>
window.open(app.localURL, "_blank") window.open(app.publicURL, "_blank")
} }
> >
<Home className="h-4 w-4" /> <Link className="h-4 w-4" />
Local URL Public URL
</Button> </Button>
)} {app.localURL && (
</div>
<div className="flex flex-col gap-2">
<Button
variant="destructive"
size="icon"
className="h-9 w-9"
onClick={() => deleteApplication(app.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button <Button
size="icon" variant="outline"
className="h-9 w-9" className="gap-2 w-full"
onClick={() => openEditDialog(app)} onClick={() =>
window.open(app.localURL, "_blank")
}
> >
<Pencil className="h-4 w-4" /> <Home className="h-4 w-4" />
Local URL
</Button> </Button>
</AlertDialogTrigger> )}
<AlertDialogContent> </div>
<AlertDialogHeader> <div className="flex flex-col gap-2">
<AlertDialogTitle> <Button
Edit Application variant="destructive"
</AlertDialogTitle> size="icon"
<AlertDialogDescription> className="h-9 w-9"
<div className="space-y-4 pt-4"> onClick={() => deleteApplication(app.id)}
<div className="grid w-full items-center gap-1.5"> >
<Label>Name</Label> <Trash2 className="h-4 w-4" />
<Input </Button>
placeholder="e.g. Portainer" <AlertDialog>
value={editName} <AlertDialogTrigger asChild>
onChange={(e) => <Button
setEditName(e.target.value) size="icon"
} className="h-9 w-9"
/> onClick={() => openEditDialog(app)}
</div> >
<div className="grid w-full items-center gap-1.5"> <Pencil className="h-4 w-4" />
<Label>Server</Label> </Button>
<Select </AlertDialogTrigger>
value={ <AlertDialogContent>
editServerId !== null <AlertDialogHeader>
? String(editServerId) <AlertDialogTitle>
: undefined Edit Application
} </AlertDialogTitle>
onValueChange={(v) => <AlertDialogDescription>
setEditServerId(Number(v)) <div className="space-y-4 pt-4">
} <div className="grid w-full items-center gap-1.5">
required <Label>Name</Label>
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select server" />
</SelectTrigger>
<SelectContent>
{servers.map((server) => (
<SelectItem
key={server.id}
value={String(server.id)}
>
{server.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid w-full items-center gap-1.5">
<Label>
Description{" "}
<span className="text-stone-600">
(optional)
</span>
</Label>
<Textarea
placeholder="Application description"
value={editDescription}
onChange={(e) =>
setEditDescription(e.target.value)
}
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label>
Icon URL{" "}
<span className="text-stone-600">
(optional)
</span>
</Label>
<div className="flex gap-2">
<Input <Input
placeholder="https://example.com/icon.png" placeholder="e.g. Portainer"
value={editIcon} value={editName}
onChange={(e) => onChange={(e) =>
setEditIcon(e.target.value) setEditName(e.target.value)
}
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label>Server</Label>
<Select
value={
editServerId !== null
? String(editServerId)
: undefined
}
onValueChange={(v) =>
setEditServerId(Number(v))
}
required
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select server" />
</SelectTrigger>
<SelectContent>
{servers.map((server) => (
<SelectItem
key={server.id}
value={String(server.id)}
>
{server.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid w-full items-center gap-1.5">
<Label>
Description{" "}
<span className="text-stone-600">
(optional)
</span>
</Label>
<Textarea
placeholder="Application description"
value={editDescription}
onChange={(e) =>
setEditDescription(e.target.value)
}
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label>
Icon URL{" "}
<span className="text-stone-600">
(optional)
</span>
</Label>
<div className="flex gap-2">
<Input
placeholder="https://example.com/icon.png"
value={editIcon}
onChange={(e) =>
setEditIcon(e.target.value)
}
/>
<Button variant="outline" size="icon" onClick={generateEditIconURL}>
<Zap />
</Button>
</div>
</div>
<div className="grid w-full items-center gap-1.5">
<Label>Public URL</Label>
<Input
placeholder="https://example.com"
value={editPublicURL}
onChange={(e) =>
setEditPublicURL(e.target.value)
}
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label>
Local URL{" "}
<span className="text-stone-600">
(optional)
</span>
</Label>
<Input
placeholder="http://localhost:3000"
value={editLocalURL}
onChange={(e) =>
setEditLocalURL(e.target.value)
} }
/> />
<Button variant="outline" size="icon" onClick={generateEditIconURL}>
<Zap />
</Button>
</div> </div>
</div> </div>
<div className="grid w-full items-center gap-1.5"> </AlertDialogDescription>
<Label>Public URL</Label> </AlertDialogHeader>
<Input <AlertDialogFooter>
placeholder="https://example.com" <AlertDialogCancel>Cancel</AlertDialogCancel>
value={editPublicURL} <AlertDialogAction
onChange={(e) => onClick={edit}
setEditPublicURL(e.target.value) disabled={
} !editName || !editPublicURL || !editServerId
/> }
</div> >
<div className="grid w-full items-center gap-1.5"> Save Changes
<Label> </AlertDialogAction>
Local URL{" "} </AlertDialogFooter>
<span className="text-stone-600"> </AlertDialogContent>
(optional) </AlertDialog>
</span> </div>
</Label>
<Input
placeholder="http://localhost:3000"
value={editLocalURL}
onChange={(e) =>
setEditLocalURL(e.target.value)
}
/>
</div>
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={edit}
disabled={
!editName || !editPublicURL || !editServerId
}
>
Save Changes
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div> </div>
</div> </div>
</div> </div>
</div> </CardHeader>
</CardHeader> </Card>
</Card> )
))} ))}
</div> </div>
) : ( ) : (