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

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