Skip to content

Commit

Permalink
Feedback widget (#927)
Browse files Browse the repository at this point in the history
* first commit

* feedback widget

---------

Co-authored-by: Jordan <[email protected]>
  • Loading branch information
Fbasham and will0684 authored Nov 22, 2023
1 parent 5a65291 commit 5c862c6
Show file tree
Hide file tree
Showing 13 changed files with 285 additions and 12 deletions.
2 changes: 2 additions & 0 deletions AzurePipelines/dev-build-and-push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions AzurePipelines/pr-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions AzurePipelines/prod-build-and-push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
71 changes: 71 additions & 0 deletions __tests__/api/feedback-widget.test.js
Original file line number Diff line number Diff line change
@@ -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",
});
});
});
132 changes: 132 additions & 0 deletions components/organisms/Feedback.js
Original file line number Diff line number Diff line change
@@ -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 (
<div className="sm:flex items-center justify-between gap-20 bg-gray-light-200 p-5 max-w-[568px] border rounded">
{isSubmitted && (
<div className="flex gap-5 items-center">
<Image
src="/success_img.svg"
alt=""
width={25}
height={25}
style={{ width: 25, height: 25 }}
priority
></Image>
<p>{t("feedback.thank-you")}</p>
</div>
)}

{isProvidingFeedback && !isSubmitted && (
<form onSubmit={handleSubmit} className="space-y-5">
<input type="hidden" name="page" value={router.asPath} />
<fieldset>
<legend className="font-bold mb-2">
{t("feedback.what-was-wrong")}
</legend>
<label className="flex gap-2">
<input
type="radio"
name="what-was-wrong"
value="cant-find-info"
required
></input>
{t("feedback.cant-find-info")}
</label>
<label className="flex gap-2">
<input
type="radio"
name="what-was-wrong"
value="hard-to-understand"
></input>
{t("feedback.hard-to-understand")}
</label>
<label className="flex gap-2">
<input
type="radio"
name="what-was-wrong"
value="there-was-an-error"
></input>
{t("feedback.there-was-an-error")}
</label>
<label className="flex gap-2">
<input
type="radio"
name="what-was-wrong"
value="other-reason"
></input>

{t("feedback.other-reason")}
</label>
</fieldset>
<label className="flex flex-col gap-2">
<span className="font-bold">
{t("feedback.provide-more-details")}
</span>
<span id="extra-info" className="font-[500] text-xs">
{t("feedback.no-protected-info")}
</span>
<span id="maximum-characters" className="font-[300] text-xs">
{t("feedback.maximum-characters")}
</span>
<textarea
name="extra-details"
aria-describedby="extra-info maximum-characters"
maxLength={300}
className="p-1"
></textarea>
</label>
<button className="bg-multi-blue-blue70 hover:bg-multi-blue-blue60e text-white rounded py-1 px-2">
{t("feedback.submit")}
</button>
</form>
)}

{!isSubmitted && !isProvidingFeedback && (
<>
<p className="font-semibold text-sm">{t("feedback.did-you-find")}</p>
<div className="flex gap-2">
<button
onClick={() => setIsSubmitted(true)}
className="bg-multi-blue-blue70 hover:bg-multi-blue-blue60e text-white rounded py-1 px-2"
>
{t("feedback.yes")}
</button>
<button
onClick={() => setIsProvidingFeedback(true)}
className="bg-multi-blue-blue70 hover:bg-multi-blue-blue60e text-white rounded py-1 px-2"
>
{t("feedback.no")}
</button>
</div>
</>
)}
</div>
);
}

export default Feedback;
3 changes: 2 additions & 1 deletion components/organisms/Layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -115,7 +116,7 @@ export const Layout = ({
<div className="mt-12">
<h2 className="sr-only">{t("siteFooter")}</h2>
<div className="layout-container mt-5">
<ReportAProblem />
<Feedback />
</div>
<div className="layout-container mb-2">
<DateModified date={dateModifiedOverride} />
Expand Down
19 changes: 19 additions & 0 deletions lib/notify/postFeedbackToGcNotify.js
Original file line number Diff line number Diff line change
@@ -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,
},
}),
}
);
}
3 changes: 0 additions & 3 deletions pages/404.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -165,7 +164,6 @@ export default function error404(props) {
</p>
</div>
</div>
<ReportAProblem language={"en"} />
</div>
<div className="flex items-center justify-center circle-background my-8 mx-4 lg:mt-0 lightbulb-bg shrink-0">
<span className="relative lightbulb">
Expand Down Expand Up @@ -206,7 +204,6 @@ export default function error404(props) {
</p>
</div>
</div>
<ReportAProblem language="fr" />
</div>
</div>
</section>
Expand Down
3 changes: 0 additions & 3 deletions pages/500.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -191,7 +190,6 @@ export default function error500(props) {
</p>
</div>
</div>
<ReportAProblem language={"en"} />
</div>
<div className="flex items-center justify-center circle-background my-8 lg:mt-0 lightbulb-bg">
<span className="relative lightbulb">
Expand Down Expand Up @@ -232,7 +230,6 @@ export default function error500(props) {
</p>
</div>
</div>
<ReportAProblem language="fr" />
</div>
</div>
</section>
Expand Down
21 changes: 21 additions & 0 deletions pages/api/submit-feedback.js
Original file line number Diff line number Diff line change
@@ -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" });
}
}
}
3 changes: 0 additions & 3 deletions pages/error.js
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -296,7 +295,6 @@ export default function ErrorPage(props) {
</div>
)}
</div>
<ReportAProblem language="en" />
</div>
<div className="flex items-center justify-center circle-background my-8 lg:mt-0 lightbulb-bg">
<span className="relative lightbulb">
Expand Down Expand Up @@ -418,7 +416,6 @@ export default function ErrorPage(props) {
</div>
)}
</div>
<ReportAProblem language="fr" />
</div>
</div>
</section>
Expand Down
18 changes: 17 additions & 1 deletion public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
Loading

0 comments on commit 5c862c6

Please sign in to comment.