diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1ec689b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,16 @@
+# Next.js
+web/.next/
+web/out/
+
+# Node
+web/node_modules/
+
+# Logs
+web/*.log
+
+# Environment variables
+web/.env
+web/.env.local
+web/.env.development.local
+web/.env.test.local
+web/.env.production.local
\ No newline at end of file
diff --git a/web/app/components/.DS_Store b/web/app/components/.DS_Store
new file mode 100644
index 0000000..6bade33
Binary files /dev/null and b/web/app/components/.DS_Store differ
diff --git a/web/app/components/CTA.tsx b/web/app/components/CTA.tsx
new file mode 100644
index 0000000..6b2c331
--- /dev/null
+++ b/web/app/components/CTA.tsx
@@ -0,0 +1,18 @@
+import { Button } from "@/components/ui/button"
+
+export default function CTA() {
+ return (
+
+
+
Ready to Streamline Your Workflow?
+
+ Join thousands of teams already using StreamLine to boost their productivity.
+
+
+ Start Your Free Trial
+
+
+
+ )
+}
+
diff --git a/web/app/components/DocSidebar.tsx b/web/app/components/DocSidebar.tsx
new file mode 100644
index 0000000..a91e41d
--- /dev/null
+++ b/web/app/components/DocSidebar.tsx
@@ -0,0 +1,36 @@
+import Link from "next/link"
+import { usePathname } from "next/navigation"
+
+const sidebarItems = [
+ { title: "Introduction", href: "/docs/introduction" },
+ { title: "Installation", href: "/docs/installation" },
+ { title: "Getting Started", href: "/docs/getting-started" },
+ { title: "Features", href: "/docs/features" },
+ { title: "API", href: "/docs/api" },
+ { title: "FAQ", href: "/docs/faq" },
+]
+
+export default function DocSidebar() {
+ const pathname = usePathname()
+
+ return (
+
+ Documentation
+
+ {sidebarItems.map((item) => (
+
+
+ {item.title}
+
+
+ ))}
+
+
+ )
+}
+
diff --git a/web/app/components/Features.tsx b/web/app/components/Features.tsx
new file mode 100644
index 0000000..f37498d
--- /dev/null
+++ b/web/app/components/Features.tsx
@@ -0,0 +1,44 @@
+import { CheckCircle, Zap, Users, TrendingUp } from "lucide-react"
+
+const features = [
+ {
+ icon: ,
+ title: "Task Management",
+ description: "Organize and prioritize tasks with ease.",
+ },
+ {
+ icon: ,
+ title: "Real-time Collaboration",
+ description: "Work together seamlessly in real-time.",
+ },
+ {
+ icon: ,
+ title: "Team Communication",
+ description: "Stay connected with built-in messaging.",
+ },
+ {
+ icon: ,
+ title: "Analytics Dashboard",
+ description: "Track progress and gain insights with powerful analytics.",
+ },
+]
+
+export default function Features() {
+ return (
+
+
+
Key Features
+
+ {features.map((feature, index) => (
+
+
{feature.icon}
+
{feature.title}
+
{feature.description}
+
+ ))}
+
+
+
+ )
+}
+
diff --git a/web/app/components/Footer.tsx b/web/app/components/Footer.tsx
new file mode 100644
index 0000000..3f74cc6
--- /dev/null
+++ b/web/app/components/Footer.tsx
@@ -0,0 +1,76 @@
+import Link from "next/link"
+import { Facebook, Twitter, Instagram, Linkedin } from "lucide-react"
+
+export default function Footer() {
+ return (
+
+ )
+}
+
diff --git a/web/app/components/Header.tsx b/web/app/components/Header.tsx
new file mode 100644
index 0000000..50327b6
--- /dev/null
+++ b/web/app/components/Header.tsx
@@ -0,0 +1,27 @@
+import Link from "next/link"
+import { Button } from "@/components/ui/button"
+
+export default function Header() {
+ return (
+
+ )
+}
+
diff --git a/web/app/components/Hero.tsx b/web/app/components/Hero.tsx
new file mode 100644
index 0000000..3ec585a
--- /dev/null
+++ b/web/app/components/Hero.tsx
@@ -0,0 +1,23 @@
+import { Button } from "@/components/ui/button"
+
+export default function Hero() {
+ return (
+
+
+
+ Everything App
+
+ for your teams
+
+
+ Huly, an open-source platform, serves as an all-in-one replacement of Linear, Jira, Slack, and Notion.
+
+
+ Try it free
+
+
+
+
+ )
+}
+
diff --git a/web/app/components/Navbar.tsx b/web/app/components/Navbar.tsx
new file mode 100644
index 0000000..d077eda
--- /dev/null
+++ b/web/app/components/Navbar.tsx
@@ -0,0 +1,47 @@
+import Link from "next/link"
+import { Button } from "@/components/ui/button"
+
+export default function Navbar() {
+ return (
+
+
+
+
+
+ StreamLine
+
+
+
+
+ Pricing
+
+
+ Docs
+
+
+ Resources
+
+
+ Community
+
+
+ Download
+
+
+
+
+
+
+ Sign In
+
+ Get Started
+
+
+
+
+ )
+}
+
diff --git a/web/app/components/Pricing.tsx b/web/app/components/Pricing.tsx
new file mode 100644
index 0000000..47bf609
--- /dev/null
+++ b/web/app/components/Pricing.tsx
@@ -0,0 +1,53 @@
+import { Check } from "lucide-react"
+import { Button } from "@/components/ui/button"
+
+const plans = [
+ {
+ name: "Basic",
+ price: "$9",
+ features: ["5 team members", "10 projects", "Basic analytics", "Email support"],
+ },
+ {
+ name: "Pro",
+ price: "$29",
+ features: ["Unlimited team members", "Unlimited projects", "Advanced analytics", "Priority support"],
+ },
+ {
+ name: "Enterprise",
+ price: "Custom",
+ features: ["Custom features", "Dedicated account manager", "On-premise deployment", "24/7 phone support"],
+ },
+]
+
+export default function Pricing() {
+ return (
+
+
+
Choose Your Plan
+
+ {plans.map((plan, index) => (
+
+
{plan.name}
+
+ {plan.price}
+ /month
+
+
+ {plan.features.map((feature, featureIndex) => (
+
+
+ {feature}
+
+ ))}
+
+
+ {index === 2 ? "Contact Sales" : "Get Started"}
+
+
+ ))}
+
+
+
+ )
+}
+
diff --git a/web/app/components/ProductPreview.tsx b/web/app/components/ProductPreview.tsx
new file mode 100644
index 0000000..b127e56
--- /dev/null
+++ b/web/app/components/ProductPreview.tsx
@@ -0,0 +1,21 @@
+import Image from "next/image"
+
+export default function ProductPreview() {
+ return (
+
+
+ {/* Glow effect */}
+
+
+ )
+}
+
diff --git a/web/app/components/Testimonials.tsx b/web/app/components/Testimonials.tsx
new file mode 100644
index 0000000..81fc6ac
--- /dev/null
+++ b/web/app/components/Testimonials.tsx
@@ -0,0 +1,37 @@
+const testimonials = [
+ {
+ quote: "StreamLine has revolutionized our team's workflow. It's a game-changer!",
+ author: "Jane Doe",
+ company: "Tech Innovators Inc.",
+ },
+ {
+ quote: "The best project management tool we've ever used. Highly recommended!",
+ author: "John Smith",
+ company: "Creative Solutions LLC",
+ },
+ {
+ quote: "StreamLine helped us increase productivity by 40%. It's incredible!",
+ author: "Emily Johnson",
+ company: "Startup Ventures",
+ },
+]
+
+export default function Testimonials() {
+ return (
+
+
+
What Our Customers Say
+
+ {testimonials.map((testimonial, index) => (
+
+
"{testimonial.quote}"
+
{testimonial.author}
+
{testimonial.company}
+
+ ))}
+
+
+
+ )
+}
+
diff --git a/web/app/components/ui/.DS_Store b/web/app/components/ui/.DS_Store
new file mode 100644
index 0000000..5008ddf
Binary files /dev/null and b/web/app/components/ui/.DS_Store differ
diff --git a/web/app/components/ui/button.tsx b/web/app/components/ui/button.tsx
new file mode 100644
index 0000000..36496a2
--- /dev/null
+++ b/web/app/components/ui/button.tsx
@@ -0,0 +1,56 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 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 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+ {
+ 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",
+ },
+ 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",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+ return (
+
+ )
+ }
+)
+Button.displayName = "Button"
+
+export { Button, buttonVariants }
diff --git a/web/app/components/ui/steps.tsx b/web/app/components/ui/steps.tsx
new file mode 100644
index 0000000..58c6ae9
--- /dev/null
+++ b/web/app/components/ui/steps.tsx
@@ -0,0 +1,35 @@
+import React from "react"
+
+interface StepProps {
+ title: string
+ children: React.ReactNode
+}
+
+const Step: React.FC = ({ title, children }) => (
+
+
{title}
+ {children}
+
+)
+
+interface StepsProps {
+ children: React.ReactNode
+}
+
+const Steps: React.FC & { Step: typeof Step } = ({ children }) => (
+
+ {React.Children.map(children, (child, index) => (
+
+
+ {index + 1}
+
+
{child}
+
+ ))}
+
+)
+
+Steps.Step = Step
+
+export { Steps }
+
diff --git a/web/app/docs/installation/page.tsx b/web/app/docs/installation/page.tsx
new file mode 100644
index 0000000..13d7cfe
--- /dev/null
+++ b/web/app/docs/installation/page.tsx
@@ -0,0 +1,33 @@
+export default function InstallationPage() {
+ return (
+
+
Installing ProxMenux
+
+
Installation
+
To install ProxMenux, simply run the following command in your Proxmox server terminal:
+
+
+ bash -c "$(wget -qLO - https://raw.githubusercontent.com/MacRimi/ProxMenux/main/install_proxmenux.sh)"
+
+
+
+
How to Use
+
+ Once installed, launch ProxMenux by running:
+
+
+ menu
+
+
+
Troubleshooting
+
+ If you encounter any issues during installation or usage, please check the{" "}
+
+ GitHub Issues
+ {" "}
+ page or open a new issue if your problem isn't already addressed.
+
+
+ )
+}
+
diff --git a/web/app/docs/introduction/page.tsx b/web/app/docs/introduction/page.tsx
new file mode 100644
index 0000000..20b5287
--- /dev/null
+++ b/web/app/docs/introduction/page.tsx
@@ -0,0 +1,25 @@
+export default function IntroductionPage() {
+ return (
+
+
Introduction to ProxMenux
+
+ ProxMenux is a tool designed to simplify and streamline the management of Proxmox VE through a menu-based interface. It allows users to execute shell scripts in an organized way, eliminating the need to manually enter complex commands.
+
+ It is designed for both experienced Proxmox VE administrators and less experienced users, providing a more accessible and efficient way to manage their infrastructure.
+
+
+
Key Features
+
+ Menu-based interface for easy script execution.
+ Organized categories for quick access to available functions.
+ Scripts hosted on GitHub, always accessible and up to date.
+ Automatic text translation using Google Translate.
+ Simplified Proxmox VE management, reducing the complexity of common tasks.
+
+
+ The following sections of this documentation provide instructions on how to install ProxMenux and detailed explanations of each available script.
+
+
+ )
+}
+
diff --git a/web/app/docs/layout.tsx b/web/app/docs/layout.tsx
new file mode 100644
index 0000000..df5e869
--- /dev/null
+++ b/web/app/docs/layout.tsx
@@ -0,0 +1,16 @@
+import type React from "react"
+import DocSidebar from "@/components/DocSidebar"
+import Footer from "@/components/footer"
+
+export default function DocsLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+ )
+}
+
diff --git a/web/app/globals.css b/web/app/globals.css
new file mode 100644
index 0000000..1186f27
--- /dev/null
+++ b/web/app/globals.css
@@ -0,0 +1,60 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ :root {
+ --background: 0 0% 100%;
+ --foreground: 240 10% 3.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 240 10% 3.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 240 10% 3.9%;
+ --primary: 240 5.9% 10%;
+ --primary-foreground: 0 0% 98%;
+ --secondary: 240 4.8% 95.9%;
+ --secondary-foreground: 240 5.9% 10%;
+ --muted: 240 4.8% 95.9%;
+ --muted-foreground: 240 3.8% 46.1%;
+ --accent: 240 4.8% 95.9%;
+ --accent-foreground: 240 5.9% 10%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 240 5.9% 90%;
+ --input: 240 5.9% 90%;
+ --ring: 240 5.9% 10%;
+ --radius: 0.5rem;
+ }
+
+ .dark {
+ --background: 240 10% 3.9%;
+ --foreground: 0 0% 98%;
+ --card: 240 10% 3.9%;
+ --card-foreground: 0 0% 98%;
+ --popover: 240 10% 3.9%;
+ --popover-foreground: 0 0% 98%;
+ --primary: 0 0% 98%;
+ --primary-foreground: 240 5.9% 10%;
+ --secondary: 240 3.7% 15.9%;
+ --secondary-foreground: 0 0% 98%;
+ --muted: 240 3.7% 15.9%;
+ --muted-foreground: 240 5% 64.9%;
+ --accent: 240 3.7% 15.9%;
+ --accent-foreground: 0 0% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 240 3.7% 15.9%;
+ --input: 240 3.7% 15.9%;
+ --ring: 240 4.9% 83.9%;
+ }
+}
+
+@layer base {
+ * {
+ @apply border-border;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+}
+
diff --git a/web/app/guides/[slug]/page.tsx b/web/app/guides/[slug]/page.tsx
new file mode 100644
index 0000000..6c90183
--- /dev/null
+++ b/web/app/guides/[slug]/page.tsx
@@ -0,0 +1,30 @@
+import fs from "fs"
+import path from "path"
+import { remark } from "remark"
+import html from "remark-html"
+
+async function getGuideContent(slug: string) {
+ const guidePath = path.join(process.cwd(), "guides", `${slug}.md`)
+ const fileContents = fs.readFileSync(guidePath, "utf8")
+
+ const result = await remark().use(html).process(fileContents)
+ return result.toString()
+}
+
+export async function generateStaticParams() {
+ const guideFiles = fs.readdirSync(path.join(process.cwd(), "guides"))
+ return guideFiles.map((file) => ({
+ slug: file.replace(/\.md$/, ""),
+ }))
+}
+
+export default async function GuidePage({ params }: { params: { slug: string } }) {
+ const guideContent = await getGuideContent(params.slug)
+
+ return (
+
+ )
+}
+
diff --git a/web/app/guides/page.tsx b/web/app/guides/page.tsx
new file mode 100644
index 0000000..3176df6
--- /dev/null
+++ b/web/app/guides/page.tsx
@@ -0,0 +1,44 @@
+import Link from "next/link"
+
+interface Guide {
+ title: string
+ description: string
+ slug: string
+}
+
+const guides: Guide[] = [
+ {
+ title: "Setting up NVIDIA Drivers on Proxmox VE with GPU Passthrough",
+ description:
+ "Learn how to install and configure NVIDIA drivers on your Proxmox VE host and enable GPU passthrough to your virtual machines.",
+ slug: "nvidia_proxmox",
+ },
+ {
+ title: "Ejemplo de Guía Adicional",
+ description: "Esta es una guía de ejemplo para mostrar cómo se manejan múltiples guías.",
+ slug: "example_guide",
+ },
+ // Añade más guías aquí según sea necesario
+]
+
+export default function GuidesPage() {
+ return (
+
+
ProxMenux Guides
+
Complementary guides to make the most of your Proxmox VE.
+
+ {guides.map((guide) => (
+
+
{guide.title}
+
{guide.description}
+
+ ))}
+
+
+ )
+}
+
diff --git a/web/app/layout.tsx b/web/app/layout.tsx
new file mode 100644
index 0000000..2d6a68b
--- /dev/null
+++ b/web/app/layout.tsx
@@ -0,0 +1,35 @@
+import "./globals.css"
+import { Inter } from "next/font/google"
+import type React from "react"
+import type { Metadata } from "next"
+import Navbar from "@/components/navbar"
+import MouseMoveEffect from "@/components/mouse-move-effect"
+
+const inter = Inter({ subsets: ["latin"] })
+
+export const metadata: Metadata = {
+ title: "ProxMenux - A menu-driven script for Proxmox VE management",
+ description:
+ "ProxMenux is a tool designed to execute shell scripts in an organized manner for Proxmox VE management.",
+ generator: 'v0.dev'
+}
+
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode
+}) {
+ return (
+
+
+
+
+ {children}
+
+
+ )
+}
+
+
+
+import './globals.css'
\ No newline at end of file
diff --git a/web/app/page.tsx b/web/app/page.tsx
new file mode 100644
index 0000000..67e5883
--- /dev/null
+++ b/web/app/page.tsx
@@ -0,0 +1,18 @@
+import Hero from "@/components/hero"
+import Resources from "@/components/resources"
+import SupportProject from "@/components/support-project"
+import Footer from "@/components/footer"
+
+export default function Home() {
+ return (
+
+ {" "}
+ {/* Added pt-16 for navbar space */}
+
+
+
+
+
+ )
+}
+
diff --git a/web/components.json b/web/components.json
new file mode 100644
index 0000000..d9ef0ae
--- /dev/null
+++ b/web/components.json
@@ -0,0 +1,21 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "default",
+ "rsc": true,
+ "tsx": true,
+ "tailwind": {
+ "config": "tailwind.config.ts",
+ "css": "app/globals.css",
+ "baseColor": "neutral",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ },
+ "iconLibrary": "lucide"
+}
\ No newline at end of file
diff --git a/web/hooks/use-mobile.tsx b/web/hooks/use-mobile.tsx
new file mode 100644
index 0000000..2b0fe1d
--- /dev/null
+++ b/web/hooks/use-mobile.tsx
@@ -0,0 +1,19 @@
+import * as React from "react"
+
+const MOBILE_BREAKPOINT = 768
+
+export function useIsMobile() {
+ const [isMobile, setIsMobile] = React.useState(undefined)
+
+ React.useEffect(() => {
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
+ const onChange = () => {
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
+ }
+ mql.addEventListener("change", onChange)
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
+ return () => mql.removeEventListener("change", onChange)
+ }, [])
+
+ return !!isMobile
+}
diff --git a/web/hooks/use-toast.ts b/web/hooks/use-toast.ts
new file mode 100644
index 0000000..02e111d
--- /dev/null
+++ b/web/hooks/use-toast.ts
@@ -0,0 +1,194 @@
+"use client"
+
+// Inspired by react-hot-toast library
+import * as React from "react"
+
+import type {
+ ToastActionElement,
+ ToastProps,
+} from "@/components/ui/toast"
+
+const TOAST_LIMIT = 1
+const TOAST_REMOVE_DELAY = 1000000
+
+type ToasterToast = ToastProps & {
+ id: string
+ title?: React.ReactNode
+ description?: React.ReactNode
+ action?: ToastActionElement
+}
+
+const actionTypes = {
+ ADD_TOAST: "ADD_TOAST",
+ UPDATE_TOAST: "UPDATE_TOAST",
+ DISMISS_TOAST: "DISMISS_TOAST",
+ REMOVE_TOAST: "REMOVE_TOAST",
+} as const
+
+let count = 0
+
+function genId() {
+ count = (count + 1) % Number.MAX_SAFE_INTEGER
+ return count.toString()
+}
+
+type ActionType = typeof actionTypes
+
+type Action =
+ | {
+ type: ActionType["ADD_TOAST"]
+ toast: ToasterToast
+ }
+ | {
+ type: ActionType["UPDATE_TOAST"]
+ toast: Partial
+ }
+ | {
+ type: ActionType["DISMISS_TOAST"]
+ toastId?: ToasterToast["id"]
+ }
+ | {
+ type: ActionType["REMOVE_TOAST"]
+ toastId?: ToasterToast["id"]
+ }
+
+interface State {
+ toasts: ToasterToast[]
+}
+
+const toastTimeouts = new Map>()
+
+const addToRemoveQueue = (toastId: string) => {
+ if (toastTimeouts.has(toastId)) {
+ return
+ }
+
+ const timeout = setTimeout(() => {
+ toastTimeouts.delete(toastId)
+ dispatch({
+ type: "REMOVE_TOAST",
+ toastId: toastId,
+ })
+ }, TOAST_REMOVE_DELAY)
+
+ toastTimeouts.set(toastId, timeout)
+}
+
+export const reducer = (state: State, action: Action): State => {
+ switch (action.type) {
+ case "ADD_TOAST":
+ return {
+ ...state,
+ toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
+ }
+
+ case "UPDATE_TOAST":
+ return {
+ ...state,
+ toasts: state.toasts.map((t) =>
+ t.id === action.toast.id ? { ...t, ...action.toast } : t
+ ),
+ }
+
+ case "DISMISS_TOAST": {
+ const { toastId } = action
+
+ // ! Side effects ! - This could be extracted into a dismissToast() action,
+ // but I'll keep it here for simplicity
+ if (toastId) {
+ addToRemoveQueue(toastId)
+ } else {
+ state.toasts.forEach((toast) => {
+ addToRemoveQueue(toast.id)
+ })
+ }
+
+ return {
+ ...state,
+ toasts: state.toasts.map((t) =>
+ t.id === toastId || toastId === undefined
+ ? {
+ ...t,
+ open: false,
+ }
+ : t
+ ),
+ }
+ }
+ case "REMOVE_TOAST":
+ if (action.toastId === undefined) {
+ return {
+ ...state,
+ toasts: [],
+ }
+ }
+ return {
+ ...state,
+ toasts: state.toasts.filter((t) => t.id !== action.toastId),
+ }
+ }
+}
+
+const listeners: Array<(state: State) => void> = []
+
+let memoryState: State = { toasts: [] }
+
+function dispatch(action: Action) {
+ memoryState = reducer(memoryState, action)
+ listeners.forEach((listener) => {
+ listener(memoryState)
+ })
+}
+
+type Toast = Omit
+
+function toast({ ...props }: Toast) {
+ const id = genId()
+
+ const update = (props: ToasterToast) =>
+ dispatch({
+ type: "UPDATE_TOAST",
+ toast: { ...props, id },
+ })
+ const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
+
+ dispatch({
+ type: "ADD_TOAST",
+ toast: {
+ ...props,
+ id,
+ open: true,
+ onOpenChange: (open) => {
+ if (!open) dismiss()
+ },
+ },
+ })
+
+ return {
+ id: id,
+ dismiss,
+ update,
+ }
+}
+
+function useToast() {
+ const [state, setState] = React.useState(memoryState)
+
+ React.useEffect(() => {
+ listeners.push(setState)
+ return () => {
+ const index = listeners.indexOf(setState)
+ if (index > -1) {
+ listeners.splice(index, 1)
+ }
+ }
+ }, [state])
+
+ return {
+ ...state,
+ toast,
+ dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
+ }
+}
+
+export { useToast, toast }
diff --git a/web/lib/utils.ts b/web/lib/utils.ts
new file mode 100644
index 0000000..bd0c391
--- /dev/null
+++ b/web/lib/utils.ts
@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from "clsx"
+import { twMerge } from "tailwind-merge"
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
+}
diff --git a/web/next.config.js b/web/next.config.js
index f5d4549..e272422 100644
--- a/web/next.config.js
+++ b/web/next.config.js
@@ -7,15 +7,7 @@ const nextConfig = {
assetPrefix: "/ProxMenux/",
basePath: "/ProxMenux",
distDir: "out",
- webpack: (config, { isServer }) => {
- if (!isServer) {
- config.resolve.fallback = {
- ...config.resolve.fallback,
- fs: false,
- }
- }
- return config
- },
}
module.exports = nextConfig
+
diff --git a/web/package.json b/web/package.json
new file mode 100644
index 0000000..a1965e1
--- /dev/null
+++ b/web/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "proxmenux-docs",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "lint": "next lint"
+ },
+ "dependencies": {
+ "next": "13.4.7",
+ "react": "18.2.0",
+ "react-dom": "18.2.0",
+ "remark": "^14.0.3",
+ "remark-html": "^15.0.2",
+ "@radix-ui/react-slot": "^1.0.2",
+ "class-variance-authority": "^0.7.0",
+ "clsx": "^2.0.0",
+ "lucide-react": "^0.284.0",
+ "tailwind-merge": "^1.14.0",
+ "tailwindcss-animate": "^1.0.7"
+ },
+ "devDependencies": {
+ "@types/node": "^20.8.4",
+ "@types/react": "^18.2.27",
+ "autoprefixer": "^10.4.16",
+ "postcss": "^8.4.31",
+ "tailwindcss": "^3.3.3",
+ "typescript": "^5.2.2"
+ }
+}
+
diff --git a/web/public/placeholder-logo.png b/web/public/placeholder-logo.png
new file mode 100644
index 0000000..6528839
Binary files /dev/null and b/web/public/placeholder-logo.png differ
diff --git a/web/public/placeholder-logo.svg b/web/public/placeholder-logo.svg
new file mode 100644
index 0000000..b1695aa
--- /dev/null
+++ b/web/public/placeholder-logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/public/placeholder-user.jpg b/web/public/placeholder-user.jpg
new file mode 100644
index 0000000..6faa819
Binary files /dev/null and b/web/public/placeholder-user.jpg differ
diff --git a/web/public/placeholder.jpg b/web/public/placeholder.jpg
new file mode 100644
index 0000000..a6bf2ee
Binary files /dev/null and b/web/public/placeholder.jpg differ
diff --git a/web/public/placeholder.svg b/web/public/placeholder.svg
new file mode 100644
index 0000000..e763910
--- /dev/null
+++ b/web/public/placeholder.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/styles/globals.css b/web/styles/globals.css
new file mode 100644
index 0000000..ac68442
--- /dev/null
+++ b/web/styles/globals.css
@@ -0,0 +1,94 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+body {
+ font-family: Arial, Helvetica, sans-serif;
+}
+
+@layer utilities {
+ .text-balance {
+ text-wrap: balance;
+ }
+}
+
+@layer base {
+ :root {
+ --background: 0 0% 100%;
+ --foreground: 0 0% 3.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 0 0% 3.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 0 0% 3.9%;
+ --primary: 0 0% 9%;
+ --primary-foreground: 0 0% 98%;
+ --secondary: 0 0% 96.1%;
+ --secondary-foreground: 0 0% 9%;
+ --muted: 0 0% 96.1%;
+ --muted-foreground: 0 0% 45.1%;
+ --accent: 0 0% 96.1%;
+ --accent-foreground: 0 0% 9%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 0 0% 89.8%;
+ --input: 0 0% 89.8%;
+ --ring: 0 0% 3.9%;
+ --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%;
+ --radius: 0.5rem;
+ --sidebar-background: 0 0% 98%;
+ --sidebar-foreground: 240 5.3% 26.1%;
+ --sidebar-primary: 240 5.9% 10%;
+ --sidebar-primary-foreground: 0 0% 98%;
+ --sidebar-accent: 240 4.8% 95.9%;
+ --sidebar-accent-foreground: 240 5.9% 10%;
+ --sidebar-border: 220 13% 91%;
+ --sidebar-ring: 217.2 91.2% 59.8%;
+ }
+ .dark {
+ --background: 0 0% 3.9%;
+ --foreground: 0 0% 98%;
+ --card: 0 0% 3.9%;
+ --card-foreground: 0 0% 98%;
+ --popover: 0 0% 3.9%;
+ --popover-foreground: 0 0% 98%;
+ --primary: 0 0% 98%;
+ --primary-foreground: 0 0% 9%;
+ --secondary: 0 0% 14.9%;
+ --secondary-foreground: 0 0% 98%;
+ --muted: 0 0% 14.9%;
+ --muted-foreground: 0 0% 63.9%;
+ --accent: 0 0% 14.9%;
+ --accent-foreground: 0 0% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 0 0% 14.9%;
+ --input: 0 0% 14.9%;
+ --ring: 0 0% 83.1%;
+ --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%;
+ --sidebar-background: 240 5.9% 10%;
+ --sidebar-foreground: 240 4.8% 95.9%;
+ --sidebar-primary: 224.3 76.3% 48%;
+ --sidebar-primary-foreground: 0 0% 100%;
+ --sidebar-accent: 240 3.7% 15.9%;
+ --sidebar-accent-foreground: 240 4.8% 95.9%;
+ --sidebar-border: 240 3.7% 15.9%;
+ --sidebar-ring: 217.2 91.2% 59.8%;
+ }
+}
+
+@layer base {
+ * {
+ @apply border-border;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+}
diff --git a/web/tailwind.config.js b/web/tailwind.config.js
new file mode 100644
index 0000000..2ae83ea
--- /dev/null
+++ b/web/tailwind.config.js
@@ -0,0 +1,72 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ darkMode: ["class"],
+ content: ["./app/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}", "./pages/**/*.{ts,tsx}"],
+ 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 },
+ },
+ },
+ animation: {
+ "accordion-down": "accordion-down 0.2s ease-out",
+ "accordion-up": "accordion-up 0.2s ease-out",
+ },
+ },
+ },
+ plugins: [require("tailwindcss-animate")],
+}
+
diff --git a/web/tsconfig.json b/web/tsconfig.json
new file mode 100644
index 0000000..364f802
--- /dev/null
+++ b/web/tsconfig.json
@@ -0,0 +1,29 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./*"]
+ }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "exclude": ["node_modules"]
+}
+