diff --git a/static-site/.eslintrc.yaml b/static-site/.eslintrc.yaml index 88cd73d72..fb78f6089 100644 --- a/static-site/.eslintrc.yaml +++ b/static-site/.eslintrc.yaml @@ -10,7 +10,7 @@ extends: - eslint:recommended - plugin:react/recommended - plugin:react-hooks/recommended - - plugin:@typescript-eslint/recommended + - plugin:@typescript-eslint/strict # We use the recommended Next.js eslint configuration `next/core-web-vitals` as per # # As of April 2024, the extension simply adds the `@next/next/core-web-vitals` plugin: @@ -27,6 +27,7 @@ ignorePatterns: - public/ rules: + "@typescript-eslint/consistent-type-assertions": ["error", { assertionStyle: 'never' }] react/prop-types: off # Remove this override once all props have been typed using PropTypes or TypeScript. '@next/next/no-img-element': off # Remove this if we use next.js optimisations for '@next/next/no-html-link-for-pages': off diff --git a/static-site/app/blog/[id]/page.tsx b/static-site/app/blog/[id]/page.tsx index bb8e77262..343c5e408 100644 --- a/static-site/app/blog/[id]/page.tsx +++ b/static-site/app/blog/[id]/page.tsx @@ -27,6 +27,18 @@ export function generateStaticParams(): BlogPostParams[] { }); } +type PopulatedMetadata = Metadata & { + metadataBase: URL + openGraph: { + description: string + images: { url: string}[] + siteName: string + title: string + type: "website" + url: URL | string + } +} + // generate opengraph and other metadata tags export async function generateMetadata({ params, @@ -37,7 +49,7 @@ export async function generateMetadata({ // set up some defaults that are independent of the specific blog post const baseUrl = new URL(siteUrl); - const metadata: Metadata = { + const metadata: PopulatedMetadata = { metadataBase: baseUrl, openGraph: { description: siteTitleAlt, @@ -61,9 +73,9 @@ export async function generateMetadata({ metadata.title = blogPost.title; metadata.description = description; - metadata.openGraph!.description = description; - metadata.openGraph!.title = `${siteTitle}: ${blogPost.title}`; - metadata.openGraph!.url = `/blog/${blogPost.blogUrlName}`; + metadata.openGraph.description = description; + metadata.openGraph.title = `${siteTitle}: ${blogPost.title}`; + metadata.openGraph.url = `/blog/${blogPost.blogUrlName}`; } return metadata; diff --git a/static-site/src/components/ListResources/errors.tsx b/static-site/src/components/ErrorBoundary.tsx similarity index 87% rename from static-site/src/components/ListResources/errors.tsx rename to static-site/src/components/ErrorBoundary.tsx index c4bd9eeff..f1cadbb6e 100644 --- a/static-site/src/components/ListResources/errors.tsx +++ b/static-site/src/components/ErrorBoundary.tsx @@ -1,11 +1,7 @@ import React, { ErrorInfo, ReactNode } from "react"; -import { ErrorContainer } from "../../pages/404"; +import { ErrorContainer } from "../pages/404"; -export class InternalError extends Error { - constructor(message: string) { - super(message); - } -} +export class InternalError extends Error {} interface Props { children: ReactNode; diff --git a/static-site/src/components/Groups/Tiles/index.tsx b/static-site/src/components/Groups/Tiles/index.tsx index 518c50be6..4071b64e8 100644 --- a/static-site/src/components/Groups/Tiles/index.tsx +++ b/static-site/src/components/Groups/Tiles/index.tsx @@ -7,9 +7,18 @@ import { UserContext } from "../../../layouts/userDataWrapper"; import { GroupTile } from "./types"; import { Group } from "../types"; import { ExpandableTiles } from "../../ExpandableTiles"; +import { ErrorBoundary, InternalError } from "../../ErrorBoundary"; export const GroupTiles = () => { + return ( + + + + ); +}; + +const GroupTilesUnhandled = () => { const { visibleGroups } = useContext(UserContext); return ( a.name.localeCompare(b.name)) .map((group) => { - const groupColor = colors[0]!; - colors.push(colors.shift()!); + if (colors[0] === undefined) { + throw new InternalError("Colors are missing."); + } + const groupColor = colors[0]; + + // Rotate the colors + colors.push(colors.shift()!); // eslint-disable-line @typescript-eslint/no-non-null-assertion const tile: GroupTile = { img: "empty.png", diff --git a/static-site/src/components/ListResources/IndividualResource.tsx b/static-site/src/components/ListResources/IndividualResource.tsx index 2e1ad18f6..eaa73fd1d 100644 --- a/static-site/src/components/ListResources/IndividualResource.tsx +++ b/static-site/src/components/ListResources/IndividualResource.tsx @@ -5,7 +5,7 @@ import { MdHistory } from "react-icons/md"; import { SetModalResourceContext } from './Modal'; import { ResourceDisplayName, Resource, DisplayNamedResource } from './types'; import { IconType } from 'react-icons'; -import { InternalError } from './errors'; +import { InternalError } from '../ErrorBoundary'; export const LINK_COLOR = '#5097BA' export const LINK_HOVER_COLOR = '#31586c' @@ -139,9 +139,17 @@ export const IndividualResource = ({ throw new InternalError("ref must be defined and the parent must be a div (IndividualResourceContainer)."); } + // The type of ref.current.parentNode is ParentNode which does not have an + // offsetTop property. I don't think there is a way to appease the + // TypeScript compiler other than a type assertion. It is loosely coupled + // to the check above for parentNode.nodeName. + // Note: this doesn't strictly have to be a div, but that's what it is in + // current usage of the component at the time of writing. + const parentNode = ref.current.parentNode as HTMLDivElement // eslint-disable-line @typescript-eslint/consistent-type-assertions + /* The column CSS is great but doesn't allow us to know if an element is at the top of its column, so we resort to JS */ - if (ref.current.offsetTop===(ref.current.parentNode as HTMLDivElement).offsetTop) { + if (ref.current.offsetTop===parentNode.offsetTop) { setTopOfColumn(true); } }, []); diff --git a/static-site/src/components/ListResources/Modal.tsx b/static-site/src/components/ListResources/Modal.tsx index e94bca1fb..49db5664c 100644 --- a/static-site/src/components/ListResources/Modal.tsx +++ b/static-site/src/components/ListResources/Modal.tsx @@ -5,7 +5,7 @@ import * as d3 from "d3"; import { MdClose } from "react-icons/md"; import { dodge } from "./dodge"; import { Resource, VersionedResource } from './types'; -import { InternalError } from './errors'; +import { InternalError } from '../ErrorBoundary'; export const SetModalResourceContext = createContext> | null>(null); diff --git a/static-site/src/components/ListResources/ResourceGroup.tsx b/static-site/src/components/ListResources/ResourceGroup.tsx index 5a9345aba..01309086d 100644 --- a/static-site/src/components/ListResources/ResourceGroup.tsx +++ b/static-site/src/components/ListResources/ResourceGroup.tsx @@ -6,7 +6,7 @@ import { IndividualResource, getMaxResourceWidth, TooltipWrapper, IconContainer, ResourceLinkWrapper, ResourceLink, LINK_COLOR, LINK_HOVER_COLOR } from "./IndividualResource" import { SetModalResourceContext } from "./Modal"; import { DisplayNamedResource, Group, QuickLink, Resource } from './types'; -import { InternalError } from './errors'; +import { InternalError } from '../ErrorBoundary'; const ResourceGroupHeader = ({ group, diff --git a/static-site/src/components/ListResources/index.tsx b/static-site/src/components/ListResources/index.tsx index 853d2e7f7..d69ba71ff 100644 --- a/static-site/src/components/ListResources/index.tsx +++ b/static-site/src/components/ListResources/index.tsx @@ -14,7 +14,7 @@ import {ResourceModal, SetModalResourceContext} from "./Modal"; import { ExpandableTiles } from "../ExpandableTiles"; import { FilterTile, FilterOption, Group, QuickLink, Resource, ResourceListingInfo, SortMethod, convertVersionedResource } from './types'; import { HugeSpacer } from "../../layouts/generalComponents"; -import { ErrorBoundary } from './errors'; +import { ErrorBoundary, InternalError } from '../ErrorBoundary'; const LIST_ANCHOR = "list"; @@ -182,7 +182,12 @@ function SortOptions({sortMethod, changeSortMethod}: { changeSortMethod: React.Dispatch>, }) { function onChangeValue(event: FormEvent): void { - changeSortMethod(event.currentTarget.value as SortMethod); + const sortMethod = event.currentTarget.value; + if (sortMethod !== "alphabetical" && + sortMethod !== "lastUpdated") { + throw new InternalError(`Unhandled sort method: '${sortMethod}'`); + } + changeSortMethod(sortMethod); } return ( diff --git a/static-site/src/components/ListResources/types.ts b/static-site/src/components/ListResources/types.ts index 57c8e5c64..02390fe97 100644 --- a/static-site/src/components/ListResources/types.ts +++ b/static-site/src/components/ListResources/types.ts @@ -1,4 +1,4 @@ -import { InternalError } from "./errors"; +import { InternalError } from "../ErrorBoundary"; import { Tile } from "../ExpandableTiles/types" export interface FilterOption { diff --git a/static-site/src/components/ListResources/useDataFetch.ts b/static-site/src/components/ListResources/useDataFetch.ts index 939aaf45f..c18db4399 100644 --- a/static-site/src/components/ListResources/useDataFetch.ts +++ b/static-site/src/components/ListResources/useDataFetch.ts @@ -1,5 +1,6 @@ import { useState, useEffect } from 'react'; import { Group, Resource, ResourceListingInfo } from './types'; +import { InternalError } from '../ErrorBoundary'; /** @@ -65,8 +66,16 @@ function partitionByPathogen( const sortedDates = [...dates].sort(); const nameParts = name.split('/'); - // split() will always return at least 1 string - const groupName = nameParts[0]!; + + if (nameParts[0] === undefined) { + throw new InternalError(`Name is not properly formatted: '${name}'`); + } + + if (sortedDates[0] === undefined) { + throw new InternalError("Resource does not have any dates."); + } + + const groupName = nameParts[0]; const resourceDetails: Resource = { name, @@ -77,14 +86,13 @@ function partitionByPathogen( lastUpdated: sortedDates.at(-1), }; if (versioned) { - resourceDetails.firstUpdated = sortedDates[0]!; + resourceDetails.firstUpdated = sortedDates[0]; resourceDetails.dates = sortedDates; resourceDetails.nVersions = sortedDates.length; resourceDetails.updateCadence = updateCadence(sortedDates.map((date)=> new Date(date))); } - if (!store[groupName]) store[groupName] = []; - store[groupName]!.push(resourceDetails) + (store[groupName] ??= []).push(resourceDetails) return store; }, {}); diff --git a/static-site/src/components/splash/index.tsx b/static-site/src/components/splash/index.tsx index bc6ee8b05..ac1bdf92d 100644 --- a/static-site/src/components/splash/index.tsx +++ b/static-site/src/components/splash/index.tsx @@ -116,6 +116,7 @@ const Splash = () => { + {/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */} diff --git a/static-site/src/sections/group-members-page.tsx b/static-site/src/sections/group-members-page.tsx index 00776938c..3b8f6d48a 100644 --- a/static-site/src/sections/group-members-page.tsx +++ b/static-site/src/sections/group-members-page.tsx @@ -40,7 +40,7 @@ const GroupMembersPage = ({ groupName }: {groupName: string}) => { roles = await rolesResponse.json(); members = await membersResponse.json(); } catch (err) { - const errorMessage = (err as Error).message + const errorMessage = err instanceof Error ? err.message : String(err) if(!ignore) { setErrorMessage({ title: "An error occurred when trying to fetch group membership data", @@ -77,7 +77,7 @@ const GroupMembersPage = ({ groupName }: {groupName: string}) => { {roles && members - ? + ? : Fetching group members...} ) @@ -150,7 +150,7 @@ export async function canViewGroupMembers(groupName: string) { const allowedMethods = new Set(groupMemberOptions.headers.get("Allow")?.split(/\s*,\s*/)); return allowedMethods.has("GET"); } catch (err) { - const errorMessage = (err as Error).message + const errorMessage = err instanceof Error ? err.message : String(err) console.error("Cannot check user permissions to view group members", errorMessage); } return false