feat(feedback): always-on nav button with GitHub/email handoff when analytics is off (#428)

Keep the top-nav feedback button always visible (icon plus label on desktop, icon-only on mobile) instead of hiding it when an instance opts out of analytics. When analytics is off, the dialog keeps the typed message and hands off to a prefilled GitHub issue plus a contact@snapotter.com email, with no fake Thanks. Adds a feedback.yml issue template, URL builders, and feedback strings across all 21 locales.

Claude-Session: https://claude.ai/code/session_01XVrHKXwzZDWBWgkGQdPZ3A
This commit is contained in:
SnapOtter
2026-07-04 17:37:12 +08:00
committed by GitHub
parent 7b04317ed2
commit 5dcc06a99e
28 changed files with 302 additions and 10 deletions
+36
View File
@@ -0,0 +1,36 @@
import { describe, expect, it } from "vitest";
import {
buildFeedbackGithubUrl,
buildFeedbackMailtoUrl,
SNAPOTTER_FEEDBACK_EMAIL,
} from "@/lib/feedback";
describe("buildFeedbackGithubUrl", () => {
it("targets the feedback issue template and prefills the message", () => {
const url = buildFeedbackGithubUrl("The export button is slow");
expect(url).toContain("https://github.com/snapotter-hq/snapotter/issues/new");
expect(url).toContain("template=feedback.yml");
// URLSearchParams encodes spaces as "+"
expect(url).toContain("details=The+export+button+is+slow");
});
it("trims and clamps the message to 2000 characters", () => {
const url = buildFeedbackGithubUrl(` hi ${"x".repeat(2100)}`);
const details = new URL(url).searchParams.get("details") ?? "";
expect(details.startsWith("hi")).toBe(true);
expect(details.length).toBeLessThanOrEqual(2000);
});
});
describe("buildFeedbackMailtoUrl", () => {
it("builds a mailto to the project address with an encoded body", () => {
const url = buildFeedbackMailtoUrl("Love the app, one bug");
expect(url.startsWith(`mailto:${SNAPOTTER_FEEDBACK_EMAIL}`)).toBe(true);
expect(url).toContain("subject=SnapOtter%20feedback");
expect(url).toContain("body=Love%20the%20app%2C%20one%20bug");
});
it("uses contact@snapotter.com", () => {
expect(SNAPOTTER_FEEDBACK_EMAIL).toBe("contact@snapotter.com");
});
});