Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ module.exports = {
es6: true,
node: true,
},
extends: ["react-app"],
globals: {
Atomics: "readonly",
SharedArrayBuffer: "readonly",
Expand All @@ -15,5 +14,6 @@ module.exports = {
},
ecmaVersion: 2018,
},
extends: ["react-app"],
rules: {},
};
13 changes: 13 additions & 0 deletions src/App.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@ import React from "react";
import ReactDOM from "react-dom";
import App from "./App";

const originalFetch = window.fetch;
const mockFetch = jest.fn(() => new Promise(() => {}));

beforeAll(() => {
window.fetch = mockFetch;
global.fetch = mockFetch;
});

afterAll(() => {
window.fetch = originalFetch;
global.fetch = originalFetch;
});

it("renders without crashing", () => {
const div = document.createElement("div");
ReactDOM.render(<App />, div);
Expand Down
72 changes: 72 additions & 0 deletions src/components/weatherWidget/WeatherWidget.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
.weather-widget {
display: inline-flex;
flex-direction: column;
gap: 0.75rem;
padding: 1rem 1.1rem;
margin-top: 1.5rem;
border: 1px solid;
border-radius: 18px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
min-width: 260px;
max-width: 360px;
}

.weather-widget__header {
display: flex;
flex-direction: column;
gap: 0.2rem;
}

.weather-widget__label {
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
}

.weather-widget__location {
font-size: 0.95rem;
line-height: 1.2;
}

.weather-widget__state {
font-size: 0.95rem;
line-height: 1.4;
}

.weather-widget__content {
display: flex;
align-items: center;
gap: 0.9rem;
}

.weather-widget__icon {
font-size: 2rem;
width: 2rem;
text-align: center;
}

.weather-widget__reading {
display: flex;
flex-direction: column;
gap: 0.15rem;
}

.weather-widget__temperature {
font-size: 2rem;
font-weight: 700;
line-height: 1;
font-family: "Google Sans Bold";
}

.weather-widget__condition {
font-size: 0.95rem;
line-height: 1.3;
}

@media (max-width: 768px) {
.weather-widget {
width: 100%;
max-width: 100%;
}
}
148 changes: 148 additions & 0 deletions src/components/weatherWidget/WeatherWidget.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import React, { useEffect, useState } from "react";
import "./WeatherWidget.css";

const weatherCodeMap = {
0: { label: "Clear sky", icon: "fa-sun" },
1: { label: "Mostly clear", icon: "fa-sun" },
2: { label: "Partly cloudy", icon: "fa-cloud-sun" },
3: { label: "Overcast", icon: "fa-cloud" },
45: { label: "Fog", icon: "fa-smog" },
48: { label: "Rime fog", icon: "fa-smog" },
51: { label: "Light drizzle", icon: "fa-cloud-rain" },
53: { label: "Drizzle", icon: "fa-cloud-rain" },
55: { label: "Heavy drizzle", icon: "fa-cloud-showers-heavy" },
61: { label: "Light rain", icon: "fa-cloud-rain" },
63: { label: "Rain", icon: "fa-cloud-showers-heavy" },
65: { label: "Heavy rain", icon: "fa-cloud-showers-heavy" },
71: { label: "Light snow", icon: "fa-snowflake" },
73: { label: "Snow", icon: "fa-snowflake" },
75: { label: "Heavy snow", icon: "fa-snowflake" },
80: { label: "Rain showers", icon: "fa-cloud-showers-heavy" },
81: { label: "Rain showers", icon: "fa-cloud-showers-heavy" },
82: { label: "Violent rain showers", icon: "fa-bolt" },
95: { label: "Thunderstorm", icon: "fa-bolt" },
96: { label: "Thunderstorm with hail", icon: "fa-bolt" },
99: { label: "Thunderstorm with hail", icon: "fa-bolt" },
};

function getWeatherInfo(weatherCode) {
return weatherCodeMap[weatherCode] || {
label: "Weather update",
icon: "fa-cloud-sun",
};
}

export default function WeatherWidget({
theme,
label = "Pontianak, West Borneo",
latitude = 0.0263,
longitude = 109.3425,
}) {
const [status, setStatus] = useState("loading");
const [weather, setWeather] = useState(null);
const [error, setError] = useState("");

useEffect(() => {
let isActive = true;

if (typeof window === "undefined" || typeof window.fetch !== "function") {
setStatus("error");
setError("Weather data is unavailable in this browser.");
return undefined;
}

setStatus("loading");
setError("");

const request = window.fetch(
`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&current_weather=true&timezone=Asia%2FJakarta`
);

if (!request || typeof request.then !== "function") {
setStatus("error");
setError("Weather request could not be started.");
return undefined;
}

request
.then((response) => {
if (!response.ok) {
throw new Error(`Weather request failed with status ${response.status}.`);
}

return response.json();
})
.then((data) => {
if (!isActive) {
return;
}

if (!data.current_weather) {
throw new Error("Weather data is missing from the response.");
}

setWeather(data.current_weather);
setStatus("success");
})
.catch((fetchError) => {
if (!isActive) {
return;
}

setStatus("error");
setError(fetchError.message);
});

return () => {
isActive = false;
};
}, [latitude, longitude]);

const weatherInfo = weather ? getWeatherInfo(weather.weathercode) : null;

return (
<div
className="weather-widget"
style={{
backgroundColor: theme.body,
borderColor: theme.highlight,
color: theme.text,
}}
>
<div className="weather-widget__header">
<span className="weather-widget__label" style={{ color: theme.secondaryText }}>
Live temperature (°C)
</span>
<span className="weather-widget__location" style={{ color: theme.secondaryText }}>
{label}
</span>
</div>

{status === "loading" && (
<div className="weather-widget__state" style={{ color: theme.secondaryText }}>
Loading temperature...
</div>
)}

{status === "error" && (
<div className="weather-widget__state" style={{ color: theme.secondaryText }}>
{error}
</div>
)}

{status === "success" && weather && weatherInfo && (
<div className="weather-widget__content" aria-live="polite">
<i className={`fas ${weatherInfo.icon} weather-widget__icon`} aria-hidden="true" />
<div className="weather-widget__reading">
<div className="weather-widget__temperature">
{Math.round(weather.temperature)}°C
</div>
<div className="weather-widget__condition" style={{ color: theme.secondaryText }}>
{weatherInfo.label} · Wind {Math.round(weather.windspeed)} km/h
</div>
</div>
</div>
)}
</div>
);
}
10 changes: 10 additions & 0 deletions src/containers/greeting/Greeting.css
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@
height: auto;
}

.greeting-weather-div {
display: flex;
justify-content: center;
margin-top: 1rem;
}

/* Media Query */
@media (max-width: 1380px) {
.greeting-text {
Expand Down Expand Up @@ -87,6 +93,10 @@
display: block;
}

.greeting-weather-div {
margin-top: 1.25rem;
}

.portfolio-repo-btn-div {
width: 100%;
display: flex;
Expand Down
4 changes: 4 additions & 0 deletions src/containers/greeting/Greeting.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Button from "../../components/button/Button";
import { greeting } from "../../portfolio";
import { Fade } from "react-reveal";
import FeelingProud from "./FeelingProud";
import WeatherWidget from "../../components/weatherWidget/WeatherWidget";

export default function Greeting(props) {
const theme = props.theme;
Expand Down Expand Up @@ -50,6 +51,9 @@ export default function Greeting(props) {
src={require("../../assests/images/feelingProud.svg")}
></img> */}
<FeelingProud theme={theme} />
<div className="greeting-weather-div">
<WeatherWidget theme={theme} />
</div>
</div>
</div>
</div>
Expand Down
Loading