diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml index ca23f326..435c5c80 100644 --- a/.github/workflows/deploy-pages.yml +++ b/.github/workflows/deploy-pages.yml @@ -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 diff --git a/frontend/.env.local b/frontend/.env.local deleted file mode 100644 index 478cdb20..00000000 --- a/frontend/.env.local +++ /dev/null @@ -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" diff --git a/frontend/example.env b/frontend/example.env deleted file mode 100644 index fcdd4567..00000000 --- a/frontend/example.env +++ /dev/null @@ -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 diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs index 528a6a3e..a2529f34 100644 --- a/frontend/next.config.mjs +++ b/frontend/next.config.mjs @@ -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; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d0ac45ee..c2e0b68f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/frontend/package.json b/frontend/package.json index be0c307b..6bd130d2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/public/metadata/docker.json b/frontend/public/metadata/docker.json deleted file mode 100644 index 55e76004..00000000 --- a/frontend/public/metadata/docker.json +++ /dev/null @@ -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" - } - ] -} diff --git a/frontend/public/metadata/nginxproxymanager.json b/frontend/public/metadata/nginxproxymanager.json deleted file mode 100644 index 6424080f..00000000 --- a/frontend/public/metadata/nginxproxymanager.json +++ /dev/null @@ -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." - } - ] -} diff --git a/frontend/src/app/api/categories/route.ts b/frontend/src/app/api/categories/route.ts index 6b6368fb..52470a12 100644 --- a/frontend/src/app/api/categories/route.ts +++ b/frontend/src/app/api/categories/route.ts @@ -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 => { + 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 => { + 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 - +
diff --git a/frontend/src/app/manifest.ts b/frontend/src/app/manifest.ts index 0e4cb5dd..1cfc38f5 100644 --- a/frontend/src/app/manifest.ts +++ b/frontend/src/app/manifest.ts @@ -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", diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index ea486cb0..67577c6c 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -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 ; @@ -80,7 +81,7 @@ export default function Page() {
)}
- {latestScripts.slice(startIndex, endIndex).map((item) => ( + {latestScripts.slice(startIndex, endIndex).map((script) => (

- {item.title} {item.item_type} + {script.name} {getDisplayValueFromType(script.type)}

- {extractDate(item.created)} + {extractDate(script.date_created)}

- {item.description} + {script.description} @@ -106,7 +121,7 @@ export function LatestScripts({ items }: { items: Category[] }) { 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 (
@@ -153,9 +151,9 @@ export function MostViewedScripts({ items }: { items: Category[] }) { )}
- {mostViewedScripts.slice(startIndex, endIndex).map((item) => ( + {mostViewedScripts.map((script) => ( @@ -163,7 +161,7 @@ export function MostViewedScripts({ items }: { items: Category[] }) {

- {item.title} {item.item_type} + {script.name} {getDisplayValueFromType(script.type)}

- {extractDate(item.created)} + {extractDate(script.date_created)}

- {item.description} + {script.description} @@ -191,7 +189,7 @@ export function MostViewedScripts({ items }: { items: Category[] }) { @@ -202,18 +200,6 @@ export function MostViewedScripts({ items }: { items: Category[] }) { ))}
-
- {page > 1 && ( - - )} - {endIndex < mostViewedScripts.length && ( - - )} -
); } diff --git a/frontend/src/app/scripts/_components/ScriptItem.tsx b/frontend/src/app/scripts/_components/ScriptItem.tsx index d1e273e7..840246d9 100644 --- a/frontend/src/app/scripts/_components/ScriptItem.tsx +++ b/frontend/src/app/scripts/_components/ScriptItem.tsx @@ -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({
((e.currentTarget as HTMLImageElement).src = "/logo.png") } height={400} - alt={item.title} + alt={item.name} unoptimized />
-

{item.title}

+

{item.name} {getDisplayValueFromType(item.type)}

- Date added: {extractDate(item.created)} + Date added: {extractDate(item.date_created)}

@@ -76,7 +77,7 @@ function ScriptItem({

- How to {item.item_type ? "install" : "use"} + How to {item.type ? "install" : "use"}

diff --git a/frontend/src/app/scripts/_components/ScriptItems/Alerts.tsx b/frontend/src/app/scripts/_components/ScriptItems/Alerts.tsx index db54f16d..e4c56e16 100644 --- a/frontend/src/app/scripts/_components/ScriptItems/Alerts.tsx +++ b/frontend/src/app/scripts/_components/ScriptItems/Alerts.tsx @@ -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) => (

- {TextCopyBlock(alert.content)} + {TextCopyBlock(note.text)}

))} diff --git a/frontend/src/app/scripts/_components/ScriptItems/Buttons.tsx b/frontend/src/app/scripts/_components/ScriptItems/Buttons.tsx index 89bc2c0b..00a66782 100644 --- a/frontend/src/app/scripts/_components/ScriptItems/Buttons.tsx +++ b/frontend/src/app/scripts/_components/ScriptItems/Buttons.tsx @@ -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 (
{item.website && ( @@ -49,26 +34,16 @@ export default function Buttons({ item }: { item: Script }) { )} - {item.post_install && ( + { - )} - {item.installCommand && sourceUrl && ( - - )} + }
); } diff --git a/frontend/src/app/scripts/_components/ScriptItems/DefaultPassword.tsx b/frontend/src/app/scripts/_components/ScriptItems/DefaultPassword.tsx index 93c56156..55193d8f 100644 --- a/frontend/src/app/scripts/_components/ScriptItems/DefaultPassword.tsx +++ b/frontend/src/app/scripts/_components/ScriptItems/DefaultPassword.tsx @@ -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 (
@@ -17,7 +17,7 @@ export default function DefaultPassword({ item }: { item: Script }) {

You can use the following credentials to login to the {""} - {item.title} {item.item_type}. + {item.name} {item.type}.

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}
@@ -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}
diff --git a/frontend/src/app/scripts/_components/ScriptItems/DefaultSettings.tsx b/frontend/src/app/scripts/_components/ScriptItems/DefaultSettings.tsx index 05f3ae62..f1b2d8bb 100644 --- a/frontend/src/app/scripts/_components/ScriptItems/DefaultSettings.tsx +++ b/frontend/src/app/scripts/_components/ScriptItems/DefaultSettings.tsx @@ -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 && (

Default settings

- CPU: {item.default_cpu} + CPU: {defaultSettings?.resources.cpu}vCPU

- RAM: {item.default_ram} + RAM: {getDisplayValueFromRAM(defaultSettings?.resources.ram ?? 0)}

- HDD: {item.default_hdd} + HDD: {defaultSettings?.resources.hdd}GB

)} - {hasAlpineScript && ( + {defaultAlpineSettings && (

Default Alpine settings

- CPU: {item.expand.alpine_script.default_cpu} + CPU: {defaultAlpineSettings?.resources.cpu}vCPU

- RAM: {item.expand.alpine_script.default_ram} + RAM: {getDisplayValueFromRAM(defaultAlpineSettings?.resources.ram ?? 0)}

- HDD: {item.expand.alpine_script.default_hdd} + HDD: {defaultAlpineSettings?.resources.hdd}GB

)} diff --git a/frontend/src/app/scripts/_components/ScriptItems/InstallCommand.tsx b/frontend/src/app/scripts/_components/ScriptItems/InstallCommand.tsx index 31e1e302..795569b1 100644 --- a/frontend/src/app/scripts/_components/ScriptItems/InstallCommand.tsx +++ b/frontend/src/app/scripts/_components/ScriptItems/InstallCommand.tsx @@ -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) => ( <>

{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. )}

{isAlpine && (

- 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

)} @@ -36,7 +46,7 @@ export default function InstallCommand({ item }: { item: Script }) { return (
- {hasAlpineScript ? ( + {alpineScript ? ( Default @@ -44,25 +54,23 @@ export default function InstallCommand({ item }: { item: Script }) { {renderInstructions()} - {installCommand} + {getInstallCommand(defaultScript?.script)} - {expand.alpine_script && ( - <> - {renderInstructions(true)} - - {expand.alpine_script.installCommand} - - - )} + {renderInstructions(true)} + + {getInstallCommand(alpineScript.script)} + - ) : ( + ) : defaultScript?.script ? ( <> {renderInstructions()} - {installCommand && {installCommand}} + + {getInstallCommand(defaultScript.script)} + - )} + ) : null}
); } diff --git a/frontend/src/app/scripts/_components/ScriptItems/InterFaces.tsx b/frontend/src/app/scripts/_components/ScriptItems/InterFaces.tsx index e5bab0e8..2d2ae1f1 100644 --- a/frontend/src/app/scripts/_components/ScriptItems/InterFaces.tsx +++ b/frontend/src/app/scripts/_components/ScriptItems/InterFaces.tsx @@ -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 = ({ ); -export default function InterFaces({ item }: { item: Item }) { - const { interface: iface, port } = item; +export default function InterFaces({item} : {item : Script}) { return (
- {iface || (port && port !== 0) ? ( + {item.interface_port !== null ? (

- {iface ? "Interface:" : "Default Port:"} + {"Default Interface:"}

{" "}
) : null} diff --git a/frontend/src/app/scripts/_components/ScriptItems/Tooltips.tsx b/frontend/src/app/scripts/_components/ScriptItems/Tooltips.tsx index f0af794b..7e70b68e 100644 --- a/frontend/src/app/scripts/_components/ScriptItems/Tooltips.tsx +++ b/frontend/src/app/scripts/_components/ScriptItems/Tooltips.tsx @@ -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 && ( )}
diff --git a/frontend/src/app/scripts/_components/Sidebar.tsx b/frontend/src/app/scripts/_components/Sidebar.tsx index a993e118..f1618e17 100644 --- a/frontend/src/app/scripts/_components/Sidebar.tsx +++ b/frontend/src/app/scripts/_components/Sidebar.tsx @@ -18,7 +18,7 @@ const Sidebar = ({

Categories

{items.reduce( - (acc, category) => acc + category.expand.items.length, + (acc, category) => acc + category.scripts.length, 0, )}{" "} Total scripts diff --git a/frontend/src/app/scripts/page.tsx b/frontend/src/app/scripts/page.tsx index 2ccbc7fc..2e82a314 100644 --- a/frontend/src/app/scripts/page.tsx +++ b/frontend/src/app/scripts/page.tsx @@ -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 ( diff --git a/frontend/src/app/sitemap.ts b/frontend/src/app/sitemap.ts index b2484727..9377f25d 100644 --- a/frontend/src/app/sitemap.ts +++ b/frontend/src/app/sitemap.ts @@ -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 { + 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, }, ]; } diff --git a/frontend/src/components/CommandMenu.tsx b/frontend/src/components/CommandMenu.tsx index e5157e03..cc6d690f 100644 --- a/frontend/src/components/CommandMenu.tsx +++ b/frontend/src/components/CommandMenu.tsx @@ -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 VM; + case "ct": + return ( + LXC + ); + case "misc": + return MISC; + } + 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); }} > Search scripts... @@ -85,41 +79,40 @@ export default function CommandMenu() { - Search scripts - + Search scripts + - {isLoading ? "Loading..." : "No scripts found."} + + {isLoading ? "Loading..." : "No scripts found."} + {links.map((category) => ( - {category.expand.items.map((script) => ( + {category.scripts.map((script) => ( { setOpen(false); - router.push(`/scripts?id=${script.title}`); + router.push(`/scripts?id=${script.name}`); }} >

setOpen(false)}> ((e.currentTarget as HTMLImageElement).src = "/logo.png") } width={16} + height={16} alt="" className="h-5 w-5" /> - {script.title} - - {script.item_type} - + {script.name} + {formattedBadge(script.type)}
))} diff --git a/frontend/src/components/Footer.tsx b/frontend/src/components/Footer.tsx index d76a44a2..83aa05ce 100644 --- a/frontend/src/components/Footer.tsx +++ b/frontend/src/components/Footer.tsx @@ -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() {
Website build by the community. The source code is avaliable on{" "} { const handleScroll = () => { @@ -56,7 +53,6 @@ function Navbar() { /> Proxmox VE Helper-Scripts - {/* */}
@@ -81,28 +77,7 @@ function Navbar() { ))} - - - - - - - Theme Toggle - - - +
diff --git a/frontend/src/components/ui/codeblock.tsx b/frontend/src/components/ui/codeblock.tsx index f4251e53..16cc1bcc 100644 --- a/frontend/src/components/ui/codeblock.tsx +++ b/frontend/src/components/ui/codeblock.tsx @@ -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) => {
+ + + Theme Toggle + + + + ); +} diff --git a/frontend/src/config/siteConfig.tsx b/frontend/src/config/siteConfig.tsx index cd87e0e4..d32952c9 100644 --- a/frontend/src/config/siteConfig.tsx +++ b/frontend/src/config/siteConfig.tsx @@ -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: , 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: , text: "Change Log", }, { - href: "https://github.com/community-scripts/ProxmoxVE/discussions", + href: `https://github.com/community-scripts/${basePath}/discussions`, event: "Discussions", icon: , 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", +}; diff --git a/frontend/src/lib/data.ts b/frontend/src/lib/data.ts new file mode 100644 index 00000000..8c665e1a --- /dev/null +++ b/frontend/src/lib/data.ts @@ -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 => { + const response = await fetch("api/categories"); + const categories = await response.json(); + return sortCategories(categories); +}; diff --git a/frontend/src/lib/pocketbase.ts b/frontend/src/lib/pocketbase.ts deleted file mode 100644 index 86dcbace..00000000 --- a/frontend/src/lib/pocketbase.ts +++ /dev/null @@ -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}`; -}; diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index b65d5a0a..3ce26cd1 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -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[]; +} \ No newline at end of file diff --git a/frontend/src/styles/globals.css b/frontend/src/styles/globals.css index e9820c8a..d0ca4158 100644 --- a/frontend/src/styles/globals.css +++ b/frontend/src/styles/globals.css @@ -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;