"use client"; import { useState } from "react"; import Image from "next/image"; import { api } from "~/trpc/react"; import type { Script } from "~/types/script"; import { DiffViewer } from "./DiffViewer"; import { TextViewer } from "./TextViewer"; import { ExecutionModeModal } from "./ExecutionModeModal"; interface ScriptDetailModalProps { script: Script | null; isOpen: boolean; onClose: () => void; onInstallScript?: ( scriptPath: string, scriptName: string, mode?: "local" | "ssh", server?: any, ) => void; } export function ScriptDetailModal({ script, isOpen, onClose, onInstallScript, }: ScriptDetailModalProps) { const [imageError, setImageError] = useState(false); const [isLoading, setIsLoading] = useState(false); const [loadMessage, setLoadMessage] = useState(null); const [diffViewerOpen, setDiffViewerOpen] = useState(false); const [selectedDiffFile, setSelectedDiffFile] = useState(null); const [textViewerOpen, setTextViewerOpen] = useState(false); const [executionModeOpen, setExecutionModeOpen] = useState(false); // Check if script files exist locally const { data: scriptFilesData, refetch: refetchScriptFiles, isLoading: scriptFilesLoading, } = api.scripts.checkScriptFiles.useQuery( { slug: script?.slug ?? "" }, { enabled: !!script && isOpen }, ); // Compare local and remote script content (run in parallel, not dependent on scriptFilesData) const { data: comparisonData, refetch: refetchComparison, isLoading: comparisonLoading, } = api.scripts.compareScriptContent.useQuery( { slug: script?.slug ?? "" }, { enabled: !!script && isOpen }, ); // Load script mutation const loadScriptMutation = api.scripts.loadScript.useMutation({ onSuccess: (data) => { setIsLoading(false); if (data.success) { const message = "message" in data ? data.message : "Script loaded successfully"; setLoadMessage(`✅ ${message}`); // Refetch script files status and comparison data to update the UI void refetchScriptFiles(); void refetchComparison(); } else { const error = "error" in data ? data.error : "Failed to load script"; setLoadMessage(`❌ ${error}`); } // Clear message after 5 seconds setTimeout(() => setLoadMessage(null), 5000); }, onError: (error) => { setIsLoading(false); setLoadMessage(`❌ Error: ${error.message}`); setTimeout(() => setLoadMessage(null), 5000); }, }); if (!isOpen || !script) return null; const handleImageError = () => { setImageError(true); }; const handleBackdropClick = (e: React.MouseEvent) => { if (e.target === e.currentTarget) { onClose(); } }; const handleLoadScript = async () => { if (!script) return; setIsLoading(true); setLoadMessage(null); loadScriptMutation.mutate({ slug: script.slug }); }; const handleInstallScript = () => { if (!script) return; setExecutionModeOpen(true); }; const handleExecuteScript = (mode: "local" | "ssh", server?: any) => { if (!script || !onInstallScript) return; // Find the script path (CT or tools) const scriptMethod = script.install_methods?.find( (method) => method.script, ); if (scriptMethod?.script) { const scriptPath = `scripts/${scriptMethod.script}`; const scriptName = script.name; // Pass execution mode and server info to the parent onInstallScript(scriptPath, scriptName, mode, server); // Scroll to top of the page to see the terminal window.scrollTo({ top: 0, behavior: "smooth" }); onClose(); // Close the modal when starting installation } }; const handleViewScript = () => { setTextViewerOpen(true); }; return (
{/* Header */}
{script.logo && !imageError ? ( {`${script.name} ) : (
{script.name.charAt(0).toUpperCase()}
)}

{script.name}

{script.type.toUpperCase()} {script.updateable && ( Updateable )} {script.privileged && ( Privileged )}
{/* Install Button - only show if script files exist */} {scriptFilesData?.success && scriptFilesData.ctExists && onInstallScript && ( )} {/* View Button - only show if script files exist */} {scriptFilesData?.success && (scriptFilesData.ctExists || scriptFilesData.installExists) && ( )} {/* Load/Update Script Button */} {(() => { const hasLocalFiles = scriptFilesData?.success && (scriptFilesData.ctExists || scriptFilesData.installExists); const hasDifferences = comparisonData?.success && comparisonData.hasDifferences; const isUpToDate = hasLocalFiles && !hasDifferences; if (!hasLocalFiles) { // No local files - show Load Script button return ( ); } else if (isUpToDate) { // Local files exist and are up to date - show disabled Update button return ( ); } else { // Local files exist but have differences - show Update button return ( ); } })()}
{/* Load Message */} {loadMessage && (
{loadMessage}
)} {/* Script Files Status */} {(scriptFilesLoading || comparisonLoading) && (
Loading script status...
)} {scriptFilesData?.success && !scriptFilesLoading && (() => { // Determine script type from the first install method const firstScript = script?.install_methods?.[0]?.script; let scriptType = "Script"; if (firstScript?.startsWith("ct/")) { scriptType = "CT Script"; } else if (firstScript?.startsWith("tools/")) { scriptType = "Tools Script"; } else if (firstScript?.startsWith("vm/")) { scriptType = "VM Script"; } else if (firstScript?.startsWith("vw/")) { scriptType = "VW Script"; } return (
{scriptType}:{" "} {scriptFilesData.ctExists ? "Available" : "Not loaded"}
Install Script:{" "} {scriptFilesData.installExists ? "Available" : "Not loaded"}
{scriptFilesData?.success && (scriptFilesData.ctExists || scriptFilesData.installExists) && comparisonData?.success && !comparisonLoading && (
Status:{" "} {comparisonData.hasDifferences ? "Update available" : "Up to date"}
)}
{scriptFilesData.files.length > 0 && (
Files: {scriptFilesData.files.join(", ")}
)}
); })()} {/* Content */}
{/* Description */}

Description

{script.description}

{/* Basic Information */}

Basic Information

Slug
{script.slug}
Date Created
{script.date_created}
Categories
{script.categories.join(", ")}
{script.interface_port && (
Interface Port
{script.interface_port}
)} {script.config_path && (
Config Path
{script.config_path}
)}

Links

{script.website && ( )} {script.documentation && ( )}
{/* Install Methods - Hide for PVE and ADDON types as they typically don't have install methods */} {script.install_methods.length > 0 && script.type !== "pve" && script.type !== "addon" && (

Install Methods

{script.install_methods.map((method, index) => (

{method.type}

{method.script}
CPU
{method.resources.cpu} cores
RAM
{method.resources.ram} MB
HDD
{method.resources.hdd} GB
OS
{method.resources.os} {method.resources.version}
))}
)} {/* Default Credentials */} {(script.default_credentials.username ?? script.default_credentials.password) && (

Default Credentials

{script.default_credentials.username && (
Username
{script.default_credentials.username}
)} {script.default_credentials.password && (
Password
{script.default_credentials.password}
)}
)} {/* Notes */} {script.notes.length > 0 && (

Notes

    {script.notes.map((note, index) => { // Handle both object and string note formats const noteText = typeof note === "string" ? note : note.text; const noteType = typeof note === "string" ? "info" : note.type; return (
  • {noteType} {noteText}
  • ); })}
)}
{/* Diff Viewer Modal */} {selectedDiffFile && ( { setDiffViewerOpen(false); setSelectedDiffFile(null); }} /> )} {/* Text Viewer Modal */} {script && ( method.script?.startsWith("ct/")) ?.script?.split("/") .pop() ?? `${script.slug}.sh` } isOpen={textViewerOpen} onClose={() => setTextViewerOpen(false)} /> )} {/* Execution Mode Modal */} {script && ( setExecutionModeOpen(false)} onExecute={handleExecuteScript} /> )}
); }