diff --git a/.github/ISSUE_TEMPLATE/feedback.yml b/.github/ISSUE_TEMPLATE/feedback.yml
new file mode 100644
index 00000000..fb736340
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feedback.yml
@@ -0,0 +1,16 @@
+name: General feedback
+description: Share general feedback, ideas, or reactions about SnapOtter.
+title: "[Feedback]: "
+labels: ["feedback"]
+body:
+ - type: markdown
+ attributes:
+ value: Thanks for taking a moment to share feedback.
+ - type: textarea
+ id: details
+ attributes:
+ label: Your feedback
+ description: What is on your mind?
+ placeholder: Tell us what you think...
+ validations:
+ required: true
diff --git a/apps/web/src/components/feedback/feedback-dialog.tsx b/apps/web/src/components/feedback/feedback-dialog.tsx
index 7dd1eba0..b1159d19 100644
--- a/apps/web/src/components/feedback/feedback-dialog.tsx
+++ b/apps/web/src/components/feedback/feedback-dialog.tsx
@@ -9,6 +9,8 @@ import { useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "@/contexts/i18n-context";
import { useFocusTrap } from "@/hooks/use-focus-trap";
import {
+ buildFeedbackGithubUrl,
+ buildFeedbackMailtoUrl,
type FeedbackErrorCategory,
type FeedbackFrictionArea,
type FeedbackImportantArea,
@@ -115,6 +117,7 @@ export function FeedbackDialog({
const isAdminInstall = source === "admin_installer";
const isSearchMiss = source === "search_miss";
+ const isGlobal = source === "global";
const canSubmit = Boolean(
message.trim() || sentiment || feedbackType !== "other" || isAdminInstall,
);
@@ -203,7 +206,28 @@ export function FeedbackDialog({
{submitted ? (
- {isSearchMiss && !accepted ? (
+ {isGlobal && !accepted ? (
+
+
{t.feedback.offlineDescription}
+
{t.feedback.offlinePublicNote}
+
+
+ ) : isSearchMiss && !accepted ? (
s.config);
const connectionStatus = useConnectionStore((s) => s.status);
const bannerVisible = connectionStatus !== "connected";
- const feedbackEnabled = Boolean(analyticsConfig?.enabled);
// Load global settings (disabled tools, experimental flag, default theme) on
// every authenticated page, not just the home grid. Without this, navigating
@@ -51,7 +48,6 @@ export function AppLayout({ children, breadcrumb, navVariant }: AppLayoutProps)
onHelpClick={() => setHelpOpen(true)}
onFeedbackClick={() => setFeedbackOpen(true)}
onSettingsClick={() => setSettingsOpen(true)}
- feedbackEnabled={feedbackEnabled}
/>
{/* Main content area */}
diff --git a/apps/web/src/components/layout/top-nav.tsx b/apps/web/src/components/layout/top-nav.tsx
index 75d34ffa..e899ddca 100644
--- a/apps/web/src/components/layout/top-nav.tsx
+++ b/apps/web/src/components/layout/top-nav.tsx
@@ -28,7 +28,6 @@ interface TopNavProps {
onHelpClick: () => void;
onFeedbackClick?: () => void;
onSettingsClick: () => void;
- feedbackEnabled?: boolean;
}
interface NavLinkItem {
@@ -59,7 +58,6 @@ export function TopNav({
onHelpClick,
onFeedbackClick,
onSettingsClick,
- feedbackEnabled = false,
}: TopNavProps) {
const location = useLocation();
const isMobile = useMobile();
@@ -114,7 +112,7 @@ export function TopNav({
- {feedbackEnabled && onFeedbackClick && (
+ {onFeedbackClick && (
}
{!isMobile && }
- {feedbackEnabled && onFeedbackClick && (
+ {onFeedbackClick && (
)}
diff --git a/apps/web/src/lib/feedback.ts b/apps/web/src/lib/feedback.ts
index 00a68b5a..99a87d20 100644
--- a/apps/web/src/lib/feedback.ts
+++ b/apps/web/src/lib/feedback.ts
@@ -155,3 +155,33 @@ export function shouldShowUsageSurvey({
export async function submitFeedback(payload: FeedbackPayload): Promise {
return apiPost("/v1/feedback", payload);
}
+
+const FEEDBACK_ISSUE_NEW_URL = "https://github.com/snapotter-hq/snapotter/issues/new";
+const MAX_FEEDBACK_LEN = 2000;
+
+export const SNAPOTTER_FEEDBACK_EMAIL = "contact@snapotter.com";
+
+/** Normalize newlines, trim, and clamp so the message is safe to put in a URL. */
+function sanitizeFeedbackMessage(message: string): string {
+ return message.replace(/\r\n/g, "\n").trim().slice(0, MAX_FEEDBACK_LEN);
+}
+
+/**
+ * Prefilled GitHub issue URL for general feedback. Blank issues are disabled on
+ * the repo, so we must target a template by file name; `details` matches the
+ * `id` of the textarea in `.github/ISSUE_TEMPLATE/feedback.yml`.
+ */
+export function buildFeedbackGithubUrl(message: string): string {
+ const params = new URLSearchParams({
+ template: "feedback.yml",
+ details: sanitizeFeedbackMessage(message),
+ });
+ return `${FEEDBACK_ISSUE_NEW_URL}?${params.toString()}`;
+}
+
+/** Prefilled mailto to the project address, for users who prefer a private channel. */
+export function buildFeedbackMailtoUrl(message: string): string {
+ const subject = encodeURIComponent("SnapOtter feedback");
+ const body = encodeURIComponent(sanitizeFeedbackMessage(message));
+ return `mailto:${SNAPOTTER_FEEDBACK_EMAIL}?subject=${subject}&body=${body}`;
+}
diff --git a/packages/shared/src/i18n/ar.ts b/packages/shared/src/i18n/ar.ts
index b1e54399..630cced4 100644
--- a/packages/shared/src/i18n/ar.ts
+++ b/packages/shared/src/i18n/ar.ts
@@ -42,6 +42,7 @@ export const ar: TranslationKeys = {
},
feedback: {
navLabel: "Share feedback",
+ navButtonLabel: "ملاحظات",
dialogTitle: "Share feedback",
toolDialogTitle: "How did this tool work?",
failedDialogTitle: "Report a tool issue",
@@ -65,6 +66,11 @@ export const ar: TranslationKeys = {
searchMissTitle: "طلب أداة",
searchMissContext: "لقد بحثت عن: {query}",
searchMissDiscussionsFallback: "تعذّر تسجيل ذلك. افتح طلبًا في Discussions.",
+ offlineDescription:
+ "التحليلات معطّلة في هذا المثيل، لذلك لا تُسجَّل الملاحظات هنا. لا يزال بإمكانك التواصل مباشرةً مع فريق SnapOtter.",
+ offlinePublicNote: "مشكلات GitHub علنية.",
+ offlineGithubButton: "افتح مشكلة على GitHub",
+ offlineEmailButton: "راسلنا",
thanksTitle: "Thanks for the feedback.",
thanksDescription: "It helps us improve SnapOtter without collecting files or private content.",
quickThanks: "Thanks for the signal.",
diff --git a/packages/shared/src/i18n/de.ts b/packages/shared/src/i18n/de.ts
index 8494fb4a..32756be0 100644
--- a/packages/shared/src/i18n/de.ts
+++ b/packages/shared/src/i18n/de.ts
@@ -42,6 +42,7 @@ export const de: TranslationKeys = {
},
feedback: {
navLabel: "Share feedback",
+ navButtonLabel: "Feedback",
dialogTitle: "Share feedback",
toolDialogTitle: "How did this tool work?",
failedDialogTitle: "Report a tool issue",
@@ -66,6 +67,11 @@ export const de: TranslationKeys = {
searchMissContext: "Du hast gesucht nach: {query}",
searchMissDiscussionsFallback:
"Das konnten wir nicht speichern. Stelle eine Anfrage in Discussions.",
+ offlineDescription:
+ "Für diese Instanz ist die Analyse deaktiviert, daher wird Feedback hier nicht erfasst. Du kannst das SnapOtter-Team weiterhin direkt erreichen.",
+ offlinePublicNote: "GitHub-Issues sind öffentlich.",
+ offlineGithubButton: "Ein GitHub-Issue öffnen",
+ offlineEmailButton: "Schreib uns",
thanksTitle: "Thanks for the feedback.",
thanksDescription: "It helps us improve SnapOtter without collecting files or private content.",
quickThanks: "Thanks for the signal.",
diff --git a/packages/shared/src/i18n/en.ts b/packages/shared/src/i18n/en.ts
index 45f38960..a50d84e1 100644
--- a/packages/shared/src/i18n/en.ts
+++ b/packages/shared/src/i18n/en.ts
@@ -40,6 +40,7 @@ export const en = {
},
feedback: {
navLabel: "Share feedback",
+ navButtonLabel: "Feedback",
dialogTitle: "Share feedback",
toolDialogTitle: "How did this tool work?",
failedDialogTitle: "Report a tool issue",
@@ -63,6 +64,11 @@ export const en = {
searchMissTitle: "Request a tool",
searchMissContext: "You searched for: {query}",
searchMissDiscussionsFallback: "We couldn't record that. Open a request in Discussions.",
+ offlineDescription:
+ "This instance has analytics turned off, so feedback isn't recorded here. You can still reach the SnapOtter team directly.",
+ offlinePublicNote: "GitHub issues are public.",
+ offlineGithubButton: "Open a GitHub issue",
+ offlineEmailButton: "Email us",
thanksTitle: "Thanks for the feedback.",
thanksDescription: "It helps us improve SnapOtter without collecting files or private content.",
quickThanks: "Thanks for the signal.",
diff --git a/packages/shared/src/i18n/es.ts b/packages/shared/src/i18n/es.ts
index 479a11f0..1ec2d46c 100644
--- a/packages/shared/src/i18n/es.ts
+++ b/packages/shared/src/i18n/es.ts
@@ -42,6 +42,7 @@ export const es: TranslationKeys = {
},
feedback: {
navLabel: "Share feedback",
+ navButtonLabel: "Comentarios",
dialogTitle: "Share feedback",
toolDialogTitle: "How did this tool work?",
failedDialogTitle: "Report a tool issue",
@@ -65,6 +66,11 @@ export const es: TranslationKeys = {
searchMissTitle: "Solicitar una herramienta",
searchMissContext: "Buscaste: {query}",
searchMissDiscussionsFallback: "No pudimos registrarlo. Abre una solicitud en Discussions.",
+ offlineDescription:
+ "Esta instancia tiene la analítica desactivada, así que los comentarios no se registran aquí. Aún puedes contactar directamente con el equipo de SnapOtter.",
+ offlinePublicNote: "Las incidencias de GitHub son públicas.",
+ offlineGithubButton: "Abrir una incidencia en GitHub",
+ offlineEmailButton: "Escríbenos",
thanksTitle: "Thanks for the feedback.",
thanksDescription: "It helps us improve SnapOtter without collecting files or private content.",
quickThanks: "Thanks for the signal.",
diff --git a/packages/shared/src/i18n/fr.ts b/packages/shared/src/i18n/fr.ts
index 1b354e6f..018f1634 100644
--- a/packages/shared/src/i18n/fr.ts
+++ b/packages/shared/src/i18n/fr.ts
@@ -42,6 +42,7 @@ export const fr: TranslationKeys = {
},
feedback: {
navLabel: "Share feedback",
+ navButtonLabel: "Commentaires",
dialogTitle: "Share feedback",
toolDialogTitle: "How did this tool work?",
failedDialogTitle: "Report a tool issue",
@@ -66,6 +67,11 @@ export const fr: TranslationKeys = {
searchMissContext: "Vous avez recherché : {query}",
searchMissDiscussionsFallback:
"Nous n'avons pas pu l'enregistrer. Ouvrez une demande dans Discussions.",
+ offlineDescription:
+ "Cette instance a l'analytique désactivée, donc les commentaires ne sont pas enregistrés ici. Vous pouvez tout de même contacter directement l'équipe SnapOtter.",
+ offlinePublicNote: "Les tickets GitHub sont publics.",
+ offlineGithubButton: "Ouvrir un ticket GitHub",
+ offlineEmailButton: "Écrivez-nous",
thanksTitle: "Thanks for the feedback.",
thanksDescription: "It helps us improve SnapOtter without collecting files or private content.",
quickThanks: "Thanks for the signal.",
diff --git a/packages/shared/src/i18n/hi.ts b/packages/shared/src/i18n/hi.ts
index 1928dc79..868c0d59 100644
--- a/packages/shared/src/i18n/hi.ts
+++ b/packages/shared/src/i18n/hi.ts
@@ -42,6 +42,7 @@ export const hi: TranslationKeys = {
},
feedback: {
navLabel: "Share feedback",
+ navButtonLabel: "प्रतिक्रिया",
dialogTitle: "Share feedback",
toolDialogTitle: "How did this tool work?",
failedDialogTitle: "Report a tool issue",
@@ -65,6 +66,11 @@ export const hi: TranslationKeys = {
searchMissTitle: "टूल का अनुरोध करें",
searchMissContext: "आपने खोजा: {query}",
searchMissDiscussionsFallback: "हम इसे रिकॉर्ड नहीं कर सके. Discussions में अनुरोध खोलें.",
+ offlineDescription:
+ "इस इंस्टेंस पर एनालिटिक्स बंद है, इसलिए प्रतिक्रिया यहाँ रिकॉर्ड नहीं होती. फिर भी आप SnapOtter टीम से सीधे संपर्क कर सकते हैं.",
+ offlinePublicNote: "GitHub issue सार्वजनिक होते हैं.",
+ offlineGithubButton: "GitHub issue खोलें",
+ offlineEmailButton: "हमें ईमेल करें",
thanksTitle: "Thanks for the feedback.",
thanksDescription: "It helps us improve SnapOtter without collecting files or private content.",
quickThanks: "Thanks for the signal.",
diff --git a/packages/shared/src/i18n/id.ts b/packages/shared/src/i18n/id.ts
index c43722bb..a487b994 100644
--- a/packages/shared/src/i18n/id.ts
+++ b/packages/shared/src/i18n/id.ts
@@ -42,6 +42,7 @@ export const id: TranslationKeys = {
},
feedback: {
navLabel: "Share feedback",
+ navButtonLabel: "Masukan",
dialogTitle: "Share feedback",
toolDialogTitle: "How did this tool work?",
failedDialogTitle: "Report a tool issue",
@@ -65,6 +66,11 @@ export const id: TranslationKeys = {
searchMissTitle: "Minta alat",
searchMissContext: "Anda mencari: {query}",
searchMissDiscussionsFallback: "Kami tidak dapat mencatatnya. Buka permintaan di Discussions.",
+ offlineDescription:
+ "Instance ini menonaktifkan analitik, jadi masukan tidak dicatat di sini. Anda tetap bisa menghubungi tim SnapOtter secara langsung.",
+ offlinePublicNote: "Issue GitHub bersifat publik.",
+ offlineGithubButton: "Buka issue GitHub",
+ offlineEmailButton: "Kirim email ke kami",
thanksTitle: "Thanks for the feedback.",
thanksDescription: "It helps us improve SnapOtter without collecting files or private content.",
quickThanks: "Thanks for the signal.",
diff --git a/packages/shared/src/i18n/it.ts b/packages/shared/src/i18n/it.ts
index 266acd2e..cfbf3920 100644
--- a/packages/shared/src/i18n/it.ts
+++ b/packages/shared/src/i18n/it.ts
@@ -42,6 +42,7 @@ export const it: TranslationKeys = {
},
feedback: {
navLabel: "Condividi feedback",
+ navButtonLabel: "Feedback",
dialogTitle: "Condividi feedback",
toolDialogTitle: "Come ha funzionato questo strumento?",
failedDialogTitle: "Segnala un problema dello strumento",
@@ -67,6 +68,11 @@ export const it: TranslationKeys = {
searchMissContext: "Hai cercato: {query}",
searchMissDiscussionsFallback:
"Non siamo riusciti a registrarlo. Apri una richiesta in Discussions.",
+ offlineDescription:
+ "Questa istanza ha l'analisi disattivata, quindi il feedback non viene registrato qui. Puoi comunque contattare direttamente il team di SnapOtter.",
+ offlinePublicNote: "Le issue di GitHub sono pubbliche.",
+ offlineGithubButton: "Apri una issue su GitHub",
+ offlineEmailButton: "Scrivici",
thanksTitle: "Grazie per il feedback.",
thanksDescription:
"Ci aiuta a migliorare SnapOtter senza raccogliere file o contenuti privati.",
diff --git a/packages/shared/src/i18n/ja.ts b/packages/shared/src/i18n/ja.ts
index dede15ba..5b900433 100644
--- a/packages/shared/src/i18n/ja.ts
+++ b/packages/shared/src/i18n/ja.ts
@@ -42,6 +42,7 @@ export const ja: TranslationKeys = {
},
feedback: {
navLabel: "Share feedback",
+ navButtonLabel: "フィードバック",
dialogTitle: "Share feedback",
toolDialogTitle: "How did this tool work?",
failedDialogTitle: "Report a tool issue",
@@ -66,6 +67,11 @@ export const ja: TranslationKeys = {
searchMissContext: "検索した内容: {query}",
searchMissDiscussionsFallback:
"記録できませんでした。Discussions でリクエストを開いてください。",
+ offlineDescription:
+ "このインスタンスは分析が無効になっているため、フィードバックはここには記録されません。SnapOtter チームに直接連絡することもできます。",
+ offlinePublicNote: "GitHub の Issue は公開されます。",
+ offlineGithubButton: "GitHub で Issue を作成",
+ offlineEmailButton: "メールで連絡",
thanksTitle: "Thanks for the feedback.",
thanksDescription: "It helps us improve SnapOtter without collecting files or private content.",
quickThanks: "Thanks for the signal.",
diff --git a/packages/shared/src/i18n/ko.ts b/packages/shared/src/i18n/ko.ts
index a8f259f7..dcf920dc 100644
--- a/packages/shared/src/i18n/ko.ts
+++ b/packages/shared/src/i18n/ko.ts
@@ -42,6 +42,7 @@ export const ko: TranslationKeys = {
},
feedback: {
navLabel: "Share feedback",
+ navButtonLabel: "피드백",
dialogTitle: "Share feedback",
toolDialogTitle: "How did this tool work?",
failedDialogTitle: "Report a tool issue",
@@ -65,6 +66,11 @@ export const ko: TranslationKeys = {
searchMissTitle: "도구 요청",
searchMissContext: "검색한 내용: {query}",
searchMissDiscussionsFallback: "기록하지 못했습니다. Discussions에서 요청을 열어 주세요.",
+ offlineDescription:
+ "이 인스턴스는 분석이 꺼져 있어 피드백이 여기에 기록되지 않습니다. 그래도 SnapOtter 팀에 직접 연락할 수 있습니다.",
+ offlinePublicNote: "GitHub 이슈는 공개됩니다.",
+ offlineGithubButton: "GitHub 이슈 열기",
+ offlineEmailButton: "이메일 보내기",
thanksTitle: "Thanks for the feedback.",
thanksDescription: "It helps us improve SnapOtter without collecting files or private content.",
quickThanks: "Thanks for the signal.",
diff --git a/packages/shared/src/i18n/nl.ts b/packages/shared/src/i18n/nl.ts
index 91bef607..d37cc9d7 100644
--- a/packages/shared/src/i18n/nl.ts
+++ b/packages/shared/src/i18n/nl.ts
@@ -42,6 +42,7 @@ export const nl: TranslationKeys = {
},
feedback: {
navLabel: "Share feedback",
+ navButtonLabel: "Feedback",
dialogTitle: "Share feedback",
toolDialogTitle: "How did this tool work?",
failedDialogTitle: "Report a tool issue",
@@ -66,6 +67,11 @@ export const nl: TranslationKeys = {
searchMissContext: "Je zocht op: {query}",
searchMissDiscussionsFallback:
"We konden dit niet vastleggen. Open een verzoek in Discussions.",
+ offlineDescription:
+ "Deze instantie heeft analyse uitgeschakeld, dus feedback wordt hier niet vastgelegd. Je kunt het SnapOtter-team nog steeds rechtstreeks bereiken.",
+ offlinePublicNote: "GitHub-issues zijn openbaar.",
+ offlineGithubButton: "Een GitHub-issue openen",
+ offlineEmailButton: "Mail ons",
thanksTitle: "Thanks for the feedback.",
thanksDescription: "It helps us improve SnapOtter without collecting files or private content.",
quickThanks: "Thanks for the signal.",
diff --git a/packages/shared/src/i18n/pl.ts b/packages/shared/src/i18n/pl.ts
index f284af1f..987affb4 100644
--- a/packages/shared/src/i18n/pl.ts
+++ b/packages/shared/src/i18n/pl.ts
@@ -42,6 +42,7 @@ export const pl: TranslationKeys = {
},
feedback: {
navLabel: "Share feedback",
+ navButtonLabel: "Opinie",
dialogTitle: "Share feedback",
toolDialogTitle: "How did this tool work?",
failedDialogTitle: "Report a tool issue",
@@ -65,6 +66,11 @@ export const pl: TranslationKeys = {
searchMissTitle: "Poproś o narzędzie",
searchMissContext: "Szukano: {query}",
searchMissDiscussionsFallback: "Nie udało się tego zapisać. Otwórz prośbę w Discussions.",
+ offlineDescription:
+ "Ta instancja ma wyłączoną analitykę, więc opinie nie są tu zapisywane. Nadal możesz skontaktować się bezpośrednio z zespołem SnapOtter.",
+ offlinePublicNote: "Zgłoszenia w GitHub są publiczne.",
+ offlineGithubButton: "Otwórz zgłoszenie w GitHub",
+ offlineEmailButton: "Napisz do nas",
thanksTitle: "Thanks for the feedback.",
thanksDescription: "It helps us improve SnapOtter without collecting files or private content.",
quickThanks: "Thanks for the signal.",
diff --git a/packages/shared/src/i18n/pt-BR.ts b/packages/shared/src/i18n/pt-BR.ts
index cc444783..78580152 100644
--- a/packages/shared/src/i18n/pt-BR.ts
+++ b/packages/shared/src/i18n/pt-BR.ts
@@ -42,6 +42,7 @@ export const ptBR: TranslationKeys = {
},
feedback: {
navLabel: "Share feedback",
+ navButtonLabel: "Feedback",
dialogTitle: "Share feedback",
toolDialogTitle: "How did this tool work?",
failedDialogTitle: "Report a tool issue",
@@ -66,6 +67,11 @@ export const ptBR: TranslationKeys = {
searchMissContext: "Você pesquisou: {query}",
searchMissDiscussionsFallback:
"Não conseguimos registrar. Abra uma solicitação no Discussions.",
+ offlineDescription:
+ "Esta instância está com a análise desativada, então o feedback não é registrado aqui. Mesmo assim, você pode falar diretamente com a equipe do SnapOtter.",
+ offlinePublicNote: "As issues do GitHub são públicas.",
+ offlineGithubButton: "Abrir uma issue no GitHub",
+ offlineEmailButton: "Envie um e-mail",
thanksTitle: "Thanks for the feedback.",
thanksDescription: "It helps us improve SnapOtter without collecting files or private content.",
quickThanks: "Thanks for the signal.",
diff --git a/packages/shared/src/i18n/ru.ts b/packages/shared/src/i18n/ru.ts
index 3d4de770..edc16c8d 100644
--- a/packages/shared/src/i18n/ru.ts
+++ b/packages/shared/src/i18n/ru.ts
@@ -42,6 +42,7 @@ export const ru: TranslationKeys = {
},
feedback: {
navLabel: "Share feedback",
+ navButtonLabel: "Отзыв",
dialogTitle: "Share feedback",
toolDialogTitle: "How did this tool work?",
failedDialogTitle: "Report a tool issue",
@@ -65,6 +66,11 @@ export const ru: TranslationKeys = {
searchMissTitle: "Запросить инструмент",
searchMissContext: "Вы искали: {query}",
searchMissDiscussionsFallback: "Не удалось записать. Откройте запрос в Discussions.",
+ offlineDescription:
+ "В этом экземпляре аналитика отключена, поэтому отзывы здесь не записываются. Вы всё равно можете напрямую связаться с командой SnapOtter.",
+ offlinePublicNote: "Issue на GitHub публичны.",
+ offlineGithubButton: "Открыть issue на GitHub",
+ offlineEmailButton: "Напишите нам",
thanksTitle: "Thanks for the feedback.",
thanksDescription: "It helps us improve SnapOtter without collecting files or private content.",
quickThanks: "Thanks for the signal.",
diff --git a/packages/shared/src/i18n/sv.ts b/packages/shared/src/i18n/sv.ts
index 1908e90d..af40d824 100644
--- a/packages/shared/src/i18n/sv.ts
+++ b/packages/shared/src/i18n/sv.ts
@@ -42,6 +42,7 @@ export const sv: TranslationKeys = {
},
feedback: {
navLabel: "Share feedback",
+ navButtonLabel: "Feedback",
dialogTitle: "Share feedback",
toolDialogTitle: "How did this tool work?",
failedDialogTitle: "Report a tool issue",
@@ -66,6 +67,11 @@ export const sv: TranslationKeys = {
searchMissContext: "Du sökte efter: {query}",
searchMissDiscussionsFallback:
"Vi kunde inte registrera det. Öppna en förfrågan i Discussions.",
+ offlineDescription:
+ "Den här instansen har stängt av analysen, så feedback registreras inte här. Du kan ändå nå SnapOtter-teamet direkt.",
+ offlinePublicNote: "GitHub-ärenden är offentliga.",
+ offlineGithubButton: "Öppna ett GitHub-ärende",
+ offlineEmailButton: "Mejla oss",
thanksTitle: "Thanks for the feedback.",
thanksDescription: "It helps us improve SnapOtter without collecting files or private content.",
quickThanks: "Thanks for the signal.",
diff --git a/packages/shared/src/i18n/th.ts b/packages/shared/src/i18n/th.ts
index 408240ca..ffe6f8c5 100644
--- a/packages/shared/src/i18n/th.ts
+++ b/packages/shared/src/i18n/th.ts
@@ -42,6 +42,7 @@ export const th: TranslationKeys = {
},
feedback: {
navLabel: "Share feedback",
+ navButtonLabel: "ความคิดเห็น",
dialogTitle: "Share feedback",
toolDialogTitle: "How did this tool work?",
failedDialogTitle: "Report a tool issue",
@@ -65,6 +66,11 @@ export const th: TranslationKeys = {
searchMissTitle: "ขอเครื่องมือ",
searchMissContext: "คุณค้นหา: {query}",
searchMissDiscussionsFallback: "เราบันทึกไม่ได้ เปิดคำขอใน Discussions",
+ offlineDescription:
+ "อินสแตนซ์นี้ปิดการวิเคราะห์ไว้ ความคิดเห็นจึงไม่ถูกบันทึกที่นี่ คุณยังสามารถติดต่อทีม SnapOtter ได้โดยตรง",
+ offlinePublicNote: "Issue บน GitHub เป็นสาธารณะ",
+ offlineGithubButton: "เปิด Issue บน GitHub",
+ offlineEmailButton: "ส่งอีเมลถึงเรา",
thanksTitle: "Thanks for the feedback.",
thanksDescription: "It helps us improve SnapOtter without collecting files or private content.",
quickThanks: "Thanks for the signal.",
diff --git a/packages/shared/src/i18n/tr.ts b/packages/shared/src/i18n/tr.ts
index 75921a9d..b8f0c185 100644
--- a/packages/shared/src/i18n/tr.ts
+++ b/packages/shared/src/i18n/tr.ts
@@ -42,6 +42,7 @@ export const tr: TranslationKeys = {
},
feedback: {
navLabel: "Share feedback",
+ navButtonLabel: "Geri bildirim",
dialogTitle: "Share feedback",
toolDialogTitle: "How did this tool work?",
failedDialogTitle: "Report a tool issue",
@@ -65,6 +66,11 @@ export const tr: TranslationKeys = {
searchMissTitle: "Araç iste",
searchMissContext: "Şunu aradın: {query}",
searchMissDiscussionsFallback: "Bunu kaydedemedik. Discussions'ta bir istek aç.",
+ offlineDescription:
+ "Bu örnekte analiz kapalı, bu yüzden geri bildirim burada kaydedilmez. Yine de SnapOtter ekibine doğrudan ulaşabilirsin.",
+ offlinePublicNote: "GitHub issue'ları herkese açıktır.",
+ offlineGithubButton: "GitHub'da issue aç",
+ offlineEmailButton: "Bize e-posta gönder",
thanksTitle: "Thanks for the feedback.",
thanksDescription: "It helps us improve SnapOtter without collecting files or private content.",
quickThanks: "Thanks for the signal.",
diff --git a/packages/shared/src/i18n/uk.ts b/packages/shared/src/i18n/uk.ts
index 692d8738..ca6bdc0f 100644
--- a/packages/shared/src/i18n/uk.ts
+++ b/packages/shared/src/i18n/uk.ts
@@ -42,6 +42,7 @@ export const uk: TranslationKeys = {
},
feedback: {
navLabel: "Share feedback",
+ navButtonLabel: "Відгук",
dialogTitle: "Share feedback",
toolDialogTitle: "How did this tool work?",
failedDialogTitle: "Report a tool issue",
@@ -65,6 +66,11 @@ export const uk: TranslationKeys = {
searchMissTitle: "Запитати інструмент",
searchMissContext: "Ви шукали: {query}",
searchMissDiscussionsFallback: "Не вдалося записати. Відкрийте запит у Discussions.",
+ offlineDescription:
+ "У цьому екземплярі аналітику вимкнено, тому відгуки тут не записуються. Ви все одно можете напряму звернутися до команди SnapOtter.",
+ offlinePublicNote: "Issue на GitHub є публічними.",
+ offlineGithubButton: "Відкрити issue на GitHub",
+ offlineEmailButton: "Напишіть нам",
thanksTitle: "Thanks for the feedback.",
thanksDescription: "It helps us improve SnapOtter without collecting files or private content.",
quickThanks: "Thanks for the signal.",
diff --git a/packages/shared/src/i18n/vi.ts b/packages/shared/src/i18n/vi.ts
index 2aa034f2..8f2b82be 100644
--- a/packages/shared/src/i18n/vi.ts
+++ b/packages/shared/src/i18n/vi.ts
@@ -42,6 +42,7 @@ export const vi: TranslationKeys = {
},
feedback: {
navLabel: "Share feedback",
+ navButtonLabel: "Phản hồi",
dialogTitle: "Share feedback",
toolDialogTitle: "How did this tool work?",
failedDialogTitle: "Report a tool issue",
@@ -66,6 +67,11 @@ export const vi: TranslationKeys = {
searchMissContext: "Bạn đã tìm: {query}",
searchMissDiscussionsFallback:
"Chúng tôi không ghi lại được. Mở một yêu cầu trong Discussions.",
+ offlineDescription:
+ "Bản cài đặt này đã tắt phân tích, nên phản hồi không được ghi lại ở đây. Bạn vẫn có thể liên hệ trực tiếp với nhóm SnapOtter.",
+ offlinePublicNote: "Issue trên GitHub là công khai.",
+ offlineGithubButton: "Mở một issue trên GitHub",
+ offlineEmailButton: "Gửi email cho chúng tôi",
thanksTitle: "Thanks for the feedback.",
thanksDescription: "It helps us improve SnapOtter without collecting files or private content.",
quickThanks: "Thanks for the signal.",
diff --git a/packages/shared/src/i18n/zh-CN.ts b/packages/shared/src/i18n/zh-CN.ts
index 86f9e4cf..044254b6 100644
--- a/packages/shared/src/i18n/zh-CN.ts
+++ b/packages/shared/src/i18n/zh-CN.ts
@@ -42,6 +42,7 @@ export const zhCN: TranslationKeys = {
},
feedback: {
navLabel: "Share feedback",
+ navButtonLabel: "反馈",
dialogTitle: "Share feedback",
toolDialogTitle: "How did this tool work?",
failedDialogTitle: "Report a tool issue",
@@ -65,6 +66,11 @@ export const zhCN: TranslationKeys = {
searchMissTitle: "请求工具",
searchMissContext: "你搜索了:{query}",
searchMissDiscussionsFallback: "我们无法记录。请在 Discussions 中发起请求。",
+ offlineDescription:
+ "此实例已关闭分析功能,因此反馈不会记录在这里。你仍然可以直接联系 SnapOtter 团队。",
+ offlinePublicNote: "GitHub issue 是公开的。",
+ offlineGithubButton: "创建 GitHub issue",
+ offlineEmailButton: "给我们发邮件",
thanksTitle: "Thanks for the feedback.",
thanksDescription: "It helps us improve SnapOtter without collecting files or private content.",
quickThanks: "Thanks for the signal.",
diff --git a/packages/shared/src/i18n/zh-TW.ts b/packages/shared/src/i18n/zh-TW.ts
index d23a84c8..85b94aab 100644
--- a/packages/shared/src/i18n/zh-TW.ts
+++ b/packages/shared/src/i18n/zh-TW.ts
@@ -42,6 +42,7 @@ export const zhTW: TranslationKeys = {
},
feedback: {
navLabel: "Share feedback",
+ navButtonLabel: "意見回饋",
dialogTitle: "Share feedback",
toolDialogTitle: "How did this tool work?",
failedDialogTitle: "Report a tool issue",
@@ -65,6 +66,11 @@ export const zhTW: TranslationKeys = {
searchMissTitle: "請求工具",
searchMissContext: "你搜尋了:{query}",
searchMissDiscussionsFallback: "我們無法記錄。請在 Discussions 中發起請求。",
+ offlineDescription:
+ "此執行個體已關閉分析功能,因此意見回饋不會記錄在這裡。你仍然可以直接聯絡 SnapOtter 團隊。",
+ offlinePublicNote: "GitHub issue 是公開的。",
+ offlineGithubButton: "建立 GitHub issue",
+ offlineEmailButton: "寄電子郵件給我們",
thanksTitle: "Thanks for the feedback.",
thanksDescription: "It helps us improve SnapOtter without collecting files or private content.",
quickThanks: "Thanks for the signal.",
diff --git a/tests/unit/web/feedback-dialog-offline.test.tsx b/tests/unit/web/feedback-dialog-offline.test.tsx
new file mode 100644
index 00000000..bb85b244
--- /dev/null
+++ b/tests/unit/web/feedback-dialog-offline.test.tsx
@@ -0,0 +1,65 @@
+// @vitest-environment jsdom
+
+import { cleanup, fireEvent, render, screen } from "@testing-library/react";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { FeedbackDialog } from "@/components/feedback/feedback-dialog";
+
+const submitFeedback = vi.hoisted(() => vi.fn());
+
+vi.mock("@/lib/feedback", async (importOriginal) => {
+ const actual: Record = await importOriginal();
+ return { ...actual, submitFeedback };
+});
+
+const MESSAGE_PLACEHOLDER = "Tell us what worked, what broke, or what would make SnapOtter better.";
+
+beforeEach(() => {
+ submitFeedback.mockReset();
+});
+
+afterEach(() => {
+ cleanup();
+ vi.restoreAllMocks();
+});
+
+describe("FeedbackDialog global off-state handoff", () => {
+ it("reveals GitHub and email handoff when the server does not record the feedback", async () => {
+ submitFeedback.mockResolvedValue({ ok: true, accepted: false });
+ render();
+
+ fireEvent.change(screen.getByPlaceholderText(MESSAGE_PLACEHOLDER), {
+ target: { value: "The queue stalls on large PDFs" },
+ });
+ fireEvent.click(screen.getByRole("button", { name: "Send feedback" }));
+
+ const githubLink = await screen.findByRole("link", { name: "Open a GitHub issue" });
+ expect(githubLink.getAttribute("href")).toContain("template=feedback.yml");
+ expect(githubLink.getAttribute("href")).toContain("issues/new");
+
+ const emailLink = screen.getByRole("link", { name: "Email us" });
+ expect(emailLink.getAttribute("href")).toContain("mailto:contact@snapotter.com");
+
+ // The typed message must actually reach both handoff targets, not just the static parts.
+ const githubDetails = new URL(githubLink.getAttribute("href") ?? "").searchParams.get(
+ "details",
+ );
+ expect(githubDetails).toBe("The queue stalls on large PDFs");
+ const emailBody = new URL(emailLink.getAttribute("href") ?? "").searchParams.get("body");
+ expect(emailBody).toBe("The queue stalls on large PDFs");
+
+ expect(screen.queryByText("Thanks for the feedback.")).toBeNull();
+ });
+
+ it("shows the normal thanks when the feedback is recorded", async () => {
+ submitFeedback.mockResolvedValue({ ok: true, accepted: true });
+ render();
+
+ fireEvent.change(screen.getByPlaceholderText(MESSAGE_PLACEHOLDER), {
+ target: { value: "Great tool" },
+ });
+ fireEvent.click(screen.getByRole("button", { name: "Send feedback" }));
+
+ expect(await screen.findByText("Thanks for the feedback.")).toBeDefined();
+ expect(screen.queryByRole("link", { name: "Open a GitHub issue" })).toBeNull();
+ });
+});
diff --git a/tests/unit/web/feedback-links.test.ts b/tests/unit/web/feedback-links.test.ts
new file mode 100644
index 00000000..ac878ee4
--- /dev/null
+++ b/tests/unit/web/feedback-links.test.ts
@@ -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");
+ });
+});