diff --git a/AzurePipelines/dev-build-and-push.yml b/AzurePipelines/dev-build-and-push.yml index 2f031233a2..f61ebb20d4 100644 --- a/AzurePipelines/dev-build-and-push.yml +++ b/AzurePipelines/dev-build-and-push.yml @@ -51,6 +51,8 @@ stages: --build-arg REVALIDATION_TOKEN=$(REVALIDATION_TOKEN) --build-arg GIT_SHA=$(Build.SourceVersion) --build-arg ENVIRONMENT=$(ENVIRONMENT) + --build-arg NOTIFY_API_KEY=$(NOTIFY_API_KEY) + --build-arg NOTIFY_FEEDBACK_TEMPLATE_ID=$(NOTIFY_FEEDBACK_TEMPLATE_ID) - task: Docker@2 inputs: diff --git a/AzurePipelines/pr-preview.yml b/AzurePipelines/pr-preview.yml index 9425489234..dcdcc8724e 100644 --- a/AzurePipelines/pr-preview.yml +++ b/AzurePipelines/pr-preview.yml @@ -48,6 +48,8 @@ steps: --build-arg REVALIDATION_TOKEN=$(REVALIDATION_TOKEN) --build-arg GIT_SHA=$(GIT_SHA) --build-arg ENVIRONMENT=$(ENVIRONMENT) + --build-arg NOTIFY_API_KEY=$(NOTIFY_API_KEY) + --build-arg NOTIFY_FEEDBACK_TEMPLATE_ID=$(NOTIFY_FEEDBACK_TEMPLATE_ID) - task: Docker@2 displayName: "Push image" diff --git a/AzurePipelines/prod-build-and-push.yml b/AzurePipelines/prod-build-and-push.yml index 6aa60bfa75..0aa52b3b61 100644 --- a/AzurePipelines/prod-build-and-push.yml +++ b/AzurePipelines/prod-build-and-push.yml @@ -55,6 +55,8 @@ stages: --build-arg REVALIDATION_TOKEN=$(REVALIDATION_TOKEN) --build-arg GIT_SHA=$(GIT_SHA) --build-arg ENVIRONMENT=$(ENVIRONMENT) + --build-arg NOTIFY_API_KEY=$(NOTIFY_API_KEY) + --build-arg NOTIFY_FEEDBACK_TEMPLATE_ID=$(NOTIFY_FEEDBACK_TEMPLATE_ID) - task: Docker@2 inputs: diff --git a/__tests__/api/feedback-widget.test.js b/__tests__/api/feedback-widget.test.js new file mode 100644 index 0000000000..a7ba765460 --- /dev/null +++ b/__tests__/api/feedback-widget.test.js @@ -0,0 +1,71 @@ +import { postFeedbackToGcNotify } from "../../lib/notify/postFeedbackToGcNotify"; +import handler from "../../pages/api/submit-feedback"; +import { createMocks } from "node-mocks-http"; + +jest.mock("../../lib/notify/postFeedbackToGcNotify"); + +describe("Feeback widget api tests", () => { + beforeEach(() => { + postFeedbackToGcNotify.mockRestore(); + }); + + it("it should post to GC Notify", async () => { + postFeedbackToGcNotify.mockReturnValue( + new Promise((resolve) => resolve({ ok: true })) + ); + const { req, res } = createMocks({ + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: { + page: "/home", + "what-was-wrong": "input-field-name", + }, + }); + await handler(req, res); + expect(res._getStatusCode()).toBe(200); + expect(JSON.parse(res._getData())).toEqual({ + page: "/home", + "what-was-wrong": "input-field-name", + }); + }); + + it("it should error if required field isn't included", async () => { + const { req, res } = createMocks({ + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: { + page: "/home", + }, + }); + await handler(req, res); + expect(res._getStatusCode()).toBe(400); + expect(JSON.parse(res._getData())).toEqual({ + message: "required field missing", + }); + }); + + it("it should error if there was a bad post request", async () => { + postFeedbackToGcNotify.mockReturnValue( + new Promise((resolve) => resolve({ ok: false })) + ); + const { req, res } = createMocks({ + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: { + page: "/home", + "what-was-wrong": "input-field-name", + }, + }); + await handler(req, res); + expect(res._getStatusCode()).toBe(500); + expect(JSON.parse(res._getData())).toEqual({ + message: "something went wrong", + }); + }); +}); diff --git a/components/organisms/Feedback.js b/components/organisms/Feedback.js new file mode 100644 index 0000000000..9afe50208a --- /dev/null +++ b/components/organisms/Feedback.js @@ -0,0 +1,132 @@ +import { useState } from "react"; +import Image from "next/image"; +import { useRouter } from "next/router"; +import { useTranslation } from "next-i18next"; + +function Feedback() { + const [isSubmitted, setIsSubmitted] = useState(false); + const [isProvidingFeedback, setIsProvidingFeedback] = useState(false); + + const router = useRouter(); + const { t } = useTranslation("common"); + + async function handleSubmit(e) { + e.preventDefault(); + try { + await fetch("/api/submit-feedback", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(Object.fromEntries(new FormData(e.target))), + }); + } finally { + setIsSubmitted(true); + } + } + + return ( +
+ {isSubmitted && ( +
+ +

{t("feedback.thank-you")}

+
+ )} + + {isProvidingFeedback && !isSubmitted && ( +
+ +
+ + {t("feedback.what-was-wrong")} + + + + + +
+ + +
+ )} + + {!isSubmitted && !isProvidingFeedback && ( + <> +

{t("feedback.did-you-find")}

+
+ + +
+ + )} +
+ ); +} + +export default Feedback; diff --git a/components/organisms/Layout.js b/components/organisms/Layout.js index 02576bbede..83e57ae891 100644 --- a/components/organisms/Layout.js +++ b/components/organisms/Layout.js @@ -9,6 +9,7 @@ import { DateModified } from "../atoms/DateModified"; import { Breadcrumb } from "../atoms/Breadcrumb"; import { Footer } from "../design-system/Footer"; +import Feedback from "./Feedback"; /** * Component which defines the layout of the page for all screen sizes @@ -115,7 +116,7 @@ export const Layout = ({

{t("siteFooter")}

- +
diff --git a/lib/notify/postFeedbackToGcNotify.js b/lib/notify/postFeedbackToGcNotify.js new file mode 100644 index 0000000000..b83be06f3f --- /dev/null +++ b/lib/notify/postFeedbackToGcNotify.js @@ -0,0 +1,19 @@ +export async function postFeedbackToGcNotify(data) { + return await fetch( + `${process.env.NOTIFY_BASE_API_URL}/v2/notifications/email`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `ApiKey-v1 ${process.env.NOTIFY_API_KEY}`, + }, + body: JSON.stringify({ + email_address: process.env.THANK_YOU_EMAIL, + template_id: process.env.NOTIFY_FEEDBACK_TEMPLATE_ID, + personalisation: { + ...data, + }, + }), + } + ); +} diff --git a/pages/404.js b/pages/404.js index 7cce3acd18..b28a6270e7 100644 --- a/pages/404.js +++ b/pages/404.js @@ -2,7 +2,6 @@ import Head from "next/head"; import { useTranslation } from "next-i18next"; import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import Link from "next/link"; -import { ReportAProblem } from "../components/organisms/ReportAProblem"; import { ActionButton } from "../components/atoms/ActionButton"; import { useEffect, useState } from "react"; import { useRouter } from "next/router"; @@ -165,7 +164,6 @@ export default function error404(props) {

-
@@ -206,7 +204,6 @@ export default function error404(props) {

- diff --git a/pages/500.js b/pages/500.js index e6d8009a56..219f7a0cf7 100644 --- a/pages/500.js +++ b/pages/500.js @@ -2,7 +2,6 @@ import Head from "next/head"; import { useTranslation } from "next-i18next"; import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import Link from "next/link"; -import { ReportAProblem } from "../components/organisms/ReportAProblem"; import { ActionButton } from "../components/atoms/ActionButton"; import { useEffect, useState } from "react"; import { useRouter } from "next/router"; @@ -191,7 +190,6 @@ export default function error500(props) {

-
@@ -232,7 +230,6 @@ export default function error500(props) {

- diff --git a/pages/api/submit-feedback.js b/pages/api/submit-feedback.js new file mode 100644 index 0000000000..b71a54998d --- /dev/null +++ b/pages/api/submit-feedback.js @@ -0,0 +1,21 @@ +import { postFeedbackToGcNotify } from "../../lib/notify/postFeedbackToGcNotify"; + +export default async function handler(req, res) { + const data = req.body; + + if (!data["what-was-wrong"]) { + res.status(400).json({ message: "required field missing" }); + } else { + try { + let r = await postFeedbackToGcNotify(data); + + if (r.ok) { + res.status(200).json(data); + } else { + throw new Exception("bad request"); + } + } catch (e) { + res.status(500).json({ message: "something went wrong" }); + } + } +} diff --git a/pages/error.js b/pages/error.js index eafd66da6c..0e0c7fdd90 100644 --- a/pages/error.js +++ b/pages/error.js @@ -1,6 +1,5 @@ import Head from "next/head"; import Link from "next/link"; -import { ReportAProblem } from "../components/organisms/ReportAProblem"; import { ActionButton } from "../components/atoms/ActionButton"; import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { useTranslation } from "next-i18next"; @@ -296,7 +295,6 @@ export default function ErrorPage(props) { )} -
@@ -418,7 +416,6 @@ export default function ErrorPage(props) {
)} - diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 39a15cd765..3f8b2d33e1 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -502,5 +502,21 @@ }, "searchPlaceholderText": "Search Canada.ca", "searchButtonHoverText": "Search bar button", - "monthDropdownPlaceholder": "Select month" + "monthDropdownPlaceholder": "Select month", + + "feedback": { + "thank-you": "Thank you for your feedback.", + "what-was-wrong": "What was wrong?", + "cant-find-info": "I can't find the information", + "hard-to-understand": "The information is hard to understand", + "there-was-an-error": "There was an error or something didn't work", + "other-reason": "Other reason", + "provide-more-details": "Please provide more details", + "no-protected-info": "You will not receive a reply. Don't include personal information (telephone, email, SIN, financial, medical, or work details).", + "maximum-characters": "Maximum 300 characters", + "submit": "Submit", + "did-you-find": "Did you find what you're looking for?", + "yes": "Yes", + "no": "No" + } } diff --git a/public/locales/fr/common.json b/public/locales/fr/common.json index c8eaab7766..7ef0ec99d0 100644 --- a/public/locales/fr/common.json +++ b/public/locales/fr/common.json @@ -492,5 +492,21 @@ }, "searchPlaceholderText": "Rechercher dans Canada.ca", "searchButtonHoverText": "Bouton du champ de recherche", - "monthDropdownPlaceholder": "Sélectionner le mois" + "monthDropdownPlaceholder": "Sélectionner le mois", + + "feedback": { + "thank-you": "Merci de vos commentaires", + "what-was-wrong": "Qu'est-ce qui n'allait pas?", + "cant-find-info": "Je ne peux pas trouver l'information", + "hard-to-understand": "L'information est difficile à comprendre", + "there-was-an-error": "Il y avait une erreur / quelque chose ne fonctionnait pas", + "other-reason": "Autre raison", + "provide-more-details": "Veuillez fournir plus de détails", + "no-protected-info": "Vous ne recevrez aucune réponse. N'incluez pas de renseignements personnels (téléphone, courriel, NAS, renseignements financiers, médicaux ou professionnels)", + "maximum-characters": "Maximum de 300 caractères", + "submit": "Soumettre", + "did-you-find": "Avez-vous trouvé ce que vous cherchiez?", + "yes": "Oui", + "no": "Non" + } }