-
-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
19 changed files
with
598 additions
and
119 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import styled from "@emotion/styled"; | ||
import { formatInTimeZone } from "date-fns-tz"; | ||
import { useAppSelector } from "../../hooks"; | ||
import { timeZoneSelector } from "../weather/weatherSlice"; | ||
|
||
const Table = styled.table` | ||
width: 100%; | ||
padding: 0; | ||
border-collapse: collapse; | ||
border: none; | ||
text-align: center; | ||
`; | ||
|
||
const THead = styled.thead` | ||
position: sticky; | ||
top: 0; | ||
transform: translateY(-0.5px); | ||
z-index: 1; | ||
background: var(--bg-bottom-sheet); | ||
`; | ||
|
||
const DayLabelCell = styled.th` | ||
text-align: start; | ||
padding: 8px 16px; | ||
`; | ||
|
||
interface DayProps { | ||
date: Date; | ||
hours: React.ReactNode[]; | ||
} | ||
|
||
export default function Day({ hours, date }: DayProps) { | ||
const timeZone = useAppSelector(timeZoneSelector); | ||
if (!timeZone) throw new Error("timeZone needed"); | ||
|
||
return ( | ||
<Table> | ||
<THead> | ||
<tr> | ||
<DayLabelCell> | ||
{formatInTimeZone(date, timeZone, "eeee, LLL d")} | ||
</DayLabelCell> | ||
</tr> | ||
</THead> | ||
<tbody>{hours}</tbody> | ||
</Table> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { useAppSelector } from "../../hooks"; | ||
import OutlookTable from "./OutlookTable"; | ||
|
||
export default function Outlook() { | ||
const weather = useAppSelector((state) => state.weather.weather); | ||
|
||
if (weather === "failed") return; | ||
if (!weather || weather === "pending") return; | ||
|
||
return <OutlookTable weather={weather} />; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
import { formatInTimeZone } from "date-fns-tz"; | ||
import { timeZoneSelector } from "../weather/weatherSlice"; | ||
import { useAppSelector } from "../../hooks"; | ||
import styled from "@emotion/styled"; | ||
import WindIndicator from "../rap/WindIndicator"; | ||
import WindSpeed from "./WindSpeed"; | ||
import { | ||
faCloudMoon, | ||
faClouds, | ||
faCloudsMoon, | ||
faCloudsSun, | ||
faCloudSun, | ||
faMoon, | ||
faSun, | ||
} from "@fortawesome/pro-duotone-svg-icons"; | ||
import SunCalc from "suncalc"; | ||
import { TemperatureText } from "../rap/cells/Temperature"; | ||
import { Aside } from "../rap/cells/Altitude"; | ||
import { | ||
TemperatureUnit, | ||
TimeFormat, | ||
} from "../rap/extra/settings/settingEnums"; | ||
import { cToF } from "../weather/aviation/DetailedAviationReport"; | ||
import { Observations } from "../weather/header/Weather"; | ||
import { NWSWeatherObservation } from "../../services/nwsWeather"; | ||
import { IconProp } from "@fortawesome/fontawesome-svg-core"; | ||
|
||
const Row = styled.tr<{ day: boolean }>` | ||
display: flex; | ||
> * { | ||
flex: 1; | ||
} | ||
border-bottom: 1px solid #77777715; | ||
background: ${({ day }) => (day ? "#ffffff07" : "transparent")}; | ||
`; | ||
|
||
const TimeCell = styled.td` | ||
font-size: 12px; | ||
display: flex; | ||
align-items: center; | ||
justify-content: center; | ||
`; | ||
|
||
const StyledObservations = styled(Observations)` | ||
font-size: 1em; | ||
margin-right: 0; | ||
`; | ||
|
||
interface OutlookRowProps { | ||
hour: Date; | ||
windDirection: number; | ||
windSpeed: number; | ||
windGust: number; | ||
temperature: number; | ||
observations: NWSWeatherObservation[] | number; | ||
skyCover: number; | ||
} | ||
|
||
export default function OutlookRow({ | ||
hour, | ||
windDirection, | ||
windSpeed, | ||
windGust, | ||
temperature: inCelsius, | ||
observations, | ||
skyCover, | ||
}: OutlookRowProps) { | ||
const timeFormat = useAppSelector((state) => state.user.timeFormat); | ||
|
||
const timeZone = useAppSelector(timeZoneSelector); | ||
if (!timeZone) throw new Error("timeZone needed"); | ||
|
||
const coordinates = useAppSelector((state) => state.weather.coordinates); | ||
if (!coordinates) throw new Error("coordinates not found"); | ||
|
||
const temperatureUnit = useAppSelector((state) => state.user.temperatureUnit); | ||
|
||
const time = formatInTimeZone(hour, timeZone, timeFormatString(timeFormat)); | ||
|
||
const isDay = | ||
SunCalc.getPosition(hour, coordinates.lat, coordinates.lon).altitude > 0; | ||
|
||
const temperatureUnitLabel = (() => { | ||
switch (temperatureUnit) { | ||
case TemperatureUnit.Celsius: | ||
return "C"; | ||
case TemperatureUnit.Fahrenheit: | ||
return "F"; | ||
} | ||
})(); | ||
|
||
const temperature = (() => { | ||
switch (temperatureUnit) { | ||
case TemperatureUnit.Celsius: | ||
return inCelsius; | ||
case TemperatureUnit.Fahrenheit: | ||
return cToF(inCelsius); | ||
} | ||
})(); | ||
|
||
console.log(observations); | ||
|
||
return ( | ||
<Row day={isDay}> | ||
<TimeCell>{time}</TimeCell> | ||
<td | ||
style={{ | ||
maxWidth: "20px", | ||
display: "flex", | ||
alignItems: "center", | ||
justifyContent: "center", | ||
}} | ||
> | ||
<StyledObservations | ||
data={observations} | ||
defaultIcon={getDefaultIcon(skyCover, isDay)} | ||
/> | ||
</td> | ||
<td> | ||
<TemperatureText temperature={inCelsius}> | ||
{Math.round(temperature)} <Aside>°{temperatureUnitLabel}</Aside>{" "} | ||
</TemperatureText> | ||
</td> | ||
<td style={{ maxWidth: "3%" }} /> | ||
<td style={{ textAlign: "start" }}> | ||
<WindSpeed speed={windSpeed} gust={windGust} /> | ||
</td> | ||
<td style={{ textAlign: "start", maxWidth: "16%" }}> | ||
<WindIndicator direction={windDirection} /> | ||
</td> | ||
</Row> | ||
); | ||
} | ||
|
||
function timeFormatString(timeFormat: TimeFormat): string { | ||
switch (timeFormat) { | ||
case TimeFormat.Twelve: | ||
return "hha"; | ||
case TimeFormat.TwentyFour: | ||
return "HHmm"; | ||
} | ||
} | ||
|
||
function getDefaultIcon(skyCover: number, isDay: boolean): IconProp { | ||
switch (true) { | ||
case skyCover < 20: | ||
return isDay ? faSun : faMoon; | ||
case skyCover < 60: | ||
return isDay ? faCloudSun : faCloudMoon; | ||
case skyCover < 80: | ||
return isDay ? faCloudsSun : faCloudsMoon; | ||
default: | ||
return faClouds; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
import { addDays, eachHourOfInterval, startOfDay } from "date-fns"; | ||
import { findValue, NWSWeather } from "../../services/nwsWeather"; | ||
import { Weather } from "../weather/weatherSlice"; | ||
import { OpenMeteoWeather } from "../../services/openMeteo"; | ||
import { useMemo } from "react"; | ||
import OutlookRow from "./OutlookRow"; | ||
import compact from "lodash/fp/compact"; | ||
import styled from "@emotion/styled"; | ||
import Day from "./Day"; | ||
|
||
const Rows = styled.div``; | ||
|
||
interface OutlookTableProps { | ||
weather: Weather; | ||
} | ||
|
||
function getOutlook( | ||
mapFn: (hour: Date, index: number) => React.ReactNode | undefined, | ||
) { | ||
const hours = eachHourOfInterval({ | ||
start: new Date(), | ||
end: addDays(new Date(), 7), | ||
}); | ||
|
||
const data = compact( | ||
hours.map((hour, index) => ({ node: mapFn(hour, index), hour })), | ||
); | ||
|
||
return Object.entries( | ||
Object.groupBy(data, ({ hour }) => startOfDay(hour).getTime()), | ||
).map(([timeStr, hours]) => ({ | ||
date: new Date(+timeStr), | ||
hours: hours!.map(({ node }) => node), | ||
})); | ||
} | ||
|
||
export default function OutlookTable({ weather }: OutlookTableProps) { | ||
const rows = (() => { | ||
if ("properties" in weather) return <NWSOutlookRows weather={weather} />; | ||
return <OpenMeteoOutlookRows weather={weather} />; | ||
})(); | ||
|
||
return <Rows>{rows}</Rows>; | ||
} | ||
|
||
function NWSOutlookRows({ weather }: { weather: NWSWeather }) { | ||
const days = useMemo( | ||
() => | ||
getOutlook((hour, index) => { | ||
const windDirection = findValue( | ||
hour, | ||
weather.properties.windDirection, | ||
)?.value; | ||
const windSpeed = findValue(hour, weather.properties.windSpeed)?.value; | ||
const windGust = findValue(hour, weather.properties.windGust)?.value; | ||
const temperature = findValue( | ||
hour, | ||
weather.properties.temperature, | ||
)?.value; | ||
const observations = findValue(hour, weather.properties.weather)?.value; | ||
const skyCover = findValue(hour, weather.properties.skyCover)?.value; | ||
|
||
if (windDirection == null) return; | ||
if (windSpeed == null) return; | ||
if (windGust == null) return; | ||
if (temperature == null) return; | ||
if (observations == null) return; | ||
if (skyCover == null) return; | ||
|
||
return ( | ||
<OutlookRow | ||
key={index} | ||
hour={hour} | ||
windDirection={windDirection} | ||
windSpeed={windSpeed} | ||
windGust={windGust} | ||
temperature={temperature} | ||
observations={observations} | ||
skyCover={skyCover} | ||
/> | ||
); | ||
}), | ||
[weather], | ||
); | ||
|
||
return days.map(({ date, hours }, index) => ( | ||
<Day key={index} date={date} hours={hours} /> | ||
)); | ||
} | ||
function OpenMeteoOutlookRows({ weather }: { weather: OpenMeteoWeather }) { | ||
const days = useMemo( | ||
() => | ||
getOutlook((hour, index) => { | ||
const data = weather.byUnixTimestamp[hour.getTime() / 1_000]; | ||
|
||
if (!data) return; | ||
|
||
return ( | ||
<OutlookRow | ||
key={index} | ||
hour={hour} | ||
windDirection={data.windDirection} | ||
windSpeed={data.windSpeed} | ||
windGust={data.windGust} | ||
temperature={data.temperature} | ||
observations={data.weather} | ||
skyCover={data.cloudCover} | ||
/> | ||
); | ||
}), | ||
[weather], | ||
); | ||
|
||
return days.map(({ date, hours }, index) => ( | ||
<Day key={index} date={date} hours={hours} /> | ||
)); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import styled from "@emotion/styled"; | ||
import { formatWind } from "../../helpers/taf"; | ||
import { useAppSelector } from "../../hooks"; | ||
import { SpeedUnit } from "metar-taf-parser"; | ||
import { toMph, WindIcon } from "../weather/header/Wind"; | ||
import { HeaderType } from "../weather/WeatherHeader"; | ||
import { faWindsock } from "@fortawesome/pro-duotone-svg-icons"; | ||
|
||
const Speed = styled.div` | ||
word-spacing: -2px; | ||
`; | ||
|
||
const StyledWindIcon = styled(WindIcon)` | ||
margin-right: 12px; | ||
`; | ||
interface WindSpeedProps { | ||
speed: number; | ||
gust: number; | ||
} | ||
|
||
export default function WindSpeed({ speed, gust }: WindSpeedProps) { | ||
const speedUnit = useAppSelector((state) => state.user.speedUnit); | ||
const speedFormatted = formatWind( | ||
speed, | ||
SpeedUnit.KilometersPerHour, | ||
speedUnit, | ||
false, | ||
); | ||
const gustFormatted = formatWind( | ||
gust, | ||
SpeedUnit.KilometersPerHour, | ||
speedUnit, | ||
false, | ||
); | ||
|
||
return ( | ||
<> | ||
<Speed> | ||
<StyledWindIcon | ||
headerType={HeaderType.Normal} | ||
speed={Math.round(toMph(speed))} | ||
gust={Math.round(toMph(gust))} | ||
icon={faWindsock} | ||
/> | ||
{speedFormatted}G{gustFormatted} | ||
</Speed> | ||
</> | ||
); | ||
} |
Oops, something went wrong.