start web

This commit is contained in:
MacRimi 2025-02-13 17:28:49 +01:00
parent a28bc19a38
commit 6a44aa0153
37 changed files with 1221 additions and 9 deletions

16
.gitignore vendored Normal file
View File

@ -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

BIN
web/app/components/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,18 @@
import { Button } from "@/components/ui/button"
export default function CTA() {
return (
<section className="py-20 bg-primary text-white">
<div className="container mx-auto text-center">
<h2 className="text-3xl font-bold mb-6">Ready to Streamline Your Workflow?</h2>
<p className="text-xl mb-8 max-w-2xl mx-auto">
Join thousands of teams already using StreamLine to boost their productivity.
</p>
<Button size="lg" variant="secondary">
Start Your Free Trial
</Button>
</div>
</section>
)
}

View File

@ -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 (
<nav className="w-64 bg-gray-100 p-6">
<h2 className="text-lg font-semibold mb-4">Documentation</h2>
<ul className="space-y-2">
{sidebarItems.map((item) => (
<li key={item.href}>
<Link
href={item.href}
className={`block p-2 rounded ${
pathname === item.href ? "bg-blue-500 text-white" : "text-gray-700 hover:bg-gray-200"
}`}
>
{item.title}
</Link>
</li>
))}
</ul>
</nav>
)
}

View File

@ -0,0 +1,44 @@
import { CheckCircle, Zap, Users, TrendingUp } from "lucide-react"
const features = [
{
icon: <CheckCircle className="h-8 w-8 text-primary" />,
title: "Task Management",
description: "Organize and prioritize tasks with ease.",
},
{
icon: <Zap className="h-8 w-8 text-primary" />,
title: "Real-time Collaboration",
description: "Work together seamlessly in real-time.",
},
{
icon: <Users className="h-8 w-8 text-primary" />,
title: "Team Communication",
description: "Stay connected with built-in messaging.",
},
{
icon: <TrendingUp className="h-8 w-8 text-primary" />,
title: "Analytics Dashboard",
description: "Track progress and gain insights with powerful analytics.",
},
]
export default function Features() {
return (
<section id="features" className="py-20 bg-gray-50">
<div className="container mx-auto">
<h2 className="text-3xl font-bold text-center mb-12">Key Features</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
{features.map((feature, index) => (
<div key={index} className="bg-white p-6 rounded-lg shadow-md">
<div className="mb-4">{feature.icon}</div>
<h3 className="text-xl font-semibold mb-2">{feature.title}</h3>
<p className="text-gray-600">{feature.description}</p>
</div>
))}
</div>
</div>
</section>
)
}

View File

@ -0,0 +1,76 @@
import Link from "next/link"
import { Facebook, Twitter, Instagram, Linkedin } from "lucide-react"
export default function Footer() {
return (
<footer className="bg-gray-900 text-white py-12">
<div className="container mx-auto grid grid-cols-1 md:grid-cols-4 gap-8">
<div>
<h3 className="text-lg font-semibold mb-4">StreamLine</h3>
<p className="text-gray-400">Streamlining your workflow, one task at a time.</p>
</div>
<div>
<h4 className="text-lg font-semibold mb-4">Product</h4>
<ul className="space-y-2">
<li>
<Link href="#features" className="text-gray-400 hover:text-white">
Features
</Link>
</li>
<li>
<Link href="#pricing" className="text-gray-400 hover:text-white">
Pricing
</Link>
</li>
<li>
<Link href="#" className="text-gray-400 hover:text-white">
Integrations
</Link>
</li>
</ul>
</div>
<div>
<h4 className="text-lg font-semibold mb-4">Company</h4>
<ul className="space-y-2">
<li>
<Link href="#" className="text-gray-400 hover:text-white">
About Us
</Link>
</li>
<li>
<Link href="#" className="text-gray-400 hover:text-white">
Careers
</Link>
</li>
<li>
<Link href="#" className="text-gray-400 hover:text-white">
Contact
</Link>
</li>
</ul>
</div>
<div>
<h4 className="text-lg font-semibold mb-4">Connect</h4>
<div className="flex space-x-4">
<Link href="#" className="text-gray-400 hover:text-white">
<Facebook className="h-6 w-6" />
</Link>
<Link href="#" className="text-gray-400 hover:text-white">
<Twitter className="h-6 w-6" />
</Link>
<Link href="#" className="text-gray-400 hover:text-white">
<Instagram className="h-6 w-6" />
</Link>
<Link href="#" className="text-gray-400 hover:text-white">
<Linkedin className="h-6 w-6" />
</Link>
</div>
</div>
</div>
<div className="container mx-auto mt-8 pt-8 border-t border-gray-800 text-center text-gray-400">
<p>&copy; 2025 StreamLine. All rights reserved.</p>
</div>
</footer>
)
}

View File

@ -0,0 +1,27 @@
import Link from "next/link"
import { Button } from "@/components/ui/button"
export default function Header() {
return (
<header className="py-4 px-6 bg-white shadow-sm">
<div className="container mx-auto flex justify-between items-center">
<Link href="/" className="text-2xl font-bold text-primary">
StreamLine
</Link>
<nav className="hidden md:flex space-x-6">
<Link href="#features" className="text-gray-600 hover:text-primary">
Features
</Link>
<Link href="#testimonials" className="text-gray-600 hover:text-primary">
Testimonials
</Link>
<Link href="#pricing" className="text-gray-600 hover:text-primary">
Pricing
</Link>
</nav>
<Button>Get Started</Button>
</div>
</header>
)
}

View File

@ -0,0 +1,23 @@
import { Button } from "@/components/ui/button"
export default function Hero() {
return (
<div className="relative pt-32 pb-20 sm:pt-40 sm:pb-24">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h1 className="text-4xl sm:text-6xl lg:text-7xl font-bold tracking-tight mb-8">
Everything App
<br />
<span className="text-gray-400">for your teams</span>
</h1>
<p className="max-w-2xl mx-auto text-lg sm:text-xl text-gray-400 mb-10">
Huly, an open-source platform, serves as an all-in-one replacement of Linear, Jira, Slack, and Notion.
</p>
<Button className="relative group px-8 py-6 text-lg bg-gradient-to-r from-primary to-accent hover:opacity-90">
<span className="relative z-10">Try it free</span>
<div className="absolute inset-0 bg-white/20 blur-lg group-hover:blur-xl transition-all duration-300 opacity-0 group-hover:opacity-100" />
</Button>
</div>
</div>
)
}

View File

@ -0,0 +1,47 @@
import Link from "next/link"
import { Button } from "@/components/ui/button"
export default function Navbar() {
return (
<nav className="fixed top-0 left-0 right-0 z-50 bg-background/80 backdrop-blur-md border-b border-white/10">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
<div className="flex items-center">
<Link
href="/"
className="text-2xl font-bold bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent"
>
StreamLine
</Link>
<div className="hidden md:block ml-10">
<div className="flex items-center space-x-8">
<Link href="#" className="text-sm text-gray-300 hover:text-white">
Pricing
</Link>
<Link href="/docs" className="text-sm text-gray-300 hover:text-white">
Docs
</Link>
<Link href="#" className="text-sm text-gray-300 hover:text-white">
Resources
</Link>
<Link href="#" className="text-sm text-gray-300 hover:text-white">
Community
</Link>
<Link href="#" className="text-sm text-gray-300 hover:text-white">
Download
</Link>
</div>
</div>
</div>
<div className="flex items-center space-x-4">
<Button variant="ghost" className="text-sm">
Sign In
</Button>
<Button className="text-sm bg-gradient-to-r from-primary to-accent hover:opacity-90">Get Started</Button>
</div>
</div>
</div>
</nav>
)
}

View File

@ -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 (
<section id="pricing" className="py-20 bg-gray-50">
<div className="container mx-auto">
<h2 className="text-3xl font-bold text-center mb-12">Choose Your Plan</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{plans.map((plan, index) => (
<div key={index} className="bg-white p-8 rounded-lg shadow-md">
<h3 className="text-2xl font-bold mb-4">{plan.name}</h3>
<p className="text-4xl font-bold mb-6">
{plan.price}
<span className="text-lg font-normal text-gray-600">/month</span>
</p>
<ul className="mb-8">
{plan.features.map((feature, featureIndex) => (
<li key={featureIndex} className="flex items-center mb-2">
<Check className="h-5 w-5 text-primary mr-2" />
<span>{feature}</span>
</li>
))}
</ul>
<Button className="w-full" variant={index === 1 ? "default" : "outline"}>
{index === 2 ? "Contact Sales" : "Get Started"}
</Button>
</div>
))}
</div>
</div>
</section>
)
}

View File

@ -0,0 +1,21 @@
import Image from "next/image"
export default function ProductPreview() {
return (
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-20">
<div className="relative rounded-lg overflow-hidden border border-white/10 shadow-2xl">
<div className="absolute inset-0 bg-gradient-to-tr from-primary/10 via-accent/10 to-transparent" />
<Image
src="https://sjc.microlink.io/dACoBD81V0jhbU_TaUYiRrOVrhAXOh8TCYVdXmvVaYFIpbvF9B17bU0pnQF3gHfzVAOFzC-nwZVACScUpFYQsg.jpeg"
alt="Huly App Interface"
width={1200}
height={800}
className="w-full h-auto"
/>
</div>
{/* Glow effect */}
<div className="absolute -inset-x-20 top-0 h-[500px] bg-gradient-conic opacity-30 blur-3xl" />
</div>
)
}

View File

@ -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 (
<section id="testimonials" className="py-20 bg-white">
<div className="container mx-auto">
<h2 className="text-3xl font-bold text-center mb-12">What Our Customers Say</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{testimonials.map((testimonial, index) => (
<div key={index} className="bg-gray-50 p-6 rounded-lg">
<p className="text-lg mb-4">"{testimonial.quote}"</p>
<p className="font-semibold">{testimonial.author}</p>
<p className="text-sm text-gray-600">{testimonial.company}</p>
</div>
))}
</div>
</div>
</section>
)
}

BIN
web/app/components/ui/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -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<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@ -0,0 +1,35 @@
import React from "react"
interface StepProps {
title: string
children: React.ReactNode
}
const Step: React.FC<StepProps> = ({ title, children }) => (
<div className="mb-8">
<h3 className="text-xl font-semibold mb-2 text-gray-900">{title}</h3>
{children}
</div>
)
interface StepsProps {
children: React.ReactNode
}
const Steps: React.FC<StepsProps> & { Step: typeof Step } = ({ children }) => (
<div className="space-y-4">
{React.Children.map(children, (child, index) => (
<div className="flex items-start">
<div className="flex-shrink-0 w-8 h-8 bg-blue-500 text-white rounded-full flex items-center justify-center mr-4 mt-1">
{index + 1}
</div>
<div className="flex-grow">{child}</div>
</div>
))}
</div>
)
Steps.Step = Step
export { Steps }

View File

@ -0,0 +1,33 @@
export default function InstallationPage() {
return (
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-8 text-gray-900">
<h1 className="text-3xl sm:text-4xl font-bold mb-6">Installing ProxMenux</h1>
<h2 className="text-2xl font-semibold mt-8 mb-4">Installation</h2>
<p className="mb-4">To install ProxMenux, simply run the following command in your Proxmox server terminal:</p>
<pre className="bg-gray-100 p-4 rounded-md overflow-x-auto text-sm">
<code>
bash -c "$(wget -qLO - https://raw.githubusercontent.com/MacRimi/ProxMenux/main/install_proxmenux.sh)"
</code>
</pre>
<h2 className="text-2xl font-semibold mt-8 mb-4">How to Use</h2>
<p className="mb-4">
Once installed, launch <strong>ProxMenux</strong> by running:
</p>
<pre className="bg-gray-100 p-4 rounded-md overflow-x-auto text-sm">
<code>menu</code>
</pre>
<h2 className="text-2xl font-semibold mt-8 mb-4">Troubleshooting</h2>
<p className="mb-4">
If you encounter any issues during installation or usage, please check the{" "}
<a href="https://github.com/MacRimi/ProxMenux/issues" className="text-blue-600 hover:underline">
GitHub Issues
</a>{" "}
page or open a new issue if your problem isn't already addressed.
</p>
</div>
)
}

View File

@ -0,0 +1,25 @@
export default function IntroductionPage() {
return (
<div className="max-w-3xl mx-auto">
<h1 className="text-3xl font-bold mb-6">Introduction to ProxMenux</h1>
<p className="mb-4">
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.
</p>
<h2 className="text-2xl font-semibold mt-8 mb-4">Key Features</h2>
<ul className="list-disc pl-6 space-y-2">
<li>Menu-based interface for easy script execution.</li>
<li>Organized categories for quick access to available functions.</li>
<li>Scripts hosted on GitHub, always accessible and up to date.</li>
<li>Automatic text translation using Google Translate.</li>
<li>Simplified Proxmox VE management, reducing the complexity of common tasks.</li>
</ul>
<p className="mt-6">
The following sections of this documentation provide instructions on how to install ProxMenux and detailed explanations of each available script.
</p>
</div>
)
}

16
web/app/docs/layout.tsx Normal file
View File

@ -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 (
<div className="flex flex-col min-h-screen bg-white text-gray-900">
<div className="flex flex-col md:flex-row flex-1">
<DocSidebar />
<main className="flex-1 p-4 md:p-6">{children}</main>
</div>
<Footer />
</div>
)
}

60
web/app/globals.css Normal file
View File

@ -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;
}
}

View File

@ -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 (
<div className="container mx-auto px-4 py-16 max-w-3xl">
<div className="prose prose-lg dark:prose-invert" dangerouslySetInnerHTML={{ __html: guideContent }} />
</div>
)
}

44
web/app/guides/page.tsx Normal file
View File

@ -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 (
<div className="container mx-auto px-4 py-16">
<h1 className="text-4xl font-bold mb-8">ProxMenux Guides</h1>
<p className="text-xl mb-8">Complementary guides to make the most of your Proxmox VE.</p>
<div className="grid md:grid-cols-2 gap-6">
{guides.map((guide) => (
<Link
key={guide.slug}
href={`/guides/${guide.slug}`}
className="block p-6 bg-white rounded-lg shadow-md hover:shadow-lg transition-shadow"
>
<h2 className="text-2xl font-semibold mb-2 text-gray-900">{guide.title}</h2>
<p className="text-gray-600">{guide.description}</p>
</Link>
))}
</div>
</div>
)
}

35
web/app/layout.tsx Normal file
View File

@ -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 (
<html lang="en" className="dark">
<body className={`${inter.className} bg-background text-foreground antialiased`}>
<Navbar />
<MouseMoveEffect />
<div className="pt-16 md:pt-16">{children}</div>
</body>
</html>
)
}
import './globals.css'

18
web/app/page.tsx Normal file
View File

@ -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 (
<div className="min-h-screen bg-gradient-to-b from-gray-900 to-gray-800 text-white pt-16">
{" "}
{/* Added pt-16 for navbar space */}
<Hero />
<Resources />
<SupportProject />
<Footer />
</div>
)
}

21
web/components.json Normal file
View File

@ -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"
}

19
web/hooks/use-mobile.tsx Normal file
View File

@ -0,0 +1,19 @@
import * as React from "react"
const MOBILE_BREAKPOINT = 768
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(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
}

194
web/hooks/use-toast.ts Normal file
View File

@ -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<ToasterToast>
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
interface State {
toasts: ToasterToast[]
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
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<ToasterToast, "id">
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<State>(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 }

6
web/lib/utils.ts Normal file
View 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))
}

View File

@ -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

33
web/package.json Normal file
View File

@ -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"
}
}

Binary file not shown.

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="215" height="48" fill="none"><path fill="#000" d="M57.588 9.6h6L73.828 38h-5.2l-2.36-6.88h-11.36L52.548 38h-5.2l10.24-28.4Zm7.16 17.16-4.16-12.16-4.16 12.16h8.32Zm23.694-2.24c-.186-1.307-.706-2.32-1.56-3.04-.853-.72-1.866-1.08-3.04-1.08-1.68 0-2.986.613-3.92 1.84-.906 1.227-1.36 2.947-1.36 5.16s.454 3.933 1.36 5.16c.934 1.227 2.24 1.84 3.92 1.84 1.254 0 2.307-.373 3.16-1.12.854-.773 1.387-1.867 1.6-3.28l5.12.24c-.186 1.68-.733 3.147-1.64 4.4-.906 1.227-2.08 2.173-3.52 2.84-1.413.667-2.986 1-4.72 1-2.08 0-3.906-.453-5.48-1.36-1.546-.907-2.76-2.2-3.64-3.88-.853-1.68-1.28-3.627-1.28-5.84 0-2.24.427-4.187 1.28-5.84.88-1.68 2.094-2.973 3.64-3.88 1.574-.907 3.4-1.36 5.48-1.36 1.68 0 3.227.32 4.64.96 1.414.64 2.56 1.56 3.44 2.76.907 1.2 1.454 2.6 1.64 4.2l-5.12.28Zm11.486-7.72.12 3.4c.534-1.227 1.307-2.173 2.32-2.84 1.04-.693 2.267-1.04 3.68-1.04 1.494 0 2.76.387 3.8 1.16 1.067.747 1.827 1.813 2.28 3.2.507-1.44 1.294-2.52 2.36-3.24 1.094-.747 2.414-1.12 3.96-1.12 1.414 0 2.64.307 3.68.92s1.84 1.52 2.4 2.72c.56 1.2.84 2.667.84 4.4V38h-4.96V25.92c0-1.813-.293-3.187-.88-4.12-.56-.96-1.413-1.44-2.56-1.44-.906 0-1.68.213-2.32.64-.64.427-1.133 1.053-1.48 1.88-.32.827-.48 1.84-.48 3.04V38h-4.56V25.92c0-1.2-.133-2.213-.4-3.04-.24-.827-.626-1.453-1.16-1.88-.506-.427-1.133-.64-1.88-.64-.906 0-1.68.227-2.32.68-.64.427-1.133 1.053-1.48 1.88-.32.827-.48 1.827-.48 3V38h-4.96V16.8h4.48Zm26.723 10.6c0-2.24.427-4.187 1.28-5.84.854-1.68 2.067-2.973 3.64-3.88 1.574-.907 3.4-1.36 5.48-1.36 1.84 0 3.494.413 4.96 1.24 1.467.827 2.64 2.08 3.52 3.76.88 1.653 1.347 3.693 1.4 6.12v1.32h-15.08c.107 1.813.614 3.227 1.52 4.24.907.987 2.134 1.48 3.68 1.48.987 0 1.88-.253 2.68-.76a4.803 4.803 0 0 0 1.84-2.2l5.08.36c-.64 2.027-1.84 3.64-3.6 4.84-1.733 1.173-3.733 1.76-6 1.76-2.08 0-3.906-.453-5.48-1.36-1.573-.907-2.786-2.2-3.64-3.88-.853-1.68-1.28-3.627-1.28-5.84Zm15.16-2.04c-.213-1.733-.76-3.013-1.64-3.84-.853-.827-1.893-1.24-3.12-1.24-1.44 0-2.6.453-3.48 1.36-.88.88-1.44 2.12-1.68 3.72h9.92ZM163.139 9.6V38h-5.04V9.6h5.04Zm8.322 7.2.24 5.88-.64-.36c.32-2.053 1.094-3.56 2.32-4.52 1.254-.987 2.787-1.48 4.6-1.48 2.32 0 4.107.733 5.36 2.2 1.254 1.44 1.88 3.387 1.88 5.84V38h-4.96V25.92c0-1.253-.12-2.28-.36-3.08-.24-.8-.64-1.413-1.2-1.84-.533-.427-1.253-.64-2.16-.64-1.44 0-2.573.48-3.4 1.44-.8.933-1.2 2.307-1.2 4.12V38h-4.96V16.8h4.48Zm30.003 7.72c-.186-1.307-.706-2.32-1.56-3.04-.853-.72-1.866-1.08-3.04-1.08-1.68 0-2.986.613-3.92 1.84-.906 1.227-1.36 2.947-1.36 5.16s.454 3.933 1.36 5.16c.934 1.227 2.24 1.84 3.92 1.84 1.254 0 2.307-.373 3.16-1.12.854-.773 1.387-1.867 1.6-3.28l5.12.24c-.186 1.68-.733 3.147-1.64 4.4-.906 1.227-2.08 2.173-3.52 2.84-1.413.667-2.986 1-4.72 1-2.08 0-3.906-.453-5.48-1.36-1.546-.907-2.76-2.2-3.64-3.88-.853-1.68-1.28-3.627-1.28-5.84 0-2.24.427-4.187 1.28-5.84.88-1.68 2.094-2.973 3.64-3.88 1.574-.907 3.4-1.36 5.48-1.36 1.68 0 3.227.32 4.64.96 1.414.64 2.56 1.56 3.44 2.76.907 1.2 1.454 2.6 1.64 4.2l-5.12.28Zm11.443 8.16V38h-5.6v-5.32h5.6Z"/><path fill="#171717" fill-rule="evenodd" d="m7.839 40.783 16.03-28.054L20 6 0 40.783h7.839Zm8.214 0H40L27.99 19.894l-4.02 7.032 3.976 6.914H20.02l-3.967 6.943Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

BIN
web/public/placeholder.jpg Normal file

Binary file not shown.

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1200" fill="none"><rect width="1200" height="1200" fill="#EAEAEA" rx="3"/><g opacity=".5"><g opacity=".5"><path fill="#FAFAFA" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/></g><path stroke="url(#a)" stroke-width="2.418" d="M0-1.209h553.581" transform="scale(1 -1) rotate(45 1163.11 91.165)"/><path stroke="url(#b)" stroke-width="2.418" d="M404.846 598.671h391.726"/><path stroke="url(#c)" stroke-width="2.418" d="M599.5 795.742V404.017"/><path stroke="url(#d)" stroke-width="2.418" d="m795.717 796.597-391.441-391.44"/><path fill="#fff" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/><g clip-path="url(#e)"><path fill="#666" fill-rule="evenodd" d="M616.426 586.58h-31.434v16.176l3.553-3.554.531-.531h9.068l.074-.074 8.463-8.463h2.565l7.18 7.181V586.58Zm-15.715 14.654 3.698 3.699 1.283 1.282-2.565 2.565-1.282-1.283-5.2-5.199h-6.066l-5.514 5.514-.073.073v2.876a2.418 2.418 0 0 0 2.418 2.418h26.598a2.418 2.418 0 0 0 2.418-2.418v-8.317l-8.463-8.463-7.181 7.181-.071.072Zm-19.347 5.442v4.085a6.045 6.045 0 0 0 6.046 6.045h26.598a6.044 6.044 0 0 0 6.045-6.045v-7.108l1.356-1.355-1.282-1.283-.074-.073v-17.989h-38.689v23.43l-.146.146.146.147Z" clip-rule="evenodd"/></g><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/></g><defs><linearGradient id="a" x1="554.061" x2="-.48" y1=".083" y2=".087" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="b" x1="796.912" x2="404.507" y1="599.963" y2="599.965" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="c" x1="600.792" x2="600.794" y1="403.677" y2="796.082" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="d" x1="404.85" x2="796.972" y1="403.903" y2="796.02" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><clipPath id="e"><path fill="#fff" d="M581.364 580.535h38.689v38.689h-38.689z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

94
web/styles/globals.css Normal file
View File

@ -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;
}
}

72
web/tailwind.config.js Normal file
View File

@ -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")],
}

29
web/tsconfig.json Normal file
View File

@ -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"]
}