mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
Zero client JS, 165 static pages, self-hosted fonts, Otter Orange design system. Includes tool SEO data for all 157 tools, enterprise page, and updated e2e landing tests.
117 lines
3.2 KiB
Plaintext
117 lines
3.2 KiB
Plaintext
---
|
|
import { TOOLS } from "@snapotter/shared";
|
|
|
|
const toolCount = TOOLS.length;
|
|
|
|
function formatNumber(n: number): string {
|
|
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1).replace(/\.0$/, "")}M`;
|
|
if (n >= 1_000) return `${(n / 1_000).toFixed(1).replace(/\.0$/, "")}k`;
|
|
return n.toString();
|
|
}
|
|
|
|
let starCount = 0;
|
|
try {
|
|
const res = await fetch("https://api.github.com/repos/snapotter-hq/snapotter", {
|
|
headers: { Accept: "application/vnd.github.v3+json" },
|
|
});
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
starCount = data.stargazers_count ?? 0;
|
|
}
|
|
} catch {
|
|
starCount = 0;
|
|
}
|
|
|
|
let dockerPulls = 0;
|
|
try {
|
|
const res = await fetch("https://hub.docker.com/v2/repositories/snapotter/snapotter/");
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
dockerPulls = data.pull_count ?? 0;
|
|
}
|
|
} catch {
|
|
dockerPulls = 0;
|
|
}
|
|
|
|
const stats = [
|
|
{
|
|
value: toolCount.toString(),
|
|
label: "Processing Tools",
|
|
sublabel: "Across 5 file modalities",
|
|
},
|
|
{
|
|
value: starCount > 0 ? formatNumber(starCount) : "--",
|
|
label: "GitHub Stars",
|
|
sublabel: "Open-source & growing",
|
|
href: "https://github.com/snapotter-hq/snapotter",
|
|
},
|
|
{
|
|
value: dockerPulls > 0 ? formatNumber(dockerPulls) : "--",
|
|
label: "Docker Pulls",
|
|
sublabel: "Deployed by organizations worldwide",
|
|
href: "https://hub.docker.com/r/snapotter/snapotter",
|
|
},
|
|
{
|
|
value: "21",
|
|
label: "Languages",
|
|
sublabel: "Fully localized UI",
|
|
},
|
|
];
|
|
|
|
const badges = [
|
|
"GDPR Compliant",
|
|
"Air-Gapped Ready",
|
|
"SAML SSO",
|
|
"Audit Logging",
|
|
"Docker & K8s",
|
|
"ARM + x86",
|
|
];
|
|
---
|
|
|
|
<section class="border-y border-border bg-surface px-6 py-16 md:py-20">
|
|
<div class="mx-auto max-w-5xl">
|
|
|
|
<!-- Stats grid -->
|
|
<div class="reveal grid grid-cols-2 gap-10 sm:gap-12 lg:grid-cols-4">
|
|
{stats.map((stat) => {
|
|
const inner = (
|
|
<div class="text-center">
|
|
<p class="font-display text-5xl font-extrabold tracking-tight text-primary md:text-6xl">
|
|
{stat.value}
|
|
</p>
|
|
<p class="mt-2 text-sm font-semibold text-foreground">{stat.label}</p>
|
|
<p class="mt-0.5 text-xs text-muted">{stat.sublabel}</p>
|
|
</div>
|
|
);
|
|
|
|
return stat.href ? (
|
|
<a
|
|
href={stat.href}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="group rounded-xl p-3 transition-colors hover:bg-background-alt"
|
|
>
|
|
{inner}
|
|
</a>
|
|
) : (
|
|
<div class="p-3">{inner}</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<!-- Divider -->
|
|
<div class="reveal mx-auto my-10 h-px w-24 bg-border md:my-12" />
|
|
|
|
<!-- Enterprise compliance badges -->
|
|
<div class="reveal flex flex-wrap items-center justify-center gap-2.5">
|
|
{badges.map((badge) => (
|
|
<span class="inline-flex items-center gap-1.5 rounded-full border border-border bg-background px-3.5 py-1.5 text-xs font-medium text-muted">
|
|
<svg class="h-3.5 w-3.5 text-success" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
|
|
{badge}
|
|
</span>
|
|
))}
|
|
</div>
|
|
|
|
</div>
|
|
</section>
|