diff --git a/package.json b/package.json index 6516c76..1c34046 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@code4ro/reusable-components", - "version": "0.1.1", + "version": "0.1.2", "description": "Component library for code4ro", "keywords": [ "code4ro", diff --git a/src/components/BarChart/BarChart.tsx b/src/components/BarChart/BarChart.tsx index cfd7227..ead8faf 100644 --- a/src/components/BarChart/BarChart.tsx +++ b/src/components/BarChart/BarChart.tsx @@ -1,5 +1,5 @@ import React, { ComponentType, ReactNode } from "react"; -import { themable, ThemableComponentProps } from "../../util/theme"; +import { themable, ThemableComponentProps } from "../../hooks/theme"; type Props = { width: number; diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index 21bd2d4..c07cd62 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -1,5 +1,5 @@ import React, { ComponentProps, forwardRef } from "react"; -import { themable, ThemedComponent, ThemedComponentProps } from "../../util/theme"; +import { themable, ThemedComponent, ThemedComponentProps } from "../../hooks/theme"; import cssClasses from "./Button.module.scss"; export const Button = themable>( diff --git a/src/components/ColoredSquare/ColoredSquare.tsx b/src/components/ColoredSquare/ColoredSquare.tsx index 6215cb5..d33dddf 100644 --- a/src/components/ColoredSquare/ColoredSquare.tsx +++ b/src/components/ColoredSquare/ColoredSquare.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { themable } from "../../util/theme"; +import { themable } from "../../hooks/theme"; import cssClasses from "./ColoredSquare.module.scss"; type Props = { diff --git a/src/components/ElectionMap/ElectionMap.tsx b/src/components/ElectionMap/ElectionMap.tsx index 32c55b1..20b7416 100644 --- a/src/components/ElectionMap/ElectionMap.tsx +++ b/src/components/ElectionMap/ElectionMap.tsx @@ -1,6 +1,6 @@ import React, { PropsWithChildren, useMemo } from "react"; import { ElectionScopeIncomplete } from "../../types/Election"; -import { themable } from "../../util/theme"; +import { themable } from "../../hooks/theme"; import cssClasses from "./ElectionMap.module.scss"; import RomaniaMap from "../../assets/romania-map.svg"; import WorldMap from "../../assets/world-map.svg"; diff --git a/src/components/ElectionObservationSection/ElectionObservationSection.tsx b/src/components/ElectionObservationSection/ElectionObservationSection.tsx index 1870478..f2ca600 100644 --- a/src/components/ElectionObservationSection/ElectionObservationSection.tsx +++ b/src/components/ElectionObservationSection/ElectionObservationSection.tsx @@ -1,7 +1,7 @@ import React, { PropsWithChildren, ReactNode } from "react"; import { ElectionObservation } from "../../types/Election"; import { formatGroupedNumber } from "../../util/format"; -import { ClassNames, themable, useTheme } from "../../util/theme"; +import { ClassNames, themable, useTheme } from "../../hooks/theme"; import { DivBodyLarge, Heading2 } from "../Typography/Typography"; import cssClasses from "./ElectionObservationSection.module.scss"; import BallotDrop from "../../assets/ballot-drop.svg"; diff --git a/src/components/ElectionResultsProcess/ElectionResultsProcess.tsx b/src/components/ElectionResultsProcess/ElectionResultsProcess.tsx index d2b3d86..6fff25f 100644 --- a/src/components/ElectionResultsProcess/ElectionResultsProcess.tsx +++ b/src/components/ElectionResultsProcess/ElectionResultsProcess.tsx @@ -1,7 +1,7 @@ import React, { PropsWithChildren, ReactNode } from "react"; import { ElectionResults } from "../../types/Election"; import { formatGroupedNumber } from "../../util/format"; -import { ClassNames, themable } from "../../util/theme"; +import { ClassNames, themable } from "../../hooks/theme"; import { DivBodyHuge, Heading2 } from "../Typography/Typography"; import cssClasses from "./ElectionResultsProcess.module.scss"; import BallotFillIn from "../../assets/ballot-fill-in.svg"; diff --git a/src/components/ElectionResultsSeats/ElectionResultsSeats.tsx b/src/components/ElectionResultsSeats/ElectionResultsSeats.tsx index e15de4d..f762b78 100644 --- a/src/components/ElectionResultsSeats/ElectionResultsSeats.tsx +++ b/src/components/ElectionResultsSeats/ElectionResultsSeats.tsx @@ -1,7 +1,7 @@ import React, { memo, useCallback, useMemo, useState } from "react"; import { ElectionResults } from "../../types/Election"; import { electionCandidateColor, formatGroupedNumber } from "../../util/format"; -import { ClassNames, mergeClasses, themable } from "../../util/theme"; +import { ClassNames, mergeClasses, themable } from "../../hooks/theme"; import { ColoredSquare } from "../ColoredSquare/ColoredSquare"; import { DivBody, DivLabel } from "../Typography/Typography"; import useDimensions from "react-use-dimensions"; diff --git a/src/components/ElectionResultsStackedBar/ElectionResultsStackedBar.tsx b/src/components/ElectionResultsStackedBar/ElectionResultsStackedBar.tsx index 4d934cf..bb0a63c 100644 --- a/src/components/ElectionResultsStackedBar/ElectionResultsStackedBar.tsx +++ b/src/components/ElectionResultsStackedBar/ElectionResultsStackedBar.tsx @@ -1,6 +1,6 @@ import React, { useMemo } from "react"; import { ElectionResults } from "../../types/Election"; -import { themable } from "../../util/theme"; +import { themable } from "../../hooks/theme"; import { HorizontalStackedBar, HorizontalStackedBarItem } from "../HorizontalStackedBar/HorizontalStackedBar"; import { PartyResultCard } from "../PartyResultCard/PartyResultCard"; import { PartyResultInline } from "../PartyResultInline/PartyResultInline"; diff --git a/src/components/ElectionResultsSummarySection/ElectionResultsSummarySection.tsx b/src/components/ElectionResultsSummarySection/ElectionResultsSummarySection.tsx index 0aeb4ae..9872a04 100644 --- a/src/components/ElectionResultsSummarySection/ElectionResultsSummarySection.tsx +++ b/src/components/ElectionResultsSummarySection/ElectionResultsSummarySection.tsx @@ -6,7 +6,7 @@ import { electionScopeIsComplete, electionTypeInvolvesDiaspora, } from "../../types/Election"; -import { themable } from "../../util/theme"; +import { themable } from "../../hooks/theme"; import useDimensions from "react-use-dimensions"; import cssClasses from "./ElectionResultsSummarySection.module.scss"; import { ElectionResultsStackedBar } from "../ElectionResultsStackedBar/ElectionResultsStackedBar"; @@ -17,7 +17,7 @@ import { ElectionScopeIncompleteWarning } from "../Warning/ElectionScopeIncomple import { ElectionResultsSummaryTable } from "../ElectionResultsSummaryTable/ElectionResultsSummaryTable"; type Props = { - meta: ElectionBallotMeta; + meta?: ElectionBallotMeta; scope: ElectionScopeIncompleteResolved; results?: ElectionResults | null; separator?: ReactNode; @@ -33,7 +33,7 @@ export const ElectionResultsSummarySection = themable( cssClasses, defaultConstants, )(({ classes, results, meta, scope, constants, separator }) => { - const involvesDiaspora = electionTypeInvolvesDiaspora(meta.type); + const involvesDiaspora = !!meta && electionTypeInvolvesDiaspora(meta.type); const [measureRef, { width }] = useDimensions(); @@ -74,7 +74,7 @@ export const ElectionResultsSummarySection = themable( {results && !mobileMap && separator} {!mobileMap && (
- {!fullWidthMap && results && ( + {!fullWidthMap && meta && results && ( )} {map} diff --git a/src/components/ElectionResultsSummaryTable/ElectionResultsSummaryTable.tsx b/src/components/ElectionResultsSummaryTable/ElectionResultsSummaryTable.tsx index 46b441e..8321d52 100644 --- a/src/components/ElectionResultsSummaryTable/ElectionResultsSummaryTable.tsx +++ b/src/components/ElectionResultsSummaryTable/ElectionResultsSummaryTable.tsx @@ -1,6 +1,6 @@ import React from "react"; import { electionHasSeats, ElectionBallotMeta, ElectionResults } from "../../types/Election"; -import { themable } from "../../util/theme"; +import { themable } from "../../hooks/theme"; import cssClasses from "./ElectionResultsSummaryTable.module.scss"; import { DivBody, Heading3, makeTypographyComponent } from "../Typography/Typography"; import { lightFormat, parseISO } from "date-fns"; diff --git a/src/components/ElectionResultsTableSection/ElectionResultsTableSection.tsx b/src/components/ElectionResultsTableSection/ElectionResultsTableSection.tsx index c6235bf..0e4b924 100644 --- a/src/components/ElectionResultsTableSection/ElectionResultsTableSection.tsx +++ b/src/components/ElectionResultsTableSection/ElectionResultsTableSection.tsx @@ -3,7 +3,7 @@ import { ResultsTable } from "../ResultsTable/ResultsTable"; import { Heading2 } from "../Typography/Typography"; import { electionHasSeats, ElectionBallotMeta, ElectionResults, ElectionResultsCandidate } from "../../types/Election"; import { formatGroupedNumber, formatPercentage, fractionOf } from "../../util/format"; -import { ClassNames, themable } from "../../util/theme"; +import { ClassNames, themable } from "../../hooks/theme"; import cssClasses from "./ElectionResultsTableSection.module.scss"; import { Button } from "../Button/Button"; diff --git a/src/components/ElectionScopePicker/ElectionScopePicker.tsx b/src/components/ElectionScopePicker/ElectionScopePicker.tsx index 0ccd5e2..73f55f9 100644 --- a/src/components/ElectionScopePicker/ElectionScopePicker.tsx +++ b/src/components/ElectionScopePicker/ElectionScopePicker.tsx @@ -1,11 +1,12 @@ import React, { useCallback, useMemo } from "react"; import Select from "react-select"; import { ElectionScope, ElectionScopeIncomplete } from "../../types/Election"; -import { APIRequestState, useApiResponse } from "../../util/api"; +import { APIRequestState } from "../../util/api"; import { ElectionScopeAPI, OptionWithID } from "../../util/electionApi"; -import { themable, useTheme } from "../../util/theme"; +import { themable, useTheme } from "../../hooks/theme"; import { Label } from "../Typography/Typography"; import cssClasses from "./ElectionScopePicker.module.scss"; +import { useApiResponse } from "../../hooks/useApiResponse"; type Props = { apiData: ElectionScopePickerAPIData; diff --git a/src/components/ElectionTimeline/ElectionTimeline.tsx b/src/components/ElectionTimeline/ElectionTimeline.tsx index 7ed5561..e1c78d2 100644 --- a/src/components/ElectionTimeline/ElectionTimeline.tsx +++ b/src/components/ElectionTimeline/ElectionTimeline.tsx @@ -1,7 +1,7 @@ import { parseISO } from "date-fns"; import React, { useEffect, useMemo, useState } from "react"; import { ElectionBallotMeta } from "../../types/Election"; -import { mergeClasses, themable } from "../../util/theme"; +import { mergeClasses, themable } from "../../hooks/theme"; import cssClasses from "./ElectionTimeline.module.scss"; type Props = { diff --git a/src/components/ElectionTurnoutBars/ElectionTurnoutBars.tsx b/src/components/ElectionTurnoutBars/ElectionTurnoutBars.tsx index 104f05e..8e9152b 100644 --- a/src/components/ElectionTurnoutBars/ElectionTurnoutBars.tsx +++ b/src/components/ElectionTurnoutBars/ElectionTurnoutBars.tsx @@ -1,6 +1,6 @@ import React from "react"; import { formatGroupedNumber, formatPercentage } from "../../util/format"; -import { themable, useTheme } from "../../util/theme"; +import { themable, useTheme } from "../../hooks/theme"; import { PercentageBars } from "../PercentageBars/PercentageBars"; import { PercentageBarsLegend } from "../PercentageBarsLegend/PercentageBarsLegend"; diff --git a/src/components/ElectionTurnoutBreakdownChart/ElectionTurnoutBreakdownChart.tsx b/src/components/ElectionTurnoutBreakdownChart/ElectionTurnoutBreakdownChart.tsx index 8d8c92e..d534a31 100644 --- a/src/components/ElectionTurnoutBreakdownChart/ElectionTurnoutBreakdownChart.tsx +++ b/src/components/ElectionTurnoutBreakdownChart/ElectionTurnoutBreakdownChart.tsx @@ -1,7 +1,7 @@ import React from "react"; import { ElectionScope, ElectionTurnoutBreakdown } from "../../types/Election"; import { formatGroupedNumber, formatPercentage } from "../../util/format"; -import { themable } from "../../util/theme"; +import { themable } from "../../hooks/theme"; import { BarChart } from "../BarChart/BarChart"; import { PercentageBarsLegend } from "../PercentageBarsLegend/PercentageBarsLegend"; import cssClasses from "./ElectionTurnoutBreakdownChart.module.scss"; diff --git a/src/components/ElectionTurnoutSection/ElectionTurnoutSection.tsx b/src/components/ElectionTurnoutSection/ElectionTurnoutSection.tsx index 4c2993c..a50d511 100644 --- a/src/components/ElectionTurnoutSection/ElectionTurnoutSection.tsx +++ b/src/components/ElectionTurnoutSection/ElectionTurnoutSection.tsx @@ -7,7 +7,7 @@ import { electionTypeInvolvesDiaspora, } from "../../types/Election"; import { formatGroupedNumber, formatPercentage, getScopeName } from "../../util/format"; -import { mergeClasses, themable } from "../../util/theme"; +import { mergeClasses, themable } from "../../hooks/theme"; import { ElectionMap } from "../ElectionMap/ElectionMap"; import { ElectionTurnoutBars } from "../ElectionTurnoutBars/ElectionTurnoutBars"; import { ElectionTurnoutBreakdownChart } from "../ElectionTurnoutBreakdownChart/ElectionTurnoutBreakdownChart"; @@ -18,7 +18,7 @@ import BallotCheckmark from "../../assets/ballot-checkmark.svg"; import { ElectionScopeIncompleteWarning } from "../Warning/ElectionScopeIncompleteWarning"; type Props = { - meta: ElectionBallotMeta; + meta?: ElectionBallotMeta; scope: ElectionScopeIncompleteResolved; turnout?: ElectionTurnout | null; }; @@ -34,7 +34,7 @@ export const ElectionTurnoutSection = themable( cssClasses, defaultConstants, )(({ meta, scope, turnout, classes, constants }) => { - const involvesDiaspora = electionTypeInvolvesDiaspora(meta.type); + const involvesDiaspora = !!meta && electionTypeInvolvesDiaspora(meta.type); const [measureRef, { width }] = useDimensions(); diff --git a/src/components/HereMap/HereMap.tsx b/src/components/HereMap/HereMap.tsx index a5e9185..b02f5f9 100644 --- a/src/components/HereMap/HereMap.tsx +++ b/src/components/HereMap/HereMap.tsx @@ -1,5 +1,5 @@ import React, { createContext, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; -import { themable } from "../../util/theme"; +import { themable } from "../../hooks/theme"; import cssClasses from "./HereMap.module.scss"; type OnFeatureSelect = (featureId: number) => unknown; diff --git a/src/components/HorizontalStackedBar/HorizontalStackedBar.tsx b/src/components/HorizontalStackedBar/HorizontalStackedBar.tsx index ae30d57..cdba750 100644 --- a/src/components/HorizontalStackedBar/HorizontalStackedBar.tsx +++ b/src/components/HorizontalStackedBar/HorizontalStackedBar.tsx @@ -1,6 +1,6 @@ import React, { ReactNode } from "react"; import cssClasses from "./HorizontalStackedBar.module.scss"; -import { themable } from "../../util/theme"; +import { themable } from "../../hooks/theme"; import { DivBody } from "../Typography/Typography"; export type HorizontalStackedBarItem = { diff --git a/src/components/PartyResultCard/PartyResultCard.tsx b/src/components/PartyResultCard/PartyResultCard.tsx index 35030a9..ad16c51 100644 --- a/src/components/PartyResultCard/PartyResultCard.tsx +++ b/src/components/PartyResultCard/PartyResultCard.tsx @@ -1,6 +1,6 @@ import React from "react"; import cssClasses from "./PartyResultCard.module.scss"; -import { mergeClasses, themable } from "../../util/theme"; +import { mergeClasses, themable } from "../../hooks/theme"; import { formatPercentage } from "../../util/format"; import { DivBodyMedium, DivHeading1 } from "../Typography/Typography"; import { ColoredSquare } from "../ColoredSquare/ColoredSquare"; diff --git a/src/components/PartyResultInline/PartyResultInline.tsx b/src/components/PartyResultInline/PartyResultInline.tsx index 51d0db0..fe7a40c 100644 --- a/src/components/PartyResultInline/PartyResultInline.tsx +++ b/src/components/PartyResultInline/PartyResultInline.tsx @@ -1,6 +1,6 @@ import React from "react"; import { formatGroupedNumber, formatPercentage } from "../../util/format"; -import { themable } from "../../util/theme"; +import { themable } from "../../hooks/theme"; import { ColoredSquare } from "../ColoredSquare/ColoredSquare"; import { DivBody, Label } from "../Typography/Typography"; import cssClasses from "./PartyResultInline.module.scss"; diff --git a/src/components/PercentageBars/PercentageBars.tsx b/src/components/PercentageBars/PercentageBars.tsx index 5f8e503..249cdc6 100644 --- a/src/components/PercentageBars/PercentageBars.tsx +++ b/src/components/PercentageBars/PercentageBars.tsx @@ -1,6 +1,6 @@ import React, { ReactNode } from "react"; import cssClasses from "./PercentageBars.module.scss"; -import { mergeClasses, themable } from "../../util/theme"; +import { mergeClasses, themable } from "../../hooks/theme"; type Props = { total?: number; // Defaults to the max value in items diff --git a/src/components/PercentageBarsLegend/PercentageBarsLegend.tsx b/src/components/PercentageBarsLegend/PercentageBarsLegend.tsx index 0f23b7c..527cb06 100644 --- a/src/components/PercentageBarsLegend/PercentageBarsLegend.tsx +++ b/src/components/PercentageBarsLegend/PercentageBarsLegend.tsx @@ -1,6 +1,6 @@ import React, { ReactNode } from "react"; import cssClasses from "./PercentageBarsLegend.module.scss"; -import { mergeClasses, themable } from "../../util/theme"; +import { mergeClasses, themable } from "../../hooks/theme"; import { DivBody } from "../Typography/Typography"; type Props = { diff --git a/src/components/ResultsTable/ResultsTable.tsx b/src/components/ResultsTable/ResultsTable.tsx index 2bfbea5..11e161b 100644 --- a/src/components/ResultsTable/ResultsTable.tsx +++ b/src/components/ResultsTable/ResultsTable.tsx @@ -1,5 +1,5 @@ import React, { ComponentProps, forwardRef } from "react"; -import { themable, ThemedComponent, ThemedComponentProps } from "../../util/theme"; +import { themable, ThemedComponent, ThemedComponentProps } from "../../hooks/theme"; import cssClasses from "./ResultsTable.module.scss"; export const ResultsTable = themable>( diff --git a/src/components/Typography/Typography.tsx b/src/components/Typography/Typography.tsx index 4614273..a7ee921 100644 --- a/src/components/Typography/Typography.tsx +++ b/src/components/Typography/Typography.tsx @@ -1,5 +1,5 @@ import React, { ComponentProps, ComponentType, forwardRef } from "react"; -import { mergeClasses, PropsObject, themable, ThemableComponent, ThemedComponentProps } from "../../util/theme"; +import { mergeClasses, PropsObject, themable, ThemableComponent, ThemedComponentProps } from "../../hooks/theme"; import cssClasses from "./Typography.module.scss"; export function makeTypographyComponent( diff --git a/src/hooks/electionApiHooks.tsx b/src/hooks/electionApiHooks.tsx new file mode 100644 index 0000000..7336413 --- /dev/null +++ b/src/hooks/electionApiHooks.tsx @@ -0,0 +1,36 @@ +import { useEffect, useState } from "react"; +import { ElectionBallot, ElectionScope } from "../types/Election"; +import { APIRequestState } from "../util/api"; +import { ElectionAPI } from "../util/electionApi"; +import { useApiResponse } from "./useApiResponse"; + +export const useBallotData = ( + api: ElectionAPI, + ballotId: number | null, + scope: ElectionScope | null, + autoRefreshInterval: number = 60 * 1000, +): APIRequestState => { + const [timerToken, setTimerToken] = useState(false); + + const state = useApiResponse(() => (ballotId != null && scope ? api.getBallot(ballotId, scope) : null), [ + scope, + ballotId, + timerToken, + ]); + + // Changing the timerToken will re-trigger useApiResponse + useEffect(() => { + if (state.loading || !state.data?.meta.live) { + return undefined; + } + + const timerId = setTimeout(() => { + setTimerToken((x) => !x); + }, autoRefreshInterval); + + return () => { + clearTimeout(timerId); + }; + }, [state.data, state.loading, timerToken, autoRefreshInterval]); + return state; +}; diff --git a/src/util/theme.tsx b/src/hooks/theme.tsx similarity index 100% rename from src/util/theme.tsx rename to src/hooks/theme.tsx diff --git a/src/hooks/useApiResponse.ts b/src/hooks/useApiResponse.ts new file mode 100644 index 0000000..5cd3d20 --- /dev/null +++ b/src/hooks/useApiResponse.ts @@ -0,0 +1,76 @@ +import { useEffect, useState } from "react"; +import { APIInvocation, APIRequestState } from "../util/api"; + +export type UseAPIResponseOptions = { + invocation?: APIInvocation | null; + discardPreviousData?: boolean; // Defaults to false + discardDataOnError?: boolean; // Defaults to false + abortPreviousRequest?: boolean; // Defaults to true +}; + +export function useApiResponse( + makeInvocation: () => APIInvocation | UseAPIResponseOptions | undefined | void | null, + dependencies: unknown[], +): APIRequestState { + const [state, setState] = useState>({ + data: null, + hasData: false, + loading: false, + error: null, + }); + + useEffect(() => { + const inv = makeInvocation(); + const options: UseAPIResponseOptions = (typeof inv === "function" ? { invocation: inv } : inv) || {}; + const { + invocation, + discardPreviousData = false, + discardDataOnError = false, + abortPreviousRequest = true, + } = options; + + if (!invocation && !discardPreviousData) return; + + setState((prevState) => ({ + data: discardPreviousData ? null : prevState.data, + hasData: discardPreviousData ? false : prevState.hasData, + loading: invocation != null, + error: invocation != null || discardPreviousData ? null : prevState.error, + })); + + if (!invocation) return; + + const abortController = new AbortController(); + + let mounted = true; + invocation(abortController.signal).then( + (data) => { + if (!mounted) return; + setState({ + data, + hasData: true, + loading: false, + error: null, + }); + }, + (error) => { + if (!mounted) return; + setState((prevState) => ({ + data: discardDataOnError ? null : prevState.data, + hasData: discardDataOnError ? false : prevState.hasData, + loading: false, + error, + })); + }, + ); + + return () => { + mounted = false; + if (abortPreviousRequest) { + abortController.abort(); + } + }; + }, dependencies); + + return state; +} diff --git a/src/index.ts b/src/index.ts index 61c426b..fb9e601 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,14 @@ -export * from "./util/theme"; export * from "./util/api"; export * from "./util/electionApi"; + export * from "./constants/servers"; + export * from "./types/Election"; +export * from "./hooks/theme"; +export * from "./hooks/useApiResponse"; +export * from "./hooks/electionApiHooks"; + export * from "./components/Typography/Typography"; export * from "./components/Button/Button"; export * from "./components/ColoredSquare/ColoredSquare"; diff --git a/src/stories/APIIntegration.stories.tsx b/src/stories/APIIntegration.stories.tsx index 84bb397..d80cd7e 100644 --- a/src/stories/APIIntegration.stories.tsx +++ b/src/stories/APIIntegration.stories.tsx @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import React, { useMemo, useState } from "react"; -import { useApiResponse } from "../util/api"; +import { useApiResponse } from "../hooks/useApiResponse"; import { ElectionAPI, makeElectionApi } from "../util/electionApi"; import { mockElectionAPI } from "../util/mocks"; import { ElectionObservationSection } from "../components/ElectionObservationSection/ElectionObservationSection"; diff --git a/src/util/api.ts b/src/util/api.ts index f49c7ce..c88d9da 100644 --- a/src/util/api.ts +++ b/src/util/api.ts @@ -1,5 +1,3 @@ -import { useEffect, useState } from "react"; - export type APIInvocation = (abortSignal?: AbortSignal) => Promise; export type APIRequestState = { @@ -9,80 +7,6 @@ export type APIRequestState = { error: Error | null; }; -export type UseAPIResponseOptions = { - invocation?: APIInvocation | null; - discardPreviousData?: boolean; // Defaults to false - discardDataOnError?: boolean; // Defaults to false - abortPreviousRequest?: boolean; // Defaults to true -}; - -export function useApiResponse( - makeInvocation: () => APIInvocation | UseAPIResponseOptions | undefined | void | null, - dependencies: unknown[], -): APIRequestState { - const [state, setState] = useState>({ - data: null, - hasData: false, - loading: false, - error: null, - }); - - useEffect(() => { - const inv = makeInvocation(); - const options: UseAPIResponseOptions = (typeof inv === "function" ? { invocation: inv } : inv) || {}; - const { - invocation, - discardPreviousData = false, - discardDataOnError = false, - abortPreviousRequest = true, - } = options; - - if (!invocation && !discardPreviousData) return; - - setState((prevState) => ({ - data: discardPreviousData ? null : prevState.data, - hasData: discardPreviousData ? false : prevState.hasData, - loading: invocation != null, - error: invocation != null || discardPreviousData ? null : prevState.error, - })); - - if (!invocation) return; - - const abortController = new AbortController(); - - let mounted = true; - invocation(abortController.signal).then( - (data) => { - if (!mounted) return; - setState({ - data, - hasData: true, - loading: false, - error: null, - }); - }, - (error) => { - if (!mounted) return; - setState((prevState) => ({ - data: discardDataOnError ? null : prevState.data, - hasData: discardDataOnError ? false : prevState.hasData, - loading: false, - error, - })); - }, - ); - - return () => { - mounted = false; - if (abortPreviousRequest) { - abortController.abort(); - } - }; - }, dependencies); - - return state; -} - /* eslint-disable @typescript-eslint/no-explicit-any */ export type JSONFetchOptions = any> = {