feat: train, news and weather
This commit is contained in:
33
src/App.tsx
33
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 (
|
||||
<>
|
||||
|
||||
<textarea value={JSON.stringify(state)}></textarea>)
|
||||
</>
|
||||
<Container>
|
||||
<Pane>
|
||||
<h1>Next trains</h1>
|
||||
<TrainSchedule {...{ state, dispatch }} />
|
||||
</Pane>
|
||||
<Pane>
|
||||
<h1>News</h1>
|
||||
<NewsWidget {...{ state, dispatch }} />
|
||||
<h1>Weather</h1>
|
||||
<WeatherWidget {...{ state, dispatch }} />
|
||||
</Pane>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
48
src/components/Departure.spec.tsx
Normal file
48
src/components/Departure.spec.tsx
Normal file
@@ -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;
|
||||
`;
|
||||
|
||||
30
src/components/Departure.tsx
Normal file
30
src/components/Departure.tsx
Normal file
@@ -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 <DepartureStyled>
|
||||
<TimeStyled>{timeStr}</TimeStyled>
|
||||
<DestinationStyled>{departure.station}</DestinationStyled>
|
||||
<DelayStyled>{departure.canceled === '1'? 'CANCELED': ''}</DelayStyled>
|
||||
<DelayStyled>{delay}</DelayStyled>
|
||||
<InfoStyled>{departure.left === '1'? "LEFT" : ""}</InfoStyled>
|
||||
<PlatformStyled>Platform: {departure.platform}</PlatformStyled>
|
||||
</DepartureStyled>
|
||||
}
|
||||
120
src/components/NewsArticle.tsx
Normal file
120
src/components/NewsArticle.tsx
Normal file
@@ -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 (
|
||||
<NewsContainer>
|
||||
<NewsHeader>
|
||||
<TitleLink href={article.link} target="_blank" rel="noopener noreferrer">
|
||||
{article.title}
|
||||
</TitleLink>
|
||||
<PubDate>{formattedDate}</PubDate>
|
||||
</NewsHeader>
|
||||
|
||||
<Description>{article.description}</Description>
|
||||
|
||||
<Footer>
|
||||
<CreatorSource>
|
||||
By: <strong>{article.creator || 'N/A'}</strong> from <strong>{article.source_name || 'N/A'}</strong>
|
||||
</CreatorSource>
|
||||
<Keywords>Tags: {keywordString || 'none'}</Keywords>
|
||||
</Footer>
|
||||
</NewsContainer>
|
||||
);
|
||||
};
|
||||
38
src/components/NowTime.tsx
Normal file
38
src/components/NowTime.tsx
Normal file
@@ -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 <TimeStyled>{time}</TimeStyled>;
|
||||
};
|
||||
|
||||
3
src/components/index.tsx
Normal file
3
src/components/index.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './Departure';
|
||||
export * from './NewsArticle';
|
||||
export * from './NowTime';
|
||||
12
src/containers/NewsWidget.tsx
Normal file
12
src/containers/NewsWidget.tsx
Normal file
@@ -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<Action>
|
||||
}
|
||||
export const NewsWidget = ({ state }: NewsDispatchProps ) => {
|
||||
return <p>{state.news?.slice(0,2).map(article => <NewsArticle {...{article}} />)}</p>
|
||||
}
|
||||
24
src/containers/TrainSchedule.tsx
Normal file
24
src/containers/TrainSchedule.tsx
Normal file
@@ -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<Action>,
|
||||
state: State,
|
||||
}
|
||||
|
||||
|
||||
export const TrainSchedule = ({ state } : TrainScheduleProps) => {
|
||||
|
||||
return <>
|
||||
{state.liveboard?.departures.departure.map(departure => <Departure {...{departure}} />)}
|
||||
<NowTime />
|
||||
</>
|
||||
}
|
||||
99
src/containers/WeatherWidget.tsx
Normal file
99
src/containers/WeatherWidget.tsx
Normal file
@@ -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<Action>;
|
||||
};
|
||||
|
||||
export const WeatherWidget = ({ state }: WeatherDispatchProps) => {
|
||||
const weather = state.weather?.current;
|
||||
if (!weather) return null;
|
||||
|
||||
return (
|
||||
<WeatherContainer>
|
||||
<WeatherHeader>
|
||||
<img src={weather.condition.icon} alt={weather.condition.text} />
|
||||
<h2>{weather.condition.text}</h2>
|
||||
</WeatherHeader>
|
||||
|
||||
<WeatherDetails>
|
||||
<div className="weather-item">
|
||||
<WiThermometer />
|
||||
<span>{weather.temp_c}°C</span>
|
||||
</div>
|
||||
<div className="weather-item">
|
||||
<WiRaindrops />
|
||||
<span>{weather.precip_mm} mm</span>
|
||||
</div>
|
||||
<div className="weather-item">
|
||||
<WiStrongWind />
|
||||
<span>{weather.gust_kph} kph</span>
|
||||
</div>
|
||||
</WeatherDetails>
|
||||
</WeatherContainer>
|
||||
);
|
||||
};
|
||||
3
src/containers/index.tsx
Normal file
3
src/containers/index.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './TrainSchedule';
|
||||
export * from './WeatherWidget';
|
||||
export * from './NewsWidget';
|
||||
@@ -1 +1,3 @@
|
||||
export * from './useLoadTrainSchedule';
|
||||
export * from './useNewsApi';
|
||||
export * from './useWeatherApi';
|
||||
|
||||
@@ -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<Action>) => {
|
||||
(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}))
|
||||
}
|
||||
|
||||
25
src/hooks/useNewsApi.tsx
Normal file
25
src/hooks/useNewsApi.tsx
Normal file
@@ -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<Action>,
|
||||
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}));
|
||||
}
|
||||
})()
|
||||
}, [])
|
||||
}
|
||||
25
src/hooks/useWeatherApi.tsx
Normal file
25
src/hooks/useWeatherApi.tsx
Normal file
@@ -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<Action>,
|
||||
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 }));
|
||||
}
|
||||
})()
|
||||
}, [])
|
||||
}
|
||||
@@ -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<LoadNews, "type">) => ({
|
||||
type: loadNews,
|
||||
...args
|
||||
} as LoadNews),
|
||||
loadNewsSuccess: (args: Omit<LoadNewsSuccess, "type">) => ({
|
||||
type: loadNewsSuccess,
|
||||
...args
|
||||
} as LoadNewsSuccess),
|
||||
loadNewsError: (args: Omit<LoadNewsError, "type">) => ({
|
||||
type: loadNewsError,
|
||||
...args
|
||||
} as LoadNewsError),
|
||||
loadWeather: (args: Omit<LoadWeather, "type">) => ({
|
||||
type: loadWeather,
|
||||
...args
|
||||
} as LoadWeather),
|
||||
loadWeatherSuccess: (args: Omit<LoadWeatherSuccess, "type">) => ({
|
||||
type: loadWeatherSuccess,
|
||||
...args
|
||||
} as LoadWeatherSuccess),
|
||||
loadWeatherError: (args: Omit<LoadWeatherError, "type">) => ({
|
||||
type: loadWeatherError,
|
||||
...args
|
||||
} as LoadWeatherError),
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
2
src/styles.tsx
Normal file
2
src/styles.tsx
Normal file
@@ -0,0 +1,2 @@
|
||||
export const accentColor = '#074c87'
|
||||
export const accentColor2 = '#032d5d'
|
||||
29
src/types/article.tsx
Normal file
29
src/types/article.tsx
Normal file
@@ -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;
|
||||
}
|
||||
57
src/types/liveboard.tsx
Normal file
57
src/types/liveboard.tsx
Normal file
@@ -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;
|
||||
}
|
||||
57
src/types/weather.tsx
Normal file
57
src/types/weather.tsx
Normal file
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user