mirror of
https://github.com/community-scripts/Proxmox.git
synced 2026-04-05 09:04:01 -04:00
refactor: refactor the code base to use more server components
This commit is contained in:
@@ -27,6 +27,7 @@
|
||||
"@radix-ui/react-tabs": "^1.1.0",
|
||||
"@radix-ui/react-tooltip": "^1.1.2",
|
||||
"@vercel/analytics": "^1.2.2",
|
||||
"@vercel/og": "^0.6.2",
|
||||
"@vercel/speed-insights": "^1.0.12",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -41,6 +42,7 @@
|
||||
"react": "^18",
|
||||
"react-code-blocks": "^0.1.6",
|
||||
"react-dom": "^18",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-icons": "^5.1.0",
|
||||
"react-simple-typewriter": "^5.0.1",
|
||||
"sharp": "^0.33.4",
|
||||
|
||||
51
src/app/api/og/route.tsx
Normal file
51
src/app/api/og/route.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { ImageResponse } from "next/og";
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
export const config = {
|
||||
runtime: "edge",
|
||||
}
|
||||
|
||||
export default function handler(req: NextRequest) {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const title = searchParams.get("title");
|
||||
const logo = searchParams.get("logo");
|
||||
|
||||
if (!title || !logo) {
|
||||
return new Response("Invalid request", { status: 400 });
|
||||
}
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
className="bg-background h-full w-full flex flex-col justify-center items-center"
|
||||
>
|
||||
<img
|
||||
src={logo}
|
||||
alt={title}
|
||||
style={{
|
||||
maxWidth: "100%",
|
||||
maxHeight: "100%",
|
||||
objectFit: "contain",
|
||||
}}
|
||||
/>
|
||||
<p
|
||||
style={{
|
||||
color: "white",
|
||||
fontSize: "24px",
|
||||
fontWeight: "bold",
|
||||
position: "absolute",
|
||||
bottom: "20px",
|
||||
left: "20px",
|
||||
textShadow: "2px 2px 2px black",
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -6,9 +6,10 @@ export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const response = await pb.collection("categories").getFullList<Category>({
|
||||
expand: "items.alerts,items.alpine_script,items.default_login",
|
||||
sort: "order",
|
||||
const response = await pb.collection("categories").getFullList<Category>({
|
||||
expand: "items",
|
||||
fields:
|
||||
"catagoryName, expand.items.title, expand.items.logo, expand.items.item_type, expand.items.isMostViewed, id",
|
||||
});
|
||||
|
||||
return NextResponse.json(response);
|
||||
26
src/app/api/scripts/latest/route.ts
Normal file
26
src/app/api/scripts/latest/route.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { pb } from "@/lib/pocketbase";
|
||||
import { Script } from "@/lib/types";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const page = parseInt(req.nextUrl.searchParams.get("page") || "1", 10);
|
||||
|
||||
try {
|
||||
const response = await pb
|
||||
.collection("proxmox_scripts")
|
||||
.getList<Script>(page, 3, {
|
||||
sort: "-created",
|
||||
fields: "title, logo, description, created, id",
|
||||
});
|
||||
|
||||
return NextResponse.json(response);
|
||||
} catch (error) {
|
||||
console.error("Error fetching categories:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch script" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
24
src/app/api/scripts/mostviewed/route.ts
Normal file
24
src/app/api/scripts/mostviewed/route.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { pb } from "@/lib/pocketbase";
|
||||
import { Script } from "@/lib/types";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const response = await pb
|
||||
.collection("proxmox_scripts")
|
||||
.getList<Script>(1, 3, {
|
||||
filter: `isMostViewed = true`,
|
||||
fields: "title, logo, description, created, id"
|
||||
})
|
||||
|
||||
return NextResponse.json(response);
|
||||
} catch (error) {
|
||||
console.error("Error fetching categories:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch script" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
26
src/app/api/scripts/script/route.ts
Normal file
26
src/app/api/scripts/script/route.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { pb } from "@/lib/pocketbase";
|
||||
import { Script } from "@/lib/types";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const title = searchParams.get("title");
|
||||
|
||||
try {
|
||||
const response = await pb
|
||||
.collection("proxmox_scripts")
|
||||
.getFirstListItem<Script>(`title="${title}"`, {
|
||||
expand: "alerts,alpine_script,default_login",
|
||||
});
|
||||
|
||||
return NextResponse.json(response);
|
||||
} catch (error) {
|
||||
console.error("Error fetching categories:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch script" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
115
src/app/scripts/[id]/page.tsx
Normal file
115
src/app/scripts/[id]/page.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import Image from "next/image";
|
||||
import { extractDate } from "@/lib/time";
|
||||
import CloseButton from "@/components/closeButton";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import DefaultSettings from "../_components/ScriptItems/DefaultSettings";
|
||||
import InterFaces from "../_components/ScriptItems/InterFaces";
|
||||
import Buttons from "../_components/ScriptItems/Buttons";
|
||||
import Description from "../_components/ScriptItems/Description";
|
||||
import Alerts from "../_components/ScriptItems/Alerts";
|
||||
import Tooltips from "../_components/ScriptItems/Tooltips";
|
||||
import InstallCommand from "../_components/ScriptItems/InstallCommand";
|
||||
import DefaultPassword from "../_components/ScriptItems/DefaultPassword";
|
||||
import type { Metadata, ResolvingMetadata } from "next";
|
||||
|
||||
type Props = {
|
||||
params: { id: string };
|
||||
searchParams: { [key: string]: string | string[] | undefined };
|
||||
};
|
||||
|
||||
export async function generateMetadata({ params, searchParams }: Props,
|
||||
parent: ResolvingMetadata
|
||||
): Promise<Metadata> {
|
||||
const id = params.id;
|
||||
|
||||
const data = await fetch(`http://localhost:3000/api/scripts/script?title=${id}`).then((res) => res.json());
|
||||
const imgURL = `http://localhost:3000/api/og?title=${data.title}?logo=${data.logo}`;
|
||||
|
||||
return {
|
||||
title: `${data.title} | Proxmox VE Helper-Scripts`,
|
||||
description: `This script will fully install and configure ${data.title} on your Proxmox VE host.`,
|
||||
openGraph: {
|
||||
title: `${data.title} | Proxmox VE Helper-Scripts`,
|
||||
description: `This script will fully install and configure ${data.title} on your Proxmox VE host.`,
|
||||
images: [
|
||||
{
|
||||
url: imgURL,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: data.title,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function Page({ params }: { params: { id: string } }) {
|
||||
const data = await fetch(
|
||||
`http://localhost:3000/api/scripts/script?title=${params.id}`,
|
||||
).then((res) => res.json());
|
||||
return (
|
||||
<div>
|
||||
{data && (
|
||||
<div className="mr-7 mt-0 flex w-full min-w-fit">
|
||||
<div className="flex w-full min-w-fit">
|
||||
<div className="flex w-full flex-col">
|
||||
<div className="datas-center flex h-[36px] min-w-max justify-between">
|
||||
<h2 className="text-lg font-semibold">Selected Script</h2>
|
||||
<CloseButton />
|
||||
</div>
|
||||
<div className="rounded-lg border bg-accent/20 p-4">
|
||||
<div className="flex justify-between">
|
||||
<div className="flex">
|
||||
<Image
|
||||
className="h-32 w-32 rounded-lg bg-accent/60 object-contain p-3 shadow-md"
|
||||
src={data.logo}
|
||||
width={400}
|
||||
height={400}
|
||||
alt={data.title}
|
||||
priority
|
||||
/>
|
||||
<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">{data.title}</h1>
|
||||
<p className="w-full text-sm text-muted-foreground">
|
||||
Date added: {extractDate(data.created)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-5">
|
||||
<DefaultSettings item={data} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden flex-col justify-between gap-2 sm:flex">
|
||||
<InterFaces item={data} />
|
||||
<Buttons item={data} />
|
||||
</div>
|
||||
</div>
|
||||
<Separator className="mt-4" />
|
||||
<div>
|
||||
<div className="mt-4">
|
||||
<Description item={data} />
|
||||
<Alerts item={data} />
|
||||
</div>
|
||||
<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 {data.data_type ? "install" : "use"}
|
||||
</h2>
|
||||
<Tooltips item={data} />
|
||||
</div>
|
||||
<Separator className="w-full"></Separator>
|
||||
<InstallCommand item={data} />
|
||||
</div>
|
||||
</div>
|
||||
<DefaultPassword item={data} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
"use client";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -6,8 +7,8 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Category } from "@/lib/types";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Category, Script } from "@/lib/types";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { extractDate } from "@/lib/time";
|
||||
@@ -15,16 +16,23 @@ import { Button } from "@/components/ui/button";
|
||||
|
||||
const ITEMS_PER_PAGE = 3;
|
||||
|
||||
export function LatestScripts({ items }: { items: Category[] }) {
|
||||
export function LatestScripts() {
|
||||
const [page, setPage] = useState(1);
|
||||
const [latestScripts, setLatestScripts] = useState<Script[]>([]);
|
||||
|
||||
const latestScripts = useMemo(() => {
|
||||
if (!items) return [];
|
||||
const scripts = items.flatMap((category) => category.expand.items || []);
|
||||
return scripts.sort(
|
||||
(a, b) => new Date(b.created).getTime() - new Date(a.created).getTime(),
|
||||
);
|
||||
}, [items]);
|
||||
useEffect(() => {
|
||||
fetch(`/api/scripts/latest?page=${page}`)
|
||||
.then((res) => res.json())
|
||||
.then((data: any) => {
|
||||
if (Array.isArray(data.items)) {
|
||||
setLatestScripts(data.items as Script[]);
|
||||
} else {
|
||||
console.error("Unexpected data format: ", data);
|
||||
setLatestScripts([]);
|
||||
}
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
}, []);
|
||||
|
||||
const goToNextPage = () => {
|
||||
setPage((prevPage) => prevPage + 1);
|
||||
@@ -37,10 +45,6 @@ export function LatestScripts({ items }: { items: Category[] }) {
|
||||
const startIndex = (page - 1) * ITEMS_PER_PAGE;
|
||||
const endIndex = page * ITEMS_PER_PAGE;
|
||||
|
||||
if (!items) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
{latestScripts.length > 0 && (
|
||||
@@ -67,7 +71,7 @@ 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.map((item) => (
|
||||
<Card
|
||||
key={item.id}
|
||||
className="min-w-[250px] flex-1 flex-grow bg-accent/30"
|
||||
@@ -99,10 +103,7 @@ export function LatestScripts({ items }: { items: Category[] }) {
|
||||
<CardFooter className="">
|
||||
<Button asChild variant="secondary">
|
||||
<Link
|
||||
href={{
|
||||
pathname: "/scripts",
|
||||
query: { id: item.title },
|
||||
}}
|
||||
href={`/scripts/${item.title}`}
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
View Script
|
||||
@@ -116,40 +117,32 @@ export function LatestScripts({ items }: { items: Category[] }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function MostViewedScripts({ items }: { items: Category[] }) {
|
||||
const [page, setPage] = useState(1);
|
||||
export function MostViewedScripts() {
|
||||
const [scripts, setScripts] = useState<Script[]>([]);
|
||||
|
||||
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;
|
||||
useEffect(() => {
|
||||
fetch("/api/scripts/mostviewed")
|
||||
.then((res) => res.json())
|
||||
.then((data: any) => {
|
||||
if (Array.isArray(data.items)) {
|
||||
setScripts(data.items as Script[]);
|
||||
} else {
|
||||
console.error("Unexpected data format: ", data);
|
||||
setScripts([]);
|
||||
}
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
{mostViewedScripts.length > 0 && (
|
||||
{scripts.length > 0 && (
|
||||
<>
|
||||
<h2 className="text-lg font-semibold">Most Viewed Scripts</h2>
|
||||
</>
|
||||
)}
|
||||
<div className="min-w flex w-full flex-row flex-wrap gap-4">
|
||||
{mostViewedScripts.slice(startIndex, endIndex).map((item) => (
|
||||
{scripts.map((item: Script) => (
|
||||
<Card
|
||||
key={item.id}
|
||||
className="min-w-[250px] flex-1 flex-grow bg-accent/30"
|
||||
@@ -181,10 +174,7 @@ export function MostViewedScripts({ items }: { items: Category[] }) {
|
||||
<CardFooter className="">
|
||||
<Button asChild variant="secondary">
|
||||
<Link
|
||||
href={{
|
||||
pathname: "/scripts",
|
||||
query: { id: item.title },
|
||||
}}
|
||||
href={`/scripts/${item.title}`}
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
View Script
|
||||
@@ -194,18 +184,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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
"use client";
|
||||
import Image from "next/image";
|
||||
import { useEffect, useState } from "react";
|
||||
import { extractDate } from "@/lib/time";
|
||||
import { X } from "lucide-react";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Category, Script } from "@/lib/types";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { MostViewedScripts, LatestScripts } from "./ScriptInfoBlocks";
|
||||
|
||||
import DefaultPassword from "./ScriptItems/DefaultPassword";
|
||||
import InstallCommand from "./ScriptItems/InstallCommand";
|
||||
import Tooltips from "./ScriptItems/Tooltips";
|
||||
import Alerts from "./ScriptItems/Alerts";
|
||||
import Description from "./ScriptItems/Description";
|
||||
import Buttons from "./ScriptItems/Buttons";
|
||||
import DefaultSettings from "./ScriptItems/DefaultSettings";
|
||||
import InterFaces from "./ScriptItems/InterFaces";
|
||||
|
||||
function ScriptItem({
|
||||
items,
|
||||
selectedScript,
|
||||
setSelectedScript,
|
||||
}: {
|
||||
items: Category[];
|
||||
selectedScript: string | null;
|
||||
setSelectedScript: (script: string | null) => void;
|
||||
}) {
|
||||
const [item, setItem] = useState<Script | null>(null);
|
||||
const searchParams = useSearchParams();
|
||||
const id = searchParams.get("id");
|
||||
|
||||
useEffect(() => {
|
||||
if (items) {
|
||||
const script = items
|
||||
.map((category) => category.expand.items)
|
||||
.flat()
|
||||
.find((script) => script.title === id);
|
||||
setItem(script || null);
|
||||
|
||||
if (script && !selectedScript) {
|
||||
setSelectedScript(script.title);
|
||||
}
|
||||
}
|
||||
}, [id, items, setSelectedScript, selectedScript]);
|
||||
|
||||
const closeScript = () => {
|
||||
window.history.pushState({}, document.title, window.location.pathname);
|
||||
setSelectedScript(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{item && (
|
||||
<div className="mr-7 mt-0 flex w-full min-w-fit">
|
||||
<div className="flex w-full min-w-fit">
|
||||
<div className="flex w-full flex-col">
|
||||
<div className="flex h-[36px] min-w-max items-center justify-between">
|
||||
<h2 className="text-lg font-semibold">Selected Script</h2>
|
||||
<X onClick={closeScript} className="cursor-pointer" />
|
||||
</div>
|
||||
<div className="rounded-lg border bg-accent/20 p-4">
|
||||
<div className="flex justify-between">
|
||||
<div className="flex">
|
||||
<Image
|
||||
className="h-32 w-32 rounded-lg bg-accent/60 object-contain p-3 shadow-md"
|
||||
src={item.logo}
|
||||
width={400}
|
||||
height={400}
|
||||
alt={item.title}
|
||||
priority
|
||||
/>
|
||||
<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>
|
||||
<p className="w-full text-sm text-muted-foreground">
|
||||
Date added: {extractDate(item.created)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-5">
|
||||
<DefaultSettings item={item} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden flex-col justify-between gap-2 sm:flex">
|
||||
<InterFaces item={item} />
|
||||
<Buttons item={item} />
|
||||
</div>
|
||||
</div>
|
||||
<Separator className="mt-4" />
|
||||
<div>
|
||||
<div className="mt-4">
|
||||
<Description item={item} />
|
||||
<Alerts item={item} />
|
||||
</div>
|
||||
<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"}
|
||||
</h2>
|
||||
<Tooltips item={item} />
|
||||
</div>
|
||||
<Separator className="w-full"></Separator>
|
||||
<InstallCommand item={item} />
|
||||
</div>
|
||||
</div>
|
||||
<DefaultPassword item={item} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{id ? null : (
|
||||
<div className="flex w-full flex-col gap-5">
|
||||
<LatestScripts items={items} />
|
||||
<MostViewedScripts items={items} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ScriptItem;
|
||||
@@ -1,3 +1,4 @@
|
||||
'use client'
|
||||
import TextCopyBlock from "@/lib/TextCopyBlock";
|
||||
import { Script } from "@/lib/types";
|
||||
import { Info } from "lucide-react";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
'use client';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import handleCopy from '@/lib/handleCopy';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
'use client';
|
||||
import TextCopyBlock from '@/lib/TextCopyBlock';
|
||||
import { Script } from '@/lib/types';
|
||||
import React from 'react'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
'use client';
|
||||
import CodeCopyButton from '@/components/ui/code-copy-button';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Script } from '@/lib/types';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
'use client';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import handleCopy from '@/lib/handleCopy';
|
||||
import React from 'react'
|
||||
|
||||
@@ -11,40 +11,51 @@ import { EyeOff, Eye, Star } from "lucide-react";
|
||||
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { Category } from "@/lib/types";
|
||||
import { Badge } from "../../../components/ui/badge";
|
||||
import { Button } from "../../../components/ui/button";
|
||||
import classNames from "clsx";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
const ScriptBrowser = ({
|
||||
items,
|
||||
selectedScript,
|
||||
setSelectedScript,
|
||||
}: {
|
||||
items: Category[];
|
||||
selectedScript: string | null;
|
||||
setSelectedScript: (script: string | null) => void;
|
||||
}) => {
|
||||
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 ScriptBrowser = () => {
|
||||
const [links, setLinks] = useState<Category[]>([]);
|
||||
const [expandedItem, setExpandedItem] = useState<string | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const linkRefs = useRef<{ [key: string]: HTMLAnchorElement | null }>({});
|
||||
const searchParams = useSearchParams();
|
||||
const pathname = usePathname();
|
||||
const [selectedScript, setSelectedScript] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (items) {
|
||||
setLinks(items);
|
||||
}
|
||||
}, [items]);
|
||||
fetch("/api/scripts/categories")
|
||||
.then((res) => res.json())
|
||||
.then((data: any) => setLinks(data as Category[]))
|
||||
.catch((err) => console.error(err));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const id = searchParams.get("id");
|
||||
if (id) {
|
||||
setSelectedScript(id);
|
||||
const scriptTitle = pathname.split("/scripts/")[1];
|
||||
if (scriptTitle) {
|
||||
setSelectedScript(decodeURIComponent(scriptTitle));
|
||||
} else {
|
||||
setSelectedScript(null);
|
||||
}
|
||||
}, [searchParams, setSelectedScript]);
|
||||
}, [pathname]);
|
||||
|
||||
const handleSelected = useCallback(
|
||||
(title: string) => {
|
||||
@@ -119,10 +130,7 @@ const ScriptBrowser = ({
|
||||
{category.expand.items.map((script, index) => (
|
||||
<p key={index}>
|
||||
<Link
|
||||
href={{
|
||||
pathname: "/scripts",
|
||||
query: { id: script.title },
|
||||
}}
|
||||
href={`/scripts/${script.title}`}
|
||||
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
|
||||
? "rounded-lg bg-accent font-semibold dark:bg-accent/30 dark:text-white"
|
||||
16
src/app/scripts/layout.tsx
Normal file
16
src/app/scripts/layout.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import Sidebar from "./_components/Sidebar";
|
||||
|
||||
export default async function ScriptLayout({children}: { children: React.ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-3">
|
||||
<div className="mt-20 flex sm:px-4 xl:px-0">
|
||||
<div className="hidden sm:flex">
|
||||
<Sidebar />
|
||||
</div>
|
||||
<div className="mx-7 w-full sm:mx-0 sm:ml-7">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,85 +1,28 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import ScriptItem from "@/app/scripts/_components/ScriptItem";
|
||||
import ScriptBrowser from "@/app/scripts/_components/ScriptBrowser";
|
||||
import { Category } from "@/lib/types";
|
||||
|
||||
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 default function Page() {
|
||||
const [links, setLinks] = useState<Category[]>([]);
|
||||
const [selectedScript, setSelectedScript] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCategories = async (): Promise<void> => {
|
||||
try {
|
||||
const response = await fetch("/api/categories");
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch categories");
|
||||
}
|
||||
const categories: Category[] = await response.json();
|
||||
if (categories.length === 0) {
|
||||
throw new Error("Empty response");
|
||||
}
|
||||
const sortedCategories = sortCategories(categories);
|
||||
setLinks(sortedCategories);
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchCategories();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex h-screen w-full flex-col items-center justify-center gap-5 bg-background px-4 md:px-6">
|
||||
<div className="space-y-2 text-center">
|
||||
<Loader2 className="h-10 w-10 animate-spin" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import React, { Suspense } from 'react'
|
||||
import { LatestScripts, MostViewedScripts } from './_components/ScriptInfoBlocks';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
|
||||
export default function page() {
|
||||
return (
|
||||
<div className="mb-3">
|
||||
<div className="mt-20 flex sm:px-4 xl:px-0">
|
||||
<div className="hidden sm:flex">
|
||||
<ScriptBrowser
|
||||
items={links}
|
||||
selectedScript={selectedScript}
|
||||
setSelectedScript={setSelectedScript}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx-7 w-full sm:mx-0 sm:ml-7">
|
||||
<ScriptItem
|
||||
items={links}
|
||||
selectedScript={selectedScript}
|
||||
setSelectedScript={setSelectedScript}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full flex-col gap-5">
|
||||
<Suspense
|
||||
fallback={
|
||||
<div>
|
||||
<Loader2 className="h-10 w-10 animate-spin" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<LatestScripts />
|
||||
</Suspense>
|
||||
<Suspense
|
||||
fallback={
|
||||
<div>
|
||||
<Loader2 className="h-10 w-10 animate-spin" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<MostViewedScripts />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import React from "react";
|
||||
import { Button } from "./ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -12,7 +14,7 @@ import {
|
||||
CommandSeparator,
|
||||
CommandShortcut,
|
||||
} from "@/components/ui/command";
|
||||
import { Category } from "@/lib/types";
|
||||
import { Category, Script } from "@/lib/types";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
@@ -37,6 +39,7 @@ const sortCategories = (categories: Category[]): Category[] => {
|
||||
export default function CommandMenu() {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [links, setLinks] = React.useState<Category[]>([]);
|
||||
const [mostViewed, setMostViewed] = React.useState<Script[]>([]);
|
||||
const router = useRouter();
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -54,7 +57,7 @@ export default function CommandMenu() {
|
||||
React.useEffect(() => {
|
||||
const fetchCategories = async (): Promise<void> => {
|
||||
try {
|
||||
const response = await fetch("/api/categories");
|
||||
const response = await fetch("/api/scripts/categories");
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch categories");
|
||||
}
|
||||
@@ -101,7 +104,7 @@ export default function CommandMenu() {
|
||||
value={script.title}
|
||||
onSelect={() => {
|
||||
setOpen(false);
|
||||
router.push(`/scripts?id=${script.title}`);
|
||||
router.push(`/scripts/${script.title}`);
|
||||
}}
|
||||
>
|
||||
<div className="flex gap-2" onClick={() => setOpen(false)}>
|
||||
|
||||
@@ -25,7 +25,7 @@ export default function MobileNav() {
|
||||
const [links, setLinks] = useState<Category[]>([]);
|
||||
|
||||
const fetchLinks = async () => {
|
||||
const res = await fetch("/api/categories", {
|
||||
const res = await fetch("/api/scripts/categories", {
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error("Failed to fetch categories");
|
||||
|
||||
14
src/components/closeButton.tsx
Normal file
14
src/components/closeButton.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
"use client";
|
||||
import { X } from "lucide-react";
|
||||
import React from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function CloseButton() {
|
||||
const router = useRouter();
|
||||
|
||||
const closeScript = () => {
|
||||
router.push("/scripts");
|
||||
};
|
||||
|
||||
return <X onClick={closeScript} className="cursor-pointer" />;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
'use client';
|
||||
import { Button } from "@/components/ui/button";
|
||||
import handleCopy from "./handleCopy";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user