diff --git a/www/CLAUDE.md b/www/CLAUDE.md new file mode 100644 index 0000000..b2c3acd --- /dev/null +++ b/www/CLAUDE.md @@ -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 `
` and `` 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
\ No newline at end of file
diff --git a/www/public/favicon.svg b/www/public/favicon.svg
index 1047619..15006d0 100644
--- a/www/public/favicon.svg
+++ b/www/public/favicon.svg
@@ -1,19 +1,13 @@
 
   
-    
-      
-      
+    
+      
+      
     
   
-  
-  <\!-- Background circle -->
-  
-  
-  <\!-- Mirror/sync icon -->
+  
   
-    <\!-- First arrow (top) -->
     
-    <\!-- Second arrow (bottom) -->
-    
+    
   
 
diff --git a/www/public/robots.txt b/www/public/robots.txt
new file mode 100644
index 0000000..2cfe3e3
--- /dev/null
+++ b/www/public/robots.txt
@@ -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
\ No newline at end of file
diff --git a/www/public/sitemap.xml b/www/public/sitemap.xml
new file mode 100644
index 0000000..fbe0e33
--- /dev/null
+++ b/www/public/sitemap.xml
@@ -0,0 +1,9 @@
+
+
+  
+    https://gitea-mirror.com/
+    2025-01-08
+    weekly
+    1.0
+  
+
\ No newline at end of file
diff --git a/www/src/components/CTA.tsx b/www/src/components/CTA.tsx
new file mode 100644
index 0000000..e188d0c
--- /dev/null
+++ b/www/src/components/CTA.tsx
@@ -0,0 +1,69 @@
+import React from 'react';
+import { Button } from './ui/button';
+import { ArrowRight, Star, GitFork, Users } from 'lucide-react';
+
+export function CTA() {
+  return (
+    
+
+
+ {/* Background pattern */} +
+ +
+

+ Start Protecting Your Code Today +

+

+ Join developers who trust Gitea Mirror to keep their repositories safe and accessible. + Free, open source, and ready to deploy. +

+ + {/* Stats */} +
+
+ + 500+ Stars +
+
+ + 50+ Forks +
+
+ + Active Community +
+
+ + +
+
+ + {/* Open source note */} +
+

+ Gitea Mirror is licensed under GPL-3.0. + + View License + +

+
+
+
+ ); +} \ No newline at end of file diff --git a/www/src/components/Features.tsx b/www/src/components/Features.tsx new file mode 100644 index 0000000..a4d54e6 --- /dev/null +++ b/www/src/components/Features.tsx @@ -0,0 +1,127 @@ +import React from 'react'; +import { + RefreshCw, + Building2, + FolderTree, + Activity, + Lock, + Heart, + Zap, + Shield, + GitBranch +} from 'lucide-react'; + +const features = [ + { + title: "Automated Mirroring", + description: "Set it and forget it. Automatically sync your GitHub repositories to Gitea on a schedule.", + icon: RefreshCw, + gradient: "from-blue-500 to-cyan-500" + }, + { + title: "Bulk Operations", + description: "Mirror entire organizations or user accounts with a single configuration.", + icon: Building2, + gradient: "from-purple-500 to-pink-500" + }, + { + title: "Preserve Structure", + description: "Maintain your GitHub organization structure or customize how repos are organized.", + icon: FolderTree, + gradient: "from-green-500 to-emerald-500" + }, + { + title: "Real-time Status", + description: "Monitor mirror progress with live updates and detailed activity logs.", + icon: Activity, + gradient: "from-orange-500 to-red-500" + }, + { + title: "Secure & Private", + description: "Self-hosted solution keeps your code on your infrastructure with full control.", + icon: Lock, + gradient: "from-indigo-500 to-purple-500" + }, + { + title: "Open Source", + description: "Free, transparent, and community-driven development. Contribute and customize.", + icon: Heart, + gradient: "from-pink-500 to-rose-500" + } +]; + +export function Features() { + return ( +
+
+
+

+ Everything You Need for + Reliable Backups +

+

+ Powerful features designed to keep your code safe and accessible, no matter what happens. +

+
+ +
+ {features.map((feature, index) => { + const Icon = feature.icon; + return ( +
+
+ +
+ +
+ +

{feature.title}

+

{feature.description}

+
+ ); + })} +
+ + {/* Additional feature highlights */} +
+
+
+ +
+
+

Lightning Fast

+

Optimized for speed

+
+
+
+
+ +
+
+

Enterprise Ready

+

Scale with confidence

+
+
+
+
+ +
+
+

Full Git Support

+

All features preserved

+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/www/src/components/Footer.tsx b/www/src/components/Footer.tsx new file mode 100644 index 0000000..aa66e31 --- /dev/null +++ b/www/src/components/Footer.tsx @@ -0,0 +1,81 @@ +import React from 'react'; +import { Github, Book, MessageSquare, Bug } from 'lucide-react'; + +export function Footer() { + const links = [ + { + title: "Source Code", + href: "https://github.com/RayLabsHQ/gitea-mirror", + icon: Github + }, + { + title: "Documentation", + href: "https://github.com/RayLabsHQ/gitea-mirror/tree/main/docs", + icon: Book + }, + { + title: "Discussions", + href: "https://github.com/RayLabsHQ/gitea-mirror/discussions", + icon: MessageSquare + }, + { + title: "Report Issue", + href: "https://github.com/RayLabsHQ/gitea-mirror/issues", + icon: Bug + } + ]; + + return ( +
+
+
+ {/* Logo and tagline */} +
+
+ Gitea Mirror + Gitea Mirror +
+

+ Keep your GitHub code safe and synced +

+
+ + {/* Links */} + + + {/* Copyright */} +
+

© {new Date().getFullYear()} Gitea Mirror. Open source under GPL-3.0 License.

+

+ Made with dedication by the{' '} + + RayLabs team + +

+
+
+
+
+ ); +} \ No newline at end of file diff --git a/www/src/components/Header.tsx b/www/src/components/Header.tsx new file mode 100644 index 0000000..9914241 --- /dev/null +++ b/www/src/components/Header.tsx @@ -0,0 +1,118 @@ +import React, { useState, useEffect } from 'react'; +import { ThemeToggle } from './ThemeToggle'; +import { Github, Menu, X } from 'lucide-react'; +import { Button } from './ui/button'; + +export function Header() { + const [isMenuOpen, setIsMenuOpen] = useState(false); + const [isScrolled, setIsScrolled] = useState(false); + + useEffect(() => { + const handleScroll = () => { + setIsScrolled(window.scrollY > 10); + }; + + window.addEventListener('scroll', handleScroll); + return () => window.removeEventListener('scroll', handleScroll); + }, []); + + const navLinks = [ + { href: '#features', label: 'Features' }, + { href: '#screenshots', label: 'Screenshots' }, + { href: '#installation', label: 'Installation' } + ]; + + const handleNavClick = () => { + setIsMenuOpen(false); + }; + + return ( +
+
+
+ {/* Logo */} + + Gitea Mirror + Gitea Mirror + + + {/* Desktop Navigation */} + + + {/* Desktop Actions */} + + + {/* Mobile Actions */} +
+ + +
+
+
+ + {/* Mobile Menu */} +
+
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/www/src/components/Hero.tsx b/www/src/components/Hero.tsx new file mode 100644 index 0000000..8743e2d --- /dev/null +++ b/www/src/components/Hero.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { Button } from './ui/button'; +import { ArrowRight, Github, Shield, RefreshCw } from 'lucide-react'; + +export function Hero() { + return ( +
+ {/* Background gradients */} +
+
+
+
+
+ +
+
+
+
+ Gitea Mirror Logo +
+
+ +

+ + Keep Your Code + +
+ + Safe & Synced + +

+ +

+ Automatically mirror your GitHub repositories to self-hosted Gitea. + Never lose access to your code with continuous backup and synchronization. +

+ +
+
+ + Self-Hosted +
+ +
+ + Auto-Sync +
+ +
+ + Open Source +
+
+ +
+ + +
+
+
+ ); +} \ No newline at end of file diff --git a/www/src/components/Installation.tsx b/www/src/components/Installation.tsx new file mode 100644 index 0000000..7a4a907 --- /dev/null +++ b/www/src/components/Installation.tsx @@ -0,0 +1,165 @@ +import React, { useState } from 'react'; +import { Button } from './ui/button'; +import { Copy, Check, Terminal, Container, Cloud } from 'lucide-react'; + +type InstallMethod = 'docker' | 'manual' | 'proxmox'; + +export function Installation() { + const [activeMethod, setActiveMethod] = useState('docker'); + const [copiedCommand, setCopiedCommand] = useState(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 ( +
+
+
+

+ Get Started in Minutes +

+

+ Choose your preferred installation method +

+
+ + {/* Installation method tabs */} +
+ {(Object.entries(installMethods) as [InstallMethod, typeof installMethods[InstallMethod]][]).map(([method, config]) => { + const Icon = config.icon; + return ( + + ); + })} +
+ + {/* Installation steps */} +
+ {installMethods[activeMethod].steps.map((step, index) => ( +
+
+
+ {index + 1} +
+
+

{step.title}

+
+
+                      {step.command}
+                    
+ +
+
+
+ {index < installMethods[activeMethod].steps.length - 1 && ( +
+ )} +
+ ))} +
+ + {/* Additional info */} +
+

+ First user becomes admin. After installation, + create your account and configure GitHub and Gitea connections through the web interface. +

+
+
+
+ ); +} \ No newline at end of file diff --git a/www/src/components/Screenshots.tsx b/www/src/components/Screenshots.tsx new file mode 100644 index 0000000..80927e1 --- /dev/null +++ b/www/src/components/Screenshots.tsx @@ -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(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 ( +
+
+
+

+ See It In Action +

+

+ A clean, intuitive interface designed for efficiency and ease of use +

+
+ +
+ {/* Screenshot viewer */} +
+
+ + + {current.title} + +
+ + {/* Navigation buttons - hidden on mobile, visible on desktop */} + + +
+ + {/* Screenshot info */} +
+

{current.title}

+

{current.description}

+
+ + {/* Dots indicator */} +
+ {screenshots.map((_, index) => ( +
+ + {/* Mobile swipe hint */} +

+ Swipe left or right to navigate +

+
+ + {/* Thumbnail grid - visible on larger screens */} +
+ {screenshots.map((screenshot, index) => ( + + ))} +
+
+
+ ); +} \ No newline at end of file diff --git a/www/src/components/ThemeToggle.tsx b/www/src/components/ThemeToggle.tsx new file mode 100644 index 0000000..968b331 --- /dev/null +++ b/www/src/components/ThemeToggle.tsx @@ -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 ( + + ); +} \ No newline at end of file diff --git a/www/src/pages/index.astro b/www/src/pages/index.astro index 7b4a9d8..d3b78b4 100644 --- a/www/src/pages/index.astro +++ b/www/src/pages/index.astro @@ -1,343 +1,166 @@ --- import '../styles/global.css'; -import { Button } from '../components/ui/button'; +import { Header } from '../components/Header'; +import { Hero } from '../components/Hero'; +import { Features } from '../components/Features'; +import { Screenshots } from '../components/Screenshots'; +import { Installation } from '../components/Installation'; +import { CTA } from '../components/CTA'; +import { Footer } from '../components/Footer'; -const features = [ - { - title: "Automated Mirroring", - description: "Set it and forget it. Automatically sync your GitHub repositories to Gitea on a schedule.", - icon: "🔄" - }, - { - title: "Bulk Operations", - description: "Mirror entire organizations or user accounts with a single configuration.", - icon: "📦" - }, - { - title: "Preserve Structure", - description: "Maintain your GitHub organization structure or customize how repos are organized.", - icon: "🏗️" - }, - { - title: "Real-time Status", - description: "Monitor mirror progress with live updates and detailed activity logs.", - icon: "📊" - }, - { - title: "Secure & Private", - description: "Self-hosted solution keeps your code on your infrastructure.", - icon: "🔒" - }, - { - title: "Open Source", - description: "Free, transparent, and community-driven development.", - icon: "💚" - } -]; +const siteUrl = 'https://gitea-mirror.com'; +const title = 'Gitea Mirror - Automated GitHub to Gitea Repository Mirroring & Backup'; +const description = 'Automatically mirror and backup your GitHub repositories to self-hosted Gitea. Keep your code safe with scheduled syncing, bulk operations, and real-time monitoring. Free and open source.'; +const keywords = 'github backup, gitea mirror, repository sync, github to gitea, git mirror, code backup, self-hosted git, repository migration, github mirror tool, gitea sync, automated backup, github repository backup, git repository mirror, self-hosted backup solution'; -const steps = [ - { number: "1", title: "Install", description: "Deploy with Docker or run directly with Bun" }, - { number: "2", title: "Connect", description: "Add your GitHub and Gitea credentials" }, - { number: "3", title: "Configure", description: "Select repositories or organizations to mirror" }, - { number: "4", title: "Relax", description: "Let Gitea Mirror handle the synchronization" } -]; - -const screenshots = [ - { src: "/assets/dashboard.png", alt: "Dashboard view", mobile: "/assets/dashboard_mobile.png" }, - { src: "/assets/repositories.png", alt: "Repository management", mobile: "/assets/repositories_mobile.png" }, - { src: "/assets/configuration.png", alt: "Configuration interface", mobile: "/assets/configuration_mobile.png" }, - { src: "/assets/activity.png", alt: "Activity monitoring", mobile: "/assets/activity_mobile.png" } -]; +// Structured data for SEO +const structuredData = { + "@context": "https://schema.org", + "@type": "SoftwareApplication", + "name": "Gitea Mirror", + "applicationCategory": "DeveloperApplication", + "operatingSystem": "Linux, macOS, Windows", + "offers": { + "@type": "Offer", + "price": "0", + "priceCurrency": "USD" + }, + "description": description, + "url": siteUrl, + "author": { + "@type": "Organization", + "name": "RayLabs", + "url": "https://github.com/RayLabsHQ" + }, + "softwareVersion": "2.22.0", + "screenshot": [ + `${siteUrl}/assets/dashboard.png`, + `${siteUrl}/assets/repositories.png`, + `${siteUrl}/assets/organisation.png` + ], + "featureList": [ + "Automated repository mirroring", + "Bulk organization sync", + "Real-time monitoring", + "Self-hosted solution", + "Open source" + ], + "softwareRequirements": "Docker or Bun runtime" +}; --- + + - - Gitea Mirror - Automated GitHub to Gitea Repository Backup & Sync - - - + + {title} + + + + + - - - + - + + + + + + + + - - - - - + + + + + + + - + + + + + + + + + + - -
-
-
-
- -
-
-

- Keep Your GitHub Code - Safe & Synced -

-

- Automatically mirror your GitHub repositories to self-hosted Gitea. - Never worry about losing access to your code with continuous backup and synchronization. -

- -
-
-
- - -
-
-
-

Why You Need Repository Mirroring

-

- GitHub is great, but what happens if you lose access? Service outages, account issues, or policy changes - can lock you out of your own code. Gitea Mirror ensures you always have a backup on infrastructure you control. -

-
-
-
- - -
-
-
-

Everything You Need for Reliable Backups

-
-
- {features.map((feature, index) => ( -
-
{feature.icon}
-

{feature.title}

-

{feature.description}

-
- ))} -
-
-
- - -
-
-
-

See It In Action

-

- A clean, intuitive interface for managing your repository mirrors -

-
-
-
- {screenshots.map((screenshot, index) => ( -
- - - {screenshot.alt} - -
- ))} -
-
-
-
- - -
-
-
-

Get Started in Minutes

-
-
-
- {steps.map((step, index) => ( -
-
-
- {step.number} -
-
-
-

{step.title}

-

{step.description}

-
-
- ))} -
-
-
-
- - -
-
-
-

Free & Open Source

-

- Gitea Mirror is completely free and open source. Self-host with confidence knowing you have - full control over your infrastructure and data. -

-
-

- Coming Soon: Support development with an optional - supporter tier ($20/year) for priority features and dedicated support. -

-
- -
-
-
- - - +
+ +
+ + + + + +
+ +