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