mirror of
https://github.com/community-scripts/ProxmoxVE.git
synced 2025-06-28 18:17:37 +00:00
Remove npm legacy errors, created single source of truth for ESlint. updated analytics url. updated script background (#5498)
* Update ScriptAccordion and ScriptItem components for improved styling * Add README.md for Proxmox VE Helper-Scripts Frontend * Remove testing dependencies and related test files from the frontend project * Update analytics URL in siteConfig to point to community-scripts.org * Refactor ESLint configuration to have one source of truth and run "npm lint" to apply new changes * Update lint script in package.json to remove npm * Add 'next' option to ESLint configuration for improved compatibility * Update package dependencies and versions in package.json and package-lock.json * Refactor theme provider import and enhance calendar component for dynamic icon rendering * rename sidebar, alerts and buttons * rename description and interfaces files * rename more files * change folder name * Refactor tooltip logic to improve updateable condition handling * Enhance CommandMenu to prevent duplicate scripts across categories * Remove test step from frontend CI/CD workflow
This commit is contained in:
3
.github/workflows/frontend-cicd.yml
generated
vendored
3
.github/workflows/frontend-cicd.yml
generated
vendored
@ -44,9 +44,6 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci --prefer-offline --legacy-peer-deps
|
run: npm ci --prefer-offline --legacy-peer-deps
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: npm run test
|
|
||||||
|
|
||||||
- name: Configure Next.js for pages
|
- name: Configure Next.js for pages
|
||||||
uses: actions/configure-pages@v5
|
uses: actions/configure-pages@v5
|
||||||
with:
|
with:
|
||||||
|
5
frontend/.eslintrc.json
generated
5
frontend/.eslintrc.json
generated
@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": ["next/core-web-vitals"],
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"plugins": ["@typescript-eslint"]
|
|
||||||
}
|
|
51
frontend/.vscode/settings.json
generated
vendored
Normal file
51
frontend/.vscode/settings.json
generated
vendored
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
// Disable the default formatter, use eslint instead
|
||||||
|
"prettier.enable": false,
|
||||||
|
"editor.formatOnSave": false,
|
||||||
|
|
||||||
|
// Auto fix
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": "explicit",
|
||||||
|
"source.organizeImports": "never"
|
||||||
|
},
|
||||||
|
|
||||||
|
// Silent the stylistic rules in you IDE, but still auto fix them
|
||||||
|
"eslint.rules.customizations": [
|
||||||
|
{ "rule": "style/*", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "format/*", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*-indent", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*-spacing", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*-spaces", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*-order", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*-dangle", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*-newline", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*quotes", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*semi", "severity": "off", "fixable": true }
|
||||||
|
],
|
||||||
|
|
||||||
|
// Enable eslint for all supported languages
|
||||||
|
"eslint.validate": [
|
||||||
|
"javascript",
|
||||||
|
"javascriptreact",
|
||||||
|
"typescript",
|
||||||
|
"typescriptreact",
|
||||||
|
"vue",
|
||||||
|
"html",
|
||||||
|
"markdown",
|
||||||
|
"json",
|
||||||
|
"json5",
|
||||||
|
"jsonc",
|
||||||
|
"yaml",
|
||||||
|
"toml",
|
||||||
|
"xml",
|
||||||
|
"gql",
|
||||||
|
"graphql",
|
||||||
|
"astro",
|
||||||
|
"svelte",
|
||||||
|
"css",
|
||||||
|
"less",
|
||||||
|
"scss",
|
||||||
|
"pcss",
|
||||||
|
"postcss"
|
||||||
|
]
|
||||||
|
}
|
281
frontend/README.md
Normal file
281
frontend/README.md
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
# Proxmox VE Helper-Scripts Frontend
|
||||||
|
|
||||||
|
> 🚀 **Modern frontend for the Community-Scripts Proxmox VE Helper-Scripts repository**
|
||||||
|
|
||||||
|
A comprehensive, user-friendly interface built with Next.js that provides access to 300+ automation scripts for Proxmox Virtual Environment management. This frontend serves as the official website for the Community-Scripts organization's Proxmox VE Helper-Scripts repository.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
## 🌟 Features
|
||||||
|
|
||||||
|
### Core Functionality
|
||||||
|
|
||||||
|
- **📜 Script Management**: Browse, search, and filter 300+ Proxmox VE scripts
|
||||||
|
- **📱 Responsive Design**: Mobile-first approach with modern UI/UX
|
||||||
|
- **🔍 Advanced Search**: Fuzzy search with category filtering
|
||||||
|
- **📊 Analytics Integration**: Built-in analytics for usage tracking
|
||||||
|
- **🌙 Dark/Light Mode**: Theme switching with system preference detection
|
||||||
|
- **⚡ Performance Optimized**: Static site generation for lightning-fast loading
|
||||||
|
|
||||||
|
### Technical Features
|
||||||
|
|
||||||
|
- **🎨 Modern UI Components**: Built with Radix UI and shadcn/ui
|
||||||
|
- **📈 Data Visualization**: Charts and metrics using Chart.js
|
||||||
|
- **🔄 State Management**: React Query for efficient data fetching
|
||||||
|
- **📝 Type Safety**: Full TypeScript implementation
|
||||||
|
- **🚀 Static Export**: Optimized for GitHub Pages deployment
|
||||||
|
|
||||||
|
## 🛠️ Tech Stack
|
||||||
|
|
||||||
|
### Frontend Framework
|
||||||
|
|
||||||
|
- **[Next.js 15.2.4](https://nextjs.org/)** - React framework with App Router
|
||||||
|
- **[React 19.0.0](https://react.dev/)** - Latest React with concurrent features
|
||||||
|
- **[TypeScript 5.8.2](https://www.typescriptlang.org/)** - Type-safe JavaScript
|
||||||
|
|
||||||
|
### Styling & UI
|
||||||
|
|
||||||
|
- **[Tailwind CSS 3.4.17](https://tailwindcss.com/)** - Utility-first CSS framework
|
||||||
|
- **[Radix UI](https://www.radix-ui.com/)** - Unstyled, accessible UI components
|
||||||
|
- **[shadcn/ui](https://ui.shadcn.com/)** - Re-usable components built on Radix UI
|
||||||
|
- **[Framer Motion](https://www.framer.com/motion/)** - Animation library
|
||||||
|
- **[Lucide React](https://lucide.dev/)** - Icon library
|
||||||
|
|
||||||
|
### Data & State Management
|
||||||
|
|
||||||
|
- **[TanStack Query 5.71.1](https://tanstack.com/query)** - Powerful data synchronization
|
||||||
|
- **[Zod 3.24.2](https://zod.dev/)** - TypeScript-first schema validation
|
||||||
|
- **[nuqs 2.4.1](https://nuqs.47ng.com/)** - Type-safe search params state manager
|
||||||
|
|
||||||
|
### Development Tools
|
||||||
|
|
||||||
|
- **[Vitest 3.1.1](https://vitest.dev/)** - Fast unit testing framework
|
||||||
|
- **[React Testing Library](https://testing-library.com/react)** - Simple testing utilities
|
||||||
|
- **[ESLint](https://eslint.org/)** - Code linting and formatting
|
||||||
|
- **[Prettier](https://prettier.io/)** - Code formatting
|
||||||
|
|
||||||
|
### Additional Libraries
|
||||||
|
|
||||||
|
- **[Chart.js](https://www.chartjs.org/)** - Data visualization
|
||||||
|
- **[Fuse.js](https://fusejs.io/)** - Fuzzy search
|
||||||
|
- **[date-fns](https://date-fns.org/)** - Date utility library
|
||||||
|
- **[Next Themes](https://github.com/pacocoursey/next-themes)** - Theme management
|
||||||
|
|
||||||
|
## 🚀 Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- **Node.js 18+** (recommend using the latest LTS version)
|
||||||
|
- **npm**, **yarn**, **pnpm**, or **bun** package manager
|
||||||
|
- **Git** for version control
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
1. **Clone the repository**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/community-scripts/ProxmoxVE.git
|
||||||
|
cd ProxmoxVE/frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Install dependencies**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using npm
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Using yarn
|
||||||
|
yarn install
|
||||||
|
|
||||||
|
# Using pnpm
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# Using bun
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Start the development server**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# or
|
||||||
|
yarn dev
|
||||||
|
# or
|
||||||
|
pnpm dev
|
||||||
|
# or
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Open your browser**
|
||||||
|
|
||||||
|
Navigate to [http://localhost:3000](http://localhost:3000) to see the application running.
|
||||||
|
|
||||||
|
### Environment Configuration
|
||||||
|
|
||||||
|
The application uses the following environment variables:
|
||||||
|
|
||||||
|
- `BASE_PATH`: Set to "ProxmoxVE" for GitHub Pages deployment
|
||||||
|
- Analytics configuration is handled in `src/config/siteConfig.tsx`
|
||||||
|
|
||||||
|
## 🧪 Development
|
||||||
|
|
||||||
|
### Available Scripts
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Development
|
||||||
|
npm run dev # Start development server with Turbopack
|
||||||
|
npm run build # Build for production
|
||||||
|
npm run start # Start production server (after build)
|
||||||
|
|
||||||
|
# Code Quality
|
||||||
|
npm run lint # Run ESLint
|
||||||
|
npm run typecheck # Run TypeScript type checking
|
||||||
|
npm run format:write # Format code with Prettier
|
||||||
|
npm run format:check # Check code formatting
|
||||||
|
|
||||||
|
# Deployment
|
||||||
|
npm run deploy # Build and deploy to GitHub Pages
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development Workflow
|
||||||
|
|
||||||
|
1. **Feature Development**
|
||||||
|
|
||||||
|
- Create a new branch for your feature
|
||||||
|
- Follow the established TypeScript and React patterns
|
||||||
|
- Use the existing component library (shadcn/ui)
|
||||||
|
- Ensure responsive design principles
|
||||||
|
|
||||||
|
2. **Code Standards**
|
||||||
|
|
||||||
|
- Follow TypeScript strict mode
|
||||||
|
- Use functional components with hooks
|
||||||
|
- Implement proper error boundaries
|
||||||
|
- Write descriptive variable and function names
|
||||||
|
- Use early returns for better readability
|
||||||
|
|
||||||
|
3. **Styling Guidelines**
|
||||||
|
|
||||||
|
- Use Tailwind CSS utility classes
|
||||||
|
- Follow mobile-first responsive design
|
||||||
|
- Implement dark/light mode considerations
|
||||||
|
- Use CSS variables from the design system
|
||||||
|
|
||||||
|
4. **Testing**
|
||||||
|
- Write unit tests for utility functions
|
||||||
|
- Test React components with React Testing Library
|
||||||
|
- Ensure accessibility standards are met
|
||||||
|
- Run tests before committing
|
||||||
|
|
||||||
|
### Component Development
|
||||||
|
|
||||||
|
The project uses a component-driven development approach:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Example component structure
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
interface ComponentProps {
|
||||||
|
title: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Component = ({ title, className }: ComponentProps) => {
|
||||||
|
return (
|
||||||
|
<div className={cn("default-classes", className)}>
|
||||||
|
<Button>{title}</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration for Static Export
|
||||||
|
|
||||||
|
The application is configured for static export in `next.config.mjs`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const nextConfig = {
|
||||||
|
output: "export",
|
||||||
|
basePath: `/ProxmoxVE`,
|
||||||
|
images: {
|
||||||
|
unoptimized: true // Required for static export
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
We welcome contributions from the community! Here's how you can help:
|
||||||
|
|
||||||
|
### Getting Started
|
||||||
|
|
||||||
|
1. **Fork the repository** on GitHub
|
||||||
|
2. **Clone your fork** locally
|
||||||
|
3. **Create a new branch** for your feature or bugfix
|
||||||
|
4. **Make your changes** following our coding standards
|
||||||
|
5. **Submit a pull request** with a clear description
|
||||||
|
|
||||||
|
### Contribution Guidelines
|
||||||
|
|
||||||
|
#### Code Style
|
||||||
|
|
||||||
|
- Follow the existing TypeScript and React patterns
|
||||||
|
- Use descriptive variable and function names
|
||||||
|
- Implement proper error handling
|
||||||
|
- Write self-documenting code with appropriate comments
|
||||||
|
|
||||||
|
#### Component Guidelines
|
||||||
|
|
||||||
|
- Use functional components with hooks
|
||||||
|
- Implement proper TypeScript types
|
||||||
|
- Follow accessibility best practices
|
||||||
|
- Ensure responsive design
|
||||||
|
- Use the existing design system components
|
||||||
|
|
||||||
|
#### Pull Request Process
|
||||||
|
|
||||||
|
1. Update documentation if needed
|
||||||
|
2. Update the README if you've added new features
|
||||||
|
3. Request review from maintainers
|
||||||
|
|
||||||
|
### Areas for Contribution
|
||||||
|
|
||||||
|
- **🐛 Bug fixes**: Report and fix issues
|
||||||
|
- **✨ New features**: Enhance functionality
|
||||||
|
- **📚 Documentation**: Improve guides and examples
|
||||||
|
- **🎨 UI/UX**: Improve design and user experience
|
||||||
|
- **♿ Accessibility**: Enhance accessibility features
|
||||||
|
- **🚀 Performance**: Optimize loading and runtime performance
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
|
## 🙏 Acknowledgments
|
||||||
|
|
||||||
|
- **[tteck](https://github.com/tteck)** - Original creator of the Proxmox VE Helper-Scripts
|
||||||
|
- **[Community-Scripts Organization](https://github.com/community-scripts)** - Maintaining and expanding the project
|
||||||
|
- **[Proxmox Community](https://forum.proxmox.com/)** - For continuous feedback and support
|
||||||
|
- **All Contributors** - Thank you for your valuable contributions!
|
||||||
|
|
||||||
|
## 📚 Additional Resources
|
||||||
|
|
||||||
|
- **[Proxmox VE Documentation](https://pve.proxmox.com/pve-docs/)**
|
||||||
|
- **[Community Scripts Repository](https://github.com/community-scripts/ProxmoxVE)**
|
||||||
|
- **[Discord Community](https://discord.gg/2wvnMDgdnU)**
|
||||||
|
- **[GitHub Discussions](https://github.com/community-scripts/ProxmoxVE/discussions)**
|
||||||
|
|
||||||
|
## 🔗 Links
|
||||||
|
|
||||||
|
- **🌐 Live Website**: [https://community-scripts.github.io/ProxmoxVE/](https://community-scripts.github.io/ProxmoxVE/)
|
||||||
|
- **💬 Discord Server**: [https://discord.gg/2wvnMDgdnU](https://discord.gg/2wvnMDgdnU)
|
||||||
|
- **📝 Change Log**: [https://github.com/community-scripts/ProxmoxVE/blob/main/CHANGELOG.md](https://github.com/community-scripts/ProxmoxVE/blob/main/CHANGELOG.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Made with ❤️ by the Community-Scripts team and contributors**
|
41
frontend/eslint.config.mjs
Normal file
41
frontend/eslint.config.mjs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import antfu from "@antfu/eslint-config";
|
||||||
|
|
||||||
|
export default antfu(
|
||||||
|
{
|
||||||
|
type: "app",
|
||||||
|
typescript: true,
|
||||||
|
formatters: true,
|
||||||
|
next: true,
|
||||||
|
stylistic: {
|
||||||
|
indent: 2,
|
||||||
|
semi: true,
|
||||||
|
quotes: "double",
|
||||||
|
},
|
||||||
|
ignores: ["src/components/ui/**", "README.md", "public/json/**"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
"ts/no-redeclare": "off",
|
||||||
|
"ts/consistent-type-definitions": ["error", "type"],
|
||||||
|
"no-console": ["warn"],
|
||||||
|
"antfu/no-top-level-await": ["off"],
|
||||||
|
"node/prefer-global/process": ["off"],
|
||||||
|
"node/no-process-env": ["error"],
|
||||||
|
"perfectionist/sort-imports": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
type: "line-length",
|
||||||
|
order: "desc",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
"unicorn/filename-case": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
case: "kebabCase",
|
||||||
|
ignore: ["README.md"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
8376
frontend/package-lock.json
generated
8376
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
30
frontend/package.json
generated
30
frontend/package.json
generated
@ -1,22 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "proxmox-helper-scripts-website",
|
"name": "proxmox-helper-scripts-website",
|
||||||
|
"type": "module",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "MIT",
|
|
||||||
"private": true,
|
"private": true,
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Bram Suurd",
|
"name": "Bram Suurd",
|
||||||
"url": "https://github.com/community-scripts"
|
"url": "https://github.com/community-scripts"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack",
|
"dev": "next dev --turbopack",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "eslint . --fix",
|
||||||
"test": "vitest",
|
|
||||||
"deploy": "next build && touch out/.nojekyll && git add out/ && git commit -m \"Deploy\" && git subtree push --prefix out origin gh-pages",
|
|
||||||
"format:write": "prettier --write \"**/*.{ts,tsx,mdx}\" --cache",
|
|
||||||
"format:check": "prettier --check \"**/*.{ts,tsx,mdx}\" --cache",
|
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -45,7 +41,7 @@
|
|||||||
"lucide-react": "^0.453.0",
|
"lucide-react": "^0.453.0",
|
||||||
"mini-svg-data-uri": "^1.4.4",
|
"mini-svg-data-uri": "^1.4.4",
|
||||||
"next": "15.2.4",
|
"next": "15.2.4",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.4.4",
|
||||||
"nuqs": "^2.4.1",
|
"nuqs": "^2.4.1",
|
||||||
"pocketbase": "^0.21.5",
|
"pocketbase": "^0.21.5",
|
||||||
"prettier-plugin-organize-imports": "^4.1.0",
|
"prettier-plugin-organize-imports": "^4.1.0",
|
||||||
@ -53,7 +49,7 @@
|
|||||||
"react-chartjs-2": "^5.3.0",
|
"react-chartjs-2": "^5.3.0",
|
||||||
"react-code-blocks": "^0.1.6",
|
"react-code-blocks": "^0.1.6",
|
||||||
"react-datepicker": "^7.6.0",
|
"react-datepicker": "^7.6.0",
|
||||||
"react-day-picker": "8.10.1",
|
"react-day-picker": "^9.4.3",
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "19.0.0",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-simple-typewriter": "^5.0.1",
|
"react-simple-typewriter": "^5.0.1",
|
||||||
@ -64,9 +60,10 @@
|
|||||||
"zod": "^3.24.2"
|
"zod": "^3.24.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@antfu/eslint-config": "^4.16.1",
|
||||||
|
"@eslint-react/eslint-plugin": "^1.52.2",
|
||||||
|
"@next/eslint-plugin-next": "^15.3.4",
|
||||||
"@tanstack/eslint-plugin-query": "^5.68.0",
|
"@tanstack/eslint-plugin-query": "^5.68.0",
|
||||||
"@testing-library/dom": "^10.4.0",
|
|
||||||
"@testing-library/react": "^16.2.0",
|
|
||||||
"@types/node": "^22.13.16",
|
"@types/node": "^22.13.16",
|
||||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
|
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
|
||||||
@ -75,6 +72,9 @@
|
|||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"eslint": "^9.23.0",
|
"eslint": "^9.23.0",
|
||||||
"eslint-config-next": "15.0.2",
|
"eslint-config-next": "15.0.2",
|
||||||
|
"eslint-plugin-format": "^1.0.1",
|
||||||
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.20",
|
||||||
"jsdom": "^25.0.1",
|
"jsdom": "^25.0.1",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.3",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
@ -83,11 +83,13 @@
|
|||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"tailwindcss-animated": "^1.1.2",
|
"tailwindcss-animated": "^1.1.2",
|
||||||
"typescript": "^5.8.2",
|
"typescript": "^5.8.2",
|
||||||
"vite-tsconfig-paths": "^5.1.4",
|
"vite-tsconfig-paths": "^5.1.4"
|
||||||
"vitest": "^3.1.1"
|
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1"
|
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
|
"react": "19.0.0",
|
||||||
|
"react-dom": "19.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
import { screen } from "@testing-library/dom";
|
|
||||||
import { render } from "@testing-library/react";
|
|
||||||
import { describe, expect, it } from "vitest";
|
|
||||||
import Page from "@/app/page";
|
|
||||||
|
|
||||||
describe("Page", () => {
|
|
||||||
it("should show button to view scripts", () => {
|
|
||||||
render(<Page />);
|
|
||||||
expect(screen.getByRole("button", { name: "View Scripts" })).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,56 +0,0 @@
|
|||||||
import { describe, it, assert, beforeAll } from "vitest";
|
|
||||||
import { promises as fs } from "fs";
|
|
||||||
import path from "path";
|
|
||||||
import { ScriptSchema, type Script } from "@/app/json-editor/_schemas/schemas";
|
|
||||||
import { Metadata } from "@/lib/types";
|
|
||||||
console.log('Current directory: ' + process.cwd());
|
|
||||||
const jsonDir = "public/json";
|
|
||||||
const metadataFileName = "metadata.json";
|
|
||||||
const versionsFileName = "versions.json";
|
|
||||||
const encoding = "utf-8";
|
|
||||||
|
|
||||||
const fileNames = (await fs.readdir(jsonDir))
|
|
||||||
.filter((fileName) => fileName !== metadataFileName && fileName !== versionsFileName);
|
|
||||||
|
|
||||||
describe.each(fileNames)("%s", async (fileName) => {
|
|
||||||
let script: Script;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
const filePath = path.resolve(jsonDir, fileName);
|
|
||||||
const fileContent = await fs.readFile(filePath, encoding)
|
|
||||||
script = JSON.parse(fileContent);
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
it("should have valid json according to script schema", () => {
|
|
||||||
ScriptSchema.parse(script);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have a corresponding script file", () => {
|
|
||||||
script.install_methods.forEach((method) => {
|
|
||||||
const scriptPath = path.resolve("..", method.script)
|
|
||||||
//FIXME: Dose note account for new dir structure and files in /script/tools
|
|
||||||
|
|
||||||
assert(fs.stat(scriptPath), `Script file not found: ${scriptPath}`)
|
|
||||||
})
|
|
||||||
});
|
|
||||||
})
|
|
||||||
|
|
||||||
describe(`${metadataFileName}`, async () => {
|
|
||||||
let metadata: Metadata;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
const filePath = path.resolve(jsonDir, metadataFileName);
|
|
||||||
const fileContent = await fs.readFile(filePath, encoding)
|
|
||||||
metadata = JSON.parse(fileContent);
|
|
||||||
})
|
|
||||||
it("should have valid json according to metadata schema", () => {
|
|
||||||
// TODO: create zod schema for metadata. Move zod schemas to /lib/types.ts
|
|
||||||
assert(metadata.categories.length > 0);
|
|
||||||
metadata.categories.forEach((category) => {
|
|
||||||
assert.isString(category.name)
|
|
||||||
assert.isNumber(category.id)
|
|
||||||
assert.isNumber(category.sort_order)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})
|
|
@ -1,4 +0,0 @@
|
|||||||
import { vi } from "vitest";
|
|
||||||
|
|
||||||
// Mock canvas getContext
|
|
||||||
HTMLCanvasElement.prototype.getContext = vi.fn();
|
|
@ -1,7 +1,8 @@
|
|||||||
import { Metadata, Script } from "@/lib/types";
|
|
||||||
import { promises as fs } from "fs";
|
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import path from "path";
|
import { promises as fs } from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
import type { Metadata, Script } from "@/lib/types";
|
||||||
|
|
||||||
export const dynamic = "force-static";
|
export const dynamic = "force-static";
|
||||||
|
|
||||||
@ -10,21 +11,21 @@ const metadataFileName = "metadata.json";
|
|||||||
const versionFileName = "version.json";
|
const versionFileName = "version.json";
|
||||||
const encoding = "utf-8";
|
const encoding = "utf-8";
|
||||||
|
|
||||||
const getMetadata = async () => {
|
async function getMetadata() {
|
||||||
const filePath = path.resolve(jsonDir, metadataFileName);
|
const filePath = path.resolve(jsonDir, metadataFileName);
|
||||||
const fileContent = await fs.readFile(filePath, encoding);
|
const fileContent = await fs.readFile(filePath, encoding);
|
||||||
const metadata: Metadata = JSON.parse(fileContent);
|
const metadata: Metadata = JSON.parse(fileContent);
|
||||||
return metadata;
|
return metadata;
|
||||||
};
|
}
|
||||||
|
|
||||||
const getScripts = async () => {
|
async function getScripts() {
|
||||||
const filePaths = (await fs.readdir(jsonDir))
|
const filePaths = (await fs.readdir(jsonDir))
|
||||||
.filter((fileName) =>
|
.filter(fileName =>
|
||||||
fileName.endsWith(".json") &&
|
fileName.endsWith(".json")
|
||||||
fileName !== metadataFileName &&
|
&& fileName !== metadataFileName
|
||||||
fileName !== versionFileName
|
&& fileName !== versionFileName,
|
||||||
)
|
)
|
||||||
.map((fileName) => path.resolve(jsonDir, fileName));
|
.map(fileName => path.resolve(jsonDir, fileName));
|
||||||
|
|
||||||
const scripts = await Promise.all(
|
const scripts = await Promise.all(
|
||||||
filePaths.map(async (filePath) => {
|
filePaths.map(async (filePath) => {
|
||||||
@ -34,7 +35,7 @@ const getScripts = async () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
return scripts;
|
return scripts;
|
||||||
};
|
}
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
@ -43,7 +44,7 @@ export async function GET() {
|
|||||||
|
|
||||||
const categories = metadata.categories
|
const categories = metadata.categories
|
||||||
.map((category) => {
|
.map((category) => {
|
||||||
category.scripts = scripts.filter((script) =>
|
category.scripts = scripts.filter(script =>
|
||||||
script.categories?.includes(category.id),
|
script.categories?.includes(category.id),
|
||||||
);
|
);
|
||||||
return category;
|
return category;
|
||||||
@ -51,7 +52,8 @@ export async function GET() {
|
|||||||
.sort((a, b) => a.sort_order - b.sort_order);
|
.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);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Failed to fetch categories" },
|
{ error: "Failed to fetch categories" },
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { AppVersion } from "@/lib/types";
|
|
||||||
import { error } from "console";
|
|
||||||
import { promises as fs } from "fs";
|
|
||||||
// import Error from "next/error";
|
// import Error from "next/error";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import path from "path";
|
import { promises as fs } from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
import type { AppVersion } from "@/lib/types";
|
||||||
|
|
||||||
export const dynamic = "force-static";
|
export const dynamic = "force-static";
|
||||||
|
|
||||||
@ -11,33 +11,32 @@ const jsonDir = "public/json";
|
|||||||
const versionsFileName = "versions.json";
|
const versionsFileName = "versions.json";
|
||||||
const encoding = "utf-8";
|
const encoding = "utf-8";
|
||||||
|
|
||||||
const getVersions = async () => {
|
async function getVersions() {
|
||||||
const filePath = path.resolve(jsonDir, versionsFileName);
|
const filePath = path.resolve(jsonDir, versionsFileName);
|
||||||
const fileContent = await fs.readFile(filePath, encoding);
|
const fileContent = await fs.readFile(filePath, encoding);
|
||||||
const versions: AppVersion[] = JSON.parse(fileContent);
|
const versions: AppVersion[] = JSON.parse(fileContent);
|
||||||
|
|
||||||
const modifiedVersions = versions.map(version => {
|
const modifiedVersions = versions.map((version) => {
|
||||||
let newName = version.name;
|
let newName = version.name;
|
||||||
newName = newName.toLowerCase().replace(/[^a-z0-9/]/g, '');
|
newName = newName.toLowerCase().replace(/[^a-z0-9/]/g, "");
|
||||||
return { ...version, name: newName, date: new Date(version.date) };
|
return { ...version, name: newName, date: new Date(version.date) };
|
||||||
});
|
});
|
||||||
|
|
||||||
return modifiedVersions;
|
return modifiedVersions;
|
||||||
};
|
}
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const versions = await getVersions();
|
const versions = await getVersions();
|
||||||
return NextResponse.json(versions);
|
return NextResponse.json(versions);
|
||||||
|
}
|
||||||
} catch (error) {
|
catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
const err = error as globalThis.Error;
|
const err = error as globalThis.Error;
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
name: err.name,
|
name: err.name,
|
||||||
message: err.message || "An unexpected error occurred",
|
message: err.message || "An unexpected error occurred",
|
||||||
version: "No version found - Error"
|
version: "No version found - Error",
|
||||||
}, {
|
}, {
|
||||||
status: 500,
|
status: 500,
|
||||||
});
|
});
|
||||||
|
@ -1,18 +1,20 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
|
||||||
import { Category } from "@/lib/types";
|
|
||||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
import type { Category } from "@/lib/types";
|
||||||
|
|
||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
|
||||||
const defaultLogo = "/default-logo.png"; // Fallback logo path
|
const defaultLogo = "/default-logo.png"; // Fallback logo path
|
||||||
const MAX_DESCRIPTION_LENGTH = 100; // Set max length for description
|
const MAX_DESCRIPTION_LENGTH = 100; // Set max length for description
|
||||||
const MAX_LOGOS = 5; // Max logos to display at once
|
const MAX_LOGOS = 5; // Max logos to display at once
|
||||||
|
|
||||||
const formattedBadge = (type: string) => {
|
function formattedBadge(type: string) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "vm":
|
case "vm":
|
||||||
return <Badge className="text-blue-500/75 border-blue-500/75 badge">VM</Badge>;
|
return <Badge className="text-blue-500/75 border-blue-500/75 badge">VM</Badge>;
|
||||||
@ -24,9 +26,9 @@ const formattedBadge = (type: string) => {
|
|||||||
return <Badge className="text-green-500/75 border-green-500/75 badge">ADDON</Badge>;
|
return <Badge className="text-green-500/75 border-green-500/75 badge">ADDON</Badge>;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
}
|
||||||
|
|
||||||
const CategoryView = () => {
|
function CategoryView() {
|
||||||
const [categories, setCategories] = useState<Category[]>([]);
|
const [categories, setCategories] = useState<Category[]>([]);
|
||||||
const [selectedCategoryIndex, setSelectedCategoryIndex] = useState<number | null>(null);
|
const [selectedCategoryIndex, setSelectedCategoryIndex] = useState<number | null>(null);
|
||||||
const [currentScripts, setCurrentScripts] = useState<any[]>([]);
|
const [currentScripts, setCurrentScripts] = useState<any[]>([]);
|
||||||
@ -36,6 +38,7 @@ const CategoryView = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchCategories = async () => {
|
const fetchCategories = async () => {
|
||||||
try {
|
try {
|
||||||
|
// eslint-disable-next-line node/no-process-env
|
||||||
const basePath = process.env.NODE_ENV === "production" ? "/ProxmoxVE" : "";
|
const basePath = process.env.NODE_ENV === "production" ? "/ProxmoxVE" : "";
|
||||||
const response = await fetch(`${basePath}/api/categories`);
|
const response = await fetch(`${basePath}/api/categories`);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@ -50,7 +53,8 @@ const CategoryView = () => {
|
|||||||
initialLogoIndices[category.name] = 0;
|
initialLogoIndices[category.name] = 0;
|
||||||
});
|
});
|
||||||
setLogoIndices(initialLogoIndices);
|
setLogoIndices(initialLogoIndices);
|
||||||
} catch (error) {
|
}
|
||||||
|
catch (error) {
|
||||||
console.error("Error fetching categories:", error);
|
console.error("Error fetching categories:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -74,8 +78,8 @@ const CategoryView = () => {
|
|||||||
|
|
||||||
const navigateCategory = (direction: "prev" | "next") => {
|
const navigateCategory = (direction: "prev" | "next") => {
|
||||||
if (selectedCategoryIndex !== null) {
|
if (selectedCategoryIndex !== null) {
|
||||||
const newIndex =
|
const newIndex
|
||||||
direction === "prev"
|
= direction === "prev"
|
||||||
? (selectedCategoryIndex - 1 + categories.length) % categories.length
|
? (selectedCategoryIndex - 1 + categories.length) % categories.length
|
||||||
: (selectedCategoryIndex + 1) % categories.length;
|
: (selectedCategoryIndex + 1) % categories.length;
|
||||||
setSelectedCategoryIndex(newIndex);
|
setSelectedCategoryIndex(newIndex);
|
||||||
@ -86,12 +90,13 @@ const CategoryView = () => {
|
|||||||
const switchLogos = (categoryName: string, direction: "prev" | "next") => {
|
const switchLogos = (categoryName: string, direction: "prev" | "next") => {
|
||||||
setLogoIndices((prev) => {
|
setLogoIndices((prev) => {
|
||||||
const currentIndex = prev[categoryName] || 0;
|
const currentIndex = prev[categoryName] || 0;
|
||||||
const category = categories.find((cat) => cat.name === categoryName);
|
const category = categories.find(cat => cat.name === categoryName);
|
||||||
if (!category || !category.scripts) return prev;
|
if (!category || !category.scripts)
|
||||||
|
return prev;
|
||||||
|
|
||||||
const totalLogos = category.scripts.length;
|
const totalLogos = category.scripts.length;
|
||||||
const newIndex =
|
const newIndex
|
||||||
direction === "prev"
|
= direction === "prev"
|
||||||
? (currentIndex - MAX_LOGOS + totalLogos) % totalLogos
|
? (currentIndex - MAX_LOGOS + totalLogos) % totalLogos
|
||||||
: (currentIndex + MAX_LOGOS) % totalLogos;
|
: (currentIndex + MAX_LOGOS) % totalLogos;
|
||||||
|
|
||||||
@ -109,35 +114,49 @@ const CategoryView = () => {
|
|||||||
const hdd = script.install_methods[0]?.resources.hdd;
|
const hdd = script.install_methods[0]?.resources.hdd;
|
||||||
|
|
||||||
const resourceParts = [];
|
const resourceParts = [];
|
||||||
if (cpu)
|
if (cpu) {
|
||||||
resourceParts.push(
|
resourceParts.push(
|
||||||
<span key="cpu">
|
<span key="cpu">
|
||||||
<b>CPU:</b> {cpu}vCPU
|
<b>CPU:</b>
|
||||||
|
{" "}
|
||||||
|
{cpu}
|
||||||
|
vCPU
|
||||||
</span>,
|
</span>,
|
||||||
);
|
);
|
||||||
if (ram)
|
}
|
||||||
|
if (ram) {
|
||||||
resourceParts.push(
|
resourceParts.push(
|
||||||
<span key="ram">
|
<span key="ram">
|
||||||
<b>RAM:</b> {ram}MB
|
<b>RAM:</b>
|
||||||
|
{" "}
|
||||||
|
{ram}
|
||||||
|
MB
|
||||||
</span>,
|
</span>,
|
||||||
);
|
);
|
||||||
if (hdd)
|
}
|
||||||
|
if (hdd) {
|
||||||
resourceParts.push(
|
resourceParts.push(
|
||||||
<span key="hdd">
|
<span key="hdd">
|
||||||
<b>HDD:</b> {hdd}GB
|
<b>HDD:</b>
|
||||||
|
{" "}
|
||||||
|
{hdd}
|
||||||
|
GB
|
||||||
</span>,
|
</span>,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return resourceParts.length > 0 ? (
|
return resourceParts.length > 0
|
||||||
<div className="text-sm text-gray-400">
|
? (
|
||||||
{resourceParts.map((part, index) => (
|
<div className="text-sm text-gray-400">
|
||||||
<React.Fragment key={index}>
|
{resourceParts.map((part, index) => (
|
||||||
{part}
|
<React.Fragment key={index}>
|
||||||
{index < resourceParts.length - 1 && " | "}
|
{part}
|
||||||
</React.Fragment>
|
{index < resourceParts.length - 1 && " | "}
|
||||||
))}
|
</React.Fragment>
|
||||||
</div>
|
))}
|
||||||
) : null;
|
</div>
|
||||||
|
)
|
||||||
|
: null;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -145,145 +164,151 @@ const CategoryView = () => {
|
|||||||
{categories.length === 0 && (
|
{categories.length === 0 && (
|
||||||
<p className="text-center text-gray-500">No categories available. Please check the API endpoint.</p>
|
<p className="text-center text-gray-500">No categories available. Please check the API endpoint.</p>
|
||||||
)}
|
)}
|
||||||
{selectedCategoryIndex !== null ? (
|
{selectedCategoryIndex !== null
|
||||||
<div>
|
? (
|
||||||
{/* Header with Navigation */}
|
<div>
|
||||||
<div className="flex items-center justify-between mb-6">
|
{/* Header with Navigation */}
|
||||||
<Button
|
<div className="flex items-center justify-between mb-6">
|
||||||
variant="ghost"
|
<Button
|
||||||
onClick={() => navigateCategory("prev")}
|
variant="ghost"
|
||||||
className="p-2 transition-transform duration-300 hover:scale-105"
|
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">
|
<ChevronLeft className="h-6 w-6" />
|
||||||
<h3 className="text-lg font-bold script-text text-center hover:text-blue-600 transition-colors duration-300">
|
</Button>
|
||||||
{script.name}
|
<h2 className="text-3xl font-semibold transition-opacity duration-300 hover:opacity-90">
|
||||||
</h3>
|
{categories[selectedCategoryIndex].name}
|
||||||
<img
|
</h2>
|
||||||
src={script.logo || defaultLogo}
|
<Button
|
||||||
alt={script.name || "Script logo"}
|
variant="ghost"
|
||||||
className="h-12 w-12 object-contain mx-auto"
|
onClick={() => navigateCategory("next")}
|
||||||
/>
|
className="p-2 transition-transform duration-300 hover:scale-105"
|
||||||
<p className="text-sm text-gray-500 text-center">
|
>
|
||||||
<b>Created at:</b> {script.date_created || "No date available"}
|
<ChevronRight className="h-6 w-6" />
|
||||||
</p>
|
</Button>
|
||||||
<p
|
</div>
|
||||||
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 */}
|
{/* Scripts Grid */}
|
||||||
<div className="mt-8 text-center">
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
|
||||||
<Button
|
{currentScripts
|
||||||
variant="default"
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
onClick={handleBackClick}
|
.map(script => (
|
||||||
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"
|
<Card
|
||||||
>
|
key={script.name}
|
||||||
Back to Categories
|
className="p-4 cursor-pointer hover:shadow-md transition-shadow duration-300"
|
||||||
</Button>
|
onClick={() => handleScriptClick(script.slug)}
|
||||||
</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" />
|
<CardContent className="flex flex-col gap-4">
|
||||||
</Button>
|
<h3 className="text-lg font-bold script-text text-center hover:text-blue-600 transition-colors duration-300">
|
||||||
{category.scripts &&
|
{script.name}
|
||||||
category.scripts
|
</h3>
|
||||||
.slice(logoIndices[category.name] || 0, (logoIndices[category.name] || 0) + MAX_LOGOS)
|
<img
|
||||||
.map((script, i) => (
|
src={script.logo || defaultLogo}
|
||||||
<div key={i} className="flex flex-col items-center">
|
alt={script.name || "Script logo"}
|
||||||
<img
|
className="h-12 w-12 object-contain mx-auto"
|
||||||
src={script.logo || defaultLogo}
|
/>
|
||||||
alt={script.name || "Script logo"}
|
<p className="text-sm text-gray-500 text-center">
|
||||||
title={script.name}
|
<b>Created at:</b>
|
||||||
className="h-8 w-8 object-contain cursor-pointer"
|
{" "}
|
||||||
onClick={(e) => {
|
{script.date_created || "No date available"}
|
||||||
e.stopPropagation();
|
</p>
|
||||||
handleScriptClick(script.slug);
|
<p
|
||||||
}}
|
className="text-sm text-gray-700 hover:text-gray-900 text-center transition-colors duration-300"
|
||||||
/>
|
title={script.description || "No description available."}
|
||||||
{formattedBadge(script.type)}
|
>
|
||||||
</div>
|
{truncateDescription(script.description || "No description available.")}
|
||||||
))}
|
</p>
|
||||||
<Button
|
{renderResources(script)}
|
||||||
variant="ghost"
|
</CardContent>
|
||||||
onClick={(e) => {
|
</Card>
|
||||||
e.stopPropagation();
|
))}
|
||||||
switchLogos(category.name, "next");
|
</div>
|
||||||
}}
|
|
||||||
className="p-1 transition-transform duration-300 hover:scale-110"
|
{/* Back to Categories Button */}
|
||||||
>
|
<div className="mt-8 text-center">
|
||||||
<ChevronRight className="h-4 w-4" />
|
<Button
|
||||||
</Button>
|
variant="default"
|
||||||
</div>
|
onClick={handleBackClick}
|
||||||
<p className="text-sm text-gray-400 text-center">
|
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"
|
||||||
{(category as any).description || "No description available."}
|
>
|
||||||
</p>
|
Back to Categories
|
||||||
</CardContent>
|
</Button>
|
||||||
</Card>
|
</div>
|
||||||
))}
|
</div>
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
export default CategoryView;
|
export default CategoryView;
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { JSX, useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import DatePicker from 'react-datepicker';
|
import "react-datepicker/dist/react-datepicker.css";
|
||||||
import 'react-datepicker/dist/react-datepicker.css';
|
|
||||||
import ApplicationChart from "../../components/ApplicationChart";
|
|
||||||
|
|
||||||
interface DataModel {
|
import ApplicationChart from "../../components/application-chart";
|
||||||
|
|
||||||
|
type DataModel = {
|
||||||
id: number;
|
id: number;
|
||||||
ct_type: number;
|
ct_type: number;
|
||||||
disk_size: number;
|
disk_size: number;
|
||||||
@ -22,13 +22,13 @@ interface DataModel {
|
|||||||
error: string;
|
error: string;
|
||||||
type: string;
|
type: string;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
};
|
||||||
|
|
||||||
interface SummaryData {
|
type SummaryData = {
|
||||||
total_entries: number;
|
total_entries: number;
|
||||||
status_count: Record<string, number>;
|
status_count: Record<string, number>;
|
||||||
nsapp_count: Record<string, number>;
|
nsapp_count: Record<string, number>;
|
||||||
}
|
};
|
||||||
|
|
||||||
const DataFetcher: React.FC = () => {
|
const DataFetcher: React.FC = () => {
|
||||||
const [data, setData] = useState<DataModel[]>([]);
|
const [data, setData] = useState<DataModel[]>([]);
|
||||||
@ -37,16 +37,18 @@ const DataFetcher: React.FC = () => {
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [itemsPerPage, setItemsPerPage] = useState(25);
|
const [itemsPerPage, setItemsPerPage] = useState(25);
|
||||||
const [sortConfig, setSortConfig] = useState<{ key: string; direction: 'ascending' | 'descending' } | null>(null);
|
const [sortConfig, setSortConfig] = useState<{ key: string; direction: "ascending" | "descending" } | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchSummary = async () => {
|
const fetchSummary = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("https://api.htl-braunau.at/data/summary");
|
const response = await fetch("https://api.htl-braunau.at/data/summary");
|
||||||
if (!response.ok) throw new Error(`Failed to fetch summary: ${response.statusText}`);
|
if (!response.ok)
|
||||||
|
throw new Error(`Failed to fetch summary: ${response.statusText}`);
|
||||||
const result: SummaryData = await response.json();
|
const result: SummaryData = await response.json();
|
||||||
setSummary(result);
|
setSummary(result);
|
||||||
} catch (err) {
|
}
|
||||||
|
catch (err) {
|
||||||
setError((err as Error).message);
|
setError((err as Error).message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -58,13 +60,16 @@ const DataFetcher: React.FC = () => {
|
|||||||
const fetchPaginatedData = async () => {
|
const fetchPaginatedData = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`https://api.htl-braunau.at/data/paginated?page=${currentPage}&limit=${itemsPerPage === 0 ? '' : itemsPerPage}`);
|
const response = await fetch(`https://api.htl-braunau.at/data/paginated?page=${currentPage}&limit=${itemsPerPage === 0 ? "" : itemsPerPage}`);
|
||||||
if (!response.ok) throw new Error(`Failed to fetch data: ${response.statusText}`);
|
if (!response.ok)
|
||||||
|
throw new Error(`Failed to fetch data: ${response.statusText}`);
|
||||||
const result: DataModel[] = await response.json();
|
const result: DataModel[] = await response.json();
|
||||||
setData(result);
|
setData(result);
|
||||||
} catch (err) {
|
}
|
||||||
|
catch (err) {
|
||||||
setError((err as Error).message);
|
setError((err as Error).message);
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -73,26 +78,35 @@ const DataFetcher: React.FC = () => {
|
|||||||
}, [currentPage, itemsPerPage]);
|
}, [currentPage, itemsPerPage]);
|
||||||
|
|
||||||
const sortedData = React.useMemo(() => {
|
const sortedData = React.useMemo(() => {
|
||||||
if (!sortConfig) return data;
|
if (!sortConfig)
|
||||||
|
return data;
|
||||||
const sorted = [...data].sort((a, b) => {
|
const sorted = [...data].sort((a, b) => {
|
||||||
if (a[sortConfig.key] < b[sortConfig.key]) {
|
if (a[sortConfig.key] < b[sortConfig.key]) {
|
||||||
return sortConfig.direction === 'ascending' ? -1 : 1;
|
return sortConfig.direction === "ascending" ? -1 : 1;
|
||||||
}
|
}
|
||||||
if (a[sortConfig.key] > b[sortConfig.key]) {
|
if (a[sortConfig.key] > b[sortConfig.key]) {
|
||||||
return sortConfig.direction === 'ascending' ? 1 : -1;
|
return sortConfig.direction === "ascending" ? 1 : -1;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
return sorted;
|
return sorted;
|
||||||
}, [data, sortConfig]);
|
}, [data, sortConfig]);
|
||||||
|
|
||||||
if (loading) return <p>Loading...</p>;
|
if (loading)
|
||||||
if (error) return <p>Error: {error}</p>;
|
return <p>Loading...</p>;
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<p>
|
||||||
|
Error:
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const requestSort = (key: string) => {
|
const requestSort = (key: string) => {
|
||||||
let direction: 'ascending' | 'descending' = 'ascending';
|
let direction: "ascending" | "descending" = "ascending";
|
||||||
if (sortConfig && sortConfig.key === key && sortConfig.direction === 'ascending') {
|
if (sortConfig && sortConfig.key === key && sortConfig.direction === "ascending") {
|
||||||
direction = 'descending';
|
direction = "descending";
|
||||||
}
|
}
|
||||||
setSortConfig({ key, direction });
|
setSortConfig({ key, direction });
|
||||||
};
|
};
|
||||||
@ -102,8 +116,8 @@ const DataFetcher: React.FC = () => {
|
|||||||
const year = date.getFullYear();
|
const year = date.getFullYear();
|
||||||
const month = date.getMonth() + 1;
|
const month = date.getMonth() + 1;
|
||||||
const day = date.getDate();
|
const day = date.getDate();
|
||||||
const hours = String(date.getHours()).padStart(2, '0');
|
const hours = String(date.getHours()).padStart(2, "0");
|
||||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
const minutes = String(date.getMinutes()).padStart(2, "0");
|
||||||
const timezoneOffset = dateString.slice(-6);
|
const timezoneOffset = dateString.slice(-6);
|
||||||
return `${day}.${month}.${year} ${hours}:${minutes} ${timezoneOffset} GMT`;
|
return `${day}.${month}.${year} ${hours}:${minutes} ${timezoneOffset} GMT`;
|
||||||
};
|
};
|
||||||
@ -114,49 +128,76 @@ const DataFetcher: React.FC = () => {
|
|||||||
<ApplicationChart data={summary} />
|
<ApplicationChart data={summary} />
|
||||||
<p className="text-lg font-bold mt-4"> </p>
|
<p className="text-lg font-bold mt-4"> </p>
|
||||||
<div className="mb-4 flex justify-between items-center">
|
<div className="mb-4 flex justify-between items-center">
|
||||||
<p className="text-lg font-bold">{summary?.total_entries} results found</p>
|
<p className="text-lg font-bold">
|
||||||
<p className="text-lg font">Status Legend: 🔄 installing {summary?.status_count["installing"] ?? 0} | ✔️ completed {summary?.status_count["done"] ?? 0} | ❌ failed {summary?.status_count["failed"] ?? 0} | ❓ unknown</p>
|
{summary?.total_entries}
|
||||||
</div>
|
{" "}
|
||||||
|
results found
|
||||||
|
</p>
|
||||||
|
<p className="text-lg font">
|
||||||
|
Status Legend: 🔄 installing
|
||||||
|
{summary?.status_count.installing ?? 0}
|
||||||
|
{" "}
|
||||||
|
| ✔️ completed
|
||||||
|
{summary?.status_count.done ?? 0}
|
||||||
|
{" "}
|
||||||
|
| ❌ failed
|
||||||
|
{summary?.status_count.failed ?? 0}
|
||||||
|
{" "}
|
||||||
|
| ❓ unknown
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<div className="overflow-y-auto lg:overflow-y-visible">
|
<div className="overflow-y-auto lg:overflow-y-visible">
|
||||||
<table className="min-w-full table-auto border-collapse">
|
<table className="min-w-full table-auto border-collapse">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('status')}>Status</th>
|
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort("status")}>Status</th>
|
||||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('type')}>Type</th>
|
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort("type")}>Type</th>
|
||||||
<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("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_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("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("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("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("ram_size")}>RAM Size</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("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("pve_version")}>PVE Version</th>
|
||||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('error')}>Error Message</th>
|
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort("error")}>Error Message</th>
|
||||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('created_at')}>Created At</th>
|
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort("created_at")}>Created At</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{sortedData.map((item, index) => (
|
{sortedData.map((item, index) => (
|
||||||
<tr key={index}>
|
<tr key={index}>
|
||||||
<td className="px-4 py-2 border-b">
|
<td className="px-4 py-2 border-b">
|
||||||
{item.status === "done" ? (
|
{item.status === "done"
|
||||||
"✔️"
|
? (
|
||||||
) : item.status === "failed" ? (
|
"✔️"
|
||||||
"❌"
|
)
|
||||||
) : item.status === "installing" ? (
|
: item.status === "failed"
|
||||||
"🔄"
|
? (
|
||||||
) : (
|
"❌"
|
||||||
item.status
|
)
|
||||||
)}
|
: item.status === "installing"
|
||||||
|
? (
|
||||||
|
"🔄"
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
item.status
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2 border-b">
|
||||||
|
{item.type === "lxc"
|
||||||
|
? (
|
||||||
|
"📦"
|
||||||
|
)
|
||||||
|
: item.type === "vm"
|
||||||
|
? (
|
||||||
|
"🖥️"
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
item.type
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2 border-b">{item.type === "lxc" ? (
|
|
||||||
"📦"
|
|
||||||
) : item.type === "vm" ? (
|
|
||||||
"🖥️"
|
|
||||||
) : (
|
|
||||||
item.type
|
|
||||||
)}</td>
|
|
||||||
<td className="px-4 py-2 border-b">{item.nsapp}</td>
|
<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_type}</td>
|
||||||
<td className="px-4 py-2 border-b">{item.os_version}</td>
|
<td className="px-4 py-2 border-b">{item.os_version}</td>
|
||||||
@ -175,11 +216,14 @@ const DataFetcher: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-4 flex justify-between items-center">
|
<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>
|
<button onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))} disabled={currentPage === 1} className="p-2 border">Previous</button>
|
||||||
<span>Page {currentPage}</span>
|
<span>
|
||||||
|
Page
|
||||||
|
{currentPage}
|
||||||
|
</span>
|
||||||
<button onClick={() => setCurrentPage(prev => prev + 1)} className="p-2 border">Next</button>
|
<button onClick={() => setCurrentPage(prev => prev + 1)} className="p-2 border">Next</button>
|
||||||
<select
|
<select
|
||||||
value={itemsPerPage}
|
value={itemsPerPage}
|
||||||
onChange={(e) => setItemsPerPage(Number(e.target.value))}
|
onChange={e => setItemsPerPage(Number(e.target.value))}
|
||||||
className="p-2 border"
|
className="p-2 border"
|
||||||
>
|
>
|
||||||
<option value={10}>10</option>
|
<option value={10}>10</option>
|
||||||
|
@ -1,150 +0,0 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import { AlertColors } from "@/config/siteConfig";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { PlusCircle, Trash2 } from "lucide-react";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { ScriptSchema, type Script } from "../_schemas/schemas";
|
|
||||||
import { memo, useCallback, useRef } from "react";
|
|
||||||
|
|
||||||
type NoteProps = {
|
|
||||||
script: Script;
|
|
||||||
setScript: (script: Script) => void;
|
|
||||||
setIsValid: (isValid: boolean) => void;
|
|
||||||
setZodErrors: (zodErrors: z.ZodError | null) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
function Note({
|
|
||||||
script,
|
|
||||||
setScript,
|
|
||||||
setIsValid,
|
|
||||||
setZodErrors,
|
|
||||||
}: NoteProps) {
|
|
||||||
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
|
|
||||||
|
|
||||||
const addNote = useCallback(() => {
|
|
||||||
setScript({
|
|
||||||
...script,
|
|
||||||
notes: [...script.notes, { text: "", type: "" }],
|
|
||||||
});
|
|
||||||
}, [script, setScript]);
|
|
||||||
|
|
||||||
const updateNote = useCallback((
|
|
||||||
index: number,
|
|
||||||
key: keyof Script["notes"][number],
|
|
||||||
value: string,
|
|
||||||
) => {
|
|
||||||
const updated: Script = {
|
|
||||||
...script,
|
|
||||||
notes: script.notes.map((note, i) =>
|
|
||||||
i === index ? { ...note, [key]: value } : note,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
const result = ScriptSchema.safeParse(updated);
|
|
||||||
setIsValid(result.success);
|
|
||||||
setZodErrors(result.success ? null : result.error);
|
|
||||||
setScript(updated);
|
|
||||||
// Restore focus after state update
|
|
||||||
if (key === "text") {
|
|
||||||
setTimeout(() => {
|
|
||||||
inputRefs.current[index]?.focus();
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
}, [script, setScript, setIsValid, setZodErrors]);
|
|
||||||
|
|
||||||
const removeNote = useCallback((index: number) => {
|
|
||||||
setScript({
|
|
||||||
...script,
|
|
||||||
notes: script.notes.filter((_, i) => i !== index),
|
|
||||||
});
|
|
||||||
}, [script, setScript]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h3 className="text-xl font-semibold">Notes</h3>
|
|
||||||
{script.notes.map((note, index) => (
|
|
||||||
<NoteItem key={index} note={note} index={index} updateNote={updateNote} removeNote={removeNote} />
|
|
||||||
))}
|
|
||||||
<Button type="button" size="sm" onClick={addNote}>
|
|
||||||
<PlusCircle className="mr-2 h-4 w-4" /> Add Note
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const NoteItem = memo(
|
|
||||||
({
|
|
||||||
note,
|
|
||||||
index,
|
|
||||||
updateNote,
|
|
||||||
removeNote,
|
|
||||||
}: {
|
|
||||||
note: Script["notes"][number];
|
|
||||||
index: number;
|
|
||||||
updateNote: (index: number, key: keyof Script["notes"][number], value: string) => void;
|
|
||||||
removeNote: (index: number) => void;
|
|
||||||
}) => {
|
|
||||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
||||||
|
|
||||||
const handleTextChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
updateNote(index, "text", e.target.value);
|
|
||||||
setTimeout(() => {
|
|
||||||
inputRef.current?.focus();
|
|
||||||
}, 0);
|
|
||||||
}, [index, updateNote]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-2 border p-4 rounded">
|
|
||||||
<Input
|
|
||||||
placeholder="Note Text"
|
|
||||||
value={note.text}
|
|
||||||
onChange={handleTextChange}
|
|
||||||
ref={inputRef}
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
value={note.type}
|
|
||||||
onValueChange={(value) => updateNote(index, "type", value)}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="flex-1">
|
|
||||||
<SelectValue placeholder="Type" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{Object.keys(AlertColors).map((type) => (
|
|
||||||
<SelectItem key={type} value={type}>
|
|
||||||
<span className="flex items-center gap-2">
|
|
||||||
{type.charAt(0).toUpperCase() + type.slice(1)}{" "}
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"size-4 rounded-full border",
|
|
||||||
AlertColors[type as keyof typeof AlertColors],
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="destructive"
|
|
||||||
type="button"
|
|
||||||
onClick={() => removeNote(index)}
|
|
||||||
>
|
|
||||||
<Trash2 className="mr-2 h-4 w-4" /> Remove Note
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
NoteItem.displayName = 'NoteItem';
|
|
||||||
|
|
||||||
|
|
||||||
export default memo(Note);
|
|
@ -1,4 +1,9 @@
|
|||||||
import { Label } from "@/components/ui/label";
|
import type { z } from "zod";
|
||||||
|
|
||||||
|
import { memo } from "react";
|
||||||
|
|
||||||
|
import type { Category } from "@/lib/types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@ -6,11 +11,10 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Category } from "@/lib/types";
|
import { Label } from "@/components/ui/label";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { z } from "zod";
|
|
||||||
import { type Script } from "../_schemas/schemas";
|
import type { Script } from "../_schemas/schemas";
|
||||||
import { memo } from "react";
|
|
||||||
|
|
||||||
type CategoryProps = {
|
type CategoryProps = {
|
||||||
script: Script;
|
script: Script;
|
||||||
@ -20,10 +24,10 @@ type CategoryProps = {
|
|||||||
categories: Category[];
|
categories: Category[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const CategoryTag = memo(({
|
const CategoryTag = memo(({
|
||||||
category,
|
category,
|
||||||
onRemove
|
onRemove,
|
||||||
}: {
|
}: {
|
||||||
category: Category;
|
category: Category;
|
||||||
onRemove: () => void;
|
onRemove: () => void;
|
||||||
}) => (
|
}) => (
|
||||||
@ -53,7 +57,7 @@ const CategoryTag = memo(({
|
|||||||
</span>
|
</span>
|
||||||
));
|
));
|
||||||
|
|
||||||
CategoryTag.displayName = 'CategoryTag';
|
CategoryTag.displayName = "CategoryTag";
|
||||||
|
|
||||||
function Categories({
|
function Categories({
|
||||||
script,
|
script,
|
||||||
@ -79,14 +83,16 @@ function Categories({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Label>
|
<Label>
|
||||||
Category <span className="text-red-500">*</span>
|
Category
|
||||||
|
{" "}
|
||||||
|
<span className="text-red-500">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Select onValueChange={(value) => addCategory(Number(value))}>
|
<Select onValueChange={value => addCategory(Number(value))}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select a category" />
|
<SelectValue placeholder="Select a category" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{categories.map((category) => (
|
{categories.map(category => (
|
||||||
<SelectItem key={category.id} value={category.id.toString()}>
|
<SelectItem key={category.id} value={category.id.toString()}>
|
||||||
{category.name}
|
{category.name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
@ -101,13 +107,15 @@ function Categories({
|
|||||||
>
|
>
|
||||||
{script.categories.map((categoryId) => {
|
{script.categories.map((categoryId) => {
|
||||||
const category = categoryMap.get(categoryId);
|
const category = categoryMap.get(categoryId);
|
||||||
return category ? (
|
return category
|
||||||
<CategoryTag
|
? (
|
||||||
key={categoryId}
|
<CategoryTag
|
||||||
category={category}
|
key={categoryId}
|
||||||
onRemove={() => removeCategory(categoryId)}
|
category={category}
|
||||||
/>
|
onRemove={() => removeCategory(categoryId)}
|
||||||
) : null;
|
/>
|
||||||
|
)
|
||||||
|
: null;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@ -1,11 +1,16 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
import type { z } from "zod";
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
||||||
import { OperatingSystems } from "@/config/siteConfig";
|
|
||||||
import { PlusCircle, Trash2 } from "lucide-react";
|
import { PlusCircle, Trash2 } from "lucide-react";
|
||||||
import { memo, useCallback, useRef } from "react";
|
import { memo, useCallback, useRef } from "react";
|
||||||
import { z } from "zod";
|
|
||||||
import { InstallMethodSchema, ScriptSchema, type Script } from "../_schemas/schemas";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import { OperatingSystems } from "@/config/site-config";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
|
||||||
|
import type { Script } from "../_schemas/schemas";
|
||||||
|
|
||||||
|
import { InstallMethodSchema, ScriptSchema } from "../_schemas/schemas";
|
||||||
|
|
||||||
type InstallMethodProps = {
|
type InstallMethodProps = {
|
||||||
script: Script;
|
script: Script;
|
||||||
@ -28,9 +33,11 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
|
|||||||
|
|
||||||
if (type === "pve") {
|
if (type === "pve") {
|
||||||
scriptPath = `tools/pve/${slug}.sh`;
|
scriptPath = `tools/pve/${slug}.sh`;
|
||||||
} else if (type === "addon") {
|
}
|
||||||
|
else if (type === "addon") {
|
||||||
scriptPath = `tools/addon/${slug}.sh`;
|
scriptPath = `tools/addon/${slug}.sh`;
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
scriptPath = `${type}/${slug}.sh`;
|
scriptPath = `${type}/${slug}.sh`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,8 +72,8 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
|
|||||||
const updatedMethod = { ...method, [key]: value };
|
const updatedMethod = { ...method, [key]: value };
|
||||||
|
|
||||||
if (key === "type") {
|
if (key === "type") {
|
||||||
updatedMethod.script =
|
updatedMethod.script
|
||||||
value === "alpine" ? `${prev.type}/alpine-${prev.slug}.sh` : `${prev.type}/${prev.slug}.sh`;
|
= value === "alpine" ? `${prev.type}/alpine-${prev.slug}.sh` : `${prev.type}/${prev.slug}.sh`;
|
||||||
|
|
||||||
// Set OS to Alpine and reset version if type is alpine
|
// Set OS to Alpine and reset version if type is alpine
|
||||||
if (value === "alpine") {
|
if (value === "alpine") {
|
||||||
@ -89,7 +96,8 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
|
|||||||
setIsValid(result.success);
|
setIsValid(result.success);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
setZodErrors(result.error);
|
setZodErrors(result.error);
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
setZodErrors(null);
|
setZodErrors(null);
|
||||||
}
|
}
|
||||||
return updated;
|
return updated;
|
||||||
@ -100,7 +108,7 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
|
|||||||
|
|
||||||
const removeInstallMethod = useCallback(
|
const removeInstallMethod = useCallback(
|
||||||
(index: number) => {
|
(index: number) => {
|
||||||
setScript((prev) => ({
|
setScript(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
install_methods: prev.install_methods.filter((_, i) => i !== index),
|
install_methods: prev.install_methods.filter((_, i) => i !== index),
|
||||||
}));
|
}));
|
||||||
@ -113,7 +121,7 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
|
|||||||
<h3 className="text-xl font-semibold">Install Methods</h3>
|
<h3 className="text-xl font-semibold">Install Methods</h3>
|
||||||
{script.install_methods.map((method, index) => (
|
{script.install_methods.map((method, index) => (
|
||||||
<div key={index} className="space-y-2 border p-4 rounded">
|
<div key={index} className="space-y-2 border p-4 rounded">
|
||||||
<Select value={method.type} onValueChange={(value) => updateInstallMethod(index, "type", value)}>
|
<Select value={method.type} onValueChange={value => updateInstallMethod(index, "type", value)}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Type" />
|
<SelectValue placeholder="Type" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
@ -130,12 +138,11 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
|
|||||||
placeholder="CPU in Cores"
|
placeholder="CPU in Cores"
|
||||||
type="number"
|
type="number"
|
||||||
value={method.resources.cpu || ""}
|
value={method.resources.cpu || ""}
|
||||||
onChange={(e) =>
|
onChange={e =>
|
||||||
updateInstallMethod(index, "resources", {
|
updateInstallMethod(index, "resources", {
|
||||||
...method.resources,
|
...method.resources,
|
||||||
cpu: e.target.value ? Number(e.target.value) : null,
|
cpu: e.target.value ? Number(e.target.value) : null,
|
||||||
})
|
})}
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
@ -144,12 +151,11 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
|
|||||||
placeholder="RAM in MB"
|
placeholder="RAM in MB"
|
||||||
type="number"
|
type="number"
|
||||||
value={method.resources.ram || ""}
|
value={method.resources.ram || ""}
|
||||||
onChange={(e) =>
|
onChange={e =>
|
||||||
updateInstallMethod(index, "resources", {
|
updateInstallMethod(index, "resources", {
|
||||||
...method.resources,
|
...method.resources,
|
||||||
ram: e.target.value ? Number(e.target.value) : null,
|
ram: e.target.value ? Number(e.target.value) : null,
|
||||||
})
|
})}
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
@ -158,31 +164,29 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
|
|||||||
placeholder="HDD in GB"
|
placeholder="HDD in GB"
|
||||||
type="number"
|
type="number"
|
||||||
value={method.resources.hdd || ""}
|
value={method.resources.hdd || ""}
|
||||||
onChange={(e) =>
|
onChange={e =>
|
||||||
updateInstallMethod(index, "resources", {
|
updateInstallMethod(index, "resources", {
|
||||||
...method.resources,
|
...method.resources,
|
||||||
hdd: e.target.value ? Number(e.target.value) : null,
|
hdd: e.target.value ? Number(e.target.value) : null,
|
||||||
})
|
})}
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Select
|
<Select
|
||||||
value={method.resources.os || undefined}
|
value={method.resources.os || undefined}
|
||||||
onValueChange={(value) =>
|
onValueChange={value =>
|
||||||
updateInstallMethod(index, "resources", {
|
updateInstallMethod(index, "resources", {
|
||||||
...method.resources,
|
...method.resources,
|
||||||
os: value || null,
|
os: value || null,
|
||||||
version: null, // Reset version when OS changes
|
version: null, // Reset version when OS changes
|
||||||
})
|
})}
|
||||||
}
|
|
||||||
disabled={method.type === "alpine"}
|
disabled={method.type === "alpine"}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="OS" />
|
<SelectValue placeholder="OS" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{OperatingSystems.map((os) => (
|
{OperatingSystems.map(os => (
|
||||||
<SelectItem key={os.name} value={os.name}>
|
<SelectItem key={os.name} value={os.name}>
|
||||||
{os.name}
|
{os.name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
@ -191,19 +195,18 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
|
|||||||
</Select>
|
</Select>
|
||||||
<Select
|
<Select
|
||||||
value={method.resources.version || undefined}
|
value={method.resources.version || undefined}
|
||||||
onValueChange={(value) =>
|
onValueChange={value =>
|
||||||
updateInstallMethod(index, "resources", {
|
updateInstallMethod(index, "resources", {
|
||||||
...method.resources,
|
...method.resources,
|
||||||
version: value || null,
|
version: value || null,
|
||||||
})
|
})}
|
||||||
}
|
|
||||||
disabled={method.type === "alpine"}
|
disabled={method.type === "alpine"}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Version" />
|
<SelectValue placeholder="Version" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{OperatingSystems.find((os) => os.name === method.resources.os)?.versions.map((version) => (
|
{OperatingSystems.find(os => os.name === method.resources.os)?.versions.map(version => (
|
||||||
<SelectItem key={version.slug} value={version.name}>
|
<SelectItem key={version.slug} value={version.name}>
|
||||||
{version.name}
|
{version.name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
@ -212,12 +215,16 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="destructive" size="sm" type="button" onClick={() => removeInstallMethod(index)}>
|
<Button variant="destructive" size="sm" type="button" onClick={() => removeInstallMethod(index)}>
|
||||||
<Trash2 className="mr-2 h-4 w-4" /> Remove Install Method
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
|
{" "}
|
||||||
|
Remove Install Method
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<Button type="button" size="sm" disabled={script.install_methods.length >= 2} onClick={addInstallMethod}>
|
<Button type="button" size="sm" disabled={script.install_methods.length >= 2} onClick={addInstallMethod}>
|
||||||
<PlusCircle className="mr-2 h-4 w-4" /> Add Install Method
|
<PlusCircle className="mr-2 h-4 w-4" />
|
||||||
|
{" "}
|
||||||
|
Add Install Method
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
159
frontend/src/app/json-editor/_components/note.tsx
Normal file
159
frontend/src/app/json-editor/_components/note.tsx
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
import type { z } from "zod";
|
||||||
|
|
||||||
|
import { PlusCircle, Trash2 } from "lucide-react";
|
||||||
|
import { memo, useCallback, useRef } from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { AlertColors } from "@/config/site-config";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
import type { Script } from "../_schemas/schemas";
|
||||||
|
|
||||||
|
import { ScriptSchema } from "../_schemas/schemas";
|
||||||
|
|
||||||
|
const NoteItem = memo(
|
||||||
|
({
|
||||||
|
note,
|
||||||
|
index,
|
||||||
|
updateNote,
|
||||||
|
removeNote,
|
||||||
|
}: {
|
||||||
|
note: Script["notes"][number];
|
||||||
|
index: number;
|
||||||
|
updateNote: (index: number, key: keyof Script["notes"][number], value: string) => void;
|
||||||
|
removeNote: (index: number) => void;
|
||||||
|
}) => {
|
||||||
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
|
const handleTextChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
updateNote(index, "text", e.target.value);
|
||||||
|
setTimeout(() => {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}, 0);
|
||||||
|
}, [index, updateNote]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2 border p-4 rounded">
|
||||||
|
<Input
|
||||||
|
placeholder="Note Text"
|
||||||
|
value={note.text}
|
||||||
|
onChange={handleTextChange}
|
||||||
|
ref={inputRef}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
value={note.type}
|
||||||
|
onValueChange={value => updateNote(index, "type", value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="flex-1">
|
||||||
|
<SelectValue placeholder="Type" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{Object.keys(AlertColors).map(type => (
|
||||||
|
<SelectItem key={type} value={type}>
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
{type.charAt(0).toUpperCase() + type.slice(1)}
|
||||||
|
{" "}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"size-4 rounded-full border",
|
||||||
|
AlertColors[type as keyof typeof AlertColors],
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="destructive"
|
||||||
|
type="button"
|
||||||
|
onClick={() => removeNote(index)}
|
||||||
|
>
|
||||||
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
|
{" "}
|
||||||
|
Remove Note
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
type NoteProps = {
|
||||||
|
script: Script;
|
||||||
|
setScript: (script: Script) => void;
|
||||||
|
setIsValid: (isValid: boolean) => void;
|
||||||
|
setZodErrors: (zodErrors: z.ZodError | null) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function Note({
|
||||||
|
script,
|
||||||
|
setScript,
|
||||||
|
setIsValid,
|
||||||
|
setZodErrors,
|
||||||
|
}: NoteProps) {
|
||||||
|
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
|
||||||
|
|
||||||
|
const addNote = useCallback(() => {
|
||||||
|
setScript({
|
||||||
|
...script,
|
||||||
|
notes: [...script.notes, { text: "", type: "" }],
|
||||||
|
});
|
||||||
|
}, [script, setScript]);
|
||||||
|
|
||||||
|
const updateNote = useCallback((
|
||||||
|
index: number,
|
||||||
|
key: keyof Script["notes"][number],
|
||||||
|
value: string,
|
||||||
|
) => {
|
||||||
|
const updated: Script = {
|
||||||
|
...script,
|
||||||
|
notes: script.notes.map((note, i) =>
|
||||||
|
i === index ? { ...note, [key]: value } : note,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
const result = ScriptSchema.safeParse(updated);
|
||||||
|
setIsValid(result.success);
|
||||||
|
setZodErrors(result.success ? null : result.error);
|
||||||
|
setScript(updated);
|
||||||
|
// Restore focus after state update
|
||||||
|
if (key === "text") {
|
||||||
|
setTimeout(() => {
|
||||||
|
inputRefs.current[index]?.focus();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}, [script, setScript, setIsValid, setZodErrors]);
|
||||||
|
|
||||||
|
const removeNote = useCallback((index: number) => {
|
||||||
|
setScript({
|
||||||
|
...script,
|
||||||
|
notes: script.notes.filter((_, i) => i !== index),
|
||||||
|
});
|
||||||
|
}, [script, setScript]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3 className="text-xl font-semibold">Notes</h3>
|
||||||
|
{script.notes.map((note, index) => (
|
||||||
|
<NoteItem key={index} note={note} index={index} updateNote={updateNote} removeNote={removeNote} />
|
||||||
|
))}
|
||||||
|
<Button type="button" size="sm" onClick={addNote}>
|
||||||
|
<PlusCircle className="mr-2 h-4 w-4" />
|
||||||
|
{" "}
|
||||||
|
Add Note
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
NoteItem.displayName = "NoteItem";
|
||||||
|
|
||||||
|
export default memo(Note);
|
@ -2,7 +2,7 @@ import { z } from "zod";
|
|||||||
|
|
||||||
export const InstallMethodSchema = z.object({
|
export const InstallMethodSchema = z.object({
|
||||||
type: z.enum(["default", "alpine"], {
|
type: z.enum(["default", "alpine"], {
|
||||||
errorMap: () => ({ message: "Type must be either 'default' or 'alpine'" })
|
errorMap: () => ({ message: "Type must be either 'default' or 'alpine'" }),
|
||||||
}),
|
}),
|
||||||
script: z.string().min(1, "Script content cannot be empty"),
|
script: z.string().min(1, "Script content cannot be empty"),
|
||||||
resources: z.object({
|
resources: z.object({
|
||||||
@ -25,7 +25,7 @@ export const ScriptSchema = z.object({
|
|||||||
categories: z.array(z.number()),
|
categories: z.array(z.number()),
|
||||||
date_created: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be in YYYY-MM-DD format").min(1, "Date is required"),
|
date_created: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be in YYYY-MM-DD format").min(1, "Date is required"),
|
||||||
type: z.enum(["vm", "ct", "pve", "addon", "turnkey"], {
|
type: z.enum(["vm", "ct", "pve", "addon", "turnkey"], {
|
||||||
errorMap: () => ({ message: "Type must be either 'vm', 'ct', 'pve', 'addon' or 'turnkey'" })
|
errorMap: () => ({ message: "Type must be either 'vm', 'ct', 'pve', 'addon' or 'turnkey'" }),
|
||||||
}),
|
}),
|
||||||
updateable: z.boolean(),
|
updateable: z.boolean(),
|
||||||
privileged: z.boolean(),
|
privileged: z.boolean(),
|
||||||
|
@ -1,26 +1,32 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
import type { z } from "zod";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Calendar } from "@/components/ui/calendar";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { fetchCategories } from "@/lib/data";
|
|
||||||
import { Category } from "@/lib/types";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { format } from "date-fns";
|
|
||||||
import { CalendarIcon, Check, Clipboard, Download } from "lucide-react";
|
import { CalendarIcon, Check, Clipboard, Download } from "lucide-react";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { format } from "date-fns";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
|
||||||
import Categories from "./_components/Categories";
|
import type { Category } from "@/lib/types";
|
||||||
import InstallMethod from "./_components/InstallMethod";
|
|
||||||
import Note from "./_components/Note";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { ScriptSchema, type Script } from "./_schemas/schemas";
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
|
import { Calendar } from "@/components/ui/calendar";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { fetchCategories } from "@/lib/data";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
import type { Script } from "./_schemas/schemas";
|
||||||
|
|
||||||
|
import InstallMethod from "./_components/install-method";
|
||||||
|
import { ScriptSchema } from "./_schemas/schemas";
|
||||||
|
import Categories from "./_components/categories";
|
||||||
|
import Note from "./_components/note";
|
||||||
|
|
||||||
const initialScript: Script = {
|
const initialScript: Script = {
|
||||||
name: "",
|
name: "",
|
||||||
@ -54,7 +60,7 @@ export default function JSONGenerator() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchCategories()
|
fetchCategories()
|
||||||
.then(setCategories)
|
.then(setCategories)
|
||||||
.catch((error) => console.error("Error fetching categories:", error));
|
.catch(error => console.error("Error fetching categories:", error));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const updateScript = useCallback((key: keyof Script, value: Script[keyof Script]) => {
|
const updateScript = useCallback((key: keyof Script, value: Script[keyof Script]) => {
|
||||||
@ -67,11 +73,14 @@ export default function JSONGenerator() {
|
|||||||
|
|
||||||
if (updated.type === "pve") {
|
if (updated.type === "pve") {
|
||||||
scriptPath = `tools/pve/${updated.slug}.sh`;
|
scriptPath = `tools/pve/${updated.slug}.sh`;
|
||||||
} else if (updated.type === "addon") {
|
}
|
||||||
|
else if (updated.type === "addon") {
|
||||||
scriptPath = `tools/addon/${updated.slug}.sh`;
|
scriptPath = `tools/addon/${updated.slug}.sh`;
|
||||||
} else if (method.type === "alpine") {
|
}
|
||||||
|
else if (method.type === "alpine") {
|
||||||
scriptPath = `${updated.type}/alpine-${updated.slug}.sh`;
|
scriptPath = `${updated.type}/alpine-${updated.slug}.sh`;
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
scriptPath = `${updated.type}/${updated.slug}.sh`;
|
scriptPath = `${updated.type}/${updated.slug}.sh`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,7 +145,10 @@ export default function JSONGenerator() {
|
|||||||
<div className="mt-2 space-y-1">
|
<div className="mt-2 space-y-1">
|
||||||
{zodErrors.errors.map((error, index) => (
|
{zodErrors.errors.map((error, index) => (
|
||||||
<AlertDescription key={index} className="p-1 text-red-500">
|
<AlertDescription key={index} className="p-1 text-red-500">
|
||||||
{error.path.join(".")} - {error.message}
|
{error.path.join(".")}
|
||||||
|
{" "}
|
||||||
|
-
|
||||||
|
{error.message}
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -154,25 +166,31 @@ export default function JSONGenerator() {
|
|||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<Label>
|
<Label>
|
||||||
Name <span className="text-red-500">*</span>
|
Name
|
||||||
|
{" "}
|
||||||
|
<span className="text-red-500">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input placeholder="Example" value={script.name} onChange={(e) => updateScript("name", e.target.value)} />
|
<Input placeholder="Example" value={script.name} onChange={e => updateScript("name", e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label>
|
<Label>
|
||||||
Slug <span className="text-red-500">*</span>
|
Slug
|
||||||
|
{" "}
|
||||||
|
<span className="text-red-500">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input placeholder="example" value={script.slug} onChange={(e) => updateScript("slug", e.target.value)} />
|
<Input placeholder="example" value={script.slug} onChange={e => updateScript("slug", e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label>
|
<Label>
|
||||||
Logo <span className="text-red-500">*</span>
|
Logo
|
||||||
|
{" "}
|
||||||
|
<span className="text-red-500">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Full logo URL"
|
placeholder="Full logo URL"
|
||||||
value={script.logo || ""}
|
value={script.logo || ""}
|
||||||
onChange={(e) => updateScript("logo", e.target.value || null)}
|
onChange={e => updateScript("logo", e.target.value || null)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -180,17 +198,19 @@ export default function JSONGenerator() {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Path to config file"
|
placeholder="Path to config file"
|
||||||
value={script.config_path || ""}
|
value={script.config_path || ""}
|
||||||
onChange={(e) => updateScript("config_path", e.target.value || null)}
|
onChange={e => updateScript("config_path", e.target.value || null)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label>
|
<Label>
|
||||||
Description <span className="text-red-500">*</span>
|
Description
|
||||||
|
{" "}
|
||||||
|
<span className="text-red-500">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder="Example"
|
placeholder="Example"
|
||||||
value={script.description}
|
value={script.description}
|
||||||
onChange={(e) => updateScript("description", e.target.value)}
|
onChange={e => updateScript("description", e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Categories script={script} setScript={setScript} categories={categories} />
|
<Categories script={script} setScript={setScript} categories={categories} />
|
||||||
@ -200,7 +220,7 @@ export default function JSONGenerator() {
|
|||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild className="flex-1">
|
<PopoverTrigger asChild className="flex-1">
|
||||||
<Button
|
<Button
|
||||||
variant={"outline"}
|
variant="outline"
|
||||||
className={cn("pl-3 text-left font-normal w-full", !script.date_created && "text-muted-foreground")}
|
className={cn("pl-3 text-left font-normal w-full", !script.date_created && "text-muted-foreground")}
|
||||||
>
|
>
|
||||||
{formattedDate || <span>Pick a date</span>}
|
{formattedDate || <span>Pick a date</span>}
|
||||||
@ -219,7 +239,7 @@ export default function JSONGenerator() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2 w-full">
|
<div className="flex flex-col gap-2 w-full">
|
||||||
<Label>Type</Label>
|
<Label>Type</Label>
|
||||||
<Select value={script.type} onValueChange={(value) => updateScript("type", value)}>
|
<Select value={script.type} onValueChange={value => updateScript("type", value)}>
|
||||||
<SelectTrigger className="flex-1">
|
<SelectTrigger className="flex-1">
|
||||||
<SelectValue placeholder="Type" />
|
<SelectValue placeholder="Type" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
@ -234,11 +254,11 @@ export default function JSONGenerator() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="w-full flex gap-5">
|
<div className="w-full flex gap-5">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Switch checked={script.updateable} onCheckedChange={(checked) => updateScript("updateable", checked)} />
|
<Switch checked={script.updateable} onCheckedChange={checked => updateScript("updateable", checked)} />
|
||||||
<label>Updateable</label>
|
<label>Updateable</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Switch checked={script.privileged} onCheckedChange={(checked) => updateScript("privileged", checked)} />
|
<Switch checked={script.privileged} onCheckedChange={checked => updateScript("privileged", checked)} />
|
||||||
<label>Privileged</label>
|
<label>Privileged</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -246,18 +266,18 @@ export default function JSONGenerator() {
|
|||||||
placeholder="Interface Port"
|
placeholder="Interface Port"
|
||||||
type="number"
|
type="number"
|
||||||
value={script.interface_port || ""}
|
value={script.interface_port || ""}
|
||||||
onChange={(e) => updateScript("interface_port", e.target.value ? Number(e.target.value) : null)}
|
onChange={e => updateScript("interface_port", e.target.value ? Number(e.target.value) : null)}
|
||||||
/>
|
/>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Website URL"
|
placeholder="Website URL"
|
||||||
value={script.website || ""}
|
value={script.website || ""}
|
||||||
onChange={(e) => updateScript("website", e.target.value || null)}
|
onChange={e => updateScript("website", e.target.value || null)}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Documentation URL"
|
placeholder="Documentation URL"
|
||||||
value={script.documentation || ""}
|
value={script.documentation || ""}
|
||||||
onChange={(e) => updateScript("documentation", e.target.value || null)}
|
onChange={e => updateScript("documentation", e.target.value || null)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<InstallMethod script={script} setScript={setScript} setIsValid={setIsValid} setZodErrors={setZodErrors} />
|
<InstallMethod script={script} setScript={setScript} setIsValid={setIsValid} setZodErrors={setZodErrors} />
|
||||||
@ -265,22 +285,20 @@ export default function JSONGenerator() {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Username"
|
placeholder="Username"
|
||||||
value={script.default_credentials.username || ""}
|
value={script.default_credentials.username || ""}
|
||||||
onChange={(e) =>
|
onChange={e =>
|
||||||
updateScript("default_credentials", {
|
updateScript("default_credentials", {
|
||||||
...script.default_credentials,
|
...script.default_credentials,
|
||||||
username: e.target.value || null,
|
username: e.target.value || null,
|
||||||
})
|
})}
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
value={script.default_credentials.password || ""}
|
value={script.default_credentials.password || ""}
|
||||||
onChange={(e) =>
|
onChange={e =>
|
||||||
updateScript("default_credentials", {
|
updateScript("default_credentials", {
|
||||||
...script.default_credentials,
|
...script.default_credentials,
|
||||||
password: e.target.value || null,
|
password: e.target.value || null,
|
||||||
})
|
})}
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<Note script={script} setScript={setScript} setIsValid={setIsValid} setZodErrors={setZodErrors} />
|
<Note script={script} setScript={setScript} setIsValid={setIsValid} setZodErrors={setZodErrors} />
|
||||||
</form>
|
</form>
|
||||||
|
@ -1,18 +1,20 @@
|
|||||||
import Footer from "@/components/Footer";
|
|
||||||
import Navbar from "@/components/Navbar";
|
|
||||||
import QueryProvider from "@/components/query-provider";
|
|
||||||
import { ThemeProvider } from "@/components/theme-provider";
|
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
|
||||||
import { analytics, basePath } from "@/config/siteConfig";
|
|
||||||
import "@/styles/globals.css";
|
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Inter } from "next/font/google";
|
|
||||||
import { NuqsAdapter } from "nuqs/adapters/next/app";
|
import { NuqsAdapter } from "nuqs/adapters/next/app";
|
||||||
|
import { Inter } from "next/font/google";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
import { ThemeProvider } from "@/components/theme-provider";
|
||||||
|
import { analytics, basePath } from "@/config/site-config";
|
||||||
|
import "@/styles/globals.css";
|
||||||
|
import QueryProvider from "@/components/query-provider";
|
||||||
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
|
import Footer from "@/components/footer";
|
||||||
|
import Navbar from "@/components/navbar";
|
||||||
|
|
||||||
const inter = Inter({ subsets: ["latin"] });
|
const inter = Inter({ subsets: ["latin"] });
|
||||||
|
|
||||||
export const metadata : Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Proxmox VE Helper-Scripts",
|
title: "Proxmox VE Helper-Scripts",
|
||||||
description:
|
description:
|
||||||
"The official website for the Proxmox VE Helper-Scripts (Community) Repository. Featuring over 300+ scripts to help you manage your Proxmox VE environment.",
|
"The official website for the Proxmox VE Helper-Scripts (Community) Repository. Featuring over 300+ scripts to help you manage your Proxmox VE environment.",
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { basePath } from "@/config/siteConfig";
|
|
||||||
import type { MetadataRoute } from "next";
|
import type { MetadataRoute } from "next";
|
||||||
|
|
||||||
export const generateStaticParams = () => {
|
import { basePath } from "@/config/site-config";
|
||||||
|
|
||||||
|
export function generateStaticParams() {
|
||||||
return [];
|
return [];
|
||||||
};
|
}
|
||||||
|
|
||||||
export default function manifest(): MetadataRoute.Manifest {
|
export default function manifest(): MetadataRoute.Manifest {
|
||||||
return {
|
return {
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import FAQ from "@/components/FAQ";
|
import { ArrowRightIcon, ExternalLink } from "lucide-react";
|
||||||
import AnimatedGradientText from "@/components/ui/animated-gradient-text";
|
import { useEffect, useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { FaGithub } from "react-icons/fa";
|
||||||
import { CardFooter } from "@/components/ui/card";
|
import { useTheme } from "next-themes";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@ -11,15 +13,14 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import Particles from "@/components/ui/particles";
|
import AnimatedGradientText from "@/components/ui/animated-gradient-text";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { basePath } from "@/config/siteConfig";
|
import { CardFooter } from "@/components/ui/card";
|
||||||
|
import Particles from "@/components/ui/particles";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { basePath } from "@/config/site-config";
|
||||||
|
import FAQ from "@/components/faq";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { ArrowRightIcon, ExternalLink } from "lucide-react";
|
|
||||||
import { useTheme } from "next-themes";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { FaGithub } from "react-icons/fa";
|
|
||||||
|
|
||||||
function CustomArrowRightIcon() {
|
function CustomArrowRightIcon() {
|
||||||
return <ArrowRightIcon className="h-4 w-4" width={1} />;
|
return <ArrowRightIcon className="h-4 w-4" width={1} />;
|
||||||
@ -50,7 +51,9 @@ export default function Page() {
|
|||||||
`p-px ![mask-composite:subtract]`,
|
`p-px ![mask-composite:subtract]`,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
❤️ <Separator className="mx-2 h-4" orientation="vertical" />
|
❤️
|
||||||
|
{" "}
|
||||||
|
<Separator className="mx-2 h-4" orientation="vertical" />
|
||||||
<span
|
<span
|
||||||
className={cn(
|
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`,
|
`animate-gradient bg-gradient-to-r from-[#ffaa40] via-[#9c40ff] to-[#ffaa40] bg-[length:var(--bg-size)_100%] bg-clip-text text-transparent`,
|
||||||
@ -78,7 +81,9 @@ export default function Page() {
|
|||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex items-center justify-center"
|
className="flex items-center justify-center"
|
||||||
>
|
>
|
||||||
<FaGithub className="mr-2 h-4 w-4" /> Tteck's GitHub
|
<FaGithub className="mr-2 h-4 w-4" />
|
||||||
|
{" "}
|
||||||
|
Tteck's GitHub
|
||||||
</a>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
<Button className="w-full" asChild>
|
<Button className="w-full" asChild>
|
||||||
@ -88,7 +93,9 @@ export default function Page() {
|
|||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex items-center justify-center"
|
className="flex items-center justify-center"
|
||||||
>
|
>
|
||||||
<ExternalLink className="mr-2 h-4 w-4" /> Proxmox Helper Scripts
|
<ExternalLink className="mr-2 h-4 w-4" />
|
||||||
|
{" "}
|
||||||
|
Proxmox Helper Scripts
|
||||||
</a>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
@ -104,7 +111,10 @@ export default function Page() {
|
|||||||
We are a community-driven initiative that simplifies the setup of Proxmox Virtual Environment (VE).
|
We are a community-driven initiative that simplifies the setup of Proxmox Virtual Environment (VE).
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
With 300+ scripts to help you manage your <b>Proxmox VE environment</b>. Whether you're a seasoned
|
With 300+ scripts to help you manage your
|
||||||
|
{" "}
|
||||||
|
<b>Proxmox VE environment</b>
|
||||||
|
. Whether you're a seasoned
|
||||||
user or a newcomer, we've got you covered.
|
user or a newcomer, we've got you covered.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { basePath } from "@/config/siteConfig";
|
|
||||||
import type { MetadataRoute } from "next";
|
import type { MetadataRoute } from "next";
|
||||||
|
|
||||||
|
import { basePath } from "@/config/site-config";
|
||||||
|
|
||||||
export const dynamic = "force-static";
|
export const dynamic = "force-static";
|
||||||
|
|
||||||
export default function robots(): MetadataRoute.Robots {
|
export default function robots(): MetadataRoute.Robots {
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
import handleCopy from "@/components/handleCopy";
|
|
||||||
import { buttonVariants } from "@/components/ui/button";
|
|
||||||
import { Script } from "@/lib/types";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { ClipboardIcon } from "lucide-react";
|
|
||||||
|
|
||||||
export default function InterFaces({ item }: { item: Script }) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-2 w-full">
|
|
||||||
{item.interface_port !== null ? (
|
|
||||||
<div className="flex items-center justify-end">
|
|
||||||
<h2 className="mr-2 text-end text-lg font-semibold">Default Interface:</h2>
|
|
||||||
<span className={cn(buttonVariants({ size: "sm", variant: "outline" }), "flex items-center gap-2")}>
|
|
||||||
{item.interface_port}
|
|
||||||
<ClipboardIcon onClick={() => handleCopy("default interface", String(item.interface_port))} className="size-4 cursor-pointer" />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,51 +1,84 @@
|
|||||||
import CodeCopyButton from "@/components/ui/code-copy-button";
|
import { Info } from "lucide-react";
|
||||||
|
|
||||||
|
import type { Script } from "@/lib/types";
|
||||||
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
import { Info } from "lucide-react";
|
import CodeCopyButton from "@/components/ui/code-copy-button";
|
||||||
import { basePath } from "@/config/siteConfig";
|
import { basePath } from "@/config/site-config";
|
||||||
import { Script } from "@/lib/types";
|
|
||||||
import { getDisplayValueFromType } from "../ScriptInfoBlocks";
|
|
||||||
|
|
||||||
const getInstallCommand = (scriptPath = "", isAlpine = false, useGitea = false) => {
|
import { getDisplayValueFromType } from "../script-info-blocks";
|
||||||
|
|
||||||
|
function getInstallCommand(scriptPath = "", isAlpine = false, useGitea = false) {
|
||||||
const githubUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main/${scriptPath}`;
|
const githubUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main/${scriptPath}`;
|
||||||
const giteaUrl = `https://git.community-scripts.org/community-scripts/${basePath}/raw/branch/main/${scriptPath}`;
|
const giteaUrl = `https://git.community-scripts.org/community-scripts/${basePath}/raw/branch/main/${scriptPath}`;
|
||||||
const url = useGitea ? giteaUrl : githubUrl;
|
const url = useGitea ? giteaUrl : githubUrl;
|
||||||
return `bash -c "$(curl -fsSL ${url})"`;
|
return isAlpine ? `bash -c "$(curl -fsSL ${url})"` : `bash -c "$(curl -fsSL ${url})"`;
|
||||||
};
|
}
|
||||||
|
|
||||||
export default function InstallCommand({ item }: { item: Script }) {
|
export default function InstallCommand({ item }: { item: Script }) {
|
||||||
const alpineScript = item.install_methods.find((method) => method.type === "alpine");
|
const alpineScript = item.install_methods.find(method => method.type === "alpine");
|
||||||
const defaultScript = item.install_methods.find((method) => method.type === "default");
|
const defaultScript = item.install_methods.find(method => method.type === "default");
|
||||||
|
|
||||||
const renderInstructions = (isAlpine = false) => (
|
const renderInstructions = (isAlpine = false) => (
|
||||||
<>
|
<>
|
||||||
<p className="text-sm mt-2">
|
<p className="text-sm mt-2">
|
||||||
{isAlpine ? (
|
{isAlpine
|
||||||
<>
|
? (
|
||||||
As an alternative option, you can use Alpine Linux and the {item.name} package to create a {item.name}{" "}
|
<>
|
||||||
{getDisplayValueFromType(item.type)} container with faster creation time and minimal system resource usage.
|
As an alternative option, you can use Alpine Linux and the
|
||||||
You are also obliged to adhere to updates provided by the package maintainer.
|
{" "}
|
||||||
</>
|
{item.name}
|
||||||
) : item.type === "pve" ? (
|
{" "}
|
||||||
<>
|
package to create a
|
||||||
To use the {item.name} script, run the command below **only** in the Proxmox VE Shell. This script is
|
{" "}
|
||||||
intended for managing or enhancing the host system directly.
|
{item.name}
|
||||||
</>
|
{" "}
|
||||||
) : item.type === "addon" ? (
|
{getDisplayValueFromType(item.type)}
|
||||||
<>
|
{" "}
|
||||||
This script enhances an existing setup. You can use it inside a running LXC container or directly on the
|
container with faster creation time and minimal system resource usage.
|
||||||
Proxmox VE host to extend functionality with {item.name}.
|
You are also obliged to adhere to updates provided by the package maintainer.
|
||||||
</>
|
</>
|
||||||
) : (
|
)
|
||||||
<>
|
: item.type === "pve"
|
||||||
To create a new Proxmox VE {item.name} {getDisplayValueFromType(item.type)}, run the command below in the
|
? (
|
||||||
Proxmox VE Shell.
|
<>
|
||||||
</>
|
To use the
|
||||||
)}
|
{" "}
|
||||||
|
{item.name}
|
||||||
|
{" "}
|
||||||
|
script, run the command below **only** in the Proxmox VE Shell. This script is
|
||||||
|
intended for managing or enhancing the host system directly.
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
: item.type === "addon"
|
||||||
|
? (
|
||||||
|
<>
|
||||||
|
This script enhances an existing setup. You can use it inside a running LXC container or directly on the
|
||||||
|
Proxmox VE host to extend functionality with
|
||||||
|
{" "}
|
||||||
|
{item.name}
|
||||||
|
.
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<>
|
||||||
|
To create a new Proxmox VE
|
||||||
|
{" "}
|
||||||
|
{item.name}
|
||||||
|
{" "}
|
||||||
|
{getDisplayValueFromType(item.type)}
|
||||||
|
, run the command below in the
|
||||||
|
Proxmox VE Shell.
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
{isAlpine && (
|
{isAlpine && (
|
||||||
<p className="mt-2 text-sm">
|
<p className="mt-2 text-sm">
|
||||||
To create a new Proxmox VE Alpine-{item.name} {getDisplayValueFromType(item.type)}, run the command below in
|
To create a new Proxmox VE Alpine-
|
||||||
|
{item.name}
|
||||||
|
{" "}
|
||||||
|
{getDisplayValueFromType(item.type)}
|
||||||
|
, run the command below in
|
||||||
the Proxmox VE Shell.
|
the Proxmox VE Shell.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@ -56,7 +89,9 @@ export default function InstallCommand({ item }: { item: Script }) {
|
|||||||
<Alert className="mt-3 mb-3">
|
<Alert className="mt-3 mb-3">
|
||||||
<Info className="h-4 w-4" />
|
<Info className="h-4 w-4" />
|
||||||
<AlertDescription className="text-sm">
|
<AlertDescription className="text-sm">
|
||||||
<strong>When to use Gitea:</strong> GitHub may have issues including slow connections, delayed updates after bug
|
<strong>When to use Gitea:</strong>
|
||||||
|
{" "}
|
||||||
|
GitHub may have issues including slow connections, delayed updates after bug
|
||||||
fixes, no IPv6 support, API rate limits (60/hour). Use our Gitea mirror as a reliable alternative when
|
fixes, no IPv6 support, API rate limits (60/hour). Use our Gitea mirror as a reliable alternative when
|
||||||
experiencing these issues.
|
experiencing these issues.
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
@ -81,7 +116,8 @@ export default function InstallCommand({ item }: { item: Script }) {
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
);
|
);
|
||||||
} else if (defaultScript?.script) {
|
}
|
||||||
|
else if (defaultScript?.script) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{renderInstructions()}
|
{renderInstructions()}
|
||||||
@ -109,4 +145,4 @@ export default function InstallCommand({ item }: { item: Script }) {
|
|||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -1,43 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import type { Category, Script } from "@/lib/types";
|
|
||||||
import ScriptAccordion from "./ScriptAccordion";
|
|
||||||
|
|
||||||
const Sidebar = ({
|
|
||||||
items,
|
|
||||||
selectedScript,
|
|
||||||
setSelectedScript,
|
|
||||||
}: {
|
|
||||||
items: Category[];
|
|
||||||
selectedScript: string | null;
|
|
||||||
setSelectedScript: (script: string | null) => void;
|
|
||||||
}) => {
|
|
||||||
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-[350px] flex-col sm:max-w-[350px]">
|
|
||||||
<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;
|
|
@ -1,17 +1,17 @@
|
|||||||
import { CPUIcon, HDDIcon, RAMIcon } from "@/components/icons/resource-icons";
|
import { CPUIcon, HDDIcon, RAMIcon } from "@/components/icons/resource-icons";
|
||||||
import { getDisplayValueFromRAM } from "@/lib/utils/resource-utils";
|
import { getDisplayValueFromRAM } from "@/lib/utils/resource-utils";
|
||||||
|
|
||||||
interface ResourceDisplayProps {
|
type ResourceDisplayProps = {
|
||||||
title: string;
|
title: string;
|
||||||
cpu: number | null;
|
cpu: number | null;
|
||||||
ram: number | null;
|
ram: number | null;
|
||||||
hdd: number | null;
|
hdd: number | null;
|
||||||
}
|
};
|
||||||
|
|
||||||
interface IconTextProps {
|
type IconTextProps = {
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
label: string;
|
label: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
function IconText({ icon, label }: IconTextProps) {
|
function IconText({ icon, label }: IconTextProps) {
|
||||||
return (
|
return (
|
||||||
@ -27,7 +27,8 @@ export function ResourceDisplay({ title, cpu, ram, hdd }: ResourceDisplayProps)
|
|||||||
const hasRAM = typeof ram === "number" && ram > 0;
|
const hasRAM = typeof ram === "number" && ram > 0;
|
||||||
const hasHDD = typeof hdd === "number" && hdd > 0;
|
const hasHDD = typeof hdd === "number" && hdd > 0;
|
||||||
|
|
||||||
if (!hasCPU && !hasRAM && !hasHDD) return null;
|
if (!hasCPU && !hasRAM && !hasHDD)
|
||||||
|
return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
@ -1,18 +1,18 @@
|
|||||||
import { useCallback, useEffect, useRef } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import type { Category } from "@/lib/types";
|
||||||
|
|
||||||
import { formattedBadge } from "@/components/CommandMenu";
|
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
AccordionContent,
|
AccordionContent,
|
||||||
AccordionItem,
|
AccordionItem,
|
||||||
AccordionTrigger,
|
AccordionTrigger,
|
||||||
} from "@/components/ui/accordion";
|
} from "@/components/ui/accordion";
|
||||||
import { Category } from "@/lib/types";
|
import { formattedBadge } from "@/components/command-menu";
|
||||||
|
import { basePath } from "@/config/site-config";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { basePath } from "@/config/siteConfig";
|
|
||||||
|
|
||||||
export default function ScriptAccordion({
|
export default function ScriptAccordion({
|
||||||
items,
|
items,
|
||||||
@ -41,8 +41,8 @@ export default function ScriptAccordion({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedScript) {
|
if (selectedScript) {
|
||||||
const category = items.find((category) =>
|
const category = items.find(category =>
|
||||||
category.scripts.some((script) => script.slug === selectedScript),
|
category.scripts.some(script => script.slug === selectedScript),
|
||||||
);
|
);
|
||||||
if (category) {
|
if (category) {
|
||||||
setExpandedItem(category.name);
|
setExpandedItem(category.name);
|
||||||
@ -58,11 +58,11 @@ export default function ScriptAccordion({
|
|||||||
collapsible
|
collapsible
|
||||||
className="overflow-y-scroll max-h-[calc(100vh-225px)] overflow-x-hidden p-2"
|
className="overflow-y-scroll max-h-[calc(100vh-225px)] overflow-x-hidden p-2"
|
||||||
>
|
>
|
||||||
{items.map((category) => (
|
{items.map(category => (
|
||||||
<AccordionItem
|
<AccordionItem
|
||||||
key={category.id + ":category"}
|
key={`${category.id}:category`}
|
||||||
value={category.name}
|
value={category.name}
|
||||||
className={cn("sm:text-md flex flex-col border-none", {
|
className={cn("sm:text-sm flex flex-col border-none", {
|
||||||
"rounded-lg bg-accent/30": expandedItem === category.name,
|
"rounded-lg bg-accent/30": expandedItem === category.name,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
@ -72,11 +72,15 @@ export default function ScriptAccordion({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="mr-2 flex w-full items-center justify-between">
|
<div className="mr-2 flex w-full items-center justify-between">
|
||||||
<span className="pl-2 text-left">{category.name} </span>
|
<span className="pl-2 text-left">
|
||||||
|
{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">
|
<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.scripts.length}
|
{category.scripts.length}
|
||||||
</span>
|
</span>
|
||||||
</div>{" "}
|
</div>
|
||||||
|
{" "}
|
||||||
</AccordionTrigger>
|
</AccordionTrigger>
|
||||||
<AccordionContent
|
<AccordionContent
|
||||||
data-state={expandedItem === category.name ? "open" : "closed"}
|
data-state={expandedItem === category.name ? "open" : "closed"}
|
||||||
@ -109,10 +113,9 @@ export default function ScriptAccordion({
|
|||||||
height={16}
|
height={16}
|
||||||
width={16}
|
width={16}
|
||||||
unoptimized
|
unoptimized
|
||||||
onError={(e) =>
|
onError={e =>
|
||||||
((e.currentTarget as HTMLImageElement).src =
|
((e.currentTarget as HTMLImageElement).src
|
||||||
`/${basePath}/logo.png`)
|
= `/${basePath}/logo.png`)}
|
||||||
}
|
|
||||||
alt={script.name}
|
alt={script.name}
|
||||||
className="mr-1 w-4 h-4 rounded-full"
|
className="mr-1 w-4 h-4 rounded-full"
|
||||||
/>
|
/>
|
@ -1,16 +1,18 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { basePath, mostPopularScripts } from "@/config/siteConfig";
|
|
||||||
import { extractDate } from "@/lib/time";
|
|
||||||
import { Category, Script } from "@/lib/types";
|
|
||||||
import { CalendarPlus } from "lucide-react";
|
import { CalendarPlus } from "lucide-react";
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useMemo, useState } from "react";
|
|
||||||
|
import type { Category, Script } from "@/lib/types";
|
||||||
|
|
||||||
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { basePath, mostPopularScripts } from "@/config/site-config";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { extractDate } from "@/lib/time";
|
||||||
|
|
||||||
const ITEMS_PER_PAGE = 3;
|
const ITEMS_PER_PAGE = 3;
|
||||||
|
|
||||||
export const getDisplayValueFromType = (type: string) => {
|
export function getDisplayValueFromType(type: string) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "ct":
|
case "ct":
|
||||||
return "LXC";
|
return "LXC";
|
||||||
@ -22,15 +24,16 @@ export const getDisplayValueFromType = (type: string) => {
|
|||||||
default:
|
default:
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export function LatestScripts({ items }: { items: Category[] }) {
|
export function LatestScripts({ items }: { items: Category[] }) {
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
|
|
||||||
const latestScripts = useMemo(() => {
|
const latestScripts = useMemo(() => {
|
||||||
if (!items) return [];
|
if (!items)
|
||||||
|
return [];
|
||||||
|
|
||||||
const scripts = items.flatMap((category) => category.scripts || []);
|
const scripts = items.flatMap(category => category.scripts || []);
|
||||||
|
|
||||||
// Filter out duplicates by slug
|
// Filter out duplicates by slug
|
||||||
const uniqueScriptsMap = new Map<string, Script>();
|
const uniqueScriptsMap = new Map<string, Script>();
|
||||||
@ -46,11 +49,11 @@ export function LatestScripts({ items }: { items: Category[] }) {
|
|||||||
}, [items]);
|
}, [items]);
|
||||||
|
|
||||||
const goToNextPage = () => {
|
const goToNextPage = () => {
|
||||||
setPage((prevPage) => prevPage + 1);
|
setPage(prevPage => prevPage + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const goToPreviousPage = () => {
|
const goToPreviousPage = () => {
|
||||||
setPage((prevPage) => prevPage - 1);
|
setPage(prevPage => prevPage - 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const startIndex = (page - 1) * ITEMS_PER_PAGE;
|
const startIndex = (page - 1) * ITEMS_PER_PAGE;
|
||||||
@ -80,7 +83,7 @@ export function LatestScripts({ items }: { items: Category[] }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="min-w flex w-full flex-row flex-wrap gap-4">
|
<div className="min-w flex w-full flex-row flex-wrap gap-4">
|
||||||
{latestScripts.slice(startIndex, endIndex).map((script) => (
|
{latestScripts.slice(startIndex, endIndex).map(script => (
|
||||||
<Card key={script.slug} className="min-w-[250px] flex-1 flex-grow bg-accent/30">
|
<Card key={script.slug} className="min-w-[250px] flex-1 flex-grow bg-accent/30">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-3">
|
<CardTitle className="flex items-center gap-3">
|
||||||
@ -91,13 +94,15 @@ export function LatestScripts({ items }: { items: Category[] }) {
|
|||||||
height={64}
|
height={64}
|
||||||
width={64}
|
width={64}
|
||||||
alt=""
|
alt=""
|
||||||
onError={(e) => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)}
|
onError={e => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)}
|
||||||
className="h-11 w-11 object-contain"
|
className="h-11 w-11 object-contain"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<p className="text-lg line-clamp-1">
|
<p className="text-lg line-clamp-1">
|
||||||
{script.name} {getDisplayValueFromType(script.type)}
|
{script.name}
|
||||||
|
{" "}
|
||||||
|
{getDisplayValueFromType(script.type)}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-muted-foreground flex items-center gap-1">
|
<p className="text-sm text-muted-foreground flex items-center gap-1">
|
||||||
<CalendarPlus className="h-4 w-4" />
|
<CalendarPlus className="h-4 w-4" />
|
||||||
@ -130,7 +135,7 @@ export function LatestScripts({ items }: { items: Category[] }) {
|
|||||||
|
|
||||||
export function MostViewedScripts({ items }: { items: Category[] }) {
|
export function MostViewedScripts({ items }: { items: Category[] }) {
|
||||||
const mostViewedScripts = items.reduce((acc: Script[], category) => {
|
const mostViewedScripts = items.reduce((acc: Script[], category) => {
|
||||||
const foundScripts = category.scripts.filter((script) => mostPopularScripts.includes(script.slug));
|
const foundScripts = category.scripts.filter(script => mostPopularScripts.includes(script.slug));
|
||||||
return acc.concat(foundScripts);
|
return acc.concat(foundScripts);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -142,7 +147,7 @@ export function MostViewedScripts({ items }: { items: Category[] }) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div className="min-w flex w-full flex-row flex-wrap gap-4">
|
<div className="min-w flex w-full flex-row flex-wrap gap-4">
|
||||||
{mostViewedScripts.map((script) => (
|
{mostViewedScripts.map(script => (
|
||||||
<Card key={script.slug} className="min-w-[250px] flex-1 flex-grow bg-accent/30">
|
<Card key={script.slug} className="min-w-[250px] flex-1 flex-grow bg-accent/30">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-3">
|
<CardTitle className="flex items-center gap-3">
|
||||||
@ -153,13 +158,15 @@ export function MostViewedScripts({ items }: { items: Category[] }) {
|
|||||||
height={64}
|
height={64}
|
||||||
width={64}
|
width={64}
|
||||||
alt=""
|
alt=""
|
||||||
onError={(e) => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)}
|
onError={e => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)}
|
||||||
className="h-11 w-11 object-contain"
|
className="h-11 w-11 object-contain"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<p className="line-clamp-1 text-lg">
|
<p className="line-clamp-1 text-lg">
|
||||||
{script.name} {getDisplayValueFromType(script.type)}
|
{script.name}
|
||||||
|
{" "}
|
||||||
|
{getDisplayValueFromType(script.type)}
|
||||||
</p>
|
</p>
|
||||||
<p className="flex items-center gap-1 text-sm text-muted-foreground">
|
<p className="flex items-center gap-1 text-sm text-muted-foreground">
|
||||||
<CalendarPlus className="h-4 w-4" />
|
<CalendarPlus className="h-4 w-4" />
|
@ -1,31 +1,32 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { extractDate } from "@/lib/time";
|
|
||||||
import { AppVersion, Script } from "@/lib/types";
|
|
||||||
|
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
|
import { Suspense } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
import { Separator } from "@/components/ui/separator";
|
import type { AppVersion, Script } from "@/lib/types";
|
||||||
import { basePath } from "@/config/siteConfig";
|
|
||||||
import { useVersions } from "@/hooks/useVersions";
|
|
||||||
import { cleanSlug } from "@/lib/utils/resource-utils";
|
|
||||||
import { Suspense } from "react";
|
|
||||||
import { ResourceDisplay } from "./ResourceDisplay";
|
|
||||||
import { getDisplayValueFromType } from "./ScriptInfoBlocks";
|
|
||||||
import Alerts from "./ScriptItems/Alerts";
|
|
||||||
import Buttons from "./ScriptItems/Buttons";
|
|
||||||
import ConfigFile from "./ScriptItems/ConfigFile";
|
|
||||||
import DefaultPassword from "./ScriptItems/DefaultPassword";
|
|
||||||
import Description from "./ScriptItems/Description";
|
|
||||||
import InstallCommand from "./ScriptItems/InstallCommand";
|
|
||||||
import InterFaces from "./ScriptItems/InterFaces";
|
|
||||||
import Tooltips from "./ScriptItems/Tooltips";
|
|
||||||
|
|
||||||
interface ScriptItemProps {
|
import { cleanSlug } from "@/lib/utils/resource-utils";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { useVersions } from "@/hooks/use-versions";
|
||||||
|
import { basePath } from "@/config/site-config";
|
||||||
|
import { extractDate } from "@/lib/time";
|
||||||
|
|
||||||
|
import { getDisplayValueFromType } from "./script-info-blocks";
|
||||||
|
import DefaultPassword from "./script-items/default-password";
|
||||||
|
import InstallCommand from "./script-items/install-command";
|
||||||
|
import { ResourceDisplay } from "./resource-display";
|
||||||
|
import Description from "./script-items/description";
|
||||||
|
import ConfigFile from "./script-items/config-file";
|
||||||
|
import InterFaces from "./script-items/interfaces";
|
||||||
|
import Tooltips from "./script-items/tool-tips";
|
||||||
|
import Buttons from "./script-items/buttons";
|
||||||
|
import Alerts from "./script-items/alerts";
|
||||||
|
|
||||||
|
type ScriptItemProps = {
|
||||||
item: Script;
|
item: Script;
|
||||||
setSelectedScript: (script: string | null) => void;
|
setSelectedScript: (script: string | null) => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
function ScriptHeader({ item }: { item: Script }) {
|
function ScriptHeader({ item }: { item: Script }) {
|
||||||
const defaultInstallMethod = item.install_methods?.[0];
|
const defaultInstallMethod = item.install_methods?.[0];
|
||||||
@ -40,7 +41,7 @@ function ScriptHeader({ item }: { item: Script }) {
|
|||||||
className="h-32 w-32 rounded-xl bg-gradient-to-br from-accent/40 to-accent/60 object-contain p-3 shadow-lg transition-transform hover:scale-105"
|
className="h-32 w-32 rounded-xl bg-gradient-to-br from-accent/40 to-accent/60 object-contain p-3 shadow-lg transition-transform hover:scale-105"
|
||||||
src={item.logo || `/${basePath}/logo.png`}
|
src={item.logo || `/${basePath}/logo.png`}
|
||||||
width={400}
|
width={400}
|
||||||
onError={(e) => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)}
|
onError={e => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)}
|
||||||
height={400}
|
height={400}
|
||||||
alt={item.name}
|
alt={item.name}
|
||||||
unoptimized
|
unoptimized
|
||||||
@ -58,10 +59,15 @@ function ScriptHeader({ item }: { item: Script }) {
|
|||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
<div className="mt-1 flex items-center gap-3 text-sm text-muted-foreground">
|
<div className="mt-1 flex items-center gap-3 text-sm text-muted-foreground">
|
||||||
<span>Added {extractDate(item.date_created)}</span>
|
<span>
|
||||||
|
Added
|
||||||
|
{extractDate(item.date_created)}
|
||||||
|
</span>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
<span className=" capitalize">
|
<span className=" capitalize">
|
||||||
{os} {version}
|
{os}
|
||||||
|
{" "}
|
||||||
|
{version}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -76,10 +82,10 @@ function ScriptHeader({ item }: { item: Script }) {
|
|||||||
hdd={defaultInstallMethod.resources.hdd}
|
hdd={defaultInstallMethod.resources.hdd}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{item.install_methods.find((method) => method.type === "alpine")?.resources && (
|
{item.install_methods.find(method => method.type === "alpine")?.resources && (
|
||||||
<ResourceDisplay
|
<ResourceDisplay
|
||||||
title="Alpine"
|
title="Alpine"
|
||||||
{...item.install_methods.find((method) => method.type === "alpine")!.resources!}
|
{...item.install_methods.find(method => method.type === "alpine")!.resources!}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -108,7 +114,8 @@ function VersionInfo({ item }: { item: Script }) {
|
|||||||
return cleanName === cleanSlug(item.slug) || cleanName.includes(cleanSlug(item.slug));
|
return cleanName === cleanSlug(item.slug) || cleanName.includes(cleanSlug(item.slug));
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!matchedVersion) return null;
|
if (!matchedVersion)
|
||||||
|
return null;
|
||||||
|
|
||||||
return <span className="font-medium text-sm">{matchedVersion.version}</span>;
|
return <span className="font-medium text-sm">{matchedVersion.version}</span>;
|
||||||
}
|
}
|
||||||
@ -132,7 +139,7 @@ export function ScriptItem({ item, setSelectedScript }: ScriptItemProps) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-xl border border-border bg-gradient-to-b from-card/30 to-background/50 backdrop-blur-sm shadow-sm">
|
<div className="rounded-xl border border-border bg-accent/30 backdrop-blur-sm shadow-sm">
|
||||||
<div className="p-6 space-y-6">
|
<div className="p-6 space-y-6">
|
||||||
<Suspense fallback={<div className="animate-pulse h-32 bg-accent/20 rounded-xl" />}>
|
<Suspense fallback={<div className="animate-pulse h-32 bg-accent/20 rounded-xl" />}>
|
||||||
<ScriptHeader item={item} />
|
<ScriptHeader item={item} />
|
||||||
@ -144,7 +151,9 @@ export function ScriptItem({ item, setSelectedScript }: ScriptItemProps) {
|
|||||||
<div className="mt-4 rounded-lg border shadow-sm">
|
<div className="mt-4 rounded-lg border shadow-sm">
|
||||||
<div className="flex gap-3 px-4 py-2 bg-accent/25">
|
<div className="flex gap-3 px-4 py-2 bg-accent/25">
|
||||||
<h2 className="text-lg font-semibold">
|
<h2 className="text-lg font-semibold">
|
||||||
How to {item.type === "pve" ? "use" : item.type === "addon" ? "apply" : "install"}
|
How to
|
||||||
|
{" "}
|
||||||
|
{item.type === "pve" ? "use" : item.type === "addon" ? "apply" : "install"}
|
||||||
</h2>
|
</h2>
|
||||||
<Tooltips item={item} />
|
<Tooltips item={item} />
|
||||||
</div>
|
</div>
|
@ -1,19 +1,21 @@
|
|||||||
import TextCopyBlock from "@/components/TextCopyBlock";
|
|
||||||
import { AlertColors } from "@/config/siteConfig";
|
|
||||||
import { Script } from "@/lib/types";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { AlertCircle, NotepadText } from "lucide-react";
|
import { AlertCircle, NotepadText } from "lucide-react";
|
||||||
|
|
||||||
|
import type { Script } from "@/lib/types";
|
||||||
|
|
||||||
|
import TextCopyBlock from "@/components/text-copy-block";
|
||||||
|
import { AlertColors } from "@/config/site-config";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type NoteProps = {
|
type NoteProps = {
|
||||||
text: string;
|
text: string;
|
||||||
type: keyof typeof AlertColors;
|
type: keyof typeof AlertColors;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default function Alerts({ item }: { item: Script }) {
|
export default function Alerts({ item }: { item: Script }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{item?.notes?.length > 0 &&
|
{item?.notes?.length > 0
|
||||||
item.notes.map((note: NoteProps, index: number) => (
|
&& item.notes.map((note: NoteProps, index: number) => (
|
||||||
<div key={index} className="mt-4 flex flex-col shadow-sm gap-2">
|
<div key={index} className="mt-4 flex flex-col shadow-sm gap-2">
|
||||||
<p
|
<p
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -21,11 +23,13 @@ export default function Alerts({ item }: { item: Script }) {
|
|||||||
AlertColors[note.type],
|
AlertColors[note.type],
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{note.type == "info" ? (
|
{note.type === "info"
|
||||||
<NotepadText className="h-4 min-h-4 w-4 min-w-4" />
|
? (
|
||||||
) : (
|
<NotepadText className="h-4 min-h-4 w-4 min-w-4" />
|
||||||
<AlertCircle className="h-4 min-h-4 w-4 min-w-4" />
|
)
|
||||||
)}
|
: (
|
||||||
|
<AlertCircle className="h-4 min-h-4 w-4 min-w-4" />
|
||||||
|
)}
|
||||||
<span>{TextCopyBlock(note.text)}</span>
|
<span>{TextCopyBlock(note.text)}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
@ -1,20 +1,22 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
import { BookOpenText, Code, Globe, LinkIcon, RefreshCcw } from "lucide-react";
|
||||||
|
|
||||||
|
import type { Script } from "@/lib/types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { basePath } from "@/config/siteConfig";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Script } from "@/lib/types";
|
import { basePath } from "@/config/site-config";
|
||||||
import { BookOpenText, Code, Globe, LinkIcon, RefreshCcw } from "lucide-react";
|
|
||||||
|
|
||||||
const generateInstallSourceUrl = (slug: string) => {
|
function generateInstallSourceUrl(slug: string) {
|
||||||
const baseUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main`;
|
const baseUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main`;
|
||||||
return `${baseUrl}/install/${slug}-install.sh`;
|
return `${baseUrl}/install/${slug}-install.sh`;
|
||||||
};
|
}
|
||||||
|
|
||||||
const generateSourceUrl = (slug: string, type: string) => {
|
function generateSourceUrl(slug: string, type: string) {
|
||||||
const baseUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main`;
|
const baseUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main`;
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@ -29,18 +31,18 @@ const generateSourceUrl = (slug: string, type: string) => {
|
|||||||
default:
|
default:
|
||||||
return `${baseUrl}/ct/${slug}.sh`; // fallback for "ct"
|
return `${baseUrl}/ct/${slug}.sh`; // fallback for "ct"
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const generateUpdateUrl = (slug: string) => {
|
function generateUpdateUrl(slug: string) {
|
||||||
const baseUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main`;
|
const baseUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main`;
|
||||||
return `${baseUrl}/ct/${slug}.sh`;
|
return `${baseUrl}/ct/${slug}.sh`;
|
||||||
};
|
}
|
||||||
|
|
||||||
interface LinkItem {
|
type LinkItem = {
|
||||||
href: string;
|
href: string;
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
text: string;
|
text: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default function Buttons({ item }: { item: Script }) {
|
export default function Buttons({ item }: { item: Script }) {
|
||||||
const isCtOrDefault = ["ct"].includes(item.type);
|
const isCtOrDefault = ["ct"].includes(item.type);
|
||||||
@ -76,7 +78,8 @@ export default function Buttons({ item }: { item: Script }) {
|
|||||||
},
|
},
|
||||||
].filter(Boolean) as LinkItem[];
|
].filter(Boolean) as LinkItem[];
|
||||||
|
|
||||||
if (links.length === 0) return null;
|
if (links.length === 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
@ -1,13 +1,15 @@
|
|||||||
import handleCopy from "@/components/handleCopy";
|
import type { Script } from "@/lib/types";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Script } from "@/lib/types";
|
import handleCopy from "@/components/handle-copy";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
export default function DefaultPassword({ item }: { item: Script }) {
|
export default function DefaultPassword({ item }: { item: Script }) {
|
||||||
const { username, password } = item.default_credentials;
|
const { username, password } = item.default_credentials;
|
||||||
const hasDefaultLogin = username || password;
|
const hasDefaultLogin = username || password;
|
||||||
|
|
||||||
if (!hasDefaultLogin) return null;
|
if (!hasDefaultLogin)
|
||||||
|
return null;
|
||||||
|
|
||||||
const copyCredential = (type: "username" | "password") => {
|
const copyCredential = (type: "username" | "password") => {
|
||||||
handleCopy(type, item.default_credentials[type] ?? "");
|
handleCopy(type, item.default_credentials[type] ?? "");
|
||||||
@ -21,18 +23,27 @@ export default function DefaultPassword({ item }: { item: Script }) {
|
|||||||
<Separator className="w-full" />
|
<Separator className="w-full" />
|
||||||
<div className="flex flex-col gap-2 p-4">
|
<div className="flex flex-col gap-2 p-4">
|
||||||
<p className="mb-2 text-sm">
|
<p className="mb-2 text-sm">
|
||||||
You can use the following credentials to login to the {item.name} {item.type}.
|
You can use the following credentials to login to the
|
||||||
|
{" "}
|
||||||
|
{item.name}
|
||||||
|
{" "}
|
||||||
|
{item.type}
|
||||||
|
.
|
||||||
</p>
|
</p>
|
||||||
{["username", "password"].map((type) => {
|
{["username", "password"].map((type) => {
|
||||||
const value = item.default_credentials[type as "username" | "password"];
|
const value = item.default_credentials[type as "username" | "password"];
|
||||||
return value && value.trim() !== "" ? (
|
return value && value.trim() !== ""
|
||||||
<div key={type} className="text-sm">
|
? (
|
||||||
{type.charAt(0).toUpperCase() + type.slice(1)}:{" "}
|
<div key={type} className="text-sm">
|
||||||
<Button variant="secondary" size="null" onClick={() => copyCredential(type as "username" | "password")}>
|
{type.charAt(0).toUpperCase() + type.slice(1)}
|
||||||
{value}
|
:
|
||||||
</Button>
|
{" "}
|
||||||
</div>
|
<Button variant="secondary" size="null" onClick={() => copyCredential(type as "username" | "password")}>
|
||||||
) : null;
|
{value}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
: null;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@ -1,4 +1,4 @@
|
|||||||
import { Script } from "@/lib/types";
|
import type { Script } from "@/lib/types";
|
||||||
|
|
||||||
export default function DefaultSettings({ item }: { item: Script }) {
|
export default function DefaultSettings({ item }: { item: Script }) {
|
||||||
const getDisplayValueFromRAM = (ram: number) => (ram >= 1024 ? `${Math.floor(ram / 1024)}GB` : `${ram}MB`);
|
const getDisplayValueFromRAM = (ram: number) => (ram >= 1024 ? `${Math.floor(ram / 1024)}GB` : `${ram}MB`);
|
||||||
@ -8,15 +8,26 @@ export default function DefaultSettings({ item }: { item: Script }) {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-md font-semibold">{title}</h2>
|
<h2 className="text-md font-semibold">{title}</h2>
|
||||||
<p className="text-sm text-muted-foreground">CPU: {cpu}vCPU</p>
|
<p className="text-sm text-muted-foreground">
|
||||||
<p className="text-sm text-muted-foreground">RAM: {getDisplayValueFromRAM(ram ?? 0)}</p>
|
CPU:
|
||||||
<p className="text-sm text-muted-foreground">HDD: {hdd}GB</p>
|
{cpu}
|
||||||
|
vCPU
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
RAM:
|
||||||
|
{getDisplayValueFromRAM(ram ?? 0)}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
HDD:
|
||||||
|
{hdd}
|
||||||
|
GB
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultSettings = item.install_methods.find((method) => method.type === "default");
|
const defaultSettings = item.install_methods.find(method => method.type === "default");
|
||||||
const defaultAlpineSettings = item.install_methods.find((method) => method.type === "alpine");
|
const defaultAlpineSettings = item.install_methods.find(method => method.type === "alpine");
|
||||||
|
|
||||||
const hasDefaultSettings = defaultSettings?.resources && Object.values(defaultSettings.resources).some(Boolean);
|
const hasDefaultSettings = defaultSettings?.resources && Object.values(defaultSettings.resources).some(Boolean);
|
||||||
|
|
@ -1,5 +1,6 @@
|
|||||||
import TextCopyBlock from "@/components/TextCopyBlock";
|
import type { Script } from "@/lib/types";
|
||||||
import { Script } from "@/lib/types";
|
|
||||||
|
import TextCopyBlock from "@/components/text-copy-block";
|
||||||
|
|
||||||
export default function Description({ item }: { item: Script }) {
|
export default function Description({ item }: { item: Script }) {
|
||||||
return (
|
return (
|
@ -0,0 +1,149 @@
|
|||||||
|
import { Info } from "lucide-react";
|
||||||
|
|
||||||
|
import type { Script } from "@/lib/types";
|
||||||
|
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
|
import CodeCopyButton from "@/components/ui/code-copy-button";
|
||||||
|
import { basePath } from "@/config/site-config";
|
||||||
|
|
||||||
|
import { getDisplayValueFromType } from "../script-info-blocks";
|
||||||
|
|
||||||
|
function getInstallCommand(scriptPath = "", isAlpine = false, useGitea = false) {
|
||||||
|
const githubUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main/${scriptPath}`;
|
||||||
|
const giteaUrl = `https://git.community-scripts.org/community-scripts/${basePath}/raw/branch/main/${scriptPath}`;
|
||||||
|
const url = useGitea ? giteaUrl : githubUrl;
|
||||||
|
return isAlpine ? `bash -c "$(curl -fsSL ${url})"` : `bash -c "$(curl -fsSL ${url})"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function InstallCommand({ item }: { item: Script }) {
|
||||||
|
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
|
||||||
|
{" "}
|
||||||
|
{item.name}
|
||||||
|
{" "}
|
||||||
|
package to create a
|
||||||
|
{" "}
|
||||||
|
{item.name}
|
||||||
|
{" "}
|
||||||
|
{getDisplayValueFromType(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 === "pve"
|
||||||
|
? (
|
||||||
|
<>
|
||||||
|
To use the
|
||||||
|
{" "}
|
||||||
|
{item.name}
|
||||||
|
{" "}
|
||||||
|
script, run the command below **only** in the Proxmox VE Shell. This script is
|
||||||
|
intended for managing or enhancing the host system directly.
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
: item.type === "addon"
|
||||||
|
? (
|
||||||
|
<>
|
||||||
|
This script enhances an existing setup. You can use it inside a running LXC container or directly on the
|
||||||
|
Proxmox VE host to extend functionality with
|
||||||
|
{" "}
|
||||||
|
{item.name}
|
||||||
|
.
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<>
|
||||||
|
To create a new Proxmox VE
|
||||||
|
{" "}
|
||||||
|
{item.name}
|
||||||
|
{" "}
|
||||||
|
{getDisplayValueFromType(item.type)}
|
||||||
|
, run the command below in the
|
||||||
|
Proxmox VE Shell.
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
{isAlpine && (
|
||||||
|
<p className="mt-2 text-sm">
|
||||||
|
To create a new Proxmox VE Alpine-
|
||||||
|
{item.name}
|
||||||
|
{" "}
|
||||||
|
{getDisplayValueFromType(item.type)}
|
||||||
|
, run the command below in
|
||||||
|
the Proxmox VE Shell.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderGiteaInfo = () => (
|
||||||
|
<Alert className="mt-3 mb-3">
|
||||||
|
<Info className="h-4 w-4" />
|
||||||
|
<AlertDescription className="text-sm">
|
||||||
|
<strong>When to use Gitea:</strong>
|
||||||
|
{" "}
|
||||||
|
GitHub may have issues including slow connections, delayed updates after bug
|
||||||
|
fixes, no IPv6 support, API rate limits (60/hour). Use our Gitea mirror as a reliable alternative when
|
||||||
|
experiencing these issues.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderScriptTabs = (useGitea = false) => {
|
||||||
|
if (alpineScript) {
|
||||||
|
return (
|
||||||
|
<Tabs defaultValue="default" className="mt-2 w-full max-w-4xl">
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value="default">Default</TabsTrigger>
|
||||||
|
<TabsTrigger value="alpine">Alpine Linux</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent value="default">
|
||||||
|
{renderInstructions()}
|
||||||
|
<CodeCopyButton>{getInstallCommand(defaultScript?.script, false, useGitea)}</CodeCopyButton>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="alpine">
|
||||||
|
{renderInstructions(true)}
|
||||||
|
<CodeCopyButton>{getInstallCommand(alpineScript.script, true, useGitea)}</CodeCopyButton>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else if (defaultScript?.script) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{renderInstructions()}
|
||||||
|
<CodeCopyButton>{getInstallCommand(defaultScript.script, false, useGitea)}</CodeCopyButton>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4">
|
||||||
|
<Tabs defaultValue="github" className="w-full max-w-4xl">
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value="github">GitHub</TabsTrigger>
|
||||||
|
<TabsTrigger value="gitea">Gitea</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent value="github">
|
||||||
|
{renderScriptTabs(false)}
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="gitea">
|
||||||
|
{renderGiteaInfo()}
|
||||||
|
{renderScriptTabs(true)}
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
import { ClipboardIcon } from "lucide-react";
|
||||||
|
|
||||||
|
import type { Script } from "@/lib/types";
|
||||||
|
|
||||||
|
import { buttonVariants } from "@/components/ui/button";
|
||||||
|
import handleCopy from "@/components/handle-copy";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export default function InterFaces({ item }: { item: Script }) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2 w-full">
|
||||||
|
{item.interface_port !== null
|
||||||
|
? (
|
||||||
|
<div className="flex items-center justify-end">
|
||||||
|
<h2 className="mr-2 text-end text-lg font-semibold">Default Interface:</h2>
|
||||||
|
<span className={cn(buttonVariants({ size: "sm", variant: "outline" }), "flex items-center gap-2")}>
|
||||||
|
{item.interface_port}
|
||||||
|
<ClipboardIcon onClick={() => handleCopy("default interface", String(item.interface_port))} className="size-4 cursor-pointer" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,22 +1,27 @@
|
|||||||
import { Badge, type BadgeProps } from "@/components/ui/badge";
|
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
|
||||||
import { Script } from "@/lib/types";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { CircleHelp } from "lucide-react";
|
import { CircleHelp } from "lucide-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
interface TooltipProps {
|
import type { BadgeProps } from "@/components/ui/badge";
|
||||||
|
import type { Script } from "@/lib/types";
|
||||||
|
|
||||||
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
type TooltipProps = {
|
||||||
variant: BadgeProps["variant"];
|
variant: BadgeProps["variant"];
|
||||||
label: string;
|
label: string;
|
||||||
content?: string;
|
content?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
const TooltipBadge: React.FC<TooltipProps> = ({ variant, label, content }) => (
|
const TooltipBadge: React.FC<TooltipProps> = ({ variant, label, content }) => (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip delayDuration={100}>
|
<Tooltip delayDuration={100}>
|
||||||
<TooltipTrigger className={cn("flex items-center", !content && "cursor-default")}>
|
<TooltipTrigger className={cn("flex items-center", !content && "cursor-default")}>
|
||||||
<Badge variant={variant} className="flex items-center gap-1">
|
<Badge variant={variant} className="flex items-center gap-1">
|
||||||
{label} {content && <CircleHelp className="size-3" />}
|
{label}
|
||||||
|
{" "}
|
||||||
|
{content && <CircleHelp className="size-3" />}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
{content && (
|
{content && (
|
46
frontend/src/app/scripts/_components/sidebar.tsx
Normal file
46
frontend/src/app/scripts/_components/sidebar.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { Category, Script } from "@/lib/types";
|
||||||
|
|
||||||
|
import ScriptAccordion from "./script-accordion";
|
||||||
|
|
||||||
|
function Sidebar({
|
||||||
|
items,
|
||||||
|
selectedScript,
|
||||||
|
setSelectedScript,
|
||||||
|
}: {
|
||||||
|
items: Category[];
|
||||||
|
selectedScript: string | null;
|
||||||
|
setSelectedScript: (script: string | null) => void;
|
||||||
|
}) {
|
||||||
|
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-[350px] flex-col sm:max-w-[350px]">
|
||||||
|
<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;
|
@ -1,8 +1,8 @@
|
|||||||
import { AppVersion } from "@/lib/types";
|
import type { AppVersion } from "@/lib/types";
|
||||||
|
|
||||||
interface VersionBadgeProps {
|
type VersionBadgeProps = {
|
||||||
version: AppVersion;
|
version: AppVersion;
|
||||||
}
|
};
|
||||||
|
|
||||||
export function VersionBadge({ version }: VersionBadgeProps) {
|
export function VersionBadge({ version }: VersionBadgeProps) {
|
||||||
return (
|
return (
|
@ -1,18 +1,20 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
import { Suspense, useEffect, useState } from "react";
|
||||||
export const dynamic = "force-static";
|
|
||||||
|
|
||||||
import { ScriptItem } from "@/app/scripts/_components/ScriptItem";
|
|
||||||
import { fetchCategories } from "@/lib/data";
|
|
||||||
import { Category, Script } from "@/lib/types";
|
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { useQueryState } from "nuqs";
|
import { useQueryState } from "nuqs";
|
||||||
import { Suspense, useEffect, useState } from "react";
|
|
||||||
|
import type { Category, Script } from "@/lib/types";
|
||||||
|
|
||||||
|
import { ScriptItem } from "@/app/scripts/_components/script-item";
|
||||||
|
import { fetchCategories } from "@/lib/data";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
LatestScripts,
|
LatestScripts,
|
||||||
MostViewedScripts,
|
MostViewedScripts,
|
||||||
} from "./_components/ScriptInfoBlocks";
|
} from "./_components/script-info-blocks";
|
||||||
import Sidebar from "./_components/Sidebar";
|
import Sidebar from "./_components/sidebar";
|
||||||
|
|
||||||
|
export const dynamic = "force-static";
|
||||||
|
|
||||||
function ScriptContent() {
|
function ScriptContent() {
|
||||||
const [selectedScript, setSelectedScript] = useQueryState("id");
|
const [selectedScript, setSelectedScript] = useQueryState("id");
|
||||||
@ -22,9 +24,9 @@ function ScriptContent() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedScript && links.length > 0) {
|
if (selectedScript && links.length > 0) {
|
||||||
const script = links
|
const script = links
|
||||||
.map((category) => category.scripts)
|
.map(category => category.scripts)
|
||||||
.flat()
|
.flat()
|
||||||
.find((script) => script.slug === selectedScript);
|
.find(script => script.slug === selectedScript);
|
||||||
setItem(script);
|
setItem(script);
|
||||||
}
|
}
|
||||||
}, [selectedScript, links]);
|
}, [selectedScript, links]);
|
||||||
@ -34,7 +36,7 @@ function ScriptContent() {
|
|||||||
.then((categories) => {
|
.then((categories) => {
|
||||||
setLinks(categories);
|
setLinks(categories);
|
||||||
})
|
})
|
||||||
.catch((error) => console.error(error));
|
.catch(error => console.error(error));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -48,14 +50,16 @@ function ScriptContent() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-4 w-full sm:mx-0 sm:ml-4">
|
<div className="mx-4 w-full sm:mx-0 sm:ml-4">
|
||||||
{selectedScript && item ? (
|
{selectedScript && item
|
||||||
<ScriptItem item={item} setSelectedScript={setSelectedScript} />
|
? (
|
||||||
) : (
|
<ScriptItem item={item} setSelectedScript={setSelectedScript} />
|
||||||
<div className="flex w-full flex-col gap-5">
|
)
|
||||||
<LatestScripts items={links} />
|
: (
|
||||||
<MostViewedScripts items={links} />
|
<div className="flex w-full flex-col gap-5">
|
||||||
</div>
|
<LatestScripts items={links} />
|
||||||
)}
|
<MostViewedScripts items={links} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -65,13 +69,13 @@ function ScriptContent() {
|
|||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
return (
|
||||||
<Suspense
|
<Suspense
|
||||||
fallback={
|
fallback={(
|
||||||
<div className="flex h-screen w-full flex-col items-center justify-center gap-5 bg-background px-4 md:px-6">
|
<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">
|
<div className="space-y-2 text-center">
|
||||||
<Loader2 className="h-10 w-10 animate-spin" />
|
<Loader2 className="h-10 w-10 animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
>
|
>
|
||||||
<ScriptContent />
|
<ScriptContent />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { basePath } from "@/config/siteConfig";
|
|
||||||
import type { MetadataRoute } from "next";
|
import type { MetadataRoute } from "next";
|
||||||
|
|
||||||
|
import { basePath } from "@/config/site-config";
|
||||||
|
|
||||||
export const dynamic = "force-static";
|
export const dynamic = "force-static";
|
||||||
|
|
||||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||||
let domain = "community-scripts.github.io";
|
const domain = "community-scripts.github.io";
|
||||||
let protocol = "https";
|
const protocol = "https";
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
url: `${protocol}://${domain}/${basePath}`,
|
url: `${protocol}://${domain}/${basePath}`,
|
||||||
@ -18,6 +19,6 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|||||||
{
|
{
|
||||||
url: `${protocol}://${domain}/${basePath}/json-editor`,
|
url: `${protocol}://${domain}/${basePath}/json-editor`,
|
||||||
lastModified: new Date(),
|
lastModified: new Date(),
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -1,86 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
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";
|
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
|
||||||
|
|
||||||
function Navbar() {
|
|
||||||
const [isScrolled, setIsScrolled] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleScroll = () => {
|
|
||||||
setIsScrolled(window.scrollY > 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("scroll", handleScroll);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("scroll", handleScroll);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
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-[1440px] 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 VE 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,12 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { ArcElement, Chart as ChartJS, Tooltip as ChartTooltip, Legend } from "chart.js";
|
||||||
import {
|
import ChartDataLabels from "chartjs-plugin-datalabels";
|
||||||
Dialog,
|
import { BarChart3, PieChart } from "lucide-react";
|
||||||
DialogContent,
|
import React, { useState } from "react";
|
||||||
DialogHeader,
|
import { Pie } from "react-chartjs-2";
|
||||||
DialogTitle,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@ -21,21 +20,23 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { Chart as ChartJS, ArcElement, Tooltip as ChartTooltip, Legend } from "chart.js";
|
import {
|
||||||
import ChartDataLabels from "chartjs-plugin-datalabels";
|
Dialog,
|
||||||
import { BarChart3, PieChart } from "lucide-react";
|
DialogContent,
|
||||||
import React, { useState } from "react";
|
DialogHeader,
|
||||||
import { Pie, Bar } from "react-chartjs-2";
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
ChartJS.register(ArcElement, ChartTooltip, Legend, ChartDataLabels);
|
ChartJS.register(ArcElement, ChartTooltip, Legend, ChartDataLabels);
|
||||||
|
|
||||||
interface SummaryData {
|
type SummaryData = {
|
||||||
nsapp_count: Record<string, number>;
|
nsapp_count: Record<string, number>;
|
||||||
}
|
};
|
||||||
|
|
||||||
interface ApplicationChartProps {
|
type ApplicationChartProps = {
|
||||||
data: SummaryData | null;
|
data: SummaryData | null;
|
||||||
}
|
};
|
||||||
|
|
||||||
const ITEMS_PER_PAGE = 20;
|
const ITEMS_PER_PAGE = 20;
|
||||||
const CHART_COLORS = [
|
const CHART_COLORS = [
|
||||||
@ -61,14 +62,15 @@ export default function ApplicationChart({ data }: ApplicationChartProps) {
|
|||||||
const [chartStartIndex, setChartStartIndex] = useState(0);
|
const [chartStartIndex, setChartStartIndex] = useState(0);
|
||||||
const [tableLimit, setTableLimit] = useState(ITEMS_PER_PAGE);
|
const [tableLimit, setTableLimit] = useState(ITEMS_PER_PAGE);
|
||||||
|
|
||||||
if (!data) return null;
|
if (!data)
|
||||||
|
return null;
|
||||||
|
|
||||||
const sortedApps = Object.entries(data.nsapp_count)
|
const sortedApps = Object.entries(data.nsapp_count)
|
||||||
.sort(([, a], [, b]) => b - a);
|
.sort(([, a], [, b]) => b - a);
|
||||||
|
|
||||||
const chartApps = sortedApps.slice(
|
const chartApps = sortedApps.slice(
|
||||||
chartStartIndex,
|
chartStartIndex,
|
||||||
chartStartIndex + ITEMS_PER_PAGE
|
chartStartIndex + ITEMS_PER_PAGE,
|
||||||
);
|
);
|
||||||
|
|
||||||
const chartData = {
|
const chartData = {
|
||||||
@ -141,14 +143,18 @@ export default function ApplicationChart({ data }: ApplicationChartProps) {
|
|||||||
onClick={() => setChartStartIndex(Math.max(0, chartStartIndex - ITEMS_PER_PAGE))}
|
onClick={() => setChartStartIndex(Math.max(0, chartStartIndex - ITEMS_PER_PAGE))}
|
||||||
disabled={chartStartIndex === 0}
|
disabled={chartStartIndex === 0}
|
||||||
>
|
>
|
||||||
Previous {ITEMS_PER_PAGE}
|
Previous
|
||||||
|
{" "}
|
||||||
|
{ITEMS_PER_PAGE}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setChartStartIndex(chartStartIndex + ITEMS_PER_PAGE)}
|
onClick={() => setChartStartIndex(chartStartIndex + ITEMS_PER_PAGE)}
|
||||||
disabled={chartStartIndex + ITEMS_PER_PAGE >= sortedApps.length}
|
disabled={chartStartIndex + ITEMS_PER_PAGE >= sortedApps.length}
|
||||||
>
|
>
|
||||||
Next {ITEMS_PER_PAGE}
|
Next
|
||||||
|
{" "}
|
||||||
|
{ITEMS_PER_PAGE}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
@ -190,4 +196,4 @@ export default function ApplicationChart({ data }: ApplicationChartProps) {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -1,3 +1,10 @@
|
|||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { Sparkles } from "lucide-react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import type { Category, Script } from "@/lib/types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CommandDialog,
|
CommandDialog,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
@ -6,22 +13,16 @@ import {
|
|||||||
CommandItem,
|
CommandItem,
|
||||||
CommandList,
|
CommandList,
|
||||||
} from "@/components/ui/command";
|
} from "@/components/ui/command";
|
||||||
import { basePath } from "@/config/siteConfig";
|
import { basePath } from "@/config/site-config";
|
||||||
import { fetchCategories } from "@/lib/data";
|
import { fetchCategories } from "@/lib/data";
|
||||||
import { Category, Script } from "@/lib/types";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import Image from "next/image";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import React from "react";
|
|
||||||
import { Badge } from "./ui/badge";
|
|
||||||
import { Button } from "./ui/button";
|
|
||||||
import { DialogTitle } from "./ui/dialog";
|
|
||||||
import { Sparkles } from "lucide-react";
|
|
||||||
import { TooltipContent, TooltipProvider } from "./ui/tooltip";
|
|
||||||
import { TooltipTrigger } from "./ui/tooltip";
|
|
||||||
import { Tooltip } from "./ui/tooltip";
|
|
||||||
|
|
||||||
export const formattedBadge = (type: string) => {
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip";
|
||||||
|
import { DialogTitle } from "./ui/dialog";
|
||||||
|
import { Button } from "./ui/button";
|
||||||
|
import { Badge } from "./ui/badge";
|
||||||
|
|
||||||
|
export function formattedBadge(type: string) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "vm":
|
case "vm":
|
||||||
return <Badge className="text-blue-500/75 border-blue-500/75">VM</Badge>;
|
return <Badge className="text-blue-500/75 border-blue-500/75">VM</Badge>;
|
||||||
@ -33,12 +34,13 @@ export const formattedBadge = (type: string) => {
|
|||||||
return <Badge className="text-green-500/75 border-green-500/75">ADDON</Badge>;
|
return <Badge className="text-green-500/75 border-green-500/75">ADDON</Badge>;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
}
|
||||||
|
|
||||||
// random Script
|
// random Script
|
||||||
function getRandomScript(categories: Category[]): Script | null {
|
function getRandomScript(categories: Category[]): Script | null {
|
||||||
const allScripts = categories.flatMap((cat) => cat.scripts || []);
|
const allScripts = categories.flatMap(cat => cat.scripts || []);
|
||||||
if (allScripts.length === 0) return null;
|
if (allScripts.length === 0)
|
||||||
|
return null;
|
||||||
const idx = Math.floor(Math.random() * allScripts.length);
|
const idx = Math.floor(Math.random() * allScripts.length);
|
||||||
return allScripts[idx];
|
return allScripts[idx];
|
||||||
}
|
}
|
||||||
@ -49,18 +51,6 @@ export default function CommandMenu() {
|
|||||||
const [isLoading, setIsLoading] = React.useState(false);
|
const [isLoading, setIsLoading] = React.useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const down = (e: KeyboardEvent) => {
|
|
||||||
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
|
||||||
e.preventDefault();
|
|
||||||
fetchSortedCategories();
|
|
||||||
setOpen((open) => !open);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener("keydown", down);
|
|
||||||
return () => document.removeEventListener("keydown", down);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const fetchSortedCategories = () => {
|
const fetchSortedCategories = () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
fetchCategories()
|
fetchCategories()
|
||||||
@ -74,6 +64,18 @@ export default function CommandMenu() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const down = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
||||||
|
e.preventDefault();
|
||||||
|
fetchSortedCategories();
|
||||||
|
setOpen(open => !open);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("keydown", down);
|
||||||
|
return () => document.removeEventListener("keydown", down);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const openRandomScript = async () => {
|
const openRandomScript = async () => {
|
||||||
if (links.length === 0) {
|
if (links.length === 0) {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@ -84,10 +86,12 @@ export default function CommandMenu() {
|
|||||||
if (randomScript) {
|
if (randomScript) {
|
||||||
router.push(`/scripts?id=${randomScript.slug}`);
|
router.push(`/scripts?id=${randomScript.slug}`);
|
||||||
}
|
}
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
const randomScript = getRandomScript(links);
|
const randomScript = getRandomScript(links);
|
||||||
if (randomScript) {
|
if (randomScript) {
|
||||||
router.push(`/scripts?id=${randomScript.slug}`);
|
router.push(`/scripts?id=${randomScript.slug}`);
|
||||||
@ -110,7 +114,8 @@ export default function CommandMenu() {
|
|||||||
>
|
>
|
||||||
<span className="inline-flex">Search scripts...</span>
|
<span className="inline-flex">Search scripts...</span>
|
||||||
<kbd className="pointer-events-none absolute right-[0.3rem] top-[0.45rem] hidden h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium opacity-100 sm:flex">
|
<kbd className="pointer-events-none absolute right-[0.3rem] top-[0.45rem] hidden h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium opacity-100 sm:flex">
|
||||||
<span className="text-xs">⌘</span>K
|
<span className="text-xs">⌘</span>
|
||||||
|
K
|
||||||
</kbd>
|
</kbd>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
@ -134,54 +139,34 @@ export default function CommandMenu() {
|
|||||||
<CommandInput placeholder="Search for a script..." />
|
<CommandInput placeholder="Search for a script..." />
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty>{isLoading ? "Loading..." : "No scripts found."}</CommandEmpty>
|
<CommandEmpty>{isLoading ? "Loading..." : "No scripts found."}</CommandEmpty>
|
||||||
{(() => {
|
{links.map(category => (
|
||||||
// Track seen scripts globally to avoid duplicates across all categories
|
<CommandGroup key={`category:${category.name}`} heading={category.name}>
|
||||||
const globalSeenScripts = new Set<string>();
|
{category.scripts.map(script => (
|
||||||
|
<CommandItem
|
||||||
return links.map((category) => {
|
key={`script:${script.slug}`}
|
||||||
const uniqueScripts = category.scripts.filter((script) => {
|
value={`${script.slug}-${script.name}`}
|
||||||
if (globalSeenScripts.has(script.slug)) {
|
onSelect={() => {
|
||||||
return false;
|
setOpen(false);
|
||||||
}
|
router.push(`/scripts?id=${script.slug}`);
|
||||||
globalSeenScripts.add(script.slug);
|
}}
|
||||||
return true;
|
>
|
||||||
});
|
<div className="flex gap-2" onClick={() => setOpen(false)}>
|
||||||
|
<Image
|
||||||
// Only render category if it has unique scripts
|
src={script.logo || `/${basePath}/logo.png`}
|
||||||
if (uniqueScripts.length === 0) {
|
onError={e => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)}
|
||||||
return null;
|
unoptimized
|
||||||
}
|
width={16}
|
||||||
|
height={16}
|
||||||
return (
|
alt=""
|
||||||
<CommandGroup key={`category:${category.name}`} heading={category.name}>
|
className="h-5 w-5"
|
||||||
{uniqueScripts.map((script) => (
|
/>
|
||||||
<CommandItem
|
<span>{script.name}</span>
|
||||||
key={`script:${script.slug}`}
|
<span>{formattedBadge(script.type)}</span>
|
||||||
value={`${script.slug}-${script.name}`}
|
</div>
|
||||||
onSelect={() => {
|
</CommandItem>
|
||||||
setOpen(false);
|
))}
|
||||||
router.push(`/scripts?id=${script.slug}`);
|
</CommandGroup>
|
||||||
}}
|
))}
|
||||||
>
|
|
||||||
<div className="flex gap-2" onClick={() => setOpen(false)}>
|
|
||||||
<Image
|
|
||||||
src={script.logo || `/${basePath}/logo.png`}
|
|
||||||
onError={(e) => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)}
|
|
||||||
unoptimized
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
alt=""
|
|
||||||
className="h-5 w-5"
|
|
||||||
/>
|
|
||||||
<span>{script.name}</span>
|
|
||||||
<span>{formattedBadge(script.type)}</span>
|
|
||||||
</div>
|
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
</CommandGroup>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
})()}
|
|
||||||
</CommandList>
|
</CommandList>
|
||||||
</CommandDialog>
|
</CommandDialog>
|
||||||
</>
|
</>
|
@ -1,7 +1,8 @@
|
|||||||
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||||
import { Plus } from "lucide-react";
|
import { Plus } from "lucide-react";
|
||||||
import { FAQ_Items } from "../config/faqConfig";
|
|
||||||
import { Accordion, AccordionContent, AccordionItem } from "./ui/accordion";
|
import { Accordion, AccordionContent, AccordionItem } from "./ui/accordion";
|
||||||
|
import { FAQ_Items } from "../config/faq-config";
|
||||||
|
|
||||||
export default function FAQ() {
|
export default function FAQ() {
|
||||||
return (
|
return (
|
@ -1,16 +1,19 @@
|
|||||||
import { basePath } from "@/config/siteConfig";
|
import { FileJson, Server } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { FileJson, Server, ExternalLink } from "lucide-react";
|
|
||||||
import { buttonVariants } from "./ui/button";
|
import { basePath } from "@/config/site-config";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
import { buttonVariants } from "./ui/button";
|
||||||
|
|
||||||
export default function Footer() {
|
export default function Footer() {
|
||||||
return (
|
return (
|
||||||
<div className="supports-backdrop-blur:bg-background/90 mt-auto border-t w-full flex justify-between border-border bg-background/40 py-4 backdrop-blur-lg">
|
<div className="supports-backdrop-blur:bg-background/90 mt-auto border-t w-full flex justify-between border-border bg-background/40 py-4 backdrop-blur-lg">
|
||||||
<div className="mx-6 w-full flex justify-between text-xs sm:text-sm text-muted-foreground">
|
<div className="mx-6 w-full flex justify-between text-xs sm:text-sm text-muted-foreground">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<p>
|
<p>
|
||||||
Website built by the community. The source code is available on{" "}
|
Website built by the community. The source code is available on
|
||||||
|
{" "}
|
||||||
<Link
|
<Link
|
||||||
href={`https://github.com/community-scripts/${basePath}/tree/main/frontend`}
|
href={`https://github.com/community-scripts/${basePath}/tree/main/frontend`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@ -28,13 +31,17 @@ export default function Footer() {
|
|||||||
href="/json-editor"
|
href="/json-editor"
|
||||||
className={cn(buttonVariants({ variant: "link" }), "text-muted-foreground flex items-center gap-2")}
|
className={cn(buttonVariants({ variant: "link" }), "text-muted-foreground flex items-center gap-2")}
|
||||||
>
|
>
|
||||||
<FileJson className="h-4 w-4" /> JSON Editor
|
<FileJson className="h-4 w-4" />
|
||||||
|
{" "}
|
||||||
|
JSON Editor
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href="/data"
|
href="/data"
|
||||||
className={cn(buttonVariants({ variant: "link" }), "text-muted-foreground flex items-center gap-2")}
|
className={cn(buttonVariants({ variant: "link" }), "text-muted-foreground flex items-center gap-2")}
|
||||||
>
|
>
|
||||||
<Server className="h-4 w-4" /> API Data
|
<Server className="h-4 w-4" />
|
||||||
|
{" "}
|
||||||
|
API Data
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@ -2,14 +2,15 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
interface ModalProps {
|
type ModalProps = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
};
|
||||||
|
|
||||||
const Modal: React.FC<ModalProps> = ({ isOpen, onClose, children }) => {
|
const Modal: React.FC<ModalProps> = ({ isOpen, onClose, children }) => {
|
||||||
if (!isOpen) return null;
|
if (!isOpen)
|
||||||
|
return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center z-50">
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center z-50">
|
86
frontend/src/components/navbar.tsx
Normal file
86
frontend/src/components/navbar.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
"use client";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import { navbarLinks } from "@/config/site-config";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip";
|
||||||
|
import StarOnGithubButton from "./ui/star-on-github-button";
|
||||||
|
import { ThemeToggle } from "./ui/theme-toggle";
|
||||||
|
import CommandMenu from "./command-menu";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
function Navbar() {
|
||||||
|
const [isScrolled, setIsScrolled] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleScroll = () => {
|
||||||
|
setIsScrolled(window.scrollY > 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("scroll", handleScroll);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("scroll", handleScroll);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
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-[1440px] 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 VE 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,5 +1,6 @@
|
|||||||
import { ClipboardIcon } from "lucide-react";
|
import { ClipboardIcon } from "lucide-react";
|
||||||
import handleCopy from "./handleCopy";
|
|
||||||
|
import handleCopy from "./handle-copy";
|
||||||
|
|
||||||
export default function TextCopyBlock(description: string) {
|
export default function TextCopyBlock(description: string) {
|
||||||
const pattern = /`([^`]*)`/g;
|
const pattern = /`([^`]*)`/g;
|
||||||
@ -19,7 +20,8 @@ export default function TextCopyBlock(description: string) {
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
return part;
|
return part;
|
||||||
}
|
}
|
||||||
});
|
});
|
@ -1,7 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import type { ThemeProviderProps } from "next-themes";
|
||||||
|
|
||||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||||
import { type ThemeProviderProps } from "next-themes/dist/types";
|
|
||||||
|
|
||||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import * as React from "react"
|
import type { VariantProps } from "class-variance-authority";
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cva } from "class-variance-authority";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const alertVariants = cva(
|
const alertVariants = cva(
|
||||||
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
||||||
@ -16,8 +18,8 @@ const alertVariants = cva(
|
|||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: "default",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
|
|
||||||
const Alert = React.forwardRef<
|
const Alert = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
@ -29,8 +31,8 @@ const Alert = React.forwardRef<
|
|||||||
className={cn(alertVariants({ variant }), className)}
|
className={cn(alertVariants({ variant }), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
Alert.displayName = "Alert"
|
Alert.displayName = "Alert";
|
||||||
|
|
||||||
const AlertTitle = React.forwardRef<
|
const AlertTitle = React.forwardRef<
|
||||||
HTMLParagraphElement,
|
HTMLParagraphElement,
|
||||||
@ -41,8 +43,8 @@ const AlertTitle = React.forwardRef<
|
|||||||
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
AlertTitle.displayName = "AlertTitle"
|
AlertTitle.displayName = "AlertTitle";
|
||||||
|
|
||||||
const AlertDescription = React.forwardRef<
|
const AlertDescription = React.forwardRef<
|
||||||
HTMLParagraphElement,
|
HTMLParagraphElement,
|
||||||
@ -53,7 +55,7 @@ const AlertDescription = React.forwardRef<
|
|||||||
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
AlertDescription.displayName = "AlertDescription"
|
AlertDescription.displayName = "AlertDescription";
|
||||||
|
|
||||||
export { Alert, AlertTitle, AlertDescription }
|
export { Alert, AlertDescription, AlertTitle };
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ export default function AnimatedGradientText({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`absolute inset-0 block h-full w-full animate-gradient bg-gradient-to-r from-[#ffaa40]/50 via-[#9c40ff]/50 to-[#ffaa40]/50 bg-[length:var(--bg-size)_100%] p-[1px] [border-radius:inherit] ![mask-composite:subtract] [mask:linear-gradient(#fff_0_0)_content-box,linear-gradient(#fff_0_0)]`}
|
className="absolute inset-0 block h-full w-full animate-gradient bg-gradient-to-r from-[#ffaa40]/50 via-[#9c40ff]/50 to-[#ffaa40]/50 bg-[length:var(--bg-size)_100%] p-[1px] [border-radius:inherit] ![mask-composite:subtract] [mask:linear-gradient(#fff_0_0)_content-box,linear-gradient(#fff_0_0)]"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import { cva, type VariantProps } from "class-variance-authority";
|
import type { VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
|
import { cva } from "class-variance-authority";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@ -26,9 +28,7 @@ const badgeVariants = cva(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export interface BadgeProps
|
export type BadgeProps = {} & React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof badgeVariants>;
|
||||||
extends React.HTMLAttributes<HTMLDivElement>,
|
|
||||||
VariantProps<typeof badgeVariants> {}
|
|
||||||
|
|
||||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||||
return (
|
return (
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
import { cn } from "@/lib/utils";
|
import type { VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
import { Slot, Slottable } from "@radix-ui/react-slot";
|
import { Slot, Slottable } from "@radix-ui/react-slot";
|
||||||
import { cva, type VariantProps } from "class-variance-authority";
|
import { cva } from "class-variance-authority";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
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",
|
||||||
{
|
{
|
||||||
@ -47,21 +50,19 @@ const buttonVariants = cva(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
interface IconProps {
|
type IconProps = {
|
||||||
Icon: React.ElementType;
|
Icon: React.ElementType;
|
||||||
iconPlacement: "left" | "right";
|
iconPlacement: "left" | "right";
|
||||||
}
|
};
|
||||||
|
|
||||||
interface IconRefProps {
|
type IconRefProps = {
|
||||||
Icon?: never;
|
Icon?: never;
|
||||||
iconPlacement?: undefined;
|
iconPlacement?: undefined;
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface ButtonProps
|
export type ButtonProps = {
|
||||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
||||||
VariantProps<typeof buttonVariants> {
|
|
||||||
asChild?: boolean;
|
asChild?: boolean;
|
||||||
}
|
} & React.ButtonHTMLAttributes<HTMLButtonElement> & VariantProps<typeof buttonVariants>;
|
||||||
|
|
||||||
export type ButtonIconProps = IconProps | IconRefProps;
|
export type ButtonIconProps = IconProps | IconRefProps;
|
||||||
|
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
import { ChevronLeft, ChevronRight } from "lucide-react"
|
import { DayPicker } from "react-day-picker";
|
||||||
import { DayPicker } from "react-day-picker"
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { buttonVariants } from "@/components/ui/button";
|
||||||
import { buttonVariants } from "@/components/ui/button"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export type CalendarProps = React.ComponentProps<typeof DayPicker>
|
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
|
||||||
|
|
||||||
function Calendar({
|
function Calendar({
|
||||||
className,
|
className,
|
||||||
@ -27,7 +27,7 @@ function Calendar({
|
|||||||
nav: "space-x-1 flex items-center",
|
nav: "space-x-1 flex items-center",
|
||||||
nav_button: cn(
|
nav_button: cn(
|
||||||
buttonVariants({ variant: "outline" }),
|
buttonVariants({ variant: "outline" }),
|
||||||
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
|
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
|
||||||
),
|
),
|
||||||
nav_button_previous: "absolute left-1",
|
nav_button_previous: "absolute left-1",
|
||||||
nav_button_next: "absolute right-1",
|
nav_button_next: "absolute right-1",
|
||||||
@ -39,7 +39,7 @@ function Calendar({
|
|||||||
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
|
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
|
||||||
day: cn(
|
day: cn(
|
||||||
buttonVariants({ variant: "ghost" }),
|
buttonVariants({ variant: "ghost" }),
|
||||||
"h-9 w-9 p-0 font-normal aria-selected:opacity-100"
|
"h-9 w-9 p-0 font-normal aria-selected:opacity-100",
|
||||||
),
|
),
|
||||||
day_range_end: "day-range-end",
|
day_range_end: "day-range-end",
|
||||||
day_selected:
|
day_selected:
|
||||||
@ -54,13 +54,17 @@ function Calendar({
|
|||||||
...classNames,
|
...classNames,
|
||||||
}}
|
}}
|
||||||
components={{
|
components={{
|
||||||
IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />,
|
Chevron: ({ ...props }) => {
|
||||||
IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />,
|
if (props.orientation === "left") {
|
||||||
|
return <ChevronLeft className="h-4 w-4" />;
|
||||||
|
}
|
||||||
|
return <ChevronRight className="h-4 w-4" />;
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
Calendar.displayName = "Calendar"
|
Calendar.displayName = "Calendar";
|
||||||
|
|
||||||
export { Calendar }
|
export { Calendar };
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { CheckIcon, ClipboardIcon } from "lucide-react";
|
import { CheckIcon, ClipboardIcon } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
import { Card } from "./card";
|
import { Card } from "./card";
|
||||||
|
|
||||||
export default function CodeCopyButton({
|
export default function CodeCopyButton({
|
||||||
@ -26,7 +28,7 @@ export default function CodeCopyButton({
|
|||||||
|
|
||||||
setHasCopied(true);
|
setHasCopied(true);
|
||||||
|
|
||||||
let warning = localStorage.getItem("warning");
|
const warning = localStorage.getItem("warning");
|
||||||
|
|
||||||
if (warning === null) {
|
if (warning === null) {
|
||||||
localStorage.setItem("warning", "1");
|
localStorage.setItem("warning", "1");
|
||||||
@ -50,11 +52,13 @@ export default function CodeCopyButton({
|
|||||||
className={cn("bg-muted px-3 py-4")}
|
className={cn("bg-muted px-3 py-4")}
|
||||||
title="Copy"
|
title="Copy"
|
||||||
>
|
>
|
||||||
{hasCopied ? (
|
{hasCopied
|
||||||
<CheckIcon className="h-4 w-4" />
|
? (
|
||||||
) : (
|
<CheckIcon className="h-4 w-4" />
|
||||||
<ClipboardIcon className="h-4 w-4" />
|
)
|
||||||
)}
|
: (
|
||||||
|
<ClipboardIcon className="h-4 w-4" />
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,14 +1,18 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { basePath } from "@/config/siteConfig";
|
import type { VariantProps } from "class-variance-authority";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority";
|
import { cva } from "class-variance-authority";
|
||||||
import { Clipboard, Copy } from "lucide-react";
|
import { Clipboard, Copy } from "lucide-react";
|
||||||
import Link from "next/link";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Button } from "./button";
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import { basePath } from "@/config/site-config";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
import { Separator } from "./separator";
|
import { Separator } from "./separator";
|
||||||
|
import { Button } from "./button";
|
||||||
|
|
||||||
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",
|
||||||
@ -40,23 +44,24 @@ const buttonVariants = cva(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCopy = (type: string, value: string) => {
|
function handleCopy(type: string, value: string) {
|
||||||
navigator.clipboard.writeText(value);
|
navigator.clipboard.writeText(value);
|
||||||
|
|
||||||
let amountOfScriptsCopied = localStorage.getItem("amountOfScriptsCopied");
|
let amountOfScriptsCopied = localStorage.getItem("amountOfScriptsCopied");
|
||||||
|
|
||||||
if (amountOfScriptsCopied === null) {
|
if (amountOfScriptsCopied === null) {
|
||||||
localStorage.setItem("amountOfScriptsCopied", "1");
|
localStorage.setItem("amountOfScriptsCopied", "1");
|
||||||
} else {
|
}
|
||||||
amountOfScriptsCopied = (parseInt(amountOfScriptsCopied) + 1).toString();
|
else {
|
||||||
|
amountOfScriptsCopied = (Number.parseInt(amountOfScriptsCopied) + 1).toString();
|
||||||
localStorage.setItem("amountOfScriptsCopied", amountOfScriptsCopied);
|
localStorage.setItem("amountOfScriptsCopied", amountOfScriptsCopied);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
parseInt(amountOfScriptsCopied) === 3 ||
|
Number.parseInt(amountOfScriptsCopied) === 3
|
||||||
parseInt(amountOfScriptsCopied) === 10 ||
|
|| Number.parseInt(amountOfScriptsCopied) === 10
|
||||||
parseInt(amountOfScriptsCopied) === 25 ||
|
|| Number.parseInt(amountOfScriptsCopied) === 25
|
||||||
parseInt(amountOfScriptsCopied) === 50 ||
|
|| Number.parseInt(amountOfScriptsCopied) === 50
|
||||||
parseInt(amountOfScriptsCopied) === 100
|
|| Number.parseInt(amountOfScriptsCopied) === 100
|
||||||
) {
|
) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
toast.info(
|
toast.info(
|
||||||
@ -86,17 +91,20 @@ const handleCopy = (type: string, value: string) => {
|
|||||||
toast.success(
|
toast.success(
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Clipboard className="h-4 w-4" />
|
<Clipboard className="h-4 w-4" />
|
||||||
<span>Copied {type} to clipboard</span>
|
<span>
|
||||||
|
Copied
|
||||||
|
{type}
|
||||||
|
{" "}
|
||||||
|
to clipboard
|
||||||
|
</span>
|
||||||
</div>,
|
</div>,
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
export interface CodeBlockProps
|
export type CodeBlockProps = {
|
||||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
||||||
VariantProps<typeof buttonVariants> {
|
|
||||||
asChild?: boolean;
|
asChild?: boolean;
|
||||||
code: string;
|
code: string;
|
||||||
}
|
} & React.ButtonHTMLAttributes<HTMLButtonElement> & VariantProps<typeof buttonVariants>;
|
||||||
|
|
||||||
const CodeBlock = React.forwardRef<HTMLDivElement, CodeBlockProps>(
|
const CodeBlock = React.forwardRef<HTMLDivElement, CodeBlockProps>(
|
||||||
({ className, variant, size, asChild = false, code }, ref) => {
|
({ className, variant, size, asChild = false, code }, ref) => {
|
||||||
@ -121,7 +129,10 @@ const CodeBlock = React.forwardRef<HTMLDivElement, CodeBlockProps>(
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<p className="flex items-center gap-2">
|
<p className="flex items-center gap-2">
|
||||||
{code} <Separator orientation="vertical" />{" "}
|
{code}
|
||||||
|
{" "}
|
||||||
|
<Separator orientation="vertical" />
|
||||||
|
{" "}
|
||||||
<Copy
|
<Copy
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
size={16}
|
size={16}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { type DialogProps } from "@radix-ui/react-dialog";
|
import type { DialogProps } from "@radix-ui/react-dialog";
|
||||||
|
|
||||||
import { Command as CommandPrimitive } from "cmdk";
|
import { Command as CommandPrimitive } from "cmdk";
|
||||||
import { Search } from "lucide-react";
|
import { Search } from "lucide-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
@ -23,9 +24,9 @@ const Command = React.forwardRef<
|
|||||||
));
|
));
|
||||||
Command.displayName = CommandPrimitive.displayName;
|
Command.displayName = CommandPrimitive.displayName;
|
||||||
|
|
||||||
interface CommandDialogProps extends DialogProps {}
|
type CommandDialogProps = {} & DialogProps;
|
||||||
|
|
||||||
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
|
function CommandDialog({ children, ...props }: CommandDialogProps) {
|
||||||
return (
|
return (
|
||||||
<Dialog {...props}>
|
<Dialog {...props}>
|
||||||
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
||||||
@ -35,7 +36,7 @@ const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
const CommandInput = React.forwardRef<
|
const CommandInput = React.forwardRef<
|
||||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||||
@ -126,10 +127,10 @@ const CommandItem = React.forwardRef<
|
|||||||
|
|
||||||
CommandItem.displayName = CommandPrimitive.Item.displayName;
|
CommandItem.displayName = CommandPrimitive.Item.displayName;
|
||||||
|
|
||||||
const CommandShortcut = ({
|
function CommandShortcut({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
}: React.HTMLAttributes<HTMLSpanElement>) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -139,7 +140,7 @@ const CommandShortcut = ({
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
CommandShortcut.displayName = "CommandShortcut";
|
CommandShortcut.displayName = "CommandShortcut";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { CheckIcon, ClipboardIcon } from "lucide-react";
|
import { CheckIcon, ClipboardIcon } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { toast } from "sonner";
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
import { Card } from "./card";
|
import { Card } from "./card";
|
||||||
|
|
||||||
export default function CodeCopyButton({
|
export default function CodeCopyButton({
|
||||||
@ -26,7 +27,6 @@ export default function CodeCopyButton({
|
|||||||
|
|
||||||
setHasCopied(true);
|
setHasCopied(true);
|
||||||
|
|
||||||
|
|
||||||
// toast.success(`copied ${type} to clipboard`, {
|
// toast.success(`copied ${type} to clipboard`, {
|
||||||
// icon: <ClipboardCheck className="h-4 w-4" />,
|
// icon: <ClipboardCheck className="h-4 w-4" />,
|
||||||
// });
|
// });
|
||||||
@ -42,11 +42,13 @@ export default function CodeCopyButton({
|
|||||||
className={cn(" right-0 cursor-pointer bg-muted px-3 py-4")}
|
className={cn(" right-0 cursor-pointer bg-muted px-3 py-4")}
|
||||||
onClick={() => handleCopy("install command", children)}
|
onClick={() => handleCopy("install command", children)}
|
||||||
>
|
>
|
||||||
{hasCopied ? (
|
{hasCopied
|
||||||
<CheckIcon className="h-4 w-4" />
|
? (
|
||||||
) : (
|
<CheckIcon className="h-4 w-4" />
|
||||||
<ClipboardIcon className="h-4 w-4" />
|
)
|
||||||
)}
|
: (
|
||||||
|
<ClipboardIcon className="h-4 w-4" />
|
||||||
|
)}
|
||||||
<span className="sr-only">Copy</span>
|
<span className="sr-only">Copy</span>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
@ -53,32 +53,36 @@ const DialogContent = React.forwardRef<
|
|||||||
));
|
));
|
||||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||||
|
|
||||||
const DialogHeader = ({
|
function DialogHeader({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
<div
|
return (
|
||||||
className={cn(
|
<div
|
||||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
className={cn(
|
||||||
className,
|
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||||
)}
|
className,
|
||||||
{...props}
|
)}
|
||||||
/>
|
{...props}
|
||||||
);
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
DialogHeader.displayName = "DialogHeader";
|
DialogHeader.displayName = "DialogHeader";
|
||||||
|
|
||||||
const DialogFooter = ({
|
function DialogFooter({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
<div
|
return (
|
||||||
className={cn(
|
<div
|
||||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
className={cn(
|
||||||
className,
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||||
)}
|
className,
|
||||||
{...props}
|
)}
|
||||||
/>
|
{...props}
|
||||||
);
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
DialogFooter.displayName = "DialogFooter";
|
DialogFooter.displayName = "DialogFooter";
|
||||||
|
|
||||||
const DialogTitle = React.forwardRef<
|
const DialogTitle = React.forwardRef<
|
||||||
|
@ -37,8 +37,8 @@ const DropdownMenuSubTrigger = React.forwardRef<
|
|||||||
<ChevronRight className="ml-auto h-4 w-4" />
|
<ChevronRight className="ml-auto h-4 w-4" />
|
||||||
</DropdownMenuPrimitive.SubTrigger>
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
));
|
));
|
||||||
DropdownMenuSubTrigger.displayName =
|
DropdownMenuSubTrigger.displayName
|
||||||
DropdownMenuPrimitive.SubTrigger.displayName;
|
= DropdownMenuPrimitive.SubTrigger.displayName;
|
||||||
|
|
||||||
const DropdownMenuSubContent = React.forwardRef<
|
const DropdownMenuSubContent = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||||
@ -53,8 +53,8 @@ const DropdownMenuSubContent = React.forwardRef<
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
DropdownMenuSubContent.displayName =
|
DropdownMenuSubContent.displayName
|
||||||
DropdownMenuPrimitive.SubContent.displayName;
|
= DropdownMenuPrimitive.SubContent.displayName;
|
||||||
|
|
||||||
const DropdownMenuContent = React.forwardRef<
|
const DropdownMenuContent = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||||
@ -113,8 +113,8 @@ const DropdownMenuCheckboxItem = React.forwardRef<
|
|||||||
{children}
|
{children}
|
||||||
</DropdownMenuPrimitive.CheckboxItem>
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
));
|
));
|
||||||
DropdownMenuCheckboxItem.displayName =
|
DropdownMenuCheckboxItem.displayName
|
||||||
DropdownMenuPrimitive.CheckboxItem.displayName;
|
= DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||||
|
|
||||||
const DropdownMenuRadioItem = React.forwardRef<
|
const DropdownMenuRadioItem = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||||
@ -168,17 +168,17 @@ const DropdownMenuSeparator = React.forwardRef<
|
|||||||
));
|
));
|
||||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||||
|
|
||||||
const DropdownMenuShortcut = ({
|
function DropdownMenuShortcut({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
}: React.HTMLAttributes<HTMLSpanElement>) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@ -2,8 +2,7 @@ import * as React from "react";
|
|||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export interface InputProps
|
export type InputProps = {} & React.InputHTMLAttributes<HTMLInputElement>;
|
||||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
|
||||||
|
|
||||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
({ className, type, ...props }, ref) => {
|
({ className, type, ...props }, ref) => {
|
||||||
|
@ -1,26 +1,28 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import type { VariantProps } from "class-variance-authority";
|
||||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||||
|
import { cva } from "class-variance-authority";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const labelVariants = cva(
|
const labelVariants = cva(
|
||||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
||||||
)
|
);
|
||||||
|
|
||||||
const Label = React.forwardRef<
|
const Label = React.forwardRef<
|
||||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||||
VariantProps<typeof labelVariants>
|
& VariantProps<typeof labelVariants>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<LabelPrimitive.Root
|
<LabelPrimitive.Root
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(labelVariants(), className)}
|
className={cn(labelVariants(), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
Label.displayName = LabelPrimitive.Root.displayName
|
Label.displayName = LabelPrimitive.Root.displayName;
|
||||||
|
|
||||||
export { Label }
|
export { Label };
|
||||||
|
@ -53,7 +53,8 @@ const NavigationMenuTrigger = React.forwardRef<
|
|||||||
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}{" "}
|
{children}
|
||||||
|
{" "}
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
|
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
@ -94,8 +95,8 @@ const NavigationMenuViewport = React.forwardRef<
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
));
|
));
|
||||||
NavigationMenuViewport.displayName =
|
NavigationMenuViewport.displayName
|
||||||
NavigationMenuPrimitive.Viewport.displayName;
|
= NavigationMenuPrimitive.Viewport.displayName;
|
||||||
|
|
||||||
const NavigationMenuIndicator = React.forwardRef<
|
const NavigationMenuIndicator = React.forwardRef<
|
||||||
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
|
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
|
||||||
@ -112,8 +113,8 @@ const NavigationMenuIndicator = React.forwardRef<
|
|||||||
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
|
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
|
||||||
</NavigationMenuPrimitive.Indicator>
|
</NavigationMenuPrimitive.Indicator>
|
||||||
));
|
));
|
||||||
NavigationMenuIndicator.displayName =
|
NavigationMenuIndicator.displayName
|
||||||
NavigationMenuPrimitive.Indicator.displayName;
|
= NavigationMenuPrimitive.Indicator.displayName;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
NavigationMenu,
|
NavigationMenu,
|
||||||
|
@ -30,10 +30,10 @@ export default function NumberTicker({
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
isInView &&
|
isInView
|
||||||
setTimeout(() => {
|
&& setTimeout(() => {
|
||||||
motionValue.set(direction === "down" ? 0 : value);
|
motionValue.set(direction === "down" ? 0 : value);
|
||||||
}, delay * 1000);
|
}, delay * 1000);
|
||||||
}, [motionValue, isInView, delay, value, direction]);
|
}, [motionValue, isInView, delay, value, direction]);
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
interface MousePosition {
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
type MousePosition = {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
function MousePosition(): MousePosition {
|
function MousePosition(): MousePosition {
|
||||||
const [mousePosition, setMousePosition] = useState<MousePosition>({
|
const [mousePosition, setMousePosition] = useState<MousePosition>({
|
||||||
@ -29,7 +30,7 @@ function MousePosition(): MousePosition {
|
|||||||
return mousePosition;
|
return mousePosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ParticlesProps {
|
type ParticlesProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
quantity?: number;
|
quantity?: number;
|
||||||
staticity?: number;
|
staticity?: number;
|
||||||
@ -39,18 +40,18 @@ interface ParticlesProps {
|
|||||||
color?: string;
|
color?: string;
|
||||||
vx?: number;
|
vx?: number;
|
||||||
vy?: number;
|
vy?: number;
|
||||||
}
|
};
|
||||||
function hexToRgb(hex: string): number[] {
|
function hexToRgb(hex: string): number[] {
|
||||||
hex = hex.replace("#", "");
|
hex = hex.replace("#", "");
|
||||||
|
|
||||||
if (hex.length === 3) {
|
if (hex.length === 3) {
|
||||||
hex = hex
|
hex = hex
|
||||||
.split("")
|
.split("")
|
||||||
.map((char) => char + char)
|
.map(char => char + char)
|
||||||
.join("");
|
.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
const hexInt = parseInt(hex, 16);
|
const hexInt = Number.parseInt(hex, 16);
|
||||||
const red = (hexInt >> 16) & 255;
|
const red = (hexInt >> 16) & 255;
|
||||||
const green = (hexInt >> 8) & 255;
|
const green = (hexInt >> 8) & 255;
|
||||||
const blue = hexInt & 255;
|
const blue = hexInt & 255;
|
||||||
@ -150,7 +151,7 @@ const Particles: React.FC<ParticlesProps> = ({
|
|||||||
const translateY = 0;
|
const translateY = 0;
|
||||||
const pSize = Math.floor(Math.random() * 2) + size;
|
const pSize = Math.floor(Math.random() * 2) + size;
|
||||||
const alpha = 0;
|
const alpha = 0;
|
||||||
const targetAlpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1));
|
const targetAlpha = Number.parseFloat((Math.random() * 0.6 + 0.1).toFixed(1));
|
||||||
const dx = (Math.random() - 0.5) * 0.1;
|
const dx = (Math.random() - 0.5) * 0.1;
|
||||||
const dy = (Math.random() - 0.5) * 0.1;
|
const dy = (Math.random() - 0.5) * 0.1;
|
||||||
const magnetism = 0.1 + Math.random() * 4;
|
const magnetism = 0.1 + Math.random() * 4;
|
||||||
@ -213,8 +214,8 @@ const Particles: React.FC<ParticlesProps> = ({
|
|||||||
start2: number,
|
start2: number,
|
||||||
end2: number,
|
end2: number,
|
||||||
): number => {
|
): number => {
|
||||||
const remapped =
|
const remapped
|
||||||
((value - start1) * (end2 - start2)) / (end1 - start1) + start2;
|
= ((value - start1) * (end2 - start2)) / (end1 - start1) + start2;
|
||||||
return remapped > 0 ? remapped : 0;
|
return remapped > 0 ? remapped : 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -229,7 +230,7 @@ const Particles: React.FC<ParticlesProps> = ({
|
|||||||
canvasSize.current.h - circle.y - circle.translateY - circle.size, // distance from bottom edge
|
canvasSize.current.h - circle.y - circle.translateY - circle.size, // distance from bottom edge
|
||||||
];
|
];
|
||||||
const closestEdge = edge.reduce((a, b) => Math.min(a, b));
|
const closestEdge = edge.reduce((a, b) => Math.min(a, b));
|
||||||
const remapClosestEdge = parseFloat(
|
const remapClosestEdge = Number.parseFloat(
|
||||||
remapValue(closestEdge, 0, 20, 0, 1).toFixed(2),
|
remapValue(closestEdge, 0, 20, 0, 1).toFixed(2),
|
||||||
);
|
);
|
||||||
if (remapClosestEdge > 1) {
|
if (remapClosestEdge > 1) {
|
||||||
@ -237,26 +238,27 @@ const Particles: React.FC<ParticlesProps> = ({
|
|||||||
if (circle.alpha > circle.targetAlpha) {
|
if (circle.alpha > circle.targetAlpha) {
|
||||||
circle.alpha = circle.targetAlpha;
|
circle.alpha = circle.targetAlpha;
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
circle.alpha = circle.targetAlpha * remapClosestEdge;
|
circle.alpha = circle.targetAlpha * remapClosestEdge;
|
||||||
}
|
}
|
||||||
circle.x += circle.dx + vx;
|
circle.x += circle.dx + vx;
|
||||||
circle.y += circle.dy + vy;
|
circle.y += circle.dy + vy;
|
||||||
circle.translateX +=
|
circle.translateX
|
||||||
(mouse.current.x / (staticity / circle.magnetism) - circle.translateX) /
|
+= (mouse.current.x / (staticity / circle.magnetism) - circle.translateX)
|
||||||
ease;
|
/ ease;
|
||||||
circle.translateY +=
|
circle.translateY
|
||||||
(mouse.current.y / (staticity / circle.magnetism) - circle.translateY) /
|
+= (mouse.current.y / (staticity / circle.magnetism) - circle.translateY)
|
||||||
ease;
|
/ ease;
|
||||||
|
|
||||||
drawCircle(circle, true);
|
drawCircle(circle, true);
|
||||||
|
|
||||||
// circle gets out of the canvas
|
// circle gets out of the canvas
|
||||||
if (
|
if (
|
||||||
circle.x < -circle.size ||
|
circle.x < -circle.size
|
||||||
circle.x > canvasSize.current.w + circle.size ||
|
|| circle.x > canvasSize.current.w + circle.size
|
||||||
circle.y < -circle.size ||
|
|| circle.y < -circle.size
|
||||||
circle.y > canvasSize.current.h + circle.size
|
|| circle.y > canvasSize.current.h + circle.size
|
||||||
) {
|
) {
|
||||||
// remove the circle from the array
|
// remove the circle from the array
|
||||||
circles.current.splice(i, 1);
|
circles.current.splice(i, 1);
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Popover = PopoverPrimitive.Root
|
const Popover = PopoverPrimitive.Root;
|
||||||
|
|
||||||
const PopoverTrigger = PopoverPrimitive.Trigger
|
const PopoverTrigger = PopoverPrimitive.Trigger;
|
||||||
|
|
||||||
const PopoverContent = React.forwardRef<
|
const PopoverContent = React.forwardRef<
|
||||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||||
@ -20,12 +20,12 @@ const PopoverContent = React.forwardRef<
|
|||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</PopoverPrimitive.Portal>
|
</PopoverPrimitive.Portal>
|
||||||
))
|
));
|
||||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||||
|
|
||||||
export { Popover, PopoverTrigger, PopoverContent }
|
export { Popover, PopoverContent, PopoverTrigger };
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import { Check, ChevronDown, ChevronUp } from "lucide-react";
|
||||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||||
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Select = SelectPrimitive.Root
|
const Select = SelectPrimitive.Root;
|
||||||
|
|
||||||
const SelectGroup = SelectPrimitive.Group
|
const SelectGroup = SelectPrimitive.Group;
|
||||||
|
|
||||||
const SelectValue = SelectPrimitive.Value
|
const SelectValue = SelectPrimitive.Value;
|
||||||
|
|
||||||
const SelectTrigger = React.forwardRef<
|
const SelectTrigger = React.forwardRef<
|
||||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||||
@ -20,7 +20,7 @@ const SelectTrigger = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
@ -29,8 +29,8 @@ const SelectTrigger = React.forwardRef<
|
|||||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||||
</SelectPrimitive.Icon>
|
</SelectPrimitive.Icon>
|
||||||
</SelectPrimitive.Trigger>
|
</SelectPrimitive.Trigger>
|
||||||
))
|
));
|
||||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||||
|
|
||||||
const SelectScrollUpButton = React.forwardRef<
|
const SelectScrollUpButton = React.forwardRef<
|
||||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||||
@ -40,14 +40,14 @@ const SelectScrollUpButton = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex cursor-default items-center justify-center py-1",
|
"flex cursor-default items-center justify-center py-1",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ChevronUp className="h-4 w-4" />
|
<ChevronUp className="h-4 w-4" />
|
||||||
</SelectPrimitive.ScrollUpButton>
|
</SelectPrimitive.ScrollUpButton>
|
||||||
))
|
));
|
||||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
||||||
|
|
||||||
const SelectScrollDownButton = React.forwardRef<
|
const SelectScrollDownButton = React.forwardRef<
|
||||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||||
@ -57,15 +57,15 @@ const SelectScrollDownButton = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex cursor-default items-center justify-center py-1",
|
"flex cursor-default items-center justify-center py-1",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ChevronDown className="h-4 w-4" />
|
<ChevronDown className="h-4 w-4" />
|
||||||
</SelectPrimitive.ScrollDownButton>
|
</SelectPrimitive.ScrollDownButton>
|
||||||
))
|
));
|
||||||
SelectScrollDownButton.displayName =
|
SelectScrollDownButton.displayName
|
||||||
SelectPrimitive.ScrollDownButton.displayName
|
= SelectPrimitive.ScrollDownButton.displayName;
|
||||||
|
|
||||||
const SelectContent = React.forwardRef<
|
const SelectContent = React.forwardRef<
|
||||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||||
@ -76,9 +76,9 @@ const SelectContent = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
position === "popper" &&
|
position === "popper"
|
||||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
&& "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
position={position}
|
position={position}
|
||||||
{...props}
|
{...props}
|
||||||
@ -87,8 +87,8 @@ const SelectContent = React.forwardRef<
|
|||||||
<SelectPrimitive.Viewport
|
<SelectPrimitive.Viewport
|
||||||
className={cn(
|
className={cn(
|
||||||
"p-1",
|
"p-1",
|
||||||
position === "popper" &&
|
position === "popper"
|
||||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
&& "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@ -96,8 +96,8 @@ const SelectContent = React.forwardRef<
|
|||||||
<SelectScrollDownButton />
|
<SelectScrollDownButton />
|
||||||
</SelectPrimitive.Content>
|
</SelectPrimitive.Content>
|
||||||
</SelectPrimitive.Portal>
|
</SelectPrimitive.Portal>
|
||||||
))
|
));
|
||||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
||||||
|
|
||||||
const SelectLabel = React.forwardRef<
|
const SelectLabel = React.forwardRef<
|
||||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||||
@ -108,8 +108,8 @@ const SelectLabel = React.forwardRef<
|
|||||||
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
||||||
|
|
||||||
const SelectItem = React.forwardRef<
|
const SelectItem = React.forwardRef<
|
||||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||||
@ -119,7 +119,7 @@ const SelectItem = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
@ -131,8 +131,8 @@ const SelectItem = React.forwardRef<
|
|||||||
|
|
||||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
</SelectPrimitive.Item>
|
</SelectPrimitive.Item>
|
||||||
))
|
));
|
||||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
||||||
|
|
||||||
const SelectSeparator = React.forwardRef<
|
const SelectSeparator = React.forwardRef<
|
||||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||||
@ -143,18 +143,18 @@ const SelectSeparator = React.forwardRef<
|
|||||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Select,
|
Select,
|
||||||
SelectGroup,
|
|
||||||
SelectValue,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectLabel,
|
SelectGroup,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectSeparator,
|
SelectLabel,
|
||||||
SelectScrollUpButton,
|
|
||||||
SelectScrollDownButton,
|
SelectScrollDownButton,
|
||||||
}
|
SelectScrollUpButton,
|
||||||
|
SelectSeparator,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
};
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import type { VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
import * as SheetPrimitive from "@radix-ui/react-dialog";
|
import * as SheetPrimitive from "@radix-ui/react-dialog";
|
||||||
import { cva, type VariantProps } from "class-variance-authority";
|
import { cva } from "class-variance-authority";
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
@ -49,9 +51,7 @@ const sheetVariants = cva(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
interface SheetContentProps
|
type SheetContentProps = {} & React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content> & VariantProps<typeof sheetVariants>;
|
||||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
|
||||||
VariantProps<typeof sheetVariants> {}
|
|
||||||
|
|
||||||
const SheetContent = React.forwardRef<
|
const SheetContent = React.forwardRef<
|
||||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||||
@ -74,32 +74,36 @@ const SheetContent = React.forwardRef<
|
|||||||
));
|
));
|
||||||
SheetContent.displayName = SheetPrimitive.Content.displayName;
|
SheetContent.displayName = SheetPrimitive.Content.displayName;
|
||||||
|
|
||||||
const SheetHeader = ({
|
function SheetHeader({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
<div
|
return (
|
||||||
className={cn(
|
<div
|
||||||
"flex flex-col space-y-2 text-center sm:text-left",
|
className={cn(
|
||||||
className,
|
"flex flex-col space-y-2 text-center sm:text-left",
|
||||||
)}
|
className,
|
||||||
{...props}
|
)}
|
||||||
/>
|
{...props}
|
||||||
);
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
SheetHeader.displayName = "SheetHeader";
|
SheetHeader.displayName = "SheetHeader";
|
||||||
|
|
||||||
const SheetFooter = ({
|
function SheetFooter({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
<div
|
return (
|
||||||
className={cn(
|
<div
|
||||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
className={cn(
|
||||||
className,
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||||
)}
|
className,
|
||||||
{...props}
|
)}
|
||||||
/>
|
{...props}
|
||||||
);
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
SheetFooter.displayName = "SheetFooter";
|
SheetFooter.displayName = "SheetFooter";
|
||||||
|
|
||||||
const SheetTitle = React.forwardRef<
|
const SheetTitle = React.forwardRef<
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useTheme } from "next-themes";
|
|
||||||
import { Toaster as Sonner } from "sonner";
|
import { Toaster as Sonner } from "sonner";
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
|
|
||||||
type ToasterProps = React.ComponentProps<typeof Sonner>;
|
type ToasterProps = React.ComponentProps<typeof Sonner>;
|
||||||
|
|
||||||
const Toaster = ({ ...props }: ToasterProps) => {
|
function Toaster({ ...props }: ToasterProps) {
|
||||||
const { theme = "system" } = useTheme();
|
const { theme = "system" } = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -26,6 +26,6 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
export { Toaster };
|
export { Toaster };
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { basePath } from "@/config/siteConfig";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { FaGithub, FaStar } from "react-icons/fa";
|
import { FaGithub, FaStar } from "react-icons/fa";
|
||||||
import { buttonVariants } from "./button";
|
import { useEffect, useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import { basePath } from "@/config/site-config";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
import NumberTicker from "./number-ticker";
|
import NumberTicker from "./number-ticker";
|
||||||
|
import { buttonVariants } from "./button";
|
||||||
|
|
||||||
export default function StarOnGithubButton() {
|
export default function StarOnGithubButton() {
|
||||||
const [stars, setStars] = useState(0);
|
const [stars, setStars] = useState(0);
|
||||||
@ -23,7 +25,8 @@ export default function StarOnGithubButton() {
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setStars(data.stargazers_count || stars);
|
setStars(data.stargazers_count || stars);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
}
|
||||||
|
catch (error) {
|
||||||
console.error("Error fetching stars:", error);
|
console.error("Error fetching stars:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -43,7 +46,8 @@ export default function StarOnGithubButton() {
|
|||||||
<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" />
|
<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">
|
<div className="flex items-center">
|
||||||
<FaGithub className="size-4" />
|
<FaGithub className="size-4" />
|
||||||
<span className="ml-1">Star on GitHub</span>{" "}
|
<span className="ml-1">Star on GitHub</span>
|
||||||
|
{" "}
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-2 flex items-center gap-1 text-sm md:flex">
|
<div className="ml-2 flex items-center gap-1 text-sm md:flex">
|
||||||
<FaStar className="size-4 text-gray-500 transition-all duration-300 group-hover:text-yellow-300" />
|
<FaStar className="size-4 text-gray-500 transition-all duration-300 group-hover:text-yellow-300" />
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as SwitchPrimitives from "@radix-ui/react-switch";
|
||||||
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Switch = React.forwardRef<
|
const Switch = React.forwardRef<
|
||||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||||
@ -12,18 +12,18 @@ const Switch = React.forwardRef<
|
|||||||
<SwitchPrimitives.Root
|
<SwitchPrimitives.Root
|
||||||
className={cn(
|
className={cn(
|
||||||
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
>
|
>
|
||||||
<SwitchPrimitives.Thumb
|
<SwitchPrimitives.Thumb
|
||||||
className={cn(
|
className={cn(
|
||||||
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
|
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</SwitchPrimitives.Root>
|
</SwitchPrimitives.Root>
|
||||||
))
|
));
|
||||||
Switch.displayName = SwitchPrimitives.Root.displayName
|
Switch.displayName = SwitchPrimitives.Root.displayName;
|
||||||
|
|
||||||
export { Switch }
|
export { Switch };
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Table = React.forwardRef<
|
const Table = React.forwardRef<
|
||||||
HTMLTableElement,
|
HTMLTableElement,
|
||||||
@ -13,16 +13,16 @@ const Table = React.forwardRef<
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))
|
));
|
||||||
Table.displayName = "Table"
|
Table.displayName = "Table";
|
||||||
|
|
||||||
const TableHeader = React.forwardRef<
|
const TableHeader = React.forwardRef<
|
||||||
HTMLTableSectionElement,
|
HTMLTableSectionElement,
|
||||||
React.HTMLAttributes<HTMLTableSectionElement>
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
||||||
))
|
));
|
||||||
TableHeader.displayName = "TableHeader"
|
TableHeader.displayName = "TableHeader";
|
||||||
|
|
||||||
const TableBody = React.forwardRef<
|
const TableBody = React.forwardRef<
|
||||||
HTMLTableSectionElement,
|
HTMLTableSectionElement,
|
||||||
@ -33,8 +33,8 @@ const TableBody = React.forwardRef<
|
|||||||
className={cn("[&_tr:last-child]:border-0", className)}
|
className={cn("[&_tr:last-child]:border-0", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
TableBody.displayName = "TableBody"
|
TableBody.displayName = "TableBody";
|
||||||
|
|
||||||
const TableFooter = React.forwardRef<
|
const TableFooter = React.forwardRef<
|
||||||
HTMLTableSectionElement,
|
HTMLTableSectionElement,
|
||||||
@ -44,12 +44,12 @@ const TableFooter = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
TableFooter.displayName = "TableFooter"
|
TableFooter.displayName = "TableFooter";
|
||||||
|
|
||||||
const TableRow = React.forwardRef<
|
const TableRow = React.forwardRef<
|
||||||
HTMLTableRowElement,
|
HTMLTableRowElement,
|
||||||
@ -59,12 +59,12 @@ const TableRow = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
TableRow.displayName = "TableRow"
|
TableRow.displayName = "TableRow";
|
||||||
|
|
||||||
const TableHead = React.forwardRef<
|
const TableHead = React.forwardRef<
|
||||||
HTMLTableCellElement,
|
HTMLTableCellElement,
|
||||||
@ -74,12 +74,12 @@ const TableHead = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
TableHead.displayName = "TableHead"
|
TableHead.displayName = "TableHead";
|
||||||
|
|
||||||
const TableCell = React.forwardRef<
|
const TableCell = React.forwardRef<
|
||||||
HTMLTableCellElement,
|
HTMLTableCellElement,
|
||||||
@ -89,12 +89,12 @@ const TableCell = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
TableCell.displayName = "TableCell"
|
TableCell.displayName = "TableCell";
|
||||||
|
|
||||||
const TableCaption = React.forwardRef<
|
const TableCaption = React.forwardRef<
|
||||||
HTMLTableCaptionElement,
|
HTMLTableCaptionElement,
|
||||||
@ -105,16 +105,16 @@ const TableCaption = React.forwardRef<
|
|||||||
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
TableCaption.displayName = "TableCaption"
|
TableCaption.displayName = "TableCaption";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Table,
|
Table,
|
||||||
TableHeader,
|
|
||||||
TableBody,
|
TableBody,
|
||||||
|
TableCaption,
|
||||||
|
TableCell,
|
||||||
TableFooter,
|
TableFooter,
|
||||||
TableHead,
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
TableCell,
|
};
|
||||||
TableCaption,
|
|
||||||
}
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Textarea = React.forwardRef<
|
const Textarea = React.forwardRef<
|
||||||
HTMLTextAreaElement,
|
HTMLTextAreaElement,
|
||||||
@ -10,13 +10,13 @@ const Textarea = React.forwardRef<
|
|||||||
<textarea
|
<textarea
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
Textarea.displayName = "Textarea"
|
Textarea.displayName = "Textarea";
|
||||||
|
|
||||||
export { Textarea }
|
export { Textarea };
|
||||||
|
@ -2,21 +2,24 @@
|
|||||||
|
|
||||||
import { MoonIcon, SunIcon } from "@radix-ui/react-icons";
|
import { MoonIcon, SunIcon } from "@radix-ui/react-icons";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
import { Button } from "./button";
|
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "./tooltip";
|
} from "./tooltip";
|
||||||
|
import { Button } from "./button";
|
||||||
|
|
||||||
export function ThemeToggle() {
|
export function ThemeToggle() {
|
||||||
const { setTheme, theme: currentTheme } = useTheme();
|
const { setTheme, theme: currentTheme } = useTheme();
|
||||||
|
|
||||||
const handleChangeTheme = (theme: "light" | "dark") => {
|
const handleChangeTheme = (theme: "light" | "dark") => {
|
||||||
if (theme === currentTheme) return;
|
if (theme === currentTheme)
|
||||||
|
return;
|
||||||
|
|
||||||
if (!document.startViewTransition) return setTheme(theme);
|
if (!document.startViewTransition)
|
||||||
|
return setTheme(theme);
|
||||||
document.startViewTransition(() => setTheme(theme));
|
document.startViewTransition(() => setTheme(theme));
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -31,8 +34,7 @@ export function ThemeToggle() {
|
|||||||
className="px-2"
|
className="px-2"
|
||||||
aria-label="Toggle theme"
|
aria-label="Toggle theme"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleChangeTheme(currentTheme === "dark" ? "light" : "dark")
|
handleChangeTheme(currentTheme === "dark" ? "light" : "dark")}
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<SunIcon className="size-[1.2rem] text-neutral-800 dark:hidden dark:text-neutral-200" />
|
<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" />
|
<MoonIcon className="hidden size-[1.2rem] text-neutral-800 dark:block dark:text-neutral-200" />
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const TooltipProvider = TooltipPrimitive.Provider
|
const TooltipProvider = TooltipPrimitive.Provider;
|
||||||
|
|
||||||
const Tooltip = TooltipPrimitive.Root
|
const Tooltip = TooltipPrimitive.Root;
|
||||||
|
|
||||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||||
|
|
||||||
const TooltipContent = React.forwardRef<
|
const TooltipContent = React.forwardRef<
|
||||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||||
@ -20,11 +20,11 @@ const TooltipContent = React.forwardRef<
|
|||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]",
|
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||||
|
|
||||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
|
||||||
|
@ -25,9 +25,9 @@ export const FAQ_Items = [
|
|||||||
"Updates via our LXC scripts might not pull the absolute latest version for a few reasons:\n- A bug in the application's release naming on GitHub.\n- A bug in our update script.\n- We intentionally pinned the version. This happens if a newer version has breaking changes or serious bugs that could affect your data or LXC stability. We wait for fixes before allowing the update.",
|
"Updates via our LXC scripts might not pull the absolute latest version for a few reasons:\n- A bug in the application's release naming on GitHub.\n- A bug in our update script.\n- We intentionally pinned the version. This happens if a newer version has breaking changes or serious bugs that could affect your data or LXC stability. We wait for fixes before allowing the update.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Why am I getting a "502 Bad Gateway" error?',
|
title: "Why am I getting a \"502 Bad Gateway\" error?",
|
||||||
content:
|
content:
|
||||||
'A "502 Bad Gateway" error usually means the application inside the LXC is not running or responding correctly. Check the application\'s logs first. If you use a reverse proxy, check its logs too. If you still have problems after checking the logs, report the issue, providing details from the logs.',
|
"A \"502 Bad Gateway\" error usually means the application inside the LXC is not running or responding correctly. Check the application's logs first. If you use a reverse proxy, check its logs too. If you still have problems after checking the logs, report the issue, providing details from the logs.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "What should I do if a script fails during execution?",
|
title: "What should I do if a script fails during execution?",
|
72
frontend/src/config/site-config.tsx
Normal file
72
frontend/src/config/site-config.tsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { MessagesSquare, Scroll } from "lucide-react";
|
||||||
|
import { FaDiscord, FaGithub } from "react-icons/fa";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import type { OperatingSystem } from "@/lib/types";
|
||||||
|
|
||||||
|
// eslint-disable-next-line node/no-process-env
|
||||||
|
export const basePath = process.env.BASE_PATH || "";
|
||||||
|
|
||||||
|
export const navbarLinks = [
|
||||||
|
{
|
||||||
|
href: `https://github.com/community-scripts/${basePath}`,
|
||||||
|
event: "Github",
|
||||||
|
icon: <FaGithub className="h-4 w-4" />,
|
||||||
|
text: "Github",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: `https://discord.gg/2wvnMDgdnU`,
|
||||||
|
event: "Discord",
|
||||||
|
icon: <FaDiscord className="h-4 w-4" />,
|
||||||
|
text: "Discord",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: `https://github.com/community-scripts/${basePath}/blob/main/CHANGELOG.md`,
|
||||||
|
event: "Change Log",
|
||||||
|
icon: <Scroll className="h-4 w-4" />,
|
||||||
|
text: "Change Log",
|
||||||
|
mobileHidden: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: `https://github.com/community-scripts/${basePath}/discussions`,
|
||||||
|
event: "Discussions",
|
||||||
|
icon: <MessagesSquare className="h-4 w-4" />,
|
||||||
|
text: "Discussions",
|
||||||
|
mobileHidden: true,
|
||||||
|
},
|
||||||
|
].filter(Boolean) as {
|
||||||
|
href: string;
|
||||||
|
event: string;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
text: string;
|
||||||
|
mobileHidden?: boolean;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
export const mostPopularScripts = ["post-pve-install", "docker", "homeassistant"];
|
||||||
|
|
||||||
|
export const analytics = {
|
||||||
|
url: "analytics.community-scripts.org",
|
||||||
|
token: "aefee1b9-2a12-4ac2-9d82-a63113edc62e",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AlertColors = {
|
||||||
|
warning: "border-red-500/25 bg-destructive/25",
|
||||||
|
info: "border-cyan-500/25 bg-cyan-50 dark:border-cyan-900 dark:bg-cyan-900/25",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OperatingSystems: OperatingSystem[] = [
|
||||||
|
{
|
||||||
|
name: "Debian",
|
||||||
|
versions: [
|
||||||
|
{ name: "11", slug: "bullseye" },
|
||||||
|
{ name: "12", slug: "bookworm" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ubuntu",
|
||||||
|
versions: [
|
||||||
|
{ name: "22.04", slug: "jammy" },
|
||||||
|
{ name: "24.04", slug: "noble" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
@ -1,72 +0,0 @@
|
|||||||
import { OperatingSystem } from "@/lib/types";
|
|
||||||
import { MessagesSquare, Scroll } from "lucide-react";
|
|
||||||
import React from "react";
|
|
||||||
import { FaDiscord, FaGithub } from "react-icons/fa";
|
|
||||||
|
|
||||||
export const basePath = process.env.BASE_PATH;
|
|
||||||
|
|
||||||
const isMobile = typeof window !== "undefined" && window.innerWidth < 640;
|
|
||||||
|
|
||||||
export const navbarLinks = [
|
|
||||||
{
|
|
||||||
href: `https://github.com/community-scripts/${basePath}`,
|
|
||||||
event: "Github",
|
|
||||||
icon: <FaGithub className="h-4 w-4" />,
|
|
||||||
text: "Github",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: `https://discord.gg/2wvnMDgdnU`,
|
|
||||||
event: "Discord",
|
|
||||||
icon: <FaDiscord className="h-4 w-4" />,
|
|
||||||
text: "Discord",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: `https://github.com/community-scripts/${basePath}/blob/main/CHANGELOG.md`,
|
|
||||||
event: "Change Log",
|
|
||||||
icon: <Scroll className="h-4 w-4" />,
|
|
||||||
text: "Change Log",
|
|
||||||
mobileHidden: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: `https://github.com/community-scripts/${basePath}/discussions`,
|
|
||||||
event: "Discussions",
|
|
||||||
icon: <MessagesSquare className="h-4 w-4" />,
|
|
||||||
text: "Discussions",
|
|
||||||
mobileHidden: true,
|
|
||||||
},
|
|
||||||
].filter(Boolean) as {
|
|
||||||
href: string;
|
|
||||||
event: string;
|
|
||||||
icon: React.ReactNode;
|
|
||||||
text: string;
|
|
||||||
mobileHidden?: boolean;
|
|
||||||
}[];
|
|
||||||
|
|
||||||
export const mostPopularScripts = ["post-pve-install", "docker", "homeassistant"];
|
|
||||||
|
|
||||||
export const analytics = {
|
|
||||||
url: "analytics.proxmoxve-scripts.com",
|
|
||||||
token: "aefee1b9-2a12-4ac2-9d82-a63113edc62e",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AlertColors = {
|
|
||||||
warning: "border-red-500/25 bg-destructive/25",
|
|
||||||
info: "border-cyan-500/25 bg-cyan-50 dark:border-cyan-900 dark:bg-cyan-900/25",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const OperatingSystems: OperatingSystem[] = [
|
|
||||||
{
|
|
||||||
name: "Debian",
|
|
||||||
versions: [
|
|
||||||
{ name: "11", slug: "bullseye" },
|
|
||||||
{ name: "12", slug: "bookworm" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Ubuntu",
|
|
||||||
versions: [
|
|
||||||
{ name: "22.04", slug: "jammy" },
|
|
||||||
{ name: "24.04", slug: "noble" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
@ -1,9 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { fetchVersions } from "@/lib/data";
|
|
||||||
import { AppVersion } from "@/lib/types";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import type { AppVersion } from "@/lib/types";
|
||||||
|
|
||||||
|
import { fetchVersions } from "@/lib/data";
|
||||||
|
|
||||||
export function useVersions() {
|
export function useVersions() {
|
||||||
return useQuery<AppVersion[]>({
|
return useQuery<AppVersion[]>({
|
||||||
queryKey: ["versions"],
|
queryKey: ["versions"],
|
@ -1,18 +1,18 @@
|
|||||||
import { Category } from "./types";
|
import type { Category } from "./types";
|
||||||
|
|
||||||
export const fetchCategories = async () => {
|
export async function fetchCategories() {
|
||||||
const response = await fetch("api/categories");
|
const response = await fetch("api/categories");
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to fetch categories: ${response.statusText}`);
|
throw new Error(`Failed to fetch categories: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
const categories: Category[] = await response.json();
|
const categories: Category[] = await response.json();
|
||||||
return categories;
|
return categories;
|
||||||
};
|
}
|
||||||
|
|
||||||
export const fetchVersions = async () => {
|
export async function fetchVersions() {
|
||||||
const response = await fetch(`api/versions`);
|
const response = await fetch(`api/versions`);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to fetch versions: ${response.statusText}`);
|
throw new Error(`Failed to fetch versions: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
return response.json();
|
return response.json();
|
||||||
};
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { AlertColors } from "@/config/siteConfig";
|
import type { AlertColors } from "@/config/site-config";
|
||||||
|
|
||||||
export type Script = {
|
export type Script = {
|
||||||
name: string;
|
name: string;
|
||||||
@ -48,18 +48,18 @@ export type Metadata = {
|
|||||||
categories: Category[];
|
categories: Category[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface Version {
|
export type Version = {
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface OperatingSystem {
|
export type OperatingSystem = {
|
||||||
name: string;
|
name: string;
|
||||||
versions: Version[];
|
versions: Version[];
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface AppVersion {
|
export type AppVersion = {
|
||||||
name: string;
|
name: string;
|
||||||
version: string;
|
version: string;
|
||||||
date: Date;
|
date: Date;
|
||||||
}
|
};
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { clsx, type ClassValue } from "clsx";
|
import type { ClassValue } from "clsx";
|
||||||
|
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import { clsx } from "clsx";
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs));
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: 0 0% 100%;
|
--background: 0 0% 100%;
|
||||||
@ -92,4 +91,4 @@
|
|||||||
.glass {
|
.glass {
|
||||||
backdrop-filter: blur(15px) saturate(100%);
|
backdrop-filter: blur(15px) saturate(100%);
|
||||||
-webkit-backdrop-filter: blur(15px) saturate(100%);
|
-webkit-backdrop-filter: blur(15px) saturate(100%);
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
|
/* eslint-disable ts/no-require-imports */
|
||||||
|
//
|
||||||
import type { Config } from "tailwindcss";
|
import type { Config } from "tailwindcss";
|
||||||
|
|
||||||
const svgToDataUri = require("mini-svg-data-uri");
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
default: flattenColorPalette,
|
default: flattenColorPalette,
|
||||||
} = require("tailwindcss/lib/util/flattenColorPalette");
|
} = require("tailwindcss/lib/util/flattenColorPalette");
|
||||||
|
const svgToDataUri = require("mini-svg-data-uri");
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
darkMode: ["class"],
|
darkMode: ["class"],
|
||||||
@ -73,11 +74,11 @@ const config = {
|
|||||||
from: { height: "var(--radix-accordion-content-height)" },
|
from: { height: "var(--radix-accordion-content-height)" },
|
||||||
to: { height: "0" },
|
to: { height: "0" },
|
||||||
},
|
},
|
||||||
shine: {
|
"shine": {
|
||||||
from: { backgroundPosition: "200% 0" },
|
from: { backgroundPosition: "200% 0" },
|
||||||
to: { backgroundPosition: "-200% 0" },
|
to: { backgroundPosition: "-200% 0" },
|
||||||
},
|
},
|
||||||
gradient: {
|
"gradient": {
|
||||||
to: {
|
to: {
|
||||||
backgroundPosition: "var(--bg-size) 0",
|
backgroundPosition: "var(--bg-size) 0",
|
||||||
},
|
},
|
||||||
@ -89,11 +90,11 @@ const config = {
|
|||||||
"50%": {
|
"50%": {
|
||||||
"background-position": "100% 100%",
|
"background-position": "100% 100%",
|
||||||
},
|
},
|
||||||
to: {
|
"to": {
|
||||||
"background-position": "0% 0%",
|
"background-position": "0% 0%",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
moveHorizontal: {
|
"moveHorizontal": {
|
||||||
"0%": {
|
"0%": {
|
||||||
transform: "translateX(-50%) translateY(-10%)",
|
transform: "translateX(-50%) translateY(-10%)",
|
||||||
},
|
},
|
||||||
@ -104,7 +105,7 @@ const config = {
|
|||||||
transform: "translateX(-50%) translateY(-10%)",
|
transform: "translateX(-50%) translateY(-10%)",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
moveInCircle: {
|
"moveInCircle": {
|
||||||
"0%": {
|
"0%": {
|
||||||
transform: "rotate(0deg)",
|
transform: "rotate(0deg)",
|
||||||
},
|
},
|
||||||
@ -115,7 +116,7 @@ const config = {
|
|||||||
transform: "rotate(360deg)",
|
transform: "rotate(360deg)",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
moveVertical: {
|
"moveVertical": {
|
||||||
"0%": {
|
"0%": {
|
||||||
transform: "translateY(-50%)",
|
transform: "translateY(-50%)",
|
||||||
},
|
},
|
||||||
@ -130,8 +131,8 @@ const config = {
|
|||||||
animation: {
|
animation: {
|
||||||
"accordion-down": "accordion-down 0.2s ease-out",
|
"accordion-down": "accordion-down 0.2s ease-out",
|
||||||
"accordion-up": "accordion-up 0.2s ease-out",
|
"accordion-up": "accordion-up 0.2s ease-out",
|
||||||
shine: "shine 8s ease-in-out infinite",
|
"shine": "shine 8s ease-in-out infinite",
|
||||||
gradient: "gradient 8s linear infinite",
|
"gradient": "gradient 8s linear infinite",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -168,8 +169,8 @@ const config = {
|
|||||||
} satisfies Config;
|
} satisfies Config;
|
||||||
|
|
||||||
function addVariablesForColors({ addBase, theme }: any) {
|
function addVariablesForColors({ addBase, theme }: any) {
|
||||||
let allColors = flattenColorPalette(theme("colors"));
|
const allColors = flattenColorPalette(theme("colors"));
|
||||||
let newVars = Object.fromEntries(
|
const newVars = Object.fromEntries(
|
||||||
Object.entries(allColors).map(([key, val]) => [`--${key}`, val]),
|
Object.entries(allColors).map(([key, val]) => [`--${key}`, val]),
|
||||||
);
|
);
|
||||||
addBase({
|
addBase({
|
||||||
|
30
frontend/tsconfig.json
generated
30
frontend/tsconfig.json
generated
@ -1,32 +1,32 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"incremental": true,
|
||||||
|
"target": "ES2017",
|
||||||
|
"jsx": "preserve",
|
||||||
"lib": [
|
"lib": [
|
||||||
"dom",
|
"dom",
|
||||||
"dom.iterable",
|
"dom.iterable",
|
||||||
"esnext"
|
"esnext"
|
||||||
],
|
],
|
||||||
"allowJs": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"strict": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"jsx": "preserve",
|
|
||||||
"incremental": true,
|
|
||||||
"plugins": [
|
|
||||||
{
|
|
||||||
"name": "next"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": [
|
||||||
"./src/*"
|
"./src/*"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"target": "ES2017"
|
"resolveJsonModule": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"next-env.d.ts",
|
"next-env.d.ts",
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { defineConfig } from 'vitest/config'
|
import tsconfigPaths from "vite-tsconfig-paths";
|
||||||
import react from '@vitejs/plugin-react'
|
import { defineConfig } from "vitest/config";
|
||||||
import tsconfigPaths from 'vite-tsconfig-paths'
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [tsconfigPaths(), react()],
|
plugins: [tsconfigPaths(), react()],
|
||||||
test: {
|
test: {
|
||||||
environment: "jsdom",
|
environment: "jsdom",
|
||||||
setupFiles: ["src/__tests__/setupTests.ts"]
|
setupFiles: ["src/__tests__/setupTests.ts"],
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
Reference in New Issue
Block a user