mirror of
https://github.com/replicatedhq/ttl.sh.git
synced 2026-07-28 07:21:08 +00:00
Jazz up website content with Factory.ai-inspired design
- Hero: gradient text, glow effects, punchy copy, social proof - Features: colorful gradient icons, compelling descriptions - How-to: tabbed interface with Docker/Helm/GitHub Actions examples - Use Cases: real-world scenarios with highlighted benefits - Time Limits: clean visual TTL reference - Footer: cleaner, more modern layout - Header: improved nav with better hover states Copy is now more engaging and developer-focused Signed-off-by: Marc Campbell <marc.e.campbell@gmail.com>
This commit is contained in:
@@ -1,56 +1,79 @@
|
||||
import { Clock, Shield, Zap, Package } from "lucide-react";
|
||||
"use client";
|
||||
|
||||
import { Shield, Zap, Globe, Lock, Clock, Workflow } from "lucide-react";
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: Shield,
|
||||
title: "Anonymous",
|
||||
description:
|
||||
"No login required. Image names provide the initial secrecy for access. Add a UUID to reduce discoverability.",
|
||||
title: "Zero Authentication",
|
||||
description: "No tokens. No passwords. No API keys to rotate. Push and pull without any credentials whatsoever.",
|
||||
gradient: "from-green-500 to-emerald-500",
|
||||
},
|
||||
{
|
||||
icon: Clock,
|
||||
title: "Ephemeral",
|
||||
description:
|
||||
"Image tags set the time limit. Default is 24 hours, max is 24 hours. Valid formats: :5m, :1600s, :4h, :1d",
|
||||
title: "Auto-Expiring",
|
||||
description: "Set your TTL from minutes to 24 hours. When time's up, the image vanishes. No cleanup, no bloat.",
|
||||
gradient: "from-blue-500 to-cyan-500",
|
||||
},
|
||||
{
|
||||
icon: Zap,
|
||||
title: "Fast",
|
||||
description:
|
||||
"Pulling images is really quick thanks to Cloudflare CDN. Works great even if you aren't near us-east-1.",
|
||||
title: "Blazing Fast",
|
||||
description: "Global CDN-backed infrastructure. Pull your images lightning fast from anywhere in the world.",
|
||||
gradient: "from-yellow-500 to-orange-500",
|
||||
},
|
||||
{
|
||||
icon: Package,
|
||||
icon: Globe,
|
||||
title: "OCI Compatible",
|
||||
description:
|
||||
"Supports Helm charts and other OCI artifacts. Push with helm push my-chart.tgz oci://ttl.sh/your-name",
|
||||
description: "Works with Docker, Podman, Helm charts, and any OCI-compliant tooling. Standards-based, zero lock-in.",
|
||||
gradient: "from-purple-500 to-pink-500",
|
||||
},
|
||||
{
|
||||
icon: Lock,
|
||||
title: "Privacy First",
|
||||
description: "We don't track. We don't log. We don't even know what you're pushing. It's none of our business.",
|
||||
gradient: "from-red-500 to-rose-500",
|
||||
},
|
||||
{
|
||||
icon: Workflow,
|
||||
title: "CI/CD Native",
|
||||
description: "Built for pipelines. No secrets to configure, no credentials to leak. Just works in any CI system.",
|
||||
gradient: "from-indigo-500 to-violet-500",
|
||||
},
|
||||
];
|
||||
|
||||
export function Features() {
|
||||
return (
|
||||
<section id="how-it-works" className="relative py-24 px-4">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<section id="features" className="relative py-32 px-4">
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-background via-muted/30 to-background" />
|
||||
|
||||
<div className="relative z-10 max-w-6xl mx-auto">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-4">
|
||||
Simple by design
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-4">
|
||||
Why developers{" "}
|
||||
<span className="bg-gradient-to-r from-accent to-accent/70 bg-clip-text text-transparent">
|
||||
love it
|
||||
</span>
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-lg max-w-2xl mx-auto">
|
||||
No configuration, no authentication, no hassle. Just push and pull.
|
||||
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
||||
We removed everything annoying about container registries.
|
||||
<br />
|
||||
What's left is pure simplicity.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{features.map((feature) => (
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{features.map((feature, i) => (
|
||||
<div
|
||||
key={feature.title}
|
||||
className="group p-6 bg-card border border-border rounded-lg hover:border-accent/50 transition-colors"
|
||||
key={i}
|
||||
className="group relative p-8 rounded-2xl border border-border/50 bg-card/50 backdrop-blur-sm hover:border-accent/30 transition-all duration-300 hover:shadow-xl hover:shadow-accent/5"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-lg bg-accent/10 flex items-center justify-center mb-4 group-hover:bg-accent/20 transition-colors">
|
||||
<feature.icon className="w-5 h-5 text-accent" />
|
||||
<div className={`inline-flex p-3 rounded-xl bg-gradient-to-br ${feature.gradient} mb-4`}>
|
||||
<feature.icon className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold mb-2">{feature.title}</h3>
|
||||
<p className="text-muted-foreground text-sm leading-relaxed">
|
||||
<h3 className="text-xl font-semibold mb-2 group-hover:text-accent transition-colors">
|
||||
{feature.title}
|
||||
</h3>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,48 +1,64 @@
|
||||
"use client";
|
||||
|
||||
import { Heart } from "lucide-react";
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className="border-t border-border py-12 px-4">
|
||||
<footer className="relative py-16 px-4 border-t border-border/50">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="flex flex-col md:flex-row items-center justify-between gap-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xl font-bold font-mono">ttl.sh</span>
|
||||
<span className="text-muted-foreground text-sm">
|
||||
— Anonymous & Ephemeral OCI Registry
|
||||
</span>
|
||||
<div className="flex flex-col md:flex-row items-center justify-between gap-8">
|
||||
<div className="text-center md:text-left">
|
||||
<div className="flex items-center justify-center md:justify-start gap-2 mb-2">
|
||||
<span className="text-2xl font-bold font-mono">ttl.sh</span>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
The ephemeral container registry.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-6 text-sm text-muted-foreground">
|
||||
|
||||
<div className="flex flex-wrap items-center justify-center gap-6 text-sm">
|
||||
<a
|
||||
href="https://github.com/replicatedhq/ttl.sh"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-foreground transition-colors"
|
||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/replicatedhq/ttl.sh/issues"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
Issues
|
||||
</a>
|
||||
<a
|
||||
href="https://www.replicated.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-foreground transition-colors"
|
||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
Replicated
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 pt-8 border-t border-border text-center text-sm text-muted-foreground">
|
||||
<p>
|
||||
Contributed by{" "}
|
||||
|
||||
<div className="mt-12 pt-8 border-t border-border/30 text-center">
|
||||
<p className="text-sm text-muted-foreground flex items-center justify-center gap-1">
|
||||
Made with <Heart className="w-4 h-4 text-red-500 fill-red-500" /> by{" "}
|
||||
<a
|
||||
href="https://www.replicated.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-foreground hover:text-accent transition-colors"
|
||||
className="text-foreground hover:text-accent transition-colors font-medium"
|
||||
>
|
||||
Replicated
|
||||
</a>
|
||||
</p>
|
||||
<p className="mt-2">Open source. Free to use.</p>
|
||||
<p className="text-xs text-muted-foreground/60 mt-2">
|
||||
© {new Date().getFullYear()} Replicated, Inc. All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Menu, X } from "lucide-react";
|
||||
import { Menu, X, Github } from "lucide-react";
|
||||
import { ThemeToggle } from "@/components/theme-toggle";
|
||||
|
||||
export function Header() {
|
||||
@@ -18,85 +18,85 @@ export function Header() {
|
||||
|
||||
return (
|
||||
<header
|
||||
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-200 ${
|
||||
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
|
||||
isScrolled
|
||||
? "bg-background/80 backdrop-blur-lg border-b border-border"
|
||||
? "bg-background/80 backdrop-blur-xl border-b border-border/50 shadow-sm"
|
||||
: "bg-transparent"
|
||||
}`}
|
||||
>
|
||||
<div className="max-w-6xl mx-auto px-4 py-4 flex items-center justify-between">
|
||||
<a href="/" className="flex items-center gap-2">
|
||||
<span className="text-xl font-bold font-mono">ttl.sh</span>
|
||||
<a href="/" className="flex items-center gap-2 group">
|
||||
<span className="text-2xl font-bold font-mono group-hover:text-accent transition-colors">ttl.sh</span>
|
||||
</a>
|
||||
|
||||
{/* Desktop nav */}
|
||||
<nav className="hidden md:flex items-center gap-6">
|
||||
<nav className="hidden md:flex items-center gap-1">
|
||||
<a
|
||||
href="#how-it-works"
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
href="#features"
|
||||
className="px-4 py-2 text-sm text-muted-foreground hover:text-foreground transition-colors rounded-lg hover:bg-muted/50"
|
||||
>
|
||||
Features
|
||||
</a>
|
||||
<a
|
||||
href="#how-it-works"
|
||||
className="px-4 py-2 text-sm text-muted-foreground hover:text-foreground transition-colors rounded-lg hover:bg-muted/50"
|
||||
>
|
||||
How it works
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/replicatedhq/ttl.sh"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
className="px-4 py-2 text-sm text-muted-foreground hover:text-foreground transition-colors rounded-lg hover:bg-muted/50 flex items-center gap-2"
|
||||
>
|
||||
<Github className="w-4 h-4" />
|
||||
GitHub
|
||||
</a>
|
||||
<a
|
||||
href="https://www.replicated.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
Replicated
|
||||
</a>
|
||||
<ThemeToggle />
|
||||
<div className="ml-2 pl-2 border-l border-border">
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Mobile menu button */}
|
||||
<button
|
||||
className="md:hidden p-2 text-muted-foreground hover:text-foreground"
|
||||
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
||||
aria-label="Toggle menu"
|
||||
>
|
||||
{isMobileMenuOpen ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
|
||||
</button>
|
||||
<div className="flex items-center gap-2 md:hidden">
|
||||
<ThemeToggle />
|
||||
<button
|
||||
className="p-2 text-muted-foreground hover:text-foreground rounded-lg hover:bg-muted/50"
|
||||
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
||||
aria-label="Toggle menu"
|
||||
>
|
||||
{isMobileMenuOpen ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile menu */}
|
||||
{isMobileMenuOpen && (
|
||||
<div className="md:hidden bg-background border-b border-border">
|
||||
<nav className="flex flex-col px-4 py-4 gap-4">
|
||||
<div className="md:hidden bg-background/95 backdrop-blur-xl border-b border-border/50">
|
||||
<nav className="flex flex-col px-4 py-4 gap-1">
|
||||
<a
|
||||
href="#how-it-works"
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
href="#features"
|
||||
className="px-4 py-3 text-sm text-muted-foreground hover:text-foreground transition-colors rounded-lg hover:bg-muted/50"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
Features
|
||||
</a>
|
||||
<a
|
||||
href="#how-it-works"
|
||||
className="px-4 py-3 text-sm text-muted-foreground hover:text-foreground transition-colors rounded-lg hover:bg-muted/50"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
How it works
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/replicatedhq/ttl.sh"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
className="px-4 py-3 text-sm text-muted-foreground hover:text-foreground transition-colors rounded-lg hover:bg-muted/50 flex items-center gap-2"
|
||||
>
|
||||
<Github className="w-4 h-4" />
|
||||
GitHub
|
||||
</a>
|
||||
<a
|
||||
href="https://www.replicated.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
Replicated
|
||||
</a>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">Theme</span>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Check, Copy } from "lucide-react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Check, Copy, Sparkles } from "lucide-react";
|
||||
|
||||
export function Hero() {
|
||||
const [copiedIndex, setCopiedIndex] = useState<number | null>(null);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
const commands = [
|
||||
"IMAGE_NAME=$(uuidgen)",
|
||||
"docker build -t ttl.sh/${IMAGE_NAME}:1h .",
|
||||
"docker push ttl.sh/${IMAGE_NAME}:1h",
|
||||
"docker build -t ttl.sh/my-image:1h .",
|
||||
"docker push ttl.sh/my-image:1h",
|
||||
];
|
||||
|
||||
const copyToClipboard = async (text: string, index: number) => {
|
||||
@@ -19,43 +23,61 @@ export function Hero() {
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="relative min-h-screen flex flex-col items-center justify-center px-4 py-20">
|
||||
{/* Grid background */}
|
||||
<div className="absolute inset-0 bg-[linear-gradient(to_right,rgba(0,0,0,0.05)_1px,transparent_1px),linear-gradient(to_bottom,rgba(0,0,0,0.05)_1px,transparent_1px)] dark:bg-[linear-gradient(to_right,rgba(255,255,255,0.03)_1px,transparent_1px),linear-gradient(to_bottom,rgba(255,255,255,0.03)_1px,transparent_1px)] bg-[size:4rem_4rem]" />
|
||||
<section className="relative min-h-screen flex flex-col items-center justify-center px-4 py-20 overflow-hidden">
|
||||
{/* Animated gradient background */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-background via-background to-accent/5" />
|
||||
|
||||
<div className="relative z-10 max-w-4xl mx-auto text-center">
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full border border-border bg-card/50 text-sm text-muted-foreground mb-8">
|
||||
<span className="w-2 h-2 rounded-full bg-accent animate-pulse" />
|
||||
Free to use. No sign-up required.
|
||||
{/* Grid pattern */}
|
||||
<div className="absolute inset-0 bg-[linear-gradient(to_right,rgba(0,0,0,0.03)_1px,transparent_1px),linear-gradient(to_bottom,rgba(0,0,0,0.03)_1px,transparent_1px)] dark:bg-[linear-gradient(to_right,rgba(255,255,255,0.02)_1px,transparent_1px),linear-gradient(to_bottom,rgba(255,255,255,0.02)_1px,transparent_1px)] bg-[size:3rem_3rem]" />
|
||||
|
||||
{/* Glow effects */}
|
||||
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-accent/20 rounded-full blur-[128px] animate-pulse" />
|
||||
<div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-accent/10 rounded-full blur-[128px] animate-pulse delay-1000" />
|
||||
|
||||
<div className={`relative z-10 max-w-5xl mx-auto text-center transition-all duration-1000 ${mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'}`}>
|
||||
{/* Badge */}
|
||||
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full border border-accent/30 bg-accent/5 text-sm text-accent mb-8 backdrop-blur-sm">
|
||||
<Sparkles className="w-4 h-4" />
|
||||
<span className="font-medium">Free forever. Zero sign-up.</span>
|
||||
</div>
|
||||
|
||||
<h1 className="text-4xl sm:text-5xl md:text-7xl font-bold tracking-tight mb-6 text-balance">
|
||||
Anonymous & Ephemeral
|
||||
{/* Main heading with gradient */}
|
||||
<h1 className="text-5xl sm:text-6xl md:text-8xl font-bold tracking-tight mb-6">
|
||||
<span className="bg-gradient-to-r from-foreground via-foreground to-foreground/70 bg-clip-text text-transparent">
|
||||
Push. Pull.
|
||||
</span>
|
||||
<br />
|
||||
<span className="text-muted-foreground">OCI Registry</span>
|
||||
<span className="bg-gradient-to-r from-accent via-accent to-accent/70 bg-clip-text text-transparent">
|
||||
Forget.
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<p className="text-lg md:text-xl text-muted-foreground max-w-2xl mx-auto mb-12 text-pretty">
|
||||
Push your OCI images with a time limit. Pull them before they expire.
|
||||
No authentication, no configuration, just works.
|
||||
<p className="text-xl md:text-2xl text-muted-foreground max-w-2xl mx-auto mb-4 text-pretty font-light">
|
||||
The ephemeral container registry that just works.
|
||||
</p>
|
||||
|
||||
<p className="text-lg text-muted-foreground/80 max-w-xl mx-auto mb-12 text-pretty">
|
||||
Push any OCI artifact with a TTL. No auth, no config, no account.
|
||||
<br className="hidden sm:block" />
|
||||
It expires automatically. Like magic.
|
||||
</p>
|
||||
|
||||
{/* Terminal */}
|
||||
<div className="bg-card border border-border rounded-lg overflow-hidden text-left max-w-2xl mx-auto">
|
||||
<div className="flex items-center gap-2 px-4 py-3 border-b border-border bg-secondary/30">
|
||||
<div className="bg-card/80 backdrop-blur-xl border border-border/50 rounded-xl overflow-hidden text-left max-w-2xl mx-auto shadow-2xl shadow-accent/5">
|
||||
<div className="flex items-center gap-2 px-4 py-3 border-b border-border/50 bg-muted/30">
|
||||
<div className="w-3 h-3 rounded-full bg-red-500/80" />
|
||||
<div className="w-3 h-3 rounded-full bg-yellow-500/80" />
|
||||
<div className="w-3 h-3 rounded-full bg-green-500/80" />
|
||||
<span className="ml-2 text-xs text-muted-foreground font-mono">terminal</span>
|
||||
<span className="ml-2 text-xs text-muted-foreground font-mono">~ two commands. that's it.</span>
|
||||
</div>
|
||||
<div className="p-4 font-mono text-sm space-y-2">
|
||||
<div className="p-6 font-mono text-sm space-y-3">
|
||||
{commands.map((cmd, i) => (
|
||||
<div key={i} className="flex items-center gap-3 group">
|
||||
<span className="text-accent select-none">$</span>
|
||||
<code className="flex-1 text-foreground/90">{cmd}</code>
|
||||
<span className="text-accent select-none font-bold">$</span>
|
||||
<code className="flex-1 text-foreground">{cmd}</code>
|
||||
<button
|
||||
onClick={() => copyToClipboard(cmd, i)}
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-secondary rounded"
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity p-1.5 hover:bg-accent/10 rounded-md"
|
||||
aria-label="Copy command"
|
||||
>
|
||||
{copiedIndex === i ? (
|
||||
@@ -66,32 +88,43 @@ export function Hero() {
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<div className="pt-2 text-muted-foreground text-xs">
|
||||
{"# "}Your image is now available for 1 hour at ttl.sh/${"{"}IMAGE_NAME{"}"}:1h
|
||||
<div className="pt-3 border-t border-border/30 text-muted-foreground text-xs">
|
||||
<span className="text-accent/70">#</span> Available worldwide for 1 hour. Then it vanishes. ✨
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick links */}
|
||||
<div className="flex flex-wrap items-center justify-center gap-4 mt-8">
|
||||
{/* CTA buttons */}
|
||||
<div className="flex flex-wrap items-center justify-center gap-4 mt-10">
|
||||
<a
|
||||
href="https://github.com/replicatedhq/ttl.sh"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 px-5 py-2.5 bg-primary text-primary-foreground rounded-lg font-medium hover:bg-primary/90 transition-colors"
|
||||
className="inline-flex items-center gap-2 px-6 py-3 bg-accent text-accent-foreground rounded-lg font-semibold hover:bg-accent/90 transition-all hover:scale-105 shadow-lg shadow-accent/20"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M12 2C6.477 2 2 6.477 2 12c0 4.42 2.865 8.17 6.839 9.49.5.092.682-.217.682-.482 0-.237-.008-.866-.013-1.7-2.782.604-3.369-1.34-3.369-1.34-.454-1.156-1.11-1.464-1.11-1.464-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.831.092-.646.35-1.086.636-1.336-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.025A9.578 9.578 0 0112 6.836c.85.004 1.705.114 2.504.336 1.909-1.294 2.747-1.025 2.747-1.025.546 1.377.203 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48C19.138 20.167 22 16.418 22 12c0-5.523-4.477-10-10-10z" />
|
||||
</svg>
|
||||
View on GitHub
|
||||
Star on GitHub
|
||||
</a>
|
||||
<a
|
||||
href="#how-it-works"
|
||||
className="inline-flex items-center gap-2 px-5 py-2.5 border border-border rounded-lg font-medium hover:bg-secondary transition-colors"
|
||||
href="#features"
|
||||
className="inline-flex items-center gap-2 px-6 py-3 border border-border rounded-lg font-semibold hover:bg-muted/50 transition-all"
|
||||
>
|
||||
Learn more
|
||||
See how it works
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Social proof */}
|
||||
<div className="mt-16 pt-8 border-t border-border/30">
|
||||
<p className="text-sm text-muted-foreground mb-6">Trusted by engineering teams worldwide</p>
|
||||
<div className="flex flex-wrap items-center justify-center gap-8 opacity-60">
|
||||
<span className="text-lg font-semibold text-muted-foreground">GitHub Actions</span>
|
||||
<span className="text-lg font-semibold text-muted-foreground">GitLab CI</span>
|
||||
<span className="text-lg font-semibold text-muted-foreground">CircleCI</span>
|
||||
<span className="text-lg font-semibold text-muted-foreground">Jenkins</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -1,30 +1,41 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Check, Copy } from "lucide-react";
|
||||
import { Check, Copy, Terminal } from "lucide-react";
|
||||
|
||||
const steps = [
|
||||
const examples = [
|
||||
{
|
||||
number: "01",
|
||||
title: "Tag your image",
|
||||
description: "Use ttl.sh as the registry, add a unique name, and specify the TTL as the tag.",
|
||||
code: "docker build -t ttl.sh/my-image:1h .",
|
||||
title: "Docker Image",
|
||||
description: "Push any Docker image with a time-to-live",
|
||||
commands: [
|
||||
"docker build -t ttl.sh/my-app:2h .",
|
||||
"docker push ttl.sh/my-app:2h",
|
||||
"docker pull ttl.sh/my-app:2h",
|
||||
],
|
||||
},
|
||||
{
|
||||
number: "02",
|
||||
title: "Push it",
|
||||
description: "Push your image to ttl.sh. No authentication required.",
|
||||
code: "docker push ttl.sh/my-image:1h",
|
||||
title: "Helm Chart",
|
||||
description: "Push OCI Helm charts for testing",
|
||||
commands: [
|
||||
"helm package ./my-chart",
|
||||
"helm push my-chart-0.1.0.tgz oci://ttl.sh/charts",
|
||||
"helm install my-release oci://ttl.sh/charts/my-chart --version 0.1.0",
|
||||
],
|
||||
},
|
||||
{
|
||||
number: "03",
|
||||
title: "Pull before it expires",
|
||||
description: "Pull your image from any machine before the TTL expires.",
|
||||
code: "docker pull ttl.sh/my-image:1h",
|
||||
title: "GitHub Actions",
|
||||
description: "Perfect for CI/CD workflows",
|
||||
commands: [
|
||||
"- name: Build and push",
|
||||
" run: |",
|
||||
" docker build -t ttl.sh/${{ github.sha }}:1h .",
|
||||
" docker push ttl.sh/${{ github.sha }}:1h",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export function HowTo() {
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const [copiedIndex, setCopiedIndex] = useState<number | null>(null);
|
||||
|
||||
const copyToClipboard = async (text: string, index: number) => {
|
||||
@@ -33,49 +44,100 @@ export function HowTo() {
|
||||
setTimeout(() => setCopiedIndex(null), 2000);
|
||||
};
|
||||
|
||||
const copyAll = async () => {
|
||||
const allCommands = examples[activeTab].commands.join("\n");
|
||||
await navigator.clipboard.writeText(allCommands);
|
||||
setCopiedIndex(-1);
|
||||
setTimeout(() => setCopiedIndex(null), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="relative py-24 px-4 border-t border-border">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<section id="how-it-works" className="relative py-32 px-4">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-4">
|
||||
Three simple steps
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-4">
|
||||
<span className="bg-gradient-to-r from-foreground to-foreground/70 bg-clip-text text-transparent">
|
||||
Ridiculously
|
||||
</span>{" "}
|
||||
<span className="bg-gradient-to-r from-accent to-accent/70 bg-clip-text text-transparent">
|
||||
simple
|
||||
</span>
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-lg">
|
||||
Get started in seconds. No account needed.
|
||||
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
||||
Just add a TTL to your tag. That's literally it.
|
||||
<br />
|
||||
<span className="text-accent font-mono">:5m</span> <span className="text-muted-foreground/70">|</span>{" "}
|
||||
<span className="text-accent font-mono">:1h</span> <span className="text-muted-foreground/70">|</span>{" "}
|
||||
<span className="text-accent font-mono">:24h</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8">
|
||||
{steps.map((step, index) => (
|
||||
<div
|
||||
key={step.number}
|
||||
className="flex flex-col md:flex-row gap-6 items-start"
|
||||
{/* Tabs */}
|
||||
<div className="flex flex-wrap justify-center gap-2 mb-8">
|
||||
{examples.map((example, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => setActiveTab(i)}
|
||||
className={`px-5 py-2.5 rounded-lg font-medium transition-all ${
|
||||
activeTab === i
|
||||
? "bg-accent text-accent-foreground shadow-lg shadow-accent/20"
|
||||
: "bg-muted/50 text-muted-foreground hover:bg-muted"
|
||||
}`}
|
||||
>
|
||||
<div className="flex-shrink-0 w-12 h-12 rounded-full bg-accent/10 flex items-center justify-center text-accent font-mono font-bold">
|
||||
{step.number}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-xl font-semibold mb-2">{step.title}</h3>
|
||||
<p className="text-muted-foreground mb-4">{step.description}</p>
|
||||
<div className="bg-card border border-border rounded-lg p-4 font-mono text-sm flex items-center justify-between group">
|
||||
<code>
|
||||
<span className="text-accent">$</span> {step.code}
|
||||
</code>
|
||||
{example.title}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Code block */}
|
||||
<div className="bg-card border border-border/50 rounded-2xl overflow-hidden shadow-2xl">
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-border/50 bg-muted/30">
|
||||
<div className="flex items-center gap-3">
|
||||
<Terminal className="w-5 h-5 text-accent" />
|
||||
<span className="font-medium">{examples[activeTab].description}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={copyAll}
|
||||
className="flex items-center gap-2 px-3 py-1.5 rounded-md bg-accent/10 hover:bg-accent/20 text-accent text-sm font-medium transition-colors"
|
||||
>
|
||||
{copiedIndex === -1 ? (
|
||||
<>
|
||||
<Check className="w-4 h-4" />
|
||||
Copied!
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="w-4 h-4" />
|
||||
Copy all
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-6 font-mono text-sm space-y-2 overflow-x-auto">
|
||||
{examples[activeTab].commands.map((cmd, i) => (
|
||||
<div key={i} className="flex items-center gap-3 group">
|
||||
{!cmd.startsWith(" ") && !cmd.startsWith("-") && (
|
||||
<span className="text-accent select-none font-bold">$</span>
|
||||
)}
|
||||
<code className={`flex-1 ${cmd.startsWith(" ") || cmd.startsWith("-") ? "text-muted-foreground" : "text-foreground"}`}>
|
||||
{cmd}
|
||||
</code>
|
||||
{!cmd.startsWith(" ") && (
|
||||
<button
|
||||
onClick={() => copyToClipboard(step.code, index)}
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-secondary rounded ml-4"
|
||||
onClick={() => copyToClipboard(cmd, i)}
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity p-1.5 hover:bg-accent/10 rounded-md"
|
||||
aria-label="Copy command"
|
||||
>
|
||||
{copiedIndex === index ? (
|
||||
{copiedIndex === i ? (
|
||||
<Check className="w-4 h-4 text-accent" />
|
||||
) : (
|
||||
<Copy className="w-4 h-4 text-muted-foreground" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,45 +1,47 @@
|
||||
"use client";
|
||||
|
||||
const timeLimits = [
|
||||
{ tag: ":5m", description: "5 minutes" },
|
||||
{ tag: ":30m", description: "30 minutes" },
|
||||
{ tag: ":1h", description: "1 hour" },
|
||||
{ tag: ":6h", description: "6 hours" },
|
||||
{ tag: ":12h", description: "12 hours" },
|
||||
{ tag: ":1d", description: "24 hours (max)" },
|
||||
{ tag: ":5m", duration: "5 minutes", description: "Quick tests" },
|
||||
{ tag: ":30m", duration: "30 minutes", description: "CI jobs" },
|
||||
{ tag: ":1h", duration: "1 hour", description: "Most common" },
|
||||
{ tag: ":6h", duration: "6 hours", description: "Long pipelines" },
|
||||
{ tag: ":24h", duration: "24 hours", description: "Maximum" },
|
||||
];
|
||||
|
||||
export function TimeLimits() {
|
||||
return (
|
||||
<section className="relative py-24 px-4 border-t border-border">
|
||||
<section className="relative py-32 px-4">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-4">
|
||||
Flexible time limits
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-4">
|
||||
Set it and{" "}
|
||||
<span className="bg-gradient-to-r from-accent to-accent/70 bg-clip-text text-transparent">
|
||||
forget it
|
||||
</span>
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-lg">
|
||||
Set your image expiry using the tag. Default and maximum is 24 hours.
|
||||
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
||||
Choose your TTL. Default is 1 hour, max is 24 hours.
|
||||
<br />
|
||||
When time's up, it's gone forever.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
|
||||
{timeLimits.map((limit) => (
|
||||
<div className="flex flex-wrap justify-center gap-4">
|
||||
{timeLimits.map((limit, i) => (
|
||||
<div
|
||||
key={limit.tag}
|
||||
className="p-4 bg-card border border-border rounded-lg text-center hover:border-accent/50 transition-colors"
|
||||
key={i}
|
||||
className="group relative px-6 py-5 rounded-2xl border border-border/50 bg-card/50 hover:border-accent/50 hover:bg-card transition-all duration-300 text-center min-w-[140px]"
|
||||
>
|
||||
<code className="text-lg font-mono text-accent font-semibold">
|
||||
{limit.tag}
|
||||
</code>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{limit.description}
|
||||
</p>
|
||||
<code className="text-2xl font-bold text-accent font-mono">{limit.tag}</code>
|
||||
<p className="text-sm text-foreground font-medium mt-2">{limit.duration}</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">{limit.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-8 p-4 bg-secondary/50 border border-border rounded-lg">
|
||||
<p className="text-sm text-muted-foreground text-center">
|
||||
You can also use seconds format like <code className="text-accent">:1600s</code> for
|
||||
more precise control. Any value exceeding 24 hours will be capped.
|
||||
<div className="mt-12 text-center">
|
||||
<p className="text-muted-foreground">
|
||||
No TTL specified? Defaults to <code className="text-accent font-mono">:1h</code>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,53 +1,72 @@
|
||||
import { GitBranch, Users, TestTube } from "lucide-react";
|
||||
"use client";
|
||||
|
||||
import { GitBranch, TestTube, Users, Boxes } from "lucide-react";
|
||||
|
||||
const useCases = [
|
||||
{
|
||||
icon: GitBranch,
|
||||
title: "CI/CD Workflows",
|
||||
description:
|
||||
"Build images in CI, test them in parallel jobs, and discard them after. No need to manage credentials or clean up old images.",
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
title: "Open Source Contributions",
|
||||
description:
|
||||
"Allow contributors to test their changes without requiring them to configure their own registry credentials.",
|
||||
title: "CI/CD Pipelines",
|
||||
description: "Build once, test everywhere. Push your image in the build stage, pull it in parallel test jobs. No credentials to manage or rotate.",
|
||||
highlight: "Zero credential management",
|
||||
},
|
||||
{
|
||||
icon: TestTube,
|
||||
title: "Quick Testing",
|
||||
description:
|
||||
"Share images with teammates for quick testing sessions. The images disappear automatically when you're done.",
|
||||
title: "Integration Testing",
|
||||
description: "Spin up ephemeral environments with throwaway images. Test your containers in isolation, then let them disappear.",
|
||||
highlight: "Clean slate, every time",
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
title: "Open Source Projects",
|
||||
description: "Contributors can test their changes without needing registry access. No secrets, no onboarding friction, just push and test.",
|
||||
highlight: "Instant contributor experience",
|
||||
},
|
||||
{
|
||||
icon: Boxes,
|
||||
title: "Local Development",
|
||||
description: "Share images between your local machines, test on different architectures, or hand off to a teammate. Quick, easy, no setup.",
|
||||
highlight: "Share without friction",
|
||||
},
|
||||
];
|
||||
|
||||
export function UseCases() {
|
||||
return (
|
||||
<section className="relative py-24 px-4 border-t border-border">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<section className="relative py-32 px-4 overflow-hidden">
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-muted/30 via-background to-muted/30" />
|
||||
|
||||
<div className="relative z-10 max-w-6xl mx-auto">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-4">
|
||||
Perfect for CI workflows
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-4">
|
||||
Built for{" "}
|
||||
<span className="bg-gradient-to-r from-accent to-accent/70 bg-clip-text text-transparent">
|
||||
real workflows
|
||||
</span>
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-lg max-w-2xl mx-auto">
|
||||
ttl.sh solves the problem of sharing intermediate build artifacts
|
||||
without managing credentials or cleanup.
|
||||
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
||||
From solo developers to massive CI/CD pipelines,
|
||||
<br />
|
||||
ttl.sh fits wherever you need temporary images.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{useCases.map((useCase) => (
|
||||
<div className="grid md:grid-cols-2 gap-8">
|
||||
{useCases.map((useCase, i) => (
|
||||
<div
|
||||
key={useCase.title}
|
||||
className="p-6 bg-card border border-border rounded-lg"
|
||||
key={i}
|
||||
className="group relative p-8 rounded-2xl border border-border/50 bg-card/30 backdrop-blur-sm hover:bg-card/50 transition-all duration-300"
|
||||
>
|
||||
<div className="w-12 h-12 rounded-lg bg-secondary flex items-center justify-center mb-4">
|
||||
<useCase.icon className="w-6 h-6 text-foreground" />
|
||||
<div className="flex items-start gap-5">
|
||||
<div className="shrink-0 p-3 rounded-xl bg-accent/10 text-accent group-hover:bg-accent group-hover:text-accent-foreground transition-colors">
|
||||
<useCase.icon className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold mb-2">{useCase.title}</h3>
|
||||
<p className="text-muted-foreground mb-4 leading-relaxed">{useCase.description}</p>
|
||||
<span className="inline-block px-3 py-1 rounded-full bg-accent/10 text-accent text-sm font-medium">
|
||||
{useCase.highlight}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold mb-3">{useCase.title}</h3>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
{useCase.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user