diff --git a/messages/en.json b/messages/en.json
index 081f50eb..cbe121ed 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -246,6 +246,9 @@
"officialFeed": "Official Feed",
"officialFeedTooltip": "The transit provider has confirmed this feed should be shared with riders. This has been confirmed either by the transit provider providing the feed on their website or from personalized confirmation with the Mobility Database team.",
"officialFeedTooltipShort": "Verified feed: Confirmed by the transit provider or the Mobility Database team for rider use.",
+ "communityFeed": "Community Feed",
+ "communityFeedTooltip": "This feed is not created on behalf of the transit authority. It may have been created by a community member or third party.",
+ "communityFeedTooltipShort": "Community feed: Created by a third party unaffiliated with the transit provider.",
"seeDetailPageProviders": "See detail page to view {providersCount} others",
"openFullQualityReport": "Open Full Quality Report",
"subscribe": "Subscribe to get feed update notifications",
diff --git a/messages/fr.json b/messages/fr.json
index 43b77d6e..9c53de8e 100644
--- a/messages/fr.json
+++ b/messages/fr.json
@@ -246,6 +246,9 @@
"officialFeed": "Official Feed",
"officialFeedTooltip": "The transit provider has confirmed this feed should be shared with riders. This has been confirmed either by the transit provider providing the feed on their website or from personalized confirmation with the Mobility Database team.",
"officialFeedTooltipShort": "Verified feed: Confirmed by the transit provider or the Mobility Database team for rider use.",
+ "communityFeed": "Community Feed",
+ "communityFeedTooltip": "This feed has not been officially confirmed by the transit provider. It may have been created by the community or a third party.",
+ "communityFeedTooltipShort": "Community feed: Not officially confirmed by the transit provider.",
"seeDetailPageProviders": "See detail page to view {providersCount} others",
"openFullQualityReport": "Open Full Quality Report",
"subscribe": "S'abonner",
diff --git a/src/app/components/FeedVerificationChip.spec.tsx b/src/app/components/FeedVerificationChip.spec.tsx
new file mode 100644
index 00000000..07ada921
--- /dev/null
+++ b/src/app/components/FeedVerificationChip.spec.tsx
@@ -0,0 +1,100 @@
+import React, { type JSX } from 'react';
+import { render, screen } from '@testing-library/react';
+import { ThemeProvider } from '@mui/material';
+import { theme } from '../Theme';
+import FeedVerificationChip from './FeedVerificationChip';
+
+// next-intl is globally mocked in setupTests.ts: useTranslations returns (key) => key
+
+function wrapper({ children }: { children: React.ReactNode }): JSX.Element {
+ return {children};
+}
+
+describe('FeedVerificationChip', () => {
+ describe('when status is undefined', () => {
+ it('renders nothing', () => {
+ const { container } = render(, { wrapper });
+ expect(container).toBeEmptyDOMElement();
+ });
+ });
+
+ describe('when status is true (official feed)', () => {
+ it('renders the official chip with label in long display mode (default)', () => {
+ render(, { wrapper });
+ expect(screen.getByTestId('official-feed-chip')).toBeInTheDocument();
+ expect(screen.getByText('officialFeed')).toBeInTheDocument();
+ });
+
+ it('renders the official icon in short display mode', () => {
+ render(, {
+ wrapper,
+ });
+ expect(screen.getByTestId('official-feed-icon')).toBeInTheDocument();
+ expect(
+ screen.queryByTestId('official-feed-chip'),
+ ).not.toBeInTheDocument();
+ });
+ });
+
+ describe('when status is false (community feed)', () => {
+ it('renders the community chip with label in long display mode (default)', () => {
+ render(, { wrapper });
+ expect(screen.getByTestId('community-feed-chip')).toBeInTheDocument();
+ expect(screen.getByText('communityFeed')).toBeInTheDocument();
+ });
+
+ it('renders the community icon in short display mode', () => {
+ render(, {
+ wrapper,
+ });
+ expect(screen.getByTestId('community-feed-icon')).toBeInTheDocument();
+ expect(
+ screen.queryByTestId('community-feed-chip'),
+ ).not.toBeInTheDocument();
+ });
+ });
+
+ describe('long display mode (isLongDisplay=true)', () => {
+ it('renders official chip — not icon — for status=true', () => {
+ render(, {
+ wrapper,
+ });
+ expect(screen.getByTestId('official-feed-chip')).toBeInTheDocument();
+ expect(
+ screen.queryByTestId('official-feed-icon'),
+ ).not.toBeInTheDocument();
+ });
+
+ it('renders community chip — not icon — for status=false', () => {
+ render(, {
+ wrapper,
+ });
+ expect(screen.getByTestId('community-feed-chip')).toBeInTheDocument();
+ expect(
+ screen.queryByTestId('community-feed-icon'),
+ ).not.toBeInTheDocument();
+ });
+ });
+
+ describe('short display mode (isLongDisplay=false)', () => {
+ it('renders official icon — not chip — for status=true', () => {
+ render(, {
+ wrapper,
+ });
+ expect(screen.getByTestId('official-feed-icon')).toBeInTheDocument();
+ expect(
+ screen.queryByTestId('official-feed-chip'),
+ ).not.toBeInTheDocument();
+ });
+
+ it('renders community icon — not chip — for status=false', () => {
+ render(, {
+ wrapper,
+ });
+ expect(screen.getByTestId('community-feed-icon')).toBeInTheDocument();
+ expect(
+ screen.queryByTestId('community-feed-chip'),
+ ).not.toBeInTheDocument();
+ });
+ });
+});
diff --git a/src/app/components/FeedVerificationChip.tsx b/src/app/components/FeedVerificationChip.tsx
new file mode 100644
index 00000000..c62e84fd
--- /dev/null
+++ b/src/app/components/FeedVerificationChip.tsx
@@ -0,0 +1,80 @@
+'use client';
+import { Chip, Tooltip } from '@mui/material';
+import { useTranslations } from 'next-intl';
+import VerifiedIcon from '@mui/icons-material/Verified';
+import GroupsIcon from '@mui/icons-material/Groups';
+
+interface FeedVerificationChipProps {
+ isLongDisplay?: boolean;
+ status?: boolean;
+}
+
+export default function FeedVerificationChip({
+ isLongDisplay = true,
+ status,
+}: FeedVerificationChipProps): React.ReactElement | null {
+ const t = useTranslations('feeds');
+
+ if (status == undefined) {
+ return null;
+ }
+
+ if (!status) {
+ return isLongDisplay ? (
+
+ }
+ label={t('communityFeed')}
+ variant='filled'
+ >
+
+ ) : (
+
+ ({
+ display: 'block',
+ ml: 0,
+ mr: 2,
+ opacity: 0.6,
+ backgroundColor: theme.vars.palette.grey[400],
+ color: theme.vars.palette.text.primary,
+ borderRadius: '50%',
+ padding: '0.2rem',
+ })}
+ >
+
+ );
+ }
+
+ return isLongDisplay ? (
+
+ ({
+ background: `linear-gradient(25deg, ${theme.vars.palette.primary.light}, ${theme.vars.palette.primary.dark})`,
+ color: 'white',
+ })}
+ icon={}
+ label={t('officialFeed')}
+ >
+
+ ) : (
+
+ ({
+ display: 'block',
+ borderRadius: '50%',
+ padding: '0.1rem',
+ ml: 0,
+ mr: 2,
+ background: `linear-gradient(25deg, ${theme.vars.palette.primary.light}, ${theme.vars.palette.primary.dark})`,
+ color: 'white',
+ })}
+ >
+
+ );
+}
diff --git a/src/app/components/OfficialChip.tsx b/src/app/components/OfficialChip.tsx
deleted file mode 100644
index fcab85a7..00000000
--- a/src/app/components/OfficialChip.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-'use client';
-// For this component to become server we will need 'cssVariables' support in MUI
-
-import { Chip, Tooltip } from '@mui/material';
-import { useTranslations } from 'next-intl';
-import { verificationBadgeStyle } from '../styles/VerificationBadge.styles';
-import VerifiedIcon from '@mui/icons-material/Verified';
-
-interface OfficialChipProps {
- isLongDisplay?: boolean;
-}
-
-export default function OfficialChip({
- isLongDisplay = true,
-}: OfficialChipProps): React.ReactElement {
- const t = useTranslations('feeds');
- return (
- <>
- {isLongDisplay ? (
-
- }
- label={t('officialFeed')}
- >
-
- ) : (
-
- ({
- display: 'block',
- borderRadius: '50%',
- padding: '0.1rem',
- ml: 0,
- mr: 2,
- ...verificationBadgeStyle(theme),
- })}
- >
-
- )}
- >
- );
-}
diff --git a/src/app/screens/Feed/FeedView.tsx b/src/app/screens/Feed/FeedView.tsx
index 202d66d8..45803317 100644
--- a/src/app/screens/Feed/FeedView.tsx
+++ b/src/app/screens/Feed/FeedView.tsx
@@ -7,7 +7,7 @@ import Typography from '@mui/material/Typography';
// Components
import FeedTitle from './components/FeedTitle';
-import OfficialChip from '../../components/OfficialChip';
+import FeedVerificationChip from '../../components/FeedVerificationChip';
import DataQualitySummary from './components/DataQualitySummary';
import FeedSummary from './components/FeedSummary';
import FeedNavigationControls from './components/FeedNavigationControls';
@@ -194,14 +194,16 @@ export default async function FeedView({
{feed?.data_type === 'gtfs' && (
)}
- {feed?.data_type === 'gtfs_rt' && feed.official === true && (
+ {feed?.data_type === 'gtfs_rt' && feed.official != null && (
-
+
)}
diff --git a/src/app/screens/Feed/components/DataQualitySummary.tsx b/src/app/screens/Feed/components/DataQualitySummary.tsx
index 36f80819..5cb9a33a 100644
--- a/src/app/screens/Feed/components/DataQualitySummary.tsx
+++ b/src/app/screens/Feed/components/DataQualitySummary.tsx
@@ -5,13 +5,13 @@ import { type components } from '../../../services/feeds/types';
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
import { WarningContentBox } from '../../../components/WarningContentBox';
import { FeedStatusChip } from '../../../components/FeedStatus';
-import OfficialChip from '../../../components/OfficialChip';
+import FeedVerificationChip from '../../../components/FeedVerificationChip';
import { getTranslations } from 'next-intl/server';
import { getUserRemoteConfigValues } from '../../../../lib/remote-config.server';
export interface DataQualitySummaryProps {
feedStatus: components['schemas']['Feed']['status'];
- isOfficialFeed: boolean;
+ isOfficialFeed: boolean | undefined;
latestDataset: components['schemas']['GtfsDataset'] | undefined;
}
@@ -36,7 +36,7 @@ export default async function DataQualitySummary({
{config.enableFeedStatusBadge && (
)}
- {isOfficialFeed && }
+
{latestDataset?.validation_report !== undefined &&
latestDataset.validation_report !== null && (
<>
diff --git a/src/app/screens/Feeds/AdvancedSearchTable.tsx b/src/app/screens/Feeds/AdvancedSearchTable.tsx
index 650efadf..0f23bb6b 100644
--- a/src/app/screens/Feeds/AdvancedSearchTable.tsx
+++ b/src/app/screens/Feeds/AdvancedSearchTable.tsx
@@ -23,7 +23,7 @@ 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';
+import FeedVerificationChip from '../../components/FeedVerificationChip';
import { getFeatureComponentDecorators } from '../../utils/consts';
import PopoverList from './PopoverList';
import ProviderTitle from './ProviderTitle';
@@ -337,9 +337,10 @@ export default function AdvancedSearchTable({
>
- {feed.official === true && (
-
- )}
+
{feed.data_type !== 'gbfs' && (
- {feed.official === true && (
-
- )}
+
diff --git a/src/app/styles/VerificationBadge.styles.ts b/src/app/styles/VerificationBadge.styles.ts
deleted file mode 100644
index 4d46207e..00000000
--- a/src/app/styles/VerificationBadge.styles.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { type Theme } from '@mui/material';
-import { type SystemStyleObject } from '@mui/system';
-
-export const verificationBadgeStyle = (
- theme: Theme,
-): SystemStyleObject => ({
- background: `linear-gradient(25deg, ${theme.vars.palette.primary.light}, ${theme.vars.palette.primary.dark})`,
- color: 'white',
-});