mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-09 13:06:45 +03:00
Optimised static comp with .astro
This commit is contained in:
90
www/src/components/Features.astro
Normal file
90
www/src/components/Features.astro
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
---
|
||||||
|
import {
|
||||||
|
RefreshCw,
|
||||||
|
Building2,
|
||||||
|
FolderTree,
|
||||||
|
Activity,
|
||||||
|
Lock,
|
||||||
|
Heart,
|
||||||
|
} 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-primary/10 to-accent/10",
|
||||||
|
iconColor: "text-primary"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Bulk Operations",
|
||||||
|
description: "Mirror entire organizations or user accounts with a single configuration.",
|
||||||
|
icon: Building2,
|
||||||
|
gradient: "from-accent/10 to-accent-teal/10",
|
||||||
|
iconColor: "text-accent"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Preserve Structure",
|
||||||
|
description: "Maintain your GitHub organization structure or customize how repos are organized.",
|
||||||
|
icon: FolderTree,
|
||||||
|
gradient: "from-accent-teal/10 to-primary/10",
|
||||||
|
iconColor: "text-accent-teal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Real-time Status",
|
||||||
|
description: "Monitor mirror progress with live updates and detailed activity logs.",
|
||||||
|
icon: Activity,
|
||||||
|
gradient: "from-accent-coral/10 to-primary/10",
|
||||||
|
iconColor: "text-accent-coral"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Secure & Private",
|
||||||
|
description: "Self-hosted solution keeps your code on your infrastructure with full control.",
|
||||||
|
icon: Lock,
|
||||||
|
gradient: "from-accent-purple/10 to-primary/10",
|
||||||
|
iconColor: "text-accent-purple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Open Source",
|
||||||
|
description: "Free, transparent, and community-driven development. Contribute and customize.",
|
||||||
|
icon: Heart,
|
||||||
|
gradient: "from-primary/10 to-accent-purple/10",
|
||||||
|
iconColor: "text-primary"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
---
|
||||||
|
|
||||||
|
<section id="features" class="py-16 sm:py-24 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="max-w-7xl mx-auto">
|
||||||
|
<div class="text-center mb-12 sm:mb-16">
|
||||||
|
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold tracking-tight px-4">
|
||||||
|
Everything You Need for
|
||||||
|
<span class="text-gradient from-primary to-accent block sm:inline"> Reliable Backups</span>
|
||||||
|
</h2>
|
||||||
|
<p class="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 class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6 lg:gap-8">
|
||||||
|
{features.map((feature) => {
|
||||||
|
const Icon = feature.icon;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class={`group relative p-6 sm:p-8 rounded-xl sm:rounded-2xl border bg-gradient-to-br ${feature.gradient} backdrop-blur-sm hover:shadow-lg hover:shadow-primary/10 transition-all duration-300 hover:-translate-y-1 hover:border-primary/30 overflow-hidden`}
|
||||||
|
>
|
||||||
|
<div class="absolute inset-0 bg-gradient-to-br from-transparent to-background/50 opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
||||||
|
<div class="relative">
|
||||||
|
<div class={`inline-flex p-2.5 sm:p-3 rounded-lg bg-background/80 backdrop-blur-sm mb-3 sm:mb-4 ${feature.iconColor} shadow-sm`}>
|
||||||
|
<Icon className="w-5 h-5 sm:w-6 sm:h-6" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="text-lg sm:text-xl font-semibold mb-2">{feature.title}</h3>
|
||||||
|
<p class="text-sm sm:text-base text-muted-foreground">{feature.description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import {
|
|
||||||
RefreshCw,
|
|
||||||
Building2,
|
|
||||||
FolderTree,
|
|
||||||
Activity,
|
|
||||||
Lock,
|
|
||||||
Heart,
|
|
||||||
} 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-primary/10 to-accent/10",
|
|
||||||
iconColor: "text-primary"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Bulk Operations",
|
|
||||||
description: "Mirror entire organizations or user accounts with a single configuration.",
|
|
||||||
icon: Building2,
|
|
||||||
gradient: "from-accent/10 to-accent-teal/10",
|
|
||||||
iconColor: "text-accent"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Preserve Structure",
|
|
||||||
description: "Maintain your GitHub organization structure or customize how repos are organized.",
|
|
||||||
icon: FolderTree,
|
|
||||||
gradient: "from-accent-teal/10 to-primary/10",
|
|
||||||
iconColor: "text-accent-teal"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Real-time Status",
|
|
||||||
description: "Monitor mirror progress with live updates and detailed activity logs.",
|
|
||||||
icon: Activity,
|
|
||||||
gradient: "from-accent-coral/10 to-primary/10",
|
|
||||||
iconColor: "text-accent-coral"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Secure & Private",
|
|
||||||
description: "Self-hosted solution keeps your code on your infrastructure with full control.",
|
|
||||||
icon: Lock,
|
|
||||||
gradient: "from-accent-purple/10 to-primary/10",
|
|
||||||
iconColor: "text-accent-purple"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Open Source",
|
|
||||||
description: "Free, transparent, and community-driven development. Contribute and customize.",
|
|
||||||
icon: Heart,
|
|
||||||
gradient: "from-primary/10 to-accent-purple/10",
|
|
||||||
iconColor: "text-primary"
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
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="text-gradient from-primary to-accent 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-gradient-to-br ${feature.gradient} backdrop-blur-sm hover:shadow-lg hover:shadow-primary/10 transition-all duration-300 hover:-translate-y-1 hover:border-primary/30 overflow-hidden`}
|
|
||||||
>
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-br from-transparent to-background/50 opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
|
||||||
<div className="relative">
|
|
||||||
<div className={`inline-flex p-2.5 sm:p-3 rounded-lg bg-background/80 backdrop-blur-sm mb-3 sm:mb-4 ${feature.iconColor} shadow-sm`}>
|
|
||||||
<Icon className="w-5 h-5 sm:w-6 sm:h-6" />
|
|
||||||
</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>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
88
www/src/components/Footer.astro
Normal file
88
www/src/components/Footer.astro
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
---
|
||||||
|
import { Github, Book, MessageSquare, Bug } from 'lucide-react';
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
---
|
||||||
|
|
||||||
|
<footer class="border-t py-8 sm:py-12 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="max-w-7xl mx-auto">
|
||||||
|
<div class="flex flex-col items-center gap-6 sm:gap-8">
|
||||||
|
<!-- Logo and tagline -->
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="flex items-center justify-center gap-2 mb-2">
|
||||||
|
<img
|
||||||
|
src="/logo-light.svg"
|
||||||
|
alt="Gitea Mirror"
|
||||||
|
class="w-6 h-6 sm:w-8 sm:h-8 dark:hidden"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src="/logo-dark.svg"
|
||||||
|
alt="Gitea Mirror"
|
||||||
|
class="w-6 h-6 sm:w-8 sm:h-8 hidden dark:block"
|
||||||
|
/>
|
||||||
|
<span class="font-semibold text-base sm:text-lg">Gitea Mirror</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs sm:text-sm text-muted-foreground">
|
||||||
|
Keep your GitHub code safe and synced
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Links -->
|
||||||
|
<nav class="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
|
||||||
|
href={link.href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="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 class="text-center text-xs sm:text-sm text-muted-foreground px-4">
|
||||||
|
<p>© {currentYear} Gitea Mirror. Open source under GPL-3.0 License.</p>
|
||||||
|
<p class="mt-1">
|
||||||
|
Made with dedication by the{' '}
|
||||||
|
<a
|
||||||
|
href="https://github.com/RayLabsHQ"
|
||||||
|
class="underline hover:text-foreground transition-colors"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
RayLabs team
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
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="/logo-light.svg"
|
|
||||||
alt="Gitea Mirror"
|
|
||||||
className="w-6 h-6 sm:w-8 sm:h-8 dark:hidden"
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
src="/logo-dark.svg"
|
|
||||||
alt="Gitea Mirror"
|
|
||||||
className="w-6 h-6 sm:w-8 sm:h-8 hidden dark:block"
|
|
||||||
/>
|
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
245
www/src/components/Screenshots.astro
Normal file
245
www/src/components/Screenshots.astro
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
---
|
||||||
|
import { Image } from 'astro:assets';
|
||||||
|
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||||
|
import { Button } from './ui/button';
|
||||||
|
|
||||||
|
// Import all images
|
||||||
|
import dashboardDesktop from '../../public/assets/dashboard.png';
|
||||||
|
import dashboardMobile from '../../public/assets/dashboard_mobile.png';
|
||||||
|
import organisationDesktop from '../../public/assets/organisation.png';
|
||||||
|
import organisationMobile from '../../public/assets/organisation_mobile.png';
|
||||||
|
import repositoriesDesktop from '../../public/assets/repositories.png';
|
||||||
|
import repositoriesMobile from '../../public/assets/repositories_mobile.png';
|
||||||
|
import configurationDesktop from '../../public/assets/configuration.png';
|
||||||
|
import configurationMobile from '../../public/assets/configuration_mobile.png';
|
||||||
|
import activityDesktop from '../../public/assets/activity.png';
|
||||||
|
import activityMobile from '../../public/assets/activity_mobile.png';
|
||||||
|
|
||||||
|
const screenshots = [
|
||||||
|
{
|
||||||
|
title: "Dashboard Overview",
|
||||||
|
description: "Monitor all your mirrored repositories in one place",
|
||||||
|
desktop: dashboardDesktop,
|
||||||
|
mobile: dashboardMobile
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Organization Management",
|
||||||
|
description: "Easily manage and sync entire GitHub organizations",
|
||||||
|
desktop: organisationDesktop,
|
||||||
|
mobile: organisationMobile
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Repository Control",
|
||||||
|
description: "Fine-grained control over individual repository mirrors",
|
||||||
|
desktop: repositoriesDesktop,
|
||||||
|
mobile: repositoriesMobile
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Configuration",
|
||||||
|
description: "Simple and intuitive configuration interface",
|
||||||
|
desktop: configurationDesktop,
|
||||||
|
mobile: configurationMobile
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Activity Monitoring",
|
||||||
|
description: "Track sync progress and view detailed logs",
|
||||||
|
desktop: activityDesktop,
|
||||||
|
mobile: activityMobile
|
||||||
|
}
|
||||||
|
];
|
||||||
|
---
|
||||||
|
|
||||||
|
<section id="screenshots" class="py-16 sm:py-24 px-4 sm:px-6 lg:px-8 bg-gradient-to-b from-muted/30 via-primary/5 to-muted/30">
|
||||||
|
<div class="max-w-7xl mx-auto">
|
||||||
|
<div class="text-center mb-8 sm:mb-16">
|
||||||
|
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold tracking-tight">
|
||||||
|
See It In Action
|
||||||
|
</h2>
|
||||||
|
<p class="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 class="relative max-w-5xl mx-auto">
|
||||||
|
<!-- Screenshot viewer -->
|
||||||
|
<div id="screenshot-container" class="relative group">
|
||||||
|
<div class="aspect-[9/16] sm:aspect-[16/10] overflow-hidden rounded-lg sm:rounded-2xl bg-card border shadow-lg">
|
||||||
|
{screenshots.map((screenshot, index) => (
|
||||||
|
<picture data-index={index} class={index === 0 ? 'block' : 'hidden'}>
|
||||||
|
<source media="(max-width: 640px)" srcset={screenshot.mobile.src} />
|
||||||
|
<Image
|
||||||
|
src={screenshot.desktop}
|
||||||
|
alt={screenshot.title}
|
||||||
|
class="w-full h-full object-cover object-top"
|
||||||
|
draggable={false}
|
||||||
|
loading={index === 0 ? 'eager' : 'lazy'}
|
||||||
|
/>
|
||||||
|
</picture>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Navigation buttons -->
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="screenshot-nav-prev 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"
|
||||||
|
aria-label="Previous screenshot"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="screenshot-nav-next 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"
|
||||||
|
aria-label="Next screenshot"
|
||||||
|
>
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Screenshot info -->
|
||||||
|
<div class="mt-6 sm:mt-8 text-center">
|
||||||
|
<h3 id="screenshot-title" class="text-lg sm:text-xl font-semibold">{screenshots[0].title}</h3>
|
||||||
|
<p id="screenshot-description" class="mt-2 text-sm sm:text-base text-muted-foreground">{screenshots[0].description}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dots indicator -->
|
||||||
|
<div class="mt-6 sm:mt-8 flex justify-center gap-2">
|
||||||
|
{screenshots.map((_, index) => (
|
||||||
|
<button
|
||||||
|
data-index={index}
|
||||||
|
class={`screenshot-dot transition-all duration-300 ${
|
||||||
|
index === 0
|
||||||
|
? 'w-8 h-2 bg-primary rounded-full'
|
||||||
|
: 'w-2 h-2 bg-muted-foreground/30 hover:bg-muted-foreground/50 rounded-full'
|
||||||
|
}`}
|
||||||
|
aria-label={`Go to screenshot ${index + 1}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile swipe hint -->
|
||||||
|
<p class="mt-4 text-xs text-muted-foreground text-center sm:hidden">
|
||||||
|
Swipe left or right to navigate
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Thumbnail grid -->
|
||||||
|
<div class="hidden lg:grid grid-cols-5 gap-4 mt-12 px-8">
|
||||||
|
{screenshots.map((screenshot, index) => (
|
||||||
|
<button
|
||||||
|
data-index={index}
|
||||||
|
class={`screenshot-thumb relative overflow-hidden rounded-lg transition-all duration-300 ${
|
||||||
|
index === 0
|
||||||
|
? 'ring-2 ring-primary shadow-lg scale-105'
|
||||||
|
: 'opacity-60 hover:opacity-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={screenshot.desktop}
|
||||||
|
alt={screenshot.title}
|
||||||
|
class="w-full h-full object-cover"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script define:vars={{ screenshots }}>
|
||||||
|
let currentIndex = 0;
|
||||||
|
let touchStart = 0;
|
||||||
|
let touchEnd = 0;
|
||||||
|
const minSwipeDistance = 50;
|
||||||
|
|
||||||
|
const container = document.getElementById('screenshot-container');
|
||||||
|
const pictures = container.querySelectorAll('picture');
|
||||||
|
const dots = document.querySelectorAll('.screenshot-dot');
|
||||||
|
const thumbs = document.querySelectorAll('.screenshot-thumb');
|
||||||
|
const titleEl = document.getElementById('screenshot-title');
|
||||||
|
const descriptionEl = document.getElementById('screenshot-description');
|
||||||
|
const prevBtn = container.querySelector('.screenshot-nav-prev');
|
||||||
|
const nextBtn = container.querySelector('.screenshot-nav-next');
|
||||||
|
|
||||||
|
function updateView(newIndex) {
|
||||||
|
// Hide current, show new
|
||||||
|
pictures[currentIndex].classList.add('hidden');
|
||||||
|
pictures[newIndex].classList.remove('hidden');
|
||||||
|
|
||||||
|
// Update dots
|
||||||
|
dots[currentIndex].classList.remove('w-8', 'h-2', 'bg-primary');
|
||||||
|
dots[currentIndex].classList.add('w-2', 'h-2', 'bg-muted-foreground/30');
|
||||||
|
dots[newIndex].classList.remove('w-2', 'h-2', 'bg-muted-foreground/30');
|
||||||
|
dots[newIndex].classList.add('w-8', 'h-2', 'bg-primary');
|
||||||
|
|
||||||
|
// Update thumbnails
|
||||||
|
if (thumbs.length > 0) {
|
||||||
|
thumbs[currentIndex].classList.remove('ring-2', 'ring-primary', 'shadow-lg', 'scale-105');
|
||||||
|
thumbs[currentIndex].classList.add('opacity-60');
|
||||||
|
thumbs[newIndex].classList.remove('opacity-60');
|
||||||
|
thumbs[newIndex].classList.add('ring-2', 'ring-primary', 'shadow-lg', 'scale-105');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update text
|
||||||
|
titleEl.textContent = screenshots[newIndex].title;
|
||||||
|
descriptionEl.textContent = screenshots[newIndex].description;
|
||||||
|
|
||||||
|
currentIndex = newIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToPrevious() {
|
||||||
|
const newIndex = currentIndex === 0 ? screenshots.length - 1 : currentIndex - 1;
|
||||||
|
updateView(newIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToNext() {
|
||||||
|
const newIndex = (currentIndex + 1) % screenshots.length;
|
||||||
|
updateView(newIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Touch handling
|
||||||
|
container.addEventListener('touchstart', (e) => {
|
||||||
|
touchEnd = 0;
|
||||||
|
touchStart = e.targetTouches[0].clientX;
|
||||||
|
});
|
||||||
|
|
||||||
|
container.addEventListener('touchmove', (e) => {
|
||||||
|
touchEnd = e.targetTouches[0].clientX;
|
||||||
|
});
|
||||||
|
|
||||||
|
container.addEventListener('touchend', () => {
|
||||||
|
if (!touchStart || !touchEnd) return;
|
||||||
|
|
||||||
|
const distance = touchStart - touchEnd;
|
||||||
|
const isLeftSwipe = distance > minSwipeDistance;
|
||||||
|
const isRightSwipe = distance < -minSwipeDistance;
|
||||||
|
|
||||||
|
if (isLeftSwipe && currentIndex < screenshots.length - 1) {
|
||||||
|
goToNext();
|
||||||
|
}
|
||||||
|
if (isRightSwipe && currentIndex > 0) {
|
||||||
|
goToPrevious();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Button navigation
|
||||||
|
prevBtn?.addEventListener('click', goToPrevious);
|
||||||
|
nextBtn?.addEventListener('click', goToNext);
|
||||||
|
|
||||||
|
// Dot navigation
|
||||||
|
dots.forEach((dot, index) => {
|
||||||
|
dot.addEventListener('click', () => updateView(index));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Thumbnail navigation
|
||||||
|
thumbs.forEach((thumb, index) => {
|
||||||
|
thumb.addEventListener('click', () => updateView(index));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keyboard navigation
|
||||||
|
window.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'ArrowLeft') goToPrevious();
|
||||||
|
if (e.key === 'ArrowRight') goToNext();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
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-gradient-to-b from-muted/30 via-primary/5 to-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-[9/16] sm:aspect-[16/10] overflow-hidden rounded-lg sm:rounded-2xl bg-card border shadow-lg">
|
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -2,11 +2,11 @@
|
|||||||
import '../styles/global.css';
|
import '../styles/global.css';
|
||||||
import { Header } from '../components/Header';
|
import { Header } from '../components/Header';
|
||||||
import { Hero } from '../components/Hero';
|
import { Hero } from '../components/Hero';
|
||||||
import { Features } from '../components/Features';
|
import Features from '../components/Features.astro';
|
||||||
import { Screenshots } from '../components/Screenshots';
|
import Screenshots from '../components/Screenshots.astro';
|
||||||
import { Installation } from '../components/Installation';
|
import { Installation } from '../components/Installation';
|
||||||
import { CTA } from '../components/CTA';
|
import { CTA } from '../components/CTA';
|
||||||
import { Footer } from '../components/Footer';
|
import Footer from '../components/Footer.astro';
|
||||||
|
|
||||||
const siteUrl = 'https://gitea-mirror.com';
|
const siteUrl = 'https://gitea-mirror.com';
|
||||||
const title = 'Gitea Mirror - Automated GitHub to Gitea Repository Mirroring & Backup';
|
const title = 'Gitea Mirror - Automated GitHub to Gitea Repository Mirroring & Backup';
|
||||||
@@ -118,13 +118,13 @@ const structuredData = {
|
|||||||
|
|
||||||
<main>
|
<main>
|
||||||
<Hero client:load />
|
<Hero client:load />
|
||||||
<Features client:load />
|
<Features />
|
||||||
<Screenshots client:load />
|
<Screenshots />
|
||||||
<Installation client:load />
|
<Installation client:load />
|
||||||
<CTA client:load />
|
<CTA client:load />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<Footer client:load />
|
<Footer />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Smooth scrolling */
|
/* Smooth scrolling */
|
||||||
|
|||||||
Reference in New Issue
Block a user