53 Commits

Author SHA1 Message Date
theis.gaedigk ab6ccdcc48 Merge branch 'development' into debian12 2026-03-26 23:32:25 +01:00
theis.gaedigk 1b83ed51cb changed logo 2026-03-26 23:31:52 +01:00
theis.gaedigk 2e6ea96fbb Merge branch 'development' into debian12 2026-03-26 23:06:58 +01:00
theis.gaedigk 6f839a67f4 added icon 2026-03-26 23:06:20 +01:00
theis.gaedigk bdb8f9bb64 updated conf 2026-01-27 10:49:59 +01:00
theis.gaedigk df3802ff50 changed container name 2025-11-29 17:28:47 +01:00
theis.gaedigk 388cff9368 changed back to old header 2025-11-26 19:39:58 +01:00
theis.gaedigk 3d6c9bfecd fix: update docker-compose configuration and restore backend service 2025-11-26 19:33:11 +01:00
theis.gaedigk 77111958a2 Merge branch 'development' into debian12 2025-11-26 19:32:59 +01:00
theis.gaedigk 04c82dd9d4 added react build 2025-11-26 19:28:15 +01:00
theis.gaedigk 9b0f7daa2c fix: update error response status and improve success message handling in weather fetching 2025-08-03 23:52:23 +02:00
theis.gaedigk 20bee1018c fixed bug: Error handling when submitting form with wrong api key 2025-08-03 23:47:32 +02:00
theis.gaedigk 0361a743ef feat: add last updated timestamp to WeatherData component 2025-08-03 22:24:17 +02:00
theis.gaedigk f32ddd837b feat: update release notes to include last updated time feature in web panel 2025-08-03 22:21:46 +02:00
theis.gaedigk 38ef97b553 feat: add last updated timestamp to WeatherData component 2025-08-03 22:21:19 +02:00
theis.gaedigk f70629fa55 fix: remove port from weather API URL for consistency 2025-08-03 20:42:18 +02:00
theis.gaedigk d84d876735 fix: update weather API URL to use HTTPS for improved security 2025-08-03 20:36:04 +02:00
theis.gaedigk 574f094bac changed protocol 2025-08-03 20:31:13 +02:00
theis.gaedigk 71da186bd6 changed networking for hosting 2025-08-03 20:26:24 +02:00
theis.gaedigk ff77629a01 feat: add release notes with new features and improvements 2025-08-03 19:52:35 +02:00
theis.gaedigk 4c42bd94ea fix: add cursor pointer to buttons for better UX and update WeatherForm instructions 2025-08-03 19:45:01 +02:00
theis.gaedigk 4f98bf4e9a refactor: uncomment frontend service configuration in docker-compose.yml 2025-08-03 19:14:39 +02:00
theis.gaedigk 43262846a5 feat: implement weather fetching API and update frontend components for improved user experience 2025-08-03 19:10:20 +02:00
theis.gaedigk b6b6146ad0 Merge branch 'development' into debian12fullstack 2025-08-03 17:27:10 +02:00
theis.gaedigk 784822fa9a removed loading state and fixed bug with that 2025-08-03 03:27:32 +02:00
admin 0da92ead3c feat: add lucide-react icons to components and update dependencies 2025-08-02 22:53:47 +02:00
theis.gaedigk 16ea1aa4aa removed test changes 2025-08-02 15:01:18 +02:00
theis.gaedigk 905dd6ad22 fix: remove period from API key update message for consistency 2025-08-02 14:59:18 +02:00
theis.gaedigk e67dce4113 docs: update README to clarify auto deployment process 2025-08-02 14:56:03 +02:00
theis.gaedigk 5bc7bea4a1 removed test changes from the commit before.
- It works!
2025-08-02 14:50:34 +02:00
theis.gaedigk 59e6e2d9d7 added test changes 2025-08-02 14:49:26 +02:00
theis.gaedigk 65c7170acf fix: improve error handling in auto-pull script 2025-08-02 14:43:41 +02:00
theis.gaedigk 2d1bbeb827 added auto pull 2025-08-02 14:38:10 +02:00
theis.gaedigk f6d29a279a Merge branch 'main' into debian12
merged bug fix by first loading in to the page
2025-08-02 02:30:17 +02:00
theis.gaedigk f4fffb73ff bug fix: update default unit in ChangePreferences component from celsius to metric 2025-08-02 02:28:36 +02:00
theis.gaedigk e22fa64e69 fix: adjust server configuration in vite.config.ts for improved host settings 2025-08-02 02:24:13 +02:00
theis.gaedigk c15d0869c6 Update frontend/src/utils/apiFunc.ts 2025-08-02 02:18:46 +02:00
theis.gaedigk bbbc8b9edd Update frontend/vite.config.ts 2025-08-02 02:16:48 +02:00
theis.gaedigk 72754f5949 Update frontend/vite.config.ts 2025-08-02 02:13:04 +02:00
theis.gaedigk c1e96eb7d2 Update docker-compose.yml 2025-08-02 02:10:32 +02:00
theis.gaedigk ae410bda2b Update frontend/vite.config.ts 2025-08-02 02:09:59 +02:00
theis.gaedigk 400d77cd5a Update docker-compose.yml 2025-08-02 02:07:26 +02:00
theis.gaedigk 5708bfa1b3 changed README.md accordingly 2025-08-02 01:55:20 +02:00
theis.gaedigk 71a29ad9de Merge branch 'main' into debian12
updated host version to 1.0.1
2025-08-02 01:55:05 +02:00
theis.gaedigk d98fab004f fix: correct numbering in installation instructions for clarity 2025-08-01 21:49:23 +02:00
theis.gaedigk e56e998467 docs: enhance installation instructions and clarify Docker usage in README 2025-08-01 21:43:02 +02:00
theis.gaedigk ee6469379f added docker functionality 2025-08-01 21:23:58 +02:00
theis.gaedigk 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
theis.gaedigk dbbca57f59 fix: update README to clarify web version availability and hosted version details 2025-08-01 19:51:50 +02:00
theis.gaedigk 97b5190442 docs: update version information in README to reflect current development status 2025-08-01 19:49:59 +02:00
theis.gaedigk a2ee1b3d1f chore: remove detailed usage instructions and features from README for web-only version 2025-08-01 19:48:39 +02:00
theis.gaedigk 7d91eb64eb Merge branch 'v1.0.0-local' into debian12 2025-08-01 19:45:26 +02:00
theis.gaedigk a3df60178b changed ports 2025-08-01 01:09:58 +02:00
23 changed files with 266 additions and 263 deletions
+2
View File
@@ -112,3 +112,5 @@ backend/public/uploads/
config/
secrets/
keys/
icon/
+9 -50
View File
@@ -1,56 +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
1. Clone the repository:
```bash
git clone https://git.the1s.de/theis.gaedigk/weather-app.git
```
2. Navigate to the frontend project directory:
```bash
cd weather-app/frontend
```
3. Install dependencies:
```bash
npm install
```
4. Start the development server:
```bash
npm run dev
```
5. 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
**1.0.0**
Currently hosted version: **development branch (latest)**
## 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.
+9
View File
@@ -0,0 +1,9 @@
## Release Notes
### New Features
- Changed **web panel** backend, so you don't need to set the API key anymore. [Web Panel](https://weather.the1s.de)
- You can now see the **last updated** time in the web panel.
### Improvements
- Changed Website Icons to lucide react icons
- Changed overall handling of the panel
+63
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(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;
+4 -2
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 });
});
+8 -7
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>
</head>
<body>
<h1>You have reached the backend views index page!</h1>
<p>Currently, there is nothing to display!</p>
</body>
</html>
+13 -14
View File
@@ -1,21 +1,20 @@
services:
# frontend:
# container_name: frontend
# build: ./frontend
# ports:
# - "7002:7002"
# environment:
# - CHOKIDAR_USEPOLLING=true
# volumes:
# - ./frontend:/app
# - /app/node_modules
# restart: unless-stopped
frontend:
container_name: weather-frontend
build: ./frontend
networks:
- proxynet
restart: unless-stopped
backend:
container_name: backend
container_name: weather-backend
build: ./backend
ports:
- "7001:7001"
networks:
- proxynet
volumes:
- ./backend:/bikelane-backend
restart: unless-stopped
networks:
proxynet:
external: true
+12 -5
View File
@@ -1,12 +1,19 @@
FROM node:20-alpine
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
EXPOSE 7002
FROM nginx:alpine AS runner
CMD ["npm", "start"]
WORKDIR /usr/share/nginx/html
COPY --from=builder /app/dist .
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
+2 -6
View File
@@ -1,12 +1,8 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link
rel="icon"
type="image/svg+xml"
href="./src/assets/cloud-sun-fill.png"
/>
<link rel="icon" type="image/png" href="/icon_weather-app_dark.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Weather App</title>
</head>
+26
View File
@@ -0,0 +1,26 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location = /backend {
return 301 /backend/;
}
location /backend/ {
proxy_pass http://weather-backend:7001/;
}
location ~* \.(?:js|mjs|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
expires 1y;
access_log off;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
}
+10
View File
@@ -12,6 +12,7 @@
"bootstrap-icons": "^1.13.1",
"js-cookie": "^3.0.5",
"jscookie": "^1.1.0",
"lucide-react": "^0.536.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-toastify": "^11.0.5",
@@ -3221,6 +3222,15 @@
"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": {
"version": "0.30.17",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
+1
View File
@@ -14,6 +14,7 @@
"bootstrap-icons": "^1.13.1",
"js-cookie": "^3.0.5",
"jscookie": "^1.1.0",
"lucide-react": "^0.536.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-toastify": "^11.0.5",
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

+4 -16
View File
@@ -1,6 +1,7 @@
import { useState } from "react";
import React from "react";
import { changeAPIcookie } from "../utils/changeAPIcookie";
import { KeyRound } from "lucide-react";
interface Props {
currentAPIKey: string;
@@ -38,20 +39,7 @@ const ChangeAPI: React.FC<Props> = ({ currentAPIKey, onClose }) => {
API Key:
</label>
<div className="flex items-center gap-2 w-full">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={2}
stroke="currentColor"
className="w-7 h-7 flex-shrink-0"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15.75 5.25a3 3 0 0 1 3 3m3 0a6 6 0 0 1-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1 1 21.75 8.25Z"
/>
</svg>
<KeyRound size={32} />
<input
type="text"
id="apiKey"
@@ -64,8 +52,8 @@ const ChangeAPI: React.FC<Props> = ({ currentAPIKey, onClose }) => {
/>
</div>
<button
type="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 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={handleUpdate}
>
Update API Key
+9 -46
View File
@@ -1,8 +1,9 @@
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") || "celsius";
const getInitialUnit = () => localStorage.getItem("unit") || "metric";
interface Props {
onClose: () => void;
@@ -35,43 +36,18 @@ const ChangePreferences: React.FC<Props> = ({ onClose }) => {
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">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={2}
stroke="currentColor"
className="w-7 h-7 text-blue-500"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M10.5 6h9.75M10.5 6a1.5 1.5 0 1 1-3 0m3 0a1.5 1.5 0 1 0-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-9.75 0h9.75"
/>
</svg>
<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">
<svg
className="w-5 h-5 text-blue-400"
fill="none"
stroke="currentColor"
strokeWidth={2}
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M8 12h8m-4-4v8"
/>
</svg>
<Thermometer />
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)}
>
@@ -82,25 +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">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={2}
stroke="currentColor"
className="w-5 h-5 text-blue-400"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3 8.25V18a2.25 2.25 0 0 0 2.25 2.25h13.5A2.25 2.25 0 0 0 21 18V8.25m-18 0V6a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 6v2.25m-18 0h18M5.25 6h.008v.008H5.25V6ZM7.5 6h.008v.008H7.5V6Zm2.25 0h.008v.008H9.75V6Z"
/>
</svg>
<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)}
>
@@ -114,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>
+11 -54
View File
@@ -1,9 +1,11 @@
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";
import logo from "/icon_weather-app_default.png";
import { Github, KeyRound, Settings2 } from "lucide-react";
const Header: React.FC = () => {
const [apiCard, setApiCard] = useState(false);
@@ -20,58 +22,27 @@ 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">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="size-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15.75 5.25a3 3 0 0 1 3 3m3 0a6 6 0 0 1-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1 1 21.75 8.25Z"
/>
</svg>
<KeyRound />
Set API Key
</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">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="size-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 0 1 1.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.559.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.894.149c-.424.07-.764.383-.929.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 0 1-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.398.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 0 1-.12-1.45l.527-.737c.25-.35.272-.806.108-1.204-.165-.397-.506-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.108-1.204l-.526-.738a1.125 1.125 0 0 1 .12-1.45l.773-.773a1.125 1.125 0 0 1 1.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894Z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
/>
</svg>
<Settings2 />
Preferences
</span>
</button>
@@ -80,23 +51,9 @@ 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">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="size-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m5.231 13.481L15 17.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v16.5c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Zm3.75 11.625a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z"
/>
</svg>
Docs
<Github /> Docs
</span>
</button>
</a>
+12 -5
View File
@@ -1,6 +1,6 @@
import React from "react";
import sunriseIcon from "../assets/icons/sunrise-fill.svg";
import sunsetIcon from "../assets/icons/sunset-fill.svg";
import { Sunrise, Sunset } from "lucide-react";
import { getDateTime } from "../utils/utils";
const WeatherData: React.FC = () => {
const weatherRaw = localStorage.getItem("weather");
@@ -24,7 +24,9 @@ const WeatherData: React.FC = () => {
? "°C"
: localStorage.getItem("unit") === "imperial"
? "°F"
: "K"}
: localStorage.getItem("unit") === "standard"
? "K"
: "°C"}
</p>
<p className="flex items-center justify-center gap-2">
{weatherData?.sys?.country && (
@@ -51,7 +53,7 @@ const WeatherData: React.FC = () => {
)}
<div className="flex flex-col items-center gap-4 mb-2">
<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">
Sunrise:{" "}
{weatherData?.sys?.sunrise
@@ -63,7 +65,7 @@ const WeatherData: React.FC = () => {
</span>
</div>
<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">
Sunset:{" "}
{weatherData?.sys?.sunset
@@ -74,6 +76,11 @@ const WeatherData: React.FC = () => {
: "--:--"}
</span>
</div>
<div className="flex items-center gap-3">
<span className="text-base text-gray-600 italic font-semibold">
Last updated: {getDateTime()}
</span>
</div>
</div>
</div>
</div>
+10 -25
View File
@@ -1,16 +1,14 @@
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";
import { X } from "lucide-react";
const WeatherCard: React.FC = () => {
const [city, setCity] = useState("");
const [loading, setLoading] = useState(false);
const [weatherData, setWeatherData] = useState(false);
const getAPIKey = () => Cookies.get("apiKey") || "";
const getUnit = () => localStorage.getItem("unit") || "metric";
const handleCityChange = (event: React.ChangeEvent<HTMLInputElement>) => {
@@ -19,13 +17,11 @@ const WeatherCard: React.FC = () => {
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setLoading(true);
setWeatherData(false);
toast
.promise(fetchWeather(city, getAPIKey(), getUnit()), {
.promise(fetchWeather(city, getUnit()), {
pending: "Fetching weather data...",
success: "Weather data loaded successfully!",
error:
"Failed to load weather data. Please check your entered city name. (Error: x4040)",
})
.then(() => {
if (localStorage.getItem("weather")) {
@@ -33,7 +29,6 @@ const WeatherCard: React.FC = () => {
} else {
setWeatherData(false);
}
setLoading(false);
});
};
@@ -54,6 +49,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>Make sure to set your API key in the header section.</strong>
</p>
<form onSubmit={handleSubmit} className="flex flex-col gap-4 mt-4">
<label htmlFor="city" className="font-medium text-gray-700">
Enter City:
@@ -68,10 +66,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>
@@ -82,23 +80,10 @@ 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"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-5 h-5"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 18 18 6M6 6l12 12"
/>
</svg>
<X />
</button>
)}
</div>
+26 -26
View File
@@ -1,31 +1,31 @@
import { myToast } from "./toastify";
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) {
myToast(
"You are not authorized to access this resource. Please check your API key. (Error: x4010)",
"error"
);
} else if (response.ok) {
return response.json();
} else {
myToast("Error fetching location data. (Error: x32)", "error");
export const fetchWeather = async (city: string, units: string) => {
try {
const response = await fetch(
`https://weather.the1s.de/backend/api/fetchWeather?city=${encodeURIComponent(
city
)}&units=${encodeURIComponent(units)}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
}
});
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));
myToast(responseData.success, "success");
return;
} catch (error) {
const errorMsg = JSON.stringify(error);
myToast(errorMsg, "error");
return null;
}
};
+24
View File
@@ -0,0 +1,24 @@
export const getDateTime = () => {
const now = new Date();
const day = now.getDate();
const month = now.toLocaleString("en-GB", { month: "long" });
const hours = now.getHours().toString().padStart(2, "0");
const minutes = now.getMinutes().toString().padStart(2, "0");
// Ordinal suffix
const getOrdinal = (n: number) => {
if (n > 3 && n < 21) return "th";
switch (n % 10) {
case 1:
return "st";
case 2:
return "nd";
case 3:
return "rd";
default:
return "th";
}
};
return `${day}${getOrdinal(day)} ${month}, ${hours}:${minutes}`;
};
+7 -2
View File
@@ -1,12 +1,17 @@
import { defineConfig } from "vite";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [tailwindcss()],
server: {
host: "0.0.0.0",
allowedHosts: ["weather.the1s.de"],
port: 7002,
watch: {
usePolling: true,
watch: { usePolling: true },
hmr: {
host: "weather.the1s.de",
port: 7002,
protocol: "wss",
},
},
});