website
This commit is contained in:
parent
b67b565d23
commit
0406049c89
@ -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>
|
||||||
</>
|
</>
|
||||||
|
@ -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(),
|
||||||
|
@ -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>
|
||||||
|
@ -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'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'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're a seasoned
|
||||||
</div>
|
user or a newcomer, we'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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
42
frontend/src/app/scripts/_components/ResourceDisplay.tsx
Normal file
42
frontend/src/app/scripts/_components/ResourceDisplay.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -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",
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
13
frontend/src/app/scripts/_components/VersionBadge.tsx
Normal file
13
frontend/src/app/scripts/_components/VersionBadge.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -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";
|
||||||
|
7
frontend/src/lib/utils/resource-utils.ts
Normal file
7
frontend/src/lib/utils/resource-utils.ts
Normal 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();
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user