diff --git a/frontend/next-env.d.ts b/frontend/next-env.d.ts
index 4f11a03dc..fd36f9494 100644
--- a/frontend/next-env.d.ts
+++ b/frontend/next-env.d.ts
@@ -1,5 +1,6 @@
///
///
+///
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 1932ea3d1..77e549aad 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -10,13 +10,14 @@
"hasInstallScript": true,
"dependencies": {
"@next/third-parties": "^14.0.4",
- "@trussworks/react-uswds": "^5.0.0",
+ "@trussworks/react-uswds": "^7.0.0",
"@uswds/uswds": "^3.6.0",
"i18next": "^23.0.0",
"js-cookie": "^3.0.5",
"lodash": "^4.17.21",
- "next": "^13.5.2",
+ "next": "^14.0.3",
"next-i18next": "^15.0.0",
+ "next-intl": "^3.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^14.0.0",
@@ -3486,6 +3487,92 @@
"integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==",
"dev": true
},
+ "node_modules/@formatjs/ecma402-abstract": {
+ "version": "1.18.2",
+ "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.18.2.tgz",
+ "integrity": "sha512-+QoPW4csYALsQIl8GbN14igZzDbuwzcpWrku9nyMXlaqAlwRBgl5V+p0vWMGFqHOw37czNXaP/lEk4wbLgcmtA==",
+ "dependencies": {
+ "@formatjs/intl-localematcher": "0.5.4",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@formatjs/ecma402-abstract/node_modules/@formatjs/intl-localematcher": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.4.tgz",
+ "integrity": "sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==",
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@formatjs/fast-memoize": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-1.2.1.tgz",
+ "integrity": "sha512-Rg0e76nomkz3vF9IPlKeV+Qynok0r7YZjL6syLz4/urSg0IbjPZCB/iYUMNsYA643gh4mgrX3T7KEIFIxJBQeg==",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@formatjs/icu-messageformat-parser": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.1.0.tgz",
+ "integrity": "sha512-Qxv/lmCN6hKpBSss2uQ8IROVnta2r9jd3ymUEIjm2UyIkUCHVcbUVRGL/KS/wv7876edvsPe+hjHVJ4z8YuVaw==",
+ "dependencies": {
+ "@formatjs/ecma402-abstract": "1.11.4",
+ "@formatjs/icu-skeleton-parser": "1.3.6",
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@formatjs/icu-messageformat-parser/node_modules/@formatjs/ecma402-abstract": {
+ "version": "1.11.4",
+ "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz",
+ "integrity": "sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==",
+ "dependencies": {
+ "@formatjs/intl-localematcher": "0.2.25",
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@formatjs/icu-messageformat-parser/node_modules/@formatjs/intl-localematcher": {
+ "version": "0.2.25",
+ "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.25.tgz",
+ "integrity": "sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@formatjs/icu-skeleton-parser": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.6.tgz",
+ "integrity": "sha512-I96mOxvml/YLrwU2Txnd4klA7V8fRhb6JG/4hm3VMNmeJo1F03IpV2L3wWt7EweqNLES59SZ4d6hVOPCSf80Bg==",
+ "dependencies": {
+ "@formatjs/ecma402-abstract": "1.11.4",
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@formatjs/icu-skeleton-parser/node_modules/@formatjs/ecma402-abstract": {
+ "version": "1.11.4",
+ "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz",
+ "integrity": "sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==",
+ "dependencies": {
+ "@formatjs/intl-localematcher": "0.2.25",
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@formatjs/icu-skeleton-parser/node_modules/@formatjs/intl-localematcher": {
+ "version": "0.2.25",
+ "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.25.tgz",
+ "integrity": "sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@formatjs/intl-localematcher": {
+ "version": "0.2.32",
+ "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.32.tgz",
+ "integrity": "sha512-k/MEBstff4sttohyEpXxCmC3MqbUn9VvHGlZ8fauLzkbwXmVrEeyzS+4uhrvAk9DWU9/7otYWxyDox4nT/KVLQ==",
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.14",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
@@ -4814,9 +4901,9 @@
}
},
"node_modules/@next/env": {
- "version": "13.5.6",
- "resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.6.tgz",
- "integrity": "sha512-Yac/bV5sBGkkEXmAX5FWPS9Mmo2rthrOPRQQNfycJPkjUAUclomCPH7QFVCDQ4Mp2k2K1SSM6m0zrxYrOwtFQw=="
+ "version": "14.1.0",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-14.1.0.tgz",
+ "integrity": "sha512-Py8zIo+02ht82brwwhTg36iogzFqGLPXlRGKQw5s+qP/kMNc4MAyDeEwBKDijk6zTIbegEgu8Qy7C1LboslQAw=="
},
"node_modules/@next/eslint-plugin-next": {
"version": "13.5.6",
@@ -4848,9 +4935,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
- "version": "13.5.6",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.6.tgz",
- "integrity": "sha512-5nvXMzKtZfvcu4BhtV0KH1oGv4XEW+B+jOfmBdpFI3C7FrB/MfujRpWYSBBO64+qbW8pkZiSyQv9eiwnn5VIQA==",
+ "version": "14.1.0",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.1.0.tgz",
+ "integrity": "sha512-nUDn7TOGcIeyQni6lZHfzNoo9S0euXnu0jhsbMOmMJUBfgsnESdjN97kM7cBqQxZa8L/bM9om/S5/1dzCrW6wQ==",
"cpu": [
"arm64"
],
@@ -4863,9 +4950,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
- "version": "13.5.6",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.6.tgz",
- "integrity": "sha512-6cgBfxg98oOCSr4BckWjLLgiVwlL3vlLj8hXg2b+nDgm4bC/qVXXLfpLB9FHdoDu4057hzywbxKvmYGmi7yUzA==",
+ "version": "14.1.0",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.1.0.tgz",
+ "integrity": "sha512-1jgudN5haWxiAl3O1ljUS2GfupPmcftu2RYJqZiMJmmbBT5M1XDffjUtRUzP4W3cBHsrvkfOFdQ71hAreNQP6g==",
"cpu": [
"x64"
],
@@ -4878,9 +4965,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
- "version": "13.5.6",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.6.tgz",
- "integrity": "sha512-txagBbj1e1w47YQjcKgSU4rRVQ7uF29YpnlHV5xuVUsgCUf2FmyfJ3CPjZUvpIeXCJAoMCFAoGnbtX86BK7+sg==",
+ "version": "14.1.0",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.1.0.tgz",
+ "integrity": "sha512-RHo7Tcj+jllXUbK7xk2NyIDod3YcCPDZxj1WLIYxd709BQ7WuRYl3OWUNG+WUfqeQBds6kvZYlc42NJJTNi4tQ==",
"cpu": [
"arm64"
],
@@ -4893,9 +4980,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
- "version": "13.5.6",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.6.tgz",
- "integrity": "sha512-cGd+H8amifT86ZldVJtAKDxUqeFyLWW+v2NlBULnLAdWsiuuN8TuhVBt8ZNpCqcAuoruoSWynvMWixTFcroq+Q==",
+ "version": "14.1.0",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.1.0.tgz",
+ "integrity": "sha512-v6kP8sHYxjO8RwHmWMJSq7VZP2nYCkRVQ0qolh2l6xroe9QjbgV8siTbduED4u0hlk0+tjS6/Tuy4n5XCp+l6g==",
"cpu": [
"arm64"
],
@@ -4908,9 +4995,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
- "version": "13.5.6",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.6.tgz",
- "integrity": "sha512-Mc2b4xiIWKXIhBy2NBTwOxGD3nHLmq4keFk+d4/WL5fMsB8XdJRdtUlL87SqVCTSaf1BRuQQf1HvXZcy+rq3Nw==",
+ "version": "14.1.0",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.1.0.tgz",
+ "integrity": "sha512-zJ2pnoFYB1F4vmEVlb/eSe+VH679zT1VdXlZKX+pE66grOgjmKJHKacf82g/sWE4MQ4Rk2FMBCRnX+l6/TVYzQ==",
"cpu": [
"x64"
],
@@ -4923,9 +5010,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
- "version": "13.5.6",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.6.tgz",
- "integrity": "sha512-CFHvP9Qz98NruJiUnCe61O6GveKKHpJLloXbDSWRhqhkJdZD2zU5hG+gtVJR//tyW897izuHpM6Gtf6+sNgJPQ==",
+ "version": "14.1.0",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.1.0.tgz",
+ "integrity": "sha512-rbaIYFt2X9YZBSbH/CwGAjbBG2/MrACCVu2X0+kSykHzHnYH5FjHxwXLkcoJ10cX0aWCEynpu+rP76x0914atg==",
"cpu": [
"x64"
],
@@ -4938,9 +5025,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
- "version": "13.5.6",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.6.tgz",
- "integrity": "sha512-aFv1ejfkbS7PUa1qVPwzDHjQWQtknzAZWGTKYIAaS4NMtBlk3VyA6AYn593pqNanlicewqyl2jUhQAaFV/qXsg==",
+ "version": "14.1.0",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.1.0.tgz",
+ "integrity": "sha512-o1N5TsYc8f/HpGt39OUQpQ9AKIGApd3QLueu7hXk//2xq5Z9OxmV6sQfNp8C7qYmiOlHYODOGqNNa0e9jvchGQ==",
"cpu": [
"arm64"
],
@@ -4953,9 +5040,9 @@
}
},
"node_modules/@next/swc-win32-ia32-msvc": {
- "version": "13.5.6",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.6.tgz",
- "integrity": "sha512-XqqpHgEIlBHvzwG8sp/JXMFkLAfGLqkbVsyN+/Ih1mR8INb6YCc2x/Mbwi6hsAgUnqQztz8cvEbHJUbSl7RHDg==",
+ "version": "14.1.0",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.1.0.tgz",
+ "integrity": "sha512-XXIuB1DBRCFwNO6EEzCTMHT5pauwaSj4SWs7CYnME57eaReAKBXCnkUE80p/pAZcewm7hs+vGvNqDPacEXHVkw==",
"cpu": [
"ia32"
],
@@ -4968,9 +5055,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
- "version": "13.5.6",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.6.tgz",
- "integrity": "sha512-Cqfe1YmOS7k+5mGu92nl5ULkzpKuxJrP3+4AEuPmrpFZ3BHxTY3TnHmU1On3bFmFFs6FbTcdF58CCUProGpIGQ==",
+ "version": "14.1.0",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.1.0.tgz",
+ "integrity": "sha512-9WEbVRRAqJ3YFVqEZIxUqkiO8l1nool1LmNxygr5HWF8AcSYsEpneUDhmjUVJEzO2A04+oPtZdombzzPPkTtgg==",
"cpu": [
"x64"
],
@@ -7645,14 +7732,14 @@
}
},
"node_modules/@trussworks/react-uswds": {
- "version": "5.5.0",
- "resolved": "https://registry.npmjs.org/@trussworks/react-uswds/-/react-uswds-5.5.0.tgz",
- "integrity": "sha512-PZRklIYWIIXI+hTw2XILPHKictZXwnPaSZf9L1mRTmfj1IncktPPpCCigwfIxIG6MSMzZYXx/TOAwjb8n4sjhw==",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/@trussworks/react-uswds/-/react-uswds-7.0.0.tgz",
+ "integrity": "sha512-N3O0BRuhyLoZKjYM0Mq+XIXJ3ShvCEh6uhqwRstIde6E9m0Au6JLy1YEv9A8eapBxDvyqZW3jiVTGzW/JqS+eA==",
"engines": {
"node": ">= 16.20.0"
},
"peerDependencies": {
- "@uswds/uswds": "^3.6.0",
+ "@uswds/uswds": "^3.7.1",
"react": "^16.x || ^17.x || ^18.x",
"react-dom": "^16.x || ^17.x || ^18.x"
}
@@ -13966,7 +14053,8 @@
"node_modules/glob-to-regexp": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
- "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="
+ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
+ "dev": true
},
"node_modules/glob/node_modules/brace-expansion": {
"version": "2.0.1",
@@ -14748,6 +14836,34 @@
"node": ">= 0.4"
}
},
+ "node_modules/intl-messageformat": {
+ "version": "9.13.0",
+ "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-9.13.0.tgz",
+ "integrity": "sha512-7sGC7QnSQGa5LZP7bXLDhVDtQOeKGeBFGHF2Y8LVBwYZoQZCgWeKoPGTa5GMG8g/TzDgeXuYJQis7Ggiw2xTOw==",
+ "dependencies": {
+ "@formatjs/ecma402-abstract": "1.11.4",
+ "@formatjs/fast-memoize": "1.2.1",
+ "@formatjs/icu-messageformat-parser": "2.1.0",
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/intl-messageformat/node_modules/@formatjs/ecma402-abstract": {
+ "version": "1.11.4",
+ "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz",
+ "integrity": "sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==",
+ "dependencies": {
+ "@formatjs/intl-localematcher": "0.2.25",
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/intl-messageformat/node_modules/@formatjs/intl-localematcher": {
+ "version": "0.2.25",
+ "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.25.tgz",
+ "integrity": "sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
"node_modules/invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
@@ -18244,7 +18360,6 @@
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
- "dev": true,
"engines": {
"node": ">= 0.6"
}
@@ -18256,34 +18371,34 @@
"dev": true
},
"node_modules/next": {
- "version": "13.5.6",
- "resolved": "https://registry.npmjs.org/next/-/next-13.5.6.tgz",
- "integrity": "sha512-Y2wTcTbO4WwEsVb4A8VSnOsG1I9ok+h74q0ZdxkwM3EODqrs4pasq7O0iUxbcS9VtWMicG7f3+HAj0r1+NtKSw==",
+ "version": "14.1.0",
+ "resolved": "https://registry.npmjs.org/next/-/next-14.1.0.tgz",
+ "integrity": "sha512-wlzrsbfeSU48YQBjZhDzOwhWhGsy+uQycR8bHAOt1LY1bn3zZEcDyHQOEoN3aWzQ8LHCAJ1nqrWCc9XF2+O45Q==",
"dependencies": {
- "@next/env": "13.5.6",
+ "@next/env": "14.1.0",
"@swc/helpers": "0.5.2",
"busboy": "1.6.0",
- "caniuse-lite": "^1.0.30001406",
+ "caniuse-lite": "^1.0.30001579",
+ "graceful-fs": "^4.2.11",
"postcss": "8.4.31",
- "styled-jsx": "5.1.1",
- "watchpack": "2.4.0"
+ "styled-jsx": "5.1.1"
},
"bin": {
"next": "dist/bin/next"
},
"engines": {
- "node": ">=16.14.0"
+ "node": ">=18.17.0"
},
"optionalDependencies": {
- "@next/swc-darwin-arm64": "13.5.6",
- "@next/swc-darwin-x64": "13.5.6",
- "@next/swc-linux-arm64-gnu": "13.5.6",
- "@next/swc-linux-arm64-musl": "13.5.6",
- "@next/swc-linux-x64-gnu": "13.5.6",
- "@next/swc-linux-x64-musl": "13.5.6",
- "@next/swc-win32-arm64-msvc": "13.5.6",
- "@next/swc-win32-ia32-msvc": "13.5.6",
- "@next/swc-win32-x64-msvc": "13.5.6"
+ "@next/swc-darwin-arm64": "14.1.0",
+ "@next/swc-darwin-x64": "14.1.0",
+ "@next/swc-linux-arm64-gnu": "14.1.0",
+ "@next/swc-linux-arm64-musl": "14.1.0",
+ "@next/swc-linux-x64-gnu": "14.1.0",
+ "@next/swc-linux-x64-musl": "14.1.0",
+ "@next/swc-win32-arm64-msvc": "14.1.0",
+ "@next/swc-win32-ia32-msvc": "14.1.0",
+ "@next/swc-win32-x64-msvc": "14.1.0"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",
@@ -18335,6 +18450,26 @@
"react-i18next": ">= 13.5.0"
}
},
+ "node_modules/next-intl": {
+ "version": "3.9.1",
+ "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-3.9.1.tgz",
+ "integrity": "sha512-1j+5lLTY5kHshqnVoeAep+gQO1xX1KlJU3irZkdCOhr0woo562qLKOsGGVh+7bB5luBMu9qQBQy7ZwNK81Z2Ig==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/amannn"
+ }
+ ],
+ "dependencies": {
+ "@formatjs/intl-localematcher": "^0.2.32",
+ "negotiator": "^0.6.3",
+ "use-intl": "^3.9.1"
+ },
+ "peerDependencies": {
+ "next": "^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/next/node_modules/postcss": {
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
@@ -23540,6 +23675,18 @@
}
}
},
+ "node_modules/use-intl": {
+ "version": "3.9.1",
+ "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-3.9.1.tgz",
+ "integrity": "sha512-tWcT636/jYC0hILyFTLmiuE+ovbvPPBXwN/OOQxxE+4bssHXeeksdWXzpDsKqE37aV62DFrQ2jhumV8udecjNA==",
+ "dependencies": {
+ "@formatjs/ecma402-abstract": "^1.11.4",
+ "intl-messageformat": "^9.3.18"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/use-resize-observer": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/use-resize-observer/-/use-resize-observer-9.1.0.tgz",
@@ -23694,6 +23841,7 @@
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
"integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==",
+ "dev": true,
"dependencies": {
"glob-to-regexp": "^0.4.1",
"graceful-fs": "^4.1.2"
diff --git a/frontend/package.json b/frontend/package.json
index 9e7c73b63..f6b30937e 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -23,13 +23,14 @@
},
"dependencies": {
"@next/third-parties": "^14.0.4",
- "@trussworks/react-uswds": "^5.0.0",
+ "@trussworks/react-uswds": "^7.0.0",
"@uswds/uswds": "^3.6.0",
"i18next": "^23.0.0",
"js-cookie": "^3.0.5",
"lodash": "^4.17.21",
- "next": "^13.5.2",
+ "next": "^14.0.3",
"next-i18next": "^15.0.0",
+ "next-intl": "^3.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^14.0.0",
diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx
new file mode 100644
index 000000000..ea54f234b
--- /dev/null
+++ b/frontend/src/app/layout.tsx
@@ -0,0 +1,35 @@
+import "src/styles/styles.scss";
+
+import Layout from "src/components/AppLayout";
+/**
+ * Root layout component, wraps all pages.
+ * @see https://nextjs.org/docs/app/api-reference/file-conventions/layout
+ */
+import { Metadata } from "next";
+
+export const metadata: Metadata = {
+ icons: [`${process.env.NEXT_PUBLIC_BASE_PATH ?? ""}/img/favicon.ico`],
+};
+
+interface LayoutProps {
+ children: React.ReactNode;
+
+ // TODO: use for i18n when ready
+ // params: {
+ // locale: string;
+ // };
+}
+
+export default function RootLayout({ children }: LayoutProps) {
+ return (
+
+
+ {/* Separate layout component for the inner-body UI elements since Storybook
+ and tests trip over the fact that this file renders an tag */}
+
+ {/* TODO: Add locale="english" prop when ready for i18n */}
+ {children}
+
+
+ );
+}
diff --git a/frontend/src/app/search/page.tsx b/frontend/src/app/search/page.tsx
new file mode 100644
index 000000000..7b99d4573
--- /dev/null
+++ b/frontend/src/app/search/page.tsx
@@ -0,0 +1,60 @@
+"use client";
+
+import React, { useState } from "react";
+import {
+ SearchFetcher,
+ fetchSearchOpportunities,
+} from "../../services/searchfetcher/SearchFetcher";
+
+import { APISearchFetcher } from "../../services/searchfetcher/APISearchFetcher";
+import { MockSearchFetcher } from "../../services/searchfetcher/MockSearchFetcher";
+import { Opportunity } from "../../types/searchTypes";
+import PageNotFound from "../../pages/404";
+import { useFeatureFlags } from "src/hooks/useFeatureFlags";
+
+const useMockData = true;
+const searchFetcher: SearchFetcher = useMockData
+ ? new MockSearchFetcher()
+ : new APISearchFetcher();
+
+// TODO: use for i18n when ready
+// interface RouteParams {
+// locale: string;
+// }
+
+// interface SearchProps {
+// initialOpportunities: Opportunity[];
+// }
+
+export default function Search() {
+ const { featureFlagsManager, mounted } = useFeatureFlags();
+ const [searchResults, setSearchResults] = useState([]);
+
+ const handleButtonClick = (event: React.MouseEvent) => {
+ event.preventDefault();
+ performSearch().catch((e) => console.log(e));
+ };
+
+ const performSearch = async () => {
+ const opportunities = await fetchSearchOpportunities(searchFetcher);
+ setSearchResults(opportunities);
+ };
+
+ if (!mounted) return null;
+ if (!featureFlagsManager.isFeatureEnabled("showSearchV0")) {
+ return ;
+ }
+
+ return (
+ <>
+
+
+ {searchResults.map((opportunity) => (
+ -
+ {opportunity.id}, {opportunity.title}
+
+ ))}
+
+ >
+ );
+}
diff --git a/frontend/src/components/AppLayout.tsx b/frontend/src/components/AppLayout.tsx
new file mode 100644
index 000000000..0096ec643
--- /dev/null
+++ b/frontend/src/components/AppLayout.tsx
@@ -0,0 +1,26 @@
+import Footer from "./Footer";
+import GrantsIdentifier from "./GrantsIdentifier";
+import Header from "./Header";
+
+type Props = {
+ children: React.ReactNode;
+ // TODO: pass locale into Layout when we setup i18n
+ // locale?: string;
+};
+
+const Layout = ({ children }: Props) => {
+ return (
+ // Stick the footer to the bottom of the page
+
+ );
+};
+
+export default Layout;
diff --git a/frontend/src/components/Footer.tsx b/frontend/src/components/Footer.tsx
index 3adf13bee..c4cb7e163 100644
--- a/frontend/src/components/Footer.tsx
+++ b/frontend/src/components/Footer.tsx
@@ -1,3 +1,5 @@
+"use client";
+
import { ExternalRoutes } from "src/constants/routes";
import { assetPath } from "src/utils/assetPath";
diff --git a/frontend/src/components/GrantsIdentifier.tsx b/frontend/src/components/GrantsIdentifier.tsx
index 75c015092..8fb07f04f 100644
--- a/frontend/src/components/GrantsIdentifier.tsx
+++ b/frontend/src/components/GrantsIdentifier.tsx
@@ -1,3 +1,5 @@
+"use client";
+
import { ExternalRoutes } from "src/constants/routes";
import { Trans, useTranslation } from "next-i18next";
diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx
index 01beeff41..28c6887fa 100644
--- a/frontend/src/components/Header.tsx
+++ b/frontend/src/components/Header.tsx
@@ -1,3 +1,5 @@
+"use client";
+
import { assetPath } from "src/utils/assetPath";
import { useTranslation } from "next-i18next";
diff --git a/frontend/src/pages/search.tsx b/frontend/src/pages/search.tsx
deleted file mode 100644
index f18f16f3e..000000000
--- a/frontend/src/pages/search.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-import type { GetStaticProps, NextPage } from "next";
-import { useFeatureFlags } from "src/hooks/useFeatureFlags";
-
-import { serverSideTranslations } from "next-i18next/serverSideTranslations";
-import React, { useState } from "react";
-
-import { APISearchFetcher } from "../services/searchfetcher/APISearchFetcher";
-import { MockSearchFetcher } from "../services/searchfetcher/MockSearchFetcher";
-import {
- fetchSearchOpportunities,
- SearchFetcher,
-} from "../services/searchfetcher/SearchFetcher";
-import { Opportunity } from "../types/searchTypes";
-import PageNotFound from "./404";
-
-const useMockData = true;
-const searchFetcher: SearchFetcher = useMockData
- ? new MockSearchFetcher()
- : new APISearchFetcher();
-
-interface SearchProps {
- initialOpportunities: Opportunity[];
-}
-
-const Search: NextPage = ({ initialOpportunities = [] }) => {
- const { featureFlagsManager, mounted } = useFeatureFlags();
- const [searchResults, setSearchResults] =
- useState(initialOpportunities);
-
- const handleButtonClick = (event: React.MouseEvent) => {
- event.preventDefault();
- performSearch().catch((e) => console.log(e));
- };
-
- const performSearch = async () => {
- const opportunities = await fetchSearchOpportunities(searchFetcher);
- setSearchResults(opportunities);
- };
-
- if (!mounted) return null;
- if (!featureFlagsManager.isFeatureEnabled("showSearchV0")) {
- return ;
- }
-
- return (
- <>
-
-
- {searchResults.map((opportunity) => (
- -
- {opportunity.id}, {opportunity.title}
-
- ))}
-
- >
- );
-};
-
-export const getStaticProps: GetStaticProps = async ({ locale }) => {
- // Always pre-render the initial search results
- // TODO (1189): If the URL has query params - they will need to be included in the search here
- const initialOpportunities: Opportunity[] =
- await fetchSearchOpportunities(searchFetcher);
- const translations = await serverSideTranslations(locale ?? "en");
-
- return {
- props: {
- initialOpportunities,
- ...translations,
- },
- };
-};
-
-export default Search;
diff --git a/frontend/stories/pages/search.stories.tsx b/frontend/stories/pages/search.stories.tsx
index 04ed8b08b..e278ce84e 100644
--- a/frontend/stories/pages/search.stories.tsx
+++ b/frontend/stories/pages/search.stories.tsx
@@ -1,5 +1,5 @@
import { Meta } from "@storybook/react";
-import Search from "src/pages/search";
+import Search from "../../src/app/search/page";
const meta: Meta = {
title: "Pages/Search",
diff --git a/frontend/tests/components/AppLayout.test.tsx b/frontend/tests/components/AppLayout.test.tsx
new file mode 100644
index 000000000..0d7279217
--- /dev/null
+++ b/frontend/tests/components/AppLayout.test.tsx
@@ -0,0 +1,29 @@
+import { render, screen } from "@testing-library/react";
+import { axe } from "jest-axe";
+
+import AppLayout from "src/components/AppLayout";
+
+describe("AppLayout", () => {
+ it("renders children in main section", () => {
+ render(
+
+ child
+ ,
+ );
+
+ const header = screen.getByRole("heading", { name: /child/i, level: 1 });
+
+ expect(header).toBeInTheDocument();
+ });
+
+ it("passes accessibility scan", async () => {
+ const { container } = render(
+
+ child
+ ,
+ );
+ const results = await axe(container);
+
+ expect(results).toHaveNoViolations();
+ });
+});
diff --git a/frontend/tests/components/Header.test.tsx b/frontend/tests/components/Header.test.tsx
index c902f8ddc..8e3abbc49 100644
--- a/frontend/tests/components/Header.test.tsx
+++ b/frontend/tests/components/Header.test.tsx
@@ -20,8 +20,7 @@ const props = {
describe("Header", () => {
it("toggles the mobile nav menu", async () => {
render();
-
- const menuButton = screen.getByRole("button", { name: /menu/i });
+ const menuButton = screen.getByTestId("navMenuButton");
expect(menuButton).toBeInTheDocument();
expect(screen.getByRole("link", { name: /home/i })).toHaveAttribute(
diff --git a/frontend/tests/pages/search.test.tsx b/frontend/tests/pages/search.test.tsx
index 19519de9d..c8ccd7d1d 100644
--- a/frontend/tests/pages/search.test.tsx
+++ b/frontend/tests/pages/search.test.tsx
@@ -1,9 +1,8 @@
-import { render, screen, waitFor } from "@testing-library/react";
+import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { axe } from "jest-axe";
import { useFeatureFlags } from "src/hooks/useFeatureFlags";
-import Search from "src/pages/search";
-import { MOCKOPPORTUNITIES } from "../../src/services/searchfetcher/MockSearchFetcher";
+import Search from "../../src/app/search/page";
jest.mock("src/hooks/useFeatureFlags");
@@ -21,28 +20,33 @@ const setFeatureFlag = (flag: string, value: boolean) => {
describe("Search", () => {
it("passes accessibility scan", async () => {
setFeatureFlag("showSearchV0", true);
- const { container } = render(
- ,
- );
+ const { container } = render();
const results = await waitFor(() => axe(container));
expect(results).toHaveNoViolations();
});
describe("Search feature flag", () => {
- it("renders search results when feature flag is on", () => {
+ it("renders search results when feature flag is on", async () => {
setFeatureFlag("showSearchV0", true);
- render();
- expect(screen.getByText(/sunt aut/i)).toBeInTheDocument();
+ render();
+ fireEvent.click(screen.getByRole("button", { name: /update results/i }));
+
+ await waitFor(() => {
+ expect(screen.getByText(/sunt aut/i)).toBeInTheDocument();
+ });
});
- it("renders PageNotFound when feature flag is off", () => {
+ it("renders PageNotFound when feature flag is off", async () => {
setFeatureFlag("showSearchV0", false);
- render();
- expect(
- screen.getByText(
- /The page you have requested cannot be displayed because it does not exist, has been moved/i,
- ),
- ).toBeInTheDocument();
+ render();
+
+ await waitFor(() => {
+ expect(
+ screen.getByText(
+ /The page you have requested cannot be displayed because it does not exist, has been moved/i,
+ ),
+ ).toBeInTheDocument();
+ });
});
});
});
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
index d3dc67260..dc547387b 100644
--- a/frontend/tsconfig.json
+++ b/frontend/tsconfig.json
@@ -24,8 +24,19 @@
"@next/third-parties/google": [
"./node_modules/@next/third-parties/dist/google"
]
- }
+ },
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ]
},
- "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "__mocks__/styleMock.js"],
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ "__mocks__/styleMock.js",
+ ".next/types/**/*.ts"
+ ],
"exclude": ["node_modules"]
}