App dialogs

This commit is contained in:
2025-08-04 15:28:23 -05:00
parent e63d0dfee8
commit 7f84a67456
8 changed files with 232 additions and 6 deletions
+3
View File
@@ -0,0 +1,3 @@
{
"install": "sh test"
}
+1
View File
@@ -0,0 +1 @@
{}
+1 -1
View File
@@ -185,7 +185,7 @@
--popover-foreground: oklch(0.9108 0.0427 269.5516); --popover-foreground: oklch(0.9108 0.0427 269.5516);
--primary: oklch(0.7182 0.1105 239.784); --primary: oklch(0.7182 0.1105 239.784);
--primary-foreground: oklch(0.3233 0.1025 253.8852); --primary-foreground: oklch(0.3233 0.1025 253.8852);
--secondary: oklch(0.5544 0.0407 257.4166); --secondary: oklch(0.38 0.06 258);
--secondary-foreground: oklch(0.9288 0.0126 255.5078); --secondary-foreground: oklch(0.9288 0.0126 255.5078);
--muted: oklch(0.3923 0.0613 254.3914); --muted: oklch(0.3923 0.0613 254.3914);
--muted-foreground: oklch(0.7584 0.0419 263.598); --muted-foreground: oklch(0.7584 0.0419 263.598);
+78
View File
@@ -0,0 +1,78 @@
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Button, buttonVariants } from "@/components/ui/button";
import { Plus } from "lucide-react";
import { cn } from "@/lib/utils";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useState } from "react";
import Session from "@/lib/session";
import { toast } from "sonner";
export default function AddAppDialog({
session,
}: {
session: Session | null;
}) {
const [open, setOpen] = useState(false);
const [name, setName] = useState("");
const [ip, setIp] = useState("");
const [location, setLocation] = useState<string | undefined>(undefined);
const [coordinates, setCoordinates] = useState<[number, number] | undefined>(
undefined
);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger
className={cn(buttonVariants({ variant: "default" }))}
onClick={() => setOpen(true)}
>
<Plus className="m-0 p-0" /> New
</DialogTrigger>
<DialogContent className="max-h-[85vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Add Application</DialogTitle>
</DialogHeader>
<Label className="mt-2">
Application Name<span className="text-red-500">*</span>
</Label>
<Input
placeholder="Sentryx chat, My Project, etc"
required
onInput={(e) => setName(e.currentTarget.value)}
/>
<Label className="mt-2">
Application service<span className="text-red-500">*</span>
</Label>
<Input
placeholder="my-app.service"
required
onInput={(e) => setIp(e.currentTarget.value)}
/>
<DialogFooter>
<Button variant="secondary" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button
disabled={name == "" || ip == ""}
onClick={() => {
setOpen(false);
}}
>
Add App
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
+1 -1
View File
@@ -87,7 +87,7 @@ export default function AppComponent({
return ( return (
<Card <Card
className="w-full transition-transform duration-500 hover:scale-102 hover:cursor-pointer !select-none border-accent border-2" className="w-full transition-transform duration-500 hover:scale-102 hover:cursor-pointer !select-none border-secondary border-2"
onClick={() => { onClick={() => {
redirect(`/servers/${index}`); redirect(`/servers/${index}`);
}} }}
+7 -4
View File
@@ -10,6 +10,8 @@ import AppComponent from "./AppComponent";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { toast } from "sonner"; import { toast } from "sonner";
import AddAppDialog from "./AddAppDialog";
import InstallAppDialog from "./IntallAppDialog";
interface Server2 extends Server { interface Server2 extends Server {
services: Service[], services: Service[],
@@ -40,12 +42,13 @@ export default function ClientPage({ serverId }: { serverId: string }) {
<Card> <Card>
<CardHeader className="flex"> <CardHeader className="flex">
<div> <div>
<CardTitle>Sentryx Apps</CardTitle> <CardTitle>Sentryx Applications</CardTitle>
<CardDescription>Your Sentryx apps on this server</CardDescription> <CardDescription>Your Sentryx apps on this server</CardDescription>
</div> </div>
<Button className="ml-auto" onClick={() => { <div className="ml-auto w-max flex gap-3">
toast.warning("Functionality not added yet"); <InstallAppDialog session={session} />
}}>Add</Button> <AddAppDialog session={session} />
</div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-5"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-5">
@@ -0,0 +1,79 @@
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Button, buttonVariants } from "@/components/ui/button";
import { HardDriveDownload, Plus } from "lucide-react";
import { cn } from "@/lib/utils";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useState } from "react";
import Session from "@/lib/session";
import { toast } from "sonner";
import { FileUpload } from "@/components/FileUpload";
export default function InstallAppDialog({
session,
}: {
session: Session | null;
}) {
const [open, setOpen] = useState(false);
const [name, setName] = useState("");
const [script, setScript] = useState<{ install?: string } | undefined>({});
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger
className={cn(buttonVariants({ variant: "secondary" }))}
onClick={() => setOpen(true)}
>
<HardDriveDownload className="m-0 p-0" /> Install
</DialogTrigger>
<DialogContent className="max-h-[85vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Install Application</DialogTitle>
</DialogHeader>
<Label className="mt-2">
Application Name<span className="text-red-500">*</span>
</Label>
<Input
placeholder="Sentryx chat, My Project, etc"
required
onInput={(e) => setName(e.currentTarget.value)}
/>
<Label className="mt-2">
Application script<span className="text-red-500">*</span>
</Label>
<FileUpload setSelected={(contents) => {
try {
setScript(JSON.parse(contents))
console.log(contents)
} catch {
setScript(undefined)
}
}} />
<DialogFooter>
<Button variant="secondary" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button
disabled={name == "" || !script || !(script.install)}
onClick={() => {
setOpen(false);
}}
>
Install
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
+62
View File
@@ -0,0 +1,62 @@
import { useState, useRef } from "react"
import { Button } from "@/components/ui/button"
import { cn } from "@/lib/utils" // optional utility for combining classes
export function FileUpload({setSelected} : {setSelected: (contents: string) => void}) {
const inputRef = useRef<HTMLInputElement | null>(null)
const [fileName, setFileName] = useState<string | null>(null)
const [isDragging, setIsDragging] = useState(false)
const [isUnsupported, setIsUnsupported] = useState(false)
const handleFiles = (files: FileList | null) => {
const file = files?.[0]
if (!file) return
setIsUnsupported(!file.name.endsWith('.json'));
if (isUnsupported) return
setFileName(file.name)
const reader = new FileReader()
reader.onload = (e) => {
setSelected(e.target?.result as string)
}
reader.readAsText(file)
}
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault()
setIsDragging(false)
handleFiles(e.dataTransfer.files)
}
return (
<div className="space-y-2">
<input
ref={inputRef}
type="file"
onChange={(e) => handleFiles(e.target.files)}
className="hidden"
/>
<div
onClick={() => inputRef.current?.click()}
onDragOver={(e) => {
e.preventDefault()
setIsDragging(true)
}}
onDragLeave={() => setIsDragging(false)}
onDrop={handleDrop}
className={cn(
"flex h-32 cursor-pointer items-center justify-center rounded-md border-2 border-dashed transition-colors",
isDragging ? "border-primary bg-muted" : "border-muted",
isUnsupported && "border-destructive"
)}
>
<span className="text-muted-foreground">
{isUnsupported ? "Unsupported file type" : (fileName || "Click or drag file to upload")}
</span>
</div>
</div>
)
}