Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 54 additions & 77 deletions app/integration/integration-catalog.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
'use client';

import { ArrowUpRightIcon, PlugZapIcon } from "lucide-react";
import Link from "next/link";
import { useEffect, useMemo, useState, type KeyboardEvent } from "react";

import { cn } from "@/lib/utils";
import type { IntegrationCategory, IntegrationProject } from "@/data/integrations";
import { cn } from "@/lib/utils";

interface IntegrationCatalogProps {
categories: IntegrationCategory[];
Expand Down Expand Up @@ -104,11 +105,14 @@ export default function IntegrationCatalog({ categories }: IntegrationCatalogPro
};

return (
<div className="mt-8">
<nav className="flex gap-px overflow-x-auto border border-border bg-border" role="tablist" aria-label="Integration category tabs">
<div>
<nav
className="flex gap-px overflow-x-auto border border-border bg-border xl:grid xl:grid-cols-7"
role="tablist"
aria-label="Integration category tabs"
>
Comment on lines +109 to +113
{tabs.map((tab, index) => {
const isActive = activeTab === tab.id;
const palette = getCategoryPalette(tab.id);

return (
<button
Expand All @@ -122,18 +126,21 @@ export default function IntegrationCatalog({ categories }: IntegrationCatalogPro
onClick={() => setActiveTab(tab.id)}
onKeyDown={(event) => handleTabKeyDown(event, index)}
className={cn(
"group relative min-h-12 min-w-40 border-0 bg-card px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground transition-colors hover:text-foreground focus:outline-hidden focus-visible:ring-2 focus-visible:ring-brand/40",
isActive ? cn("text-foreground", palette.tabActive) : "hover:bg-muted/40",
"group relative min-h-16 min-w-44 bg-card px-4 py-4 text-left text-[10px] font-semibold uppercase tracking-[0.16em] text-muted-foreground transition-colors hover:bg-muted/40 hover:text-foreground focus:outline-hidden focus-visible:ring-2 focus-visible:ring-brand/40 xl:min-w-0",
isActive && "bg-muted/35 text-foreground",
)}
>
<span
aria-hidden="true"
className={cn(
"absolute inset-y-0 left-0 w-0.5 bg-transparent transition-colors",
isActive && palette.tabStripe,
isActive && "bg-brand",
)}
/>
{tab.label}
<span className="block font-mono text-[8px] tracking-[0.14em] text-muted-foreground">
{String(index).padStart(2, "0")}
</span>
<span className="mt-1 block">{tab.label}</span>
</button>
);
})}
Expand All @@ -143,94 +150,64 @@ export default function IntegrationCatalog({ categories }: IntegrationCatalogPro
id="integration-tab-panel"
role="tabpanel"
aria-labelledby={`integration-tab-${activeTab}`}
className="mt-5 border border-border bg-card p-4 sm:p-5"
className="mt-4 border border-border bg-background p-4 sm:p-5"
>
<div className="mb-4 flex flex-wrap items-center justify-between gap-3 border-b border-border pb-4">
<p className="text-[10px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">Active tab</p>
<div className="flex items-center gap-3">
<p className="text-[10px] font-semibold uppercase tracking-[0.18em] text-brand">{activeTabLabel}</p>
<p className="text-[10px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">{projects.length} projects</p>
<p className="font-mono text-[10px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">Documentation paths</p>
<div className="flex items-center gap-3 font-mono text-[10px] font-semibold uppercase tracking-[0.18em]">
<p className="text-brand">{activeTabLabel}</p>
<span className="h-3 w-px bg-border" aria-hidden="true" />
<p className="text-muted-foreground">{projects.length} projects</p>
</div>
</div>

<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
{projects.map((project, index) => {
const palette = getCategoryPalette(project.categoryId);

return (
<article
key={`${project.categoryLabel}-${project.name}`}
data-motion-delay={String(index % 4)}
className={cn(
"motion-card motion-reveal group relative flex min-h-56 flex-col overflow-hidden border p-5 transition-colors",
"border-border bg-card hover:border-brand/60 hover:bg-muted/30",
)}
>
<span
aria-hidden="true"
className={cn(
"absolute inset-x-0 top-0 h-0.5",
palette.cardTop,
)}
/>
<span
aria-hidden="true"
className={cn(
"pointer-events-none absolute -right-12 -top-12 h-24 w-24 blur-2xl",
palette.cardAura,
)}
/>

<div className="mb-4 flex items-center justify-between gap-4">
<span className={cn("text-[10px] font-semibold uppercase tracking-[0.16em]", palette.caseText)}>
Case.{String(index + 1).padStart(2, "0")}
</span>
<span className="text-[10px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">{project.categoryLabel}</span>
</div>

<div>
<h3 className="text-lg font-semibold leading-tight text-foreground">
{project.name}
</h3>
<p className="mt-3 text-sm leading-6 text-muted-foreground">{project.description}</p>
</div>

<div className="mt-auto flex items-center gap-2 pt-6">
<ActionButton href={project.docsUrl} label="Docs" />
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{projects.map((project, index) => (
<article
key={`${project.categoryLabel}-${project.name}`}
data-motion-delay={String(index % 4)}
className="motion-card motion-reveal group flex min-h-64 flex-col border border-border bg-card p-5 transition-colors hover:border-brand/40 hover:bg-muted/20 sm:p-6"
>
<div className="flex items-start justify-between gap-4">
<span className="motion-icon-tile flex size-11 items-center justify-center border border-border bg-background text-brand">
<PlugZapIcon className="size-4" />
</span>
<div className="text-right font-mono text-[9px] font-semibold uppercase tracking-[0.15em] text-muted-foreground">
<p>{project.categoryLabel}</p>
<p className="mt-1 text-brand">Case.{String(index + 1).padStart(2, "0")}</p>
</div>
</article>
);
})}
</div>

<div className="mt-8">
<h3 className="text-xl font-semibold leading-tight tracking-tight text-foreground">
{project.name}
</h3>
<p className="mt-3 text-sm leading-7 text-muted-foreground">{project.description}</p>
</div>

<div className="mt-auto pt-7">
<ActionButton href={project.docsUrl} />
</div>
</article>
))}
</div>
</div>
</div>
);
}

function getCategoryPalette(categoryId: string) {
return {
tabActive: categoryId === "all" ? "bg-[#0062FF]/12" : "bg-[#0062FF]/10",
tabStripe: "bg-[#0062FF]",
cardTop: "bg-[#0062FF]",
cardAura: "bg-[#0062FF]/25",
caseText: "text-[#0062FF]",
};
}

function ActionButton({ href, label }: { href: string; label: string }) {
function ActionButton({ href }: { href: string }) {
const isExternal = href.startsWith("http://") || href.startsWith("https://");

return (
<Link
href={href}
target={isExternal ? "_blank" : undefined}
rel={isExternal ? "noreferrer noopener external" : undefined}
className="inline-flex items-center border border-brand bg-brand px-3 py-2 text-[10px] font-semibold uppercase tracking-[0.14em] text-brand-foreground transition-colors hover:bg-brand/90"
className="motion-button group inline-flex h-10 items-center gap-2 border border-border bg-background px-3 text-[10px] font-semibold uppercase tracking-[0.14em] text-foreground hover:border-foreground hover:bg-foreground hover:text-background"
>
<span>{label}</span>
<span className="motion-arrow ml-1.5" aria-hidden="true">
</span>
<span>Open docs</span>
<ArrowUpRightIcon className="motion-arrow size-3.5" aria-hidden="true" />
</Link>
);
}
}
140 changes: 110 additions & 30 deletions app/integration/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { BoxesIcon, DatabaseIcon, NetworkIcon } from "lucide-react";
import type { Metadata } from "next";

import { SITE_CONFIG } from "@/app.config";
Expand Down Expand Up @@ -37,6 +38,15 @@ export const metadata: Metadata = {
},
};

const integrationSignals = [
{ label: "Protocol", value: "S3 API" },
{ label: "Categories", value: String(integrationCategories.length) },
{
label: "Guides",
value: String(integrationCategories.reduce((total, category) => total + category.projects.length, 0)),
},
];

export default function IntegrationPage() {
const jsonLd = {
"@context": "https://schema.org",
Expand All @@ -58,49 +68,119 @@ export default function IntegrationPage() {
};

return (
<main className="relative flex-1">
<main className="relative z-10 flex-1 text-foreground">
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>

<section className="relative overflow-hidden border-y border-border py-16 text-foreground sm:py-24">
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 opacity-70 [background-image:linear-gradient(90deg,var(--border)_1px,transparent_1px),linear-gradient(0deg,var(--border)_1px,transparent_1px)] [background-size:34px_34px]"
/>
<div aria-hidden="true" className="pointer-events-none absolute -top-24 -left-20 h-72 w-72 bg-[#0062FF]/16 blur-3xl" />
<div aria-hidden="true" className="pointer-events-none absolute -right-16 top-20 h-80 w-80 bg-[#0062FF]/12 blur-3xl" />
<section className="relative overflow-hidden border-b border-border bg-background">
<div className="mx-auto grid max-w-7xl gap-12 px-4 py-20 sm:px-6 lg:grid-cols-[1.04fr_0.96fr] lg:items-center lg:gap-16 lg:px-8 lg:py-28">
<div className="relative z-10">
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-brand">
Integration ecosystem
</p>
<h1 className="mt-5 font-display text-4xl font-extrabold leading-[1.04] tracking-[-0.04em] text-foreground sm:text-6xl">
Connect RustFS to the tools you already run.
</h1>
<p className="mt-6 max-w-3xl text-base leading-8 text-muted-foreground">
Build on one S3-compatible storage foundation across AI, DevOps, backup, security, analytics, and cloud-native infrastructure.
</p>

<div className="relative mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="mb-8 flex items-center gap-3 text-[11px] font-semibold uppercase tracking-[0.22em] text-muted-foreground">
<span className="h-1 w-24 bg-brand" />
<span>Integration cases</span>
<dl className="mt-9 grid grid-cols-3 border-y border-border">
{integrationSignals.map((signal, index) => (
<div
key={signal.label}
className={`py-4 ${index > 0 ? "border-l border-border pl-4 sm:pl-6" : "pr-4 sm:pr-6"}`}
>
<dt className="font-mono text-[9px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">
{signal.label}
</dt>
<dd className="mt-2 text-sm font-semibold text-foreground sm:text-base">{signal.value}</dd>
</div>
))}
</dl>
</div>

<h1 className="max-w-5xl font-display text-5xl font-semibold tracking-tight text-foreground sm:text-7xl">
RustFS ecosystem integrations.
</h1>
<p className="mt-6 max-w-3xl text-sm leading-7 text-muted-foreground">
Connect RustFS with DevOps pipelines, AI platforms, cloud-native systems, and analytics engines through
standard S3-compatible workflows.
</p>
<div className="relative min-h-[28rem] overflow-hidden border border-border bg-card p-5 sm:p-7" aria-hidden="true">
<div className="absolute inset-0 opacity-45 [background-image:linear-gradient(90deg,var(--border)_1px,transparent_1px),linear-gradient(0deg,var(--border)_1px,transparent_1px)] [background-size:32px_32px]" />
<div className="relative flex h-full min-h-[24rem] flex-col justify-between border border-border bg-background/90 p-5">
<div className="flex items-center justify-between font-mono text-[9px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">
<span>Ecosystem map</span>
<span className="flex items-center gap-2 text-brand"><span className="size-1.5 bg-brand" /> compatible</span>
</div>

<IntegrationCatalog categories={integrationCategories} />
<div className="grid grid-cols-[1fr_auto_1fr] items-center gap-3 sm:gap-5">
<div className="grid gap-2">
{["AI / ML", "DevOps", "Backup"].map((label) => (
<span key={label} className="border border-border bg-card px-3 py-3 text-center font-mono text-[9px] font-semibold uppercase tracking-[0.12em] text-muted-foreground">
{label}
</span>
))}
</div>

<section className="mt-6 border border-border bg-card/70 p-4 sm:p-5" aria-label="Trademark notice">
<p className="text-[10px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">Trademark notice</p>
<p className="mt-2 text-sm leading-7 text-muted-foreground">
All third-party project names are referenced only to describe technical compatibility and integration scenarios.
Third-party trademarks, names, and brands are the property of their respective owners. RustFS does not imply
endorsement, partnership, or affiliation unless explicitly stated in separate written agreements.
</p>
<p className="mt-2 text-sm leading-7 text-muted-foreground">
External documentation links may direct to third-party websites managed by their respective owners.
<div className="flex flex-col items-center">
<span className="h-px w-5 bg-border sm:w-8" />
<div className="grid size-20 shrink-0 place-items-center border border-brand bg-brand/10 text-brand sm:size-24">
<DatabaseIcon className="size-5" />
<span className="font-mono text-[9px] font-semibold uppercase tracking-[0.14em]">RustFS</span>
</div>
<span className="h-px w-5 bg-border sm:w-8" />
</div>

<div className="grid gap-2">
{["Security", "Big data", "Proxy"].map((label) => (
<span key={label} className="border border-border bg-card px-3 py-3 text-center font-mono text-[9px] font-semibold uppercase tracking-[0.12em] text-muted-foreground">
{label}
</span>
))}
</div>
Comment on lines +131 to +137
</div>

<div className="grid grid-cols-3 gap-px bg-border font-mono text-[9px] font-semibold uppercase tracking-[0.12em] text-muted-foreground">
<span className="flex items-center justify-center gap-2 bg-card px-2 py-3"><NetworkIcon className="size-3 text-brand" /> connect</span>
<span className="flex items-center justify-center gap-2 bg-card px-2 py-3"><DatabaseIcon className="size-3 text-brand" /> store</span>
<span className="flex items-center justify-center gap-2 bg-card px-2 py-3"><BoxesIcon className="size-3 text-brand" /> scale</span>
</div>
</div>
</div>
</div>
</section>

<section className="border-b border-border bg-muted/20 py-20 sm:py-24 lg:py-28">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="mb-9 lg:mb-12">
<div className="mb-7 flex items-center gap-4 text-[10px] font-semibold uppercase tracking-[0.2em] text-muted-foreground">
<span className="h-px w-8 shrink-0 bg-brand" aria-hidden="true" />
<span className="shrink-0">Integration directory</span>
<span className="h-px min-w-8 flex-1 bg-border" aria-hidden="true" />
</div>
<h2 className="text-4xl font-semibold leading-[1.04] tracking-[-0.035em] text-foreground md:text-5xl">
Find your integration path.
</h2>
<p className="mt-5 text-base leading-7 text-muted-foreground">
Browse implementation guides by workload. Each path opens the most relevant RustFS or upstream documentation.
</p>
</div>

<IntegrationCatalog categories={integrationCategories} />

<section className="mt-4 grid border border-border bg-card lg:grid-cols-[0.7fr_1.3fr]" aria-label="Trademark notice">
<div className="border-b border-border p-5 lg:border-r lg:border-b-0 sm:p-6">
<p className="font-mono text-[10px] font-semibold uppercase tracking-[0.18em] text-brand">Compatibility first</p>
<p className="mt-3 text-sm font-semibold leading-6 text-foreground">
One S3-compatible layer for the infrastructure around your data.
</p>
</div>
<div className="p-5 sm:p-6">
<p className="font-mono text-[10px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">Trademark notice</p>
<p className="mt-3 text-sm leading-7 text-muted-foreground">
Third-party project names describe technical compatibility only. Their trademarks, names, brands, and external documentation remain the property and responsibility of their respective owners. No endorsement, partnership, or affiliation is implied unless separately stated in writing.
</p>
</div>
</section>
</div>
</section>
</main>
);
}
}