Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feedback widget #927

Merged
merged 3 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading