diff --git a/challenge/package.json b/challenge/package.json index fac12b9..7433fe1 100644 --- a/challenge/package.json +++ b/challenge/package.json @@ -18,6 +18,7 @@ "chokidar": "^3.5.3", "express": "^4.18.2", "local-storage": "^2.0.0", + "lucide-react": "^0.488.0", "prop-types": "^15.8.1", "react": "^18.0.0", "react-dom": "^18.0.0", @@ -50,8 +51,8 @@ }, "devDependencies": { "autoprefixer": "^10.4.20", + "dotenv": "^16.4.5", "postcss": "^8.4.45", - "tailwindcss": "^3.4.10", - "dotenv": "^16.4.5" + "tailwindcss": "^3.4.10" } } diff --git a/challenge/src/components/Announcements.jsx b/challenge/src/components/Announcements.jsx new file mode 100644 index 0000000..4df6d87 --- /dev/null +++ b/challenge/src/components/Announcements.jsx @@ -0,0 +1,124 @@ +import React, { useState, useEffect } from "react"; +import { Cloud } from "lucide-react"; +import "./announcements.css"; + +export default function AnnouncementsPage() { + const [current, setCurrent] = useState(null); + const [forecast, setForecast] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [unit, setUnit] = useState("imperial"); + + const API_KEY = "31893d8dd93053c2be70003e332f20a5"; + const lat = 40.5408; + const lon = -74.4815; + const currentUrl = `https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&units=${unit}&appid=${API_KEY}`; + const forecastUrl = `https://api.openweathermap.org/data/2.5/forecast?lat=${lat}&lon=${lon}&units=${unit}&appid=${API_KEY}`; + + useEffect(() => { + const ctrl = new AbortController(); + const signal = ctrl.signal; + + async function fetchData() { + setLoading(true); + setError(null); + + try { + const [resCur, resFor] = await Promise.all([ + fetch(currentUrl, { signal }), + fetch(forecastUrl, { signal }), + ]); + + if (resCur.status === 401 || resFor.status === 401) { + throw new Error("Unauthorized: invalid API key"); + } + if (!resCur.ok) + throw new Error(`Current weather error: ${resCur.status}`); + if (!resFor.ok) throw new Error(`Forecast error: ${resFor.status}`); + + setCurrent(await resCur.json()); + setForecast(await resFor.json()); + } catch (err) { + if (err.name !== "AbortError") setError(err.message); + } finally { + setLoading(false); + } + } + + fetchData(); + return () => ctrl.abort(); + }, [currentUrl, forecastUrl, unit]); + + if (loading) return

Loading weather…

; + if (error) return

Error: {error}

; + + const localMs = (current.dt + current.timezone) * 1000; + const date = new Date(localMs); + const weekday = date.toLocaleDateString("en-US", { weekday: "long" }); + const time = date.toLocaleTimeString("en-US", { + hour: "numeric", + minute: "2-digit", + hour12: true, + }); + const pop = Math.round((forecast.list[0]?.pop || 0) * 100); + const temp = Math.round(current.main.temp); + const humidity = current.main.humidity; + const windSpeed = Math.round(current.wind.speed); + const weatherMain = current.weather[0].main.toLowerCase(); + const weatherDesc = current.weather[0].description; + + return ( +
+
+
+ +
+
+ {temp} +
+ + | + +
+
+ Precipitation: {pop}% + Humidity: {humidity}% + + Wind: {windSpeed} {unit === "imperial" ? "mph" : "m/s"} + +
+
+
+
+ +
+
Weather
+
+ {weekday} {time} +
+
{weatherDesc}
+
+
+ + {weatherMain !== "rain" && ( +
+ THE WEATHER IS CONDUCIVE TO MEDITATION AT HILL CENTER +
+ )} +
+ ); +} diff --git a/challenge/src/components/Pages/Home.jsx b/challenge/src/components/Pages/Home.jsx index 9c0283d..4fb910f 100644 --- a/challenge/src/components/Pages/Home.jsx +++ b/challenge/src/components/Pages/Home.jsx @@ -16,6 +16,7 @@ import GetName from "../GetName"; import RandomColorButton from "../RandomColorButton"; import MoviePage from "../MoviePage"; import Pokesearch from "../Pokesearch"; +import Announcements from "../Announcements"; import News from "../News"; const Home = (props) => { @@ -71,6 +72,7 @@ const Home = (props) => { + diff --git a/challenge/src/components/announcements.css b/challenge/src/components/announcements.css new file mode 100644 index 0000000..70c8f5a --- /dev/null +++ b/challenge/src/components/announcements.css @@ -0,0 +1,115 @@ +.status { + padding: 16px; + text-align: center; + font-family: sans-serif; +} +.status.error { + color: #dc2626; +} + +.announcements-bar { + width: 100%; + font-family: sans-serif; +} + +.announcements-container { + width: 500px; + border-top-right-radius: 25px; + border-top-left-radius: 25px; + background: #ffffff; + margin: 0 auto; + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px; +} + +.announcements-left { + display: flex; + align-items: center; + gap: 16px; +} + +.announcements-icon { + width: 40px; + height: 40px; + color: #000000; +} + +.announcements-temp-row { + display: flex; + align-items: flex-start; + gap: 8px; +} + +.announcements-temp { + font-size: 3rem; + font-weight: bold; + color: #111827; +} + +.unit-toggle { + display: flex; + align-items: flex-start; + gap: 4px; + font-size: 0.875rem; +} +.unit-button { + background: none; + border: none; + padding: 2px; + cursor: pointer; + color: #4b5563; +} + +.unit-button.selected { + text-decoration: underline; + color: #111827; +} + +.separator { + color: #9ca3af; +} + +.stats { + margin-top: 4px; + font-size: 0.75rem; + color: #4b5563; + display: flex; + flex-direction: column; + gap: 2px; +} + +.announcements-right { + text-align: right; +} + +.weather-title { + font-size: 1.125rem; + font-weight: 600; + color: #111827; +} + +.weather-time { + font-size: 0.875rem; + color: #6b7280; +} + +.weather-desc { + margin-top: 4px; + font-size: 0.875rem; + color: #6b7280; + text-transform: capitalize; +} + +.meditation-banner { + background: #ecfdf5; + color: #047857; + padding: 16px; + text-align: center; + font-weight: bold; + width: 500px; + border-bottom-right-radius: 25px; + border-bottom-left-radius: 25px; + margin: 0 auto; +}