mirror of
https://github.com/community-scripts/ProxmoxVE.git
synced 2025-09-11 09:54:36 +00:00
Switch from Pocketbase data retrieval to JSON (#100)
* Add new animation for switching themes. * Remove unused metadata files from testing * increase duration on theme switch * Reduce animation duration for view transition effect to improve responsiveness * Fetch categories and scripts from external sources, updating `GET` endpoint to aggregate data. Adjust type definitions for Script and Category * Refactor all components to use data from new API * Refactor `InterFaces` component to use updated `Script` type and streamline interface/port handling for better clarity * Refactor `CommandMenu` component to utilize updated `Category` and `Script` types, simplifying the sorting logic and enhancing clarity * Fix animation duration in `globals.css` to ensure proper view transition functionality across the application * Remove unnecessary console log for file name in `fetchAllMetaDataFiles` to clean up code * Refactor category fetching in `ScriptContent` and `CommandMenu` to utilize centralized `fetchCategories` for improved maintainability * Use `formattedBadge` in `ScriptAccordion` and `CommandMenu` for consistent badge rendering across script types * Refactor source URL generation in `Buttons` component to enhance clarity and streamline the installation script logic * Check default settings availability in `DefaultSettings` component and handle undefined values more gracefully in rendering * Fix install command generation to handle optional script parameter and update copy button logic for improved functionality * Add most popular scripts feature and update script rendering logic in `ScriptInfoBlocks` component * Enhance `ScriptItem` component to display correct type naming alongside script name for better clarity in the UI * Add conversion utility to display RAM in GB for better readability in `DefaultSettings` component * Refactor Next.js config to use dynamic basePath and update sitemap URLs for improved adaptability and host configuration * Refactor site configuration to utilize centralized settings for analytics and base path; replace PocketBase imports with new data module * Refactor sitemap generation to use centralized basePath from config, enhancing adaptability for URL management * Refactor to replace PocketBase with a new data module across components * Refactor layout to use centralized analytics configuration * Update deployment workflow to include JSON files for GitHub Pages publishing * Remove caching step from GitHub Pages deploy workflow to avoid caching * Remove basePath from Next.js config to simplify configuration and avoid potential issues with path resolution * Add category sorting and fetching logic in data.ts * Add analytics configuration and basePath to siteConfig * Remove obsolete environment files for analytics and PocketBase * Update sitemap to use a fixed domain for the generated sitemap instead of deriving from headers * Refactor layout to utilize basePath for metadata base URL and image links for better configurability * use cleaner `basePath` variable around codebase for easier management * Update frontend/src/app/api/categories/route.ts Co-authored-by: Håvard Gjøby Thom <34199185+havardthom@users.noreply.github.com> * Update frontend/src/app/api/categories/route.ts Co-authored-by: Håvard Gjøby Thom <34199185+havardthom@users.noreply.github.com> * Update frontend/src/app/api/categories/route.ts Co-authored-by: Håvard Gjøby Thom <34199185+havardthom@users.noreply.github.com> * Update frontend/src/components/CommandMenu.tsx Co-authored-by: Håvard Gjøby Thom <34199185+havardthom@users.noreply.github.com> * Update frontend/src/components/ui/theme-toggle.tsx Co-authored-by: Håvard Gjøby Thom <34199185+havardthom@users.noreply.github.com> * Update frontend/src/components/CommandMenu.tsx Co-authored-by: Håvard Gjøby Thom <34199185+havardthom@users.noreply.github.com> * Update frontend/src/app/api/categories/route.ts Co-authored-by: Håvard Gjøby Thom <34199185+havardthom@users.noreply.github.com> * Update frontend/src/app/api/categories/route.ts Co-authored-by: Håvard Gjøby Thom <34199185+havardthom@users.noreply.github.com> * Update frontend/src/app/scripts/_components/ScriptItems/DefaultPassword.tsx Co-authored-by: Håvard Gjøby Thom <34199185+havardthom@users.noreply.github.com> * Update frontend/src/app/scripts/_components/ScriptItems/DefaultSettings.tsx Co-authored-by: Håvard Gjøby Thom <34199185+havardthom@users.noreply.github.com> * Update src/lib/data.ts with necessary changes. Co-authored-by: Håvard Gjøby Thom <34199185+havardthom@users.noreply.github.com> * Update src/app/api/categories/route.ts with necessary modifications * Update frontend/src/app/scripts/_components/ScriptItems/InstallCommand.tsx Co-authored-by: Håvard Gjøby Thom <34199185+havardthom@users.noreply.github.com> * Update src/components/CommandMenu.tsx with necessary improvements * Add renamed themetoggle * Update frontend/src/app/scripts/_components/ScriptInfoBlocks.tsx Co-authored-by: Håvard Gjøby Thom <34199185+havardthom@users.noreply.github.com> * Update frontend/src/app/scripts/_components/ScriptItems/DefaultSettings.tsx Co-authored-by: Håvard Gjøby Thom <34199185+havardthom@users.noreply.github.com> * Update frontend/src/app/scripts/_components/ScriptItems/DefaultSettings.tsx with new settings configuration * Update src/app/scripts/_components/ScriptInfoBlocks.tsx with enhancements and fixes * Update src/app/scripts/_components/ScriptItems/InstallCommand.tsx * Update src/app/scripts/_components/ScriptItem.tsx * Update src/app/scripts/_components/ScriptAccordion.tsx with necessary adjustments and improvements * Update Interfaces to use strict check * updated interfaces to use normal string label instead of jsx * Update configuration to use environment variable for BASE_PATH and reflect changes in siteConfig * force static base path * Update CommandMenu.tsx Co-authored-by: Håvard Gjøby Thom <34199185+havardthom@users.noreply.github.com> * Update DefaultSettings.tsx Co-authored-by: Håvard Gjøby Thom <34199185+havardthom@users.noreply.github.com> * Update DefaultSettings.tsx Co-authored-by: Håvard Gjøby Thom <34199185+havardthom@users.noreply.github.com> * Ensure fetchScripts returns a typed Script array by specifying return type in map function * Remove commented-out import for unused Category type in CommandMenu component * Fix fetch URLs by removing unnecessary slashes and ensure proper return type in fetchScripts map function * Refactor MostViewedScripts to ensure proper type annotations and improve array concatenation method for better readability * Update BASE_PATH handling in next.config and fix fetch URLs to ensure correct path structure in API routes --------- Co-authored-by: Håvard Gjøby Thom <34199185+havardthom@users.noreply.github.com>
This commit is contained in:
parent
97008d0273
commit
93fd495f65
37 changed files with 474 additions and 453 deletions
|
@ -1,23 +1,45 @@
|
|||
import { pb } from "@/lib/pocketbase";
|
||||
import { Category } from "@/lib/types";
|
||||
import { basePath } from "@/config/siteConfig";
|
||||
import { Category, Script } from "@/lib/types";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export const dynamic = "force-static";
|
||||
|
||||
const fetchCategories = async (): Promise<Category[]> => {
|
||||
const response = await fetch(
|
||||
`https://raw.githubusercontent.com/community-scripts/${basePath}/refs/heads/main/json/metadata.json`,
|
||||
);
|
||||
const data = await response.json();
|
||||
return data.categories;
|
||||
};
|
||||
|
||||
const fetchScripts = async (): Promise<Script[]> => {
|
||||
const response = await fetch(
|
||||
`https://api.github.com/repos/community-scripts/${basePath}/contents/json`,
|
||||
);
|
||||
const files: { download_url: string }[] = await response.json();
|
||||
const scripts = await Promise.all(
|
||||
files.map(async (file) : Promise<Script> => {
|
||||
const response = await fetch(file.download_url);
|
||||
const script = await response.json();
|
||||
return script;
|
||||
}),
|
||||
);
|
||||
return scripts;
|
||||
};
|
||||
|
||||
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);
|
||||
const categories = await fetchCategories();
|
||||
const scripts = await fetchScripts();
|
||||
for (const category of categories) {
|
||||
category.scripts = scripts.filter((script) => script.categories.includes(category.id));
|
||||
}
|
||||
return NextResponse.json(categories);
|
||||
} catch (error) {
|
||||
console.error("Error fetching categories:", error);
|
||||
console.error(error as Error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch categories" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import "@/styles/globals.css";
|
|||
import { Inter } from "next/font/google";
|
||||
import React from "react";
|
||||
import { NuqsAdapter } from "nuqs/adapters/next/app";
|
||||
import { analytics, basePath } from "@/config/siteConfig";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
|
@ -34,7 +35,7 @@ export const metadata = {
|
|||
address: false,
|
||||
telephone: false,
|
||||
},
|
||||
metadataBase: new URL("https://community-scripts.github.io/Proxmox/"),
|
||||
metadataBase: new URL(`https://community-scripts.github.io/${basePath}/`),
|
||||
openGraph: {
|
||||
title: "Proxmox VE Helper-Scripts",
|
||||
description:
|
||||
|
@ -42,7 +43,7 @@ export const metadata = {
|
|||
url: "/defaultimg.png",
|
||||
images: [
|
||||
{
|
||||
url: "https://community-scripts.github.io/Proxmox/defaultimg.png",
|
||||
url: `https://community-scripts.github.io/${basePath}/defaultimg.png`,
|
||||
},
|
||||
],
|
||||
locale: "en_US",
|
||||
|
@ -60,15 +61,20 @@ export default function RootLayout({
|
|||
<head>
|
||||
<script
|
||||
defer
|
||||
src={`https://${process.env.NEXT_PUBLIC_ANALYTICS_URL}/script.js`}
|
||||
data-website-id={process.env.NEXT_PUBLIC_ANALYTICS_TOKEN}
|
||||
src={`https://${analytics.url}/script.js`}
|
||||
data-website-id={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>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="dark"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<div className="flex w-full flex-col justify-center">
|
||||
<Navbar />
|
||||
<div className="flex min-h-screen flex-col justify-center">
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { basePath } from "@/config/siteConfig";
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
export const generateStaticParams = () => {
|
||||
|
@ -9,13 +10,13 @@ export default function manifest(): MetadataRoute.Manifest {
|
|||
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.",
|
||||
"A Re-designed Front-end for the Proxmox VE Helper-Scripts Repository. Featuring over 200+ scripts to help you manage your Proxmox VE environment.",
|
||||
theme_color: "#030712",
|
||||
background_color: "#030712",
|
||||
display: "standalone",
|
||||
orientation: "portrait",
|
||||
scope: "/Proxmox/",
|
||||
start_url: "/Proxmox/",
|
||||
scope: `${basePath}`,
|
||||
start_url: `${basePath}`,
|
||||
icons: [
|
||||
{
|
||||
src: "logo.png",
|
||||
|
|
|
@ -11,6 +11,7 @@ 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";
|
||||
import { basePath } from "@/config/siteConfig";
|
||||
|
||||
function CustomArrowRightIcon() {
|
||||
return <ArrowRightIcon className="h-4 w-4" width={1} />;
|
||||
|
@ -80,7 +81,7 @@ export default function Page() {
|
|||
</Button>
|
||||
<Button className="w-full" asChild>
|
||||
<a
|
||||
href="https://github.com/community-scripts/ProxmoxVE"
|
||||
href={`https://github.com/community-scripts/${basePath}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center"
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { basePath } from "@/config/siteConfig";
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
export const dynamic = "force-static";
|
||||
|
@ -8,6 +9,6 @@ export default function robots(): MetadataRoute.Robots {
|
|||
userAgent: "*",
|
||||
allow: "/",
|
||||
},
|
||||
sitemap: "https://community-scripts.github.io/Proxmox/sitemap.xml",
|
||||
sitemap: `https://community-scripts.github.io/${basePath}/sitemap.xml`,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import Image from "next/image";
|
|||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { Badge } from "../../../components/ui/badge";
|
||||
import { formattedBadge } from "@/components/CommandMenu";
|
||||
|
||||
export default function ScriptAccordion({
|
||||
items,
|
||||
|
@ -42,10 +43,10 @@ export default function ScriptAccordion({
|
|||
useEffect(() => {
|
||||
if (selectedScript) {
|
||||
const category = items.find((category) =>
|
||||
category.expand.items.some((script) => script.title === selectedScript),
|
||||
category.scripts.some((script) => script.name === selectedScript),
|
||||
);
|
||||
if (category) {
|
||||
setExpandedItem(category.catagoryName);
|
||||
setExpandedItem(category.name);
|
||||
handleSelected(selectedScript);
|
||||
}
|
||||
}
|
||||
|
@ -60,82 +61,68 @@ export default function ScriptAccordion({
|
|||
{items.map((category) => (
|
||||
<AccordionItem
|
||||
key={category.id + ":category"}
|
||||
value={category.catagoryName}
|
||||
value={category.name}
|
||||
className={cn("sm:text-md flex flex-col border-none", {
|
||||
"rounded-lg bg-accent/30": expandedItem === category.catagoryName,
|
||||
"rounded-lg bg-accent/30": expandedItem === category.name,
|
||||
})}
|
||||
>
|
||||
<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="pl-2">{category.name} </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}
|
||||
{category.scripts.length}
|
||||
</span>
|
||||
</div>{" "}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent
|
||||
data-state={
|
||||
expandedItem === category.catagoryName ? "open" : "closed"
|
||||
expandedItem === category.name ? "open" : "closed"
|
||||
}
|
||||
className="pt-0"
|
||||
>
|
||||
{category.expand.items
|
||||
{category.scripts
|
||||
.slice()
|
||||
.sort((a, b) => a.title.localeCompare(b.title))
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((script, index) => (
|
||||
<div key={index}>
|
||||
<Link
|
||||
href={{
|
||||
pathname: "/scripts",
|
||||
query: { id: script.title },
|
||||
query: { id: script.name},
|
||||
}}
|
||||
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
|
||||
selectedScript === script.name
|
||||
? "rounded-lg bg-accent font-semibold dark:bg-accent/30 dark:text-white"
|
||||
: ""
|
||||
}`}
|
||||
onClick={() => handleSelected(script.title)}
|
||||
onClick={() => handleSelected(script.name)}
|
||||
ref={(el) => {
|
||||
linkRefs.current[script.title] = el;
|
||||
linkRefs.current[script.name] = 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>
|
||||
<div className="flex items-center">
|
||||
<Image
|
||||
src={script.logo || "/logo.png"}
|
||||
height={16}
|
||||
width={16}
|
||||
unoptimized
|
||||
onError={(e) =>
|
||||
((e.currentTarget as HTMLImageElement).src =
|
||||
"/logo.png")
|
||||
}
|
||||
alt={script.name}
|
||||
className="mr-1 w-4 h-4 rounded-full"
|
||||
/>
|
||||
<span className="flex items-center gap-2">
|
||||
{script.name}
|
||||
</span>
|
||||
</div>
|
||||
{formattedBadge(script.type)}
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
|
|
|
@ -7,8 +7,9 @@ import {
|
|||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { mostPopularScripts } from "@/config/siteConfig";
|
||||
import { extractDate } from "@/lib/time";
|
||||
import { Category } from "@/lib/types";
|
||||
import { Category, Script } from "@/lib/types";
|
||||
import { CalendarPlus } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
@ -16,14 +17,28 @@ import { useMemo, useState } from "react";
|
|||
|
||||
const ITEMS_PER_PAGE = 3;
|
||||
|
||||
export const getDisplayValueFromType = (type: string) => {
|
||||
switch (type) {
|
||||
case "ct":
|
||||
return "LXC";
|
||||
case "vm":
|
||||
return "VM";
|
||||
case "misc":
|
||||
return "";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
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 || []);
|
||||
const scripts = items.flatMap((category) => category.scripts || []);
|
||||
return scripts.sort(
|
||||
(a, b) => new Date(b.created).getTime() - new Date(a.created).getTime(),
|
||||
(a, b) =>
|
||||
new Date(b.date_created).getTime() - new Date(a.date_created).getTime(),
|
||||
);
|
||||
}, [items]);
|
||||
|
||||
|
@ -68,16 +83,16 @@ export function LatestScripts({ items }: { items: Category[] }) {
|
|||
</div>
|
||||
)}
|
||||
<div className="min-w flex w-full flex-row flex-wrap gap-4">
|
||||
{latestScripts.slice(startIndex, endIndex).map((item) => (
|
||||
{latestScripts.slice(startIndex, endIndex).map((script) => (
|
||||
<Card
|
||||
key={item.id}
|
||||
key={script.name}
|
||||
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}
|
||||
src={script.logo || "/logo.png"}
|
||||
unoptimized
|
||||
height={64}
|
||||
width={64}
|
||||
|
@ -87,18 +102,18 @@ export function LatestScripts({ items }: { items: Category[] }) {
|
|||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className="text-lg line-clamp-1">
|
||||
{item.title} {item.item_type}
|
||||
{script.name} {getDisplayValueFromType(script.type)}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground flex items-center gap-1">
|
||||
<CalendarPlus className="h-4 w-4" />
|
||||
{extractDate(item.created)}
|
||||
{extractDate(script.date_created)}
|
||||
</p>
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardDescription className="line-clamp-3 text-card-foreground">
|
||||
{item.description}
|
||||
{script.description}
|
||||
</CardDescription>
|
||||
</CardContent>
|
||||
<CardFooter className="">
|
||||
|
@ -106,7 +121,7 @@ export function LatestScripts({ items }: { items: Category[] }) {
|
|||
<Link
|
||||
href={{
|
||||
pathname: "/scripts",
|
||||
query: { id: item.title },
|
||||
query: { id: script.name },
|
||||
}}
|
||||
>
|
||||
View Script
|
||||
|
@ -121,29 +136,12 @@ export function LatestScripts({ items }: { items: Category[] }) {
|
|||
}
|
||||
|
||||
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;
|
||||
const mostViewedScripts = items.reduce((acc: Script[], category) => {
|
||||
const foundScripts = category.scripts.filter((script) =>
|
||||
mostPopularScripts.includes(script.name),
|
||||
);
|
||||
return acc.concat(foundScripts);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
|
@ -153,9 +151,9 @@ export function MostViewedScripts({ items }: { items: Category[] }) {
|
|||
</>
|
||||
)}
|
||||
<div className="min-w flex w-full flex-row flex-wrap gap-4">
|
||||
{mostViewedScripts.slice(startIndex, endIndex).map((item) => (
|
||||
{mostViewedScripts.map((script) => (
|
||||
<Card
|
||||
key={item.id}
|
||||
key={script.name}
|
||||
className="min-w-[250px] flex-1 flex-grow bg-accent/30"
|
||||
>
|
||||
<CardHeader>
|
||||
|
@ -163,7 +161,7 @@ export function MostViewedScripts({ items }: { items: Category[] }) {
|
|||
<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}
|
||||
src={script.logo || "/logo.png"}
|
||||
height={64}
|
||||
width={64}
|
||||
alt=""
|
||||
|
@ -172,18 +170,18 @@ export function MostViewedScripts({ items }: { items: Category[] }) {
|
|||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className="line-clamp-1 text-lg">
|
||||
{item.title} {item.item_type}
|
||||
{script.name} {getDisplayValueFromType(script.type)}
|
||||
</p>
|
||||
<p className="flex items-center gap-1 text-sm text-muted-foreground">
|
||||
<CalendarPlus className="h-4 w-4" />
|
||||
{extractDate(item.created)}
|
||||
{extractDate(script.date_created)}
|
||||
</p>
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardDescription className="line-clamp-3 text-card-foreground break-words">
|
||||
{item.description}
|
||||
{script.description}
|
||||
</CardDescription>
|
||||
</CardContent>
|
||||
<CardFooter className="">
|
||||
|
@ -191,7 +189,7 @@ export function MostViewedScripts({ items }: { items: Category[] }) {
|
|||
<Link
|
||||
href={{
|
||||
pathname: "/scripts",
|
||||
query: { id: item.title },
|
||||
query: { id: script.name },
|
||||
}}
|
||||
prefetch={false}
|
||||
>
|
||||
|
@ -202,18 +200,6 @@ export function MostViewedScripts({ items }: { items: Category[] }) {
|
|||
</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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import { Script } from "@/lib/types";
|
|||
import { X } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
|
||||
import { getDisplayValueFromType } from "./ScriptInfoBlocks";
|
||||
import Alerts from "./ScriptItems/Alerts";
|
||||
import Buttons from "./ScriptItems/Buttons";
|
||||
import DefaultPassword from "./ScriptItems/DefaultPassword";
|
||||
|
@ -39,21 +40,21 @@ function ScriptItem({
|
|||
<div className="flex">
|
||||
<Image
|
||||
className="h-32 w-32 rounded-lg bg-accent/60 object-contain p-3 shadow-md"
|
||||
src={item.logo}
|
||||
src={item.logo || "/logo.png"}
|
||||
width={400}
|
||||
onError={(e) =>
|
||||
((e.currentTarget as HTMLImageElement).src = "/logo.png")
|
||||
}
|
||||
height={400}
|
||||
alt={item.title}
|
||||
alt={item.name}
|
||||
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>
|
||||
<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.created)}
|
||||
Date added: {extractDate(item.date_created)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-5">
|
||||
|
@ -76,7 +77,7 @@ function ScriptItem({
|
|||
<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"}
|
||||
How to {item.type ? "install" : "use"}
|
||||
</h2>
|
||||
<Tooltips item={item} />
|
||||
</div>
|
||||
|
|
|
@ -5,12 +5,12 @@ 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) => (
|
||||
{item?.notes?.length > 0 &&
|
||||
item.notes.map((note: 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>
|
||||
<span>{TextCopyBlock(note.text)}</span>
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
|
|
|
@ -1,33 +1,18 @@
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { basePath } from "@/config/siteConfig";
|
||||
import { Script } from "@/lib/types";
|
||||
import { BookOpenText, Code, ExternalLink, Globe } from "lucide-react";
|
||||
import { BookOpenText, Code, Globe } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useMemo } from "react";
|
||||
|
||||
const generateSourceUrl = (slug: string, type: string) => {
|
||||
if (type === "ct") {
|
||||
return `https://raw.githubusercontent.com/community-scripts/${basePath}/main/install/${slug}-install.sh`;
|
||||
} else {
|
||||
return `https://raw.githubusercontent.com/community-scripts/${basePath}/main/${type}/${slug}.sh`;
|
||||
}
|
||||
};
|
||||
|
||||
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 && (
|
||||
|
@ -49,26 +34,16 @@ export default function Buttons({ item }: { item: Script }) {
|
|||
</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)}>
|
||||
<Link target="_blank" href={generateSourceUrl(item.slug, item.type)}>
|
||||
<span className="flex items-center gap-2">
|
||||
<Code className="h-4 w-4" />
|
||||
Source Code
|
||||
</span>
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import handleCopy from "@/components/handleCopy";
|
|||
import { Script } from "@/lib/types";
|
||||
|
||||
export default function DefaultPassword({ item }: { item: Script }) {
|
||||
const hasDefaultLogin = item?.expand?.default_login !== undefined;
|
||||
const hasDefaultLogin = item.default_credentials.username && item.default_credentials.password;
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
@ -17,7 +17,7 @@ export default function DefaultPassword({ item }: { item: Script }) {
|
|||
<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}.
|
||||
{item.name} {item.type}.
|
||||
</p>
|
||||
<div className="text-sm">
|
||||
Username:{" "}
|
||||
|
@ -25,10 +25,10 @@ export default function DefaultPassword({ item }: { item: Script }) {
|
|||
variant={"secondary"}
|
||||
size={"null"}
|
||||
onClick={() =>
|
||||
handleCopy("username", item.expand.default_login.username)
|
||||
handleCopy("username", item.default_credentials.username ?? "")
|
||||
}
|
||||
>
|
||||
{item.expand.default_login.username}
|
||||
{item.default_credentials.username}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
|
@ -37,10 +37,10 @@ export default function DefaultPassword({ item }: { item: Script }) {
|
|||
variant={"secondary"}
|
||||
size={"null"}
|
||||
onClick={() =>
|
||||
handleCopy("password", item.expand.default_login.password)
|
||||
handleCopy("password", item.default_credentials.password ?? "")
|
||||
}
|
||||
>
|
||||
{item.expand.default_login.password}
|
||||
{item.default_credentials.password}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,35 +1,53 @@
|
|||
import { Script } from "@/lib/types";
|
||||
|
||||
export default function DefaultSettings({ item }: { item: Script }) {
|
||||
const hasAlpineScript = item?.expand?.alpine_script !== undefined;
|
||||
const defaultSettings = item.install_methods.find(
|
||||
(method) => method.type === "default",
|
||||
);
|
||||
|
||||
const defaultSettingsAvailable =
|
||||
defaultSettings?.resources.cpu ||
|
||||
defaultSettings?.resources.ram ||
|
||||
defaultSettings?.resources.hdd;
|
||||
|
||||
const defaultAlpineSettings = item.install_methods.find(
|
||||
(method) => method.type === "alpine",
|
||||
);
|
||||
|
||||
const getDisplayValueFromRAM = (ram: number) => {
|
||||
if (ram >= 1024) {
|
||||
return (ram / 1024).toFixed(0) + "GB";
|
||||
}
|
||||
return ram + "MB";
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{item.default_cpu && (
|
||||
{defaultSettingsAvailable && (
|
||||
<div>
|
||||
<h2 className="text-md font-semibold">Default settings</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
CPU: {item.default_cpu}
|
||||
CPU: {defaultSettings?.resources.cpu}vCPU
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
RAM: {item.default_ram}
|
||||
RAM: {getDisplayValueFromRAM(defaultSettings?.resources.ram ?? 0)}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
HDD: {item.default_hdd}
|
||||
HDD: {defaultSettings?.resources.hdd}GB
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{hasAlpineScript && (
|
||||
{defaultAlpineSettings && (
|
||||
<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}
|
||||
CPU: {defaultAlpineSettings?.resources.cpu}vCPU
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
RAM: {item.expand.alpine_script.default_ram}
|
||||
RAM: {getDisplayValueFromRAM(defaultAlpineSettings?.resources.ram ?? 0)}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
HDD: {item.expand.alpine_script.default_hdd}
|
||||
HDD: {defaultAlpineSettings?.resources.hdd}GB
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -1,33 +1,43 @@
|
|||
import CodeCopyButton from "@/components/ui/code-copy-button";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { basePath } from "@/config/siteConfig";
|
||||
import { Script } from "@/lib/types";
|
||||
|
||||
const getInstallCommand = (scriptPath?: string) => {
|
||||
return `bash -c "$(wget -qLO - https://github.com/community-scripts/${basePath}/raw/main/${scriptPath})"`;
|
||||
}
|
||||
|
||||
export default function InstallCommand({ item }: { item: Script }) {
|
||||
const { title, item_type, installCommand, expand } = item;
|
||||
const hasAlpineScript = expand?.alpine_script !== undefined;
|
||||
const alpineScript = item.install_methods.find(
|
||||
(method) => method.type === "alpine",
|
||||
);
|
||||
|
||||
const defaultScript = item.install_methods.find(
|
||||
(method) => method.type === "default"
|
||||
);
|
||||
|
||||
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
|
||||
As an alternative option, you can use Alpine Linux and the {item.name}{" "}
|
||||
package to create a {item.name} {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 ? (
|
||||
) : item.type ? (
|
||||
<>
|
||||
To create a new Proxmox VE {title} {item_type}, run the command
|
||||
To create a new Proxmox VE {item.name} {item.type}, run the command
|
||||
below in the Proxmox VE Shell.
|
||||
</>
|
||||
) : (
|
||||
<>To use the {title} script, run the command below in the shell.</>
|
||||
<>To use the {item.name} 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
|
||||
To create a new Proxmox VE Alpine-{item.name} {item.type}, run the command
|
||||
below in the Proxmox VE Shell
|
||||
</p>
|
||||
)}
|
||||
|
@ -36,7 +46,7 @@ export default function InstallCommand({ item }: { item: Script }) {
|
|||
|
||||
return (
|
||||
<div className="p-4">
|
||||
{hasAlpineScript ? (
|
||||
{alpineScript ? (
|
||||
<Tabs defaultValue="default" className="mt-2 w-full max-w-4xl">
|
||||
<TabsList>
|
||||
<TabsTrigger value="default">Default</TabsTrigger>
|
||||
|
@ -44,25 +54,23 @@ export default function InstallCommand({ item }: { item: Script }) {
|
|||
</TabsList>
|
||||
<TabsContent value="default">
|
||||
{renderInstructions()}
|
||||
<CodeCopyButton>{installCommand}</CodeCopyButton>
|
||||
<CodeCopyButton>{getInstallCommand(defaultScript?.script)}</CodeCopyButton>
|
||||
</TabsContent>
|
||||
<TabsContent value="alpine">
|
||||
{expand.alpine_script && (
|
||||
<>
|
||||
{renderInstructions(true)}
|
||||
<CodeCopyButton>
|
||||
{expand.alpine_script.installCommand}
|
||||
</CodeCopyButton>
|
||||
</>
|
||||
)}
|
||||
{renderInstructions(true)}
|
||||
<CodeCopyButton>
|
||||
{getInstallCommand(alpineScript.script)}
|
||||
</CodeCopyButton>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
) : (
|
||||
) : defaultScript?.script ? (
|
||||
<>
|
||||
{renderInstructions()}
|
||||
{installCommand && <CodeCopyButton>{installCommand}</CodeCopyButton>}
|
||||
<CodeCopyButton>
|
||||
{getInstallCommand(defaultScript.script)}
|
||||
</CodeCopyButton>
|
||||
</>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,11 +2,7 @@ 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;
|
||||
}
|
||||
import { Script } from "@/lib/types";
|
||||
|
||||
const CopyButton = ({
|
||||
label,
|
||||
|
@ -24,19 +20,18 @@ const CopyButton = ({
|
|||
</span>
|
||||
);
|
||||
|
||||
export default function InterFaces({ item }: { item: Item }) {
|
||||
const { interface: iface, port } = item;
|
||||
export default function InterFaces({item} : {item : Script}) {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
{iface || (port && port !== 0) ? (
|
||||
{item.interface_port !== null ? (
|
||||
<div className="flex items-center justify-end">
|
||||
<h2 className="mr-2 text-end text-lg font-semibold">
|
||||
{iface ? "Interface:" : "Default Port:"}
|
||||
{"Default Interface:"}
|
||||
</h2>{" "}
|
||||
<CopyButton
|
||||
label={iface ? "interface" : "port"}
|
||||
value={iface || port!}
|
||||
label="default interface"
|
||||
value={item.interface_port}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
|
|
@ -37,11 +37,11 @@ export default function Tooltips({ item }: { item: Script }) {
|
|||
content="This script will be run in a privileged LXC"
|
||||
/>
|
||||
)}
|
||||
{item.isUpdateable && (
|
||||
{item.updateable && (
|
||||
<TooltipBadge
|
||||
variant="success"
|
||||
label="Updateable"
|
||||
content={`To Update ${item.title}, run the command below (or type update) in the LXC Console.`}
|
||||
content={`To Update ${item.name}, run the command below (or type update) in the LXC Console.`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -18,7 +18,7 @@ const Sidebar = ({
|
|||
<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,
|
||||
(acc, category) => acc + category.scripts.length,
|
||||
0,
|
||||
)}{" "}
|
||||
Total scripts
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
LatestScripts,
|
||||
MostViewedScripts,
|
||||
} from "./_components/ScriptInfoBlocks";
|
||||
import { fetchCategories } from "@/lib/data";
|
||||
|
||||
function ScriptContent() {
|
||||
const [selectedScript, setSelectedScript] = useQueryState("id");
|
||||
|
@ -21,41 +22,19 @@ function ScriptContent() {
|
|||
useEffect(() => {
|
||||
if (selectedScript && links.length > 0) {
|
||||
const script = links
|
||||
.map((category) => category.expand.items)
|
||||
.map((category) => category.scripts)
|
||||
.flat()
|
||||
.find((script) => script.title === selectedScript);
|
||||
.find((script) => script.name === 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?_=${process.env.NEXT_PUBLIC_BUILD_TIME || Date.now()}`,
|
||||
)
|
||||
.then((response) => response.json())
|
||||
.then((categories) => {
|
||||
const sortedCategories = sortCategories(categories);
|
||||
setLinks(sortedCategories);
|
||||
})
|
||||
.catch((error) => console.error(error));
|
||||
fetchCategories()
|
||||
.then((categories) => {
|
||||
setLinks(categories);
|
||||
})
|
||||
.catch((error) => console.error(error));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,20 +1,19 @@
|
|||
import { basePath } from "@/config/siteConfig";
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
export const dynamic = "force-static";
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
let domain = "community-scripts.github.io";
|
||||
let protocol = "https";
|
||||
return [
|
||||
{
|
||||
url: "https://community-scripts.github.io/Proxmox/",
|
||||
url: `${protocol}://${domain}/${basePath}`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "yearly",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: "https://community-scripts.github.io/Proxmox/scripts",
|
||||
url: `${protocol}://${domain}/${basePath}/scripts`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "monthly",
|
||||
priority: 1,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
|
@ -6,30 +6,28 @@ import {
|
|||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import { fetchCategories } from "@/lib/data";
|
||||
import { Category } from "@/lib/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/navigation";
|
||||
import React, { useEffect } from "react";
|
||||
import React from "react";
|
||||
import { Badge } from "./ui/badge";
|
||||
import { Button } from "./ui/button";
|
||||
import { DialogTitle } from "./ui/dialog";
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
export const formattedBadge = (type: string) => {
|
||||
switch (type) {
|
||||
case "vm":
|
||||
return <Badge className="text-blue-500/75 border-blue-500/75">VM</Badge>;
|
||||
case "ct":
|
||||
return (
|
||||
<Badge className="text-yellow-500/75 border-yellow-500/75">LXC</Badge>
|
||||
);
|
||||
case "misc":
|
||||
return <Badge className="text-red-500/75 border-red-500/75">MISC</Badge>;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export default function CommandMenu() {
|
||||
|
@ -50,21 +48,17 @@ export default function CommandMenu() {
|
|||
return () => document.removeEventListener("keydown", down);
|
||||
}, []);
|
||||
|
||||
const fetchCategories = async () => {
|
||||
const fetchSortedCategories = () => {
|
||||
setIsLoading(true);
|
||||
fetch(
|
||||
`api/categories?_=${process.env.NEXT_PUBLIC_BUILD_TIME || Date.now()}`,
|
||||
)
|
||||
.then((response) => response.json())
|
||||
.then((categories) => {
|
||||
const sortedCategories = sortCategories(categories);
|
||||
setLinks(sortedCategories);
|
||||
setIsLoading(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
setIsLoading(false);
|
||||
console.error(error);
|
||||
});
|
||||
fetchCategories()
|
||||
.then((categories) => {
|
||||
setLinks(categories);
|
||||
setIsLoading(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
setIsLoading(false);
|
||||
console.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -75,8 +69,8 @@ export default function CommandMenu() {
|
|||
"relative h-9 w-full justify-start rounded-[0.5rem] bg-muted/50 text-sm font-normal text-muted-foreground shadow-none sm:pr-12 md:w-40 lg:w-64",
|
||||
)}
|
||||
onClick={() => {
|
||||
fetchCategories();
|
||||
setOpen(true)
|
||||
fetchSortedCategories();
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
<span className="inline-flex">Search scripts...</span>
|
||||
|
@ -85,41 +79,40 @@ export default function CommandMenu() {
|
|||
</kbd>
|
||||
</Button>
|
||||
<CommandDialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTitle className="sr-only">Search scripts</DialogTitle>
|
||||
<CommandInput placeholder="search for a script..." />
|
||||
<DialogTitle className="sr-only">Search scripts</DialogTitle>
|
||||
<CommandInput placeholder="Search for a script..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>{isLoading ? "Loading..." : "No scripts found."}</CommandEmpty>
|
||||
<CommandEmpty>
|
||||
{isLoading ? "Loading..." : "No scripts found."}
|
||||
</CommandEmpty>
|
||||
{links.map((category) => (
|
||||
<CommandGroup
|
||||
key={"category:" + category.catagoryName}
|
||||
heading={category.catagoryName}
|
||||
key={`category:${category.name}`}
|
||||
heading={category.name}
|
||||
>
|
||||
{category.expand.items.map((script) => (
|
||||
{category.scripts.map((script) => (
|
||||
<CommandItem
|
||||
key={"script:" + script.id}
|
||||
value={script.title}
|
||||
key={`script:${script.name}`}
|
||||
value={script.name}
|
||||
onSelect={() => {
|
||||
setOpen(false);
|
||||
router.push(`/scripts?id=${script.title}`);
|
||||
router.push(`/scripts?id=${script.name}`);
|
||||
}}
|
||||
>
|
||||
<div className="flex gap-2" onClick={() => setOpen(false)}>
|
||||
<Image
|
||||
src={script.logo}
|
||||
unoptimized
|
||||
height={16}
|
||||
src={script.logo || "/logo.png"}
|
||||
onError={(e) =>
|
||||
((e.currentTarget as HTMLImageElement).src =
|
||||
"/logo.png")
|
||||
}
|
||||
width={16}
|
||||
height={16}
|
||||
alt=""
|
||||
className="h-5 w-5"
|
||||
/>
|
||||
<span>{script.title}</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{script.item_type}
|
||||
</span>
|
||||
<span>{script.name}</span>
|
||||
<span>{formattedBadge(script.type)}</span>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { basePath } from "@/config/siteConfig";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function Footer() {
|
||||
|
@ -7,7 +8,7 @@ export default function Footer() {
|
|||
<div className="mx-6 w-full max-w-7xl text-sm text-muted-foreground">
|
||||
Website build by the community. The source code is avaliable on{" "}
|
||||
<Link
|
||||
href="https://github.com/community-scripts/Proxmox"
|
||||
href={`https://github.com/community-scripts/${basePath}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="font-semibold underline-offset-2 duration-300 hover:underline"
|
||||
|
|
|
@ -6,11 +6,9 @@ import { useEffect, useState } from "react";
|
|||
|
||||
import { navbarLinks } from "@/config/siteConfig";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { MoonIcon, SunIcon } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import CommandMenu from "./CommandMenu";
|
||||
import StarOnGithubButton from "./ui/star-on-github-button";
|
||||
import { ThemeToggle } from "./ui/theme-toggle";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
|
@ -22,7 +20,6 @@ export const dynamic = "force-dynamic";
|
|||
|
||||
function Navbar() {
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
|
@ -56,7 +53,6 @@ function Navbar() {
|
|||
/>
|
||||
<span className="hidden lg:block">Proxmox VE Helper-Scripts</span>
|
||||
</Link>
|
||||
{/* <MobileNav /> */}
|
||||
<div className="flex gap-2">
|
||||
<CommandMenu />
|
||||
<StarOnGithubButton />
|
||||
|
@ -81,28 +77,7 @@ function Navbar() {
|
|||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
))}
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={100}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
type="button"
|
||||
size="icon"
|
||||
className={cn("px-2")}
|
||||
aria-label="Toggle theme"
|
||||
onClick={() =>
|
||||
setTheme(theme === "dark" ? "light" : "dark")
|
||||
}
|
||||
>
|
||||
<SunIcon className="size-[1.2rem] text-neutral-800 dark:hidden dark:text-neutral-200" />
|
||||
<MoonIcon className="hidden size-[1.2rem] text-neutral-800 dark:block dark:text-neutral-200" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="text-xs">
|
||||
Theme Toggle
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -8,6 +8,7 @@ import * as React from "react";
|
|||
import { toast } from "sonner";
|
||||
import { Button } from "./button";
|
||||
import { Separator } from "./separator";
|
||||
import { basePath } from "@/config/siteConfig";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
|
@ -67,7 +68,7 @@ const handleCopy = (type: string, value: string) => {
|
|||
<div>
|
||||
<Button className="text-white">
|
||||
<Link
|
||||
href="https://github.com/community-scripts/ProxmoxVE"
|
||||
href={`https://github.com/community-scripts/${basePath}`}
|
||||
data-umami-event="Star on Github"
|
||||
target="_blank"
|
||||
>
|
||||
|
|
|
@ -4,6 +4,7 @@ import { useEffect, useState } from "react";
|
|||
import { FaGithub, FaStar } from "react-icons/fa";
|
||||
import NumberTicker from "./number-ticker";
|
||||
import { buttonVariants } from "./button";
|
||||
import { basePath } from "@/config/siteConfig";
|
||||
|
||||
export default function StarOnGithubButton() {
|
||||
const [stars, setStars] = useState(0);
|
||||
|
@ -11,7 +12,7 @@ export default function StarOnGithubButton() {
|
|||
useEffect(() => {
|
||||
const fetchStars = async () => {
|
||||
try {
|
||||
const res = await fetch("https://api.github.com/repos/community-scripts/ProxmoxVE", {
|
||||
const res = await fetch(`https://api.github.com/repos/community-scripts/${basePath}`, {
|
||||
next: { revalidate: 60 * 60 * 24 },
|
||||
});
|
||||
|
||||
|
@ -34,7 +35,7 @@ export default function StarOnGithubButton() {
|
|||
"group relative justify-center gap-2 rounded-md transition-all duration-300 ease-out hover:ring-2 hover:ring-primary hover:ring-offset-2",
|
||||
)}
|
||||
target="_blank"
|
||||
href="https://github.com/community-scripts/ProxmoxVE"
|
||||
href={`https://github.com/community-scripts/${basePath}`}
|
||||
>
|
||||
<span className="absolute right-0 -mt-12 h-32 translate-x-12 rotate-12 bg-white opacity-10 transition-all duration-1000 ease-out group-hover:-translate-x-40" />
|
||||
<div className="flex items-center">
|
||||
|
|
42
frontend/src/components/ui/theme-toggle.tsx
Normal file
42
frontend/src/components/ui/theme-toggle.tsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
"use client";
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./tooltip";
|
||||
import { Button } from "./button";
|
||||
import { MoonIcon, SunIcon } from "@radix-ui/react-icons";
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { setTheme, theme: currentTheme } = useTheme();
|
||||
|
||||
const handleChangeTheme = (theme: "light" | "dark") => {
|
||||
if (theme === currentTheme) return;
|
||||
|
||||
if (!document.startViewTransition) return setTheme(theme);
|
||||
document.startViewTransition(() => setTheme(theme));
|
||||
};
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={100}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
type="button"
|
||||
size="icon"
|
||||
className="px-2"
|
||||
aria-label="Toggle theme"
|
||||
onClick={() =>
|
||||
handleChangeTheme(currentTheme === "dark" ? "light" : "dark")
|
||||
}
|
||||
>
|
||||
<SunIcon className="size-[1.2rem] text-neutral-800 dark:hidden dark:text-neutral-200" />
|
||||
<MoonIcon className="hidden size-[1.2rem] text-neutral-800 dark:block dark:text-neutral-200" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="text-xs">
|
||||
Theme Toggle
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
|
@ -1,23 +1,36 @@
|
|||
import { MessagesSquare, Scroll } from "lucide-react";
|
||||
import { FaGithub } from "react-icons/fa";
|
||||
|
||||
export const basePath = process.env.BASE_PATH;
|
||||
|
||||
export const navbarLinks = [
|
||||
{
|
||||
href: "https://github.com/community-scripts/ProxmoxVE",
|
||||
href: `https://github.com/community-scripts/${basePath}`,
|
||||
event: "Github",
|
||||
icon: <FaGithub className="h-4 w-4" />,
|
||||
text: "Github",
|
||||
},
|
||||
{
|
||||
href: "https://github.com/community-scripts/ProxmoxVE/blob/main/CHANGELOG.md",
|
||||
href: `https://github.com/community-scripts/${basePath}/blob/main/CHANGELOG.md`,
|
||||
event: "Change Log",
|
||||
icon: <Scroll className="h-4 w-4" />,
|
||||
text: "Change Log",
|
||||
},
|
||||
{
|
||||
href: "https://github.com/community-scripts/ProxmoxVE/discussions",
|
||||
href: `https://github.com/community-scripts/${basePath}/discussions`,
|
||||
event: "Discussions",
|
||||
icon: <MessagesSquare className="h-4 w-4" />,
|
||||
text: "Discussions",
|
||||
},
|
||||
];
|
||||
|
||||
export const mostPopularScripts = [
|
||||
"Proxmox VE Post Install",
|
||||
"Docker",
|
||||
"Home Assistant OS",
|
||||
];
|
||||
|
||||
export const analytics = {
|
||||
url: "analytics.proxmoxve-scripts.com",
|
||||
token: "b60d3032-1a11-4244-a100-81d26c5c49a7",
|
||||
};
|
||||
|
|
23
frontend/src/lib/data.ts
Normal file
23
frontend/src/lib/data.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { Category } from "./types";
|
||||
|
||||
const sortCategories = (categories: Category[]) => {
|
||||
return categories.sort((a, b) => {
|
||||
if (a.name === "Proxmox VE Tools") {
|
||||
return -1;
|
||||
} else if (b.name === "Proxmox VE Tools") {
|
||||
return 1;
|
||||
} else if (a.name === "Miscellaneous") {
|
||||
return 1;
|
||||
} else if (b.name === "Miscellaneous") {
|
||||
return -1;
|
||||
} else {
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchCategories = async (): Promise<Category[]> => {
|
||||
const response = await fetch("api/categories");
|
||||
const categories = await response.json();
|
||||
return sortCategories(categories);
|
||||
};
|
|
@ -1,10 +0,0 @@
|
|||
import PocketBase from "pocketbase";
|
||||
|
||||
export const pb = new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL);
|
||||
export const pbBackup = new PocketBase(
|
||||
process.env.NEXT_PUBLIC_POCKETBASE_URL_BACKUP,
|
||||
);
|
||||
|
||||
export const getImageURL = (recordId: string, fileName: string) => {
|
||||
return `${process.env.NEXT_PUBLIC_POCKETBASE_URL}/${recordId}/${fileName}`;
|
||||
};
|
|
@ -1,55 +1,44 @@
|
|||
// these are all the interfaces that are used in the site. these all come from the pocketbase database
|
||||
|
||||
export interface Script {
|
||||
title: string;
|
||||
description: string;
|
||||
documentation: string;
|
||||
website: string;
|
||||
logo: string;
|
||||
created: string;
|
||||
updated: string;
|
||||
id: string;
|
||||
item_type: string;
|
||||
interface: string;
|
||||
installCommand: string;
|
||||
port: number;
|
||||
post_install: string;
|
||||
default_cpu: string;
|
||||
default_hdd: string;
|
||||
default_ram: string;
|
||||
isUpdateable: boolean;
|
||||
isMostViewed: boolean;
|
||||
export type Script = {
|
||||
name: string;
|
||||
slug: string;
|
||||
categories: number[];
|
||||
date_created: string;
|
||||
type: "vm" | "ct" | "misc";
|
||||
updateable: boolean;
|
||||
privileged: boolean;
|
||||
alpineScript: alpine_script;
|
||||
expand: {
|
||||
alpine_script: alpine_script;
|
||||
alerts: alerts[];
|
||||
default_login: default_login;
|
||||
interface_port: number | null;
|
||||
documentation: string | null;
|
||||
website: string | null;
|
||||
logo: string | null;
|
||||
description: string;
|
||||
install_methods: {
|
||||
type: "default" | "alpine";
|
||||
script: string;
|
||||
resources: {
|
||||
cpu: number | null;
|
||||
ram: number | null;
|
||||
hdd: number | null;
|
||||
os: string | null;
|
||||
version: number | null;
|
||||
};
|
||||
}[];
|
||||
default_credentials: {
|
||||
username: string | null;
|
||||
password: string | null;
|
||||
};
|
||||
notes: [{
|
||||
text: string;
|
||||
type: string;
|
||||
}]
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
catagoryName: string;
|
||||
categoryId: string;
|
||||
id: string;
|
||||
created: string;
|
||||
expand: {
|
||||
items: Script[];
|
||||
};
|
||||
export type Category = {
|
||||
name: string;
|
||||
id: number;
|
||||
sort_order: number;
|
||||
scripts: Script[];
|
||||
}
|
||||
|
||||
interface alpine_script {
|
||||
installCommand: string;
|
||||
default_cpu: string;
|
||||
default_hdd: string;
|
||||
default_ram: string;
|
||||
}
|
||||
|
||||
interface alerts {
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface default_login {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
export type ScriptList = {
|
||||
categories: Category[];
|
||||
}
|
|
@ -30,6 +30,29 @@
|
|||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
--expo-out: linear(
|
||||
0 0%,
|
||||
0.1684 2.66%,
|
||||
0.3165 5.49%,
|
||||
0.446 8.52%,
|
||||
0.5581 11.78%,
|
||||
0.6535 15.29%,
|
||||
0.7341 19.11%,
|
||||
0.8011 23.3%,
|
||||
0.8557 27.93%,
|
||||
0.8962 32.68%,
|
||||
0.9283 38.01%,
|
||||
0.9529 44.08%,
|
||||
0.9711 51.14%,
|
||||
0.9833 59.06%,
|
||||
0.9915 68.74%,
|
||||
1 100%
|
||||
);
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: hsl(var(--accent));
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.dark {
|
||||
|
@ -58,8 +81,46 @@
|
|||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
}
|
||||
|
||||
::view-transition-group(root) {
|
||||
animation-duration: 0.7bun s;
|
||||
animation-timing-function: var(--expo-out);
|
||||
}
|
||||
|
||||
::view-transition-new(root) {
|
||||
animation-name: reveal-light;
|
||||
}
|
||||
|
||||
::view-transition-old(root),
|
||||
.dark::view-transition-old(root) {
|
||||
animation: none;
|
||||
z-index: -1;
|
||||
}
|
||||
.dark::view-transition-new(root) {
|
||||
animation-name: reveal-dark;
|
||||
}
|
||||
|
||||
@keyframes reveal-dark {
|
||||
from {
|
||||
clip-path: polygon(50% -71%, -50% 71%, -50% 71%, 50% -71%);
|
||||
}
|
||||
to {
|
||||
clip-path: polygon(50% -71%, -50% 71%, 50% 171%, 171% 50%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes reveal-light {
|
||||
from {
|
||||
clip-path: polygon(171% 50%, 50% 171%, 50% 171%, 171% 50%);
|
||||
}
|
||||
to {
|
||||
clip-path: polygon(171% 50%, 50% 171%, -50% 71%, 50% -71%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue