37 Commits

Author SHA1 Message Date
43262846a5 feat: implement weather fetching API and update frontend components for improved user experience 2025-08-03 19:10:20 +02:00
b6b6146ad0 Merge branch 'development' into debian12fullstack 2025-08-03 17:27:10 +02:00
784822fa9a removed loading state and fixed bug with that 2025-08-03 03:27:32 +02:00
0da92ead3c feat: add lucide-react icons to components and update dependencies 2025-08-02 22:53:47 +02:00
16ea1aa4aa removed test changes 2025-08-02 15:01:18 +02:00
905dd6ad22 fix: remove period from API key update message for consistency 2025-08-02 14:59:18 +02:00
e67dce4113 docs: update README to clarify auto deployment process 2025-08-02 14:56:03 +02:00
5bc7bea4a1 removed test changes from the commit before.
- It works!
2025-08-02 14:50:34 +02:00
59e6e2d9d7 added test changes 2025-08-02 14:49:26 +02:00
65c7170acf fix: improve error handling in auto-pull script 2025-08-02 14:43:41 +02:00
2d1bbeb827 added auto pull 2025-08-02 14:38:10 +02:00
f6d29a279a Merge branch 'main' into debian12
merged bug fix by first loading in to the page
2025-08-02 02:30:17 +02:00
f4fffb73ff bug fix: update default unit in ChangePreferences component from celsius to metric 2025-08-02 02:28:36 +02:00
e22fa64e69 fix: adjust server configuration in vite.config.ts for improved host settings 2025-08-02 02:24:13 +02:00
c15d0869c6 Update frontend/src/utils/apiFunc.ts 2025-08-02 02:18:46 +02:00
bbbc8b9edd Update frontend/vite.config.ts 2025-08-02 02:16:48 +02:00
72754f5949 Update frontend/vite.config.ts 2025-08-02 02:13:04 +02:00
c1e96eb7d2 Update docker-compose.yml 2025-08-02 02:10:32 +02:00
ae410bda2b Update frontend/vite.config.ts 2025-08-02 02:09:59 +02:00
400d77cd5a Update docker-compose.yml 2025-08-02 02:07:26 +02:00
5708bfa1b3 changed README.md accordingly 2025-08-02 01:55:20 +02:00
71a29ad9de Merge branch 'main' into debian12
updated host version to 1.0.1
2025-08-02 01:55:05 +02:00
d98fab004f fix: correct numbering in installation instructions for clarity 2025-08-01 21:49:23 +02:00
e56e998467 docs: enhance installation instructions and clarify Docker usage in README 2025-08-01 21:43:02 +02:00
ee6469379f added docker functionality 2025-08-01 21:23:58 +02:00
62094299d4 fix: change button type to submit in ChangeAPI component and update temperature unit logic in WeatherData component 2025-08-01 20:25:03 +02:00
dbbca57f59 fix: update README to clarify web version availability and hosted version details 2025-08-01 19:51:50 +02:00
97b5190442 docs: update version information in README to reflect current development status 2025-08-01 19:49:59 +02:00
a2ee1b3d1f chore: remove detailed usage instructions and features from README for web-only version 2025-08-01 19:48:39 +02:00
7d91eb64eb Merge branch 'v1.0.0-local' into debian12 2025-08-01 19:45:26 +02:00
d8bbc7e62a fix: add missing newlines for improved markdown formatting in README 2025-08-01 19:43:44 +02:00
11e6e1684c Refactor code structure for improved readability and maintainability 2025-08-01 19:43:11 +02:00
a3df60178b changed ports 2025-08-01 01:09:58 +02:00
8341e50dc8 feat: enhance ChangeAPI and ChangePreferences components; add "API key update" functionality and save preferences feature 2025-08-01 01:06:58 +02:00
6f3d945213 added readme 2025-07-31 18:04:50 +02:00
f4881be087 added preferences card 2025-07-31 17:55:47 +02:00
3ecac5a854 refactor: comment out frontend service configuration in docker-compose.yml 2025-07-31 17:38:06 +02:00
21 changed files with 406 additions and 122 deletions

View File

@@ -0,0 +1,15 @@
# Weather App
This version is only meant for publishing on the web. It is not meant for local development or use.
You can find the web version of the Weather App at [https://weather.the1s.de](https://weather.the1s.de).
## Version
Currently hosted version: **1.0.1**
## Dev info
All changes that are made to this branch (`debian12`) will be automatically deployed to the [web version](https://weather.the1s.de) of the Weather App.
This is done by using an auto pull script (`auto-pull.sh`) that runs on the server where the web version is hosted.

6
auto-pull.sh Normal file
View File

@@ -0,0 +1,6 @@
#!/bin/bash
cd /pfad/zu/deinem/repo
while true; do
git pull || echo "git pull failed"
sleep 10
done

63
backend/routes/api.js Normal file
View File

@@ -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;

View File

@@ -1,7 +1,7 @@
import express from "express"; import express from "express";
import cors from "cors"; import cors from "cors";
import env from "dotenv"; import apiRouter from "./routes/api.js";
env.config();
const app = express(); const app = express();
const port = 7001; const port = 7001;
@@ -10,6 +10,8 @@ app.use(express.urlencoded({ extended: true }));
app.set("view engine", "ejs"); app.set("view engine", "ejs");
app.use(express.json()); app.use(express.json());
app.use("/api", apiRouter);
app.get("/", (req, res) => { app.get("/", (req, res) => {
res.render("index.ejs", { title: port }); res.render("index.ejs", { title: port });
}); });

View File

@@ -1,11 +1,12 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Backend | <%= title %></title> <title>Backend | <%= title %></title>
</head> </head>
<body> <body>
You have reached the backend views index page. <h1>You have reached the backend views index page!</h1>
</body> <p>Currently, there is nothing to display!</p>
</html> </body>
</html>

View File

@@ -1,29 +1,26 @@
services: services:
frontend: # frontend:
container_name: frontend # container_name: weather-frontend
build: ./frontend # build: ./frontend
ports: # ports:
- "7002:7002" # - "7002:7002"
networks: # # networks:
- proxynet # # - proxynet
environment: # environment:
- CHOKIDAR_USEPOLLING=true # - CHOKIDAR_USEPOLLING=true
volumes: # volumes:
- ./frontend:/app # - ./frontend:/app
- /app/node_modules # - /app/node_modules
restart: unless-stopped # restart: unless-stopped
backend: backend:
container_name: backend container_name: weather-backend
build: ./backend build: ./backend
ports: ports:
- "7001:7001" - "7001:7001"
networks:
- proxynet
volumes: volumes:
- ./backend:/bikelane-backend - ./backend:/app
- /app/node_modules
restart: unless-stopped restart: unless-stopped
#networks:
networks: # proxynet:
proxynet: # external: true
external: true

View File

@@ -9,4 +9,4 @@ COPY . .
EXPOSE 7002 EXPOSE 7002
CMD ["npm", "start"] CMD ["npm", "run", "dev"]

View File

@@ -5,7 +5,7 @@
<link <link
rel="icon" rel="icon"
type="image/svg+xml" type="image/svg+xml"
href="./src/assets/cloud-sun-fill.svg" href="./src/assets/cloud-sun-fill.png"
/> />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Weather App</title> <title>Weather App</title>

View File

@@ -12,6 +12,7 @@
"bootstrap-icons": "^1.13.1", "bootstrap-icons": "^1.13.1",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"jscookie": "^1.1.0", "jscookie": "^1.1.0",
"lucide-react": "^0.536.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-toastify": "^11.0.5", "react-toastify": "^11.0.5",
@@ -3221,6 +3222,15 @@
"yallist": "^3.0.2" "yallist": "^3.0.2"
} }
}, },
"node_modules/lucide-react": {
"version": "0.536.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.536.0.tgz",
"integrity": "sha512-2PgvNa9v+qz4Jt/ni8vPLt4jwoFybXHuubQT8fv4iCW5TjDxkbZjNZZHa485ad73NSEn/jdsEtU57eE1g+ma8A==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/magic-string": { "node_modules/magic-string": {
"version": "0.30.17", "version": "0.30.17",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",

View File

@@ -14,6 +14,7 @@
"bootstrap-icons": "^1.13.1", "bootstrap-icons": "^1.13.1",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"jscookie": "^1.1.0", "jscookie": "^1.1.0",
"lucide-react": "^0.536.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-toastify": "^11.0.5", "react-toastify": "^11.0.5",

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Binary file not shown.

View File

@@ -1,22 +1,29 @@
import { useState } from "react"; import { useState } from "react";
import React from "react"; import React from "react";
import { changeAPIcookie } from "../utils/changeAPIcookie"; import { changeAPIcookie } from "../utils/changeAPIcookie";
import { KeyRound } from "lucide-react";
interface Props { interface Props {
currentAPIKey: string; currentAPIKey: string;
onClose: () => void;
} }
const ChangeAPI: React.FC<Props> = ({ currentAPIKey }) => { const ChangeAPI: React.FC<Props> = ({ currentAPIKey, onClose }) => {
const [apiKey, setApiKey] = useState(currentAPIKey); const [apiKey, setApiKey] = useState(currentAPIKey);
const handleUpdate = () => {
changeAPIcookie(apiKey);
onClose();
};
return ( return (
<div className="w-full"> <div className="w-full">
<h2 className="text-2xl font-bold mb-2 text-blue-700">Change API Key</h2> <h2 className="text-2xl font-bold mb-2 text-blue-700">Change API Key</h2>
<p className="mb-2 text-gray-600"> <h4 className="mb-2 font-semibold text-gray-600">
Update your API key to fetch weather data. Update your API key to fetch weather data.
</p> </h4>
<div className="mb-6 flex items-center gap-2 text-gray-600"> <div className="mb-6 flex items-center gap-2 font-style: italic text-gray-500 flex-wrap text-">
<span>We are using</span> <p>We are using</p>
<a <a
href="https://openweathermap.org/api" href="https://openweathermap.org/api"
className="text-blue-600 font-semibold underline hover:text-blue-800" className="text-blue-600 font-semibold underline hover:text-blue-800"
@@ -25,26 +32,29 @@ const ChangeAPI: React.FC<Props> = ({ currentAPIKey }) => {
> >
OpenWeatherMap OpenWeatherMap
</a> </a>
<span>API for fetching weather data.</span> <p>API for fetching weather data.</p>
</div> </div>
<form className="flex flex-col gap-4"> <form className="flex flex-col gap-4">
<label htmlFor="apiKey" className="font-medium text-gray-700"> <label htmlFor="apiKey" className="font-medium text-gray-700">
API Key: API Key:
</label> </label>
<input <div className="flex items-center gap-2 w-full">
type="text" <KeyRound size={32} />
id="apiKey" <input
name="apiKey" type="text"
placeholder="Enter your API key" id="apiKey"
value={apiKey} name="apiKey"
onChange={(e) => setApiKey(e.target.value)} placeholder="Enter your API key"
required value={apiKey}
className="border border-blue-300 rounded-xl px-4 py-3 focus:outline-none focus:ring-2 focus:ring-blue-400 bg-blue-50 text-blue-900 font-mono" onChange={(e) => setApiKey(e.target.value)}
/> required
className="border border-blue-300 rounded-xl px-6 py-3 focus:outline-none focus:ring-2 focus:ring-blue-400 bg-blue-50 text-blue-900 font-mono w-full"
/>
</div>
<button <button
type="button" type="submit"
className="bg-gradient-to-r from-blue-600 to-blue-400 text-white font-bold px-6 py-3 rounded-xl shadow-lg hover:from-blue-700 hover:to-blue-500 transition-all" className="bg-gradient-to-r from-blue-600 to-blue-400 text-white font-bold px-6 py-3 rounded-xl shadow-lg hover:from-blue-700 hover:to-blue-500 transition-all"
onClick={() => changeAPIcookie(apiKey)} onClick={handleUpdate}
> >
Update API Key Update API Key
</button> </button>

View File

@@ -0,0 +1,89 @@
import React, { useState } from "react";
import { myToast } from "../utils/toastify";
import { Settings2, SunMoon, Thermometer } from "lucide-react";
const getInitialTheme = () => localStorage.getItem("theme") || "light";
const getInitialUnit = () => localStorage.getItem("unit") || "metric";
interface Props {
onClose: () => void;
}
const ChangePreferences: React.FC<Props> = ({ onClose }) => {
const [unit, setUnit] = useState(getInitialUnit());
const [theme, setTheme] = useState(getInitialTheme());
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
localStorage.setItem("unit", unit);
localStorage.setItem("theme", theme);
myToast("Preferences saved successfully!", "success");
onClose();
};
// Theme-Optionen so sortieren, dass die aktuelle Auswahl oben steht
const themeOptions =
theme === "dark"
? [
{ value: "dark", label: "Dark" },
{ value: "light", label: "Light" },
]
: [
{ value: "light", label: "Light" },
{ value: "dark", label: "Dark" },
];
return (
<div className="w-full max-w-md mx-auto bg-white rounded-2xl shadow-2xl border border-blue-200 p-8">
<h2 className="text-3xl font-extrabold mb-6 text-blue-700 flex items-center gap-2">
<Settings2 />
Preferences
</h2>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Unit */}
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2 items-center gap-2">
<Thermometer />
Temperature Unit
</label>
<select
className="cursor-pointer w-full border-2 border-blue-200 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-400 transition"
value={unit}
onChange={(e) => setUnit(e.target.value)}
>
<option value="metric">Celsius (°C)</option>
<option value="imperial">Imperial (°F)</option>
<option value="standard">Standard (K)</option>
</select>
</div>
{/* Theme */}
<div>
<label className="cursor-pointer block text-sm font-semibold text-gray-700 mb-2 items-center gap-2">
<SunMoon />
Theme
</label>
<select
className="cursor-pointer w-full border-2 border-blue-200 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-400 transition"
value={theme}
onChange={(e) => setTheme(e.target.value)}
>
{themeOptions.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
{/* Submit */}
<button
type="submit"
className="cursor-pointer w-full bg-gradient-to-r from-blue-600 to-blue-400 text-white py-3 rounded-xl font-bold text-lg shadow-lg hover:from-blue-700 hover:to-blue-500 transition-all focus:outline-none focus:ring-2 focus:ring-blue-400"
>
Save Preferences
</button>
</form>
</div>
);
};
export default ChangePreferences;

View File

@@ -1,11 +1,15 @@
import React from "react"; import React from "react";
import ChangeAPI from "./ChangeAPI"; import ChangeAPI from "./ChangeAPI";
import ChangePreferences from "./ChangePreferences";
import { myToast } from "../utils/toastify";
import { useState } from "react"; import { useState } from "react";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import logo from "../assets/cloud-sun-fill.svg"; import logo from "../assets/cloud-sun-fill.png";
import { Github, KeyRound, Settings2 } from "lucide-react";
const Header: React.FC = () => { const Header: React.FC = () => {
const [apiCard, setApiCard] = useState(false); const [apiCard, setApiCard] = useState(false);
const [preferencesCard, setPreferencesCard] = useState(false);
const apiKey = Cookies.get("apiKey") || ""; const apiKey = Cookies.get("apiKey") || "";
return ( return (
@@ -18,26 +22,42 @@ const Header: React.FC = () => {
className="w-10 h-10 drop-shadow-lg" className="w-10 h-10 drop-shadow-lg"
/> />
<h1 className="text-4xl font-extrabold tracking-wide drop-shadow-lg"> <h1 className="text-4xl font-extrabold tracking-wide drop-shadow-lg">
Weather App Weather App - <strong>Web</strong>
</h1> </h1>
</div> </div>
<button <div className="flex items-center gap-4">
className="bg-white text-blue-700 font-bold px-6 py-3 rounded-xl shadow-lg hover:bg-blue-100 transition-all border border-blue-200" {" "}
onClick={() => setApiCard(true)} {/* Added container for buttons */}
> <button
<span className="flex items-center gap-2"> className="bg-gray-200 text-gray-400 font-bold px-6 py-3 rounded-xl shadow-lg border border-gray-300 flex items-center gap-2 cursor-not-allowed opacity-60"
<svg onClick={() => myToast("You don't need to set an API Key!", "info")}
className="w-5 h-5" >
fill="none" <span className="flex items-center gap-2">
stroke="currentColor" <KeyRound />
strokeWidth="2" Set API Key
viewBox="0 0 24 24" </span>
> </button>
<path d="M12 4v16m8-8H4" /> <button
</svg> className="cursor-pointer bg-white text-blue-700 font-bold px-6 py-3 rounded-xl shadow-lg hover:bg-blue-100 transition-all border border-blue-200 flex items-center gap-2"
Set API Key onClick={() => setPreferencesCard(true)}
</span> >
</button> <span className="flex items-center gap-2">
<Settings2 />
Preferences
</span>
</button>
<a
href="https://github.com/theis-js/weather-app/wiki"
target="_blank"
rel="noopener noreferrer"
>
<button className="cursor-help bg-white text-blue-700 font-bold px-6 py-3 rounded-xl shadow-lg hover:bg-blue-100 transition-all border border-blue-200 flex items-center gap-2">
<span className="flex items-center gap-2">
<Github /> Docs
</span>
</button>
</a>
</div>
</header> </header>
{apiCard && ( {apiCard && (
<div className="fixed inset-0 bg-gray-900 bg-opacity-60 flex items-center justify-center z-50 transition-all"> <div className="fixed inset-0 bg-gray-900 bg-opacity-60 flex items-center justify-center z-50 transition-all">
@@ -49,7 +69,25 @@ const Header: React.FC = () => {
> >
&times; &times;
</button> </button>
<ChangeAPI currentAPIKey={apiKey} /> <ChangeAPI
currentAPIKey={apiKey}
onClose={() => setApiCard(false)}
/>
</div>
</div>
)}
{preferencesCard && (
<div className="fixed inset-0 bg-gray-900 bg-opacity-60 flex items-center justify-center z-50 transition-all">
<div className="bg-white rounded-2xl shadow-2xl p-10 w-full max-w-md relative border-2 border-blue-200">
<button
className="absolute top-4 right-4 text-gray-400 hover:text-blue-600 text-2xl font-bold"
onClick={() => setPreferencesCard(false)}
aria-label="Close"
>
&times;
</button>
<ChangePreferences onClose={() => setPreferencesCard(false)} />
</div> </div>
</div> </div>
)} )}

View File

@@ -1,6 +1,5 @@
import React from "react"; import React from "react";
import sunriseIcon from "../assets/icons/sunrise-fill.svg"; import { Sunrise, Sunset } from "lucide-react";
import sunsetIcon from "../assets/icons/sunset-fill.svg";
const WeatherData: React.FC = () => { const WeatherData: React.FC = () => {
const weatherRaw = localStorage.getItem("weather"); const weatherRaw = localStorage.getItem("weather");
@@ -19,7 +18,27 @@ const WeatherData: React.FC = () => {
{weatherData?.name} {weatherData?.name}
</h2> </h2>
<p className="text-5xl font-extrabold mb-2 text-blue-900"> <p className="text-5xl font-extrabold mb-2 text-blue-900">
{weatherData?.main?.temp} °C {weatherData?.main?.temp}{" "}
{localStorage.getItem("unit") === "metric"
? "°C"
: localStorage.getItem("unit") === "imperial"
? "°F"
: localStorage.getItem("unit") === "standard"
? "K"
: "°C"}
</p>
<p className="flex items-center justify-center gap-2">
{weatherData?.sys?.country && (
<>
<img
src={`https://flagcdn.com/${weatherData.sys.country.toLowerCase()}.svg`}
alt={weatherData.sys.country}
width={24}
height={18}
/>
<span>{weatherData.sys.country}</span>
</>
)}
</p> </p>
<p className="text-lg mb-4 capitalize text-blue-600"> <p className="text-lg mb-4 capitalize text-blue-600">
{weatherData?.weather?.[0]?.description} {weatherData?.weather?.[0]?.description}
@@ -33,7 +52,7 @@ const WeatherData: React.FC = () => {
)} )}
<div className="flex flex-col items-center gap-4 mb-2"> <div className="flex flex-col items-center gap-4 mb-2">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<img src={sunriseIcon} alt="Sunrise Icon" className="w-7 h-7" /> <Sunrise />
<span className="text-base text-blue-700 font-semibold"> <span className="text-base text-blue-700 font-semibold">
Sunrise:{" "} Sunrise:{" "}
{weatherData?.sys?.sunrise {weatherData?.sys?.sunrise
@@ -45,7 +64,7 @@ const WeatherData: React.FC = () => {
</span> </span>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<img src={sunsetIcon} alt="Sunset Icon" className="w-7 h-7" /> <Sunset />
<span className="text-base text-blue-700 font-semibold"> <span className="text-base text-blue-700 font-semibold">
Sunset:{" "} Sunset:{" "}
{weatherData?.sys?.sunset {weatherData?.sys?.sunset

View File

@@ -1,17 +1,15 @@
import React from "react"; import React from "react";
import { useState } from "react"; import { useState } from "react";
import { fetchWeather } from "../utils/apiFunc"; import { fetchWeather } from "../utils/apiFunc";
import Cookies from "js-cookie";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import WeatherData from "./WeatherData"; import WeatherData from "./WeatherData";
import { useEffect } from "react"; import { useEffect } from "react";
import { X } from "lucide-react";
const WeatherCard: React.FC = () => { const WeatherCard: React.FC = () => {
const [city, setCity] = useState(""); const [city, setCity] = useState("");
const [loading, setLoading] = useState(false);
const [weatherData, setWeatherData] = useState(false); const [weatherData, setWeatherData] = useState(false);
console.log(loading); // only for better reading because a syntax error would appear const getUnit = () => localStorage.getItem("unit") || "metric";
const getAPIKey = () => Cookies.get("apiKey") || "";
const handleCityChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handleCityChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setCity(event.target.value); setCity(event.target.value);
@@ -19,12 +17,13 @@ const WeatherCard: React.FC = () => {
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => { const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
setLoading(true); setWeatherData(false);
toast toast
.promise(fetchWeather(city, getAPIKey(), "metric"), { .promise(fetchWeather(city, getUnit()), {
pending: "Fetching weather data...", pending: "Fetching weather data...",
success: "Weather data loaded successfully!", success: "Weather data loaded successfully!",
error: "Error loading weather data! (Check console for details)", error:
"Failed to load weather data. Please check your entered city name. (Error: x4040)",
}) })
.then(() => { .then(() => {
if (localStorage.getItem("weather")) { if (localStorage.getItem("weather")) {
@@ -32,7 +31,6 @@ const WeatherCard: React.FC = () => {
} else { } else {
setWeatherData(false); setWeatherData(false);
} }
setLoading(false);
}); });
}; };
@@ -53,6 +51,9 @@ const WeatherCard: React.FC = () => {
<p className="mb-2 text-gray-600"> <p className="mb-2 text-gray-600">
Current weather will be displayed here. Current weather will be displayed here.
</p> </p>
<p className="mb-2 text-gray-600">
<strong>You don't need an API Key!</strong>
</p>
<form onSubmit={handleSubmit} className="flex flex-col gap-4 mt-4"> <form onSubmit={handleSubmit} className="flex flex-col gap-4 mt-4">
<label htmlFor="city" className="font-medium text-gray-700"> <label htmlFor="city" className="font-medium text-gray-700">
Enter City: Enter City:
@@ -67,12 +68,27 @@ const WeatherCard: React.FC = () => {
required required
className="border border-blue-300 rounded-xl px-4 py-3 focus:outline-none focus:ring-2 focus:ring-blue-400 bg-blue-50 text-blue-900 font-mono" className="border border-blue-300 rounded-xl px-4 py-3 focus:outline-none focus:ring-2 focus:ring-blue-400 bg-blue-50 text-blue-900 font-mono"
/> />
<button <div className="cursor-pointer flex items-center gap-2">
type="submit" <button
className="bg-gradient-to-r from-blue-600 to-blue-400 text-white font-bold px-6 py-3 rounded-xl shadow-lg hover:from-blue-700 hover:to-blue-500 transition-all" type="submit"
> className="cursor-pointer flex-1 bg-gradient-to-r from-blue-600 to-blue-400 text-white font-bold px-4 py-3 rounded-xl shadow-lg hover:from-blue-700 hover:to-blue-500 transition-all"
Get Weather >
</button> Get Weather
</button>
{weatherData && (
<button
type="button"
onClick={() => {
setWeatherData(false);
localStorage.removeItem("weather");
}}
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"
>
<X />
</button>
)}
</div>
</form> </form>
{weatherData && <WeatherData />} {weatherData && <WeatherData />}
</div> </div>

View File

@@ -1,30 +1,30 @@
export type units = "metric" | "imperial"; import { myToast } from "./toastify";
export const fetchWeather = async ( export const fetchWeather = async (city: string, units: string) => {
city: string, try {
apiKey: string, const response = await fetch(
units: units `http://localhost:7001/api/fetchWeather?city=${encodeURIComponent(
) => { city
// Get location data )}&units=${encodeURIComponent(units)}`,
const location = await fetch( {
`http://api.openweathermap.org/geo/1.0/direct?q=${city}&appid=${apiKey}` method: "GET",
).then((response) => { headers: {
if (response.status === 401) { "Content-Type": "application/json",
console.error( },
"You are not authorized to access this resource. Please check your API key." }
); );
} else if (response.ok) {
return response.json(); const responseData = await response.json();
} else {
console.error("Error fetching location data: ", response.statusText); if (!response.ok) {
myToast(responseData.error, "error");
return;
} }
}); localStorage.setItem("weather", JSON.stringify(responseData.data));
const lat = location[0].lat; return;
const lon = location[0].lon; } catch (error) {
const errorMsg = JSON.stringify(error);
// Get weather data myToast(errorMsg, "error");
const weather = await fetch( return null;
`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));
}; };

View File

@@ -2,6 +2,18 @@ import Cookies from "js-cookie";
import { myToast } from "./toastify"; import { myToast } from "./toastify";
export const changeAPIcookie = (newApiKey: string) => { export const changeAPIcookie = (newApiKey: string) => {
let apiKey15 = newApiKey.slice(0, 15);
Cookies.set("apiKey", newApiKey); Cookies.set("apiKey", newApiKey);
myToast("API key updated successfully!", "success"); if (Cookies.get("apiKey") === newApiKey) {
myToast(
"API key updated successfully!" +
" " +
"Your new API key: " +
apiKey15 +
"...",
"success"
);
} else {
myToast("Failed to update API key. (Error: x30)", "error");
}
}; };

View File

@@ -1,12 +1,17 @@
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import tailwindcss from "@tailwindcss/vite"; import tailwindcss from "@tailwindcss/vite";
export default defineConfig({ export default defineConfig({
plugins: [tailwindcss()], plugins: [tailwindcss()],
server: { server: {
host: "0.0.0.0", host: "0.0.0.0",
allowedHosts: ["weather.the1s.de"],
port: 7002, port: 7002,
watch: { watch: { usePolling: true },
usePolling: true, hmr: {
host: "weather.the1s.de",
port: 7002,
protocol: "wss",
}, },
}, },
}); });