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}

+
+ + {t.feedback.offlineGithubButton} + + + {t.feedback.offlineEmailButton} + +
+
+ ) : 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 && ( )} 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"); + }); +});