From ec2c8e6d8101bb9442ef8d7459d441288edeecd7 Mon Sep 17 00:00:00 2001 From: Alexander Harding <2166114+aeharding@users.noreply.github.com> Date: Mon, 15 May 2023 22:14:12 -0500 Subject: [PATCH] Add METAR support --- .../rap/extra/reportMetadata/Legend.tsx | 2 +- .../extra/reportMetadata/ReportMetadata.tsx | 2 +- .../aviation/DetailedAviationReport.tsx | 11 +- src/features/weather/aviation/Forecast.tsx | 54 +++--- src/features/weather/aviation/MetarDetail.tsx | 180 ++++++++++++++++++ .../weather/aviation/cells/Ceiling.tsx | 29 +++ .../weather/aviation/{ => cells}/Cloud.tsx | 4 +- .../weather/aviation/cells/Clouds.tsx | 22 +++ .../weather/aviation/cells/Humidity.tsx | 16 ++ .../weather/aviation/cells/Pressure.tsx | 22 +++ .../weather/aviation/cells/Remarks.tsx | 83 ++++++++ .../weather/aviation/cells/Temperature.tsx | 34 ++++ src/features/weather/weatherSlice.ts | 15 +- src/features/weather/weatherSliceLazy.ts | 32 +++- src/helpers/taf.ts | 1 + src/services/aviationWeather.ts | 52 ++++- src/shared/RelativeTime.tsx | 35 ++++ 17 files changed, 552 insertions(+), 42 deletions(-) create mode 100644 src/features/weather/aviation/MetarDetail.tsx create mode 100644 src/features/weather/aviation/cells/Ceiling.tsx rename src/features/weather/aviation/{ => cells}/Cloud.tsx (70%) create mode 100644 src/features/weather/aviation/cells/Clouds.tsx create mode 100644 src/features/weather/aviation/cells/Humidity.tsx create mode 100644 src/features/weather/aviation/cells/Pressure.tsx create mode 100644 src/features/weather/aviation/cells/Remarks.tsx create mode 100644 src/features/weather/aviation/cells/Temperature.tsx create mode 100644 src/shared/RelativeTime.tsx diff --git a/src/features/rap/extra/reportMetadata/Legend.tsx b/src/features/rap/extra/reportMetadata/Legend.tsx index e206a46..b737c34 100644 --- a/src/features/rap/extra/reportMetadata/Legend.tsx +++ b/src/features/rap/extra/reportMetadata/Legend.tsx @@ -75,7 +75,7 @@ export default function Legend({ showTaf, showNws, showOp40 }: LegendProps) { {showTaf && ( - Terminal Aerodrome Forecast (TAF) location + METAR & TAF location )} diff --git a/src/features/rap/extra/reportMetadata/ReportMetadata.tsx b/src/features/rap/extra/reportMetadata/ReportMetadata.tsx index 9c4a3cd..c2e1acc 100644 --- a/src/features/rap/extra/reportMetadata/ReportMetadata.tsx +++ b/src/features/rap/extra/reportMetadata/ReportMetadata.tsx @@ -128,7 +128,7 @@ const MapController = () => { ]; const airportPosition: LatLngExpression | undefined = aviationWeather && typeof aviationWeather === "object" - ? [aviationWeather.lat, aviationWeather.lon] + ? [aviationWeather.taf.lat, aviationWeather.taf.lon] : undefined; useEffect(() => { diff --git a/src/features/weather/aviation/DetailedAviationReport.tsx b/src/features/weather/aviation/DetailedAviationReport.tsx index 84ce684..c32a32e 100644 --- a/src/features/weather/aviation/DetailedAviationReport.tsx +++ b/src/features/weather/aviation/DetailedAviationReport.tsx @@ -11,6 +11,8 @@ import Forecast, { getTimeFormatString, } from "./Forecast"; import { TemperatureUnit } from "../../rap/extra/settings/settingEnums"; +import { metarReport } from "../weatherSliceLazy"; +import MetarDetail from "./MetarDetail"; const Container = styled.div` overflow: hidden; @@ -29,7 +31,7 @@ const Description = styled.div` margin: 0 1rem 1rem; `; -const Forecasts = styled.div` +export const Forecasts = styled.div` display: flex; flex-direction: column; gap: 1.5rem; @@ -60,13 +62,14 @@ export default function DetailedAviationReport({ const timeZone = useAppSelector(timeZoneSelector); const temperatureUnit = useAppSelector((state) => state.user.temperatureUnit); const timeFormat = useAppSelector((state) => state.user.timeFormat); + const metar = useAppSelector(metarReport); function formatTemperature(temperatureInC: number): string { switch (temperatureUnit) { case TemperatureUnit.Celsius: return `${temperatureInC}℃`; case TemperatureUnit.Fahrenheit: - return `${cToF(temperatureInC)}℉`; + return `${Math.round(cToF(temperatureInC))}℉`; } } @@ -74,6 +77,8 @@ export default function DetailedAviationReport({ return ( + {metar ? : ""} + Forecast @@ -135,5 +140,5 @@ export default function DetailedAviationReport({ } export function cToF(celsius: number): number { - return Math.round((celsius * 9) / 5 + 32); + return (celsius * 9) / 5 + 32; } diff --git a/src/features/weather/aviation/Forecast.tsx b/src/features/weather/aviation/Forecast.tsx index 8d22e05..6ededb4 100644 --- a/src/features/weather/aviation/Forecast.tsx +++ b/src/features/weather/aviation/Forecast.tsx @@ -12,7 +12,6 @@ import React from "react"; import { notEmpty } from "../../../helpers/array"; import { capitalizeFirstLetter } from "../../../helpers/string"; import { - determineCeilingFromClouds, FlightCategory, formatDescriptive, formatHeight, @@ -26,12 +25,15 @@ import { } from "../../../helpers/taf"; import { useAppSelector } from "../../../hooks"; import { timeZoneSelector } from "../weatherSlice"; -import Cloud from "./Cloud"; import Wind from "./cells/Wind"; import WindShear from "./cells/WindShear"; import { TimeFormat } from "../../rap/extra/settings/settingEnums"; +import Clouds from "./cells/Clouds"; +import Ceiling from "./cells/Ceiling"; -const Container = styled.div<{ type: WeatherChangeType | undefined }>` +export const Container = styled.div<{ + type: WeatherChangeType | undefined | "METAR"; +}>` padding: 1rem; display: flex; flex-direction: column; @@ -54,22 +56,26 @@ const Container = styled.div<{ type: WeatherChangeType | undefined }>` return css` border-left-color: #0095ff5d; `; + case "METAR": + return css` + border-left-color: #6d0050; + `; } }} `; -const Header = styled.div` +export const Header = styled.div` display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: -0.25rem; `; -const Text = styled.p` +export const Text = styled.p` margin: 0; `; -const Category = styled.div<{ category: FlightCategory }>` +export const Category = styled.div<{ category: FlightCategory }>` display: inline-block; padding: 2px 8px; @@ -79,7 +85,7 @@ const Category = styled.div<{ category: FlightCategory }>` ${({ category }) => getFlightCategoryCssColor(category)} `; -const Table = styled.table` +export const Table = styled.table` width: 100%; table-layout: fixed; @@ -95,7 +101,7 @@ const Table = styled.table` } `; -const Raw = styled.div` +export const Raw = styled.div` padding: 0.5rem; background: rgba(0, 0, 0, 0.5); @@ -114,7 +120,6 @@ export default function Forecast({ data }: ForecastProps) { if (!timeZone) throw new Error("timezone undefined"); - const ceiling = determineCeilingFromClouds(data.clouds); const category = getFlightCategory( data.visibility, data.clouds, @@ -200,15 +205,10 @@ export default function Forecast({ data }: ForecastProps) { Clouds - {data.clouds.map((cloud, index) => ( - - -
-
- ))} - {data.verticalVisibility != null ? ( - <>Obscured sky - ) : undefined} + ) : ( @@ -229,14 +229,10 @@ export default function Forecast({ data }: ForecastProps) { Ceiling - {ceiling?.height != null - ? `${formatHeight(ceiling.height, heightUnit)} AGL` - : data.verticalVisibility - ? `Vertical visibility ${formatHeight( - data.verticalVisibility, - heightUnit - )} AGL` - : `At least ${formatHeight(12_000, heightUnit)} AGL`} + ) : ( @@ -305,7 +301,9 @@ export default function Forecast({ data }: ForecastProps) { ))} - ) : undefined} + ) : ( + "" + )} {data.raw} @@ -313,7 +311,7 @@ export default function Forecast({ data }: ForecastProps) { ); } -function formatWeather(weather: IWeatherCondition[]): React.ReactNode { +export function formatWeather(weather: IWeatherCondition[]): React.ReactNode { return ( <> {capitalizeFirstLetter( diff --git a/src/features/weather/aviation/MetarDetail.tsx b/src/features/weather/aviation/MetarDetail.tsx new file mode 100644 index 0000000..48de591 --- /dev/null +++ b/src/features/weather/aviation/MetarDetail.tsx @@ -0,0 +1,180 @@ +import { IMetarDated, RemarkType } from "metar-taf-parser"; +import { + Category, + Container, + Header, + Raw, + Table, + formatWeather, +} from "./Forecast"; +import { formatVisibility, getFlightCategory } from "../../../helpers/taf"; +import Wind from "./cells/Wind"; +import WindShear from "./cells/WindShear"; +import { useAppSelector } from "../../../hooks"; +import { Forecasts } from "./DetailedAviationReport"; +import Temperature from "./cells/Temperature"; +import Pressure from "./cells/Pressure"; +import RelativeTime from "../../../shared/RelativeTime"; +import Remarks from "./cells/Remarks"; +import Clouds from "./cells/Clouds"; +import Ceiling from "./cells/Ceiling"; +import Humidity from "./cells/Humidity"; + +interface MetarDetailProps { + metar: IMetarDated; +} + +export default function MetarDetail({ metar }: MetarDetailProps) { + const distanceUnit = useAppSelector((state) => state.user.distanceUnit); + const aviationWeather = useAppSelector( + (state) => state.weather.aviationWeather + ); + + if ( + !aviationWeather || + aviationWeather === "failed" || + aviationWeather === "not-available" || + aviationWeather === "pending" || + !aviationWeather.metar + ) + return <>; + + const category = getFlightCategory( + metar.visibility, + metar.clouds, + metar.verticalVisibility + ); + + const highPrecisionTemperatureDewPoint = (() => { + for (const remark of metar.remarks) { + if (remark.type === RemarkType.HourlyTemperatureDewPoint) return remark; + } + })(); + const temperature = + highPrecisionTemperatureDewPoint?.temperature ?? metar.temperature; + const dewPoint = highPrecisionTemperatureDewPoint?.dewPoint ?? metar.dewPoint; + + return ( + + +
+ + Observed conditions + + {category} +
+ + + + {temperature != null && ( + + + + + )} + {dewPoint != null && ( + + + + + )} + {metar.altimeter && ( + + + + + )} + {metar.wind && ( + + + + + )} + {metar.windShear && ( + + + + + )} + {metar.clouds.length || metar.verticalVisibility != null ? ( + + + + + ) : ( + "" + )} + {metar.visibility && ( + + + + + )} + {metar.visibility && + (metar.clouds.length || metar.verticalVisibility != null) ? ( + + + + + ) : ( + "" + )} + {metar.weatherConditions.length ? ( + + + + + ) : undefined} + {metar.remarks.length ? ( + + + + + ) : ( + "" + )} + +
Temperature + +
Dew Point + {" "} + {temperature != null ? ( + <> + {" "} + [{" "} + {" "} + ] + + ) : ( + "" + )} +
Pressure + +
Wind + +
Wind Shear + +
Clouds + +
Visibility + {formatVisibility(metar.visibility, distanceUnit)}{" "} + {metar.visibility.ndv && "No directional visibility"}{" "} +
Ceiling + +
Weather{formatWeather(metar.weatherConditions)}
Remarks + +
+ {aviationWeather.metar.raw} +
+
+ ); +} diff --git a/src/features/weather/aviation/cells/Ceiling.tsx b/src/features/weather/aviation/cells/Ceiling.tsx new file mode 100644 index 0000000..1af1a52 --- /dev/null +++ b/src/features/weather/aviation/cells/Ceiling.tsx @@ -0,0 +1,29 @@ +import { ICloud } from "metar-taf-parser"; +import { + determineCeilingFromClouds, + formatHeight, +} from "../../../../helpers/taf"; +import { useAppSelector } from "../../../../hooks"; + +interface CeilingProps { + clouds: ICloud[]; + verticalVisibility: number | undefined; +} + +export default function Ceiling({ clouds, verticalVisibility }: CeilingProps) { + const heightUnit = useAppSelector((state) => state.user.heightUnit); + const ceiling = determineCeilingFromClouds(clouds); + + return ( + <> + {ceiling?.height != null + ? `${formatHeight(ceiling.height, heightUnit)} AGL` + : verticalVisibility + ? `Vertical visibility ${formatHeight( + verticalVisibility, + heightUnit + )} AGL` + : `At least ${formatHeight(12_000, heightUnit)} AGL`} + + ); +} diff --git a/src/features/weather/aviation/Cloud.tsx b/src/features/weather/aviation/cells/Cloud.tsx similarity index 70% rename from src/features/weather/aviation/Cloud.tsx rename to src/features/weather/aviation/cells/Cloud.tsx index f1077f5..ff262ab 100644 --- a/src/features/weather/aviation/Cloud.tsx +++ b/src/features/weather/aviation/cells/Cloud.tsx @@ -1,6 +1,6 @@ import { ICloud } from "metar-taf-parser"; -import { formatCloud } from "../../../helpers/taf"; -import { useAppSelector } from "../../../hooks"; +import { formatCloud } from "../../../../helpers/taf"; +import { useAppSelector } from "../../../../hooks"; interface CloudProps { data: ICloud; diff --git a/src/features/weather/aviation/cells/Clouds.tsx b/src/features/weather/aviation/cells/Clouds.tsx new file mode 100644 index 0000000..2fd073c --- /dev/null +++ b/src/features/weather/aviation/cells/Clouds.tsx @@ -0,0 +1,22 @@ +import { ICloud } from "metar-taf-parser"; +import Cloud from "./Cloud"; +import React from "react"; + +interface CloudsProps { + clouds: ICloud[]; + verticalVisibility: number | undefined; +} + +export default function Clouds({ clouds, verticalVisibility }: CloudsProps) { + return ( + <> + {clouds.map((cloud, index) => ( + + +
+
+ ))} + {verticalVisibility != null ? <>Obscured sky : undefined} + + ); +} diff --git a/src/features/weather/aviation/cells/Humidity.tsx b/src/features/weather/aviation/cells/Humidity.tsx new file mode 100644 index 0000000..9813653 --- /dev/null +++ b/src/features/weather/aviation/cells/Humidity.tsx @@ -0,0 +1,16 @@ +interface HumidityProps { + temperature: number; + dewPoint: number; +} + +export default function Humidity({ temperature, dewPoint }: HumidityProps) { + return <>RH = {Math.round(getRh(temperature, dewPoint))}%; +} + +// from https://www.weather.gov/mfl/calculator +function getRh(airTemp: number, dewPoint: number) { + var tc = airTemp; + var tdc = dewPoint; + + return 100.0 * Math.pow((112 - 0.1 * tc + tdc) / (112 + 0.9 * tc), 8); +} diff --git a/src/features/weather/aviation/cells/Pressure.tsx b/src/features/weather/aviation/cells/Pressure.tsx new file mode 100644 index 0000000..4c8f438 --- /dev/null +++ b/src/features/weather/aviation/cells/Pressure.tsx @@ -0,0 +1,22 @@ +import { AltimeterUnit, IAltimeter } from "metar-taf-parser"; + +interface PressureProps { + altimeter: IAltimeter; +} + +export default function Temperature({ altimeter }: PressureProps) { + const pressureLabel = (() => { + switch (altimeter.unit) { + case AltimeterUnit.HPa: + return "mb"; + case AltimeterUnit.InHg: + return "inches Hg"; + } + })(); + + return ( + <> + {altimeter.value} {pressureLabel} + + ); +} diff --git a/src/features/weather/aviation/cells/Remarks.tsx b/src/features/weather/aviation/cells/Remarks.tsx new file mode 100644 index 0000000..198d281 --- /dev/null +++ b/src/features/weather/aviation/cells/Remarks.tsx @@ -0,0 +1,83 @@ +import styled from "@emotion/styled"; +import { Remark } from "metar-taf-parser"; +import { capitalizeFirstLetter } from "../../../../helpers/string"; +import { css } from "@emotion/react"; +import { useState } from "react"; + +const List = styled.ul` + margin: 0; + padding: 0; + font-size: 0.85em; + color: rgba(255, 255, 255, 0.5); + + span { + color: white; + } +`; + +const Collapser = styled.div<{ collapsed: boolean }>` + ${({ collapsed }) => + collapsed && + css` + position: relative; + + &::after { + content: "Expand"; + position: absolute; + bottom: 0; + left: 50%; + transform: translateX(-50%); + + font-size: 0.8em; + backdrop-filter: blur(4px); + + border: 1px solid rgba(255, 255, 255, 0.4); + border-radius: 50px; + padding: 0.25rem 0.5rem; + } + + ${List} { + max-height: 75px; + overflow: hidden; + + // Hack to fix bullets being hidden by overflow + padding: 0 1rem; + margin: 0 -1rem; + + mask: linear-gradient( + to bottom, + rgba(0, 0, 0, 1) 0, + rgba(0, 0, 0, 1) 45%, + rgba(0, 0, 0, 0.2) 75%, + rgba(0, 0, 0, 0) 95%, + rgba(0, 0, 0, 0) 0 + ) + 100% 50% / 100% 100% repeat-x; + } + `} +`; + +interface RemarksProps { + remarks: Remark[]; +} + +export default function Remarks({ remarks }: RemarksProps) { + const [collapsed, setCollapsed] = useState(remarks.length > 3); + + return ( + setCollapsed(false)}> + + {remarks.map((remark, index) => ( +
  • + + {remark.description + ? capitalizeFirstLetter(remark.description) + : remark.raw} + +

    +
  • + ))} +
    +
    + ); +} diff --git a/src/features/weather/aviation/cells/Temperature.tsx b/src/features/weather/aviation/cells/Temperature.tsx new file mode 100644 index 0000000..f0915ee --- /dev/null +++ b/src/features/weather/aviation/cells/Temperature.tsx @@ -0,0 +1,34 @@ +import { useAppSelector } from "../../../../hooks"; +import { TemperatureUnit } from "../../../rap/extra/settings/settingEnums"; +import { cToF } from "../DetailedAviationReport"; + +interface TemperatureProps { + temperatureInC: number; +} + +export default function Temperature({ temperatureInC }: TemperatureProps) { + const temperatureUnit = useAppSelector((state) => state.user.temperatureUnit); + const temperatureLabel = (() => { + switch (temperatureUnit) { + case TemperatureUnit.Celsius: + return "℃"; + case TemperatureUnit.Fahrenheit: + return "℉"; + } + })(); + + const temperature = (() => { + switch (temperatureUnit) { + case TemperatureUnit.Celsius: + return temperatureInC; + case TemperatureUnit.Fahrenheit: + return Math.round(cToF(temperatureInC)); + } + })(); + + return ( + <> + {temperature} {temperatureLabel} + + ); +} diff --git a/src/features/weather/weatherSlice.ts b/src/features/weather/weatherSlice.ts index bc2954c..74927fa 100644 --- a/src/features/weather/weatherSlice.ts +++ b/src/features/weather/weatherSlice.ts @@ -43,12 +43,17 @@ export type AlertsResult = // Request failed | "failed"; +interface AviationWeather { + taf: aviationWeatherService.TAFReport; + metar?: aviationWeatherService.METARReport; +} + type AviationWeatherResult = // component has requested a weather, to be batched in next bulk request | "pending" // the weather data (finished resolving) - | aviationWeatherService.TAFReport + | AviationWeather // Request failed | "failed" @@ -317,7 +322,7 @@ export const weatherReducer = createSlice({ */ aviationWeatherReceived: ( state, - action: PayloadAction + action: PayloadAction ) => { if (state.aviationWeather === "pending") { state.aviationWeather = action.payload; @@ -732,7 +737,11 @@ export const getWeather = if (!rawTAF) { dispatch(aviationWeatherNotAvailable()); } else { - dispatch(aviationWeatherReceived(rawTAF)); + const rawMETAR = await aviationWeatherService.getMETAR( + rawTAF?.stationId + ); + + dispatch(aviationWeatherReceived({ taf: rawTAF, metar: rawMETAR })); } } catch (e: unknown) { dispatch(aviationWeatherFailed()); diff --git a/src/features/weather/weatherSliceLazy.ts b/src/features/weather/weatherSliceLazy.ts index 9704645..661996b 100644 --- a/src/features/weather/weatherSliceLazy.ts +++ b/src/features/weather/weatherSliceLazy.ts @@ -1,5 +1,5 @@ import { createSelector } from "@reduxjs/toolkit"; -import { ParseError, parseTAFAsForecast } from "metar-taf-parser"; +import { ParseError, parseMetar, parseTAFAsForecast } from "metar-taf-parser"; import { RootState } from "../../store"; const tafReportSelector = (state: RootState) => state.weather.aviationWeather; @@ -16,8 +16,34 @@ export const tafReport = createSelector( return; try { - return parseTAFAsForecast(aviationWeather.raw, { - issued: new Date(aviationWeather.issued), + return parseTAFAsForecast(aviationWeather.taf.raw, { + issued: new Date(aviationWeather.taf.issued), + }); + } catch (e) { + if (e instanceof ParseError) { + console.error(e); + return; + } + throw e; + } + } +); + +export const metarReport = createSelector( + [tafReportSelector], + (aviationWeather) => { + if ( + !aviationWeather || + aviationWeather === "pending" || + aviationWeather === "failed" || + aviationWeather === "not-available" || + !aviationWeather.metar + ) + return; + + try { + return parseMetar(aviationWeather.metar.raw, { + issued: new Date(aviationWeather.metar.observed), }); } catch (e) { if (e instanceof ParseError) { diff --git a/src/helpers/taf.ts b/src/helpers/taf.ts index eef1e7f..22dcf51 100644 --- a/src/helpers/taf.ts +++ b/src/helpers/taf.ts @@ -217,6 +217,7 @@ function convertMilesToUserDistance( switch (distanceUnit) { case DistanceUnit.Kilometers: { if (distanceInMiles === 6) return 10; + if (distanceInMiles === 10) return 16; return distanceInMiles * 1.60934; } case DistanceUnit.Miles: diff --git a/src/services/aviationWeather.ts b/src/services/aviationWeather.ts index 9484e45..1beec74 100644 --- a/src/services/aviationWeather.ts +++ b/src/services/aviationWeather.ts @@ -9,6 +9,7 @@ export interface TAFReport { issued: string; lat: number; lon: number; + stationId: string; } // Proxy to https://www.aviationweather.gov/adds/dataserver_current/httpparam @@ -27,7 +28,13 @@ export async function getTAF({ radialDistance: `35;${lon},${lat}`, hoursBeforeNow: 3, mostRecent: true, - fields: ["raw_text", "issue_time", "latitude", "longitude"].join(","), + fields: [ + "raw_text", + "issue_time", + "latitude", + "longitude", + "station_id", + ].join(","), }, }); const parsed = parser.parse(response.data); @@ -39,6 +46,49 @@ export async function getTAF({ issued: parsed.response.data.TAF.issue_time, lat: +parsed.response.data.TAF.latitude, lon: +parsed.response.data.TAF.longitude, + stationId: parsed.response.data.TAF.station_id, + }; +} + +export interface METARReport { + raw: string; + observed: string; + lat: number; + lon: number; + stationId: string; +} + +// Proxy to https://www.aviationweather.gov/adds/dataserver_current/httpparam +export async function getMETAR( + stationId: string +): Promise { + const response = await axios.get("/api/aviationweather", { + params: { + dataSource: "metars", + requestType: "retrieve", + format: "xml", + mostRecentForEachStation: "constraint", + hoursBeforeNow: 3, + stationString: stationId, + fields: [ + "raw_text", + "observation_time", + "latitude", + "longitude", + "station_id", + ].join(","), + }, + }); + const parsed = parser.parse(response.data); + + if (!parsed.response.data?.METAR) return; + + return { + raw: parsed.response.data.METAR.raw_text, + observed: parsed.response.data.METAR.observation_time, + lat: +parsed.response.data.METAR.latitude, + lon: +parsed.response.data.METAR.longitude, + stationId: parsed.response.data.METAR.station_id, }; } diff --git a/src/shared/RelativeTime.tsx b/src/shared/RelativeTime.tsx new file mode 100644 index 0000000..0c070bb --- /dev/null +++ b/src/shared/RelativeTime.tsx @@ -0,0 +1,35 @@ +import styled from "@emotion/styled"; +import { formatDistanceStrict } from "date-fns"; +import { formatInTimeZone } from "date-fns-tz"; +import { useAppSelector } from "../hooks"; +import useInterval from "../helpers/useInterval"; +import { useState } from "react"; + +const Span = styled.time` + text-decoration: underline; + text-decoration-style: dashed; + text-decoration-color: rgba(255, 255, 255, 0.4); +`; + +interface RelativeTimeProps { + date: Date; +} + +export default function RelativeTime({ date }: RelativeTimeProps) { + const timeZone = useAppSelector((state) => state.weather.timeZone); + const [distance, setDistance] = useState(getDistance()); + + useInterval(() => { + setDistance(getDistance()); + }, 5000); + + function getDistance() { + return formatDistanceStrict(date, new Date(), { addSuffix: true }); + } + + if (!timeZone) throw new Error("timeZone not defined"); + + return ( + {distance} + ); +}