diff --git a/web/src/components/JotFormEmbed/JotFormEmbed.module.scss b/web/src/components/JotFormEmbed/JotFormEmbed.module.scss
new file mode 100644
index 000000000..52ff438a0
--- /dev/null
+++ b/web/src/components/JotFormEmbed/JotFormEmbed.module.scss
@@ -0,0 +1,8 @@
+.iframe {
+ background: transparent;
+ border: 0;
+ max-width: 100%;
+ min-height: 540px;
+ min-width: 100%;
+ width: 100%;
+}
diff --git a/web/src/components/JotFormEmbed/JotFormEmbed.test.tsx b/web/src/components/JotFormEmbed/JotFormEmbed.test.tsx
new file mode 100644
index 000000000..7cc0f2f24
--- /dev/null
+++ b/web/src/components/JotFormEmbed/JotFormEmbed.test.tsx
@@ -0,0 +1,177 @@
+import { render, screen, fireEvent } from "@testing-library/react";
+
+import { JotFormEmbed } from "./JotFormEmbed";
+
+describe("JotFormEmbed", () => {
+ it("should render iframe with title", () => {
+ render();
+
+ expect(screen.getByTitle("This is a title")).toHaveProperty(
+ "tagName",
+ "IFRAME"
+ );
+ });
+
+ it("should create iframe source URL from form id, JotForm base URL and isIframeEmbed querystring", () => {
+ render();
+
+ expect(screen.getByTitle("This is a title")).toHaveAttribute(
+ "src",
+ "https://next-web-tests.jotform.com/1234?isIframeEmbed=1"
+ );
+ });
+
+ it("should allow full screen on iframe", () => {
+ render();
+
+ expect(screen.getByTitle("This is a title")).toHaveAttribute(
+ "allowfullscreen",
+ ""
+ );
+ });
+
+ it("should allow geolocation, microphone and camera on iframe", () => {
+ render();
+
+ expect(screen.getByTitle("This is a title")).toHaveAttribute(
+ "allow",
+ "geolocation; microphone; camera"
+ );
+ });
+
+ it("should use hidden overflow style", () => {
+ render();
+
+ expect(screen.getByTitle("This is a title")).toHaveStyle({
+ overflow: "hidden",
+ });
+ });
+
+ it("should add data attribute with form ID for GTM tracking", () => {
+ render();
+
+ expect(screen.getByTitle("This is a title")).toHaveAttribute(
+ "data-jotform-id",
+ "1234"
+ );
+ });
+
+ it("should use given initial height", () => {
+ render(
+
+ );
+
+ expect(screen.getByTitle("This is a title")).toHaveStyle({
+ height: "999px",
+ });
+ });
+
+ it("should set height from iframe post message", () => {
+ render();
+
+ fireEvent(
+ window,
+ new MessageEvent("message", {
+ data: "setHeight:987:1234",
+ origin: "https://next-web-tests.jotform.com",
+ })
+ );
+
+ expect(screen.getByTitle("This is a title")).toHaveStyle({
+ height: "987px",
+ });
+ });
+
+ it("should call given onSubmit callback prop after form submission event", () => {
+ const onSubmit = jest.fn();
+
+ render(
+
+ );
+
+ fireEvent(
+ window,
+ new MessageEvent("message", {
+ data: {
+ action: "submission-completed",
+ formID: "1234",
+ },
+ origin: "https://next-web-tests.jotform.com",
+ })
+ );
+
+ expect(onSubmit).toHaveBeenCalled();
+ });
+
+ it("should push submit event to data layer after form submission message", () => {
+ const dataLayerPush = jest.spyOn(window.dataLayer, "push");
+
+ render();
+
+ fireEvent(
+ window,
+ new MessageEvent("message", {
+ data: {
+ action: "submission-completed",
+ formID: "1234",
+ },
+ origin: "https://next-web-tests.jotform.com",
+ })
+ );
+
+ expect(dataLayerPush).toHaveBeenCalledWith({
+ event: "Jotform Message",
+ jf_id: "1234",
+ jf_title: "This is a title",
+ jf_type: "submit",
+ });
+
+ dataLayerPush.mockReset();
+ });
+
+ it("should push progress event to data layer after scroll into view message", () => {
+ const dataLayerPush = jest.spyOn(window.dataLayer, "push");
+
+ render();
+
+ fireEvent(
+ window,
+ new MessageEvent("message", {
+ data: "scrollIntoView::1234",
+ origin: "https://next-web-tests.jotform.com",
+ })
+ );
+
+ expect(dataLayerPush).toHaveBeenCalledWith({
+ event: "Jotform Message",
+ jf_id: "1234",
+ jf_title: "This is a title",
+ jf_type: "progress",
+ });
+
+ dataLayerPush.mockReset();
+ });
+
+ it("should scroll iframe into view in response to scrollIntoView message", () => {
+ render();
+
+ const iframe = screen.getByTitle("This is a title"),
+ scrollIntoView = jest.fn();
+
+ iframe.scrollIntoView = scrollIntoView;
+
+ fireEvent(
+ window,
+ new MessageEvent("message", {
+ data: "scrollIntoView::1234",
+ origin: "https://next-web-tests.jotform.com",
+ })
+ );
+
+ expect(scrollIntoView).toHaveBeenCalled();
+ });
+});
diff --git a/web/src/components/JotFormEmbed/JotFormEmbed.tsx b/web/src/components/JotFormEmbed/JotFormEmbed.tsx
new file mode 100644
index 000000000..6595de608
--- /dev/null
+++ b/web/src/components/JotFormEmbed/JotFormEmbed.tsx
@@ -0,0 +1,188 @@
+import React, {
+ CSSProperties,
+ FC,
+ useCallback,
+ useEffect,
+ useRef,
+ useState,
+} from "react";
+
+import { publicRuntimeConfig } from "@/config";
+import type { FormID } from "@/feeds/jotform/jotform";
+import { logger } from "@/logger";
+
+import styles from "./JotFormEmbed.module.scss";
+
+const jotFormBaseURL = publicRuntimeConfig.jotForm.baseURL;
+
+interface JotFormEmbedProps {
+ jotFormID: FormID;
+ title: string;
+ /** An optional, initial height */
+ height?: number;
+ onSubmit?: () => void;
+}
+
+type JFMessageObject = {
+ action: "submission-completed";
+ formID: FormID;
+};
+
+type JFMessageName =
+ | "scrollIntoView"
+ | "setHeight"
+ | "setMinHeight"
+ | "collapseErrorPage"
+ | "reloadPage"
+ | "loadScript"
+ | "exitFullscreen";
+
+type JFMessageString = `${JFMessageName}:${number | ""}:${FormID}`;
+
+type JFMessageEvent = MessageEvent;
+
+export const JotFormEmbed: FC = ({
+ jotFormID,
+ title,
+ height,
+ onSubmit,
+}) => {
+ const iframeRef = useRef(null),
+ [styleOverrides, setStyleOverrides] = useState({
+ height: height ? `${height}px` : undefined,
+ }),
+ handleIFrameMessage = useCallback(
+ (content?: JFMessageEvent) => {
+ if (!iframeRef.current || !content || content.origin != jotFormBaseURL)
+ return;
+
+ const { data } = content;
+
+ // The form completion message is an object rather than a string like other messages so handle it first
+ if (
+ typeof data === "object" &&
+ data.action === "submission-completed"
+ ) {
+ window.dataLayer.push({
+ event: "Jotform Message",
+ jf_type: "submit",
+ jf_id: jotFormID,
+ jf_title: title,
+ });
+ if (onSubmit) onSubmit();
+ return;
+ }
+
+ // Ignore non-string messages as they should all be strings in the format like "setHeight:1577:230793530776059"
+ if (typeof data !== "string") return;
+
+ const messageParts = data.split(":"),
+ [messageName, value, targetFormID] = messageParts,
+ iframe = iframeRef.current;
+
+ if (targetFormID !== jotFormID) {
+ logger.warn(
+ `Form with ID ${jotFormID} didn't match event with form ID ${targetFormID}`
+ );
+ return;
+ }
+
+ switch (messageName as JFMessageName) {
+ case "scrollIntoView":
+ if (typeof iframe.scrollIntoView === "function")
+ iframe.scrollIntoView();
+ // There's no 'page event' sent from JotForm for multi page forms,
+ // but scrollIntoView is fired for pages so we use this as the closest thing to track pagination
+ window.dataLayer.push({
+ event: "Jotform Message",
+ jf_type: "progress",
+ jf_id: jotFormID,
+ jf_title: title,
+ });
+ break;
+ case "setHeight": {
+ const height = parseInt(value, 10) + "px";
+ setStyleOverrides((s) => ({ ...s, height }));
+ break;
+ }
+ case "setMinHeight": {
+ const minHeight = parseInt(value, 10) + "px";
+ setStyleOverrides((s) => ({ ...s, minHeight }));
+ break;
+ }
+ case "reloadPage":
+ if (iframe.contentWindow) {
+ try {
+ iframe.contentWindow.location.reload();
+ } catch (e) {
+ window.location.reload();
+ }
+ } else window.location.reload();
+ break;
+ case "collapseErrorPage":
+ if (iframe.clientHeight > window.innerHeight) {
+ iframe.style.height = window.innerHeight + "px";
+ }
+ break;
+ case "exitFullscreen":
+ if (window.document.exitFullscreen)
+ window.document.exitFullscreen();
+ break;
+ case "loadScript": {
+ let src = value;
+ if (messageParts.length > 3) {
+ src = value + ":" + messageParts[2];
+ }
+
+ const script = document.createElement("script");
+ script.src = src;
+ script.type = "text/javascript";
+ document.body.appendChild(script);
+ break;
+ }
+ default:
+ break;
+ }
+
+ if (iframe.contentWindow && iframe.contentWindow.postMessage) {
+ const urls = {
+ docurl: encodeURIComponent(global.document.URL),
+ referrer: encodeURIComponent(global.document.referrer),
+ };
+ iframe.contentWindow.postMessage(
+ JSON.stringify({ type: "urls", value: urls }),
+ "*"
+ );
+ }
+ },
+ [jotFormID, onSubmit, iframeRef, title]
+ );
+
+ useEffect(() => {
+ window.addEventListener("message", handleIFrameMessage, false);
+
+ return () =>
+ window.removeEventListener("message", handleIFrameMessage, false);
+ }, [handleIFrameMessage]);
+
+ useEffect(() => {
+ // Only hide the iframe scroll bar once JS has kicked in and we know we can respond to the setHeight message
+ setStyleOverrides((s) => ({ ...s, overflow: "hidden" }));
+ }, []);
+
+ return (
+
+ );
+};
diff --git a/web/src/components/JotFormPage/JotFormPage.test.tsx b/web/src/components/JotFormPage/JotFormPage.test.tsx
new file mode 100644
index 000000000..80be6be9b
--- /dev/null
+++ b/web/src/components/JotFormPage/JotFormPage.test.tsx
@@ -0,0 +1,74 @@
+import { render, screen, within } from "@testing-library/react";
+
+import { JotFormPage, type JotFormPageProps } from "./JotFormPage";
+
+const props: JotFormPageProps = {
+ formID: "1234",
+ formName: "Form title",
+ height: 567,
+ lead: "Some lead copy",
+ parentPages: [
+ {
+ title: "What we do",
+ path: "/about/what-we-do",
+ },
+ {
+ title: "About",
+ path: "/about",
+ },
+ ],
+};
+
+describe("JotFormPage", () => {
+ it("should render form name and parent pages in page title", () => {
+ render();
+
+ expect(document.title).toBe("Form title | What we do | About");
+ });
+
+ it("should render home breadcrumb", () => {
+ render();
+
+ expect(screen.getByText("Home")).toHaveAttribute("href", "/");
+ });
+
+ it("should render breadcrumbs in reverse order", () => {
+ render();
+
+ const breadcrumbs = screen.getByRole("navigation", { name: "Breadcrumbs" });
+
+ expect(
+ within(breadcrumbs)
+ .getAllByRole("link")
+ .map((a) => a.textContent)
+ ).toStrictEqual(["Home", "About", "What we do"]);
+ });
+
+ it("should render form name as heading 1", () => {
+ render();
+
+ expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent(
+ props.formName
+ );
+ });
+
+ it("should render content start id on page header", () => {
+ render();
+
+ const h1 = screen.getByRole("heading", { level: 1 });
+
+ // eslint-disable-next-line testing-library/no-node-access
+ expect(h1.parentNode).toHaveAttribute("id", "content-start");
+ });
+
+ it("should render iframe with form id and title", () => {
+ render();
+
+ const iframe = screen.getByTitle(props.formName);
+
+ expect(iframe).toHaveAttribute(
+ "src",
+ "https://next-web-tests.jotform.com/1234?isIframeEmbed=1"
+ );
+ });
+});
diff --git a/web/src/components/JotFormPage/JotFormPage.tsx b/web/src/components/JotFormPage/JotFormPage.tsx
new file mode 100644
index 000000000..52643db24
--- /dev/null
+++ b/web/src/components/JotFormPage/JotFormPage.tsx
@@ -0,0 +1,48 @@
+import { NextSeo } from "next-seo";
+import { FC } from "react";
+
+import { Breadcrumbs, Breadcrumb } from "@nice-digital/nds-breadcrumbs";
+import { PageHeader } from "@nice-digital/nds-page-header";
+
+import { JotFormEmbed } from "@/components/JotFormEmbed/JotFormEmbed";
+
+import { FormProps } from "./getGetServerSideProps";
+
+export type JotFormPageProps = FormProps & {
+ formName: string;
+ parentPages: { title: string; path?: string }[];
+ lead: string;
+};
+
+export const JotFormPage: FC = ({
+ height,
+ formID,
+ formName,
+ parentPages,
+ lead,
+}) => {
+ return (
+ <>
+ p.title)].join(" | ")}
+ description={lead}
+ />
+
+
+ {[
+ { title: "Home", path: "/" },
+ ...parentPages.slice().reverse(),
+ { title: formName },
+ ].map(({ title, path }) => (
+
+ {title}
+
+ ))}
+
+
+
+
+
+ >
+ );
+};
diff --git a/web/src/components/JotFormPage/getGetServerSideProps.test.ts b/web/src/components/JotFormPage/getGetServerSideProps.test.ts
new file mode 100644
index 000000000..039615074
--- /dev/null
+++ b/web/src/components/JotFormPage/getGetServerSideProps.test.ts
@@ -0,0 +1,131 @@
+import { getForm } from "@/feeds/jotform/jotform";
+import { logger } from "@/logger";
+
+import { getGetServerSideProps } from "./getGetServerSideProps";
+
+import type { GetServerSidePropsContext } from "next";
+
+jest.mock("@/feeds/jotform/jotform");
+jest.mock("@/logger");
+
+const loggerMock = jest.mocked(logger);
+
+const getFormMock = (getForm as jest.Mock).mockResolvedValue({
+ responseCode: 200,
+ message: "success",
+ content: {
+ id: "223412731228044",
+ username: "nice_teams",
+ title: "NICE UK open content licence form",
+ height: "539",
+ status: "ENABLED",
+ created_at: "2022-12-08 06:31:44",
+ updated_at: "2023-03-27 05:03:55",
+ last_submission: "2023-03-24 14:43:43",
+ new: "34",
+ count: "46",
+ type: "LEGACY",
+ favorite: "0",
+ archived: "0",
+ url: "https://nice.jotform.com/223412731228044",
+ },
+ duration: "14.98ms",
+});
+
+const getServerSidePropsContext = {
+ resolvedUrl: "/forms/anything",
+} as GetServerSidePropsContext;
+
+describe("getGetServerSideProps", () => {
+ it("should return valid props after successful API form response", async () => {
+ getFormMock.mockResolvedValueOnce({
+ responseCode: 200,
+ message: "success",
+ content: {
+ status: "ENABLED",
+ height: "987",
+ },
+ duration: "17.33ms",
+ });
+
+ await expect(
+ getGetServerSideProps("1234")(getServerSidePropsContext)
+ ).resolves.toStrictEqual({
+ props: {
+ height: "987",
+ formID: "1234",
+ },
+ });
+ });
+
+ it("should log error message and throw when the API request to JotForm fails", async () => {
+ const error = new Error("Test error");
+ getFormMock.mockRejectedValueOnce(error);
+
+ await expect(
+ getGetServerSideProps("1234")(getServerSidePropsContext)
+ ).rejects.toBe(error);
+
+ expect(loggerMock.error).toHaveBeenCalledWith(
+ "Could not get form from JotForm API with id 1234 at URL /forms/anything",
+ error
+ );
+ });
+
+ it("should log message and return not found when the form is not found", async () => {
+ getFormMock.mockResolvedValueOnce({
+ responseCode: 404,
+ message: "Requested URL (/form/1234) is not available!",
+ content: "",
+ duration: "14.48ms",
+ info: "https://api.jotform.com/docs",
+ });
+
+ const result = await getGetServerSideProps("1234")(
+ getServerSidePropsContext
+ );
+
+ expect(result).toStrictEqual({ notFound: true });
+
+ expect(loggerMock.info).toHaveBeenCalledWith(
+ "Couldn't find form with id 1234 at URL /forms/anything"
+ );
+ });
+
+ it("should log message and throw when the request to JotForm API is unauthorized", async () => {
+ getFormMock.mockResolvedValueOnce({
+ responseCode: 401,
+ message: "You're not authorized to use (/form-id) ",
+ content: "",
+ duration: "15.15ms",
+ info: "https://api.jotform.com/docs#form-id",
+ });
+
+ await expect(
+ getGetServerSideProps("1234")(getServerSidePropsContext)
+ ).rejects.toThrow(
+ "Got 401 unauthorized response for form with id 1234 at URL /forms/anything"
+ );
+ });
+
+ it("should log warning message and return not found when form is disabled", async () => {
+ getFormMock.mockResolvedValueOnce({
+ responseCode: 200,
+ message: "success",
+ content: {
+ status: "DISABLED",
+ },
+ duration: "17.33ms",
+ });
+
+ const result = await getGetServerSideProps("1234")(
+ getServerSidePropsContext
+ );
+
+ expect(result).toStrictEqual({ notFound: true });
+
+ expect(loggerMock.warn).toHaveBeenCalledWith(
+ "Form with id 1234 at URL /forms/anything is disabled"
+ );
+ });
+});
diff --git a/web/src/components/JotFormPage/getGetServerSideProps.ts b/web/src/components/JotFormPage/getGetServerSideProps.ts
new file mode 100644
index 000000000..30742f038
--- /dev/null
+++ b/web/src/components/JotFormPage/getGetServerSideProps.ts
@@ -0,0 +1,48 @@
+import { GetServerSideProps } from "next";
+
+import { type FormID, getForm } from "@/feeds/jotform/jotform";
+import { logger } from "@/logger";
+
+export type FormProps = {
+ height: number;
+ formID: FormID;
+};
+
+// Note: this is a separate file rather than named export from the page component, because the NextJS tree shaking was trying to include needle client side, and causing a build error
+export const getGetServerSideProps =
+ (formID: FormID): GetServerSideProps =>
+ async ({ resolvedUrl }) => {
+ try {
+ const formResponse = await getForm(formID);
+
+ if (formResponse.responseCode === 404) {
+ logger.info(
+ `Couldn't find form with id ${formID} at URL ${resolvedUrl}`
+ );
+ return { notFound: true };
+ } else if (formResponse.responseCode === 401)
+ throw Error(
+ `Got 401 unauthorized response for form with id ${formID} at URL ${resolvedUrl}`
+ );
+
+ const { height, status } = formResponse.content;
+
+ if (status === "DISABLED") {
+ logger.warn(`Form with id ${formID} at URL ${resolvedUrl} is disabled`);
+ return { notFound: true };
+ }
+
+ return {
+ props: {
+ height,
+ formID,
+ },
+ };
+ } catch (e) {
+ logger.error(
+ `Could not get form from JotForm API with id ${formID} at URL ${resolvedUrl}`,
+ e
+ );
+ throw e;
+ }
+ };
diff --git a/web/src/feeds/jotform/jotform.ts b/web/src/feeds/jotform/jotform.ts
new file mode 100644
index 000000000..1671b517b
--- /dev/null
+++ b/web/src/feeds/jotform/jotform.ts
@@ -0,0 +1,22 @@
+import needle from "needle";
+
+import { serverRuntimeConfig, publicRuntimeConfig } from "@/config";
+
+import type { FormID, GetFormResponse } from "./types";
+
+export * from "./types";
+
+const { apiKey } = serverRuntimeConfig.feeds.jotForm,
+ { baseURL } = publicRuntimeConfig.jotForm;
+
+export const getForm = async (formID: FormID): Promise => {
+ const response = await needle(
+ "get",
+ `${baseURL}/API/form/${formID}?apiKey=${apiKey}`,
+ {
+ json: true,
+ }
+ );
+
+ return response.body as GetFormResponse;
+};
diff --git a/web/src/feeds/jotform/types.ts b/web/src/feeds/jotform/types.ts
new file mode 100644
index 000000000..cdf937fc7
--- /dev/null
+++ b/web/src/feeds/jotform/types.ts
@@ -0,0 +1,51 @@
+export type FormID = `${number}`;
+
+export type Duration = `${number}ms`;
+
+export type NotFoundResponse = {
+ responseCode: 404;
+ message: string;
+ content: "";
+ duration: Duration;
+ info: string;
+};
+
+export type NotAuthorizedResponse = {
+ responseCode: 401;
+ message: string;
+ content: "";
+ duration: Duration;
+ info: string;
+};
+
+export type FormStatus = "ENABLED" | "DISABLED";
+
+export type FormType = "LEGACY" | "CARD";
+
+export type FormSuccessResponse = {
+ responseCode: 200;
+ message: "success";
+ content: {
+ id: string;
+ username: string;
+ title: string;
+ height: number;
+ status: FormStatus;
+ /** Format like "2023-03-21 09:27:18" */
+ created_at: string;
+ updated_at: string;
+ last_submission: string;
+ new: `${number}`;
+ count: `${number}`;
+ type: FormType;
+ favorite: `${number}`;
+ archived: `${number}`;
+ url: string;
+ };
+ duration: Duration;
+};
+
+export type GetFormResponse =
+ | NotAuthorizedResponse
+ | NotFoundResponse
+ | FormSuccessResponse;
diff --git a/web/src/pages/forms/__snapshots__/interventional-procedures-notification.page.test.tsx.snap b/web/src/pages/forms/__snapshots__/interventional-procedures-notification.page.test.tsx.snap
new file mode 100644
index 000000000..afc93a007
--- /dev/null
+++ b/web/src/pages/forms/__snapshots__/interventional-procedures-notification.page.test.tsx.snap
@@ -0,0 +1,186 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`InterventionalProceduresNotificationForm should match snapshot 1`] = `
+
+`;
diff --git a/web/src/pages/forms/__snapshots__/use-of-nice-content-in-the-uk.page.test.tsx.snap b/web/src/pages/forms/__snapshots__/use-of-nice-content-in-the-uk.page.test.tsx.snap
new file mode 100644
index 000000000..c367f9ffc
--- /dev/null
+++ b/web/src/pages/forms/__snapshots__/use-of-nice-content-in-the-uk.page.test.tsx.snap
@@ -0,0 +1,96 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`UseOfNICEContentInTheUKForm should match snapshot 1`] = `
+
+`;
diff --git a/web/src/pages/forms/interventional-procedures-notification.page.test.tsx b/web/src/pages/forms/interventional-procedures-notification.page.test.tsx
new file mode 100644
index 000000000..704452964
--- /dev/null
+++ b/web/src/pages/forms/interventional-procedures-notification.page.test.tsx
@@ -0,0 +1,46 @@
+import { render } from "@testing-library/react";
+import { GetServerSidePropsContext } from "next";
+
+import { FormProps } from "@/components/JotFormPage/getGetServerSideProps";
+
+import InterventionalProceduresNotificationForm, {
+ getServerSideProps,
+} from "./interventional-procedures-notification.page";
+
+jest.mock("@/feeds/jotform/jotform", () => ({
+ getForm: jest.fn().mockResolvedValue({
+ responseCode: 200,
+ message: "success",
+ content: {
+ id: "230793530776059",
+ username: "nice_teams",
+ title: "Interventional procedures notification form",
+ height: "539",
+ status: "ENABLED",
+ created_at: "2023-03-21 09:27:18",
+ updated_at: "2023-03-27 05:10:51",
+ last_submission: "2023-03-24 11:33:20",
+ new: "3",
+ count: "3",
+ type: "LEGACY",
+ favorite: "0",
+ archived: "0",
+ url: "https://nice.jotform.com/230793530776059",
+ },
+ duration: "23.46ms",
+ }),
+}));
+
+describe("InterventionalProceduresNotificationForm", () => {
+ it("should match snapshot", async () => {
+ const props = (await getServerSideProps({
+ resolvedUrl: "/forms/interventional-procedures-notification",
+ } as GetServerSidePropsContext)) as { props: FormProps };
+
+ const { container } = render(
+
+ );
+
+ expect(container).toMatchSnapshot();
+ });
+});
diff --git a/web/src/pages/forms/interventional-procedures-notification.page.tsx b/web/src/pages/forms/interventional-procedures-notification.page.tsx
new file mode 100644
index 000000000..e3d5f4467
--- /dev/null
+++ b/web/src/pages/forms/interventional-procedures-notification.page.tsx
@@ -0,0 +1,45 @@
+import {
+ type FormProps,
+ getGetServerSideProps,
+} from "@/components/JotFormPage/getGetServerSideProps";
+import { JotFormPage } from "@/components/JotFormPage/JotFormPage";
+
+export const getServerSideProps = getGetServerSideProps("230793530776059");
+
+export default function InterventionalProceduresNotificationForm(
+ props: FormProps
+): JSX.Element {
+ return (
+
+ );
+}
diff --git a/web/src/pages/forms/use-of-nice-content-in-the-uk.page.test.tsx b/web/src/pages/forms/use-of-nice-content-in-the-uk.page.test.tsx
new file mode 100644
index 000000000..cd40ba47b
--- /dev/null
+++ b/web/src/pages/forms/use-of-nice-content-in-the-uk.page.test.tsx
@@ -0,0 +1,46 @@
+import { render } from "@testing-library/react";
+import { GetServerSidePropsContext } from "next";
+
+import { FormProps } from "@/components/JotFormPage/getGetServerSideProps";
+
+import UseOfNICEContentInTheUKForm, {
+ getServerSideProps,
+} from "./use-of-nice-content-in-the-uk.page";
+
+jest.mock("@/feeds/jotform/jotform", () => ({
+ getForm: jest.fn().mockResolvedValue({
+ responseCode: 200,
+ message: "success",
+ content: {
+ id: "223412731228044",
+ username: "nice_teams",
+ title: "NICE UK open content licence form",
+ height: "539",
+ status: "ENABLED",
+ created_at: "2022-12-08 06:31:44",
+ updated_at: "2023-03-27 05:03:55",
+ last_submission: "2023-03-24 14:43:43",
+ new: "34",
+ count: "46",
+ type: "LEGACY",
+ favorite: "0",
+ archived: "0",
+ url: "https://nice.jotform.com/223412731228044",
+ },
+ duration: "14.98ms",
+ }),
+}));
+
+describe("UseOfNICEContentInTheUKForm", () => {
+ it("should match snapshot", async () => {
+ const props = (await getServerSideProps({
+ resolvedUrl: "/forms/use-of-nice-content-in-the-uk",
+ } as GetServerSidePropsContext)) as { props: FormProps };
+
+ const { container } = render(
+
+ );
+
+ expect(container).toMatchSnapshot();
+ });
+});
diff --git a/web/src/pages/forms/use-of-nice-content-in-the-uk.page.tsx b/web/src/pages/forms/use-of-nice-content-in-the-uk.page.tsx
new file mode 100644
index 000000000..27412a261
--- /dev/null
+++ b/web/src/pages/forms/use-of-nice-content-in-the-uk.page.tsx
@@ -0,0 +1,25 @@
+import {
+ type FormProps,
+ getGetServerSideProps,
+} from "@/components/JotFormPage/getGetServerSideProps";
+import { JotFormPage } from "@/components/JotFormPage/JotFormPage";
+
+export const getServerSideProps = getGetServerSideProps("223412731228044");
+
+export default function UseOfNICEContentInTheUKForm(
+ props: FormProps
+): JSX.Element {
+ return (
+
+ );
+}