14 Commits

Author SHA1 Message Date
20bee1018c fixed bug: Error handling when submitting form with wrong api key 2025-08-03 23:47:32 +02:00
f32ddd837b feat: update release notes to include last updated time feature in web panel 2025-08-03 22:21:46 +02:00
38ef97b553 feat: add last updated timestamp to WeatherData component 2025-08-03 22:21:19 +02:00
ff77629a01 feat: add release notes with new features and improvements 2025-08-03 19:52:35 +02:00
4c42bd94ea fix: add cursor pointer to buttons for better UX and update WeatherForm instructions 2025-08-03 19:45:01 +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
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
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
97b5190442 docs: update version information in README to reflect current development status 2025-08-01 19:49:59 +02:00
14 changed files with 161 additions and 169 deletions

View File

@@ -13,24 +13,63 @@ This is a simple weather application that allows users to view current weather d
- Display weather data in a user-friendly format - Display weather data in a user-friendly format
## Installation ## 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: 1. Clone the repository:
```bash ```bash
git clone https://git.the1s.de/theis.gaedigk/weather-app.git git clone https://git.the1s.de/theis.gaedigk/weather-app.git
``` ```
2. Navigate to the frontend project directory:
**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 ```bash
cd weather-app/frontend cd weather-app/frontend
``` ```
3. Install dependencies: 2. Install dependencies:
```bash ```bash
npm install npm install
``` ```
4. Start the development server: 3. Start the development server:
```bash ```bash
npm run dev npm run dev
``` ```
5. Open your browser and go to `http://localhost:7002` to view the app. 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. **Note:** There is also a backend server directory, which is currently not in use. - You can ignore it for now.
## Usage ## Usage
@@ -53,4 +92,4 @@ This is a simple weather application that allows users to view current weather d
## Version ## Version
**1.0.0** **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.**

9
RELEASE_NOTES.md Normal file
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

View File

@@ -1,21 +1,20 @@
services: services:
# frontend: frontend:
# container_name: frontend container_name: frontend
# build: ./frontend build: ./frontend
# ports:
# - "7002:7002"
# environment:
# - CHOKIDAR_USEPOLLING=true
# volumes:
# - ./frontend:/app
# - /app/node_modules
# restart: unless-stopped
backend:
container_name: backend
build: ./backend
ports: ports:
- "7001:7001" - "7002:7002"
environment:
- CHOKIDAR_USEPOLLING=true
volumes: volumes:
- ./backend:/bikelane-backend - ./frontend:/app
- /app/node_modules
restart: unless-stopped restart: unless-stopped
# backend:
# container_name: backend
# build: ./backend
# ports:
# - "7001:7001"
#volumes:
# - ./backend:/bikelane-backend
# restart: unless-stopped

View File

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

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",

View File

@@ -1,6 +1,7 @@
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;
@@ -38,20 +39,7 @@ const ChangeAPI: React.FC<Props> = ({ currentAPIKey, onClose }) => {
API Key: API Key:
</label> </label>
<div className="flex items-center gap-2 w-full"> <div className="flex items-center gap-2 w-full">
<svg <KeyRound size={32} />
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>
<input <input
type="text" type="text"
id="apiKey" id="apiKey"
@@ -64,8 +52,8 @@ const ChangeAPI: React.FC<Props> = ({ currentAPIKey, onClose }) => {
/> />
</div> </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="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} onClick={handleUpdate}
> >
Update API Key Update API Key

View File

@@ -1,8 +1,9 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { myToast } from "../utils/toastify"; import { myToast } from "../utils/toastify";
import { Settings2, SunMoon, Thermometer } from "lucide-react";
const getInitialTheme = () => localStorage.getItem("theme") || "light"; const getInitialTheme = () => localStorage.getItem("theme") || "light";
const getInitialUnit = () => localStorage.getItem("unit") || "celsius"; const getInitialUnit = () => localStorage.getItem("unit") || "metric";
interface Props { interface Props {
onClose: () => void; onClose: () => void;
@@ -35,43 +36,18 @@ const ChangePreferences: React.FC<Props> = ({ onClose }) => {
return ( return (
<div className="w-full max-w-md mx-auto bg-white rounded-2xl shadow-2xl border border-blue-200 p-8"> <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"> <h2 className="text-3xl font-extrabold mb-6 text-blue-700 flex items-center gap-2">
<svg <Settings2 />
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>
Preferences Preferences
</h2> </h2>
<form onSubmit={handleSubmit} className="space-y-6"> <form onSubmit={handleSubmit} className="space-y-6">
{/* Unit */} {/* Unit */}
<div> <div>
<label className="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">
<svg <Thermometer />
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>
Temperature Unit Temperature Unit
</label> </label>
<select <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} value={unit}
onChange={(e) => setUnit(e.target.value)} onChange={(e) => setUnit(e.target.value)}
> >
@@ -83,24 +59,11 @@ const ChangePreferences: React.FC<Props> = ({ onClose }) => {
{/* Theme */} {/* Theme */}
<div> <div>
<label className="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">
<svg <SunMoon />
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>
Theme Theme
</label> </label>
<select <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} value={theme}
onChange={(e) => setTheme(e.target.value)} onChange={(e) => setTheme(e.target.value)}
> >
@@ -114,7 +77,7 @@ const ChangePreferences: React.FC<Props> = ({ onClose }) => {
{/* Submit */} {/* Submit */}
<button <button
type="submit" 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 Save Preferences
</button> </button>

View File

@@ -4,6 +4,7 @@ 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 "../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);
@@ -20,58 +21,27 @@ 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>Local hosted</strong>
</h1> </h1>
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
{" "} {" "}
{/* Added container for buttons */} {/* Added container for buttons */}
<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={() => setApiCard(true)} onClick={() => setApiCard(true)}
> >
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<svg <KeyRound />
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>
Set API Key Set API Key
</span> </span>
</button> </button>
<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)} onClick={() => setPreferencesCard(true)}
> >
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<svg <Settings2 />
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>
Preferences Preferences
</span> </span>
</button> </button>
@@ -80,23 +50,9 @@ const Header: React.FC = () => {
target="_blank" target="_blank"
rel="noopener noreferrer" 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"> <span className="flex items-center gap-2">
<svg <Github /> Docs
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
</span> </span>
</button> </button>
</a> </a>

View File

@@ -1,6 +1,6 @@
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"; import { getDateTime } from "../utils/utils";
const WeatherData: React.FC = () => { const WeatherData: React.FC = () => {
const weatherRaw = localStorage.getItem("weather"); const weatherRaw = localStorage.getItem("weather");
@@ -24,7 +24,9 @@ const WeatherData: React.FC = () => {
? "°C" ? "°C"
: localStorage.getItem("unit") === "imperial" : localStorage.getItem("unit") === "imperial"
? "°F" ? "°F"
: "K"} : localStorage.getItem("unit") === "standard"
? "K"
: "°C"}
</p> </p>
<p className="flex items-center justify-center gap-2"> <p className="flex items-center justify-center gap-2">
{weatherData?.sys?.country && ( {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 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
@@ -63,7 +65,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
@@ -74,6 +76,11 @@ const WeatherData: React.FC = () => {
: "--:--"} : "--:--"}
</span> </span>
</div> </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> </div>
</div> </div>

View File

@@ -5,10 +5,10 @@ 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);
const getAPIKey = () => Cookies.get("apiKey") || ""; const getAPIKey = () => Cookies.get("apiKey") || "";
const getUnit = () => localStorage.getItem("unit") || "metric"; const getUnit = () => localStorage.getItem("unit") || "metric";
@@ -19,13 +19,10 @@ const WeatherCard: React.FC = () => {
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => { const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
setLoading(true);
toast toast
.promise(fetchWeather(city, getAPIKey(), getUnit()), { .promise(fetchWeather(city, getAPIKey(), getUnit()), {
pending: "Fetching weather data...", pending: "Fetching weather data...",
success: "Weather data loaded successfully!", success: "Weather data loaded successfully!",
error:
"Failed to load weather data. Please check your entered city name. (Error: x4040)",
}) })
.then(() => { .then(() => {
if (localStorage.getItem("weather")) { if (localStorage.getItem("weather")) {
@@ -33,7 +30,6 @@ const WeatherCard: React.FC = () => {
} else { } else {
setWeatherData(false); setWeatherData(false);
} }
setLoading(false);
}); });
}; };
@@ -54,6 +50,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>Make sure to set your API key in the header section.</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:
@@ -71,7 +70,7 @@ const WeatherCard: React.FC = () => {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
type="submit" 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 Get Weather
</button> </button>
@@ -82,23 +81,10 @@ const WeatherCard: React.FC = () => {
setWeatherData(false); setWeatherData(false);
localStorage.removeItem("weather"); 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" aria-label="Close weather data"
> >
<svg <X />
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>
</button> </button>
)} )}
</div> </div>

View File

@@ -1,4 +1,5 @@
import { myToast } from "./toastify"; import { myToast } from "./toastify";
import Cookies from "js-cookie";
export const fetchWeather = async ( export const fetchWeather = async (
city: string, city: string,
@@ -10,10 +11,14 @@ export const fetchWeather = async (
`http://api.openweathermap.org/geo/1.0/direct?q=${city}&appid=${apiKey}` `http://api.openweathermap.org/geo/1.0/direct?q=${city}&appid=${apiKey}`
).then((response) => { ).then((response) => {
if (response.status === 401) { if (response.status === 401) {
myToast( if (Cookies.get("apiKey") === undefined || Cookies.get("apiKey") === "") {
"You are not authorized to access this resource. Please check your API key. (Error: x4010)", myToast("You have to enter an API key!", "error");
"error" } else {
); myToast(
"You are not authorized to access this resource. Please check your API key. (Error: x4010)",
"error"
);
}
} else if (response.ok) { } else if (response.ok) {
return response.json(); return response.json();
} else { } else {

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}`;
};

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",
}, },
}, },
}); });