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

92
www/CLAUDE.md Normal file
View File

@@ -0,0 +1,92 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is the marketing website for Gitea Mirror, built with Astro and Tailwind CSS v4. It serves as a landing page to showcase the Gitea Mirror application's features and provide getting started information.
**Note**: This is NOT the main Gitea Mirror application. The actual application is located in the parent directory (`../`).
## Essential Commands
```bash
bun install # Install dependencies
bun run dev # Start development server (port 4321)
bun run build # Build for production
bun run preview # Preview production build
```
## Architecture & Key Concepts
### Technology Stack
- **Framework**: Astro (v5.0.5) - Static site generator with React integration
- **UI**: React (v19.0.0) + Tailwind CSS v4
- **Runtime**: Bun
- **Styling**: Tailwind CSS v4 with Vite plugin
### Project Structure
- `/src/pages/` - Astro pages (single `index.astro` page)
- `/src/components/` - React components for UI sections
- `Hero.tsx` - Landing hero section
- `Features.tsx` - Feature showcase
- `GettingStarted.tsx` - Installation and setup guide
- `Screenshots.tsx` - Product screenshots gallery
- `Footer.tsx` - Page footer
- `/src/layouts/` - Layout wrapper components
- `/public/assets/` - Static assets (shared with main project)
- `/public/favicon.svg` - Site favicon
### Key Implementation Details
1. **Single Page Application**: The entire website is a single page (`index.astro`) composed of React components.
2. **Responsive Design**: All components use Tailwind CSS for responsive layouts with mobile-first approach.
3. **Asset Sharing**: Screenshots and images are shared with the main Gitea Mirror project (located in `/public/assets/`).
4. **Component Pattern**: Each major section is a separate React component with TypeScript interfaces for props.
### Development Guidelines
**When updating content:**
- Hero section copy is in `Hero.tsx`
- Features are defined in `Features.tsx` as an array
- Getting started steps are in `GettingStarted.tsx`
- Screenshots are referenced from `/public/assets/`
**When adding new sections:**
1. Create a new component in `/src/components/`
2. Import and add it to `index.astro`
3. Follow the existing pattern of full-width sections with container constraints
**Styling conventions:**
- Use Tailwind CSS v4 classes exclusively
- Follow the existing color scheme (zinc/neutral grays, blue accents)
- Maintain consistent spacing using Tailwind's spacing scale
- Keep mobile responsiveness in mind
### Common Tasks
**Updating screenshots:**
- Screenshots should match those in the main application
- Place new screenshots in `/public/assets/`
- Update the `Screenshots.tsx` component to reference new images
**Modifying feature list:**
- Edit the `features` array in `Features.tsx`
- Each feature needs: icon, title, and description
- Icons come from `lucide-react`
**Changing getting started steps:**
- Edit the content in `GettingStarted.tsx`
- Docker and direct installation tabs are separate sections
- Code blocks use `<pre>` and `<code>` tags with Tailwind styling
## Relationship to Main Project
This website showcases the Gitea Mirror application located in the parent directory. When making updates:
- Ensure feature descriptions match actual capabilities
- Keep version numbers and requirements synchronized
- Use the same screenshots as the main application's documentation
- Maintain consistent branding and messaging

View File

@@ -1,19 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<defs>
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#f97316;stop-opacity:1" />
<stop offset="100%" style="stop-color:#ea580c;stop-opacity:1" />
<linearGradient id="gitea-mirror-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#3b82f6;stop-opacity:1" />
<stop offset="100%" style="stop-color:#8b5cf6;stop-opacity:1" />
</linearGradient>
</defs>
<\!-- Background circle -->
<circle cx="16" cy="16" r="15" fill="url(#grad)" />
<\!-- Mirror/sync icon -->
<rect width="32" height="32" rx="6" fill="url(#gitea-mirror-gradient)"/>
<g fill="white" transform="translate(16, 16)">
<\!-- First arrow (top) -->
<path d="M -8 -2 L -3 -2 L -3 -5 L 2 0 L -3 5 L -3 2 L -8 2 C -8 2 -10 2 -10 0 C -10 -2 -8 -2 -8 -2 Z" />
<\!-- Second arrow (bottom) -->
<path d="M 8 2 L 3 2 L 3 5 L -2 0 L 3 -5 L 3 -2 L 8 -2 C 8 -2 10 -2 10 0 C 10 2 8 2 8 2 Z" opacity="0.8" />
<path d="M 8 2 L 3 2 L 3 5 L -2 0 L 3 -5 L 3 -2 L 8 -2 C 8 -2 10 -2 10 0 C 10 2 8 2 8 2 Z" opacity="0.9" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 781 B

After

Width:  |  Height:  |  Size: 686 B

8
www/public/robots.txt Normal file
View File

@@ -0,0 +1,8 @@
# Robots.txt for Gitea Mirror
User-agent: *
Allow: /
Sitemap: https://gitea-mirror.com/sitemap.xml
# Crawl-delay for responsible crawling
User-agent: *
Crawl-delay: 1

9
www/public/sitemap.xml Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://gitea-mirror.com/</loc>
<lastmod>2025-01-08</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
</urlset>

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

View File

@@ -1,343 +1,166 @@
---
import '../styles/global.css';
import { Button } from '../components/ui/button';
import { Header } from '../components/Header';
import { Hero } from '../components/Hero';
import { Features } from '../components/Features';
import { Screenshots } from '../components/Screenshots';
import { Installation } from '../components/Installation';
import { CTA } from '../components/CTA';
import { Footer } from '../components/Footer';
const features = [
{
title: "Automated Mirroring",
description: "Set it and forget it. Automatically sync your GitHub repositories to Gitea on a schedule.",
icon: "🔄"
},
{
title: "Bulk Operations",
description: "Mirror entire organizations or user accounts with a single configuration.",
icon: "📦"
},
{
title: "Preserve Structure",
description: "Maintain your GitHub organization structure or customize how repos are organized.",
icon: "🏗️"
},
{
title: "Real-time Status",
description: "Monitor mirror progress with live updates and detailed activity logs.",
icon: "📊"
},
{
title: "Secure & Private",
description: "Self-hosted solution keeps your code on your infrastructure.",
icon: "🔒"
},
{
title: "Open Source",
description: "Free, transparent, and community-driven development.",
icon: "💚"
}
];
const siteUrl = 'https://gitea-mirror.com';
const title = 'Gitea Mirror - Automated GitHub to Gitea Repository Mirroring & Backup';
const description = 'Automatically mirror and backup your GitHub repositories to self-hosted Gitea. Keep your code safe with scheduled syncing, bulk operations, and real-time monitoring. Free and open source.';
const keywords = 'github backup, gitea mirror, repository sync, github to gitea, git mirror, code backup, self-hosted git, repository migration, github mirror tool, gitea sync, automated backup, github repository backup, git repository mirror, self-hosted backup solution';
const steps = [
{ number: "1", title: "Install", description: "Deploy with Docker or run directly with Bun" },
{ number: "2", title: "Connect", description: "Add your GitHub and Gitea credentials" },
{ number: "3", title: "Configure", description: "Select repositories or organizations to mirror" },
{ number: "4", title: "Relax", description: "Let Gitea Mirror handle the synchronization" }
];
const screenshots = [
{ src: "/assets/dashboard.png", alt: "Dashboard view", mobile: "/assets/dashboard_mobile.png" },
{ src: "/assets/repositories.png", alt: "Repository management", mobile: "/assets/repositories_mobile.png" },
{ src: "/assets/configuration.png", alt: "Configuration interface", mobile: "/assets/configuration_mobile.png" },
{ src: "/assets/activity.png", alt: "Activity monitoring", mobile: "/assets/activity_mobile.png" }
];
// Structured data for SEO
const structuredData = {
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "Gitea Mirror",
"applicationCategory": "DeveloperApplication",
"operatingSystem": "Linux, macOS, Windows",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
},
"description": description,
"url": siteUrl,
"author": {
"@type": "Organization",
"name": "RayLabs",
"url": "https://github.com/RayLabsHQ"
},
"softwareVersion": "2.22.0",
"screenshot": [
`${siteUrl}/assets/dashboard.png`,
`${siteUrl}/assets/repositories.png`,
`${siteUrl}/assets/organisation.png`
],
"featureList": [
"Automated repository mirroring",
"Bulk organization sync",
"Real-time monitoring",
"Self-hosted solution",
"Open source"
],
"softwareRequirements": "Docker or Bun runtime"
};
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/png" href="/assets/logo.png" />
<meta name="generator" content={Astro.generator} />
<!-- SEO Meta Tags -->
<title>Gitea Mirror - Automated GitHub to Gitea Repository Backup & Sync</title>
<meta name="description" content="Automatically mirror and backup your GitHub repositories to self-hosted Gitea. Keep your code safe with scheduled syncing, bulk operations, and real-time monitoring." />
<meta name="keywords" content="github backup, gitea mirror, repository sync, github to gitea, git mirror, code backup, self-hosted git, repository migration" />
<meta name="robots" content="index, follow" />
<!-- Primary Meta Tags -->
<title>{title}</title>
<meta name="title" content={title} />
<meta name="description" content={description} />
<meta name="keywords" content={keywords} />
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1" />
<meta name="author" content="RayLabs" />
<!-- Open Graph -->
<meta property="og:title" content="Gitea Mirror - Automated GitHub to Gitea Repository Backup" />
<meta property="og:description" content="Keep your GitHub repositories safely backed up to your self-hosted Gitea instance with automated mirroring." />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:image" content="/assets/dashboard.png" />
<meta property="og:url" content={siteUrl} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={`${siteUrl}/assets/dashboard.png`} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:site_name" content="Gitea Mirror" />
<meta property="og:locale" content="en_US" />
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Gitea Mirror - GitHub to Gitea Backup Solution" />
<meta name="twitter:description" content="Automated repository mirroring from GitHub to self-hosted Gitea instances." />
<meta name="twitter:image" content="/assets/dashboard.png" />
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content={siteUrl} />
<meta property="twitter:title" content={title} />
<meta property="twitter:description" content={description} />
<meta property="twitter:image" content={`${siteUrl}/assets/dashboard.png`} />
<meta name="twitter:creator" content="@RayLabsHQ" />
<!-- Canonical URL -->
<link rel="canonical" href="https://gitea-mirror.com" />
<link rel="canonical" href={siteUrl} />
<!-- Additional Meta Tags -->
<meta name="theme-color" content="#3b82f6" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content="Gitea Mirror" />
<!-- Structured Data -->
<script type="application/ld+json" set:html={JSON.stringify(structuredData)} />
<!-- Preconnect to external domains -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="dns-prefetch" href="https://github.com" />
<!-- Theme detection script (prevent flash) -->
<script is:inline>
const theme = localStorage.getItem('theme') ||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
document.documentElement.classList.toggle('dark', theme === 'dark');
</script>
</head>
<body class="min-h-screen bg-background text-foreground antialiased">
<!-- Hero Section -->
<section class="relative overflow-hidden px-6 py-24 sm:py-32 lg:px-8">
<div class="absolute inset-0 -z-10 bg-gradient-to-br from-primary/5 via-transparent to-transparent"></div>
<div class="animate-float absolute -top-20 -right-20 h-72 w-72 rounded-full bg-primary/10 blur-3xl"></div>
<div class="animate-float-delayed absolute -bottom-20 -left-20 h-72 w-72 rounded-full bg-primary/10 blur-3xl"></div>
<Header client:load />
<div class="mx-auto max-w-7xl text-center">
<div class="animate-fade-in">
<h1 class="text-4xl font-bold tracking-tight sm:text-6xl bg-gradient-to-r from-foreground to-foreground/70 bg-clip-text">
Keep Your GitHub Code
<span class="block text-primary">Safe & Synced</span>
</h1>
<p class="mt-6 text-lg leading-8 text-muted-foreground max-w-2xl mx-auto">
Automatically mirror your GitHub repositories to self-hosted Gitea.
Never worry about losing access to your code with continuous backup and synchronization.
</p>
<div class="mt-10 flex items-center justify-center gap-4 flex-wrap">
<Button size="lg" className="animate-fade-in-up" asChild>
<a href="https://github.com/yourusername/gitea-mirror" target="_blank" rel="noopener noreferrer">
Get Started Free
<svg className="ml-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</a>
</Button>
<Button size="lg" variant="outline" className="animate-fade-in-up animation-delay-200" asChild>
<a href="https://github.com/yourusername/gitea-mirror" target="_blank" rel="noopener noreferrer">
View on GitHub
</a>
</Button>
</div>
</div>
</div>
</section>
<main>
<Hero client:load />
<Features client:load />
<Screenshots client:load />
<Installation client:load />
<CTA client:load />
</main>
<!-- Problem/Solution Section -->
<section class="px-6 py-24 sm:py-32 lg:px-8 bg-muted/30">
<div class="mx-auto max-w-7xl">
<div class="mx-auto max-w-2xl text-center">
<h2 class="text-3xl font-bold tracking-tight sm:text-4xl">Why You Need Repository Mirroring</h2>
<p class="mt-6 text-lg leading-8 text-muted-foreground">
GitHub is great, but what happens if you lose access? Service outages, account issues, or policy changes
can lock you out of your own code. Gitea Mirror ensures you always have a backup on infrastructure you control.
</p>
</div>
</div>
</section>
<!-- Features Grid -->
<section class="px-6 py-24 sm:py-32 lg:px-8">
<div class="mx-auto max-w-7xl">
<div class="mx-auto max-w-2xl text-center mb-16">
<h2 class="text-3xl font-bold tracking-tight sm:text-4xl">Everything You Need for Reliable Backups</h2>
</div>
<div class="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3">
{features.map((feature, index) => (
<div
class="group relative rounded-2xl border bg-card p-8 hover:shadow-lg transition-all duration-300 hover:-translate-y-1 animate-fade-in-up"
style={`animation-delay: ${index * 100}ms`}
>
<div class="text-4xl mb-4">{feature.icon}</div>
<h3 class="text-xl font-semibold mb-2">{feature.title}</h3>
<p class="text-muted-foreground">{feature.description}</p>
</div>
))}
</div>
</div>
</section>
<!-- Screenshots Section -->
<section class="px-6 py-24 sm:py-32 lg:px-8 bg-muted/30">
<div class="mx-auto max-w-7xl">
<div class="mx-auto max-w-2xl text-center mb-16">
<h2 class="text-3xl font-bold tracking-tight sm:text-4xl">See It In Action</h2>
<p class="mt-6 text-lg leading-8 text-muted-foreground">
A clean, intuitive interface for managing your repository mirrors
</p>
</div>
<div class="relative">
<div class="flex overflow-x-auto gap-6 pb-4 snap-x snap-mandatory scrollbar-hide">
{screenshots.map((screenshot, index) => (
<div
class="flex-none w-full sm:w-[48%] lg:w-[32%] snap-center animate-fade-in-up"
style={`animation-delay: ${index * 150}ms`}
>
<picture>
<source media="(max-width: 640px)" srcset={screenshot.mobile} />
<img
src={screenshot.src}
alt={screenshot.alt}
class="rounded-lg shadow-2xl w-full"
loading="lazy"
/>
</picture>
</div>
))}
</div>
</div>
</div>
</section>
<!-- How It Works -->
<section class="px-6 py-24 sm:py-32 lg:px-8">
<div class="mx-auto max-w-7xl">
<div class="mx-auto max-w-2xl text-center mb-16">
<h2 class="text-3xl font-bold tracking-tight sm:text-4xl">Get Started in Minutes</h2>
</div>
<div class="mx-auto max-w-3xl">
<div class="space-y-8">
{steps.map((step, index) => (
<div
class="flex gap-4 animate-fade-in-left"
style={`animation-delay: ${index * 200}ms`}
>
<div class="flex-none">
<div class="flex h-12 w-12 items-center justify-center rounded-full bg-primary text-primary-foreground font-bold text-lg">
{step.number}
</div>
</div>
<div class="flex-auto">
<h3 class="text-xl font-semibold mb-1">{step.title}</h3>
<p class="text-muted-foreground">{step.description}</p>
</div>
</div>
))}
</div>
</div>
</div>
</section>
<!-- Open Source Section -->
<section class="px-6 py-24 sm:py-32 lg:px-8 bg-muted/30">
<div class="mx-auto max-w-7xl text-center">
<div class="mx-auto max-w-2xl">
<h2 class="text-3xl font-bold tracking-tight sm:text-4xl mb-6">Free & Open Source</h2>
<p class="text-lg leading-8 text-muted-foreground mb-8">
Gitea Mirror is completely free and open source. Self-host with confidence knowing you have
full control over your infrastructure and data.
</p>
<div class="rounded-lg border bg-card/50 p-6 mb-8">
<p class="text-sm text-muted-foreground">
<span class="font-semibold text-foreground">Coming Soon:</span> Support development with an optional
supporter tier ($20/year) for priority features and dedicated support.
</p>
</div>
<div class="flex items-center justify-center gap-4 flex-wrap">
<Button size="lg" asChild>
<a href="https://github.com/yourusername/gitea-mirror" target="_blank" rel="noopener noreferrer">
View Source Code
<svg className="ml-2 h-4 w-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
</a>
</Button>
</div>
</div>
</div>
</section>
<!-- Footer -->
<footer class="border-t">
<div class="mx-auto max-w-7xl px-6 py-12 lg:px-8">
<div class="flex flex-col items-center justify-between gap-4 sm:flex-row">
<p class="text-sm text-muted-foreground">
© 2025 Gitea Mirror. Open source under MIT License.
</p>
<div class="flex gap-6">
<a href="https://github.com/yourusername/gitea-mirror" class="text-sm text-muted-foreground hover:text-foreground transition-colors">
GitHub
</a>
<a href="https://github.com/yourusername/gitea-mirror/wiki" class="text-sm text-muted-foreground hover:text-foreground transition-colors">
Documentation
</a>
<a href="https://github.com/yourusername/gitea-mirror/issues" class="text-sm text-muted-foreground hover:text-foreground transition-colors">
Support
</a>
</div>
</div>
</div>
</footer>
<Footer client:load />
<style>
/* Smooth scroll for screenshot carousel */
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
/* Smooth scrolling */
html {
scroll-behavior: smooth;
}
/* CSS Animations */
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(10px);
/* Blob animation */
@keyframes blob {
0% {
transform: translate(0px, 0px) scale(1);
}
to {
opacity: 1;
transform: translateY(0);
33% {
transform: translate(30px, -50px) scale(1.1);
}
66% {
transform: translate(-20px, 20px) scale(0.9);
}
100% {
transform: translate(0px, 0px) scale(1);
}
}
@keyframes fade-in-up {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
.animate-blob {
animation: blob 7s infinite;
}
@keyframes fade-in-left {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
.animation-delay-2000 {
animation-delay: 2s;
}
@keyframes float {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-20px);
}
.animation-delay-4000 {
animation-delay: 4s;
}
.animate-fade-in {
animation: fade-in 0.6s ease-out forwards;
}
.animate-fade-in-up {
opacity: 0;
animation: fade-in-up 0.6s ease-out forwards;
}
.animate-fade-in-left {
opacity: 0;
animation: fade-in-left 0.6s ease-out forwards;
}
.animate-float {
animation: float 6s ease-in-out infinite;
}
.animate-float-delayed {
animation: float 6s ease-in-out 3s infinite;
}
.animation-delay-200 {
animation-delay: 200ms;
/* Grid background pattern */
.bg-grid-white\/10 {
background-image: linear-gradient(to right, rgba(255, 255, 255, 0.1) 1px, transparent 1px),
linear-gradient(to bottom, rgba(255, 255, 255, 0.1) 1px, transparent 1px);
background-size: 20px 20px;
}
</style>
</body>

View File

@@ -1,16 +0,0 @@
---
title: 'Markdown + Tailwind'
layout: ../layouts/main.astro
---
<div class="grid place-items-center h-screen content-center">
<div class="py-2 px-4 bg-purple-500 text-white font-semibold rounded-lg shadow-md">
Tailwind classes also work in Markdown!
</div>
<a
href="/"
class="p-4 underline hover:text-purple-500 transition-colors ease-in-out duration-200"
>
Go home
</a>
</div>

View File

@@ -2,6 +2,7 @@
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@custom-media --xs (width >= 475px);
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
@@ -42,72 +43,72 @@
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--radius: 0.5rem;
--background: oklch(0.99 0 0);
--foreground: oklch(0.15 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--card-foreground: oklch(0.15 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--popover-foreground: oklch(0.15 0 0);
--primary: oklch(0.5 0.2 250);
--primary-foreground: oklch(0.98 0 0);
--secondary: oklch(0.96 0 0);
--secondary-foreground: oklch(0.15 0 0);
--muted: oklch(0.96 0 0);
--muted-foreground: oklch(0.45 0 0);
--accent: oklch(0.96 0 0);
--accent-foreground: oklch(0.15 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--border: oklch(0.9 0 0);
--input: oklch(0.9 0 0);
--ring: oklch(0.5 0.2 250);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
--sidebar: oklch(0.98 0 0);
--sidebar-foreground: oklch(0.15 0 0);
--sidebar-primary: oklch(0.5 0.2 250);
--sidebar-primary-foreground: oklch(0.98 0 0);
--sidebar-accent: oklch(0.96 0 0);
--sidebar-accent-foreground: oklch(0.15 0 0);
--sidebar-border: oklch(0.9 0 0);
--sidebar-ring: oklch(0.5 0.2 250);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--background: oklch(0.1 0 0);
--foreground: oklch(0.95 0 0);
--card: oklch(0.15 0 0);
--card-foreground: oklch(0.95 0 0);
--popover: oklch(0.15 0 0);
--popover-foreground: oklch(0.95 0 0);
--primary: oklch(0.6 0.2 250);
--primary-foreground: oklch(0.1 0 0);
--secondary: oklch(0.2 0 0);
--secondary-foreground: oklch(0.95 0 0);
--muted: oklch(0.25 0 0);
--muted-foreground: oklch(0.65 0 0);
--accent: oklch(0.25 0 0);
--accent-foreground: oklch(0.95 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--border: oklch(0.3 0 0);
--input: oklch(0.3 0 0);
--ring: oklch(0.6 0.2 250);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
--sidebar: oklch(0.15 0 0);
--sidebar-foreground: oklch(0.95 0 0);
--sidebar-primary: oklch(0.6 0.2 250);
--sidebar-primary-foreground: oklch(0.95 0 0);
--sidebar-accent: oklch(0.25 0 0);
--sidebar-accent-foreground: oklch(0.95 0 0);
--sidebar-border: oklch(0.3 0 0);
--sidebar-ring: oklch(0.6 0.2 250);
}
@layer base {