diff --git a/src/App.css b/src/App.css index b9d355d..f38f0ad 100644 --- a/src/App.css +++ b/src/App.css @@ -5,38 +5,5 @@ text-align: center; } -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} diff --git a/src/App.tsx b/src/App.tsx index a99ccef..86fba3f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,13 +1,19 @@ -import { useReducer } from 'react' +import { useReducer, useState } from 'react' import { useLoadTrainSchedule } from './hooks/useLoadTrainSchedule' -import { initialState, reducer } from './state' +import { actions, initialState, reducer } from './state' import './App.css' import { NewsWidget, TrainSchedule, WeatherWidget } from './containers'; -import {useNewsApi, useWeatherApi} from './hooks'; +import {useGiteaApi, useNewsApi, useWeatherApi} from './hooks'; import styled from 'styled-components'; +import {IssueWidget} from './containers/IssuesWidget'; +import {IoSettingsSharp} from 'react-icons/io5'; +import type {Station} from './types'; +//import {NativeSelectRoot} from '@chakra-ui/react'; + + const Container = styled.div` display: flex; @@ -23,25 +29,54 @@ const Pane = styled.div` function App() { - const [state, dispatch] = useReducer( reducer, initialState,); + const [state, dispatch] = useReducer( reducer, initialState, ); + const [settingOpened, setSettingOpened] = useState(false); - useLoadTrainSchedule(state, dispatch); - useNewsApi({state, dispatch}); - useWeatherApi({state, dispatch}); + + const { reloadTrainSchedule } = useLoadTrainSchedule(state, dispatch); + const { reloadNews } = useNewsApi({state, dispatch}); + const { reloadWeather } = useWeatherApi({state, dispatch}); + const { reloadIssues } = useGiteaApi({state, dispatch}) + + const { selectedLocation } = state; + const setSelectedLocation = (location: string) => { + dispatch(actions.setSelectedLocation({ location })) + reloadNews(); + reloadWeather(); + reloadIssues(); + reloadTrainSchedule(); + } + + const mainContent = <> + +

Next trains in {state.selectedLocation}

+ +
+ +

Weather

+ +

Issues

+ +

News

+ +
+ + + const settingContent = <> + + + + return ( - - -

Next trains

- -
- -

Weather

- -

News

- -
-
+ <> + +
setSettingOpened(!settingOpened)}>
+ {settingOpened ? settingContent: mainContent} +
+ ) } diff --git a/src/components/Departure.spec.tsx b/src/components/Departure.spec.tsx index 334513b..8106310 100644 --- a/src/components/Departure.spec.tsx +++ b/src/components/Departure.spec.tsx @@ -31,8 +31,8 @@ export const InfoStyled = styled.div` `; export const DestinationStyled = styled.div` - font-size: 1.2rem; - font-weight: 600; + font-size: 1rem; + font-weight: 400; `; export const DelayStyled = styled.div` diff --git a/src/components/Issue.tsx b/src/components/Issue.tsx new file mode 100644 index 0000000..e6b5fc8 --- /dev/null +++ b/src/components/Issue.tsx @@ -0,0 +1,26 @@ +import type { IssueType } from "../types/issues" + +import styled from 'styled-components'; +import {TitleLink} from "./NewsArticle"; + + +export const IssueHeader = styled.div` + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 0.75rem; +`; + + +export const Issue = ({ issue }: { issue: IssueType }) => { + return ( + + + + {issue.title} + + + + + ); +}; diff --git a/src/components/Pager.tsx b/src/components/Pager.tsx index 1ba64e2..b9fb72f 100644 --- a/src/components/Pager.tsx +++ b/src/components/Pager.tsx @@ -1,6 +1,6 @@ import React from "react"; import styled from "styled-components"; -import {accentColor} from "../styles"; +import { accentColor } from "../styles"; const Container = styled.div` @@ -12,8 +12,8 @@ const Container = styled.div` `; const Dot = styled.button<{ $active: boolean }>` - width: 12px; - height: 12px; + width: 20px; + height: 20px; padding: 0; margin: 0; border: none; diff --git a/src/components/index.tsx b/src/components/index.tsx index 3bda937..1efe395 100644 --- a/src/components/index.tsx +++ b/src/components/index.tsx @@ -1,3 +1,4 @@ export * from './Departure'; export * from './NewsArticle'; export * from './NowTime'; +export * from './Issue'; diff --git a/src/containers/IssuesWidget.tsx b/src/containers/IssuesWidget.tsx new file mode 100644 index 0000000..a9de6d6 --- /dev/null +++ b/src/containers/IssuesWidget.tsx @@ -0,0 +1,39 @@ +import type { Dispatch } from "react" +import styled from "styled-components"; +import { SpinnerDiamond } from "spinners-react"; + +import type { Action, State } from "../state" +import { Issue } from "../components"; + +export const IssueContainer = 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; +`; + + +type IssueDispatchProps = { + state: State; + dispatch: Dispatch; +}; + +export const IssueWidget = ({ state }: IssueDispatchProps) => { + return state.issuesLoading + ? + : state.issuesError + ? {state.issuesError?.message} + : ( + + {state.issues?.slice(0,3)?.map(issue => )} + + ); +}; diff --git a/src/hooks/index.tsx b/src/hooks/index.tsx index 987967f..4ad384f 100644 --- a/src/hooks/index.tsx +++ b/src/hooks/index.tsx @@ -1,3 +1,4 @@ export * from './useLoadTrainSchedule'; export * from './useNewsApi'; export * from './useWeatherApi'; +export * from './useGiteaApi'; diff --git a/src/hooks/useGiteaApi.tsx b/src/hooks/useGiteaApi.tsx new file mode 100644 index 0000000..6dd0d37 --- /dev/null +++ b/src/hooks/useGiteaApi.tsx @@ -0,0 +1,31 @@ +import { useEffect, type Dispatch } from "react"; +import { actions, type Action, type State } from "../state"; +import type {IssuesResponse} from "../types/issues"; + +const apiKey = 'a70bbe6b9f70747278ba3ec7a701b2b279be2efc'; + +export const useGiteaApi = ({ dispatch }: { state : State, dispatch: Dispatch }) => { + const reloadIssues = (async () => { + try { + dispatch(actions.loadGiteaIssue({})); + const answer = await fetch(`https://git.boomjacky.art/api/v1/repos/boomjacky/trainhour/issues?state=all`, { + method: "GET", + headers: { + "Authorization": `token ${apiKey}`, + "Accept": "application/json" + } + }) + const data = await answer.json() as IssuesResponse; + dispatch(actions.loadGiteaIssueSuccess({ data })); + } catch(error) { + dispatch(actions.loadGiteaIssueError({ error: error as Error })); + } + + }) + + useEffect(() => { + reloadIssues(); + }, []) + + return { reloadIssues } +} diff --git a/src/hooks/useLoadTrainSchedule.tsx b/src/hooks/useLoadTrainSchedule.tsx index 5b4022c..671807a 100644 --- a/src/hooks/useLoadTrainSchedule.tsx +++ b/src/hooks/useLoadTrainSchedule.tsx @@ -4,44 +4,48 @@ import { flatten, range } from 'lodash'; import axios from 'axios'; import type { LiveBoard } from "../types/liveboard"; +import type {StationsRoot} from "../types/station"; const irailApiUrl = 'https://api.irail.be'; const lookahead = 3; -export const useLoadTrainSchedule = (_: State, dispatch: Dispatch) => { +export const useLoadTrainSchedule = (state: State, dispatch: Dispatch) => { + const reloadTrainSchedule = (async () => { + try { + dispatch(actions.loadTrainSchedule({})) + const now = new Date(); + const pad = (num: number) => num.toString().padStart(2, '0'); + + const departures = + flatten(await + Promise.all(range(0, lookahead).map(async hourPadding => { + + 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() + hourPadding) ; + const minutes = pad(now.getMinutes()); + const time = `${hours}${minutes}` + + const answer = await axios.get(`${irailApiUrl}/liveboard?station=${state.selectedLocation}&date=${date}&time=${time}&format=json&lang=en&alerts=true`) + + const liveboard = answer.data as LiveBoard; + + return liveboard.departures.departure; + }))) + + const answer = await axios.get(`${irailApiUrl}/stations?format=json&lang=en&alerts=true`); + const stations = (answer.data as StationsRoot).station; + + dispatch(actions.loadTrainScheduleSuccess({ departures, stations })) + } catch(error) { + dispatch(actions.loadTrainScheduleError({ error: error as Error})) + } + }) useEffect(() => { - (async () => { - try { - dispatch(actions.loadTrainSchedule({})) - const now = new Date(); - const pad = (num: number) => num.toString().padStart(2, '0'); - - const departures = - flatten(await - Promise.all(range(0, lookahead).map(async hourPadding => { - - 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() + hourPadding) ; - 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; - - return liveboard.departures.departure; - }))) - - - - dispatch(actions.loadTrainScheduleSuccess({ departures })) - } catch(error) { - dispatch(actions.loadTrainScheduleError({ error: error as Error})) - } - })() + reloadTrainSchedule() }, []) + return { reloadTrainSchedule } } diff --git a/src/hooks/useNewsApi.tsx b/src/hooks/useNewsApi.tsx index ac52c01..6068646 100644 --- a/src/hooks/useNewsApi.tsx +++ b/src/hooks/useNewsApi.tsx @@ -10,16 +10,19 @@ export type UseNewsApiProps = { } export const useNewsApi = ({dispatch}: UseNewsApiProps) => { + const reloadNews = (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})); + } + }) 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})); - } - })() + reloadNews() }, []) + + return { reloadNews } } diff --git a/src/hooks/useWeatherApi.tsx b/src/hooks/useWeatherApi.tsx index b934e7d..efab296 100644 --- a/src/hooks/useWeatherApi.tsx +++ b/src/hooks/useWeatherApi.tsx @@ -2,24 +2,27 @@ import { useEffect, type Dispatch } from "react" import { actions, type Action, type State } from "../state" import type {WeatherData} from "../types/weather"; -const weatherApiUrl = `https://api.weatherapi.com/v1/current.json?key=176d6e98c8894466aa6205455253010&q=Nivelles&aqi=no`; - export type UseWeatherApiProps = { dispatch: Dispatch, state: State } -export const useWeatherApi = ({ dispatch}: UseWeatherApiProps) => { +export const useWeatherApi = ({ dispatch, state }: UseWeatherApiProps) => { + + const reloadWeather = (async () => { + try { + const weatherApiUrl = `https://api.weatherapi.com/v1/current.json?key=176d6e98c8894466aa6205455253010&q=${state.selectedLocation}&aqi=no`; + + 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 })); + } + }); 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 })); - } - })() + reloadWeather(); }, []) + return { reloadWeather } } diff --git a/src/state/actions/actions.ts b/src/state/actions/actions.ts index 9271299..8947398 100644 --- a/src/state/actions/actions.ts +++ b/src/state/actions/actions.ts @@ -8,6 +8,10 @@ import { loadNewsError, loadNewsSuccess, loadNews, + loadGiteaIssueError, + loadGiteaIssueSuccess, + loadGiteaIssue, + setSelectedLocation, } from './consts' import { @@ -20,6 +24,10 @@ import { type LoadNewsError, type LoadNewsSuccess, type LoadNews, + type LoadGiteaIssueError, + type LoadGiteaIssueSuccess, + type LoadGiteaIssue, + type SetSelectedLocation, } from './types'; export const actions = { @@ -59,4 +67,20 @@ export const actions = { type: loadWeatherError, ...args } as LoadWeatherError), + loadGiteaIssue: (args: Omit) => ({ + type: loadGiteaIssue, + ...args + } as LoadGiteaIssue), + loadGiteaIssueSuccess: (args: Omit) => ({ + type: loadGiteaIssueSuccess, + ...args + } as LoadGiteaIssueSuccess), + loadGiteaIssueError: (args: Omit) => ({ + type: loadGiteaIssueError, + ...args + } as LoadGiteaIssueError), + setSelectedLocation: (args: Omit) => ({ + type: setSelectedLocation, + ...args + } as SetSelectedLocation), } diff --git a/src/state/actions/consts.ts b/src/state/actions/consts.ts index 86c2339..b712116 100644 --- a/src/state/actions/consts.ts +++ b/src/state/actions/consts.ts @@ -11,3 +11,9 @@ export const loadNewsError: ActionType = 'loadNewsError'; export const loadWeather: ActionType = 'loadWeather'; export const loadWeatherSuccess: ActionType = 'loadWeatherSuccess'; export const loadWeatherError: ActionType = 'loadWeatherError'; + +export const loadGiteaIssue: ActionType = 'loadGiteaIssue'; +export const loadGiteaIssueSuccess: ActionType = 'loadGiteaIssueSuccess'; +export const loadGiteaIssueError: ActionType = 'loadGiteaIssueError'; + +export const setSelectedLocation: ActionType = 'setSelectedLocation'; diff --git a/src/state/actions/types.ts b/src/state/actions/types.ts index 37bfd51..c72f2ae 100644 --- a/src/state/actions/types.ts +++ b/src/state/actions/types.ts @@ -1,6 +1,8 @@ export type ActionType = string; +import type {Station} from '../../types'; import type {Article} from '../../types/article'; +import type {IssuesResponse} from '../../types/issues'; import type { DepartureType } from '../../types/liveboard'; import type {WeatherData} from '../../types/weather'; import { @@ -13,6 +15,10 @@ import { loadWeather, loadWeatherError, loadWeatherSuccess, + loadGiteaIssueSuccess, + loadGiteaIssueError, + loadGiteaIssue, + setSelectedLocation, } from './consts' export type Action = { @@ -25,7 +31,8 @@ export type LoadTrainSchedule = { export type LoadTrainScheduleSuccess = { type: typeof loadTrainScheduleSuccess, - departures: DepartureType[] + departures: DepartureType[], + stations: Station[] } export type LoadTrainScheduleError = { @@ -62,3 +69,22 @@ export type LoadWeatherError = { error: Error } +export type LoadGiteaIssue = { + type: typeof loadGiteaIssue, +} + +export type LoadGiteaIssueSuccess = { + type: typeof loadGiteaIssueSuccess, + data: IssuesResponse +} + +export type LoadGiteaIssueError = { + type: typeof loadGiteaIssueError, + error: Error +} + +export type SetSelectedLocation = { + type: typeof setSelectedLocation, + location: string, +} + diff --git a/src/state/initialState.ts b/src/state/initialState.ts index 605befd..0f60cc5 100644 --- a/src/state/initialState.ts +++ b/src/state/initialState.ts @@ -10,4 +10,9 @@ export const initialState: State = { weather: undefined, weatherError: undefined, weatherLoading: false, + issues: undefined, + issuesError: undefined, + issuesLoading: false, + stations: undefined, + selectedLocation: 'Nivelles' }; diff --git a/src/state/reducer.ts b/src/state/reducer.ts index 87ce298..98ff650 100644 --- a/src/state/reducer.ts +++ b/src/state/reducer.ts @@ -17,6 +17,13 @@ import { type LoadWeatherSuccess, type LoadNewsError, type LoadNewsSuccess, + loadGiteaIssueSuccess, + loadGiteaIssue, + type LoadGiteaIssueSuccess, + loadGiteaIssueError, + type LoadGiteaIssueError, + setSelectedLocation, + type SetSelectedLocation, } from './actions'; @@ -34,6 +41,7 @@ export const reducerInner = (state: State, action: Action): State => { return { ...state, departures: (action as LoadTrainScheduleSuccess).departures, + stations: (action as LoadTrainScheduleSuccess).stations, trainScheduleLoading: false, } } @@ -85,6 +93,33 @@ export const reducerInner = (state: State, action: Action): State => { weatherError: (action as LoadWeatherError).error, weatherLoading: false, } + } + else if(action.type === loadGiteaIssue) { + return { + ...state, + issuesError: undefined, + issuesLoading: true, + } + } + else if(action.type === loadGiteaIssueSuccess) { + return { + ...state, + issues: (action as LoadGiteaIssueSuccess).data, + issuesLoading: false, + } + } + else if(action.type === loadGiteaIssueError) { + return { + ...state, + issuesLoading: false, + issuesError: (action as LoadGiteaIssueError).error, + } + } + else if(action.type === setSelectedLocation) { + return { + ...state, + selectedLocation: (action as SetSelectedLocation).location, + } } return state; } diff --git a/src/state/state.ts b/src/state/state.ts index 927048f..d482cba 100644 --- a/src/state/state.ts +++ b/src/state/state.ts @@ -1,4 +1,6 @@ +import type {Station} from "../types"; import type { Article } from "../types/article"; +import type {IssuesResponse} from "../types/issues"; import type { DepartureType } from "../types/liveboard"; import type { WeatherData } from "../types/weather"; @@ -14,4 +16,11 @@ export type State = { news: Article[] | undefined, newsLoading: boolean, newsError: Error | undefined, + + issues: IssuesResponse | undefined, + issuesLoading: boolean, + issuesError: Error | undefined, + + stations: undefined | Station[], + selectedLocation: string, } diff --git a/src/types/issues.tsx b/src/types/issues.tsx new file mode 100644 index 0000000..9257a73 --- /dev/null +++ b/src/types/issues.tsx @@ -0,0 +1,64 @@ +interface User { + id: number; + login: string; + login_name: string; + source_id: number; + full_name: string; + email: string; + avatar_url: string; + html_url: string; + language: string; + is_admin: boolean; + last_login: string; + created: string; + restricted: boolean; + active: boolean; + prohibit_login: boolean; + location: string; + website: string; + description: string; + visibility: string; + followers_count: number; + following_count: number; + starred_repos_count: number; + username: string; +} + +interface Repository { + id: number; + name: string; + owner: string; + full_name: string; +} + +export interface IssueType { + id: number; + url: string; + html_url: string; + number: number; + user: User; + original_author: string; + original_author_id: number; + title: string; + body: string; + ref: string; + assets: any[]; + labels: string[]; + milestone: any | null; + assignee: any | null; + assignees: any | null; + state: string; + is_locked: boolean; + comments: number; + created_at: string; + updated_at: string; + closed_at: string | null; + due_date: string | null; + time_estimate: number; + pull_request: any | null; + repository: Repository; + pin_order: number; +} + +export type IssuesResponse = IssueType[]; + diff --git a/src/types/station.tsx b/src/types/station.tsx new file mode 100644 index 0000000..81c3901 --- /dev/null +++ b/src/types/station.tsx @@ -0,0 +1,15 @@ +export interface StationsRoot { + version: string + timestamp: string + station: Station[] +} + +export interface Station { + "@id": string + id: string + name: string + locationX: string + locationY: string + standardname: string +} +