22 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
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
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
dbbca57f59 fix: update README to clarify web version availability and hosted version details 2025-08-01 19:51:50 +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
a3df60178b changed ports 2025-08-01 01:09:58 +02:00
10 changed files with 155 additions and 160 deletions

View File

@@ -1,95 +1,15 @@
# Weather App
This is a simple weather application that allows users to view current weather data for a specified location. The app is built using React and TypeScript, and it fetches weather data from an external API.
This version is only meant for publishing on the web. It is not meant for local development or use.
> For that we use the OpenWeatherMap API. You can get your own API key by signing up at [OpenWeatherMap](https://openweathermap.org/api).
## Features
- Search for weather by city name
- Display current weather conditions including temperature, humidity, and wind speed
- Responsive design for mobile and desktop views
- Change API key preferences
- Display weather data in a user-friendly format
## Installation
### Prerequisites
To install and run this application, you need the following tools:
- Git (for cloning the repository)
**and**
- Node.js (v14 or higher)
- npm (Node Package Manager)
**or**
- Docker (for running the app in a container)
### 1st step - Get the source code
**You can either clone the repository or download the latest release. Keep in mind that the cloned version may contain bugs.**
1. Clone the repository:
```bash
git clone https://git.the1s.de/theis.gaedigk/weather-app.git
```
**or**
1. Download the latest release from the [releases page](https://git.the1s.de/theis.gaedigk/weather-app/releases/latest).
2. Unzip the downloaded file to your desired location.
#### 2nd step - Using Node.js and npm
1. Navigate to the frontend project directory:
```bash
cd weather-app/frontend
```
2. Install dependencies:
```bash
npm install
```
3. Start the development server:
```bash
npm run dev
```
4. Open your browser and go to `http://localhost:7002` to view the app.
**Note:** There is also a backend server directory, which is currently not in use. - You can ignore it for now.
**or**
#### 2nd step - Using Docker
1. Navigate to the root path project directory:
```bash
cd weather-app
```
2. Run in a Docker container:
```bash
docker compose up -d --build
```
3. Open your browser and go to `http://localhost:7002` to view the app.
**Note:** There is also a backend server directory, which is currently not in use. - You can ignore it for now.
## Usage
1. Get an API key from [OpenWeatherMap](https://openweathermap.org/api).
2. Click on the "Set API Key" button in the header to enter your API key.
3. Enter a city name in the search bar and press Enter or click the "Get Weather" button.
**Now you can view the current weather data for the specified city!**
# Other Information
## Technologies Used
- React
- TypeScript
- Tailwind CSS
- OpenWeatherMap API
- Vite
You can find the web version of the Weather App at [https://weather.the1s.de](https://weather.the1s.de).
## Version
**On this branch (main) you will find the latest version of the weather app, which includes several improvements and bug fixes. But it is not yet fully functional. The app is still in development, and some features may not work as expected.**
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 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 });
});

View File

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

View File

@@ -1,20 +1,26 @@
services:
frontend:
container_name: 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"
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

View File

@@ -47,7 +47,7 @@ const ChangePreferences: React.FC<Props> = ({ onClose }) => {
Temperature Unit
</label>
<select
className="w-full border-2 border-blue-200 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-400 transition"
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)}
>
@@ -58,12 +58,12 @@ const ChangePreferences: React.FC<Props> = ({ onClose }) => {
</div>
{/* Theme */}
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2 items-center gap-2">
<label className="cursor-pointer block text-sm font-semibold text-gray-700 mb-2 items-center gap-2">
<SunMoon />
Theme
</label>
<select
className="w-full border-2 border-blue-200 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-400 transition"
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)}
>
@@ -77,7 +77,7 @@ const ChangePreferences: React.FC<Props> = ({ onClose }) => {
{/* Submit */}
<button
type="submit"
className="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"
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>

View File

@@ -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"
/>
<h1 className="text-4xl font-extrabold tracking-wide drop-shadow-lg">
Weather App
Weather App - <strong>Web</strong>
</h1>
</div>
<div className="flex items-center gap-4">
{" "}
{/* Added container for buttons */}
<button
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 flex items-center gap-2"
onClick={() => setApiCard(true)}
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"
onClick={() => myToast("You don't need to set an API Key!", "info")}
>
<span className="flex items-center gap-2">
<KeyRound />
@@ -37,7 +38,7 @@ const Header: React.FC = () => {
</span>
</button>
<button
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 flex items-center gap-2"
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"
onClick={() => setPreferencesCard(true)}
>
<span className="flex items-center gap-2">
@@ -50,7 +51,7 @@ const Header: React.FC = () => {
target="_blank"
rel="noopener noreferrer"
>
<button 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 flex items-center gap-2">
<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>

View File

@@ -1,7 +1,6 @@
import React from "react";
import { useState } from "react";
import { fetchWeather } from "../utils/apiFunc";
import Cookies from "js-cookie";
import { toast } from "react-toastify";
import WeatherData from "./WeatherData";
import { useEffect } from "react";
@@ -10,7 +9,6 @@ import { X } from "lucide-react";
const WeatherCard: React.FC = () => {
const [city, setCity] = useState("");
const [weatherData, setWeatherData] = useState(false);
const getAPIKey = () => Cookies.get("apiKey") || "";
const getUnit = () => localStorage.getItem("unit") || "metric";
const handleCityChange = (event: React.ChangeEvent<HTMLInputElement>) => {
@@ -19,8 +17,9 @@ const WeatherCard: React.FC = () => {
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setWeatherData(false);
toast
.promise(fetchWeather(city, getAPIKey(), getUnit()), {
.promise(fetchWeather(city, getUnit()), {
pending: "Fetching weather data...",
success: "Weather data loaded successfully!",
error:
@@ -52,6 +51,9 @@ const WeatherCard: React.FC = () => {
<p className="mb-2 text-gray-600">
Current weather will be displayed here.
</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">
<label htmlFor="city" className="font-medium text-gray-700">
Enter City:
@@ -66,10 +68,10 @@ const WeatherCard: React.FC = () => {
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"
/>
<div className="flex items-center gap-2">
<div className="cursor-pointer flex items-center gap-2">
<button
type="submit"
className="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"
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>
@@ -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"
>
<X />

View File

@@ -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(
`http://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;
}
};