This commit is contained in:
CanbiZ 2025-04-09 14:27:09 +02:00
parent b67b565d23
commit 0406049c89
18 changed files with 567 additions and 653 deletions

View File

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

View File

@ -24,8 +24,8 @@ export const ScriptSchema = z.object({
slug: z.string().min(1, "Slug is required"), slug: z.string().min(1, "Slug is required"),
categories: z.array(z.number()), 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"), 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"], { type: z.enum(["vm", "ct", "pve", "addon", "turnkey"], {
errorMap: () => ({ message: "Type must be either 'vm', 'ct', 'misc' or 'turnkey'" }) errorMap: () => ({ message: "Type must be either 'vm', 'ct', 'pve', 'addon' or 'turnkey'" })
}), }),
updateable: z.boolean(), updateable: z.boolean(),
privileged: z.boolean(), privileged: z.boolean(),

View File

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

View File

@ -1,4 +1,5 @@
"use client"; "use client";
import FAQ from "@/components/FAQ";
import AnimatedGradientText from "@/components/ui/animated-gradient-text"; import AnimatedGradientText from "@/components/ui/animated-gradient-text";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { CardFooter } from "@/components/ui/card"; import { CardFooter } from "@/components/ui/card";
@ -34,99 +35,109 @@ export default function Page() {
}, [theme]); }, [theme]);
return ( return (
<div className="w-full mt-16"> <>
<Particles <div className="w-full mt-16">
className="absolute inset-0 -z-40" <Particles className="absolute inset-0 -z-40" quantity={100} ease={80} color={color} refresh />
quantity={100} <div className="container mx-auto">
ease={80} <div className="flex h-[80vh] flex-col items-center justify-center gap-4 py-20 lg:py-40">
color={color} <Dialog>
refresh <DialogTrigger>
/> <div>
<div className="container mx-auto"> <AnimatedGradientText>
<div className="flex h-[80vh] flex-col items-center justify-center gap-4 py-20 lg:py-40"> <div
<Dialog> className={cn(
<DialogTrigger> `absolute inset-0 block size-full animate-gradient bg-gradient-to-r from-[#ffaa40]/50 via-[#9c40ff]/50 to-[#ffaa40]/50 bg-[length:var(--bg-size)_100%] [border-radius:inherit] [mask:linear-gradient(#fff_0_0)_content-box,linear-gradient(#fff_0_0)]`,
<div> `p-px ![mask-composite:subtract]`,
<AnimatedGradientText> )}
<div />
className={cn( <Separator className="mx-2 h-4" orientation="vertical" />
`absolute inset-0 block size-full animate-gradient bg-gradient-to-r from-[#ffaa40]/50 via-[#9c40ff]/50 to-[#ffaa40]/50 bg-[length:var(--bg-size)_100%] [border-radius:inherit] [mask:linear-gradient(#fff_0_0)_content-box,linear-gradient(#fff_0_0)]`, <span
`p-px ![mask-composite:subtract]`, className={cn(
)} `animate-gradient bg-gradient-to-r from-[#ffaa40] via-[#9c40ff] to-[#ffaa40] bg-[length:var(--bg-size)_100%] bg-clip-text text-transparent`,
/> `inline`,
<Separator className="mx-2 h-4" orientation="vertical" /> )}
<span >
className={cn( Scripts by tteck
`animate-gradient bg-gradient-to-r from-[#ffaa40] via-[#9c40ff] to-[#ffaa40] bg-[length:var(--bg-size)_100%] bg-clip-text text-transparent`, </span>
`inline`, </AnimatedGradientText>
)} </div>
> </DialogTrigger>
Develop Instance <DialogContent>
</span> <DialogHeader>
</AnimatedGradientText> <DialogTitle>Thank You!</DialogTitle>
</div> <DialogDescription>
</DialogTrigger> A big thank you to tteck and the many contributors who have made this project possible. Your hard
<DialogContent> work is truly appreciated by the entire Proxmox community!
<DialogHeader> </DialogDescription>
<DialogTitle>Thank You!</DialogTitle> </DialogHeader>
<DialogDescription> <CardFooter className="flex flex-col gap-2">
A big thank you to tteck and the many contributors who have <Button className="w-full" variant="outline" asChild>
made this project possible. Your hard work is truly <a
appreciated by the entire Proxmox community! href="https://github.com/tteck"
</DialogDescription> target="_blank"
</DialogHeader> rel="noopener noreferrer"
<CardFooter className="flex flex-col gap-2"> className="flex items-center justify-center"
<Button className="w-full" variant="outline" asChild> >
<a <FaGithub className="mr-2 h-4 w-4" /> Tteck&apos;s GitHub
href="https://github.com/tteck" </a>
target="_blank" </Button>
rel="noopener noreferrer" <Button className="w-full" asChild>
className="flex items-center justify-center" <a
> href={`https://github.com/community-scripts/${basePath}`}
<FaGithub className="mr-2 h-4 w-4" /> Tteck&apos;s GitHub target="_blank"
</a> rel="noopener noreferrer"
</Button> className="flex items-center justify-center"
<Button className="w-full" asChild> >
<a <ExternalLink className="mr-2 h-4 w-4" /> Proxmox Helper Scripts
href={`https://github.com/community-scripts/${basePath}`} </a>
target="_blank" </Button>
rel="noopener noreferrer" </CardFooter>
className="flex items-center justify-center" </DialogContent>
> </Dialog>
<ExternalLink className="mr-2 h-4 w-4" /> Proxmox Helper
Scripts
</a>
</Button>
</CardFooter>
</DialogContent>
</Dialog>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<h1 className="max-w-2xl text-center text-3xl font-semibold tracking-tighter md:text-7xl"> <h1 className="max-w-2xl text-center text-3xl font-semibold tracking-tighter md:text-7xl">
Beta Scripts Make managing your Homelab a breeze
</h1> </h1>
<div className="max-w-2xl gap-2 flex flex-col text-center sm:text-lg text-sm leading-relaxed tracking-tight text-muted-foreground md:text-xl"> <div className="max-w-2xl gap-2 flex flex-col text-center sm:text-lg text-sm leading-relaxed tracking-tight text-muted-foreground md:text-xl">
<p> <p>
On this Website you can find a collection of scripts that are under development and only for testing. We are a community-driven initiative that simplifies the setup of Proxmox Virtual Environment (VE).
We do not provide any support for these scripts when run in production, but you can help us by testing them and providing feedback. </p>
</p> <p>
</div> With 300+ scripts to help you manage your <b>Proxmox VE environment</b>. Whether you&#39;re a seasoned
</div> user or a newcomer, we&#39;ve got you covered.
<div className="flex flex-row gap-3"> </p>
<Link href="/scripts"> </div>
<Button </div>
size="lg" <div className="flex flex-row gap-3">
variant="expandIcon" <Link href="/scripts">
Icon={CustomArrowRightIcon} <Button
iconPlacement="right" size="lg"
className="hover:" variant="expandIcon"
> Icon={CustomArrowRightIcon}
View Scripts iconPlacement="right"
</Button> className="hover:"
</Link> >
</div> View Scripts
</div> </Button>
</div> </Link>
</div> </div>
); </div>
{/* FAQ Section */}
<div className="py-20" id="faq">
<div className="max-w-4xl mx-auto px-4">
<div className="text-center mb-12">
<h2 className="text-3xl font-bold tracking-tighter md:text-5xl mb-4">Frequently Asked Questions</h2>
<p className="text-muted-foreground text-lg">
Find answers to common questions about our Proxmox VE scripts
</p>
</div>
<FAQ />
</div>
</div>
</div>
</div>
</>
);
} }

View File

@ -0,0 +1,42 @@
import { CPUIcon, HDDIcon, RAMIcon } from "@/components/icons/resource-icons";
import { getDisplayValueFromRAM } from "@/lib/utils/resource-utils";
interface ResourceDisplayProps {
title: string;
cpu: number | null;
ram: number | null;
hdd: number | null;
}
interface IconTextProps {
icon: React.ReactNode;
label: string;
}
function IconText({ icon, label }: IconTextProps) {
return (
<span className="inline-flex items-center gap-1.5 rounded-md bg-accent/20 px-2 py-1 text-sm">
{icon}
<span className="text-foreground/90">{label}</span>
</span>
);
}
export function ResourceDisplay({ title, cpu, ram, hdd }: ResourceDisplayProps) {
const hasCPU = typeof cpu === "number" && cpu > 0;
const hasRAM = typeof ram === "number" && ram > 0;
const hasHDD = typeof hdd === "number" && hdd > 0;
if (!hasCPU && !hasRAM && !hasHDD) return null;
return (
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm font-medium text-muted-foreground">{title}</span>
<div className="flex flex-wrap gap-2">
{hasCPU && <IconText icon={<CPUIcon />} label={`${cpu} vCPU`} />}
{hasRAM && <IconText icon={<RAMIcon />} label={getDisplayValueFromRAM(ram!)} />}
{hasHDD && <IconText icon={<HDDIcon />} label={`${hdd} GB`} />}
</div>
</div>
);
}

View File

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

View File

@ -1,168 +1,162 @@
"use client";
import { Separator } from "@/components/ui/separator";
import { extractDate } from "@/lib/time"; import { extractDate } from "@/lib/time";
import { Script, AppVersion } from "@/lib/types"; import { AppVersion, Script } from "@/lib/types";
import { fetchVersions } from "@/lib/data";
import { X } from "lucide-react"; import { X } from "lucide-react";
import Image from "next/image"; import Image from "next/image";
import { Separator } from "@/components/ui/separator";
import { basePath } from "@/config/siteConfig";
import { useVersions } from "@/hooks/useVersions";
import { cleanSlug } from "@/lib/utils/resource-utils";
import { Suspense } from "react";
import { ResourceDisplay } from "./ResourceDisplay";
import { getDisplayValueFromType } from "./ScriptInfoBlocks"; import { getDisplayValueFromType } from "./ScriptInfoBlocks";
import Alerts from "./ScriptItems/Alerts"; import Alerts from "./ScriptItems/Alerts";
import Buttons from "./ScriptItems/Buttons"; import Buttons from "./ScriptItems/Buttons";
import DefaultPassword from "./ScriptItems/DefaultPassword"; import DefaultPassword from "./ScriptItems/DefaultPassword";
import DefaultSettings from "./ScriptItems/DefaultSettings";
import Description from "./ScriptItems/Description"; import Description from "./ScriptItems/Description";
import InstallCommand from "./ScriptItems/InstallCommand"; import InstallCommand from "./ScriptItems/InstallCommand";
import InterFaces from "./ScriptItems/InterFaces"; import InterFaces from "./ScriptItems/InterFaces";
import Tooltips from "./ScriptItems/Tooltips"; import Tooltips from "./ScriptItems/Tooltips";
import { basePath } from "@/config/siteConfig";
import { useEffect, useState } from "react";
interface ScriptItemProps { interface ScriptItemProps {
item: Script; item: Script;
setSelectedScript: (script: string | null) => void; setSelectedScript: (script: string | null) => void;
} }
function ScriptItem({ function ScriptHeader({ item }: { item: Script }) {
item, const defaultInstallMethod = item.install_methods?.[0];
setSelectedScript, const os = defaultInstallMethod?.resources?.os || "Proxmox Node";
}: ScriptItemProps) { const version = defaultInstallMethod?.resources?.version || "";
return (
const closeScript = () => { <div className="flex flex-col lg:flex-row gap-6 w-full">
window.history.pushState({}, document.title, window.location.pathname); <div className="flex flex-col md:flex-row gap-6 flex-grow">
setSelectedScript(null); <div className="flex-shrink-0">
}; <Image
className="h-32 w-32 rounded-xl bg-gradient-to-br from-accent/40 to-accent/60 object-contain p-3 shadow-lg transition-transform hover:scale-105"
const [versions, setVersions] = useState<AppVersion[]>([]); src={item.logo || `/${basePath}/logo.png`}
width={400}
onError={(e) => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)}
useEffect(() => { height={400}
fetchVersions() alt={item.name}
.then((fetchedVersions) => { unoptimized
console.log("Fetched Versions: ", fetchedVersions); />
if (Array.isArray(fetchedVersions)) {
setVersions(fetchedVersions);
} else if (fetchedVersions && typeof fetchedVersions === "object") {
setVersions([fetchedVersions]);
} else {
setVersions([]);
}
})
.catch((error) => console.error("Error fetching versions:", error));
}, []);
const defaultInstallMethod = item.install_methods?.[0];
const os = defaultInstallMethod?.resources?.os || "Proxmox Node";
const version = defaultInstallMethod?.resources?.version || "";
const [linksVisible, setLinksVisible] = useState<boolean>(false);
return (
<div className="mr-7 mt-0 flex w-full min-w-fit">
<div className="flex w-full min-w-fit">
<div className="flex w-full flex-col">
<div className="flex h-[36px] min-w-max items-center justify-between">
<h2 className="text-lg font-semibold">Selected Script</h2>
<X onClick={closeScript} className="cursor-pointer" />
</div>
<div className="rounded-lg border bg-accent/20 p-4">
<div className="flex justify-between">
<div className="flex">
<Image
className="h-32 w-32 rounded-lg bg-accent/60 object-contain p-3 shadow-md"
src={item.logo || `/${basePath}/logo.png`}
width={400}
onError={(e) =>
((e.currentTarget as HTMLImageElement).src =
`/${basePath}/logo.png`)
}
height={400}
alt={item.name}
unoptimized
/>
<div className="ml-4 flex flex-col justify-between">
<div className="flex h-full w-full flex-col mb-4">
<div>
<h1 className="text-lg font-semibold">
{item.name} {getDisplayValueFromType(item.type)}
</h1>
<p className="w-full text-sm text-muted-foreground">
Date added: {extractDate(item.date_created)}
</p>
<p className="text-sm text-muted-foreground">
Default OS: {os} {version}
</p>
</div>
<div>{versions.length === 0 ? (<p>Loading versions...</p>) :
(<>
<p className="text-l text-foreground">Version:</p>
<p className="text-l text-muted-foreground">{versions.find((v) =>
v.name === item.slug.replace(/[^a-z0-9]/g, '') ||
v.name.includes(item.slug.replace(/[^a-z0-9]/g, '')) ||
v.name.replace(/[^a-z0-9]/g, '') === item.slug.replace(/[^a-z0-9]/g, '')
)?.version || "No Version information found"
}</p>
<p className="text-l text-foreground">Latest changes:</p>
<p className="text-l text-muted-foreground">
{(() => {
const matchedVersion = versions.find((v) =>
v.name === item.slug.replace(/[^a-z0-9]/g, '') ||
v.name.includes(item.slug.replace(/[^a-z0-9]/g, '')) ||
v.name.replace(/[^a-z0-9]/g, '') === item.slug.replace(/[^a-z0-9]/g, '')
);
return matchedVersion?.date ?
extractDate(matchedVersion.date as unknown as string) :
"No date information found"
})()}
</p>
</>)
}
</div>
</div>
</div>
</div>
<div className="flex flex-col items-end gap-4 ml-auto">
<DefaultSettings item={item} />
<InterFaces item={item} />
<div>
<>
<button
onClick={() => setLinksVisible(!linksVisible)}
className="flex items-right justify-right gap-1 mb-2 rounded-md border border-accent bg-accent/20 px-2 py-1 text-l hover:bg-accent w-30"
>
Show Links {linksVisible ? '▲' : '▼'}
</button>
{linksVisible && <Buttons item={item} />}
</>
</div>
</div>
</div>
<div>
<div className="mt-4">
<Description item={item} />
<Alerts item={item} />
</div>
<div className="mt-4 rounded-lg border bg-accent/50">
<div className="flex gap-3 px-4 py-2">
<h2 className="text-lg font-semibold">
How to {item.type == "misc" ? "use" : "install"}
</h2>
<Tooltips item={item} />
</div>
<Separator className="w-full"></Separator>
<InstallCommand item={item} />
</div>
</div>
<DefaultPassword item={item} />
</div>
</div>
</div>
</div> </div>
); <div className="flex flex-col justify-between flex-grow space-y-4">
<div className="space-y-2">
<div className="flex items-start justify-between">
<div>
<h1 className="text-2xl font-semibold tracking-tight flex items-center gap-2">
{item.name}
<VersionInfo item={item} />
<span className="inline-flex items-center rounded-md bg-accent/30 px-2 py-1 text-sm">
{getDisplayValueFromType(item.type)}
</span>
</h1>
<div className="mt-1 flex items-center gap-3 text-sm text-muted-foreground">
<span>Added {extractDate(item.date_created)}</span>
<span></span>
<span className=" capitalize">
{os} {version}
</span>
</div>
</div>
{/* <VersionInfo item={item} /> */}
</div>
<div className="flex flex-col gap-2 text-sm text-muted-foreground">
{defaultInstallMethod?.resources && (
<ResourceDisplay
title="Default"
cpu={defaultInstallMethod.resources.cpu}
ram={defaultInstallMethod.resources.ram}
hdd={defaultInstallMethod.resources.hdd}
/>
)}
{item.install_methods.find((method) => method.type === "alpine")?.resources && (
<ResourceDisplay
title="Alpine"
{...item.install_methods.find((method) => method.type === "alpine")!.resources!}
/>
)}
</div>
</div>
</div>
</div>
<div className="flex flex-col gap-4 justify-between">
<InterFaces item={item} />
<div className="flex justify-end">
<Buttons item={item} />
</div>
</div>
</div>
);
} }
export default ScriptItem; function VersionInfo({ item }: { item: Script }) {
const { data: versions = [], isLoading } = useVersions();
if (isLoading || versions.length === 0) {
return <p className="text-sm text-muted-foreground">Loading versions...</p>;
}
const matchedVersion = versions.find((v: AppVersion) => {
const cleanName = v.name.replace(/[^a-z0-9]/gi, "").toLowerCase();
return cleanName === cleanSlug(item.slug) || cleanName.includes(cleanSlug(item.slug));
});
if (!matchedVersion) return null;
return <span className="font-medium text-sm">{matchedVersion.version}</span>;
}
export function ScriptItem({ item, setSelectedScript }: ScriptItemProps) {
const closeScript = () => {
window.history.pushState({}, document.title, window.location.pathname);
setSelectedScript(null);
};
return (
<div className="w-full max-w-5xl mx-auto">
<div className="flex w-full flex-col">
<div className="mb-3 flex items-center justify-between">
<h2 className="text-2xl font-semibold tracking-tight text-foreground/90">Selected Script</h2>
<button
onClick={closeScript}
className="rounded-full p-2 text-muted-foreground hover:bg-card/50 transition-colors"
>
<X className="h-5 w-5" />
</button>
</div>
<div className="rounded-xl border border-border/40 bg-gradient-to-b from-card/30 to-background/50 backdrop-blur-sm shadow-sm">
<div className="p-6 space-y-6">
<Suspense fallback={<div className="animate-pulse h-32 bg-accent/20 rounded-xl" />}>
<ScriptHeader item={item} />
</Suspense>
<Description item={item} />
<Alerts item={item} />
<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 === "pve" ? "use" : item.type === "addon" ? "apply" : "install"}
</h2>
<Tooltips item={item} />
</div>
<Separator />
<div className="">
<InstallCommand item={item} />
</div>
</div>
<DefaultPassword item={item} />
</div>
</div>
</div>
</div>
);
}

View File

@ -14,7 +14,7 @@ export default function Alerts({ item }: { item: Script }) {
<> <>
{item?.notes?.length > 0 && {item?.notes?.length > 0 &&
item.notes.map((note: NoteProps, index: number) => ( item.notes.map((note: NoteProps, index: number) => (
<div key={index} className="mt-4 flex flex-col gap-2"> <div key={index} className="mt-4 flex flex-col shadow-sm gap-2">
<p <p
className={cn( className={cn(
"inline-flex items-center gap-2 rounded-lg border p-2 pl-4 text-sm", "inline-flex items-center gap-2 rounded-lg border p-2 pl-4 text-sm",

View File

@ -1,82 +1,99 @@
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { basePath } from "@/config/siteConfig"; import { basePath } from "@/config/siteConfig";
import { Script } from "@/lib/types"; import { Script } from "@/lib/types";
import { BookOpenText, Code, Globe, RefreshCcw } from "lucide-react"; import { BookOpenText, Code, Globe, LinkIcon, RefreshCcw } from "lucide-react";
import Link from "next/link";
const generateInstallSourceUrl = (slug: string) => { const generateInstallSourceUrl = (slug: string) => {
const baseUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main`; const baseUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main`;
return `${baseUrl}/install/${slug}-install.sh`; return `${baseUrl}/install/${slug}-install.sh`;
}; };
const generateSourceUrl = (slug: string, type: string) => { const generateSourceUrl = (slug: string, type: string) => {
const baseUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main`; const baseUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main`;
return type === "vm" ? `${baseUrl}/vm/${slug}.sh` : `${baseUrl}/misc/${slug}.sh`;
return `${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) => { const generateUpdateUrl = (slug: string) => {
const baseUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main`; const baseUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main`;
return `${baseUrl}/ct/${slug}.sh`; return `${baseUrl}/ct/${slug}.sh`;
}; };
interface ButtonLinkProps { interface LinkItem {
href: string; href: string;
icon: React.ReactNode; icon: React.ReactNode;
text: string; text: string;
} }
const ButtonLink = ({ href, icon, text }: ButtonLinkProps) => (
<Button variant="secondary" asChild>
<Link target="_blank" href={href}>
<span className="flex items-center gap-2">
{icon}
{text}
</span>
</Link>
</Button>
);
export default function Buttons({ item }: { item: Script }) { export default function Buttons({ item }: { item: Script }) {
const isCtOrDefault = ["ct"].includes(item.type); const isCtOrDefault = ["ct"].includes(item.type);
const installSourceUrl = isCtOrDefault ? generateInstallSourceUrl(item.slug) : null; const installSourceUrl = isCtOrDefault ? generateInstallSourceUrl(item.slug) : null;
const updateSourceUrl = isCtOrDefault ? generateUpdateUrl(item.slug) : null; const updateSourceUrl = isCtOrDefault ? generateUpdateUrl(item.slug) : null;
const sourceUrl = !isCtOrDefault ? generateSourceUrl(item.slug, item.type) : null; const sourceUrl = !isCtOrDefault ? generateSourceUrl(item.slug, item.type) : null;
const buttons = [ const links = [
item.website && { item.website && {
href: item.website, href: item.website,
icon: <Globe className="h-4 w-4" />, icon: <Globe className="h-4 w-4" />,
text: "Website", text: "Website",
}, },
item.documentation && { item.documentation && {
href: item.documentation, href: item.documentation,
icon: <BookOpenText className="h-4 w-4" />, icon: <BookOpenText className="h-4 w-4" />,
text: "Documentation", text: "Documentation",
}, },
installSourceUrl && { installSourceUrl && {
href: installSourceUrl, href: installSourceUrl,
icon: <Code className="h-4 w-4" />, icon: <Code className="h-4 w-4" />,
text: "Install-Source", text: "Install Source",
}, },
updateSourceUrl && { updateSourceUrl && {
href: updateSourceUrl, href: updateSourceUrl,
icon: <RefreshCcw className="h-4 w-4" />, icon: <RefreshCcw className="h-4 w-4" />,
text: "Update-Source", text: "Update Source",
}, },
sourceUrl && { sourceUrl && {
href: sourceUrl, href: sourceUrl,
icon: <Code className="h-4 w-4" />, icon: <Code className="h-4 w-4" />,
text: "Source Code", text: "Source Code",
}, },
].filter(Boolean) as ButtonLinkProps[]; ].filter(Boolean) as LinkItem[];
return ( if (links.length === 0) return null;
<div className="flex flex-wrap justify-end gap-2"> return (
{buttons.map((props, index) => ( <DropdownMenu>
<ButtonLink key={index} {...props} /> <DropdownMenuTrigger asChild>
))} <Button variant="outline" className="flex items-center gap-2">
</div> <LinkIcon className="size-4" />
); Links
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{links.map((link, index) => (
<DropdownMenuItem key={index} asChild>
<a href={link.href} target="_blank" rel="noopener noreferrer" className="flex items-center gap-2">
<span className="text-muted-foreground size-4">{link.icon}</span>
<span>{link.text}</span>
</a>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
} }

View File

@ -14,24 +14,19 @@ export default function DefaultPassword({ item }: { item: Script }) {
}; };
return ( return (
<div className="mt-4 rounded-lg border bg-accent/50"> <div className="mt-4 rounded-lg border shadow-sm">
<div className="flex gap-3 px-4 py-2"> <div className="flex gap-3 px-4 py-2 bg-accent/25">
<h2 className="text-lg font-semibold">Default Login Credentials</h2> <h2 className="text-lg font-semibold">Default Login Credentials</h2>
</div> </div>
<Separator className="w-full" /> <Separator className="w-full" />
<div className="flex flex-col gap-2 p-4"> <div className="flex flex-col gap-2 p-4">
<p className="mb-2 text-sm"> <p className="mb-2 text-sm">
You can use the following credentials to login to the {item.name}{" "} You can use the following credentials to login to the {item.name} {item.type}.
{item.type}.
</p> </p>
{["username", "password"].map((type) => ( {["username", "password"].map((type) => (
<div key={type} className="text-sm"> <div key={type} className="text-sm">
{type.charAt(0).toUpperCase() + type.slice(1)}:{" "} {type.charAt(0).toUpperCase() + type.slice(1)}:{" "}
<Button <Button variant="secondary" size="null" onClick={() => copyCredential(type as "username" | "password")}>
variant="secondary"
size="null"
onClick={() => copyCredential(type as "username" | "password")}
>
{item.default_credentials[type as "username" | "password"]} {item.default_credentials[type as "username" | "password"]}
</Button> </Button>
</div> </div>

View File

@ -1,51 +1,29 @@
import { Script } from "@/lib/types"; import { Script } from "@/lib/types";
export default function DefaultSettings({ item }: { item: Script }) { export default function DefaultSettings({ item }: { item: Script }) {
const getDisplayValueFromRAM = (ram: number) => const getDisplayValueFromRAM = (ram: number) => (ram >= 1024 ? `${Math.floor(ram / 1024)}GB` : `${ram}MB`);
ram >= 1024 ? `${Math.floor(ram / 1024)}GB` : `${ram}MB`;
const ResourceDisplay = ({ const ResourceDisplay = ({ settings, title }: { settings: (typeof item.install_methods)[0]; title: string }) => {
settings,
title,
}: {
settings: (typeof item.install_methods)[0];
title: string;
}) => {
const { cpu, ram, hdd } = settings.resources; const { cpu, ram, hdd } = settings.resources;
return ( return (
<div> <div>
<h2 className="text-md font-semibold">{title}</h2> <h2 className="text-md font-semibold">{title}</h2>
<p className="text-sm text-muted-foreground">CPU: {cpu}vCPU</p> <p className="text-sm text-muted-foreground">CPU: {cpu}vCPU</p>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">RAM: {getDisplayValueFromRAM(ram ?? 0)}</p>
RAM: {getDisplayValueFromRAM(ram ?? 0)}
</p>
<p className="text-sm text-muted-foreground">HDD: {hdd}GB</p> <p className="text-sm text-muted-foreground">HDD: {hdd}GB</p>
</div> </div>
); );
}; };
const defaultSettings = item.install_methods.find( const defaultSettings = item.install_methods.find((method) => method.type === "default");
(method) => method.type === "default", const defaultAlpineSettings = item.install_methods.find((method) => method.type === "alpine");
);
const defaultAlpineSettings = item.install_methods.find(
(method) => method.type === "alpine",
);
const hasDefaultSettings = const hasDefaultSettings = defaultSettings?.resources && Object.values(defaultSettings.resources).some(Boolean);
defaultSettings?.resources &&
Object.values(defaultSettings.resources).some(Boolean);
return ( return (
<> <div className="space-y-4 flex-col flex">
{hasDefaultSettings && ( {hasDefaultSettings && <ResourceDisplay settings={defaultSettings} title="Default settings" />}
<ResourceDisplay settings={defaultSettings} title="Default settings" /> {defaultAlpineSettings && <ResourceDisplay settings={defaultAlpineSettings} title="Default Alpine settings" />}
)} </div>
{defaultAlpineSettings && (
<ResourceDisplay
settings={defaultAlpineSettings}
title="Default Alpine settings"
/>
)}
</>
); );
} }

View File

@ -1,21 +1,11 @@
import TextCopyBlock from "@/components/TextCopyBlock"; import TextCopyBlock from "@/components/TextCopyBlock";
import { Script } from "@/lib/types"; import { Script } from "@/lib/types";
import { AlertColors } from "@/config/siteConfig";
import { AlertCircle, NotepadText } from "lucide-react";
import { cn } from "@/lib/utils";
export default function Description({ item }: { item: Script }) { export default function Description({ item }: { item: Script }) {
return ( return (
<div className="p-2"> <div className="p-2">
<h2 className="mb-2 max-w-prose text-lg font-semibold">Description</h2> <h2 className="mb-2 max-w-prose text-lg font-semibold">Description</h2>
<p className={cn( <p className="text-sm text-muted-foreground">
"inline-flex items-center gap-2 rounded-lg border p-2 pl-4 text-lg pr-4",
AlertColors["warning"],
)} >
<AlertCircle className="h-4 min-h-4 w-4 min-w-4" />
<span>Only use for testing, not in production!</span>
</p>
<p className="text-sm text-muted-foreground pt-4">
{TextCopyBlock(item.description)} {TextCopyBlock(item.description)}
</p> </p>
</div> </div>

View File

@ -5,85 +5,73 @@ import { Script } from "@/lib/types";
import { getDisplayValueFromType } from "../ScriptInfoBlocks"; import { getDisplayValueFromType } from "../ScriptInfoBlocks";
const getInstallCommand = (scriptPath = "", isAlpine = false) => { const getInstallCommand = (scriptPath = "", isAlpine = false) => {
const url = `https://github.com/community-scripts/${basePath}/raw/main/${scriptPath}`; const url = `https://raw.githubusercontent.com/community-scripts/${basePath}/main/${scriptPath}`;
return isAlpine return isAlpine ? `bash -c "$(curl -fsSL ${url})"` : `bash -c "$(curl -fsSL ${url})"`;
? `bash -c "$(curl -fsSL ${url})"`
: `bash -c "$(curl -fsSL ${url})"`;
}; };
export default function InstallCommand({ item }: { item: Script }) { export default function InstallCommand({ item }: { item: Script }) {
const alpineScript = item.install_methods.find( const alpineScript = item.install_methods.find((method) => method.type === "alpine");
(method) => method.type === "alpine",
);
const defaultScript = item.install_methods.find( const defaultScript = item.install_methods.find((method) => method.type === "default");
(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"> {renderInstructions()}
{isAlpine ? ( <CodeCopyButton>{getInstallCommand(defaultScript.script)}</CodeCopyButton>
<>
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>
)}
</> </>
); ) : null}
</div>
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>
);
} }

View File

@ -4,39 +4,18 @@ import { Script } from "@/lib/types";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { ClipboardIcon } from "lucide-react"; import { ClipboardIcon } from "lucide-react";
const CopyButton = ({
label,
value,
}: {
label: string;
value: string | number;
}) => (
<span
className={cn(
buttonVariants({ size: "sm", variant: "secondary" }),
"flex items-center gap-2",
)}
>
{value}
<ClipboardIcon
onClick={() => handleCopy(label, String(value))}
className="size-4 cursor-pointer"
/>
</span>
);
export default function InterFaces({ item }: { item: Script }) { export default function InterFaces({ item }: { item: Script }) {
return ( return (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2 w-full">
{item.interface_port !== null ? ( {item.interface_port !== null ? (
<div className="flex items-center justify-end"> <div className="flex items-center justify-end">
<h2 className="mr-2 text-end text-l font-semibold"> <h2 className="mr-2 text-end text-lg font-semibold">Default Interface:</h2>
{"Default Interface:"} <span className={cn(buttonVariants({ size: "sm", variant: "outline" }), "flex items-center gap-2")}>
</h2>{" "} {item.interface_port}
<ClipboardIcon onClick={() => handleCopy("default interface", String(item.interface_port))} className="size-4 cursor-pointer" />
<CopyButton label="default interface" value={item.interface_port} /> </span>
</div>
) : null}
</div> </div>
); ) : null}
</div>
);
} }

View File

@ -0,0 +1,13 @@
import { AppVersion } from "@/lib/types";
interface VersionBadgeProps {
version: AppVersion;
}
export function VersionBadge({ version }: VersionBadgeProps) {
return (
<div className="flex items-center">
<span className="font-medium text-sm">{version.version}</span>
</div>
);
}

View File

@ -2,7 +2,7 @@
export const dynamic = "force-static"; export const dynamic = "force-static";
import ScriptItem from "@/app/scripts/_components/ScriptItem"; import { ScriptItem } from "@/app/scripts/_components/ScriptItem";
import { fetchCategories } from "@/lib/data"; import { fetchCategories } from "@/lib/data";
import { Category, Script } from "@/lib/types"; import { Category, Script } from "@/lib/types";
import { Loader2 } from "lucide-react"; import { Loader2 } from "lucide-react";

View File

@ -0,0 +1,7 @@
export function getDisplayValueFromRAM(ram: number): string {
return ram >= 1024 ? `${Math.floor(ram / 1024)}GB` : `${ram}MB`;
}
export function cleanSlug(slug: string): string {
return slug.replace(/[^a-z0-9]/gi, "").toLowerCase();
}