Add landing page for voicebox.sh

- Created a new landing page using Next.js 16, featuring a modern design with Tailwind CSS.
- Implemented essential components including Header, Footer, and DownloadSection for user navigation and download options.
- Configured Tailwind CSS and PostCSS for styling, and set up TypeScript for type safety.
- Added a README with setup instructions, project structure, and deployment guidelines.
- Integrated GitHub links for easy access to the repository and releases.
- Established a consistent UI with reusable components and responsive design.
This commit is contained in:
Jamie Pine
2026-01-25 13:40:18 -08:00
parent f80f870e93
commit d3ca3a9deb
28 changed files with 1177 additions and 2 deletions
@@ -0,0 +1,65 @@
'use client';
import { Download, Laptop, Monitor, Terminal } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { DOWNLOAD_LINKS, LATEST_VERSION } from '@/lib/constants';
export function DownloadSection() {
const downloads = [
{
platform: 'Mac',
icon: Laptop,
link: DOWNLOAD_LINKS.mac,
description: 'macOS (Intel + Apple Silicon)',
},
{
platform: 'Windows',
icon: Monitor,
link: DOWNLOAD_LINKS.windows,
description: 'Windows x64',
},
{
platform: 'Linux',
icon: Terminal,
link: DOWNLOAD_LINKS.linux,
description: 'Linux AppImage',
},
];
return (
<div className="space-y-6">
<div className="text-center">
<p className="text-sm text-muted-foreground mb-2">Latest Version</p>
<p className="text-2xl font-bold">{LATEST_VERSION}</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 sm:gap-6">
{downloads.map(({ platform, icon: Icon, link, description }) => (
<Card key={platform} className="hover:border-primary/50 transition-colors">
<CardContent className="p-6">
<div className="flex flex-col items-center text-center space-y-4">
<div className="p-3 rounded-lg bg-muted">
<Icon className="h-8 w-8" />
</div>
<div>
<h3 className="text-lg font-semibold mb-1">{platform}</h3>
<p className="text-sm text-muted-foreground">{description}</p>
</div>
<Button
asChild
size="lg"
className="w-full"
>
<a href={link} download>
<Download className="h-4 w-4 mr-2" />
Download
</a>
</Button>
</div>
</CardContent>
</Card>
))}
</div>
</div>
);
}
+64
View File
@@ -0,0 +1,64 @@
import Link from 'next/link';
import { Separator } from '@/components/ui/separator';
import { GITHUB_REPO } from '@/lib/constants';
export function Footer() {
return (
<footer className="border-t border-border pt-8 sm:pt-12 pb-6 sm:pb-8 mt-12 sm:mt-16 md:mt-20">
<div className="container mx-auto px-4">
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6 sm:gap-8 mb-6 sm:mb-8">
<div>
<h3 className="font-bold text-lg mb-4">voicebox</h3>
<p className="text-muted-foreground text-sm">
Professional voice cloning powered by Qwen3-TTS. Desktop app for Mac, Windows, and Linux.
</p>
</div>
<div>
<h4 className="font-semibold mb-3">Product</h4>
<ul className="space-y-2 text-muted-foreground text-sm">
<li>
<a href="#features" className="hover:text-foreground transition-colors">
Features
</a>
</li>
<li>
<a href="#download" className="hover:text-foreground transition-colors">
Download
</a>
</li>
<li>
<Link href={GITHUB_REPO} target="_blank" rel="noopener noreferrer" className="hover:text-foreground transition-colors">
GitHub
</Link>
</li>
</ul>
</div>
<div>
<h4 className="font-semibold mb-3">Resources</h4>
<ul className="space-y-2 text-muted-foreground text-sm">
<li>
<Link href={GITHUB_REPO} target="_blank" rel="noopener noreferrer" className="hover:text-foreground transition-colors">
Source Code
</Link>
</li>
<li>
<Link href={`${GITHUB_REPO}/releases`} target="_blank" rel="noopener noreferrer" className="hover:text-foreground transition-colors">
Releases
</Link>
</li>
<li>
<Link href={`${GITHUB_REPO}/issues`} target="_blank" rel="noopener noreferrer" className="hover:text-foreground transition-colors">
Issues
</Link>
</li>
</ul>
</div>
</div>
<Separator className="my-8" />
<div className="text-center text-muted-foreground text-sm space-y-2">
<p>© 2026 voicebox. All rights reserved.</p>
</div>
</div>
</footer>
);
}
+46
View File
@@ -0,0 +1,46 @@
'use client';
import Link from 'next/link';
import { Github } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { GITHUB_REPO } from '@/lib/constants';
export function Header() {
return (
<header className="border-b border-border bg-background/80 backdrop-blur-xl sticky top-0 z-50">
<div className="container mx-auto px-4">
<div className="flex items-center justify-between h-16">
{/* Logo */}
<Link href="/" className="flex items-center gap-2 font-bold text-xl sm:text-2xl hover:opacity-80 transition-opacity">
voicebox
</Link>
{/* Actions */}
<div className="flex items-center gap-2 sm:gap-4">
<Button
variant="outline"
size="sm"
asChild
className="hidden sm:flex"
>
<a href={GITHUB_REPO} target="_blank" rel="noopener noreferrer">
<Github className="h-4 w-4 mr-2" />
GitHub
</a>
</Button>
<Button
variant="ghost"
size="icon"
asChild
className="sm:hidden"
>
<a href={GITHUB_REPO} target="_blank" rel="noopener noreferrer" aria-label="GitHub">
<Github className="h-5 w-5" />
</a>
</Button>
</div>
</div>
</div>
</header>
);
}
+55
View File
@@ -0,0 +1,55 @@
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 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',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md text-lg 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 };
+78
View File
@@ -0,0 +1,78 @@
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 bg-card 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-6', 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('text-sm text-muted-foreground', className)}
{...props}
/>
));
CardDescription.displayName = 'CardDescription';
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
));
CardContent.displayName = 'CardContent';
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('flex items-center p-6 pt-0', className)}
{...props}
/>
));
CardFooter.displayName = 'CardFooter';
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
@@ -0,0 +1,26 @@
import { ReactNode } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './card';
import { cn } from '@/lib/utils';
interface FeatureCardProps {
title: string;
description: string;
icon?: ReactNode;
className?: string;
}
export function FeatureCard({ title, description, icon, className }: FeatureCardProps) {
return (
<Card className={cn('text-center', className)}>
<CardHeader>
{icon && <div className="flex justify-center mb-2">{icon}</div>}
<CardTitle className="text-xl sm:text-2xl">{title}</CardTitle>
</CardHeader>
<CardContent>
<CardDescription className="text-sm sm:text-base">
{description}
</CardDescription>
</CardContent>
</Card>
);
}
+45
View File
@@ -0,0 +1,45 @@
'use client';
import { ReactNode } from 'react';
import { cn } from '@/lib/utils';
import Image from 'next/image';
interface HeroProps {
title: ReactNode;
description: string;
actions?: ReactNode;
className?: string;
showLogo?: boolean;
}
export function Hero({ title, description, actions, className, showLogo = true }: HeroProps) {
return (
<section className={cn('relative text-center pt-8 sm:pt-12 md:pt-14 -mb-6 sm:-mb-8 md:-mb-10 overflow-hidden -mx-4 sm:-mx-6 md:-mx-4 -mt-4 sm:mt-0 md:mt-0', className)}>
<div className="relative z-10 px-4">
{showLogo && (
<div className="flex justify-center mb-4 sm:mb-6">
<Image
src="/voicebox-logo.png"
alt="Voicebox Logo"
width={1024}
height={1024}
className="w-32 sm:w-40 md:w-48 h-auto"
priority
/>
</div>
)}
<h1 className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold mb-4 sm:mb-6">
{title}
</h1>
<p className="text-base sm:text-lg md:text-xl text-foreground/60 mb-6 sm:mb-8 max-w-2xl mx-auto px-2">
{description}
</p>
{actions && (
<div className="flex flex-col sm:flex-row gap-3 sm:gap-4 justify-center px-2">
{actions}
</div>
)}
</div>
</section>
);
}
+24
View File
@@ -0,0 +1,24 @@
import { cn } from '@/lib/utils';
import { ReactNode } from 'react';
interface SectionProps {
children: ReactNode;
className?: string;
id?: string;
}
export function Section({ children, className, id }: SectionProps) {
return (
<section id={id} className={cn('space-y-4 sm:space-y-6', className)}>
{children}
</section>
);
}
export function SectionTitle({ children, className }: { children: ReactNode; className?: string }) {
return (
<h2 className={cn('text-2xl sm:text-3xl md:text-4xl font-bold text-center md:text-left', className)}>
{children}
</h2>
);
}
+28
View File
@@ -0,0 +1,28 @@
import * as React from 'react';
import * as SeparatorPrimitive from '@radix-ui/react-separator';
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 };