mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-09 21:16:48 +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 { Header } from '../components/Header';
|
||||
import { Hero } from '../components/Hero';
|
||||
import { Features } from '../components/Features';
|
||||
import { Screenshots } from '../components/Screenshots';
|
||||
import Features from '../components/Features.astro';
|
||||
import Screenshots from '../components/Screenshots.astro';
|
||||
import { Installation } from '../components/Installation';
|
||||
import { CTA } from '../components/CTA';
|
||||
import { Footer } from '../components/Footer';
|
||||
import Footer from '../components/Footer.astro';
|
||||
|
||||
const siteUrl = 'https://gitea-mirror.com';
|
||||
const title = 'Gitea Mirror - Automated GitHub to Gitea Repository Mirroring & Backup';
|
||||
@@ -118,13 +118,13 @@ const structuredData = {
|
||||
|
||||
<main>
|
||||
<Hero client:load />
|
||||
<Features client:load />
|
||||
<Screenshots client:load />
|
||||
<Features />
|
||||
<Screenshots />
|
||||
<Installation client:load />
|
||||
<CTA client:load />
|
||||
</main>
|
||||
|
||||
<Footer client:load />
|
||||
<Footer />
|
||||
|
||||
<style>
|
||||
/* Smooth scrolling */
|
||||
|
||||
Reference in New Issue
Block a user