feat(shared): add apiToolPath() and section-prefixed routes; drop MODALITY_URL_SLUG

This commit is contained in:
SnapOtter
2026-06-20 11:05:44 +08:00
parent 790747188a
commit 9ca901f8d5
4 changed files with 47 additions and 18 deletions
+14 -8
View File
@@ -1,10 +1,5 @@
import { import { AUDIO_INPUTS, IMAGE_INPUTS, SUBTITLE_INPUTS, VIDEO_INPUTS } from "./modality.js";
AUDIO_INPUTS, import { toolSection } from "./section.js";
IMAGE_INPUTS,
MODALITY_URL_SLUG,
SUBTITLE_INPUTS,
VIDEO_INPUTS,
} from "./modality.js";
import type { CategoryInfo, SocialMediaPreset, Tool } from "./types.js"; import type { CategoryInfo, SocialMediaPreset, Tool } from "./types.js";
export const CATEGORIES: CategoryInfo[] = [ export const CATEGORIES: CategoryInfo[] = [
@@ -1790,7 +1785,18 @@ export const TOOLS: Tool[] = [
]; ];
for (const tool of TOOLS) { for (const tool of TOOLS) {
tool.route = `/${MODALITY_URL_SLUG[tool.modality]}${tool.route}`; const slug = `/${toolSection(tool)}`;
if (!tool.route.startsWith(`${slug}/`)) {
tool.route = `${slug}${tool.route}`;
}
}
export function apiToolPath(
toolOrId: string | Pick<Tool, "id" | "modality" | "acceptedInputs">,
): string {
const tool = typeof toolOrId === "string" ? TOOLS.find((t) => t.id === toolOrId) : toolOrId;
if (!tool) throw new Error(`apiToolPath: unknown tool "${String(toolOrId)}"`);
return `/api/v1/tools/${toolSection(tool)}/${tool.id}`;
} }
export const SOCIAL_MEDIA_PRESETS: SocialMediaPreset[] = [ export const SOCIAL_MEDIA_PRESETS: SocialMediaPreset[] = [
-8
View File
@@ -117,14 +117,6 @@ export const DOCUMENT_INPUTS = [
]; ];
export const FILE_INPUTS = [".csv", ".json", ".xml", ".yaml", ".yml", ".zip"]; export const FILE_INPUTS = [".csv", ".json", ".xml", ".yaml", ".yml", ".zip"];
export const MODALITY_URL_SLUG: Record<Modality, string> = {
image: "image",
video: "video",
audio: "audio",
document: "document",
file: "data",
};
export function detectModalityFromMime(mime: string): Modality { export function detectModalityFromMime(mime: string): Modality {
if (mime.startsWith("image/")) return "image"; if (mime.startsWith("image/")) return "image";
if (mime.startsWith("video/")) return "video"; if (mime.startsWith("video/")) return "video";
+2 -2
View File
@@ -62,9 +62,9 @@ test.describe("Navigation", () => {
}); });
test("clicking a tool on home page navigates to tool page", async ({ loggedInPage: page }) => { test("clicking a tool on home page navigates to tool page", async ({ loggedInPage: page }) => {
// Routes are /:modality/:toolId (App.tsx line 243). // Routes are /:section/:toolId (App.tsx line 243).
// Resize route = /image/resize (constants.ts: route "/resize" + modality "image", // Resize route = /image/resize (constants.ts: route "/resize" + modality "image",
// post-processed at line 1793 with MODALITY_URL_SLUG prefix). // section prefix added via toolSection() in the route post-processing loop).
await page.goto("/"); await page.goto("/");
// Click on Resize tool // Click on Resize tool
+31
View File
@@ -0,0 +1,31 @@
import { apiToolPath, TOOLS } from "@snapotter/shared";
import { describe, expect, it } from "vitest";
describe("apiToolPath", () => {
it("builds /api/v1/tools/<section>/<id>", () => {
expect(apiToolPath("crop")).toBe("/api/v1/tools/image/crop");
expect(apiToolPath("merge-pdf")).toBe("/api/v1/tools/pdf/merge-pdf");
expect(apiToolPath("word-to-pdf")).toBe("/api/v1/tools/files/word-to-pdf");
expect(apiToolPath("csv-json")).toBe("/api/v1/tools/files/csv-json");
});
it("throws on unknown tool", () => {
expect(() => apiToolPath("nope")).toThrow(/unknown tool/);
});
});
describe("route values", () => {
it("are section-prefixed and unique", () => {
const routes = TOOLS.map((t) => t.route);
expect(routes).toContain("/pdf/merge-pdf");
expect(routes).toContain("/files/csv-json");
expect(routes).toContain("/image/crop");
expect(new Set(routes).size).toBe(TOOLS.length); // no collisions
// every route is exactly /<section>/<id> (two segments)
expect(routes.every((r) => r.split("/").length === 3)).toBe(true);
});
it("has globally unique ids", () => {
expect(new Set(TOOLS.map((t) => t.id)).size).toBe(TOOLS.length);
});
});