Use static assets instead of fetching from github (#156)
Some checks are pending
Deploy Next.js site to Pages / build (push) Waiting to run
Deploy Next.js site to Pages / deploy (push) Blocked by required conditions

This commit is contained in:
Håvard Gjøby Thom 2024-11-09 20:06:54 +01:00 committed by GitHub
parent 2af11d145f
commit d199762427
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 136 additions and 107 deletions

1
frontend/public/json Symbolic link
View File

@ -0,0 +1 @@
../../json

View File

@ -1,26 +1,30 @@
import { basePath } from "@/config/siteConfig"; import { Metadata, Script } from "@/lib/types";
import { Category, Script } from "@/lib/types"; import { promises as fs } from "fs";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import path from "path";
export const dynamic = "force-static"; export const dynamic = "force-static";
const fetchCategories = async (): Promise<Category[]> => { const jsonDir = "public/json";
const response = await fetch( const metadataFileName = "metadata.json";
`https://raw.githubusercontent.com/community-scripts/${basePath}/refs/heads/main/json/metadata.json`, const encoding = "utf-8";
);
const data = await response.json(); const getMetadata = async () => {
return data.categories; const filePath = path.resolve(jsonDir, metadataFileName);
const fileContent = await fs.readFile(filePath, encoding);
const metadata: Metadata = JSON.parse(fileContent);
return metadata;
}; };
const fetchScripts = async (): Promise<Script[]> => { const getScripts = async () => {
const response = await fetch( const filePaths = (await fs.readdir(jsonDir))
`https://api.github.com/repos/community-scripts/${basePath}/contents/json`, .filter((fileName) => fileName !== metadataFileName)
); .map((fileName) => path.resolve(jsonDir, fileName));
const files: { download_url: string }[] = await response.json();
const scripts = await Promise.all( const scripts = await Promise.all(
files.map(async (file) : Promise<Script> => { filePaths.map(async (filePath) => {
const response = await fetch(file.download_url); const fileContent = await fs.readFile(filePath, encoding);
const script = await response.json(); const script: Script = JSON.parse(fileContent);
return script; return script;
}), }),
); );
@ -29,11 +33,18 @@ const fetchScripts = async (): Promise<Script[]> => {
export async function GET() { export async function GET() {
try { try {
const categories = await fetchCategories(); const metadata = await getMetadata();
const scripts = await fetchScripts(); const scripts = await getScripts();
for (const category of categories) {
category.scripts = scripts.filter((script) => script.categories.includes(category.id)); const categories = metadata.categories
} .map((category) => {
category.scripts = scripts.filter((script) =>
script.categories.includes(category.id),
);
return category;
})
.sort((a, b) => a.sort_order - b.sort_order);
return NextResponse.json(categories); return NextResponse.json(categories);
} catch (error) { } catch (error) {
console.error(error as Error); console.error(error as Error);

View File

@ -2,11 +2,11 @@ import Footer from "@/components/Footer";
import Navbar from "@/components/Navbar"; import Navbar from "@/components/Navbar";
import { ThemeProvider } from "@/components/theme-provider"; import { ThemeProvider } from "@/components/theme-provider";
import { Toaster } from "@/components/ui/sonner"; import { Toaster } from "@/components/ui/sonner";
import { analytics, basePath } from "@/config/siteConfig";
import "@/styles/globals.css"; import "@/styles/globals.css";
import { Inter } from "next/font/google"; import { Inter } from "next/font/google";
import React from "react";
import { NuqsAdapter } from "nuqs/adapters/next/app"; import { NuqsAdapter } from "nuqs/adapters/next/app";
import { analytics, basePath } from "@/config/siteConfig"; import React from "react";
const inter = Inter({ subsets: ["latin"] }); const inter = Inter({ subsets: ["latin"] });
@ -65,7 +65,6 @@ export default function RootLayout({
data-website-id={analytics.token} data-website-id={analytics.token}
></script> ></script>
<link rel="manifest" href="manifest.webmanifest" /> <link rel="manifest" href="manifest.webmanifest" />
<link rel="preconnect" href={process.env.NEXT_PUBLIC_POCKETBASE_URL} />
<link rel="preconnect" href="https://api.github.com" /> <link rel="preconnect" href="https://api.github.com" />
</head> </head>
<body className={inter.className}> <body className={inter.className}>

View File

@ -1,17 +1,24 @@
"use client"; "use client";
import AnimatedGradientText from "@/components/ui/animated-gradient-text"; import AnimatedGradientText from "@/components/ui/animated-gradient-text";
import Particles from "@/components/ui/particles";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { CardFooter } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import Particles from "@/components/ui/particles";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { basePath } from "@/config/siteConfig";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { ArrowRightIcon, ExternalLink } from "lucide-react"; import { ArrowRightIcon, ExternalLink } from "lucide-react";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import Link from "next/link"; import Link from "next/link";
import { useEffect, useState } from "react"; 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 { FaGithub } from "react-icons/fa";
import { basePath } from "@/config/siteConfig";
function CustomArrowRightIcon() { function CustomArrowRightIcon() {
return <ArrowRightIcon className="h-4 w-4" width={1} />; return <ArrowRightIcon className="h-4 w-4" width={1} />;

View File

@ -52,7 +52,9 @@ function ScriptItem({
<div className="ml-4 flex flex-col justify-between"> <div className="ml-4 flex flex-col justify-between">
<div className="flex h-full w-full flex-col justify-between"> <div className="flex h-full w-full flex-col justify-between">
<div> <div>
<h1 className="text-lg font-semibold">{item.name} {getDisplayValueFromType(item.type)}</h1> <h1 className="text-lg font-semibold">
{item.name} {getDisplayValueFromType(item.type)}
</h1>
<p className="w-full text-sm text-muted-foreground"> <p className="w-full text-sm text-muted-foreground">
Date added: {extractDate(item.date_created)} Date added: {extractDate(item.date_created)}
</p> </p>

View File

@ -1,10 +1,11 @@
import handleCopy from "@/components/handleCopy";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import handleCopy from "@/components/handleCopy";
import { Script } from "@/lib/types"; import { Script } from "@/lib/types";
export default function DefaultPassword({ item }: { item: Script }) { export default function DefaultPassword({ item }: { item: Script }) {
const hasDefaultLogin = item.default_credentials.username && item.default_credentials.password; const hasDefaultLogin =
item.default_credentials.username && item.default_credentials.password;
return ( return (
<div> <div>
@ -25,7 +26,10 @@ export default function DefaultPassword({ item }: { item: Script }) {
variant={"secondary"} variant={"secondary"}
size={"null"} size={"null"}
onClick={() => onClick={() =>
handleCopy("username", item.default_credentials.username ?? "") handleCopy(
"username",
item.default_credentials.username ?? "",
)
} }
> >
{item.default_credentials.username} {item.default_credentials.username}
@ -37,7 +41,10 @@ export default function DefaultPassword({ item }: { item: Script }) {
variant={"secondary"} variant={"secondary"}
size={"null"} size={"null"}
onClick={() => onClick={() =>
handleCopy("password", item.default_credentials.password ?? "") handleCopy(
"password",
item.default_credentials.password ?? "",
)
} }
> >
{item.default_credentials.password} {item.default_credentials.password}

View File

@ -44,7 +44,8 @@ export default function DefaultSettings({ item }: { item: Script }) {
CPU: {defaultAlpineSettings?.resources.cpu}vCPU CPU: {defaultAlpineSettings?.resources.cpu}vCPU
</p> </p>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
RAM: {getDisplayValueFromRAM(defaultAlpineSettings?.resources.ram ?? 0)} RAM:{" "}
{getDisplayValueFromRAM(defaultAlpineSettings?.resources.ram ?? 0)}
</p> </p>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
HDD: {defaultAlpineSettings?.resources.hdd}GB HDD: {defaultAlpineSettings?.resources.hdd}GB

View File

@ -6,7 +6,7 @@ import { getDisplayValueFromType } from "../ScriptInfoBlocks";
const getInstallCommand = (scriptPath?: string) => { const getInstallCommand = (scriptPath?: string) => {
return `bash -c "$(wget -qLO - https://github.com/community-scripts/${basePath}/raw/main/${scriptPath})"`; return `bash -c "$(wget -qLO - https://github.com/community-scripts/${basePath}/raw/main/${scriptPath})"`;
} };
export default function InstallCommand({ item }: { item: Script }) { export default function InstallCommand({ item }: { item: Script }) {
const alpineScript = item.install_methods.find( const alpineScript = item.install_methods.find(
@ -14,7 +14,7 @@ export default function InstallCommand({ item }: { item: Script }) {
); );
const defaultScript = item.install_methods.find( const defaultScript = item.install_methods.find(
(method) => method.type === "default" (method) => method.type === "default",
); );
const renderInstructions = (isAlpine = false) => ( const renderInstructions = (isAlpine = false) => (
@ -60,7 +60,9 @@ export default function InstallCommand({ item }: { item: Script }) {
</TabsList> </TabsList>
<TabsContent value="default"> <TabsContent value="default">
{renderInstructions()} {renderInstructions()}
<CodeCopyButton>{getInstallCommand(defaultScript?.script)}</CodeCopyButton> <CodeCopyButton>
{getInstallCommand(defaultScript?.script)}
</CodeCopyButton>
</TabsContent> </TabsContent>
<TabsContent value="alpine"> <TabsContent value="alpine">
{renderInstructions(true)} {renderInstructions(true)}

View File

@ -1,8 +1,8 @@
import { Button, buttonVariants } from "@/components/ui/button";
import handleCopy from "@/components/handleCopy"; import handleCopy from "@/components/handleCopy";
import { buttonVariants } from "@/components/ui/button";
import { Script } from "@/lib/types";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { ClipboardIcon } from "lucide-react"; import { ClipboardIcon } from "lucide-react";
import { Script } from "@/lib/types";
const CopyButton = ({ const CopyButton = ({
label, label,
@ -11,7 +11,12 @@ const CopyButton = ({
label: string; label: string;
value: string | number; value: string | number;
}) => ( }) => (
<span className={cn(buttonVariants({size: "sm", variant: "secondary"}), "flex items-center gap-2")}> <span
className={cn(
buttonVariants({ size: "sm", variant: "secondary" }),
"flex items-center gap-2",
)}
>
{value} {value}
<ClipboardIcon <ClipboardIcon
onClick={() => handleCopy(label, String(value))} onClick={() => handleCopy(label, String(value))}
@ -20,8 +25,7 @@ const CopyButton = ({
</span> </span>
); );
export default function InterFaces({item} : {item : Script}) { export default function InterFaces({ item }: { item: Script }) {
return ( return (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
{item.interface_port !== null ? ( {item.interface_port !== null ? (
@ -29,10 +33,7 @@ export default function InterFaces({item} : {item : Script}) {
<h2 className="mr-2 text-end text-lg font-semibold"> <h2 className="mr-2 text-end text-lg font-semibold">
{"Default Interface:"} {"Default Interface:"}
</h2>{" "} </h2>{" "}
<CopyButton <CopyButton label="default interface" value={item.interface_port} />
label="default interface"
value={item.interface_port}
/>
</div> </div>
) : null} ) : null}
</div> </div>

View File

@ -17,15 +17,16 @@ const Sidebar = ({
<div className="flex items-end justify-between pb-4"> <div className="flex items-end justify-between pb-4">
<h1 className="text-xl font-bold">Categories</h1> <h1 className="text-xl font-bold">Categories</h1>
<p className="text-xs italic text-muted-foreground"> <p className="text-xs italic text-muted-foreground">
{items.reduce( {items.reduce((acc, category) => acc + category.scripts.length, 0)}{" "}
(acc, category) => acc + category.scripts.length,
0,
)}{" "}
Total scripts Total scripts
</p> </p>
</div> </div>
<div className="rounded-lg"> <div className="rounded-lg">
<ScriptAccordion items={items} selectedScript={selectedScript} setSelectedScript={setSelectedScript} /> <ScriptAccordion
items={items}
selectedScript={selectedScript}
setSelectedScript={setSelectedScript}
/>
</div> </div>
</div> </div>
); );

View File

@ -1,5 +1,6 @@
"use client"; "use client";
import { basePath } from "@/config/siteConfig";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { cva, type VariantProps } from "class-variance-authority"; import { cva, type VariantProps } from "class-variance-authority";
import { Clipboard, Copy } from "lucide-react"; import { Clipboard, Copy } from "lucide-react";
@ -8,7 +9,6 @@ import * as React from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { Button } from "./button"; import { Button } from "./button";
import { Separator } from "./separator"; import { Separator } from "./separator";
import { basePath } from "@/config/siteConfig";
const buttonVariants = cva( 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", "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",
@ -135,4 +135,4 @@ const CodeBlock = React.forwardRef<HTMLDivElement, CodeBlockProps>(
); );
CodeBlock.displayName = "CodeBlock"; CodeBlock.displayName = "CodeBlock";
export { CodeBlock, buttonVariants }; export { buttonVariants, CodeBlock };

View File

@ -123,6 +123,6 @@ export {
NavigationMenuLink, NavigationMenuLink,
NavigationMenuList, NavigationMenuList,
NavigationMenuTrigger, NavigationMenuTrigger,
NavigationMenuViewport,
navigationMenuTriggerStyle, navigationMenuTriggerStyle,
NavigationMenuViewport,
}; };

View File

@ -1,10 +1,10 @@
import { basePath } from "@/config/siteConfig";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import Link from "next/link"; import Link from "next/link";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { FaGithub, FaStar } from "react-icons/fa"; import { FaGithub, FaStar } from "react-icons/fa";
import NumberTicker from "./number-ticker";
import { buttonVariants } from "./button"; import { buttonVariants } from "./button";
import { basePath } from "@/config/siteConfig"; import NumberTicker from "./number-ticker";
export default function StarOnGithubButton() { export default function StarOnGithubButton() {
const [stars, setStars] = useState(0); const [stars, setStars] = useState(0);
@ -12,9 +12,12 @@ export default function StarOnGithubButton() {
useEffect(() => { useEffect(() => {
const fetchStars = async () => { const fetchStars = async () => {
try { try {
const res = await fetch(`https://api.github.com/repos/community-scripts/${basePath}`, { const res = await fetch(
next: { revalidate: 60 * 60 * 24 }, `https://api.github.com/repos/community-scripts/${basePath}`,
}); {
next: { revalidate: 60 * 60 * 24 },
},
);
if (res.ok) { if (res.ok) {
const data = await res.json(); const data = await res.json();

View File

@ -1,9 +1,14 @@
"use client"; "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"; import { MoonIcon, SunIcon } from "@radix-ui/react-icons";
import { useTheme } from "next-themes";
import { Button } from "./button";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "./tooltip";
export function ThemeToggle() { export function ThemeToggle() {
const { setTheme, theme: currentTheme } = useTheme(); const { setTheme, theme: currentTheme } = useTheme();

View File

@ -1,7 +1,7 @@
import { MessagesSquare, Scroll } from "lucide-react"; import { MessagesSquare, Scroll } from "lucide-react";
import { FaGithub } from "react-icons/fa"; import { FaGithub } from "react-icons/fa";
export const basePath = process.env.BASE_PATH; export const basePath = process.env.BASE_PATH;
export const navbarLinks = [ export const navbarLinks = [
{ {
@ -17,7 +17,7 @@ export const navbarLinks = [
text: "Change Log", text: "Change Log",
}, },
{ {
href: `https://github.com/community-scripts/${basePath}/discussions`, href: `https://github.com/community-scripts/${basePath}/discussions`,
event: "Discussions", event: "Discussions",
icon: <MessagesSquare className="h-4 w-4" />, icon: <MessagesSquare className="h-4 w-4" />,
text: "Discussions", text: "Discussions",

View File

@ -1,23 +1,10 @@
import { Category } from "./types"; import { Category } from "./types";
const sortCategories = (categories: Category[]) => { export const fetchCategories = async () => {
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 response = await fetch("api/categories");
const categories = await response.json(); if (!response.ok) {
return sortCategories(categories); throw new Error(`Failed to fetch categories: ${response.statusText}`);
}
const categories: Category[] = await response.json();
return categories;
}; };

View File

@ -26,19 +26,21 @@ export type Script = {
username: string | null; username: string | null;
password: string | null; password: string | null;
}; };
notes: [{ notes: [
text: string; {
type: string; text: string;
}] type: string;
} },
];
};
export type Category = { export type Category = {
name: string; name: string;
id: number; id: number;
sort_order: number; sort_order: number;
scripts: Script[]; scripts: Script[];
} };
export type ScriptList = { export type Metadata = {
categories: Category[]; categories: Category[];
} };

View File

@ -1,23 +1,23 @@
{ {
"categories": "categories":
[ [
{"name": "Miscellaneous", "id": 0, "sort_order": 99.0},
{"name": "Proxmox VE Tools", "id": 1, "sort_order": 1.0}, {"name": "Proxmox VE Tools", "id": 1, "sort_order": 1.0},
{"name": "Home Assistant", "id": 2, "sort_order": 2.0}, {"name": "AdBlocker - DNS", "id": 13, "sort_order": 2.0},
{"name": "Automation", "id": 3, "sort_order": 3.0}, {"name": "Automation", "id": 3, "sort_order": 3.0},
{"name": "MQTT", "id": 4, "sort_order": 4.0}, {"name": "Dashboards", "id": 15, "sort_order": 4.0},
{"name": "Database", "id": 5, "sort_order": 5.0}, {"name": "Database", "id": 5, "sort_order": 5.0},
{"name": "Zigbee - Zwave", "id": 6, "sort_order": 6.0}, {"name": "Docker - Kubernetes", "id": 8, "sort_order": 6.0},
{"name": "Monitoring - Analytics", "id": 7, "sort_order": 7.0}, {"name": "Document - Notes", "id": 14, "sort_order": 7.0},
{"name": "Docker - Kubernetes", "id": 8, "sort_order": 8.0}, {"name": "File - Code", "id": 16, "sort_order": 8.0},
{"name": "Operating System", "id": 9, "sort_order": 9.0}, {"name": "Home Assistant", "id": 2, "sort_order": 9.0},
{"name": "TurnKey", "id": 10, "sort_order": 10.0}, {"name": "Media - Photo", "id": 12, "sort_order": 10.0},
{"name": "Server - Networking", "id": 11, "sort_order": 11.0}, {"name": "Monitoring - Analytics", "id": 7, "sort_order": 11.0},
{"name": "Media - Photo", "id": 12, "sort_order": 12.0}, {"name": "MQTT", "id": 4, "sort_order": 12.0},
{"name": "AdBlocker - DNS", "id": 13, "sort_order": 13.0}, {"name": "NVR - DVR", "id": 17, "sort_order": 13.0},
{"name": "Document - Notes", "id": 14, "sort_order": 14.0}, {"name": "Operating System", "id": 9, "sort_order": 14.0},
{"name": "Dashboards", "id": 15, "sort_order": 15.0}, {"name": "Server - Networking", "id": 11, "sort_order": 15.0},
{"name": "File - Code", "id": 16, "sort_order": 16.0}, {"name": "TurnKey", "id": 10, "sort_order": 16.0},
{"name": "NVR - DVR", "id": 17, "sort_order": 17.0} {"name": "Zigbee - Zwave", "id": 6, "sort_order": 17.0},
{"name": "Miscellaneous", "id": 0, "sort_order": 99.0}
] ]
} }