Updated website deisgn

This commit is contained in:
Arunavo Ray
2025-07-08 21:58:45 +05:30
parent bb1842bc10
commit b55d6a5629
15 changed files with 1167 additions and 381 deletions

View File

@@ -0,0 +1,69 @@
import React from 'react';
import { Button } from './ui/button';
import { ArrowRight, Star, GitFork, Users } from 'lucide-react';
export function CTA() {
return (
<section className="py-16 sm:py-24 px-4 sm:px-6 lg:px-8">
<div className="max-w-4xl mx-auto">
<div className="relative overflow-hidden rounded-2xl sm:rounded-3xl bg-gradient-to-r from-blue-600 to-purple-600 p-6 sm:p-8 md:p-12 text-center">
{/* Background pattern */}
<div className="absolute inset-0 bg-grid-white/10 [mask-image:linear-gradient(0deg,transparent,rgba(255,255,255,0.5))]" />
<div className="relative">
<h2 className="text-2xl sm:text-3xl md:text-4xl font-bold text-white mb-4">
Start Protecting Your Code Today
</h2>
<p className="text-base sm:text-lg text-white/90 mb-6 sm:mb-8 max-w-2xl mx-auto px-4">
Join developers who trust Gitea Mirror to keep their repositories safe and accessible.
Free, open source, and ready to deploy.
</p>
{/* Stats */}
<div className="flex flex-wrap items-center justify-center gap-4 sm:gap-6 md:gap-8 mb-6 sm:mb-8 text-white/80 text-sm sm:text-base">
<div className="flex items-center gap-2">
<Star className="w-4 h-4 sm:w-5 sm:h-5" />
<span className="font-semibold">500+ Stars</span>
</div>
<div className="flex items-center gap-2">
<GitFork className="w-4 h-4 sm:w-5 sm:h-5" />
<span className="font-semibold">50+ Forks</span>
</div>
<div className="flex items-center gap-2">
<Users className="w-4 h-4 sm:w-5 sm:h-5" />
<span className="font-semibold">Active Community</span>
</div>
</div>
<div className="flex flex-col sm:flex-row items-center justify-center gap-3 sm:gap-4">
<Button size="lg" variant="secondary" className="group w-full sm:w-auto min-h-[48px]" asChild>
<a href="https://github.com/RayLabsHQ/gitea-mirror" target="_blank" rel="noopener noreferrer">
Get Started Now
<ArrowRight className="ml-2 h-4 w-4 transition-transform group-hover:translate-x-1" />
</a>
</Button>
<Button size="lg" variant="ghost" className="text-white hover:bg-white/20 w-full sm:w-auto min-h-[48px]" asChild>
<a href="https://github.com/RayLabsHQ/gitea-mirror/discussions" target="_blank" rel="noopener noreferrer">
Join Community
</a>
</Button>
</div>
</div>
</div>
{/* Open source note */}
<div className="mt-8 sm:mt-12 text-center">
<p className="text-xs sm:text-sm text-muted-foreground">
Gitea Mirror is licensed under GPL-3.0.
<a href="https://github.com/RayLabsHQ/gitea-mirror/blob/main/LICENSE"
className="ml-1 underline hover:text-foreground transition-colors"
target="_blank"
rel="noopener noreferrer">
View License
</a>
</p>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,127 @@
import React from 'react';
import {
RefreshCw,
Building2,
FolderTree,
Activity,
Lock,
Heart,
Zap,
Shield,
GitBranch
} from 'lucide-react';
const features = [
{
title: "Automated Mirroring",
description: "Set it and forget it. Automatically sync your GitHub repositories to Gitea on a schedule.",
icon: RefreshCw,
gradient: "from-blue-500 to-cyan-500"
},
{
title: "Bulk Operations",
description: "Mirror entire organizations or user accounts with a single configuration.",
icon: Building2,
gradient: "from-purple-500 to-pink-500"
},
{
title: "Preserve Structure",
description: "Maintain your GitHub organization structure or customize how repos are organized.",
icon: FolderTree,
gradient: "from-green-500 to-emerald-500"
},
{
title: "Real-time Status",
description: "Monitor mirror progress with live updates and detailed activity logs.",
icon: Activity,
gradient: "from-orange-500 to-red-500"
},
{
title: "Secure & Private",
description: "Self-hosted solution keeps your code on your infrastructure with full control.",
icon: Lock,
gradient: "from-indigo-500 to-purple-500"
},
{
title: "Open Source",
description: "Free, transparent, and community-driven development. Contribute and customize.",
icon: Heart,
gradient: "from-pink-500 to-rose-500"
}
];
export function Features() {
return (
<section id="features" className="py-16 sm:py-24 px-4 sm:px-6 lg:px-8">
<div className="max-w-7xl mx-auto">
<div className="text-center mb-12 sm:mb-16">
<h2 className="text-2xl sm:text-3xl md:text-4xl font-bold tracking-tight px-4">
Everything You Need for
<span className="bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent block sm:inline"> Reliable Backups</span>
</h2>
<p className="mt-4 text-base sm:text-lg text-muted-foreground max-w-2xl mx-auto px-4">
Powerful features designed to keep your code safe and accessible, no matter what happens.
</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6 lg:gap-8">
{features.map((feature, index) => {
const Icon = feature.icon;
return (
<div
key={index}
className="group relative p-6 sm:p-8 rounded-xl sm:rounded-2xl border bg-card hover:shadow-xl transition-all duration-300 hover:-translate-y-1"
>
<div className="absolute inset-0 bg-gradient-to-r opacity-0 group-hover:opacity-5 rounded-xl sm:rounded-2xl transition-opacity duration-300"
style={{
backgroundImage: `linear-gradient(to right, var(--tw-gradient-stops))`,
'--tw-gradient-from': feature.gradient.split(' ')[1],
'--tw-gradient-to': feature.gradient.split(' ')[3],
}}
/>
<div className={`inline-flex p-2.5 sm:p-3 rounded-lg bg-gradient-to-r ${feature.gradient} mb-3 sm:mb-4`}>
<Icon className="w-5 h-5 sm:w-6 sm:h-6 text-white" />
</div>
<h3 className="text-lg sm:text-xl font-semibold mb-2">{feature.title}</h3>
<p className="text-sm sm:text-base text-muted-foreground">{feature.description}</p>
</div>
);
})}
</div>
{/* Additional feature highlights */}
<div className="mt-12 sm:mt-16 grid grid-cols-1 sm:grid-cols-3 gap-4 sm:gap-6 lg:gap-8 p-6 sm:p-8 rounded-xl sm:rounded-2xl bg-muted/30">
<div className="flex items-center gap-3 sm:gap-4">
<div className="p-2 rounded-lg bg-blue-500/10 flex-shrink-0">
<Zap className="w-4 h-4 sm:w-5 sm:h-5 text-blue-600 dark:text-blue-400" />
</div>
<div>
<p className="font-semibold text-sm sm:text-base">Lightning Fast</p>
<p className="text-xs sm:text-sm text-muted-foreground">Optimized for speed</p>
</div>
</div>
<div className="flex items-center gap-3 sm:gap-4">
<div className="p-2 rounded-lg bg-green-500/10 flex-shrink-0">
<Shield className="w-4 h-4 sm:w-5 sm:h-5 text-green-600 dark:text-green-400" />
</div>
<div>
<p className="font-semibold text-sm sm:text-base">Enterprise Ready</p>
<p className="text-xs sm:text-sm text-muted-foreground">Scale with confidence</p>
</div>
</div>
<div className="flex items-center gap-3 sm:gap-4">
<div className="p-2 rounded-lg bg-purple-500/10 flex-shrink-0">
<GitBranch className="w-4 h-4 sm:w-5 sm:h-5 text-purple-600 dark:text-purple-400" />
</div>
<div>
<p className="font-semibold text-sm sm:text-base">Full Git Support</p>
<p className="text-xs sm:text-sm text-muted-foreground">All features preserved</p>
</div>
</div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,81 @@
import React from 'react';
import { Github, Book, MessageSquare, Bug } from 'lucide-react';
export function Footer() {
const links = [
{
title: "Source Code",
href: "https://github.com/RayLabsHQ/gitea-mirror",
icon: Github
},
{
title: "Documentation",
href: "https://github.com/RayLabsHQ/gitea-mirror/tree/main/docs",
icon: Book
},
{
title: "Discussions",
href: "https://github.com/RayLabsHQ/gitea-mirror/discussions",
icon: MessageSquare
},
{
title: "Report Issue",
href: "https://github.com/RayLabsHQ/gitea-mirror/issues",
icon: Bug
}
];
return (
<footer className="border-t py-8 sm:py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-7xl mx-auto">
<div className="flex flex-col items-center gap-6 sm:gap-8">
{/* Logo and tagline */}
<div className="text-center">
<div className="flex items-center justify-center gap-2 mb-2">
<img src="/assets/logo-no-bg.png" alt="Gitea Mirror" className="w-6 h-6 sm:w-8 sm:h-8" />
<span className="font-semibold text-base sm:text-lg">Gitea Mirror</span>
</div>
<p className="text-xs sm:text-sm text-muted-foreground">
Keep your GitHub code safe and synced
</p>
</div>
{/* Links */}
<nav className="grid grid-cols-2 sm:flex items-center justify-center gap-4 sm:gap-6 text-center">
{links.map((link) => {
const Icon = link.icon;
return (
<a
key={link.title}
href={link.href}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center gap-2 text-xs sm:text-sm text-muted-foreground hover:text-foreground transition-colors py-2 sm:py-0"
>
<Icon className="w-3 h-3 sm:w-4 sm:h-4" />
<span>{link.title}</span>
</a>
);
})}
</nav>
{/* Copyright */}
<div className="text-center text-xs sm:text-sm text-muted-foreground px-4">
<p>© {new Date().getFullYear()} Gitea Mirror. Open source under GPL-3.0 License.</p>
<p className="mt-1">
Made with dedication by the{' '}
<a
href="https://github.com/RayLabsHQ"
className="underline hover:text-foreground transition-colors"
target="_blank"
rel="noopener noreferrer"
>
RayLabs team
</a>
</p>
</div>
</div>
</div>
</footer>
);
}

View File

@@ -0,0 +1,118 @@
import React, { useState, useEffect } from 'react';
import { ThemeToggle } from './ThemeToggle';
import { Github, Menu, X } from 'lucide-react';
import { Button } from './ui/button';
export function Header() {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [isScrolled, setIsScrolled] = useState(false);
useEffect(() => {
const handleScroll = () => {
setIsScrolled(window.scrollY > 10);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
const navLinks = [
{ href: '#features', label: 'Features' },
{ href: '#screenshots', label: 'Screenshots' },
{ href: '#installation', label: 'Installation' }
];
const handleNavClick = () => {
setIsMenuOpen(false);
};
return (
<header className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
isScrolled ? 'backdrop-blur-lg bg-background/80 border-b shadow-sm' : 'bg-background/50'
}`}>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
{/* Logo */}
<a href="#" className="flex items-center gap-2 group">
<img
src="/assets/logo-no-bg.png"
alt="Gitea Mirror"
className="w-8 h-8 transition-transform group-hover:scale-110"
/>
<span className="font-semibold text-lg hidden sm:block">Gitea Mirror</span>
</a>
{/* Desktop Navigation */}
<nav className="hidden md:flex items-center gap-8">
{navLinks.map((link) => (
<a
key={link.href}
href={link.href}
className="text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
>
{link.label}
</a>
))}
</nav>
{/* Desktop Actions */}
<div className="hidden md:flex items-center gap-4">
<ThemeToggle />
<Button variant="outline" size="sm" asChild>
<a href="https://github.com/RayLabsHQ/gitea-mirror" target="_blank" rel="noopener noreferrer">
<Github className="w-4 h-4 mr-2" />
Star on GitHub
</a>
</Button>
</div>
{/* Mobile Actions */}
<div className="flex md:hidden items-center gap-2">
<ThemeToggle />
<Button
variant="ghost"
size="icon"
onClick={() => setIsMenuOpen(!isMenuOpen)}
aria-label="Toggle menu"
className="w-10 h-10"
>
{isMenuOpen ? (
<X className="h-5 w-5" />
) : (
<Menu className="h-5 w-5" />
)}
</Button>
</div>
</div>
</div>
{/* Mobile Menu */}
<div className={`md:hidden transition-all duration-300 ease-in-out ${
isMenuOpen ? 'max-h-96 opacity-100' : 'max-h-0 opacity-0 overflow-hidden'
}`}>
<div className="bg-background/95 backdrop-blur-lg border-b">
<nav className="px-4 py-4 space-y-3">
{navLinks.map((link) => (
<a
key={link.href}
href={link.href}
onClick={handleNavClick}
className="block py-2 text-base font-medium text-muted-foreground hover:text-foreground transition-colors"
>
{link.label}
</a>
))}
<div className="pt-3 border-t">
<Button variant="outline" size="sm" className="w-full" asChild>
<a href="https://github.com/RayLabsHQ/gitea-mirror" target="_blank" rel="noopener noreferrer">
<Github className="w-4 h-4 mr-2" />
Star on GitHub
</a>
</Button>
</div>
</nav>
</div>
</div>
</header>
);
}

View File

@@ -0,0 +1,75 @@
import React from 'react';
import { Button } from './ui/button';
import { ArrowRight, Github, Shield, RefreshCw } from 'lucide-react';
export function Hero() {
return (
<section className="relative min-h-[100vh] pt-20 pb-10 flex items-center justify-center px-4 sm:px-6 lg:px-8 overflow-hidden">
{/* Background gradients */}
<div className="absolute inset-0 -z-10">
<div className="absolute top-20 -left-4 w-48 h-48 sm:w-72 sm:h-72 bg-purple-300 rounded-full mix-blend-multiply filter blur-xl opacity-20 animate-blob dark:opacity-10"></div>
<div className="absolute top-20 -right-4 w-48 h-48 sm:w-72 sm:h-72 bg-yellow-300 rounded-full mix-blend-multiply filter blur-xl opacity-20 animate-blob animation-delay-2000 dark:opacity-10"></div>
<div className="absolute -bottom-8 left-10 w-48 h-48 sm:w-72 sm:h-72 bg-pink-300 rounded-full mix-blend-multiply filter blur-xl opacity-20 animate-blob animation-delay-4000 dark:opacity-10"></div>
</div>
<div className="max-w-7xl mx-auto text-center w-full">
<div className="mb-6 sm:mb-8 flex justify-center">
<div className="relative">
<div className="absolute inset-0 bg-gradient-to-r from-blue-600 to-purple-600 rounded-full blur-lg opacity-75 animate-pulse"></div>
<img
src="/assets/logo-no-bg.png"
alt="Gitea Mirror Logo"
className="relative w-20 h-20 sm:w-24 sm:h-24 md:w-32 md:h-32"
/>
</div>
</div>
<h1 className="text-3xl xs:text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold tracking-tight leading-tight">
<span className="bg-gradient-to-r from-gray-900 to-gray-600 dark:from-white dark:to-gray-400 bg-clip-text text-transparent">
Keep Your Code
</span>
<br />
<span className="bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
Safe & Synced
</span>
</h1>
<p className="mt-4 sm:mt-6 text-base sm:text-lg md:text-xl text-muted-foreground max-w-3xl mx-auto px-4">
Automatically mirror your GitHub repositories to self-hosted Gitea.
Never lose access to your code with continuous backup and synchronization.
</p>
<div className="mt-6 sm:mt-8 flex flex-wrap items-center justify-center gap-3 text-xs sm:text-sm text-muted-foreground px-4">
<div className="flex items-center gap-2">
<Shield className="w-3 h-3 sm:w-4 sm:h-4" />
<span>Self-Hosted</span>
</div>
<span className="text-gray-300 dark:text-gray-700 hidden xs:inline"></span>
<div className="flex items-center gap-2">
<RefreshCw className="w-3 h-3 sm:w-4 sm:h-4" />
<span>Auto-Sync</span>
</div>
<span className="text-gray-300 dark:text-gray-700 hidden xs:inline"></span>
<div className="flex items-center gap-2">
<Github className="w-3 h-3 sm:w-4 sm:h-4" />
<span>Open Source</span>
</div>
</div>
<div className="mt-8 sm:mt-10 flex flex-col sm:flex-row items-center justify-center gap-3 sm:gap-4 px-4">
<Button size="lg" className="group w-full sm:w-auto min-h-[48px] text-base" asChild>
<a href="https://github.com/RayLabsHQ/gitea-mirror" target="_blank" rel="noopener noreferrer">
Get Started
<ArrowRight className="ml-2 h-4 w-4 transition-transform group-hover:translate-x-1" />
</a>
</Button>
<Button size="lg" variant="outline" className="w-full sm:w-auto min-h-[48px] text-base" asChild>
<a href="#features">
View Features
</a>
</Button>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,165 @@
import React, { useState } from 'react';
import { Button } from './ui/button';
import { Copy, Check, Terminal, Container, Cloud } from 'lucide-react';
type InstallMethod = 'docker' | 'manual' | 'proxmox';
export function Installation() {
const [activeMethod, setActiveMethod] = useState<InstallMethod>('docker');
const [copiedCommand, setCopiedCommand] = useState<string | null>(null);
const copyToClipboard = async (text: string, commandId: string) => {
await navigator.clipboard.writeText(text);
setCopiedCommand(commandId);
setTimeout(() => setCopiedCommand(null), 2000);
};
const installMethods = {
docker: {
icon: Container,
title: "Docker",
description: "Recommended for most users",
steps: [
{
title: "Clone the repository",
command: "git clone https://github.com/RayLabsHQ/gitea-mirror.git\ncd gitea-mirror",
id: "docker-clone"
},
{
title: "Start with Docker Compose",
command: "docker compose -f docker-compose.alt.yml up -d",
id: "docker-start"
},
{
title: "Access the application",
command: "# Open http://localhost:4321 in your browser",
id: "docker-access"
}
]
},
manual: {
icon: Terminal,
title: "Manual",
description: "For development or custom setups",
steps: [
{
title: "Install Bun runtime",
command: "curl -fsSL https://bun.sh/install | bash",
id: "manual-bun"
},
{
title: "Clone and setup",
command: "git clone https://github.com/RayLabsHQ/gitea-mirror.git\ncd gitea-mirror\nbun run setup",
id: "manual-setup"
},
{
title: "Start the application",
command: "bun run dev",
id: "manual-start"
}
]
},
proxmox: {
icon: Cloud,
title: "Proxmox LXC",
description: "One-click install for Proxmox VE",
steps: [
{
title: "Run the installation script",
command: 'bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/gitea-mirror.sh)"',
id: "proxmox-install"
},
{
title: "Follow the prompts",
command: "# The script will guide you through the setup",
id: "proxmox-follow"
}
]
}
};
return (
<section id="installation" className="py-16 sm:py-24 px-4 sm:px-6 lg:px-8">
<div className="max-w-4xl mx-auto">
<div className="text-center mb-8 sm:mb-16">
<h2 className="text-2xl sm:text-3xl md:text-4xl font-bold tracking-tight">
Get Started in Minutes
</h2>
<p className="mt-4 text-base sm:text-lg text-muted-foreground">
Choose your preferred installation method
</p>
</div>
{/* Installation method tabs */}
<div className="flex flex-col sm:flex-row flex-wrap justify-center gap-3 sm:gap-4 mb-8 sm:mb-12">
{(Object.entries(installMethods) as [InstallMethod, typeof installMethods[InstallMethod]][]).map(([method, config]) => {
const Icon = config.icon;
return (
<button
key={method}
onClick={() => setActiveMethod(method)}
className={`flex items-center gap-3 px-4 sm:px-6 py-3 rounded-lg border transition-all min-h-[60px] ${
activeMethod === method
? 'bg-primary text-primary-foreground border-primary'
: 'bg-card hover:bg-muted border-border'
}`}
>
<Icon className="w-5 h-5 flex-shrink-0" />
<div className="text-left">
<p className="font-semibold text-sm sm:text-base">{config.title}</p>
<p className={`text-xs ${activeMethod === method ? 'text-primary-foreground/80' : 'text-muted-foreground'}`}>
{config.description}
</p>
</div>
</button>
);
})}
</div>
{/* Installation steps */}
<div className="space-y-4 sm:space-y-6">
{installMethods[activeMethod].steps.map((step, index) => (
<div key={step.id} className="relative">
<div className="flex items-start gap-3 sm:gap-4">
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center">
<span className="text-sm font-semibold text-primary">{index + 1}</span>
</div>
<div className="flex-grow min-w-0">
<h3 className="font-semibold mb-2 text-sm sm:text-base">{step.title}</h3>
<div className="relative group">
<pre className="bg-muted/50 rounded-lg p-3 sm:p-4 pr-12 overflow-x-auto text-xs sm:text-sm">
<code className="break-all sm:break-normal">{step.command}</code>
</pre>
<Button
variant="ghost"
size="icon"
className="absolute top-1 right-1 sm:top-2 sm:right-2 w-8 h-8 sm:w-9 sm:h-9 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity"
onClick={() => copyToClipboard(step.command, step.id)}
>
{copiedCommand === step.id ? (
<Check className="h-3 w-3 sm:h-4 sm:w-4 text-green-600" />
) : (
<Copy className="h-3 w-3 sm:h-4 sm:w-4" />
)}
</Button>
</div>
</div>
</div>
{index < installMethods[activeMethod].steps.length - 1 && (
<div className="absolute left-4 top-10 bottom-0 w-[1px] bg-border -z-10" />
)}
</div>
))}
</div>
{/* Additional info */}
<div className="mt-8 sm:mt-12 p-4 sm:p-6 rounded-lg bg-muted/30 border">
<p className="text-xs sm:text-sm text-muted-foreground">
<strong className="text-foreground">First user becomes admin.</strong> After installation,
create your account and configure GitHub and Gitea connections through the web interface.
</p>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,200 @@
import React, { useState, useRef, useEffect } from 'react';
import { ChevronLeft, ChevronRight } from 'lucide-react';
import { Button } from './ui/button';
const screenshots = [
{
title: "Dashboard Overview",
description: "Monitor all your mirrored repositories in one place",
desktop: "/assets/dashboard.png",
mobile: "/assets/dashboard_mobile.png"
},
{
title: "Organization Management",
description: "Easily manage and sync entire GitHub organizations",
desktop: "/assets/organisation.png",
mobile: "/assets/organisation_mobile.png"
},
{
title: "Repository Control",
description: "Fine-grained control over individual repository mirrors",
desktop: "/assets/repositories.png",
mobile: "/assets/repositories_mobile.png"
},
{
title: "Configuration",
description: "Simple and intuitive configuration interface",
desktop: "/assets/configuration.png",
mobile: "/assets/configuration_mobile.png"
},
{
title: "Activity Monitoring",
description: "Track sync progress and view detailed logs",
desktop: "/assets/activity.png",
mobile: "/assets/activity_mobile.png"
}
];
export function Screenshots() {
const [currentIndex, setCurrentIndex] = useState(0);
const [touchStart, setTouchStart] = useState(0);
const [touchEnd, setTouchEnd] = useState(0);
const containerRef = useRef<HTMLDivElement>(null);
const minSwipeDistance = 50;
const onTouchStart = (e: React.TouchEvent) => {
setTouchEnd(0);
setTouchStart(e.targetTouches[0].clientX);
};
const onTouchMove = (e: React.TouchEvent) => {
setTouchEnd(e.targetTouches[0].clientX);
};
const onTouchEnd = () => {
if (!touchStart || !touchEnd) return;
const distance = touchStart - touchEnd;
const isLeftSwipe = distance > minSwipeDistance;
const isRightSwipe = distance < -minSwipeDistance;
if (isLeftSwipe && currentIndex < screenshots.length - 1) {
setCurrentIndex(currentIndex + 1);
}
if (isRightSwipe && currentIndex > 0) {
setCurrentIndex(currentIndex - 1);
}
};
const goToPrevious = () => {
setCurrentIndex((prevIndex) =>
prevIndex === 0 ? screenshots.length - 1 : prevIndex - 1
);
};
const goToNext = () => {
setCurrentIndex((prevIndex) =>
(prevIndex + 1) % screenshots.length
);
};
// Keyboard navigation
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'ArrowLeft') goToPrevious();
if (e.key === 'ArrowRight') goToNext();
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [currentIndex]);
const current = screenshots[currentIndex];
return (
<section id="screenshots" className="py-16 sm:py-24 px-4 sm:px-6 lg:px-8 bg-muted/30">
<div className="max-w-7xl mx-auto">
<div className="text-center mb-8 sm:mb-16">
<h2 className="text-2xl sm:text-3xl md:text-4xl font-bold tracking-tight">
See It In Action
</h2>
<p className="mt-4 text-base sm:text-lg text-muted-foreground max-w-2xl mx-auto px-4">
A clean, intuitive interface designed for efficiency and ease of use
</p>
</div>
<div className="relative max-w-5xl mx-auto">
{/* Screenshot viewer */}
<div
ref={containerRef}
className="relative group"
onTouchStart={onTouchStart}
onTouchMove={onTouchMove}
onTouchEnd={onTouchEnd}
>
<div className="aspect-[16/10] overflow-hidden rounded-lg sm:rounded-2xl bg-gradient-to-br from-gray-900 to-gray-800 dark:from-gray-800 dark:to-gray-900 shadow-2xl">
<picture>
<source media="(max-width: 640px)" srcSet={current.mobile} />
<img
src={current.desktop}
alt={current.title}
className="w-full h-full object-cover object-top"
draggable={false}
/>
</picture>
</div>
{/* Navigation buttons - hidden on mobile, visible on desktop */}
<Button
variant="outline"
size="icon"
className="absolute left-2 sm:left-4 top-1/2 -translate-y-1/2 opacity-0 sm:opacity-100 group-hover:opacity-100 transition-opacity hidden sm:flex"
onClick={goToPrevious}
aria-label="Previous screenshot"
>
<ChevronLeft className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="icon"
className="absolute right-2 sm:right-4 top-1/2 -translate-y-1/2 opacity-0 sm:opacity-100 group-hover:opacity-100 transition-opacity hidden sm:flex"
onClick={goToNext}
aria-label="Next screenshot"
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
{/* Screenshot info */}
<div className="mt-6 sm:mt-8 text-center">
<h3 className="text-lg sm:text-xl font-semibold">{current.title}</h3>
<p className="mt-2 text-sm sm:text-base text-muted-foreground">{current.description}</p>
</div>
{/* Dots indicator */}
<div className="mt-6 sm:mt-8 flex justify-center gap-2">
{screenshots.map((_, index) => (
<button
key={index}
className={`transition-all duration-300 ${
index === currentIndex
? 'w-8 h-2 bg-primary rounded-full'
: 'w-2 h-2 bg-muted-foreground/30 hover:bg-muted-foreground/50 rounded-full'
}`}
onClick={() => setCurrentIndex(index)}
aria-label={`Go to screenshot ${index + 1}`}
/>
))}
</div>
{/* Mobile swipe hint */}
<p className="mt-4 text-xs text-muted-foreground text-center sm:hidden">
Swipe left or right to navigate
</p>
</div>
{/* Thumbnail grid - visible on larger screens */}
<div className="hidden lg:grid grid-cols-5 gap-4 mt-12 px-8">
{screenshots.map((screenshot, index) => (
<button
key={index}
onClick={() => setCurrentIndex(index)}
className={`relative overflow-hidden rounded-lg transition-all duration-300 ${
index === currentIndex
? 'ring-2 ring-primary shadow-lg scale-105'
: 'opacity-60 hover:opacity-100'
}`}
>
<img
src={screenshot.desktop}
alt={screenshot.title}
className="w-full h-full object-cover"
/>
</button>
))}
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,40 @@
import React, { useEffect, useState } from 'react';
import { Moon, Sun } from 'lucide-react';
import { Button } from './ui/button';
export function ThemeToggle() {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
useEffect(() => {
// Check for saved theme preference or default to light
const savedTheme = localStorage.getItem('theme') as 'light' | 'dark' | null;
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const initialTheme = savedTheme || (prefersDark ? 'dark' : 'light');
setTheme(initialTheme);
document.documentElement.classList.toggle('dark', initialTheme === 'dark');
}, []);
const toggleTheme = () => {
const newTheme = theme === 'light' ? 'dark' : 'light';
setTheme(newTheme);
localStorage.setItem('theme', newTheme);
document.documentElement.classList.toggle('dark', newTheme === 'dark');
};
return (
<Button
variant="ghost"
size="icon"
onClick={toggleTheme}
className="w-10 h-10 rounded-full"
aria-label={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
>
{theme === 'light' ? (
<Moon className="h-5 w-5 transition-all" />
) : (
<Sun className="h-5 w-5 transition-all" />
)}
</Button>
);
}