mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix: update tool count to 52 and add ai-canvas-expand to landing page
ai-canvas-expand was added to constants.ts and route files but never added to the landing page bento grid, causing all hardcoded counts to remain at 51. This updates all references across source, docs, i18n, and tests to reflect the correct count of 52 tools.
This commit is contained in:
@@ -15,7 +15,7 @@
|
||||
|
||||
## Key Features
|
||||
|
||||
- **51 image tools** - Resize, crop, compress, convert, watermark, color adjust, beautify screenshots, generate memes, vectorize, create GIFs, find duplicates, generate passport photos, and more. Supports 55+ input formats (including 23 camera RAW formats) and 14 output formats
|
||||
- **52 image tools** - Resize, crop, compress, convert, watermark, color adjust, beautify screenshots, generate memes, vectorize, create GIFs, find duplicates, generate passport photos, and more. Supports 55+ input formats (including 23 camera RAW formats) and 14 output formats
|
||||
- **Local AI** - Remove backgrounds, upscale images, restore and colorize old photos, erase objects, blur faces, enhance faces, extract text (OCR). All on your hardware - no internet required
|
||||
- **Pipelines** - Chain tools into reusable workflows with unlimited steps. Batch process unlimited images at once
|
||||
- **REST API** - Every tool available via API with API key auth. Interactive docs at `/api/docs`
|
||||
|
||||
@@ -3,7 +3,7 @@ info:
|
||||
title: SnapOtter API
|
||||
version: 1.16.0
|
||||
description: |
|
||||
REST API for SnapOtter, a self-hosted image processing platform with 51 tools.
|
||||
REST API for SnapOtter, a self-hosted image processing platform with 52 tools.
|
||||
|
||||
## Authentication
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ function generateLlmsTxt(spec: OpenAPISpec): string {
|
||||
lines.push(`# ${spec.info.title}`);
|
||||
lines.push("");
|
||||
lines.push(
|
||||
"> Self-hosted image processing API with 51 tools. Resize, compress, convert, remove backgrounds, upscale, run OCR, and more.",
|
||||
"> Self-hosted image processing API with 52 tools. Resize, compress, convert, remove backgrounds, upscale, run OCR, and more.",
|
||||
);
|
||||
lines.push("");
|
||||
lines.push("## Docs");
|
||||
|
||||
@@ -5,7 +5,7 @@ import pkg from "../package.json";
|
||||
export default defineConfig({
|
||||
title: "SnapOtter",
|
||||
description:
|
||||
"Documentation for SnapOtter - A Self Hosted Image Manipulator. 51 tools, local AI, pipelines, REST API.",
|
||||
"Documentation for SnapOtter - A Self Hosted Image Manipulator. 52 tools, local AI, pipelines, REST API.",
|
||||
base: "/",
|
||||
appearance: { initialValue: "light" },
|
||||
srcDir: ".",
|
||||
@@ -53,7 +53,7 @@ export default defineConfig({
|
||||
`,
|
||||
customTemplateVariables: {
|
||||
description:
|
||||
"SnapOtter is a self-hosted, open-source image processing platform with 51 tools including AI/ML. Runs in a single Docker container with GPU auto-detection.",
|
||||
"SnapOtter is a self-hosted, open-source image processing platform with 52 tools including AI/ML. Runs in a single Docker container with GPU auto-detection.",
|
||||
details:
|
||||
"Resize, compress, convert, remove backgrounds, upscale, run OCR, and more - without sending images to external services.",
|
||||
},
|
||||
|
||||
@@ -43,7 +43,7 @@ Shared TypeScript types, constants (like `APP_VERSION` and tool definitions), an
|
||||
|
||||
### API (`apps/api`)
|
||||
|
||||
A Fastify v5 server exposing 51 tool routes (35 standard image operations + 15 AI-powered + editor) that handles:
|
||||
A Fastify v5 server exposing 52 tool routes (36 standard image operations + 15 AI-powered + editor) that handles:
|
||||
- File uploads, temporary workspace management, and persistent file storage
|
||||
- User file library with version chains (`user_files` table) -- each processed result links back to its source file and records which tool was applied, with auto-generated thumbnails for the Files page
|
||||
- Tool execution (routes each tool request to the image engine or AI bridge)
|
||||
|
||||
@@ -64,7 +64,7 @@ pnpm dev
|
||||
|
||||
## What You Can Do
|
||||
|
||||
### Image Processing (51 Tools)
|
||||
### Image Processing (52 Tools)
|
||||
|
||||
| Category | Tools |
|
||||
|----------|-------|
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ layout: home
|
||||
hero:
|
||||
name: "SnapOtter"
|
||||
text: "A Self Hosted Image Manipulator"
|
||||
tagline: 51 tools. Local AI. No cloud. Your images never leave your home.
|
||||
tagline: 52 tools. Local AI. No cloud. Your images never leave your home.
|
||||
actions:
|
||||
- theme: brand
|
||||
text: Get started
|
||||
@@ -14,7 +14,7 @@ hero:
|
||||
link: /api/rest
|
||||
|
||||
features:
|
||||
- title: 51 Image Tools
|
||||
- title: 52 Image Tools
|
||||
details: Resize, crop, compress, convert, watermark, color adjust, vectorize, create GIFs, build collages, generate passport photos, find duplicates, and more.
|
||||
- title: Local AI
|
||||
details: 15 AI-powered tools - remove backgrounds, upscale, enhance images, restore and colorize old photos, erase objects, blur faces, enhance faces, extract text (OCR), fix fake transparency. All on your hardware, no internet required.
|
||||
|
||||
@@ -8,7 +8,7 @@ const nunito = Nunito({ subsets: ["latin"], variable: "--font-nunito" });
|
||||
export const metadata: Metadata = {
|
||||
title: "SnapOtter | Self-Hosted Image Processing",
|
||||
description:
|
||||
"51 image processing tools with local AI. Runs 100% offline. No data leaves your network. Open source and free forever.",
|
||||
"52 image processing tools with local AI. Runs 100% offline. No data leaves your network. Open source and free forever.",
|
||||
metadataBase: new URL("https://snapotter.com"),
|
||||
icons: {
|
||||
icon: [
|
||||
@@ -24,7 +24,7 @@ export const metadata: Metadata = {
|
||||
openGraph: {
|
||||
title: "SnapOtter | Self-Hosted Image Processing",
|
||||
description:
|
||||
"51 image processing tools with local AI. Runs 100% offline. No data leaves your network.",
|
||||
"52 image processing tools with local AI. Runs 100% offline. No data leaves your network.",
|
||||
url: "https://snapotter.com",
|
||||
siteName: "SnapOtter",
|
||||
type: "website",
|
||||
@@ -41,7 +41,7 @@ export const metadata: Metadata = {
|
||||
card: "summary_large_image",
|
||||
title: "SnapOtter | Self-Hosted Image Processing",
|
||||
description:
|
||||
"51 image processing tools with local AI. Runs 100% offline. No data leaves your network.",
|
||||
"52 image processing tools with local AI. Runs 100% offline. No data leaves your network.",
|
||||
images: ["/og-image.svg"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -245,6 +245,12 @@ const tools: { name: string; description: string; category: string; icon: Lucide
|
||||
category: "ai",
|
||||
icon: Scaling,
|
||||
},
|
||||
{
|
||||
name: "AI Canvas Expand",
|
||||
description: "Expand canvas with AI-powered fill",
|
||||
category: "ai",
|
||||
icon: Maximize2,
|
||||
},
|
||||
{
|
||||
name: "PNG Transparency Fixer",
|
||||
description: "Fix fake transparent PNGs with AI matting",
|
||||
@@ -415,7 +421,7 @@ export function BentoGrid() {
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<FadeIn>
|
||||
<h2 className="font-[family-name:var(--font-nunito)] text-center text-3xl font-bold tracking-tight md:text-4xl">
|
||||
51 tools. Zero cloud dependency.
|
||||
52 tools. Zero cloud dependency.
|
||||
</h2>
|
||||
<p className="mx-auto mt-4 max-w-xl text-center text-lg text-muted">
|
||||
Search to find exactly what you need. Every tool runs 100% locally.
|
||||
|
||||
@@ -7,7 +7,7 @@ const freePlan = {
|
||||
price: "Free",
|
||||
subtitle: "For everyone. Forever.",
|
||||
features: [
|
||||
"All 51 image processing tools",
|
||||
"All 52 image processing tools",
|
||||
"Unlimited usage, no hidden caps",
|
||||
"Full REST API with OpenAPI docs",
|
||||
"Pipeline automation",
|
||||
|
||||
@@ -11,7 +11,7 @@ const phrases = [
|
||||
"No limits. No hidden caps.",
|
||||
"Works fully offline.",
|
||||
"Unlimited batch processing.",
|
||||
"51 image tools.",
|
||||
"52 image tools.",
|
||||
"15 AI models. Your hardware.",
|
||||
"Lightning fast. Built on Sharp.",
|
||||
"Air-gapped ready.",
|
||||
|
||||
@@ -1607,7 +1607,7 @@ export const en = {
|
||||
heading: "About",
|
||||
appName: "SnapOtter",
|
||||
appDescription:
|
||||
"A self-hosted, privacy-first image processing suite with 51 tools. Resize, compress, convert, watermark, and automate your image workflows without sending data to the cloud.",
|
||||
"A self-hosted, privacy-first image processing suite with 52 tools. Resize, compress, convert, watermark, and automate your image workflows without sending data to the cloud.",
|
||||
versionLabel: "Version:",
|
||||
linksHeading: "Links",
|
||||
githubLink: "GitHub Repository",
|
||||
@@ -1650,7 +1650,7 @@ export const en = {
|
||||
"No limits. No hidden caps.",
|
||||
"Works fully offline.",
|
||||
"Unlimited batch processing.",
|
||||
"51 image tools.",
|
||||
"52 image tools.",
|
||||
"15 AI models. Your hardware.",
|
||||
"Lightning fast. Built on Sharp.",
|
||||
"Air-gapped ready.",
|
||||
|
||||
@@ -1663,7 +1663,7 @@ export const nl: TranslationKeys = {
|
||||
heading: "Over",
|
||||
appName: "SnapOtter",
|
||||
appDescription:
|
||||
"Een zelf-gehoste, privacy-first beeldverwerkingssuite met 51 tools. Schalen, comprimeren, converteren, watermerken en je beeldworkflows automatiseren zonder data naar de cloud te sturen.",
|
||||
"Een zelf-gehoste, privacy-first beeldverwerkingssuite met 52 tools. Schalen, comprimeren, converteren, watermerken en je beeldworkflows automatiseren zonder data naar de cloud te sturen.",
|
||||
versionLabel: "Versie:",
|
||||
linksHeading: "Links",
|
||||
githubLink: "GitHub-repository",
|
||||
|
||||
@@ -13,7 +13,7 @@ test.describe("Docs Homepage", () => {
|
||||
await expect(page.getByText("SnapOtter").first()).toBeVisible();
|
||||
await expect(page.getByText("A Self Hosted Image Manipulator")).toBeVisible();
|
||||
await expect(
|
||||
page.getByText("51 tools. Local AI. No cloud. Your images never leave your home."),
|
||||
page.getByText("52 tools. Local AI. No cloud. Your images never leave your home."),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -29,7 +29,7 @@ test.describe("Docs Homepage", () => {
|
||||
|
||||
test("features section renders all 6 feature cards", async ({ page }) => {
|
||||
const features = [
|
||||
"51 Image Tools",
|
||||
"52 Image Tools",
|
||||
"Local AI",
|
||||
"Pipelines",
|
||||
"REST API",
|
||||
|
||||
@@ -57,9 +57,9 @@ test.describe("Landing Homepage", () => {
|
||||
});
|
||||
|
||||
test("bento grid renders with search and tool count", async ({ page }) => {
|
||||
await expect(page.getByText("51 tools. Zero cloud dependency.")).toBeVisible();
|
||||
await expect(page.getByText("52 tools. Zero cloud dependency.")).toBeVisible();
|
||||
await expect(page.getByPlaceholder("Search tools...")).toBeVisible();
|
||||
await expect(page.getByText(/Showing 51 of 51 tools/)).toBeVisible();
|
||||
await expect(page.getByText(/Showing 52 of 52 tools/)).toBeVisible();
|
||||
});
|
||||
|
||||
test("enterprise section renders feature cards", async ({ page }) => {
|
||||
|
||||
@@ -9,28 +9,28 @@ test.describe("BentoGrid Interactions", () => {
|
||||
const input = page.getByPlaceholder("Search tools...");
|
||||
await input.fill("resize");
|
||||
await expect(page.getByText("Resize", { exact: true }).first()).toBeVisible();
|
||||
await expect(page.getByText(/Showing \d+ of 51 tools/)).toBeVisible();
|
||||
await expect(page.getByText(/Showing \d+ of 52 tools/)).toBeVisible();
|
||||
});
|
||||
|
||||
test("category pill filters tools", async ({ page }) => {
|
||||
await page.getByText(/AI Tools/).click();
|
||||
await expect(page.getByText(/Showing 15 of 51 tools/)).toBeVisible();
|
||||
await expect(page.getByText(/Showing 16 of 52 tools/)).toBeVisible();
|
||||
await expect(page.getByText("Remove Background")).toBeVisible();
|
||||
});
|
||||
|
||||
test("clicking All resets category filter", async ({ page }) => {
|
||||
await page.getByText(/AI Tools/).click();
|
||||
await expect(page.getByText(/Showing 15 of 51 tools/)).toBeVisible();
|
||||
await expect(page.getByText(/Showing 16 of 52 tools/)).toBeVisible();
|
||||
|
||||
await page.getByText(/All \(51\)/).click();
|
||||
await expect(page.getByText(/Showing 51 of 51 tools/)).toBeVisible();
|
||||
await page.getByText(/All \(52\)/).click();
|
||||
await expect(page.getByText(/Showing 52 of 52 tools/)).toBeVisible();
|
||||
});
|
||||
|
||||
test("search with no results shows empty state", async ({ page }) => {
|
||||
const input = page.getByPlaceholder("Search tools...");
|
||||
await input.fill("xyznonexistent");
|
||||
await expect(page.getByText("No tools found. Try a different search.")).toBeVisible();
|
||||
await expect(page.getByText(/Showing 0 of 51 tools/)).toBeVisible();
|
||||
await expect(page.getByText(/Showing 0 of 52 tools/)).toBeVisible();
|
||||
});
|
||||
|
||||
test("combined search and category filter works", async ({ page }) => {
|
||||
|
||||
@@ -47,7 +47,7 @@ function generateLlmsTxt(spec: OpenAPISpec): string {
|
||||
lines.push(`# ${spec.info.title}`);
|
||||
lines.push("");
|
||||
lines.push(
|
||||
"> Self-hosted image processing API with 51 tools. Resize, compress, convert, remove backgrounds, upscale, run OCR, and more.",
|
||||
"> Self-hosted image processing API with 52 tools. Resize, compress, convert, remove backgrounds, upscale, run OCR, and more.",
|
||||
);
|
||||
lines.push("");
|
||||
lines.push("## Docs");
|
||||
|
||||
@@ -25,7 +25,7 @@ function getCountText(): string {
|
||||
describe("BentoGrid", () => {
|
||||
it("renders the section heading", () => {
|
||||
render(<BentoGrid />);
|
||||
expect(screen.getByText("51 tools. Zero cloud dependency.")).toBeDefined();
|
||||
expect(screen.getByText("52 tools. Zero cloud dependency.")).toBeDefined();
|
||||
});
|
||||
|
||||
it("renders the search input", () => {
|
||||
@@ -36,12 +36,12 @@ describe("BentoGrid", () => {
|
||||
it("shows all tools by default", () => {
|
||||
render(<BentoGrid />);
|
||||
const text = getCountText();
|
||||
expect(text).toMatch(/Showing 51 of 51 tools/);
|
||||
expect(text).toMatch(/Showing 52 of 52 tools/);
|
||||
});
|
||||
|
||||
it("renders category filter pills including All", () => {
|
||||
render(<BentoGrid />);
|
||||
expect(screen.getByText((_, el) => el?.textContent === "All (51)")).toBeDefined();
|
||||
expect(screen.getByText((_, el) => el?.textContent === "All (52)")).toBeDefined();
|
||||
expect(screen.getByText(/Essentials/)).toBeDefined();
|
||||
expect(screen.getByText(/AI Tools/)).toBeDefined();
|
||||
expect(screen.getByText(/Optimization/)).toBeDefined();
|
||||
@@ -53,7 +53,7 @@ describe("BentoGrid", () => {
|
||||
fireEvent.change(input, { target: { value: "resize" } });
|
||||
expect(screen.getByText("Resize")).toBeDefined();
|
||||
const text = getCountText();
|
||||
expect(text).toMatch(/Showing \d+ of 51 tools/);
|
||||
expect(text).toMatch(/Showing \d+ of 52 tools/);
|
||||
expect(screen.queryByText("OCR / Text Extraction")).toBeNull();
|
||||
});
|
||||
|
||||
@@ -62,7 +62,7 @@ describe("BentoGrid", () => {
|
||||
const aiButton = screen.getByText(/AI Tools/);
|
||||
fireEvent.click(aiButton);
|
||||
const text = getCountText();
|
||||
expect(text).toMatch(/Showing 15 of 51 tools/);
|
||||
expect(text).toMatch(/Showing 16 of 52 tools/);
|
||||
expect(screen.getByText("Remove Background")).toBeDefined();
|
||||
expect(screen.queryByText("Resize")).toBeNull();
|
||||
});
|
||||
@@ -73,7 +73,7 @@ describe("BentoGrid", () => {
|
||||
fireEvent.change(input, { target: { value: "xyznonexistent" } });
|
||||
expect(screen.getByText("No tools found. Try a different search.")).toBeDefined();
|
||||
const text = getCountText();
|
||||
expect(text).toMatch(/Showing 0 of 51 tools/);
|
||||
expect(text).toMatch(/Showing 0 of 52 tools/);
|
||||
});
|
||||
|
||||
it("combines search and category filter", () => {
|
||||
@@ -89,9 +89,9 @@ describe("BentoGrid", () => {
|
||||
it("clicking All resets category filter", () => {
|
||||
render(<BentoGrid />);
|
||||
fireEvent.click(screen.getByText(/AI Tools/));
|
||||
expect(getCountText()).toMatch(/Showing 15 of 51 tools/);
|
||||
fireEvent.click(screen.getByText((_, el) => el?.textContent === "All (51)"));
|
||||
expect(getCountText()).toMatch(/Showing 51 of 51 tools/);
|
||||
expect(getCountText()).toMatch(/Showing 16 of 52 tools/);
|
||||
fireEvent.click(screen.getByText((_, el) => el?.textContent === "All (52)"));
|
||||
expect(getCountText()).toMatch(/Showing 52 of 52 tools/);
|
||||
});
|
||||
|
||||
it("renders each tool with name and description", () => {
|
||||
|
||||
@@ -157,7 +157,7 @@ describe("Pricing", () => {
|
||||
|
||||
it("renders free plan features", () => {
|
||||
render(<Pricing />);
|
||||
expect(screen.getByText("All 51 image processing tools")).toBeDefined();
|
||||
expect(screen.getByText("All 52 image processing tools")).toBeDefined();
|
||||
expect(screen.getByText("Unlimited usage, no hidden caps")).toBeDefined();
|
||||
expect(screen.getByText("15 local AI models included")).toBeDefined();
|
||||
expect(screen.getByText("AGPL-3.0 licensed")).toBeDefined();
|
||||
|
||||
Reference in New Issue
Block a user