diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 3ebed0e0e3..39a584f0fb 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -14,6 +14,9 @@
"source=${env:HOME}${env:USERPROFILE}/.gnupg,target=/home/vscode/.gnupg,type=bind",
"source=${env:HOME}${env:USERPROFILE}/.npmrc,target=/home/vscode/.npmrc,type=bind"
],
+ "runArgs": [
+ "--network=host"
+ ],
"remoteEnv": { "LOCAL_WORKSPACE_FOLDER": "${localWorkspaceFolder}" },
"postAttachCommand": "docker build -f https://raw.githubusercontent.com/NHSDigital/eps-workflow-quality-checks/refs/tags/v4.0.4/dockerfiles/nhsd-git-secrets.dockerfile -t git-secrets . && poetry run pre-commit install --install-hooks -f",
"features": {
@@ -27,24 +30,25 @@
"customizations": {
"vscode": {
"extensions": [
- "AmazonWebServices.aws-toolkit-vscode",
- "redhat.vscode-yaml",
- "ms-python.python",
- "ms-python.flake8",
- "eamodio.gitlens",
- "github.vscode-pull-request-github",
- "orta.vscode-jest",
- "42crunch.vscode-openapi",
- "mermade.openapi-lint",
- "christian-kohler.npm-intellisense",
- "dbaeumer.vscode-eslint",
- "lfm.vscode-makefile-term",
- "GrapeCity.gc-excelviewer",
- "redhat.vscode-xml",
- "streetsidesoftware.code-spell-checker",
- "timonwong.shellcheck",
- "mkhl.direnv",
- "github.vscode-github-actions"
+ "AmazonWebServices.aws-toolkit-vscode",
+ "redhat.vscode-yaml",
+ "ms-python.python",
+ "ms-python.flake8",
+ "eamodio.gitlens",
+ "github.vscode-pull-request-github",
+ "orta.vscode-jest",
+ "42crunch.vscode-openapi",
+ "mermade.openapi-lint",
+ "christian-kohler.npm-intellisense",
+ "dbaeumer.vscode-eslint",
+ "lfm.vscode-makefile-term",
+ "GrapeCity.gc-excelviewer",
+ "redhat.vscode-xml",
+ "streetsidesoftware.code-spell-checker",
+ "timonwong.shellcheck",
+ "mkhl.direnv",
+ "github.vscode-github-actions",
+ "Gruntfuggly.todo-tree"
],
"settings": {
"python.defaultInterpreterPath": "/workspaces/eps-prescription-tracker-ui/.venv/bin/python",
diff --git a/.github/workflows/deploy_website_content.yml b/.github/workflows/deploy_website_content.yml
index 074807b66f..5bbb0096cc 100644
--- a/.github/workflows/deploy_website_content.yml
+++ b/.github/workflows/deploy_website_content.yml
@@ -76,8 +76,9 @@ jobs:
export NEXT_PUBLIC_userPoolClientId=${userPoolClientId}
export NEXT_PUBLIC_userPoolId=${userPoolId}
export NEXT_PUBLIC_redirectSignIn="https://${fullCloudfrontDomain}/site/selectyourrole.html"
- export NEXT_PUBLIC_redirectSignOut="https://${fullCloudfrontDomain}/site/"
+ export NEXT_PUBLIC_redirectSignOut="https://${fullCloudfrontDomain}/site/logout.html"
export NEXT_PUBLIC_COMMIT_ID=${{ inputs.COMMIT_ID }}
+ export NEXT_PUBLIC_TARGET_ENVIRONMENT=${{ inputs.TARGET_ENVIRONMENT }}
cd .build
make react-build
diff --git a/.vscode/eps-prescription-tracker-ui.code-workspace b/.vscode/eps-prescription-tracker-ui.code-workspace
index 282e7acfee..b58e0e4403 100644
--- a/.vscode/eps-prescription-tracker-ui.code-workspace
+++ b/.vscode/eps-prescription-tracker-ui.code-workspace
@@ -44,7 +44,6 @@
"name": "packages/trackerUserInfoLambda",
"path": "../packages/trackerUserInfoLambda"
}
-
],
"settings": {
"jest.disabledWorkspaceFolders": [
@@ -144,7 +143,8 @@
},
"typescript.tsdk": "eps-prescription-tracker-ui-monorepo/node_modules/typescript/lib",
"eslint.useFlatConfig": true,
- "eslint.format.enable": true
+ "eslint.format.enable": true,
+ "editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"extensions": {
"recommendations": [
diff --git a/README.md b/README.md
index 8ca7836650..9a40dbfcab 100644
--- a/README.md
+++ b/README.md
@@ -98,11 +98,13 @@ export NEXT_PUBLIC_userPoolId="eu-west-2_deadbeef"
export LOCAL_DEV=true
# DON'T TOUCH!
-export API_DOMAIN_OVERRIDE=https://${SERVICE_NAME}.dev.eps.national.nhs.uk/
+export NEXT_PUBLIC_TARGET_ENVIRONMENT=dev # enables mock auth
+export BASE_PATH="/site" # Hosts the site at `localhost:3000/site`
+export API_DOMAIN_OVERRIDE=https://${SERVICE_NAME}.dev.eps.national.nhs.uk/ # Proxies the actual deployed backend for this PR
export NEXT_PUBLIC_hostedLoginDomain=${SERVICE_NAME}.auth.eu-west-2.amazoncognito.com
-export NEXT_PUBLIC_redirectSignIn=http://localhost:3000/selectyourrole/
-export NEXT_PUBLIC_redirectSignOut=http://localhost:3000/
+export NEXT_PUBLIC_redirectSignIn=http://localhost:3000/site/selectyourrole
+export NEXT_PUBLIC_redirectSignOut=http://localhost:3000/site/logout
export NEXT_PUBLIC_COMMIT_ID="Local Development Server"
@@ -121,11 +123,12 @@ userPoolClientId=$(aws cloudformation list-exports --region eu-west-2 --query "E
userPoolId=$(aws cloudformation list-exports --region eu-west-2 --query "Exports[?Name=='${SERVICE_NAME}-stateful-resources:userPool:Id'].Value" --output text)
echo $userPoolClientId
echo $userPoolId
+
```
-For me, the aws terminal console installed in the dev container refuses to work. Another approach is to use the browser console, accessed by clicking the terminal icon next to the search bar on the AWS web dashboard.
+For me, the aws terminal console installed in the dev container refuses to work without causing a headache. Another approach is to use the browser console, accessed by clicking the terminal icon next to the search bar on the AWS web dashboard.
-n.b. Ensure you've properly sourced these variables! Direnv can sometimes miss changes.
+n.b. Ensure you've properly sourced these variables! `direnv` can sometimes miss changes on my machine.
```
source .envrc
```
diff --git a/packages/cdk/resources/Cognito.ts b/packages/cdk/resources/Cognito.ts
index 7e9fee9765..63db7943bd 100644
--- a/packages/cdk/resources/Cognito.ts
+++ b/packages/cdk/resources/Cognito.ts
@@ -186,24 +186,31 @@ export class Cognito extends Construct {
}
const callbackUrls = [
- `https://${props.fullCloudfrontDomain}/site/`,
+ `https://${props.fullCloudfrontDomain}/site/selectyourrole`,
// FIXME: This is temporary, until we get routing fixed
`https://${props.fullCloudfrontDomain}/site/selectyourrole.html`,
- `https://${props.fullCloudfrontDomain}/auth_demo/`,
- `https://${props.fullCloudfrontDomain}/site/selectyourrole/`,
+ // TODO: This is for the proof-of-concept login page, and can probably be deleted soon.
+ `https://${props.fullCloudfrontDomain}/auth_demo`,
`https://${props.fullCloudfrontDomain}/oauth2/idpresponse`
]
const logoutUrls = [
- `https://${props.fullCloudfrontDomain}/site/`,
- `https://${props.fullCloudfrontDomain}/site/auth_demo.html`,
- `https://${props.fullCloudfrontDomain}/auth_demo/`
+ `https://${props.fullCloudfrontDomain}/site/logout`,
+ `https://${props.fullCloudfrontDomain}/site/logout.html`,
+ `https://${props.fullCloudfrontDomain}/auth_demo`
]
if (props.useLocalhostCallback) {
+ // Local, without base path set
+ callbackUrls.push("http://localhost:3000/selectyourrole/")
+ logoutUrls.push("http://localhost:3000/logout/")
+ // Local, with base path set to /site
+ logoutUrls.push("http://localhost:3000/site/logout/")
+ callbackUrls.push("http://localhost:3000/site/selectyourrole/")
+ // Auth demo stuff
callbackUrls.push("http://localhost:3000/auth/")
callbackUrls.push("http://localhost:3000/auth_demo/")
- callbackUrls.push("http://localhost:3000/selectyourrole/")
+ // Root path, just in case
logoutUrls.push("http://localhost:3000/")
}
// add the web client
diff --git a/packages/cpt-ui/__tests__/EpsModal.test.tsx b/packages/cpt-ui/__tests__/EpsModal.test.tsx
new file mode 100644
index 0000000000..5c002ce818
--- /dev/null
+++ b/packages/cpt-ui/__tests__/EpsModal.test.tsx
@@ -0,0 +1,95 @@
+import React from "react";
+import { render, screen, fireEvent } from "@testing-library/react";
+import "@testing-library/jest-dom";
+import { EpsModal } from "@/components/EpsModal";
+
+describe("EpsModal", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test("does not render the modal when isOpen is false", () => {
+ render(
+
+
Modal Content
+
+ );
+ // The content should not be in the document
+ expect(screen.queryByText(/Modal Content/i)).not.toBeInTheDocument();
+ });
+
+ test("renders the modal when isOpen is true", () => {
+ render(
+
+
Modal Content
+
+ );
+ // The content should appear in the document
+ expect(screen.getByText(/Modal Content/i)).toBeInTheDocument();
+ });
+
+ test("calls onClose when user clicks outside modal content", () => {
+ const onCloseMock = jest.fn();
+ render(
+
+
Modal Content
+
+ );
+
+ const overlay = screen.getByTestId("eps-modal-overlay");
+ const modalContent = screen.getByTestId("modal-content");
+
+ // Clicking directly on the content should NOT trigger onClose
+ fireEvent.click(modalContent);
+ expect(onCloseMock).not.toHaveBeenCalled();
+
+ // Clicking on the overlay (outside the content) should trigger onClose
+ fireEvent.click(overlay);
+ expect(onCloseMock).toHaveBeenCalledTimes(1);
+ });
+
+ test("calls onClose when user clicks the close button", () => {
+ const onCloseMock = jest.fn();
+ render(
+
+
+
+ {/* Conditionally show "change role" or "confirm role" */}
+ {pathname !== "/" ? (
+
+
+ {HEADER_CHANGE_ROLE_BUTTON}
+
+
+ ) : (
+
+
+ {HEADER_CONFIRM_ROLE_BUTTON}
+
+
+ )}
+
+ {/* FIXME: Only the selectyourrole and changerole links get put in the collapsible menu when on mobile */}
+ {pathname === "/selectyourrole" ? (
+
+
+ {HEADER_CONFIRM_ROLE_BUTTON}
+
+
+ ) : (
+
+
+ {HEADER_SELECT_YOUR_ROLE_BUTTON}
+
+
+ )}
+
+ {/* FIXME: Only shows the Log out link if the user is signed in, but introduces a lag on page reload. */}
+ {auth?.isSignedIn && (
+
+
+
+ );
+}
diff --git a/packages/cpt-ui/components/EpsModal.tsx b/packages/cpt-ui/components/EpsModal.tsx
new file mode 100644
index 0000000000..ec29d24e93
--- /dev/null
+++ b/packages/cpt-ui/components/EpsModal.tsx
@@ -0,0 +1,68 @@
+"use client";
+import React, { useEffect } from "react";
+
+import "@/assets/styles/EpsModal.scss";
+
+interface EpsModalProps {
+ readonly children: React.ReactNode;
+ readonly isOpen: boolean;
+ readonly onClose: () => void;
+}
+
+export function EpsModal({ children, isOpen, onClose }: EpsModalProps) {
+ // Close modal on `Escape` key
+ useEffect(() => {
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === "Escape") {
+ onClose();
+ }
+ };
+
+ window.addEventListener("keydown", handleKeyDown);
+ return () => window.removeEventListener("keydown", handleKeyDown);
+ }, [onClose]);
+
+ // Close if user clicks outside the modal content
+ const handleBackdropClick = (e: React.MouseEvent) => {
+ if (e.target === e.currentTarget) {
+ onClose();
+ }
+ };
+
+ // Close if user activates on the background
+ const handleBackdropActivate = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter" || e.key === " ") {
+ onClose();
+ }
+ };
+
+ // If the modal isn’t open, don’t render anything
+ if (!isOpen) return null;
+
+ return (
+ // This should be a button for accessibility, but we can't have buttons be descendants of buttons,
+ // and the modal children will have buttons in it.
+ // (making this a button does actually work, so this might be a FIXME to solve the hydration error)
+
+
+
+ )
+}
diff --git a/packages/cpt-ui/components/EpsSpinner.tsx b/packages/cpt-ui/components/EpsSpinner.tsx
new file mode 100644
index 0000000000..ad22e2545d
--- /dev/null
+++ b/packages/cpt-ui/components/EpsSpinner.tsx
@@ -0,0 +1,110 @@
+'use client';
+import React from 'react';
+
+import { EpsSpinnerStrings } from '@/constants/ui-strings/EpsSpinnerStrings';
+
+
+function Spinner({
+ radius = 100,
+ thickness = 12,
+ fraction = 0.2, // The fraction of the hoop that is green
+ speed = 1 // The speed (in seconds) for one full rotation
+}) {
+
+ // The portion that should appear green is defined by "fraction"
+ // If fraction = 0.25, then 25% of the hoop is green and 75% is grey
+ const circumference = 2 * Math.PI * radius;
+ const offset = circumference * (1 - fraction);
+
+ return (
+ // FIXME: In theory, this should be a