merge frontend website into scripts repo

This commit is contained in:
Bram Suurd 2024-11-04 23:55:08 +01:00
parent 103e2bea08
commit 56837d7dcd
72 changed files with 11679 additions and 0 deletions

View file

@ -0,0 +1,23 @@
import { pb } from "@/lib/pocketbase";
import { Category } from "@/lib/types";
import { NextResponse } from "next/server";
export const dynamic = "force-static";
export async function GET() {
try {
const response = await pb.collection("categories").getFullList<Category>({
expand: "items.alerts,items.alpine_script,items.default_login",
sort: "order",
});
return NextResponse.json(response);
} catch (error) {
console.error("Error fetching categories:", error);
return NextResponse.json(
{ error: "Failed to fetch categories" },
{ status: 500 },
);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -0,0 +1,88 @@
import Footer from "@/components/Footer";
import Navbar from "@/components/Navbar";
import { ThemeProvider } from "@/components/theme-provider";
import { Toaster } from "@/components/ui/sonner";
import "@/styles/globals.css";
import { Inter } from "next/font/google";
import React from "react";
import { NuqsAdapter } from "nuqs/adapters/next/app";
const inter = Inter({ subsets: ["latin"] });
export const metadata = {
title: "Proxmox VE Helper-Scripts",
generator: "Next.js",
applicationName: "Proxmox VE Helper-Scripts",
referrer: "origin-when-cross-origin",
keywords: [
"Proxmox VE",
"Helper-Scripts",
"tteck",
"helper",
"scripts",
"proxmox",
"VE",
],
authors: { name: "Bram Suurd" },
creator: "Bram Suurd",
publisher: "Bram Suurd",
description:
"A Front-end for the Proxmox VE Helper-Scripts (Community) Repository. Featuring over 200+ scripts to help you manage your Proxmox VE environment.",
favicon: "/app/favicon.ico",
formatDetection: {
email: false,
address: false,
telephone: false,
},
metadataBase: new URL("https://community-scripts.github.io/Proxmox/"),
openGraph: {
title: "Proxmox VE Helper-Scripts",
description:
"A Front-end for the Proxmox VE Helper-Scripts (Community) Repository. Featuring over 200+ scripts to help you manage your Proxmox VE environment.",
url: "/defaultimg.png",
images: [
{
url: "https://community-scripts.github.io/Proxmox/defaultimg.png",
},
],
locale: "en_US",
type: "website",
},
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<script
defer
src={`https://${process.env.NEXT_PUBLIC_ANALYTICS_URL}/script.js`}
data-website-id={process.env.NEXT_PUBLIC_ANALYTICS_TOKEN}
></script>
<link rel="manifest" href="manifest.webmanifest" />
<link rel="preconnect" href={process.env.NEXT_PUBLIC_POCKETBASE_URL} />
<link rel="preconnect" href="https://api.github.com" />
</head>
<body className={inter.className}>
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem>
<div className="flex w-full flex-col justify-center">
<Navbar />
<div className="flex min-h-screen flex-col justify-center">
<div className="flex w-full justify-center">
<div className="w-full max-w-7xl ">
<NuqsAdapter>{children}</NuqsAdapter>
<Toaster richColors />
</div>
</div>
<Footer />
</div>
</div>
</ThemeProvider>
</body>
</html>
);
}

View file

@ -0,0 +1,27 @@
import type { MetadataRoute } from "next";
export const generateStaticParams = () => {
return [];
};
export default function manifest(): MetadataRoute.Manifest {
return {
name: "Proxmox VE Helper-Scripts",
short_name: "Proxmox VE Helper-Scripts",
description:
"A Re-designed Front-end for the Proxmox VE Helper-Scripts Repository. Featuring over 150+ scripts to help you manage your Proxmox VE environment.",
theme_color: "#030712",
background_color: "#030712",
display: "standalone",
orientation: "portrait",
scope: "/Proxmox/",
start_url: "/Proxmox/",
icons: [
{
src: "logo.png",
sizes: "512x512",
type: "image/png",
},
],
};
}

View file

@ -0,0 +1,20 @@
"use client";
import { Button } from "@/components/ui/button";
export default function NotFoundPage() {
return (
<div className="flex h-screen w-full flex-col items-center justify-center gap-5 bg-background px-4 md:px-6">
<div className="space-y-2 text-center">
<h1 className="text-4xl font-bold tracking-tighter sm:text-5xl md:text-6xl">
404
</h1>
<p className="text-muted-foreground md:text-xl">
Oops, the page you are looking for could not be found.
</p>
</div>
<Button onClick={() => window.history.back()} variant="secondary">
Go Back
</Button>
</div>
);
}

123
frontend/src/app/page.tsx Normal file
View file

@ -0,0 +1,123 @@
"use client";
import AnimatedGradientText from "@/components/ui/animated-gradient-text";
import Particles from "@/components/ui/particles";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { cn } from "@/lib/utils";
import { ArrowRightIcon, ExternalLink } from "lucide-react";
import { useTheme } from "next-themes";
import Link from "next/link";
import { useEffect, useState } from "react";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { CardFooter } from "@/components/ui/card";
import { FaGithub } from "react-icons/fa";
function CustomArrowRightIcon() {
return <ArrowRightIcon className="h-4 w-4" width={1} />;
}
export default function Page() {
const { theme } = useTheme();
const [color, setColor] = useState("#000000");
useEffect(() => {
setColor(theme === "dark" ? "#ffffff" : "#000000");
}, [theme]);
return (
<div className="w-full">
<Particles
className="absolute inset-0 -z-40"
quantity={100}
ease={80}
color={color}
refresh
/>
<div className="container mx-auto">
<div className="flex h-[80vh] flex-col items-center justify-center gap-4 py-20 lg:py-40">
<Dialog>
<DialogTrigger>
<div>
<AnimatedGradientText>
<div
className={cn(
`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)]`,
`p-px ![mask-composite:subtract]`,
)}
/>
<Separator className="mx-2 h-4" orientation="vertical" />
<span
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`,
)}
>
Scripts by Tteck
</span>
</AnimatedGradientText>
</div>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Thank You!</DialogTitle>
<DialogDescription>
A big thank you to Tteck and the many contributors who have
made this project possible. Your hard work is truly
appreciated by the entire Proxmox community!
</DialogDescription>
</DialogHeader>
<CardFooter className="flex flex-col gap-2">
<Button className="w-full" variant="outline" asChild>
<a
href="https://github.com/tteck"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center"
>
<FaGithub className="mr-2 h-4 w-4" /> Tteck&apos;s GitHub
</a>
</Button>
<Button className="w-full" asChild>
<a
href="https://github.com/community-scripts/ProxmoxVE"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center"
>
<ExternalLink className="mr-2 h-4 w-4" /> Proxmox Helper
Scripts
</a>
</Button>
</CardFooter>
</DialogContent>
</Dialog>
<div className="flex flex-col gap-4">
<h1 className="max-w-2xl text-center text-5xl font-semibold tracking-tighter md:text-7xl">
Make managing your Homelab a breeze
</h1>
<p className="max-w-2xl text-center text-lg leading-relaxed tracking-tight text-muted-foreground md:text-xl">
200+ scripts to help you manage your <b>Proxmox VE environment</b>
. Whether you&apos;re a seasoned user or a newcomer, Proxmox VE
Helper Scripts has got you covered.
</p>
</div>
<div className="flex flex-row gap-3">
<Link href="/scripts">
<Button
size="lg"
variant="expandIcon"
Icon={CustomArrowRightIcon}
iconPlacement="right"
className="hover:"
>
View Scripts
</Button>
</Link>
</div>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,13 @@
import type { MetadataRoute } from "next";
export const dynamic = "force-static";
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: "*",
allow: "/",
},
sitemap: "https://community-scripts.github.io/Proxmox/sitemap.xml",
};
}

View file

@ -0,0 +1,147 @@
import { useCallback, useEffect, useRef } from "react";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Category } from "@/lib/types";
import { cn } from "@/lib/utils";
import { Star } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { useState } from "react";
import { Badge } from "../../../components/ui/badge";
export default function ScriptAccordion({
items,
selectedScript,
setSelectedScript,
}: {
items: Category[];
selectedScript: string | null;
setSelectedScript: (script: string | null) => void;
}) {
const [expandedItem, setExpandedItem] = useState<string | undefined>(
undefined,
);
const linkRefs = useRef<{ [key: string]: HTMLAnchorElement | null }>({});
const handleAccordionChange = (value: string | undefined) => {
setExpandedItem(value);
};
const handleSelected = useCallback(
(title: string) => {
setSelectedScript(title);
},
[setSelectedScript],
);
useEffect(() => {
if (selectedScript) {
const category = items.find((category) =>
category.expand.items.some((script) => script.title === selectedScript),
);
if (category) {
setExpandedItem(category.catagoryName);
handleSelected(selectedScript);
}
}
}, [selectedScript, items, handleSelected]);
return (
<Accordion
type="single"
value={expandedItem}
onValueChange={handleAccordionChange}
collapsible
>
{items.map((category) => (
<AccordionItem
key={category.id + ":category"}
value={category.catagoryName}
className={cn("sm:text-md flex flex-col border-none", {
"rounded-lg bg-accent/30": expandedItem === category.catagoryName,
})}
>
<AccordionTrigger
className={cn(
"duration-250 rounded-lg transition ease-in-out hover:-translate-y-1 hover:scale-105 hover:bg-accent",
{ "": expandedItem === category.catagoryName },
)}
>
<div className="mr-2 flex w-full items-center justify-between">
<span className="pl-2">{category.catagoryName} </span>
<span className="rounded-full bg-gray-200 px-2 py-1 text-xs text-muted-foreground hover:no-underline dark:bg-blue-800/20">
{category.expand.items.length}
</span>
</div>{" "}
</AccordionTrigger>
<AccordionContent
data-state={
expandedItem === category.catagoryName ? "open" : "closed"
}
className="pt-0"
>
{category.expand.items
.slice()
.sort((a, b) => a.title.localeCompare(b.title))
.map((script, index) => (
<div key={index}>
<Link
href={{
pathname: "/scripts",
query: { id: script.title },
}}
prefetch={false}
className={`flex cursor-pointer items-center justify-between gap-1 px-1 py-1 text-muted-foreground hover:rounded-lg hover:bg-accent/60 hover:dark:bg-accent/20 ${
selectedScript === script.title
? "rounded-lg bg-accent font-semibold dark:bg-accent/30 dark:text-white"
: ""
}`}
onClick={() => handleSelected(script.title)}
ref={(el) => {
linkRefs.current[script.title] = el;
}}
>
<Image
src={script.logo}
height={16}
width={16}
unoptimized
onError={(e) =>
((e.currentTarget as HTMLImageElement).src =
"/logo.png")
}
alt={script.title}
className="mr-1 w-4 h-4 rounded-full"
/>
<span className="flex items-center gap-2">
{script.title}
{script.isMostViewed && (
<Star className="h-3 w-3 text-yellow-500"></Star>
)}
</span>
<Badge
className={cn(
"ml-auto w-[37.69px] justify-center text-center",
{
"text-primary/75": script.item_type === "VM",
"text-yellow-500/75": script.item_type === "LXC",
"border-none": script.item_type === "",
hidden: !["VM", "LXC", ""].includes(script.item_type),
},
)}
>
{script.item_type}
</Badge>
</Link>
</div>
))}
</AccordionContent>
</AccordionItem>
))}
</Accordion>
);
}

View file

@ -0,0 +1,219 @@
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { extractDate } from "@/lib/time";
import { Category } from "@/lib/types";
import { CalendarPlus } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { useMemo, useState } from "react";
const ITEMS_PER_PAGE = 3;
export function LatestScripts({ items }: { items: Category[] }) {
const [page, setPage] = useState(1);
const latestScripts = useMemo(() => {
if (!items) return [];
const scripts = items.flatMap((category) => category.expand.items || []);
return scripts.sort(
(a, b) => new Date(b.created).getTime() - new Date(a.created).getTime(),
);
}, [items]);
const goToNextPage = () => {
setPage((prevPage) => prevPage + 1);
};
const goToPreviousPage = () => {
setPage((prevPage) => prevPage - 1);
};
const startIndex = (page - 1) * ITEMS_PER_PAGE;
const endIndex = page * ITEMS_PER_PAGE;
if (!items) {
return null;
}
return (
<div className="">
{latestScripts.length > 0 && (
<div className="flex w-full items-center justify-between">
<h2 className="text-lg font-semibold">Newest Scripts</h2>
<div className="flex items-center justify-end gap-1">
{page > 1 && (
<div
className="cursor-pointer select-none p-2 text-sm font-semibold"
onClick={goToPreviousPage}
>
Previous
</div>
)}
{endIndex < latestScripts.length && (
<div
onClick={goToNextPage}
className="cursor-pointer select-none p-2 text-sm font-semibold"
>
{page === 1 ? "More.." : "Next"}
</div>
)}
</div>
</div>
)}
<div className="min-w flex w-full flex-row flex-wrap gap-4">
{latestScripts.slice(startIndex, endIndex).map((item) => (
<Card
key={item.id}
className="min-w-[250px] flex-1 flex-grow bg-accent/30"
>
<CardHeader>
<CardTitle className="flex items-center gap-3">
<div className="flex h-16 w-16 items-center justify-center rounded-lg bg-accent p-1">
<Image
src={item.logo}
unoptimized
height={64}
width={64}
alt=""
className="h-11 w-11 object-contain"
/>
</div>
<div className="flex flex-col">
<p className="text-lg line-clamp-1">
{item.title} {item.item_type}
</p>
<p className="text-sm text-muted-foreground flex items-center gap-1">
<CalendarPlus className="h-4 w-4" />
{extractDate(item.created)}
</p>
</div>
</CardTitle>
</CardHeader>
<CardContent>
<CardDescription className="line-clamp-3 text-card-foreground">
{item.description}
</CardDescription>
</CardContent>
<CardFooter className="">
<Button asChild variant="outline">
<Link
href={{
pathname: "/scripts",
query: { id: item.title },
}}
>
View Script
</Link>
</Button>
</CardFooter>
</Card>
))}
</div>
</div>
);
}
export function MostViewedScripts({ items }: { items: Category[] }) {
const [page, setPage] = useState(1);
const mostViewedScripts = useMemo(() => {
if (!items) return [];
const scripts = items.flatMap((category) => category.expand.items || []);
const mostViewedScripts = scripts
.filter((script) => script.isMostViewed)
.map((script) => ({
...script,
}));
return mostViewedScripts;
}, [items]);
const goToNextPage = () => {
setPage((prevPage) => prevPage + 1);
};
const goToPreviousPage = () => {
setPage((prevPage) => prevPage - 1);
};
const startIndex = (page - 1) * ITEMS_PER_PAGE;
const endIndex = page * ITEMS_PER_PAGE;
return (
<div className="">
{mostViewedScripts.length > 0 && (
<>
<h2 className="text-lg font-semibold">Most Viewed Scripts</h2>
</>
)}
<div className="min-w flex w-full flex-row flex-wrap gap-4">
{mostViewedScripts.slice(startIndex, endIndex).map((item) => (
<Card
key={item.id}
className="min-w-[250px] flex-1 flex-grow bg-accent/30"
>
<CardHeader>
<CardTitle className="flex items-center gap-3">
<div className="flex max-h-16 min-h-16 min-w-16 max-w-16 items-center justify-center rounded-lg bg-accent p-1">
<Image
unoptimized
src={item.logo}
height={64}
width={64}
alt=""
className="h-11 w-11 object-contain"
/>
</div>
<div className="flex flex-col">
<p className="line-clamp-1 text-lg">
{item.title} {item.item_type}
</p>
<p className="flex items-center gap-1 text-sm text-muted-foreground">
<CalendarPlus className="h-4 w-4" />
{extractDate(item.created)}
</p>
</div>
</CardTitle>
</CardHeader>
<CardContent>
<CardDescription className="line-clamp-3 text-card-foreground break-words">
{item.description}
</CardDescription>
</CardContent>
<CardFooter className="">
<Button asChild variant="outline">
<Link
href={{
pathname: "/scripts",
query: { id: item.title },
}}
prefetch={false}
>
View Script
</Link>
</Button>
</CardFooter>
</Card>
))}
</div>
<div className="flex justify-end gap-1 p-2">
{page > 1 && (
<Button onClick={goToPreviousPage} variant="outline">
Previous
</Button>
)}
{endIndex < mostViewedScripts.length && (
<Button onClick={goToNextPage} variant="outline">
{page === 1 ? "More.." : "Next"}
</Button>
)}
</div>
</div>
);
}

View file

@ -0,0 +1,95 @@
"use client";
import { Separator } from "@/components/ui/separator";
import { extractDate } from "@/lib/time";
import { Script } from "@/lib/types";
import { X } from "lucide-react";
import Image from "next/image";
import Alerts from "./ScriptItems/Alerts";
import Buttons from "./ScriptItems/Buttons";
import DefaultPassword from "./ScriptItems/DefaultPassword";
import DefaultSettings from "./ScriptItems/DefaultSettings";
import Description from "./ScriptItems/Description";
import InstallCommand from "./ScriptItems/InstallCommand";
import InterFaces from "./ScriptItems/InterFaces";
import Tooltips from "./ScriptItems/Tooltips";
function ScriptItem({
item,
setSelectedScript,
}: {
item: Script;
setSelectedScript: (script: string | null) => void;
}) {
const closeScript = () => {
window.history.pushState({}, document.title, window.location.pathname);
setSelectedScript(null);
};
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}
width={400}
onError={(e) =>
((e.currentTarget as HTMLImageElement).src = "/logo.png")
}
height={400}
alt={item.title}
unoptimized
/>
<div className="ml-4 flex flex-col justify-between">
<div className="flex h-full w-full flex-col justify-between">
<div>
<h1 className="text-lg font-semibold">{item.title}</h1>
<p className="w-full text-sm text-muted-foreground">
Date added: {extractDate(item.created)}
</p>
</div>
<div className="flex gap-5">
<DefaultSettings item={item} />
</div>
</div>
</div>
</div>
<div className="hidden flex-col justify-between gap-2 sm:flex">
<InterFaces item={item} />
<Buttons item={item} />
</div>
</div>
<Separator className="mt-4" />
<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.item_type ? "install" : "use"}
</h2>
<Tooltips item={item} />
</div>
<Separator className="w-full"></Separator>
<InstallCommand item={item} />
</div>
</div>
<DefaultPassword item={item} />
</div>
</div>
</div>
</div>
);
}
export default ScriptItem;

View file

@ -0,0 +1,19 @@
import TextCopyBlock from "@/lib/TextCopyBlock";
import { Script } from "@/lib/types";
import { Info } from "lucide-react";
export default function Alerts({ item }: { item: Script }) {
return (
<>
{item.expand?.alerts?.length > 0 &&
item.expand.alerts.map((alert: any, index: number) => (
<div key={index} className="mt-4 flex flex-col gap-2">
<p className="inline-flex items-center gap-2 rounded-lg border border-red-500/25 bg-destructive/25 p-2 pl-4 text-sm">
<Info className="h-4 min-h-4 w-4 min-w-4" />
<span>{TextCopyBlock(alert.content)}</span>
</p>
</div>
))}
</>
);
}

View file

@ -0,0 +1,74 @@
import { Button } from "@/components/ui/button";
import { Script } from "@/lib/types";
import { BookOpenText, Code, ExternalLink, Globe } from "lucide-react";
import Link from "next/link";
import { useMemo } from "react";
export default function Buttons({ item }: { item: Script }) {
const pattern = useMemo(
() =>
/(https:\/\/github\.com\/community-scripts\/ProxmoxVE\/raw\/main\/(ct|misc|vm)\/([^\/]+)\.sh)/,
[],
);
const transformUrlToInstallScript = (url: string): string => {
if (url.includes("/pve/")) {
return url;
} else if (url.includes("/ct/")) {
return url.replace("/ct/", "/install/").replace(/\.sh$/, "-install.sh");
}
return url;
};
const sourceUrl = useMemo(() => {
if (item.installCommand) {
const match = item.installCommand.match(pattern);
return match ? transformUrlToInstallScript(match[0]) : null;
}
return null;
}, [item.installCommand, pattern]);
return (
<div className="flex flex-wrap justify-end gap-2">
{item.website && (
<Button variant="secondary" asChild>
<Link target="_blank" href={item.website}>
<span className="flex items-center gap-2">
<Globe className="h-4 w-4" /> Website
</span>
</Link>
</Button>
)}
{item.documentation && (
<Button variant="secondary" asChild>
<Link target="_blank" href={item.documentation}>
<span className="flex items-center gap-2">
<BookOpenText className="h-4 w-4" />
Documentation
</span>
</Link>
</Button>
)}
{item.post_install && (
<Button variant="secondary" asChild>
<Link target="_blank" href={item.post_install}>
<span className="flex items-center gap-2">
<ExternalLink className="h-4 w-4" />
Post Install
</span>
</Link>
</Button>
)}
{item.installCommand && sourceUrl && (
<Button variant="secondary" asChild>
<Link target="_blank" href={transformUrlToInstallScript(sourceUrl)}>
<span className="flex items-center gap-2">
<Code className="h-4 w-4" />
Source Code
</span>
</Link>
</Button>
)}
</div>
);
}

View file

@ -0,0 +1,51 @@
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import handleCopy from "@/components/handleCopy";
import { Script } from "@/lib/types";
export default function DefaultPassword({ item }: { item: Script }) {
const hasDefaultLogin = item?.expand?.default_login !== undefined;
return (
<div>
{hasDefaultLogin && (
<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">Default Login Credentials</h2>
</div>
<Separator className="w-full"></Separator>
<div className="flex flex-col gap-2 p-4">
<p className="mb-2 text-sm">
You can use the following credentials to login to the {""}
{item.title} {item.item_type}.
</p>
<div className="text-sm">
Username:{" "}
<Button
variant={"secondary"}
size={"null"}
onClick={() =>
handleCopy("username", item.expand.default_login.username)
}
>
{item.expand.default_login.username}
</Button>
</div>
<div className="text-sm">
Password:{" "}
<Button
variant={"secondary"}
size={"null"}
onClick={() =>
handleCopy("password", item.expand.default_login.password)
}
>
{item.expand.default_login.password}
</Button>
</div>
</div>
</div>
)}
</div>
);
}

View file

@ -0,0 +1,38 @@
import { Script } from "@/lib/types";
export default function DefaultSettings({ item }: { item: Script }) {
const hasAlpineScript = item?.expand?.alpine_script !== undefined;
return (
<>
{item.default_cpu && (
<div>
<h2 className="text-md font-semibold">Default settings</h2>
<p className="text-sm text-muted-foreground">
CPU: {item.default_cpu}
</p>
<p className="text-sm text-muted-foreground">
RAM: {item.default_ram}
</p>
<p className="text-sm text-muted-foreground">
HDD: {item.default_hdd}
</p>
</div>
)}
{hasAlpineScript && (
<div>
<h2 className="text-md font-semibold">Default Alpine settings</h2>
<p className="text-sm text-muted-foreground">
CPU: {item.expand.alpine_script.default_cpu}
</p>
<p className="text-sm text-muted-foreground">
RAM: {item.expand.alpine_script.default_ram}
</p>
<p className="text-sm text-muted-foreground">
HDD: {item.expand.alpine_script.default_hdd}
</p>
</div>
)}
</>
);
}

View file

@ -0,0 +1,13 @@
import TextCopyBlock from "@/lib/TextCopyBlock";
import { Script } from "@/lib/types";
export default function Description({ item }: { item: Script }) {
return (
<div className="p-2">
<h2 className="mb-2 max-w-prose text-lg font-semibold">Description</h2>
<p className="text-sm text-muted-foreground">
{TextCopyBlock(item.description)}
</p>
</div>
);
}

View file

@ -0,0 +1,68 @@
import CodeCopyButton from "@/components/ui/code-copy-button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Script } from "@/lib/types";
export default function InstallCommand({ item }: { item: Script }) {
const { title, item_type, installCommand, expand } = item;
const hasAlpineScript = expand?.alpine_script !== undefined;
const renderInstructions = (isAlpine = false) => (
<>
<p className="text-sm mt-2">
{isAlpine ? (
<>
As an alternative option, you can use Alpine Linux and the {title}{" "}
package to create a {title} {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 ? (
<>
To create a new Proxmox VE {title} {item_type}, run the command
below in the Proxmox VE Shell.
</>
) : (
<>To use the {title} script, run the command below in the shell.</>
)}
</p>
{isAlpine && (
<p className="mt-2 text-sm">
To create a new Proxmox VE Alpine-{title} {item_type}, run the command
below in the Proxmox VE Shell
</p>
)}
</>
);
return (
<div className="p-4">
{hasAlpineScript ? (
<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>{installCommand}</CodeCopyButton>
</TabsContent>
<TabsContent value="alpine">
{expand.alpine_script && (
<>
{renderInstructions(true)}
<CodeCopyButton>
{expand.alpine_script.installCommand}
</CodeCopyButton>
</>
)}
</TabsContent>
</Tabs>
) : (
<>
{renderInstructions()}
{installCommand && <CodeCopyButton>{installCommand}</CodeCopyButton>}
</>
)}
</div>
);
}

View file

@ -0,0 +1,45 @@
import { Button, buttonVariants } from "@/components/ui/button";
import handleCopy from "@/components/handleCopy";
import { cn } from "@/lib/utils";
import { ClipboardIcon } from "lucide-react";
interface Item {
interface?: string;
port?: number;
}
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: Item }) {
const { interface: iface, port } = item;
return (
<div className="flex flex-col gap-2">
{iface || (port && port !== 0) ? (
<div className="flex items-center justify-end">
<h2 className="mr-2 text-end text-lg font-semibold">
{iface ? "Interface:" : "Default Port:"}
</h2>{" "}
<CopyButton
label={iface ? "interface" : "port"}
value={iface || port!}
/>
</div>
) : null}
</div>
);
}

View file

@ -0,0 +1,49 @@
import { Badge } from "@/components/ui/badge";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Script } from "@/lib/types";
import React from "react";
interface TooltipProps {
variant: "warning" | "success";
label: string;
content: string;
}
const TooltipBadge: React.FC<TooltipProps> = ({ variant, label, content }) => (
<TooltipProvider>
<Tooltip delayDuration={100}>
<TooltipTrigger className="flex items-center">
<Badge variant={variant}>{label}</Badge>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-sm">
{content}
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
export default function Tooltips({ item }: { item: Script }) {
return (
<div className="flex items-center gap-2">
{item.privileged && (
<TooltipBadge
variant="warning"
label="Privileged"
content="This script will be run in a privileged LXC"
/>
)}
{item.isUpdateable && (
<TooltipBadge
variant="success"
label="Updateable"
content={`To Update ${item.title}, run the command below (or type update) in the LXC Console.`}
/>
)}
</div>
);
}

View file

@ -0,0 +1,34 @@
"use client";
import { Category } from "@/lib/types";
import ScriptAccordion from "./ScriptAccordion";
const Sidebar = ({
items,
selectedScript,
setSelectedScript,
}: {
items: Category[];
selectedScript: string | null;
setSelectedScript: (script: string | null) => void;
}) => {
return (
<div className="flex min-w-72 flex-col sm:max-w-72">
<div className="flex items-end justify-between pb-4">
<h1 className="text-xl font-bold">Categories</h1>
<p className="text-xs italic text-muted-foreground">
{items.reduce(
(acc, category) => acc + category.expand.items.length,
0,
)}{" "}
Total scripts
</p>
</div>
<div className="rounded-lg">
<ScriptAccordion items={items} selectedScript={selectedScript} setSelectedScript={setSelectedScript} />
</div>
</div>
);
};
export default Sidebar;

View file

@ -0,0 +1,98 @@
"use client";
export const dynamic = "force-static";
import ScriptItem from "@/app/scripts/_components/ScriptItem";
import { Category, Script } from "@/lib/types";
import { Loader2 } from "lucide-react";
import { Suspense, useEffect, useState } from "react";
import Sidebar from "./_components/Sidebar";
import { useQueryState } from "nuqs";
import {
LatestScripts,
MostViewedScripts,
} from "./_components/ScriptInfoBlocks";
function ScriptContent() {
const [selectedScript, setSelectedScript] = useQueryState("id");
const [links, setLinks] = useState<Category[]>([]);
const [item, setItem] = useState<Script>();
useEffect(() => {
if (selectedScript && links.length > 0) {
const script = links
.map((category) => category.expand.items)
.flat()
.find((script) => script.title === selectedScript);
setItem(script);
}
}, [selectedScript, links]);
const sortCategories = (categories: Category[]): Category[] => {
return categories.sort((a: Category, b: Category) => {
if (
a.catagoryName === "Proxmox VE Tools" &&
b.catagoryName !== "Proxmox VE Tools"
) {
return -1;
} else if (
a.catagoryName !== "Proxmox VE Tools" &&
b.catagoryName === "Proxmox VE Tools"
) {
return 1;
} else {
return a.catagoryName.localeCompare(b.catagoryName);
}
});
};
useEffect(() => {
fetch("api/categories")
.then((response) => response.json())
.then((categories) => {
const sortedCategories = sortCategories(categories);
setLinks(sortedCategories);
})
.catch((error) => console.error(error));
}, []);
return (
<div className="mb-3">
<div className="mt-20 flex sm:px-4 xl:px-0">
<div className="hidden sm:flex">
<Sidebar
items={links}
selectedScript={selectedScript}
setSelectedScript={setSelectedScript}
/>
</div>
<div className="mx-7 w-full sm:mx-0 sm:ml-7">
{selectedScript && item ? (
<ScriptItem item={item} setSelectedScript={setSelectedScript} />
) : (
<div className="flex w-full flex-col gap-5">
<LatestScripts items={links} />
<MostViewedScripts items={links} />
</div>
)}
</div>
</div>
</div>
);
}
export default function Page() {
return (
<Suspense
fallback={
<div className="flex h-screen w-full flex-col items-center justify-center gap-5 bg-background px-4 md:px-6">
<div className="space-y-2 text-center">
<Loader2 className="h-10 w-10 animate-spin" />
</div>
</div>
}
>
<ScriptContent />
</Suspense>
);
}

View file

@ -0,0 +1,20 @@
import type { MetadataRoute } from "next";
export const dynamic = "force-static";
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: "https://community-scripts.github.io/Proxmox/",
lastModified: new Date(),
changeFrequency: "yearly",
priority: 0.8,
},
{
url: "https://community-scripts.github.io/Proxmox/scripts",
lastModified: new Date(),
changeFrequency: "monthly",
priority: 1,
},
];
}