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 { initialState, reducer } from './state'
|
||||||
|
|
||||||
import './App.css'
|
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() {
|
function App() {
|
||||||
|
|
||||||
const [state, dispatch] = useReducer( reducer, initialState,);
|
const [state, dispatch] = useReducer( reducer, initialState,);
|
||||||
|
|
||||||
useLoadTrainSchedule(state, dispatch);
|
useLoadTrainSchedule(state, dispatch);
|
||||||
|
useNewsApi({state, dispatch});
|
||||||
|
useWeatherApi({state, dispatch});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Container>
|
||||||
|
<Pane>
|
||||||
<textarea value={JSON.stringify(state)}></textarea>)
|
<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 './useLoadTrainSchedule';
|
||||||
|
export * from './useNewsApi';
|
||||||
|
export * from './useWeatherApi';
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { useEffect, type Dispatch } from "react"
|
import { useEffect, type Dispatch } from "react"
|
||||||
import { actions, type Action, type State } from "../state"
|
import { actions, type Action, type State } from "../state"
|
||||||
|
|
||||||
import type { Data } from "../types";
|
|
||||||
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import type {LiveBoard} from "../types/liveboard";
|
||||||
|
|
||||||
const irailApiUrl = 'https://api.irail.be';
|
const irailApiUrl = 'https://api.irail.be';
|
||||||
|
|
||||||
@@ -12,9 +11,24 @@ export const useLoadTrainSchedule = (_: State, dispatch: Dispatch<Action>) => {
|
|||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
dispatch(actions.loadTrainSchedule({}))
|
dispatch(actions.loadTrainSchedule({}))
|
||||||
const answer = await axios.get(`${irailApiUrl}/stations?format=json&lang=en`)
|
const now = new Date();
|
||||||
const { station } = answer.data as Data;
|
|
||||||
dispatch(actions.loadTrainScheduleSuccess({ stations: station }))
|
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) {
|
} catch(error) {
|
||||||
dispatch(actions.loadTrainScheduleError({ error: error as 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,
|
loadTrainSchedule,
|
||||||
loadTrainScheduleSuccess,
|
loadTrainScheduleSuccess,
|
||||||
loadTrainScheduleError,
|
loadTrainScheduleError,
|
||||||
|
loadWeatherError,
|
||||||
|
loadWeatherSuccess,
|
||||||
|
loadWeather,
|
||||||
|
loadNewsError,
|
||||||
|
loadNewsSuccess,
|
||||||
|
loadNews,
|
||||||
} from './consts'
|
} from './consts'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type LoadTrainSchedule,
|
type LoadTrainSchedule,
|
||||||
type LoadTrainScheduleSuccess,
|
type LoadTrainScheduleSuccess,
|
||||||
type LoadTrainScheduleError,
|
type LoadTrainScheduleError,
|
||||||
|
type LoadWeatherError,
|
||||||
|
type LoadWeatherSuccess,
|
||||||
|
type LoadWeather,
|
||||||
|
type LoadNewsError,
|
||||||
|
type LoadNewsSuccess,
|
||||||
|
type LoadNews,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
@@ -23,4 +35,28 @@ export const actions = {
|
|||||||
type: loadTrainScheduleError,
|
type: loadTrainScheduleError,
|
||||||
...args
|
...args
|
||||||
} as LoadTrainScheduleError),
|
} 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 loadTrainScheduleSuccess: ActionType = 'loadTrainScheduleSuccess';
|
||||||
export const loadTrainScheduleError: ActionType = 'loadTrainScheduleError';
|
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;
|
export type ActionType = string;
|
||||||
|
|
||||||
import type { Station } from '../../types';
|
import type {Article} from '../../types/article';
|
||||||
import { loadTrainSchedule, loadTrainScheduleError, loadTrainScheduleSuccess } from './consts'
|
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 = {
|
export type Action = {
|
||||||
type: ActionType
|
type: ActionType
|
||||||
@@ -13,7 +25,7 @@ export type LoadTrainSchedule = {
|
|||||||
|
|
||||||
export type LoadTrainScheduleSuccess = {
|
export type LoadTrainScheduleSuccess = {
|
||||||
type: typeof loadTrainScheduleSuccess,
|
type: typeof loadTrainScheduleSuccess,
|
||||||
stations: Station[]
|
liveboard: LiveBoard
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LoadTrainScheduleError = {
|
export type LoadTrainScheduleError = {
|
||||||
@@ -21,3 +33,32 @@ export type LoadTrainScheduleError = {
|
|||||||
error: Error
|
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";
|
import { type State } from "./state";
|
||||||
|
|
||||||
export const initialState: State = {
|
export const initialState: State = {
|
||||||
stations: undefined,
|
liveboard: undefined,
|
||||||
error: undefined,
|
trainScheduleError: undefined,
|
||||||
loading: false,
|
trainScheduleLoading: false,
|
||||||
|
news: undefined,
|
||||||
|
newsError: undefined,
|
||||||
|
newsLoading: false,
|
||||||
|
weather: undefined,
|
||||||
|
weatherError: undefined,
|
||||||
|
weatherLoading: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,52 +5,96 @@ import {
|
|||||||
loadTrainSchedule,
|
loadTrainSchedule,
|
||||||
loadTrainScheduleError,
|
loadTrainScheduleError,
|
||||||
loadTrainScheduleSuccess,
|
loadTrainScheduleSuccess,
|
||||||
|
loadNews,
|
||||||
|
loadNewsSuccess,
|
||||||
|
loadNewsError,
|
||||||
|
loadWeatherError,
|
||||||
|
loadWeatherSuccess,
|
||||||
|
loadWeather,
|
||||||
type LoadTrainScheduleError,
|
type LoadTrainScheduleError,
|
||||||
type LoadTrainScheduleSuccess,
|
type LoadTrainScheduleSuccess,
|
||||||
|
type LoadWeatherError,
|
||||||
|
type LoadWeatherSuccess,
|
||||||
|
type LoadNewsError,
|
||||||
|
type LoadNewsSuccess,
|
||||||
} from './actions';
|
} from './actions';
|
||||||
|
|
||||||
|
|
||||||
//export const reducerInner = (state: State, action: Action): State => {
|
export const reducerInner = (state: State, action: Action): State => {
|
||||||
export const reducer = (state: State, action: Action): State => {
|
//export const reducer = (state: State, action: Action): State => {
|
||||||
|
|
||||||
if(action.type === loadTrainSchedule) {
|
if(action.type === loadTrainSchedule) {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
error: undefined,
|
trainScheduleError: undefined,
|
||||||
loading: true,
|
trainScheduleLoading: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if(action.type === loadTrainScheduleSuccess) {
|
else if(action.type === loadTrainScheduleSuccess) {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
stations: (action as LoadTrainScheduleSuccess).stations,
|
liveboard: (action as LoadTrainScheduleSuccess).liveboard,
|
||||||
loading: false,
|
trainScheduleLoading: false,
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if(action.type === loadTrainScheduleError) {
|
else if(action.type === loadTrainScheduleError) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
error: (action as LoadTrainScheduleError).error,
|
trainScheduleError: (action as LoadTrainScheduleError).error,
|
||||||
loading: false,
|
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;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
export const reducer = (state: State, action: Action) => {
|
export const reducer = (state: State, action: Action) => {
|
||||||
if(action.type !== tick) {
|
console.log(`TS - ${action.type}`);
|
||||||
console.log(`MD - ${action.type}`);
|
|
||||||
console.log({action})
|
console.log({action})
|
||||||
console.log({state})
|
console.log({state})
|
||||||
}
|
|
||||||
const newState = reducerInner(state, action);
|
const newState = reducerInner(state, action);
|
||||||
|
|
||||||
if(action.type !== tick) {
|
|
||||||
console.log({newState})
|
console.log({newState})
|
||||||
}
|
|
||||||
return 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 = {
|
export type State = {
|
||||||
loading: boolean,
|
trainScheduleLoading: boolean,
|
||||||
stations: Station[] | undefined,
|
liveboard: LiveBoard | undefined,
|
||||||
error: Error | 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