@@ -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>
|
|
||||||
24
www/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# build output
|
||||||
|
dist/
|
||||||
|
# generated types
|
||||||
|
.astro/
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
|
||||||
|
# environment variables
|
||||||
|
.env
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# macOS-specific files
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# jetbrains setting folder
|
||||||
|
.idea/
|
||||||
92
www/CLAUDE.md
Normal 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
|
||||||
13
www/README.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Astro with Tailwind
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bun create astro@latest -- --template with-tailwindcss
|
||||||
|
```
|
||||||
|
|
||||||
|
[](https://stackblitz.com/github/withastro/astro/tree/latest/examples/with-tailwindcss)
|
||||||
|
[](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/with-tailwindcss)
|
||||||
|
[](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/with-tailwindcss/devcontainer.json)
|
||||||
|
|
||||||
|
Astro comes with [Tailwind](https://tailwindcss.com) support out of the box. This example showcases how to style your Astro project with Tailwind.
|
||||||
|
|
||||||
|
For complete setup instructions, please see our [Tailwind Integration Guide](https://docs.astro.build/en/guides/integrations-guide/tailwind).
|
||||||
14
www/astro.config.mjs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// @ts-check
|
||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
|
||||||
|
import react from '@astrojs/react';
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
vite: {
|
||||||
|
plugins: [tailwindcss()]
|
||||||
|
},
|
||||||
|
|
||||||
|
integrations: [react()]
|
||||||
|
});
|
||||||
1025
www/bun.lock
Normal file
21
www/components.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": false,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "",
|
||||||
|
"css": "src/styles/global.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide"
|
||||||
|
}
|
||||||
33
www/package.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "www",
|
||||||
|
"type": "module",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"build": "astro build",
|
||||||
|
"preview": "astro preview",
|
||||||
|
"astro": "astro"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/mdx": "^4.3.0",
|
||||||
|
"@astrojs/react": "^4.3.0",
|
||||||
|
"@radix-ui/react-icons": "^1.3.2",
|
||||||
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
|
"@tailwindcss/vite": "^4.1.3",
|
||||||
|
"@types/canvas-confetti": "^1.9.0",
|
||||||
|
"@types/react": "^19.1.8",
|
||||||
|
"@types/react-dom": "^19.1.6",
|
||||||
|
"astro": "^5.11.0",
|
||||||
|
"canvas-confetti": "^1.9.3",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"lucide-react": "^0.525.0",
|
||||||
|
"react": "^19.1.0",
|
||||||
|
"react-dom": "^19.1.0",
|
||||||
|
"tailwind-merge": "^3.3.1",
|
||||||
|
"tailwindcss": "^4.1.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"tw-animate-css": "^1.3.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
www/public/assets/activity.png
Normal file
|
After Width: | Height: | Size: 854 KiB |
BIN
www/public/assets/activity_mobile.png
Normal file
|
After Width: | Height: | Size: 276 KiB |
BIN
www/public/assets/configuration.png
Normal file
|
After Width: | Height: | Size: 950 KiB |
BIN
www/public/assets/configuration_mobile.png
Normal file
|
After Width: | Height: | Size: 255 KiB |
BIN
www/public/assets/dashboard.png
Normal file
|
After Width: | Height: | Size: 943 KiB |
BIN
www/public/assets/dashboard_mobile.png
Normal file
|
After Width: | Height: | Size: 215 KiB |
BIN
www/public/assets/logo-no-bg.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
www/public/assets/logo.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
www/public/assets/organisation.png
Normal file
|
After Width: | Height: | Size: 844 KiB |
BIN
www/public/assets/organisation_mobile.png
Normal file
|
After Width: | Height: | Size: 221 KiB |
BIN
www/public/assets/repositories.png
Normal file
|
After Width: | Height: | Size: 970 KiB |
BIN
www/public/assets/repositories_mobile.png
Normal file
|
After Width: | Height: | Size: 227 KiB |
200
www/public/favicon.svg
Normal file
|
After Width: | Height: | Size: 21 KiB |
16
www/public/logo-dark.svg
Normal file
|
After Width: | Height: | Size: 13 KiB |
16
www/public/logo-light.svg
Normal file
|
After Width: | Height: | Size: 13 KiB |
16
www/public/logo.svg
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
www/public/og-image.png
Normal file
|
After Width: | Height: | Size: 338 KiB |
8
www/public/robots.txt
Normal 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
@@ -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>
|
||||||
19
www/src/components/Button.astro
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
// Click button, get confetti!
|
||||||
|
// Styled by Tailwind :)
|
||||||
|
---
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="appearance-none py-2 px-4 bg-purple-500 text-white font-semibold rounded-lg shadow-md hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-400 focus:ring-opacity-75"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import confetti from 'canvas-confetti';
|
||||||
|
const button = document.body.querySelector('button');
|
||||||
|
|
||||||
|
if (button) {
|
||||||
|
button.addEventListener('click', () => confetti());
|
||||||
|
}
|
||||||
|
</script>
|
||||||
60
www/src/components/CTA.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Button } from './ui/button';
|
||||||
|
import { ArrowRight } from 'lucide-react';
|
||||||
|
import { GitHubStats } from './GitHubStats';
|
||||||
|
|
||||||
|
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-card/80 backdrop-blur-sm border border-primary/10 p-6 sm:p-8 md:p-12 text-center shadow-xl">
|
||||||
|
{/* Subtle gradient accent */}
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-transparent to-accent/5 pointer-events-none" />
|
||||||
|
<div className="absolute -top-24 -right-24 w-48 h-48 bg-primary/20 rounded-full blur-3xl pointer-events-none" />
|
||||||
|
<div className="absolute -bottom-24 -left-24 w-48 h-48 bg-accent/20 rounded-full blur-3xl pointer-events-none" />
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
<h2 className="text-2xl sm:text-3xl md:text-4xl font-bold mb-4">
|
||||||
|
<span className="block sm:inline">Start Protecting</span>
|
||||||
|
<span className="text-gradient from-primary via-accent to-accent-purple block sm:inline"> Your Code Today</span>
|
||||||
|
</h2>
|
||||||
|
<p className="text-base sm:text-lg text-muted-foreground 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 */}
|
||||||
|
<GitHubStats />
|
||||||
|
|
||||||
|
<div className="flex flex-col sm:flex-row items-center justify-center gap-3 sm:gap-4">
|
||||||
|
<Button size="lg" className="group w-full sm:w-auto min-h-[48px] bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90 shadow-lg shadow-primary/25 hover:shadow-xl hover:shadow-primary/30 transition-all duration-300" 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="outline" className="w-full sm:w-auto min-h-[48px] bg-background/80 backdrop-blur-sm hover:bg-primary/10 hover:border-primary/30 hover:text-foreground transition-all duration-300" 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
94
www/src/components/Features.tsx
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
90
www/src/components/Footer.tsx
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
60
www/src/components/GitHubButton.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Github, Star } from 'lucide-react';
|
||||||
|
import { Button } from './ui/button';
|
||||||
|
|
||||||
|
export function GitHubButton() {
|
||||||
|
const [stars, setStars] = useState<number | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchStars = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('https://api.github.com/repos/RayLabsHQ/gitea-mirror');
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setStars(data.stargazers_count);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch GitHub stars:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchStars();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Mobile version - compact with text */}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="md:hidden hover:bg-primary/10 hover:border-primary/30 hover:text-foreground transition-all duration-300 px-3"
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<a href="https://github.com/RayLabsHQ/gitea-mirror" target="_blank" rel="noopener noreferrer" className="flex items-center gap-1.5">
|
||||||
|
<Star className="w-4 h-4" />
|
||||||
|
<span className="font-semibold">{stars || '—'}</span>
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Desktop version - full button */}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="hidden md:flex hover:bg-primary/10 hover:border-primary/30 hover:text-foreground transition-all duration-300"
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<a href="https://github.com/RayLabsHQ/gitea-mirror" target="_blank" rel="noopener noreferrer" className="flex items-center">
|
||||||
|
<Github className="w-4 h-4 mr-2" />
|
||||||
|
<span>Star on GitHub</span>
|
||||||
|
{stars !== null && (
|
||||||
|
<>
|
||||||
|
<span className="mx-2 text-muted-foreground">•</span>
|
||||||
|
<Star className="w-3 h-3 mr-1" />
|
||||||
|
<span>{stars}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
59
www/src/components/GitHubStats.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Star, GitFork, Users } from 'lucide-react';
|
||||||
|
|
||||||
|
interface GitHubRepo {
|
||||||
|
stargazers_count: number;
|
||||||
|
forks_count: number;
|
||||||
|
open_issues_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GitHubStats() {
|
||||||
|
const [stats, setStats] = useState<GitHubRepo | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchStats = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('https://api.github.com/repos/RayLabsHQ/gitea-mirror');
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setStats(data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch GitHub stats:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchStats();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<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">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-wrap items-center justify-center gap-4 sm:gap-6 md:gap-8 mb-6 sm:mb-8 text-foreground 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">{stats?.stargazers_count || 0} 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">{stats?.forks_count || 0} 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
73
www/src/components/Header.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { ThemeToggle } from './ThemeToggle';
|
||||||
|
import { GitHubButton } from './GitHubButton';
|
||||||
|
|
||||||
|
export function Header() {
|
||||||
|
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' }
|
||||||
|
];
|
||||||
|
|
||||||
|
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="/logo-light.svg"
|
||||||
|
alt="Gitea Mirror Logo"
|
||||||
|
className="h-6 w-6 dark:hidden"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src="/logo-dark.svg"
|
||||||
|
alt="Gitea Mirror Logo"
|
||||||
|
className="h-6 w-6 hidden dark:block"
|
||||||
|
/>
|
||||||
|
<span className="text-lg sm:text-xl font-bold">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 />
|
||||||
|
<GitHubButton />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Actions */}
|
||||||
|
<div className="flex md:hidden items-center gap-3">
|
||||||
|
<GitHubButton />
|
||||||
|
<ThemeToggle />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
78
www/src/components/Hero.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Button } from './ui/button';
|
||||||
|
import { ArrowRight, Shield, RefreshCw } from 'lucide-react';
|
||||||
|
import { GitHubLogoIcon } from '@radix-ui/react-icons';
|
||||||
|
|
||||||
|
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">
|
||||||
|
{/* Elegant gradient background */}
|
||||||
|
<div className="absolute inset-0 -z-10 overflow-hidden">
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-transparent to-accent/5"></div>
|
||||||
|
<div className="absolute -top-1/2 -left-1/2 w-full h-full bg-gradient-radial from-primary/10 to-transparent blur-3xl"></div>
|
||||||
|
<div className="absolute -bottom-1/2 -right-1/2 w-full h-full bg-gradient-radial from-accent/10 to-transparent blur-3xl"></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">
|
||||||
|
<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 dark:hidden"
|
||||||
|
/>
|
||||||
|
<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 hidden dark:block"
|
||||||
|
/>
|
||||||
|
</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="text-foreground">
|
||||||
|
Keep Your Code
|
||||||
|
</span>
|
||||||
|
<br />
|
||||||
|
<span className="text-gradient from-primary via-accent to-accent-purple">
|
||||||
|
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 px-3 py-1 rounded-full bg-primary/10 text-primary">
|
||||||
|
<Shield className="w-3 h-3 sm:w-4 sm:h-4" />
|
||||||
|
<span className="font-medium">Self-Hosted</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 px-3 py-1 rounded-full bg-accent/10 text-accent">
|
||||||
|
<RefreshCw className="w-3 h-3 sm:w-4 sm:h-4" />
|
||||||
|
<span className="font-medium">Auto-Sync</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 px-3 py-1 rounded-full bg-accent-purple/10 text-accent-purple">
|
||||||
|
<GitHubLogoIcon className="w-3 h-3 sm:w-4 sm:h-4" />
|
||||||
|
<span className="font-medium">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 bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90 shadow-lg shadow-primary/25 hover:shadow-xl hover:shadow-primary/30 transition-all duration-300" 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 border-primary/20 hover:bg-primary/10 hover:border-primary/30 hover:text-foreground transition-all duration-300" asChild>
|
||||||
|
<a href="#features">
|
||||||
|
View Features
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
169
www/src/components/Installation.tsx
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
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-gradient-to-r from-primary to-accent text-primary-foreground border-transparent shadow-lg shadow-primary/25'
|
||||||
|
: 'bg-card hover:bg-muted border-border hover:border-primary/30'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<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-gradient-to-br from-primary to-accent flex items-center justify-center shadow-md shadow-primary/20">
|
||||||
|
<span className="text-sm font-semibold text-primary-foreground">{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">
|
||||||
|
<div className="relative overflow-hidden rounded-lg">
|
||||||
|
<pre className="bg-muted/50 p-3 sm:p-4 pr-10 sm:pr-12 overflow-x-auto text-[11px] sm:text-sm font-mono scrollbar-thin scrollbar-thumb-muted-foreground/20 scrollbar-track-transparent">
|
||||||
|
<code className="block whitespace-nowrap">{step.command}</code>
|
||||||
|
</pre>
|
||||||
|
{/* Scroll indicator gradient for mobile */}
|
||||||
|
<div className="absolute inset-y-0 right-0 w-8 bg-gradient-to-l from-muted/50 to-transparent pointer-events-none sm:hidden" />
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="absolute top-1 right-1 sm:top-2 sm:right-2 w-7 h-7 sm:w-9 sm:h-9 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity z-10"
|
||||||
|
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>
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
200
www/src/components/Screenshots.tsx
Normal 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-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>
|
||||||
|
);
|
||||||
|
}
|
||||||
40
www/src/components/ThemeToggle.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
59
www/src/components/ui/button.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
|
outline:
|
||||||
|
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||||
|
ghost:
|
||||||
|
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||||
|
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||||
|
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||||
|
icon: "size-9",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Button({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"button"> &
|
||||||
|
VariantProps<typeof buttonVariants> & {
|
||||||
|
asChild?: boolean
|
||||||
|
}) {
|
||||||
|
const Comp = asChild ? Slot : "button"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="button"
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
||||||
16
www/src/layouts/main.astro
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
import '../styles/global.css';
|
||||||
|
const { content } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<title>{content.title}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<slot />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
6
www/src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { clsx, type ClassValue } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
186
www/src/pages/index.astro
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
---
|
||||||
|
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 { Installation } from '../components/Installation';
|
||||||
|
import { CTA } from '../components/CTA';
|
||||||
|
import { Footer } from '../components/Footer';
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
|
// 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} />
|
||||||
|
|
||||||
|
<!-- 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 / Facebook -->
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:url" content={siteUrl} />
|
||||||
|
<meta property="og:title" content={title} />
|
||||||
|
<meta property="og:description" content={description} />
|
||||||
|
<meta property="og:image" content={`${siteUrl}/og-image.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 -->
|
||||||
|
<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}/og-image.png`} />
|
||||||
|
<meta name="twitter:creator" content="@RayLabsHQ" />
|
||||||
|
|
||||||
|
<!-- Canonical URL -->
|
||||||
|
<link rel="canonical" href={siteUrl} />
|
||||||
|
|
||||||
|
<!-- Additional Meta Tags -->
|
||||||
|
<meta name="theme-color" content="#5b6fff" />
|
||||||
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
|
<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" is:inline 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>
|
||||||
|
|
||||||
|
<!-- 100% privacy-first analytics -->
|
||||||
|
<script async src="https://scripts.simpleanalyticscdn.com/latest.js" is:inline></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="min-h-screen bg-background text-foreground antialiased">
|
||||||
|
<Header client:load />
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<Hero client:load />
|
||||||
|
<Features client:load />
|
||||||
|
<Screenshots client:load />
|
||||||
|
<Installation client:load />
|
||||||
|
<CTA client:load />
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<Footer client:load />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Smooth scrolling */
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Blob animation */
|
||||||
|
@keyframes blob {
|
||||||
|
0% {
|
||||||
|
transform: translate(0px, 0px) scale(1);
|
||||||
|
}
|
||||||
|
33% {
|
||||||
|
transform: translate(30px, -50px) scale(1.1);
|
||||||
|
}
|
||||||
|
66% {
|
||||||
|
transform: translate(-20px, 20px) scale(0.9);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate(0px, 0px) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-blob {
|
||||||
|
animation: blob 7s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animation-delay-2000 {
|
||||||
|
animation-delay: 2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animation-delay-4000 {
|
||||||
|
animation-delay: 4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smooth gradient animations */
|
||||||
|
@keyframes gradient-shift {
|
||||||
|
0%, 100% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-gradient {
|
||||||
|
background-size: 200% 200%;
|
||||||
|
animation: gradient-shift 15s ease infinite;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
211
www/src/styles/global.css
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
@import 'tailwindcss';
|
||||||
|
@import "tw-animate-css";
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
@custom-media --xs (width >= 475px);
|
||||||
|
|
||||||
|
@import 'tailwindcss/theme' layer(theme);
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
--color-accent-purple: var(--accent-purple);
|
||||||
|
--color-accent-teal: var(--accent-teal);
|
||||||
|
--color-accent-coral: var(--accent-coral);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--radius: 0.5rem;
|
||||||
|
--background: oklch(0.99 0 0);
|
||||||
|
--foreground: oklch(0.15 0 0);
|
||||||
|
--card: oklch(0.985 0 0);
|
||||||
|
--card-foreground: oklch(0.15 0 0);
|
||||||
|
--popover: oklch(0.985 0 0);
|
||||||
|
--popover-foreground: oklch(0.15 0 0);
|
||||||
|
--primary: oklch(0.55 0.25 255);
|
||||||
|
--primary-foreground: oklch(0.99 0 0);
|
||||||
|
--secondary: oklch(0.96 0.02 240);
|
||||||
|
--secondary-foreground: oklch(0.15 0 0);
|
||||||
|
--muted: oklch(0.96 0.01 240);
|
||||||
|
--muted-foreground: oklch(0.45 0.02 240);
|
||||||
|
--accent: oklch(0.7 0.2 190);
|
||||||
|
--accent-foreground: oklch(0.99 0 0);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.94 0.01 240);
|
||||||
|
--input: oklch(0.94 0.01 240);
|
||||||
|
--ring: oklch(0.55 0.25 255);
|
||||||
|
--chart-1: oklch(0.55 0.25 255);
|
||||||
|
--chart-2: oklch(0.7 0.2 190);
|
||||||
|
--chart-3: oklch(0.7 0.15 150);
|
||||||
|
--chart-4: oklch(0.7 0.2 30);
|
||||||
|
--chart-5: oklch(0.6 0.25 280);
|
||||||
|
--sidebar: oklch(0.985 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.15 0 0);
|
||||||
|
--sidebar-primary: oklch(0.55 0.25 255);
|
||||||
|
--sidebar-primary-foreground: oklch(0.99 0 0);
|
||||||
|
--sidebar-accent: oklch(0.96 0.02 240);
|
||||||
|
--sidebar-accent-foreground: oklch(0.15 0 0);
|
||||||
|
--sidebar-border: oklch(0.94 0.01 240);
|
||||||
|
--sidebar-ring: oklch(0.55 0.25 255);
|
||||||
|
--accent-purple: oklch(0.6 0.25 280);
|
||||||
|
--accent-teal: oklch(0.7 0.2 190);
|
||||||
|
--accent-coral: oklch(0.7 0.2 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0.08 0.01 240);
|
||||||
|
--foreground: oklch(0.98 0 0);
|
||||||
|
--card: oklch(0.13 0.02 240);
|
||||||
|
--card-foreground: oklch(0.98 0 0);
|
||||||
|
--popover: oklch(0.13 0.02 240);
|
||||||
|
--popover-foreground: oklch(0.98 0 0);
|
||||||
|
--primary: oklch(0.7 0.25 255);
|
||||||
|
--primary-foreground: oklch(0.08 0 0);
|
||||||
|
--secondary: oklch(0.18 0.03 240);
|
||||||
|
--secondary-foreground: oklch(0.98 0 0);
|
||||||
|
--muted: oklch(0.18 0.02 240);
|
||||||
|
--muted-foreground: oklch(0.7 0.02 240);
|
||||||
|
--accent: oklch(0.75 0.2 190);
|
||||||
|
--accent-foreground: oklch(0.08 0 0);
|
||||||
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
|
--border: oklch(0.22 0.02 240);
|
||||||
|
--input: oklch(0.22 0.02 240);
|
||||||
|
--ring: oklch(0.7 0.25 255);
|
||||||
|
--chart-1: oklch(0.7 0.25 255);
|
||||||
|
--chart-2: oklch(0.75 0.2 190);
|
||||||
|
--chart-3: oklch(0.75 0.15 150);
|
||||||
|
--chart-4: oklch(0.75 0.2 30);
|
||||||
|
--chart-5: oklch(0.65 0.25 280);
|
||||||
|
--sidebar: oklch(0.13 0.02 240);
|
||||||
|
--sidebar-foreground: oklch(0.98 0 0);
|
||||||
|
--sidebar-primary: oklch(0.7 0.25 255);
|
||||||
|
--sidebar-primary-foreground: oklch(0.98 0 0);
|
||||||
|
--sidebar-accent: oklch(0.18 0.03 240);
|
||||||
|
--sidebar-accent-foreground: oklch(0.98 0 0);
|
||||||
|
--sidebar-border: oklch(0.22 0.02 240);
|
||||||
|
--sidebar-ring: oklch(0.7 0.25 255);
|
||||||
|
--accent-purple: oklch(0.65 0.25 280);
|
||||||
|
--accent-teal: oklch(0.75 0.2 190);
|
||||||
|
--accent-coral: oklch(0.75 0.2 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom gradient utilities */
|
||||||
|
@layer utilities {
|
||||||
|
.bg-gradient-radial {
|
||||||
|
background-image: radial-gradient(circle at center, var(--tw-gradient-stops));
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-gradient {
|
||||||
|
@apply bg-gradient-to-r bg-clip-text text-transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradient-border {
|
||||||
|
position: relative;
|
||||||
|
background: linear-gradient(var(--background), var(--background)) padding-box,
|
||||||
|
linear-gradient(to right, var(--tw-gradient-stops)) border-box;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow-sm {
|
||||||
|
box-shadow: 0 0 20px -5px var(--tw-shadow-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow-md {
|
||||||
|
box-shadow: 0 0 40px -10px var(--tw-shadow-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow-lg {
|
||||||
|
box-shadow: 0 0 60px -15px var(--tw-shadow-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Accent color utilities */
|
||||||
|
.text-accent-purple {
|
||||||
|
color: var(--accent-purple);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-accent-teal {
|
||||||
|
color: var(--accent-teal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-accent-coral {
|
||||||
|
color: var(--accent-coral);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-accent-purple {
|
||||||
|
background-color: var(--accent-purple);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-accent-teal {
|
||||||
|
background-color: var(--accent-teal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-accent-coral {
|
||||||
|
background-color: var(--accent-coral);
|
||||||
|
}
|
||||||
|
|
||||||
|
.from-accent-purple\/10 {
|
||||||
|
--tw-gradient-from: oklch(from var(--accent-purple) l c h / 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.from-accent-teal\/10 {
|
||||||
|
--tw-gradient-from: oklch(from var(--accent-teal) l c h / 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.from-accent-coral\/10 {
|
||||||
|
--tw-gradient-from: oklch(from var(--accent-coral) l c h / 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.to-accent-purple\/10 {
|
||||||
|
--tw-gradient-to: oklch(from var(--accent-purple) l c h / 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.to-accent-teal\/10 {
|
||||||
|
--tw-gradient-to: oklch(from var(--accent-teal) l c h / 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.to-accent-coral\/10 {
|
||||||
|
--tw-gradient-to: oklch(from var(--accent-coral) l c h / 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
www/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"extends": "astro/tsconfigs/strict",
|
||||||
|
"include": [
|
||||||
|
".astro/types.d.ts",
|
||||||
|
"**/*"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "react",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./src/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||