digitalfabrik/integreat-app

View on GitHub
shared/routes/InternalPathnameParser.ts

Summary

Maintainability
A
0 mins
Test Coverage
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import {
  CATEGORIES_ROUTE,
  DISCLAIMER_ROUTE,
  EVENTS_ROUTE,
  JPAL_TRACKING_ROUTE,
  LANDING_ROUTE,
  LOCAL_NEWS_TYPE,
  NEWS_ROUTE,
  POIS_ROUTE,
  RESERVED_CITY_CONTENT_SLUGS,
  SEARCH_ROUTE,
  TU_NEWS_TYPE,
} from '.'

import normalizePath from '../utils/normalizePath'
import { RouteInformationType } from './RouteInformationTypes'
import { parseQueryParams } from './query'

const ENTITY_ID_INDEX = 3

class InternalPathnameParser {
  _parts: Array<string>
  _length: number
  _fallbackLanguageCode: string
  _fixedCity: string | null
  _queryParams: URLSearchParams

  constructor(pathname: string, languageCode: string, fixedCity: string | null, query?: string) {
    this._fixedCity = fixedCity
    this._fallbackLanguageCode = languageCode
    this._parts = this.pathnameParts(normalizePath(pathname))
    this._length = this._parts.length
    this._queryParams = new URLSearchParams(query)
  }

  pathnameParts = (pathname: string): string[] => {
    const parts = pathname.split('/').filter(Boolean)
    const [first, second, ...rest] = parts

    const isLanguageIndependentUrl = !!first && !!second && (RESERVED_CITY_CONTENT_SLUGS as string[]).includes(second)
    if (isLanguageIndependentUrl) {
      return [first, this._fallbackLanguageCode, second, ...rest]
    }

    return parts
  }

  isFixedCity = (): boolean => !!this._fixedCity && this._length >= 1 && this._parts[0] === this._fixedCity

  isCityContentFeatureRoute = (feature: string): boolean => this._length > 2 && this._parts[2] === feature

  languageCode = (): string => this._parts[1] ?? this._fallbackLanguageCode

  jpalTracking = (): RouteInformationType => {
    if (this._length > 0 && this._length <= 2 && this._parts[0] === JPAL_TRACKING_ROUTE) {
      const trackingCode = this._parts[1] ?? null
      return {
        route: JPAL_TRACKING_ROUTE,
        trackingCode,
      }
    }

    return null
  }

  landing = (): RouteInformationType => {
    // There is no landing route if there is a fixed city
    if (this._fixedCity) {
      return null
    }

    if (this._length === 0 || this._parts[0] === LANDING_ROUTE) {
      // '/', '/landing' or '/landing/de'
      return {
        route: LANDING_ROUTE,
        languageCode: this.languageCode(),
      }
    }

    return null
  }

  dashboard = (): RouteInformationType => {
    const fixedCity = this._fixedCity

    if (fixedCity) {
      // '/', '/landing', '/abapp' or '/abapp/'de'
      if (this._length <= 2 && (this._length === 0 || this.isFixedCity() || this._parts[0] === LANDING_ROUTE)) {
        const cityContentPath = `/${fixedCity}/${this.languageCode()}`
        return {
          route: CATEGORIES_ROUTE,
          cityCode: fixedCity,
          languageCode: this.languageCode(),
          cityContentPath,
        }
      }
    } else if (this._length > 0 && this._length <= 2 && this._parts[0] !== LANDING_ROUTE) {
      const cityCode = this._parts[0]!
      // '/ansbach/de', '/ansbach'
      const cityContentPath = `/${cityCode}/${this.languageCode()}`
      return {
        route: CATEGORIES_ROUTE,
        cityCode,
        languageCode: this.languageCode(),
        cityContentPath,
      }
    }

    return null
  }

  cityContentParams = (
    feature: string,
  ): {
    cityCode: string
    languageCode: string
  } | null => {
    if ((this._fixedCity && !this.isFixedCity()) || !this.isCityContentFeatureRoute(feature)) {
      return null
    }

    // '/augsburg/de/<feature>' or '/augsburg/de/<feature>/id'
    return {
      cityCode: this._parts[0]!,
      languageCode: this._parts[1]!,
    }
  }

  events = (): RouteInformationType => {
    const params = this.cityContentParams(EVENTS_ROUTE)

    if (!params) {
      return null
    }

    // Single events are identified via their slug, e.g. 'my-event-1234'
    const slug = this._length > ENTITY_ID_INDEX ? this._parts[this._length - 1] : undefined
    return { ...params, route: EVENTS_ROUTE, slug }
  }

  pois = (): RouteInformationType => {
    const params = this.cityContentParams(POIS_ROUTE)

    if (!params) {
      return null
    }

    // Single pois are identified via their slug, e.g. 'my-poi-1234'
    const slug = this._length > ENTITY_ID_INDEX ? this._parts[ENTITY_ID_INDEX] : undefined
    const { multipoi, poiCategoryId, zoom } = parseQueryParams(this._queryParams)

    return { ...params, route: POIS_ROUTE, slug, multipoi, poiCategoryId, zoom }
  }

  news = (): RouteInformationType => {
    const params = this.cityContentParams(NEWS_ROUTE)

    if (!params) {
      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
    return {
      route: NEWS_ROUTE,
      cityCode: this._parts[0]!,
      languageCode: this._parts[1]!,
      newsType,
      newsId: newsId ? parseInt(newsId, 10) : undefined,
    }
  }

  disclaimer = (): RouteInformationType => {
    const params = this.cityContentParams(DISCLAIMER_ROUTE)

    if (!params) {
      return null
    }

    return {
      route: DISCLAIMER_ROUTE,
      ...params,
    }
  }

  search = (): RouteInformationType => {
    const params = this.cityContentParams(SEARCH_ROUTE)

    if (!params) {
      return null
    }

    return {
      route: SEARCH_ROUTE,
      searchText: parseQueryParams(this._queryParams).searchText,
      ...params,
    }
  }

  categories = (): RouteInformationType => {
    if (this._fixedCity && !this.isFixedCity()) {
      return null
    }

    if (
      this._length > 2 &&
      !([SEARCH_ROUTE, DISCLAIMER_ROUTE, POIS_ROUTE, EVENTS_ROUTE, NEWS_ROUTE] as string[]).includes(this._parts[2]!)
    ) {
      return {
        route: CATEGORIES_ROUTE,
        cityCode: this._parts[0]!,
        languageCode: this._parts[1]!,
        cityContentPath: `/${this._parts.join('/')}`,
      }
    }

    return null
  }

  route = (): RouteInformationType =>
    this.landing() ||
    this.jpalTracking() ||
    this.events() ||
    this.pois() ||
    this.disclaimer() ||
    this.news() ||
    this.search() ||
    this.dashboard() ||
    this.categories()
}

export default InternalPathnameParser