diff --git a/web-app/package.json b/web-app/package.json
index 511141268..e164abc6c 100644
--- a/web-app/package.json
+++ b/web-app/package.json
@@ -37,6 +37,7 @@
"react-dom": "^17.0.0 || ^18.0.0",
"react-ga4": "^2.1.0",
"react-google-recaptcha": "^3.1.0",
+ "react-helmet-async": "^2.0.5",
"react-hook-form": "^7.52.1",
"react-i18next": "^14.1.2",
"react-leaflet": "^4.2.1",
diff --git a/web-app/public/index.html b/web-app/public/index.html
index 0c379f496..7bd5101da 100644
--- a/web-app/public/index.html
+++ b/web-app/public/index.html
@@ -24,7 +24,7 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
-
+
diff --git a/web-app/public/locales/en/feeds.json b/web-app/public/locales/en/feeds.json
index 021cea4e7..1f2c8bca2 100644
--- a/web-app/public/locales/en/feeds.json
+++ b/web-app/public/locales/en/feeds.json
@@ -82,5 +82,6 @@
"oldVehiclePositionsFeed": "Old Vehicle Positions feed link",
"relatedGtfsScheduleFeed": "Link to related GTFS Schedule feed",
"isAuthRequired": "Is authentication required for the feed?",
- "isAuthRequiredDetails": " Select \"Yes\" if a user has to login or provide credentials to download the feed"
+ "isAuthRequiredDetails": " Select \"Yes\" if a user has to login or provide credentials to download the feed",
+ "detailPageDescription": "Explore the {{formattedName}} {{dataTypeVerbose}} feed details with access to a quality data insights"
}
diff --git a/web-app/src/app/components/Footer.tsx b/web-app/src/app/components/Footer.tsx
index ca5d94c56..b10070582 100644
--- a/web-app/src/app/components/Footer.tsx
+++ b/web-app/src/app/components/Footer.tsx
@@ -35,6 +35,7 @@ const Footer: React.FC = () => {
{
@@ -44,6 +45,7 @@ const Footer: React.FC = () => {
{
@@ -53,6 +55,7 @@ const Footer: React.FC = () => {
{
@@ -62,6 +65,7 @@ const Footer: React.FC = () => {
{
diff --git a/web-app/src/app/components/Header.tsx b/web-app/src/app/components/Header.tsx
index db50739be..d7a5df8f5 100644
--- a/web-app/src/app/components/Header.tsx
+++ b/web-app/src/app/components/Header.tsx
@@ -383,7 +383,10 @@ export default function DrawerAppBar(): React.ReactElement {
}}
className='btn-link'
>
-
+
-
+
About{' '}
-
+
GBFS Feeds Metrics
{error != null && (
diff --git a/web-app/src/app/screens/Analytics/GBFSNoticeAnalytics/index.tsx b/web-app/src/app/screens/Analytics/GBFSNoticeAnalytics/index.tsx
index 3c62cc6c1..5f39189dd 100644
--- a/web-app/src/app/screens/Analytics/GBFSNoticeAnalytics/index.tsx
+++ b/web-app/src/app/screens/Analytics/GBFSNoticeAnalytics/index.tsx
@@ -213,7 +213,12 @@ export default function GBFSNoticeAnalytics(): React.ReactElement {
return (
-
+
GBFS Notices Metrics
{error != null && (
diff --git a/web-app/src/app/screens/Analytics/GBFSVersionAnalytics/index.tsx b/web-app/src/app/screens/Analytics/GBFSVersionAnalytics/index.tsx
index 183f42ef9..8d042a7c4 100644
--- a/web-app/src/app/screens/Analytics/GBFSVersionAnalytics/index.tsx
+++ b/web-app/src/app/screens/Analytics/GBFSVersionAnalytics/index.tsx
@@ -228,7 +228,12 @@ export default function GBFSVersionAnalytics(): React.ReactElement {
return (
-
+
GBFS Versions Metrics{' '}
{error != null && (
diff --git a/web-app/src/app/screens/Analytics/GTFSFeatureAnalytics/index.tsx b/web-app/src/app/screens/Analytics/GTFSFeatureAnalytics/index.tsx
index a581d550e..19902444d 100644
--- a/web-app/src/app/screens/Analytics/GTFSFeatureAnalytics/index.tsx
+++ b/web-app/src/app/screens/Analytics/GTFSFeatureAnalytics/index.tsx
@@ -261,7 +261,12 @@ export default function GTFSFeatureAnalytics(): React.ReactElement {
return (
-
+
GTFS Features Metrics{' '}
{error != null && (
diff --git a/web-app/src/app/screens/Analytics/GTFSFeedAnalytics/index.tsx b/web-app/src/app/screens/Analytics/GTFSFeedAnalytics/index.tsx
index 046b28d7d..a5416fc8b 100644
--- a/web-app/src/app/screens/Analytics/GTFSFeedAnalytics/index.tsx
+++ b/web-app/src/app/screens/Analytics/GTFSFeedAnalytics/index.tsx
@@ -306,7 +306,12 @@ export default function GTFSFeedAnalytics(): React.ReactElement {
return (
-
+
GTFS Feeds Metrics{' '}
{error != null && (
diff --git a/web-app/src/app/screens/Analytics/GTFSNoticeAnalytics/index.tsx b/web-app/src/app/screens/Analytics/GTFSNoticeAnalytics/index.tsx
index 6aa952734..d051d5290 100644
--- a/web-app/src/app/screens/Analytics/GTFSNoticeAnalytics/index.tsx
+++ b/web-app/src/app/screens/Analytics/GTFSNoticeAnalytics/index.tsx
@@ -267,7 +267,12 @@ export default function GTFSNoticeAnalytics(): React.ReactElement {
return (
-
+
GTFS Notices Metrics{' '}
{error != null && (
diff --git a/web-app/src/app/screens/ContactUs.tsx b/web-app/src/app/screens/ContactUs.tsx
index 8452f787f..cbbddfdae 100644
--- a/web-app/src/app/screens/ContactUs.tsx
+++ b/web-app/src/app/screens/ContactUs.tsx
@@ -50,7 +50,12 @@ export default function ContactUs(): React.ReactElement {
return (
-
+
{t('title')}
-
+
Frequently Asked Questions (FAQ){' '}
-
+ {!hasFeedName && noLatestDataset
+ ? 'GTFS Schedule feed'
+ : hasFeedName
+ ? assocFeed.feed_name
+ : ''}
+
+
-
- {!hasFeedName && noLatestDataset
- ? 'GTFS Schedule feed'
- : hasFeedName
- ? assocFeed.feed_name
- : ''}
-
-
- {assocFeed.latest_dataset?.downloaded_at !== undefined && (
-
- Last updated on{' '}
- {new Date(assocFeed.latest_dataset?.downloaded_at).toDateString()}
-
- )}
-
-
+ {assocFeed.latest_dataset?.downloaded_at !== undefined && (
+
+ Last updated on{' '}
+ {new Date(assocFeed.latest_dataset?.downloaded_at).toDateString()}
+
+ )}
+
);
};
@@ -76,38 +72,35 @@ const renderAssociatedGTFSRTFeedRow = (
assocGTFSRTFeed.feed_name !== undefined && assocGTFSRTFeed.feed_name !== '';
return (
-
-
- {hasFeedName ? assocGTFSRTFeed.feed_name : assocGTFSRTFeed.provider}
+
+ {hasFeedName ? assocGTFSRTFeed.feed_name : assocGTFSRTFeed.provider}
+
+ {assocGTFSRTFeed.entity_types !== undefined && (
+
+ {assocGTFSRTFeed.entity_types
+ .map(
+ (entityType) =>
+ ({
+ tu: 'Trip Updates',
+ vp: 'Vehicle Positions',
+ sa: 'Service Alerts',
+ })[entityType],
+ )
+ .join(' and ')}
- {assocGTFSRTFeed.entity_types !== undefined && (
-
- {assocGTFSRTFeed.entity_types
- .map(
- (entityType) =>
- ({
- tu: 'Trip Updates',
- vp: 'Vehicle Positions',
- sa: 'Service Alerts',
- })[entityType],
- )
- .join(' and ')}
-
- )}
-
+ )}
);
};
diff --git a/web-app/src/app/screens/Feed/Feed.spec.tsx b/web-app/src/app/screens/Feed/Feed.spec.tsx
index 47bdc873c..7c7f1f139 100644
--- a/web-app/src/app/screens/Feed/Feed.spec.tsx
+++ b/web-app/src/app/screens/Feed/Feed.spec.tsx
@@ -1,5 +1,10 @@
import { cleanup, render, screen } from '@testing-library/react';
-import { formatProvidersSorted, getFeedTitleElement } from '.';
+import {
+ formatProvidersSorted,
+ generateDescriptionMetaTag,
+ generatePageTitle,
+ getFeedTitleElement,
+} from '.';
import {
type GTFSFeedType,
type GTFSRTFeedType,
@@ -137,4 +142,107 @@ describe('Feed page', () => {
expect(screen.getByText('AVL')).toBeTruthy();
expect(screen.queryByText('+')).toBeNull();
});
+
+ it('should generate the correct page title', () => {
+ const titleAllInfo = generatePageTitle(
+ ['Department of Transport', 'Public Transport'],
+ 'gtfs',
+ 'Darwin public bus network',
+ );
+ expect(titleAllInfo).toEqual(
+ 'Department of Transport, Darwin public bus network gtfs schedule feed - Mobility Database',
+ );
+
+ const titleNoProviders = generatePageTitle(
+ [],
+ 'gtfs',
+ 'Darwin public bus network',
+ );
+ expect(titleNoProviders).toEqual(
+ 'Darwin public bus network gtfs schedule feed - Mobility Database',
+ );
+
+ const titleNoName = generatePageTitle(
+ ['Department of Transport', 'Public Transport'],
+ 'gtfs',
+ '',
+ );
+ expect(titleNoName).toEqual(
+ 'Department of Transport gtfs schedule feed - Mobility Database',
+ );
+
+ const titleAllInfoRT = generatePageTitle(
+ ['Department of Transport', 'Public Transport'],
+ 'gtfs_rt',
+ 'Darwin public bus network',
+ );
+ expect(titleAllInfoRT).toEqual(
+ 'Department of Transport, Darwin public bus network gtfs realtime feed - Mobility Database',
+ );
+
+ const titleAllEmpty = generatePageTitle([], 'gtfs', '');
+ expect(titleAllEmpty).toEqual('Mobility Database');
+ });
+
+ it('should generate the correct page description', () => {
+ const mockT = jest.fn((key, params) => {
+ switch (key) {
+ case 'common:gtfsSchedule':
+ return 'GTFS schedule';
+ case 'common:gtfsRealtime':
+ return 'GTFS realtime';
+ case 'detailPageDescription':
+ return `Explore the ${params.formattedName} ${params.dataTypeVerbose} feed details with access to a quality data insights`;
+ break;
+ }
+ }) as unknown as TFunction<'feeds', undefined>;
+
+ const descriptionAllInfo = generateDescriptionMetaTag(
+ mockT,
+ ['Department of Transport', 'Public Transport'],
+ 'gtfs',
+ 'Darwin public bus network',
+ );
+ expect(descriptionAllInfo).toEqual(
+ 'Explore the Department of Transport, Darwin public bus network GTFS schedule feed details with access to a quality data insights',
+ );
+
+ const descriptionNoProviders = generateDescriptionMetaTag(
+ mockT,
+ [],
+ 'gtfs',
+ 'Darwin public bus network',
+ );
+ expect(descriptionNoProviders).toEqual(
+ 'Explore the Darwin public bus network GTFS schedule feed details with access to a quality data insights',
+ );
+
+ const descriptionNoName = generateDescriptionMetaTag(
+ mockT,
+ ['Department of Transport', 'Public Transport'],
+ 'gtfs',
+ '',
+ );
+ expect(descriptionNoName).toEqual(
+ 'Explore the Department of Transport GTFS schedule feed details with access to a quality data insights',
+ );
+
+ const descriptionAllInfoRT = generateDescriptionMetaTag(
+ mockT,
+ ['Department of Transport', 'Public Transport'],
+ 'gtfs_rt',
+ 'Darwin public bus network',
+ );
+ expect(descriptionAllInfoRT).toEqual(
+ 'Explore the Department of Transport, Darwin public bus network GTFS realtime feed details with access to a quality data insights',
+ );
+
+ const descriptionAllEmpty = generateDescriptionMetaTag(
+ mockT,
+ [],
+ 'gtfs',
+ '',
+ );
+ expect(descriptionAllEmpty).toEqual('');
+ });
});
diff --git a/web-app/src/app/screens/Feed/index.tsx b/web-app/src/app/screens/Feed/index.tsx
index 108bb946c..7154b1f3d 100644
--- a/web-app/src/app/screens/Feed/index.tsx
+++ b/web-app/src/app/screens/Feed/index.tsx
@@ -49,6 +49,7 @@ import {
import { Trans, useTranslation } from 'react-i18next';
import { type TFunction } from 'i18next';
import { theme } from '../../Theme';
+import { Helmet, HelmetProvider } from 'react-helmet-async';
export function formatProvidersSorted(provider: string): string[] {
const providers = provider.split(',').filter((n) => n);
@@ -57,6 +58,62 @@ export function formatProvidersSorted(provider: string): string[] {
return providersSorted;
}
+export function getFeedFormattedName(
+ sortedProviders: string[],
+ dataType: 'gtfs' | 'gtfs_rt',
+ feedName?: string,
+): string {
+ let formattedName = '';
+ if (sortedProviders[0] !== undefined && sortedProviders[0] !== '') {
+ formattedName += sortedProviders[0];
+ }
+ if (feedName !== undefined && feedName !== '') {
+ if (formattedName !== '') {
+ formattedName += ', ';
+ }
+ formattedName += `${feedName}`;
+ }
+ return formattedName;
+}
+
+export function generateDescriptionMetaTag(
+ t: TFunction<'feeds'>,
+ sortedProviders: string[],
+ dataType: 'gtfs' | 'gtfs_rt',
+ feedName?: string,
+): string {
+ const formattedName = getFeedFormattedName(
+ sortedProviders,
+ dataType,
+ feedName,
+ );
+ if (
+ sortedProviders.length === 0 &&
+ (feedName === undefined || feedName === '')
+ ) {
+ return '';
+ }
+ const dataTypeVerbose =
+ dataType === 'gtfs' ? t('common:gtfsSchedule') : t('common:gtfsRealtime');
+ return t('detailPageDescription', { formattedName, dataTypeVerbose });
+}
+
+export function generatePageTitle(
+ sortedProviders: string[],
+ dataType: 'gtfs' | 'gtfs_rt',
+ feedName?: string,
+): string {
+ let newDocTitle = getFeedFormattedName(sortedProviders, dataType, feedName);
+ const dataTypeVerbose = dataType === 'gtfs' ? 'schedule' : 'realtime';
+
+ if (newDocTitle !== '') {
+ newDocTitle += ` gtfs ${dataTypeVerbose} feed - `;
+ }
+
+ newDocTitle += 'Mobility Database';
+ return newDocTitle;
+}
+
export function getFeedTitleElement(
sortedProviders: string[],
feed: GTFSFeedType | GTFSRTFeedType,
@@ -81,6 +138,7 @@ export function getFeedTitleElement(
}
return (
{
const { t } = useTranslation('feeds');
@@ -116,6 +175,13 @@ const wrapComponent = (
sx={{ width: '100%', m: 'auto', px: 0 }}
maxWidth='xl'
>
+ {descriptionMeta !== undefined && (
+
+
+
+
+
+ )}
{t('common:loading')});
+ return wrapComponent(
+ feedLoadingStatus,
+ undefined,
+ {t('common:loading')} ,
+ );
}
const hasDatasets = datasets !== undefined && datasets.length > 0;
const hasFeedRedirect =
@@ -215,6 +282,12 @@ export default function Feed(): React.ReactElement {
return wrapComponent(
feedLoadingStatus,
+ generateDescriptionMetaTag(
+ t,
+ sortedProviders,
+ feed.data_type,
+ feed?.feed_name,
+ ),
-
+
Frequently Asked Questions about Adding Feeds
{
expect(screen.getByText('Utah Transit Authority (UTA)')).toBeTruthy();
const parentElement = screen.getByText('Angel Island Tiburon Ferry');
const spanElement = within(parentElement).getByText('+ 2', {
- selector: 'a',
+ selector: 'span',
});
expect(parentElement).toBeTruthy();
expect(spanElement).toBeTruthy();
diff --git a/web-app/src/app/screens/Feeds/SearchTable.tsx b/web-app/src/app/screens/Feeds/SearchTable.tsx
index 21907fa2f..f121abcb2 100644
--- a/web-app/src/app/screens/Feeds/SearchTable.tsx
+++ b/web-app/src/app/screens/Feeds/SearchTable.tsx
@@ -2,8 +2,7 @@ import * as React from 'react';
import {
Box,
Chip,
- IconButton,
- Popover,
+ Popper,
Table,
TableBody,
TableCell,
@@ -21,11 +20,10 @@ import {
import BusAlertIcon from '@mui/icons-material/BusAlert';
import DirectionsBusIcon from '@mui/icons-material/DirectionsBus';
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
-import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import GtfsRtEntities from './GtfsRtEntities';
-import CloseIcon from '@mui/icons-material/Close';
import { theme } from '../../Theme';
+import { Link } from 'react-router-dom';
export interface SearchTableProps {
feedsData: AllFeedsType | undefined;
@@ -78,12 +76,12 @@ export const getDataTypeElement = (
export default function SearchTable({
feedsData,
}: SearchTableProps): React.ReactElement {
+ const [anchorEl, setAnchorEl] = React.useState(null);
const [providersPopoverData, setProvidersPopoverData] = React.useState<
string[] | undefined
>(undefined);
const { t } = useTranslation('feeds');
if (feedsData === undefined) return <>>;
- const navigate = useNavigate();
const getProviderElement = (
feed: GTFSFeedType | GTFSRTFeedType,
@@ -97,20 +95,25 @@ export default function SearchTable({
let manyProviders: JSX.Element | undefined;
if (providers.length > 1) {
manyProviders = (
- {
- event.stopPropagation();
+ onMouseEnter={(event) => {
setProvidersPopoverData(providers);
+ setAnchorEl(event.currentTarget);
+ }}
+ onMouseLeave={() => {
+ setProvidersPopoverData(undefined);
+ setAnchorEl(null);
}}
>
+ {providers.length - 1}
-
+
);
}
return (
@@ -131,8 +134,11 @@ export default function SearchTable({
);
};
+ // Reason for all component overrite is for SEO purposes.
+ // TODO: This code is stretching the limits using to refactor out of table
return (
-
- {t('transitProvider')}
- {t('location')}
- {t('feedDescription')}
- {t('dataType')}
+
+
+ {t('transitProvider')}
+
+ {t('location')}
+
+ {t('feedDescription')}
+
+ {t('dataType')}
-
{feedsData?.results?.map((feed) => (
{
- navigate(`/feeds/${feed.id}`);
- }}
>
- {getProviderElement(feed)}
- {getLocationName(feed.locations)}
- {feed.feed_name}
-
+
+ {getProviderElement(feed)}
+
+
+ {getLocationName(feed.locations)}
+
+
+ {feed.feed_name}
+
+
{getDataTypeElement(feed.data_type)}
{feed.data_type === 'gtfs_rt' && (
@@ -214,49 +232,46 @@ export default function SearchTable({
))}
+
{providersPopoverData !== undefined && (
- {
- setProvidersPopoverData(undefined);
- }}
- anchorReference='none'
+ anchorEl={anchorEl}
+ placement='top'
sx={{
- display: 'flex',
- justifyContent: 'center',
- alignItems: 'center',
+ backgroundColor: theme.palette.background.paper,
+ boxShadow: '0px 1px 4px 2px rgba(0,0,0,0.2)',
}}
>
-
+
Transit Providers - {providersPopoverData[0]}
- {
- setProvidersPopoverData(undefined);
- }}
- >
-
-
-
+
- {providersPopoverData.map((provider) => (
+ {providersPopoverData.slice(0, 10).map((provider) => (
{provider}
))}
+ {providersPopoverData.length > 10 && (
+
+
+ See detail page to view {providersPopoverData.length - 10}{' '}
+ others
+
+
+ )}
-
+
)}
);
diff --git a/web-app/src/app/screens/Feeds/index.tsx b/web-app/src/app/screens/Feeds/index.tsx
index 2a97e73fa..92d776b99 100644
--- a/web-app/src/app/screens/Feeds/index.tsx
+++ b/web-app/src/app/screens/Feeds/index.tsx
@@ -166,10 +166,19 @@ export default function Feed(): React.ReactElement {
>
-
+
{t('feeds')}
- {t('searchFor')}
+ {activeSearch !== '' && (
+
+ {t('searchFor')}: {activeSearch}
+
+ )}
-
-
{t('dataType')}
-
- {
- setActivePagination(1);
- setSelectedFeedTypes({
- ...selectedFeedTypes,
- gtfs: e.target.checked,
- });
- }}
- />
- }
- label={t('common:gtfsSchedule')}
- />
- {
- setActivePagination(1);
- setSelectedFeedTypes({
- ...selectedFeedTypes,
- gtfs_rt: e.target.checked,
- });
- }}
- />
- }
- label={t('common:gtfsRealtime')}
- />
-
-
+ {t('dataType')}
+
+ {
+ setActivePagination(1);
+ setSelectedFeedTypes({
+ ...selectedFeedTypes,
+ gtfs: e.target.checked,
+ });
+ }}
+ />
+ }
+ label={t('common:gtfsSchedule')}
+ />
+ {
+ setActivePagination(1);
+ setSelectedFeedTypes({
+ ...selectedFeedTypes,
+ gtfs_rt: e.target.checked,
+ });
+ }}
+ />
+ }
+ label={t('common:gtfsRealtime')}
+ />
+
- {/* Content Area */}
{feedStatus === 'loading' && (
@@ -345,9 +351,7 @@ export default function Feed(): React.ReactElement {
{getSearchResultNumbers()}
-
-
Currently serving over
-
+
2000
transit data feeds from
-
+
70
countries.
diff --git a/web-app/src/app/screens/PrivacyPolicy.tsx b/web-app/src/app/screens/PrivacyPolicy.tsx
index 1cc05a722..a579828cc 100644
--- a/web-app/src/app/screens/PrivacyPolicy.tsx
+++ b/web-app/src/app/screens/PrivacyPolicy.tsx
@@ -17,6 +17,7 @@ export default function TermsAndConditions(): React.ReactElement {
}}
>
;
+ historicalMetrics: Record;
noticeMetrics: NoticeMetrics[];
featuresMetrics: FeatureMetrics[];
status: 'loading' | 'loaded' | 'failed';
@@ -19,7 +19,7 @@ interface GTFSAnalyticsState {
const initialState: GTFSAnalyticsState = {
feedMetrics: [],
- historicalMetrics: new Map(),
+ historicalMetrics: {},
noticeMetrics: [],
featuresMetrics: [],
status: 'loading',
diff --git a/web-app/src/app/store/gtfs-analytics-reducer.ts b/web-app/src/app/store/gtfs-analytics-reducer.ts
index b2083b07f..259ee962f 100644
--- a/web-app/src/app/store/gtfs-analytics-reducer.ts
+++ b/web-app/src/app/store/gtfs-analytics-reducer.ts
@@ -8,7 +8,7 @@ import {
interface GTFSAnalyticsState {
feedMetrics: GTFSFeedMetrics[];
- historicalMetrics: Map;
+ historicalMetrics: Record;
noticeMetrics: NoticeMetrics[];
featuresMetrics: FeatureMetrics[];
status: 'loading' | 'loaded' | 'failed';
@@ -19,7 +19,7 @@ interface GTFSAnalyticsState {
const initialState: GTFSAnalyticsState = {
feedMetrics: [],
- historicalMetrics: new Map(),
+ historicalMetrics: {},
noticeMetrics: [],
featuresMetrics: [],
status: 'loading',
diff --git a/web-app/yarn.lock b/web-app/yarn.lock
index e7da529b6..36c1c0051 100644
--- a/web-app/yarn.lock
+++ b/web-app/yarn.lock
@@ -8715,6 +8715,13 @@ internal-slot@^1.0.4, internal-slot@^1.0.5:
resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009"
integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==
+invariant@^2.2.4:
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
+ integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
+ dependencies:
+ loose-envify "^1.0.0"
+
ip-regex@^4.1.0:
version "4.3.0"
resolved "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz"
@@ -10434,7 +10441,7 @@ long@^5.0.0:
resolved "https://registry.npmjs.org/long/-/long-5.2.3.tgz"
integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==
-loose-envify@^1.1.0, loose-envify@^1.4.0:
+loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@@ -12513,6 +12520,11 @@ react-fast-compare@^2.0.1:
resolved "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz"
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
+react-fast-compare@^3.2.2:
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz#929a97a532304ce9fee4bcae44234f1ce2c21d49"
+ integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==
+
react-ga4@^2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/react-ga4/-/react-ga4-2.1.0.tgz"
@@ -12526,6 +12538,15 @@ react-google-recaptcha@^3.1.0:
prop-types "^15.5.0"
react-async-script "^1.2.0"
+react-helmet-async@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/react-helmet-async/-/react-helmet-async-2.0.5.tgz#cfc70cd7bb32df7883a8ed55502a1513747223ec"
+ integrity sha512-rYUYHeus+i27MvFE+Jaa4WsyBKGkL6qVgbJvSBoX8mbsWoABJXdEO0bZyi0F6i+4f0NuIb8AvqPMj3iXFHkMwg==
+ dependencies:
+ invariant "^2.2.4"
+ react-fast-compare "^3.2.2"
+ shallowequal "^1.1.0"
+
react-hook-form@^7.52.1:
version "7.52.1"
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.52.1.tgz#ec2c96437b977f8b89ae2d541a70736c66284852"
@@ -13410,6 +13431,11 @@ setprototypeof@1.2.0:
resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz"
integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
+shallowequal@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8"
+ integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==
+
shebang-command@^1.2.0:
version "1.2.0"
resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz"