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:
Marc Campbell
2026-02-03 00:40:37 +00:00
parent 3867e10558
commit 545872edee
7 changed files with 371 additions and 216 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
)}

View File

@@ -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&apos;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>
);

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>