diff --git a/package.json b/package.json index 33af784d..4bd37d1f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mobility-database", - "version": "1.0.0", + "version": "1.0.1", "private": true, "dependencies": { "@emotion/cache": "11.14.0", diff --git a/src/app/[locale]/feeds/components/FeedsScreen.tsx b/src/app/[locale]/feeds/components/FeedsScreen.tsx index 46cfff56..d38c20a1 100644 --- a/src/app/[locale]/feeds/components/FeedsScreen.tsx +++ b/src/app/[locale]/feeds/components/FeedsScreen.tsx @@ -58,6 +58,7 @@ export default function FeedsScreen(): React.ReactElement { features: selectedFeatures, gbfsVersions: selectedGbfsVersions, licenses: selectedLicenses, + licenseTags: selectedLicenseTags, hasTransitFeedsRedirect: hasTransitFeedsRedirectParam, } = derivedPageState; @@ -95,6 +96,7 @@ export default function FeedsScreen(): React.ReactElement { features: selectedFeatures, gbfsVersions: selectedGbfsVersions, licenses: selectedLicenses, + licenseTags: selectedLicenseTags, utmSource, ...overrides, }); @@ -109,6 +111,7 @@ export default function FeedsScreen(): React.ReactElement { selectedFeatures, selectedGbfsVersions, selectedLicenses, + selectedLicenseTags, utmSource, router, ], @@ -134,6 +137,7 @@ export default function FeedsScreen(): React.ReactElement { features: [], gbfsVersions: [], licenses: [], + licenseTags: [], isOfficial: false, }); } @@ -288,6 +292,7 @@ export default function FeedsScreen(): React.ReactElement { selectedFeatures={selectedFeatures} selectedGbfsVersions={selectedGbfsVersions} selectedLicenses={selectedLicenses} + selectedLicenseTags={selectedLicenseTags} setSelectedFeedTypes={(feedTypes) => { navigate({ feedTypes: { ...feedTypes }, page: 1 }); }} @@ -303,6 +308,9 @@ export default function FeedsScreen(): React.ReactElement { setSelectedLicenses={(licenses) => { navigate({ licenses, page: 1 }); }} + setSelectedLicenseTags={(licenseTags) => { + navigate({ licenseTags, page: 1 }); + }} isOfficialTagFilterEnabled={isOfficialTagFilterEnabled} areFeatureFiltersEnabled={areFeatureFiltersEnabled} areGBFSFiltersEnabled={areGBFSFiltersEnabled} @@ -417,9 +425,27 @@ export default function FeedsScreen(): React.ReactElement { /> ))} + {selectedLicenseTags.map((licenseTag) => ( + { + navigate({ + licenseTags: selectedLicenseTags.filter( + (lt) => lt !== licenseTag, + ), + }); + }} + /> + ))} + {(selectedFeatures.length > 0 || selectedGbfsVersions.length > 0 || selectedLicenses.length > 0 || + selectedLicenseTags.length > 0 || isOfficialFeedSearch || selectedFeedTypes.gtfs_rt || selectedFeedTypes.gtfs || @@ -553,6 +579,7 @@ export default function FeedsScreen(): React.ReactElement { feedsData={feedsData} selectedFeatures={selectedFeatures} selectedGbfsVersions={selectedGbfsVersions} + selectedLicenseTags={selectedLicenseTags} isLoadingFeeds={isLoading || isValidating} /> )} diff --git a/src/app/[locale]/feeds/lib/useFeedsSearch.ts b/src/app/[locale]/feeds/lib/useFeedsSearch.ts index 541924c7..175eb572 100644 --- a/src/app/[locale]/feeds/lib/useFeedsSearch.ts +++ b/src/app/[locale]/feeds/lib/useFeedsSearch.ts @@ -63,6 +63,7 @@ export function deriveSearchParams(searchParams: URLSearchParams): { features: string[]; gbfsVersions: string[]; licenses: string[]; + licenseTags: string[]; hasTransitFeedsRedirect: boolean; } { const feedTypes = getInitialSelectedFeedTypes(searchParams); @@ -76,6 +77,8 @@ export function deriveSearchParams(searchParams: URLSearchParams): { gbfsVersions: searchParams.get('gbfs_versions')?.split(',').filter(Boolean) ?? [], licenses: searchParams.get('licenses')?.split(',').filter(Boolean) ?? [], + licenseTags: + searchParams.get('license_tags')?.split(',').filter(Boolean) ?? [], hasTransitFeedsRedirect: searchParams.get('utm_source') === 'transitfeeds', }; } @@ -116,6 +119,7 @@ function buildSwrKey(derived: ReturnType): string { features, gbfsVersions, licenses, + licenseTags, } = derived; const flags = deriveFilterFlags(feedTypes); const cacheWindow = Math.floor(Date.now() / CACHE_TTL_MS); @@ -138,6 +142,9 @@ function buildSwrKey(derived: ReturnType): string { if (licenses.length > 0) { params.set('licenses', licenses.join(',')); } + if (licenseTags.length > 0) { + params.set('license_tags', licenseTags.join(',')); + } return `feeds-search?${params.toString()}`; } @@ -156,6 +163,7 @@ async function feedsFetcher( features, gbfsVersions, licenses, + licenseTags, } = derivedSearchParams; const flags = deriveFilterFlags(feedTypes); const offset = (page - 1) * SEARCH_LIMIT; @@ -175,6 +183,7 @@ async function feedsFetcher( ? gbfsVersions.join(',').replaceAll('v', '') : undefined, license_ids: licenses.length > 0 ? licenses.join(',') : undefined, + license_tags: licenseTags.length > 0 ? licenseTags.join(',') : undefined, }, }; @@ -249,6 +258,7 @@ export function buildSearchUrl( features?: string[]; gbfsVersions?: string[]; licenses?: string[]; + licenseTags?: string[]; utmSource?: string | null; }, ): string { @@ -278,6 +288,9 @@ export function buildSearchUrl( if (filters.licenses != null && filters.licenses.length > 0) { params.set('licenses', filters.licenses.join(',')); } + if (filters.licenseTags != null && filters.licenseTags.length > 0) { + params.set('license_tags', filters.licenseTags.join(',')); + } if (filters.isOfficial === true) { params.set('official', 'true'); } diff --git a/src/app/screens/Feeds/AdvancedSearchTable.tsx b/src/app/screens/Feeds/AdvancedSearchTable.tsx index 623d489d..d13d512b 100644 --- a/src/app/screens/Feeds/AdvancedSearchTable.tsx +++ b/src/app/screens/Feeds/AdvancedSearchTable.tsx @@ -20,6 +20,7 @@ import * as React from 'react'; import { FeedStatusIndicator } from '../../components/FeedStatus'; import { useTranslations } from 'next-intl'; import LockIcon from '@mui/icons-material/Lock'; +import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; import GtfsRtEntities from './GtfsRtEntities'; import { getEmojiFlag, type TCountryCode } from 'countries-list'; import OfficialChip from '../../components/OfficialChip'; @@ -33,18 +34,28 @@ export interface AdvancedSearchTableProps { feedsData: AllFeedsType | undefined; selectedFeatures: string[] | undefined; selectedGbfsVersions: string[] | undefined; + selectedLicenseTags: string[] | undefined; isLoadingFeeds: boolean; } interface DetailsContainerProps { children: React.ReactNode; feedSearchItem: SearchFeedItem; + selectedLicenseTags: string[]; } const DetailsContainer = ({ children, feedSearchItem, + selectedLicenseTags, }: DetailsContainerProps): React.ReactElement => { + const licenseTags = feedSearchItem.source_info?.license_tags; + const licenseTagsTitle = + licenseTags != null && licenseTags.length > 0 + ? licenseTags.join(', ') + : undefined; + const matchingTags = + licenseTags?.filter((tag) => selectedLicenseTags.includes(tag)) ?? []; return ( {children} - - + {matchingTags.map((tag) => ( + + ))} + - {feedSearchItem.source_info?.license_id} - - + + {licenseTagsTitle != null && ( + + )} + + {feedSearchItem.source_info?.license_id} + + + + ); }; @@ -77,11 +120,15 @@ const renderGTFSDetails = ( gtfsFeed: SearchFeedItem, selectedFeatures: string[], theme: Theme, + selectedLicenseTags: string[], ): React.ReactElement => { const feedFeatures = gtfsFeed?.latest_dataset?.validation_report?.features ?? []; return ( - + {gtfsFeed?.feed_name != null && ( { return ( - + { return ( - + {gbfsFeedSearchElement.versions?.map((version: string, index: number) => ( - {renderGTFSDetails(feed, selectedFeatures ?? [], theme)} + {renderGTFSDetails( + feed, + selectedFeatures ?? [], + theme, + selectedLicenseTags ?? [], + )} )} {feed.data_type === 'gtfs_rt' && ( - {renderGTFSRTDetails(feed)} + {renderGTFSRTDetails(feed, selectedLicenseTags ?? [])} )} {feed.data_type === 'gbfs' && ( - {renderGBFSDetails(feed, selectedGbfsVersions ?? [], theme)} + {renderGBFSDetails( + feed, + selectedGbfsVersions ?? [], + theme, + selectedLicenseTags ?? [], + )} )} diff --git a/src/app/screens/Feeds/SearchFilters.tsx b/src/app/screens/Feeds/SearchFilters.tsx index 393aeca9..d62cdc99 100644 --- a/src/app/screens/Feeds/SearchFilters.tsx +++ b/src/app/screens/Feeds/SearchFilters.tsx @@ -28,27 +28,40 @@ interface SearchFiltersProps { selectedFeatures: string[]; selectedGbfsVersions: string[]; selectedLicenses: string[]; + selectedLicenseTags: string[]; setSelectedFeedTypes: (selectedFeedTypes: Record) => void; setIsOfficialFeedSearch: (isOfficialFeedSearch: boolean) => void; setSelectedFeatures: (selectedFeatures: string[]) => void; setSelectedGbfsVerions: (selectedVersions: string[]) => void; setSelectedLicenses: (selectedLicenses: string[]) => void; + setSelectedLicenseTags: (selectedLicenseTags: string[]) => void; isOfficialTagFilterEnabled: boolean; areFeatureFiltersEnabled: boolean; areGBFSFiltersEnabled: boolean; } +const LICENSE_TAGS = [ + 'family:CC', + 'family:ODC', + 'notes:attribution-required', + 'notes:share-alike', + 'license:public-domain', + 'license:government-open-license', +]; + export function SearchFilters({ selectedFeedTypes, isOfficialFeedSearch, selectedFeatures, selectedGbfsVersions, selectedLicenses, + selectedLicenseTags, setSelectedFeedTypes, setIsOfficialFeedSearch, setSelectedFeatures, setSelectedGbfsVerions, setSelectedLicenses, + setSelectedLicenseTags, isOfficialTagFilterEnabled, areFeatureFiltersEnabled, areGBFSFiltersEnabled, @@ -66,6 +79,7 @@ export function SearchFilters({ tags: isOfficialTagFilterEnabled, gbfsVersions: true, licenses: true, + licenseTags: true, }); const [featureCheckboxData, setFeatureCheckboxData] = useState< CheckboxStructure[] @@ -353,6 +367,53 @@ export function SearchFilters({ > + + { + setExpandedCategories({ + ...expandedCategories, + licenseTags: !expandedCategories.licenseTags, + }); + }} + > + } + aria-controls='panel-license-tags-content' + sx={{ + px: 0, + }} + > + License Tags + + + ({ + title: tag, + checked: selectedLicenseTags.includes(tag), + type: 'checkbox', + }))} + onCheckboxChange={(checkboxData) => { + const selectedTagIds: string[] = []; + checkboxData.forEach((checkbox) => { + if (checkbox.checked) { + selectedTagIds.push(checkbox.title); + } + }); + setSelectedLicenseTags([...selectedTagIds]); + }} + > + + ); }