core: move misc scripts to structured addon/pve paths | Refactor JSON Editor & Script Mapping (#3765)

* Move Scripts to Tools / Add-Ons

* fix json editor slug generating

* update type in jsons

* remove wrong method

* move copy-data to tools
This commit is contained in:
CanbiZ
2025-04-09 13:10:02 +02:00
committed by GitHub
parent f2f10376ac
commit 3dffd02f08
88 changed files with 1327 additions and 1481 deletions

View File

@ -1,12 +1,12 @@
"use client";
import React, { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Category } from "@/lib/types";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { useRouter } from "next/navigation";
import React, { useEffect, useState } from "react";
const defaultLogo = "/default-logo.png"; // Fallback logo path
const MAX_DESCRIPTION_LENGTH = 100; // Set max length for description
@ -17,11 +17,11 @@ const formattedBadge = (type: string) => {
case "vm":
return <Badge className="text-blue-500/75 border-blue-500/75 badge">VM</Badge>;
case "ct":
return (
<Badge className="text-yellow-500/75 border-yellow-500/75 badge">LXC</Badge>
);
case "misc":
return <Badge className="text-green-500/75 border-green-500/75 badge">MISC</Badge>;
return <Badge className="text-yellow-500/75 border-yellow-500/75 badge">LXC</Badge>;
case "pve":
return <Badge className="text-orange-500/75 border-orange-500/75 badge">PVE</Badge>;
case "addon":
return <Badge className="text-green-500/75 border-green-500/75 badge">ADDON</Badge>;
}
return null;
};
@ -100,9 +100,7 @@ const CategoryView = () => {
};
const truncateDescription = (text: string) => {
return text.length > MAX_DESCRIPTION_LENGTH
? `${text.slice(0, MAX_DESCRIPTION_LENGTH)}...`
: text;
return text.length > MAX_DESCRIPTION_LENGTH ? `${text.slice(0, MAX_DESCRIPTION_LENGTH)}...` : text;
};
const renderResources = (script: any) => {
@ -111,9 +109,24 @@ const CategoryView = () => {
const hdd = script.install_methods[0]?.resources.hdd;
const resourceParts = [];
if (cpu) resourceParts.push(<span key="cpu"><b>CPU:</b> {cpu}vCPU</span>);
if (ram) resourceParts.push(<span key="ram"><b>RAM:</b> {ram}MB</span>);
if (hdd) resourceParts.push(<span key="hdd"><b>HDD:</b> {hdd}GB</span>);
if (cpu)
resourceParts.push(
<span key="cpu">
<b>CPU:</b> {cpu}vCPU
</span>,
);
if (ram)
resourceParts.push(
<span key="ram">
<b>RAM:</b> {ram}MB
</span>,
);
if (hdd)
resourceParts.push(
<span key="hdd">
<b>HDD:</b> {hdd}GB
</span>,
);
return resourceParts.length > 0 ? (
<div className="text-sm text-gray-400">

View File

@ -1,12 +1,6 @@
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { OperatingSystems } from "@/config/siteConfig";
import { PlusCircle, Trash2 } from "lucide-react";
import { memo, useCallback, useRef } from "react";
@ -20,21 +14,29 @@ type InstallMethodProps = {
setZodErrors: (zodErrors: z.ZodError | null) => void;
};
function InstallMethod({
script,
setScript,
setIsValid,
setZodErrors,
}: InstallMethodProps) {
function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallMethodProps) {
const cpuRefs = useRef<(HTMLInputElement | null)[]>([]);
const ramRefs = useRef<(HTMLInputElement | null)[]>([]);
const hddRefs = useRef<(HTMLInputElement | null)[]>([]);
const addInstallMethod = useCallback(() => {
setScript((prev) => {
const { type, slug } = prev;
const newMethodType = "default";
let scriptPath = "";
if (type === "pve") {
scriptPath = `tools/pve/${slug}.sh`;
} else if (type === "addon") {
scriptPath = `tools/addon/${slug}.sh`;
} else {
scriptPath = `${type}/${slug}.sh`;
}
const method = InstallMethodSchema.parse({
type: "default",
script: `${prev.type}/${prev.slug}.sh`,
type: newMethodType,
script: scriptPath,
resources: {
cpu: null,
ram: null,
@ -43,6 +45,7 @@ function InstallMethod({
version: null,
},
});
return {
...prev,
install_methods: [...prev.install_methods, method],
@ -63,9 +66,7 @@ function InstallMethod({
if (key === "type") {
updatedMethod.script =
value === "alpine"
? `${prev.type}/alpine-${prev.slug}.sh`
: `${prev.type}/${prev.slug}.sh`;
value === "alpine" ? `${prev.type}/alpine-${prev.slug}.sh` : `${prev.type}/${prev.slug}.sh`;
// Set OS to Alpine and reset version if type is alpine
if (value === "alpine") {
@ -112,10 +113,7 @@ function InstallMethod({
<h3 className="text-xl font-semibold">Install Methods</h3>
{script.install_methods.map((method, index) => (
<div key={index} className="space-y-2 border p-4 rounded">
<Select
value={method.type}
onValueChange={(value) => updateInstallMethod(index, "type", value)}
>
<Select value={method.type} onValueChange={(value) => updateInstallMethod(index, "type", value)}>
<SelectTrigger>
<SelectValue placeholder="Type" />
</SelectTrigger>
@ -205,9 +203,7 @@ function InstallMethod({
<SelectValue placeholder="Version" />
</SelectTrigger>
<SelectContent>
{OperatingSystems.find(
(os) => os.name === method.resources.os,
)?.versions.map((version) => (
{OperatingSystems.find((os) => os.name === method.resources.os)?.versions.map((version) => (
<SelectItem key={version.slug} value={version.name}>
{version.name}
</SelectItem>
@ -215,22 +211,12 @@ function InstallMethod({
</SelectContent>
</Select>
</div>
<Button
variant="destructive"
size="sm"
type="button"
onClick={() => removeInstallMethod(index)}
>
<Button variant="destructive" size="sm" type="button" onClick={() => removeInstallMethod(index)}>
<Trash2 className="mr-2 h-4 w-4" /> Remove Install Method
</Button>
</div>
))}
<Button
type="button"
size="sm"
disabled={script.install_methods.length >= 2}
onClick={addInstallMethod}
>
<Button type="button" size="sm" disabled={script.install_methods.length >= 2} onClick={addInstallMethod}>
<PlusCircle className="mr-2 h-4 w-4" /> Add Install Method
</Button>
</>

View File

@ -24,8 +24,8 @@ export const ScriptSchema = z.object({
slug: z.string().min(1, "Slug is required"),
categories: z.array(z.number()),
date_created: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be in YYYY-MM-DD format").min(1, "Date is required"),
type: z.enum(["vm", "ct", "misc", "turnkey"], {
errorMap: () => ({ message: "Type must be either 'vm', 'ct', 'misc' or 'turnkey'" })
type: z.enum(["vm", "ct", "pve", "addon", "turnkey"], {
errorMap: () => ({ message: "Type must be either 'vm', 'ct', 'pve', 'addon' or 'turnkey'" })
}),
updateable: z.boolean(),
privileged: z.boolean(),

View File

@ -5,18 +5,8 @@ import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import { fetchCategories } from "@/lib/data";
@ -66,29 +56,37 @@ export default function JSONGenerator() {
.catch((error) => console.error("Error fetching categories:", error));
}, []);
const updateScript = useCallback(
(key: keyof Script, value: Script[keyof Script]) => {
setScript((prev) => {
const updated = { ...prev, [key]: value };
const updateScript = useCallback((key: keyof Script, value: Script[keyof Script]) => {
setScript((prev) => {
const updated = { ...prev, [key]: value };
if (key === "type" || key === "slug") {
updated.install_methods = updated.install_methods.map((method) => ({
if (updated.slug && updated.type) {
updated.install_methods = updated.install_methods.map((method) => {
let scriptPath = "";
if (updated.type === "pve") {
scriptPath = `tools/pve/${updated.slug}.sh`;
} else if (updated.type === "addon") {
scriptPath = `tools/addon/${updated.slug}.sh`;
} else if (method.type === "alpine") {
scriptPath = `${updated.type}/alpine-${updated.slug}.sh`;
} else {
scriptPath = `${updated.type}/${updated.slug}.sh`;
}
return {
...method,
script:
method.type === "alpine"
? `${updated.type}/alpine-${updated.slug}.sh`
: `${updated.type}/${updated.slug}.sh`,
}));
}
script: scriptPath,
};
});
}
const result = ScriptSchema.safeParse(updated);
setIsValid(result.success);
setZodErrors(result.success ? null : result.error);
return updated;
});
},
[],
);
const result = ScriptSchema.safeParse(updated);
setIsValid(result.success);
setZodErrors(result.success ? null : result.error);
return updated;
});
}, []);
const handleCopy = useCallback(() => {
navigator.clipboard.writeText(JSON.stringify(script, null, 2));
@ -101,13 +99,13 @@ export default function JSONGenerator() {
const jsonString = JSON.stringify(script, null, 2);
const blob = new Blob([jsonString], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${script.slug || "script"}.json`;
document.body.appendChild(a);
a.click();
URL.revokeObjectURL(url);
document.body.removeChild(a);
}, [script]);
@ -120,16 +118,13 @@ export default function JSONGenerator() {
);
const formattedDate = useMemo(
() =>
script.date_created ? format(script.date_created, "PPP") : undefined,
() => (script.date_created ? format(script.date_created, "PPP") : undefined),
[script.date_created],
);
const validationAlert = useMemo(
() => (
<Alert
className={cn("text-black", isValid ? "bg-green-100" : "bg-red-100")}
>
<Alert className={cn("text-black", isValid ? "bg-green-100" : "bg-red-100")}>
<AlertTitle>{isValid ? "Valid JSON" : "Invalid JSON"}</AlertTitle>
<AlertDescription>
{isValid
@ -160,21 +155,13 @@ export default function JSONGenerator() {
<Label>
Name <span className="text-red-500">*</span>
</Label>
<Input
placeholder="Example"
value={script.name}
onChange={(e) => updateScript("name", e.target.value)}
/>
<Input placeholder="Example" value={script.name} onChange={(e) => updateScript("name", e.target.value)} />
</div>
<div>
<Label>
Slug <span className="text-red-500">*</span>
</Label>
<Input
placeholder="example"
value={script.slug}
onChange={(e) => updateScript("slug", e.target.value)}
/>
<Input placeholder="example" value={script.slug} onChange={(e) => updateScript("slug", e.target.value)} />
</div>
</div>
<div>
@ -197,11 +184,7 @@ export default function JSONGenerator() {
onChange={(e) => updateScript("description", e.target.value)}
/>
</div>
<Categories
script={script}
setScript={setScript}
categories={categories}
/>
<Categories script={script} setScript={setScript} categories={categories} />
<div className="flex gap-2">
<div className="flex flex-col gap-2 w-full">
<Label>Date Created</Label>
@ -209,10 +192,7 @@ export default function JSONGenerator() {
<PopoverTrigger asChild className="flex-1">
<Button
variant={"outline"}
className={cn(
"pl-3 text-left font-normal w-full",
!script.date_created && "text-muted-foreground",
)}
className={cn("pl-3 text-left font-normal w-full", !script.date_created && "text-muted-foreground")}
>
{formattedDate || <span>Pick a date</span>}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
@ -230,38 +210,26 @@ export default function JSONGenerator() {
</div>
<div className="flex flex-col gap-2 w-full">
<Label>Type</Label>
<Select
value={script.type}
onValueChange={(value) => updateScript("type", value)}
>
<Select value={script.type} onValueChange={(value) => updateScript("type", value)}>
<SelectTrigger className="flex-1">
<SelectValue placeholder="Type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="ct">LXC Container</SelectItem>
<SelectItem value="vm">Virtual Machine</SelectItem>
<SelectItem value="misc">Miscellaneous</SelectItem>
<SelectItem value="pve">PVE-Tool</SelectItem>
<SelectItem value="addon">Add-On</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="w-full flex gap-5">
<div className="flex items-center space-x-2">
<Switch
checked={script.updateable}
onCheckedChange={(checked) =>
updateScript("updateable", checked)
}
/>
<Switch checked={script.updateable} onCheckedChange={(checked) => updateScript("updateable", checked)} />
<label>Updateable</label>
</div>
<div className="flex items-center space-x-2">
<Switch
checked={script.privileged}
onCheckedChange={(checked) =>
updateScript("privileged", checked)
}
/>
<Switch checked={script.privileged} onCheckedChange={(checked) => updateScript("privileged", checked)} />
<label>Privileged</label>
</div>
</div>
@ -269,12 +237,7 @@ export default function JSONGenerator() {
placeholder="Interface Port"
type="number"
value={script.interface_port || ""}
onChange={(e) =>
updateScript(
"interface_port",
e.target.value ? Number(e.target.value) : null,
)
}
onChange={(e) => updateScript("interface_port", e.target.value ? Number(e.target.value) : null)}
/>
<div className="flex gap-2">
<Input
@ -285,17 +248,10 @@ export default function JSONGenerator() {
<Input
placeholder="Documentation URL"
value={script.documentation || ""}
onChange={(e) =>
updateScript("documentation", e.target.value || null)
}
onChange={(e) => updateScript("documentation", e.target.value || null)}
/>
</div>
<InstallMethod
script={script}
setScript={setScript}
setIsValid={setIsValid}
setZodErrors={setZodErrors}
/>
<InstallMethod script={script} setScript={setScript} setIsValid={setIsValid} setZodErrors={setZodErrors} />
<h3 className="text-xl font-semibold">Default Credentials</h3>
<Input
placeholder="Username"
@ -317,34 +273,21 @@ export default function JSONGenerator() {
})
}
/>
<Note
script={script}
setScript={setScript}
setIsValid={setIsValid}
setZodErrors={setZodErrors}
/>
<Note script={script} setScript={setScript} setIsValid={setIsValid} setZodErrors={setZodErrors} />
</form>
</div>
<div className="w-1/2 p-4 bg-background overflow-y-auto">
{validationAlert}
<div className="relative">
<div className="absolute right-2 top-2 flex gap-1">
<Button
size="icon"
variant="outline"
onClick={handleCopy}
>
<Button size="icon" variant="outline" onClick={handleCopy}>
{isCopied ? <Check className="h-4 w-4" /> : <Clipboard className="h-4 w-4" />}
</Button>
<Button
size="icon"
variant="outline"
onClick={handleDownload}
>
<Button size="icon" variant="outline" onClick={handleDownload}>
<Download className="h-4 w-4" />
</Button>
</div>
<pre className="mt-4 p-4 bg-secondary rounded shadow overflow-x-scroll">
{JSON.stringify(script, null, 2)}
</pre>

View File

@ -1,12 +1,5 @@
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { basePath, mostPopularScripts } from "@/config/siteConfig";
import { extractDate } from "@/lib/time";
import { Category, Script } from "@/lib/types";
@ -23,7 +16,8 @@ export const getDisplayValueFromType = (type: string) => {
return "LXC";
case "vm":
return "VM";
case "misc":
case "pve":
case "addon":
return "";
default:
return "";
@ -35,7 +29,7 @@ export function LatestScripts({ items }: { items: Category[] }) {
const latestScripts = useMemo(() => {
if (!items) return [];
const scripts = items.flatMap((category) => category.scripts || []);
// Filter out duplicates by slug
@ -47,8 +41,7 @@ export function LatestScripts({ items }: { items: Category[] }) {
});
return Array.from(uniqueScriptsMap.values()).sort(
(a, b) =>
new Date(b.date_created).getTime() - new Date(a.date_created).getTime(),
(a, b) => new Date(b.date_created).getTime() - new Date(a.date_created).getTime(),
);
}, [items]);
@ -59,7 +52,7 @@ export function LatestScripts({ items }: { items: Category[] }) {
const goToPreviousPage = () => {
setPage((prevPage) => prevPage - 1);
};
const startIndex = (page - 1) * ITEMS_PER_PAGE;
const endIndex = page * ITEMS_PER_PAGE;
@ -74,18 +67,12 @@ export function LatestScripts({ items }: { items: Category[] }) {
<h2 className="text-lg font-semibold">Newest Scripts</h2>
<div className="flex items-center justify-end gap-1">
{page > 1 && (
<div
className="cursor-pointer select-none p-2 text-sm font-semibold"
onClick={goToPreviousPage}
>
<div className="cursor-pointer select-none p-2 text-sm font-semibold" onClick={goToPreviousPage}>
Previous
</div>
)}
{endIndex < latestScripts.length && (
<div
onClick={goToNextPage}
className="cursor-pointer select-none p-2 text-sm font-semibold"
>
<div onClick={goToNextPage} className="cursor-pointer select-none p-2 text-sm font-semibold">
{page === 1 ? "More.." : "Next"}
</div>
)}
@ -94,10 +81,7 @@ export function LatestScripts({ items }: { items: Category[] }) {
)}
<div className="min-w flex w-full flex-row flex-wrap gap-4">
{latestScripts.slice(startIndex, endIndex).map((script) => (
<Card
key={script.slug}
className="min-w-[250px] flex-1 flex-grow bg-accent/30"
>
<Card key={script.slug} className="min-w-[250px] flex-1 flex-grow bg-accent/30">
<CardHeader>
<CardTitle className="flex items-center gap-3">
<div className="flex h-16 w-16 min-w-16 items-center justify-center rounded-lg bg-accent p-1">
@ -107,10 +91,7 @@ export function LatestScripts({ items }: { items: Category[] }) {
height={64}
width={64}
alt=""
onError={(e) =>
((e.currentTarget as HTMLImageElement).src =
`/${basePath}/logo.png`)
}
onError={(e) => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)}
className="h-11 w-11 object-contain"
/>
</div>
@ -126,9 +107,7 @@ export function LatestScripts({ items }: { items: Category[] }) {
</CardTitle>
</CardHeader>
<CardContent>
<CardDescription className="line-clamp-3 text-card-foreground">
{script.description}
</CardDescription>
<CardDescription className="line-clamp-3 text-card-foreground">{script.description}</CardDescription>
</CardContent>
<CardFooter className="">
<Button asChild variant="outline">
@ -151,9 +130,7 @@ export function LatestScripts({ items }: { items: Category[] }) {
export function MostViewedScripts({ items }: { items: Category[] }) {
const mostViewedScripts = items.reduce((acc: Script[], category) => {
const foundScripts = category.scripts.filter((script) =>
mostPopularScripts.includes(script.slug),
);
const foundScripts = category.scripts.filter((script) => mostPopularScripts.includes(script.slug));
return acc.concat(foundScripts);
}, []);
@ -166,10 +143,7 @@ export function MostViewedScripts({ items }: { items: Category[] }) {
)}
<div className="min-w flex w-full flex-row flex-wrap gap-4">
{mostViewedScripts.map((script) => (
<Card
key={script.slug}
className="min-w-[250px] flex-1 flex-grow bg-accent/30"
>
<Card key={script.slug} className="min-w-[250px] flex-1 flex-grow bg-accent/30">
<CardHeader>
<CardTitle className="flex items-center gap-3">
<div className="flex size-16 min-w-16 items-center justify-center rounded-lg bg-accent p-1">
@ -179,10 +153,7 @@ export function MostViewedScripts({ items }: { items: Category[] }) {
height={64}
width={64}
alt=""
onError={(e) =>
((e.currentTarget as HTMLImageElement).src =
`/${basePath}/logo.png`)
}
onError={(e) => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)}
className="h-11 w-11 object-contain"
/>
</div>

View File

@ -14,12 +14,12 @@ import { Suspense } from "react";
import { ResourceDisplay } from "./ResourceDisplay";
import { getDisplayValueFromType } from "./ScriptInfoBlocks";
import Alerts from "./ScriptItems/Alerts";
import Buttons from "./ScriptItems/Buttons";
import DefaultPassword from "./ScriptItems/DefaultPassword";
import Description from "./ScriptItems/Description";
import InstallCommand from "./ScriptItems/InstallCommand";
import Tooltips from "./ScriptItems/Tooltips";
import InterFaces from "./ScriptItems/InterFaces";
import Buttons from "./ScriptItems/Buttons";
import Tooltips from "./ScriptItems/Tooltips";
interface ScriptItemProps {
item: Script;
@ -142,7 +142,9 @@ export function ScriptItem({ item, setSelectedScript }: ScriptItemProps) {
<div className="mt-4 rounded-lg border shadow-sm">
<div className="flex gap-3 px-4 py-2 bg-accent/25">
<h2 className="text-lg font-semibold">How to {item.type === "misc" ? "use" : "install"}</h2>
<h2 className="text-lg font-semibold">
How to {item.type === "pve" ? "use" : item.type === "addon" ? "apply" : "install"}
</h2>
<Tooltips item={item} />
</div>
<Separator />

View File

@ -16,7 +16,17 @@ const generateInstallSourceUrl = (slug: string) => {
const generateSourceUrl = (slug: string, type: string) => {
const baseUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main`;
return type === "vm" ? `${baseUrl}/vm/${slug}.sh` : `${baseUrl}/misc/${slug}.sh`;
switch (type) {
case "vm":
return `${baseUrl}/vm/${slug}.sh`;
case "pve":
return `${baseUrl}/tools/pve/${slug}.sh`;
case "addon":
return `${baseUrl}/tools/addon/${slug}.sh`;
default:
return `${baseUrl}/ct/${slug}.sh`; // fallback for "ct"
}
};
const generateUpdateUrl = (slug: string) => {

View File

@ -5,85 +5,73 @@ import { Script } from "@/lib/types";
import { getDisplayValueFromType } from "../ScriptInfoBlocks";
const getInstallCommand = (scriptPath = "", isAlpine = false) => {
const url = `https://raw.githubusercontent.com/community-scripts/${basePath}/main/${scriptPath}`;
return isAlpine
? `bash -c "$(curl -fsSL ${url})"`
: `bash -c "$(curl -fsSL ${url})"`;
const url = `https://raw.githubusercontent.com/community-scripts/${basePath}/main/${scriptPath}`;
return isAlpine ? `bash -c "$(curl -fsSL ${url})"` : `bash -c "$(curl -fsSL ${url})"`;
};
export default function InstallCommand({ item }: { item: Script }) {
const alpineScript = item.install_methods.find(
(method) => method.type === "alpine",
);
const alpineScript = item.install_methods.find((method) => method.type === "alpine");
const defaultScript = item.install_methods.find(
(method) => method.type === "default",
);
const defaultScript = item.install_methods.find((method) => method.type === "default");
const renderInstructions = (isAlpine = false) => (
const renderInstructions = (isAlpine = false) => (
<>
<p className="text-sm mt-2">
{isAlpine ? (
<>
As an alternative option, you can use Alpine Linux and the {item.name} package to create a {item.name}{" "}
{getDisplayValueFromType(item.type)} container with faster creation time and minimal system resource usage.
You are also obliged to adhere to updates provided by the package maintainer.
</>
) : item.type === "pve" ? (
<>
To use the {item.name} script, run the command below **only** in the Proxmox VE Shell. This script is
intended for managing or enhancing the host system directly.
</>
) : item.type === "addon" ? (
<>
This script enhances an existing setup. You can use it inside a running LXC container or directly on the
Proxmox VE host to extend functionality with {item.name}.
</>
) : (
<>
To create a new Proxmox VE {item.name} {getDisplayValueFromType(item.type)}, run the command below in the
Proxmox VE Shell.
</>
)}
</p>
{isAlpine && (
<p className="mt-2 text-sm">
To create a new Proxmox VE Alpine-{item.name} {getDisplayValueFromType(item.type)}, run the command below in
the Proxmox VE Shell.
</p>
)}
</>
);
return (
<div className="p-4">
{alpineScript ? (
<Tabs defaultValue="default" className="mt-2 w-full max-w-4xl">
<TabsList>
<TabsTrigger value="default">Default</TabsTrigger>
<TabsTrigger value="alpine">Alpine Linux</TabsTrigger>
</TabsList>
<TabsContent value="default">
{renderInstructions()}
<CodeCopyButton>{getInstallCommand(defaultScript?.script)}</CodeCopyButton>
</TabsContent>
<TabsContent value="alpine">
{renderInstructions(true)}
<CodeCopyButton>{getInstallCommand(alpineScript.script, true)}</CodeCopyButton>
</TabsContent>
</Tabs>
) : defaultScript?.script ? (
<>
<p className="text-sm mt-2">
{isAlpine ? (
<>
As an alternative option, you can use Alpine Linux and the{" "}
{item.name} package to create a {item.name}{" "}
{getDisplayValueFromType(item.type)} container with faster creation
time and minimal system resource usage. You are also obliged to
adhere to updates provided by the package maintainer.
</>
) : item.type == "misc" ? (
<>
To use the {item.name} script, run the command below in the shell.
</>
) : (
<>
{" "}
To create a new Proxmox VE {item.name}{" "}
{getDisplayValueFromType(item.type)}, run the command below in the
Proxmox VE Shell.
</>
)}
</p>
{isAlpine && (
<p className="mt-2 text-sm">
To create a new Proxmox VE Alpine-{item.name}{" "}
{getDisplayValueFromType(item.type)}, run the command below in the
Proxmox VE Shell
</p>
)}
{renderInstructions()}
<CodeCopyButton>{getInstallCommand(defaultScript.script)}</CodeCopyButton>
</>
);
return (
<div className="p-4">
{alpineScript ? (
<Tabs defaultValue="default" className="mt-2 w-full max-w-4xl">
<TabsList>
<TabsTrigger value="default">Default</TabsTrigger>
<TabsTrigger value="alpine">Alpine Linux</TabsTrigger>
</TabsList>
<TabsContent value="default">
{renderInstructions()}
<CodeCopyButton>
{getInstallCommand(defaultScript?.script)}
</CodeCopyButton>
</TabsContent>
<TabsContent value="alpine">
{renderInstructions(true)}
<CodeCopyButton>
{getInstallCommand(alpineScript.script, true)}
</CodeCopyButton>
</TabsContent>
</Tabs>
) : defaultScript?.script ? (
<>
{renderInstructions()}
<CodeCopyButton>
{getInstallCommand(defaultScript.script)}
</CodeCopyButton>
</>
) : null}
</div>
);
}
) : null}
</div>
);
}

View File

@ -6,6 +6,7 @@ import {
CommandItem,
CommandList,
} from "@/components/ui/command";
import { basePath } from "@/config/siteConfig";
import { fetchCategories } from "@/lib/data";
import { Category } from "@/lib/types";
import { cn } from "@/lib/utils";
@ -15,18 +16,17 @@ import React from "react";
import { Badge } from "./ui/badge";
import { Button } from "./ui/button";
import { DialogTitle } from "./ui/dialog";
import { basePath } from "@/config/siteConfig";
export const formattedBadge = (type: string) => {
switch (type) {
case "vm":
return <Badge className="text-blue-500/75 border-blue-500/75">VM</Badge>;
case "ct":
return (
<Badge className="text-yellow-500/75 border-yellow-500/75">LXC</Badge>
);
case "misc":
return <Badge className="text-green-500/75 border-green-500/75">MISC</Badge>;
return <Badge className="text-yellow-500/75 border-yellow-500/75">LXC</Badge>;
case "pve":
return <Badge className="text-orange-500/75 border-orange-500/75">PVE</Badge>;
case "addon":
return <Badge className="text-green-500/75 border-green-500/75">ADDON</Badge>;
}
return null;
};
@ -84,14 +84,9 @@ export default function CommandMenu() {
<DialogTitle className="sr-only">Search scripts</DialogTitle>
<CommandInput placeholder="Search for a script..." />
<CommandList>
<CommandEmpty>
{isLoading ? "Loading..." : "No scripts found."}
</CommandEmpty>
<CommandEmpty>{isLoading ? "Loading..." : "No scripts found."}</CommandEmpty>
{links.map((category) => (
<CommandGroup
key={`category:${category.name}`}
heading={category.name}
>
<CommandGroup key={`category:${category.name}`} heading={category.name}>
{category.scripts.map((script) => (
<CommandItem
key={`script:${script.slug}`}
@ -104,10 +99,7 @@ export default function CommandMenu() {
<div className="flex gap-2" onClick={() => setOpen(false)}>
<Image
src={script.logo || `/${basePath}/logo.png`}
onError={(e) =>
((e.currentTarget as HTMLImageElement).src =
`/${basePath}/logo.png`)
}
onError={(e) => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)}
unoptimized
width={16}
height={16}

View File

@ -5,7 +5,7 @@ export type Script = {
slug: string;
categories: number[];
date_created: string;
type: "vm" | "ct" | "misc";
type: "vm" | "ct" | "pve" | "addon";
updateable: boolean;
privileged: boolean;
interface_port: number | null;
@ -61,4 +61,4 @@ export interface AppVersion {
name: string;
version: string;
date: Date;
}
}