Skip to content
Open
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 app/(gcforms)/[locale]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { auth } from "@lib/auth";
import { ClientContexts } from "@clientComponents/globals/ClientContexts";
import { ReactHydrationCheck } from "@clientComponents/globals";
import { NotifyCatcher } from "@lib/notifyCatcher/NotifyCatcher";
import { checkAll } from "@lib/cache/flags";

export default async function Layout({ children }: { children: React.ReactNode }) {
Expand All @@ -13,6 +14,7 @@ export default async function Layout({ children }: { children: React.ReactNode }
<ClientContexts session={session} featureFlags={featureFlags}>
{children}
</ClientContexts>
{process.env.APP_ENV === "local" && <NotifyCatcher />}
</>
);
}
16 changes: 16 additions & 0 deletions lib/integration/notifyConnector.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { GCNotifyConnector, type Personalisation } from "@gcforms/connectors";
import { logMessage } from "@lib/logger";
import { notifyCatcher } from "@lib/notifyCatcher";

const gcNotifyConnector = GCNotifyConnector.default(process.env.NOTIFY_API_KEY ?? "");

Expand All @@ -10,6 +11,21 @@ export const sendEmail = async (email: string, personalisation: Personalisation,
return;
}

if (process.env.APP_ENV === "local") {
try {
notifyCatcher(email, personalisation);
} catch (e) {
logMessage.error("NotifyCatcher failed to catch the email", e);

// just log it out
logMessage.info("Development Notify email sending:", "", { email, personalisation });
logMessage.info("To: " + email);
logMessage.info("Subject: " + personalisation.subject);
logMessage.info("Body: " + personalisation.formResponse);
}
return;
}

const templateId = process.env.TEMPLATE_ID;
if (!templateId) {
throw new Error("No Notify template ID configured.");
Expand Down
175 changes: 175 additions & 0 deletions lib/notifyCatcher/NotifyCatcher.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { fetchMessages, resetMessages } from "./actions";
import Markdown from "markdown-to-jsx";

interface Message {
email: string;
personalisation: {
subject: string;
formResponse: string;
};
}

export const NotifyCatcher = () => {
const ref = useRef<HTMLDivElement>(null);
const [messages, setMessages] = useState<Message[]>([]);
const [loading, setLoading] = useState(true);
const [visible, setVisible] = useState(false);

const getMessages = async () => {
setLoading(true);
const messages = await fetchMessages();
setMessages(messages);
setLoading(false);
};

const open = () => {
getMessages();
setVisible(true);
};

const handleClickOutside = (event: MouseEvent) => {
if (ref.current && !ref.current.contains(event.target as Node)) {
setVisible(false);
}
};

const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
setVisible(false);
}
};

const clearMessages = async () => {
resetMessages();
setMessages([]);
};

useEffect(() => {
getMessages();
document.addEventListener("mousedown", handleClickOutside);
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
document.removeEventListener("keydown", handleKeyDown);
};
}, []);

return (
<>
{!visible && (
<div
className="fixed bottom-0 right-0 mb-10 mr-10 cursor-pointer rounded-md bg-violet-700 p-2 text-white shadow-sm"
onClick={() => open()}
>
<svg
xmlns="http://www.w3.org/2000/svg"
height="24px"
viewBox="0 -960 960 960"
width="24px"
fill="#FFF"
className="mr-2 inline-block"
>
<path d="M160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h640q33 0 56.5 23.5T880-720v480q0 33-23.5 56.5T800-160H160Zm320-280L160-640v400h640v-400L480-440Zm0-80 320-200H160l320 200ZM160-640v-80 480-400Z" />
</svg>
Notify Intercept
</div>
)}
{visible && (
<div
ref={ref}
className="fixed bottom-0 right-0 mb-10 mr-10 h-[600px] w-2/5 overflow-x-hidden border border-slate-700 bg-white shadow-md"
>
{loading ? (
<div className="h-[600px] p-80">Loading...</div>
) : (
<>
<div className="flex justify-between border-b border-slate-300 px-4 pt-4 shadow-sm">
<div>
<svg
xmlns="http://www.w3.org/2000/svg"
height="24px"
viewBox="0 -960 960 960"
width="24px"
className="mr-2 inline-block"
>
<path d="M160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h640q33 0 56.5 23.5T880-720v480q0 33-23.5 56.5T800-160H160Zm320-280L160-640v400h640v-400L480-440Zm0-80 320-200H160l320 200ZM160-640v-80 480-400Z" />
</svg>
<h2 className="inline-block text-lg">Notify Intercept</h2>
</div>
<div className="flex gap-2">
{messages.length > 0 && (
<button onClick={() => clearMessages()}>
<svg
xmlns="http://www.w3.org/2000/svg"
height="24px"
viewBox="0 -960 960 960"
width="24px"
fill="#5f6368"
>
<path d="M600-240v-80h160v80H600Zm0-320v-80h280v80H600Zm0 160v-80h240v80H600ZM120-640H80v-80h160v-60h160v60h160v80h-40v360q0 33-23.5 56.5T440-200H200q-33 0-56.5-23.5T120-280v-360Zm80 0v360h240v-360H200Zm0 0v360-360Z" />
</svg>
</button>
)}
<button onClick={() => getMessages()}>
<svg
xmlns="http://www.w3.org/2000/svg"
height="24px"
viewBox="0 -960 960 960"
width="24px"
fill="#5f6368"
>
<path d="M480-160q-134 0-227-93t-93-227q0-134 93-227t227-93q69 0 132 28.5T720-690v-110h80v280H520v-80h168q-32-56-87.5-88T480-720q-100 0-170 70t-70 170q0 100 70 170t170 70q77 0 139-44t87-116h84q-28 106-114 173t-196 67Z" />
</svg>
</button>
<button onClick={() => setVisible(false)}>
<svg
xmlns="http://www.w3.org/2000/svg"
height="24px"
viewBox="0 -960 960 960"
width="24px"
fill="#5f6368"
>
<path d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z" />
</svg>
</button>
</div>
</div>
<div className="max-h-[500px] overflow-y-scroll px-4">
{messages.length === 0 && (
<div className="my-4 rounded-md border border-violet-700 bg-violet-50 p-4">
No messages to show
</div>
)}
{messages &&
messages.map((message, index) => {
return (
<div
className="my-4 rounded-md border border-violet-700 bg-violet-50 p-4"
key={index}
>
<dl className="grid grid-cols-[auto,1fr] gap-x-4">
<dt className="font-semibold">To:</dt>
<dd>{message.email}</dd>
<dt className="font-semibold">Subject:</dt>
<dd>{message.personalisation.subject}</dd>
</dl>
<div className="border-t border-violet-700 mt-4 pt-4">

Check warning on line 158 in lib/notifyCatcher/NotifyCatcher.tsx

View workflow job for this annotation

GitHub Actions / eslint_pr

Invalid Tailwind CSS classnames order

Check warning on line 158 in lib/notifyCatcher/NotifyCatcher.tsx

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

lib/notifyCatcher/NotifyCatcher.tsx#L158

[tailwindcss/classnames-order] Invalid Tailwind CSS classnames order
<div className="whitespace-pre-line">
<Markdown options={{ forceBlock: true }}>
{message.personalisation.formResponse}
</Markdown>
</div>
</div>
</div>
);
})}
</div>
</>
)}
</div>
)}
</>
);
};
13 changes: 13 additions & 0 deletions lib/notifyCatcher/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"use server";
import { getRedisInstance } from "@lib/integration/redisConnector";

export const fetchMessages = async () => {
const redis = await getRedisInstance();
const emails = await redis.lrange("notifyEmails", 0, -1);
return emails.map((email) => JSON.parse(email));
};

export const resetMessages = async () => {
const redis = await getRedisInstance();
await redis.del("notifyEmails");
};
15 changes: 15 additions & 0 deletions lib/notifyCatcher/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { type Personalisation } from "@gcforms/connectors";
import { getRedisInstance } from "@lib/integration/redisConnector";
import { logMessage } from "@lib/logger";

export const notifyCatcher = async (email: string, personalisation: Personalisation) => {
try {
const redis = await getRedisInstance();
await redis.lpush("notifyEmails", JSON.stringify({ email, personalisation }));
} catch (e) {
logMessage.info("Development Notify email sending:", "", { email, personalisation });
logMessage.info("To: " + email);
logMessage.info("Subject: " + personalisation.subject);
logMessage.info("Body: " + personalisation.formResponse);
}
};
6 changes: 5 additions & 1 deletion tailwind.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,10 @@ module.exports = {
xxs: { max: "290px" },
},
},
content: ["./app/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}"],
content: [
"./app/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./lib/notifyCatcher/**/*.{ts,tsx}",
],
plugins: [],
} satisfies Config;
Loading