From 5aaddbca40f23bcc84a8ad2993a4896f6a656a6d Mon Sep 17 00:00:00 2001 From: Jonathan Bourdon Date: Thu, 30 Apr 2026 13:33:10 -0400 Subject: [PATCH] feat: add feedback submission flow --- frontend/package-lock.json | 50 ++ frontend/package.json | 1 + frontend/src/App.vue | 3 + .../components/FeedbackFloatingButton.vue | 50 ++ .../components/FeedbackSubmissionDialog.vue | 711 ++++++++++++++++++ .../stores/feedbackSubmissionStore.js | 45 ++ frontend/src/locales/en.json | 35 + frontend/src/locales/fr.json | 35 + frontend/src/main.js | 2 + 9 files changed, 932 insertions(+) create mode 100644 frontend/src/features/feedback/components/FeedbackFloatingButton.vue create mode 100644 frontend/src/features/feedback/components/FeedbackSubmissionDialog.vue create mode 100644 frontend/src/features/feedback/stores/feedbackSubmissionStore.js diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8f95674..50c4f15 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,6 +15,7 @@ "@vueuse/head": "^2.0.0", "@xtiannyeto/vue-auth-social": "^0.1.9", "axios": "^1.6.7", + "html2canvas": "^1.4.1", "i18n": "^0.15.1", "jwt-decode": "^4.0.0", "pinia": "^2.1.7", @@ -2161,6 +2162,15 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -2497,6 +2507,15 @@ "node": ">= 8" } }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -3565,6 +3584,19 @@ "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==" }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -5341,6 +5373,15 @@ "node": ">=14.0.0" } }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -5539,6 +5580,15 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/uuid": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 09b1ccd..c2bf744 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,6 +19,7 @@ "@vueuse/head": "^2.0.0", "@xtiannyeto/vue-auth-social": "^0.1.9", "axios": "^1.6.7", + "html2canvas": "^1.4.1", "i18n": "^0.15.1", "jwt-decode": "^4.0.0", "pinia": "^2.1.7", diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 69566e0..f772bc0 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -28,6 +28,8 @@ + + @@ -39,6 +41,7 @@ import { mdiChevronLeft, mdiChevronRight } from '@mdi/js'; import AppBar from '@/layouts/main/AppBar.vue'; import AppSidebar from '@/layouts/main/AppSidebar.vue'; + import FeedbackFloatingButton from '@/features/feedback/components/FeedbackFloatingButton.vue'; const route = useRoute(); const authStore = useAuthStore(); diff --git a/frontend/src/features/feedback/components/FeedbackFloatingButton.vue b/frontend/src/features/feedback/components/FeedbackFloatingButton.vue new file mode 100644 index 0000000..baf3a18 --- /dev/null +++ b/frontend/src/features/feedback/components/FeedbackFloatingButton.vue @@ -0,0 +1,50 @@ + + + + + diff --git a/frontend/src/features/feedback/components/FeedbackSubmissionDialog.vue b/frontend/src/features/feedback/components/FeedbackSubmissionDialog.vue new file mode 100644 index 0000000..e2fc9ec --- /dev/null +++ b/frontend/src/features/feedback/components/FeedbackSubmissionDialog.vue @@ -0,0 +1,711 @@ + + + + + diff --git a/frontend/src/features/feedback/stores/feedbackSubmissionStore.js b/frontend/src/features/feedback/stores/feedbackSubmissionStore.js new file mode 100644 index 0000000..669445c --- /dev/null +++ b/frontend/src/features/feedback/stores/feedbackSubmissionStore.js @@ -0,0 +1,45 @@ +import { ref } from 'vue'; +import { defineStore } from 'pinia'; +import { useClient } from '@/plugins/api.js'; + +export const useFeedbackSubmissionStore = defineStore('feedback-submission', () => { + const client = useClient(); + const isSubmitting = ref(false); + const error = ref(null); + + async function submitFeedback(payload, screenshotBlob) { + if (isSubmitting.value) { + throw new Error('A feedback submission is already in progress.'); + } + + isSubmitting.value = true; + error.value = null; + + try { + const response = await client.post('/api/feedback', payload); + let report = response.data; + + if (screenshotBlob && report?.id) { + const formData = new FormData(); + formData.append('file', screenshotBlob, 'feedback-screenshot.jpg'); + + const screenshotResponse = await client.post(`/api/my-feedback/${report.id}/screenshot`, formData); + report = screenshotResponse.data ?? report; + } + + return report; + } catch (submitError) { + console.error('Failed to submit feedback:', submitError); + error.value = 'Failed to submit feedback.'; + throw submitError; + } finally { + isSubmitting.value = false; + } + } + + return { + isSubmitting, + error, + submitFeedback, + }; +}); diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 427b7d3..94d58db 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -99,6 +99,41 @@ "assetRevisionCreated": "Asset revision created" } }, + "feedback": { + "button": "Feedback", + "open": "Send product feedback", + "eyebrow": "Product feedback", + "title": "Send feedback", + "capture": "Capture screen", + "removeCapture": "Remove capture", + "noCapture": "Capture the current app viewport if a screenshot would help.", + "captureFailed": "The screenshot could not be captured. You can still submit feedback without it.", + "submit": "Submit feedback", + "submitted": "Feedback submitted.", + "submitFailed": "Feedback could not be submitted.", + "discardConfirm": "Discard this unsent feedback?", + "textPrompt": "Text label", + "types": { + "bug": "Bug", + "suggestion": "Suggestion", + "request": "Request" + }, + "fields": { + "type": "Type", + "description": "Description", + "descriptionPlaceholder": "Describe what happened, what you expected, or what would improve the workflow." + }, + "tools": { + "crop": "Crop", + "arrow": "Arrow", + "ellipse": "Circle", + "line": "Line", + "freehand": "Freehand", + "text": "Text label", + "undo": "Undo", + "clear": "Clear and reset" + } + }, "sidebar": { "allClients": "All clients", "allChannels": "All channels", diff --git a/frontend/src/locales/fr.json b/frontend/src/locales/fr.json index 8994c0f..47879dc 100644 --- a/frontend/src/locales/fr.json +++ b/frontend/src/locales/fr.json @@ -99,6 +99,41 @@ "assetRevisionCreated": "Révision de ressource créée" } }, + "feedback": { + "button": "Feedback", + "open": "Envoyer un feedback produit", + "eyebrow": "Feedback produit", + "title": "Envoyer un feedback", + "capture": "Capturer l'écran", + "removeCapture": "Retirer la capture", + "noCapture": "Capturez la vue actuelle de l'application si une image peut aider.", + "captureFailed": "La capture d'écran n'a pas pu être faite. Vous pouvez quand même envoyer un feedback sans image.", + "submit": "Envoyer le feedback", + "submitted": "Feedback envoyé.", + "submitFailed": "Le feedback n'a pas pu être envoyé.", + "discardConfirm": "Supprimer ce feedback non envoyé ?", + "textPrompt": "Libellé texte", + "types": { + "bug": "Bug", + "suggestion": "Suggestion", + "request": "Demande" + }, + "fields": { + "type": "Type", + "description": "Description", + "descriptionPlaceholder": "Décrivez ce qui s'est produit, ce que vous attendiez ou ce qui améliorerait le workflow." + }, + "tools": { + "crop": "Recadrer", + "arrow": "Flèche", + "ellipse": "Cercle", + "line": "Ligne", + "freehand": "Main levée", + "text": "Texte", + "undo": "Annuler", + "clear": "Effacer et réinitialiser" + } + }, "sidebar": { "allClients": "Tous les clients", "allChannels": "Tous les canaux", diff --git a/frontend/src/main.js b/frontend/src/main.js index effd04a..79bd05d 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -14,6 +14,7 @@ import { VIcon, VProgressCircular, VProgressLinear, + VSelect, VSnackbar, VTextarea, VTextField, @@ -42,6 +43,7 @@ const vuetify = createVuetify({ VProgressLinear, VProgressCircular, VIcon, + VSelect, VTextField, VSnackbar, VForm,