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:
Bram Suurd 2024-11-06 23:47:04 +01:00 committed by GitHub
parent 97008d0273
commit 93fd495f65
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 474 additions and 453 deletions

View File

@ -9,6 +9,7 @@ on:
branches: ["main"]
paths:
- frontend/**
- json/**
workflow_dispatch:
@ -57,14 +58,6 @@ jobs:
uses: actions/configure-pages@v5
with:
static_site_generator: next
- name: Restore cache
uses: actions/cache@v4
with:
path: |
frontend/.next/cache
key: ${{ runner.os }}-nextjs-${{ hashFiles('frontend/**/package-lock.json', 'frontend/**/yarn.lock') }}-${{ hashFiles('frontend/**.[jt]s', 'frontend/**.[jt]sx') }}
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('frontend/**/package-lock.json', 'frontend/**/yarn.lock') }}-
- name: Install dependencies
run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }} --legacy-peer-deps
- name: Build with Next.js

View File

@ -1,3 +0,0 @@
NEXT_PUBLIC_ANALYTICS_TOKEN="b60d3032-1a11-4244-a100-81d26c5c49a7"
NEXT_PUBLIC_ANALYTICS_URL="analytics.proxmoxve-scripts.com"
NEXT_PUBLIC_POCKETBASE_URL="https://pocketbase.proxmoxve-scripts.com"

View File

@ -1,4 +0,0 @@
NEXT_PUBLIC_POCKETBASE_URL=https://pocketbase.proxmoxve-scripts.com
NEXT_PUBLIC_ANALYTICS_URL=https://analytics.proxmoxve-scripts.com
NEXT_PUBLIC_ANALYTICS_TOKEN=b60d130323-1a11-4244-a1010-81d263c5c49a7
NODE_ENV=production

View File

@ -15,11 +15,11 @@ const nextConfig = {
},
env: {
NEXT_PUBLIC_BUILD_TIME: `${Date.now()}`,
BASE_PATH: "ProxmoxVE",
},
output: "export",
basePath: "/ProxmoxVE",
basePath: `/${process.env.BASE_PATH}`,
};
export default nextConfig;

View File

@ -12,6 +12,7 @@
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-icons": "^1.3.1",
"@radix-ui/react-navigation-menu": "^1.1.4",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
@ -21,7 +22,7 @@
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"framer-motion": "^11.11.10",
"framer-motion": "^11.11.11",
"fuse.js": "^7.0.0",
"lucide-react": "^0.453.0",
"mini-svg-data-uri": "^1.4.4",
@ -1335,6 +1336,14 @@
}
}
},
"node_modules/@radix-ui/react-icons": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.1.tgz",
"integrity": "sha512-QvYompk0X+8Yjlo/Fv4McrzxohDdM5GgLHyQcPpcsPvlOSXCGFjdbuyGL5dzRbg0GpknAjQJJZzdiRK7iWVuFQ==",
"peerDependencies": {
"react": "^16.x || ^17.x || ^18.x || ^19.x"
}
},
"node_modules/@radix-ui/react-id": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz",

View File

@ -22,6 +22,7 @@
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-icons": "^1.3.1",
"@radix-ui/react-navigation-menu": "^1.1.4",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
@ -31,7 +32,7 @@
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"framer-motion": "^11.11.10",
"framer-motion": "^11.11.11",
"fuse.js": "^7.0.0",
"lucide-react": "^0.453.0",
"mini-svg-data-uri": "^1.4.4",

View File

@ -1,23 +0,0 @@
{
"slug": "docker",
"logo": "https://raw.githubusercontent.com/loganmarchione/homelab-svg-assets/main/assets/docker.svg",
"description": "Docker is an open-source project for automating the deployment of applications as portable, self-sufficient containers.",
"date_created": "2024-05-02",
"website": "https://www.docker.com/",
"documentation": "",
"default_credentials": {
"username": "",
"password": ""
},
"alerts": [
{
"alert": "If the LXC is created Privileged, the script will automatically set up USB passthrough."
},
{
"alert": "Run Compose V2 by replacing the hyphen (-) with a space, using `docker compose`, instead of `docker-compose`."
},
{
"alert": "Options to Install Portainer and/or Docker Compose V2"
}
]
}

View File

@ -1,20 +0,0 @@
{
"slug": "nginxproxymanager",
"logo": "https://raw.githubusercontent.com/loganmarchione/homelab-svg-assets/main/assets/nginxproxymanager.svg",
"description": "Nginx Proxy Manager is a tool that provides a web-based interface to manage Nginx reverse proxies. It enables users to easily and securely expose their services to the internet by providing features such as HTTPS encryption, domain mapping, and access control. It eliminates the need for manual configuration of Nginx reverse proxies, making it easy for users to quickly and securely expose their services to the public.",
"date_created": "2024-05-02",
"website": "https://nginxproxymanager.com/",
"documentation": "",
"default_credentials": {
"username": "admin",
"password": "admin"
},
"alerts": [
{
"alert": "Since there are hundreds of Certbot instances, it's necessary to install the specific Certbot of your preference."
},
{
"alert": "This is another example of an alert."
}
]
}

View File

@ -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 },
);
}
}

View File

@ -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">

View File

@ -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",

View File

@ -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"

View File

@ -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`,
};
}

View File

@ -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>
))}

View File

@ -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>
);
}

View File

@ -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>

View File

@ -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>
))}

View File

@ -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>
);
}

View File

@ -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>

View File

@ -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>
)}

View File

@ -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>
);
}

View File

@ -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}

View File

@ -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>

View File

@ -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

View File

@ -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 (

View File

@ -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,
},
];
}

View File

@ -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>
))}

View File

@ -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"

View File

@ -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>

View File

@ -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"
>

View File

@ -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">

View 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>
);
}

View File

@ -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
View 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);
};

View File

@ -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}`;
};

View File

@ -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[];
}

View File

@ -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;