mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-19 14:50:38 -07:00
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 };
|
||||
Reference in New Issue
Block a user