From 2572fe00077df2ba7e98448751698729d2c2e5f2 Mon Sep 17 00:00:00 2001
From: Aaron Couch
Date: Thu, 23 May 2024 16:26:15 -0400
Subject: [PATCH 1/8] Move Pages to App Router (#7)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Summary
Fixes #6
### Time to review: __60 mins__
## Changes proposed
### Move pages from page to app router:
1. Move all pages to
[`[locale]`](https://next-intl-docs.vercel.app/docs/getting-started/app-router/with-i18n-routing#getting-started)
folder
2. Add
[`generateMetata()`](https://nextjs.org/docs/app/api-reference/functions/generate-metadata#generatemetadata-function)
function and [next-intl
`getTranslations()`](https://next-intl-docs.vercel.app/docs/environments/metadata-route-handlers#metadata-api)
implementation
* @rylew1 commented we could remove this from each page. To do that we
could use [prop
arguments](https://nextjs.org/docs/app/api-reference/functions/generate-metadata#with-segment-props)
and update the based on the param. There is also more we can do with the
metadata to properly add [app links and twitter
cards](https://nextjs.org/docs/app/api-reference/functions/generate-metadata#applinks).
TODO: create ticket
4. Replace i18n's `useTranslation` with next-intl's `useTranslations`
5. Remove hard-coded strings that were present b/c we were still b/w
i18next and next-intl
#### Changes
* [Move process page to
app](https://github.com/navapbc/simpler-grants-gov/pull/7/commits/32ba4ee29365444aa3260237912c340def137ad2)
* [Move research page to
app](https://github.com/navapbc/simpler-grants-gov/pull/7/commits/5b5ad1a5ecb21f82378c5d9eaf58bcdd246aa8fc)
* [Move health page to
app](https://github.com/navapbc/simpler-grants-gov/pull/7/commits/a3e62551644aa85fdef5921e89123cad66b4ec1c)
* [Move feature flag page to
app](https://github.com/navapbc/simpler-grants-gov/pull/7/commits/395baed01983914f9ba04242abf6e379c4695216)
* [Move search page to app
router](https://github.com/navapbc/simpler-grants-gov/pull/7/commits/1e261e3d9723d83a34492e05facc2a04b96d19d8)
* [Move newsletter pages to app
router](https://github.com/navapbc/simpler-grants-gov/pull/7/commits/b509ef8a1744ae51f96de17fad266b6baa566962)
* [Move home page to app
router](https://github.com/navapbc/simpler-grants-gov/pull/7/commits/de1be98ac22e68266bc61bd619821a2da1045c36)
* [Move home page to app
router](https://github.com/navapbc/simpler-grants-gov/pull/7/commits/74077aeb617650fb3eefb23a237fa3f95814ab5d)
* [Move 404 page to app
router](https://github.com/navapbc/simpler-grants-gov/pull/7/commits/ccbc9563ba63647db7fb1ee8365f166396014e9d)
### Misc
1. [Delete hello
api](https://github.com/navapbc/simpler-grants-gov/pull/7/commits/5bad6ea9c5656515bed20a215c8751b825214368)
* This was left from the project creation
2. [Add USWDS icon
component](https://github.com/navapbc/simpler-grants-gov/pull/7/commits/0120c7bd15f5ec4f8f36ab7249c3c802c1b47c34)
* as noted in a slack discussion, when trying to access [one of the
icons](https://github.com/trussworks/react-uswds/blob/main/src/components/Icon/Icons.ts)
using `` next errors: `You cannot dot into a client module
from a server component. You can only pass the imported name through`.
I'm not sure why it thinks the Icon component is a client module. [Dan
A.
suggests](https://github.com/vercel/next.js/issues/51593#issuecomment-1748001262)
trussworks should re-export as named exports. I tried importing the SVGs
directly from the trussworks library but [svgr requires a custom webpack
config](https://react-svgr.com/docs/next/) which is a road I didn't want
to go down and [react svg](https://www.npmjs.com/package/react-svg)
through an error in the app router 😥 .
* I implemented @sawyerh 's
[suggestion](https://github.com/navapbc/simpler-grants-gov/pull/7/commits/0120c7bd15f5ec4f8f36ab7249c3c802c1b47c34#diff-dadb35bd2f3f61f2c179f033cd0a2874fc343974236f2fb8613664703c751429),
which did not work initially b/c next reported the USWDS icon was
corrupt, which was fixed by adding a `viewBox` to the svg element 😮💨 .
* [Remove unused
WtGIContent](https://github.com/navapbc/simpler-grants-gov/pull/7/commits/75490f73af2d2c2ec3705c2a02d2d27692316fc6)
### Layout and component updates
* [Move layout and update for app
router](https://github.com/navapbc/simpler-grants-gov/pull/7/commits/af112fd25935549048c9941be7c160b28c01ae7f)
* [Update global components for the app
router](https://github.com/navapbc/simpler-grants-gov/pull/7/commits/40119e66c99b2c4047967111cf06e1f904e6eab6)
### Remaining next-intl config and removal of
* [Move i18n strings for app
router](https://github.com/navapbc/simpler-grants-gov/pull/7/commits/eb3c07c82f4dfe2845f9532943c3ef71b1789ff0)
* [Adds next-intl config and removes
i18n](https://github.com/navapbc/simpler-grants-gov/pull/7/commits/c546571fdc5443e15cbcecc48db2ef33fcad77c0)
* [Update tests for app
router](https://github.com/navapbc/simpler-grants-gov/pull/7/commits/3b9b1931bb36fa9795a38e0898c9f40ad8deb4af)
* [Removes i18next and next-i18n
packages](https://github.com/navapbc/simpler-grants-gov/pull/7/commits/9d2e08ad44f21b0f8b62a60cc66fdda7843314b2)
* [Update storybook settings for app
router](https://github.com/navapbc/simpler-grants-gov/pull/7/commits/39f115d6eb8f57d8aefbe12002b12c01d1dad2c4)
---
frontend/.storybook/I18nStoryWrapper.tsx | 30 ++
frontend/.storybook/i18next.js | 22 --
frontend/.storybook/main.js | 6 +-
frontend/.storybook/preview.js | 43 ---
frontend/.storybook/preview.tsx | 64 ++++
frontend/jest.config.js | 5 +-
frontend/next-i18next.config.js | 52 ----
frontend/next.config.js | 8 +-
frontend/package-lock.json | 92 ++----
frontend/package.json | 3 -
frontend/public/img/uswds-sprite.svg | 1 +
frontend/public/locales/en/common.json | 294 ------------------
frontend/public/locales/es/common.json | 8 -
.../dev/feature-flags/FeatureFlagsTable.tsx | 61 ++++
.../app/[locale]/dev/feature-flags/page.tsx | 30 ++
frontend/src/app/[locale]/health/page.tsx | 3 +
.../[locale]/newsletter/NewsletterForm.tsx | 217 +++++++++++++
.../[locale]/newsletter/confirmation/page.tsx | 66 ++++
frontend/src/app/[locale]/newsletter/page.tsx | 71 +++++
.../[locale]/newsletter/unsubscribe/page.tsx | 69 ++++
frontend/src/app/[locale]/page.tsx | 31 ++
.../src/app/[locale]/process/ProcessIntro.tsx | 51 +++
.../app/[locale]/process/ProcessInvolved.tsx | 67 ++++
.../[locale]/process}/ProcessMilestones.tsx | 147 ++++-----
frontend/src/app/[locale]/process/page.tsx | 38 +++
.../[locale]/research}/ResearchArchetypes.tsx | 13 +-
.../app/[locale]/research/ResearchImpact.tsx | 80 +++++
.../[locale]/research}/ResearchIntro.tsx | 4 +-
.../research}/ResearchMethodology.tsx | 48 ++-
.../[locale]/research}/ResearchThemes.tsx | 4 +-
frontend/src/app/[locale]/research/page.tsx | 41 +++
.../app/{ => [locale]}/search/SearchForm.tsx | 22 +-
.../src/app/{ => [locale]}/search/actions.ts | 6 +-
.../src/app/{ => [locale]}/search/error.tsx | 2 +-
.../src/app/{ => [locale]}/search/loading.tsx | 2 +-
.../src/app/{ => [locale]}/search/page.tsx | 15 +-
frontend/src/app/layout.tsx | 4 +-
frontend/src/app/not-found.tsx | 15 +-
frontend/src/app/template.tsx | 2 +-
frontend/src/components/AppBetaAlert.tsx | 23 --
frontend/src/components/AppLayout.tsx | 59 ----
frontend/src/components/BetaAlert.tsx | 47 +--
frontend/src/components/Footer.tsx | 88 +++---
frontend/src/components/GrantsIdentifier.tsx | 58 ++--
frontend/src/components/Header.tsx | 38 +--
frontend/src/components/Hero.tsx | 13 +-
frontend/src/components/Layout.tsx | 64 ++--
frontend/src/components/USWDSIcon.tsx | 23 ++
.../content/FundingContent.tsx | 6 +-
.../content/IndexGoalContent.tsx | 15 +-
.../content/ProcessAndResearchContent.tsx | 19 +-
.../components/search/SearchResultsList.tsx | 2 +-
frontend/src/hooks/useSearchFormState.ts | 2 +-
frontend/src/i18n/messages/en/index.ts | 9 +-
frontend/src/middleware.ts | 2 +-
frontend/src/pages/404.tsx | 39 ---
frontend/src/pages/_app.tsx | 28 --
frontend/src/pages/api/hello.ts | 13 -
frontend/src/pages/content/ProcessIntro.tsx | 58 ----
.../src/pages/content/ProcessInvolved.tsx | 70 -----
frontend/src/pages/content/ResearchImpact.tsx | 83 -----
frontend/src/pages/content/WtGIContent.tsx | 90 ------
frontend/src/pages/dev/feature-flags.tsx | 87 ------
frontend/src/pages/health.tsx | 15 -
frontend/src/pages/index.tsx | 40 ---
.../src/pages/newsletter/confirmation.tsx | 71 -----
frontend/src/pages/newsletter/index.tsx | 276 ----------------
frontend/src/pages/newsletter/unsubscribe.tsx | 78 -----
frontend/src/pages/process.tsx | 45 ---
frontend/src/pages/research.tsx | 49 ---
.../components/FundingContent.stories.tsx | 2 +-
.../components/GoalContent.stories.tsx | 2 +-
.../components/ProcessContent.stories.tsx | 2 +-
.../components/ReaserchImpact.stories.tsx | 2 +-
.../components/ReaserchIntro.stories.tsx | 2 +-
.../components/ReaserchThemes.stories.tsx | 2 +-
.../components/ResearchArchetypes.stories.tsx | 2 +-
.../ResearchMethodology.stories.tsx | 2 +-
.../components/WtGIContent.stories.tsx | 17 -
frontend/stories/pages/404.stories.tsx | 2 +-
frontend/stories/pages/Index.stories.tsx | 2 +-
frontend/stories/pages/process.stories.tsx | 2 +-
frontend/stories/pages/research.stories.tsx | 2 +-
frontend/stories/pages/search.stories.tsx | 2 +-
frontend/tests/components/AppLayout.test.tsx | 29 --
frontend/tests/components/BetaAlert.test.tsx | 11 +-
frontend/tests/components/Footer.test.tsx | 20 +-
.../tests/components/FullWidthAlert.test.tsx | 2 +-
.../tests/components/FundingContent.test.tsx | 4 +-
.../tests/components/GoalContent.test.tsx | 4 +-
.../components/GrantsIdentifier.test.tsx | 19 +-
frontend/tests/components/Header.test.tsx | 14 +-
frontend/tests/components/Hero.test.tsx | 2 +-
frontend/tests/components/Layout.test.tsx | 10 +-
.../ProcessAndResearchContent.test.tsx | 4 +-
.../tests/components/ProcessIntro.test.tsx | 4 +-
.../tests/components/ProcessInvolved.test.tsx | 4 +-
.../components/ProcessMilestones.test.tsx | 4 +-
.../components/ResearchArchetypes.test.tsx | 4 +-
.../tests/components/ResearchImpact.test.tsx | 4 +-
.../tests/components/ResearchIntro.test.tsx | 4 +-
.../components/ResearchMethodology.test.tsx | 4 +-
.../tests/components/ResearchThemes.test.tsx | 4 +-
frontend/tests/components/USWDSIcon.test.tsx | 12 +
.../tests/components/WtGIContent.test.tsx | 10 -
frontend/tests/e2e/404.spec.ts | 20 ++
frontend/tests/e2e/newsletter.spec.ts | 2 +-
frontend/tests/e2e/search/search.spec.ts | 2 +
frontend/tests/errors.test.ts | 2 +-
frontend/tests/jest-i18n.ts | 53 ----
frontend/tests/pages/404.test.tsx | 27 --
.../tests/pages/dev/feature-flags.test.tsx | 12 +-
frontend/tests/pages/index.test.tsx | 5 +-
.../pages/newsletter/confirmation.test.tsx | 4 +-
.../tests/pages/newsletter/index.test.tsx | 18 +-
.../pages/newsletter/unsubscribe.test.tsx | 4 +-
frontend/tests/pages/process.test.tsx | 5 +-
frontend/tests/pages/research.test.tsx | 5 +-
frontend/tests/playwright.config.ts | 1 +
frontend/tsconfig.json | 1 +
120 files changed, 1345 insertions(+), 2279 deletions(-)
create mode 100644 frontend/.storybook/I18nStoryWrapper.tsx
delete mode 100644 frontend/.storybook/i18next.js
delete mode 100644 frontend/.storybook/preview.js
create mode 100644 frontend/.storybook/preview.tsx
delete mode 100644 frontend/next-i18next.config.js
create mode 100644 frontend/public/img/uswds-sprite.svg
delete mode 100644 frontend/public/locales/en/common.json
delete mode 100644 frontend/public/locales/es/common.json
create mode 100644 frontend/src/app/[locale]/dev/feature-flags/FeatureFlagsTable.tsx
create mode 100644 frontend/src/app/[locale]/dev/feature-flags/page.tsx
create mode 100644 frontend/src/app/[locale]/health/page.tsx
create mode 100644 frontend/src/app/[locale]/newsletter/NewsletterForm.tsx
create mode 100644 frontend/src/app/[locale]/newsletter/confirmation/page.tsx
create mode 100644 frontend/src/app/[locale]/newsletter/page.tsx
create mode 100644 frontend/src/app/[locale]/newsletter/unsubscribe/page.tsx
create mode 100644 frontend/src/app/[locale]/page.tsx
create mode 100644 frontend/src/app/[locale]/process/ProcessIntro.tsx
create mode 100644 frontend/src/app/[locale]/process/ProcessInvolved.tsx
rename frontend/src/{pages/content => app/[locale]/process}/ProcessMilestones.tsx (50%)
create mode 100644 frontend/src/app/[locale]/process/page.tsx
rename frontend/src/{pages/content => app/[locale]/research}/ResearchArchetypes.tsx (91%)
create mode 100644 frontend/src/app/[locale]/research/ResearchImpact.tsx
rename frontend/src/{pages/content => app/[locale]/research}/ResearchIntro.tsx (81%)
rename frontend/src/{pages/content => app/[locale]/research}/ResearchMethodology.tsx (53%)
rename frontend/src/{pages/content => app/[locale]/research}/ResearchThemes.tsx (93%)
create mode 100644 frontend/src/app/[locale]/research/page.tsx
rename frontend/src/app/{ => [locale]}/search/SearchForm.tsx (80%)
rename frontend/src/app/{ => [locale]}/search/actions.ts (68%)
rename frontend/src/app/{ => [locale]}/search/error.tsx (98%)
rename frontend/src/app/{ => [locale]}/search/loading.tsx (88%)
rename frontend/src/app/{ => [locale]}/search/page.tsx (71%)
delete mode 100644 frontend/src/components/AppBetaAlert.tsx
delete mode 100644 frontend/src/components/AppLayout.tsx
create mode 100644 frontend/src/components/USWDSIcon.tsx
rename frontend/src/{pages => components}/content/FundingContent.tsx (93%)
rename frontend/src/{pages => components}/content/IndexGoalContent.tsx (80%)
rename frontend/src/{pages => components}/content/ProcessAndResearchContent.tsx (77%)
delete mode 100644 frontend/src/pages/404.tsx
delete mode 100644 frontend/src/pages/_app.tsx
delete mode 100644 frontend/src/pages/api/hello.ts
delete mode 100644 frontend/src/pages/content/ProcessIntro.tsx
delete mode 100644 frontend/src/pages/content/ProcessInvolved.tsx
delete mode 100644 frontend/src/pages/content/ResearchImpact.tsx
delete mode 100644 frontend/src/pages/content/WtGIContent.tsx
delete mode 100644 frontend/src/pages/dev/feature-flags.tsx
delete mode 100644 frontend/src/pages/health.tsx
delete mode 100644 frontend/src/pages/index.tsx
delete mode 100644 frontend/src/pages/newsletter/confirmation.tsx
delete mode 100644 frontend/src/pages/newsletter/index.tsx
delete mode 100644 frontend/src/pages/newsletter/unsubscribe.tsx
delete mode 100644 frontend/src/pages/process.tsx
delete mode 100644 frontend/src/pages/research.tsx
delete mode 100644 frontend/stories/components/WtGIContent.stories.tsx
delete mode 100644 frontend/tests/components/AppLayout.test.tsx
create mode 100644 frontend/tests/components/USWDSIcon.test.tsx
delete mode 100644 frontend/tests/components/WtGIContent.test.tsx
create mode 100644 frontend/tests/e2e/404.spec.ts
delete mode 100644 frontend/tests/jest-i18n.ts
delete mode 100644 frontend/tests/pages/404.test.tsx
diff --git a/frontend/.storybook/I18nStoryWrapper.tsx b/frontend/.storybook/I18nStoryWrapper.tsx
new file mode 100644
index 000000000..f05ca54a0
--- /dev/null
+++ b/frontend/.storybook/I18nStoryWrapper.tsx
@@ -0,0 +1,30 @@
+/**
+ * @file Storybook decorator, enabling internationalization for each story.
+ * @see https://storybook.js.org/docs/writing-stories/decorators
+ */
+import { StoryContext } from "@storybook/react";
+
+import { NextIntlClientProvider } from "next-intl";
+import React from "react";
+
+import { defaultLocale, formats, timeZone } from "../src/i18n/config";
+
+const I18nStoryWrapper = (
+ Story: React.ComponentType,
+ context: StoryContext,
+) => {
+ const locale = (context.globals.locale as string) ?? defaultLocale;
+
+ return (
+
+
+
+ );
+};
+
+export default I18nStoryWrapper;
diff --git a/frontend/.storybook/i18next.js b/frontend/.storybook/i18next.js
deleted file mode 100644
index a3eeb9d9c..000000000
--- a/frontend/.storybook/i18next.js
+++ /dev/null
@@ -1,22 +0,0 @@
-// Configure i18next for Storybook
-// See https://storybook.js.org/addons/storybook-react-i18next
-import i18nConfig from "../next-i18next.config";
-import i18next from "i18next";
-import LanguageDetector from "i18next-browser-languagedetector";
-import Backend from "i18next-http-backend";
-import { initReactI18next } from "react-i18next";
-
-i18next
- .use(initReactI18next)
- .use(LanguageDetector)
- .use(Backend)
- .init({
- ...i18nConfig,
- backend: {
- loadPath: `${
- process.env.NEXT_PUBLIC_BASE_PATH ?? ""
- }/locales/{{lng}}/{{ns}}.json`,
- },
- });
-
-export default i18next;
diff --git a/frontend/.storybook/main.js b/frontend/.storybook/main.js
index 6ef68f43b..a673aa99b 100644
--- a/frontend/.storybook/main.js
+++ b/frontend/.storybook/main.js
@@ -25,11 +25,7 @@ function blockSearchEnginesInHead(head) {
*/
const config = {
stories: ["../stories/**/*.stories.@(mdx|js|jsx|ts|tsx)"],
- addons: [
- "@storybook/addon-essentials",
- "storybook-react-i18next",
- "@storybook/addon-designs",
- ],
+ addons: ["@storybook/addon-essentials", "@storybook/addon-designs"],
framework: {
name: "@storybook/nextjs",
options: {
diff --git a/frontend/.storybook/preview.js b/frontend/.storybook/preview.js
deleted file mode 100644
index d75bcc900..000000000
--- a/frontend/.storybook/preview.js
+++ /dev/null
@@ -1,43 +0,0 @@
-// @ts-check
-import i18nConfig from "../next-i18next.config";
-
-// Apply global styling to our stories
-import "../src/styles/styles.scss";
-
-// Import i18next config.
-import i18n from "./i18next.js";
-
-// Generate the options for the Language menu using the locale codes.
-// Teams can override these labels, but this helps ensure that the language
-// is at least exposed in the list.
-const initialLocales = {};
-i18nConfig.i18n.locales.forEach((locale) => (initialLocales[locale] = locale));
-
-const parameters = {
- actions: { argTypesRegex: "^on[A-Z].*" },
- controls: {
- matchers: {
- color: /(background|color)$/i,
- date: /Date$/,
- },
- },
- // Configure i18next and locale/dropdown options.
- i18n,
-};
-
-/**
- * @type {import("@storybook/react").Preview}
- */
-const preview = {
- parameters,
- globals: {
- locale: "en",
- locales: {
- ...initialLocales,
- en: "English",
- es: "Español",
- },
- },
-};
-
-export default preview;
diff --git a/frontend/.storybook/preview.tsx b/frontend/.storybook/preview.tsx
new file mode 100644
index 000000000..9b135fadc
--- /dev/null
+++ b/frontend/.storybook/preview.tsx
@@ -0,0 +1,64 @@
+/**
+ * @file Setup the toolbar, styling, and global context for each Storybook story.
+ * @see https://storybook.js.org/docs/configure#configure-story-rendering
+ */
+import { Loader, Preview } from "@storybook/react";
+
+import "../src/styles/styles.scss";
+
+import { defaultLocale, locales } from "../src/i18n/config";
+import { getMessagesWithFallbacks } from "../src/i18n/getMessagesWithFallbacks";
+import I18nStoryWrapper from "./I18nStoryWrapper";
+
+const parameters = {
+ nextjs: {
+ appDirectory: true,
+ },
+ controls: {
+ matchers: {
+ color: /(background|color)$/i,
+ date: /Date$/,
+ },
+ },
+ options: {
+ storySort: {
+ method: "alphabetical",
+ order: [
+ "Welcome",
+ "Core",
+ // Storybook infers the title when not explicitly set, but is case-sensitive
+ // so we need to explicitly set both casings here for this to properly sort.
+ "Components",
+ "components",
+ "Templates",
+ "Pages",
+ "pages",
+ ],
+ },
+ },
+};
+
+const i18nMessagesLoader: Loader = async (context) => {
+ const messages = await getMessagesWithFallbacks(
+ context.globals.locale as string,
+ );
+ return { messages };
+};
+
+const preview: Preview = {
+ loaders: [i18nMessagesLoader],
+ decorators: [I18nStoryWrapper],
+ parameters,
+ globalTypes: {
+ locale: {
+ description: "Active language",
+ defaultValue: defaultLocale,
+ toolbar: {
+ icon: "globe",
+ items: locales,
+ },
+ },
+ },
+};
+
+export default preview;
diff --git a/frontend/jest.config.js b/frontend/jest.config.js
index 8b2ea6cd4..0ed77a928 100644
--- a/frontend/jest.config.js
+++ b/frontend/jest.config.js
@@ -9,10 +9,7 @@ const createJestConfig = nextJest({
// Add any custom config to be passed to Jest
/** @type {import('jest').Config} */
const customJestConfig = {
- setupFilesAfterEnv: [
- "/tests/jest.setup.js",
- "/tests/jest-i18n.ts",
- ],
+ setupFilesAfterEnv: ["/tests/jest.setup.js"],
testEnvironment: "jsdom",
// if using TypeScript with a baseUrl set to the root directory then you need the below for alias' to work
moduleDirectories: ["node_modules", "/"],
diff --git a/frontend/next-i18next.config.js b/frontend/next-i18next.config.js
deleted file mode 100644
index 5ebe1d0a8..000000000
--- a/frontend/next-i18next.config.js
+++ /dev/null
@@ -1,52 +0,0 @@
-// @ts-check
-/**
- * Next.js i18n routing options
- * https://nextjs.org/docs/advanced-features/i18n-routing
- * @type {import('next').NextConfig['i18n']}
- */
-const i18n = {
- defaultLocale: "en",
- // Source of truth for the list of languages supported by the application. Other tools (i18next, Storybook, tests) reference this.
- // These must be BCP47 language tags: https://en.wikipedia.org/wiki/IETF_language_tag#List_of_common_primary_language_subtags
- locales: ["en", "es"],
-};
-
-/**
- * i18next and react-i18next options
- * https://www.i18next.com/overview/configuration-options
- * https://react.i18next.com/latest/i18next-instance
- * @type {import("i18next").InitOptions}
- */
-const i18next = {
- // Default namespace to load, typically overridden within components,
- // but set here to prevent the system from attempting to load
- // translation.json, which is the default, and doesn't exist
- // in this codebase
- ns: "common",
- defaultNS: "common",
- fallbackLng: i18n.defaultLocale,
- interpolation: {
- escapeValue: false, // React already does escaping
- },
-};
-
-/**
- * next-i18next options
- * https://github.com/i18next/next-i18next#options
- * @type {Partial}
- */
-const nextI18next = {
- // Locale resources are loaded once when the server is started, which
- // is good for production but not ideal for local development. Show
- // updates to locale files without having to restart the server:
- reloadOnPrerender: process.env.NODE_ENV === "development",
-};
-
-/**
- * @type {import("next-i18next").UserConfig}
- */
-module.exports = {
- i18n,
- ...i18next,
- ...nextI18next,
-};
diff --git a/frontend/next.config.js b/frontend/next.config.js
index a7743e46f..9e5127fac 100644
--- a/frontend/next.config.js
+++ b/frontend/next.config.js
@@ -1,5 +1,5 @@
// @ts-check
-const { i18n } = require("./next-i18next.config");
+
const withNextIntl = require("next-intl/plugin")("./src/i18n/server.ts");
const sassOptions = require("./scripts/sassOptions");
@@ -16,17 +16,11 @@ const appSassOptions = sassOptions(basePath);
/** @type {import('next').NextConfig} */
const nextConfig = {
basePath,
- i18n,
reactStrictMode: true,
// Output only the necessary files for a deployment, excluding irrelevant node_modules
// https://nextjs.org/docs/app/api-reference/next-config-js/output
output: "standalone",
sassOptions: appSassOptions,
- transpilePackages: [
- // Continue to support older browsers (ES5)
- // https://github.com/i18next/i18next/issues/1948
- "i18next",
- ],
};
module.exports = withNextIntl(nextConfig);
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 68a0052ab..c4253e22e 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -13,15 +13,12 @@
"@opentelemetry/api": "^1.8.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": "^14.1.4",
- "next-i18next": "^15.0.0",
"next-intl": "^3.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
- "react-i18next": "^14.0.0",
"server-only": "^0.0.1",
"sharp": "^0.33.0",
"use-debounce": "^10.0.0"
@@ -2143,6 +2140,7 @@
"version": "7.23.9",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz",
"integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==",
+ "dev": true,
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
@@ -9188,15 +9186,6 @@
"@types/node": "*"
}
},
- "node_modules/@types/hoist-non-react-statics": {
- "version": "3.3.5",
- "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz",
- "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==",
- "dependencies": {
- "@types/react": "*",
- "hoist-non-react-statics": "^3.3.0"
- }
- },
"node_modules/@types/html-minifier-terser": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
@@ -9378,7 +9367,8 @@
"node_modules/@types/prop-types": {
"version": "15.7.11",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
- "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng=="
+ "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==",
+ "dev": true
},
"node_modules/@types/qs": {
"version": "6.9.11",
@@ -9396,6 +9386,7 @@
"version": "18.2.74",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.74.tgz",
"integrity": "sha512-9AEqNZZyBx8OdZpxzQlaFEVCSFUM2YXJH46yPOiOpm078k6ZLOCcuAzGum/zK8YBwY+dbahVNbHrbgrAwIRlqw==",
+ "dev": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
@@ -12064,16 +12055,6 @@
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
"dev": true
},
- "node_modules/core-js": {
- "version": "3.35.1",
- "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.35.1.tgz",
- "integrity": "sha512-IgdsbxNyMskrTFxa9lWHyMwAJU5gXOPP+1yO+K59d50VLVAIDAbs7gIv705KzALModfK3ZrSZTPNpC0PQgIZuw==",
- "hasInstallScript": true,
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/core-js"
- }
- },
"node_modules/core-js-compat": {
"version": "3.35.1",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.1.tgz",
@@ -12470,7 +12451,8 @@
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
- "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
+ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
+ "dev": true
},
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
@@ -15649,14 +15631,6 @@
"minimalistic-crypto-utils": "^1.0.1"
}
},
- "node_modules/hoist-non-react-statics": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
- "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
- "dependencies": {
- "react-is": "^16.7.0"
- }
- },
"node_modules/hosted-git-info": {
"version": "2.8.9",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
@@ -15722,6 +15696,8 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
+ "dev": true,
+ "peer": true,
"dependencies": {
"void-elements": "3.1.0"
}
@@ -15860,6 +15836,7 @@
"version": "23.8.2",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-23.8.2.tgz",
"integrity": "sha512-Z84zyEangrlERm0ZugVy4bIt485e/H8VecGUZkZWrH7BDePG6jT73QdL9EA1tRTTVVMpry/MgWIP1FjEn0DRXA==",
+ "dev": true,
"funding": [
{
"type": "individual",
@@ -15874,6 +15851,7 @@
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
}
],
+ "peer": true,
"dependencies": {
"@babel/runtime": "^7.23.2"
}
@@ -15887,11 +15865,6 @@
"@babel/runtime": "^7.23.2"
}
},
- "node_modules/i18next-fs-backend": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-2.3.1.tgz",
- "integrity": "sha512-tvfXskmG/9o+TJ5Fxu54sSO5OkY6d+uMn+K6JiUGLJrwxAVfer+8V3nU8jq3ts9Pe5lXJv4b1N7foIjJ8Iy2Gg=="
- },
"node_modules/i18next-http-backend": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-2.4.3.tgz",
@@ -19716,41 +19689,6 @@
}
}
},
- "node_modules/next-i18next": {
- "version": "15.2.0",
- "resolved": "https://registry.npmjs.org/next-i18next/-/next-i18next-15.2.0.tgz",
- "integrity": "sha512-Rl5yZ4oGffsB0AjRykZ5PzNQ2M6am54MaMayldGmH/UKZisrIxk2SKEPJvaHhKlWe1qgdNi2FkodwK8sEjfEmg==",
- "funding": [
- {
- "type": "individual",
- "url": "https://locize.com/i18next.html"
- },
- {
- "type": "individual",
- "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
- },
- {
- "type": "individual",
- "url": "https://locize.com"
- }
- ],
- "dependencies": {
- "@babel/runtime": "^7.23.2",
- "@types/hoist-non-react-statics": "^3.3.4",
- "core-js": "^3",
- "hoist-non-react-statics": "^3.3.2",
- "i18next-fs-backend": "^2.3.1"
- },
- "engines": {
- "node": ">=14"
- },
- "peerDependencies": {
- "i18next": ">= 23.7.13",
- "next": ">= 12.0.0",
- "react": ">= 17.0.2",
- "react-i18next": ">= 13.5.0"
- }
- },
"node_modules/next-intl": {
"version": "3.11.1",
"resolved": "https://registry.npmjs.org/next-intl/-/next-intl-3.11.1.tgz",
@@ -22187,6 +22125,8 @@
"version": "14.0.1",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-14.0.1.tgz",
"integrity": "sha512-TMV8hFismBmpMdIehoFHin/okfvgjFhp723RYgIqB4XyhDobVMyukyM3Z8wtTRmajyFMZrBl/OaaXF2P6WjUAw==",
+ "dev": true,
+ "peer": true,
"dependencies": {
"@babel/runtime": "^7.22.5",
"html-parse-stringify": "^3.0.1"
@@ -22207,7 +22147,8 @@
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
- "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "dev": true
},
"node_modules/react-refresh": {
"version": "0.14.0",
@@ -22520,7 +22461,8 @@
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
- "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
+ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
+ "dev": true
},
"node_modules/regenerator-transform": {
"version": "0.15.2",
@@ -25176,6 +25118,8 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
+ "dev": true,
+ "peer": true,
"engines": {
"node": ">=0.10.0"
}
diff --git a/frontend/package.json b/frontend/package.json
index 048464bb1..114ae1cb9 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -28,15 +28,12 @@
"@opentelemetry/api": "^1.8.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": "^14.1.4",
- "next-i18next": "^15.0.0",
"next-intl": "^3.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
- "react-i18next": "^14.0.0",
"server-only": "^0.0.1",
"sharp": "^0.33.0",
"use-debounce": "^10.0.0"
diff --git a/frontend/public/img/uswds-sprite.svg b/frontend/public/img/uswds-sprite.svg
new file mode 100644
index 000000000..8ae1eca92
--- /dev/null
+++ b/frontend/public/img/uswds-sprite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/locales/en/common.json b/frontend/public/locales/en/common.json
deleted file mode 100644
index 788b4be6e..000000000
--- a/frontend/public/locales/en/common.json
+++ /dev/null
@@ -1,294 +0,0 @@
-{
- "Beta_alert": {
- "alert_title": "Attention! Go to www.grants.gov to search and apply for grants.",
- "alert": "Simpler.Grants.gov is a work in progress. Thank you for your patience as we build this new website."
- },
- "Index": {
- "page_title": "Simpler.Grants.gov",
- "meta_description": "A one‑stop shop for all federal discretionary funding to make it easy for you to discover, understand, and apply for opportunities.",
- "goal": {
- "title": "The goal",
- "paragraph_1": "We want Grants.gov to be an extremely simple, accessible, and easy-to-use tool for posting, finding, sharing, and applying for federal financial assistance. Our mission is to increase access to grants and improve the grants experience for everyone.",
- "title_2": "For applicants",
- "paragraph_2": "We’re improving the way you search for and discover funding opportunities, making it easier to find and apply.",
- "title_3": "For grantmakers",
- "paragraph_3": "If you work for a federal grantmaking agency, we’re making it easier for your communities to find the funding they need.",
- "cta": "Sign up for project updates"
- },
- "process_and_research": {
- "title_1": "The process",
- "title_2": "The research",
- "paragraph_1": "This project is transparent, iterative, and agile. All of the code we’re writing is open source and our roadmap is public. As we release new versions, you can try out functional software and give us feedback on what works and what can be improved to inform what happens next.",
- "paragraph_2": "We conducted extensive research in 2023 to gather insights from applicants, potential applicants, and grantmakers. We’re using these findings to guide our work. And your ongoing feedback will inform and inspire new features as we build a simpler Grants.gov together.",
- "cta_1": "Learn about what’s happening",
- "cta_2": "Read the research findings"
- },
- "fo_title": "Improvements to funding opportunity announcements",
- "fo_paragraph_1": "Funding opportunities should not only be easy to find, share, and apply for. They should also be easy to read and understand. Our objective is to simplify and organize funding opportunities announcements. ",
- "fo_paragraph_2": "We want to help grantmakers write clear, concise announcements that encourage strong submissions from qualified applicants and make opportunities more accessible to everyone.",
- "fo_title_2": "View our grant announcement prototypes",
- "fo_paragraph_3": "We recently simplified the language of four grant announcements and applied visual and user‑centered design principles to increase their readability and usability.",
- "acl_prototype": "Link to ACL Notice of Funding Opportunity example pdf",
- "acf_prototype": "Link to ACF Notice of Funding Opportunity example pdf",
- "cdc_prototype": "Link to CDC Notice of Funding Opportunity example pdf",
- "samhsa_prototype": "Link to SAMHSA Notice of Funding Opportunity example pdf",
- "fo_title_3": "We want to hear from you!",
- "fo_paragraph_4": "We value your feedback. Tell us what you think of grant announcements and grants.gov.",
- "fo_title_4": "Are you a first‑time applicant? Created a workspace but haven't applied yet?",
- "fo_paragraph_5": "We're especially interested in hearing from first‑time applicants and organizations that have never applied for funding opportunities. We encourage you to review our announcements and share your feedback, regardless of your experience with federal grants.",
- "wtgi_paragraph_2": "Questions? Contact us at {{email}}."
- },
- "Research": {
- "page_title": "Research | Simpler.Grants.gov",
- "meta_description": "A one‑stop shop for all federal discretionary funding to make it easy for you to discover, understand, and apply for opportunities.",
- "intro": {
- "title": "Our existing research",
- "content": "We conducted extensive research in 2023 to gather insights from applicants, potential applicants, and grantmakers. We’re using these findings to guide our work. And your ongoing feedback will inform and inspire new features as we build a simpler Grants.gov together."
- },
- "methodology": {
- "title": "The methodology",
- "paragraph_1": "
Applicants and grantmakers were selected for a series of user interviews to better understand their experience using Grants.gov. We recruited equitably to ensure a diverse pool of participants.
The quantity of participants was well above industry standards. Of the applicants who were interviewed, 26% were first-time applicants, 39% were occasional applicants, and 34% were frequent applicants.
With the findings from these interviews, we defined user archetypes and general themes to guide the Simpler.Grants.gov user experience.
Examine existing user journeys and behaviors, identifying how Grants.gov fits into their overall approach
Learn from user experiences, roles, challenges
Identify barriers and how a simpler Grants.gov can create a more intuitive user experience, especially for new users
",
- "title_3": "Want to be notified when there are upcoming user research efforts?",
- "cta": "Sign up for project updates"
- },
- "archetypes": {
- "title": "Applicant archetypes",
- "paragraph_1": "Archetypes are compelling summaries that highlight the types of applicants that Grants.gov serves. They’re informed by and summarize user research data, and represent user behaviors, attitudes, motivations, pain points, and goals. We’ll use these archetypes to influence our design decisions, guide the product’s direction, and keep our work human-centered. ",
- "novice": {
- "title": "The Novice",
- "paragraph_1": "Applicants lacking familiarity with the grant application process, including first-time or infrequent applicants and those who never apply",
- "paragraph_2": "Novices are often new to the grants application process. They face a steep learning curve to find and apply for funding opportunities. Solving their needs will generate a more inclusive Grants.gov experience."
- },
- "collaborator": {
- "title": "The Collaborator",
- "paragraph_1": "Applicants who've applied before, working with colleagues or partner organizations to increase their chances of success",
- "paragraph_2": "Collaborators have more familiarity with Grants.gov. But they face challenges with coordinating application materials, and often resorting to tools and resources outside of Grants.gov."
- },
- "maestro": {
- "title": "The Maestro",
- "paragraph_1": "Frequent applicants familiar with Grants.gov, who are often directly responsible for managing multiple applications at once",
- "paragraph_2": "Maestros have an established approach to applying, which may include software and tools outside of Grants.gov. Their primary concerns are rooted in determining grant feasibility and staying ahead of deadlines."
- },
- "supervisor": {
- "title": "The Supervisor",
- "paragraph_1": "Applicants who have a more senior role at organizations and have less frequent direct involvement with Grants.gov than Maestros.",
- "paragraph_2": "Supervisors are responsible for oversight, approvals, final submissions, and keeping registrations up to date. Their time is limited, as they're often busy with the organization's other needs."
- }
- },
- "themes": {
- "title": "General themes",
- "paragraph_1": "The existing Grants.gov website works best for those who use it regularly. Larger organizations and teams of Collaborators and Maestros are typically more familiar with the ins and outs of the system. To create a simpler Grants.gov with an intuitive user experience that addresses the needs of all archetypes, four themes were defined:",
- "title_2": "Frictionless functionality ",
- "paragraph_2": "Reduce the burden on applicants and grantmakers, from both a process and systems perspective, by addressing the pain points that negatively affect their experience",
- "title_3": "Sophisticated self-direction",
- "paragraph_3": "Meet users where they are during crucial moments, by providing a guided journey through opt-in contextual support that reduces their need to find help outside the system",
- "title_4": "Demystify the grants process",
- "paragraph_4": "Ensure that all users have the same easy access to instructional and educational information that empowers them to have a smoother, informed, and confident user experience",
- "title_5": "Create an ownable identity",
- "paragraph_5": "Create a presence that reflections our mission and supports our users through visual brand, content strategy, and user interface design systems"
- },
- "impact": {
- "title": "Where can we have the most impact?",
- "paragraph_1": "The most burden is on the Novice to become an expert on the grants process and system. In order to execute our mission, there is a need to improve awareness, access, and choice. This requires reaching out to those who are unfamiliar with the grant application process.",
- "paragraph_2": "There are many common barriers that users face:",
- "title_2": "Are there challenges you’ve experienced that aren’t captured here?",
- "paragraph_3": "If you would like to share your experiences and challenges as either an applicant or grantmaker, reach out to us at simpler@grants.gov or sign up for project updates to be notified of upcoming user research efforts.",
- "boxes": [
- {
- "title": "Digital connectivity",
- "content": "Depending on availability and geography, a stable internet connection is not a guarantee to support a digital-only experience."
- },
- {
- "title": "Organization size",
- "content": "Not all organizations have dedicated resources for seeking grant funding. Many are 1-person shops who are trying to do it all."
- },
- {
- "title": "Overworked",
- "content": "New organizations are often too burdened with internal paperwork and infrastructure to support external funding and reporting."
- },
- {
- "title": "Expertise",
- "content": "Small organizations face higher turnover, and alumni often take their institutional knowledge and expertise with them when they leave."
- },
- {
- "title": "Cognitive load",
- "content": "Applicants often apply for funding through several agencies, requiring they learn multiple processes and satisfy varying requirements."
- },
- {
- "title": "Language",
- "content": "Applicants are faced with a lot of jargon without context or definitions, which is especially difficult when English is not their native language."
- },
- {
- "title": "Education",
- "content": "It often requires a high level of education to comprehend the complexity and language of funding opportunity announcements."
- },
- {
- "title": "Lost at the start",
- "content": "Novices don’t see a clear call-to-action for getting started, and they have trouble finding the one-on-one help at the beginning of the process."
- },
- {
- "title": "Overwhelmed by search",
- "content": "New applicants misuse the keyword search function and have trouble understanding the acronyms and terminology."
- },
- {
- "title": "Confused by announcements",
- "content": "Novices have difficulty determining their eligibility and understanding the details of the funding opportunity announcement."
- },
- {
- "title": "Time",
- "content": "Most individuals wear a lot of hats (community advocate, program lead, etc.) and \"grants applicant\" is only part of their responsibilities and requires efficiency."
- },
- {
- "title": "Blindsided by requirements",
- "content": "New applicants are caught off guard by SAM.gov registration and often miss the format and file name requirements."
- }
- ]
- }
- },
- "Process": {
- "page_title": "Process | Simpler.Grants.gov",
- "meta_description": "A one‑stop shop for all federal discretionary funding to make it easy for you to discover, understand, and apply for opportunities.",
- "intro": {
- "title": "Our open process",
- "content": "This project is transparent, iterative, and agile. All of the code we’re writing is open source and our roadmap is public. As we regularly release new versions of Simpler.Grants.gov, you'll see what we're building and prioritizing. With each iteration, you'll be able to try out functional software and give us feedback on what works and what can be improved to inform what happens next.",
- "boxes": [
- {
- "title": "Transparent",
- "content": "We’re building a simpler Grants.gov in the open. You can see our plans and our progress. And you can join us in shaping the vision and details of the features we build."
- },
- {
- "title": "Iterative",
- "content": "We’re releasing features early and often through a continuous cycle of planning, implementation, and assessment. Each cycle will incrementally improve the product, as we incorporate your feedback from the prior iteration."
- },
- {
- "title": "Agile",
- "content": "We’re building a simpler Grants.gov with you, not for you. Our process gives us the flexibility to swiftly respond to feedback and adapt to changing priorities and requirements."
- }
- ]
- },
- "milestones": {
- "tag": "The high-level roadmap",
- "icon_list": [
- {
- "title": "Find",
- "content": "
Improve how applicants discover funding opportunities that they’re qualified for and that meet their needs.
Improve stakeholders’ capacity to understand, analyze, and assess grants from application to acceptance.
Make non-confidential Grants.gov data open for public analysis.
"
- },
- {
- "title": "Apply",
- "content": "
Streamline the application process to make it easier for all applicants to apply for funding opportunities.
"
- }
- ],
- "roadmap_1": "Find",
- "title_1": "Milestone 1",
- "name_1": "Laying the foundation with a modern Application Programming Interface (API)",
- "paragraph_1": "To make it easier to discover funding opportunities, we’re starting with a new modern API to make grants data more accessible. Our API‑first approach will prioritize data at the beginning, and make sure data remains a priority as we iterate. It’s crucial that the Grants.gov website, 3rd‑party apps, and other services can more easily access grants data. Our new API will foster innovation and be a foundation for interacting with grants in new ways, like SMS, phone, email, chat, and notifications.",
- "sub_title_1": "What’s an API?",
- "sub_paragraph_1": "Think of the API as a liaison between the Grants.gov website and the information and services that power it. It’s software that allows two applications to talk to each other or sends data back and forth between a website and a user.",
- "sub_title_2": "Are you interested in the tech?",
- "sub_paragraph_2": "We’re building a RESTful API. And we’re starting with an initial endpoint that allows API users to retrieve basic information about each funding opportunity.",
- "cta_1": "View the API milestone on GitHub",
- "roadmap_2": "Find",
- "title_2": "Milestone 2",
- "name_2": "A new search interface accessible to everyone",
- "paragraph_2": "Once our new API is in place, we’ll begin focusing on how applicants most commonly access grants data. Our first user-facing milestone will be a simple search interface that makes data from our modern API accessible to anyone who wants to try out new ways to search for funding opportunities.",
- "sub_title_3": "Can’t wait to try out the new search?",
- "sub_paragraph_3": "Search will be the first feature on Simpler.Grants.gov that you’ll be able to test. It’ll be quite basic at first, and you’ll need to continue using www.grants.gov as we iterate. But your feedback will inform what happens next.",
- "sub_paragraph_4": "Be sure to sign up for product updates so you know when the new search is available.",
- "cta_2": "View the search milestone on GitHub"
- },
- "involved": {
- "title_1": "Do you have data expertise?",
- "paragraph_1": "We're spending time up-front collaborating with stakeholders on API design and data standards. If you have subject matter expertise with grants data, we want to talk. Contact us at simpler@grants.gov.",
- "title_2": "Are you code-savvy?",
- "paragraph_2": "If you’re interested in contributing to the open-source project or exploring the details of exactly what we’re building, check out the project at https://github.com/HHS/simpler-grants-gov or join our community at wiki.simpler.hhs.gov."
- }
- },
- "Newsletter": {
- "page_title": "Newsletter | Simpler.Grants.gov",
- "title": "Newsletter signup",
- "intro": "Subscribe to get Simpler.Grants.gov project updates in your inbox!",
- "paragraph_1": "If you sign up for the Simpler.Grants.gov newsletter, we’ll keep you informed of our progress and you’ll know about every opportunity to get involved.",
- "list": "
Hear about upcoming milestones
Be the first to know when we launch new code
Test out new features and functionalities
Participate in usability tests and other user research efforts
Learn about ways to provide feedback
",
- "disclaimer": "The Simpler.Grants.gov newsletter is powered by the Sendy data service. Personal information is not stored within Simpler.Grants.gov.",
- "errors": {
- "missing_name": "Enter your first name.",
- "missing_email": "Enter your email address.",
- "invalid_email": "Enter an email address in the correct format, like name@example.com.",
- "already_subscribed": "{{email_address}} is already subscribed. If you’re not seeing our emails, check your spam folder and add no-reply@grants.gov to your contacts, address book, or safe senders list. If you continue to not receive our emails, contact simpler@grants.gov.",
- "sendy": "Sorry, an unexpected error in our system occured when trying to save your subscription. If this continues to happen, you may email simpler@grants.gov. Error: {{sendy_error}}"
- }
- },
- "Newsletter_confirmation": {
- "page_title": "Newsletter Confirmation | Simpler.Grants.gov",
- "title": "You’re subscribed",
- "intro": "You are signed up to receive project updates from Simpler.Grants.gov.",
- "paragraph_1": "Thank you for subscribing. We’ll keep you informed of our progress and you’ll know about every opportunity to get involved.",
- "heading": "Learn more",
- "paragraph_2": "You can read all about our transparent process and what we’re doing now, or explore our existing user research and the findings that are guiding our work.",
- "disclaimer": "The Simpler.Grants.gov newsletter is powered by the Sendy data service. Personal information is not stored within Simpler.Grants.gov. "
- },
- "Newsletter_unsubscribe": {
- "page_title": "Newsletter Unsubscribe | Simpler.Grants.gov",
- "title": "You have unsubscribed",
- "intro": "You will no longer receive project updates from Simpler.Grants.gov. ",
- "paragraph_1": "Did you unsubscribe by accident? Sign up again.",
- "button_resub": "Re-subscribe",
- "heading": "Learn more",
- "paragraph_2": "You can read all about our transparent process and what we’re doing now, or explore our existing user research and the findings that are guiding our work.",
- "disclaimer": "The Simpler.Grants.gov newsletter is powered by the Sendy data service. Personal information is not stored within Simpler.Grants.gov. "
- },
- "ErrorPages": {
- "page_not_found": {
- "title": "Oops! Page Not Found",
- "message_content_1": "The page you have requested cannot be displayed because it does not exist, has been moved, or the server has been instructed not to let you view it. There is nothing to see here.",
- "visit_homepage_button": "Return Home"
- }
- },
- "Header": {
- "nav_link_home": "Home",
- "nav_link_process": "Process",
- "nav_link_research": "Research",
- "nav_link_newsletter": "Newsletter",
- "nav_menu_toggle": "Menu",
- "title": "Simpler.Grants.gov"
- },
- "Hero": {
- "title": "We're building a simpler Grants.gov!",
- "content": "This new website will be your go‑to resource to follow our progress as we improve and modernize the Grants.gov experience, making it easier to find, share, and apply for grants.",
- "github_link": "Follow on GitHub"
- },
- "Footer": {
- "agency_name": "Grants.gov",
- "agency_contact_center": "Grants.gov Program Management Office",
- "telephone": "1-877-696-6775",
- "return_to_top": "Return to top",
- "link_twitter": "Twitter",
- "link_youtube": "YouTube",
- "link_github": "Github",
- "link_rss": "RSS",
- "link_newsletter": "Newsletter",
- "link_blog": "Blog",
- "logo_alt": "Grants.gov logo"
- },
- "Identifier": {
- "identity": "An official website of the U.S. Department of Health and Human Services",
- "gov_content": "Looking for U.S. government information and services? Visit USA.gov",
- "link_about": "About HHS",
- "link_accessibility": "Accessibility support",
- "link_foia": "FOIA requests",
- "link_fear": "EEO/No Fear Act",
- "link_ig": "Office of the Inspector General",
- "link_performance": "Performance reports",
- "link_privacy": "Privacy Policy",
- "logo_alt": "HHS logo"
- },
- "Layout": {
- "skip_to_main": "Skip to main content"
- }
-}
diff --git a/frontend/public/locales/es/common.json b/frontend/public/locales/es/common.json
deleted file mode 100644
index d1d16ccfd..000000000
--- a/frontend/public/locales/es/common.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "Index": {
- "title": "Página principal"
- },
- "Header": {
- "title": "Título del sitio"
- }
-}
diff --git a/frontend/src/app/[locale]/dev/feature-flags/FeatureFlagsTable.tsx b/frontend/src/app/[locale]/dev/feature-flags/FeatureFlagsTable.tsx
new file mode 100644
index 000000000..604e58015
--- /dev/null
+++ b/frontend/src/app/[locale]/dev/feature-flags/FeatureFlagsTable.tsx
@@ -0,0 +1,61 @@
+"use client";
+import { useFeatureFlags } from "src/hooks/useFeatureFlags";
+
+import React from "react";
+import { Button, Table } from "@trussworks/react-uswds";
+
+/**
+ * View for managing feature flags
+ */
+export default function FeatureFlagsTable() {
+ const { featureFlagsManager, mounted, setFeatureFlag } = useFeatureFlags();
+
+ if (!mounted) {
+ return null;
+ }
+
+ return (
+
);
-};
-
-export default Layout;
+}
diff --git a/frontend/src/components/USWDSIcon.tsx b/frontend/src/components/USWDSIcon.tsx
new file mode 100644
index 000000000..7414d8978
--- /dev/null
+++ b/frontend/src/components/USWDSIcon.tsx
@@ -0,0 +1,23 @@
+import SpriteSVG from "public/img/uswds-sprite.svg";
+
+interface IconProps {
+ name: string;
+ className: string;
+ height?: string;
+}
+
+// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+const sprite_uri = SpriteSVG.src as string;
+
+export function USWDSIcon(props: IconProps) {
+ return (
+
+ );
+}
diff --git a/frontend/src/pages/content/FundingContent.tsx b/frontend/src/components/content/FundingContent.tsx
similarity index 93%
rename from frontend/src/pages/content/FundingContent.tsx
rename to frontend/src/components/content/FundingContent.tsx
index 60f0900e4..b72dc07be 100644
--- a/frontend/src/pages/content/FundingContent.tsx
+++ b/frontend/src/components/content/FundingContent.tsx
@@ -1,12 +1,12 @@
import { nofoPdfs } from "src/constants/nofoPdfs";
-import { useTranslation } from "next-i18next";
+import { useTranslations } from "next-intl";
import { Grid, GridContainer } from "@trussworks/react-uswds";
import NofoImageLink from "../../components/NofoImageLink";
const FundingContent = () => {
- const { t } = useTranslation("common", { keyPrefix: "Index" });
+ const t = useTranslations("Index");
return (
@@ -41,7 +41,7 @@ const FundingContent = () => {
image={pdf.image}
height={pdf.height}
width={pdf.width}
- alt={t(`${pdf.alt}`)}
+ alt={pdf.alt}
/>
))}
diff --git a/frontend/src/pages/content/IndexGoalContent.tsx b/frontend/src/components/content/IndexGoalContent.tsx
similarity index 80%
rename from frontend/src/pages/content/IndexGoalContent.tsx
rename to frontend/src/components/content/IndexGoalContent.tsx
index 83d3c4d69..a1e064e6a 100644
--- a/frontend/src/pages/content/IndexGoalContent.tsx
+++ b/frontend/src/components/content/IndexGoalContent.tsx
@@ -1,11 +1,12 @@
-import { useTranslation } from "next-i18next";
-import Link from "next/link";
-import { Button, Grid, Icon } from "@trussworks/react-uswds";
+import { useTranslations } from "next-intl";
+import Link from "next/link";
+import { Button, Grid } from "@trussworks/react-uswds";
+import { USWDSIcon } from "../USWDSIcon";
import ContentLayout from "src/components/ContentLayout";
const IndexGoalContent = () => {
- const { t } = useTranslation("common", { keyPrefix: "Index" });
+ const t = useTranslations("Index");
return (
{
diff --git a/frontend/src/pages/content/ProcessAndResearchContent.tsx b/frontend/src/components/content/ProcessAndResearchContent.tsx
similarity index 77%
rename from frontend/src/pages/content/ProcessAndResearchContent.tsx
rename to frontend/src/components/content/ProcessAndResearchContent.tsx
index 0706f32c3..e07bab385 100644
--- a/frontend/src/pages/content/ProcessAndResearchContent.tsx
+++ b/frontend/src/components/content/ProcessAndResearchContent.tsx
@@ -1,11 +1,12 @@
-import { useTranslation } from "next-i18next";
+import { useTranslations } from "next-intl";
import Link from "next/link";
-import { Button, Grid, Icon } from "@trussworks/react-uswds";
+import { Button, Grid } from "@trussworks/react-uswds";
+import { USWDSIcon } from "src/components/USWDSIcon";
import ContentLayout from "src/components/ContentLayout";
const ProcessAndResearchContent = () => {
- const { t } = useTranslation("common", { keyPrefix: "Index" });
+ const t = useTranslations("Index");
return (
{
{t("process_and_research.cta_1")}
-
@@ -44,9 +45,9 @@ const ProcessAndResearchContent = () => {
{t("process_and_research.cta_2")}
-
diff --git a/frontend/src/components/search/SearchResultsList.tsx b/frontend/src/components/search/SearchResultsList.tsx
index eeddc716b..bca660db1 100644
--- a/frontend/src/components/search/SearchResultsList.tsx
+++ b/frontend/src/components/search/SearchResultsList.tsx
@@ -1,7 +1,7 @@
"use client";
import { AgencyNamyLookup } from "src/utils/search/generateAgencyNameLookup";
-import Loading from "../../app/search/loading";
+import Loading from "../../app/[locale]/search/loading";
import SearchErrorAlert from "src/components/search/error/SearchErrorAlert";
import { SearchResponseData } from "../../types/search/searchResponseTypes";
import SearchResultsListItem from "./SearchResultsListItem";
diff --git a/frontend/src/hooks/useSearchFormState.ts b/frontend/src/hooks/useSearchFormState.ts
index f3a53bef8..0526effd9 100644
--- a/frontend/src/hooks/useSearchFormState.ts
+++ b/frontend/src/hooks/useSearchFormState.ts
@@ -4,7 +4,7 @@ import { useRef, useState } from "react";
import { QueryParamData } from "../services/search/searchfetcher/SearchFetcher";
import { SearchAPIResponse } from "../types/search/searchResponseTypes";
-import { updateResults } from "../app/search/actions";
+import { updateResults } from "../app/[locale]/search/actions";
import { useFormState } from "react-dom";
import { useSearchParamUpdater } from "./useSearchParamUpdater";
diff --git a/frontend/src/i18n/messages/en/index.ts b/frontend/src/i18n/messages/en/index.ts
index 843d4455d..11a5f900a 100644
--- a/frontend/src/i18n/messages/en/index.ts
+++ b/frontend/src/i18n/messages/en/index.ts
@@ -229,12 +229,12 @@ export const messages = {
{
title: "Find",
content:
- "
Improve how applicants discover funding opportunities that they’re qualified for and that meet their needs.
",
+ "
Improve how applicants discover funding opportunities that they’re qualified for and that meet their needs.
",
},
{
title: "Advanced reporting",
content:
- "
Improve stakeholders’ capacity to understand, analyze, and assess grants from application to acceptance.
Make non-confidential Grants.gov data open for public analysis.
",
+ "
Improve stakeholders’ capacity to understand, analyze, and assess grants from application to acceptance.
Make non-confidential Grants.gov data open for public analysis.
",
},
{
title: "Apply",
@@ -291,9 +291,9 @@ export const messages = {
invalid_email:
"Enter an email address in the correct format, like name@example.com.",
already_subscribed:
- "{{email_address}} is already subscribed. If you’re not seeing our emails, check your spam folder and add no-reply@grants.gov to your contacts, address book, or safe senders list. If you continue to not receive our emails, contact simpler@grants.gov.",
+ " is already subscribed. If you’re not seeing our emails, check your spam folder and add no-reply@grants.gov to your contacts, address book, or safe senders list. If you continue to not receive our emails, contact simpler@grants.gov.",
sendy:
- "Sorry, an unexpected error in our system occured when trying to save your subscription. If this continues to happen, you may email simpler@grants.gov. Error: {{sendy_error}}",
+ "Sorry, an unexpected error in our system occured when trying to save your subscription. If this continues to happen, you may email simpler@grants.gov. Error: ",
},
},
Newsletter_confirmation: {
@@ -323,6 +323,7 @@ export const messages = {
"The Simpler.Grants.gov newsletter is powered by the Sendy data service. Personal information is not stored within Simpler.Grants.gov. ",
},
ErrorPages: {
+ page_title: "Page Not Found | Simpler.Grants.gov",
page_not_found: {
title: "Oops! Page Not Found",
message_content_1:
diff --git a/frontend/src/middleware.ts b/frontend/src/middleware.ts
index 33d8c733b..996176d04 100644
--- a/frontend/src/middleware.ts
+++ b/frontend/src/middleware.ts
@@ -19,7 +19,7 @@ export const config = {
* - _next/image (image optimization files)
* - images (static files in public/images/ directory)
*/
- "/((?!api|_next/static|_next/image|images|site.webmanifest).*)",
+ "/((?!api|_next/static|_next/image|public|img|uswds|images|robots.txt|site.webmanifest).*)",
/**
* Fix issue where the pattern above was causing middleware
* to not run on the homepage:
diff --git a/frontend/src/pages/404.tsx b/frontend/src/pages/404.tsx
deleted file mode 100644
index 3543f0e3d..000000000
--- a/frontend/src/pages/404.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import type { GetStaticProps, NextPage } from "next";
-
-import { useTranslation } from "next-i18next";
-import { serverSideTranslations } from "next-i18next/serverSideTranslations";
-import Link from "next/link";
-import { GridContainer } from "@trussworks/react-uswds";
-
-import BetaAlert from "../components/BetaAlert";
-
-const PageNotFound: NextPage = () => {
- const { t } = useTranslation("common");
- // TODO: Remove during move to app router and next-intl upgrade
- const beta_strings = {
- alert_title: t("Beta_alert.alert_title"),
- alert: t("Beta_alert.alert"),
- };
- return (
- <>
-
-
-
```json
{
"data": {},
"errors": [],
"message": "The server could not verify that you are authorized to access the URL requested",
"status_code": 401
}
```
---------
Co-authored-by: nava-platform-bot
---
api/openapi.generated.yml | 656 +++++-------------
api/src/api/healthcheck.py | 28 +-
.../opportunities_v0/opportunity_routes.py | 4 +-
.../opportunities_v0/opportunity_schemas.py | 9 +
.../opportunities_v0_1/opportunity_routes.py | 4 +-
.../opportunities_v0_1/opportunity_schemas.py | 9 +
.../opportunities_v1/opportunity_routes.py | 4 +-
.../opportunities_v1/opportunity_schemas.py | 9 +
api/src/api/schemas/response_schema.py | 47 +-
api/src/app.py | 2 +-
api/tests/src/api/test_healthcheck.py | 6 +-
api/tests/src/api/test_route_error_format.py | 8 +-
documentation/api/api-details.md | 10 +-
13 files changed, 285 insertions(+), 511 deletions(-)
diff --git a/api/openapi.generated.yml b/api/openapi.generated.yml
index 7302ded44..b7fa2ffa0 100644
--- a/api/openapi.generated.yml
+++ b/api/openapi.generated.yml
@@ -34,55 +34,13 @@ paths:
content:
application/json:
schema:
- type: object
- properties:
- message:
- type: string
- description: The message to return
- data:
- $ref: '#/components/schemas/Healthcheck'
- status_code:
- type: integer
- description: The HTTP status code
- pagination_info:
- description: The pagination information for paginated endpoints
- type: &id001
- - object
- allOf:
- - $ref: '#/components/schemas/PaginationInfo'
- warnings:
- type: array
- items:
- type: &id002
- - object
- allOf:
- - $ref: '#/components/schemas/ValidationIssue'
+ $ref: '#/components/schemas/HealthcheckResponse'
description: Successful response
'503':
content:
application/json:
schema:
- type: object
- properties:
- message:
- type: string
- description: The message to return
- data:
- $ref: '#/components/schemas/ErrorResponse'
- status_code:
- type: integer
- description: The HTTP status code
- pagination_info:
- description: The pagination information for paginated endpoints
- type: *id001
- allOf:
- - $ref: '#/components/schemas/PaginationInfo'
- warnings:
- type: array
- items:
- type: *id002
- allOf:
- - $ref: '#/components/schemas/ValidationIssue'
+ $ref: '#/components/schemas/ErrorResponse'
description: Service Unavailable
tags:
- Health
@@ -101,83 +59,19 @@ paths:
content:
application/json:
schema:
- type: object
- properties:
- message:
- type: string
- description: The message to return
- data:
- type: array
- items:
- $ref: '#/components/schemas/OpportunityV0'
- status_code:
- type: integer
- description: The HTTP status code
- pagination_info:
- description: The pagination information for paginated endpoints
- type: &id003
- - object
- allOf:
- - $ref: '#/components/schemas/PaginationInfo'
- warnings:
- type: array
- items:
- type: &id004
- - object
- allOf:
- - $ref: '#/components/schemas/ValidationIssue'
+ $ref: '#/components/schemas/OpportunitySearchResponseV0'
description: Successful response
'422':
content:
application/json:
schema:
- type: object
- properties:
- message:
- type: string
- description: The message to return
- data:
- $ref: '#/components/schemas/ErrorResponse'
- status_code:
- type: integer
- description: The HTTP status code
- pagination_info:
- description: The pagination information for paginated endpoints
- type: *id003
- allOf:
- - $ref: '#/components/schemas/PaginationInfo'
- warnings:
- type: array
- items:
- type: *id004
- allOf:
- - $ref: '#/components/schemas/ValidationIssue'
+ $ref: '#/components/schemas/ErrorResponse'
description: Validation error
'401':
content:
application/json:
schema:
- type: object
- properties:
- message:
- type: string
- description: The message to return
- data:
- $ref: '#/components/schemas/ErrorResponse'
- status_code:
- type: integer
- description: The HTTP status code
- pagination_info:
- description: The pagination information for paginated endpoints
- type: *id003
- allOf:
- - $ref: '#/components/schemas/PaginationInfo'
- warnings:
- type: array
- items:
- type: *id004
- allOf:
- - $ref: '#/components/schemas/ValidationIssue'
+ $ref: '#/components/schemas/ErrorResponse'
description: Authentication error
tags:
- Opportunity v0
@@ -213,83 +107,19 @@ paths:
content:
application/json:
schema:
- type: object
- properties:
- message:
- type: string
- description: The message to return
- data:
- type: array
- items:
- $ref: '#/components/schemas/OpportunityV1'
- status_code:
- type: integer
- description: The HTTP status code
- pagination_info:
- description: The pagination information for paginated endpoints
- type: &id005
- - object
- allOf:
- - $ref: '#/components/schemas/PaginationInfo'
- warnings:
- type: array
- items:
- type: &id006
- - object
- allOf:
- - $ref: '#/components/schemas/ValidationIssue'
+ $ref: '#/components/schemas/OpportunitySearchResponseV1'
description: Successful response
'422':
content:
application/json:
schema:
- type: object
- properties:
- message:
- type: string
- description: The message to return
- data:
- $ref: '#/components/schemas/ErrorResponse'
- status_code:
- type: integer
- description: The HTTP status code
- pagination_info:
- description: The pagination information for paginated endpoints
- type: *id005
- allOf:
- - $ref: '#/components/schemas/PaginationInfo'
- warnings:
- type: array
- items:
- type: *id006
- allOf:
- - $ref: '#/components/schemas/ValidationIssue'
+ $ref: '#/components/schemas/ErrorResponse'
description: Validation error
'401':
content:
application/json:
schema:
- type: object
- properties:
- message:
- type: string
- description: The message to return
- data:
- $ref: '#/components/schemas/ErrorResponse'
- status_code:
- type: integer
- description: The HTTP status code
- pagination_info:
- description: The pagination information for paginated endpoints
- type: *id005
- allOf:
- - $ref: '#/components/schemas/PaginationInfo'
- warnings:
- type: array
- items:
- type: *id006
- allOf:
- - $ref: '#/components/schemas/ValidationIssue'
+ $ref: '#/components/schemas/ErrorResponse'
description: Authentication error
tags:
- Opportunity v1
@@ -325,83 +155,19 @@ paths:
content:
application/json:
schema:
- type: object
- properties:
- message:
- type: string
- description: The message to return
- data:
- type: array
- items:
- $ref: '#/components/schemas/OpportunityV01'
- status_code:
- type: integer
- description: The HTTP status code
- pagination_info:
- description: The pagination information for paginated endpoints
- type: &id007
- - object
- allOf:
- - $ref: '#/components/schemas/PaginationInfo'
- warnings:
- type: array
- items:
- type: &id008
- - object
- allOf:
- - $ref: '#/components/schemas/ValidationIssue'
+ $ref: '#/components/schemas/OpportunitySearchResponseV01'
description: Successful response
'422':
content:
application/json:
schema:
- type: object
- properties:
- message:
- type: string
- description: The message to return
- data:
- $ref: '#/components/schemas/ErrorResponse'
- status_code:
- type: integer
- description: The HTTP status code
- pagination_info:
- description: The pagination information for paginated endpoints
- type: *id007
- allOf:
- - $ref: '#/components/schemas/PaginationInfo'
- warnings:
- type: array
- items:
- type: *id008
- allOf:
- - $ref: '#/components/schemas/ValidationIssue'
+ $ref: '#/components/schemas/ErrorResponse'
description: Validation error
'401':
content:
application/json:
schema:
- type: object
- properties:
- message:
- type: string
- description: The message to return
- data:
- $ref: '#/components/schemas/ErrorResponse'
- status_code:
- type: integer
- description: The HTTP status code
- pagination_info:
- description: The pagination information for paginated endpoints
- type: *id007
- allOf:
- - $ref: '#/components/schemas/PaginationInfo'
- warnings:
- type: array
- items:
- type: *id008
- allOf:
- - $ref: '#/components/schemas/ValidationIssue'
+ $ref: '#/components/schemas/ErrorResponse'
description: Authentication error
tags:
- Opportunity v0.1
@@ -483,81 +249,19 @@ paths:
content:
application/json:
schema:
- type: object
- properties:
- message:
- type: string
- description: The message to return
- data:
- $ref: '#/components/schemas/OpportunityV0'
- status_code:
- type: integer
- description: The HTTP status code
- pagination_info:
- description: The pagination information for paginated endpoints
- type: &id009
- - object
- allOf:
- - $ref: '#/components/schemas/PaginationInfo'
- warnings:
- type: array
- items:
- type: &id010
- - object
- allOf:
- - $ref: '#/components/schemas/ValidationIssue'
+ $ref: '#/components/schemas/OpportunityGetResponseV0'
description: Successful response
'401':
content:
application/json:
schema:
- type: object
- properties:
- message:
- type: string
- description: The message to return
- data:
- $ref: '#/components/schemas/ErrorResponse'
- status_code:
- type: integer
- description: The HTTP status code
- pagination_info:
- description: The pagination information for paginated endpoints
- type: *id009
- allOf:
- - $ref: '#/components/schemas/PaginationInfo'
- warnings:
- type: array
- items:
- type: *id010
- allOf:
- - $ref: '#/components/schemas/ValidationIssue'
+ $ref: '#/components/schemas/ErrorResponse'
description: Authentication error
'404':
content:
application/json:
schema:
- type: object
- properties:
- message:
- type: string
- description: The message to return
- data:
- $ref: '#/components/schemas/ErrorResponse'
- status_code:
- type: integer
- description: The HTTP status code
- pagination_info:
- description: The pagination information for paginated endpoints
- type: *id009
- allOf:
- - $ref: '#/components/schemas/PaginationInfo'
- warnings:
- type: array
- items:
- type: *id010
- allOf:
- - $ref: '#/components/schemas/ValidationIssue'
+ $ref: '#/components/schemas/ErrorResponse'
description: Not found
tags:
- Opportunity v0
@@ -593,81 +297,19 @@ paths:
content:
application/json:
schema:
- type: object
- properties:
- message:
- type: string
- description: The message to return
- data:
- $ref: '#/components/schemas/OpportunityV1'
- status_code:
- type: integer
- description: The HTTP status code
- pagination_info:
- description: The pagination information for paginated endpoints
- type: &id011
- - object
- allOf:
- - $ref: '#/components/schemas/PaginationInfo'
- warnings:
- type: array
- items:
- type: &id012
- - object
- allOf:
- - $ref: '#/components/schemas/ValidationIssue'
+ $ref: '#/components/schemas/OpportunityGetResponseV1'
description: Successful response
'401':
content:
application/json:
schema:
- type: object
- properties:
- message:
- type: string
- description: The message to return
- data:
- $ref: '#/components/schemas/ErrorResponse'
- status_code:
- type: integer
- description: The HTTP status code
- pagination_info:
- description: The pagination information for paginated endpoints
- type: *id011
- allOf:
- - $ref: '#/components/schemas/PaginationInfo'
- warnings:
- type: array
- items:
- type: *id012
- allOf:
- - $ref: '#/components/schemas/ValidationIssue'
+ $ref: '#/components/schemas/ErrorResponse'
description: Authentication error
'404':
content:
application/json:
schema:
- type: object
- properties:
- message:
- type: string
- description: The message to return
- data:
- $ref: '#/components/schemas/ErrorResponse'
- status_code:
- type: integer
- description: The HTTP status code
- pagination_info:
- description: The pagination information for paginated endpoints
- type: *id011
- allOf:
- - $ref: '#/components/schemas/PaginationInfo'
- warnings:
- type: array
- items:
- type: *id012
- allOf:
- - $ref: '#/components/schemas/ValidationIssue'
+ $ref: '#/components/schemas/ErrorResponse'
description: Not found
tags:
- Opportunity v1
@@ -703,81 +345,19 @@ paths:
content:
application/json:
schema:
- type: object
- properties:
- message:
- type: string
- description: The message to return
- data:
- $ref: '#/components/schemas/OpportunityV01'
- status_code:
- type: integer
- description: The HTTP status code
- pagination_info:
- description: The pagination information for paginated endpoints
- type: &id013
- - object
- allOf:
- - $ref: '#/components/schemas/PaginationInfo'
- warnings:
- type: array
- items:
- type: &id014
- - object
- allOf:
- - $ref: '#/components/schemas/ValidationIssue'
+ $ref: '#/components/schemas/OpportunityGetResponseV01'
description: Successful response
'401':
content:
application/json:
schema:
- type: object
- properties:
- message:
- type: string
- description: The message to return
- data:
- $ref: '#/components/schemas/ErrorResponse'
- status_code:
- type: integer
- description: The HTTP status code
- pagination_info:
- description: The pagination information for paginated endpoints
- type: *id013
- allOf:
- - $ref: '#/components/schemas/PaginationInfo'
- warnings:
- type: array
- items:
- type: *id014
- allOf:
- - $ref: '#/components/schemas/ValidationIssue'
+ $ref: '#/components/schemas/ErrorResponse'
description: Authentication error
'404':
content:
application/json:
schema:
- type: object
- properties:
- message:
- type: string
- description: The message to return
- data:
- $ref: '#/components/schemas/ErrorResponse'
- status_code:
- type: integer
- description: The HTTP status code
- pagination_info:
- description: The pagination information for paginated endpoints
- type: *id013
- allOf:
- - $ref: '#/components/schemas/PaginationInfo'
- warnings:
- type: array
- items:
- type: *id014
- allOf:
- - $ref: '#/components/schemas/ValidationIssue'
+ $ref: '#/components/schemas/ErrorResponse'
description: Not found
tags:
- Opportunity v0.1
@@ -803,66 +383,51 @@ paths:
openapi: 3.1.0
components:
schemas:
- PaginationInfo:
+ HealthcheckResponse:
type: object
properties:
- page_offset:
- type: integer
- description: The page number that was fetched
- example: 1
- page_size:
- type: integer
- description: The size of the page fetched
- example: 25
- total_records:
- type: integer
- description: The total number of records fetchable
- example: 42
- total_pages:
- type: integer
- description: The total number of pages that can be fetched
- example: 2
- order_by:
+ message:
type: string
- description: The field that the records were sorted by
- example: id
- sort_direction:
- description: The direction the records are sorted
- enum:
- - ascending
- - descending
- type:
- - string
+ description: The message to return
+ example: Success
+ data:
+ example: null
+ status_code:
+ type: integer
+ description: The HTTP status code
+ example: 200
ValidationIssue:
type: object
properties:
type:
type: string
description: The type of error
+ example: invalid
message:
type: string
description: The message to return
+ example: Not a valid string.
field:
type: string
description: The field that failed
- Healthcheck:
- type: object
- properties:
- message:
- type: string
+ example: summary.summary_description
ErrorResponse:
type: object
properties:
+ data:
+ description: Additional data that might be useful in resolving an error
+ (see specific endpoints for details, this is used infrequently)
+ example: {}
message:
type: string
- description: The message to return
- data:
- description: The REST resource object
+ description: General description of the error
+ example: Error
status_code:
type: integer
- description: The HTTP status code
+ description: The HTTP status code of the error
errors:
type: array
+ example: []
items:
type:
- object
@@ -938,6 +503,36 @@ components:
required:
- paging
- sorting
+ PaginationInfo:
+ type: object
+ properties:
+ page_offset:
+ type: integer
+ description: The page number that was fetched
+ example: 1
+ page_size:
+ type: integer
+ description: The size of the page fetched
+ example: 25
+ total_records:
+ type: integer
+ description: The total number of records fetchable
+ example: 42
+ total_pages:
+ type: integer
+ description: The total number of pages that can be fetched
+ example: 2
+ order_by:
+ type: string
+ description: The field that the records were sorted by
+ example: id
+ sort_direction:
+ description: The direction the records are sorted
+ enum:
+ - ascending
+ - descending
+ type:
+ - string
OpportunityV0:
type: object
properties:
@@ -991,6 +586,27 @@ components:
type: string
format: date-time
readOnly: true
+ OpportunitySearchResponseV0:
+ type: object
+ properties:
+ pagination_info:
+ description: The pagination information for paginated endpoints
+ type: &id001
+ - object
+ allOf:
+ - $ref: '#/components/schemas/PaginationInfo'
+ message:
+ type: string
+ description: The message to return
+ example: Success
+ data:
+ type: array
+ items:
+ $ref: '#/components/schemas/OpportunityV0'
+ status_code:
+ type: integer
+ description: The HTTP status code
+ example: 200
FundingInstrumentFilterV1:
type: object
properties:
@@ -1440,6 +1056,26 @@ components:
type: string
format: date-time
readOnly: true
+ OpportunitySearchResponseV1:
+ type: object
+ properties:
+ pagination_info:
+ description: The pagination information for paginated endpoints
+ type: *id001
+ allOf:
+ - $ref: '#/components/schemas/PaginationInfo'
+ message:
+ type: string
+ description: The message to return
+ example: Success
+ data:
+ type: array
+ items:
+ $ref: '#/components/schemas/OpportunityV1'
+ status_code:
+ type: integer
+ description: The HTTP status code
+ example: 200
FundingInstrumentFilterV01:
type: object
properties:
@@ -1889,6 +1525,74 @@ components:
type: string
format: date-time
readOnly: true
+ OpportunitySearchResponseV01:
+ type: object
+ properties:
+ pagination_info:
+ description: The pagination information for paginated endpoints
+ type: *id001
+ allOf:
+ - $ref: '#/components/schemas/PaginationInfo'
+ message:
+ type: string
+ description: The message to return
+ example: Success
+ data:
+ type: array
+ items:
+ $ref: '#/components/schemas/OpportunityV01'
+ status_code:
+ type: integer
+ description: The HTTP status code
+ example: 200
+ OpportunityGetResponseV0:
+ type: object
+ properties:
+ message:
+ type: string
+ description: The message to return
+ example: Success
+ data:
+ type:
+ - object
+ allOf:
+ - $ref: '#/components/schemas/OpportunityV0'
+ status_code:
+ type: integer
+ description: The HTTP status code
+ example: 200
+ OpportunityGetResponseV1:
+ type: object
+ properties:
+ message:
+ type: string
+ description: The message to return
+ example: Success
+ data:
+ type:
+ - object
+ allOf:
+ - $ref: '#/components/schemas/OpportunityV1'
+ status_code:
+ type: integer
+ description: The HTTP status code
+ example: 200
+ OpportunityGetResponseV01:
+ type: object
+ properties:
+ message:
+ type: string
+ description: The message to return
+ example: Success
+ data:
+ type:
+ - object
+ allOf:
+ - $ref: '#/components/schemas/OpportunityV01'
+ status_code:
+ type: integer
+ description: The HTTP status code
+ example: 200
securitySchemes:
ApiKeyAuth:
type: apiKey
diff --git a/api/src/api/healthcheck.py b/api/src/api/healthcheck.py
index 22f944273..3740a5757 100644
--- a/api/src/api/healthcheck.py
+++ b/api/src/api/healthcheck.py
@@ -1,33 +1,39 @@
import logging
-from typing import Tuple
from apiflask import APIBlueprint
-from flask import current_app
from sqlalchemy import text
from werkzeug.exceptions import ServiceUnavailable
+import src.adapters.db as db
import src.adapters.db.flask_db as flask_db
from src.api import response
-from src.api.schemas.extension import Schema, fields
+from src.api.route_utils import raise_flask_error
+from src.api.schemas.extension import fields
+from src.api.schemas.response_schema import AbstractResponseSchema
logger = logging.getLogger(__name__)
-class HealthcheckSchema(Schema):
- message = fields.String()
+class HealthcheckResponseSchema(AbstractResponseSchema):
+ # We don't have any data to return with the healthcheck endpoint
+ data = fields.MixinField(metadata={"example": None})
healthcheck_blueprint = APIBlueprint("healthcheck", __name__, tag="Health")
@healthcheck_blueprint.get("/health")
-@healthcheck_blueprint.output(HealthcheckSchema)
+@healthcheck_blueprint.output(HealthcheckResponseSchema)
@healthcheck_blueprint.doc(responses=[200, ServiceUnavailable.code])
-def health() -> Tuple[response.ApiResponse, int]:
+@flask_db.with_db_session()
+def health(db_session: db.Session) -> response.ApiResponse:
try:
- with flask_db.get_db(current_app).get_connection() as conn:
- assert conn.scalar(text("SELECT 1 AS healthy")) == 1
- return response.ApiResponse(message="Service healthy"), 200
+ with db_session.begin():
+ if db_session.scalar(text("SELECT 1 AS healthy")) != 1:
+ raise Exception("Connection to Postgres DB failure")
+
except Exception:
logger.exception("Connection to DB failure")
- return response.ApiResponse(message="Service unavailable"), ServiceUnavailable.code
+ raise_flask_error(ServiceUnavailable.code, message="Service Unavailable")
+
+ return response.ApiResponse(message="Service healthy")
diff --git a/api/src/api/opportunities_v0/opportunity_routes.py b/api/src/api/opportunities_v0/opportunity_routes.py
index 07d46049d..675b77de8 100644
--- a/api/src/api/opportunities_v0/opportunity_routes.py
+++ b/api/src/api/opportunities_v0/opportunity_routes.py
@@ -34,7 +34,7 @@
arg_name="feature_flag_config",
)
# many=True allows us to return a list of opportunity objects
-@opportunity_blueprint.output(opportunity_schemas.OpportunityV0Schema(many=True))
+@opportunity_blueprint.output(opportunity_schemas.OpportunitySearchResponseV0Schema)
@opportunity_blueprint.auth_required(api_key_auth)
@opportunity_blueprint.doc(description=SHARED_ALPHA_DESCRIPTION)
@flask_db.with_db_session()
@@ -65,7 +65,7 @@ def opportunity_search(
@opportunity_blueprint.get("/opportunities/")
-@opportunity_blueprint.output(opportunity_schemas.OpportunityV0Schema)
+@opportunity_blueprint.output(opportunity_schemas.OpportunityGetResponseV0Schema)
@opportunity_blueprint.auth_required(api_key_auth)
@opportunity_blueprint.doc(description=SHARED_ALPHA_DESCRIPTION)
@flask_db.with_db_session()
diff --git a/api/src/api/opportunities_v0/opportunity_schemas.py b/api/src/api/opportunities_v0/opportunity_schemas.py
index 4a9aa6cbe..fd03e80dd 100644
--- a/api/src/api/opportunities_v0/opportunity_schemas.py
+++ b/api/src/api/opportunities_v0/opportunity_schemas.py
@@ -5,6 +5,7 @@
from src.api.feature_flags.feature_flag import FeatureFlag
from src.api.feature_flags.feature_flag_config import FeatureFlagConfig, get_feature_flag_config
from src.api.schemas.extension import Schema, fields
+from src.api.schemas.response_schema import AbstractResponseSchema, PaginationMixinSchema
from src.constants.lookup_constants import OpportunityCategoryLegacy
from src.pagination.pagination_schema import PaginationSchema, generate_sorting_schema
@@ -113,3 +114,11 @@ def post_load(self, data: dict, **kwargs: Any) -> FeatureFlagConfig:
feature_flag_config.enable_opportunity_log_msg = enable_opportunity_log_msg
return feature_flag_config
+
+
+class OpportunityGetResponseV0Schema(AbstractResponseSchema):
+ data = fields.Nested(OpportunityV0Schema())
+
+
+class OpportunitySearchResponseV0Schema(AbstractResponseSchema, PaginationMixinSchema):
+ data = fields.Nested(OpportunityV0Schema(many=True))
diff --git a/api/src/api/opportunities_v0_1/opportunity_routes.py b/api/src/api/opportunities_v0_1/opportunity_routes.py
index 6ae77d6d0..9006c9455 100644
--- a/api/src/api/opportunities_v0_1/opportunity_routes.py
+++ b/api/src/api/opportunities_v0_1/opportunity_routes.py
@@ -67,7 +67,7 @@
examples=examples,
)
# many=True allows us to return a list of opportunity objects
-@opportunity_blueprint.output(opportunity_schemas.OpportunityV01Schema(many=True))
+@opportunity_blueprint.output(opportunity_schemas.OpportunitySearchResponseV01Schema)
@opportunity_blueprint.auth_required(api_key_auth)
@opportunity_blueprint.doc(description=SHARED_ALPHA_DESCRIPTION)
@flask_db.with_db_session()
@@ -92,7 +92,7 @@ def opportunity_search(db_session: db.Session, search_params: dict) -> response.
@opportunity_blueprint.get("/opportunities/")
-@opportunity_blueprint.output(opportunity_schemas.OpportunityV01Schema)
+@opportunity_blueprint.output(opportunity_schemas.OpportunityGetResponseV01Schema)
@opportunity_blueprint.auth_required(api_key_auth)
@opportunity_blueprint.doc(description=SHARED_ALPHA_DESCRIPTION)
@flask_db.with_db_session()
diff --git a/api/src/api/opportunities_v0_1/opportunity_schemas.py b/api/src/api/opportunities_v0_1/opportunity_schemas.py
index a54384660..3e3c6c2fc 100644
--- a/api/src/api/opportunities_v0_1/opportunity_schemas.py
+++ b/api/src/api/opportunities_v0_1/opportunity_schemas.py
@@ -1,4 +1,5 @@
from src.api.schemas.extension import Schema, fields, validators
+from src.api.schemas.response_schema import AbstractResponseSchema, PaginationMixinSchema
from src.api.schemas.search_schema import StrSearchSchemaBuilder
from src.constants.lookup_constants import (
ApplicantType,
@@ -296,3 +297,11 @@ class OpportunitySearchRequestV01Schema(Schema):
),
required=True,
)
+
+
+class OpportunityGetResponseV01Schema(AbstractResponseSchema):
+ data = fields.Nested(OpportunityV01Schema())
+
+
+class OpportunitySearchResponseV01Schema(AbstractResponseSchema, PaginationMixinSchema):
+ data = fields.Nested(OpportunityV01Schema(many=True))
diff --git a/api/src/api/opportunities_v1/opportunity_routes.py b/api/src/api/opportunities_v1/opportunity_routes.py
index 0d94996b0..9f3e1b319 100644
--- a/api/src/api/opportunities_v1/opportunity_routes.py
+++ b/api/src/api/opportunities_v1/opportunity_routes.py
@@ -30,7 +30,7 @@
opportunity_schemas.OpportunitySearchRequestV1Schema, arg_name="search_params"
)
# many=True allows us to return a list of opportunity objects
-@opportunity_blueprint.output(opportunity_schemas.OpportunityV1Schema(many=True))
+@opportunity_blueprint.output(opportunity_schemas.OpportunitySearchResponseV1Schema)
@opportunity_blueprint.auth_required(api_key_auth)
@opportunity_blueprint.doc(description=SHARED_ALPHA_DESCRIPTION)
def opportunity_search(search_params: dict) -> response.ApiResponse:
@@ -53,7 +53,7 @@ def opportunity_search(search_params: dict) -> response.ApiResponse:
@opportunity_blueprint.get("/opportunities/")
-@opportunity_blueprint.output(opportunity_schemas.OpportunityV1Schema)
+@opportunity_blueprint.output(opportunity_schemas.OpportunityGetResponseV1Schema)
@opportunity_blueprint.auth_required(api_key_auth)
@opportunity_blueprint.doc(description=SHARED_ALPHA_DESCRIPTION)
@flask_db.with_db_session()
diff --git a/api/src/api/opportunities_v1/opportunity_schemas.py b/api/src/api/opportunities_v1/opportunity_schemas.py
index 5f72c7958..ebf47ebef 100644
--- a/api/src/api/opportunities_v1/opportunity_schemas.py
+++ b/api/src/api/opportunities_v1/opportunity_schemas.py
@@ -1,4 +1,5 @@
from src.api.schemas.extension import Schema, fields, validators
+from src.api.schemas.response_schema import AbstractResponseSchema, PaginationMixinSchema
from src.api.schemas.search_schema import StrSearchSchemaBuilder
from src.constants.lookup_constants import (
ApplicantType,
@@ -296,3 +297,11 @@ class OpportunitySearchRequestV1Schema(Schema):
),
required=True,
)
+
+
+class OpportunityGetResponseV1Schema(AbstractResponseSchema):
+ data = fields.Nested(OpportunityV1Schema())
+
+
+class OpportunitySearchResponseV1Schema(AbstractResponseSchema, PaginationMixinSchema):
+ data = fields.Nested(OpportunityV1Schema(many=True))
diff --git a/api/src/api/schemas/response_schema.py b/api/src/api/schemas/response_schema.py
index b6509699b..928ef19ab 100644
--- a/api/src/api/schemas/response_schema.py
+++ b/api/src/api/schemas/response_schema.py
@@ -3,25 +3,52 @@
class ValidationIssueSchema(Schema):
- type = fields.String(metadata={"description": "The type of error"})
- message = fields.String(metadata={"description": "The message to return"})
- field = fields.String(metadata={"description": "The field that failed"})
+ type = fields.String(metadata={"description": "The type of error", "example": "invalid"})
+ message = fields.String(
+ metadata={"description": "The message to return", "example": "Not a valid string."}
+ )
+ field = fields.String(
+ metadata={"description": "The field that failed", "example": "summary.summary_description"}
+ )
-class BaseResponseSchema(Schema):
- message = fields.String(metadata={"description": "The message to return"})
+class AbstractResponseSchema(Schema):
+ message = fields.String(metadata={"description": "The message to return", "example": "Success"})
data = fields.MixinField(metadata={"description": "The REST resource object"}, dump_default={})
- status_code = fields.Integer(metadata={"description": "The HTTP status code"}, dump_default=200)
+ status_code = fields.Integer(
+ metadata={"description": "The HTTP status code", "example": 200}, dump_default=200
+ )
-class ErrorResponseSchema(BaseResponseSchema):
- errors = fields.List(fields.Nested(ValidationIssueSchema()), dump_default=[])
+class WarningMixinSchema(Schema):
+ warnings = fields.List(
+ fields.Nested(ValidationIssueSchema()),
+ metadata={
+ "description": "A list of warnings - indicating something you may want to be aware of, but did not prevent handling of the request"
+ },
+ dump_default=[],
+ )
-class ResponseSchema(BaseResponseSchema):
+class PaginationMixinSchema(Schema):
pagination_info = fields.Nested(
PaginationInfoSchema(),
metadata={"description": "The pagination information for paginated endpoints"},
)
- warnings = fields.List(fields.Nested(ValidationIssueSchema()), dump_default=[])
+
+class ErrorResponseSchema(Schema):
+ data = fields.MixinField(
+ metadata={
+ "description": "Additional data that might be useful in resolving an error (see specific endpoints for details, this is used infrequently)",
+ "example": {},
+ },
+ dump_default={},
+ )
+ message = fields.String(
+ metadata={"description": "General description of the error", "example": "Error"}
+ )
+ status_code = fields.Integer(metadata={"description": "The HTTP status code of the error"})
+ errors = fields.List(
+ fields.Nested(ValidationIssueSchema()), metadata={"example": []}, dump_default=[]
+ )
diff --git a/api/src/app.py b/api/src/app.py
index b32322faa..98fea1607 100644
--- a/api/src/app.py
+++ b/api/src/app.py
@@ -67,7 +67,7 @@ def configure_app(app: APIFlask) -> None:
# Modify the response schema to instead use the format of our ApiResponse class
# which adds additional details to the object.
# https://apiflask.com/schema/#base-response-schema-customization
- app.config["BASE_RESPONSE_SCHEMA"] = response_schema.ResponseSchema
+ # app.config["BASE_RESPONSE_SCHEMA"] = response_schema.ResponseSchema
app.config["HTTP_ERROR_SCHEMA"] = response_schema.ErrorResponseSchema
app.config["VALIDATION_ERROR_SCHEMA"] = response_schema.ErrorResponseSchema
app.config["SWAGGER_UI_CSS"] = "/static/swagger-ui.min.css"
diff --git a/api/tests/src/api/test_healthcheck.py b/api/tests/src/api/test_healthcheck.py
index 1fe7fd533..70e699b99 100644
--- a/api/tests/src/api/test_healthcheck.py
+++ b/api/tests/src/api/test_healthcheck.py
@@ -4,6 +4,7 @@
def test_get_healthcheck_200(client):
response = client.get("/health")
assert response.status_code == 200
+ assert response.get_json()["message"] == "Service healthy"
def test_get_healthcheck_503_db_bad_state(client, monkeypatch):
@@ -11,8 +12,9 @@ def test_get_healthcheck_503_db_bad_state(client, monkeypatch):
def err_method(*args):
raise Exception("Fake Error")
- # Mock db.DB.get_session method to fail
- monkeypatch.setattr(db.DBClient, "get_connection", err_method)
+ # Mock db_session.Scalar to fail
+ monkeypatch.setattr(db.Session, "scalar", err_method)
response = client.get("/health")
assert response.status_code == 503
+ assert response.get_json()["message"] == "Service Unavailable"
diff --git a/api/tests/src/api/test_route_error_format.py b/api/tests/src/api/test_route_error_format.py
index 0b95f5529..faa025476 100644
--- a/api/tests/src/api/test_route_error_format.py
+++ b/api/tests/src/api/test_route_error_format.py
@@ -17,6 +17,7 @@
from src.api.response import ApiResponse, ValidationErrorDetail
from src.api.route_utils import raise_flask_error
from src.api.schemas.extension import Schema, fields
+from src.api.schemas.response_schema import AbstractResponseSchema, WarningMixinSchema
from src.auth.api_key_auth import api_key_auth
from src.util.dict_util import flatten_dict
from tests.src.schemas.schema_validation_utils import (
@@ -35,10 +36,14 @@ def header(api_auth_token):
return {"X-Auth": api_auth_token}
-class OutputSchema(Schema):
+class OutputData(Schema):
output_val = fields.String()
+class OutputSchema(AbstractResponseSchema, WarningMixinSchema):
+ data = fields.Nested(OutputData())
+
+
test_blueprint = APIBlueprint("test", __name__, tag="test")
@@ -111,7 +116,6 @@ def override(self):
assert resp.status_code == 500
resp_json = resp.get_json()
- assert resp_json["data"] == {}
assert resp_json["errors"] == []
assert resp_json["message"] == "Internal Server Error"
diff --git a/documentation/api/api-details.md b/documentation/api/api-details.md
index 14ee18ad2..d0cb4f62a 100644
--- a/documentation/api/api-details.md
+++ b/documentation/api/api-details.md
@@ -42,6 +42,7 @@ We would define the Marshmallow schema in-python like so:
```py
from enum import StrEnum
from src.api.schemas.extension import Schema, fields, validators
+from src.api.schemas.response_schema import AbstractResponseSchema
class Suffix(StrEnum):
SENIOR = "SR"
@@ -65,6 +66,10 @@ class NameSchema(Schema):
class ExampleSchema(Schema):
name = fields.Nested(NameSchema())
birth_date = fields.Date(metadata={"description": "Their birth date"})
+
+class ExampleResponseSchema(AbstractResponseSchema):
+ # Note that AbstractResponseSchema defines a message and status_code field as well
+ data = fields.Nested(ExampleSchema())
```
Anything specified in the metadata field is passed to the OpenAPI file that backs the swagger endpoint. The values
@@ -77,9 +82,8 @@ but it's recommended you try to populate the following:
You can specify validators that will be run when the request is being serialized by APIFlask
Defining a response works the exact same way however field validation does not occur on response, only formatting.
-The response schema only dictates the data portion of the response, the rest of the response is defined in
-[ResponseSchema](../../api/src/api/schemas/response_schema.py) which is connected to APIFlask via the `BASE_RESPONSE_SCHEMA` config.
-
+To keep our response schema following a consistent pattern, we have a few base schema classes like [AbstractResponseSchema](../../api/src/api/schemas/response_schema.py)
+that you can derive from for shared values like the message.
### Schema tips