mirror of
				https://github.com/community-scripts/ProxmoxVE.git
				synced 2025-11-04 02:12:49 +00:00 
			
		
		
		
	merge frontend website into scripts repo
This commit is contained in:
		
							
								
								
									
										83
									
								
								.github/workflows/nextjs.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								.github/workflows/nextjs.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,83 @@
 | 
			
		||||
# Sample workflow for building and deploying a Next.js site to GitHub Pages
 | 
			
		||||
#
 | 
			
		||||
# To get started with Next.js see: https://nextjs.org/docs/getting-started
 | 
			
		||||
#
 | 
			
		||||
name: Deploy Next.js site to Pages
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches: ["main"]
 | 
			
		||||
  workflow_dispatch:
 | 
			
		||||
 | 
			
		||||
permissions:
 | 
			
		||||
  contents: read
 | 
			
		||||
  pages: write
 | 
			
		||||
  id-token: write
 | 
			
		||||
 | 
			
		||||
concurrency:
 | 
			
		||||
  group: "pages"
 | 
			
		||||
  cancel-in-progress: false
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  build:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    defaults:
 | 
			
		||||
      run:
 | 
			
		||||
        working-directory: frontend  # Set default working directory for all run steps
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout
 | 
			
		||||
        uses: actions/checkout@v4
 | 
			
		||||
      - name: Detect package manager
 | 
			
		||||
        id: detect-package-manager
 | 
			
		||||
        run: |
 | 
			
		||||
          if [ -f "${{ github.workspace }}/frontend/yarn.lock" ]; then
 | 
			
		||||
            echo "manager=yarn" >> $GITHUB_OUTPUT
 | 
			
		||||
            echo "command=install" >> $GITHUB_OUTPUT
 | 
			
		||||
            echo "runner=yarn" >> $GITHUB_OUTPUT
 | 
			
		||||
            exit 0
 | 
			
		||||
          elif [ -f "${{ github.workspace }}/frontend/package.json" ]; then
 | 
			
		||||
            echo "manager=npm" >> $GITHUB_OUTPUT
 | 
			
		||||
            echo "command=ci" >> $GITHUB_OUTPUT
 | 
			
		||||
            echo "runner=npx --no-install" >> $GITHUB_OUTPUT
 | 
			
		||||
            exit 0
 | 
			
		||||
          else
 | 
			
		||||
            echo "Unable to determine package manager"
 | 
			
		||||
            exit 1
 | 
			
		||||
          fi
 | 
			
		||||
      - name: Setup Node
 | 
			
		||||
        uses: actions/setup-node@v4
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: "20"
 | 
			
		||||
          cache: ${{ steps.detect-package-manager.outputs.manager }}
 | 
			
		||||
          cache-dependency-path: frontend/package-lock.json  # Specify the path to package-lock.json
 | 
			
		||||
      - name: Setup Pages
 | 
			
		||||
        uses: actions/configure-pages@v5
 | 
			
		||||
        with:
 | 
			
		||||
          static_site_generator: next
 | 
			
		||||
      - name: Restore cache
 | 
			
		||||
        uses: actions/cache@v4
 | 
			
		||||
        with:
 | 
			
		||||
          path: |
 | 
			
		||||
            frontend/.next/cache
 | 
			
		||||
          key: ${{ runner.os }}-nextjs-${{ hashFiles('frontend/**/package-lock.json', 'frontend/**/yarn.lock') }}-${{ hashFiles('frontend/**.[jt]s', 'frontend/**.[jt]sx') }}
 | 
			
		||||
          restore-keys: |
 | 
			
		||||
            ${{ runner.os }}-nextjs-${{ hashFiles('frontend/**/package-lock.json', 'frontend/**/yarn.lock') }}-
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
        run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }} --legacy-peer-deps
 | 
			
		||||
      - name: Build with Next.js
 | 
			
		||||
        run: ${{ steps.detect-package-manager.outputs.runner }} next build
 | 
			
		||||
      - name: Upload artifact
 | 
			
		||||
        uses: actions/upload-pages-artifact@v3
 | 
			
		||||
        with:
 | 
			
		||||
          path: frontend/out
 | 
			
		||||
 | 
			
		||||
  deploy:
 | 
			
		||||
    environment:
 | 
			
		||||
      name: github-pages
 | 
			
		||||
      url: ${{ steps.deployment.outputs.page_url }}
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    needs: build
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Deploy to GitHub Pages
 | 
			
		||||
        id: deployment
 | 
			
		||||
        uses: actions/deploy-pages@v4
 | 
			
		||||
							
								
								
									
										3
									
								
								frontend/.env.local
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								frontend/.env.local
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
NEXT_PUBLIC_ANALYTICS_TOKEN="b60d3032-1a11-4244-a100-81d26c5c49a7"
 | 
			
		||||
NEXT_PUBLIC_ANALYTICS_URL="analytics.proxmoxve-scripts.com"
 | 
			
		||||
NEXT_PUBLIC_POCKETBASE_URL="https://pocketbase.proxmoxve-scripts.com"
 | 
			
		||||
							
								
								
									
										5
									
								
								frontend/.eslintrc.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								frontend/.eslintrc.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
  "extends": ["next/core-web-vitals"],
 | 
			
		||||
  "parser": "@typescript-eslint/parser",
 | 
			
		||||
  "plugins": ["@typescript-eslint"]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										39
									
								
								frontend/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								frontend/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
			
		||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
 | 
			
		||||
 | 
			
		||||
# dependencies
 | 
			
		||||
/node_modules
 | 
			
		||||
/.pnp
 | 
			
		||||
.pnp.js
 | 
			
		||||
.yarn/install-state.gz
 | 
			
		||||
 | 
			
		||||
# wrangler
 | 
			
		||||
.worker-next
 | 
			
		||||
.wrangler
 | 
			
		||||
 | 
			
		||||
# testing
 | 
			
		||||
/coverage
 | 
			
		||||
 | 
			
		||||
# next.js
 | 
			
		||||
/.next/
 | 
			
		||||
out
 | 
			
		||||
# production
 | 
			
		||||
/build
 | 
			
		||||
 | 
			
		||||
# misc
 | 
			
		||||
.DS_Store
 | 
			
		||||
*.pem
 | 
			
		||||
 | 
			
		||||
# debug
 | 
			
		||||
npm-debug.log*
 | 
			
		||||
yarn-debug.log*
 | 
			
		||||
yarn-error.log*
 | 
			
		||||
 | 
			
		||||
# # local env files
 | 
			
		||||
# .env*.local
 | 
			
		||||
# .env
 | 
			
		||||
# vercel
 | 
			
		||||
.vercel
 | 
			
		||||
 | 
			
		||||
# typescript
 | 
			
		||||
*.tsbuildinfo
 | 
			
		||||
next-env.d.ts
 | 
			
		||||
							
								
								
									
										5
									
								
								frontend/.prettierignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								frontend/.prettierignore
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
dist
 | 
			
		||||
node_modules
 | 
			
		||||
.next
 | 
			
		||||
build
 | 
			
		||||
.contentlayer
 | 
			
		||||
							
								
								
									
										3
									
								
								frontend/.prettierrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								frontend/.prettierrc
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
{
 | 
			
		||||
  "plugins": ["prettier-plugin-tailwindcss", "prettier-plugin-organize-imports"]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										21
									
								
								frontend/LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								frontend/LICENSE
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
MIT License
 | 
			
		||||
 | 
			
		||||
Copyright (c) 2024 Bram Suurd
 | 
			
		||||
 | 
			
		||||
Permission is hereby granted, free of charge, to any person obtaining a copy
 | 
			
		||||
of this software and associated documentation files (the "Software"), to deal
 | 
			
		||||
in the Software without restriction, including without limitation the rights
 | 
			
		||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 | 
			
		||||
copies of the Software, and to permit persons to whom the Software is
 | 
			
		||||
furnished to do so, subject to the following conditions:
 | 
			
		||||
 | 
			
		||||
The above copyright notice and this permission notice shall be included in all
 | 
			
		||||
copies or substantial portions of the Software.
 | 
			
		||||
 | 
			
		||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 | 
			
		||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 | 
			
		||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 | 
			
		||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 | 
			
		||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 | 
			
		||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 | 
			
		||||
SOFTWARE.
 | 
			
		||||
							
								
								
									
										17
									
								
								frontend/components.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								frontend/components.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
{
 | 
			
		||||
  "$schema": "https://ui.shadcn.com/schema.json",
 | 
			
		||||
  "style": "default",
 | 
			
		||||
  "rsc": true,
 | 
			
		||||
  "tsx": true,
 | 
			
		||||
  "tailwind": {
 | 
			
		||||
    "config": "tailwind.config.ts",
 | 
			
		||||
    "css": "@/styles/globals.css",
 | 
			
		||||
    "baseColor": "slate",
 | 
			
		||||
    "cssVariables": true,
 | 
			
		||||
    "prefix": ""
 | 
			
		||||
  },
 | 
			
		||||
  "aliases": {
 | 
			
		||||
    "components": "@/components",
 | 
			
		||||
    "utils": "@/lib/utils"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										4
									
								
								frontend/example.env
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								frontend/example.env
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
NEXT_PUBLIC_POCKETBASE_URL=https://pocketbase.proxmoxve-scripts.com
 | 
			
		||||
NEXT_PUBLIC_ANALYTICS_URL=https://analytics.proxmoxve-scripts.com
 | 
			
		||||
NEXT_PUBLIC_ANALYTICS_TOKEN=b60d130323-1a11-4244-a1010-81d263c5c49a7
 | 
			
		||||
NODE_ENV=production
 | 
			
		||||
							
								
								
									
										21
									
								
								frontend/next.config.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								frontend/next.config.mjs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
/** @type {import('next').NextConfig} */
 | 
			
		||||
const nextConfig = {
 | 
			
		||||
  webpack: (config) => {
 | 
			
		||||
    config.resolve.alias.canvas = false;
 | 
			
		||||
 | 
			
		||||
    return config;
 | 
			
		||||
  },
 | 
			
		||||
  images: {
 | 
			
		||||
    remotePatterns: [
 | 
			
		||||
      {
 | 
			
		||||
        protocol: "https",
 | 
			
		||||
        hostname: "**",
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  output: "export",
 | 
			
		||||
  // basePath: "/proxmox-helper-scripts",
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default nextConfig;
 | 
			
		||||
							
								
								
									
										7546
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										7546
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										73
									
								
								frontend/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								frontend/package.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,73 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "proxmox-helper-scripts-website",
 | 
			
		||||
  "version": "1.0.0",
 | 
			
		||||
  "license": "MIT",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "author": {
 | 
			
		||||
    "name": "Bram Suurd",
 | 
			
		||||
    "url": "https://github.com/community-scripts"
 | 
			
		||||
  },
 | 
			
		||||
  "type": "module",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "dev": "next dev --turbopack",
 | 
			
		||||
    "build": "next build",
 | 
			
		||||
    "start": "next start",
 | 
			
		||||
    "lint": "next lint",
 | 
			
		||||
    "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"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@radix-ui/react-accordion": "^1.1.2",
 | 
			
		||||
    "@radix-ui/react-dialog": "^1.0.5",
 | 
			
		||||
    "@radix-ui/react-dropdown-menu": "^2.0.6",
 | 
			
		||||
    "@radix-ui/react-navigation-menu": "^1.1.4",
 | 
			
		||||
    "@radix-ui/react-separator": "^1.1.0",
 | 
			
		||||
    "@radix-ui/react-slot": "^1.1.0",
 | 
			
		||||
    "@radix-ui/react-tabs": "^1.1.0",
 | 
			
		||||
    "@radix-ui/react-tooltip": "^1.1.2",
 | 
			
		||||
    "@vercel/analytics": "^1.2.2",
 | 
			
		||||
    "class-variance-authority": "^0.7.0",
 | 
			
		||||
    "clsx": "^2.1.1",
 | 
			
		||||
    "cmdk": "^1.0.0",
 | 
			
		||||
    "framer-motion": "^11.11.10",
 | 
			
		||||
    "fuse.js": "^7.0.0",
 | 
			
		||||
    "lucide-react": "^0.453.0",
 | 
			
		||||
    "mini-svg-data-uri": "^1.4.4",
 | 
			
		||||
    "next": "15.0.2",
 | 
			
		||||
    "next-themes": "^0.3.0",
 | 
			
		||||
    "nuqs": "^2.1.1",
 | 
			
		||||
    "pocketbase": "^0.21.4",
 | 
			
		||||
    "prettier-plugin-organize-imports": "^4.1.0",
 | 
			
		||||
    "react": "19.0.0-rc-02c0e824-20241028",
 | 
			
		||||
    "react-code-blocks": "^0.1.6",
 | 
			
		||||
    "react-dom": "19.0.0-rc-02c0e824-20241028",
 | 
			
		||||
    "react-icons": "^5.1.0",
 | 
			
		||||
    "react-simple-typewriter": "^5.0.1",
 | 
			
		||||
    "sharp": "^0.33.5",
 | 
			
		||||
    "simple-icons": "^13.5.0",
 | 
			
		||||
    "sonner": "^1.5.0",
 | 
			
		||||
    "tailwind-merge": "^2.3.0"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@types/node": "^22",
 | 
			
		||||
    "@types/react": "npm:types-react@19.0.0-rc.1",
 | 
			
		||||
    "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": "^8.8.1",
 | 
			
		||||
    "@typescript-eslint/parser": "^8.8.1",
 | 
			
		||||
    "eslint-config-next": "15.0.2",
 | 
			
		||||
    "postcss": "^8",
 | 
			
		||||
    "eslint": "^9.13.0",
 | 
			
		||||
    "prettier": "^3.2.5",
 | 
			
		||||
    "prettier-plugin-tailwindcss": "^0.6.5",
 | 
			
		||||
    "tailwindcss": "^3.4.9",
 | 
			
		||||
    "tailwindcss-animate": "^1.0.7",
 | 
			
		||||
    "tailwindcss-animated": "^1.1.2",
 | 
			
		||||
    "typescript": "^5"
 | 
			
		||||
  },
 | 
			
		||||
  "overrides": {
 | 
			
		||||
    "@types/react": "npm:types-react@19.0.0-rc.1",
 | 
			
		||||
    "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										8
									
								
								frontend/postcss.config.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								frontend/postcss.config.mjs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
/** @type {import('postcss-load-config').Config} */
 | 
			
		||||
const config = {
 | 
			
		||||
  plugins: {
 | 
			
		||||
    tailwindcss: {},
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default config;
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								frontend/public/defaultimg.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								frontend/public/defaultimg.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 76 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								frontend/public/logo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								frontend/public/logo.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 63 KiB  | 
							
								
								
									
										23
									
								
								frontend/public/metadata/docker.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								frontend/public/metadata/docker.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
{
 | 
			
		||||
  "slug": "docker",
 | 
			
		||||
  "logo": "https://raw.githubusercontent.com/loganmarchione/homelab-svg-assets/main/assets/docker.svg",
 | 
			
		||||
  "description": "Docker is an open-source project for automating the deployment of applications as portable, self-sufficient containers.",
 | 
			
		||||
  "date_created": "2024-05-02",
 | 
			
		||||
  "website": "https://www.docker.com/",
 | 
			
		||||
  "documentation": "",
 | 
			
		||||
  "default_credentials": {
 | 
			
		||||
    "username": "",
 | 
			
		||||
    "password": ""
 | 
			
		||||
  },
 | 
			
		||||
  "alerts": [
 | 
			
		||||
    {
 | 
			
		||||
      "alert": "If the LXC is created Privileged, the script will automatically set up USB passthrough."
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "alert": "Run Compose V2 by replacing the hyphen (-) with a space, using `docker compose`, instead of `docker-compose`."
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "alert": "Options to Install Portainer and/or Docker Compose V2"
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										20
									
								
								frontend/public/metadata/nginxproxymanager.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								frontend/public/metadata/nginxproxymanager.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
{
 | 
			
		||||
  "slug": "nginxproxymanager",
 | 
			
		||||
  "logo": "https://raw.githubusercontent.com/loganmarchione/homelab-svg-assets/main/assets/nginxproxymanager.svg",
 | 
			
		||||
  "description": "Nginx Proxy Manager is a tool that provides a web-based interface to manage Nginx reverse proxies. It enables users to easily and securely expose their services to the internet by providing features such as HTTPS encryption, domain mapping, and access control. It eliminates the need for manual configuration of Nginx reverse proxies, making it easy for users to quickly and securely expose their services to the public.",
 | 
			
		||||
  "date_created": "2024-05-02",
 | 
			
		||||
  "website": "https://nginxproxymanager.com/",
 | 
			
		||||
  "documentation": "",
 | 
			
		||||
  "default_credentials": {
 | 
			
		||||
    "username": "admin",
 | 
			
		||||
    "password": "admin"
 | 
			
		||||
  },
 | 
			
		||||
  "alerts": [
 | 
			
		||||
    {
 | 
			
		||||
      "alert": "Since there are hundreds of Certbot instances, it's necessary to install the specific Certbot of your preference."
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "alert": "This is another example of an alert."
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										23
									
								
								frontend/src/app/api/categories/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								frontend/src/app/api/categories/route.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
import { pb } from "@/lib/pocketbase";
 | 
			
		||||
import { Category } from "@/lib/types";
 | 
			
		||||
import { NextResponse } from "next/server";
 | 
			
		||||
 | 
			
		||||
export const dynamic = "force-static";
 | 
			
		||||
 | 
			
		||||
export async function GET() {
 | 
			
		||||
  try {
 | 
			
		||||
    const response = await pb.collection("categories").getFullList<Category>({
 | 
			
		||||
      expand: "items.alerts,items.alpine_script,items.default_login",
 | 
			
		||||
      sort: "order",
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return NextResponse.json(response);
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error("Error fetching categories:", error);
 | 
			
		||||
    return NextResponse.json(
 | 
			
		||||
      { error: "Failed to fetch categories" },
 | 
			
		||||
      { status: 500 },
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								frontend/src/app/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								frontend/src/app/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 1.8 KiB  | 
							
								
								
									
										88
									
								
								frontend/src/app/layout.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								frontend/src/app/layout.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,88 @@
 | 
			
		||||
import Footer from "@/components/Footer";
 | 
			
		||||
import Navbar from "@/components/Navbar";
 | 
			
		||||
import { ThemeProvider } from "@/components/theme-provider";
 | 
			
		||||
import { Toaster } from "@/components/ui/sonner";
 | 
			
		||||
import "@/styles/globals.css";
 | 
			
		||||
import { Inter } from "next/font/google";
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { NuqsAdapter } from "nuqs/adapters/next/app";
 | 
			
		||||
 | 
			
		||||
const inter = Inter({ subsets: ["latin"] });
 | 
			
		||||
 | 
			
		||||
export const metadata = {
 | 
			
		||||
  title: "Proxmox VE Helper-Scripts",
 | 
			
		||||
  generator: "Next.js",
 | 
			
		||||
  applicationName: "Proxmox VE Helper-Scripts",
 | 
			
		||||
  referrer: "origin-when-cross-origin",
 | 
			
		||||
  keywords: [
 | 
			
		||||
    "Proxmox VE",
 | 
			
		||||
    "Helper-Scripts",
 | 
			
		||||
    "tteck",
 | 
			
		||||
    "helper",
 | 
			
		||||
    "scripts",
 | 
			
		||||
    "proxmox",
 | 
			
		||||
    "VE",
 | 
			
		||||
  ],
 | 
			
		||||
  authors: { name: "Bram Suurd" },
 | 
			
		||||
  creator: "Bram Suurd",
 | 
			
		||||
  publisher: "Bram Suurd",
 | 
			
		||||
  description:
 | 
			
		||||
    "A Front-end for the Proxmox VE Helper-Scripts (Community) Repository. Featuring over 200+ scripts to help you manage your Proxmox VE environment.",
 | 
			
		||||
  favicon: "/app/favicon.ico",
 | 
			
		||||
  formatDetection: {
 | 
			
		||||
    email: false,
 | 
			
		||||
    address: false,
 | 
			
		||||
    telephone: false,
 | 
			
		||||
  },
 | 
			
		||||
  metadataBase: new URL("https://community-scripts.github.io/Proxmox/"),
 | 
			
		||||
  openGraph: {
 | 
			
		||||
    title: "Proxmox VE Helper-Scripts",
 | 
			
		||||
    description:
 | 
			
		||||
      "A Front-end for the Proxmox VE Helper-Scripts (Community) Repository. Featuring over 200+ scripts to help you manage your Proxmox VE environment.",
 | 
			
		||||
    url: "/defaultimg.png",
 | 
			
		||||
    images: [
 | 
			
		||||
      {
 | 
			
		||||
        url: "https://community-scripts.github.io/Proxmox/defaultimg.png",
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
    locale: "en_US",
 | 
			
		||||
    type: "website",
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default function RootLayout({
 | 
			
		||||
  children,
 | 
			
		||||
}: Readonly<{
 | 
			
		||||
  children: React.ReactNode;
 | 
			
		||||
}>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <html lang="en" suppressHydrationWarning>
 | 
			
		||||
      <head>
 | 
			
		||||
        <script
 | 
			
		||||
          defer
 | 
			
		||||
          src={`https://${process.env.NEXT_PUBLIC_ANALYTICS_URL}/script.js`}
 | 
			
		||||
          data-website-id={process.env.NEXT_PUBLIC_ANALYTICS_TOKEN}
 | 
			
		||||
        ></script>
 | 
			
		||||
        <link rel="manifest" href="manifest.webmanifest" />
 | 
			
		||||
        <link rel="preconnect" href={process.env.NEXT_PUBLIC_POCKETBASE_URL} />
 | 
			
		||||
        <link rel="preconnect" href="https://api.github.com" />
 | 
			
		||||
      </head>
 | 
			
		||||
      <body className={inter.className}>
 | 
			
		||||
        <ThemeProvider attribute="class" defaultTheme="dark" enableSystem>
 | 
			
		||||
          <div className="flex w-full flex-col justify-center">
 | 
			
		||||
            <Navbar />
 | 
			
		||||
            <div className="flex min-h-screen flex-col justify-center">
 | 
			
		||||
              <div className="flex w-full justify-center">
 | 
			
		||||
                <div className="w-full max-w-7xl ">
 | 
			
		||||
                  <NuqsAdapter>{children}</NuqsAdapter>
 | 
			
		||||
                  <Toaster richColors />
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
              <Footer />
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </ThemeProvider>
 | 
			
		||||
      </body>
 | 
			
		||||
    </html>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										27
									
								
								frontend/src/app/manifest.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								frontend/src/app/manifest.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
import type { MetadataRoute } from "next";
 | 
			
		||||
 | 
			
		||||
export const generateStaticParams = () => {
 | 
			
		||||
  return [];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default function manifest(): MetadataRoute.Manifest {
 | 
			
		||||
  return {
 | 
			
		||||
    name: "Proxmox VE Helper-Scripts",
 | 
			
		||||
    short_name: "Proxmox VE Helper-Scripts",
 | 
			
		||||
    description:
 | 
			
		||||
      "A Re-designed Front-end for the Proxmox VE Helper-Scripts Repository. Featuring over 150+ scripts to help you manage your Proxmox VE environment.",
 | 
			
		||||
    theme_color: "#030712",
 | 
			
		||||
    background_color: "#030712",
 | 
			
		||||
    display: "standalone",
 | 
			
		||||
    orientation: "portrait",
 | 
			
		||||
    scope: "/Proxmox/",
 | 
			
		||||
    start_url: "/Proxmox/",
 | 
			
		||||
    icons: [
 | 
			
		||||
      {
 | 
			
		||||
        src: "logo.png",
 | 
			
		||||
        sizes: "512x512",
 | 
			
		||||
        type: "image/png",
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										20
									
								
								frontend/src/app/not-found.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								frontend/src/app/not-found.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
"use client";
 | 
			
		||||
import { Button } from "@/components/ui/button";
 | 
			
		||||
 | 
			
		||||
export default function NotFoundPage() {
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="flex h-screen w-full flex-col items-center justify-center gap-5 bg-background px-4 md:px-6">
 | 
			
		||||
      <div className="space-y-2 text-center">
 | 
			
		||||
        <h1 className="text-4xl font-bold tracking-tighter sm:text-5xl md:text-6xl">
 | 
			
		||||
          404
 | 
			
		||||
        </h1>
 | 
			
		||||
        <p className="text-muted-foreground md:text-xl">
 | 
			
		||||
          Oops, the page you are looking for could not be found.
 | 
			
		||||
        </p>
 | 
			
		||||
      </div>
 | 
			
		||||
      <Button onClick={() => window.history.back()} variant="secondary">
 | 
			
		||||
        Go Back
 | 
			
		||||
      </Button>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										123
									
								
								frontend/src/app/page.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								frontend/src/app/page.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,123 @@
 | 
			
		||||
"use client";
 | 
			
		||||
import AnimatedGradientText from "@/components/ui/animated-gradient-text";
 | 
			
		||||
import Particles from "@/components/ui/particles";
 | 
			
		||||
import { Button } from "@/components/ui/button";
 | 
			
		||||
import { Separator } from "@/components/ui/separator";
 | 
			
		||||
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 { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
 | 
			
		||||
import { CardFooter } from "@/components/ui/card";
 | 
			
		||||
import { FaGithub } from "react-icons/fa";
 | 
			
		||||
 | 
			
		||||
function CustomArrowRightIcon() {
 | 
			
		||||
  return <ArrowRightIcon className="h-4 w-4" width={1} />;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function Page() {
 | 
			
		||||
  const { theme } = useTheme();
 | 
			
		||||
 | 
			
		||||
  const [color, setColor] = useState("#000000");
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setColor(theme === "dark" ? "#ffffff" : "#000000");
 | 
			
		||||
  }, [theme]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="w-full">
 | 
			
		||||
      <Particles
 | 
			
		||||
        className="absolute inset-0 -z-40"
 | 
			
		||||
        quantity={100}
 | 
			
		||||
        ease={80}
 | 
			
		||||
        color={color}
 | 
			
		||||
        refresh
 | 
			
		||||
      />
 | 
			
		||||
      <div className="container mx-auto">
 | 
			
		||||
        <div className="flex h-[80vh] flex-col items-center justify-center gap-4 py-20 lg:py-40">
 | 
			
		||||
          <Dialog>
 | 
			
		||||
            <DialogTrigger>
 | 
			
		||||
              <div>
 | 
			
		||||
                <AnimatedGradientText>
 | 
			
		||||
                  <div
 | 
			
		||||
                    className={cn(
 | 
			
		||||
                      `absolute inset-0 block size-full animate-gradient bg-gradient-to-r from-[#ffaa40]/50 via-[#9c40ff]/50 to-[#ffaa40]/50 bg-[length:var(--bg-size)_100%] [border-radius:inherit] [mask:linear-gradient(#fff_0_0)_content-box,linear-gradient(#fff_0_0)]`,
 | 
			
		||||
                      `p-px ![mask-composite:subtract]`,
 | 
			
		||||
                    )}
 | 
			
		||||
                  />
 | 
			
		||||
                  ❤️ <Separator className="mx-2 h-4" orientation="vertical" />
 | 
			
		||||
                  <span
 | 
			
		||||
                    className={cn(
 | 
			
		||||
                      `animate-gradient bg-gradient-to-r from-[#ffaa40] via-[#9c40ff] to-[#ffaa40] bg-[length:var(--bg-size)_100%] bg-clip-text text-transparent`,
 | 
			
		||||
                      `inline`,
 | 
			
		||||
                    )}
 | 
			
		||||
                  >
 | 
			
		||||
                    Scripts by Tteck
 | 
			
		||||
                  </span>
 | 
			
		||||
                </AnimatedGradientText>
 | 
			
		||||
              </div>
 | 
			
		||||
            </DialogTrigger>
 | 
			
		||||
            <DialogContent>
 | 
			
		||||
              <DialogHeader>
 | 
			
		||||
                <DialogTitle>Thank You!</DialogTitle>
 | 
			
		||||
                <DialogDescription>
 | 
			
		||||
                  A big thank you to Tteck and the many contributors who have
 | 
			
		||||
                  made this project possible. Your hard work is truly
 | 
			
		||||
                  appreciated by the entire Proxmox community!
 | 
			
		||||
                </DialogDescription>
 | 
			
		||||
              </DialogHeader>
 | 
			
		||||
              <CardFooter className="flex flex-col gap-2">
 | 
			
		||||
                <Button className="w-full" variant="outline" asChild>
 | 
			
		||||
                  <a
 | 
			
		||||
                    href="https://github.com/tteck"
 | 
			
		||||
                    target="_blank"
 | 
			
		||||
                    rel="noopener noreferrer"
 | 
			
		||||
                    className="flex items-center justify-center"
 | 
			
		||||
                  >
 | 
			
		||||
                    <FaGithub className="mr-2 h-4 w-4" /> Tteck's GitHub
 | 
			
		||||
                  </a>
 | 
			
		||||
                </Button>
 | 
			
		||||
                <Button className="w-full" asChild>
 | 
			
		||||
                  <a
 | 
			
		||||
                    href="https://github.com/community-scripts/ProxmoxVE"
 | 
			
		||||
                    target="_blank"
 | 
			
		||||
                    rel="noopener noreferrer"
 | 
			
		||||
                    className="flex items-center justify-center"
 | 
			
		||||
                  >
 | 
			
		||||
                    <ExternalLink className="mr-2 h-4 w-4" /> Proxmox Helper
 | 
			
		||||
                    Scripts
 | 
			
		||||
                  </a>
 | 
			
		||||
                </Button>
 | 
			
		||||
              </CardFooter>
 | 
			
		||||
            </DialogContent>
 | 
			
		||||
          </Dialog>
 | 
			
		||||
 | 
			
		||||
          <div className="flex flex-col gap-4">
 | 
			
		||||
            <h1 className="max-w-2xl text-center text-5xl font-semibold tracking-tighter md:text-7xl">
 | 
			
		||||
              Make managing your Homelab a breeze
 | 
			
		||||
            </h1>
 | 
			
		||||
            <p className="max-w-2xl text-center text-lg leading-relaxed tracking-tight text-muted-foreground md:text-xl">
 | 
			
		||||
              200+ scripts to help you manage your <b>Proxmox VE environment</b>
 | 
			
		||||
              . Whether you're a seasoned user or a newcomer, Proxmox VE
 | 
			
		||||
              Helper Scripts has got you covered.
 | 
			
		||||
            </p>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div className="flex flex-row gap-3">
 | 
			
		||||
            <Link href="/scripts">
 | 
			
		||||
              <Button
 | 
			
		||||
                size="lg"
 | 
			
		||||
                variant="expandIcon"
 | 
			
		||||
                Icon={CustomArrowRightIcon}
 | 
			
		||||
                iconPlacement="right"
 | 
			
		||||
                className="hover:"
 | 
			
		||||
              >
 | 
			
		||||
                View Scripts
 | 
			
		||||
              </Button>
 | 
			
		||||
            </Link>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										13
									
								
								frontend/src/app/robots.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								frontend/src/app/robots.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
import type { MetadataRoute } from "next";
 | 
			
		||||
 | 
			
		||||
export const dynamic = "force-static";
 | 
			
		||||
 | 
			
		||||
export default function robots(): MetadataRoute.Robots {
 | 
			
		||||
  return {
 | 
			
		||||
    rules: {
 | 
			
		||||
      userAgent: "*",
 | 
			
		||||
      allow: "/",
 | 
			
		||||
    },
 | 
			
		||||
    sitemap: "https://community-scripts.github.io/Proxmox/sitemap.xml",
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										147
									
								
								frontend/src/app/scripts/_components/ScriptAccordion.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								frontend/src/app/scripts/_components/ScriptAccordion.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,147 @@
 | 
			
		||||
import { useCallback, useEffect, useRef } from "react";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  Accordion,
 | 
			
		||||
  AccordionContent,
 | 
			
		||||
  AccordionItem,
 | 
			
		||||
  AccordionTrigger,
 | 
			
		||||
} from "@/components/ui/accordion";
 | 
			
		||||
import { Category } from "@/lib/types";
 | 
			
		||||
import { cn } from "@/lib/utils";
 | 
			
		||||
import { Star } from "lucide-react";
 | 
			
		||||
import Image from "next/image";
 | 
			
		||||
import Link from "next/link";
 | 
			
		||||
import { useState } from "react";
 | 
			
		||||
import { Badge } from "../../../components/ui/badge";
 | 
			
		||||
 | 
			
		||||
export default function ScriptAccordion({
 | 
			
		||||
  items,
 | 
			
		||||
  selectedScript,
 | 
			
		||||
  setSelectedScript,
 | 
			
		||||
}: {
 | 
			
		||||
  items: Category[];
 | 
			
		||||
  selectedScript: string | null;
 | 
			
		||||
  setSelectedScript: (script: string | null) => void;
 | 
			
		||||
}) {
 | 
			
		||||
  const [expandedItem, setExpandedItem] = useState<string | undefined>(
 | 
			
		||||
    undefined,
 | 
			
		||||
  );
 | 
			
		||||
  const linkRefs = useRef<{ [key: string]: HTMLAnchorElement | null }>({});
 | 
			
		||||
  
 | 
			
		||||
  const handleAccordionChange = (value: string | undefined) => {
 | 
			
		||||
    setExpandedItem(value);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleSelected = useCallback(
 | 
			
		||||
    (title: string) => {
 | 
			
		||||
      setSelectedScript(title);
 | 
			
		||||
    },
 | 
			
		||||
    [setSelectedScript],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (selectedScript) {
 | 
			
		||||
      const category = items.find((category) =>
 | 
			
		||||
        category.expand.items.some((script) => script.title === selectedScript),
 | 
			
		||||
      );
 | 
			
		||||
      if (category) {
 | 
			
		||||
        setExpandedItem(category.catagoryName);
 | 
			
		||||
        handleSelected(selectedScript);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }, [selectedScript, items, handleSelected]);
 | 
			
		||||
  return (
 | 
			
		||||
    <Accordion
 | 
			
		||||
      type="single"
 | 
			
		||||
      value={expandedItem}
 | 
			
		||||
      onValueChange={handleAccordionChange}
 | 
			
		||||
      collapsible
 | 
			
		||||
    >
 | 
			
		||||
      {items.map((category) => (
 | 
			
		||||
        <AccordionItem
 | 
			
		||||
          key={category.id + ":category"}
 | 
			
		||||
          value={category.catagoryName}
 | 
			
		||||
          className={cn("sm:text-md flex flex-col border-none", {
 | 
			
		||||
            "rounded-lg bg-accent/30": expandedItem === category.catagoryName,
 | 
			
		||||
          })}
 | 
			
		||||
        >
 | 
			
		||||
          <AccordionTrigger
 | 
			
		||||
            className={cn(
 | 
			
		||||
              "duration-250 rounded-lg transition ease-in-out hover:-translate-y-1 hover:scale-105 hover:bg-accent",
 | 
			
		||||
              { "": expandedItem === category.catagoryName },
 | 
			
		||||
            )}
 | 
			
		||||
          >
 | 
			
		||||
            <div className="mr-2 flex w-full items-center justify-between">
 | 
			
		||||
              <span className="pl-2">{category.catagoryName} </span>
 | 
			
		||||
              <span className="rounded-full bg-gray-200 px-2 py-1 text-xs text-muted-foreground hover:no-underline dark:bg-blue-800/20">
 | 
			
		||||
                {category.expand.items.length}
 | 
			
		||||
              </span>
 | 
			
		||||
            </div>{" "}
 | 
			
		||||
          </AccordionTrigger>
 | 
			
		||||
          <AccordionContent
 | 
			
		||||
            data-state={
 | 
			
		||||
              expandedItem === category.catagoryName ? "open" : "closed"
 | 
			
		||||
            }
 | 
			
		||||
            className="pt-0"
 | 
			
		||||
          >
 | 
			
		||||
            {category.expand.items
 | 
			
		||||
              .slice()
 | 
			
		||||
              .sort((a, b) => a.title.localeCompare(b.title))
 | 
			
		||||
              .map((script, index) => (
 | 
			
		||||
                <div key={index}>
 | 
			
		||||
                  <Link
 | 
			
		||||
                    href={{
 | 
			
		||||
                      pathname: "/scripts",
 | 
			
		||||
                      query: { id: script.title },
 | 
			
		||||
                    }}
 | 
			
		||||
                    prefetch={false}
 | 
			
		||||
                    className={`flex cursor-pointer items-center justify-between gap-1 px-1 py-1 text-muted-foreground hover:rounded-lg hover:bg-accent/60 hover:dark:bg-accent/20 ${
 | 
			
		||||
                      selectedScript === script.title
 | 
			
		||||
                        ? "rounded-lg bg-accent font-semibold dark:bg-accent/30 dark:text-white"
 | 
			
		||||
                        : ""
 | 
			
		||||
                    }`}
 | 
			
		||||
                    onClick={() => handleSelected(script.title)}
 | 
			
		||||
                    ref={(el) => {
 | 
			
		||||
                      linkRefs.current[script.title] = el;
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    <Image
 | 
			
		||||
                      src={script.logo}
 | 
			
		||||
                      height={16}
 | 
			
		||||
                      width={16}
 | 
			
		||||
                      unoptimized
 | 
			
		||||
                      onError={(e) =>
 | 
			
		||||
                        ((e.currentTarget as HTMLImageElement).src =
 | 
			
		||||
                          "/logo.png")
 | 
			
		||||
                      }
 | 
			
		||||
                      alt={script.title}
 | 
			
		||||
                      className="mr-1 w-4 h-4 rounded-full"
 | 
			
		||||
                    />
 | 
			
		||||
                    <span className="flex items-center gap-2">
 | 
			
		||||
                      {script.title}
 | 
			
		||||
                      {script.isMostViewed && (
 | 
			
		||||
                        <Star className="h-3 w-3 text-yellow-500"></Star>
 | 
			
		||||
                      )}
 | 
			
		||||
                    </span>
 | 
			
		||||
                    <Badge
 | 
			
		||||
                      className={cn(
 | 
			
		||||
                        "ml-auto w-[37.69px] justify-center text-center",
 | 
			
		||||
                        {
 | 
			
		||||
                          "text-primary/75": script.item_type === "VM",
 | 
			
		||||
                          "text-yellow-500/75": script.item_type === "LXC",
 | 
			
		||||
                          "border-none": script.item_type === "",
 | 
			
		||||
                          hidden: !["VM", "LXC", ""].includes(script.item_type),
 | 
			
		||||
                        },
 | 
			
		||||
                      )}
 | 
			
		||||
                    >
 | 
			
		||||
                      {script.item_type}
 | 
			
		||||
                    </Badge>
 | 
			
		||||
                  </Link>
 | 
			
		||||
                </div>
 | 
			
		||||
              ))}
 | 
			
		||||
          </AccordionContent>
 | 
			
		||||
        </AccordionItem>
 | 
			
		||||
      ))}
 | 
			
		||||
    </Accordion>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										219
									
								
								frontend/src/app/scripts/_components/ScriptInfoBlocks.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										219
									
								
								frontend/src/app/scripts/_components/ScriptInfoBlocks.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,219 @@
 | 
			
		||||
import { Button } from "@/components/ui/button";
 | 
			
		||||
import {
 | 
			
		||||
  Card,
 | 
			
		||||
  CardContent,
 | 
			
		||||
  CardDescription,
 | 
			
		||||
  CardFooter,
 | 
			
		||||
  CardHeader,
 | 
			
		||||
  CardTitle,
 | 
			
		||||
} from "@/components/ui/card";
 | 
			
		||||
import { extractDate } from "@/lib/time";
 | 
			
		||||
import { Category } from "@/lib/types";
 | 
			
		||||
import { CalendarPlus } from "lucide-react";
 | 
			
		||||
import Image from "next/image";
 | 
			
		||||
import Link from "next/link";
 | 
			
		||||
import { useMemo, useState } from "react";
 | 
			
		||||
 | 
			
		||||
const ITEMS_PER_PAGE = 3;
 | 
			
		||||
 | 
			
		||||
export function LatestScripts({ items }: { items: Category[] }) {
 | 
			
		||||
  const [page, setPage] = useState(1);
 | 
			
		||||
 | 
			
		||||
  const latestScripts = useMemo(() => {
 | 
			
		||||
    if (!items) return [];
 | 
			
		||||
    const scripts = items.flatMap((category) => category.expand.items || []);
 | 
			
		||||
    return scripts.sort(
 | 
			
		||||
      (a, b) => new Date(b.created).getTime() - new Date(a.created).getTime(),
 | 
			
		||||
    );
 | 
			
		||||
  }, [items]);
 | 
			
		||||
 | 
			
		||||
  const goToNextPage = () => {
 | 
			
		||||
    setPage((prevPage) => prevPage + 1);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const goToPreviousPage = () => {
 | 
			
		||||
    setPage((prevPage) => prevPage - 1);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const startIndex = (page - 1) * ITEMS_PER_PAGE;
 | 
			
		||||
  const endIndex = page * ITEMS_PER_PAGE;
 | 
			
		||||
 | 
			
		||||
  if (!items) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="">
 | 
			
		||||
      {latestScripts.length > 0 && (
 | 
			
		||||
        <div className="flex w-full items-center justify-between">
 | 
			
		||||
          <h2 className="text-lg font-semibold">Newest Scripts</h2>
 | 
			
		||||
          <div className="flex items-center justify-end gap-1">
 | 
			
		||||
            {page > 1 && (
 | 
			
		||||
              <div
 | 
			
		||||
                className="cursor-pointer select-none p-2 text-sm font-semibold"
 | 
			
		||||
                onClick={goToPreviousPage}
 | 
			
		||||
              >
 | 
			
		||||
                Previous
 | 
			
		||||
              </div>
 | 
			
		||||
            )}
 | 
			
		||||
            {endIndex < latestScripts.length && (
 | 
			
		||||
              <div
 | 
			
		||||
                onClick={goToNextPage}
 | 
			
		||||
                className="cursor-pointer select-none p-2 text-sm font-semibold"
 | 
			
		||||
              >
 | 
			
		||||
                {page === 1 ? "More.." : "Next"}
 | 
			
		||||
              </div>
 | 
			
		||||
            )}
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
      <div className="min-w flex w-full flex-row flex-wrap gap-4">
 | 
			
		||||
        {latestScripts.slice(startIndex, endIndex).map((item) => (
 | 
			
		||||
          <Card
 | 
			
		||||
            key={item.id}
 | 
			
		||||
            className="min-w-[250px] flex-1 flex-grow bg-accent/30"
 | 
			
		||||
          >
 | 
			
		||||
            <CardHeader>
 | 
			
		||||
              <CardTitle className="flex items-center gap-3">
 | 
			
		||||
                <div className="flex h-16 w-16 items-center justify-center rounded-lg bg-accent p-1">
 | 
			
		||||
                  <Image
 | 
			
		||||
                    src={item.logo}
 | 
			
		||||
                    unoptimized
 | 
			
		||||
                    height={64}
 | 
			
		||||
                    width={64}
 | 
			
		||||
                    alt=""
 | 
			
		||||
                    className="h-11 w-11 object-contain"
 | 
			
		||||
                  />
 | 
			
		||||
                </div>
 | 
			
		||||
                <div className="flex flex-col">
 | 
			
		||||
                  <p className="text-lg line-clamp-1">
 | 
			
		||||
                    {item.title} {item.item_type}
 | 
			
		||||
                  </p>
 | 
			
		||||
                  <p className="text-sm text-muted-foreground flex items-center gap-1">
 | 
			
		||||
                    <CalendarPlus className="h-4 w-4" />
 | 
			
		||||
                    {extractDate(item.created)}
 | 
			
		||||
                  </p>
 | 
			
		||||
                </div>
 | 
			
		||||
              </CardTitle>
 | 
			
		||||
            </CardHeader>
 | 
			
		||||
            <CardContent>
 | 
			
		||||
              <CardDescription className="line-clamp-3 text-card-foreground">
 | 
			
		||||
                {item.description}
 | 
			
		||||
              </CardDescription>
 | 
			
		||||
            </CardContent>
 | 
			
		||||
            <CardFooter className="">
 | 
			
		||||
              <Button asChild variant="outline">
 | 
			
		||||
                <Link
 | 
			
		||||
                  href={{
 | 
			
		||||
                    pathname: "/scripts",
 | 
			
		||||
                    query: { id: item.title },
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  View Script
 | 
			
		||||
                </Link>
 | 
			
		||||
              </Button>
 | 
			
		||||
            </CardFooter>
 | 
			
		||||
          </Card>
 | 
			
		||||
        ))}
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function MostViewedScripts({ items }: { items: Category[] }) {
 | 
			
		||||
  const [page, setPage] = useState(1);
 | 
			
		||||
 | 
			
		||||
  const mostViewedScripts = useMemo(() => {
 | 
			
		||||
    if (!items) return [];
 | 
			
		||||
    const scripts = items.flatMap((category) => category.expand.items || []);
 | 
			
		||||
    const mostViewedScripts = scripts
 | 
			
		||||
      .filter((script) => script.isMostViewed)
 | 
			
		||||
      .map((script) => ({
 | 
			
		||||
        ...script,
 | 
			
		||||
      }));
 | 
			
		||||
    return mostViewedScripts;
 | 
			
		||||
  }, [items]);
 | 
			
		||||
 | 
			
		||||
  const goToNextPage = () => {
 | 
			
		||||
    setPage((prevPage) => prevPage + 1);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const goToPreviousPage = () => {
 | 
			
		||||
    setPage((prevPage) => prevPage - 1);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const startIndex = (page - 1) * ITEMS_PER_PAGE;
 | 
			
		||||
  const endIndex = page * ITEMS_PER_PAGE;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="">
 | 
			
		||||
      {mostViewedScripts.length > 0 && (
 | 
			
		||||
        <>
 | 
			
		||||
          <h2 className="text-lg font-semibold">Most Viewed Scripts</h2>
 | 
			
		||||
        </>
 | 
			
		||||
      )}
 | 
			
		||||
      <div className="min-w flex w-full flex-row flex-wrap gap-4">
 | 
			
		||||
        {mostViewedScripts.slice(startIndex, endIndex).map((item) => (
 | 
			
		||||
          <Card
 | 
			
		||||
            key={item.id}
 | 
			
		||||
            className="min-w-[250px] flex-1 flex-grow bg-accent/30"
 | 
			
		||||
          >
 | 
			
		||||
            <CardHeader>
 | 
			
		||||
              <CardTitle className="flex items-center gap-3">
 | 
			
		||||
                <div className="flex max-h-16 min-h-16 min-w-16 max-w-16 items-center justify-center rounded-lg bg-accent p-1">
 | 
			
		||||
                  <Image
 | 
			
		||||
                    unoptimized
 | 
			
		||||
                    src={item.logo}
 | 
			
		||||
                    height={64}
 | 
			
		||||
                    width={64}
 | 
			
		||||
                    alt=""
 | 
			
		||||
                    className="h-11 w-11 object-contain"
 | 
			
		||||
                  />
 | 
			
		||||
                </div>
 | 
			
		||||
                <div className="flex flex-col">
 | 
			
		||||
                  <p className="line-clamp-1 text-lg">
 | 
			
		||||
                    {item.title} {item.item_type}
 | 
			
		||||
                  </p>
 | 
			
		||||
                  <p className="flex items-center gap-1 text-sm text-muted-foreground">
 | 
			
		||||
                    <CalendarPlus className="h-4 w-4" />
 | 
			
		||||
                    {extractDate(item.created)}
 | 
			
		||||
                  </p>
 | 
			
		||||
                </div>
 | 
			
		||||
              </CardTitle>
 | 
			
		||||
            </CardHeader>
 | 
			
		||||
            <CardContent>
 | 
			
		||||
              <CardDescription className="line-clamp-3 text-card-foreground break-words">
 | 
			
		||||
                {item.description}
 | 
			
		||||
              </CardDescription>
 | 
			
		||||
            </CardContent>
 | 
			
		||||
            <CardFooter className="">
 | 
			
		||||
              <Button asChild variant="outline">
 | 
			
		||||
                <Link
 | 
			
		||||
                  href={{
 | 
			
		||||
                    pathname: "/scripts",
 | 
			
		||||
                    query: { id: item.title },
 | 
			
		||||
                  }}
 | 
			
		||||
                  prefetch={false}
 | 
			
		||||
                >
 | 
			
		||||
                  View Script
 | 
			
		||||
                </Link>
 | 
			
		||||
              </Button>
 | 
			
		||||
            </CardFooter>
 | 
			
		||||
          </Card>
 | 
			
		||||
        ))}
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="flex justify-end gap-1 p-2">
 | 
			
		||||
        {page > 1 && (
 | 
			
		||||
          <Button onClick={goToPreviousPage} variant="outline">
 | 
			
		||||
            Previous
 | 
			
		||||
          </Button>
 | 
			
		||||
        )}
 | 
			
		||||
        {endIndex < mostViewedScripts.length && (
 | 
			
		||||
          <Button onClick={goToNextPage} variant="outline">
 | 
			
		||||
            {page === 1 ? "More.." : "Next"}
 | 
			
		||||
          </Button>
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										95
									
								
								frontend/src/app/scripts/_components/ScriptItem.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								frontend/src/app/scripts/_components/ScriptItem.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,95 @@
 | 
			
		||||
"use client";
 | 
			
		||||
import { Separator } from "@/components/ui/separator";
 | 
			
		||||
import { extractDate } from "@/lib/time";
 | 
			
		||||
import { Script } from "@/lib/types";
 | 
			
		||||
import { X } from "lucide-react";
 | 
			
		||||
import Image from "next/image";
 | 
			
		||||
 | 
			
		||||
import Alerts from "./ScriptItems/Alerts";
 | 
			
		||||
import Buttons from "./ScriptItems/Buttons";
 | 
			
		||||
import DefaultPassword from "./ScriptItems/DefaultPassword";
 | 
			
		||||
import DefaultSettings from "./ScriptItems/DefaultSettings";
 | 
			
		||||
import Description from "./ScriptItems/Description";
 | 
			
		||||
import InstallCommand from "./ScriptItems/InstallCommand";
 | 
			
		||||
import InterFaces from "./ScriptItems/InterFaces";
 | 
			
		||||
import Tooltips from "./ScriptItems/Tooltips";
 | 
			
		||||
 | 
			
		||||
function ScriptItem({
 | 
			
		||||
  item,
 | 
			
		||||
  setSelectedScript,
 | 
			
		||||
}: {
 | 
			
		||||
  item: Script;
 | 
			
		||||
  setSelectedScript: (script: string | null) => void;
 | 
			
		||||
}) {
 | 
			
		||||
  const closeScript = () => {
 | 
			
		||||
    window.history.pushState({}, document.title, window.location.pathname);
 | 
			
		||||
    setSelectedScript(null);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="mr-7 mt-0 flex w-full min-w-fit">
 | 
			
		||||
      <div className="flex w-full min-w-fit">
 | 
			
		||||
        <div className="flex w-full flex-col">
 | 
			
		||||
          <div className="flex h-[36px] min-w-max items-center justify-between">
 | 
			
		||||
            <h2 className="text-lg font-semibold">Selected Script</h2>
 | 
			
		||||
            <X onClick={closeScript} className="cursor-pointer" />
 | 
			
		||||
          </div>
 | 
			
		||||
          <div className="rounded-lg border bg-accent/20 p-4">
 | 
			
		||||
            <div className="flex justify-between">
 | 
			
		||||
              <div className="flex">
 | 
			
		||||
                <Image
 | 
			
		||||
                  className="h-32 w-32 rounded-lg bg-accent/60 object-contain p-3 shadow-md"
 | 
			
		||||
                  src={item.logo}
 | 
			
		||||
                  width={400}
 | 
			
		||||
                  onError={(e) =>
 | 
			
		||||
                    ((e.currentTarget as HTMLImageElement).src = "/logo.png")
 | 
			
		||||
                  }
 | 
			
		||||
                  height={400}
 | 
			
		||||
                  alt={item.title}
 | 
			
		||||
                  unoptimized
 | 
			
		||||
                />
 | 
			
		||||
                <div className="ml-4 flex flex-col justify-between">
 | 
			
		||||
                  <div className="flex h-full w-full flex-col justify-between">
 | 
			
		||||
                    <div>
 | 
			
		||||
                      <h1 className="text-lg font-semibold">{item.title}</h1>
 | 
			
		||||
                      <p className="w-full text-sm text-muted-foreground">
 | 
			
		||||
                        Date added: {extractDate(item.created)}
 | 
			
		||||
                      </p>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div className="flex gap-5">
 | 
			
		||||
                      <DefaultSettings item={item} />
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div className="hidden flex-col justify-between gap-2 sm:flex">
 | 
			
		||||
                <InterFaces item={item} />
 | 
			
		||||
                <Buttons item={item} />
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <Separator className="mt-4" />
 | 
			
		||||
            <div>
 | 
			
		||||
              <div className="mt-4">
 | 
			
		||||
                <Description item={item} />
 | 
			
		||||
                <Alerts item={item} />
 | 
			
		||||
              </div>
 | 
			
		||||
              <div className="mt-4 rounded-lg border bg-accent/50">
 | 
			
		||||
                <div className="flex gap-3 px-4 py-2">
 | 
			
		||||
                  <h2 className="text-lg font-semibold">
 | 
			
		||||
                    How to {item.item_type ? "install" : "use"}
 | 
			
		||||
                  </h2>
 | 
			
		||||
                  <Tooltips item={item} />
 | 
			
		||||
                </div>
 | 
			
		||||
                <Separator className="w-full"></Separator>
 | 
			
		||||
                <InstallCommand item={item} />
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <DefaultPassword item={item} />
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ScriptItem;
 | 
			
		||||
							
								
								
									
										19
									
								
								frontend/src/app/scripts/_components/ScriptItems/Alerts.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								frontend/src/app/scripts/_components/ScriptItems/Alerts.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
import TextCopyBlock from "@/lib/TextCopyBlock";
 | 
			
		||||
import { Script } from "@/lib/types";
 | 
			
		||||
import { Info } from "lucide-react";
 | 
			
		||||
 | 
			
		||||
export default function Alerts({ item }: { item: Script }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {item.expand?.alerts?.length > 0 &&
 | 
			
		||||
        item.expand.alerts.map((alert: any, index: number) => (
 | 
			
		||||
          <div key={index} className="mt-4 flex flex-col gap-2">
 | 
			
		||||
            <p className="inline-flex items-center gap-2 rounded-lg border border-red-500/25 bg-destructive/25 p-2 pl-4 text-sm">
 | 
			
		||||
              <Info className="h-4 min-h-4 w-4 min-w-4" />
 | 
			
		||||
              <span>{TextCopyBlock(alert.content)}</span>
 | 
			
		||||
            </p>
 | 
			
		||||
          </div>
 | 
			
		||||
        ))}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										74
									
								
								frontend/src/app/scripts/_components/ScriptItems/Buttons.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								frontend/src/app/scripts/_components/ScriptItems/Buttons.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,74 @@
 | 
			
		||||
import { Button } from "@/components/ui/button";
 | 
			
		||||
import { Script } from "@/lib/types";
 | 
			
		||||
import { BookOpenText, Code, ExternalLink, Globe } from "lucide-react";
 | 
			
		||||
import Link from "next/link";
 | 
			
		||||
import { useMemo } from "react";
 | 
			
		||||
 | 
			
		||||
export default function Buttons({ item }: { item: Script }) {
 | 
			
		||||
  const pattern = useMemo(
 | 
			
		||||
    () =>
 | 
			
		||||
      /(https:\/\/github\.com\/community-scripts\/ProxmoxVE\/raw\/main\/(ct|misc|vm)\/([^\/]+)\.sh)/,
 | 
			
		||||
    [],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
    const transformUrlToInstallScript = (url: string): string => {
 | 
			
		||||
      if (url.includes("/pve/")) {
 | 
			
		||||
        return url;
 | 
			
		||||
      } else if (url.includes("/ct/")) {
 | 
			
		||||
        return url.replace("/ct/", "/install/").replace(/\.sh$/, "-install.sh");
 | 
			
		||||
      }
 | 
			
		||||
      return url;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
  const sourceUrl = useMemo(() => {
 | 
			
		||||
    if (item.installCommand) {
 | 
			
		||||
      const match = item.installCommand.match(pattern);
 | 
			
		||||
      return match ? transformUrlToInstallScript(match[0]) : null;
 | 
			
		||||
    }
 | 
			
		||||
    return null;
 | 
			
		||||
  }, [item.installCommand, pattern]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="flex flex-wrap justify-end gap-2">
 | 
			
		||||
      {item.website && (
 | 
			
		||||
        <Button variant="secondary" asChild>
 | 
			
		||||
          <Link target="_blank" href={item.website}>
 | 
			
		||||
            <span className="flex items-center gap-2">
 | 
			
		||||
              <Globe className="h-4 w-4" /> Website
 | 
			
		||||
            </span>
 | 
			
		||||
          </Link>
 | 
			
		||||
        </Button>
 | 
			
		||||
      )}
 | 
			
		||||
      {item.documentation && (
 | 
			
		||||
        <Button variant="secondary" asChild>
 | 
			
		||||
          <Link target="_blank" href={item.documentation}>
 | 
			
		||||
            <span className="flex items-center gap-2">
 | 
			
		||||
              <BookOpenText className="h-4 w-4" />
 | 
			
		||||
              Documentation
 | 
			
		||||
            </span>
 | 
			
		||||
          </Link>
 | 
			
		||||
        </Button>
 | 
			
		||||
      )}
 | 
			
		||||
      {item.post_install && (
 | 
			
		||||
        <Button variant="secondary" asChild>
 | 
			
		||||
          <Link target="_blank" href={item.post_install}>
 | 
			
		||||
            <span className="flex items-center gap-2">
 | 
			
		||||
              <ExternalLink className="h-4 w-4" />
 | 
			
		||||
              Post Install
 | 
			
		||||
            </span>
 | 
			
		||||
          </Link>
 | 
			
		||||
        </Button>
 | 
			
		||||
      )}
 | 
			
		||||
      {item.installCommand && sourceUrl && (
 | 
			
		||||
        <Button variant="secondary" asChild>
 | 
			
		||||
          <Link target="_blank" href={transformUrlToInstallScript(sourceUrl)}>
 | 
			
		||||
            <span className="flex items-center gap-2">
 | 
			
		||||
              <Code className="h-4 w-4" />
 | 
			
		||||
              Source Code
 | 
			
		||||
            </span>
 | 
			
		||||
          </Link>
 | 
			
		||||
        </Button>
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,51 @@
 | 
			
		||||
import { Button } from "@/components/ui/button";
 | 
			
		||||
import { Separator } from "@/components/ui/separator";
 | 
			
		||||
import handleCopy from "@/components/handleCopy";
 | 
			
		||||
import { Script } from "@/lib/types";
 | 
			
		||||
 | 
			
		||||
export default function DefaultPassword({ item }: { item: Script }) {
 | 
			
		||||
  const hasDefaultLogin = item?.expand?.default_login !== undefined;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      {hasDefaultLogin && (
 | 
			
		||||
        <div className="mt-4 rounded-lg border bg-accent/50">
 | 
			
		||||
          <div className="flex gap-3 px-4 py-2">
 | 
			
		||||
            <h2 className="text-lg font-semibold">Default Login Credentials</h2>
 | 
			
		||||
          </div>
 | 
			
		||||
          <Separator className="w-full"></Separator>
 | 
			
		||||
          <div className="flex flex-col gap-2 p-4">
 | 
			
		||||
            <p className="mb-2 text-sm">
 | 
			
		||||
              You can use the following credentials to login to the {""}
 | 
			
		||||
              {item.title} {item.item_type}.
 | 
			
		||||
            </p>
 | 
			
		||||
            <div className="text-sm">
 | 
			
		||||
              Username:{" "}
 | 
			
		||||
              <Button
 | 
			
		||||
                variant={"secondary"}
 | 
			
		||||
                size={"null"}
 | 
			
		||||
                onClick={() =>
 | 
			
		||||
                  handleCopy("username", item.expand.default_login.username)
 | 
			
		||||
                }
 | 
			
		||||
              >
 | 
			
		||||
                {item.expand.default_login.username}
 | 
			
		||||
              </Button>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div className="text-sm">
 | 
			
		||||
              Password:{" "}
 | 
			
		||||
              <Button
 | 
			
		||||
                variant={"secondary"}
 | 
			
		||||
                size={"null"}
 | 
			
		||||
                onClick={() =>
 | 
			
		||||
                  handleCopy("password", item.expand.default_login.password)
 | 
			
		||||
                }
 | 
			
		||||
              >
 | 
			
		||||
                {item.expand.default_login.password}
 | 
			
		||||
              </Button>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,38 @@
 | 
			
		||||
import { Script } from "@/lib/types";
 | 
			
		||||
 | 
			
		||||
export default function DefaultSettings({ item }: { item: Script }) {
 | 
			
		||||
  const hasAlpineScript = item?.expand?.alpine_script !== undefined;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {item.default_cpu && (
 | 
			
		||||
        <div>
 | 
			
		||||
          <h2 className="text-md font-semibold">Default settings</h2>
 | 
			
		||||
          <p className="text-sm text-muted-foreground">
 | 
			
		||||
            CPU: {item.default_cpu}
 | 
			
		||||
          </p>
 | 
			
		||||
          <p className="text-sm text-muted-foreground">
 | 
			
		||||
            RAM: {item.default_ram}
 | 
			
		||||
          </p>
 | 
			
		||||
          <p className="text-sm text-muted-foreground">
 | 
			
		||||
            HDD: {item.default_hdd}
 | 
			
		||||
          </p>
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
      {hasAlpineScript && (
 | 
			
		||||
        <div>
 | 
			
		||||
          <h2 className="text-md font-semibold">Default Alpine settings</h2>
 | 
			
		||||
          <p className="text-sm text-muted-foreground">
 | 
			
		||||
            CPU: {item.expand.alpine_script.default_cpu}
 | 
			
		||||
          </p>
 | 
			
		||||
          <p className="text-sm text-muted-foreground">
 | 
			
		||||
            RAM: {item.expand.alpine_script.default_ram}
 | 
			
		||||
          </p>
 | 
			
		||||
          <p className="text-sm text-muted-foreground">
 | 
			
		||||
            HDD: {item.expand.alpine_script.default_hdd}
 | 
			
		||||
          </p>
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,13 @@
 | 
			
		||||
import TextCopyBlock from "@/lib/TextCopyBlock";
 | 
			
		||||
import { Script } from "@/lib/types";
 | 
			
		||||
 | 
			
		||||
export default function Description({ item }: { item: Script }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="p-2">
 | 
			
		||||
      <h2 className="mb-2 max-w-prose text-lg font-semibold">Description</h2>
 | 
			
		||||
      <p className="text-sm text-muted-foreground">
 | 
			
		||||
        {TextCopyBlock(item.description)}
 | 
			
		||||
      </p>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,68 @@
 | 
			
		||||
import CodeCopyButton from "@/components/ui/code-copy-button";
 | 
			
		||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
 | 
			
		||||
import { Script } from "@/lib/types";
 | 
			
		||||
 | 
			
		||||
export default function InstallCommand({ item }: { item: Script }) {
 | 
			
		||||
  const { title, item_type, installCommand, expand } = item;
 | 
			
		||||
  const hasAlpineScript = expand?.alpine_script !== undefined;
 | 
			
		||||
 | 
			
		||||
  const renderInstructions = (isAlpine = false) => (
 | 
			
		||||
    <>
 | 
			
		||||
      <p className="text-sm mt-2">
 | 
			
		||||
        {isAlpine ? (
 | 
			
		||||
          <>
 | 
			
		||||
            As an alternative option, you can use Alpine Linux and the {title}{" "}
 | 
			
		||||
            package to create a {title} {item_type} container with faster
 | 
			
		||||
            creation time and minimal system resource usage. You are also
 | 
			
		||||
            obliged to adhere to updates provided by the package maintainer.
 | 
			
		||||
          </>
 | 
			
		||||
        ) : item_type ? (
 | 
			
		||||
          <>
 | 
			
		||||
            To create a new Proxmox VE {title} {item_type}, run the command
 | 
			
		||||
            below in the Proxmox VE Shell.
 | 
			
		||||
          </>
 | 
			
		||||
        ) : (
 | 
			
		||||
          <>To use the {title} script, run the command below in the shell.</>
 | 
			
		||||
        )}
 | 
			
		||||
      </p>
 | 
			
		||||
      {isAlpine && (
 | 
			
		||||
        <p className="mt-2 text-sm">
 | 
			
		||||
          To create a new Proxmox VE Alpine-{title} {item_type}, run the command
 | 
			
		||||
          below in the Proxmox VE Shell
 | 
			
		||||
        </p>
 | 
			
		||||
      )}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="p-4">
 | 
			
		||||
      {hasAlpineScript ? (
 | 
			
		||||
        <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>{installCommand}</CodeCopyButton>
 | 
			
		||||
          </TabsContent>
 | 
			
		||||
          <TabsContent value="alpine">
 | 
			
		||||
            {expand.alpine_script && (
 | 
			
		||||
              <>
 | 
			
		||||
                {renderInstructions(true)}
 | 
			
		||||
                <CodeCopyButton>
 | 
			
		||||
                  {expand.alpine_script.installCommand}
 | 
			
		||||
                </CodeCopyButton>
 | 
			
		||||
              </>
 | 
			
		||||
            )}
 | 
			
		||||
          </TabsContent>
 | 
			
		||||
        </Tabs>
 | 
			
		||||
      ) : (
 | 
			
		||||
        <>
 | 
			
		||||
          {renderInstructions()}
 | 
			
		||||
          {installCommand && <CodeCopyButton>{installCommand}</CodeCopyButton>}
 | 
			
		||||
        </>
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,45 @@
 | 
			
		||||
import { Button, buttonVariants } from "@/components/ui/button";
 | 
			
		||||
import handleCopy from "@/components/handleCopy";
 | 
			
		||||
import { cn } from "@/lib/utils";
 | 
			
		||||
import { ClipboardIcon } from "lucide-react";
 | 
			
		||||
 | 
			
		||||
interface Item {
 | 
			
		||||
  interface?: string;
 | 
			
		||||
  port?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const CopyButton = ({
 | 
			
		||||
  label,
 | 
			
		||||
  value,
 | 
			
		||||
}: {
 | 
			
		||||
  label: string;
 | 
			
		||||
  value: string | number;
 | 
			
		||||
}) => (
 | 
			
		||||
  <span className={cn(buttonVariants({size: "sm", variant: "secondary"}), "flex items-center gap-2")}>
 | 
			
		||||
    {value}
 | 
			
		||||
    <ClipboardIcon
 | 
			
		||||
      onClick={() => handleCopy(label, String(value))}
 | 
			
		||||
      className="size-4 cursor-pointer"
 | 
			
		||||
    />
 | 
			
		||||
  </span>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export default function InterFaces({ item }: { item: Item }) {
 | 
			
		||||
  const { interface: iface, port } = item;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="flex flex-col gap-2">
 | 
			
		||||
      {iface || (port && port !== 0) ? (
 | 
			
		||||
        <div className="flex items-center justify-end">
 | 
			
		||||
          <h2 className="mr-2 text-end text-lg font-semibold">
 | 
			
		||||
            {iface ? "Interface:" : "Default Port:"}
 | 
			
		||||
          </h2>{" "}
 | 
			
		||||
          <CopyButton
 | 
			
		||||
            label={iface ? "interface" : "port"}
 | 
			
		||||
            value={iface || port!}
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
      ) : null}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,49 @@
 | 
			
		||||
import { Badge } from "@/components/ui/badge";
 | 
			
		||||
import {
 | 
			
		||||
  Tooltip,
 | 
			
		||||
  TooltipContent,
 | 
			
		||||
  TooltipProvider,
 | 
			
		||||
  TooltipTrigger,
 | 
			
		||||
} from "@/components/ui/tooltip";
 | 
			
		||||
import { Script } from "@/lib/types";
 | 
			
		||||
import React from "react";
 | 
			
		||||
 | 
			
		||||
interface TooltipProps {
 | 
			
		||||
  variant: "warning" | "success";
 | 
			
		||||
  label: string;
 | 
			
		||||
  content: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const TooltipBadge: React.FC<TooltipProps> = ({ variant, label, content }) => (
 | 
			
		||||
  <TooltipProvider>
 | 
			
		||||
    <Tooltip delayDuration={100}>
 | 
			
		||||
      <TooltipTrigger className="flex items-center">
 | 
			
		||||
        <Badge variant={variant}>{label}</Badge>
 | 
			
		||||
      </TooltipTrigger>
 | 
			
		||||
      <TooltipContent side="bottom" className="text-sm">
 | 
			
		||||
        {content}
 | 
			
		||||
      </TooltipContent>
 | 
			
		||||
    </Tooltip>
 | 
			
		||||
  </TooltipProvider>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export default function Tooltips({ item }: { item: Script }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="flex items-center gap-2">
 | 
			
		||||
      {item.privileged && (
 | 
			
		||||
        <TooltipBadge
 | 
			
		||||
          variant="warning"
 | 
			
		||||
          label="Privileged"
 | 
			
		||||
          content="This script will be run in a privileged LXC"
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
      {item.isUpdateable && (
 | 
			
		||||
        <TooltipBadge
 | 
			
		||||
          variant="success"
 | 
			
		||||
          label="Updateable"
 | 
			
		||||
          content={`To Update ${item.title}, run the command below (or type update) in the LXC Console.`}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										34
									
								
								frontend/src/app/scripts/_components/Sidebar.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								frontend/src/app/scripts/_components/Sidebar.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,34 @@
 | 
			
		||||
"use client";
 | 
			
		||||
 | 
			
		||||
import { Category } from "@/lib/types";
 | 
			
		||||
import ScriptAccordion from "./ScriptAccordion";
 | 
			
		||||
 | 
			
		||||
const Sidebar = ({
 | 
			
		||||
  items,
 | 
			
		||||
  selectedScript,
 | 
			
		||||
  setSelectedScript,
 | 
			
		||||
}: {
 | 
			
		||||
  items: Category[];
 | 
			
		||||
  selectedScript: string | null;
 | 
			
		||||
  setSelectedScript: (script: string | null) => void;
 | 
			
		||||
}) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="flex min-w-72 flex-col sm:max-w-72">
 | 
			
		||||
      <div className="flex items-end justify-between pb-4">
 | 
			
		||||
        <h1 className="text-xl font-bold">Categories</h1>
 | 
			
		||||
        <p className="text-xs italic text-muted-foreground">
 | 
			
		||||
          {items.reduce(
 | 
			
		||||
            (acc, category) => acc + category.expand.items.length,
 | 
			
		||||
            0,
 | 
			
		||||
          )}{" "}
 | 
			
		||||
          Total scripts
 | 
			
		||||
        </p>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="rounded-lg">
 | 
			
		||||
        <ScriptAccordion items={items} selectedScript={selectedScript} setSelectedScript={setSelectedScript} />
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Sidebar;
 | 
			
		||||
							
								
								
									
										98
									
								
								frontend/src/app/scripts/page.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								frontend/src/app/scripts/page.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,98 @@
 | 
			
		||||
"use client";
 | 
			
		||||
 | 
			
		||||
export const dynamic = "force-static";
 | 
			
		||||
 | 
			
		||||
import ScriptItem from "@/app/scripts/_components/ScriptItem";
 | 
			
		||||
import { Category, Script } from "@/lib/types";
 | 
			
		||||
import { Loader2 } from "lucide-react";
 | 
			
		||||
import { Suspense, useEffect, useState } from "react";
 | 
			
		||||
import Sidebar from "./_components/Sidebar";
 | 
			
		||||
import { useQueryState } from "nuqs";
 | 
			
		||||
import {
 | 
			
		||||
  LatestScripts,
 | 
			
		||||
  MostViewedScripts,
 | 
			
		||||
} from "./_components/ScriptInfoBlocks";
 | 
			
		||||
 | 
			
		||||
function ScriptContent() {
 | 
			
		||||
  const [selectedScript, setSelectedScript] = useQueryState("id");
 | 
			
		||||
  const [links, setLinks] = useState<Category[]>([]);
 | 
			
		||||
  const [item, setItem] = useState<Script>();
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (selectedScript && links.length > 0) {
 | 
			
		||||
      const script = links
 | 
			
		||||
        .map((category) => category.expand.items)
 | 
			
		||||
        .flat()
 | 
			
		||||
        .find((script) => script.title === selectedScript);
 | 
			
		||||
      setItem(script);
 | 
			
		||||
    }
 | 
			
		||||
  }, [selectedScript, links]);
 | 
			
		||||
 | 
			
		||||
  const sortCategories = (categories: Category[]): Category[] => {
 | 
			
		||||
    return categories.sort((a: Category, b: Category) => {
 | 
			
		||||
      if (
 | 
			
		||||
        a.catagoryName === "Proxmox VE Tools" &&
 | 
			
		||||
        b.catagoryName !== "Proxmox VE Tools"
 | 
			
		||||
      ) {
 | 
			
		||||
        return -1;
 | 
			
		||||
      } else if (
 | 
			
		||||
        a.catagoryName !== "Proxmox VE Tools" &&
 | 
			
		||||
        b.catagoryName === "Proxmox VE Tools"
 | 
			
		||||
      ) {
 | 
			
		||||
        return 1;
 | 
			
		||||
      } else {
 | 
			
		||||
        return a.catagoryName.localeCompare(b.catagoryName);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    fetch("api/categories")
 | 
			
		||||
      .then((response) => response.json())
 | 
			
		||||
      .then((categories) => {
 | 
			
		||||
        const sortedCategories = sortCategories(categories);
 | 
			
		||||
        setLinks(sortedCategories);
 | 
			
		||||
      })
 | 
			
		||||
      .catch((error) => console.error(error));
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="mb-3">
 | 
			
		||||
      <div className="mt-20 flex sm:px-4 xl:px-0">
 | 
			
		||||
        <div className="hidden sm:flex">
 | 
			
		||||
          <Sidebar
 | 
			
		||||
            items={links}
 | 
			
		||||
            selectedScript={selectedScript}
 | 
			
		||||
            setSelectedScript={setSelectedScript}
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className="mx-7 w-full sm:mx-0 sm:ml-7">
 | 
			
		||||
          {selectedScript && item ? (
 | 
			
		||||
            <ScriptItem item={item} setSelectedScript={setSelectedScript} />
 | 
			
		||||
          ) : (
 | 
			
		||||
            <div className="flex w-full flex-col gap-5">
 | 
			
		||||
              <LatestScripts items={links} />
 | 
			
		||||
              <MostViewedScripts items={links} />
 | 
			
		||||
            </div>
 | 
			
		||||
          )}
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function Page() {
 | 
			
		||||
  return (
 | 
			
		||||
    <Suspense
 | 
			
		||||
      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="space-y-2 text-center">
 | 
			
		||||
            <Loader2 className="h-10 w-10 animate-spin" />
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      }
 | 
			
		||||
    >
 | 
			
		||||
      <ScriptContent />
 | 
			
		||||
    </Suspense>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										20
									
								
								frontend/src/app/sitemap.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								frontend/src/app/sitemap.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
import type { MetadataRoute } from "next";
 | 
			
		||||
 | 
			
		||||
export const dynamic = "force-static";
 | 
			
		||||
 | 
			
		||||
export default function sitemap(): MetadataRoute.Sitemap {
 | 
			
		||||
  return [
 | 
			
		||||
    {
 | 
			
		||||
      url: "https://community-scripts.github.io/Proxmox/",
 | 
			
		||||
      lastModified: new Date(),
 | 
			
		||||
      changeFrequency: "yearly",
 | 
			
		||||
      priority: 0.8,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      url: "https://community-scripts.github.io/Proxmox/scripts",
 | 
			
		||||
      lastModified: new Date(),
 | 
			
		||||
      changeFrequency: "monthly",
 | 
			
		||||
      priority: 1,
 | 
			
		||||
    },
 | 
			
		||||
  ];
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										130
									
								
								frontend/src/components/CommandMenu.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								frontend/src/components/CommandMenu.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,130 @@
 | 
			
		||||
import {
 | 
			
		||||
  CommandDialog,
 | 
			
		||||
  CommandEmpty,
 | 
			
		||||
  CommandGroup,
 | 
			
		||||
  CommandInput,
 | 
			
		||||
  CommandItem,
 | 
			
		||||
  CommandList,
 | 
			
		||||
} from "@/components/ui/command";
 | 
			
		||||
import { Category } from "@/lib/types";
 | 
			
		||||
import { cn } from "@/lib/utils";
 | 
			
		||||
import Image from "next/image";
 | 
			
		||||
import { useRouter } from "next/navigation";
 | 
			
		||||
import React, { useEffect } from "react";
 | 
			
		||||
import { Button } from "./ui/button";
 | 
			
		||||
import { DialogTitle } from "./ui/dialog";
 | 
			
		||||
 | 
			
		||||
const sortCategories = (categories: Category[]): Category[] => {
 | 
			
		||||
  return categories.sort((a: Category, b: Category) => {
 | 
			
		||||
    if (
 | 
			
		||||
      a.catagoryName === "Proxmox VE Tools" &&
 | 
			
		||||
      b.catagoryName !== "Proxmox VE Tools"
 | 
			
		||||
    ) {
 | 
			
		||||
      return -1;
 | 
			
		||||
    } else if (
 | 
			
		||||
      a.catagoryName !== "Proxmox VE Tools" &&
 | 
			
		||||
      b.catagoryName === "Proxmox VE Tools"
 | 
			
		||||
    ) {
 | 
			
		||||
      return 1;
 | 
			
		||||
    } else {
 | 
			
		||||
      return a.catagoryName.localeCompare(b.catagoryName);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default function CommandMenu() {
 | 
			
		||||
  const [open, setOpen] = React.useState(false);
 | 
			
		||||
  const [links, setLinks] = React.useState<Category[]>([]);
 | 
			
		||||
  const router = useRouter();
 | 
			
		||||
  const [isLoading, setIsLoading] = React.useState(false);
 | 
			
		||||
 | 
			
		||||
  React.useEffect(() => {
 | 
			
		||||
    const down = (e: KeyboardEvent) => {
 | 
			
		||||
      if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        setOpen((open) => !open);
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    document.addEventListener("keydown", down);
 | 
			
		||||
    return () => document.removeEventListener("keydown", down);
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const fetchCategories = async () => {
 | 
			
		||||
    setIsLoading(true);
 | 
			
		||||
    fetch("api/categories")
 | 
			
		||||
      .then((response) => response.json())
 | 
			
		||||
      .then((categories) => {
 | 
			
		||||
        const sortedCategories = sortCategories(categories);
 | 
			
		||||
        setLinks(sortedCategories);
 | 
			
		||||
        setIsLoading(false);
 | 
			
		||||
      })
 | 
			
		||||
      .catch((error) => {
 | 
			
		||||
        setIsLoading(false);
 | 
			
		||||
        console.error(error)
 | 
			
		||||
      });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Button
 | 
			
		||||
        variant="outline"
 | 
			
		||||
        className={cn(
 | 
			
		||||
          "relative h-9 w-full justify-start rounded-[0.5rem] bg-muted/50 text-sm font-normal text-muted-foreground shadow-none sm:pr-12 md:w-40 lg:w-64",
 | 
			
		||||
        )}
 | 
			
		||||
        onClick={() => {
 | 
			
		||||
          fetchCategories();
 | 
			
		||||
          setOpen(true)
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <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">
 | 
			
		||||
          <span className="text-xs">⌘</span>K
 | 
			
		||||
        </kbd>
 | 
			
		||||
      </Button>
 | 
			
		||||
      <CommandDialog open={open} onOpenChange={setOpen}>
 | 
			
		||||
          <DialogTitle className="sr-only">Search scripts</DialogTitle>
 | 
			
		||||
        <CommandInput placeholder="search for a script..." />
 | 
			
		||||
        <CommandList>
 | 
			
		||||
          <CommandEmpty>{isLoading ? "Loading..." : "No scripts found."}</CommandEmpty>
 | 
			
		||||
          {links.map((category) => (
 | 
			
		||||
            <CommandGroup
 | 
			
		||||
              key={"category:" + category.catagoryName}
 | 
			
		||||
              heading={category.catagoryName}
 | 
			
		||||
            >
 | 
			
		||||
              {category.expand.items.map((script) => (
 | 
			
		||||
                <CommandItem
 | 
			
		||||
                  key={"script:" + script.id}
 | 
			
		||||
                  value={script.title}
 | 
			
		||||
                  onSelect={() => {
 | 
			
		||||
                    setOpen(false);
 | 
			
		||||
                    router.push(`/scripts?id=${script.title}`);
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <div className="flex gap-2" onClick={() => setOpen(false)}>
 | 
			
		||||
                    <Image
 | 
			
		||||
                      src={script.logo}
 | 
			
		||||
                      unoptimized
 | 
			
		||||
                      height={16}
 | 
			
		||||
                      onError={(e) =>
 | 
			
		||||
                        ((e.currentTarget as HTMLImageElement).src =
 | 
			
		||||
                          "/logo.png")
 | 
			
		||||
                      }
 | 
			
		||||
                      width={16}
 | 
			
		||||
                      alt=""
 | 
			
		||||
                      className="h-5 w-5"
 | 
			
		||||
                    />
 | 
			
		||||
                    <span>{script.title}</span>
 | 
			
		||||
                    <span className="text-sm text-muted-foreground">
 | 
			
		||||
                      {script.item_type}
 | 
			
		||||
                    </span>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </CommandItem>
 | 
			
		||||
              ))}
 | 
			
		||||
            </CommandGroup>
 | 
			
		||||
          ))}
 | 
			
		||||
        </CommandList>
 | 
			
		||||
      </CommandDialog>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										23
									
								
								frontend/src/components/Footer.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								frontend/src/components/Footer.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
import Link from "next/link";
 | 
			
		||||
 | 
			
		||||
export default function Footer() {
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="supports-backdrop-blur:bg-background/90 mt-auto flex border-t border-border bg-background/40 py-6 backdrop-blur-lg">
 | 
			
		||||
      <div className="flex w-full justify-between">
 | 
			
		||||
        <div className="mx-6 w-full max-w-7xl text-sm text-muted-foreground">
 | 
			
		||||
          Website build by the community. The source code is avaliable on{" "}
 | 
			
		||||
          <Link
 | 
			
		||||
            href="https://github.com/community-scripts/Proxmox"
 | 
			
		||||
            target="_blank"
 | 
			
		||||
            rel="noreferrer"
 | 
			
		||||
            className="font-semibold underline-offset-2 duration-300 hover:underline"
 | 
			
		||||
            data-umami-event="View Website Source Code on Github"
 | 
			
		||||
          >
 | 
			
		||||
            GitHub
 | 
			
		||||
          </Link>
 | 
			
		||||
          .
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										113
									
								
								frontend/src/components/Navbar.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								frontend/src/components/Navbar.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,113 @@
 | 
			
		||||
"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 { cn } from "@/lib/utils";
 | 
			
		||||
import { MoonIcon, SunIcon } from "lucide-react";
 | 
			
		||||
import { useTheme } from "next-themes";
 | 
			
		||||
import CommandMenu from "./CommandMenu";
 | 
			
		||||
import StarOnGithubButton from "./ui/star-on-github-button";
 | 
			
		||||
import {
 | 
			
		||||
  Tooltip,
 | 
			
		||||
  TooltipContent,
 | 
			
		||||
  TooltipProvider,
 | 
			
		||||
  TooltipTrigger,
 | 
			
		||||
} from "./ui/tooltip";
 | 
			
		||||
 | 
			
		||||
export const dynamic = "force-dynamic";
 | 
			
		||||
 | 
			
		||||
function Navbar() {
 | 
			
		||||
  const [isScrolled, setIsScrolled] = useState(false);
 | 
			
		||||
  const { theme, setTheme } = useTheme();
 | 
			
		||||
 | 
			
		||||
  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-7xl flex-row-reverse items-center justify-between sm:flex-row">
 | 
			
		||||
          <Link
 | 
			
		||||
            href={"/"}
 | 
			
		||||
            className="flex cursor-pointer flex-row-reverse items-center gap-2 font-semibold sm:flex-row"
 | 
			
		||||
          >
 | 
			
		||||
            <Image
 | 
			
		||||
              height={18}
 | 
			
		||||
              unoptimized
 | 
			
		||||
              width={18}
 | 
			
		||||
              alt="logo"
 | 
			
		||||
              src="logo.png"
 | 
			
		||||
            />
 | 
			
		||||
            <span className="hidden lg:block">Proxmox VE Helper-Scripts</span>
 | 
			
		||||
          </Link>
 | 
			
		||||
          {/* <MobileNav /> */}
 | 
			
		||||
          <div className="flex gap-2">
 | 
			
		||||
            <CommandMenu />
 | 
			
		||||
            <StarOnGithubButton />
 | 
			
		||||
            {navbarLinks.map(({ href, event, icon, text }) => (
 | 
			
		||||
              <TooltipProvider key={event}>
 | 
			
		||||
                <Tooltip delayDuration={100}>
 | 
			
		||||
                  <TooltipTrigger>
 | 
			
		||||
                    <Button variant="ghost" size={"icon"} asChild>
 | 
			
		||||
                      <Link
 | 
			
		||||
                        target="_blank"
 | 
			
		||||
                        href={href}
 | 
			
		||||
                        data-umami-event={event}
 | 
			
		||||
                      >
 | 
			
		||||
                        {icon}
 | 
			
		||||
                        <span className="sr-only">{text}</span>
 | 
			
		||||
                      </Link>
 | 
			
		||||
                    </Button>
 | 
			
		||||
                  </TooltipTrigger>
 | 
			
		||||
                  <TooltipContent side="bottom" className="text-xs">
 | 
			
		||||
                    {text}
 | 
			
		||||
                  </TooltipContent>
 | 
			
		||||
                </Tooltip>
 | 
			
		||||
              </TooltipProvider>
 | 
			
		||||
            ))}
 | 
			
		||||
            <TooltipProvider>
 | 
			
		||||
              <Tooltip delayDuration={100}>
 | 
			
		||||
                <TooltipTrigger asChild>
 | 
			
		||||
                  <Button
 | 
			
		||||
                    variant="ghost"
 | 
			
		||||
                    type="button"
 | 
			
		||||
                    size="icon"
 | 
			
		||||
                    className={cn("px-2")}
 | 
			
		||||
                    aria-label="Toggle theme"
 | 
			
		||||
                    onClick={() =>
 | 
			
		||||
                      setTheme(theme === "dark" ? "light" : "dark")
 | 
			
		||||
                    }
 | 
			
		||||
                  >
 | 
			
		||||
                    <SunIcon className="size-[1.2rem] text-neutral-800 dark:hidden dark:text-neutral-200" />
 | 
			
		||||
                    <MoonIcon className="hidden size-[1.2rem] text-neutral-800 dark:block dark:text-neutral-200" />
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </TooltipTrigger>
 | 
			
		||||
                <TooltipContent side="bottom" className="text-xs">
 | 
			
		||||
                  Theme Toggle
 | 
			
		||||
                </TooltipContent>
 | 
			
		||||
              </Tooltip>
 | 
			
		||||
            </TooltipProvider>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Navbar;
 | 
			
		||||
							
								
								
									
										28
									
								
								frontend/src/components/TextCopyBlock.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								frontend/src/components/TextCopyBlock.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
			
		||||
import { ClipboardIcon } from "lucide-react";
 | 
			
		||||
import handleCopy from "./handleCopy";
 | 
			
		||||
 | 
			
		||||
export default function TextCopyBlock(description: string) {
 | 
			
		||||
  const pattern = /`([^`]*)`/g;
 | 
			
		||||
  const parts = description.split(pattern);
 | 
			
		||||
 | 
			
		||||
  const formattedDescription = parts.map((part: string, index: number) => {
 | 
			
		||||
    if (index % 2 === 1) {
 | 
			
		||||
      return (
 | 
			
		||||
        <span
 | 
			
		||||
          key={index}
 | 
			
		||||
          className="bg-secondary py-1 px-2 rounded-lg inline-flex items-center gap-2"
 | 
			
		||||
        >
 | 
			
		||||
          {part}
 | 
			
		||||
          <ClipboardIcon
 | 
			
		||||
            className="size-3 cursor-pointer"
 | 
			
		||||
            onClick={() => handleCopy("command", part)}
 | 
			
		||||
          />
 | 
			
		||||
        </span>
 | 
			
		||||
      );
 | 
			
		||||
    } else {
 | 
			
		||||
      return part;
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return formattedDescription;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10
									
								
								frontend/src/components/handleCopy.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								frontend/src/components/handleCopy.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
import { ClipboardCheck } from "lucide-react";
 | 
			
		||||
import { toast } from "sonner";
 | 
			
		||||
 | 
			
		||||
export default function handleCopy(type: string, value: string) {
 | 
			
		||||
  navigator.clipboard.writeText(value);
 | 
			
		||||
 | 
			
		||||
  toast.success(`copied ${type} to clipboard`, {
 | 
			
		||||
    icon: <ClipboardCheck className="h-4 w-4" />,
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										8
									
								
								frontend/src/components/theme-provider.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								frontend/src/components/theme-provider.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
"use client";
 | 
			
		||||
 | 
			
		||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
 | 
			
		||||
import { type ThemeProviderProps } from "next-themes/dist/types";
 | 
			
		||||
 | 
			
		||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
 | 
			
		||||
  return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										57
									
								
								frontend/src/components/ui/accordion.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								frontend/src/components/ui/accordion.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,57 @@
 | 
			
		||||
"use client";
 | 
			
		||||
import * as AccordionPrimitive from "@radix-ui/react-accordion";
 | 
			
		||||
import { ChevronDown } from "lucide-react";
 | 
			
		||||
import * as React from "react";
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils";
 | 
			
		||||
 | 
			
		||||
const Accordion = AccordionPrimitive.Root;
 | 
			
		||||
 | 
			
		||||
const AccordionItem = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof AccordionPrimitive.Item>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <AccordionPrimitive.Item
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn("border-b", className)}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
));
 | 
			
		||||
AccordionItem.displayName = "AccordionItem";
 | 
			
		||||
 | 
			
		||||
const AccordionTrigger = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof AccordionPrimitive.Trigger>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
 | 
			
		||||
>(({ className, children, ...props }, ref) => (
 | 
			
		||||
  <AccordionPrimitive.Header className="flex">
 | 
			
		||||
    <AccordionPrimitive.Trigger
 | 
			
		||||
      ref={ref}
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "flex flex-1 items-center justify-between py-1 pr-2 font-medium transition-all [&[data-state=open]>svg]:rotate-180",
 | 
			
		||||
        className,
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    >
 | 
			
		||||
      {children}
 | 
			
		||||
      <ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
 | 
			
		||||
    </AccordionPrimitive.Trigger>
 | 
			
		||||
  </AccordionPrimitive.Header>
 | 
			
		||||
));
 | 
			
		||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
 | 
			
		||||
 | 
			
		||||
const AccordionContent = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof AccordionPrimitive.Content>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
 | 
			
		||||
>(({ className, children, ...props }, ref) => (
 | 
			
		||||
  <AccordionPrimitive.Content
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className="overflow-hidden py-1 text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
 | 
			
		||||
    {...props}
 | 
			
		||||
  >
 | 
			
		||||
    <div className={cn("pt-0", className)}>{children}</div>
 | 
			
		||||
  </AccordionPrimitive.Content>
 | 
			
		||||
));
 | 
			
		||||
 | 
			
		||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
 | 
			
		||||
 | 
			
		||||
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };
 | 
			
		||||
							
								
								
									
										26
									
								
								frontend/src/components/ui/animated-gradient-text.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								frontend/src/components/ui/animated-gradient-text.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
			
		||||
import { ReactNode } from "react";
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils";
 | 
			
		||||
 | 
			
		||||
export default function AnimatedGradientText({
 | 
			
		||||
  children,
 | 
			
		||||
  className,
 | 
			
		||||
}: {
 | 
			
		||||
  children: ReactNode;
 | 
			
		||||
  className?: string;
 | 
			
		||||
}) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "group relative mx-auto flex max-w-fit flex-row items-center justify-center rounded-2xl bg-white/40 px-4 py-1.5 text-sm font-medium shadow-[inset_0_-8px_10px_#8fdfff1f] backdrop-blur-sm transition-shadow duration-500 ease-out [--bg-size:300%] hover:shadow-[inset_0_-5px_10px_#8fdfff3f] dark:bg-black/40",
 | 
			
		||||
        className,
 | 
			
		||||
      )}
 | 
			
		||||
    >
 | 
			
		||||
      <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)]`}
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      {children}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										39
									
								
								frontend/src/components/ui/badge.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								frontend/src/components/ui/badge.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
			
		||||
import { cva, type VariantProps } from "class-variance-authority";
 | 
			
		||||
import * as React from "react";
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils";
 | 
			
		||||
 | 
			
		||||
const badgeVariants = cva(
 | 
			
		||||
  "inline-flex items-center rounded-full border px-1.5 py-0.1 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
 | 
			
		||||
  {
 | 
			
		||||
    variants: {
 | 
			
		||||
      variant: {
 | 
			
		||||
        default:
 | 
			
		||||
          "border-transparent  text-primary-foreground border-primary-foreground",
 | 
			
		||||
        secondary:
 | 
			
		||||
          "border-transparent  text-secondary-foreground border-secondary-foreground",
 | 
			
		||||
        destructive:
 | 
			
		||||
          "border-transparent  text-destructive-foreground border-destructive-foreground",
 | 
			
		||||
        outline: "text-foreground",
 | 
			
		||||
        success: "text-green-500 border-green-500",
 | 
			
		||||
        warning: "text-yellow-500 border-yellow-500",
 | 
			
		||||
        failure: "text-red-500 border-red-500",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    defaultVariants: {
 | 
			
		||||
      variant: "default",
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export interface BadgeProps
 | 
			
		||||
  extends React.HTMLAttributes<HTMLDivElement>,
 | 
			
		||||
    VariantProps<typeof badgeVariants> {}
 | 
			
		||||
 | 
			
		||||
function Badge({ className, variant, ...props }: BadgeProps) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div className={cn(badgeVariants({ variant }), className)} {...props} />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { Badge, badgeVariants };
 | 
			
		||||
							
								
								
									
										108
									
								
								frontend/src/components/ui/button.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								frontend/src/components/ui/button.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,108 @@
 | 
			
		||||
import { cn } from "@/lib/utils";
 | 
			
		||||
import { Slot, Slottable } from "@radix-ui/react-slot";
 | 
			
		||||
import { cva, type VariantProps } from "class-variance-authority";
 | 
			
		||||
import * as React from "react";
 | 
			
		||||
 | 
			
		||||
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",
 | 
			
		||||
  {
 | 
			
		||||
    variants: {
 | 
			
		||||
      variant: {
 | 
			
		||||
        default: "bg-primary text-primary-foreground hover:bg-primary/90",
 | 
			
		||||
        destructive:
 | 
			
		||||
          "bg-destructive text-destructive-foreground hover:bg-destructive/90",
 | 
			
		||||
        outline:
 | 
			
		||||
          "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
 | 
			
		||||
        secondary:
 | 
			
		||||
          "bg-secondary text-secondary-foreground hover:bg-secondary/80",
 | 
			
		||||
        ghost: "hover:bg-accent hover:text-accent-foreground",
 | 
			
		||||
        link: "text-primary underline-offset-4 hover:underline",
 | 
			
		||||
        expandIcon:
 | 
			
		||||
          "group relative text-primary-foreground bg-primary hover:bg-primary/90",
 | 
			
		||||
        ringHover:
 | 
			
		||||
          "bg-primary text-primary-foreground transition-all duration-300 hover:bg-primary/90 hover:ring-2 hover:ring-primary/90 hover:ring-offset-2",
 | 
			
		||||
        shine:
 | 
			
		||||
          "text-primary-foreground animate-shine bg-gradient-to-r from-primary via-primary/75 to-primary bg-[length:400%_100%] ",
 | 
			
		||||
        gooeyRight:
 | 
			
		||||
          "text-primary-foreground relative bg-primary z-0 overflow-hidden transition-all duration-500 before:absolute before:inset-0 before:-z-10 before:translate-x-[150%] before:translate-y-[150%] before:scale-[2.5] before:rounded-[100%] before:bg-gradient-to-r from-zinc-400 before:transition-transform before:duration-1000  hover:before:translate-x-[0%] hover:before:translate-y-[0%] ",
 | 
			
		||||
        gooeyLeft:
 | 
			
		||||
          "text-primary-foreground relative bg-primary z-0 overflow-hidden transition-all duration-500 after:absolute after:inset-0 after:-z-10 after:translate-x-[-150%] after:translate-y-[150%] after:scale-[2.5] after:rounded-[100%] after:bg-gradient-to-l from-zinc-400 after:transition-transform after:duration-1000  hover:after:translate-x-[0%] hover:after:translate-y-[0%] ",
 | 
			
		||||
        linkHover1:
 | 
			
		||||
          "relative after:absolute after:bg-primary after:bottom-2 after:h-[1px] after:w-2/3 after:origin-bottom-left after:scale-x-100 hover:after:origin-bottom-right hover:after:scale-x-0 after:transition-transform after:ease-in-out after:duration-300",
 | 
			
		||||
        linkHover2:
 | 
			
		||||
          "relative after:absolute after:bg-primary after:bottom-2 after:h-[1px] after:w-2/3 after:origin-bottom-right after:scale-x-0 hover:after:origin-bottom-left hover:after:scale-x-100 after:transition-transform after:ease-in-out after:duration-300",
 | 
			
		||||
      },
 | 
			
		||||
      size: {
 | 
			
		||||
        default: "h-10 px-4 py-2",
 | 
			
		||||
        sm: "h-9 rounded-md px-3",
 | 
			
		||||
        lg: "h-11 rounded-md px-8",
 | 
			
		||||
        icon: "h-9 w-9    ",
 | 
			
		||||
        null: "py-1 px-3 rouded-xs",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    defaultVariants: {
 | 
			
		||||
      variant: "default",
 | 
			
		||||
      size: "default",
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
interface IconProps {
 | 
			
		||||
  Icon: React.ElementType;
 | 
			
		||||
  iconPlacement: "left" | "right";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface IconRefProps {
 | 
			
		||||
  Icon?: never;
 | 
			
		||||
  iconPlacement?: undefined;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ButtonProps
 | 
			
		||||
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
 | 
			
		||||
    VariantProps<typeof buttonVariants> {
 | 
			
		||||
  asChild?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type ButtonIconProps = IconProps | IconRefProps;
 | 
			
		||||
 | 
			
		||||
const Button = React.forwardRef<
 | 
			
		||||
  HTMLButtonElement,
 | 
			
		||||
  ButtonProps & ButtonIconProps
 | 
			
		||||
>(
 | 
			
		||||
  (
 | 
			
		||||
    {
 | 
			
		||||
      className,
 | 
			
		||||
      variant,
 | 
			
		||||
      size,
 | 
			
		||||
      asChild = false,
 | 
			
		||||
      Icon,
 | 
			
		||||
      iconPlacement,
 | 
			
		||||
      ...props
 | 
			
		||||
    },
 | 
			
		||||
    ref,
 | 
			
		||||
  ) => {
 | 
			
		||||
    const Comp = asChild ? Slot : "button";
 | 
			
		||||
    return (
 | 
			
		||||
      <Comp
 | 
			
		||||
        className={cn(buttonVariants({ variant, size, className }))}
 | 
			
		||||
        ref={ref}
 | 
			
		||||
        {...props}
 | 
			
		||||
      >
 | 
			
		||||
        {Icon && iconPlacement === "left" && (
 | 
			
		||||
          <div className="group-hover:translate-x-100 w-0 translate-x-[0%] pr-0 opacity-0 transition-all duration-200 group-hover:w-5 group-hover:pr-2 group-hover:opacity-100">
 | 
			
		||||
            <Icon />
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
        <Slottable>{props.children}</Slottable>
 | 
			
		||||
        {Icon && iconPlacement === "right" && (
 | 
			
		||||
          <div className="w-0 translate-x-[100%] pl-0 opacity-0 transition-all duration-200 group-hover:w-5 group-hover:translate-x-0 group-hover:pl-2 group-hover:opacity-100">
 | 
			
		||||
            <Icon />
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
      </Comp>
 | 
			
		||||
    );
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
Button.displayName = "Button";
 | 
			
		||||
 | 
			
		||||
export { Button, buttonVariants };
 | 
			
		||||
							
								
								
									
										89
									
								
								frontend/src/components/ui/card.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								frontend/src/components/ui/card.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,89 @@
 | 
			
		||||
import * as React from "react";
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils";
 | 
			
		||||
 | 
			
		||||
const Card = React.forwardRef<
 | 
			
		||||
  HTMLDivElement,
 | 
			
		||||
  React.HTMLAttributes<HTMLDivElement>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <div
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "rounded-lg border text-card-foreground shadow-sm",
 | 
			
		||||
      className,
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
));
 | 
			
		||||
Card.displayName = "Card";
 | 
			
		||||
 | 
			
		||||
const CardHeader = React.forwardRef<
 | 
			
		||||
  HTMLDivElement,
 | 
			
		||||
  React.HTMLAttributes<HTMLDivElement>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <div
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn("flex flex-col space-y-1.5 p-4", className)}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
));
 | 
			
		||||
CardHeader.displayName = "CardHeader";
 | 
			
		||||
 | 
			
		||||
const CardTitle = React.forwardRef<
 | 
			
		||||
  HTMLParagraphElement,
 | 
			
		||||
  React.HTMLAttributes<HTMLHeadingElement>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <h3
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "text-2xl font-semibold leading-none tracking-tight",
 | 
			
		||||
      className,
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
));
 | 
			
		||||
CardTitle.displayName = "CardTitle";
 | 
			
		||||
 | 
			
		||||
const CardDescription = React.forwardRef<
 | 
			
		||||
  HTMLParagraphElement,
 | 
			
		||||
  React.HTMLAttributes<HTMLParagraphElement>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <p
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "min-h-[40px] text-sm text-muted-foreground sm:min-h-[60px]",
 | 
			
		||||
      className,
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
));
 | 
			
		||||
CardDescription.displayName = "CardDescription";
 | 
			
		||||
 | 
			
		||||
const CardContent = React.forwardRef<
 | 
			
		||||
  HTMLDivElement,
 | 
			
		||||
  React.HTMLAttributes<HTMLDivElement>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <div ref={ref} className={cn("p-4 pt-0", className)} {...props} />
 | 
			
		||||
));
 | 
			
		||||
CardContent.displayName = "CardContent";
 | 
			
		||||
 | 
			
		||||
const CardFooter = React.forwardRef<
 | 
			
		||||
  HTMLDivElement,
 | 
			
		||||
  React.HTMLAttributes<HTMLDivElement>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <div
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn("mt-auto items-center p-4 pt-0", className)}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
));
 | 
			
		||||
CardFooter.displayName = "CardFooter";
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  Card,
 | 
			
		||||
  CardContent,
 | 
			
		||||
  CardDescription,
 | 
			
		||||
  CardFooter,
 | 
			
		||||
  CardHeader,
 | 
			
		||||
  CardTitle,
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										66
									
								
								frontend/src/components/ui/code-copy-button.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								frontend/src/components/ui/code-copy-button.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,66 @@
 | 
			
		||||
"use client";
 | 
			
		||||
import { cn } from "@/lib/utils";
 | 
			
		||||
import { CheckIcon, ClipboardIcon } from "lucide-react";
 | 
			
		||||
import { useEffect, useState } from "react";
 | 
			
		||||
import { toast } from "sonner";
 | 
			
		||||
import { Card } from "./card";
 | 
			
		||||
 | 
			
		||||
export default function CodeCopyButton({
 | 
			
		||||
  children,
 | 
			
		||||
}: {
 | 
			
		||||
  children: React.ReactNode;
 | 
			
		||||
}) {
 | 
			
		||||
  const [hasCopied, setHasCopied] = useState(false);
 | 
			
		||||
  const isMobile = window.innerWidth <= 640;
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (hasCopied) {
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        setHasCopied(false);
 | 
			
		||||
      }, 2000);
 | 
			
		||||
    }
 | 
			
		||||
  }, [hasCopied]);
 | 
			
		||||
 | 
			
		||||
  const handleCopy = (type: string, value: any) => {
 | 
			
		||||
    navigator.clipboard.writeText(value);
 | 
			
		||||
 | 
			
		||||
    setHasCopied(true);
 | 
			
		||||
 | 
			
		||||
    let warning = localStorage.getItem("warning");
 | 
			
		||||
 | 
			
		||||
    if (warning === null) {
 | 
			
		||||
      localStorage.setItem("warning", "1");
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        toast.error(
 | 
			
		||||
          "Be careful when copying scripts from the internet. Always remember check the source!",
 | 
			
		||||
          { duration: 8000 },
 | 
			
		||||
        );
 | 
			
		||||
      }, 500);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // toast.success(`copied ${type} to clipboard`, {
 | 
			
		||||
    //   icon: <ClipboardCheck className="h-4 w-4" />,
 | 
			
		||||
    // });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="mt-4 flex">
 | 
			
		||||
      <Card className="flex items-center overflow-x-auto bg-primary-foreground pl-4">
 | 
			
		||||
        <div className="overflow-x-auto whitespace-pre-wrap text-nowrap break-all pr-4 text-sm">
 | 
			
		||||
          {!isMobile && children ? children : "Copy install command"}
 | 
			
		||||
        </div>
 | 
			
		||||
        <div
 | 
			
		||||
          className={cn(" right-0 cursor-pointer bg-muted px-3 py-4")}
 | 
			
		||||
          onClick={() => handleCopy("install command", children)}
 | 
			
		||||
        >
 | 
			
		||||
          {hasCopied ? (
 | 
			
		||||
            <CheckIcon className="h-4 w-4" />
 | 
			
		||||
          ) : (
 | 
			
		||||
            <ClipboardIcon className="h-4 w-4" />
 | 
			
		||||
          )}
 | 
			
		||||
          <span className="sr-only">Copy</span>
 | 
			
		||||
        </div>
 | 
			
		||||
      </Card>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										137
									
								
								frontend/src/components/ui/codeblock.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								frontend/src/components/ui/codeblock.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,137 @@
 | 
			
		||||
"use client";
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils";
 | 
			
		||||
import { cva, type VariantProps } from "class-variance-authority";
 | 
			
		||||
import { Clipboard, Copy } from "lucide-react";
 | 
			
		||||
import Link from "next/link";
 | 
			
		||||
import * as React from "react";
 | 
			
		||||
import { toast } from "sonner";
 | 
			
		||||
import { Button } from "./button";
 | 
			
		||||
import { Separator } from "./separator";
 | 
			
		||||
 | 
			
		||||
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",
 | 
			
		||||
  {
 | 
			
		||||
    variants: {
 | 
			
		||||
      variant: {
 | 
			
		||||
        default: "bg-primary text-primary-foreground hover:bg-primary/90",
 | 
			
		||||
        destructive:
 | 
			
		||||
          "bg-destructive text-destructive-foreground hover:bg-destructive/90",
 | 
			
		||||
        outline:
 | 
			
		||||
          "border border-input bg-background hover:bg-accent hover:border-primary hover:text-accent-foreground",
 | 
			
		||||
        secondary:
 | 
			
		||||
          "bg-secondary border-secondary text-secondary-foreground hover:bg-secondary/80 hover:border-primary",
 | 
			
		||||
        ghost: "hover:bg-accent hover:text-accent-foreground",
 | 
			
		||||
        link: "text-primary underline-offset-4 hover:underline",
 | 
			
		||||
      },
 | 
			
		||||
      size: {
 | 
			
		||||
        default: "h-10 px-4 py-2",
 | 
			
		||||
        sm: "h-9 rounded-md px-3",
 | 
			
		||||
        lg: "h-11 rounded-md px-8",
 | 
			
		||||
        icon: "h-10 w-10",
 | 
			
		||||
        null: "py-1 px-3 rouded-xs",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    defaultVariants: {
 | 
			
		||||
      variant: "default",
 | 
			
		||||
      size: "default",
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const handleCopy = (type: string, value: string) => {
 | 
			
		||||
  navigator.clipboard.writeText(value);
 | 
			
		||||
 | 
			
		||||
  let amountOfScriptsCopied = localStorage.getItem("amountOfScriptsCopied");
 | 
			
		||||
 | 
			
		||||
  if (amountOfScriptsCopied === null) {
 | 
			
		||||
    localStorage.setItem("amountOfScriptsCopied", "1");
 | 
			
		||||
  } else {
 | 
			
		||||
    amountOfScriptsCopied = (parseInt(amountOfScriptsCopied) + 1).toString();
 | 
			
		||||
    localStorage.setItem("amountOfScriptsCopied", amountOfScriptsCopied);
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
      parseInt(amountOfScriptsCopied) === 3 ||
 | 
			
		||||
      parseInt(amountOfScriptsCopied) === 10 ||
 | 
			
		||||
      parseInt(amountOfScriptsCopied) === 25 ||
 | 
			
		||||
      parseInt(amountOfScriptsCopied) === 50 ||
 | 
			
		||||
      parseInt(amountOfScriptsCopied) === 100
 | 
			
		||||
    ) {
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        toast.info(
 | 
			
		||||
          <div className="flex flex-col gap-3">
 | 
			
		||||
            <p className="lg">
 | 
			
		||||
              If you find these scripts useful, please consider starring the
 | 
			
		||||
              repository on GitHub. It helps a lot!
 | 
			
		||||
            </p>
 | 
			
		||||
            <div>
 | 
			
		||||
              <Button className="text-white">
 | 
			
		||||
                <Link
 | 
			
		||||
                  href="https://github.com/community-scripts/ProxmoxVE"
 | 
			
		||||
                  data-umami-event="Star on Github"
 | 
			
		||||
                  target="_blank"
 | 
			
		||||
                >
 | 
			
		||||
                  Star on GitHub 💫
 | 
			
		||||
                </Link>
 | 
			
		||||
              </Button>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>,
 | 
			
		||||
          { duration: 8000 },
 | 
			
		||||
        );
 | 
			
		||||
      }, 500);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  toast.success(
 | 
			
		||||
    <div className="flex items-center gap-2">
 | 
			
		||||
      <Clipboard className="h-4 w-4" />
 | 
			
		||||
      <span>Copied {type} to clipboard</span>
 | 
			
		||||
    </div>,
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export interface CodeBlockProps
 | 
			
		||||
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
 | 
			
		||||
    VariantProps<typeof buttonVariants> {
 | 
			
		||||
  asChild?: boolean;
 | 
			
		||||
  code: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const CodeBlock = React.forwardRef<HTMLDivElement, CodeBlockProps>(
 | 
			
		||||
  ({ className, variant, size, asChild = false, code }, ref) => {
 | 
			
		||||
    const copyToClipboard = () => {
 | 
			
		||||
      navigator.clipboard.writeText(code);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <div
 | 
			
		||||
        style={{
 | 
			
		||||
          position: "relative",
 | 
			
		||||
          marginBottom: "1rem",
 | 
			
		||||
          display: "flex",
 | 
			
		||||
          gap: "8px",
 | 
			
		||||
        }}
 | 
			
		||||
        ref={ref}
 | 
			
		||||
      >
 | 
			
		||||
        <pre
 | 
			
		||||
          className={cn(
 | 
			
		||||
            buttonVariants({ variant, size, className }),
 | 
			
		||||
            " flex flex-row p-4",
 | 
			
		||||
          )}
 | 
			
		||||
        >
 | 
			
		||||
          <p className="flex items-center gap-2">
 | 
			
		||||
            {code} <Separator orientation="vertical" />{" "}
 | 
			
		||||
            <Copy
 | 
			
		||||
              className="cursor-pointer"
 | 
			
		||||
              size={16}
 | 
			
		||||
              onClick={() => handleCopy("install command", code)}
 | 
			
		||||
            />
 | 
			
		||||
          </p>
 | 
			
		||||
        </pre>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
CodeBlock.displayName = "CodeBlock";
 | 
			
		||||
 | 
			
		||||
export { CodeBlock, buttonVariants };
 | 
			
		||||
							
								
								
									
										155
									
								
								frontend/src/components/ui/command.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								frontend/src/components/ui/command.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,155 @@
 | 
			
		||||
"use client";
 | 
			
		||||
 | 
			
		||||
import { type DialogProps } from "@radix-ui/react-dialog";
 | 
			
		||||
import { Command as CommandPrimitive } from "cmdk";
 | 
			
		||||
import { Search } from "lucide-react";
 | 
			
		||||
import * as React from "react";
 | 
			
		||||
 | 
			
		||||
import { Dialog, DialogContent } from "@/components/ui/dialog";
 | 
			
		||||
import { cn } from "@/lib/utils";
 | 
			
		||||
 | 
			
		||||
const Command = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof CommandPrimitive>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof CommandPrimitive>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <CommandPrimitive
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
 | 
			
		||||
      className,
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
));
 | 
			
		||||
Command.displayName = CommandPrimitive.displayName;
 | 
			
		||||
 | 
			
		||||
interface CommandDialogProps extends DialogProps {}
 | 
			
		||||
 | 
			
		||||
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <Dialog {...props}>
 | 
			
		||||
      <DialogContent className="overflow-hidden p-0 shadow-lg">
 | 
			
		||||
        <Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
 | 
			
		||||
          {children}
 | 
			
		||||
        </Command>
 | 
			
		||||
      </DialogContent>
 | 
			
		||||
    </Dialog>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const CommandInput = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof CommandPrimitive.Input>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <div className="flex items-center border-b px-3" cmdk-input-wrapper="">
 | 
			
		||||
    <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
 | 
			
		||||
    <CommandPrimitive.Input
 | 
			
		||||
      ref={ref}
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
 | 
			
		||||
        className,
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  </div>
 | 
			
		||||
));
 | 
			
		||||
 | 
			
		||||
CommandInput.displayName = CommandPrimitive.Input.displayName;
 | 
			
		||||
 | 
			
		||||
const CommandList = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof CommandPrimitive.List>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <CommandPrimitive.List
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
));
 | 
			
		||||
 | 
			
		||||
CommandList.displayName = CommandPrimitive.List.displayName;
 | 
			
		||||
 | 
			
		||||
const CommandEmpty = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof CommandPrimitive.Empty>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
 | 
			
		||||
>((props, ref) => (
 | 
			
		||||
  <CommandPrimitive.Empty
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className="py-6 text-center text-sm"
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
));
 | 
			
		||||
 | 
			
		||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
 | 
			
		||||
 | 
			
		||||
const CommandGroup = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof CommandPrimitive.Group>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <CommandPrimitive.Group
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
 | 
			
		||||
      className,
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
));
 | 
			
		||||
 | 
			
		||||
CommandGroup.displayName = CommandPrimitive.Group.displayName;
 | 
			
		||||
 | 
			
		||||
const CommandSeparator = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof CommandPrimitive.Separator>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <CommandPrimitive.Separator
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn("-mx-1 h-px bg-border", className)}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
));
 | 
			
		||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
 | 
			
		||||
 | 
			
		||||
const CommandItem = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof CommandPrimitive.Item>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <CommandPrimitive.Item
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50",
 | 
			
		||||
      className,
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
));
 | 
			
		||||
 | 
			
		||||
CommandItem.displayName = CommandPrimitive.Item.displayName;
 | 
			
		||||
 | 
			
		||||
const CommandShortcut = ({
 | 
			
		||||
  className,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <span
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "ml-auto text-xs tracking-widest text-muted-foreground",
 | 
			
		||||
        className,
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
CommandShortcut.displayName = "CommandShortcut";
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  Command,
 | 
			
		||||
  CommandDialog,
 | 
			
		||||
  CommandEmpty,
 | 
			
		||||
  CommandGroup,
 | 
			
		||||
  CommandInput,
 | 
			
		||||
  CommandItem,
 | 
			
		||||
  CommandList,
 | 
			
		||||
  CommandSeparator,
 | 
			
		||||
  CommandShortcut,
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										122
									
								
								frontend/src/components/ui/dialog.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								frontend/src/components/ui/dialog.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,122 @@
 | 
			
		||||
"use client";
 | 
			
		||||
 | 
			
		||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
 | 
			
		||||
import { X } from "lucide-react";
 | 
			
		||||
import * as React from "react";
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils";
 | 
			
		||||
 | 
			
		||||
const Dialog = DialogPrimitive.Root;
 | 
			
		||||
 | 
			
		||||
const DialogTrigger = DialogPrimitive.Trigger;
 | 
			
		||||
 | 
			
		||||
const DialogPortal = DialogPrimitive.Portal;
 | 
			
		||||
 | 
			
		||||
const DialogClose = DialogPrimitive.Close;
 | 
			
		||||
 | 
			
		||||
const DialogOverlay = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof DialogPrimitive.Overlay>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <DialogPrimitive.Overlay
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "fixed inset-0 z-50 bg-black/80  data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
 | 
			
		||||
      className,
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
));
 | 
			
		||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
 | 
			
		||||
 | 
			
		||||
const DialogContent = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof DialogPrimitive.Content>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
 | 
			
		||||
>(({ className, children, ...props }, ref) => (
 | 
			
		||||
  <DialogPortal>
 | 
			
		||||
    <DialogOverlay />
 | 
			
		||||
    <DialogPrimitive.Content
 | 
			
		||||
      ref={ref}
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-51%] gap-4 border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
 | 
			
		||||
        className,
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    >
 | 
			
		||||
      {children}
 | 
			
		||||
      <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
 | 
			
		||||
        <X className="h-4 w-4" />
 | 
			
		||||
        <span className="sr-only">Close</span>
 | 
			
		||||
      </DialogPrimitive.Close>
 | 
			
		||||
    </DialogPrimitive.Content>
 | 
			
		||||
  </DialogPortal>
 | 
			
		||||
));
 | 
			
		||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
 | 
			
		||||
 | 
			
		||||
const DialogHeader = ({
 | 
			
		||||
  className,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.HTMLAttributes<HTMLDivElement>) => (
 | 
			
		||||
  <div
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "flex flex-col space-y-1.5 text-center sm:text-left",
 | 
			
		||||
      className,
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
);
 | 
			
		||||
DialogHeader.displayName = "DialogHeader";
 | 
			
		||||
 | 
			
		||||
const DialogFooter = ({
 | 
			
		||||
  className,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.HTMLAttributes<HTMLDivElement>) => (
 | 
			
		||||
  <div
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
 | 
			
		||||
      className,
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
);
 | 
			
		||||
DialogFooter.displayName = "DialogFooter";
 | 
			
		||||
 | 
			
		||||
const DialogTitle = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof DialogPrimitive.Title>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <DialogPrimitive.Title
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "text-lg font-semibold leading-none tracking-tight",
 | 
			
		||||
      className,
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
));
 | 
			
		||||
DialogTitle.displayName = DialogPrimitive.Title.displayName;
 | 
			
		||||
 | 
			
		||||
const DialogDescription = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof DialogPrimitive.Description>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <DialogPrimitive.Description
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn("text-sm text-muted-foreground", className)}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
));
 | 
			
		||||
DialogDescription.displayName = DialogPrimitive.Description.displayName;
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  Dialog,
 | 
			
		||||
  DialogClose,
 | 
			
		||||
  DialogContent,
 | 
			
		||||
  DialogDescription,
 | 
			
		||||
  DialogFooter,
 | 
			
		||||
  DialogHeader,
 | 
			
		||||
  DialogOverlay,
 | 
			
		||||
  DialogPortal,
 | 
			
		||||
  DialogTitle,
 | 
			
		||||
  DialogTrigger,
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										200
									
								
								frontend/src/components/ui/dropdown-menu.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										200
									
								
								frontend/src/components/ui/dropdown-menu.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,200 @@
 | 
			
		||||
"use client";
 | 
			
		||||
 | 
			
		||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
 | 
			
		||||
import { Check, ChevronRight, Circle } from "lucide-react";
 | 
			
		||||
import * as React from "react";
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils";
 | 
			
		||||
 | 
			
		||||
const DropdownMenu = DropdownMenuPrimitive.Root;
 | 
			
		||||
 | 
			
		||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
 | 
			
		||||
 | 
			
		||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
 | 
			
		||||
 | 
			
		||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
 | 
			
		||||
 | 
			
		||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
 | 
			
		||||
 | 
			
		||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
 | 
			
		||||
 | 
			
		||||
const DropdownMenuSubTrigger = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
 | 
			
		||||
    inset?: boolean;
 | 
			
		||||
  }
 | 
			
		||||
>(({ className, inset, children, ...props }, ref) => (
 | 
			
		||||
  <DropdownMenuPrimitive.SubTrigger
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
 | 
			
		||||
      inset && "pl-8",
 | 
			
		||||
      className,
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  >
 | 
			
		||||
    {children}
 | 
			
		||||
    <ChevronRight className="ml-auto h-4 w-4" />
 | 
			
		||||
  </DropdownMenuPrimitive.SubTrigger>
 | 
			
		||||
));
 | 
			
		||||
DropdownMenuSubTrigger.displayName =
 | 
			
		||||
  DropdownMenuPrimitive.SubTrigger.displayName;
 | 
			
		||||
 | 
			
		||||
const DropdownMenuSubContent = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <DropdownMenuPrimitive.SubContent
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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,
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
));
 | 
			
		||||
DropdownMenuSubContent.displayName =
 | 
			
		||||
  DropdownMenuPrimitive.SubContent.displayName;
 | 
			
		||||
 | 
			
		||||
const DropdownMenuContent = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof DropdownMenuPrimitive.Content>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
 | 
			
		||||
>(({ className, sideOffset = 4, ...props }, ref) => (
 | 
			
		||||
  <DropdownMenuPrimitive.Portal>
 | 
			
		||||
    <DropdownMenuPrimitive.Content
 | 
			
		||||
      ref={ref}
 | 
			
		||||
      sideOffset={sideOffset}
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "glass z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover/50 p-1 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",
 | 
			
		||||
        className,
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  </DropdownMenuPrimitive.Portal>
 | 
			
		||||
));
 | 
			
		||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
 | 
			
		||||
 | 
			
		||||
const DropdownMenuItem = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof DropdownMenuPrimitive.Item>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
 | 
			
		||||
    inset?: boolean;
 | 
			
		||||
  }
 | 
			
		||||
>(({ className, inset, ...props }, ref) => (
 | 
			
		||||
  <DropdownMenuPrimitive.Item
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "relative flex cursor-default select-none items-center rounded-sm px-2 py-1 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
 | 
			
		||||
      inset && "pl-8",
 | 
			
		||||
      className,
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
));
 | 
			
		||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
 | 
			
		||||
 | 
			
		||||
const DropdownMenuCheckboxItem = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
 | 
			
		||||
>(({ className, children, checked, ...props }, ref) => (
 | 
			
		||||
  <DropdownMenuPrimitive.CheckboxItem
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
 | 
			
		||||
      className,
 | 
			
		||||
    )}
 | 
			
		||||
    checked={checked}
 | 
			
		||||
    {...props}
 | 
			
		||||
  >
 | 
			
		||||
    <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
 | 
			
		||||
      <DropdownMenuPrimitive.ItemIndicator>
 | 
			
		||||
        <Check className="h-4 w-4" />
 | 
			
		||||
      </DropdownMenuPrimitive.ItemIndicator>
 | 
			
		||||
    </span>
 | 
			
		||||
    {children}
 | 
			
		||||
  </DropdownMenuPrimitive.CheckboxItem>
 | 
			
		||||
));
 | 
			
		||||
DropdownMenuCheckboxItem.displayName =
 | 
			
		||||
  DropdownMenuPrimitive.CheckboxItem.displayName;
 | 
			
		||||
 | 
			
		||||
const DropdownMenuRadioItem = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
 | 
			
		||||
>(({ className, children, ...props }, ref) => (
 | 
			
		||||
  <DropdownMenuPrimitive.RadioItem
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
 | 
			
		||||
      className,
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  >
 | 
			
		||||
    <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
 | 
			
		||||
      <DropdownMenuPrimitive.ItemIndicator>
 | 
			
		||||
        <Circle className="h-2 w-2 fill-current" />
 | 
			
		||||
      </DropdownMenuPrimitive.ItemIndicator>
 | 
			
		||||
    </span>
 | 
			
		||||
    {children}
 | 
			
		||||
  </DropdownMenuPrimitive.RadioItem>
 | 
			
		||||
));
 | 
			
		||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
 | 
			
		||||
 | 
			
		||||
const DropdownMenuLabel = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof DropdownMenuPrimitive.Label>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
 | 
			
		||||
    inset?: boolean;
 | 
			
		||||
  }
 | 
			
		||||
>(({ className, inset, ...props }, ref) => (
 | 
			
		||||
  <DropdownMenuPrimitive.Label
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "px-2 py-1.5 text-sm font-semibold",
 | 
			
		||||
      inset && "pl-8",
 | 
			
		||||
      className,
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
));
 | 
			
		||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
 | 
			
		||||
 | 
			
		||||
const DropdownMenuSeparator = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <DropdownMenuPrimitive.Separator
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn("-mx-1 my-1 h-px bg-muted", className)}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
));
 | 
			
		||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
 | 
			
		||||
 | 
			
		||||
const DropdownMenuShortcut = ({
 | 
			
		||||
  className,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <span
 | 
			
		||||
      className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  DropdownMenu,
 | 
			
		||||
  DropdownMenuCheckboxItem,
 | 
			
		||||
  DropdownMenuContent,
 | 
			
		||||
  DropdownMenuGroup,
 | 
			
		||||
  DropdownMenuItem,
 | 
			
		||||
  DropdownMenuLabel,
 | 
			
		||||
  DropdownMenuPortal,
 | 
			
		||||
  DropdownMenuRadioGroup,
 | 
			
		||||
  DropdownMenuRadioItem,
 | 
			
		||||
  DropdownMenuSeparator,
 | 
			
		||||
  DropdownMenuShortcut,
 | 
			
		||||
  DropdownMenuSub,
 | 
			
		||||
  DropdownMenuSubContent,
 | 
			
		||||
  DropdownMenuSubTrigger,
 | 
			
		||||
  DropdownMenuTrigger,
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										25
									
								
								frontend/src/components/ui/input.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								frontend/src/components/ui/input.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
			
		||||
import * as React from "react";
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils";
 | 
			
		||||
 | 
			
		||||
export interface InputProps
 | 
			
		||||
  extends React.InputHTMLAttributes<HTMLInputElement> {}
 | 
			
		||||
 | 
			
		||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
 | 
			
		||||
  ({ className, type, ...props }, ref) => {
 | 
			
		||||
    return (
 | 
			
		||||
      <input
 | 
			
		||||
        type={type}
 | 
			
		||||
        className={cn(
 | 
			
		||||
          "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium 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",
 | 
			
		||||
          className,
 | 
			
		||||
        )}
 | 
			
		||||
        ref={ref}
 | 
			
		||||
        {...props}
 | 
			
		||||
      />
 | 
			
		||||
    );
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
Input.displayName = "Input";
 | 
			
		||||
 | 
			
		||||
export { Input };
 | 
			
		||||
							
								
								
									
										128
									
								
								frontend/src/components/ui/navigation-menu.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								frontend/src/components/ui/navigation-menu.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,128 @@
 | 
			
		||||
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
 | 
			
		||||
import { cva } from "class-variance-authority";
 | 
			
		||||
import { ChevronDown } from "lucide-react";
 | 
			
		||||
import * as React from "react";
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils";
 | 
			
		||||
 | 
			
		||||
const NavigationMenu = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof NavigationMenuPrimitive.Root>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
 | 
			
		||||
>(({ className, children, ...props }, ref) => (
 | 
			
		||||
  <NavigationMenuPrimitive.Root
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "relative z-10 flex max-w-max flex-1 items-center justify-center",
 | 
			
		||||
      className,
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  >
 | 
			
		||||
    {children}
 | 
			
		||||
    <NavigationMenuViewport />
 | 
			
		||||
  </NavigationMenuPrimitive.Root>
 | 
			
		||||
));
 | 
			
		||||
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;
 | 
			
		||||
 | 
			
		||||
const NavigationMenuList = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof NavigationMenuPrimitive.List>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <NavigationMenuPrimitive.List
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "group flex flex-1 list-none items-center justify-center space-x-1",
 | 
			
		||||
      className,
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
));
 | 
			
		||||
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
 | 
			
		||||
 | 
			
		||||
const NavigationMenuItem = NavigationMenuPrimitive.Item;
 | 
			
		||||
 | 
			
		||||
const navigationMenuTriggerStyle = cva(
 | 
			
		||||
  "group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50",
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const NavigationMenuTrigger = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
 | 
			
		||||
>(({ className, children, ...props }, ref) => (
 | 
			
		||||
  <NavigationMenuPrimitive.Trigger
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(navigationMenuTriggerStyle(), "group", className)}
 | 
			
		||||
    {...props}
 | 
			
		||||
  >
 | 
			
		||||
    {children}{" "}
 | 
			
		||||
    <ChevronDown
 | 
			
		||||
      className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
 | 
			
		||||
      aria-hidden="true"
 | 
			
		||||
    />
 | 
			
		||||
  </NavigationMenuPrimitive.Trigger>
 | 
			
		||||
));
 | 
			
		||||
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;
 | 
			
		||||
 | 
			
		||||
const NavigationMenuContent = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof NavigationMenuPrimitive.Content>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <NavigationMenuPrimitive.Content
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
 | 
			
		||||
      className,
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
));
 | 
			
		||||
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;
 | 
			
		||||
 | 
			
		||||
const NavigationMenuLink = NavigationMenuPrimitive.Link;
 | 
			
		||||
 | 
			
		||||
const NavigationMenuViewport = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <div className={cn("absolute left-0 top-full flex justify-center")}>
 | 
			
		||||
    <NavigationMenuPrimitive.Viewport
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
 | 
			
		||||
        className,
 | 
			
		||||
      )}
 | 
			
		||||
      ref={ref}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  </div>
 | 
			
		||||
));
 | 
			
		||||
NavigationMenuViewport.displayName =
 | 
			
		||||
  NavigationMenuPrimitive.Viewport.displayName;
 | 
			
		||||
 | 
			
		||||
const NavigationMenuIndicator = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <NavigationMenuPrimitive.Indicator
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
 | 
			
		||||
      className,
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  >
 | 
			
		||||
    <div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
 | 
			
		||||
  </NavigationMenuPrimitive.Indicator>
 | 
			
		||||
));
 | 
			
		||||
NavigationMenuIndicator.displayName =
 | 
			
		||||
  NavigationMenuPrimitive.Indicator.displayName;
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  NavigationMenu,
 | 
			
		||||
  NavigationMenuContent,
 | 
			
		||||
  NavigationMenuIndicator,
 | 
			
		||||
  NavigationMenuItem,
 | 
			
		||||
  NavigationMenuLink,
 | 
			
		||||
  NavigationMenuList,
 | 
			
		||||
  NavigationMenuTrigger,
 | 
			
		||||
  NavigationMenuViewport,
 | 
			
		||||
  navigationMenuTriggerStyle,
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										61
									
								
								frontend/src/components/ui/number-ticker.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								frontend/src/components/ui/number-ticker.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,61 @@
 | 
			
		||||
"use client";
 | 
			
		||||
 | 
			
		||||
import { useInView, useMotionValue, useSpring } from "framer-motion";
 | 
			
		||||
import { useEffect, useRef } from "react";
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils";
 | 
			
		||||
 | 
			
		||||
export default function NumberTicker({
 | 
			
		||||
  value,
 | 
			
		||||
  direction = "up",
 | 
			
		||||
  delay = 0,
 | 
			
		||||
  className,
 | 
			
		||||
  decimalPlaces = 0,
 | 
			
		||||
}: {
 | 
			
		||||
  value: number;
 | 
			
		||||
  direction?: "up" | "down";
 | 
			
		||||
  className?: string;
 | 
			
		||||
  delay?: number; // delay in s
 | 
			
		||||
  decimalPlaces?: number;
 | 
			
		||||
}) {
 | 
			
		||||
  const ref = useRef<HTMLSpanElement>(null);
 | 
			
		||||
  const motionValue = useMotionValue(direction === "down" ? value : 0);
 | 
			
		||||
  const springValue = useSpring(motionValue, {
 | 
			
		||||
    damping: 60,
 | 
			
		||||
    stiffness: 100,
 | 
			
		||||
  });
 | 
			
		||||
  const isInView = useInView(ref as React.RefObject<Element>, {
 | 
			
		||||
    once: true,
 | 
			
		||||
    margin: "0px",
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    isInView &&
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        motionValue.set(direction === "down" ? 0 : value);
 | 
			
		||||
      }, delay * 1000);
 | 
			
		||||
  }, [motionValue, isInView, delay, value, direction]);
 | 
			
		||||
 | 
			
		||||
  useEffect(
 | 
			
		||||
    () =>
 | 
			
		||||
      springValue.on("change", (latest) => {
 | 
			
		||||
        if (ref.current) {
 | 
			
		||||
          ref.current.textContent = Intl.NumberFormat("en-US", {
 | 
			
		||||
            minimumFractionDigits: decimalPlaces,
 | 
			
		||||
            maximumFractionDigits: decimalPlaces,
 | 
			
		||||
          }).format(Number(latest.toFixed(decimalPlaces)));
 | 
			
		||||
        }
 | 
			
		||||
      }),
 | 
			
		||||
    [springValue, decimalPlaces],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <span
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "inline-block tabular-nums text-black dark:text-white tracking-wider",
 | 
			
		||||
        className,
 | 
			
		||||
      )}
 | 
			
		||||
      ref={ref}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										283
									
								
								frontend/src/components/ui/particles.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										283
									
								
								frontend/src/components/ui/particles.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,283 @@
 | 
			
		||||
"use client";
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils";
 | 
			
		||||
import React, { useEffect, useRef, useState } from "react";
 | 
			
		||||
 | 
			
		||||
interface MousePosition {
 | 
			
		||||
  x: number;
 | 
			
		||||
  y: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function MousePosition(): MousePosition {
 | 
			
		||||
  const [mousePosition, setMousePosition] = useState<MousePosition>({
 | 
			
		||||
    x: 0,
 | 
			
		||||
    y: 0,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const handleMouseMove = (event: MouseEvent) => {
 | 
			
		||||
      setMousePosition({ x: event.clientX, y: event.clientY });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    window.addEventListener("mousemove", handleMouseMove);
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      window.removeEventListener("mousemove", handleMouseMove);
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  return mousePosition;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface ParticlesProps {
 | 
			
		||||
  className?: string;
 | 
			
		||||
  quantity?: number;
 | 
			
		||||
  staticity?: number;
 | 
			
		||||
  ease?: number;
 | 
			
		||||
  size?: number;
 | 
			
		||||
  refresh?: boolean;
 | 
			
		||||
  color?: string;
 | 
			
		||||
  vx?: number;
 | 
			
		||||
  vy?: number;
 | 
			
		||||
}
 | 
			
		||||
function hexToRgb(hex: string): number[] {
 | 
			
		||||
  hex = hex.replace("#", "");
 | 
			
		||||
 | 
			
		||||
  if (hex.length === 3) {
 | 
			
		||||
    hex = hex
 | 
			
		||||
      .split("")
 | 
			
		||||
      .map((char) => char + char)
 | 
			
		||||
      .join("");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const hexInt = parseInt(hex, 16);
 | 
			
		||||
  const red = (hexInt >> 16) & 255;
 | 
			
		||||
  const green = (hexInt >> 8) & 255;
 | 
			
		||||
  const blue = hexInt & 255;
 | 
			
		||||
  return [red, green, blue];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Particles: React.FC<ParticlesProps> = ({
 | 
			
		||||
  className = "",
 | 
			
		||||
  quantity = 100,
 | 
			
		||||
  staticity = 50,
 | 
			
		||||
  ease = 50,
 | 
			
		||||
  size = 0.4,
 | 
			
		||||
  refresh = false,
 | 
			
		||||
  color = "#ffffff",
 | 
			
		||||
  vx = 0,
 | 
			
		||||
  vy = 0,
 | 
			
		||||
}) => {
 | 
			
		||||
  const canvasRef = useRef<HTMLCanvasElement>(null);
 | 
			
		||||
  const canvasContainerRef = useRef<HTMLDivElement>(null);
 | 
			
		||||
  const context = useRef<CanvasRenderingContext2D | null>(null);
 | 
			
		||||
  const circles = useRef<Circle[]>([]);
 | 
			
		||||
  const mousePosition = MousePosition();
 | 
			
		||||
  const mouse = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
 | 
			
		||||
  const canvasSize = useRef<{ w: number; h: number }>({ w: 0, h: 0 });
 | 
			
		||||
  const dpr = typeof window !== "undefined" ? window.devicePixelRatio : 1;
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (canvasRef.current) {
 | 
			
		||||
      context.current = canvasRef.current.getContext("2d");
 | 
			
		||||
    }
 | 
			
		||||
    initCanvas();
 | 
			
		||||
    animate();
 | 
			
		||||
    window.addEventListener("resize", initCanvas);
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      window.removeEventListener("resize", initCanvas);
 | 
			
		||||
    };
 | 
			
		||||
  }, [color]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    onMouseMove();
 | 
			
		||||
  }, [mousePosition.x, mousePosition.y]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    initCanvas();
 | 
			
		||||
  }, [refresh]);
 | 
			
		||||
 | 
			
		||||
  const initCanvas = () => {
 | 
			
		||||
    resizeCanvas();
 | 
			
		||||
    drawParticles();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const onMouseMove = () => {
 | 
			
		||||
    if (canvasRef.current) {
 | 
			
		||||
      const rect = canvasRef.current.getBoundingClientRect();
 | 
			
		||||
      const { w, h } = canvasSize.current;
 | 
			
		||||
      const x = mousePosition.x - rect.left - w / 2;
 | 
			
		||||
      const y = mousePosition.y - rect.top - h / 2;
 | 
			
		||||
      const inside = x < w / 2 && x > -w / 2 && y < h / 2 && y > -h / 2;
 | 
			
		||||
      if (inside) {
 | 
			
		||||
        mouse.current.x = x;
 | 
			
		||||
        mouse.current.y = y;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  type Circle = {
 | 
			
		||||
    x: number;
 | 
			
		||||
    y: number;
 | 
			
		||||
    translateX: number;
 | 
			
		||||
    translateY: number;
 | 
			
		||||
    size: number;
 | 
			
		||||
    alpha: number;
 | 
			
		||||
    targetAlpha: number;
 | 
			
		||||
    dx: number;
 | 
			
		||||
    dy: number;
 | 
			
		||||
    magnetism: number;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const resizeCanvas = () => {
 | 
			
		||||
    if (canvasContainerRef.current && canvasRef.current && context.current) {
 | 
			
		||||
      circles.current.length = 0;
 | 
			
		||||
      canvasSize.current.w = canvasContainerRef.current.offsetWidth;
 | 
			
		||||
      canvasSize.current.h = canvasContainerRef.current.offsetHeight;
 | 
			
		||||
      canvasRef.current.width = canvasSize.current.w * dpr;
 | 
			
		||||
      canvasRef.current.height = canvasSize.current.h * dpr;
 | 
			
		||||
      canvasRef.current.style.width = `${canvasSize.current.w}px`;
 | 
			
		||||
      canvasRef.current.style.height = `${canvasSize.current.h}px`;
 | 
			
		||||
      context.current.scale(dpr, dpr);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const circleParams = (): Circle => {
 | 
			
		||||
    const x = Math.floor(Math.random() * canvasSize.current.w);
 | 
			
		||||
    const y = Math.floor(Math.random() * canvasSize.current.h);
 | 
			
		||||
    const translateX = 0;
 | 
			
		||||
    const translateY = 0;
 | 
			
		||||
    const pSize = Math.floor(Math.random() * 2) + size;
 | 
			
		||||
    const alpha = 0;
 | 
			
		||||
    const targetAlpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1));
 | 
			
		||||
    const dx = (Math.random() - 0.5) * 0.1;
 | 
			
		||||
    const dy = (Math.random() - 0.5) * 0.1;
 | 
			
		||||
    const magnetism = 0.1 + Math.random() * 4;
 | 
			
		||||
    return {
 | 
			
		||||
      x,
 | 
			
		||||
      y,
 | 
			
		||||
      translateX,
 | 
			
		||||
      translateY,
 | 
			
		||||
      size: pSize,
 | 
			
		||||
      alpha,
 | 
			
		||||
      targetAlpha,
 | 
			
		||||
      dx,
 | 
			
		||||
      dy,
 | 
			
		||||
      magnetism,
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const rgb = hexToRgb(color);
 | 
			
		||||
 | 
			
		||||
  const drawCircle = (circle: Circle, update = false) => {
 | 
			
		||||
    if (context.current) {
 | 
			
		||||
      const { x, y, translateX, translateY, size, alpha } = circle;
 | 
			
		||||
      context.current.translate(translateX, translateY);
 | 
			
		||||
      context.current.beginPath();
 | 
			
		||||
      context.current.arc(x, y, size, 0, 2 * Math.PI);
 | 
			
		||||
      context.current.fillStyle = `rgba(${rgb.join(", ")}, ${alpha})`;
 | 
			
		||||
      context.current.fill();
 | 
			
		||||
      context.current.setTransform(dpr, 0, 0, dpr, 0, 0);
 | 
			
		||||
 | 
			
		||||
      if (!update) {
 | 
			
		||||
        circles.current.push(circle);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const clearContext = () => {
 | 
			
		||||
    if (context.current) {
 | 
			
		||||
      context.current.clearRect(
 | 
			
		||||
        0,
 | 
			
		||||
        0,
 | 
			
		||||
        canvasSize.current.w,
 | 
			
		||||
        canvasSize.current.h,
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const drawParticles = () => {
 | 
			
		||||
    clearContext();
 | 
			
		||||
    const particleCount = quantity;
 | 
			
		||||
    for (let i = 0; i < particleCount; i++) {
 | 
			
		||||
      const circle = circleParams();
 | 
			
		||||
      drawCircle(circle);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const remapValue = (
 | 
			
		||||
    value: number,
 | 
			
		||||
    start1: number,
 | 
			
		||||
    end1: number,
 | 
			
		||||
    start2: number,
 | 
			
		||||
    end2: number,
 | 
			
		||||
  ): number => {
 | 
			
		||||
    const remapped =
 | 
			
		||||
      ((value - start1) * (end2 - start2)) / (end1 - start1) + start2;
 | 
			
		||||
    return remapped > 0 ? remapped : 0;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const animate = () => {
 | 
			
		||||
    clearContext();
 | 
			
		||||
    circles.current.forEach((circle: Circle, i: number) => {
 | 
			
		||||
      // Handle the alpha value
 | 
			
		||||
      const edge = [
 | 
			
		||||
        circle.x + circle.translateX - circle.size, // distance from left edge
 | 
			
		||||
        canvasSize.current.w - circle.x - circle.translateX - circle.size, // distance from right edge
 | 
			
		||||
        circle.y + circle.translateY - circle.size, // distance from top 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 remapClosestEdge = parseFloat(
 | 
			
		||||
        remapValue(closestEdge, 0, 20, 0, 1).toFixed(2),
 | 
			
		||||
      );
 | 
			
		||||
      if (remapClosestEdge > 1) {
 | 
			
		||||
        circle.alpha += 0.02;
 | 
			
		||||
        if (circle.alpha > circle.targetAlpha) {
 | 
			
		||||
          circle.alpha = circle.targetAlpha;
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        circle.alpha = circle.targetAlpha * remapClosestEdge;
 | 
			
		||||
      }
 | 
			
		||||
      circle.x += circle.dx + vx;
 | 
			
		||||
      circle.y += circle.dy + vy;
 | 
			
		||||
      circle.translateX +=
 | 
			
		||||
        (mouse.current.x / (staticity / circle.magnetism) - circle.translateX) /
 | 
			
		||||
        ease;
 | 
			
		||||
      circle.translateY +=
 | 
			
		||||
        (mouse.current.y / (staticity / circle.magnetism) - circle.translateY) /
 | 
			
		||||
        ease;
 | 
			
		||||
 | 
			
		||||
      drawCircle(circle, true);
 | 
			
		||||
 | 
			
		||||
      // circle gets out of the canvas
 | 
			
		||||
      if (
 | 
			
		||||
        circle.x < -circle.size ||
 | 
			
		||||
        circle.x > canvasSize.current.w + circle.size ||
 | 
			
		||||
        circle.y < -circle.size ||
 | 
			
		||||
        circle.y > canvasSize.current.h + circle.size
 | 
			
		||||
      ) {
 | 
			
		||||
        // remove the circle from the array
 | 
			
		||||
        circles.current.splice(i, 1);
 | 
			
		||||
        // create a new circle
 | 
			
		||||
        const newCircle = circleParams();
 | 
			
		||||
        drawCircle(newCircle);
 | 
			
		||||
        // update the circle position
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    window.requestAnimationFrame(animate);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      className={cn("pointer-events-none", className)}
 | 
			
		||||
      ref={canvasContainerRef}
 | 
			
		||||
      aria-hidden="true"
 | 
			
		||||
    >
 | 
			
		||||
      <canvas ref={canvasRef} className="size-full" />
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Particles;
 | 
			
		||||
							
								
								
									
										31
									
								
								frontend/src/components/ui/separator.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								frontend/src/components/ui/separator.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
			
		||||
"use client";
 | 
			
		||||
 | 
			
		||||
import * as SeparatorPrimitive from "@radix-ui/react-separator";
 | 
			
		||||
import * as React from "react";
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils";
 | 
			
		||||
 | 
			
		||||
const Separator = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof SeparatorPrimitive.Root>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
 | 
			
		||||
>(
 | 
			
		||||
  (
 | 
			
		||||
    { className, orientation = "horizontal", decorative = true, ...props },
 | 
			
		||||
    ref,
 | 
			
		||||
  ) => (
 | 
			
		||||
    <SeparatorPrimitive.Root
 | 
			
		||||
      ref={ref}
 | 
			
		||||
      decorative={decorative}
 | 
			
		||||
      orientation={orientation}
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "shrink-0 bg-border",
 | 
			
		||||
        orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
 | 
			
		||||
        className,
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  ),
 | 
			
		||||
);
 | 
			
		||||
Separator.displayName = SeparatorPrimitive.Root.displayName;
 | 
			
		||||
 | 
			
		||||
export { Separator };
 | 
			
		||||
							
								
								
									
										140
									
								
								frontend/src/components/ui/sheet.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								frontend/src/components/ui/sheet.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,140 @@
 | 
			
		||||
"use client";
 | 
			
		||||
 | 
			
		||||
import * as SheetPrimitive from "@radix-ui/react-dialog";
 | 
			
		||||
import { cva, type VariantProps } from "class-variance-authority";
 | 
			
		||||
import { X } from "lucide-react";
 | 
			
		||||
import * as React from "react";
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils";
 | 
			
		||||
 | 
			
		||||
const Sheet = SheetPrimitive.Root;
 | 
			
		||||
 | 
			
		||||
const SheetTrigger = SheetPrimitive.Trigger;
 | 
			
		||||
 | 
			
		||||
const SheetClose = SheetPrimitive.Close;
 | 
			
		||||
 | 
			
		||||
const SheetPortal = SheetPrimitive.Portal;
 | 
			
		||||
 | 
			
		||||
const SheetOverlay = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof SheetPrimitive.Overlay>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <SheetPrimitive.Overlay
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "fixed inset-0 z-50 bg-black/80  data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
 | 
			
		||||
      className,
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
    ref={ref}
 | 
			
		||||
  />
 | 
			
		||||
));
 | 
			
		||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
 | 
			
		||||
 | 
			
		||||
const sheetVariants = cva(
 | 
			
		||||
  "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
 | 
			
		||||
  {
 | 
			
		||||
    variants: {
 | 
			
		||||
      side: {
 | 
			
		||||
        top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
 | 
			
		||||
        bottom:
 | 
			
		||||
          "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
 | 
			
		||||
        left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
 | 
			
		||||
        right:
 | 
			
		||||
          "inset-y-0 right-0 h-full w-3/4  border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    defaultVariants: {
 | 
			
		||||
      side: "right",
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
interface SheetContentProps
 | 
			
		||||
  extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
 | 
			
		||||
    VariantProps<typeof sheetVariants> {}
 | 
			
		||||
 | 
			
		||||
const SheetContent = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof SheetPrimitive.Content>,
 | 
			
		||||
  SheetContentProps
 | 
			
		||||
>(({ side = "right", className, children, ...props }, ref) => (
 | 
			
		||||
  <SheetPortal>
 | 
			
		||||
    <SheetOverlay />
 | 
			
		||||
    <SheetPrimitive.Content
 | 
			
		||||
      ref={ref}
 | 
			
		||||
      className={cn(sheetVariants({ side }), className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    >
 | 
			
		||||
      {children}
 | 
			
		||||
      <SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
 | 
			
		||||
        <X className="h-4 w-4" />
 | 
			
		||||
        <span className="sr-only">Close</span>
 | 
			
		||||
      </SheetPrimitive.Close>
 | 
			
		||||
    </SheetPrimitive.Content>
 | 
			
		||||
  </SheetPortal>
 | 
			
		||||
));
 | 
			
		||||
SheetContent.displayName = SheetPrimitive.Content.displayName;
 | 
			
		||||
 | 
			
		||||
const SheetHeader = ({
 | 
			
		||||
  className,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.HTMLAttributes<HTMLDivElement>) => (
 | 
			
		||||
  <div
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "flex flex-col space-y-2 text-center sm:text-left",
 | 
			
		||||
      className,
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
);
 | 
			
		||||
SheetHeader.displayName = "SheetHeader";
 | 
			
		||||
 | 
			
		||||
const SheetFooter = ({
 | 
			
		||||
  className,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.HTMLAttributes<HTMLDivElement>) => (
 | 
			
		||||
  <div
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
 | 
			
		||||
      className,
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
);
 | 
			
		||||
SheetFooter.displayName = "SheetFooter";
 | 
			
		||||
 | 
			
		||||
const SheetTitle = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof SheetPrimitive.Title>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <SheetPrimitive.Title
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn("text-lg font-semibold text-foreground", className)}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
));
 | 
			
		||||
SheetTitle.displayName = SheetPrimitive.Title.displayName;
 | 
			
		||||
 | 
			
		||||
const SheetDescription = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof SheetPrimitive.Description>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <SheetPrimitive.Description
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn("text-sm text-muted-foreground", className)}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
));
 | 
			
		||||
SheetDescription.displayName = SheetPrimitive.Description.displayName;
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  Sheet,
 | 
			
		||||
  SheetClose,
 | 
			
		||||
  SheetContent,
 | 
			
		||||
  SheetDescription,
 | 
			
		||||
  SheetFooter,
 | 
			
		||||
  SheetHeader,
 | 
			
		||||
  SheetOverlay,
 | 
			
		||||
  SheetPortal,
 | 
			
		||||
  SheetTitle,
 | 
			
		||||
  SheetTrigger,
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										31
									
								
								frontend/src/components/ui/sonner.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								frontend/src/components/ui/sonner.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
			
		||||
"use client";
 | 
			
		||||
 | 
			
		||||
import { useTheme } from "next-themes";
 | 
			
		||||
import { Toaster as Sonner } from "sonner";
 | 
			
		||||
 | 
			
		||||
type ToasterProps = React.ComponentProps<typeof Sonner>;
 | 
			
		||||
 | 
			
		||||
const Toaster = ({ ...props }: ToasterProps) => {
 | 
			
		||||
  const { theme = "system" } = useTheme();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Sonner
 | 
			
		||||
      theme={theme as ToasterProps["theme"]}
 | 
			
		||||
      className="toaster group"
 | 
			
		||||
      toastOptions={{
 | 
			
		||||
        classNames: {
 | 
			
		||||
          toast:
 | 
			
		||||
            "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
 | 
			
		||||
          description: "group-[.toast]:text-muted-foreground",
 | 
			
		||||
          actionButton:
 | 
			
		||||
            "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
 | 
			
		||||
          cancelButton:
 | 
			
		||||
            "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
 | 
			
		||||
        },
 | 
			
		||||
      }}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export { Toaster };
 | 
			
		||||
							
								
								
									
										53
									
								
								frontend/src/components/ui/star-on-github-button.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								frontend/src/components/ui/star-on-github-button.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,53 @@
 | 
			
		||||
import { cn } from "@/lib/utils";
 | 
			
		||||
import Link from "next/link";
 | 
			
		||||
import { useEffect, useState } from "react";
 | 
			
		||||
import { FaGithub, FaStar } from "react-icons/fa";
 | 
			
		||||
import NumberTicker from "./number-ticker";
 | 
			
		||||
import { buttonVariants } from "./button";
 | 
			
		||||
 | 
			
		||||
export default function StarOnGithubButton() {
 | 
			
		||||
  const [stars, setStars] = useState(0);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const fetchStars = async () => {
 | 
			
		||||
      try {
 | 
			
		||||
        const res = await fetch("https://api.github.com/repos/community-scripts/ProxmoxVE", {
 | 
			
		||||
          next: { revalidate: 60 * 60 * 24 },
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (res.ok) {
 | 
			
		||||
          const data = await res.json();
 | 
			
		||||
          setStars(data.stargazers_count || stars);
 | 
			
		||||
        }
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        console.error("Error fetching stars:", error);
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
    fetchStars();
 | 
			
		||||
  }, [stars]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Link
 | 
			
		||||
      className={cn(
 | 
			
		||||
        buttonVariants(),
 | 
			
		||||
        "hidden h-9 min-w-[240px] gap-2 overflow-hidden whitespace-pre sm:flex lg:flex",
 | 
			
		||||
        "group relative justify-center gap-2 rounded-md transition-all duration-300 ease-out hover:ring-2 hover:ring-primary hover:ring-offset-2",
 | 
			
		||||
      )}
 | 
			
		||||
      target="_blank"
 | 
			
		||||
      href="https://github.com/community-scripts/ProxmoxVE"
 | 
			
		||||
    >
 | 
			
		||||
      <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">
 | 
			
		||||
        <FaGithub className="size-4" />
 | 
			
		||||
        <span className="ml-1">Star on GitHub</span>{" "}
 | 
			
		||||
      </div>
 | 
			
		||||
      <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" />
 | 
			
		||||
        <NumberTicker
 | 
			
		||||
          value={stars}
 | 
			
		||||
          className="font-display font-medium text-white dark:text-black"
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
    </Link>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										55
									
								
								frontend/src/components/ui/tabs.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								frontend/src/components/ui/tabs.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,55 @@
 | 
			
		||||
"use client";
 | 
			
		||||
 | 
			
		||||
import * as TabsPrimitive from "@radix-ui/react-tabs";
 | 
			
		||||
import * as React from "react";
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils";
 | 
			
		||||
 | 
			
		||||
const Tabs = TabsPrimitive.Root;
 | 
			
		||||
 | 
			
		||||
const TabsList = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof TabsPrimitive.List>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <TabsPrimitive.List
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
 | 
			
		||||
      className,
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
));
 | 
			
		||||
TabsList.displayName = TabsPrimitive.List.displayName;
 | 
			
		||||
 | 
			
		||||
const TabsTrigger = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof TabsPrimitive.Trigger>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <TabsPrimitive.Trigger
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
 | 
			
		||||
      className,
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
));
 | 
			
		||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
 | 
			
		||||
 | 
			
		||||
const TabsContent = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof TabsPrimitive.Content>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <TabsPrimitive.Content
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
 | 
			
		||||
      className,
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
));
 | 
			
		||||
TabsContent.displayName = TabsPrimitive.Content.displayName;
 | 
			
		||||
 | 
			
		||||
export { Tabs, TabsContent, TabsList, TabsTrigger };
 | 
			
		||||
							
								
								
									
										30
									
								
								frontend/src/components/ui/tooltip.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								frontend/src/components/ui/tooltip.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
"use client";
 | 
			
		||||
 | 
			
		||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
 | 
			
		||||
import * as React from "react";
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils";
 | 
			
		||||
 | 
			
		||||
const TooltipProvider = TooltipPrimitive.Provider;
 | 
			
		||||
 | 
			
		||||
const Tooltip = TooltipPrimitive.Root;
 | 
			
		||||
 | 
			
		||||
const TooltipTrigger = TooltipPrimitive.Trigger;
 | 
			
		||||
 | 
			
		||||
const TooltipContent = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof TooltipPrimitive.Content>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
 | 
			
		||||
>(({ className, sideOffset = 4, ...props }, ref) => (
 | 
			
		||||
  <TooltipPrimitive.Content
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    sideOffset={sideOffset}
 | 
			
		||||
    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",
 | 
			
		||||
      className,
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
));
 | 
			
		||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
 | 
			
		||||
 | 
			
		||||
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
 | 
			
		||||
							
								
								
									
										23
									
								
								frontend/src/config/siteConfig.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								frontend/src/config/siteConfig.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
import { MessagesSquare, Scroll } from "lucide-react";
 | 
			
		||||
import { FaGithub } from "react-icons/fa";
 | 
			
		||||
 | 
			
		||||
export const navbarLinks = [
 | 
			
		||||
  {
 | 
			
		||||
    href: "https://github.com/community-scripts/ProxmoxVE",
 | 
			
		||||
    event: "Github",
 | 
			
		||||
    icon: <FaGithub className="h-4 w-4" />,
 | 
			
		||||
    text: "Github",
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    href: "https://github.com/community-scripts/ProxmoxVE/blob/main/CHANGELOG.md",
 | 
			
		||||
    event: "Change Log",
 | 
			
		||||
    icon: <Scroll className="h-4 w-4" />,
 | 
			
		||||
    text: "Change Log",
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    href: "https://github.com/community-scripts/ProxmoxVE/discussions",
 | 
			
		||||
    event: "Discussions",
 | 
			
		||||
    icon: <MessagesSquare className="h-4 w-4" />,
 | 
			
		||||
    text: "Discussions",
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
							
								
								
									
										10
									
								
								frontend/src/lib/pocketbase.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								frontend/src/lib/pocketbase.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
import PocketBase from "pocketbase";
 | 
			
		||||
 | 
			
		||||
export const pb = new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL);
 | 
			
		||||
export const pbBackup = new PocketBase(
 | 
			
		||||
  process.env.NEXT_PUBLIC_POCKETBASE_URL_BACKUP,
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export const getImageURL = (recordId: string, fileName: string) => {
 | 
			
		||||
  return `${process.env.NEXT_PUBLIC_POCKETBASE_URL}/${recordId}/${fileName}`;
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										7
									
								
								frontend/src/lib/time.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								frontend/src/lib/time.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
export function extractDate(dateString: string): string {
 | 
			
		||||
  const date = new Date(dateString);
 | 
			
		||||
  const year = date.getFullYear();
 | 
			
		||||
  const month = String(date.getMonth() + 1).padStart(2, "0");
 | 
			
		||||
  const day = String(date.getDate()).padStart(2, "0");
 | 
			
		||||
  return `${year}-${month}-${day}`;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										55
									
								
								frontend/src/lib/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								frontend/src/lib/types.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,55 @@
 | 
			
		||||
// these are all the interfaces that are used in the site. these all come from the pocketbase database
 | 
			
		||||
 | 
			
		||||
export interface Script {
 | 
			
		||||
  title: string;
 | 
			
		||||
  description: string;
 | 
			
		||||
  documentation: string;
 | 
			
		||||
  website: string;
 | 
			
		||||
  logo: string;
 | 
			
		||||
  created: string;
 | 
			
		||||
  updated: string;
 | 
			
		||||
  id: string;
 | 
			
		||||
  item_type: string;
 | 
			
		||||
  interface: string;
 | 
			
		||||
  installCommand: string;
 | 
			
		||||
  port: number;
 | 
			
		||||
  post_install: string;
 | 
			
		||||
  default_cpu: string;
 | 
			
		||||
  default_hdd: string;
 | 
			
		||||
  default_ram: string;
 | 
			
		||||
  isUpdateable: boolean;
 | 
			
		||||
  isMostViewed: boolean;
 | 
			
		||||
  privileged: boolean;
 | 
			
		||||
  alpineScript: alpine_script;
 | 
			
		||||
  expand: {
 | 
			
		||||
    alpine_script: alpine_script;
 | 
			
		||||
    alerts: alerts[];
 | 
			
		||||
    default_login: default_login;
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Category {
 | 
			
		||||
  catagoryName: string;
 | 
			
		||||
  categoryId: string;
 | 
			
		||||
  id: string;
 | 
			
		||||
  created: string;
 | 
			
		||||
  expand: {
 | 
			
		||||
    items: Script[];
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface alpine_script {
 | 
			
		||||
  installCommand: string;
 | 
			
		||||
  default_cpu: string;
 | 
			
		||||
  default_hdd: string;
 | 
			
		||||
  default_ram: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface alerts {
 | 
			
		||||
  content: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface default_login {
 | 
			
		||||
  username: string;
 | 
			
		||||
  password: string;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										6
									
								
								frontend/src/lib/utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								frontend/src/lib/utils.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
import { clsx, type ClassValue } from "clsx";
 | 
			
		||||
import { twMerge } from "tailwind-merge";
 | 
			
		||||
 | 
			
		||||
export function cn(...inputs: ClassValue[]) {
 | 
			
		||||
  return twMerge(clsx(inputs));
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										82
									
								
								frontend/src/styles/globals.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								frontend/src/styles/globals.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,82 @@
 | 
			
		||||
@tailwind base;
 | 
			
		||||
@tailwind components;
 | 
			
		||||
@tailwind utilities;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@layer base {
 | 
			
		||||
  :root {
 | 
			
		||||
    --background: 0 0% 100%;
 | 
			
		||||
    --foreground: 224 71.4% 4.1%;
 | 
			
		||||
    --card: 0 0% 100%;
 | 
			
		||||
    --card-foreground: 224 71.4% 4.1%;
 | 
			
		||||
    --popover: 0 0% 100%;
 | 
			
		||||
    --popover-foreground: 224 71.4% 4.1%;
 | 
			
		||||
    --primary: 220.9 39.3% 11%;
 | 
			
		||||
    --primary-foreground: 210 20% 98%;
 | 
			
		||||
    --secondary: 220 14.3% 95.9%;
 | 
			
		||||
    --secondary-foreground: 220.9 39.3% 11%;
 | 
			
		||||
    --muted: 220 14.3% 95.9%;
 | 
			
		||||
    --muted-foreground: 220 8.9% 46.1%;
 | 
			
		||||
    --accent: 220 14.3% 95.9%;
 | 
			
		||||
    --accent-foreground: 220.9 39.3% 11%;
 | 
			
		||||
    --destructive: 0 84.2% 60.2%;
 | 
			
		||||
    --destructive-foreground: 210 20% 98%;
 | 
			
		||||
    --border: 220 13% 91%;
 | 
			
		||||
    --input: 220 13% 91%;
 | 
			
		||||
    --ring: 224 71.4% 4.1%;
 | 
			
		||||
    --radius: 0.5rem;
 | 
			
		||||
    --chart-1: 12 76% 61%;
 | 
			
		||||
    --chart-2: 173 58% 39%;
 | 
			
		||||
    --chart-3: 197 37% 24%;
 | 
			
		||||
    --chart-4: 43 74% 66%;
 | 
			
		||||
    --chart-5: 27 87% 67%;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .dark {
 | 
			
		||||
    --background: 224 71.4% 4.1%;
 | 
			
		||||
    --foreground: 210 20% 98%;
 | 
			
		||||
    --card: 224 71.4% 4.1%;
 | 
			
		||||
    --card-foreground: 210 20% 98%;
 | 
			
		||||
    --popover: 224 71.4% 4.1%;
 | 
			
		||||
    --popover-foreground: 210 20% 98%;
 | 
			
		||||
    --primary: 210 20% 98%;
 | 
			
		||||
    --primary-foreground: 220.9 39.3% 11%;
 | 
			
		||||
    --secondary: 215 27.9% 16.9%;
 | 
			
		||||
    --secondary-foreground: 210 20% 98%;
 | 
			
		||||
    --muted: 215 27.9% 16.9%;
 | 
			
		||||
    --muted-foreground: 217.9 10.6% 64.9%;
 | 
			
		||||
    --accent: 215 27.9% 16.9%;
 | 
			
		||||
    --accent-foreground: 210 20% 98%;
 | 
			
		||||
    --destructive: 0 62.8% 30.6%;
 | 
			
		||||
    --destructive-foreground: 210 20% 98%;
 | 
			
		||||
    --border: 215 27.9% 16.9%;
 | 
			
		||||
    --input: 215 27.9% 16.9%;
 | 
			
		||||
    --ring: 216 12.2% 83.9%;
 | 
			
		||||
    --chart-1: 220 70% 50%;
 | 
			
		||||
    --chart-2: 160 60% 45%;
 | 
			
		||||
    --chart-3: 30 80% 55%;
 | 
			
		||||
    --chart-4: 280 65% 60%;
 | 
			
		||||
    --chart-5: 340 75% 55%;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@layer base {
 | 
			
		||||
  * {
 | 
			
		||||
    @apply border-border;
 | 
			
		||||
  }
 | 
			
		||||
  body {
 | 
			
		||||
    @apply bg-background text-foreground;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
* {
 | 
			
		||||
  -ms-overflow-style: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
::-webkit-scrollbar {
 | 
			
		||||
  display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.glass {
 | 
			
		||||
  backdrop-filter: blur(15px) saturate(100%);
 | 
			
		||||
  -webkit-backdrop-filter: blur(15px) saturate(100%);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										180
									
								
								frontend/tailwind.config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										180
									
								
								frontend/tailwind.config.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,180 @@
 | 
			
		||||
import type { Config } from "tailwindcss";
 | 
			
		||||
 | 
			
		||||
const svgToDataUri = require("mini-svg-data-uri");
 | 
			
		||||
 | 
			
		||||
const {
 | 
			
		||||
  default: flattenColorPalette,
 | 
			
		||||
} = require("tailwindcss/lib/util/flattenColorPalette");
 | 
			
		||||
 | 
			
		||||
const config = {
 | 
			
		||||
  darkMode: ["class"],
 | 
			
		||||
  content: [
 | 
			
		||||
    "./pages/**/*.{ts,tsx}",
 | 
			
		||||
    "./components/**/*.{ts,tsx}",
 | 
			
		||||
    "./app/**/*.{ts,tsx}",
 | 
			
		||||
    "./src/**/*.{ts,tsx}",
 | 
			
		||||
  ],
 | 
			
		||||
  prefix: "",
 | 
			
		||||
  theme: {
 | 
			
		||||
    container: {
 | 
			
		||||
      center: true,
 | 
			
		||||
      padding: "2rem",
 | 
			
		||||
      screens: {
 | 
			
		||||
        "2xl": "1400px",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    extend: {
 | 
			
		||||
      colors: {
 | 
			
		||||
        border: "hsl(var(--border))",
 | 
			
		||||
        input: "hsl(var(--input))",
 | 
			
		||||
        ring: "hsl(var(--ring))",
 | 
			
		||||
        background: "hsl(var(--background))",
 | 
			
		||||
        foreground: "hsl(var(--foreground))",
 | 
			
		||||
        primary: {
 | 
			
		||||
          DEFAULT: "hsl(var(--primary))",
 | 
			
		||||
          foreground: "hsl(var(--primary-foreground))",
 | 
			
		||||
        },
 | 
			
		||||
        secondary: {
 | 
			
		||||
          DEFAULT: "hsl(var(--secondary))",
 | 
			
		||||
          foreground: "hsl(var(--secondary-foreground))",
 | 
			
		||||
        },
 | 
			
		||||
        destructive: {
 | 
			
		||||
          DEFAULT: "hsl(var(--destructive))",
 | 
			
		||||
          foreground: "hsl(var(--destructive-foreground))",
 | 
			
		||||
        },
 | 
			
		||||
        muted: {
 | 
			
		||||
          DEFAULT: "hsl(var(--muted))",
 | 
			
		||||
          foreground: "hsl(var(--muted-foreground))",
 | 
			
		||||
        },
 | 
			
		||||
        accent: {
 | 
			
		||||
          DEFAULT: "hsl(var(--accent))",
 | 
			
		||||
          foreground: "hsl(var(--accent-foreground))",
 | 
			
		||||
        },
 | 
			
		||||
        popover: {
 | 
			
		||||
          DEFAULT: "hsl(var(--popover))",
 | 
			
		||||
          foreground: "hsl(var(--popover-foreground))",
 | 
			
		||||
        },
 | 
			
		||||
        card: {
 | 
			
		||||
          DEFAULT: "hsl(var(--card))",
 | 
			
		||||
          foreground: "hsl(var(--card-foreground))",
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      borderRadius: {
 | 
			
		||||
        lg: "var(--radius)",
 | 
			
		||||
        md: "calc(var(--radius) - 2px)",
 | 
			
		||||
        sm: "calc(var(--radius) - 4px)",
 | 
			
		||||
      },
 | 
			
		||||
      keyframes: {
 | 
			
		||||
        "accordion-down": {
 | 
			
		||||
          from: { height: "0" },
 | 
			
		||||
          to: { height: "var(--radix-accordion-content-height)" },
 | 
			
		||||
        },
 | 
			
		||||
        "accordion-up": {
 | 
			
		||||
          from: { height: "var(--radix-accordion-content-height)" },
 | 
			
		||||
          to: { height: "0" },
 | 
			
		||||
        },
 | 
			
		||||
        shine: {
 | 
			
		||||
          from: { backgroundPosition: "200% 0" },
 | 
			
		||||
          to: { backgroundPosition: "-200% 0" },
 | 
			
		||||
        },
 | 
			
		||||
        gradient: {
 | 
			
		||||
          to: {
 | 
			
		||||
            backgroundPosition: "var(--bg-size) 0",
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        "shine-pulse": {
 | 
			
		||||
          "0%": {
 | 
			
		||||
            "background-position": "0% 0%",
 | 
			
		||||
          },
 | 
			
		||||
          "50%": {
 | 
			
		||||
            "background-position": "100% 100%",
 | 
			
		||||
          },
 | 
			
		||||
          to: {
 | 
			
		||||
            "background-position": "0% 0%",
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        moveHorizontal: {
 | 
			
		||||
          "0%": {
 | 
			
		||||
            transform: "translateX(-50%) translateY(-10%)",
 | 
			
		||||
          },
 | 
			
		||||
          "50%": {
 | 
			
		||||
            transform: "translateX(50%) translateY(10%)",
 | 
			
		||||
          },
 | 
			
		||||
          "100%": {
 | 
			
		||||
            transform: "translateX(-50%) translateY(-10%)",
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        moveInCircle: {
 | 
			
		||||
          "0%": {
 | 
			
		||||
            transform: "rotate(0deg)",
 | 
			
		||||
          },
 | 
			
		||||
          "50%": {
 | 
			
		||||
            transform: "rotate(180deg)",
 | 
			
		||||
          },
 | 
			
		||||
          "100%": {
 | 
			
		||||
            transform: "rotate(360deg)",
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        moveVertical: {
 | 
			
		||||
          "0%": {
 | 
			
		||||
            transform: "translateY(-50%)",
 | 
			
		||||
          },
 | 
			
		||||
          "50%": {
 | 
			
		||||
            transform: "translateY(50%)",
 | 
			
		||||
          },
 | 
			
		||||
          "100%": {
 | 
			
		||||
            transform: "translateY(-50%)",
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      animation: {
 | 
			
		||||
        "accordion-down": "accordion-down 0.2s ease-out",
 | 
			
		||||
        "accordion-up": "accordion-up 0.2s ease-out",
 | 
			
		||||
        shine: "shine 8s ease-in-out infinite",
 | 
			
		||||
        gradient: "gradient 8s linear infinite",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  plugins: [
 | 
			
		||||
    require(`tailwindcss-animated`),
 | 
			
		||||
    require("tailwindcss-animate"),
 | 
			
		||||
    addVariablesForColors,
 | 
			
		||||
    function ({ matchUtilities, theme }: any) {
 | 
			
		||||
      matchUtilities(
 | 
			
		||||
        {
 | 
			
		||||
          "bg-grid": (value: any) => ({
 | 
			
		||||
            backgroundImage: `url("${svgToDataUri(
 | 
			
		||||
              `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" fill="none" stroke="${value}"><path d="M0 .5H31.5V32"/></svg>`,
 | 
			
		||||
            )}")`,
 | 
			
		||||
          }),
 | 
			
		||||
          "bg-grid-small": (value: any) => ({
 | 
			
		||||
            backgroundImage: `url("${svgToDataUri(
 | 
			
		||||
              `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="8" height="8" fill="none" stroke="${value}"><path d="M0 .5H31.5V32"/></svg>`,
 | 
			
		||||
            )}")`,
 | 
			
		||||
          }),
 | 
			
		||||
          "bg-dot": (value: any) => ({
 | 
			
		||||
            backgroundImage: `url("${svgToDataUri(
 | 
			
		||||
              `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="16" height="16" fill="none"><circle fill="${value}" id="pattern-circle" cx="10" cy="10" r="1.6257413380501518"></circle></svg>`,
 | 
			
		||||
            )}")`,
 | 
			
		||||
          }),
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          values: flattenColorPalette(theme("backgroundColor")),
 | 
			
		||||
          type: "color",
 | 
			
		||||
        },
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
} satisfies Config;
 | 
			
		||||
 | 
			
		||||
function addVariablesForColors({ addBase, theme }: any) {
 | 
			
		||||
  let allColors = flattenColorPalette(theme("colors"));
 | 
			
		||||
  let newVars = Object.fromEntries(
 | 
			
		||||
    Object.entries(allColors).map(([key, val]) => [`--${key}`, val]),
 | 
			
		||||
  );
 | 
			
		||||
  addBase({
 | 
			
		||||
    ":root": newVars,
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default config;
 | 
			
		||||
							
								
								
									
										33
									
								
								frontend/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								frontend/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
			
		||||
{
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "lib": ["dom", "dom.iterable", "esnext"],
 | 
			
		||||
    "allowJs": true,
 | 
			
		||||
    "skipLibCheck": true,
 | 
			
		||||
    "strict": true,
 | 
			
		||||
    "noEmit": true,
 | 
			
		||||
    "esModuleInterop": true,
 | 
			
		||||
    "module": "esnext",
 | 
			
		||||
    "moduleResolution": "bundler",
 | 
			
		||||
    "resolveJsonModule": true,
 | 
			
		||||
    "isolatedModules": true,
 | 
			
		||||
    "jsx": "preserve",
 | 
			
		||||
    "incremental": true,
 | 
			
		||||
    "plugins": [
 | 
			
		||||
      {
 | 
			
		||||
        "name": "next"
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "paths": {
 | 
			
		||||
      "@/*": ["./src/*"]
 | 
			
		||||
    },
 | 
			
		||||
    "target": "ES2017"
 | 
			
		||||
  },
 | 
			
		||||
  "include": [
 | 
			
		||||
    "next-env.d.ts",
 | 
			
		||||
    "**/*.ts",
 | 
			
		||||
    "**/*.tsx",
 | 
			
		||||
    ".next/types/**/*.ts",
 | 
			
		||||
    "next.config.mjs",
 | 
			
		||||
  ],
 | 
			
		||||
  "exclude": ["node_modules"]
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user