From b2a5031056e65de8f29fd976b0dd0c05244a66f0 Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Thu, 30 Oct 2025 23:19:03 +0100 Subject: [PATCH] feat: train, news and weather --- src/App.tsx | 33 +++++++- src/components/Departure.spec.tsx | 48 ++++++++++++ src/components/Departure.tsx | 30 ++++++++ src/components/NewsArticle.tsx | 120 +++++++++++++++++++++++++++++ src/components/NowTime.tsx | 38 +++++++++ src/components/index.tsx | 3 + src/containers/NewsWidget.tsx | 12 +++ src/containers/TrainSchedule.tsx | 24 ++++++ src/containers/WeatherWidget.tsx | 99 ++++++++++++++++++++++++ src/containers/index.tsx | 3 + src/hooks/index.tsx | 2 + src/hooks/useLoadTrainSchedule.tsx | 24 ++++-- src/hooks/useNewsApi.tsx | 25 ++++++ src/hooks/useWeatherApi.tsx | 25 ++++++ src/state/actions/actions.ts | 36 +++++++++ src/state/actions/consts.ts | 7 ++ src/state/actions/types.ts | 47 ++++++++++- src/state/initialState.ts | 12 ++- src/state/reducer.ts | 84 +++++++++++++++----- src/state/state.ts | 20 +++-- src/styles.tsx | 2 + src/types/article.tsx | 29 +++++++ src/types/liveboard.tsx | 57 ++++++++++++++ src/types/weather.tsx | 57 ++++++++++++++ 24 files changed, 796 insertions(+), 41 deletions(-) create mode 100644 src/components/Departure.spec.tsx create mode 100644 src/components/Departure.tsx create mode 100644 src/components/NewsArticle.tsx create mode 100644 src/components/NowTime.tsx create mode 100644 src/components/index.tsx create mode 100644 src/containers/NewsWidget.tsx create mode 100644 src/containers/TrainSchedule.tsx create mode 100644 src/containers/WeatherWidget.tsx create mode 100644 src/containers/index.tsx create mode 100644 src/hooks/useNewsApi.tsx create mode 100644 src/hooks/useWeatherApi.tsx create mode 100644 src/styles.tsx create mode 100644 src/types/article.tsx create mode 100644 src/types/liveboard.tsx create mode 100644 src/types/weather.tsx diff --git a/src/App.tsx b/src/App.tsx index 2144853..652da90 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,18 +5,43 @@ import { useLoadTrainSchedule } from './hooks/useLoadTrainSchedule' import { initialState, reducer } from './state' import './App.css' +import { NewsWidget, TrainSchedule, WeatherWidget } from './containers'; +import {useNewsApi, useWeatherApi} from './hooks'; +import styled from 'styled-components'; + +const Container = styled.div` + display: flex; + height: 100vh; + width: 100vw; + gap: 2rem; +`; + +const Pane = styled.div` + max-width: 45vw; +`; + function App() { const [state, dispatch] = useReducer( reducer, initialState,); useLoadTrainSchedule(state, dispatch); + useNewsApi({state, dispatch}); + useWeatherApi({state, dispatch}); return ( - <> - - ) - + + +

Next trains

+ +
+ +

News

+ +

Weather

+ +
+
) } diff --git a/src/components/Departure.spec.tsx b/src/components/Departure.spec.tsx new file mode 100644 index 0000000..334513b --- /dev/null +++ b/src/components/Departure.spec.tsx @@ -0,0 +1,48 @@ +import styled from "styled-components"; + +export const DepartureStyled = styled.div` + display: flex; + justify-content: space-between; + align-items: stretch; + background: #0b0c10; + color: #fff; + padding: 1rem 1.5rem; + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0,0,0,0.2); + font-family: "Inter", sans-serif; +`; + +export const TimeStyled = styled.div` + font-size: 2rem; + font-weight: 200; + flex: 0 0 auto; + display: flex; + align-items: center; + padding-right: 1em; +`; + +export const InfoStyled = styled.div` + flex: 1; + display: flex; + flex-direction: column; + justify-content: space-between; + text-align: right; + padding-right: 1em; +`; + +export const DestinationStyled = styled.div` + font-size: 1.2rem; + font-weight: 600; +`; + +export const DelayStyled = styled.div` + font-size: 1rem; + color: #ff4444; + margin-right: 8px; +`; + +export const PlatformStyled = styled.div` + font-size: 0.9rem; + opacity: 0.8; +`; + diff --git a/src/components/Departure.tsx b/src/components/Departure.tsx new file mode 100644 index 0000000..e7af0d2 --- /dev/null +++ b/src/components/Departure.tsx @@ -0,0 +1,30 @@ +import type { DepartureType } from "../types/liveboard" +import { + DelayStyled, + DepartureStyled, + DestinationStyled, + InfoStyled, + PlatformStyled, + TimeStyled +} from './Departure.spec'; + + +export const Departure = ({ departure }: { departure: DepartureType }) => { + + const date = new Date(parseInt(departure.time)*1000); + + const delayInt = Math.round(parseInt(departure.delay) / 60); + const delay = delayInt > 0? `+${delayInt}`: `` + const pad = (n:number) => n <= 9? `0${n}`: n; + + const timeStr = `${pad(date.getHours())}:${pad(date.getMinutes())}`; + + return + {timeStr} + {departure.station} + {departure.canceled === '1'? 'CANCELED': ''} + {delay} + {departure.left === '1'? "LEFT" : ""} + Platform: {departure.platform} + +} diff --git a/src/components/NewsArticle.tsx b/src/components/NewsArticle.tsx new file mode 100644 index 0000000..1cbca0a --- /dev/null +++ b/src/components/NewsArticle.tsx @@ -0,0 +1,120 @@ +import styled from 'styled-components'; +import type {Article} from '../types/article'; +import {accentColor} from '../styles'; + +export const NewsContainer = styled.div` + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: stretch; + background: #0b0c10; + color: #fff; + padding: 1rem 1.5rem; + border-radius: 12px; + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4); + font-family: "Inter", sans-serif; + max-width: 600px; + margin: 1rem auto; +`; + +export const NewsHeader = styled.div` + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 0.75rem; +`; + +export const TitleLink = styled.a` + font-size: 1.25rem; + font-weight: 700; + color: ${accentColor}; + text-decoration: none; + margin-right: 1rem; + + &:hover { + text-decoration: underline; + color: ${accentColor}; + } +`; + +export const PubDate = styled.span` + font-size: 0.8rem; + color: #c5c6c7; + white-space: nowrap; +`; + +export const Description = styled.p` + font-size: 1rem; + color: #a7a7a7; + line-height: 1.4; + margin-bottom: 1rem; +`; + +export const Footer = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + font-size: 0.85rem; + padding-top: 0.5rem; + border-top: 1px solid #1f2833; +`; + +export const CreatorSource = styled.span` + color: ${accentColor}; + + strong { + font-weight: 600; + color: #fff; + } +`; + +export const Keywords = styled.span` + color: ${accentColor}; + font-style: italic; +`; + + +type NewsArticleProps = { + article: Article +} + +export const NewsArticle = ({ article }: NewsArticleProps) => { + if (!article) return null; + + // Function to format the date + const formatDate = (isoString: string) => { + try { + const date = new Date(isoString); + return date.toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + }); + } catch { + return 'Unknown Date'; + } + }; + + const formattedDate = formatDate(article.pubDate); + const keywordString = Array.isArray(article.keywords) ? article.keywords.join(', ') : article.keywords; + + return ( + + + + {article.title} + + {formattedDate} + + + {article.description} + +
+ + By: {article.creator || 'N/A'} from {article.source_name || 'N/A'} + + Tags: {keywordString || 'none'} +
+
+ ); +}; diff --git a/src/components/NowTime.tsx b/src/components/NowTime.tsx new file mode 100644 index 0000000..dbd4541 --- /dev/null +++ b/src/components/NowTime.tsx @@ -0,0 +1,38 @@ +import { useEffect, useState } from "react"; + +import styled from "styled-components"; + + +export const TimeStyled = styled.div` + display: flex; + justify-content: space-between; + align-items: stretch; + background: #0b0c10; + color: #fff; + padding: 1rem 1.5rem; + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0,0,0,0.2); + + font-size: 4rem; + font-weight: 200; + flex: 0 0 auto; + display: flex; + align-items: center; + justify-content: center; + padding-right: 1em; +`; + +export const NowTime = () => { + const [time, setTime] = useState(new Date().toLocaleTimeString("fr-FR")); + + useEffect(() => { + const timer = setInterval(() => { + setTime(new Date().toLocaleTimeString("fr-FR")); + }, 1000); + + return () => clearInterval(timer); + }, []); + + return {time}; +}; + diff --git a/src/components/index.tsx b/src/components/index.tsx new file mode 100644 index 0000000..3bda937 --- /dev/null +++ b/src/components/index.tsx @@ -0,0 +1,3 @@ +export * from './Departure'; +export * from './NewsArticle'; +export * from './NowTime'; diff --git a/src/containers/NewsWidget.tsx b/src/containers/NewsWidget.tsx new file mode 100644 index 0000000..8618652 --- /dev/null +++ b/src/containers/NewsWidget.tsx @@ -0,0 +1,12 @@ +import type { Dispatch } from "react" +import type { Action, State } from "../state" + +import { NewsArticle } from "../components" + +type NewsDispatchProps = { + state: State, + dispatch: Dispatch +} +export const NewsWidget = ({ state }: NewsDispatchProps ) => { + return

{state.news?.slice(0,2).map(article => )}

+} diff --git a/src/containers/TrainSchedule.tsx b/src/containers/TrainSchedule.tsx new file mode 100644 index 0000000..7c7d84c --- /dev/null +++ b/src/containers/TrainSchedule.tsx @@ -0,0 +1,24 @@ +import styled from 'styled-components'; + +import { type Dispatch } from 'react'; +import { + type State, + type Action +} from '../state'; + +import { Departure } from '../components'; +import { NowTime } from '../components/NowTime'; + +type TrainScheduleProps = { + dispatch: Dispatch, + state: State, +} + + +export const TrainSchedule = ({ state } : TrainScheduleProps) => { + + return <> + {state.liveboard?.departures.departure.map(departure => )} + + +} diff --git a/src/containers/WeatherWidget.tsx b/src/containers/WeatherWidget.tsx new file mode 100644 index 0000000..338c5c3 --- /dev/null +++ b/src/containers/WeatherWidget.tsx @@ -0,0 +1,99 @@ +import type { Dispatch } from "react" +import type { Action, State } from "../state" +import styled from "styled-components"; +import { WiStrongWind, WiRaindrops, WiThermometer } from "react-icons/wi"; +import {accentColor} from "../styles"; + + +export const WeatherContainer = styled.div` + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: stretch; + background: #0b0c10; + color: #fff; + padding: 1rem 1.5rem; + border-radius: 12px; + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4); + font-family: "Inter", sans-serif; + max-width: 600px; + margin: 1rem auto; +`; + + +const WeatherHeader = styled.div` + display: flex; + align-items: center; + gap: 1rem; + + img { + width: 64px; + height: 64px; + } + + h2 { + font-size: 1.5rem; + font-weight: 600; + margin: 0; + } +`; + +const WeatherDetails = styled.div` + display: flex; + justify-content: space-around; + align-items: center; + margin-top: 1rem; + font-size: 4rm; + + .weather-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.25rem; + font-size: 1rem; + + svg { + font-size: 2rem; + color: ${accentColor}; + } + + span { + font-size: 0.9rem; + opacity: 0.8; + } + } +`; + +type WeatherDispatchProps = { + state: State; + dispatch: Dispatch; +}; + +export const WeatherWidget = ({ state }: WeatherDispatchProps) => { + const weather = state.weather?.current; + if (!weather) return null; + + return ( + + + {weather.condition.text} +

{weather.condition.text}

+
+ + +
+ + {weather.temp_c}°C +
+
+ + {weather.precip_mm} mm +
+
+ + {weather.gust_kph} kph +
+
+
+ ); +}; diff --git a/src/containers/index.tsx b/src/containers/index.tsx new file mode 100644 index 0000000..740a816 --- /dev/null +++ b/src/containers/index.tsx @@ -0,0 +1,3 @@ +export * from './TrainSchedule'; +export * from './WeatherWidget'; +export * from './NewsWidget'; diff --git a/src/hooks/index.tsx b/src/hooks/index.tsx index d784905..987967f 100644 --- a/src/hooks/index.tsx +++ b/src/hooks/index.tsx @@ -1 +1,3 @@ export * from './useLoadTrainSchedule'; +export * from './useNewsApi'; +export * from './useWeatherApi'; diff --git a/src/hooks/useLoadTrainSchedule.tsx b/src/hooks/useLoadTrainSchedule.tsx index 349f6b6..90604f0 100644 --- a/src/hooks/useLoadTrainSchedule.tsx +++ b/src/hooks/useLoadTrainSchedule.tsx @@ -1,9 +1,8 @@ import { useEffect, type Dispatch } from "react" import { actions, type Action, type State } from "../state" -import type { Data } from "../types"; - import axios from 'axios'; +import type {LiveBoard} from "../types/liveboard"; const irailApiUrl = 'https://api.irail.be'; @@ -12,9 +11,24 @@ export const useLoadTrainSchedule = (_: State, dispatch: Dispatch) => { (async () => { try { dispatch(actions.loadTrainSchedule({})) - const answer = await axios.get(`${irailApiUrl}/stations?format=json&lang=en`) - const { station } = answer.data as Data; - dispatch(actions.loadTrainScheduleSuccess({ stations: station })) + const now = new Date(); + + const pad = (num: number) => num.toString().padStart(2, '0'); + + const day = pad(now.getDate()); + const month = pad(now.getMonth() + 1); + const year = now.getFullYear().toString().slice(-2); + const date = `${day}${month}${year}` + + const hours = pad(now.getHours()); + const minutes = pad(now.getMinutes()); + const time = `${hours}${minutes}` + + const answer = await axios.get(`${irailApiUrl}/liveboard?station=Nivelles&date=${date}&time=${time}&format=json&lang=en&alerts=true`) + + const liveboard = answer.data as LiveBoard; + + dispatch(actions.loadTrainScheduleSuccess({ liveboard })) } catch(error) { dispatch(actions.loadTrainScheduleError({ error: error as Error})) } diff --git a/src/hooks/useNewsApi.tsx b/src/hooks/useNewsApi.tsx new file mode 100644 index 0000000..ac52c01 --- /dev/null +++ b/src/hooks/useNewsApi.tsx @@ -0,0 +1,25 @@ +import { useEffect, type Dispatch } from "react" +import { actions, type Action, type State } from "../state" +import type {Article} from "../types/article" + +const newsUrl = 'https://newsdata.io/api/1/latest?apikey=pub_26997f21bb174c7cbab59b3651533429&q=nivelle &country=be' + +export type UseNewsApiProps = { + dispatch: Dispatch, + state: State +} + +export const useNewsApi = ({dispatch}: UseNewsApiProps) => { + useEffect(() => { + (async () => { + try { + dispatch(actions.loadNews({})); + const answer = await fetch(newsUrl); + const { results: news } = await answer.json() as { results: Article[] } + dispatch(actions.loadNewsSuccess({ news })); + } catch(error) { + dispatch(actions.loadNewsError({ error: error as Error})); + } + })() + }, []) +} diff --git a/src/hooks/useWeatherApi.tsx b/src/hooks/useWeatherApi.tsx new file mode 100644 index 0000000..9978813 --- /dev/null +++ b/src/hooks/useWeatherApi.tsx @@ -0,0 +1,25 @@ +import { useEffect, type Dispatch } from "react" +import { actions, type Action, type State } from "../state" +import type {WeatherData} from "../types/weather"; + +const weatherApiUrl = `http://api.weatherapi.com/v1/current.json?key=176d6e98c8894466aa6205455253010&q=Nivelles&aqi=no`; + +export type UseWeatherApiProps = { + dispatch: Dispatch, + state: State +} + +export const useWeatherApi = ({ dispatch}: UseWeatherApiProps) => { + useEffect(() => { + (async () => { + try { + dispatch(actions.loadWeather({})); + const answer = await fetch(weatherApiUrl); + const weather = await answer.json() as WeatherData; + dispatch(actions.loadWeatherSuccess({ weather })); + } catch(error) { + dispatch(actions.loadWeatherError({ error: error as Error })); + } + })() + }, []) +} diff --git a/src/state/actions/actions.ts b/src/state/actions/actions.ts index 6fb5778..9271299 100644 --- a/src/state/actions/actions.ts +++ b/src/state/actions/actions.ts @@ -2,12 +2,24 @@ import { loadTrainSchedule, loadTrainScheduleSuccess, loadTrainScheduleError, + loadWeatherError, + loadWeatherSuccess, + loadWeather, + loadNewsError, + loadNewsSuccess, + loadNews, } from './consts' import { type LoadTrainSchedule, type LoadTrainScheduleSuccess, type LoadTrainScheduleError, + type LoadWeatherError, + type LoadWeatherSuccess, + type LoadWeather, + type LoadNewsError, + type LoadNewsSuccess, + type LoadNews, } from './types'; export const actions = { @@ -23,4 +35,28 @@ export const actions = { type: loadTrainScheduleError, ...args } as LoadTrainScheduleError), + loadNews: (args: Omit) => ({ + type: loadNews, + ...args + } as LoadNews), + loadNewsSuccess: (args: Omit) => ({ + type: loadNewsSuccess, + ...args + } as LoadNewsSuccess), + loadNewsError: (args: Omit) => ({ + type: loadNewsError, + ...args + } as LoadNewsError), + loadWeather: (args: Omit) => ({ + type: loadWeather, + ...args + } as LoadWeather), + loadWeatherSuccess: (args: Omit) => ({ + type: loadWeatherSuccess, + ...args + } as LoadWeatherSuccess), + loadWeatherError: (args: Omit) => ({ + type: loadWeatherError, + ...args + } as LoadWeatherError), } diff --git a/src/state/actions/consts.ts b/src/state/actions/consts.ts index 0b1ef07..86c2339 100644 --- a/src/state/actions/consts.ts +++ b/src/state/actions/consts.ts @@ -4,3 +4,10 @@ export const loadTrainSchedule: ActionType = 'loadTrainSchedule'; export const loadTrainScheduleSuccess: ActionType = 'loadTrainScheduleSuccess'; export const loadTrainScheduleError: ActionType = 'loadTrainScheduleError'; +export const loadNews: ActionType = 'loadNews'; +export const loadNewsSuccess: ActionType = 'loadNewsSuccess'; +export const loadNewsError: ActionType = 'loadNewsError'; + +export const loadWeather: ActionType = 'loadWeather'; +export const loadWeatherSuccess: ActionType = 'loadWeatherSuccess'; +export const loadWeatherError: ActionType = 'loadWeatherError'; diff --git a/src/state/actions/types.ts b/src/state/actions/types.ts index 77d8b5c..3efce15 100644 --- a/src/state/actions/types.ts +++ b/src/state/actions/types.ts @@ -1,7 +1,19 @@ export type ActionType = string; -import type { Station } from '../../types'; -import { loadTrainSchedule, loadTrainScheduleError, loadTrainScheduleSuccess } from './consts' +import type {Article} from '../../types/article'; +import type { LiveBoard } from '../../types/liveboard'; +import type {WeatherData} from '../../types/weather'; +import { + loadTrainSchedule, + loadTrainScheduleError, + loadTrainScheduleSuccess, + loadNews, + loadNewsError, + loadNewsSuccess, + loadWeather, + loadWeatherError, + loadWeatherSuccess, +} from './consts' export type Action = { type: ActionType @@ -13,7 +25,7 @@ export type LoadTrainSchedule = { export type LoadTrainScheduleSuccess = { type: typeof loadTrainScheduleSuccess, - stations: Station[] + liveboard: LiveBoard } export type LoadTrainScheduleError = { @@ -21,3 +33,32 @@ export type LoadTrainScheduleError = { error: Error } +export type LoadNews = { + type: typeof loadNews, +} + +export type LoadNewsSuccess = { + type: typeof loadNewsSuccess, + news: Article[] +} + +export type LoadNewsError = { + type: typeof loadNewsError, + error: Error +} + + +export type LoadWeather = { + type: typeof loadWeather, +} + +export type LoadWeatherSuccess = { + type: typeof loadWeatherSuccess, + weather: WeatherData +} + +export type LoadWeatherError = { + type: typeof loadWeatherError, + error: Error +} + diff --git a/src/state/initialState.ts b/src/state/initialState.ts index f3bf7f2..f4ae447 100644 --- a/src/state/initialState.ts +++ b/src/state/initialState.ts @@ -1,7 +1,13 @@ import { type State } from "./state"; export const initialState: State = { - stations: undefined, - error: undefined, - loading: false, + liveboard: undefined, + trainScheduleError: undefined, + trainScheduleLoading: false, + news: undefined, + newsError: undefined, + newsLoading: false, + weather: undefined, + weatherError: undefined, + weatherLoading: false, }; diff --git a/src/state/reducer.ts b/src/state/reducer.ts index 379fefe..dd0c8ab 100644 --- a/src/state/reducer.ts +++ b/src/state/reducer.ts @@ -5,52 +5,96 @@ import { loadTrainSchedule, loadTrainScheduleError, loadTrainScheduleSuccess, + loadNews, + loadNewsSuccess, + loadNewsError, + loadWeatherError, + loadWeatherSuccess, + loadWeather, type LoadTrainScheduleError, type LoadTrainScheduleSuccess, + type LoadWeatherError, + type LoadWeatherSuccess, + type LoadNewsError, + type LoadNewsSuccess, } from './actions'; -//export const reducerInner = (state: State, action: Action): State => { -export const reducer = (state: State, action: Action): State => { +export const reducerInner = (state: State, action: Action): State => { +//export const reducer = (state: State, action: Action): State => { if(action.type === loadTrainSchedule) { return { ...state, - error: undefined, - loading: true, + trainScheduleError: undefined, + trainScheduleLoading: true, } } else if(action.type === loadTrainScheduleSuccess) { return { ...state, - stations: (action as LoadTrainScheduleSuccess).stations, - loading: false, - + liveboard: (action as LoadTrainScheduleSuccess).liveboard, + trainScheduleLoading: false, } } else if(action.type === loadTrainScheduleError) { - return { ...state, - error: (action as LoadTrainScheduleError).error, - loading: false, + trainScheduleError: (action as LoadTrainScheduleError).error, + trainScheduleLoading: false, + } + } + if(action.type === loadNews) { + return { + ...state, + newsError: undefined, + newsLoading: true, + } + } + else if(action.type === loadNewsSuccess) { + return { + ...state, + news: (action as LoadNewsSuccess).news, + newsLoading: false, + } + } + else if(action.type === loadNewsError) { + return { + ...state, + newsError: (action as LoadNewsError).error, + newsLoading: false, + } + } + if(action.type === loadWeather) { + return { + ...state, + weatherError: undefined, + weatherLoading: true, + } + } + else if(action.type === loadWeatherSuccess) { + return { + ...state, + weather: (action as LoadWeatherSuccess).weather, + weatherLoading: false, + } + } + else if(action.type === loadWeatherError) { + return { + ...state, + weatherError: (action as LoadWeatherError).error, + weatherLoading: false, } } return state; } -/* export const reducer = (state: State, action: Action) => { - if(action.type !== tick) { - console.log(`MD - ${action.type}`); - console.log({action}) - console.log({state}) - } + console.log(`TS - ${action.type}`); + console.log({action}) + console.log({state}) const newState = reducerInner(state, action); - if(action.type !== tick) { - console.log({newState}) - } + console.log({newState}) return newState; } -*/ diff --git a/src/state/state.ts b/src/state/state.ts index 82b2a25..487cea7 100644 --- a/src/state/state.ts +++ b/src/state/state.ts @@ -1,9 +1,17 @@ -import type { Station } from "../types"; +import type { Article } from "../types/article"; +import type { LiveBoard } from "../types/liveboard"; +import type { WeatherData } from "../types/weather"; export type State = { - loading: boolean, - stations: Station[] | undefined, - error: Error | undefined, + trainScheduleLoading: boolean, + liveboard: LiveBoard | undefined, + trainScheduleError: Error | undefined, + + weather: WeatherData | undefined, + weatherLoading: boolean, + weatherError: Error | undefined, + + news: Article[] | undefined, + newsLoading: boolean, + newsError: Error | undefined, } - - diff --git a/src/styles.tsx b/src/styles.tsx new file mode 100644 index 0000000..5c421b2 --- /dev/null +++ b/src/styles.tsx @@ -0,0 +1,2 @@ +export const accentColor = '#074c87' +export const accentColor2 = '#032d5d' diff --git a/src/types/article.tsx b/src/types/article.tsx new file mode 100644 index 0000000..56d6131 --- /dev/null +++ b/src/types/article.tsx @@ -0,0 +1,29 @@ +export interface Article { + article_id: string; + title: string; + link: string; + keywords: string[]; + creator: string[]; + description: string; + content: string; + pubDate: string; + pubDateTZ: string; + image_url: string; + video_url: string | null; + source_id: string; + source_name: string; + source_priority: number; + source_url: string; + source_icon: string; + language: string; + country: string[]; + category: string[]; + sentiment: string; + sentiment_stats: string; + ai_tag: string; + ai_region: string; + ai_org: string; + ai_summary: string; + ai_content: string; + duplicate: boolean; +} diff --git a/src/types/liveboard.tsx b/src/types/liveboard.tsx new file mode 100644 index 0000000..804ab59 --- /dev/null +++ b/src/types/liveboard.tsx @@ -0,0 +1,57 @@ +interface StationInfo { + '@id': string; + id: string; + name: string; + locationX: string; + locationY: string; + standardname: string; +} + +interface VehicleInfo { + name: string; + shortname: string; + number: string; + type: string; + locationX: string; + locationY: string; + '@id': string; +} + +interface PlatformInfo { + name: string; + normal: string; +} +interface Occupancy { + '@id': string; + name: 'unknown' | 'low' | 'medium' | 'high'; +} + +export interface DepartureType { + id: string; + station: string; + stationinfo: StationInfo; + time: string; + delay: string; + canceled: '0' | '1'; + left: '0' | '1'; + isExtra: '0' | '1'; + vehicle: string; + vehicleinfo: VehicleInfo; + platform: string; + platforminfo: PlatformInfo; + occupancy: Occupancy; + departureConnection: string; +} + +interface Departures { + number: string; + departure: DepartureType[]; +} + +export interface LiveBoard { + version: string; + timestamp: string; + station: string; + stationinfo: StationInfo; + departures: Departures; +} diff --git a/src/types/weather.tsx b/src/types/weather.tsx new file mode 100644 index 0000000..8ec0195 --- /dev/null +++ b/src/types/weather.tsx @@ -0,0 +1,57 @@ +export interface Condition { + text: string; + icon: string; + code: number; +} + +export interface CurrentWeather { + last_updated_epoch: number; + last_updated: string; + temp_c: number; + temp_f: number; + is_day: number; + condition: Condition; + wind_mph: number; + wind_kph: number; + wind_degree: number; + wind_dir: string; + pressure_mb: number; + pressure_in: number; + precip_mm: number; + precip_in: number; + humidity: number; + cloud: number; + feelslike_c: number; + feelslike_f: number; + windchill_c: number; + windchill_f: number; + heatindex_c: number; + heatindex_f: number; + dewpoint_c: number; + dewpoint_f: number; + vis_km: number; + vis_miles: number; + uv: number; + gust_mph: number; + gust_kph: number; + short_rad: number; + diff_rad: number; + dni: number; + gti: number; +} + +export interface Location { + name: string; + region: string; + country: string; + lat: number; + lon: number; + tz_id: string; + localtime_epoch: number; + localtime: string; +} + +export interface WeatherData { + location: Location; + current: CurrentWeather; +}