From 43262846a588a06dc4750c2f9b8dadaab959ca50 Mon Sep 17 00:00:00 2001 From: "theis.gaedigk" Date: Sun, 3 Aug 2025 19:10:20 +0200 Subject: [PATCH] feat: implement weather fetching API and update frontend components for improved user experience --- backend/routes/api.js | 63 +++++++++++++++++++ backend/server.js | 6 +- backend/views/index.ejs | 17 ++--- docker-compose.yml | 42 ++++++------- frontend/src/components/ChangePreferences.tsx | 8 +-- frontend/src/components/Header.tsx | 11 ++-- frontend/src/components/WeatherForm.tsx | 14 +++-- frontend/src/utils/apiFunc.ts | 56 ++++++++--------- 8 files changed, 140 insertions(+), 77 deletions(-) create mode 100644 backend/routes/api.js diff --git a/backend/routes/api.js b/backend/routes/api.js new file mode 100644 index 0000000..4e61d80 --- /dev/null +++ b/backend/routes/api.js @@ -0,0 +1,63 @@ +import { Router } from "express"; +import dotenv from "dotenv"; + +const router = Router(); +dotenv.config(); + +router.get("/fetchWeather", async (req, res) => { + const city = req.query.city; + const units = req.query.units; + const apiKey = process.env.API_KEY; + + try { + const locationResponse = await fetch( + `https://api.openweathermap.org/geo/1.0/direct?q=${city}&appid=${apiKey}` + ); + if (!locationResponse.ok) { + return res.status(404).json({ + error: "Error fetching location data. (Error: x32)", + success: "false", + data: null, + }); + } + const location = await locationResponse.json(); + if (!location || !location[0]) { + return res.status(404).json({ + error: "Location not found.", + success: "false", + data: null, + }); + } + + const lat = location[0].lat; + const lon = location[0].lon; + + const weatherResponse = await fetch( + `https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&appid=${apiKey}&units=${units}` + ); + + if (!weatherResponse.ok) { + return res.status(500).json({ + error: "Unexpected error! (Error: x33)", + success: "false", + data: null, + }); + } + + const weather = await weatherResponse.json(); + + res.status(200).json({ + error: "false", + success: "Weather data fetched successfully!", + data: weather, + }); + } catch (err) { + res.status(500).json({ + error: "Server error", + success: "false", + data: null, + }); + } +}); + +export default router; diff --git a/backend/server.js b/backend/server.js index 1484b05..a50e483 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,7 +1,7 @@ import express from "express"; import cors from "cors"; -import env from "dotenv"; -env.config(); +import apiRouter from "./routes/api.js"; + const app = express(); const port = 7001; @@ -10,6 +10,8 @@ app.use(express.urlencoded({ extended: true })); app.set("view engine", "ejs"); app.use(express.json()); +app.use("/api", apiRouter); + app.get("/", (req, res) => { res.render("index.ejs", { title: port }); }); diff --git a/backend/views/index.ejs b/backend/views/index.ejs index 96ced98..6d616c7 100644 --- a/backend/views/index.ejs +++ b/backend/views/index.ejs @@ -1,11 +1,12 @@ - - - + + + Backend | <%= title %> - - - You have reached the backend views index page. - - \ No newline at end of file + + +

You have reached the backend views index page!

+

Currently, there is nothing to display!

+ + diff --git a/docker-compose.yml b/docker-compose.yml index 73b316d..8ec7083 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,26 +1,26 @@ services: - frontend: - container_name: weather-frontend - build: ./frontend + # frontend: + # container_name: weather-frontend + # build: ./frontend + # ports: + # - "7002:7002" + # # networks: + # # - proxynet + # environment: + # - CHOKIDAR_USEPOLLING=true + # volumes: + # - ./frontend:/app + # - /app/node_modules + # restart: unless-stopped + backend: + container_name: weather-backend + build: ./backend ports: - - "7002:7002" - networks: - - proxynet - environment: - - CHOKIDAR_USEPOLLING=true + - "7001:7001" volumes: - - ./frontend:/app + - ./backend:/app - /app/node_modules restart: unless-stopped -# backend: -# container_name: backend -# build: ./backend -# ports: -# - "7001:7001" -#volumes: -# - ./backend:/bikelane-backend -# restart: unless-stopped - -networks: - proxynet: - external: true \ No newline at end of file +#networks: +# proxynet: +# external: true diff --git a/frontend/src/components/ChangePreferences.tsx b/frontend/src/components/ChangePreferences.tsx index a3ed89d..42906b0 100644 --- a/frontend/src/components/ChangePreferences.tsx +++ b/frontend/src/components/ChangePreferences.tsx @@ -47,7 +47,7 @@ const ChangePreferences: React.FC = ({ onClose }) => { Temperature Unit setTheme(e.target.value)} > @@ -77,7 +77,7 @@ const ChangePreferences: React.FC = ({ onClose }) => { {/* Submit */} diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index 106f19f..e5022cf 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -1,6 +1,7 @@ import React from "react"; import ChangeAPI from "./ChangeAPI"; import ChangePreferences from "./ChangePreferences"; +import { myToast } from "../utils/toastify"; import { useState } from "react"; import Cookies from "js-cookie"; import logo from "../assets/cloud-sun-fill.png"; @@ -21,15 +22,15 @@ const Header: React.FC = () => { className="w-10 h-10 drop-shadow-lg" />

- Weather App + Weather App - Web

{" "} {/* Added container for buttons */} @@ -80,7 +82,7 @@ const WeatherCard: React.FC = () => { setWeatherData(false); localStorage.removeItem("weather"); }} - className="flex-shrink-0 bg-red-500 hover:bg-red-600 text-white rounded-xl p-3 shadow-lg transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-red-400" + className="cursor-pointer flex-shrink-0 bg-red-500 hover:bg-red-600 text-white rounded-xl p-3 shadow-lg transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-red-400" aria-label="Close weather data" > diff --git a/frontend/src/utils/apiFunc.ts b/frontend/src/utils/apiFunc.ts index da37c66..1b45d1c 100644 --- a/frontend/src/utils/apiFunc.ts +++ b/frontend/src/utils/apiFunc.ts @@ -1,36 +1,30 @@ import { myToast } from "./toastify"; -import Cookies from "js-cookie"; -export const fetchWeather = async ( - city: string, - apiKey: string, - units: string -) => { - // Get location data - const location = await fetch( - `https://api.openweathermap.org/geo/1.0/direct?q=${city}&appid=${apiKey}` - ).then((response) => { - if (response.status === 401) { - if (Cookies.get("apiKey") === undefined || Cookies.get("apiKey") === "") { - myToast("You have to enter an API key!", "error"); - } else { - myToast( - "You are not authorized to access this resource. Please check your API key. (Error: x4010)", - "error" - ); +export const fetchWeather = async (city: string, units: string) => { + try { + const response = await fetch( + `http://localhost:7001/api/fetchWeather?city=${encodeURIComponent( + city + )}&units=${encodeURIComponent(units)}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + }, } - } else if (response.ok) { - return response.json(); - } else { - myToast("Error fetching location data. (Error: x32)", "error"); - } - }); - const lat = location[0].lat; - const lon = location[0].lon; + ); - // Get weather data - const weather = await fetch( - `https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&appid=${apiKey}&units=${units}` - ).then((response) => response.json()); - localStorage.setItem("weather", JSON.stringify(weather)); + const responseData = await response.json(); + + if (!response.ok) { + myToast(responseData.error, "error"); + return; + } + localStorage.setItem("weather", JSON.stringify(responseData.data)); + return; + } catch (error) { + const errorMsg = JSON.stringify(error); + myToast(errorMsg, "error"); + return null; + } };