Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@
"gbfsValidator": "GBFS Validator",
"gtfsValidator": "GTFS Validator",
"gtfsRtValidator": "GTFS RT Validator",
"tools": "Tools",
"analytics": "Analytics",
"metrics": "Metrics",
"account": "Account",
"gtfsFeatureTracker": "GTFS Feature Tracker",
"start": "Start",
"end": "End",
"showLess": "Show less",
Expand Down Expand Up @@ -138,6 +143,8 @@
"resultsFor": "{startResult}-{endResult} of {totalResults} results",
"deprecated": "Deprecated",
"searchPlaceholder": "Transit provider, feed name, or location",
"featureTrackerBanner": "See which trip planners use these features",
"featureTrackerBannerSingle": "See which trip planners use {feature}",
"noResults": "We're sorry, we found no search results for ''{activeSearch}''.",
"searchSuggestions": "Search suggestions: ",
"searchTips": {
Expand Down Expand Up @@ -539,14 +546,15 @@
"copyright": "© {year} MobilityDatabase. All rights reserved.",
"columns": {
"platform": "Platform",
"validators": "Validators",
"tools": "Tools",
"company": "Company",
"legal": "Legal"
},
"links": {
"feeds": "Feeds",
"addFeed": "Add a Feed",
"apiDocs": "API Docs",
"gtfsFeatureTracker": "GTFS Feature Tracker",
"gtfsValidator": "GTFS Validator",
"gtfsRtValidator": "GTFS-RT Validator",
"gbfsValidator": "GBFS Validator",
Expand Down
10 changes: 9 additions & 1 deletion messages/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@
"gbfsValidator": "GBFS Validator",
"gtfsValidator": "GTFS Validator",
"gtfsRtValidator": "GTFS RT Validator",
"tools": "Outils",
"analytics": "Analyses",
"metrics": "Métriques",
"account": "Compte",
"gtfsFeatureTracker": "Suivi des fonctionnalités GTFS",
"start": "Start",
"end": "End",
"showLess": "Show less",
Expand Down Expand Up @@ -138,6 +143,8 @@
"resultsFor": "{startResult}-{endResult} of {totalResults} results",
"deprecated": "Deprecated",
"searchPlaceholder": "Transit provider, feed name, or location",
"featureTrackerBanner": "Voir quels planificateurs d’itinéraires utilisent ces fonctionnalités",
"featureTrackerBannerSingle": "Voir quels planificateurs d’itinéraires utilisent {feature}",
"noResults": "We're sorry, we found no search results for ''{activeSearch}''.",
"searchSuggestions": "Search suggestions: ",
"searchTips": {
Expand Down Expand Up @@ -539,14 +546,15 @@
"copyright": "© {year} MobilityDatabase. Tous droits réservés.",
"columns": {
"platform": "Plateforme",
"validators": "Validateurs",
"tools": "Outils",
"company": "Entreprise",
"legal": "Légal"
},
"links": {
"feeds": "Flux",
"addFeed": "Ajouter un flux",
"apiDocs": "Docs API",
"gtfsFeatureTracker": "Suivi des fonctionnalités GTFS",
"gtfsValidator": "Validateur GTFS",
"gtfsRtValidator": "Validateur GTFS-RT",
"gbfsValidator": "Validateur GBFS",
Expand Down
Binary file added public/assets/tripPlannerLogos/aubin-app.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/tripPlannerLogos/gmaps.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/tripPlannerLogos/motis.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/tripPlannerLogos/transitapp.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
53 changes: 44 additions & 9 deletions src/app/[locale]/feeds/components/FeedsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
Button,
Chip,
Container,
CssBaseline,
Grid,
InputAdornment,
LinearProgress,
Expand All @@ -19,8 +18,9 @@ import {
Typography,
useTheme,
} from '@mui/material';
import { Search } from '@mui/icons-material';
import { OpenInNew, Search } from '@mui/icons-material';
import { useSearchParams, useRouter, usePathname } from 'next/navigation';
import NextLink from 'next/link';
import SearchTable from '../../../screens/Feeds/SearchTable';
import { useTranslations } from 'next-intl';
import {
Expand All @@ -39,6 +39,7 @@ import {
deriveFilterFlags,
buildSearchUrl,
} from '../lib/useFeedsSearch';
import { toFeatureAnchor } from '../../../utils/featureAnchor';

export default function FeedsScreen(): React.ReactElement {
const theme = useTheme();
Expand Down Expand Up @@ -68,6 +69,16 @@ export default function FeedsScreen(): React.ReactElement {
areGBFSFiltersEnabled,
} = deriveFilterFlags(selectedFeedTypes);

const featureTrackerHref =
selectedFeatures.length === 1
? `/gtfs-feature-tracker#${toFeatureAnchor(selectedFeatures[0])}`
: '/gtfs-feature-tracker';

const featureTrackerLabel =
selectedFeatures.length === 1
? t('featureTrackerBannerSingle', { feature: selectedFeatures[0] })
: t('featureTrackerBanner');

// SWR-powered data fetching - keyed off URL params
const { feedsData, isLoading, isValidating, isError, searchLimit } =
useFeedsSearch(searchParams);
Expand Down Expand Up @@ -179,7 +190,6 @@ export default function FeedsScreen(): React.ReactElement {
position: 'relative',
}}
>
<CssBaseline />
<Box
sx={{
display: 'flex',
Expand Down Expand Up @@ -544,13 +554,38 @@ export default function FeedsScreen(): React.ReactElement {
alignItems: 'self-end',
}}
>
<Typography
variant='subtitle2'
sx={{ fontWeight: 'bold' }}
gutterBottom
<Box
sx={{
display: 'flex',
alignItems: 'end',
gap: 2,
mr: 1,
flexWrap: 'wrap',
}}
>
{getSearchResultNumbers()}
</Typography>
<Typography
variant='subtitle2'
sx={{ fontWeight: 'bold', mb: 0 }}
gutterBottom
>
{getSearchResultNumbers()}
</Typography>
{selectedFeatures.length > 0 &&
areFeatureFiltersEnabled && (
<Button
component={NextLink}
href={featureTrackerHref}
variant='outlined'
size='small'
target='_blank'
color='primary'
endIcon={<OpenInNew />}
>
{featureTrackerLabel}
</Button>
)}
</Box>

<ToggleButtonGroup
color='primary'
value={searchView}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/* eslint-disable */
Comment thread
Alessandro100 marked this conversation as resolved.
import type { ReactElement } from 'react';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import CancelIcon from '@mui/icons-material/Cancel';
import InfoIcon from '@mui/icons-material/Info';
import AutorenewIcon from '@mui/icons-material/Autorenew';
import RemoveIcon from '@mui/icons-material/Remove';
import type {
Feature,
Consumer,
MdLinkToken,
UrlToken,
FileToken,
FieldToken,
Token,
} from './types';

// Many of these helper functions are based on parsing freeform text from the CSV, so they include some normalization and best-effort handling of unexpected values
// Once the data will come from the database with a well-defined schema, some of these parsing/normalization functions can be simplified or removed

export function getStatusText(raw: string): string {
const n = raw.toLowerCase().trim();
if (n.startsWith('yes - for every feed')) return 'Every feed';
if (n.startsWith('yes - for some feeds')) return 'Some feeds';
if (n.startsWith('yes')) return 'Yes';
if (n.startsWith('no')) return 'No';
if (n === 'integration planned') return 'Planned';
if (n === 'test in progress') return 'Test in progress';
if (n === 'partial integration') return 'Partial integration';
if (n === 'some fields are ignored') return 'Some fields ignored';
return raw || 'Unknown';
}

export function getStatusIcon(raw: string): ReactElement {
const n = raw.toLowerCase().trim();
if (n.startsWith('yes'))
return <CheckCircleIcon color='success' sx={{ fontSize: 20 }} />;
if (n.startsWith('no'))
return <CancelIcon color='error' sx={{ fontSize: 20 }} />;
if (n === 'integration planned')
return <InfoIcon color='info' sx={{ fontSize: 20 }} />;
if (n === 'test in progress')
return <AutorenewIcon color='warning' sx={{ fontSize: 20 }} />;
if (n === 'partial integration')
return <InfoIcon color='warning' sx={{ fontSize: 20 }} />;
if (n === 'some fields are ignored')
return <InfoIcon color='info' sx={{ fontSize: 20 }} />;
return <RemoveIcon sx={{ fontSize: 20, color: 'text.disabled' }} />;
}

export function isStatusSupported(raw: string): boolean {
return raw.toLowerCase().trim().startsWith('yes');
}

export function computeCategoryProgress(
features: Feature[],
consumers: Consumer[],
): number {
let supported = 0;
let total = 0;
for (const feature of features) {
for (const consumer of consumers) {
const raw = feature.support[consumer.id]?.rawStatus ?? '';
if (raw) {
total++;
if (isStatusSupported(raw)) supported++;
}
}
}
return total > 0 ? Math.round((supported / total) * 100) : 0;
}

export function formatDate(dateStr: string): string {
if (!dateStr) return '';
const d = new Date(dateStr);
if (isNaN(d.getTime())) return dateStr;
return d.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
});
Comment on lines +73 to +81
}

export function tokenizeDetail(
text: string,
knownFieldsSet: Set<string>,
): Token[] {
if (!text) return [];
const tokens: Array<MdLinkToken | UrlToken | FileToken | FieldToken> = [];

// 1. Markdown links [label](url)
const mdRe = /\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g;
let m: RegExpExecArray | null;
while ((m = mdRe.exec(text)) !== null) {
tokens.push({
type: 'mdlink',
label: m[1],
url: m[2],
start: m.index,
end: m.index + m[0].length,
});
}

// 2. Bare URLs
const urlRe = /https?:\/\/[^\s,)]+/g;
while ((m = urlRe.exec(text)) !== null) {
const overlaps = tokens.some(
(t) => m!.index >= t.start && m!.index < t.end,
);
if (!overlaps)
tokens.push({
type: 'url',
value: m[0],
start: m.index,
end: m.index + m[0].length,
});
}

// 3. .txt file names
const fileRe = /\b[a-z_]+\.txt\b/g;
while ((m = fileRe.exec(text)) !== null) {
const overlaps = tokens.some(
(t) => m!.index >= t.start && m!.index < t.end,
);
if (!overlaps)
tokens.push({
type: 'file',
value: m[0],
start: m.index,
end: m.index + m[0].length,
});
}

// 4. Known GTFS field names
const fieldRe = /\b[a-z][a-z0-9_]*[a-z0-9]\b|\b[a-z]{2,}\b/g;
while ((m = fieldRe.exec(text)) !== null) {
if (!knownFieldsSet.has(m[0])) continue;
const overlaps = tokens.some(
(t) => m!.index >= t.start && m!.index < t.end,
);
if (!overlaps)
tokens.push({
type: 'field',
value: m[0],
start: m.index,
end: m.index + m[0].length,
});
}

tokens.sort((a, b) => a.start - b.start);

const segments: Token[] = [];
let cursor = 0;
for (const tok of tokens) {
if (tok.start > cursor)
segments.push({ type: 'text', value: text.slice(cursor, tok.start) });
segments.push(tok);
cursor = tok.end;
}
if (cursor < text.length)
segments.push({ type: 'text', value: text.slice(cursor) });
return segments;
}
Loading
Loading