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