From 0ee7532a30ed5e2aec54ec5ea50c89cac82c093f Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Fri, 31 Oct 2025 14:29:46 +0100 Subject: [PATCH] feat: indicators --- src/App.tsx | 4 +- src/components/Indicators.tsx | 93 ++++++++++++++++++++++++++++++ src/components/NewsArticle.tsx | 1 + src/components/NowTime.tsx | 1 + src/components/Pager.tsx | 64 ++++++++++++++++++++ src/containers/NewsWidget.tsx | 38 +++++++++++- src/containers/TrainSchedule.tsx | 30 ++++++++-- src/containers/WeatherWidget.tsx | 49 +++++++++------- src/hooks/useLoadTrainSchedule.tsx | 34 +++++++---- src/state/actions/types.ts | 4 +- src/state/initialState.ts | 2 +- src/state/reducer.ts | 2 +- src/state/state.ts | 4 +- 13 files changed, 278 insertions(+), 48 deletions(-) create mode 100644 src/components/Indicators.tsx create mode 100644 src/components/Pager.tsx diff --git a/src/App.tsx b/src/App.tsx index 652da90..a99ccef 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -36,10 +36,10 @@ function App() { -

News

-

Weather

+

News

+
) diff --git a/src/components/Indicators.tsx b/src/components/Indicators.tsx new file mode 100644 index 0000000..30d6a0c --- /dev/null +++ b/src/components/Indicators.tsx @@ -0,0 +1,93 @@ + +import type {State} from "../state"; +import {filterTrainHour} from "../containers"; +import {mean} from "lodash"; + +import styled from "styled-components"; +import { Clock, AlertTriangle, XCircle } from "lucide-react"; +import {accentColor} from "../styles"; + +export const IndicatorStyled = styled.div` + display: flex; + justify-content: space-around; + align-items: center; + background: #0b0c10; + color: #fff; + padding: 1.5rem 2rem; + border-radius: 16px; + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35); + font-family: 'Inter', sans-serif; +`; + +const StatBlock = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + flex: 1; + + & svg { + margin-bottom: 0.3em; + color: ${accentColor}; + } + + .value { + font-weight: 600; + line-height: 1.1; + } + + .label { + font-size: 1em; + opacity: 0.7; + margin-top: 0.3em; + } +`; + +const formatTime = (seconds?: number) => { + if (seconds == null || isNaN(seconds)) return "–"; + const mins = Math.floor(seconds / 60); + const secs = Math.round(seconds % 60); + return `${mins}m ${secs}s`; +}; + +const formatPercent = (num?: number) => { + if (num == null || isNaN(num)) return "–"; + return `${(num * 100).toFixed(1)}%`; +}; + +export const Indicator = ({ state }: { state: State }) => { + const inOneHour = state.departures?.filter(filterTrainHour(60)); + const inThreeHour = state.departures?.filter(filterTrainHour(-60 * 3)); + + const averageDelayGeneral = mean(inOneHour?.map(d => parseInt(d.delay))); + const averageDelayedOnly = mean( + inOneHour?.filter(d => parseInt(d.delay) !== 0)?.map(d => parseInt(d.delay)) + ); + + const cancelled = + (inThreeHour?.filter(d => d?.canceled === '1')?.length || 0) / + (inThreeHour?.length || 1); + + return ( + + + +
{formatTime(averageDelayGeneral)}
+
Avg Delay (all)
+
+ + + +
{formatTime(averageDelayedOnly)}
+
Avg Delay (delayed only)
+
+ + + +
{formatPercent(cancelled)}
+
Cancelled
+
+
+ ); +}; + diff --git a/src/components/NewsArticle.tsx b/src/components/NewsArticle.tsx index 1cbca0a..0ce3fbd 100644 --- a/src/components/NewsArticle.tsx +++ b/src/components/NewsArticle.tsx @@ -108,6 +108,7 @@ export const NewsArticle = ({ article }: NewsArticleProps) => { {article.description} +