├── .nvmrc ├── .npmrc ├── src ├── components │ ├── ProfileDrawer │ │ ├── types.ts │ │ └── ProfileDrawer.module.scss │ ├── ThemeProvider │ │ ├── ThemeProvider.module.scss │ │ ├── types.ts │ │ └── ThemeProvider.tsx │ ├── Layout │ │ ├── iphone.png │ │ ├── Layout.module.scss │ │ └── Layout.tsx │ ├── CheckIn │ │ ├── Panel │ │ │ ├── types.ts │ │ │ ├── Panel.tsx │ │ │ └── Panel.module.scss │ │ ├── CheckIn.module.scss │ │ ├── OriginStep │ │ │ ├── types.ts │ │ │ └── OriginStep.module.scss │ │ ├── DestinationStep │ │ │ └── types.ts │ │ ├── CheckIn.context.ts │ │ ├── Search │ │ │ ├── Search.module.scss │ │ │ └── Search.tsx │ │ ├── consts.ts │ │ ├── types.ts │ │ ├── NewCurrentStatus │ │ │ ├── NewCurrentStatus.module.scss │ │ │ └── NewCurrentStatus.tsx │ │ └── CurrentStatus │ │ │ ├── CurrentStatus.module.scss │ │ │ └── CurrentStatus.tsx │ ├── StatusCard │ │ └── types.ts │ ├── ProfileImage │ │ ├── ProfileImage.module.scss │ │ └── ProfileImage.tsx │ ├── LegacyTime │ │ ├── types.ts │ │ └── LegacyTime.tsx │ ├── NewLineIndicator │ │ ├── types.ts │ │ ├── NewLineIndicator.module.scss │ │ └── NewLineIndicator.tsx │ ├── Providers │ │ ├── types.ts │ │ └── Providers.tsx │ ├── IconSkew │ │ ├── types.ts │ │ ├── IconSkew.module.scss │ │ └── IconSkew.tsx │ ├── NativeSelect │ │ ├── types.ts │ │ ├── NativeSelect.module.scss │ │ └── NativeSelect.tsx │ ├── Shimmer │ │ ├── types.ts │ │ ├── Shimmer.tsx │ │ └── Shimmer.module.scss │ ├── FilterButton │ │ ├── types.ts │ │ ├── FilterButton.module.scss │ │ └── FilterButton.tsx │ ├── LineIndicator │ │ ├── types.ts │ │ ├── LineIndicator.module.scss │ │ └── LineIndicator.tsx │ ├── ProductIcon │ │ └── types.ts │ ├── Button │ │ ├── types.ts │ │ ├── Button.tsx │ │ └── Button.module.scss │ ├── ScrollArea │ │ ├── types.ts │ │ └── ScrollArea.module.scss │ ├── Time │ │ ├── types.ts │ │ ├── Time.module.scss │ │ └── Time.tsx │ ├── LockBodyScroll │ │ └── LockBodyScroll.tsx │ ├── StopoverSelector │ │ ├── types.ts │ │ └── StopoverSelector.tsx │ ├── TripSelector │ │ └── types.ts │ ├── Notifications │ │ ├── Notifications.module.scss │ │ └── Notifications.tsx │ ├── FullscreenLoading │ │ ├── FullscreenLoading.tsx │ │ └── FullscreenLoading.module.scss │ ├── Overlay │ │ ├── types.ts │ │ └── Overlay.module.scss │ ├── Statuses │ │ ├── Statuses.module.scss │ │ └── Statuses.tsx │ ├── StatusDetails │ │ └── types.ts │ ├── AuthGuard │ │ └── AuthGuard.tsx │ ├── Navbar │ │ ├── Username.tsx │ │ ├── Navbar.module.scss │ │ └── Navbar.tsx │ ├── Route │ │ ├── types.ts │ │ ├── Route.tsx │ │ └── Route.module.scss │ ├── Profile │ │ └── Statuses │ │ │ └── Statuses.tsx │ └── Login │ │ ├── Login.module.scss │ │ └── Login.tsx ├── app │ ├── status │ │ ├── [id] │ │ │ ├── types.ts │ │ │ └── page.tsx │ │ └── page.tsx │ ├── offline │ │ └── page.tsx │ ├── page.tsx │ ├── u │ │ ├── page.tsx │ │ └── [username] │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ ├── login │ │ └── page.tsx │ ├── dashboard │ │ ├── page.tsx │ │ └── layout.tsx │ ├── datenschutz │ │ └── page.tsx │ ├── loading.tsx │ ├── not-found.tsx │ ├── traewelling │ │ ├── statuses │ │ │ ├── dashboard │ │ │ │ └── me │ │ │ │ │ └── route.ts │ │ │ ├── route.ts │ │ │ ├── current │ │ │ │ └── route.ts │ │ │ └── [status] │ │ │ │ ├── route.ts │ │ │ │ └── like │ │ │ │ └── route.ts │ │ ├── dashboard │ │ │ ├── global │ │ │ │ └── route.ts │ │ │ ├── route.ts │ │ │ └── future │ │ │ │ └── route.ts │ │ ├── user │ │ │ └── [username] │ │ │ │ └── statuses │ │ │ │ └── route.ts │ │ ├── me │ │ │ └── route.ts │ │ ├── stations │ │ │ ├── history │ │ │ │ └── route.ts │ │ │ ├── autocomplete │ │ │ │ └── route.ts │ │ │ ├── nearby │ │ │ │ └── route.ts │ │ │ ├── checkin │ │ │ │ └── route.ts │ │ │ └── [station] │ │ │ │ └── route.ts │ │ ├── notifications │ │ │ └── unread │ │ │ │ └── route.ts │ │ └── trips │ │ │ └── route.ts │ ├── global-error.jsx │ ├── impressum │ │ └── page.tsx │ ├── api │ │ ├── revalidate │ │ │ └── route.ts │ │ ├── trips │ │ │ └── route.ts │ │ ├── dashboard │ │ │ └── route.ts │ │ ├── statuses │ │ │ └── current │ │ │ │ └── route.ts │ │ └── stations │ │ │ └── [station] │ │ │ └── route.ts │ └── layout.tsx ├── hooks │ ├── useOverlayScroll │ │ ├── types.ts │ │ └── useOverlayScroll.ts │ ├── useUmami │ │ ├── types.ts │ │ └── useUmami.ts │ ├── useDepartures │ │ ├── types.ts │ │ └── useDepartures.ts │ ├── useConsecutiveOverlays │ │ ├── types.ts │ │ └── useConsecutiveOverlays.ts │ ├── useCheckIn │ │ └── useCheckIn.ts │ ├── useAppTheme │ │ └── useAppTheme.ts │ ├── useDashboard │ │ └── useDashboard.ts │ ├── useNotifications │ │ └── useNotifications.ts │ ├── useLockBodyScroll │ │ └── useLockBodyScroll.ts │ ├── useIsDesktop │ │ └── useIsDesktop.ts │ ├── useCurrentStatus │ │ └── useCurrentStatus.ts │ ├── useRecentStations │ │ └── useRecentStations.ts │ ├── useStatus │ │ └── useStatus.ts │ ├── useUserStatuses │ │ └── useUserStatus.ts │ ├── useTrip │ │ └── useTrip.ts │ ├── useStops │ │ └── useStops.ts │ └── useStationSearch │ │ └── useStationSearch.ts ├── types │ ├── db-clean-station-name.d.ts │ ├── global.d.ts │ ├── next-auth.d.ts │ └── aboard.ts ├── utils │ ├── formatDate.ts │ ├── formatTime.ts │ ├── sortByLevenshtein.ts │ ├── debounce.ts │ ├── api │ │ ├── createResponse.ts │ │ ├── createErrorResponse.ts │ │ └── getSafeUrlParams.ts │ └── parseSchedule.ts ├── helpers │ ├── getContrastColor.ts │ ├── getStopsAfter.ts │ ├── lineAppearance │ │ ├── fetcher.ts │ │ ├── index.ts │ │ └── consts.ts │ ├── identifyLineByMagic │ │ └── index.ts │ └── getLineTheme │ │ ├── getLineTheme.ts │ │ └── consts.ts ├── overlays │ ├── SelectDestination │ │ ├── types.ts │ │ ├── SelectDestination.module.scss │ │ └── SelectDestination.overlay.tsx │ └── CompleteCheckIn │ │ └── types.ts ├── page-templates │ ├── dashboard.module.scss │ └── dashboard.tsx ├── traewelling-sdk │ ├── index.ts │ ├── functions │ │ ├── auth.ts │ │ ├── notifications.ts │ │ ├── station.ts │ │ ├── dashboard.ts │ │ ├── user.ts │ │ └── status.ts │ ├── hafasTypes.ts │ └── types.ts ├── scripts │ └── UmamiScript │ │ └── UmamiScript.tsx ├── styles │ ├── fonts.ts │ └── globals.css ├── pages │ ├── _error.jsx │ └── api │ │ └── auth │ │ └── [...nextauth].ts └── contexts │ └── CheckIn │ ├── reducer.ts │ ├── CheckIn.context.tsx │ └── types.ts ├── tools └── component-generator │ ├── templates │ ├── styles.hbs │ ├── types.hbs │ └── component.hbs │ └── plopfile.js ├── public ├── favicon.ico ├── freiburg.png ├── assets │ └── icons │ │ ├── icon-48x48.png │ │ ├── icon-72x72.png │ │ ├── icon-96x96.png │ │ ├── icon-128x128.png │ │ ├── icon-144x144.png │ │ ├── icon-152x152.png │ │ ├── icon-192x192.png │ │ ├── icon-384x384.png │ │ └── icon-512x512.png └── manifest.json ├── middleware.js ├── .prettierrc.json ├── .env.example ├── .eslintrc.json ├── .vscode ├── settings.json └── tasks.json ├── .gitignore ├── sentry.server.config.ts ├── sentry.edge.config.ts ├── tsconfig.json ├── sentry.client.config.ts ├── README.md ├── package.json └── next.config.js /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-prefix="" 2 | -------------------------------------------------------------------------------- /src/components/ProfileDrawer/types.ts: -------------------------------------------------------------------------------- 1 | export type ProfileDrawerProps = {} 2 | -------------------------------------------------------------------------------- /tools/component-generator/templates/styles.hbs: -------------------------------------------------------------------------------- 1 | .base { 2 | margin: 0; 3 | } 4 | -------------------------------------------------------------------------------- /tools/component-generator/templates/types.hbs: -------------------------------------------------------------------------------- 1 | export type {{name}}Props = {} 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunrisesdev/aboard/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/freiburg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunrisesdev/aboard/HEAD/public/freiburg.png -------------------------------------------------------------------------------- /src/components/ThemeProvider/ThemeProvider.module.scss: -------------------------------------------------------------------------------- 1 | .base { 2 | display: contents; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/status/[id]/types.ts: -------------------------------------------------------------------------------- 1 | export type StatusPageProps = { 2 | params: { 3 | id: string; 4 | }; 5 | }; 6 | -------------------------------------------------------------------------------- /src/components/Layout/iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunrisesdev/aboard/HEAD/src/components/Layout/iphone.png -------------------------------------------------------------------------------- /src/hooks/useOverlayScroll/types.ts: -------------------------------------------------------------------------------- 1 | export type UseOverlayScrollProps = { 2 | disableScroll?: boolean; 3 | }; 4 | -------------------------------------------------------------------------------- /public/assets/icons/icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunrisesdev/aboard/HEAD/public/assets/icons/icon-48x48.png -------------------------------------------------------------------------------- /public/assets/icons/icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunrisesdev/aboard/HEAD/public/assets/icons/icon-72x72.png -------------------------------------------------------------------------------- /public/assets/icons/icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunrisesdev/aboard/HEAD/public/assets/icons/icon-96x96.png -------------------------------------------------------------------------------- /middleware.js: -------------------------------------------------------------------------------- 1 | export { default } from 'next-auth/middleware'; 2 | 3 | export const config = { matcher: ['/dashboard'] }; 4 | -------------------------------------------------------------------------------- /public/assets/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunrisesdev/aboard/HEAD/public/assets/icons/icon-128x128.png -------------------------------------------------------------------------------- /public/assets/icons/icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunrisesdev/aboard/HEAD/public/assets/icons/icon-144x144.png -------------------------------------------------------------------------------- /public/assets/icons/icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunrisesdev/aboard/HEAD/public/assets/icons/icon-152x152.png -------------------------------------------------------------------------------- /public/assets/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunrisesdev/aboard/HEAD/public/assets/icons/icon-192x192.png -------------------------------------------------------------------------------- /public/assets/icons/icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunrisesdev/aboard/HEAD/public/assets/icons/icon-384x384.png -------------------------------------------------------------------------------- /public/assets/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunrisesdev/aboard/HEAD/public/assets/icons/icon-512x512.png -------------------------------------------------------------------------------- /src/app/offline/page.tsx: -------------------------------------------------------------------------------- 1 | export default function Page() { 2 | return
Entschuldigung, du bist leider Offline!
; 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "semi": true, 4 | "singleQuote": true, 5 | "trailingComma": "es5" 6 | } 7 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Login from '@/components/Login/Login'; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/u/page.tsx: -------------------------------------------------------------------------------- 1 | import { notFound } from 'next/navigation'; 2 | 3 | export default async function Page() { 4 | notFound(); 5 | } 6 | -------------------------------------------------------------------------------- /src/hooks/useUmami/types.ts: -------------------------------------------------------------------------------- 1 | export type UmamiTrackEventData = { 2 | type: string; 3 | } & Record; 4 | -------------------------------------------------------------------------------- /src/app/login/page.tsx: -------------------------------------------------------------------------------- 1 | import Login from '@/components/Login/Login'; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/components/CheckIn/Panel/types.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | export type PanelProps = { 4 | children: ReactNode; 5 | }; 6 | -------------------------------------------------------------------------------- /src/app/status/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation'; 2 | 3 | export default async function Page() { 4 | redirect('/dashboard'); 5 | } 6 | -------------------------------------------------------------------------------- /src/types/db-clean-station-name.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'db-clean-station-name' { 2 | export default cleanStationName = (noisy: string): string => ''; 3 | } 4 | -------------------------------------------------------------------------------- /src/components/CheckIn/CheckIn.module.scss: -------------------------------------------------------------------------------- 1 | .base { 2 | display: contents; 3 | } 4 | 5 | .statusLink { 6 | color: unset; 7 | text-decoration: none; 8 | } 9 | -------------------------------------------------------------------------------- /src/components/StatusCard/types.ts: -------------------------------------------------------------------------------- 1 | import { AboardStatus } from '@/types/aboard'; 2 | 3 | export type StatusCardProps = { 4 | status: AboardStatus; 5 | }; 6 | -------------------------------------------------------------------------------- /src/utils/formatDate.ts: -------------------------------------------------------------------------------- 1 | export const formatDate = (value: Date) => { 2 | return value.toLocaleDateString([], { 3 | dateStyle: 'full', 4 | }); 5 | }; 6 | -------------------------------------------------------------------------------- /src/app/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | import DashboardHome from '@/page-templates/dashboard'; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/components/ProfileImage/ProfileImage.module.scss: -------------------------------------------------------------------------------- 1 | .base { 2 | border-radius: 50%; 3 | } 4 | 5 | .wrapper { 6 | display: flex; 7 | border-radius: 50%; 8 | } 9 | -------------------------------------------------------------------------------- /src/components/LegacyTime/types.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | export type LegacyTimeProps = { 4 | children: ReactNode; 5 | className?: string; 6 | }; 7 | -------------------------------------------------------------------------------- /src/components/CheckIn/OriginStep/types.ts: -------------------------------------------------------------------------------- 1 | export type StationProps = { 2 | name: string; 3 | onClick?: () => void; 4 | query?: string; 5 | rilIdentifier: string | null; 6 | }; 7 | -------------------------------------------------------------------------------- /src/utils/formatTime.ts: -------------------------------------------------------------------------------- 1 | export const formatTime = (value: Date) => { 2 | return value.toLocaleTimeString([], { 3 | hour: '2-digit', 4 | minute: '2-digit', 5 | }); 6 | }; 7 | -------------------------------------------------------------------------------- /src/app/datenschutz/page.tsx: -------------------------------------------------------------------------------- 1 | export default function Page() { 2 | return
Coming soon
; 3 | } 4 | 5 | export const metadata = { 6 | title: 'Datenschutz - aboard.at', 7 | }; 8 | -------------------------------------------------------------------------------- /src/app/loading.tsx: -------------------------------------------------------------------------------- 1 | import FullscreenLoading from '@/components/FullscreenLoading/FullscreenLoading'; 2 | 3 | export default function Loading() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/helpers/getContrastColor.ts: -------------------------------------------------------------------------------- 1 | export const getContrastColor = (r: number, g: number, b: number) => { 2 | return r * 0.299 + g * 0.587 + b * 0.114 > 150 ? '#000000' : '#FFFFFF'; 3 | }; 4 | -------------------------------------------------------------------------------- /src/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | export default function NotFound() { 2 | return ( 3 | <> 4 |

Not Found

5 |

Could not find requested resource

6 | 7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /src/components/NewLineIndicator/types.ts: -------------------------------------------------------------------------------- 1 | import { AboardLine } from '@/types/aboard'; 2 | 3 | export type NewLineIndicatorProps = { 4 | line: AboardLine; 5 | noOutline?: boolean; 6 | }; 7 | -------------------------------------------------------------------------------- /src/app/u/[username]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren } from 'react'; 2 | 3 | export default async function Layout({ children }: PropsWithChildren) { 4 | return
{children}
; 5 | } 6 | -------------------------------------------------------------------------------- /src/hooks/useDepartures/types.ts: -------------------------------------------------------------------------------- 1 | import { TransportType } from '@/traewelling-sdk/types'; 2 | 3 | export type UseDeparturesOptions = { 4 | from?: string; 5 | transportType?: TransportType; 6 | }; 7 | -------------------------------------------------------------------------------- /src/overlays/SelectDestination/types.ts: -------------------------------------------------------------------------------- 1 | import { OverlayProps } from '@/components/Overlay/types'; 2 | 3 | export type SelectDestinationOverlayProps = OverlayProps & { 4 | onComplete: () => void; 5 | }; 6 | -------------------------------------------------------------------------------- /src/hooks/useConsecutiveOverlays/types.ts: -------------------------------------------------------------------------------- 1 | import { OverlayProps } from '@/components/Overlay/types'; 2 | 3 | export type ConsecutiveOverlays = { 4 | [O in `${T}Props`]: OverlayProps; 5 | }; 6 | -------------------------------------------------------------------------------- /src/overlays/CompleteCheckIn/types.ts: -------------------------------------------------------------------------------- 1 | import { OverlayProps } from '@/components/Overlay/types'; 2 | 3 | export type CompleteCheckInOverlayProps = OverlayProps & { 4 | onComplete: () => Promise | void; 5 | }; 6 | -------------------------------------------------------------------------------- /src/components/Providers/types.ts: -------------------------------------------------------------------------------- 1 | import { Session } from 'next-auth'; 2 | import { ReactNode } from 'react'; 3 | 4 | export type ProvidersProps = { 5 | children: ReactNode; 6 | session: Session | null | undefined; 7 | }; 8 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NEXTAUTH_SECRET= 2 | NEXTAUTH_URL= 3 | 4 | # get it from: https://traewelling.de/settings/applications 5 | # redirect_url: /api/auth/callback/traewelling 6 | TRAEWELLING_CLIENT_ID= 7 | TRAEWELLING_CLIENT_SECRET= 8 | 9 | -------------------------------------------------------------------------------- /src/components/IconSkew/types.ts: -------------------------------------------------------------------------------- 1 | import { CSSProperties, ReactNode } from 'react'; 2 | 3 | export type IconSkewProps = { 4 | children: ReactNode; 5 | className?: string; 6 | gap: CSSProperties['gap']; 7 | size: number; 8 | }; 9 | -------------------------------------------------------------------------------- /src/hooks/useCheckIn/useCheckIn.ts: -------------------------------------------------------------------------------- 1 | import { CheckInContext } from '@/contexts/CheckIn/CheckIn.context'; 2 | import { useContext } from 'react'; 3 | 4 | export const useCheckIn = () => { 5 | return useContext(CheckInContext); 6 | }; 7 | -------------------------------------------------------------------------------- /src/app/traewelling/statuses/dashboard/me/route.ts: -------------------------------------------------------------------------------- 1 | import createResponse from '@/utils/api/createResponse'; 2 | 3 | export async function GET(request: Request) { 4 | return createResponse({ 5 | body: 'Hello World!', 6 | }); 7 | } 8 | -------------------------------------------------------------------------------- /src/components/NativeSelect/types.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | export type NativeSelectProps = { 4 | children: ReactNode; 5 | onSelect: (value: string) => void; 6 | options: ReactNode[]; 7 | value: string; 8 | }; 9 | -------------------------------------------------------------------------------- /src/components/IconSkew/IconSkew.module.scss: -------------------------------------------------------------------------------- 1 | .base { 2 | align-items: flex-end; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | .row { 8 | display: flex; 9 | } 10 | 11 | .cell { 12 | display: contents; 13 | } 14 | -------------------------------------------------------------------------------- /src/components/Shimmer/types.ts: -------------------------------------------------------------------------------- 1 | import { CSSProperties } from 'react'; 2 | 3 | export type ShimmerProps = { 4 | className?: string; 5 | height?: CSSProperties['height']; 6 | style?: CSSProperties; 7 | width?: CSSProperties['width']; 8 | }; 9 | -------------------------------------------------------------------------------- /src/components/CheckIn/DestinationStep/types.ts: -------------------------------------------------------------------------------- 1 | export type StopProps = { 2 | arrivalAt: string | null; 3 | isCancelled: boolean; 4 | isDelayed: boolean; 5 | name: string; 6 | onClick?: () => void; 7 | plannedArrivalAt: string | null; 8 | }; 9 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next", 3 | "rules": { 4 | "eol-last": ["error", "always"], 5 | "eqeqeq": "error", 6 | "indent": ["error", 2], 7 | "quotes": ["error", "single"], 8 | "semi": ["error", "always"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/components/FilterButton/types.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | export type FilterButtonProps = { 4 | children: ReactNode; 5 | className?: string; 6 | isActive: boolean; 7 | onClick: (value: any) => void; 8 | value: any; 9 | }; 10 | -------------------------------------------------------------------------------- /src/components/Layout/Layout.module.scss: -------------------------------------------------------------------------------- 1 | .base { 2 | overflow-x: visible; 3 | margin: 0 auto; 4 | display: block; 5 | position: relative; 6 | 7 | @media screen and (min-width: 569px) { 8 | max-width: var(--layout-width); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/components/Layout/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren } from 'react'; 2 | import styles from './Layout.module.scss'; 3 | 4 | const Layout = ({ children }: PropsWithChildren) => { 5 | return
{children}
; 6 | }; 7 | 8 | export default Layout; 9 | -------------------------------------------------------------------------------- /src/components/LineIndicator/types.ts: -------------------------------------------------------------------------------- 1 | import { HAFASProductType } from '@/traewelling-sdk/hafasTypes'; 2 | 3 | export type LineIndicatorProps = { 4 | className?: string; 5 | isInverted?: boolean; 6 | lineId: string; 7 | lineName: string; 8 | product: HAFASProductType; 9 | }; 10 | -------------------------------------------------------------------------------- /tools/component-generator/templates/component.hbs: -------------------------------------------------------------------------------- 1 | import styles from './{{name}}.module.scss'; 2 | import type { {{name}}Props } from './types'; 3 | 4 | const {{name}} = ({}: {{name}}Props) => { 5 | 6 | return
; 7 | }; 8 | 9 | export default {{name}}; 10 | -------------------------------------------------------------------------------- /src/components/ProductIcon/types.ts: -------------------------------------------------------------------------------- 1 | import { HAFASProductType } from '@/traewelling-sdk/hafasTypes'; 2 | 3 | export type ProductIconVariant = Extract< 4 | HAFASProductType, 5 | 'bus' | 'suburban' | 'subway' | 'tram' 6 | >; 7 | 8 | export type ProductIconProps = { 9 | className?: string; 10 | }; 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": "explicit", 4 | "source.organizeImports": "explicit" 5 | }, 6 | "editor.formatOnSave": true, 7 | "typescript.tsdk": "node_modules/typescript/lib", 8 | "typescript.enablePromptUseWorkspaceTsdk": true 9 | } 10 | -------------------------------------------------------------------------------- /src/components/Button/types.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | type ButtonVariant = 'error' | 'primary' | 'secondary' | 'success'; 4 | 5 | export type ButtonProps = { 6 | children: ReactNode; 7 | className?: string; 8 | disabled?: boolean; 9 | onClick?: () => void; 10 | variant?: ButtonVariant; 11 | }; 12 | -------------------------------------------------------------------------------- /src/components/ScrollArea/types.ts: -------------------------------------------------------------------------------- 1 | import { CSSProperties, ReactNode } from 'react'; 2 | 3 | export type ScrollAreaProps = { 4 | bottomFogBorderRadius?: CSSProperties['borderRadius']; 5 | children: ReactNode; 6 | className?: string; 7 | noFog?: boolean; 8 | topFogBorderRadius?: CSSProperties['borderRadius']; 9 | }; 10 | -------------------------------------------------------------------------------- /src/components/Time/types.ts: -------------------------------------------------------------------------------- 1 | import { Schedule } from '@/utils/parseSchedule'; 2 | import { CSSProperties } from 'react'; 3 | 4 | export type TimeProps = { 5 | className?: string; 6 | delayStyle: 'hidden' | 'p+a' | 'p+d'; 7 | schedule: Schedule; 8 | style?: CSSProperties; 9 | type?: 'arrival' | 'departure'; 10 | }; 11 | -------------------------------------------------------------------------------- /src/components/LockBodyScroll/LockBodyScroll.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import useLockBodyScroll from '@/hooks/useLockBodyScroll/useLockBodyScroll'; 4 | import { Fragment } from 'react'; 5 | 6 | const LockBodyScroll = () => { 7 | useLockBodyScroll(); 8 | 9 | return ; 10 | }; 11 | 12 | export default LockBodyScroll; 13 | -------------------------------------------------------------------------------- /src/components/StopoverSelector/types.ts: -------------------------------------------------------------------------------- 1 | import { AboardStopover } from '@/types/aboard'; 2 | 3 | export type StopoverProps = { 4 | onClick?: () => void; 5 | stopover: AboardStopover; 6 | }; 7 | 8 | export type StopoverSelectorProps = { 9 | onSelect: (value: AboardStopover) => void; 10 | stopovers: AboardStopover[]; 11 | }; 12 | -------------------------------------------------------------------------------- /src/page-templates/dashboard.module.scss: -------------------------------------------------------------------------------- 1 | .legal { 2 | display: flex; 3 | gap: 0.5rem; 4 | justify-content: center; 5 | margin: 2rem 0 5rem; 6 | padding: 0 0 5rem 0; 7 | 8 | a { 9 | color: var(--slate-7); 10 | text-decoration: none; 11 | font-weight: 400; 12 | font-size: 0.9rem; 13 | line-height: 1.5rem; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/components/LegacyTime/LegacyTime.tsx: -------------------------------------------------------------------------------- 1 | import { figtree } from '@/styles/fonts'; 2 | import clsx from 'clsx'; 3 | import { LegacyTimeProps } from './types'; 4 | 5 | const LegacyTime = ({ children, className }: LegacyTimeProps) => { 6 | return {children}; 7 | }; 8 | 9 | export default LegacyTime; 10 | -------------------------------------------------------------------------------- /src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | declare var umami: umami.umami; 2 | 3 | // Based on https://umami.is/docs/tracker-functions 4 | declare namespace umami { 5 | interface umami { 6 | track(view_properties?: { website: string; [key: string]: string }): void; 7 | track( 8 | event_name: string, 9 | event_data?: { [key: string]: string | number } 10 | ): void; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/sortByLevenshtein.ts: -------------------------------------------------------------------------------- 1 | import levenshtein from 'js-levenshtein'; 2 | 3 | export const sortByLevenshtein = ( 4 | elements: T[], 5 | selector: (element: T) => string, 6 | pattern: string 7 | ) => { 8 | return [...elements].sort((a, b) => { 9 | return ( 10 | levenshtein(selector(a), pattern) - levenshtein(selector(b), pattern) 11 | ); 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /src/components/TripSelector/types.ts: -------------------------------------------------------------------------------- 1 | import { AboardStation, AboardTrip } from '@/types/aboard'; 2 | 3 | export type TripProps = { 4 | onClick?: () => void; 5 | requestedStationName?: string; 6 | trip: AboardTrip; 7 | }; 8 | 9 | export type TripSelectorProps = { 10 | onSelect: (value: AboardTrip) => void; 11 | requestedStation?: AboardStation; 12 | trips: AboardTrip[]; 13 | }; 14 | -------------------------------------------------------------------------------- /src/app/dashboard/layout.tsx: -------------------------------------------------------------------------------- 1 | import { AuthGuard } from '@/components/AuthGuard/AuthGuard'; 2 | import '@/styles/globals.css'; 3 | import 'normalize.css'; 4 | 5 | export default async function RootLayout({ 6 | children, 7 | }: { 8 | children: React.ReactNode; 9 | }) { 10 | return {children}; 11 | } 12 | 13 | export const metadata = { 14 | title: 'Dashboard - aboard.at', 15 | }; 16 | -------------------------------------------------------------------------------- /src/hooks/useAppTheme/useAppTheme.ts: -------------------------------------------------------------------------------- 1 | import { CSSProperties, useLayoutEffect } from 'react'; 2 | 3 | const useAppTheme = (accent: CSSProperties['color']) => { 4 | useLayoutEffect(() => { 5 | document.body.style.setProperty('--app-theme', accent ?? ''); 6 | 7 | return () => { 8 | document.body.style.removeProperty('--app-theme'); 9 | }; 10 | }); 11 | }; 12 | 13 | export default useAppTheme; 14 | -------------------------------------------------------------------------------- /src/components/Notifications/Notifications.module.scss: -------------------------------------------------------------------------------- 1 | .base { 2 | position: relative; 3 | display: flex; 4 | 5 | text-decoration: none; 6 | color: var(--slate-12); 7 | 8 | font-size: 1.25rem; 9 | } 10 | 11 | .dot { 12 | position: absolute; 13 | height: 0.375rem; 14 | width: 0.375rem; 15 | background-color: var(--crimson-9); 16 | border-radius: 50%; 17 | right: -0.125rem; 18 | top: -0.125rem; 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/debounce.ts: -------------------------------------------------------------------------------- 1 | export const debounce = void>( 2 | fn: T, 3 | waitFor: number 4 | ) => { 5 | if (typeof window === 'undefined') { 6 | return fn; 7 | } 8 | 9 | let timeoutId: number; 10 | 11 | return (...args: Parameters) => { 12 | window.clearTimeout(timeoutId); 13 | 14 | timeoutId = window.setTimeout(() => fn.call(null, ...args), waitFor); 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /src/app/global-error.jsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as Sentry from '@sentry/nextjs'; 4 | import Error from 'next/error'; 5 | import { useEffect } from 'react'; 6 | 7 | export default function GlobalError({ error }) { 8 | useEffect(() => { 9 | Sentry.captureException(error); 10 | }, [error]); 11 | 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/components/FullscreenLoading/FullscreenLoading.tsx: -------------------------------------------------------------------------------- 1 | import LockBodyScroll from '../LockBodyScroll/LockBodyScroll'; 2 | import styles from './FullscreenLoading.module.scss'; 3 | 4 | const FullscreenLoading = () => { 5 | return ( 6 | <> 7 |
8 |
9 |
10 | 11 | 12 | ); 13 | }; 14 | 15 | export default FullscreenLoading; 16 | -------------------------------------------------------------------------------- /src/components/Overlay/types.ts: -------------------------------------------------------------------------------- 1 | import { CSSProperties, ReactNode } from 'react'; 2 | 3 | export type OverlayProps = { 4 | initialSnapPosition?: number; 5 | isActive: boolean; 6 | isHidden?: boolean; 7 | onBackdropTap?: () => void; 8 | onClose?: () => void; 9 | withBackdrop?: boolean; 10 | }; 11 | 12 | export type OverlayRootProps = OverlayProps & { 13 | children: ReactNode; 14 | className?: string; 15 | style?: CSSProperties; 16 | }; 17 | -------------------------------------------------------------------------------- /src/app/impressum/page.tsx: -------------------------------------------------------------------------------- 1 | export default function Page() { 2 | return ( 3 |
4 |

5 | Mailboxde.com GmbH 6 |
nbank, ID 176434 7 |
Äussere Weberstr. 57 8 |
02763 Zittau, GERMANY 9 |
10 | aboard@sunrises.dev 11 |

12 |
13 | ); 14 | } 15 | 16 | export const metadata = { 17 | title: 'Impressum - aboard.at', 18 | }; 19 | -------------------------------------------------------------------------------- /src/components/Statuses/Statuses.module.scss: -------------------------------------------------------------------------------- 1 | .base { 2 | display: flex; 3 | flex-direction: column; 4 | padding: 0 1rem; 5 | gap: 1rem; 6 | } 7 | 8 | .profilePicture { 9 | border-radius: 9999px; 10 | } 11 | 12 | .status { 13 | padding: 0.5rem 0.75rem; 14 | box-shadow: 0px 0px 2px rgba(0, 0, 0, 0.08), 0px 16px 48px rgba(0, 0, 0, 0.08); 15 | border-radius: 0.5rem; 16 | } 17 | 18 | 19 | .hr { 20 | border-color: var(--current-status-color); 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/hooks/useUmami/useUmami.ts: -------------------------------------------------------------------------------- 1 | const useUmami = () => { 2 | return { 3 | trackEvent: (event: string, data: any) => { 4 | try { 5 | window.umami.track(event, data); 6 | } catch (e) { 7 | console.error(e); 8 | } 9 | }, 10 | simpleEvent: (event: string) => { 11 | try { 12 | window.umami.track(event); 13 | } catch (e) { 14 | console.error(e); 15 | } 16 | }, 17 | }; 18 | }; 19 | 20 | export default useUmami; 21 | -------------------------------------------------------------------------------- /src/app/api/revalidate/route.ts: -------------------------------------------------------------------------------- 1 | import createResponse from '@/utils/api/createResponse'; 2 | import getSafeURLParams from '@/utils/api/getSafeUrlParams'; 3 | import { revalidateTag } from 'next/cache'; 4 | 5 | export async function GET(request: Request) { 6 | const { tag } = getSafeURLParams({ 7 | url: request.url, 8 | requiredParams: ['tag'], 9 | }); 10 | 11 | if (tag) { 12 | revalidateTag(tag); 13 | } 14 | 15 | return createResponse({ 16 | statusCode: 200, 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /src/app/traewelling/dashboard/global/route.ts: -------------------------------------------------------------------------------- 1 | import { TraewellingSdk } from '@/traewelling-sdk'; 2 | import createErrorResponse from '@/utils/api/createErrorResponse'; 3 | import createResponse from '@/utils/api/createResponse'; 4 | 5 | export async function GET(request: Request) { 6 | try { 7 | const data = await TraewellingSdk.dashboard.global(); 8 | return createResponse({ 9 | body: data, 10 | }); 11 | } catch (error) { 12 | return createErrorResponse({ error }); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/components/StatusDetails/types.ts: -------------------------------------------------------------------------------- 1 | import { AboardStatus, AboardStopover, AboardTrip } from '@/types/aboard'; 2 | 3 | export type NextStopoverCountdownProps = { 4 | next: AboardStopover | undefined; 5 | setNext: (value: AboardStopover | undefined) => void; 6 | stopovers: AboardStopover[]; 7 | }; 8 | 9 | export type StatusDetailsProps = { 10 | destinationIndex?: number; 11 | originIndex?: number; 12 | status: AboardStatus; 13 | stopovers?: AboardStopover[]; 14 | trip?: AboardTrip; 15 | }; 16 | -------------------------------------------------------------------------------- /src/components/AuthGuard/AuthGuard.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Login from '@/components/Login/Login'; 4 | import { useSession } from 'next-auth/react'; 5 | import { ReactNode } from 'react'; 6 | 7 | export const AuthGuard = ({ children }: { children: ReactNode }) => { 8 | const { status } = useSession(); 9 | 10 | if (status !== 'loading') { 11 | if (status === 'unauthenticated') { 12 | return ; 13 | } 14 | 15 | return <>{children}; 16 | } 17 | 18 | return
Loading
; 19 | }; 20 | -------------------------------------------------------------------------------- /src/traewelling-sdk/index.ts: -------------------------------------------------------------------------------- 1 | import * as auth from './functions/auth'; 2 | import * as dashboard from './functions/dashboard'; 3 | import * as notifications from './functions/notifications'; 4 | import * as station from './functions/station'; 5 | import * as status from './functions/status'; 6 | import * as trains from './functions/trains'; 7 | import * as user from './functions/user'; 8 | 9 | export const TraewellingSdk = { 10 | auth, 11 | dashboard, 12 | notifications, 13 | station, 14 | status, 15 | trains, 16 | user, 17 | }; 18 | -------------------------------------------------------------------------------- /src/utils/api/createResponse.ts: -------------------------------------------------------------------------------- 1 | type CreateResponseOptions = { 2 | statusCode?: number; 3 | body?: Object | string | number | any; 4 | statusText?: string; 5 | }; 6 | 7 | const createResponse = ({ 8 | statusCode = 200, 9 | statusText, 10 | body, 11 | }: CreateResponseOptions) => { 12 | return new Response(JSON.stringify(body), { 13 | status: statusCode, 14 | statusText: statusText, 15 | headers: { 16 | 'Content-Type': 'application/json', 17 | }, 18 | }); 19 | }; 20 | 21 | export default createResponse; 22 | -------------------------------------------------------------------------------- /src/components/Button/Button.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import styles from './Button.module.scss'; 3 | import { ButtonProps } from './types'; 4 | 5 | const Button = ({ 6 | children, 7 | className, 8 | disabled, 9 | onClick, 10 | variant, 11 | }: ButtonProps) => { 12 | return ( 13 | 20 | ); 21 | }; 22 | 23 | export default Button; 24 | -------------------------------------------------------------------------------- /src/helpers/getStopsAfter.ts: -------------------------------------------------------------------------------- 1 | import { Stop } from '@/traewelling-sdk/types'; 2 | 3 | export const getStopsAfter = ( 4 | plannedDeparture: string, 5 | stationId: string, 6 | stops: Stop[] 7 | ) => { 8 | const after = new Date(plannedDeparture).toISOString(); 9 | 10 | const startingAt = stops.findIndex( 11 | ({ departurePlanned, id }) => 12 | after === new Date(departurePlanned!).toISOString() && 13 | stationId === id.toString() 14 | ); 15 | 16 | return stops.slice(typeof startingAt === 'undefined' ? 0 : startingAt + 1); 17 | }; 18 | -------------------------------------------------------------------------------- /src/app/traewelling/statuses/route.ts: -------------------------------------------------------------------------------- 1 | import { TraewellingSdk } from '@/traewelling-sdk'; 2 | import createErrorResponse from '@/utils/api/createErrorResponse'; 3 | import createResponse from '@/utils/api/createResponse'; 4 | 5 | export async function GET(request: Request) { 6 | try { 7 | // CAREFUL: Authorization is optional for this function! 8 | const data = await TraewellingSdk.status.dashboard(); 9 | 10 | return createResponse({ 11 | body: data, 12 | }); 13 | } catch (error) { 14 | return createErrorResponse({ error }); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/hooks/useDashboard/useDashboard.ts: -------------------------------------------------------------------------------- 1 | import { AboardDashboardResponse } from '@/app/api/dashboard/route'; 2 | import useSWR from 'swr'; 3 | 4 | const fetcher = async (): Promise => { 5 | const response = await fetch('/api/dashboard'); 6 | 7 | if (!response.ok) { 8 | return null; 9 | } 10 | 11 | return await response.json(); 12 | }; 13 | 14 | export const useDashboard = () => { 15 | const { data, isLoading } = useSWR(['/api/dashboard'], ([_]) => fetcher()); 16 | 17 | return { 18 | isLoading, 19 | statuses: data, 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /src/scripts/UmamiScript/UmamiScript.tsx: -------------------------------------------------------------------------------- 1 | import Script from 'next/script'; 2 | 3 | const WEBSITE_IDS = { 4 | production: '13b8972f-5450-4cd9-a024-f81b0def6407', 5 | development: '', 6 | test: '', 7 | }; 8 | 9 | const UmamiScript = () => { 10 | const websiteId = WEBSITE_IDS[process.env.NODE_ENV]; 11 | 12 | if (!websiteId) { 13 | return null; 14 | } 15 | 16 | return ( 17 |