forked from forkanization/Proxmox-arm64
Merge remote-tracking branch 'upstream/main' into main_local
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { Category } from "@/lib/types";
|
||||
|
||||
const defaultLogo = "/default-logo.png"; // Fallback logo path
|
||||
const MAX_DESCRIPTION_LENGTH = 100; // Set max length for description
|
||||
const MAX_LOGOS = 5; // Max logos to display at once
|
||||
|
||||
const formattedBadge = (type: string) => {
|
||||
switch (type) {
|
||||
case "vm":
|
||||
return <Badge className="text-blue-500/75 border-blue-500/75 badge">VM</Badge>;
|
||||
case "ct":
|
||||
return (
|
||||
<Badge className="text-yellow-500/75 border-yellow-500/75 badge">LXC</Badge>
|
||||
);
|
||||
case "misc":
|
||||
return <Badge className="text-green-500/75 border-green-500/75 badge">MISC</Badge>;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const CategoryView = () => {
|
||||
const [categories, setCategories] = useState<Category[]>([]);
|
||||
const [selectedCategoryIndex, setSelectedCategoryIndex] = useState<number | null>(null);
|
||||
const [currentScripts, setCurrentScripts] = useState<any[]>([]);
|
||||
const [logoIndices, setLogoIndices] = useState<{ [key: string]: number }>({});
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCategories = async () => {
|
||||
try {
|
||||
const basePath = process.env.NODE_ENV === "production" ? "/ProxmoxVE" : "";
|
||||
const response = await fetch(`${basePath}/api/categories`);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch categories");
|
||||
}
|
||||
const data = await response.json();
|
||||
setCategories(data);
|
||||
|
||||
// Initialize logo indices
|
||||
const initialLogoIndices: { [key: string]: number } = {};
|
||||
data.forEach((category: any) => {
|
||||
initialLogoIndices[category.name] = 0;
|
||||
});
|
||||
setLogoIndices(initialLogoIndices);
|
||||
} catch (error) {
|
||||
console.error("Error fetching categories:", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchCategories();
|
||||
}, []);
|
||||
|
||||
const handleCategoryClick = (index: number) => {
|
||||
setSelectedCategoryIndex(index);
|
||||
setCurrentScripts(categories[index]?.scripts || []); // Update scripts for the selected category
|
||||
};
|
||||
|
||||
const handleBackClick = () => {
|
||||
setSelectedCategoryIndex(null);
|
||||
setCurrentScripts([]); // Clear scripts when going back
|
||||
};
|
||||
|
||||
const handleScriptClick = (scriptSlug: string) => {
|
||||
router.push(`/scripts?id=${scriptSlug}`);
|
||||
};
|
||||
|
||||
const navigateCategory = (direction: "prev" | "next") => {
|
||||
if (selectedCategoryIndex !== null) {
|
||||
const newIndex =
|
||||
direction === "prev"
|
||||
? (selectedCategoryIndex - 1 + categories.length) % categories.length
|
||||
: (selectedCategoryIndex + 1) % categories.length;
|
||||
setSelectedCategoryIndex(newIndex);
|
||||
setCurrentScripts(categories[newIndex]?.scripts || []); // Update scripts for the new category
|
||||
}
|
||||
};
|
||||
|
||||
const switchLogos = (categoryName: string, direction: "prev" | "next") => {
|
||||
setLogoIndices((prev) => {
|
||||
const currentIndex = prev[categoryName] || 0;
|
||||
const category = categories.find((cat) => cat.name === categoryName);
|
||||
if (!category || !category.scripts) return prev;
|
||||
|
||||
const totalLogos = category.scripts.length;
|
||||
const newIndex =
|
||||
direction === "prev"
|
||||
? (currentIndex - MAX_LOGOS + totalLogos) % totalLogos
|
||||
: (currentIndex + MAX_LOGOS) % totalLogos;
|
||||
|
||||
return { ...prev, [categoryName]: newIndex };
|
||||
});
|
||||
};
|
||||
|
||||
const truncateDescription = (text: string) => {
|
||||
return text.length > MAX_DESCRIPTION_LENGTH
|
||||
? `${text.slice(0, MAX_DESCRIPTION_LENGTH)}...`
|
||||
: text;
|
||||
};
|
||||
|
||||
const renderResources = (script: any) => {
|
||||
const cpu = script.install_methods[0]?.resources.cpu;
|
||||
const ram = script.install_methods[0]?.resources.ram;
|
||||
const hdd = script.install_methods[0]?.resources.hdd;
|
||||
|
||||
const resourceParts = [];
|
||||
if (cpu) resourceParts.push(<span key="cpu"><b>CPU:</b> {cpu}vCPU</span>);
|
||||
if (ram) resourceParts.push(<span key="ram"><b>RAM:</b> {ram}MB</span>);
|
||||
if (hdd) resourceParts.push(<span key="hdd"><b>HDD:</b> {hdd}GB</span>);
|
||||
|
||||
return resourceParts.length > 0 ? (
|
||||
<div className="text-sm text-gray-400">
|
||||
{resourceParts.map((part, index) => (
|
||||
<React.Fragment key={index}>
|
||||
{part}
|
||||
{index < resourceParts.length - 1 && " | "}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 mt-20">
|
||||
{categories.length === 0 && (
|
||||
<p className="text-center text-gray-500">No categories available. Please check the API endpoint.</p>
|
||||
)}
|
||||
{selectedCategoryIndex !== null ? (
|
||||
<div>
|
||||
{/* Header with Navigation */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => navigateCategory("prev")}
|
||||
className="p-2 transition-transform duration-300 hover:scale-105"
|
||||
>
|
||||
<ChevronLeft className="h-6 w-6" />
|
||||
</Button>
|
||||
<h2 className="text-3xl font-semibold transition-opacity duration-300 hover:opacity-90">
|
||||
{categories[selectedCategoryIndex].name}
|
||||
</h2>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => navigateCategory("next")}
|
||||
className="p-2 transition-transform duration-300 hover:scale-105"
|
||||
>
|
||||
<ChevronRight className="h-6 w-6" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Scripts Grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
|
||||
{currentScripts
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((script) => (
|
||||
<Card
|
||||
key={script.name}
|
||||
className="p-4 cursor-pointer hover:shadow-md transition-shadow duration-300"
|
||||
onClick={() => handleScriptClick(script.slug)}
|
||||
>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<h3 className="text-lg font-bold script-text text-center hover:text-blue-600 transition-colors duration-300">
|
||||
{script.name}
|
||||
</h3>
|
||||
<img
|
||||
src={script.logo || defaultLogo}
|
||||
alt={script.name || "Script logo"}
|
||||
className="h-12 w-12 object-contain mx-auto"
|
||||
/>
|
||||
<p className="text-sm text-gray-500 text-center">
|
||||
<b>Created at:</b> {script.date_created || "No date available"}
|
||||
</p>
|
||||
<p
|
||||
className="text-sm text-gray-700 hover:text-gray-900 text-center transition-colors duration-300"
|
||||
title={script.description || "No description available."}
|
||||
>
|
||||
{truncateDescription(script.description || "No description available.")}
|
||||
</p>
|
||||
{renderResources(script)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Back to Categories Button */}
|
||||
<div className="mt-8 text-center">
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={handleBackClick}
|
||||
className="px-6 py-2 text-white bg-blue-600 hover:bg-blue-700 rounded-lg shadow-md transition-transform duration-300 hover:scale-105"
|
||||
>
|
||||
Back to Categories
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{/* Categories Grid */}
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<h1 className="text-3xl font-semibold mb-4">Categories</h1>
|
||||
<p className="text-sm text-gray-500">
|
||||
{categories.reduce((total, category) => total + (category.scripts?.length || 0), 0)} Total scripts
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-8">
|
||||
{categories.map((category, index) => (
|
||||
<Card
|
||||
key={category.name}
|
||||
onClick={() => handleCategoryClick(index)}
|
||||
className="cursor-pointer hover:shadow-lg flex flex-col items-center justify-center py-6 transition-shadow duration-300"
|
||||
>
|
||||
<CardContent className="flex flex-col items-center">
|
||||
<h3 className="text-xl font-bold mb-4 category-title transition-colors duration-300 hover:text-blue-600">
|
||||
{category.name}
|
||||
</h3>
|
||||
<div className="flex justify-center items-center gap-2 mb-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
switchLogos(category.name, "prev");
|
||||
}}
|
||||
className="p-1 transition-transform duration-300 hover:scale-110"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
{category.scripts &&
|
||||
category.scripts
|
||||
.slice(logoIndices[category.name] || 0, (logoIndices[category.name] || 0) + MAX_LOGOS)
|
||||
.map((script, i) => (
|
||||
<div key={i} className="flex flex-col items-center">
|
||||
<img
|
||||
src={script.logo || defaultLogo}
|
||||
alt={script.name || "Script logo"}
|
||||
title={script.name}
|
||||
className="h-8 w-8 object-contain cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleScriptClick(script.slug);
|
||||
}}
|
||||
/>
|
||||
{formattedBadge(script.type)}
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
switchLogos(category.name, "next");
|
||||
}}
|
||||
className="p-1 transition-transform duration-300 hover:scale-110"
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-sm text-gray-400 text-center">
|
||||
{(category as any).description || "No description available."}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CategoryView;
|
||||
@@ -0,0 +1,236 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import DatePicker from 'react-datepicker';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
import { string } from "zod";
|
||||
|
||||
|
||||
interface DataModel {
|
||||
id: number;
|
||||
ct_type: number;
|
||||
disk_size: number;
|
||||
core_count: number;
|
||||
ram_size: number;
|
||||
verbose: string;
|
||||
os_type: string;
|
||||
os_version: string;
|
||||
hn: string;
|
||||
disableip6: string;
|
||||
ssh: string;
|
||||
tags: string;
|
||||
nsapp: string;
|
||||
created_at: string;
|
||||
method: string;
|
||||
pve_version: string;
|
||||
}
|
||||
|
||||
|
||||
const DataFetcher: React.FC = () => {
|
||||
const [data, setData] = useState<DataModel[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [startDate, setStartDate] = useState<Date | null>(null);
|
||||
const [endDate, setEndDate] = useState<Date | null>(null);
|
||||
const [sortConfig, setSortConfig] = useState<{ key: keyof DataModel | null, direction: 'ascending' | 'descending' }>({ key: 'id', direction: 'descending' });
|
||||
const [itemsPerPage, setItemsPerPage] = useState(5);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const response = await fetch("https://api.htl-braunau.at/data/json");
|
||||
if (!response.ok) throw new Error("Failed to fetch data: ${response.statusText}");
|
||||
const result: DataModel[] = await response.json();
|
||||
setData(result);
|
||||
} catch (err) {
|
||||
setError((err as Error).message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
|
||||
const filteredData = data.filter(item => {
|
||||
const matchesSearchQuery = Object.values(item).some(value =>
|
||||
value.toString().toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
const itemDate = new Date(item.created_at);
|
||||
const matchesDateRange = (!startDate || itemDate >= startDate) && (!endDate || itemDate <= endDate);
|
||||
return matchesSearchQuery && matchesDateRange;
|
||||
});
|
||||
|
||||
const sortedData = React.useMemo(() => {
|
||||
let sortableData = [...filteredData];
|
||||
if (sortConfig.key !== null) {
|
||||
sortableData.sort((a, b) => {
|
||||
if (sortConfig.key !== null && a[sortConfig.key] < b[sortConfig.key]) {
|
||||
return sortConfig.direction === 'ascending' ? -1 : 1;
|
||||
}
|
||||
if (sortConfig.key !== null && a[sortConfig.key] > b[sortConfig.key]) {
|
||||
return sortConfig.direction === 'ascending' ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
return sortableData;
|
||||
}, [filteredData, sortConfig]);
|
||||
|
||||
const requestSort = (key: keyof DataModel | null) => {
|
||||
let direction: 'ascending' | 'descending' = 'ascending';
|
||||
if (sortConfig.key === key && sortConfig.direction === 'ascending') {
|
||||
direction = 'descending';
|
||||
} else if (sortConfig.key === key && sortConfig.direction === 'descending') {
|
||||
direction = 'ascending';
|
||||
} else {
|
||||
direction = 'descending';
|
||||
}
|
||||
setSortConfig({ key, direction });
|
||||
};
|
||||
|
||||
interface SortConfig {
|
||||
key: keyof DataModel | null;
|
||||
direction: 'ascending' | 'descending';
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string): string => {
|
||||
const date = new Date(dateString);
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const timezoneOffset = dateString.slice(-6);
|
||||
return `${day}.${month}.${year} ${hours}:${minutes} ${timezoneOffset} GMT`;
|
||||
};
|
||||
|
||||
const handleItemsPerPageChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setItemsPerPage(Number(event.target.value));
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
const paginatedData = sortedData.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage);
|
||||
|
||||
if (loading) return <p>Loading...</p>;
|
||||
if (error) return <p>Error: {error}</p>;
|
||||
|
||||
|
||||
return (
|
||||
<div className="p-6 mt-20">
|
||||
<h1 className="text-2xl font-bold mb-4 text-center">Created LXCs</h1>
|
||||
<div className="mb-4 flex space-x-4">
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
value={searchQuery}
|
||||
onChange={e => setSearchQuery(e.target.value)}
|
||||
className="p-2 border"
|
||||
/>
|
||||
<label className="text-sm text-gray-600 mt-1 block">Search by keyword</label>
|
||||
</div>
|
||||
<div>
|
||||
<DatePicker
|
||||
selected={startDate}
|
||||
onChange={date => setStartDate(date)}
|
||||
selectsStart
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
placeholderText="Start date"
|
||||
className="p-2 border"
|
||||
/>
|
||||
<label className="text-sm text-gray-600 mt-1 block">Set a start date</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<DatePicker
|
||||
selected={endDate}
|
||||
onChange={date => setEndDate(date)}
|
||||
selectsEnd
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
placeholderText="End date"
|
||||
className="p-2 border"
|
||||
/>
|
||||
<label className="text-sm text-gray-600 mt-1 block">Set a end date</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4 flex justify-between items-center">
|
||||
<p className="text-lg font-bold">{filteredData.length} results found</p>
|
||||
<select value={itemsPerPage} onChange={handleItemsPerPageChange} className="p-2 border">
|
||||
<option value={5}>5</option>
|
||||
<option value={10}>10</option>
|
||||
<option value={20}>20</option>
|
||||
<option value={50}>50</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<div className="overflow-y-auto lg:overflow-y-visible">
|
||||
<table className="min-w-full table-auto border-collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('nsapp')}>Application</th>
|
||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('os_type')}>OS</th>
|
||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('os_version')}>OS Version</th>
|
||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('disk_size')}>Disk Size</th>
|
||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('core_count')}>Core Count</th>
|
||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('ram_size')}>RAM Size</th>
|
||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('hn')}>Hostname</th>
|
||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('ssh')}>SSH</th>
|
||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('verbose')}>Verb</th>
|
||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('tags')}>Tags</th>
|
||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('method')}>Method</th>
|
||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('pve_version')}>PVE Version</th>
|
||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('created_at')}>Created At</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{paginatedData.map((item, index) => (
|
||||
<tr key={index}>
|
||||
<td className="px-4 py-2 border-b">{item.nsapp}</td>
|
||||
<td className="px-4 py-2 border-b">{item.os_type}</td>
|
||||
<td className="px-4 py-2 border-b">{item.os_version}</td>
|
||||
<td className="px-4 py-2 border-b">{item.disk_size}</td>
|
||||
<td className="px-4 py-2 border-b">{item.core_count}</td>
|
||||
<td className="px-4 py-2 border-b">{item.ram_size}</td>
|
||||
<td className="px-4 py-2 border-b">{item.hn}</td>
|
||||
<td className="px-4 py-2 border-b">{item.ssh}</td>
|
||||
<td className="px-4 py-2 border-b">{item.verbose}</td>
|
||||
<td className="px-4 py-2 border-b">{item.tags.replace(/;/g, ' ')}</td>
|
||||
<td className="px-4 py-2 border-b">{item.method}</td>
|
||||
<td className="px-4 py-2 border-b">{item.pve_version}</td>
|
||||
<td className="px-4 py-2 border-b">{formatDate(item.created_at)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex justify-between items-center">
|
||||
<button
|
||||
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
|
||||
disabled={currentPage === 1}
|
||||
className="p-2 border"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<span>Page {currentPage}</span>
|
||||
<button
|
||||
onClick={() => setCurrentPage(prev => (prev * itemsPerPage < sortedData.length ? prev + 1 : prev))}
|
||||
disabled={currentPage * itemsPerPage >= sortedData.length}
|
||||
className="p-2 border"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
export default DataFetcher;
|
||||
+100
-100
@@ -34,89 +34,88 @@ export default function Page() {
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
<div className="w-full mt-16">
|
||||
<Particles
|
||||
className="absolute inset-0 -z-40"
|
||||
quantity={100}
|
||||
ease={80}
|
||||
color={color}
|
||||
refresh
|
||||
/>
|
||||
<div className="container mx-auto">
|
||||
<div className="flex h-[80vh] flex-col items-center justify-center gap-4 py-20 lg:py-40">
|
||||
<Dialog>
|
||||
<DialogTrigger>
|
||||
<div>
|
||||
<AnimatedGradientText>
|
||||
<div
|
||||
className={cn(
|
||||
`absolute inset-0 block size-full animate-gradient bg-gradient-to-r from-[#ffaa40]/50 via-[#9c40ff]/50 to-[#ffaa40]/50 bg-[length:var(--bg-size)_100%] [border-radius:inherit] [mask:linear-gradient(#fff_0_0)_content-box,linear-gradient(#fff_0_0)]`,
|
||||
`p-px ![mask-composite:subtract]`,
|
||||
)}
|
||||
/>
|
||||
❤️ <Separator className="mx-2 h-4" orientation="vertical" />
|
||||
<span
|
||||
className={cn(
|
||||
`animate-gradient bg-gradient-to-r from-[#ffaa40] via-[#9c40ff] to-[#ffaa40] bg-[length:var(--bg-size)_100%] bg-clip-text text-transparent`,
|
||||
`inline`,
|
||||
)}
|
||||
>
|
||||
Scripts by Tteck
|
||||
</span>
|
||||
</AnimatedGradientText>
|
||||
</div>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Thank You!</DialogTitle>
|
||||
<DialogDescription>
|
||||
A big thank you to Tteck, for your hard work over all of these years,
|
||||
and also to the many other contributors to the project.
|
||||
Your hard work is truly appreciated by the entire Proxmox community!
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<CardFooter className="flex flex-col gap-2">
|
||||
<Button className="w-full" variant="outline" asChild>
|
||||
<a
|
||||
href="https://github.com/tteck"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center"
|
||||
>
|
||||
<FaGithub className="mr-2 h-4 w-4" /> Tteck's GitHub
|
||||
</a>
|
||||
</Button>
|
||||
<Button className="w-full" asChild>
|
||||
<a
|
||||
href={`https://github.com/community-scripts/${basePath}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center"
|
||||
>
|
||||
<ExternalLink className="mr-2 h-4 w-4" /> Upstream Proxmox Helper
|
||||
Scripts
|
||||
</a>
|
||||
</Button>
|
||||
<Button className="w-full" asChild>
|
||||
<a
|
||||
href={`https://github.com/asylumexp/${basePath}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center"
|
||||
>
|
||||
<ExternalLink className="mr-2 h-4 w-4" /> ARM64 port - Proxmox Helper
|
||||
Scripts
|
||||
</a>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<div className="w-full mt-16">
|
||||
<Particles
|
||||
className="absolute inset-0 -z-40"
|
||||
quantity={100}
|
||||
ease={80}
|
||||
color={color}
|
||||
refresh
|
||||
/>
|
||||
<div className="container mx-auto">
|
||||
<div className="flex h-[80vh] flex-col items-center justify-center gap-4 py-20 lg:py-40">
|
||||
<Dialog>
|
||||
<DialogTrigger>
|
||||
<div>
|
||||
<AnimatedGradientText>
|
||||
<div
|
||||
className={cn(
|
||||
`absolute inset-0 block size-full animate-gradient bg-gradient-to-r from-[#ffaa40]/50 via-[#9c40ff]/50 to-[#ffaa40]/50 bg-[length:var(--bg-size)_100%] [border-radius:inherit] [mask:linear-gradient(#fff_0_0)_content-box,linear-gradient(#fff_0_0)]`,
|
||||
`p-px ![mask-composite:subtract]`,
|
||||
)}
|
||||
/>
|
||||
❤️ <Separator className="mx-2 h-4" orientation="vertical" />
|
||||
<span
|
||||
className={cn(
|
||||
`animate-gradient bg-gradient-to-r from-[#ffaa40] via-[#9c40ff] to-[#ffaa40] bg-[length:var(--bg-size)_100%] bg-clip-text text-transparent`,
|
||||
`inline`,
|
||||
)}
|
||||
>
|
||||
Scripts by tteck
|
||||
</span>
|
||||
</AnimatedGradientText>
|
||||
</div>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Thank You!</DialogTitle>
|
||||
<DialogDescription>
|
||||
A big thank you to tteck and the many contributors who have
|
||||
made this project possible. Your hard work is truly
|
||||
appreciated by the entire Proxmox community!
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<CardFooter className="flex flex-col gap-2">
|
||||
<Button className="w-full" variant="outline" asChild>
|
||||
<a
|
||||
href="https://github.com/tteck"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center"
|
||||
>
|
||||
<FaGithub className="mr-2 h-4 w-4" /> Tteck's GitHub
|
||||
</a>
|
||||
</Button>
|
||||
<Button className="w-full" asChild>
|
||||
<a
|
||||
href={`https://github.com/community-scripts/Proxmox`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center"
|
||||
>
|
||||
<ExternalLink className="mr-2 h-4 w-4" /> Upstream Proxmox Helper
|
||||
Scripts
|
||||
</a>
|
||||
</Button>
|
||||
<Button className="w-full" asChild>
|
||||
<a
|
||||
href={`https://github.com/asylumexp/Proxmox`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center"
|
||||
>
|
||||
<ExternalLink className="mr-2 h-4 w-4" /> Proxmox Scripts - ARM64 Port
|
||||
</a>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<h1 className="max-w-2xl text-center text-5xl font-semibold tracking-tighter md:text-7xl">
|
||||
Make managing your Homelab a breeze
|
||||
</h1>
|
||||
<p className="max-w-2xl text-center text-lg leading-relaxed tracking-tight text-muted-foreground md:text-xl">
|
||||
<div className="flex flex-col gap-4">
|
||||
<h1 className="max-w-2xl text-center text-3xl font-semibold tracking-tighter md:text-7xl">
|
||||
Make managing your Homelab a breeze
|
||||
</h1>
|
||||
<div className="max-w-2xl gap-2 flex flex-col text-center sm:text-lg text-sm leading-relaxed tracking-tight text-muted-foreground md:text-xl">
|
||||
This project aims to take the community-made Proxmox Helper Scripts, originally started by tteck, and port it to arm64.
|
||||
<br />
|
||||
<br />
|
||||
@@ -129,22 +128,23 @@ export default function Page() {
|
||||
the process of creating and configuring Linux containers (LXC) and
|
||||
virtual machines (VMs) on Proxmox VE.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-row gap-3">
|
||||
<Link href="/scripts">
|
||||
<Button
|
||||
size="lg"
|
||||
variant="expandIcon"
|
||||
Icon={CustomArrowRightIcon}
|
||||
iconPlacement="right"
|
||||
className="hover:"
|
||||
>
|
||||
View Scripts
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row gap-3">
|
||||
<Link href="/scripts">
|
||||
<Button
|
||||
size="lg"
|
||||
variant="expandIcon"
|
||||
Icon={CustomArrowRightIcon}
|
||||
iconPlacement="right"
|
||||
className="hover:"
|
||||
>
|
||||
View Scripts
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -35,8 +35,18 @@ export function LatestScripts({ items }: { items: Category[] }) {
|
||||
|
||||
const latestScripts = useMemo(() => {
|
||||
if (!items) return [];
|
||||
|
||||
const scripts = items.flatMap((category) => category.scripts || []);
|
||||
return scripts.sort(
|
||||
|
||||
// Filter out duplicates by slug
|
||||
const uniqueScriptsMap = new Map<string, Script>();
|
||||
scripts.forEach((script) => {
|
||||
if (!uniqueScriptsMap.has(script.slug)) {
|
||||
uniqueScriptsMap.set(script.slug, script);
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(uniqueScriptsMap.values()).sort(
|
||||
(a, b) =>
|
||||
new Date(b.date_created).getTime() - new Date(a.date_created).getTime(),
|
||||
);
|
||||
@@ -49,7 +59,7 @@ export function LatestScripts({ items }: { items: Category[] }) {
|
||||
const goToPreviousPage = () => {
|
||||
setPage((prevPage) => prevPage - 1);
|
||||
};
|
||||
|
||||
|
||||
const startIndex = (page - 1) * ITEMS_PER_PAGE;
|
||||
const endIndex = page * ITEMS_PER_PAGE;
|
||||
|
||||
@@ -90,7 +100,7 @@ export function LatestScripts({ items }: { items: Category[] }) {
|
||||
>
|
||||
<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">
|
||||
<div className="flex h-16 w-16 min-w-16 items-center justify-center rounded-lg bg-accent p-1">
|
||||
<Image
|
||||
src={script.logo || `/${basePath}/logo.png`}
|
||||
unoptimized
|
||||
@@ -142,7 +152,7 @@ export function LatestScripts({ items }: { items: Category[] }) {
|
||||
export function MostViewedScripts({ items }: { items: Category[] }) {
|
||||
const mostViewedScripts = items.reduce((acc: Script[], category) => {
|
||||
const foundScripts = category.scripts.filter((script) =>
|
||||
mostPopularScripts.includes(script.name),
|
||||
mostPopularScripts.includes(script.slug),
|
||||
);
|
||||
return acc.concat(foundScripts);
|
||||
}, []);
|
||||
@@ -162,7 +172,7 @@ export function MostViewedScripts({ items }: { items: Category[] }) {
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-3">
|
||||
<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">
|
||||
<div className="flex size-16 min-w-16 items-center justify-center rounded-lg bg-accent p-1">
|
||||
<Image
|
||||
unoptimized
|
||||
src={script.logo || `/${basePath}/logo.png`}
|
||||
|
||||
@@ -1,35 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import { Category } from "@/lib/types";
|
||||
import type { Category, Script } from "@/lib/types";
|
||||
import ScriptAccordion from "./ScriptAccordion";
|
||||
|
||||
const Sidebar = ({
|
||||
items,
|
||||
selectedScript,
|
||||
setSelectedScript,
|
||||
items,
|
||||
selectedScript,
|
||||
setSelectedScript,
|
||||
}: {
|
||||
items: Category[];
|
||||
selectedScript: string | null;
|
||||
setSelectedScript: (script: string | null) => void;
|
||||
items: Category[];
|
||||
selectedScript: string | null;
|
||||
setSelectedScript: (script: string | null) => void;
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex min-w-72 flex-col sm:max-w-72">
|
||||
<div className="flex items-end justify-between pb-4">
|
||||
<h1 className="text-xl font-bold">Categories</h1>
|
||||
<p className="text-xs italic text-muted-foreground">
|
||||
{items.reduce((acc, category) => acc + category.scripts.length, 0)}{" "}
|
||||
Total scripts
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg">
|
||||
<ScriptAccordion
|
||||
items={items}
|
||||
selectedScript={selectedScript}
|
||||
setSelectedScript={setSelectedScript}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const uniqueScripts = items.reduce((acc, category) => {
|
||||
for (const script of category.scripts) {
|
||||
if (!acc.some((s) => s.name === script.name)) {
|
||||
acc.push(script);
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}, [] as Script[]);
|
||||
|
||||
return (
|
||||
<div className="flex min-w-72 flex-col sm:max-w-72">
|
||||
<div className="flex items-end justify-between pb-4">
|
||||
<h1 className="text-xl font-bold">Categories</h1>
|
||||
<p className="text-xs italic text-muted-foreground">
|
||||
{uniqueScripts.length} Total scripts
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg">
|
||||
<ScriptAccordion
|
||||
items={items}
|
||||
selectedScript={selectedScript}
|
||||
setSelectedScript={setSelectedScript}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
export default Sidebar;
|
||||
@@ -5,7 +5,7 @@ export default function Footer() {
|
||||
return (
|
||||
<div className="supports-backdrop-blur:bg-background/90 mt-auto flex border-t border-border bg-background/40 py-6 backdrop-blur-lg">
|
||||
<div className="flex w-full justify-between">
|
||||
<div className="mx-6 w-full max-w-7xl text-sm text-muted-foreground">
|
||||
<div className="mx-6 w-full max-w-7xl text-xs sm:text-sm text-muted-foreground">
|
||||
Website built by the community. The source code is avaliable on{" "}
|
||||
<Link
|
||||
href={`https://github.com/community-scripts/${basePath}`}
|
||||
|
||||
@@ -9,12 +9,7 @@ import { navbarLinks } from "@/config/siteConfig";
|
||||
import CommandMenu from "./CommandMenu";
|
||||
import StarOnGithubButton from "./ui/star-on-github-button";
|
||||
import { ThemeToggle } from "./ui/theme-toggle";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "./ui/tooltip";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -33,56 +28,59 @@ function Navbar() {
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`fixed left-0 top-0 z-50 flex w-screen justify-center px-4 xl:px-0 ${
|
||||
isScrolled ? "glass border-b bg-background/50" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex h-20 w-full max-w-7xl flex-row-reverse items-center justify-between sm:flex-row">
|
||||
<Link
|
||||
href={"/"}
|
||||
className="flex cursor-pointer flex-row-reverse items-center gap-2 font-semibold sm:flex-row"
|
||||
>
|
||||
<Image
|
||||
height={18}
|
||||
unoptimized
|
||||
width={18}
|
||||
alt="logo"
|
||||
src="logo.png"
|
||||
/>
|
||||
<span className="hidden lg:block">Proxmox ARM64 Helper-Scripts</span>
|
||||
</Link>
|
||||
<div className="flex gap-2">
|
||||
<CommandMenu />
|
||||
<StarOnGithubButton />
|
||||
{navbarLinks.map(({ href, event, icon, text }) => (
|
||||
<TooltipProvider key={event}>
|
||||
<Tooltip delayDuration={100}>
|
||||
<TooltipTrigger>
|
||||
<Button variant="ghost" size={"icon"} asChild>
|
||||
<Link
|
||||
target="_blank"
|
||||
href={href}
|
||||
data-umami-event={event}
|
||||
>
|
||||
{icon}
|
||||
<span className="sr-only">{text}</span>
|
||||
</Link>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="text-xs">
|
||||
{text}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
))}
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
<>
|
||||
<div
|
||||
className={`fixed left-0 top-0 z-50 flex w-screen justify-center px-4 xl:px-0 ${
|
||||
isScrolled ? "glass border-b bg-background/50" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex h-20 w-full max-w-7xl items-center justify-between sm:flex-row">
|
||||
<Link
|
||||
href={"/"}
|
||||
className="flex cursor-pointer w-full justify-center sm:justify-start flex-row-reverse items-center gap-2 font-semibold sm:flex-row"
|
||||
>
|
||||
<Image
|
||||
height={18}
|
||||
unoptimized
|
||||
width={18}
|
||||
alt="logo"
|
||||
src="/ProxmoxVE/logo.png"
|
||||
className=""
|
||||
/>
|
||||
<span className="hidden md:block">Proxmox ARM64 Helper-Scripts</span>
|
||||
</Link>
|
||||
<div className="flex gap-2">
|
||||
<CommandMenu />
|
||||
<StarOnGithubButton />
|
||||
{navbarLinks.map(({ href, event, icon, text, mobileHidden }) => (
|
||||
<TooltipProvider key={event}>
|
||||
<Tooltip delayDuration={100}>
|
||||
<TooltipTrigger
|
||||
className={mobileHidden ? "hidden lg:block" : ""}
|
||||
>
|
||||
<Button variant="ghost" size={"icon"} asChild>
|
||||
<Link
|
||||
target="_blank"
|
||||
href={href}
|
||||
data-umami-event={event}
|
||||
>
|
||||
{icon}
|
||||
<span className="sr-only">{text}</span>
|
||||
</Link>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="text-xs">
|
||||
{text}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
))}
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Navbar;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { OperatingSystem } from "@/lib/types";
|
||||
import { MessagesSquare, Scroll } from "lucide-react";
|
||||
import { FaDiscord, FaGithub } from "react-icons/fa";
|
||||
import React from "react";
|
||||
import { FaDiscord, FaGithub } from "react-icons/fa";
|
||||
|
||||
export const basePath = process.env.BASE_PATH;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user