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"