From 2c45a3a9e8f282ada73af51d2db2a973817ac834 Mon Sep 17 00:00:00 2001 From: SnapOtter Date: Sat, 16 May 2026 10:02:19 +0800 Subject: [PATCH] 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. --- README.md | 2 +- apps/api/src/openapi.yaml | 2 +- apps/api/src/routes/docs.ts | 2 +- apps/docs/.vitepress/config.mts | 4 ++-- apps/docs/guide/architecture.md | 2 +- apps/docs/guide/getting-started.md | 2 +- apps/docs/index.md | 4 ++-- apps/landing/src/app/layout.tsx | 6 +++--- apps/landing/src/components/bento-grid.tsx | 8 +++++++- apps/landing/src/components/pricing.tsx | 2 +- apps/landing/src/components/typing-cursor.tsx | 2 +- packages/shared/src/i18n/en.ts | 4 ++-- packages/shared/src/i18n/nl.ts | 2 +- tests/e2e-docs/homepage.spec.ts | 4 ++-- tests/e2e-landing/homepage.spec.ts | 4 ++-- tests/e2e-landing/interactions.spec.ts | 12 ++++++------ tests/unit/api/docs-route.test.ts | 2 +- tests/unit/landing/bento-grid.test.tsx | 18 +++++++++--------- tests/unit/landing/components.test.tsx | 2 +- 19 files changed, 45 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index fc34fe25..67f82bfa 100644 --- a/README.md +++ b/README.md @@ -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` diff --git a/apps/api/src/openapi.yaml b/apps/api/src/openapi.yaml index cedeb38f..6da0c85e 100644 --- a/apps/api/src/openapi.yaml +++ b/apps/api/src/openapi.yaml @@ -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 diff --git a/apps/api/src/routes/docs.ts b/apps/api/src/routes/docs.ts index 771fb0ab..c21ec719 100644 --- a/apps/api/src/routes/docs.ts +++ b/apps/api/src/routes/docs.ts @@ -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"); diff --git a/apps/docs/.vitepress/config.mts b/apps/docs/.vitepress/config.mts index c0c5af80..29c63ef2 100644 --- a/apps/docs/.vitepress/config.mts +++ b/apps/docs/.vitepress/config.mts @@ -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.", }, diff --git a/apps/docs/guide/architecture.md b/apps/docs/guide/architecture.md index a918d094..ae86ee71 100644 --- a/apps/docs/guide/architecture.md +++ b/apps/docs/guide/architecture.md @@ -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) diff --git a/apps/docs/guide/getting-started.md b/apps/docs/guide/getting-started.md index 40ecc4ad..61242437 100644 --- a/apps/docs/guide/getting-started.md +++ b/apps/docs/guide/getting-started.md @@ -64,7 +64,7 @@ pnpm dev ## What You Can Do -### Image Processing (51 Tools) +### Image Processing (52 Tools) | Category | Tools | |----------|-------| diff --git a/apps/docs/index.md b/apps/docs/index.md index eb7fa9c9..b94fb97c 100644 --- a/apps/docs/index.md +++ b/apps/docs/index.md @@ -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. diff --git a/apps/landing/src/app/layout.tsx b/apps/landing/src/app/layout.tsx index 94cab048..5d013745 100644 --- a/apps/landing/src/app/layout.tsx +++ b/apps/landing/src/app/layout.tsx @@ -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"], }, }; diff --git a/apps/landing/src/components/bento-grid.tsx b/apps/landing/src/components/bento-grid.tsx index 2d24a189..c58ff4c4 100644 --- a/apps/landing/src/components/bento-grid.tsx +++ b/apps/landing/src/components/bento-grid.tsx @@ -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() {

- 51 tools. Zero cloud dependency. + 52 tools. Zero cloud dependency.

Search to find exactly what you need. Every tool runs 100% locally. diff --git a/apps/landing/src/components/pricing.tsx b/apps/landing/src/components/pricing.tsx index 56e4ce93..761c57e6 100644 --- a/apps/landing/src/components/pricing.tsx +++ b/apps/landing/src/components/pricing.tsx @@ -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", diff --git a/apps/landing/src/components/typing-cursor.tsx b/apps/landing/src/components/typing-cursor.tsx index 1c5df203..81994eb0 100644 --- a/apps/landing/src/components/typing-cursor.tsx +++ b/apps/landing/src/components/typing-cursor.tsx @@ -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.", diff --git a/packages/shared/src/i18n/en.ts b/packages/shared/src/i18n/en.ts index 5ad39e5b..6d4781a5 100644 --- a/packages/shared/src/i18n/en.ts +++ b/packages/shared/src/i18n/en.ts @@ -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.", diff --git a/packages/shared/src/i18n/nl.ts b/packages/shared/src/i18n/nl.ts index bcfcd51e..fcb9e761 100644 --- a/packages/shared/src/i18n/nl.ts +++ b/packages/shared/src/i18n/nl.ts @@ -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", diff --git a/tests/e2e-docs/homepage.spec.ts b/tests/e2e-docs/homepage.spec.ts index 235c191e..dc5521a0 100644 --- a/tests/e2e-docs/homepage.spec.ts +++ b/tests/e2e-docs/homepage.spec.ts @@ -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", diff --git a/tests/e2e-landing/homepage.spec.ts b/tests/e2e-landing/homepage.spec.ts index 9c599e1f..c44847b9 100644 --- a/tests/e2e-landing/homepage.spec.ts +++ b/tests/e2e-landing/homepage.spec.ts @@ -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 }) => { diff --git a/tests/e2e-landing/interactions.spec.ts b/tests/e2e-landing/interactions.spec.ts index be8eac65..1d07a579 100644 --- a/tests/e2e-landing/interactions.spec.ts +++ b/tests/e2e-landing/interactions.spec.ts @@ -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 }) => { diff --git a/tests/unit/api/docs-route.test.ts b/tests/unit/api/docs-route.test.ts index b81916a1..232eb5d9 100644 --- a/tests/unit/api/docs-route.test.ts +++ b/tests/unit/api/docs-route.test.ts @@ -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"); diff --git a/tests/unit/landing/bento-grid.test.tsx b/tests/unit/landing/bento-grid.test.tsx index 4066de68..bffcde33 100644 --- a/tests/unit/landing/bento-grid.test.tsx +++ b/tests/unit/landing/bento-grid.test.tsx @@ -25,7 +25,7 @@ function getCountText(): string { describe("BentoGrid", () => { it("renders the section heading", () => { render(); - 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(); 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(); - 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(); 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", () => { diff --git a/tests/unit/landing/components.test.tsx b/tests/unit/landing/components.test.tsx index 105e39d7..f405094c 100644 --- a/tests/unit/landing/components.test.tsx +++ b/tests/unit/landing/components.test.tsx @@ -157,7 +157,7 @@ describe("Pricing", () => { it("renders free plan features", () => { render(); - 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();