diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 032e444..ddbea2f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -13,9 +13,9 @@ jobs: if: ${{ !startsWith(github.event.head_commit.message, '[skip build]') }} steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 - name: Setup pnpm - uses: pnpm/action-setup@v2 + uses: pnpm/action-setup@v6 with: version: latest - name: Install Vercel CLI @@ -25,7 +25,7 @@ jobs: - name: Install Dependencies run: pnpm i --frozen-lockfile - name: Lint - run: pnpm run lint + run: pnpm exec eslint - name: Build Project Artifacts run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }} - name: Deploy Project Artifacts to Vercel diff --git a/.github/workflows/preview-snapshot.yml b/.github/workflows/preview-snapshot.yml index b2659a2..3321daf 100644 --- a/.github/workflows/preview-snapshot.yml +++ b/.github/workflows/preview-snapshot.yml @@ -9,9 +9,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 - name: Setup pnpm - uses: pnpm/action-setup@v2 + uses: pnpm/action-setup@v6 with: version: latest - name: Install Dependencies @@ -26,11 +26,11 @@ jobs: NEXT_PUBLIC_IP_API_KEY: ${{ secrets.NEXT_PUBLIC_IP_API_KEY }} NEXT_PUBLIC_WEATHER_API_KEY: ${{ secrets.NEXT_PUBLIC_WEATHER_API_KEY }} - name: Upload Preview - uses: ryand56/r2-upload-action@v1.2 + uses: ryand56/r2-upload-action@latest with: r2-account-id: ${{ secrets.R2_ACCOUNT_ID }} r2-access-key-id: ${{ secrets.R2_ACCESS_KEY_ID }} r2-secret-access-key: ${{ secrets.R2_SECRET_ACCESS_KEY }} r2-bucket: ${{ secrets.R2_BUCKET }} source-dir: scripts/dist/ - destination-dir: projects/weatherscan-rewritten/autopreviews/ \ No newline at end of file + destination-dir: projects/weatherscan-rewritten/autopreviews/ diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 490877c..ffdf996 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -12,14 +12,14 @@ jobs: if: ${{ !startsWith(github.event.head_commit.message, '[skip build]') }} steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 - name: Setup pnpm - uses: pnpm/action-setup@v2 + uses: pnpm/action-setup@v6 with: version: latest - name: Install Dependencies run: pnpm i --frozen-lockfile - name: Lint - run: pnpm run lint + run: pnpm exec eslint - name: Build - run: pnpm run build \ No newline at end of file + run: pnpm run build diff --git a/components/CCIcon.tsx b/components/CCIcon.tsx index 25396a6..a48c8b6 100644 --- a/components/CCIcon.tsx +++ b/components/CCIcon.tsx @@ -6,20 +6,13 @@ interface CCIconProps { windData: number } -const CCIcon = ({ iconCode, windData }: CCIconProps) => { - const [icon, setIcon] = React.useState(Icons2010.UNK); - - React.useEffect(() => { - if (typeof iconCode === "number" && typeof windData === "number") { - const mapped = getIcon(iconCode, windData); - setIcon(mapped); - } - }, [iconCode, windData]); +const CCIcon = ({ iconCode = 44, windData = 0 }: CCIconProps) => { + const icon = getIcon(iconCode, windData); return (
); diff --git a/components/Current.tsx b/components/Current.tsx index 562d9a0..744d55f 100644 --- a/components/Current.tsx +++ b/components/Current.tsx @@ -7,29 +7,34 @@ interface CurrentProps { } const Current = ({ temp, info }: CurrentProps) => { - const [cycle, setCycle] = React.useState([]); + const cycle = React.useMemo(() => { + if (!info || info.phrase === "") { + return []; + } + + return [ + `visibility ${info.visib} ${info.visib != 1 ? "miles" : "mile"}`, + `UV index ${info.uvIndex}`, + info.phrase, + `wind ${info.wind}`, + `humidity ${info.humidity}%`, + `dew point ${info.dewpt}°`, + `pressure ${info.pres}` + ]; + }, [info]); + const [idx, setIdx] = React.useState(0); - const [infoMsg, setInfoMsg] = React.useState(""); + const infoMsg = cycle[idx] ?? ""; React.useEffect(() => { let intervalTimer: NodeJS.Timeout; if (info) { if (info.phrase !== "") { - const tempCycle = [ - `visibility ${info.visib} ${info.visib != 1 ? "miles" : "mile"}`, - `UV index ${info.uvIndex}`, - info.phrase, - `wind ${info.wind}`, - `humidity ${info.humidity}%`, - `dew point ${info.dewpt}°`, - `pressure ${info.pres}` - ]; - - setCycle(tempCycle); + if (cycle.length === 0) return; intervalTimer = setInterval(() => { setIdx((idx) => { - if (idx >= (tempCycle.length - 1)) { + if (idx >= (cycle.length - 1)) { return 0; } else { return idx + 1; @@ -40,16 +45,7 @@ const Current = ({ temp, info }: CurrentProps) => { } return () => clearInterval(intervalTimer); - }, [info]); - - React.useEffect(() => { - if (cycle) { - if (cycle.length > 0) { - const msg = cycle[idx]; - setInfoMsg(msg); - } - } - }, [idx]); + }, [info, cycle.length]); return (
diff --git a/components/CustomMarquee/index.tsx b/components/CustomMarquee/index.tsx index 806f83a..4959477 100644 --- a/components/CustomMarquee/index.tsx +++ b/components/CustomMarquee/index.tsx @@ -40,69 +40,59 @@ const CustomMarquee = ({ onCycleComplete, children }: CustomMarqueeProps) => { - const [isMounted, setIsMounted] = React.useState(false); - - React.useEffect(() => { - setIsMounted(true); - }, []); - const rgbaGradientColor = `rgba(${gradientColor[0]}, ${gradientColor[1]}, ${gradientColor[2]})`; return ( - <> - {!isMounted ? null : ( +
+ {gradient && (
- {gradient && ( -
- )} -
- {children} -
- -
+ className={styles.overlay} + /> )} - +
+ {children} +
+ +
); }; diff --git a/components/DateTime.tsx b/components/DateTime.tsx index 3d1e1bc..72a2f2b 100644 --- a/components/DateTime.tsx +++ b/components/DateTime.tsx @@ -1,36 +1,47 @@ import * as React from "react"; +import { useRouter } from "next/router"; interface DateTimeProps { tz: string } const DateTime = ({ tz }: DateTimeProps) => { + const router = useRouter(); const [date, setDate] = React.useState("Jan 01"); const [time, setTime] = React.useState("12:00:00pm"); - const updateTime = () => { - const date = new Date(); - - const formatDate = date.toString().slice(4, 10).trimEnd(); - const formatTime = date.toLocaleTimeString("en-US", { - hour: "numeric", - hour12: true, - minute: "numeric", - second: "numeric", - timeZone: tz - }).replace(/ /g, "").toLowerCase(); - - setDate(formatDate); - setTime(formatTime); + const stdTimezoneOffset = (date: Date) => { + var jan = new Date(date.getFullYear(), 0, 1); + var jul = new Date(date.getFullYear(), 6, 1); + return Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset()); + }; + + const isDstObserved = (date: Date) => { + return date.getTimezoneOffset() < stdTimezoneOffset(date); }; React.useEffect(() => { - let interval: NodeJS.Timeout; + if (tz === "") return; + + const update = () => { + const date = new Date(); + + const formatDate = date.toString().slice(4, 10).trimEnd(); + const formatTime = date.toLocaleTimeString("en-US", { + hour: "numeric", + hour12: true, + minute: "numeric", + second: "numeric", + timeZone: tz + }).replace(/ /g, "").toLowerCase(); + + setDate(formatDate); + setTime(formatTime); + }; + + update(); - if (tz !== "") { - updateTime(); - interval = setInterval(updateTime, 500); - } + const interval = setInterval(update, 500); return () => clearInterval(interval); }, [tz]); diff --git a/components/Display.tsx b/components/Display.tsx index 8e2e11c..113a6c6 100644 --- a/components/Display.tsx +++ b/components/Display.tsx @@ -1,11 +1,23 @@ import * as React from "react"; -import type { Location, CurrentCond, Alert } from "../hooks/useWeather"; +import { + TemperatureUnit, + Location, + ExtraLocation, + ExtraInfo, + CurrentCond, + CurrentConds, + Alert, + MarqueeLocation, + MarqueeCities +} from "../hooks/useWeather"; import { defaults, currentDefaults, getMainLocation, getClosestLocation, + getExtraLocations, getCurrentCond, + getExtraCond, getAlerts, getAlertText } from "../hooks/useWeather"; @@ -17,7 +29,10 @@ import CCIcon from "./CCIcon"; import Current from "./Current"; import LogoArea from "./LogoArea"; import InfoMarquee from "./Marquee"; -import MarqueeSevere from "./MarqueeSevere"; +//import MarqueeSevere from "./MarqueeSevere"; +const MarqueeSevere = React.lazy(() => import( + "./MarqueeSevere" /* webpackChunkName: "marqueeSevere" */ +)); const resizeWindow = ( mainRef: React.MutableRefObject, @@ -42,15 +57,28 @@ const resizeWindow = ( interface DisplayProps { isReady: boolean + debug: boolean winSize: number[] location: string language: string + units: TemperatureUnit + muteSevere: boolean setMainVol: React.Dispatch> } -const Display = ({ isReady, winSize, location, language, setMainVol }: DisplayProps) => { +const Display = ({ + isReady, + debug, + winSize, + location, + language, + units, + muteSevere, + setMainVol +}: DisplayProps) => { + const [cityIntroLoaded, setCityIntroLoaded] = React.useState(false); const [innerWidth, innerHeight] = winSize; - const mainRef = React.useRef(); + const mainRef = React.useRef(null); const resize = () => { resizeWindow(mainRef, innerWidth, innerHeight); @@ -69,44 +97,92 @@ const Display = ({ isReady, winSize, location, language, setMainVol }: DisplayPr const [locInfo, setLocInfo] = React.useState>(defaults); const [currentInfo, setCurrentInfo] = React.useState>(currentDefaults); + const [currentExtra, setCurrentExtra] = React.useState({ + details: { + name: "", + lat: 0, + lon: 0 + } + }); + const [extraInfo, setExtraInfo] = React.useState>(new Map()); const [alerts, setAlerts] = React.useState([]); - const [focusedAlert, setFocusedAlert] = React.useState(null); + const focusedAlert = alerts[0] ?? null; const [focusedAlertText, setFocusedAlertText] = React.useState(null); + const [marqueeCities, setMarqueeCities] = React.useState(MarqueeCities); + // Location handler React.useEffect(() => { if (isReady) { if (location !== "") { - getMainLocation(location, language).then(data => { + getMainLocation(location, { language }).then(data => { setLocInfo(data); - }).catch(err => { - console.error(err); - }); + }).catch(err => console.error(err)); } else { getClosestLocation().then(data => { setLocInfo(data); - }).catch(err => { - console.error(err); - }); + }).catch(err => console.error(err)); } } }, [isReady, location]); - const fetchCurrent = (lat: number, lon: number) => { - getCurrentCond(lat, lon, language).then(data => { - setCurrentInfo(data); - }).catch(err => { - console.error(err); + const fetchCurrent = (lat: number, lon: number) : Promise => { + return new Promise((resolve, reject) => { + getCurrentCond(lat, lon, { + language, + units + }).then(data => { + setCurrentInfo(data); + resolve(data); + }).catch(err => reject(err)); + }); + }; + + const fetchExtra = async (lat: number, lon: number, initial?: Map) => { + const data = await getExtraLocations(lat, lon, { language }); + + const tempMap = new Map(initial ?? []); + const latLonMap = new Map(); + const queryLatLons: string[] = []; + + // Dirty way of doing things + for (let i = 0; i < data.length; i++) { + const location = data[i]; + const latLon = `${location.lat},${location.lon}`; + if (debug) console.log(latLon); + queryLatLons.push(latLon); + latLonMap.set(latLon, location.displayName); + } + + const extras = await getExtraCond(queryLatLons, { + language, + units }); + for (const [key, value] of Object.entries(extras)) { + const displayName = latLonMap.get(key); + if (debug) { + console.log(key); + console.log(displayName, value); + } + const latLon = key.split(","); + tempMap.set(displayName, { + details: { + name: displayName, + lat: parseFloat(latLon[0]), + lon: parseFloat(latLon[1]) + }, + current: value + }); + } + + setExtraInfo(tempMap); }; const fetchAlerts = (lat: number, lon: number) => { - getAlerts(lat, lon, language).then(data => { + getAlerts(lat, lon, { language, units }).then(data => { setAlerts(data); - }).catch(err => { - console.error(err); - }); + }).catch(err => console.error(err)); }; // Current conditions and alerts handler @@ -117,11 +193,26 @@ const Display = ({ isReady, winSize, location, language, setMainVol }: DisplayPr let intervalTimer: NodeJS.Timeout; if (lat && lon) { - fetchCurrent(lat, lon); - fetchAlerts(lat, lon); - intervalTimer = setInterval(() => { - fetchCurrent(lat, lon); + const fetchCallback = (data: CurrentCond) => { + const tempMap = new Map(); + const currentEx = { + details: { + name: locInfo.city, + lat, + lon + }, + current: data + }; + tempMap.set(locInfo.city, currentEx); + setCurrentExtra(currentEx); + + fetchExtra(lat, lon, tempMap); fetchAlerts(lat, lon); + }; + + fetchCurrent(lat, lon).then(fetchCallback).catch(err => console.error(err)); + intervalTimer = setInterval(() => { + fetchCurrent(lat, lon).then(fetchCallback).catch(err => console.error(err)); }, 300000); } @@ -130,17 +221,53 @@ const Display = ({ isReady, winSize, location, language, setMainVol }: DisplayPr }, [isReady, locInfo.latitude, locInfo.longitude]); React.useEffect(() => { - if (alerts.length > 0) { - setFocusedAlert(alerts[0]); - getAlertText(alerts[0].detailKey, language).then(texts => { + if (!isReady || !marqueeCities) return; + let latLons: string[] = []; + + for (const city of marqueeCities) { + latLons.push(`${city.latitude},${city.longitude}`); + } + + const ExtraCondCallback = (ret: CurrentConds) => { + for (const latLon of latLons) { + const cond = ret[latLon]; + const [lat, lon] = latLon.split(","); + const parsedLat = parseFloat(lat); + const parsedLon = parseFloat(lon); + + const city = marqueeCities.find(c => c.latitude === parsedLat && c.longitude === parsedLon); + if (city) city.observations = cond; + } + }; + + getExtraCond(latLons, { language, units }).then(ExtraCondCallback).catch(err => console.error(err)); + const interval = setInterval(() => { + getExtraCond(latLons, { language, units }).then(ExtraCondCallback).catch(err => console.error(err)); + }, 300000); + + return () => clearInterval(interval); + }, [isReady, marqueeCities]); + + React.useEffect(() => { + const alert = alerts[0]; + if (!alert) return; + + getAlertText(alert.detailKey, { language }) + .then(texts => { if (texts.length > 0) { setFocusedAlertText(texts[0].description); } - }).catch(err => { - console.error(err); }) - } - }, [alerts.length]); + .catch(console.error); + }, [alerts, language]); + + React.useEffect(() => { + if (debug) console.log(currentExtra); + }, [currentExtra]); + + React.useEffect(() => { + if (debug) console.log(extraInfo); + }, [extraInfo]); /* background - {isReady && } + {(isReady && locInfo && currentExtra && extraInfo.size !== 0) && } {locInfo.timezone !== "" && } {locInfo.city !== "" &&
- {focusedAlert && }
); diff --git a/components/Intro.tsx b/components/Intro.tsx index e6f3292..9933145 100644 --- a/components/Intro.tsx +++ b/components/Intro.tsx @@ -35,8 +35,8 @@ interface IntroProps { const Intro = ({ winSize, callback }: IntroProps) => { const [innerWidth, innerHeight] = winSize; - const mainRef = React.useRef(); - const intellistarRef = React.useRef(); + const mainRef = React.useRef(null); + const intellistarRef = React.useRef(null); function resize() { resizeWindow(mainRef, innerWidth, innerHeight); @@ -94,7 +94,7 @@ const Intro = ({ winSize, callback }: IntroProps) => { }, [intellistarRef]); return ( -
+
headend id:
@@ -110,6 +110,7 @@ const Intro = ({ winSize, callback }: IntroProps) => {
Save
weatherscan + Now rewritten!