3 Commits

Author SHA1 Message Date
theis.gaedigk a261ce8090 added license 2026-04-15 19:39:03 +02:00
theis.gaedigk 1b83ed51cb changed logo 2026-03-26 23:31:52 +01:00
theis.gaedigk 6f839a67f4 added icon 2026-03-26 23:06:20 +01:00
16 changed files with 157 additions and 149 deletions
+2
View File
@@ -112,3 +112,5 @@ backend/public/uploads/
config/ config/
secrets/ secrets/
keys/ keys/
icon/
+6
View File
@@ -0,0 +1,6 @@
Copyright (c) 2026 Theis Gaedigk
All rights reserved.
This source code is not to be copied, modified, or distributed in any form
without explicit written permission from the author.
+89 -9
View File
@@ -1,15 +1,95 @@
# Weather App # Weather App
This version is only meant for publishing on the web. It is not meant for local development or use. 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.
You can find the web version of the Weather App at [https://weather.the1s.de](https://weather.the1s.de). > 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
## Version ## Version
Currently hosted version: **development branch (latest)** **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.**
## 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
View File
@@ -1,6 +0,0 @@
#!/bin/bash
cd /pfad/zu/deinem/repo
while true; do
git pull || echo "git pull failed"
sleep 10
done
-63
View File
@@ -1,63 +0,0 @@
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(500).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;
+2 -4
View File
@@ -1,7 +1,7 @@
import express from "express"; import express from "express";
import cors from "cors"; import cors from "cors";
import apiRouter from "./routes/api.js"; import env from "dotenv";
env.config();
const app = express(); const app = express();
const port = 7001; const port = 7001;
@@ -10,8 +10,6 @@ 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 });
}); });
+7 -8
View File
@@ -1,12 +1,11 @@
<!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>
<h1>You have reached the backend views index page!</h1> You have reached the backend views index page.
<p>Currently, there is nothing to display!</p> </body>
</body>
</html> </html>
+10 -19
View File
@@ -1,24 +1,15 @@
services: services:
frontend: frontend:
container_name: weather-frontend container_name: frontend
build: ./frontend build: ./frontend
networks:
- proxynet
ports: ports:
- "7002:80" - "7002:80"
restart: unless-stopped restart: always
# backend:
backend: # container_name: backend
container_name: backend # build: ./backend
build: ./backend # ports:
networks: # - "7001:7001"
- proxynet #volumes:
ports: # - ./backend:/bikelane-backend
- "7001:7001" # restart: unless-stopped
volumes:
- ./backend:/bikelane-backend
restart: unless-stopped
networks:
proxynet:
external: true
+2 -6
View File
@@ -1,12 +1,8 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link <link rel="icon" type="image/png" href="/icon_weather-app_dark.png" />
rel="icon"
type="image/svg+xml"
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>
</head> </head>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

@@ -58,7 +58,7 @@ const ChangePreferences: React.FC<Props> = ({ onClose }) => {
</div> </div>
{/* Theme */} {/* Theme */}
<div> <div>
<label className="cursor-pointer block text-sm font-semibold text-gray-700 mb-2 items-center gap-2"> <label className="block text-sm font-semibold text-gray-700 mb-2 items-center gap-2">
<SunMoon /> <SunMoon />
Theme Theme
</label> </label>
+1 -1
View File
@@ -3,7 +3,7 @@ import ChangeAPI from "./ChangeAPI";
import ChangePreferences from "./ChangePreferences"; import ChangePreferences from "./ChangePreferences";
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.png"; import logo from "/icon_weather-app_default.png";
import { Github, KeyRound, Settings2 } from "lucide-react"; import { Github, KeyRound, Settings2 } from "lucide-react";
const Header: React.FC = () => { const Header: React.FC = () => {
+4 -3
View File
@@ -1,6 +1,7 @@
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";
@@ -9,6 +10,7 @@ import { X } from "lucide-react";
const WeatherCard: React.FC = () => { const WeatherCard: React.FC = () => {
const [city, setCity] = useState(""); const [city, setCity] = useState("");
const [weatherData, setWeatherData] = useState(false); const [weatherData, setWeatherData] = useState(false);
const getAPIKey = () => Cookies.get("apiKey") || "";
const getUnit = () => localStorage.getItem("unit") || "metric"; const getUnit = () => localStorage.getItem("unit") || "metric";
const handleCityChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handleCityChange = (event: React.ChangeEvent<HTMLInputElement>) => {
@@ -17,9 +19,8 @@ const WeatherCard: React.FC = () => {
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => { const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
setWeatherData(false);
toast toast
.promise(fetchWeather(city, getUnit()), { .promise(fetchWeather(city, getAPIKey(), getUnit()), {
pending: "Fetching weather data...", pending: "Fetching weather data...",
success: "Weather data loaded successfully!", success: "Weather data loaded successfully!",
}) })
@@ -66,7 +67,7 @@ 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"
/> />
<div className="cursor-pointer flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
type="submit" 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" 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"
+30 -25
View File
@@ -1,31 +1,36 @@
import { myToast } from "./toastify"; import { myToast } from "./toastify";
import Cookies from "js-cookie";
export const fetchWeather = async (city: string, units: string) => { export const fetchWeather = async (
try { city: string,
const response = await fetch( apiKey: string,
`https://backend.weather.the1s.de/api/fetchWeather?city=${encodeURIComponent( units: string
city ) => {
)}&units=${encodeURIComponent(units)}`, // Get location data
{ const location = await fetch(
method: "GET", `http://api.openweathermap.org/geo/1.0/direct?q=${city}&appid=${apiKey}`
headers: { ).then((response) => {
"Content-Type": "application/json", 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"
); );
const responseData = await response.json();
if (!response.ok) {
myToast(responseData.error, "error");
return;
} }
localStorage.setItem("weather", JSON.stringify(responseData.data)); } else if (response.ok) {
myToast(responseData.success, "success"); return response.json();
return; } else {
} catch (error) { myToast("Error fetching location data. (Error: x32)", "error");
const errorMsg = JSON.stringify(error);
myToast(errorMsg, "error");
return null;
} }
});
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));
}; };