diff --git a/assets/icons/amal-news.svg b/assets/icons/amal-news.svg new file mode 100644 index 0000000000..d2809b0ea3 --- /dev/null +++ b/assets/icons/amal-news.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/icons/tu-news-inactive.svg b/assets/icons/tu-news-inactive.svg deleted file mode 100644 index 0fba6f358f..0000000000 --- a/assets/icons/tu-news-inactive.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - output - Created with Sketch. - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/assets/icons/tu-news-active.svg b/assets/icons/tu-news.svg similarity index 100% rename from assets/icons/tu-news-active.svg rename to assets/icons/tu-news.svg diff --git a/native/src/BottomTabNavigator.tsx b/native/src/BottomTabNavigator.tsx index 93514288a1..c2be3a29ae 100644 --- a/native/src/BottomTabNavigator.tsx +++ b/native/src/BottomTabNavigator.tsx @@ -12,7 +12,6 @@ import { CATEGORIES_TAB_ROUTE, EVENTS_ROUTE, EVENTS_TAB_ROUTE, - LOCAL_NEWS_TYPE, NEWS_ROUTE, NEWS_TAB_ROUTE, PLACES_ROUTE, @@ -66,11 +65,7 @@ const EventsStackScreen = () => ( const NewsStackScreen = () => ( - + ) @@ -128,7 +123,7 @@ const BottomTabNavigator = ({ navigation }: BottomTabNavigatorProps): ReactEleme return } - const { eventsEnabled, placesEnabled, localNewsEnabled, tuNewsEnabled, chatEnabled } = cachedData.region + const { eventsEnabled, placesEnabled, newsEnabled, chatEnabled } = cachedData.region const chatVisible = buildConfig().featureFlags.chat && chatEnabled const Tabs = [ @@ -152,7 +147,7 @@ const BottomTabNavigator = ({ navigation }: BottomTabNavigatorProps): ReactEleme }} /> ), - (localNewsEnabled || tuNewsEnabled) && ( + newsEnabled && ( { categories: null as never, events: [], places: [], - localNews: [], + news: [], }, loading: false, error: null, diff --git a/native/src/assets/index.ts b/native/src/assets/index.ts index 897f719da6..43ca7181ba 100644 --- a/native/src/assets/index.ts +++ b/native/src/assets/index.ts @@ -1,12 +1,12 @@ import EventThumbnailPlaceholder1 from '../../../assets/event-thumbnail-placeholder-1.jpg' import EventThumbnailPlaceholder2 from '../../../assets/event-thumbnail-placeholder-2.jpg' import EventThumbnailPlaceholder3 from '../../../assets/event-thumbnail-placeholder-3.jpg' +import AmalNewsLogo from '../../../assets/icons/amal-news.svg' import EditLocationIcon from '../../../assets/icons/edit-location.svg' import NotAccessibleIcon from '../../../assets/icons/not-accessible.svg' import SignPostIcon from '../../../assets/icons/signpost-outline.svg' import SprungbrettIcon from '../../../assets/icons/sprungbrett.svg' -import TuNewsActiveIcon from '../../../assets/icons/tu-news-active.svg' -import TuNewsInactiveIcon from '../../../assets/icons/tu-news-inactive.svg' +import TuNewsIcon from '../../../assets/icons/tu-news.svg' import IntroLanguageIcon from '../../../assets/intro/language.svg' import IntroNewsIcon from '../../../assets/intro/news.svg' import IntroOfflineIcon from '../../../assets/intro/offline.svg' @@ -15,6 +15,7 @@ import IntroSearchIcon from '../../../assets/intro/search.svg' import IntroWelcomeIcon from '../../../assets/intro/welcome.svg' export { + AmalNewsLogo, EditLocationIcon, EventThumbnailPlaceholder1, EventThumbnailPlaceholder2, @@ -28,6 +29,5 @@ export { NotAccessibleIcon, SignPostIcon, SprungbrettIcon, - TuNewsActiveIcon, - TuNewsInactiveIcon, + TuNewsIcon, } diff --git a/native/src/components/List.tsx b/native/src/components/List.tsx index dba44e9a60..706fc4ba41 100644 --- a/native/src/components/List.tsx +++ b/native/src/components/List.tsx @@ -21,7 +21,6 @@ type ListProps = { scrollEnabled?: boolean accessibilityLabel?: string refresh?: () => void - onEndReached?: () => void style?: ViewStyle keyboardShouldPersistTaps?: 'always' | 'never' | 'handled' } @@ -34,7 +33,6 @@ const List = ({ footer, refresh, accessibilityLabel, - onEndReached, scrollEnabled, style, keyboardShouldPersistTaps = 'never', @@ -49,7 +47,6 @@ const List = ({ ListEmptyComponent={ typeof noItemsMessage === 'string' ? : noItemsMessage } - onEndReached={onEndReached} showsVerticalScrollIndicator={false} onEndReachedThreshold={1} scrollEnabled={scrollEnabled} diff --git a/native/src/components/News.tsx b/native/src/components/News.tsx index 1e01402a83..1f9172ff51 100644 --- a/native/src/components/News.tsx +++ b/native/src/components/News.tsx @@ -1,73 +1,77 @@ -import { TFunction } from 'i18next' import React, { ReactElement } from 'react' import { useTranslation } from 'react-i18next' -import { ScrollView } from 'react-native' +import { Pressable, ScrollView, View } from 'react-native' +import styled from 'styled-components/native' -import { NewsRouteType, NewsType, TU_NEWS_TYPE, replaceLinks, tuNewsLabel } from 'shared' -import { LocalNewsModel, TuNewsModel, ErrorCodes } from 'shared/api' +import { + NEWS_ROUTE, + NEWS_SOURCE_FILTERS, + NewsRouteType, + NewsSourceFilter as NewsSourceFilterType, + replaceLinks, +} from 'shared' +import { AMAL_NEWS_SOURCE, ErrorCodes, LOCAL_NEWS_SOURCE, NewsModel, RegionModel } from 'shared/api' +import { AmalNewsLogo, TuNewsIcon } from '../assets' import { NavigationProps } from '../constants/NavigationTypes' import { contentAlignmentRTLText } from '../constants/contentDirection' import useNavigate from '../hooks/useNavigate' import useSetRouteTitle from '../hooks/useSetRouteTitle' +import useSnackbar from '../hooks/useSnackbar' import useTtsPlayer from '../hooks/useTtsPlayer' +import openExternalUrl from '../utils/openExternalUrl' +import Caption from './Caption' import Failure from './Failure' import List from './List' -import LoadingSpinner from './LoadingSpinner' import NewsListItem from './NewsListItem' import Page from './Page' import TimeStamp from './TimeStamp' +import ToggleTextButtonGroup from './ToggleTextButtonGroup' +import Icon from './base/Icon' import Text from './base/Text' -const getPageTitle = ( - selectedNewsType: NewsType, - selectedNewsItem: LocalNewsModel | TuNewsModel | null | undefined, - t: TFunction, -): string => { - if (selectedNewsItem?.title) { - return selectedNewsItem.title - } - if (selectedNewsType === TU_NEWS_TYPE) { - return tuNewsLabel - } - return t('localNews.pageTitle') -} +const NewsSourceLogo = styled(Icon)` + width: 100%; + height: 64px; +` -type NewsModelsType = (LocalNewsModel | TuNewsModel)[] +const NewsSourceLink = styled(Pressable)` + padding-top: 16px; +` + +const ListHeaderContainer = styled(View)` + padding-inline: 16px; + padding-bottom: 16px; + gap: 8px; +` type NewsProps = { - news: NewsModelsType - navigateToNews: (newsId: number) => void - newsId: number | null + news: NewsModel[] + id: number | null + region: RegionModel languageCode: string - selectedNewsType: NewsType - loadMore?: () => void - loadingMore?: boolean refresh: () => void + newsSource: NewsSourceFilterType + setNewsSource: (value: NewsSourceFilterType) => void } -const News = ({ - news, - loadMore, - newsId, - languageCode, - selectedNewsType, - navigateToNews, - refresh, - loadingMore, -}: NewsProps): ReactElement => { - const selectedNewsItem = news.find(_newsItem => _newsItem.id === newsId) +const News = ({ news, id, languageCode, region, refresh, newsSource, setNewsSource }: NewsProps): ReactElement => { + const selectedNewsItem = news.find(item => item.id === id) + const { navigateTo } = useNavigate() const { t } = useTranslation('news') + const showSnackbar = useSnackbar() useTtsPlayer(selectedNewsItem) const navigation = useNavigate().navigation as NavigationProps - useSetRouteTitle({ navigation, title: getPageTitle(selectedNewsType, selectedNewsItem, t) }) - - const rendersNewsListItem = ({ item }: { item: LocalNewsModel | TuNewsModel }) => { - const navigateToNewsDetail = () => navigateToNews(item.id) + useSetRouteTitle({ navigation, title: selectedNewsItem?.title ?? t('news') }) - return - } + const rendersNewsListItem = ({ item }: { item: NewsModel }) => ( + navigateTo({ route: NEWS_ROUTE, regionCode: region.code, languageCode, id: item.id })} + /> + ) if (selectedNewsItem) { return ( @@ -75,38 +79,51 @@ const News = ({ - + + + - ) + {selectedNewsItem.source !== LOCAL_NEWS_SOURCE && ( + openExternalUrl(selectedNewsItem.externalUrl, showSnackbar)} role='link'> + + + )} + } /> ) } - if (newsId !== null) { + if (id !== null) { return } + const showNewsSourceFilter = region.localNewsEnabled && region.externalNewsEnabled + return ( : undefined} + header={ + + + {showNewsSourceFilter && ( + + )} + + } renderItem={rendersNewsListItem} refresh={refresh} /> diff --git a/native/src/components/NewsHeader.tsx b/native/src/components/NewsHeader.tsx deleted file mode 100644 index 85c4d034f5..0000000000 --- a/native/src/components/NewsHeader.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import React, { memo, ReactElement } from 'react' -import { useTranslation } from 'react-i18next' -import { StyleSheet } from 'react-native' -import { TouchableRipple } from 'react-native-paper' -import styled, { useTheme } from 'styled-components/native' - -import { LOCAL_NEWS_TYPE, NewsType, TU_NEWS_TYPE } from 'shared' -import { RegionModel } from 'shared/api' - -import { TuNewsActiveIcon, TuNewsInactiveIcon } from '../assets' -import Caption from './Caption' -import Icon from './base/Icon' -import Text from './base/Text' - -const LocalTabWrapper = styled.View<{ isSelected: boolean }>` - border-radius: 10px; - height: 50px; - text-align: center; - min-width: 160px; - align-items: center; - justify-content: center; - background-color: ${props => - props.isSelected ? props.theme.colors.secondary : props.theme.colors.onSurfaceDisabled}; -` -const HeaderContainer = styled.View` - flex-direction: row; - align-items: center; - justify-content: center; - margin-bottom: 12px; -` - -const StyledIcon = styled(Icon)` - width: 160px; - height: 50px; -` - -const styles = StyleSheet.create({ - TouchableRippleStyle: { - marginHorizontal: 10, - marginBottom: 5, - alignItems: 'center', - }, -}) - -type NewsHeaderProps = { - regionModel: RegionModel - selectedNewsType: NewsType - selectNewsType: (newsType: NewsType) => void -} - -const NewsHeader = ({ regionModel, selectedNewsType, selectNewsType }: NewsHeaderProps): ReactElement => { - const { t } = useTranslation('news') - const theme = useTheme() - const selectLocalNews = () => selectNewsType(LOCAL_NEWS_TYPE) - const selectTuNews = () => selectNewsType(TU_NEWS_TYPE) - - return ( - <> - - {regionModel.localNewsEnabled && regionModel.tuNewsEnabled && ( - - - - - {t('local')} - - - - - - - - )} - - ) -} - -export default memo(NewsHeader) diff --git a/native/src/components/NewsListItem.tsx b/native/src/components/NewsListItem.tsx index 3d42809a7f..9dbc22bf76 100644 --- a/native/src/components/NewsListItem.tsx +++ b/native/src/components/NewsListItem.tsx @@ -1,11 +1,10 @@ -import { DateTime } from 'luxon' import React, { ReactElement } from 'react' import { useTranslation } from 'react-i18next' import { StyleSheet, View } from 'react-native' import { List as PaperList } from 'react-native-paper' import styled, { useTheme } from 'styled-components/native' -import { LocalNewsModel, TuNewsModel } from 'shared/api' +import { NewsModel } from 'shared/api' import { EXCERPT_MAX_LINES } from '../constants' import { contentAlignmentRTLText, contentDirection } from '../constants/contentDirection' @@ -14,7 +13,7 @@ import TimeStamp from './TimeStamp' import Text from './base/Text' type NewsListItemProps = { - newsItem: LocalNewsModel | TuNewsModel + newsItem: NewsModel navigateToNews: () => void } @@ -41,21 +40,10 @@ export const Description = styled.View` flex-direction: column; ` -const getTimestamp = (newsItem: LocalNewsModel | TuNewsModel): DateTime | null => { - if ('timestamp' in newsItem) { - return newsItem.timestamp - } - if ('lastUpdate' in newsItem) { - return newsItem.lastUpdate - } - return null -} - const NewsListItem = ({ newsItem, navigateToNews }: NewsListItemProps): ReactElement => { const { t, i18n } = useTranslation('news') const { languageCode } = useAppContext() const theme = useTheme() - const timestamp = getTimestamp(newsItem) return ( @@ -77,13 +65,9 @@ const NewsListItem = ({ newsItem, navigateToNews }: NewsListItemProps): ReactEle {newsItem.content} - {timestamp ? ( - - - - ) : ( - - )} + + + = { + setValue: (value: T) => void + options: T[] + value: T + getLabel: (value: T) => string +} + +const ToggleTextButtonGroup = ({ + setValue, + options, + value, + getLabel, +}: ToggleTextButtonGroupProps): ReactElement => { + const theme = useTheme() + + const buttons = options.map(option => ({ + icon: value === option ? 'check' : undefined, + value: option, + label: getLabel(option), + checkedColor: theme.colors.onPrimary, + uncheckedColor: theme.colors.onSurface, + style: { + backgroundColor: value === option ? theme.colors.primary : theme.colors.background, + borderColor: theme.colors.primary, + }, + })) + + return +} + +export default ToggleTextButtonGroup diff --git a/native/src/components/__tests__/News.spec.tsx b/native/src/components/__tests__/News.spec.tsx index 9217bef61c..7789b5feea 100644 --- a/native/src/components/__tests__/News.spec.tsx +++ b/native/src/components/__tests__/News.spec.tsx @@ -2,8 +2,8 @@ import { fireEvent } from '@testing-library/react-native' import { DateTime } from 'luxon' import React from 'react' -import { LocalNewsType, TU_NEWS_TYPE, TuNewsType, replaceLinks } from 'shared' -import { LanguageModelBuilder, RegionModel, LocalNewsModel, TuNewsModel } from 'shared/api' +import { NEWS_ALL_SOURCES_FILTER, replaceLinks } from 'shared' +import { LanguageModelBuilder, NewsModel, RegionModel, RegionModelBuilder } from 'shared/api' import useNavigate from '../../hooks/useNavigate' import createNavigationPropMock from '../../testing/createNavigationPropMock' @@ -15,35 +15,27 @@ import mocked = jest.mocked jest.mock('react-i18next') jest.mock('../../components/Page') jest.mock('../../hooks/useNavigate') +jest.mock('../../utils/openExternalUrl') -const news: [TuNewsModel, TuNewsModel] = [ - new TuNewsModel({ - id: 9902, - title: 'Was ist ein Verein?', - lastUpdate: DateTime.fromISO('2020-01-20T00:00:00.000Z'), - tags: [], - content: - 'Ein Verein ist eine Gruppe von Menschen. Sie haben ein gemeinsames Interesse und organisieren. https://example.com', - eNewsNo: 'tun0000009902', - }), - new TuNewsModel({ - id: 1234, +const defaultNews: [NewsModel, NewsModel] = [ + new NewsModel({ + id: 1, title: 'Tick bite - What to do?', - tags: ['8 Gesundheit'], lastUpdate: DateTime.fromISO('2020-01-20T00:00:00.000Z'), content: - 'In summer there are often ticks in forest and meadows with high grass. These are very small animals. They feed on the blood of people or animals they sting, like mosquitoes. But they stay in the skin longer and can transmit dangerous diseases. If you have been in high grass, you should search your body very thoroughly for ticks. They like to sit in the knees, armpits or in the groin area. If you discover a tick in your skin, you should carefully pull it out with tweezers without crushing it. If the sting inflames, you must see a doctor. tünews INTERNATIONAL', - eNewsNo: 'tun0000009902', + 'In summer there are often ticks in forest and meadows with high grass. These are very small animals. They feed on the blood of people or animals they sting, like mosquitoes. But they stay in the skin longer and can transmit dangerous diseases. If you have been in high grass, you should search your body very thoroughly for ticks. They like to sit in the knees, armpits or in the groin area. If you discover a tick in your skin, you should carefully pull it out with tweezers without crushing it. If the sting inflames, you must see a doctor. https://example.com', + availableLanguages: { de: 1234 }, + externalUrl: 'https://example.com', + source: 'tunews', }), -] - -const localNews: [LocalNewsModel] = [ - new LocalNewsModel({ - id: 1234, - timestamp: DateTime.fromISO('2019-03-01T00:00:00.000'), - title: 'Local News', - content: 'Local news with url: https://example.com', - availableLanguages: {}, + new NewsModel({ + id: 2, + title: 'Test Local', + lastUpdate: DateTime.fromISO('2020-01-21T00:00:00.000Z'), + content: 'Test local news content. https://example.com', + availableLanguages: { de: 123 }, + externalUrl: 'https://example.com', + source: 'local', }), ] @@ -53,25 +45,19 @@ describe('News', () => { }) const navigation = createNavigationPropMock() - mocked(useNavigate).mockImplementation(() => ({ navigateTo: jest.fn(), navigation })) + const navigateTo = jest.fn() + mocked(useNavigate).mockImplementation(() => ({ navigateTo, navigation })) const language = 'de' - const navigateToNews = jest.fn() - const loadMore = jest.fn() - const refresh = jest.fn() const renderNews = ({ - selectedNewsType = TU_NEWS_TYPE, - newsId = null, - data = news, - loadingMore = false, - tuNewsEnabled = true, + id = null, + news = defaultNews, + externalNewsEnabled = true, localNewsEnabled = true, }: { - newsId?: number | null - data?: (LocalNewsModel | TuNewsModel)[] - loadingMore?: boolean - selectedNewsType?: TuNewsType | LocalNewsType - tuNewsEnabled?: boolean + id?: number | null + news?: NewsModel[] + externalNewsEnabled?: boolean localNewsEnabled?: boolean }) => { const regionModel = new RegionModel({ @@ -82,7 +68,7 @@ describe('News', () => { eventsEnabled: true, placesEnabled: false, localNewsEnabled, - tuNewsEnabled, + externalNewsEnabled, sortingName: 'Oldtown', prefix: 'GoT', latitude: 48.369696, @@ -92,68 +78,72 @@ describe('News', () => { chatEnabled: false, chatPrivacyPolicyUrl: null, }) - const props = { regionModel, language, navigateToNews, loadMore, refresh, selectedNewsType } - return render() + const setNewsSource = jest.fn() + const props = { regionModel, language } + return render( + , + ) } it('should show not found error if news with id not found', () => { - const { getByText } = renderNews({ newsId: 32498732984824 }) + const { getByText } = renderNews({ id: 32498732984824 }) expect(getByText('pageNotFound')).toBeTruthy() }) it('should show news detail', () => { - const { queryByText } = renderNews({ newsId: news[0].id }) - expect(queryByText(news[0].title)).toBeTruthy() - expect(queryByText(news[0].content)).toBeTruthy() + const { queryByText } = renderNews({ id: defaultNews[0].id }) + expect(queryByText(defaultNews[0].title)).toBeTruthy() + expect(queryByText(defaultNews[0].content)).toBeTruthy() - expect(queryByText(news[1].title)).toBeFalsy() + expect(queryByText(defaultNews[1].title)).toBeFalsy() }) it('should show news list', () => { const { getByText } = renderNews({}) - expect(getByText(news[0].title)).toBeTruthy() - expect(getByText(news[0].content)).toBeTruthy() + expect(getByText(defaultNews[0].title)).toBeTruthy() + expect(getByText(defaultNews[0].content)).toBeTruthy() - expect(getByText(news[1].title)).toBeTruthy() - expect(getByText(news[1].content)).toBeTruthy() + expect(getByText(defaultNews[1].title)).toBeTruthy() + expect(getByText(defaultNews[1].content)).toBeTruthy() - fireEvent.press(getByText(news[1].title)) - expect(navigateToNews).toHaveBeenCalledWith(news[1].id) + fireEvent.press(getByText(defaultNews[1].title)) + expect(navigateTo).toHaveBeenCalledWith({ id: 2, languageCode: 'de', regionCode: 'augsburg', route: 'news' }) }) it('should show currently no news', () => { - const { queryByText } = renderNews({ data: [] }) + const { queryByText } = renderNews({ news: [] }) expect(queryByText('currentlyNoNews')).toBeTruthy() - expect(queryByText(news[0].title)).toBeFalsy() - expect(queryByText(news[1].title)).toBeFalsy() - }) - - it('should show loading spinner if loading more', () => { - const { getByText, getByTestId } = renderNews({ loadingMore: true }) - expect(getByTestId('loadingSpinner')).toBeTruthy() - - expect(getByText(news[0].title)).toBeTruthy() - expect(getByText(news[1].title)).toBeTruthy() + expect(queryByText(defaultNews[0].title)).toBeFalsy() + expect(queryByText(defaultNews[1].title)).toBeFalsy() }) it('should not add links in list', () => { - const { getByText } = renderNews({ data: localNews }) - expect(getByText(localNews[0].title)).toBeTruthy() - expect(getByText(localNews[0].content)).toBeTruthy() + const { getByText } = renderNews({ news: defaultNews }) + expect(getByText(defaultNews[0].title)).toBeTruthy() + expect(getByText(defaultNews[0].content)).toBeTruthy() }) it('should not add links for tünews', () => { - const { getByText, queryByText } = renderNews({ data: news, newsId: news[0].id }) - expect(getByText(news[0].title)).toBeTruthy() - expect(getByText(news[0].content)).toBeTruthy() - expect(queryByText(replaceLinks(news[0].content))).toBeFalsy() + const { getByText, queryByText } = renderNews({ news: defaultNews, id: defaultNews[0].id }) + expect(getByText(defaultNews[0].title)).toBeTruthy() + expect(getByText(defaultNews[0].content)).toBeTruthy() + expect(queryByText(replaceLinks(defaultNews[0].content))).toBeFalsy() }) it('should add links in local news detail', () => { - const { getByText, queryByText } = renderNews({ data: localNews, newsId: localNews[0].id }) - expect(getByText(localNews[0].title)).toBeTruthy() - expect(queryByText(localNews[0].content)).toBeFalsy() - expect(queryByText(replaceLinks(localNews[0].content))).toBeTruthy() + const { getByText, queryByText } = renderNews({ news: defaultNews, id: defaultNews[1].id }) + expect(getByText(defaultNews[1].title)).toBeTruthy() + expect(queryByText(defaultNews[1].content)).toBeFalsy() + expect(queryByText(replaceLinks(defaultNews[1].content))).toBeTruthy() }) }) diff --git a/native/src/components/__tests__/NewsListItem.spec.tsx b/native/src/components/__tests__/NewsListItem.spec.tsx index 59e25535ff..e6143a0fc0 100644 --- a/native/src/components/__tests__/NewsListItem.spec.tsx +++ b/native/src/components/__tests__/NewsListItem.spec.tsx @@ -2,51 +2,40 @@ import { fireEvent, RenderAPI } from '@testing-library/react-native' import { DateTime } from 'luxon' import React from 'react' -import { LocalNewsModel, TuNewsModel } from 'shared/api' +import { NewsModel } from 'shared/api' import render from '../../testing/render' import NewsListItem from '../NewsListItem' jest.mock('react-i18next') -const tuNews = new TuNewsModel({ - id: 9902, - title: 'Was ist ein Verein?', - lastUpdate: DateTime.fromISO('2020-02-20T00:00:00.000Z'), - tags: [], - content: 'Ein Verein ist eine Gruppe von Menschen. Sie haben ein gemeinsames Interesse und organisieren.', - eNewsNo: 'tun0000009902', -}) -const localNews = new LocalNewsModel({ - id: 9902, - timestamp: DateTime.fromISO('2020-01-20T00:00:00.000Z'), - title: 'Test Push Notification', - content: 'Some "test text with lots of "html entities" which won't be displayed.', - availableLanguages: {}, +const news = new NewsModel({ + id: 217, + title: 'Tick bite - What to do?', + lastUpdate: DateTime.fromISO('2020-01-20T00:00:00.000Z'), + content: + 'In summer there are often ticks in forest and meadows with high grass. These are very small animals. They feed on the blood of people or animals they sting, like mosquitoes. But they stay in the skin longer and can transmit dangerous diseases. If you have been in high grass, you should search your body very thoroughly for ticks. They like to sit in the knees, armpits or in the groin area. If you discover a tick in your skin, you should carefully pull it out with tweezers without crushing it. If the sting inflames, you must see a doctor.', + availableLanguages: { de: 123, it: 234 }, + externalUrl: 'https://example.com', + source: 'local', }) + describe('NewsListItem', () => { const navigateToNews = jest.fn() - const renderNewsListItem = (newsItem: LocalNewsModel | TuNewsModel): RenderAPI => + const renderNewsListItem = (newsItem: NewsModel): RenderAPI => render() beforeEach(() => { jest.clearAllMocks() }) - it('should correctly render a local news item', () => { - const { getByText, queryByText } = renderNewsListItem(localNews) - expect(getByText(localNews.title)).toBeTruthy() + + it('should correctly render', () => { + const { getByText } = renderNewsListItem(news) + expect(getByText(news.title)).toBeTruthy() + expect(getByText(news.content)).toBeTruthy() expect(getByText('January 20, 2020')).toBeTruthy() - expect(queryByText('Last Update')).toBeNull() - fireEvent.press(getByText(localNews.title)) - expect(navigateToNews).toHaveBeenCalled() - }) - it('should correctly render a tu news item', () => { - const { getByText } = renderNewsListItem(tuNews) - expect(getByText(tuNews.title)).toBeTruthy() - expect(getByText(tuNews.content)).toBeTruthy() - expect(getByText('February 20, 2020')).toBeTruthy() - fireEvent.press(getByText(tuNews.title)) + fireEvent.press(getByText(news.title)) expect(navigateToNews).toHaveBeenCalled() }) }) diff --git a/native/src/constants/NavigationTypes.ts b/native/src/constants/NavigationTypes.ts index 6b2b19f22b..8024180843 100644 --- a/native/src/constants/NavigationTypes.ts +++ b/native/src/constants/NavigationTypes.ts @@ -12,7 +12,6 @@ import { IntroRouteType, RegionsRouteType, NewsRouteType, - NewsType, PdfViewModalRouteType, PlacesRouteType, RedirectRouteType, @@ -99,8 +98,7 @@ export type NestedRoutesParamsType = { slug?: string } [NEWS_ROUTE]: RouteTitle & { - newsId: number | null - newsType: NewsType + id: number | null } } diff --git a/native/src/hooks/__tests__/useNavigate.spec.tsx b/native/src/hooks/__tests__/useNavigate.spec.tsx index 4008f018b2..232919c37d 100644 --- a/native/src/hooks/__tests__/useNavigate.spec.tsx +++ b/native/src/hooks/__tests__/useNavigate.spec.tsx @@ -9,7 +9,6 @@ import { IMPRINT_ROUTE, EVENTS_ROUTE, REGIONS_ROUTE, - LOCAL_NEWS_TYPE, NEWS_ROUTE, PLACES_ROUTE, RouteInformationType, @@ -188,22 +187,9 @@ describe('useNavigate', () => { }) it('should navigate to news route', () => { - renderMockComponent({ - route: NEWS_ROUTE, - ...params, - newsType: LOCAL_NEWS_TYPE, - newsId: 1234, - }) + renderMockComponent({ route: NEWS_ROUTE, ...params, id: 1234 }) expect(navigation.push).not.toHaveBeenCalled() - expect(navigateNested).toHaveBeenCalledWith( - navigation, - NEWS_ROUTE, - { - newsType: LOCAL_NEWS_TYPE, - newsId: 1234, - }, - false, - ) + expect(navigateNested).toHaveBeenCalledWith(navigation, NEWS_ROUTE, { id: 1234 }, false) expect(navigateNested).toHaveBeenCalledTimes(1) }) @@ -278,17 +264,8 @@ describe('useNavigate', () => { }) it('should pass null for newsId when not provided', () => { - renderMockComponent({ - route: NEWS_ROUTE, - ...params, - newsType: LOCAL_NEWS_TYPE, - }) - expect(navigateNested).toHaveBeenCalledWith( - navigation, - NEWS_ROUTE, - { newsType: LOCAL_NEWS_TYPE, newsId: null }, - false, - ) + renderMockComponent({ route: NEWS_ROUTE, ...params }) + expect(navigateNested).toHaveBeenCalledWith(navigation, NEWS_ROUTE, { id: null }, false) }) it('should replace with bottom tab route when redirect=true but cannot go back for external url', () => { diff --git a/native/src/hooks/useLoadRegionContent.ts b/native/src/hooks/useLoadRegionContent.ts index 7ea36dd2db..07bdce5b9f 100644 --- a/native/src/hooks/useLoadRegionContent.ts +++ b/native/src/hooks/useLoadRegionContent.ts @@ -7,11 +7,11 @@ import { ErrorCodes, createCategoriesEndpoint, createEventsEndpoint, - createLocalNewsEndpoint, + createNewsEndpoint, createPlacesEndpoint, EventModel, LanguageModel, - LocalNewsModel, + NewsModel, PlaceModel, ReturnType, createRegionsEndpoint, @@ -28,7 +28,7 @@ import useSnackbar from './useSnackbar' type Params = { regionCode: string languageCode: string - refreshLocalNews?: boolean + refreshNews?: boolean } export type RegionContentData = { @@ -38,7 +38,7 @@ export type RegionContentData = { language: LanguageModel categories: CategoriesMapModel events: EventModel[] - localNews: LocalNewsModel[] + news: NewsModel[] places: PlaceModel[] } @@ -50,7 +50,7 @@ export type RegionContentReturn = Omit, 'erro * Hook to load all the offline available region content at once and handle errors, loading and refreshing at the same time. * Takes care of updating the data regularly. */ -const useLoadRegionContent = ({ regionCode, languageCode, refreshLocalNews }: Params): RegionContentReturn => { +const useLoadRegionContent = ({ regionCode, languageCode, refreshNews }: Params): RegionContentReturn => { const showSnackbar = useSnackbar() const previousLanguageCode = usePreviousProp({ prop: languageCode }) const params = { regionCode, languageCode, showSnackbar } @@ -83,17 +83,17 @@ const useLoadRegionContent = ({ regionCode, languageCode, refreshLocalNews }: Pa getFromDataContainer: dataContainer.getPlaces, setToDataContainer: dataContainer.setPlaces, }) - const localNewsReturn = useLoadWithCache({ + const newsReturn = useLoadWithCache({ ...params, - isAvailable: dataContainer.localNewsAvailable, - createEndpoint: createLocalNewsEndpoint, - getFromDataContainer: dataContainer.getLocalNews, - setToDataContainer: dataContainer.setLocalNews, - forceUpdate: refreshLocalNews, + isAvailable: dataContainer.newsAvailable, + createEndpoint: createNewsEndpoint, + getFromDataContainer: dataContainer.getNews, + setToDataContainer: dataContainer.setNews, + forceUpdate: refreshNews, }) useEffect(() => { - if (regionsReturn.data && categoriesReturn.data && eventsReturn.data && placesReturn.data && localNewsReturn.data) { + if (regionsReturn.data && categoriesReturn.data && eventsReturn.data && placesReturn.data && newsReturn.data) { // Load the resource cache in the background once a day and do not wait for it dataContainer.getLastUpdate(regionCode, languageCode).then(lastUpdate => { if (!lastUpdate || lastUpdate < DateTime.utc().startOf('day')) { @@ -111,7 +111,7 @@ const useLoadRegionContent = ({ regionCode, languageCode, refreshLocalNews }: Pa // WARNING: This also means that the last update is updated if everything is just loaded from the cache. dataContainer.setLastUpdate(regionCode, languageCode, DateTime.utc()).catch(captureError) } - }, [regionsReturn, categoriesReturn, eventsReturn, placesReturn, localNewsReturn, regionCode, languageCode]) + }, [regionsReturn, categoriesReturn, eventsReturn, placesReturn, newsReturn, regionCode, languageCode]) const region = regionsReturn.data?.find(it => it.code === regionCode) const language = region?.languages.find(it => it.code === languageCode) @@ -132,7 +132,7 @@ const useLoadRegionContent = ({ regionCode, languageCode, refreshLocalNews }: Pa categoriesReturn.error ?? eventsReturn.error ?? placesReturn.error ?? - localNewsReturn.error ?? + newsReturn.error ?? null ) } @@ -142,14 +142,14 @@ const useLoadRegionContent = ({ regionCode, languageCode, refreshLocalNews }: Pa categoriesReturn.loading || eventsReturn.loading || placesReturn.loading || - localNewsReturn.loading + newsReturn.loading const refresh = () => { regionsReturn.refresh() categoriesReturn.refresh() eventsReturn.refresh() placesReturn.refresh() - localNewsReturn.refresh() + newsReturn.refresh() } const data = @@ -159,7 +159,7 @@ const useLoadRegionContent = ({ regionCode, languageCode, refreshLocalNews }: Pa categoriesReturn.data && eventsReturn.data && placesReturn.data && - localNewsReturn.data + newsReturn.data ? { region, language, @@ -168,7 +168,7 @@ const useLoadRegionContent = ({ regionCode, languageCode, refreshLocalNews }: Pa categories: categoriesReturn.data, events: eventsReturn.data, places: placesReturn.data, - localNews: localNewsReturn.data, + news: newsReturn.data, } : null diff --git a/native/src/hooks/useLoadTuNews.ts b/native/src/hooks/useLoadTuNews.ts deleted file mode 100644 index 830582bec2..0000000000 --- a/native/src/hooks/useLoadTuNews.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { useCallback, useEffect, useState } from 'react' - -import { - createTuNewsEndpoint, - createTuNewsLanguagesEndpoint, - LanguageModel, - loadAsync, - ReturnType, - TuNewsModel, -} from 'shared/api' - -import { tuNewsApiUrl } from '../constants/endpoint' -import { captureError } from '../utils/sentry' - -const TU_NEWS_FETCH_COUNT_LIMIT = 20 -const FIRST_PAGE_INDEX = 1 - -type ParamsType = { - language: string -} - -type TuNewsReturnType = Omit, 'setData'> & { - loadMore?: () => void - loadingMore?: boolean - availableLanguages: LanguageModel[] | null -} - -const useLoadTuNews = ({ language }: ParamsType): TuNewsReturnType => { - const [page, setPage] = useState(FIRST_PAGE_INDEX) - const [data, setData] = useState(null) - const [availableLanguages, setAvailableLanguages] = useState(null) - const [error, setError] = useState(null) - const [loading, setLoading] = useState(true) - - const updateData = useCallback((data: TuNewsModel[] | null) => { - setData(previousData => (data && previousData ? [...previousData, ...data] : data)) - }, []) - - const load = useCallback(() => { - const request = async () => { - const tuNewsLanguages = await createTuNewsLanguagesEndpoint(tuNewsApiUrl).request(undefined) - setAvailableLanguages(tuNewsLanguages.data ?? null) - - if (!tuNewsLanguages.data?.find(languageModel => languageModel.code === language)) { - return [] - } - - const { data } = await createTuNewsEndpoint(tuNewsApiUrl).request({ - language, - page, - count: TU_NEWS_FETCH_COUNT_LIMIT, - }) - - if (!data) { - throw new Error('Data missing!') - } - - return data - } - - loadAsync(request, { setData: updateData, setError, setLoading }).catch(captureError) - }, [language, page, updateData]) - - const loadMore = useCallback(() => { - const hasMoreNews = data?.length === page * TU_NEWS_FETCH_COUNT_LIMIT - if (!loading && hasMoreNews) { - setPage(page + 1) - } - }, [page, data, loading]) - - const reset = useCallback(() => { - setData(null) - setError(null) - if (page === FIRST_PAGE_INDEX) { - load() - } else { - // This automatically triggers load - setPage(FIRST_PAGE_INDEX) - } - }, [page, load]) - - useEffect(load, [load]) - - return { - data, - error, - loading: loading && !data, - loadingMore: loading && page !== FIRST_PAGE_INDEX, - refresh: reset, - loadMore, - availableLanguages, - } -} - -export default useLoadTuNews diff --git a/native/src/hooks/useLoadTuNewsElement.ts b/native/src/hooks/useLoadTuNewsElement.ts deleted file mode 100644 index fdbb403dfe..0000000000 --- a/native/src/hooks/useLoadTuNewsElement.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { useCallback } from 'react' - -import { createTuNewsElementEndpoint, ReturnType, TuNewsModel, useLoadAsync } from 'shared/api' - -import { tuNewsApiUrl } from '../constants/endpoint' - -type UseLoadTuNewsElementProps = { - newsId: number -} - -const useLoadTuNewsElement = ({ newsId }: UseLoadTuNewsElementProps): ReturnType => { - const load = useCallback(async () => { - const payload = await createTuNewsElementEndpoint(tuNewsApiUrl).request({ id: newsId }) - if (!payload.data) { - throw new Error('Data missing!') - } - return payload.data - }, [newsId]) - - return useLoadAsync(load) -} - -export default useLoadTuNewsElement diff --git a/native/src/hooks/useNavigate.ts b/native/src/hooks/useNavigate.ts index 70b2386ad7..081dd697d0 100644 --- a/native/src/hooks/useNavigate.ts +++ b/native/src/hooks/useNavigate.ts @@ -88,15 +88,7 @@ const navigate = ( return case NEWS_ROUTE: - navigateNested( - navigation, - NEWS_ROUTE, - { - newsType: routeInformation.newsType, - newsId: routeInformation.newsId ?? null, - }, - redirect, - ) + navigateNested(navigation, NEWS_ROUTE, { id: routeInformation.id ?? null }, redirect) return case PLACES_ROUTE: diff --git a/native/src/hooks/useTtsPlayer.ts b/native/src/hooks/useTtsPlayer.ts index fa760f1c70..7df268deb9 100644 --- a/native/src/hooks/useTtsPlayer.ts +++ b/native/src/hooks/useTtsPlayer.ts @@ -2,12 +2,12 @@ import { useFocusEffect } from '@react-navigation/native' import { useCallback, useContext, useState } from 'react' import { parseHTML, segmentText } from 'shared' -import { LocalNewsModel, DocumentModel, TuNewsModel } from 'shared/api' +import { NewsModel, DocumentModel } from 'shared/api' import { TtsContext, TtsContextType } from '../components/TtsContainer' import { AppContext } from '../contexts/AppContext' -const useTtsPlayer = (model?: DocumentModel | LocalNewsModel | TuNewsModel | undefined): TtsContextType => { +const useTtsPlayer = (model?: DocumentModel | NewsModel | undefined): TtsContextType => { const { languageCode } = useContext(AppContext) const ttsContext = useContext(TtsContext) const [previousSentences] = useState(ttsContext.sentences) diff --git a/native/src/routes/LocalNews.tsx b/native/src/routes/LocalNews.tsx deleted file mode 100644 index f4c6093c47..0000000000 --- a/native/src/routes/LocalNews.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React, { ReactElement, useCallback } from 'react' - -import { LOCAL_NEWS_TYPE, NEWS_ROUTE, NewsRouteType } from 'shared' - -import News from '../components/News' -import { NavigationProps, RouteProps } from '../constants/NavigationTypes' -import useHeader from '../hooks/useHeader' -import { RegionContentData } from '../hooks/useLoadRegionContent' -import usePreviousProp from '../hooks/usePreviousProp' -import urlFromRouteInformation from '../utils/url' - -type LocalNewsProps = { - route: RouteProps - navigation: NavigationProps - newsId: number | null - data: RegionContentData - navigateToNews: (newsId: number) => void - refresh: () => void -} - -const LocalNews = ({ route, navigation, data, newsId, navigateToNews, refresh }: LocalNewsProps): ReactElement => { - const regionCode = data.region.code - const languageCode = data.language.code - const selectedLocalNews = data.localNews.find(it => it.id === newsId) - const availableLanguages = selectedLocalNews - ? Object.keys(selectedLocalNews.availableLanguages) - : data.languages.map(it => it.code) - const shareUrl = urlFromRouteInformation({ - route: NEWS_ROUTE, - regionCode, - languageCode, - newsType: LOCAL_NEWS_TYPE, - newsId: newsId ?? undefined, - }) - useHeader({ navigation, route, availableLanguages, data, shareUrl }) - - const onLanguageChange = useCallback( - (newLanguage: string) => { - if (selectedLocalNews) { - navigation.setParams({ newsId: selectedLocalNews.availableLanguages[newLanguage] }) - } - }, - [selectedLocalNews, navigation], - ) - usePreviousProp({ prop: languageCode, onPropChange: onLanguageChange }) - - return ( - - ) -} - -export default LocalNews diff --git a/native/src/routes/NewsContainer.tsx b/native/src/routes/NewsContainer.tsx index 5dbc7463cc..99803b6086 100644 --- a/native/src/routes/NewsContainer.tsx +++ b/native/src/routes/NewsContainer.tsx @@ -1,63 +1,75 @@ -import React, { ReactElement, useCallback } from 'react' +import React, { ReactElement, useCallback, useState } from 'react' -import { LOCAL_NEWS_TYPE, NEWS_ROUTE, NewsRouteType, NewsType, TU_NEWS_TYPE } from 'shared' -import { ErrorCodes } from 'shared/api' +import { + NEWS_ALL_SOURCES_FILTER, + NEWS_ROUTE, + newsFilterToSources, + NewsRouteType, + NewsSourceFilter as NewsSourceFilterType, +} from 'shared' -import NewsHeader from '../components/NewsHeader' +import News from '../components/News' import { NavigationProps, RouteProps } from '../constants/NavigationTypes' +import useHeader from '../hooks/useHeader' import useLoadRegionContent from '../hooks/useLoadRegionContent' -import useNavigate from '../hooks/useNavigate' +import usePreviousProp from '../hooks/usePreviousProp' import useRegionAppContext from '../hooks/useRegionAppContext' +import urlFromRouteInformation from '../utils/url' import LoadingErrorHandler from './LoadingErrorHandler' -import LocalNews from './LocalNews' -import TuNews from './TuNews' -import TuNewsDetail from './TuNewsDetail' type NewsContainerProps = { route: RouteProps navigation: NavigationProps } -const NewsContainer = ({ route, navigation }: NewsContainerProps): ReactElement | null => { - const { newsType, newsId } = route.params +const NewsContainer = ({ navigation, route }: NewsContainerProps): ReactElement | null => { + const { id } = route.params const { regionCode, languageCode } = useRegionAppContext() - const { data, ...response } = useLoadRegionContent({ regionCode, languageCode, refreshLocalNews: true }) - const { navigateTo } = useNavigate() + const [newsSourceFilter, setNewsSourceFilter] = useState(NEWS_ALL_SOURCES_FILTER) + const { data, ...response } = useLoadRegionContent({ + regionCode, + languageCode, + refreshNews: true, + }) - const navigateToNews = useCallback( - (newsId: number) => navigateTo({ route: NEWS_ROUTE, regionCode, languageCode, newsType, newsId }), - [regionCode, languageCode, newsType, navigateTo], - ) + const newsSources = newsFilterToSources(newsSourceFilter) + const news = data?.news.filter(news => !newsSources || newsSources.includes(news.source)) + const currentNews = id != null ? data?.news.find(it => it.id === id) : undefined + const availableLanguages = currentNews + ? Object.keys(currentNews.availableLanguages ?? {}) + : data?.languages.map(it => it.code) - const selectNewsType = (newsType: NewsType) => navigation.setParams({ newsType, newsId: null }) + const shareUrl = urlFromRouteInformation({ + route: NEWS_ROUTE, + languageCode, + regionCode, + id: id ?? undefined, + }) + useHeader({ navigation, route, availableLanguages, data, shareUrl }) - const isDisabled = data && (newsType === LOCAL_NEWS_TYPE ? !data.region.localNewsEnabled : !data.region.tuNewsEnabled) - const error = isDisabled ? ErrorCodes.PageNotFound : response.error + const onLanguageChange = useCallback( + (newLanguage: string) => { + if (currentNews) { + const newId = currentNews.availableLanguages?.[newLanguage] + navigation.setParams({ id: newId }) + } + }, + [currentNews, navigation], + ) + usePreviousProp({ prop: languageCode, onPropChange: onLanguageChange }) return ( - - {data && ( - <> - {newsId === null && ( - - )} - {newsType === LOCAL_NEWS_TYPE && ( - - )} - {newsType === TU_NEWS_TYPE && - (newsId === null ? ( - - ) : ( - - ))} - + + {data && news && ( + )} ) diff --git a/native/src/routes/TuNews.tsx b/native/src/routes/TuNews.tsx deleted file mode 100644 index 797a68fdfd..0000000000 --- a/native/src/routes/TuNews.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React, { ReactElement } from 'react' - -import { NEWS_ROUTE, NewsRouteType, TU_NEWS_TYPE } from 'shared' -import { ErrorCodes } from 'shared/api' - -import News from '../components/News' -import { NavigationProps, RouteProps } from '../constants/NavigationTypes' -import useHeader from '../hooks/useHeader' -import { RegionContentData } from '../hooks/useLoadRegionContent' -import useLoadTuNews from '../hooks/useLoadTuNews' -import urlFromRouteInformation from '../utils/url' -import LoadingErrorHandler from './LoadingErrorHandler' - -type TuNewsProps = { - route: RouteProps - navigation: NavigationProps - data: RegionContentData - navigateToNews: (newsId: number) => void -} - -const TuNews = ({ route, navigation, data, navigateToNews }: TuNewsProps): ReactElement => { - const regionCode = data.region.code - const languageCode = data.language.code - const { - data: tuNews, - availableLanguages, - loadMore, - loadingMore, - ...response - } = useLoadTuNews({ language: languageCode }) - - const availableLanguageCodes = availableLanguages?.map(it => it.code) - const shareUrl = urlFromRouteInformation({ route: NEWS_ROUTE, regionCode, languageCode, newsType: TU_NEWS_TYPE }) - useHeader({ navigation, route, availableLanguages: availableLanguageCodes, data, shareUrl }) - - const error = - availableLanguageCodes && !availableLanguageCodes.find(it => it === languageCode) - ? ErrorCodes.LanguageUnavailable - : response.error - - return ( - - {tuNews && ( - - )} - - ) -} - -export default TuNews diff --git a/native/src/routes/TuNewsDetail.tsx b/native/src/routes/TuNewsDetail.tsx deleted file mode 100644 index a63e29798a..0000000000 --- a/native/src/routes/TuNewsDetail.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React, { ReactElement } from 'react' -import styled from 'styled-components/native' - -import { NEWS_ROUTE, NewsRouteType, TU_NEWS_TYPE } from 'shared' - -import { TuNewsActiveIcon } from '../assets' -import Page from '../components/Page' -import Icon from '../components/base/Icon' -import { NavigationProps, RouteProps } from '../constants/NavigationTypes' -import useHeader from '../hooks/useHeader' -import { RegionContentData } from '../hooks/useLoadRegionContent' -import useLoadTuNewsElement from '../hooks/useLoadTuNewsElement' -import useSetRouteTitle from '../hooks/useSetRouteTitle' -import urlFromRouteInformation from '../utils/url' -import LoadingErrorHandler from './LoadingErrorHandler' - -const HeaderImageWrapper = styled.View` - margin: 20px 16px 0; - border-radius: 8px; - background-color: rgb(2, 121, 166, 0.4); -` -const StyledIcon = styled(Icon)` - height: 34px; - width: 100px; -` - -type TuNewsProps = { - route: RouteProps - navigation: NavigationProps - newsId: number - data: RegionContentData -} - -const TuNewsDetail = ({ route, navigation, data, newsId }: TuNewsProps): ReactElement => { - const regionCode = data.region.code - const languageCode = data.language.code - const { data: tuNews, ...response } = useLoadTuNewsElement({ newsId }) - useSetRouteTitle({ navigation, title: tuNews?.title }) - - const shareUrl = urlFromRouteInformation({ - route: NEWS_ROUTE, - regionCode, - languageCode, - newsType: TU_NEWS_TYPE, - newsId, - }) - useHeader({ navigation, route, availableLanguages: [languageCode], data, shareUrl }) - - return ( - - {tuNews && ( - <> - - - - - - )} - - ) -} - -export default TuNewsDetail diff --git a/native/src/routes/__tests__/Chat.spec.tsx b/native/src/routes/__tests__/Chat.spec.tsx index c2326aba64..51897da440 100644 --- a/native/src/routes/__tests__/Chat.spec.tsx +++ b/native/src/routes/__tests__/Chat.spec.tsx @@ -55,7 +55,7 @@ const data = { events: [], places: [], extra: [], - localNews: [], + news: [], } const buildMessage = (id: number, userIsAuthor: boolean) => @@ -96,7 +96,7 @@ describe('Chat', () => { () => ({ refresh: jest.fn(), - refreshLocalNews: jest.fn(), + refreshNews: jest.fn(), loading: false, error: null, data, diff --git a/native/src/routes/__tests__/LocalNews.spec.tsx b/native/src/routes/__tests__/LocalNews.spec.tsx deleted file mode 100644 index db7dcfdc83..0000000000 --- a/native/src/routes/__tests__/LocalNews.spec.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { fireEvent } from '@testing-library/react-native' -import { DateTime } from 'luxon' -import React from 'react' - -import { LOCAL_NEWS_TYPE, NEWS_ROUTE, NewsRouteType } from 'shared' -import { LanguageModelBuilder, CategoriesMapModelBuilder, RegionModelBuilder, LocalNewsModel } from 'shared/api' - -import useNavigate from '../../hooks/useNavigate' -import createNavigationScreenPropMock from '../../testing/createNavigationPropMock' -import render from '../../testing/render' -import LocalNews from '../LocalNews' - -jest.mock('react-i18next') -jest.mock('../../components/Page') -jest.mock('@react-native-community/netinfo') -jest.mock('react-native/Libraries/EventEmitter/NativeEventEmitter') -jest.mock('../../hooks/useNavigate') - -const news: [LocalNewsModel, LocalNewsModel] = [ - new LocalNewsModel({ - id: 9902, - title: 'Local news 1', - timestamp: DateTime.fromISO('2020-01-20T00:00:00.000Z'), - content: 'Local news content 2', - availableLanguages: {}, - }), - new LocalNewsModel({ - id: 1234, - title: 'Local news 2', - timestamp: DateTime.fromISO('2020-01-20T00:00:00.000Z'), - content: 'Local news content 2', - availableLanguages: {}, - }), -] - -const regions = new RegionModelBuilder(3).build() -const region = regions[0]! -const languages = new LanguageModelBuilder(3).build() -const language = languages[0]! - -const data = { - regions, - languages, - region, - language, - categories: new CategoriesMapModelBuilder(region.code, language.code).build(), - events: [], - places: [], - localNews: news, -} - -describe('LocalNews', () => { - beforeEach(() => { - jest.clearAllMocks() - }) - - const selectNews = jest.fn() - - const navigation = createNavigationScreenPropMock() - const { mocked } = jest - mocked(useNavigate).mockImplementation(() => ({ navigateTo: jest.fn(), navigation })) - const route = { - key: 'route-id-0', - params: { - newsType: LOCAL_NEWS_TYPE, - newsId: null, - }, - name: NEWS_ROUTE, - } - - const renderNews = ({ newsId = null }: { newsId?: number | null }) => - render( - , - ) - - it('should show news list', () => { - const { getByText } = renderNews({}) - expect(getByText(news[0].title)).toBeTruthy() - expect(getByText(news[1].title)).toBeTruthy() - - fireEvent.press(getByText(news[1].title)) - expect(selectNews).toHaveBeenCalledWith(news[1].id) - }) - - it('should show news detail', () => { - const { queryByText } = renderNews({ newsId: news[0].id }) - expect(queryByText(news[0].title)).toBeTruthy() - expect(queryByText(news[0].content)).toBeTruthy() - - expect(queryByText(news[1].title)).toBeFalsy() - }) -}) diff --git a/native/src/routes/__tests__/NewsContainer.spec.tsx b/native/src/routes/__tests__/NewsContainer.spec.tsx deleted file mode 100644 index a3d3688c4d..0000000000 --- a/native/src/routes/__tests__/NewsContainer.spec.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import { fireEvent } from '@testing-library/react-native' -import React from 'react' -import { Pressable, View } from 'react-native' - -import { LocalNewsType, NEWS_ROUTE, NewsRouteType, TU_NEWS_TYPE, TuNewsType } from 'shared' -import { CategoriesMapModelBuilder, RegionModelBuilder, LanguageModelBuilder } from 'shared/api' - -import Text from '../../components/base/Text' -import useLoadRegionContent from '../../hooks/useLoadRegionContent' -import useNavigate from '../../hooks/useNavigate' -import TestingAppContext from '../../testing/TestingAppContext' -import createNavigationPropMock from '../../testing/createNavigationPropMock' -import render from '../../testing/render' -import NewsContainer from '../NewsContainer' - -jest.mock('../../utils/FetcherModule') -jest.mock('@react-navigation/native') -jest.mock('@react-native-community/netinfo') -jest.mock('../../hooks/useLoadRegionContent') -jest.mock('react-i18next') -jest.mock('../../hooks/useNavigate') -jest.mock('../LocalNews', () => () => LocalNewsContent) -jest.mock('../TuNews', () => ({ navigateToNews }: { navigateToNews: (id: string) => void }) => ( - - TuNewsContent - navigateToNews('1234')}> - navigateToNews - - -)) - -jest.mock('../TuNewsDetail', () => ({ newsId }: { newsId: string }) => ( - - TuNewsDetail - {newsId} - -)) - -describe('NewsContainer', () => { - beforeEach(() => { - jest.clearAllMocks() - }) - - const navigation = createNavigationPropMock() - const navigateTo = jest.fn() - const { mocked } = jest - mocked(useNavigate).mockImplementation(() => ({ navigateTo, navigation })) - const regions = new RegionModelBuilder(3).build() - const region = regions[0]! - const languages = new LanguageModelBuilder(3).build() - const language = languages[0]! - - const data = { - regions, - languages, - region, - language, - categories: new CategoriesMapModelBuilder(region.code, language.code).build(), - events: [], - places: [], - extra: [], - localNews: [], - } - - const returnValue = { - refresh: jest.fn(), - refreshLocalNews: jest.fn(), - loading: false, - error: null, - data, - } - - const renderNews = ({ - newsType = TU_NEWS_TYPE, - newsId = null, - }: { - newsType?: LocalNewsType | TuNewsType - newsId?: number | null - }) => { - const route = { - key: 'route-id-0', - params: { - newsType, - newsId, - }, - name: NEWS_ROUTE, - } - - return render( - - - , - false, - ) - } - - it('should correctly handle switch between local and tu news', () => { - mocked(useLoadRegionContent).mockImplementation(() => returnValue) - const { getByLabelText } = renderNews({ newsId: null }) - - fireEvent.press(getByLabelText('TüNews')) - expect(navigation.setParams).toHaveBeenCalledTimes(1) - expect(navigation.setParams).toHaveBeenCalledWith({ newsType: 'tu-news', newsId: null }) - - fireEvent.press(getByLabelText('local')) - expect(navigation.setParams).toHaveBeenCalledTimes(2) - expect(navigation.setParams).toHaveBeenCalledWith({ newsType: 'local', newsId: null }) - }) - - it('should initialize news type correctly', () => { - mocked(useLoadRegionContent).mockImplementation(() => returnValue) - const { getByText, queryByText } = renderNews({ newsType: TU_NEWS_TYPE }) - - expect(getByText('TuNewsContent')).toBeTruthy() - expect(queryByText('LocalNewsContent')).toBeFalsy() - }) - - it('should render tuNews detail', () => { - mocked(useLoadRegionContent).mockImplementation(() => returnValue) - const { getByText } = renderNews({ newsId: 4321 }) - - expect(getByText('4321')).toBeTruthy() - expect(getByText('TuNewsDetail')).toBeTruthy() - }) - - it('should handle selection of news correctly', () => { - mocked(useLoadRegionContent).mockImplementation(() => returnValue) - const { getByText } = renderNews({}) - - fireEvent.press(getByText('navigateToNews')) - expect(navigateTo).toHaveBeenCalledTimes(1) - expect(navigateTo).toHaveBeenCalledWith(expect.objectContaining({ newsId: '1234' })) - }) -}) diff --git a/native/src/routes/__tests__/PlacesContainer.spec.tsx b/native/src/routes/__tests__/PlacesContainer.spec.tsx index 9d7fda16da..769565786e 100644 --- a/native/src/routes/__tests__/PlacesContainer.spec.tsx +++ b/native/src/routes/__tests__/PlacesContainer.spec.tsx @@ -45,7 +45,7 @@ const data = { language: languages[0]!, categories: {}, events: [], - localNews: [], + news: [], } describe('PlacesContainer', () => { diff --git a/native/src/routes/__tests__/TuNews.spec.tsx b/native/src/routes/__tests__/TuNews.spec.tsx deleted file mode 100644 index ff4997b26f..0000000000 --- a/native/src/routes/__tests__/TuNews.spec.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { fireEvent } from '@testing-library/react-native' -import { DateTime } from 'luxon' -import React from 'react' - -import { NEWS_ROUTE, NewsRouteType, TU_NEWS_TYPE } from 'shared' -import { CategoriesMapModelBuilder, RegionModelBuilder, LanguageModelBuilder, TuNewsModel } from 'shared/api' - -import Text from '../../components/base/Text' -import useLoadTuNews from '../../hooks/useLoadTuNews' -import useNavigate from '../../hooks/useNavigate' -import createNavigationScreenPropMock from '../../testing/createNavigationPropMock' -import render from '../../testing/render' -import TuNews from '../TuNews' - -jest.mock('react-i18next') -jest.mock('../../components/Page') -jest.mock('../../hooks/useLoadTuNews') -jest.mock('shared', () => ({ - ...jest.requireActual('shared'), - useLoadFromEndpoint: jest.fn(), -})) -jest.mock('../../components/LanguageNotAvailablePage', () => () => languageNotAvailable) -jest.mock('@react-native-community/netinfo') -jest.mock('react-native/Libraries/EventEmitter/NativeEventEmitter') -jest.mock('../../hooks/useNavigate') - -const news: [TuNewsModel, TuNewsModel] = [ - new TuNewsModel({ - id: 9902, - title: 'Was ist ein Verein?', - lastUpdate: DateTime.fromISO('2020-01-20T00:00:00.000Z'), - tags: [], - content: 'Ein Verein ist eine Gruppe von Menschen. Sie haben ein gemeinsames Interesse und organisieren.', - eNewsNo: 'tun0000009902', - }), - new TuNewsModel({ - id: 1234, - title: 'Tick bite - What to do?', - tags: ['8 Gesundheit'], - lastUpdate: DateTime.fromISO('2020-01-20T00:00:00.000Z'), - content: - 'In summer there are often ticks in forest and meadows with high grass. These are very small animals. They feed on the blood of people or animals they sting, like mosquitoes. But they stay in the skin longer and can transmit dangerous diseases. If you have been in high grass, you should search your body very thoroughly for ticks. They like to sit in the knees, armpits or in the groin area. If you discover a tick in your skin, you should carefully pull it out with tweezers without crushing it. If the sting inflames, you must see a doctor. tünews INTERNATIONAL', - eNewsNo: 'tun0000009902', - }), -] - -describe('TuNews', () => { - beforeEach(() => { - jest.clearAllMocks() - }) - - const selectNews = jest.fn() - const loadMore = jest.fn() - const refresh = jest.fn() - const navigation = createNavigationScreenPropMock() - const { mocked } = jest - mocked(useNavigate).mockImplementation(() => ({ navigateTo: jest.fn(), navigation })) - - const route = { - key: 'route-id-0', - params: { - newsType: TU_NEWS_TYPE, - newsId: null, - }, - name: NEWS_ROUTE, - } - - const regions = new RegionModelBuilder(3).build() - const region = regions[0]! - const languages = new LanguageModelBuilder(3).build() - const language = languages[0]! - - const data = { - regions, - languages, - region, - language, - categories: new CategoriesMapModelBuilder(region.code, language.code).build(), - events: [], - places: [], - localNews: [], - } - - const renderNews = () => - render() - const tuNewsResponse = { - error: null, - loading: false, - loadMore, - loadingMore: false, - refresh, - data: news, - availableLanguages: languages, - } - - it('should show news list', () => { - mocked(useLoadTuNews).mockImplementation(() => tuNewsResponse) - - const { getByText } = renderNews() - expect(getByText(news[0].title)).toBeTruthy() - expect(getByText(news[1].title)).toBeTruthy() - - fireEvent.press(getByText(news[1].title)) - expect(selectNews).toHaveBeenCalledWith(news[1].id) - }) - - it('should show language selector if language not available', async () => { - mocked(useLoadTuNews).mockImplementation(() => ({ - ...tuNewsResponse, - availableLanguages: languages.slice(1), - })) - - const { findByText, queryByText } = renderNews() - expect(await findByText('languageNotAvailable')).toBeTruthy() - - expect(queryByText(news[0].title)).toBeFalsy() - }) -}) diff --git a/native/src/utils/DataContainer.ts b/native/src/utils/DataContainer.ts index 989b5e1ba8..e8592eab59 100644 --- a/native/src/utils/DataContainer.ts +++ b/native/src/utils/DataContainer.ts @@ -1,6 +1,6 @@ import { DateTime } from 'luxon' -import { CategoriesMapModel, RegionModel, EventModel, LocalNewsModel, PlaceModel } from 'shared/api' +import { CategoriesMapModel, RegionModel, EventModel, NewsModel, PlaceModel } from 'shared/api' export type LanguageResourceCacheStateType = { [url: string]: string } export type RegionResourceCacheStateType = { [languageCode: string]: LanguageResourceCacheStateType } @@ -55,12 +55,12 @@ export type DataContainer = { * Returns an Array of local news. * @throws Will throw an error if the array is null. */ - getLocalNews: (region: string, language: string) => Promise + getNews: (region: string, language: string) => Promise /** * Sets the local news and persists them. */ - setLocalNews: (region: string, language: string, events: LocalNewsModel[]) => Promise + setNews: (region: string, language: string, events: NewsModel[]) => Promise /** * Returns the ResourceCache. @@ -101,7 +101,7 @@ export type DataContainer = { /** * Returns whether the local news have been loaded or not. */ - localNewsAvailable(region: string, language: string): Promise + newsAvailable(region: string, language: string): Promise /** * Returns whether the regions have been loaded or not. diff --git a/native/src/utils/DatabaseConnector.ts b/native/src/utils/DatabaseConnector.ts index 955e5433e5..f7183f6493 100644 --- a/native/src/utils/DatabaseConnector.ts +++ b/native/src/utils/DatabaseConnector.ts @@ -13,13 +13,14 @@ import { EventModel, FeaturedImageModel, LanguageModel, - LocalNewsModel, + NewsModel, LocationModel, OpeningHoursModel, PlaceModel, PlaceCategoryModel, OrganizationModel, OfferModel, + NewsSource, } from 'shared/api' import { @@ -130,7 +131,7 @@ type ContentRegionJsonType = { latitude: number aliases: Record | null pushNotificationsEnabled: boolean - tuNewsEnabled: boolean + externalNewsEnabled: boolean boundingBox: BBox } @@ -176,12 +177,14 @@ type ContentPlaceJsonType = { barrierFree: boolean | null } -type ContentLocalNewsJsonType = { +type ContentNewsJsonType = { id: number - timestamp: string + lastUpdate: string title: string content: string - availableLanguages: Record | undefined + source: NewsSource + availableLanguages: Record | null + externalUrl: string } type RegionCodeType = string @@ -611,34 +614,38 @@ class DatabaseConnector { return this.readFile(path, mapPlacesJson) } - async storeLocalNews(localNews: LocalNewsModel[], context: DatabaseContext): Promise { - const jsonModels = localNews.map( - (it: LocalNewsModel): ContentLocalNewsJsonType => ({ + async storeNews(news: NewsModel[], context: DatabaseContext): Promise { + const jsonModels = news.map( + (it: NewsModel): ContentNewsJsonType => ({ id: it.id, - timestamp: it.timestamp.toISO(), + lastUpdate: it.lastUpdate.toISO(), title: it.title, content: it.content, availableLanguages: it.availableLanguages, + externalUrl: it.externalUrl, + source: it.source, }), ) - await this.writeFile(this.getContentPath('localNews', context), JSON.stringify(jsonModels)) + await this.writeFile(this.getContentPath('news', context), JSON.stringify(jsonModels)) } - async loadLocalNews(context: DatabaseContext): Promise { - const path = this.getContentPath('localNews', context) - const mapLocalNewsJson = (json: ContentLocalNewsJsonType[]) => + async loadNews(context: DatabaseContext): Promise { + const path = this.getContentPath('news', context) + const mapNews = (json: ContentNewsJsonType[]) => json.map( jsonObject => - new LocalNewsModel({ + new NewsModel({ id: jsonObject.id, - timestamp: DateTime.fromISO(jsonObject.timestamp), + lastUpdate: DateTime.fromISO(jsonObject.lastUpdate), + source: jsonObject.source, title: jsonObject.title, content: jsonObject.content, - availableLanguages: jsonObject.availableLanguages ?? {}, + availableLanguages: jsonObject.availableLanguages, + externalUrl: jsonObject.externalUrl, }), ) - return this.readFile(path, mapLocalNewsJson) + return this.readFile(path, mapNews) } async storeRegions(regions: RegionModel[]): Promise { @@ -654,7 +661,7 @@ class DatabaseConnector { chatPrivacyPolicyUrl: region.chatPrivacyPolicyUrl, placesEnabled: region.placesEnabled, pushNotificationsEnabled: region.localNewsEnabled, - tuNewsEnabled: region.tuNewsEnabled, + externalNewsEnabled: region.externalNewsEnabled, sortingName: region.sortingName, longitude: region.longitude, latitude: region.latitude, @@ -677,7 +684,7 @@ class DatabaseConnector { languages: jsonObject.languages.map(it => new LanguageModel(it.code, it.name)), eventsEnabled: jsonObject.eventsEnabled, localNewsEnabled: jsonObject.pushNotificationsEnabled, - tuNewsEnabled: jsonObject.tuNewsEnabled, + externalNewsEnabled: jsonObject.externalNewsEnabled, placesEnabled: jsonObject.placesEnabled, sortingName: jsonObject.sortingName, prefix: jsonObject.prefix, @@ -856,8 +863,8 @@ class DatabaseConnector { return this._isPersisted(this.getContentPath('events', context)) } - isLocalNewsPersisted(context: DatabaseContext): Promise { - return this._isPersisted(this.getContentPath('localNews', context)) + isNewsPersisted(context: DatabaseContext): Promise { + return this._isPersisted(this.getContentPath('news', context)) } _isPersisted(path: string): Promise { diff --git a/native/src/utils/DefaultDataContainer.ts b/native/src/utils/DefaultDataContainer.ts index 7da7ee44c4..975f8819d2 100644 --- a/native/src/utils/DefaultDataContainer.ts +++ b/native/src/utils/DefaultDataContainer.ts @@ -1,7 +1,7 @@ import { difference, omitBy } from 'lodash' import { DateTime } from 'luxon' -import { CategoriesMapModel, RegionModel, EventModel, LocalNewsModel, PlaceModel } from 'shared/api' +import { CategoriesMapModel, RegionModel, EventModel, NewsModel, PlaceModel } from 'shared/api' import Cache from '../models/Cache' import DatabaseContext from '../models/DatabaseContext' @@ -15,7 +15,7 @@ type CacheType = { regions: Cache events: Cache categories: Cache - localNews: Cache + news: Cache resourceCache: Cache lastUpdate: Cache } @@ -51,11 +51,11 @@ class DefaultDataContainer implements DataContainer { (value: CategoriesMapModel, connector: DatabaseConnector, context: DatabaseContext) => connector.storeCategories(value, context), ), - localNews: new Cache( + news: new Cache( this._databaseConnector, - (connector: DatabaseConnector, context: DatabaseContext) => connector.loadLocalNews(context), - (value: LocalNewsModel[], connector: DatabaseConnector, context: DatabaseContext) => - connector.storeLocalNews(value, context), + (connector: DatabaseConnector, context: DatabaseContext) => connector.loadNews(context), + (value: NewsModel[], connector: DatabaseConnector, context: DatabaseContext) => + connector.storeNews(value, context), ), resourceCache: new Cache( this._databaseConnector, @@ -100,8 +100,8 @@ class DefaultDataContainer implements DataContainer { getPlaces = (region: string, language: string): Promise => this.caches.places.get(new DatabaseContext(region, language)) - getLocalNews = (region: string, language: string): Promise => - this.caches.localNews.get(new DatabaseContext(region, language)) + getNews = (region: string, language: string): Promise => + this.caches.news.get(new DatabaseContext(region, language)) getResourceCache = async (region: string, language: string): Promise => { const resourceCache = await this.caches.resourceCache.get(new DatabaseContext(region)) @@ -119,8 +119,8 @@ class DefaultDataContainer implements DataContainer { await this.caches.places.cache(places, new DatabaseContext(region, language)) } - setLocalNews = async (region: string, language: string, localNews: LocalNewsModel[]): Promise => { - await this.caches.localNews.cache(localNews, new DatabaseContext(region, language)) + setNews = async (region: string, language: string, news: NewsModel[]): Promise => { + await this.caches.news.cache(news, new DatabaseContext(region, language)) } setRegions = async (regions: RegionModel[]): Promise => { @@ -188,9 +188,9 @@ class DefaultDataContainer implements DataContainer { return this.isCached('events', context) || this._databaseConnector.isEventsPersisted(context) } - localNewsAvailable = async (region: string, language: string): Promise => { + newsAvailable = async (region: string, language: string): Promise => { const context = new DatabaseContext(region, language) - return this.isCached('localNews', context) || this._databaseConnector.isLocalNewsPersisted(context) + return this.isCached('news', context) || this._databaseConnector.isNewsPersisted(context) } storeLastUsage = async (region: string): Promise => { diff --git a/native/src/utils/PushNotificationsManager.ts b/native/src/utils/PushNotificationsManager.ts index 10a7c51046..1d15423a66 100644 --- a/native/src/utils/PushNotificationsManager.ts +++ b/native/src/utils/PushNotificationsManager.ts @@ -12,7 +12,7 @@ import { import { useEffect } from 'react' import { checkNotifications, requestNotifications, RESULTS } from 'react-native-permissions' -import { LOCAL_NEWS_TYPE, NEWS_ROUTE, NonNullableRouteInformationType, RouteInformationType } from 'shared' +import { NEWS_ROUTE, NonNullableRouteInformationType, RouteInformationType } from 'shared' import buildConfig from '../constants/buildConfig' import { AppContextType } from '../contexts/AppContext' @@ -109,8 +109,7 @@ const routeInformationFromMessage = (message: Message): NonNullableRouteInformat regionCode: message.data.city_code, languageCode: message.data.language_code, route: NEWS_ROUTE, - newsType: LOCAL_NEWS_TYPE, - newsId: parseInt(message.data.news_id, 10), + id: parseInt(message.data.news_id, 10), }) const openMessage = (message: Message, navigate: (routeInformation: RouteInformationType) => void): void => { diff --git a/native/src/utils/__tests__/PushNotificationsManager.spec.tsx b/native/src/utils/__tests__/PushNotificationsManager.spec.tsx index d968f86c56..d1cb4009e0 100644 --- a/native/src/utils/__tests__/PushNotificationsManager.spec.tsx +++ b/native/src/utils/__tests__/PushNotificationsManager.spec.tsx @@ -157,8 +157,7 @@ describe('PushNotificationsManager', () => { expect(navigate).toHaveBeenCalledWith({ regionCode: 'augsburg', languageCode: 'de', - newsId: 123, - newsType: 'local', + id: 123, route: 'news', }) }) @@ -170,8 +169,7 @@ describe('PushNotificationsManager', () => { expect(navigate).toHaveBeenCalledWith({ regionCode: 'augsburg', languageCode: 'de', - newsId: 123, - newsType: 'local', + id: 123, route: 'news', }) }) diff --git a/native/src/utils/__tests__/navigation.spec.ts b/native/src/utils/__tests__/navigation.spec.ts index b9b477bf28..90dcfe7991 100644 --- a/native/src/utils/__tests__/navigation.spec.ts +++ b/native/src/utils/__tests__/navigation.spec.ts @@ -53,7 +53,7 @@ describe('navigateNested', () => { const categoriesParams = { path: '/augsburg/de/category' } const eventsParams = { slug: 'some-event' } const placesParams = { slug: 'some-place' } - const newsParams = { newsId: 1, newsType: 'local' as const } + const newsParams = { id: 1 } describe('root navigator', () => { it('pushes bottom tab route with correct tab and screen', () => { diff --git a/shared/src/api/constants/index.ts b/shared/src/api/constants/index.ts index fc2e6590fe..fe3a5e3d0a 100644 --- a/shared/src/api/constants/index.ts +++ b/shared/src/api/constants/index.ts @@ -1 +1,7 @@ export const API_VERSION = 'v3' + +export const LOCAL_NEWS_SOURCE = 'local' +export const TU_NEWS_SOURCE = 'tunews' +export const AMAL_NEWS_SOURCE = 'amalnews' + +export type NewsSource = typeof LOCAL_NEWS_SOURCE | typeof TU_NEWS_SOURCE | typeof AMAL_NEWS_SOURCE diff --git a/shared/src/api/endpoints/__tests__/createFeedbackEndpoint.spec.ts b/shared/src/api/endpoints/__tests__/createFeedbackEndpoint.spec.ts index b14321013a..aa063efa49 100644 --- a/shared/src/api/endpoints/__tests__/createFeedbackEndpoint.spec.ts +++ b/shared/src/api/endpoints/__tests__/createFeedbackEndpoint.spec.ts @@ -4,7 +4,7 @@ import { EVENTS_ROUTE, PLACES_ROUTE, SEARCH_ROUTE, - TU_NEWS_TYPE, + NEWS_ROUTE, } from '../../../routes/index.ts' import { API_VERSION } from '../../constants/index.ts' import createFeedbackEndpoint, { FeedbackTypes } from '../createFeedbackEndpoint.ts' @@ -77,7 +77,7 @@ describe('feedback', () => { ${PLACES_ROUTE} | ${{ slug: '1234' }} | ${FeedbackTypes.Place} ${PLACES_ROUTE} | ${{}} | ${FeedbackTypes.Map} ${SEARCH_ROUTE} | ${{ query: 'query ' }} | ${FeedbackTypes.Search} - ${TU_NEWS_TYPE} | ${{}} | ${FeedbackTypes.Categories} + ${NEWS_ROUTE} | ${{}} | ${FeedbackTypes.Categories} `( 'should successfully request feedback for $feedbackType if rating was set', async ({ route, props, feedbackType }) => { diff --git a/shared/src/api/endpoints/__tests__/createLocalNewsElementEndpoint.spec.ts b/shared/src/api/endpoints/__tests__/createNewsElementEndpoint.spec.ts similarity index 66% rename from shared/src/api/endpoints/__tests__/createLocalNewsElementEndpoint.spec.ts rename to shared/src/api/endpoints/__tests__/createNewsElementEndpoint.spec.ts index 0312da2175..9f29086644 100644 --- a/shared/src/api/endpoints/__tests__/createLocalNewsElementEndpoint.spec.ts +++ b/shared/src/api/endpoints/__tests__/createNewsElementEndpoint.spec.ts @@ -1,33 +1,37 @@ import { DateTime } from 'luxon' import { API_VERSION } from '../../constants/index.ts' -import LocalNewsModel from '../../models/LocalNewsModel.ts' -import { JsonLocalNewsType } from '../../types.ts' -import createLocalNewsElementEndpoint from '../createLocalNewsElementEndpoint.ts' +import NewsModel from '../../models/NewsModel.ts' +import { JsonNewsType } from '../../types.ts' +import createNewsElementEndpoint from '../createNewsElementEndpoint.ts' -describe('localnews', () => { +describe('createNewsElementEndpoint', () => { const baseUrl = 'https://cms.integreat-app.de' - const localNewsElement = createLocalNewsElementEndpoint(baseUrl) + const newsElement = createNewsElementEndpoint(baseUrl) - const createNewsItem = (date: string): JsonLocalNewsType => ({ + const createNewsItem = (date: string): JsonNewsType => ({ id: 1, title: 'Tick bite - What to do?', display_date: date, - message: + content: 'In summer there are often ticks in forest and meadows with high grass. These are very small animals. They feed on the blood of people or animals they sting, like mosquitoes. But they stay in the skin longer and can transmit dangerous diseases. If you have been in high grass, you should search your body very thoroughly for ticks. They like to sit in the knees, armpits or in the groin area. If you discover a tick in your skin, you should carefully pull it out with tweezers without crushing it. If the sting inflames, you must see a doctor.', available_languages: { de: { id: 1234 } }, + source: 'local', + externalUrl: 'https://example.com', }) const item = createNewsItem('2020-03-20T17:50:00+02:00') - const createNewsItemModel = (date: DateTime): LocalNewsModel => - new LocalNewsModel({ + const createNewsItemModel = (date: DateTime): NewsModel => + new NewsModel({ id: 1, title: 'Tick bite - What to do?', - timestamp: date, + lastUpdate: date, content: 'In summer there are often ticks in forest and meadows with high grass. These are very small animals. They feed on the blood of people or animals they sting, like mosquitoes. But they stay in the skin longer and can transmit dangerous diseases. If you have been in high grass, you should search your body very thoroughly for ticks. They like to sit in the knees, armpits or in the groin area. If you discover a tick in your skin, you should carefully pull it out with tweezers without crushing it. If the sting inflames, you must see a doctor.', availableLanguages: { de: 1234 }, + externalUrl: 'https://example.com', + source: 'local', }) const itemValue = createNewsItemModel(DateTime.fromISO('2020-03-20T17:50:00+02:00')) @@ -36,19 +40,15 @@ describe('localnews', () => { language: 'en', id: '1', } + it('should map params to url', () => { - expect(localNewsElement.mapParamsToUrl(params)).toBe( - `${baseUrl}/api/${API_VERSION}/${params.region}/${params.language}/fcm/?id=${params.id}`, + expect(newsElement.mapParamsToUrl(params)).toBe( + `${baseUrl}/api/${API_VERSION}/${params.region}/${params.language}/news/${params.id}`, ) }) + it('should map fetched data to models', () => { - const itemModel = localNewsElement.mapResponse([item], params) + const itemModel = newsElement.mapResponse(item, params) expect(itemModel).toEqual(itemValue) }) - it('should throw if response is empty', () => { - expect(() => localNewsElement.mapResponse([], params)).toThrow('The local 1 does not exist here.') - }) - it('should throw a not found error if the response contains more than one item', () => { - expect(() => localNewsElement.mapResponse([item, item], params)).toThrow() - }) }) diff --git a/shared/src/api/endpoints/__tests__/createLocalNewsEndpoint.spec.ts b/shared/src/api/endpoints/__tests__/createNewsEndpoint.spec.ts similarity index 76% rename from shared/src/api/endpoints/__tests__/createLocalNewsEndpoint.spec.ts rename to shared/src/api/endpoints/__tests__/createNewsEndpoint.spec.ts index 24c18887f7..0fac2a6613 100644 --- a/shared/src/api/endpoints/__tests__/createLocalNewsEndpoint.spec.ts +++ b/shared/src/api/endpoints/__tests__/createNewsEndpoint.spec.ts @@ -1,35 +1,39 @@ import { DateTime } from 'luxon' import { API_VERSION } from '../../constants/index.ts' -import LocalNewsModel from '../../models/LocalNewsModel.ts' -import { JsonLocalNewsType } from '../../types.ts' -import createLocalNewsEndpoint from '../createLocalNewsEndpoint.ts' +import NewsModel from '../../models/NewsModel.ts' +import { JsonNewsType } from '../../types.ts' +import createNewsEndpoint from '../createNewsEndpoint.ts' -describe('localnews', () => { +describe('createNewsEndpoint', () => { const baseUrl = 'https://cms.integreat-app.de' - const localNews = createLocalNewsEndpoint(baseUrl) + const news = createNewsEndpoint(baseUrl) - const createNewsItem = (date: string): JsonLocalNewsType => ({ + const createNewsItem = (date: string): JsonNewsType => ({ id: 217, title: 'Tick bite - What to do?', display_date: date, - message: + content: 'In summer there are often ticks in forest and meadows with high grass. These are very small animals. They feed on the blood of people or animals they sting, like mosquitoes. But they stay in the skin longer and can transmit dangerous diseases. If you have been in high grass, you should search your body very thoroughly for ticks. They like to sit in the knees, armpits or in the groin area. If you discover a tick in your skin, you should carefully pull it out with tweezers without crushing it. If the sting inflames, you must see a doctor.', available_languages: { de: { id: 123 }, it: { id: 234 } }, + source: 'local', + externalUrl: 'https://example.com', }) const item1 = createNewsItem('2020-03-20T17:50:00+02:00') const item2 = createNewsItem('2020-05-20T15:00:00+02:00') const item3 = createNewsItem('2019-07-20T00:00:00+02:00') - const createNewsItemModel = (date: DateTime): LocalNewsModel => - new LocalNewsModel({ + const createNewsItemModel = (date: DateTime): NewsModel => + new NewsModel({ id: 217, title: 'Tick bite - What to do?', - timestamp: date, + lastUpdate: date, content: 'In summer there are often ticks in forest and meadows with high grass. These are very small animals. They feed on the blood of people or animals they sting, like mosquitoes. But they stay in the skin longer and can transmit dangerous diseases. If you have been in high grass, you should search your body very thoroughly for ticks. They like to sit in the knees, armpits or in the groin area. If you discover a tick in your skin, you should carefully pull it out with tweezers without crushing it. If the sting inflames, you must see a doctor.', availableLanguages: { de: 123, it: 234 }, + externalUrl: 'https://example.com', + source: 'local', }) const itemModel1 = createNewsItemModel(DateTime.fromISO('2020-03-20T17:50:00+02:00')) @@ -38,19 +42,19 @@ describe('localnews', () => { const params = { region: 'testumgebung', language: 'en', - count: 1, + page: 1, } it('should map params to url', () => { - expect(localNews.mapParamsToUrl(params)).toBe( - `https://cms.integreat-app.de/api/${API_VERSION}/${params.region}/${params.language}/fcm/?channel=news`, + expect(news.mapParamsToUrl(params)).toBe( + `https://cms.integreat-app.de/api/${API_VERSION}/${params.region}/${params.language}/news`, ) }) it('should map fetched data to models', () => { const json = [item1, item2, item3] - const localNewsModels = localNews.mapResponse(json, params) + const NewsModels = news.mapResponse(json, params) const value = [itemModel1, itemModel2, itemModel3] - expect(localNewsModels).toEqual(value) + expect(NewsModels).toEqual(value) }) }) diff --git a/shared/src/api/endpoints/__tests__/createRegionsEndpoint.spec.ts b/shared/src/api/endpoints/__tests__/createRegionsEndpoint.spec.ts index 29420f8929..91d8f32a2f 100644 --- a/shared/src/api/endpoints/__tests__/createRegionsEndpoint.spec.ts +++ b/shared/src/api/endpoints/__tests__/createRegionsEndpoint.spec.ts @@ -33,7 +33,7 @@ describe('regions', () => { live: true, events: true, pois: true, - tunews: true, + external_news: true, push_notifications: true, name_without_prefix: 'Augsburg', prefix: null, @@ -58,7 +58,7 @@ describe('regions', () => { live: true, languages: languagesJson, events: false, - tunews: false, + external_news: false, pois: false, push_notifications: false, name_without_prefix: 'Regensburg', @@ -93,7 +93,7 @@ describe('regions', () => { eventsEnabled: true, placesEnabled: true, localNewsEnabled: true, - tuNewsEnabled: true, + externalNewsEnabled: true, sortingName: 'Augsburg', prefix: null, longitude: 10.89779, @@ -116,7 +116,7 @@ describe('regions', () => { eventsEnabled: false, placesEnabled: false, localNewsEnabled: false, - tuNewsEnabled: false, + externalNewsEnabled: false, sortingName: 'Regensburg', prefix: 'Stadt', latitude: 48.369696, diff --git a/shared/src/api/endpoints/__tests__/createTuNewsElementEndpoint.spec.ts b/shared/src/api/endpoints/__tests__/createTuNewsElementEndpoint.spec.ts deleted file mode 100644 index 4aad6a5737..0000000000 --- a/shared/src/api/endpoints/__tests__/createTuNewsElementEndpoint.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { DateTime } from 'luxon' - -import TuNewsModel from '../../models/TuNewsModel.ts' -import { JsonTuNewsType } from '../../types.ts' -import createTuNewsElementEndpoint from '../createTuNewsElementEndpoint.ts' - -describe('tuNews', () => { - const baseUrl = 'https://cms-test.integreat-app.de' - const tuNewsElement = createTuNewsElementEndpoint(baseUrl) - - const createNewsItem = (id: number, displayDate: string): JsonTuNewsType => ({ - id, - title: 'Tick bite - What to do?', - tags: ['8 Gesundheit'], - display_date: displayDate, - content: - 'In summer there are often ticks in forest and meadows with high grass. These are very small animals. They feed on the blood of people or animals they sting, like mosquitoes. But they stay in the skin longer and can transmit dangerous diseases. If you have been in high grass, you should search your body very thoroughly for ticks. They like to sit in the knees, armpits or in the groin area. If you discover a tick in your skin, you should carefully pull it out with tweezers without crushing it. If the sting inflames, you must see a doctor. tünews INTERNATIONAL', - enewsno: 'tun0000009902', - }) - - const item1 = createNewsItem(1, '2020-01-20T12:04:22+00:00') - - const createNewsItemModel = (id: number, lastUpdate: DateTime): TuNewsModel => - new TuNewsModel({ - id, - title: 'Tick bite - What to do?', - tags: ['8 Gesundheit'], - lastUpdate, - content: - 'In summer there are often ticks in forest and meadows with high grass. These are very small animals. They feed on the blood of people or animals they sting, like mosquitoes. But they stay in the skin longer and can transmit dangerous diseases. If you have been in high grass, you should search your body very thoroughly for ticks. They like to sit in the knees, armpits or in the groin area. If you discover a tick in your skin, you should carefully pull it out with tweezers without crushing it. If the sting inflames, you must see a doctor. tünews INTERNATIONAL', - eNewsNo: 'tun0000009902', - }) - - const itemModel1 = createNewsItemModel(1, DateTime.fromISO('2020-01-20T12:04:22+00:00')) - const params = { - region: 'augsburg', - language: 'de', - id: 1, - } - it('should map params to url', () => { - expect(tuNewsElement.mapParamsToUrl(params)).toBe(`${baseUrl}/v1/news/${params.id}`) - }) - it('should map fetched data to models', () => { - const itemModel = tuNewsElement.mapResponse(item1, params) - expect(itemModel).toEqual(itemModel1) - }) - it('should throw a not found error if the response is empty', () => { - expect(() => tuNewsElement.mapResponse([], params)).toThrow('The tu-news 1 does not exist here.') - }) -}) diff --git a/shared/src/api/endpoints/__tests__/createTuNewsEndpoint.spec.ts b/shared/src/api/endpoints/__tests__/createTuNewsEndpoint.spec.ts deleted file mode 100644 index 75cbaac4e8..0000000000 --- a/shared/src/api/endpoints/__tests__/createTuNewsEndpoint.spec.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { DateTime } from 'luxon' - -import TuNewsModel from '../../models/TuNewsModel.ts' -import { JsonTuNewsType } from '../../types.ts' -import createTuNewsEndpoint from '../createTuNewsEndpoint.ts' - -describe('tuNews', () => { - const baseUrl = 'https://tuNews.integreat-app.de' - const tuNews = createTuNewsEndpoint(baseUrl) - - const createNewsItem = (id: number, displayDate: string): JsonTuNewsType => ({ - id, - title: 'Tick bite - What to do?', - tags: ['8 Gesundheit'], - display_date: displayDate, - content: - 'In summer there are often ticks in forest and meadows with high grass. These are very small animals. They feed on the blood of people or animals they sting, like mosquitoes. But they stay in the skin longer and can transmit dangerous diseases. If you have been in high grass, you should search your body very thoroughly for ticks. They like to sit in the knees, armpits or in the groin area. If you discover a tick in your skin, you should carefully pull it out with tweezers without crushing it. If the sting inflames, you must see a doctor. tünews INTERNATIONAL', - enewsno: 'tun0000009902', - }) - - const item1 = createNewsItem(1, '2020-01-20T12:04:22+00:00') - const item2 = createNewsItem(2, '2020-01-24T10:05:22+00:00') - const item3 = createNewsItem(3, '2020-01-22T11:06:22+00:00') - - const createNewsItemModel = (id: number, lastUpdate: DateTime): TuNewsModel => - new TuNewsModel({ - id, - title: 'Tick bite - What to do?', - tags: ['8 Gesundheit'], - lastUpdate, - content: - 'In summer there are often ticks in forest and meadows with high grass. These are very small animals. They feed on the blood of people or animals they sting, like mosquitoes. But they stay in the skin longer and can transmit dangerous diseases. If you have been in high grass, you should search your body very thoroughly for ticks. They like to sit in the knees, armpits or in the groin area. If you discover a tick in your skin, you should carefully pull it out with tweezers without crushing it. If the sting inflames, you must see a doctor. tünews INTERNATIONAL', - eNewsNo: 'tun0000009902', - }) - - const itemModel1 = createNewsItemModel(1, DateTime.fromISO('2020-01-20T12:04:22+00:00')) - const itemModel2 = createNewsItemModel(2, DateTime.fromISO('2020-01-24T10:05:22+00:00')) - const itemModel3 = createNewsItemModel(3, DateTime.fromISO('2020-01-22T11:06:22+00:00')) - const params = { - language: 'de', - page: 1, - count: 1, - } - it('should map params to url', () => { - expect(tuNews.mapParamsToUrl(params)).toBe( - `${baseUrl}/v1/news/${params.language}?page=${params.page}&count=${params.count}`, - ) - }) - const json = [item1, item2, item3] - it('should map fetched data to models', () => { - const tuNewsModels = tuNews.mapResponse(json, params) - const newsItemsValues = [itemModel1, itemModel2, itemModel3] - expect(tuNewsModels).toEqual(newsItemsValues) - }) -}) diff --git a/shared/src/api/endpoints/__tests__/createTuNewsLanguageEndpoint.spec.ts b/shared/src/api/endpoints/__tests__/createTuNewsLanguageEndpoint.spec.ts deleted file mode 100644 index 791a4d7746..0000000000 --- a/shared/src/api/endpoints/__tests__/createTuNewsLanguageEndpoint.spec.ts +++ /dev/null @@ -1,32 +0,0 @@ -import LanguageModel from '../../models/LanguageModel.ts' -import createTuNewsLanguagesEndpoint from '../createTuNewsLanguagesEndpoint.ts' - -describe('tuNews language', () => { - const baseUrl = 'https://cms-test.integreat-app.de' - const tuNewsElement = createTuNewsLanguagesEndpoint(baseUrl) - const languagesJson = [ - { - code: 'en', - name: 'English', - }, - { - code: 'de', - name: 'Deutsch', - }, - { - code: 'ar', - name: '\u0627\u0644\u0639\u0631\u0628\u064a\u0629', - }, - ] - it('should map params to url', () => { - expect(tuNewsElement.mapParamsToUrl(undefined)).toBe(`${baseUrl}/v1/news/languages`) - }) - it('should map fetched data to models', () => { - const languageModels = tuNewsElement.mapResponse(languagesJson, undefined) - expect(languageModels).toEqual([ - new LanguageModel('ar', '\u0627\u0644\u0639\u0631\u0628\u064a\u0629'), - new LanguageModel('de', 'Deutsch'), - new LanguageModel('en', 'English'), - ]) - }) -}) diff --git a/shared/src/api/endpoints/createLocalNewsElementEndpoint.ts b/shared/src/api/endpoints/createLocalNewsElementEndpoint.ts deleted file mode 100644 index d8b46e16a1..0000000000 --- a/shared/src/api/endpoints/createLocalNewsElementEndpoint.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { DateTime } from 'luxon' - -import { LOCAL_NEWS_TYPE } from '../../routes/index.ts' -import Endpoint from '../Endpoint.ts' -import EndpointBuilder from '../EndpointBuilder.ts' -import { API_VERSION } from '../constants/index.ts' -import MappingError from '../errors/MappingError.ts' -import NotFoundError from '../errors/NotFoundError.ts' -import { mapNewsAvailableLanguages } from '../mapping/mapAvailableLanguages.ts' -import LocalNewsModel from '../models/LocalNewsModel.ts' -import { JsonLocalNewsType } from '../types.ts' - -export const LOCAL_NEWS_ELEMENT_ENDPOINT_NAME = 'localNewsElement' -type ParamsType = { - region: string - language: string - id: string -} -export default (baseUrl: string): Endpoint => - new EndpointBuilder(LOCAL_NEWS_ELEMENT_ENDPOINT_NAME) - .withParamsToUrlMapper( - (params: ParamsType): string => - `${baseUrl}/api/${API_VERSION}/${params.region}/${params.language}/fcm/?id=${params.id}`, - ) - .withMapper((localNews: JsonLocalNewsType[], params: ParamsType): LocalNewsModel => { - const localNewsModel = localNews[0] - - if (!localNewsModel) { - throw new NotFoundError({ ...params, type: LOCAL_NEWS_TYPE }) - } else if (localNews.length > 1) { - throw new MappingError( - LOCAL_NEWS_ELEMENT_ENDPOINT_NAME, - `Expected count of local news to be one. Received ${localNews.length} instead`, - ) - } - - return new LocalNewsModel({ - id: localNewsModel.id, - timestamp: DateTime.fromISO(localNewsModel.display_date), - title: localNewsModel.title, - content: localNewsModel.message, - availableLanguages: mapNewsAvailableLanguages(localNewsModel.available_languages), - }) - }) - .build() diff --git a/shared/src/api/endpoints/createLocalNewsEndpoint.ts b/shared/src/api/endpoints/createLocalNewsEndpoint.ts deleted file mode 100644 index a5619b0f85..0000000000 --- a/shared/src/api/endpoints/createLocalNewsEndpoint.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { DateTime } from 'luxon' - -import Endpoint from '../Endpoint.ts' -import EndpointBuilder from '../EndpointBuilder.ts' -import { API_VERSION } from '../constants/index.ts' -import { mapNewsAvailableLanguages } from '../mapping/mapAvailableLanguages.ts' -import LocalNewsModel from '../models/LocalNewsModel.ts' -import { JsonLocalNewsType } from '../types.ts' - -export const LOCAL_NEWS_ENDPOINT_NAME = 'localNews' -type ParamsType = { - region: string - language: string -} -export default (baseUrl: string): Endpoint => - new EndpointBuilder(LOCAL_NEWS_ENDPOINT_NAME) - .withParamsToUrlMapper( - (params: ParamsType): string => - `${baseUrl}/api/${API_VERSION}/${params.region}/${params.language}/fcm/?channel=news`, - ) - .withMapper((json: JsonLocalNewsType[]): LocalNewsModel[] => - json.map( - (localNews: JsonLocalNewsType) => - new LocalNewsModel({ - id: localNews.id, - timestamp: DateTime.fromISO(localNews.display_date), - title: localNews.title, - content: localNews.message, - availableLanguages: mapNewsAvailableLanguages(localNews.available_languages), - }), - ), - ) - .build() diff --git a/shared/src/api/endpoints/createNewsElementEndpoint.ts b/shared/src/api/endpoints/createNewsElementEndpoint.ts new file mode 100644 index 0000000000..9733595b0a --- /dev/null +++ b/shared/src/api/endpoints/createNewsElementEndpoint.ts @@ -0,0 +1,25 @@ +import Endpoint from '../Endpoint.ts' +import EndpointBuilder from '../EndpointBuilder.ts' +import { API_VERSION } from '../constants/index.ts' +import mapNewsJson from '../mapping/mapNewsJson.ts' +import NewsModel from '../models/NewsModel.ts' +import { JsonNewsType } from '../types.ts' + +export const NEWS_ELEMENT_ENDPOINT_NAME = 'newsElement' + +type ParamsType = { + region: string + language: string + id: string +} + +const createNewsElementEndpoint = (baseUrl: string): Endpoint => + new EndpointBuilder(NEWS_ELEMENT_ENDPOINT_NAME) + .withParamsToUrlMapper( + ({ region, language, id }: ParamsType): string => + `${baseUrl}/api/${API_VERSION}/${region}/${language}/news/${id}`, + ) + .withMapper((json: JsonNewsType): NewsModel => mapNewsJson(json)) + .build() + +export default createNewsElementEndpoint diff --git a/shared/src/api/endpoints/createNewsEndpoint.ts b/shared/src/api/endpoints/createNewsEndpoint.ts new file mode 100644 index 0000000000..9009d2aace --- /dev/null +++ b/shared/src/api/endpoints/createNewsEndpoint.ts @@ -0,0 +1,23 @@ +import Endpoint from '../Endpoint.ts' +import EndpointBuilder from '../EndpointBuilder.ts' +import { API_VERSION } from '../constants/index.ts' +import mapNewsJson from '../mapping/mapNewsJson.ts' +import NewsModel from '../models/NewsModel.ts' +import { JsonNewsType } from '../types.ts' + +export const NEWS_ENDPOINT_NAME = 'news' + +type ParamsType = { + region: string + language: string +} + +const createNewsEndpoint = (baseUrl: string): Endpoint => + new EndpointBuilder(NEWS_ENDPOINT_NAME) + .withParamsToUrlMapper( + ({ region, language }: ParamsType): string => `${baseUrl}/api/${API_VERSION}/${region}/${language}/news`, + ) + .withMapper((json: JsonNewsType[]): NewsModel[] => json.map(mapNewsJson)) + .build() + +export default createNewsEndpoint diff --git a/shared/src/api/endpoints/createTuNewsElementEndpoint.ts b/shared/src/api/endpoints/createTuNewsElementEndpoint.ts deleted file mode 100644 index 0874441b1e..0000000000 --- a/shared/src/api/endpoints/createTuNewsElementEndpoint.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { DateTime } from 'luxon' - -import { TU_NEWS_TYPE } from '../../routes/index.ts' -import Endpoint from '../Endpoint.ts' -import EndpointBuilder from '../EndpointBuilder.ts' -import NotFoundError from '../errors/NotFoundError.ts' -import TuNewsModel from '../models/TuNewsModel.ts' -import { JsonTuNewsType } from '../types.ts' - -export const TU_NEWS_ELEMENT_ENDPOINT_NAME = 'tuNewsElement' - -type ParamsType = { - id: number -} - -export default (baseUrl: string): Endpoint => - new EndpointBuilder(TU_NEWS_ELEMENT_ENDPOINT_NAME) - .withParamsToUrlMapper((params: ParamsType): string => `${baseUrl}/v1/news/${params.id}`) - .withMapper((json: JsonTuNewsType | void[], params: ParamsType): TuNewsModel => { - // The api is not good and returns an empty array if the tuNews does not exist - if (Array.isArray(json)) { - throw new NotFoundError({ - type: TU_NEWS_TYPE, - id: params.id.toString(), - language: 'unknown', - region: 'unknown', - }) - } - - return new TuNewsModel({ - id: json.id, - title: json.title, - tags: json.tags, - lastUpdate: DateTime.fromISO(json.display_date), - content: json.content, - eNewsNo: json.enewsno, - }) - }) - .build() diff --git a/shared/src/api/endpoints/createTuNewsEndpoint.ts b/shared/src/api/endpoints/createTuNewsEndpoint.ts deleted file mode 100644 index 0b46e580ef..0000000000 --- a/shared/src/api/endpoints/createTuNewsEndpoint.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { DateTime } from 'luxon' - -import parseHTML from '../../utils/parseHTML.ts' -import Endpoint from '../Endpoint.ts' -import EndpointBuilder from '../EndpointBuilder.ts' -import TuNewsModel from '../models/TuNewsModel.ts' -import { JsonTuNewsType } from '../types.ts' - -export const TU_NEWS_ENDPOINT_NAME = 'tuNews' -type ParamsType = { - language: string - page: number - count: number -} -export default (baseUrl: string): Endpoint => - new EndpointBuilder(TU_NEWS_ENDPOINT_NAME) - .withParamsToUrlMapper( - (params: ParamsType): string => `${baseUrl}/v1/news/${params.language}?page=${params.page}&count=${params.count}`, - ) - .withMapper((json: JsonTuNewsType[]): TuNewsModel[] => - json.map( - (tuNews: JsonTuNewsType) => - new TuNewsModel({ - id: tuNews.id, - title: tuNews.title, - tags: tuNews.tags, - lastUpdate: DateTime.fromISO(tuNews.display_date), - content: parseHTML(tuNews.content), - eNewsNo: tuNews.enewsno, - }), - ), - ) - .build() diff --git a/shared/src/api/endpoints/createTuNewsLanguagesEndpoint.ts b/shared/src/api/endpoints/createTuNewsLanguagesEndpoint.ts deleted file mode 100644 index f96e8c0f25..0000000000 --- a/shared/src/api/endpoints/createTuNewsLanguagesEndpoint.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Endpoint from '../Endpoint.ts' -import EndpointBuilder from '../EndpointBuilder.ts' -import LanguageModel from '../models/LanguageModel.ts' -import { JsonTuNewsLanguageType } from '../types.ts' - -export const TU_NEWS_LANGUAGES_ENDPOINT_NAME = 'tuNewsLanguages' - -export default (baseUrl: string): Endpoint => - new EndpointBuilder(TU_NEWS_LANGUAGES_ENDPOINT_NAME) - .withParamsToUrlMapper(() => `${baseUrl}/v1/news/languages`) - .withMapper((json: JsonTuNewsLanguageType[]) => - json - .map((language: JsonTuNewsLanguageType) => new LanguageModel(language.code, language.name)) - .sort((lang1, lang2) => lang1.code.localeCompare(lang2.code)), - ) - .build() diff --git a/shared/src/api/endpoints/hooks/useLoadAsync.ts b/shared/src/api/endpoints/hooks/useLoadAsync.ts index a67b4f243f..ed12adeeca 100644 --- a/shared/src/api/endpoints/hooks/useLoadAsync.ts +++ b/shared/src/api/endpoints/hooks/useLoadAsync.ts @@ -26,6 +26,8 @@ export const loadAsync = async ( } } +type Request = (refresh: boolean) => Promise + export type Return = { data: T | null error: Error | null @@ -34,7 +36,7 @@ export type Return = { setData: Dispatch> } -export const useLoadAsync = (request: (refresh: boolean) => Promise): Return => { +export const useLoadAsync = (request: Request): Return => { const [data, setData] = useState(null) const [error, setError] = useState(null) const [loading, setLoading] = useState(true) diff --git a/shared/src/api/endpoints/testing/NewsModelBuilder.ts b/shared/src/api/endpoints/testing/NewsModelBuilder.ts index 6daa78dde4..19a148e780 100644 --- a/shared/src/api/endpoints/testing/NewsModelBuilder.ts +++ b/shared/src/api/endpoints/testing/NewsModelBuilder.ts @@ -1,8 +1,8 @@ import { DateTime } from 'luxon' -import LocalNewsModel from '../../models/LocalNewsModel.ts' +import NewsModel from '../../models/NewsModel.ts' -class LocalNewsModelBuilder { +class NewsModelBuilder { _newsCount: number _seed: string _region: string @@ -15,7 +15,7 @@ class LocalNewsModelBuilder { this._language = language } - build(): LocalNewsModel[] { + build(): NewsModel[] { return this.buildAll().map(all => all.newsItem) } @@ -24,7 +24,7 @@ class LocalNewsModelBuilder { */ buildAll(): { path: string | null | undefined - newsItem: LocalNewsModel + newsItem: NewsModel }[] { return Array.from( { @@ -32,16 +32,18 @@ class LocalNewsModelBuilder { }, () => ({ path: null, - newsItem: new LocalNewsModel({ + newsItem: new NewsModel({ id: 12, title: 'first news item', - timestamp: DateTime.fromISO('2017-11-18T19:30:00.000Z'), + lastUpdate: DateTime.fromISO('2017-11-18T19:30:00.000Z'), content: 'This is a sample news', availableLanguages: {}, + source: 'local', + externalUrl: 'https://example.com', }), }), ) } } -export default LocalNewsModelBuilder +export default NewsModelBuilder diff --git a/shared/src/api/endpoints/testing/RegionModelBuilder.ts b/shared/src/api/endpoints/testing/RegionModelBuilder.ts index 26dd1e4d7a..356c8ed021 100644 --- a/shared/src/api/endpoints/testing/RegionModelBuilder.ts +++ b/shared/src/api/endpoints/testing/RegionModelBuilder.ts @@ -12,7 +12,7 @@ const regions = [ eventsEnabled: true, placesEnabled: true, localNewsEnabled: true, - tuNewsEnabled: true, + externalNewsEnabled: true, sortingName: 'Augsburg', prefix: 'Stadt', latitude: 48.369696, @@ -35,7 +35,7 @@ const regions = [ eventsEnabled: true, placesEnabled: false, localNewsEnabled: false, - tuNewsEnabled: false, + externalNewsEnabled: false, sortingName: 'Oldtown', prefix: 'GoT', latitude: 49.017834, @@ -58,7 +58,7 @@ const regions = [ eventsEnabled: false, placesEnabled: true, localNewsEnabled: false, - tuNewsEnabled: false, + externalNewsEnabled: false, sortingName: 'Region', latitude: 48.369696, longitude: 10.892578, @@ -76,7 +76,7 @@ const regions = [ eventsEnabled: false, placesEnabled: false, localNewsEnabled: false, - tuNewsEnabled: false, + externalNewsEnabled: false, sortingName: 'OtherRegion', latitude: 48.369696, longitude: 10.892578, @@ -94,7 +94,7 @@ const regions = [ eventsEnabled: false, placesEnabled: true, localNewsEnabled: false, - tuNewsEnabled: false, + externalNewsEnabled: false, sortingName: 'Nonlive', latitude: 0, longitude: 0, @@ -112,7 +112,7 @@ const regions = [ eventsEnabled: false, placesEnabled: false, localNewsEnabled: false, - tuNewsEnabled: false, + externalNewsEnabled: false, sortingName: 'Yetanotherregion', latitude: 48.4, longitude: 10.8, diff --git a/shared/src/api/errors/NotFoundError.ts b/shared/src/api/errors/NotFoundError.ts index 0aff15ec29..9c2684405e 100644 --- a/shared/src/api/errors/NotFoundError.ts +++ b/shared/src/api/errors/NotFoundError.ts @@ -1,6 +1,4 @@ -import { LocalNewsType, TuNewsType } from '../../routes/index.ts' - -type NotFoundType = 'region' | 'category' | 'event' | 'place' | 'imprint' | TuNewsType | LocalNewsType | 'route' +type NotFoundType = 'region' | 'category' | 'event' | 'place' | 'imprint' | 'news' | 'route' const getMessage = (type: NotFoundType, id: string): string => `The ${type} ${id} does not exist here.` diff --git a/shared/src/api/index.ts b/shared/src/api/index.ts index 271fd30bbb..f7b2abc372 100644 --- a/shared/src/api/index.ts +++ b/shared/src/api/index.ts @@ -13,6 +13,7 @@ export { default as Payload } from './Payload.ts' export { default as mapAvailableLanguages } from './mapping/mapAvailableLanguages.ts' export type { ChatMessagesReturn } from './mapping/mapChatMessages.ts' export * from './types.ts' +export * from './constants/index.ts' export * from './endpoints/testing/index.ts' export { type ErrorCode, ErrorCodes, fromError } from './ErrorCodes.ts' export { setUserAgent, request } from './request.ts' @@ -48,16 +49,11 @@ export { default as createRegionsEndpoint, REGIONS_ENDPOINT_NAME } from './endpo export { default as createRegionEndpoint, REGION_ENDPOINT_NAME } from './endpoints/createRegionEndpoint.ts' export { default as createImprintEndpoint, IMPRINT_ENDPOINT_NAME } from './endpoints/createImprintEndpoint.ts' export { default as createEventsEndpoint, EVENTS_ENDPOINT_NAME } from './endpoints/createEventsEndpoint.ts' -export { default as createLocalNewsEndpoint, LOCAL_NEWS_ENDPOINT_NAME } from './endpoints/createLocalNewsEndpoint.ts' +export { default as createNewsEndpoint, NEWS_ENDPOINT_NAME } from './endpoints/createNewsEndpoint.ts' export { - default as createLocalNewsElementEndpoint, - LOCAL_NEWS_ELEMENT_ENDPOINT_NAME, -} from './endpoints/createLocalNewsElementEndpoint.ts' -export { default as createTuNewsEndpoint, TU_NEWS_ENDPOINT_NAME } from './endpoints/createTuNewsEndpoint.ts' -export { - default as createTuNewsElementEndpoint, - TU_NEWS_ELEMENT_ENDPOINT_NAME, -} from './endpoints/createTuNewsElementEndpoint.ts' + default as createNewsElementEndpoint, + NEWS_ELEMENT_ENDPOINT_NAME, +} from './endpoints/createNewsElementEndpoint.ts' export { default as createFeedbackEndpoint, FEEDBACK_ENDPOINT_NAME } from './endpoints/createFeedbackEndpoint.ts' export { default as submitMalteHelpForm, @@ -69,10 +65,6 @@ export { export { POSITIVE_RATING } from './endpoints/createFeedbackEndpoint.ts' export { NEGATIVE_RATING } from './endpoints/createFeedbackEndpoint.ts' export { CONTENT_FEEDBACK_CATEGORY } from './endpoints/createFeedbackEndpoint.ts' -export { - default as createTuNewsLanguagesEndpoint, - TU_NEWS_LANGUAGES_ENDPOINT_NAME, -} from './endpoints/createTuNewsLanguagesEndpoint.ts' export { default as createPlacesEndpoint, PLACES_ENDPOINT_NAME } from './endpoints/createPlacesEndpoint.ts' export { default as createSprungbrettJobsEndpoint, @@ -85,8 +77,7 @@ export { default as CategoryModel } from './models/CategoryModel.ts' export { default as RegionModel } from './models/RegionModel.ts' export { default as DateModel } from './models/DateModel.ts' export { default as EventModel } from './models/EventModel.ts' -export { default as LocalNewsModel } from './models/LocalNewsModel.ts' -export { default as TuNewsModel } from './models/TuNewsModel.ts' +export { default as NewsModel } from './models/NewsModel.ts' export { default as OfferModel } from './models/OfferModel.ts' export { default as LanguageModel } from './models/LanguageModel.ts' export { default as LocationModel } from './models/LocationModel.ts' diff --git a/shared/src/api/mapping/mapAvailableLanguages.ts b/shared/src/api/mapping/mapAvailableLanguages.ts index 978f85d119..4de137a09e 100644 --- a/shared/src/api/mapping/mapAvailableLanguages.ts +++ b/shared/src/api/mapping/mapAvailableLanguages.ts @@ -1,9 +1,6 @@ import normalizePath from '../../utils/normalizePath.ts' import { JsonAvailableLanguagesType } from '../types.ts' -export const mapNewsAvailableLanguages = (json: Record): Record => - Object.entries(json).reduce((availableLanguages, [code, value]) => ({ ...availableLanguages, [code]: value.id }), {}) - const mapAvailableLanguages = (json: JsonAvailableLanguagesType): Record => Object.entries(json).reduce( (availableLanguages, [code, value]) => ({ ...availableLanguages, [code]: normalizePath(value.path) }), diff --git a/shared/src/api/mapping/mapNewsJson.ts b/shared/src/api/mapping/mapNewsJson.ts new file mode 100644 index 0000000000..48b624c8d8 --- /dev/null +++ b/shared/src/api/mapping/mapNewsJson.ts @@ -0,0 +1,26 @@ +import { DateTime } from 'luxon' + +import NewsModel from '../models/NewsModel.ts' +import { JsonNewsType } from '../types.ts' + +const mapNewsAvailableLanguages = (json: Record | null): Record | null => + json + ? Object.entries(json).reduce( + (availableLanguages, [code, value]) => ({ ...availableLanguages, [code]: value.id }), + {}, + ) + : null + +const mapNewsJson = (json: JsonNewsType): NewsModel => + new NewsModel({ + id: json.id, + title: json.title, + // Remove manual heights for images which causes lots of whitespace for images in tü news details on small screens + content: json.content.replace(/\sheight="\d+"/g, ''), + source: json.source, + lastUpdate: DateTime.fromISO(json.display_date), + availableLanguages: mapNewsAvailableLanguages(json.available_languages), + externalUrl: json.externalUrl, + }) + +export default mapNewsJson diff --git a/shared/src/api/mapping/mapRegionJson.ts b/shared/src/api/mapping/mapRegionJson.ts index c1b7337bd5..f0d3e127ad 100644 --- a/shared/src/api/mapping/mapRegionJson.ts +++ b/shared/src/api/mapping/mapRegionJson.ts @@ -28,8 +28,8 @@ const mapRegionJson = (json: JsonRegionType): RegionModel => languages: mapLanguagesJson(json.languages), eventsEnabled: json.events, placesEnabled: json.pois, - tuNewsEnabled: json.tunews, localNewsEnabled: json.push_notifications, + externalNewsEnabled: json.external_news, sortingName: json.name_without_prefix, prefix: json.prefix, longitude: json.longitude, diff --git a/shared/src/api/models/LocalNewsModel.ts b/shared/src/api/models/LocalNewsModel.ts deleted file mode 100644 index 253575d95f..0000000000 --- a/shared/src/api/models/LocalNewsModel.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { decodeHTML } from 'entities' -import { DateTime } from 'luxon' - -class LocalNewsModel { - _id: number - _timestamp: DateTime - _title: string - _content: string - _availableLanguages: Record - - constructor(params: { - id: number - timestamp: DateTime - title: string - content: string - availableLanguages: Record - }) { - const { id, timestamp, title, content, availableLanguages } = params - this._id = id - this._timestamp = timestamp - this._title = decodeHTML(title) - this._content = decodeHTML(content) - this._availableLanguages = availableLanguages - } - - get availableLanguages(): Record { - return this._availableLanguages - } - - get timestamp(): DateTime { - return this._timestamp - } - - get title(): string { - return this._title - } - - get content(): string { - return this._content - } - - get id(): number { - return this._id - } -} - -export default LocalNewsModel diff --git a/shared/src/api/models/NewsModel.ts b/shared/src/api/models/NewsModel.ts new file mode 100644 index 0000000000..f953eb5d24 --- /dev/null +++ b/shared/src/api/models/NewsModel.ts @@ -0,0 +1,62 @@ +import { decodeHTML } from 'entities' +import { DateTime } from 'luxon' + +import { NewsSource } from '../constants/index.ts' + +class NewsModel { + _id: number + _title: string + _content: string + _source: NewsSource + _lastUpdate: DateTime + _availableLanguages: Record | null + constructor(params: { + id: number + title: string + content: string + lastUpdate: DateTime + source: NewsSource + availableLanguages: Record | null + externalUrl: string + }) { + this._id = params.id + this._title = decodeHTML(params.title) + this._content = decodeHTML(params.content) + this._source = params.source + this._lastUpdate = params.lastUpdate + this._availableLanguages = params.availableLanguages + this._externalUrl = params.externalUrl + } + + _externalUrl: string + + get id(): number { + return this._id + } + + get title(): string { + return this._title + } + + get content(): string { + return this._content + } + + get source(): NewsSource { + return this._source + } + + get lastUpdate(): DateTime { + return this._lastUpdate + } + + get availableLanguages(): Record | null { + return this._availableLanguages + } + + get externalUrl(): string { + return this._externalUrl + } +} + +export default NewsModel diff --git a/shared/src/api/models/RegionModel.ts b/shared/src/api/models/RegionModel.ts index 02eda606d2..0c37c0c8bd 100644 --- a/shared/src/api/models/RegionModel.ts +++ b/shared/src/api/models/RegionModel.ts @@ -14,12 +14,6 @@ class RegionModel { _eventsEnabled: boolean _placesEnabled: boolean _localNewsEnabled: boolean - _tuNewsEnabled: boolean - _sortingName: string - _prefix: string | null - _latitude: number - _longitude: number - constructor(params: { name: string code: string @@ -28,8 +22,8 @@ class RegionModel { placesEnabled: boolean eventsEnabled: boolean localNewsEnabled: boolean + externalNewsEnabled: boolean chatEnabled: boolean - tuNewsEnabled: boolean sortingName: string prefix: string | null latitude: number @@ -46,7 +40,7 @@ class RegionModel { this._eventsEnabled = params.eventsEnabled this._placesEnabled = params.placesEnabled this._localNewsEnabled = params.localNewsEnabled - this._tuNewsEnabled = params.tuNewsEnabled + this._externalNewsEnabled = params.externalNewsEnabled this._sortingName = params.sortingName this._prefix = params.prefix this._latitude = params.latitude @@ -55,6 +49,12 @@ class RegionModel { this._boundingBox = params.boundingBox this._chatPrivacyPolicyUrl = params.chatPrivacyPolicyUrl } + _sortingName: string + _prefix: string | null + _latitude: number + _longitude: number + + _externalNewsEnabled: boolean _boundingBox: BBox @@ -98,8 +98,12 @@ class RegionModel { return this._localNewsEnabled } - get tuNewsEnabled(): boolean { - return this._tuNewsEnabled + get externalNewsEnabled(): boolean { + return this._externalNewsEnabled + } + + get newsEnabled(): boolean { + return this._localNewsEnabled || this._externalNewsEnabled } get sortCategory(): string { diff --git a/shared/src/api/models/TuNewsModel.ts b/shared/src/api/models/TuNewsModel.ts deleted file mode 100644 index 9f1cccd540..0000000000 --- a/shared/src/api/models/TuNewsModel.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { decodeHTML } from 'entities' -import { DateTime } from 'luxon' - -class TuNewsModel { - _id: number - _title: string - _tags: string[] - _lastUpdate: DateTime - _content: string - _eNewsNo: string - - constructor(params: { - id: number - title: string - lastUpdate: DateTime - tags: string[] - content: string - eNewsNo: string - }) { - const { id, lastUpdate, title, tags, content, eNewsNo } = params - this._id = id - this._title = decodeHTML(title) - this._tags = tags - this._lastUpdate = lastUpdate - this._content = content - this._eNewsNo = eNewsNo - } - - get id(): number { - return this._id - } - - get title(): string { - return this._title - } - - get lastUpdate(): DateTime { - return this._lastUpdate - } - - get tags(): string[] { - return this._tags - } - - get content(): string { - return this._content - } - - get eNewsNo(): string { - return this._eNewsNo - } -} - -export default TuNewsModel diff --git a/shared/src/api/models/__tests__/RegionModel.spec.ts b/shared/src/api/models/__tests__/RegionModel.spec.ts index 741941491f..be28da9e69 100644 --- a/shared/src/api/models/__tests__/RegionModel.spec.ts +++ b/shared/src/api/models/__tests__/RegionModel.spec.ts @@ -11,7 +11,7 @@ describe('RegionModel', () => { eventsEnabled: false, placesEnabled: false, localNewsEnabled: false, - tuNewsEnabled: false, + externalNewsEnabled: false, sortingName: 'Region', prefix: 'prefix', latitude: 48.369696, diff --git a/shared/src/api/types.ts b/shared/src/api/types.ts index 55a1c1d831..d1861e91dc 100644 --- a/shared/src/api/types.ts +++ b/shared/src/api/types.ts @@ -1,3 +1,5 @@ +import { NewsSource } from './constants/index.ts' + export type JsonAvailableLanguagesType = Record< string, { @@ -147,21 +149,14 @@ export type JsonEventType = { meeting_url: string | null } -export type JsonTuNewsType = { +export type JsonNewsType = { id: number title: string - tags: string[] - display_date: string content: string - enewsno: string -} - -export type JsonLocalNewsType = { - id: number display_date: string - title: string - message: string - available_languages: Record + source: NewsSource + externalUrl: string + available_languages: Record | null } export type JsonOfferPostType = { @@ -191,11 +186,6 @@ export type JsonLanguageType = { native_name: string } -export type JsonTuNewsLanguageType = { - code: string - name: string -} - export type JsonRegionType = { name: string path: string @@ -203,8 +193,8 @@ export type JsonRegionType = { languages: JsonLanguageType[] events: boolean pois: boolean - tunews: boolean push_notifications: boolean + external_news: boolean name_without_prefix: string prefix: string | null latitude: number diff --git a/shared/src/constants/index.ts b/shared/src/constants/index.ts index cabefe9586..36e5e0b24d 100644 --- a/shared/src/constants/index.ts +++ b/shared/src/constants/index.ts @@ -29,3 +29,18 @@ export const CHAT_DEFAULT_POLLING_INTERVAL = 15000 export const CHAT_TYPING_POLLING_INTERVAL = 3000 export const QR_CODE_SIZE = 240 + +export const NEWS_ALL_SOURCES_FILTER = 'all' +export const NEWS_LOCAL_SOURCES_FILTER = 'local' +export const NEWS_NATIONAL_SOURCES_FILTER = 'national' + +export type NewsSourceFilter = + | typeof NEWS_ALL_SOURCES_FILTER + | typeof NEWS_LOCAL_SOURCES_FILTER + | typeof NEWS_NATIONAL_SOURCES_FILTER + +export const NEWS_SOURCE_FILTERS: NewsSourceFilter[] = [ + NEWS_ALL_SOURCES_FILTER, + NEWS_LOCAL_SOURCES_FILTER, + NEWS_NATIONAL_SOURCES_FILTER, +] diff --git a/shared/src/constants/news.ts b/shared/src/constants/news.ts deleted file mode 100644 index 38a13d3f66..0000000000 --- a/shared/src/constants/news.ts +++ /dev/null @@ -1 +0,0 @@ -export const tuNewsLabel = 'tünews INTERNATIONAL' diff --git a/shared/src/index.ts b/shared/src/index.ts index d3beb0ab06..cd340b9b19 100644 --- a/shared/src/index.ts +++ b/shared/src/index.ts @@ -12,6 +12,7 @@ export * from './routes/RouteInformationTypes.ts' export * from './routes/query.ts' export * from './routes/pathname.ts' export * from './utils/events.ts' +export * from './utils/news.ts' export * from './utils/search.ts' export * from './utils/sanitizeContent.ts' export * from './utils/licences.ts' @@ -22,7 +23,6 @@ export { default as findNormalizedMatches, findAllMatches } from './utils/findNo export * from './utils/index.ts' export { getCategoryTiles } from './utils/categories.ts' export * from './constants/map.ts' -export * from './constants/news.ts' export * from './utils/dateFilterUtils.ts' export * from './utils/getExcerpt.ts' export { default as getNearbyRegions } from './utils/getNearbyRegions.ts' diff --git a/shared/src/routes/InternalPathnameParser.ts b/shared/src/routes/InternalPathnameParser.ts index 858c253e28..7776dac757 100644 --- a/shared/src/routes/InternalPathnameParser.ts +++ b/shared/src/routes/InternalPathnameParser.ts @@ -6,12 +6,10 @@ import { IMPRINT_ROUTE, EVENTS_ROUTE, REGIONS_ROUTE, - LOCAL_NEWS_TYPE, NEWS_ROUTE, PLACES_ROUTE, RESERVED_REGION_CONTENT_SLUGS, SEARCH_ROUTE, - TU_NEWS_TYPE, LEGACY_REGIONS_ROUTE, LEGACY_PLACES_ROUTE, } from './index.ts' @@ -150,21 +148,13 @@ class InternalPathnameParser { return null } - // '/augsburg/de/news', '/augsburg/de/news/local', '/augsburg/de/news/tu-news', '/augsburg/de/news/local/id' - const type = this._length > ENTITY_ID_INDEX ? this._parts[ENTITY_ID_INDEX] : undefined - - if (type && type !== LOCAL_NEWS_TYPE && type !== TU_NEWS_TYPE) { - return null - } - - const newsType = type === TU_NEWS_TYPE ? TU_NEWS_TYPE : LOCAL_NEWS_TYPE - const newsId = this._length > ENTITY_ID_INDEX + 1 ? this._parts[ENTITY_ID_INDEX + 1] : undefined + // '/augsburg/de/news' and '/augsburg/de/news/id' + const id = this._length > ENTITY_ID_INDEX ? this._parts[ENTITY_ID_INDEX] : undefined return { route: NEWS_ROUTE, regionCode: this._parts[0]!, languageCode: this._parts[1]!, - newsType, - newsId: newsId ? parseInt(newsId, 10) : undefined, + id: id ? parseInt(id, 10) : undefined, } } diff --git a/shared/src/routes/RouteInformationTypes.ts b/shared/src/routes/RouteInformationTypes.ts index d8ae3c90fc..47a1ea289c 100644 --- a/shared/src/routes/RouteInformationTypes.ts +++ b/shared/src/routes/RouteInformationTypes.ts @@ -7,12 +7,10 @@ import { EventsRouteType, RegionsRouteType, LicensesRouteType, - LocalNewsType, MainImprintRouteType, NewsRouteType, PlacesRouteType, SearchRouteType, - TuNewsType, } from './index.ts' type ParamsType = { @@ -51,10 +49,8 @@ export type CategoriesRouteInformationType = ParamsType & { } export type NewsRouteInformationType = ParamsType & { - // Two levels of ids: news type and news id route: NewsRouteType - newsType: LocalNewsType | TuNewsType - newsId?: number + id?: number } export type SimpleRegionContentFeatureType = ParamsType & { diff --git a/shared/src/routes/__tests__/InternalPathnameParser.spec.ts b/shared/src/routes/__tests__/InternalPathnameParser.spec.ts index f4bef7c632..ff32ba9b55 100644 --- a/shared/src/routes/__tests__/InternalPathnameParser.spec.ts +++ b/shared/src/routes/__tests__/InternalPathnameParser.spec.ts @@ -4,11 +4,9 @@ import { IMPRINT_ROUTE, EVENTS_ROUTE, REGIONS_ROUTE, - LOCAL_NEWS_TYPE, NEWS_ROUTE, PLACES_ROUTE, SEARCH_ROUTE, - TU_NEWS_TYPE, LEGACY_REGIONS_ROUTE, LEGACY_PLACES_ROUTE, } from '../index.ts' @@ -194,67 +192,34 @@ describe('InternalPathnameParser', () => { const parser = new InternalPathnameParser(pathname, languageCode, null) expect(parser.route()).toEqual({ route: NEWS_ROUTE, - newsType: LOCAL_NEWS_TYPE, - newsId: undefined, + id: undefined, languageCode, regionCode, }) }) it('should match local news route', () => { - const pathname = `/${regionCode}/${languageCode}/${NEWS_ROUTE}/${LOCAL_NEWS_TYPE}` - const parser = new InternalPathnameParser(pathname, languageCode, null) - expect(parser.route()).toEqual({ - route: NEWS_ROUTE, - newsType: LOCAL_NEWS_TYPE, - newsId: undefined, - languageCode, - regionCode, - }) - }) - - it('should match single local news route', () => { - const pathname = `/${regionCode}/${languageCode}/${NEWS_ROUTE}/${LOCAL_NEWS_TYPE}/1234` - const parser = new InternalPathnameParser(pathname, languageCode, null) - expect(parser.route()).toEqual({ - route: NEWS_ROUTE, - newsType: LOCAL_NEWS_TYPE, - newsId: 1234, - languageCode, - regionCode, - }) - }) - - it('should match tuNews route', () => { - const pathname = `/${regionCode}/${languageCode}/${NEWS_ROUTE}/${TU_NEWS_TYPE}` + const pathname = `/${regionCode}/${languageCode}/${NEWS_ROUTE}` const parser = new InternalPathnameParser(pathname, languageCode, null) expect(parser.route()).toEqual({ route: NEWS_ROUTE, - newsType: TU_NEWS_TYPE, - newsId: undefined, + id: undefined, languageCode, regionCode, }) }) - it('should match single tuNews route', () => { - const pathname = `/${regionCode}/${languageCode}/${NEWS_ROUTE}/${TU_NEWS_TYPE}/1234` + it('should match local news detail route', () => { + const pathname = `/${regionCode}/${languageCode}/${NEWS_ROUTE}/1234` const parser = new InternalPathnameParser(pathname, languageCode, null) expect(parser.route()).toEqual({ route: NEWS_ROUTE, - newsType: TU_NEWS_TYPE, - newsId: 1234, + id: 1234, languageCode, regionCode, }) }) - it('should not match any other news type', () => { - const pathname = `/${regionCode}/${languageCode}/${NEWS_ROUTE}/random/1234` - const parser = new InternalPathnameParser(pathname, languageCode, null) - expect(parser.route()).toBeNull() - }) - it('should match categories route', () => { const pathname1 = `/${regionCode}/${languageCode}/some-category` const parser1 = new InternalPathnameParser(pathname1, languageCode, null) @@ -423,67 +388,23 @@ describe('InternalPathnameParser', () => { const parser = new InternalPathnameParser(pathname, languageCode, fixedRegion) expect(parser.route()).toEqual({ route: NEWS_ROUTE, - newsType: LOCAL_NEWS_TYPE, - newsId: undefined, + id: undefined, languageCode, regionCode: fixedRegion, }) }) - it('should match local news route', () => { - const pathname = `/${fixedRegion}/${languageCode}/${NEWS_ROUTE}/${LOCAL_NEWS_TYPE}` + it('should match local news detail route', () => { + const pathname = `/${fixedRegion}/${languageCode}/${NEWS_ROUTE}/1234` const parser = new InternalPathnameParser(pathname, languageCode, fixedRegion) expect(parser.route()).toEqual({ route: NEWS_ROUTE, - newsType: LOCAL_NEWS_TYPE, - newsId: undefined, + id: 1234, languageCode, regionCode: fixedRegion, }) }) - it('should match single local news route', () => { - const pathname = `/${fixedRegion}/${languageCode}/${NEWS_ROUTE}/${LOCAL_NEWS_TYPE}/1234` - const parser = new InternalPathnameParser(pathname, languageCode, fixedRegion) - expect(parser.route()).toEqual({ - route: NEWS_ROUTE, - newsType: LOCAL_NEWS_TYPE, - newsId: 1234, - languageCode, - regionCode: fixedRegion, - }) - }) - - it('should match tuNews route', () => { - const pathname = `/${fixedRegion}/${languageCode}/${NEWS_ROUTE}/${TU_NEWS_TYPE}` - const parser = new InternalPathnameParser(pathname, languageCode, fixedRegion) - expect(parser.route()).toEqual({ - route: NEWS_ROUTE, - newsType: TU_NEWS_TYPE, - newsId: undefined, - languageCode, - regionCode: fixedRegion, - }) - }) - - it('should match single tuNews route', () => { - const pathname = `/${fixedRegion}/${languageCode}/${NEWS_ROUTE}/${TU_NEWS_TYPE}/1234` - const parser = new InternalPathnameParser(pathname, languageCode, fixedRegion) - expect(parser.route()).toEqual({ - route: NEWS_ROUTE, - newsType: TU_NEWS_TYPE, - newsId: 1234, - languageCode, - regionCode: fixedRegion, - }) - }) - - it('should not match any other news type', () => { - const pathname = `/${fixedRegion}/${languageCode}/${NEWS_ROUTE}/random/1234` - const parser = new InternalPathnameParser(pathname, languageCode, fixedRegion) - expect(parser.route()).toBeNull() - }) - it('should match categories route', () => { const pathname1 = `/${fixedRegion}/${languageCode}/some-category` const parser1 = new InternalPathnameParser(pathname1, languageCode, fixedRegion) diff --git a/shared/src/routes/__tests__/pathname.spec.ts b/shared/src/routes/__tests__/pathname.spec.ts index 5a3a497cfb..e9618cfbcd 100644 --- a/shared/src/routes/__tests__/pathname.spec.ts +++ b/shared/src/routes/__tests__/pathname.spec.ts @@ -3,11 +3,9 @@ import { IMPRINT_ROUTE, EVENTS_ROUTE, REGIONS_ROUTE, - LOCAL_NEWS_TYPE, NEWS_ROUTE, PLACES_ROUTE, SEARCH_ROUTE, - TU_NEWS_TYPE, } from '../index.ts' import { regionContentPath, pathnameFromRouteInformation } from '../pathname.ts' @@ -139,52 +137,26 @@ describe('pathname', () => { ).toBe(`/${regionCode}/${languageCode}/${SEARCH_ROUTE}`) }) - it('should match local news route', () => { + it('should match news route', () => { expect( pathnameFromRouteInformation({ route: NEWS_ROUTE, - newsType: LOCAL_NEWS_TYPE, - newsId: undefined, + id: undefined, languageCode, regionCode, }), - ).toBe(`/${regionCode}/${languageCode}/${NEWS_ROUTE}/${LOCAL_NEWS_TYPE}`) + ).toBe(`/${regionCode}/${languageCode}/${NEWS_ROUTE}`) }) - it('should match single local news route', () => { + it('should match news detail route', () => { expect( pathnameFromRouteInformation({ route: NEWS_ROUTE, - newsType: LOCAL_NEWS_TYPE, - newsId: 1234, + id: 1234, languageCode, regionCode, }), - ).toBe(`/${regionCode}/${languageCode}/${NEWS_ROUTE}/${LOCAL_NEWS_TYPE}/1234`) - }) - - it('should match tuNews route', () => { - expect( - pathnameFromRouteInformation({ - route: NEWS_ROUTE, - newsType: TU_NEWS_TYPE, - newsId: undefined, - languageCode, - regionCode, - }), - ).toBe(`/${regionCode}/${languageCode}/${NEWS_ROUTE}/${TU_NEWS_TYPE}`) - }) - - it('should match single tuNews route', () => { - expect( - pathnameFromRouteInformation({ - route: NEWS_ROUTE, - newsType: TU_NEWS_TYPE, - newsId: 1234, - languageCode, - regionCode, - }), - ).toBe(`/${regionCode}/${languageCode}/${NEWS_ROUTE}/${TU_NEWS_TYPE}/1234`) + ).toBe(`/${regionCode}/${languageCode}/${NEWS_ROUTE}/1234`) }) it('should match categories route', () => { diff --git a/shared/src/routes/index.ts b/shared/src/routes/index.ts index b95f2a0b87..19f8009568 100644 --- a/shared/src/routes/index.ts +++ b/shared/src/routes/index.ts @@ -30,15 +30,6 @@ export const LICENSES_ROUTE: LicensesRouteType = 'licenses' export const LEGACY_REGIONS_ROUTE = 'landing' export const LEGACY_PLACES_ROUTE = 'locations' -// News types -export type LocalNewsType = 'local' -export const LOCAL_NEWS_TYPE: LocalNewsType = 'local' - -export type TuNewsType = 'tu-news' -export const TU_NEWS_TYPE: TuNewsType = 'tu-news' - -export type NewsType = LocalNewsType | TuNewsType - // Internal native routes export type CategoriesTabRouteType = 'categoriesTab' export const CATEGORIES_TAB_ROUTE: CategoriesTabRouteType = 'categoriesTab' diff --git a/shared/src/routes/pathname.ts b/shared/src/routes/pathname.ts index 55a323e23c..b1bd005a3f 100644 --- a/shared/src/routes/pathname.ts +++ b/shared/src/routes/pathname.ts @@ -60,9 +60,8 @@ export const pathnameFromRouteInformation = (routeInformation: NonNullableRouteI ) { // https://integreat.app/augsburg/de/offers, https://integreat.app/augsburg/de/search, ... const { regionCode, languageCode } = routeInformation - const newsType = routeInformation.route === NEWS_ROUTE ? routeInformation.newsType : null - const newsId = routeInformation.route === NEWS_ROUTE ? routeInformation.newsId : null - return constructPathname([regionCode, languageCode, routeInformation.route, newsType, newsId?.toString()]) + const newsId = routeInformation.route === NEWS_ROUTE ? routeInformation.id : null + return constructPathname([regionCode, languageCode, routeInformation.route, newsId?.toString()]) } // https://integreat.app/regions/de return constructPathname([REGIONS_ROUTE, routeInformation.languageCode]) diff --git a/shared/src/utils/__tests__/getNearbyRegions.spec.ts b/shared/src/utils/__tests__/getNearbyRegions.spec.ts index 8d5a0327f8..fd548788db 100644 --- a/shared/src/utils/__tests__/getNearbyRegions.spec.ts +++ b/shared/src/utils/__tests__/getNearbyRegions.spec.ts @@ -16,7 +16,7 @@ describe('getNearbyRegions', () => { eventsEnabled: true, placesEnabled: true, localNewsEnabled: false, - tuNewsEnabled: false, + externalNewsEnabled: false, sortingName: 'Augsburg', prefix: 'Stadt', longitude, diff --git a/shared/src/utils/__tests__/search.spec.ts b/shared/src/utils/__tests__/search.spec.ts index 1620302618..88990c3f1e 100644 --- a/shared/src/utils/__tests__/search.spec.ts +++ b/shared/src/utils/__tests__/search.spec.ts @@ -19,7 +19,7 @@ describe('search', () => { eventsEnabled: true, placesEnabled: true, localNewsEnabled: false, - tuNewsEnabled: false, + externalNewsEnabled: false, sortingName, prefix, aliases, diff --git a/shared/src/utils/news.ts b/shared/src/utils/news.ts new file mode 100644 index 0000000000..b9feb56cb8 --- /dev/null +++ b/shared/src/utils/news.ts @@ -0,0 +1,12 @@ +import { AMAL_NEWS_SOURCE, LOCAL_NEWS_SOURCE, NewsSource, TU_NEWS_SOURCE } from '../api/constants/index.ts' +import { NEWS_LOCAL_SOURCES_FILTER, NEWS_NATIONAL_SOURCES_FILTER, NewsSourceFilter } from '../constants/index.ts' + +export const newsFilterToSources = (newsSourcesFilter?: NewsSourceFilter): NewsSource[] | null => { + switch (newsSourcesFilter) { + case NEWS_LOCAL_SOURCES_FILTER: + return [LOCAL_NEWS_SOURCE] + case NEWS_NATIONAL_SOURCES_FILTER: + return [TU_NEWS_SOURCE, AMAL_NEWS_SOURCE] + } + return null +} diff --git a/translations/src/__mocks__/loadTranslations.ts b/translations/src/__mocks__/loadTranslations.ts index 6b60a2ee3e..42481c0e83 100644 --- a/translations/src/__mocks__/loadTranslations.ts +++ b/translations/src/__mocks__/loadTranslations.ts @@ -7,7 +7,7 @@ export const testTranslations: TranslationsType = { pageTitles: { notFound: 'Seite nicht gefunden', tuNews: 'tünews', - localNews: 'Lokale Nachrichten', + news: 'Nachrichten', }, metaDescription: 'Integreat ist Ihr digitaler Guide für Deutschland. Finden Sie lokale Informationen, Veranstaltungen und Beratung. Immer aktuell und in Ihrer Sprache.', diff --git a/translations/src/__tests__/__snapshots__/loadTranslations.spec.ts.snap b/translations/src/__tests__/__snapshots__/loadTranslations.spec.ts.snap index 1f6c7a32a6..f5bead517e 100644 --- a/translations/src/__tests__/__snapshots__/loadTranslations.spec.ts.snap +++ b/translations/src/__tests__/__snapshots__/loadTranslations.spec.ts.snap @@ -12,7 +12,7 @@ exports[`loadTranslations should correctly merge and transform translations 1`] "app": { "metaDescription": "Integreat ist Ihr digitaler Guide für Deutschland. Finden Sie lokale Informationen, Veranstaltungen und Beratung. Immer aktuell und in Ihrer Sprache.", "pageTitles": { - "localNews": "Lokale Nachrichten", + "news": "Nachrichten", "notFound": "Seite nicht gefunden", "tuNews": "tünews", }, @@ -59,7 +59,7 @@ exports[`loadTranslations should correctly transform translations 1`] = ` "app": { "metaDescription": "Integreat ist Ihr digitaler Guide für Deutschland. Finden Sie lokale Informationen, Veranstaltungen und Beratung. Immer aktuell und in Ihrer Sprache.", "pageTitles": { - "localNews": "Lokale Nachrichten", + "news": "Nachrichten", "notFound": "Seite nicht gefunden", "tuNews": "tünews", }, diff --git a/translations/src/manage.ts b/translations/src/manage.ts index e097cd7c7f..cb9fe305e5 100644 --- a/translations/src/manage.ts +++ b/translations/src/manage.ts @@ -7,8 +7,8 @@ import { fromPairs, isEmpty, isEqual, isString, mapValues, merge, sortBy, toPair import path from 'path' import config from '../src/config.js' -import { TranslationsType } from '../src/index.js' -import { KeyValueType } from '../src/types.js' +import { TranslationsType } from './index.ts' +import { KeyValueType } from './types.ts' const { unflatten } = flat diff --git a/translations/src/translations.json b/translations/src/translations.json index 45b293a486..3881eb35ac 100644 --- a/translations/src/translations.json +++ b/translations/src/translations.json @@ -10895,388 +10895,267 @@ "news": { "de": { "news": "Nachrichten", - "localNews": { - "localNews": "Lokale Nachrichten", - "pageTitle": "Lokale Nachrichten" - }, + "allNews": "Alle Nachrichten", + "localNews": "Lokale Nachrichten", + "nationalNews": "Nationale Nachrichten", + "all": "Alle", "local": "Lokal", + "national": "National", "currentlyNoNews": "Aktuell gibt es keine Nachrichten." }, "am": { "news": "ዜናዎች", - "localNews": { - "localNews": "አካባቢያዊ ዜናዎች", - "pageTitle": "አካባቢያዊ ዜናዎች" - }, + "localNews": "አካባቢያዊ ዜናዎች", "local": "አካባቢያዊ", "currentlyNoNews": "በአሁኑ ሰዐት ምንም ዜናዎች የሉም።" }, "ar": { "news": "الأخبار", - "localNews": { - "localNews": "اخبار محلية", - "pageTitle": "الأخبار المحلية" - }, + "localNews": "اخبار محلية", "local": "محلي", "currentlyNoNews": "لا تُوجد أخبار حاليًا." }, "bg": { "news": "Новини", - "localNews": { - "localNews": "Местни новини", - "pageTitle": "Местни новини" - }, + "localNews": "Местни новини", "local": "Локално", "currentlyNoNews": "В момента няма новини." }, "ckb": { "news": "هەواڵ", - "localNews": { - "localNews": "هەواڵی خۆجێی", - "pageTitle": "هەواڵی خۆجێی" - }, + "localNews": "هەواڵی خۆجێی", "local": "خۆجێی", "currentlyNoNews": "لەئێستادا هیچ هەواڵێکی تێدا نییە." }, "cs": { "news": "Zprávy", - "localNews": { - "localNews": "Lokální zprávy", - "pageTitle": "Lokální zprávy" - }, + "localNews": "Lokální zprávy", "local": "Lokální", "currentlyNoNews": "V tuto chvíli nejsou žádné novinky." }, "da": { "news": "Nyheder", - "localNews": { - "localNews": "Lokale nyheder", - "pageTitle": "Lokale nyheder" - }, + "localNews": "Lokale nyheder", "local": "Lokal", "currentlyNoNews": "Der er i øjeblikket ingen nyheder." }, "el": { "news": "Ειδήσεις", - "localNews": { - "localNews": "Τοπικές ειδήσεις", - "pageTitle": "Τοπικά νέα" - }, + "localNews": "Τοπικές ειδήσεις", "local": "Τοπικά", "currentlyNoNews": "Προς το παρόν δεν υπάρχουν ειδήσεις." }, "en": { "news": "News", - "localNews": { - "localNews": "Local news", - "pageTitle": "Local news" - }, + "allNews": "All news", + "localNews": "Local news", + "nationalNews": "National news", + "all": "All", "local": "Local", + "national": "National", "currentlyNoNews": "There is no news at the moment." }, "es": { "news": "Noticias", - "localNews": { - "localNews": "Noticias locales", - "pageTitle": "Noticias locales" - }, + "localNews": "Noticias locales", "local": "Local", "currentlyNoNews": "En la actualidad no hay noticias" }, "fi": { "news": "Viestit", - "localNews": { - "localNews": "Paikalliset uutiset", - "pageTitle": "Paikalliset uutiset" - }, + "localNews": "Paikalliset uutiset", "local": "Paikallinen", "currentlyNoNews": "Tällä hetkellä ei ole uutisia." }, "fr": { "news": "Nouvelles", - "localNews": { - "localNews": "Nouvelles locales", - "pageTitle": "Nouvelles locales" - }, + "localNews": "Nouvelles locales", "local": "Localement", "currentlyNoNews": "Aucune nouvelle pour l'instant" }, "hi": { "news": "समाचार", - "localNews": { - "localNews": "स्थानीय समाचार", - "pageTitle": "स्थानीय समाचार" - }, + "localNews": "स्थानीय समाचार", "local": "स्थानीय", "currentlyNoNews": "फ़िलहाल कोई समाचार नहीं है." }, "hr": { "news": "Vijesti", - "localNews": { - "localNews": "Lokalne vijesti", - "pageTitle": "Lokalne vijesti" - }, + "localNews": "Lokalne vijesti", "local": "Lokalno", "currentlyNoNews": "Trenutačno nema nikakvih vijesti." }, "hu": { "news": "Hírek", - "localNews": { - "localNews": "Helyi hírek", - "pageTitle": "Helyi hírek" - }, + "localNews": "Helyi hírek", "local": "Helyi", "currentlyNoNews": "Jelenleg nincsenek hírek." }, "id": { "news": "Berita", - "localNews": { - "localNews": "Berita lokal", - "pageTitle": "Berita lokal" - }, + "localNews": "Berita lokal", "local": "Lokal", "currentlyNoNews": "Tidak ada berita saat ini." }, "it": { "news": "Messaggi", - "localNews": { - "localNews": "Notizie locali", - "pageTitle": "Notizie locali" - }, + "localNews": "Notizie locali", "local": "Locale", "currentlyNoNews": "Attualmente non ci sono notizie." }, "ka": { "news": "ინფორმაციები", - "localNews": { - "localNews": "ადგილობრივი ინფორმაციები", - "pageTitle": "ადგილობრივი ამბები" - }, + "localNews": "ადგილობრივი ინფორმაციები", "local": "ადგილობრივი", "currentlyNoNews": "ამჟამად არ არსებობს არანაირი ინფორმაციები." }, "kmr": { "news": "Nûçe", - "localNews": { - "localNews": "Nûçeyên herêmî", - "pageTitle": "Nûçeyên herêmî" - }, + "localNews": "Nûçeyên herêmî", "local": "Herêmî", "currentlyNoNews": "Niha nûçe tune." }, "mk": { "news": "Вести", - "localNews": { - "localNews": "Локални вести", - "pageTitle": "Локални вести" - }, + "localNews": "Локални вести", "local": "Локално", "currentlyNoNews": "Моментално нема вести." }, "nl": { "news": "Nieuws", - "localNews": { - "localNews": "Lokaal nieuws", - "pageTitle": "Lokaal nieuws" - }, + "localNews": "Lokaal nieuws", "local": "Lokaal", "currentlyNoNews": "Op dit moment is er geen nieuws." }, "orm": { "news": "Oduu", - "localNews": { - "localNews": "Oduu naannoo", - "pageTitle": "Oduu naannoo" - }, + "localNews": "Oduu naannoo", "local": "Naannoo", "currentlyNoNews": "Yeroo ammaa oduun hin jiru" }, "pes": { "news": "اخبار", - "localNews": { - "localNews": "اخبار محلی", - "pageTitle": "اخبار محلی" - }, + "localNews": "اخبار محلی", "local": "محل", "currentlyNoNews": "در حال حاضر هیچ اخباری وجود ندارد." }, "pl": { "news": "Wiadomości", - "localNews": { - "localNews": "Wiadomości lokalne", - "pageTitle": "Wiadomości lokalne" - }, + "localNews": "Wiadomości lokalne", "local": "Lokalnie", "currentlyNoNews": "Obecnie nie ma żadnej wiadomości." }, "prs": { "news": "اخبار", - "localNews": { - "localNews": "اخبار محلی", - "pageTitle": "اخبار محلی" - }, + "localNews": "اخبار محلی", "local": "محلی", "currentlyNoNews": "در حال حاضر هیچ خبری وجود ندارد‎." }, "ps": { "news": "خبرونه", - "localNews": { - "localNews": "سیمه ییز خبرونه", - "pageTitle": "سیمه ییز خبرونه" - }, + "localNews": "سیمه ییز خبرونه", "local": "سیمه ییز", "currentlyNoNews": "دا مهال کوم خبرونه نشته." }, "pt": { "news": "Notícias", - "localNews": { - "localNews": "Notícias locais", - "pageTitle": "Notícias locais" - }, + "localNews": "Notícias locais", "local": "A nível local", "currentlyNoNews": "Não existe atualmente nenhuma notícia." }, "ro": { "news": "Știri", - "localNews": { - "localNews": "Știri locale", - "pageTitle": "Știri locale" - }, + "localNews": "Știri locale", "local": "Local", "currentlyNoNews": "În prezent nu există nicio știre." }, "rom": { "news": "Haberia", - "localNews": { - "localNews": "Lokalnikane haberia", - "pageTitle": "Lokalnikane haberia" - }, + "localNews": "Lokalnikane haberia", "local": "Lokalnikano", "currentlyNoNews": "Nane haberia ko akava vakti." }, "ru": { "news": "Новости", - "localNews": { - "localNews": "Местные новости", - "pageTitle": "Местные новости" - }, + "localNews": "Местные новости", "local": "Местная информация", "currentlyNoNews": "В настоящий момент новости отсутствуют." }, "sk": { "news": "Správy", - "localNews": { - "localNews": "Lokálne správy", - "pageTitle": "Lokálne správy" - }, + "localNews": "Lokálne správy", "local": "Lokálne", "currentlyNoNews": "Aktuálne nie sú k dispozícii žiadne správy." }, "so": { "news": "War", - "localNews": { - "localNews": "Wararka deegaanka", - "pageTitle": "Wararka deegaanka" - }, + "localNews": "Wararka deegaanka", "local": "Deegaanka", "currentlyNoNews": "Hada wax war ah ma jiraan." }, "sq": { "news": "Lajme", - "localNews": { - "localNews": "Lajme lokale", - "pageTitle": "Lajme lokale" - }, + "localNews": "Lajme lokale", "local": "Vendas", "currentlyNoNews": "Aktualisht nuk ka asnjë lajm." }, "sr-Cyrl": { "news": "Вести", - "localNews": { - "localNews": "Локалне вести", - "pageTitle": "Локалне вести" - }, + "localNews": "Локалне вести", "local": "Локално", "currentlyNoNews": "Тренутно нема вести." }, "sr-Latn": { "news": "Vesti", - "localNews": { - "localNews": "Lokalne vesti", - "pageTitle": "Lokalne vesti" - }, + "localNews": "Lokalne vesti", "local": "Lokalno", "currentlyNoNews": "Trenutno nema vesti." }, "sw": { "news": "Habari", - "localNews": { - "localNews": "Habari za nchini", - "pageTitle": "Habari za nchini" - }, + "localNews": "Habari za nchini", "local": "Nchini", "currentlyNoNews": "Hakuna habari kwa sasa." }, "th": { "news": "ข่าว", - "localNews": { - "localNews": "ข่าวท้องถิ่น", - "pageTitle": "ข่าวท้องถิ่น" - }, + "localNews": "ข่าวท้องถิ่น", "local": "ท้องถิ่น", "currentlyNoNews": "ยังไม่มีข่าวสารใดๆในขณะนี้" }, "ti": { "news": "ዜና", - "localNews": { - "localNews": "ዜና ውሽጢ ዓዲ", - "pageTitle": "ዜና ውሽጢ ዓዲ" - }, + "localNews": "ዜና ውሽጢ ዓዲ", "local": "ውሽጢ ዓዲ", "currentlyNoNews": "ኣብዚ እዋን’ዚ ዜና የለን።" }, "tr": { "news": "Bildirimler", - "localNews": { - "localNews": "yerel bildirimler", - "pageTitle": "yerel bildirimler" - }, + "localNews": "yerel bildirimler", "local": "Yerel", "currentlyNoNews": "Şu an bildirim bulunmamaktadır." }, "uk": { "news": "Новини", - "localNews": { - "localNews": "Місцеві новини", - "pageTitle": "Місцеві новини" - }, + "localNews": "Місцеві новини", "local": "Місцевий", "currentlyNoNews": "Наразі новин немає." }, "ur": { "news": "خبریں", - "localNews": { - "localNews": "مقامی خبریں", - "pageTitle": "مقامی خبریں" - }, + "localNews": "مقامی خبریں", "local": "مقامی", "currentlyNoNews": "فی الحال کوئی خبریں نہیں ہیں۔" }, "vi": { "news": "Tin tức", - "localNews": { - "localNews": "Tin tức về địa phương", - "pageTitle": "Tin tức về địa phương" - }, + "localNews": "Tin tức về địa phương", "local": "Địa phương", "currentlyNoNews": "Hiện không có tin tức nào." }, "zh-CN": { "news": "新闻", - "localNews": { - "localNews": "本地新闻", - "pageTitle": "本地新闻" - }, + "localNews": "本地新闻", "local": "本地", "currentlyNoNews": "目前没有新闻。" } diff --git a/web/src/RegionContentNavigator.tsx b/web/src/RegionContentNavigator.tsx index 7852cf338c..0bee31522e 100644 --- a/web/src/RegionContentNavigator.tsx +++ b/web/src/RegionContentNavigator.tsx @@ -1,5 +1,5 @@ import React, { FunctionComponent, ReactElement, Suspense } from 'react' -import { Navigate, Route, Routes, useLocation, useParams, useSearchParams } from 'react-router' +import { Route, Routes, useLocation, useParams, useSearchParams } from 'react-router' import { CATEGORIES_ROUTE, @@ -24,21 +24,13 @@ import LoadingSpinner from './components/LoadingSpinner' import RegionContentLayout from './components/RegionContentLayout' import { cmsApiBaseUrl } from './constants/urls' import useQueryFromEndpoint from './hooks/useQueryFromEndpoint' -import { - LOCAL_NEWS_ROUTE, - RoutePatterns, - RouteType, - TU_NEWS_DETAIL_ROUTE, - TU_NEWS_ROUTE, - RegionRouteProps, -} from './routes' +import { RoutePatterns, RouteType, RegionRouteProps, NEWS_DETAIL_ROUTE } from './routes' import lazyWithRetry from './utils/retryImport' -const TuNewsDetailPage = lazyWithRetry(() => import('./routes/TuNewsDetailPage')) -const TuNewsPage = lazyWithRetry(() => import('./routes/TuNewsPage')) const EventsPage = lazyWithRetry(() => import('./routes/EventsPage')) const CategoriesPage = lazyWithRetry(() => import('./routes/CategoriesPage')) -const LocalNewsPage = lazyWithRetry(() => import('./routes/LocalNewsPage')) +const NewsPage = lazyWithRetry(() => import('./routes/NewsPage')) +const NewsDetailPage = lazyWithRetry(() => import('./routes/NewsDetailPage')) const PlacesPage = lazyWithRetry(() => import('./routes/PlacesPage')) const SearchPage = lazyWithRetry(() => import('./routes/SearchPage')) const ImprintPage = lazyWithRetry(() => import('./routes/ImprintPage')) @@ -113,10 +105,9 @@ const RegionContentNavigator = ({ languageCode }: RegionContentNavigatorProps): } // If the region is not available yet, nothing is rendered in the routes. Therefore, we can render the route until we know whether the feature is enabled. - const eventsEnabled = !region || region.eventsEnabled - const localNewsEnabled = !region || region.localNewsEnabled - const tuNewsEnabled = !region || region.tuNewsEnabled const placesEnabled = !region || region.placesEnabled + const newsEnabled = !region || region.newsEnabled + const eventsEnabled = !region || region.eventsEnabled const render = ( route: S, @@ -155,19 +146,9 @@ const RegionContentNavigator = ({ languageCode }: RegionContentNavigatorProps): {render(IMPRINT_ROUTE, ImprintPage)} {render(CATEGORIES_ROUTE, CategoriesPage)} {eventsEnabled && render(EVENTS_ROUTE, EventsPage, ':eventId')} - {placesEnabled && render(PLACES_ROUTE, PlacesPage, ':slug')} - {localNewsEnabled && render(LOCAL_NEWS_ROUTE, LocalNewsPage, ':newsId')} - - {tuNewsEnabled && render(TU_NEWS_ROUTE, TuNewsPage)} - {tuNewsEnabled && render(TU_NEWS_DETAIL_ROUTE, TuNewsDetailPage)} - - {(localNewsEnabled || tuNewsEnabled) && ( - } - /> - )} + {newsEnabled && render(NEWS_ROUTE, NewsPage)} + {newsEnabled && render(NEWS_DETAIL_ROUTE, NewsDetailPage)} ) } diff --git a/web/src/__tests__/RegionContentNavigator.spec.tsx b/web/src/__tests__/RegionContentNavigator.spec.tsx index aba499cc0e..100db6c12f 100644 --- a/web/src/__tests__/RegionContentNavigator.spec.tsx +++ b/web/src/__tests__/RegionContentNavigator.spec.tsx @@ -63,15 +63,4 @@ describe('RegionContentNavigator', () => { const { queryByText } = renderRegionContentNavigator(PLACES_ROUTE) expect(queryByText(PLACES_ROUTE)).not.toBeTruthy() }) - - describe('redirects', () => { - it.each` - from | to - ${NEWS_ROUTE} | ${'/augsburg/de/news/local'} - `('should redirect from $from to $to', ({ from, to }) => { - mockUseQueryFromEndpointWithData(region) - const { getByText } = renderRegionContentNavigator(from) - expect(getByText(to)).toBeTruthy() - }) - }) }) diff --git a/web/src/assets/index.ts b/web/src/assets/index.ts index be94b541e6..0b8339c064 100644 --- a/web/src/assets/index.ts +++ b/web/src/assets/index.ts @@ -1,16 +1,17 @@ import EventThumbnailPlaceholder1 from '../../../assets/event-thumbnail-placeholder-1.jpg' import EventThumbnailPlaceholder2 from '../../../assets/event-thumbnail-placeholder-2.jpg' import EventThumbnailPlaceholder3 from '../../../assets/event-thumbnail-placeholder-3.jpg' +import AmalNewsLogo from '../../../assets/icons/amal-news.svg' import BookIcon from '../../../assets/icons/book.svg' import ExternalLinkIcon from '../../../assets/icons/external-link.svg' import PersonLightIcon from '../../../assets/icons/person-light.svg' import PersonIcon from '../../../assets/icons/person.svg' import ReadAloudIcon from '../../../assets/icons/readAloud.svg' import SprungbrettIcon from '../../../assets/icons/sprungbrett.svg' -import TuNewsActiveIcon from '../../../assets/icons/tu-news-active.svg' -import TuNewsInactiveIcon from '../../../assets/icons/tu-news-inactive.svg' +import TuNewsIcon from '../../../assets/icons/tu-news.svg' export { + AmalNewsLogo, BookIcon, EventThumbnailPlaceholder1, EventThumbnailPlaceholder2, @@ -20,6 +21,5 @@ export { PersonLightIcon, ReadAloudIcon, SprungbrettIcon, - TuNewsActiveIcon, - TuNewsInactiveIcon, + TuNewsIcon, } diff --git a/web/src/components/FailureSwitcher.tsx b/web/src/components/FailureSwitcher.tsx index 2134f0af19..e28e747269 100644 --- a/web/src/components/FailureSwitcher.tsx +++ b/web/src/components/FailureSwitcher.tsx @@ -7,8 +7,6 @@ import { pathnameFromRouteInformation, PLACES_ROUTE, REGIONS_ROUTE, - LOCAL_NEWS_TYPE, - TU_NEWS_TYPE, } from 'shared' import { fromError, NotFoundError } from 'shared/api' @@ -44,10 +42,9 @@ const FailureSwitcher = ({ error }: FailureSwitcherProps): ReactElement => { goToMessage: 'goTo.events', errorMessage: 'notFound.event', } - case LOCAL_NEWS_TYPE: - case TU_NEWS_TYPE: + case 'news': return { - goToPath: pathnameFromRouteInformation({ route: NEWS_ROUTE, newsType: error.type, ...params }), + goToPath: pathnameFromRouteInformation({ route: NEWS_ROUTE, ...params }), goToMessage: 'goTo.news', errorMessage: 'notFound.news', } diff --git a/web/src/components/InfiniteScrollList.tsx b/web/src/components/InfiniteScrollList.tsx deleted file mode 100644 index 98a43224e1..0000000000 --- a/web/src/components/InfiniteScrollList.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import React, { ReactElement, useCallback, useEffect, useState } from 'react' -import InfiniteScroll from 'react-infinite-scroller' - -import { loadAsync } from 'shared/api' - -import Failure from './Failure' -import FailureSwitcher from './FailureSwitcher' -import List from './base/List' - -type InfiniteScrollListProps = { - loadPage: (page: number) => Promise - noItemsMessage: string - renderItem: (item: T) => ReactElement - defaultPage: number - itemsPerPage: number -} - -const InfiniteScrollList = ({ - loadPage, - noItemsMessage, - renderItem, - defaultPage, - itemsPerPage, -}: InfiniteScrollListProps): ReactElement => { - const [data, setData] = useState([]) - const [error, setError] = useState(null) - const [loading, setLoading] = useState(false) - const [page, setPage] = useState(defaultPage) - const [hasMore, setHasMore] = useState(true) - - const load = useCallback(async () => { - if (hasMore) { - setLoading(true) - setPage(page + 1) - const request = () => loadPage(page) - const addData = (data: T[] | null) => { - if (data !== null) { - setData(oldData => (page === defaultPage ? data : oldData.concat(data))) - if (data.length !== itemsPerPage) { - setHasMore(false) - } - } - } - await loadAsync(request, { setData: addData, setError, setLoading }) - } - }, [defaultPage, page, hasMore, itemsPerPage, loadPage]) - - useEffect( - () => () => { - setData([]) - setError(null) - setLoading(false) - setHasMore(true) - setPage(defaultPage) - }, - [loadPage, defaultPage], - ) - - if (error) { - return - } - - if (data.length === 0 && !hasMore) { - return - } - - return ( - - } /> - - ) -} - -export default InfiniteScrollList diff --git a/web/src/components/NewsListItem.tsx b/web/src/components/NewsListItem.tsx index 9256fdfa8a..ec96d062d2 100644 --- a/web/src/components/NewsListItem.tsx +++ b/web/src/components/NewsListItem.tsx @@ -4,11 +4,11 @@ import ListItemText from '@mui/material/ListItemText' import Stack from '@mui/material/Stack' import Typography from '@mui/material/Typography' import { styled } from '@mui/material/styles' -import { DateTime } from 'luxon' import React, { ReactElement } from 'react' import { useTranslation } from 'react-i18next' -import { getExcerpt } from 'shared' +import { getExcerpt, NEWS_ROUTE, parseHTML, pathnameFromRouteInformation } from 'shared' +import { NewsModel } from 'shared/api' import { EXCERPT_MAX_CHARS } from '../constants' import LastUpdateInfo from './LastUpdateInfo' @@ -38,22 +38,23 @@ const StyledStack = styled(Stack)(({ theme }) => ({ })) type NewsListItemProps = { - title: string - content: string - timestamp: DateTime - to: string + news: NewsModel + regionCode: string + languageCode: string } -const NewsListItem = ({ title, content, timestamp, to }: NewsListItemProps): ReactElement => { +const NewsListItem = ({ news, regionCode, languageCode }: NewsListItemProps): ReactElement => { const { t } = useTranslation('news') - const excerpt = getExcerpt(content, { maxChars: EXCERPT_MAX_CHARS, replaceLineBreaks: false }) + const excerpt = getExcerpt(parseHTML(news.content), { maxChars: EXCERPT_MAX_CHARS, replaceLineBreaks: false }) return ( - + - - + + {t('common:more')} diff --git a/web/src/components/NewsTab.tsx b/web/src/components/NewsTab.tsx deleted file mode 100644 index 716bc65980..0000000000 --- a/web/src/components/NewsTab.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import shouldForwardProp from '@emotion/is-prop-valid' -import Typography from '@mui/material/Typography' -import { styled } from '@mui/material/styles' -import React, { ReactElement } from 'react' -import { useTranslation } from 'react-i18next' - -import { NewsType, TU_NEWS_TYPE, tuNewsLabel } from 'shared' - -import { TuNewsActiveIcon, TuNewsInactiveIcon } from '../assets' -import Link from './base/Link' - -const StyledTab = styled(Link, { shouldForwardProp })<{ tabSelected: boolean }>` - display: flex; - width: 160px; - height: 48px; - box-sizing: border-box; - flex-shrink: 0; - align-items: center; - justify-content: center; - cursor: pointer; - padding: 8px; - color: ${props => props.theme.palette.secondary.contrastText}; - object-fit: contain; - background-color: ${props => - props.tabSelected ? props.theme.palette.secondary.main : props.theme.palette.text.disabled}; - border-radius: 12px; - text-decoration: none; -` - -const TuStyledTab = styled(StyledTab)` - background-image: ${props => (props.tabSelected ? `url(${TuNewsActiveIcon})` : `url(${TuNewsInactiveIcon})`)}; - background-size: cover; - background-position: center center; -` - -type NewsTabProps = { - type: NewsType - active: boolean - destination: string -} - -const NewsTab = ({ type, active, destination }: NewsTabProps): ReactElement => { - const { t } = useTranslation('news') - if (type === TU_NEWS_TYPE) { - return - } - - return ( - - {t('local').toUpperCase()} - - ) -} - -export default NewsTab diff --git a/web/src/components/NewsTabs.tsx b/web/src/components/NewsTabs.tsx deleted file mode 100644 index 5e17db5194..0000000000 --- a/web/src/components/NewsTabs.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import Stack from '@mui/material/Stack' -import React, { ReactElement } from 'react' -import { useTranslation } from 'react-i18next' - -import { LOCAL_NEWS_TYPE, NEWS_ROUTE, NewsType, pathnameFromRouteInformation, TU_NEWS_TYPE } from 'shared' - -import NewsTab from './NewsTab' -import H1 from './base/H1' - -type NewsTabsProps = { - type: NewsType - region: string - localNewsEnabled: boolean - tuNewsEnabled: boolean - language: string -} - -const NewsTabs = ({ language, region, localNewsEnabled, tuNewsEnabled, type }: NewsTabsProps): ReactElement => { - const { t } = useTranslation('news') - const params = { route: NEWS_ROUTE, regionCode: region, languageCode: language } - const localNewsPath = pathnameFromRouteInformation({ ...params, newsType: LOCAL_NEWS_TYPE }) - const tuNewsPath = pathnameFromRouteInformation({ ...params, newsType: TU_NEWS_TYPE }) - - return ( - <> -

{t('news')}

- {localNewsEnabled && tuNewsEnabled && ( - - - - - )} - - ) -} - -export default NewsTabs diff --git a/web/src/components/Tiles.tsx b/web/src/components/Tiles.tsx index a9ecd2cdfd..f946f71607 100644 --- a/web/src/components/Tiles.tsx +++ b/web/src/components/Tiles.tsx @@ -1,4 +1,3 @@ -import Stack from '@mui/material/Stack' import { styled } from '@mui/material/styles' import React, { ReactElement } from 'react' @@ -26,14 +25,14 @@ type TilesProps = { } const Tiles = ({ title, tiles }: TilesProps): ReactElement => ( - -

{title}

+ <> +

{title}

{tiles.map(tile => ( ))} -
+ ) export default Tiles diff --git a/web/src/components/ToggleTextButtonGroup.tsx b/web/src/components/ToggleTextButtonGroup.tsx new file mode 100644 index 0000000000..d6e1c407ff --- /dev/null +++ b/web/src/components/ToggleTextButtonGroup.tsx @@ -0,0 +1,73 @@ +import CheckIcon from '@mui/icons-material/Check' +import Stack from '@mui/material/Stack' +import ToggleButton, { toggleButtonClasses } from '@mui/material/ToggleButton' +import ToggleButtonGroup from '@mui/material/ToggleButtonGroup' +import Typography from '@mui/material/Typography' +import { styled } from '@mui/material/styles' +import React, { ReactElement } from 'react' + +const StyledToggleButton = styled(ToggleButton)(({ theme }) => ({ + borderColor: theme.palette.primary.main, + + '&:first-of-type': { + borderTopLeftRadius: 64, + borderBottomLeftRadius: 64, + }, + + '&:last-of-type': { + borderTopRightRadius: 64, + borderBottomRightRadius: 64, + }, + + [`&.${toggleButtonClasses.selected}`]: { + backgroundColor: theme.palette.primary.main, + color: theme.palette.primary.contrastText, + + '&:hover': { + backgroundColor: theme.palette.primary.dark, + }, + }, +})) + +type ToggleButtonGroupProps = { + setValue: (T: T) => void + options: T[] + value: T + getLabel: (value: T) => string + className?: string +} + +const ToggleTextButtonGroup = ({ + setValue, + value, + options, + getLabel, + className, +}: ToggleButtonGroupProps): ReactElement => ( + setValue(value ?? value)} + className={className}> + {options.map(option => { + const selected = option === value + const selectedInlineEndPadding = 1.5 + return ( + + + {selected && } + {getLabel(option)} + + + ) + })} + +) + +export default ToggleTextButtonGroup diff --git a/web/src/components/__tests__/FailureSwitcher.spec.tsx b/web/src/components/__tests__/FailureSwitcher.spec.tsx index e1a1e367ed..c6c2d044a3 100644 --- a/web/src/components/__tests__/FailureSwitcher.spec.tsx +++ b/web/src/components/__tests__/FailureSwitcher.spec.tsx @@ -1,6 +1,5 @@ import React from 'react' -import { LOCAL_NEWS_TYPE, TU_NEWS_TYPE } from 'shared' import { MappingError, NotFoundError, ResponseError, fromError } from 'shared/api' import { renderWithRouterAndTheme } from '../../testing/render' @@ -21,12 +20,11 @@ describe('FailureSwitcher', () => { }) it.each` - type | id | notFoundKey | goToKey | goToPath - ${'category'} | ${'willkommen'} | ${'category'} | ${'categories'} | ${'/augsburg/de'} - ${'event'} | ${'1234'} | ${'event'} | ${'events'} | ${'/augsburg/de/events'} - ${LOCAL_NEWS_TYPE} | ${'1'} | ${'news'} | ${'news'} | ${'/augsburg/de/news/local'} - ${TU_NEWS_TYPE} | ${'1'} | ${'news'} | ${'news'} | ${'/augsburg/de/news/tu-news'} - ${'place'} | ${'1234'} | ${'place'} | ${'places'} | ${'/augsburg/de/places'} + type | id | notFoundKey | goToKey | goToPath + ${'category'} | ${'willkommen'} | ${'category'} | ${'categories'} | ${'/augsburg/de'} + ${'event'} | ${'1234'} | ${'event'} | ${'events'} | ${'/augsburg/de/events'} + ${'news'} | ${'1'} | ${'news'} | ${'news'} | ${'/augsburg/de/news'} + ${'place'} | ${'1234'} | ${'place'} | ${'places'} | ${'/augsburg/de/places'} `('should render $type not found failure', ({ type, id, notFoundKey, goToKey, goToPath }) => { const error = new NotFoundError({ type, id, language, region }) const { getByText } = renderWithRouterAndTheme() diff --git a/web/src/components/__tests__/NavigationTabs.spec.tsx b/web/src/components/__tests__/NavigationTabs.spec.tsx index 6a7126caa2..08c94c007f 100644 --- a/web/src/components/__tests__/NavigationTabs.spec.tsx +++ b/web/src/components/__tests__/NavigationTabs.spec.tsx @@ -12,7 +12,7 @@ describe('NavigationTabs', () => { const regionModel = ( eventsEnabled: boolean, placesEnabled: boolean, - tuNewsEnabled: boolean, + externalNewsEnabled: boolean, localNewsEnabled: boolean, ) => new RegionModel({ @@ -23,7 +23,7 @@ describe('NavigationTabs', () => { eventsEnabled, placesEnabled, localNewsEnabled, - tuNewsEnabled, + externalNewsEnabled, sortingName: 'Augsburg', prefix: 'Stadt', latitude: 48.369696, diff --git a/web/src/components/__tests__/NewsListItem.spec.tsx b/web/src/components/__tests__/NewsListItem.spec.tsx index c334b05cb6..516f2bb6a7 100644 --- a/web/src/components/__tests__/NewsListItem.spec.tsx +++ b/web/src/components/__tests__/NewsListItem.spec.tsx @@ -2,6 +2,7 @@ import { DateTime } from 'luxon' import React from 'react' import { getExcerpt } from 'shared' +import { NewsModel } from 'shared/api' import { EXCERPT_MAX_CHARS } from '../../constants' import { renderWithRouterAndTheme } from '../../testing/render' @@ -18,9 +19,18 @@ jest.mock('../LastUpdateInfo', () => ) describe('NewsListItem', () => { - const link = '/testumgebung/en/news/local' - const lastUpdate = DateTime.fromISO('2020-03-20T17:50:00.000Z') + const news = new NewsModel({ + id: 217, + title: 'Tick bite - What to do?', + lastUpdate, + content: + 'In summer there are often ticks in forest and meadows with high grass. These are very small animals. They feed on the blood of people or animals they sting, like mosquitoes. But they stay in the skin longer and can transmit dangerous diseases. If you have been in high grass, you should search your body very thoroughly for ticks. They like to sit in the knees, armpits or in the groin area. If you discover a tick in your skin, you should carefully pull it out with tweezers without crushing it. If the sting inflames, you must see a doctor.', + availableLanguages: { de: 123, it: 234 }, + externalUrl: 'https://example.com', + source: 'local', + }) + const title = 'Tick bite - What to do?' it('should show all the relevant information', () => { @@ -33,9 +43,7 @@ describe('NewsListItem', () => { 'If you discover a tick in your skin, you should carefully pull it out with tweezers without crushing it. ' + 'If the sting inflames, you must see a doctor.' - const { getByText } = renderWithRouterAndTheme( - , - ) + const { getByText } = renderWithRouterAndTheme() expect(getByText(title)).toBeTruthy() expect(getByText(getExcerpt(message, { maxChars: EXCERPT_MAX_CHARS, replaceLineBreaks: false }))).toBeTruthy() diff --git a/web/src/components/__tests__/NewsTab.spec.tsx b/web/src/components/__tests__/NewsTab.spec.tsx deleted file mode 100644 index bfb1be420b..0000000000 --- a/web/src/components/__tests__/NewsTab.spec.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react' - -import { LOCAL_NEWS_TYPE, TU_NEWS_TYPE } from 'shared' - -import { renderWithRouterAndTheme } from '../../testing/render' -import NewsTab from '../NewsTab' - -jest.mock('react-i18next') - -describe('NewsTab', () => { - const active = true - const destination = '/testregion/en/news/local' - - it('should render the local news tab', () => { - const { getByText, queryByLabelText } = renderWithRouterAndTheme( - , - ) - expect(getByText('NEWS:LOCAL').closest('a')).toHaveProperty('href', `http://localhost${destination}`) - expect(queryByLabelText('tuNews')).toBeFalsy() - }) - - it('should render the tünews news tab', () => { - const { queryByText, getByLabelText } = renderWithRouterAndTheme( - , - ) - expect(getByLabelText('tünews INTERNATIONAL')).toHaveProperty('href', `http://localhost${destination}`) - expect(queryByText('NEWS:LOCAL')).toBeFalsy() - }) -}) diff --git a/web/src/components/__tests__/NewsTabs.spec.tsx b/web/src/components/__tests__/NewsTabs.spec.tsx deleted file mode 100644 index bfccda01af..0000000000 --- a/web/src/components/__tests__/NewsTabs.spec.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react' - -import { LOCAL_NEWS_TYPE, tuNewsLabel } from 'shared' - -import { renderWithRouterAndTheme } from '../../testing/render' -import NewsTabs from '../NewsTabs' - -jest.mock('react-i18next') - -describe('NewsTabs', () => { - const language = 'en' - - it('should render two tabs if both local news and tuNews are enabled', () => { - const { getByLabelText } = renderWithRouterAndTheme( - , - ) - expect(getByLabelText(tuNewsLabel)).toBeDefined() - expect(getByLabelText('news:local')).toBeDefined() - }) -}) diff --git a/web/src/components/base/H1.tsx b/web/src/components/base/H1.tsx index 7a5cb72823..83c378f377 100644 --- a/web/src/components/base/H1.tsx +++ b/web/src/components/base/H1.tsx @@ -6,7 +6,7 @@ import useDimensions from '../../hooks/useDimensions' const H1 = ({ children, ...props }: TypographyProps): ReactElement => { const { mobile } = useDimensions() return ( - + {children} ) diff --git a/web/src/hooks/useTtsPlayer.ts b/web/src/hooks/useTtsPlayer.ts index dd26bd1598..2caed629fa 100644 --- a/web/src/hooks/useTtsPlayer.ts +++ b/web/src/hooks/useTtsPlayer.ts @@ -1,14 +1,11 @@ import { useContext, useEffect } from 'react' import { segmentText, parseHTML } from 'shared' -import { LocalNewsModel, DocumentModel, TuNewsModel } from 'shared/api' +import { NewsModel, DocumentModel } from 'shared/api' import { TtsContext, TtsContextType } from '../contexts/TtsContext' -const useTtsPlayer = ( - model: DocumentModel | LocalNewsModel | TuNewsModel | undefined | null, - languageCode: string, -): TtsContextType => { +const useTtsPlayer = (model: DocumentModel | NewsModel | undefined | null, languageCode: string): TtsContextType => { const tts = useContext(TtsContext) const { setSentences } = tts diff --git a/web/src/routes/LocalNewsPage.tsx b/web/src/routes/LocalNewsPage.tsx deleted file mode 100644 index 45f7f9b340..0000000000 --- a/web/src/routes/LocalNewsPage.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import React, { ReactElement } from 'react' -import { useTranslation } from 'react-i18next' -import { useParams } from 'react-router' - -import { - LOCAL_NEWS_TYPE, - NEWS_ROUTE, - NewsRouteInformationType, - pathnameFromRouteInformation, - replaceLinks, -} from 'shared' -import { createLocalNewsEndpoint, LocalNewsModel, NotFoundError } from 'shared/api' - -import FailureSwitcherWithHelmet from '../components/FailureSwitcherWithHelmet' -import Helmet from '../components/Helmet' -import NewsListItem from '../components/NewsListItem' -import NewsTabs from '../components/NewsTabs' -import Page from '../components/Page' -import RegionContentLayout, { RegionContentLayoutProps } from '../components/RegionContentLayout' -import RegionContentToolbar from '../components/RegionContentToolbar' -import SkeletonList from '../components/SkeletonList' -import SkeletonPage from '../components/SkeletonPage' -import List from '../components/base/List' -import { cmsApiBaseUrl } from '../constants/urls' -import useQueryFromEndpoint from '../hooks/useQueryFromEndpoint' -import useTtsPlayer from '../hooks/useTtsPlayer' -import { RegionRouteProps } from './index' - -const LocalNewsPage = ({ region, pathname, languageCode, regionCode }: RegionRouteProps): ReactElement | null => { - const { newsId } = useParams() - const { t } = useTranslation('news') - - const { - data: localNews, - isPending, - error: newsError, - } = useQueryFromEndpoint(createLocalNewsEndpoint, cmsApiBaseUrl, { region: regionCode, language: languageCode }) - - const newsModel = newsId ? localNews?.find((it: LocalNewsModel) => it.id.toString() === newsId) : undefined - useTtsPlayer(newsModel, languageCode) - - if (!region) { - return null - } - - const createNewsPath = ({ languageCode: newLanguageCode, newsId }: Partial): string => - pathnameFromRouteInformation({ - route: NEWS_ROUTE, - newsType: LOCAL_NEWS_TYPE, - regionCode, - languageCode: newLanguageCode ?? languageCode, - newsId, - }) - - const languageChangePaths = region.languages.map(({ code, name }) => { - const newNewsId = newsModel?.availableLanguages[code] - return { - path: newsId && newNewsId === undefined ? null : createNewsPath({ languageCode: code, newsId: newNewsId }), - name, - code, - } - }) - - const pageTitle = `${newsModel && newsModel.title ? newsModel.title : t('localNews.pageTitle')} - ${region.name}` - const locationLayoutParams: Omit = { - region, - languageChangePaths, - languageCode, - pageTitle, - slug: null, - toolbar: , - } - - if (newsError) { - const error = new NotFoundError({ - type: LOCAL_NEWS_TYPE, - id: pathname, - region: regionCode, - language: languageCode, - }) - - return ( - - - - ) - } - - if (newsId) { - if (!newsModel) { - return ( - - - - ) - } - const linkedContent = replaceLinks(newsModel.content) - return ( - - - - - ) - } - - const NewsListItems = localNews?.map(localNewsItem => { - const { id, title, content, timestamp } = localNewsItem - return ( - - ) - }) - - return ( - - - - {isPending ? : } - - ) -} - -export default LocalNewsPage diff --git a/web/src/routes/NewsDetailPage.tsx b/web/src/routes/NewsDetailPage.tsx new file mode 100644 index 0000000000..3b6c38daf8 --- /dev/null +++ b/web/src/routes/NewsDetailPage.tsx @@ -0,0 +1,95 @@ +import React, { ReactElement } from 'react' +import { useTranslation } from 'react-i18next' +import { useParams } from 'react-router' + +import { NEWS_ROUTE, pathnameFromRouteInformation } from 'shared' +import { AMAL_NEWS_SOURCE, createNewsElementEndpoint, LOCAL_NEWS_SOURCE } from 'shared/api' + +import { AmalNewsLogo, TuNewsIcon } from '../assets' +import FailureSwitcherWithHelmet from '../components/FailureSwitcherWithHelmet' +import Helmet from '../components/Helmet' +import Page from '../components/Page' +import RegionContentLayout, { RegionContentLayoutProps } from '../components/RegionContentLayout' +import RegionContentToolbar from '../components/RegionContentToolbar' +import SkeletonPage from '../components/SkeletonPage' +import Link from '../components/base/Link' +import Svg from '../components/base/Svg' +import { cmsApiBaseUrl } from '../constants/urls' +import useQueryFromEndpoint from '../hooks/useQueryFromEndpoint' +import useTtsPlayer from '../hooks/useTtsPlayer' +import { RegionRouteProps } from './index' + +const NewsDetailPage = ({ region, pathname, regionCode, languageCode }: RegionRouteProps): ReactElement | null => { + const { data: news, error } = useQueryFromEndpoint(createNewsElementEndpoint, cmsApiBaseUrl, { + region: regionCode, + language: languageCode, + // This component is only opened when there is a news ID in the route + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + id: useParams().id!, + }) + const { t } = useTranslation('news') + + useTtsPlayer(news, languageCode) + + if (!region) { + return null + } + + const pageTitle = `${news?.title ?? t('news')} - ${region.name}` + + const languageChangePaths = region.languages.map(({ code, name }) => ({ + path: + code === languageCode + ? pathname + : pathnameFromRouteInformation({ + route: NEWS_ROUTE, + regionCode, + languageCode: code, + id: news?.availableLanguages?.[code], + }), + name, + code, + })) + + const locationLayoutParams: Omit = { + region, + languageChangePaths, + languageCode, + pageTitle, + slug: null, + toolbar: , + } + + if (error) { + return ( + + + + ) + } + + return ( + + + {news ? ( + + + + ) + } + /> + ) : ( + + )} + + ) +} + +export default NewsDetailPage diff --git a/web/src/routes/NewsPage.tsx b/web/src/routes/NewsPage.tsx new file mode 100644 index 0000000000..44035ddef0 --- /dev/null +++ b/web/src/routes/NewsPage.tsx @@ -0,0 +1,88 @@ +import Stack from '@mui/material/Stack' +import { styled } from '@mui/material/styles' +import React, { ReactElement, useState } from 'react' +import { useTranslation } from 'react-i18next' + +import { + NEWS_ALL_SOURCES_FILTER, + NEWS_SOURCE_FILTERS, + NEWS_ROUTE, + NewsSourceFilter, + pathnameFromRouteInformation, + newsFilterToSources, +} from 'shared' +import { createNewsEndpoint } from 'shared/api' + +import Helmet from '../components/Helmet' +import NewsListItem from '../components/NewsListItem' +import RegionContentLayout, { RegionContentLayoutProps } from '../components/RegionContentLayout' +import SkeletonList from '../components/SkeletonList' +import ToggleTextButtonGroup from '../components/ToggleTextButtonGroup' +import H1 from '../components/base/H1' +import List from '../components/base/List' +import { cmsApiBaseUrl } from '../constants/urls' +import useDimensions from '../hooks/useDimensions' +import useQueryFromEndpoint from '../hooks/useQueryFromEndpoint' +import { RegionRouteProps } from './index' + +const NewsSourceFilterButtonGroup = styled(ToggleTextButtonGroup)({ + paddingInline: 16, +}) as typeof ToggleTextButtonGroup + +const NewsPage = ({ languageCode, regionCode, region }: RegionRouteProps): ReactElement | null => { + const [newsSourceFilter, setNewsSourceFilter] = useState(NEWS_ALL_SOURCES_FILTER) + const { desktop } = useDimensions() + const { t } = useTranslation('news') + + const { data, ...response } = useQueryFromEndpoint(createNewsEndpoint, cmsApiBaseUrl, { + region: regionCode, + language: languageCode, + }) + + if (!region) { + return null + } + + const languageChangePaths = region.languages.map(({ code, name }) => ({ + path: pathnameFromRouteInformation({ route: NEWS_ROUTE, regionCode, languageCode: code }), + name, + code, + })) + + const pageTitle = `${t('news')} - ${region.name}` + const locationLayoutParams: Omit = { + region, + languageChangePaths, + languageCode, + pageTitle, + slug: null, + } + + const newsSources = newsFilterToSources(newsSourceFilter) + const news = data?.filter(news => !newsSources || newsSources.includes(news.source)) + const newsListItems = + news?.map(item => ) ?? + [] + const getLabel = (value: NewsSourceFilter): string => t(desktop ? `${value}News` : value) + const showNewsSourceFilter = region.localNewsEnabled && region.externalNewsEnabled + + return ( + + +

{t('news')}

+ + {showNewsSourceFilter && ( + + )} + {response.isPending ? : } + +
+ ) +} + +export default NewsPage diff --git a/web/src/routes/TuNewsDetailPage.tsx b/web/src/routes/TuNewsDetailPage.tsx deleted file mode 100644 index 383178eeaf..0000000000 --- a/web/src/routes/TuNewsDetailPage.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import Stack from '@mui/material/Stack' -import { styled } from '@mui/material/styles' -import React, { ReactElement } from 'react' -import { useParams } from 'react-router' - -import { TU_NEWS_TYPE, tuNewsLabel } from 'shared' -import { createTuNewsElementEndpoint, NotFoundError } from 'shared/api' - -import { TuNewsActiveIcon } from '../assets' -import FailureSwitcherWithHelmet from '../components/FailureSwitcherWithHelmet' -import Helmet from '../components/Helmet' -import Page from '../components/Page' -import RegionContentLayout, { RegionContentLayoutProps } from '../components/RegionContentLayout' -import RegionContentToolbar from '../components/RegionContentToolbar' -import SkeletonPage from '../components/SkeletonPage' -import Svg from '../components/base/Svg' -import { tuNewsApiBaseUrl } from '../constants/urls' -import useQueryFromEndpoint from '../hooks/useQueryFromEndpoint' -import useTtsPlayer from '../hooks/useTtsPlayer' -import { RegionRouteProps } from './index' - -const TuNewsBanner = styled('div')(({ theme }) => ({ - overflow: 'hidden', - marginBlock: 24, - backgroundColor: theme.palette.tuNews.light, - borderRadius: 12, - height: 60, - alignItems: 'start', -})) - -const IconContainer = styled(Stack)(({ theme }) => ({ - backgroundColor: theme.palette.tuNews.main, - shapeRendering: 'crispEdges', -})) - -const TuNewsDetailPage = ({ region, pathname, regionCode, languageCode }: RegionRouteProps): ReactElement | null => { - // This component is only opened when there is a news ID in the route - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const newsId = useParams().newsId! - - const { data: newsModel, error: newsError } = useQueryFromEndpoint(createTuNewsElementEndpoint, tuNewsApiBaseUrl, { - id: parseInt(newsId, 10), - }) - - useTtsPlayer(newsModel, languageCode) - - if (!region) { - return null - } - - const pageTitle = `${newsModel?.title ?? tuNewsLabel} - ${region.name}` - - // Language change is not possible between tuNews detail views because we don't know the id of other languages - const languageChangePaths = region.languages.map(({ code, name }) => ({ - path: code === languageCode ? pathname : null, - name, - code, - })) - - const locationLayoutParams: Omit = { - region, - languageChangePaths, - languageCode, - pageTitle, - slug: null, - toolbar: , - } - - if (newsError) { - const error = - newsError instanceof NotFoundError - ? new NotFoundError({ - type: TU_NEWS_TYPE, - id: pathname, - region: regionCode, - language: languageCode, - }) - : newsError - - return ( - - - - ) - } - - return ( - - - - - - - - {!newsModel ? ( - - ) : ( - - )} - - ) -} - -export default TuNewsDetailPage diff --git a/web/src/routes/TuNewsPage.tsx b/web/src/routes/TuNewsPage.tsx deleted file mode 100644 index f31ca9228c..0000000000 --- a/web/src/routes/TuNewsPage.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import React, { ReactElement, useCallback } from 'react' - -import { NEWS_ROUTE, pathnameFromRouteInformation, TU_NEWS_TYPE, tuNewsLabel } from 'shared' -import { createTuNewsEndpoint, createTuNewsLanguagesEndpoint, TuNewsModel } from 'shared/api' - -import FailureSwitcherWithHelmet from '../components/FailureSwitcherWithHelmet' -import Helmet from '../components/Helmet' -import InfiniteScrollList from '../components/InfiniteScrollList' -import LanguageFailure from '../components/LanguageFailure' -import NewsListItem from '../components/NewsListItem' -import NewsTabs from '../components/NewsTabs' -import RegionContentLayout, { RegionContentLayoutProps } from '../components/RegionContentLayout' -import RegionContentToolbar from '../components/RegionContentToolbar' -import SkeletonList from '../components/SkeletonList' -import { tuNewsApiBaseUrl } from '../constants/urls' -import useQueryFromEndpoint from '../hooks/useQueryFromEndpoint' -import { RegionRouteProps } from './index' - -const DEFAULT_PAGE = 1 -const DEFAULT_COUNT = 10 - -const TuNewsPage = ({ regionCode, languageCode, region }: RegionRouteProps): ReactElement | null => { - const { data: tuNewsLanguages, error } = useQueryFromEndpoint( - createTuNewsLanguagesEndpoint, - tuNewsApiBaseUrl, - undefined, - ) - - const loadTuNews = useCallback( - async (page: number) => { - const endpoint = createTuNewsEndpoint(tuNewsApiBaseUrl) - const { data } = await endpoint.request({ language: languageCode, page, count: DEFAULT_COUNT }) - if (!data) { - throw new Error('Data missing!') - } - return data - }, - [languageCode], - ) - - if (!region) { - return null - } - - const renderTuNewsListItem = (tuNewsModel: TuNewsModel) => { - const { id, title, content, lastUpdate } = tuNewsModel - return ( - - ) - } - - const languageChangePaths = region.languages.map(({ code, name }) => { - const isLanguageAvailable = tuNewsLanguages?.find(language => language.code === code) - return { - path: isLanguageAvailable - ? pathnameFromRouteInformation({ route: NEWS_ROUTE, newsType: TU_NEWS_TYPE, regionCode, languageCode: code }) - : null, - name, - code, - } - }) - - const pageTitle = `${tuNewsLabel} - ${region.name}` - const locationLayoutParams: Omit = { - region, - languageChangePaths, - languageCode, - pageTitle, - slug: null, - toolbar: , - } - - if (error) { - return ( - - - - ) - } - - if (!tuNewsLanguages) { - return ( - - - - - ) - } - - if (!tuNewsLanguages.find(({ code }) => code === languageCode)) { - return ( - - - - - ) - } - - return ( - - - - - - ) -} - -export default TuNewsPage diff --git a/web/src/routes/__tests__/TuNewsPage.spec.tsx b/web/src/routes/__tests__/TuNewsPage.spec.tsx deleted file mode 100644 index 3bdcc98a0d..0000000000 --- a/web/src/routes/__tests__/TuNewsPage.spec.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import React from 'react' - -import { NEWS_ROUTE, pathnameFromRouteInformation, TU_NEWS_TYPE } from 'shared' -import { RegionModelBuilder, LanguageModelBuilder } from 'shared/api' - -import { - mockUseQueryFromEndpointWithData, - mockUseQueryFromEndpointWithError, -} from '../../testing/mockUseQueryFromEndpoint' -import { renderRoute } from '../../testing/render' -import TuNewsPage from '../TuNewsPage' -import { RoutePatterns, TU_NEWS_ROUTE } from '../index' - -jest.mock('../../hooks/useQueryFromEndpoint') -jest.mock('react-i18next') -jest.mock('../../components/InfiniteScrollList', () => () => 'List') -jest.mock('../../components/RegionContentHeader', () => () => null) - -describe('TuNewsPage', () => { - beforeEach(() => { - jest.clearAllMocks() - }) - - const regions = new RegionModelBuilder(2).build() - const tuNewsLanguages = new LanguageModelBuilder(2).build() - const region = regions[0]! - const language = tuNewsLanguages[0]! - - const pathname = pathnameFromRouteInformation({ - route: NEWS_ROUTE, - newsType: TU_NEWS_TYPE, - regionCode: region.code, - languageCode: language.code, - }) - const routePattern = `/:regionCode/:languageCode/${RoutePatterns[TU_NEWS_ROUTE]}` - - const renderTuNewsRoute = (languageModel = language) => - renderRoute( - , - { - routePattern, - pathname, - }, - ) - - it('should render error if loading languages fails', () => { - mockUseQueryFromEndpointWithError('my lang error') - const { getByText } = renderTuNewsRoute(language) - expect(getByText('error:unknownError')).toBeTruthy() - }) - - it('should render language failure if language is not available', () => { - mockUseQueryFromEndpointWithData(tuNewsLanguages) - const { getAllByText, getByRole } = renderTuNewsRoute(region.languages[2]!) - expect(getAllByText('error:notFound.language')).toBeTruthy() - // Available languages - tuNewsLanguages.forEach(({ name, code }) => { - expect(getAllByText(name)[0]!.closest('a')).toHaveProperty( - 'href', - `http://localhost/augsburg/${code}/news/tu-news`, - ) - }) - - // Unavailable language is rendered as a button - expect(getByRole('button', { name: region.languages[2]!.name })).toBeTruthy() - }) - - it('should render list', () => { - mockUseQueryFromEndpointWithData(tuNewsLanguages) - const { getByText } = renderTuNewsRoute() - expect(getByText('List')).toBeTruthy() - }) -}) diff --git a/web/src/routes/index.ts b/web/src/routes/index.ts index 6da4412046..946ce80718 100644 --- a/web/src/routes/index.ts +++ b/web/src/routes/index.ts @@ -6,19 +6,15 @@ import { EVENTS_ROUTE, REGIONS_ROUTE, LICENSES_ROUTE, - LOCAL_NEWS_TYPE, MAIN_IMPRINT_ROUTE, NEWS_ROUTE, NOT_FOUND_ROUTE, PLACES_ROUTE, SEARCH_ROUTE, - TU_NEWS_TYPE, } from 'shared' import { RegionModel } from 'shared/api' -export const LOCAL_NEWS_ROUTE = LOCAL_NEWS_TYPE -export const TU_NEWS_ROUTE = TU_NEWS_TYPE -export const TU_NEWS_DETAIL_ROUTE = `${TU_NEWS_ROUTE}-detail` as const +export const NEWS_DETAIL_ROUTE = `${NEWS_ROUTE}-detail` as const const languageCodePattern = ':languageCode' export const regionContentPattern = `/:regionCode/${languageCodePattern}/*` @@ -33,9 +29,8 @@ export const RoutePatterns = { // Region content routes, relative to /:regionCode/:languageCode [EVENTS_ROUTE]: EVENTS_ROUTE, [PLACES_ROUTE]: PLACES_ROUTE, - [LOCAL_NEWS_ROUTE]: `${NEWS_ROUTE}/${LOCAL_NEWS_ROUTE}`, - [TU_NEWS_ROUTE]: `${NEWS_ROUTE}/${TU_NEWS_ROUTE}`, - [TU_NEWS_DETAIL_ROUTE]: `${NEWS_ROUTE}/${TU_NEWS_ROUTE}/:newsId`, + [NEWS_ROUTE]: NEWS_ROUTE, + [NEWS_DETAIL_ROUTE]: `${NEWS_ROUTE}/:id`, [SEARCH_ROUTE]: SEARCH_ROUTE, [IMPRINT_ROUTE]: IMPRINT_ROUTE, [CATEGORIES_ROUTE]: '*', diff --git a/web/src/utils/navigationItems.ts b/web/src/utils/navigationItems.ts index b0317642c6..1b41a48fd2 100644 --- a/web/src/utils/navigationItems.ts +++ b/web/src/utils/navigationItems.ts @@ -14,8 +14,6 @@ import { } from 'shared' import { RegionModel } from 'shared/api' -import { LOCAL_NEWS_ROUTE, TU_NEWS_ROUTE } from '../routes' - type NavigationItem = { to: string value: string @@ -29,19 +27,18 @@ type GetNavigationItemsProps = { } const getNavigationItems = ({ regionModel, languageCode }: GetNavigationItemsProps): NavigationItem[] | null => { - const { eventsEnabled, placesEnabled, tuNewsEnabled, localNewsEnabled } = regionModel + const { eventsEnabled, placesEnabled, newsEnabled } = regionModel const params = { regionCode: regionModel.code, languageCode } const categoriesPath = regionContentPath(params) const eventsPath = pathnameFromRouteInformation({ route: EVENTS_ROUTE, ...params }) const placesPath = pathnameFromRouteInformation({ route: PLACES_ROUTE, ...params }) - const newsType = localNewsEnabled ? LOCAL_NEWS_ROUTE : TU_NEWS_ROUTE - const newsPath = pathnameFromRouteInformation({ route: NEWS_ROUTE, newsType, ...params }) + const newsPath = pathnameFromRouteInformation({ route: NEWS_ROUTE, ...params }) const items: (NavigationItem | null)[] = [ { value: CATEGORIES_ROUTE, to: categoriesPath, label: 'localInformationLabel', Icon: SignpostIcon }, placesEnabled ? { value: PLACES_ROUTE, to: placesPath, label: 'locations', Icon: MapIcon } : null, - localNewsEnabled || tuNewsEnabled ? { value: NEWS_ROUTE, to: newsPath, label: 'news', Icon: NewspaperIcon } : null, + newsEnabled ? { value: NEWS_ROUTE, to: newsPath, label: 'news', Icon: NewspaperIcon } : null, eventsEnabled ? { value: EVENTS_ROUTE, to: eventsPath, label: 'events', Icon: CalendarTodayIcon } : null, ] const validItems = items.filter((tab): tab is NavigationItem => tab !== null)