Compare commits
13 Commits
a2f22ec395
...
v1.0.1-loc
Author | SHA1 | Date | |
---|---|---|---|
6a22c2abfd | |||
6050c2d1c2 | |||
d98fab004f | |||
e56e998467 | |||
ee6469379f | |||
62094299d4 | |||
97b5190442 | |||
d8bbc7e62a | |||
11e6e1684c | |||
8341e50dc8 | |||
6f3d945213 | |||
f4881be087 | |||
3ecac5a854 |
98
README.md
98
README.md
@@ -0,0 +1,98 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
> 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
|
||||||
|
|
||||||
|
**v1.0.1-local**
|
||||||
|
@@ -4,26 +4,17 @@ services:
|
|||||||
build: ./frontend
|
build: ./frontend
|
||||||
ports:
|
ports:
|
||||||
- "7002:7002"
|
- "7002:7002"
|
||||||
networks:
|
|
||||||
- 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: backend
|
# build: ./backend
|
||||||
build: ./backend
|
# ports:
|
||||||
ports:
|
# - "7001:7001"
|
||||||
- "7001:7001"
|
#volumes:
|
||||||
networks:
|
# - ./backend:/bikelane-backend
|
||||||
- proxynet
|
# restart: unless-stopped
|
||||||
volumes:
|
|
||||||
- ./backend:/bikelane-backend
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
networks:
|
|
||||||
proxynet:
|
|
||||||
external: true
|
|
||||||
|
@@ -9,4 +9,4 @@ COPY . .
|
|||||||
|
|
||||||
EXPOSE 7002
|
EXPOSE 7002
|
||||||
|
|
||||||
CMD ["npm", "start"]
|
CMD ["npm", "run", "dev"]
|
@@ -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>
|
||||||
|
BIN
frontend/src/assets/cloud-sun-fill.png
Normal file
BIN
frontend/src/assets/cloud-sun-fill.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
BIN
frontend/src/assets/icons/logo.psd
Normal file
BIN
frontend/src/assets/icons/logo.psd
Normal file
Binary file not shown.
BIN
frontend/src/assets/logo.psd
Normal file
BIN
frontend/src/assets/logo.psd
Normal file
Binary file not shown.
@@ -4,19 +4,25 @@ import { changeAPIcookie } from "../utils/changeAPIcookie";
|
|||||||
|
|
||||||
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 +31,42 @@ 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"
|
<svg
|
||||||
id="apiKey"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
name="apiKey"
|
fill="none"
|
||||||
placeholder="Enter your API key"
|
viewBox="0 0 24 24"
|
||||||
value={apiKey}
|
strokeWidth={2}
|
||||||
onChange={(e) => setApiKey(e.target.value)}
|
stroke="currentColor"
|
||||||
required
|
className="w-7 h-7 flex-shrink-0"
|
||||||
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"
|
>
|
||||||
/>
|
<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
|
||||||
|
type="text"
|
||||||
|
id="apiKey"
|
||||||
|
name="apiKey"
|
||||||
|
placeholder="Enter your API key"
|
||||||
|
value={apiKey}
|
||||||
|
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>
|
||||||
|
126
frontend/src/components/ChangePreferences.tsx
Normal file
126
frontend/src/components/ChangePreferences.tsx
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { myToast } from "../utils/toastify";
|
||||||
|
|
||||||
|
const getInitialTheme = () => localStorage.getItem("theme") || "light";
|
||||||
|
const getInitialUnit = () => localStorage.getItem("unit") || "celsius";
|
||||||
|
|
||||||
|
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">
|
||||||
|
<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>
|
||||||
|
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>
|
||||||
|
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"
|
||||||
|
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="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>
|
||||||
|
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"
|
||||||
|
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="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;
|
@@ -1,11 +1,13 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import ChangeAPI from "./ChangeAPI";
|
import ChangeAPI from "./ChangeAPI";
|
||||||
|
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.svg";
|
import logo from "../assets/cloud-sun-fill.png";
|
||||||
|
|
||||||
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 (
|
||||||
@@ -21,23 +23,84 @@ const Header: React.FC = () => {
|
|||||||
Weather App
|
Weather App
|
||||||
</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-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"
|
||||||
<svg
|
onClick={() => setApiCard(true)}
|
||||||
className="w-5 h-5"
|
>
|
||||||
fill="none"
|
<span className="flex items-center gap-2">
|
||||||
stroke="currentColor"
|
<svg
|
||||||
strokeWidth="2"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
fill="none"
|
||||||
>
|
viewBox="0 0 24 24"
|
||||||
<path d="M12 4v16m8-8H4" />
|
strokeWidth={1.5}
|
||||||
</svg>
|
stroke="currentColor"
|
||||||
Set API Key
|
className="size-6"
|
||||||
</span>
|
>
|
||||||
</button>
|
<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
|
||||||
|
</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"
|
||||||
|
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>
|
||||||
|
Preferences
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<a
|
||||||
|
href="https://github.com/theis-js/weather-app/wiki"
|
||||||
|
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">
|
||||||
|
<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
|
||||||
|
</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 +112,25 @@ const Header: React.FC = () => {
|
|||||||
>
|
>
|
||||||
×
|
×
|
||||||
</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"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
<ChangePreferences onClose={() => setPreferencesCard(false)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@@ -19,7 +19,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}
|
||||||
|
@@ -10,8 +10,8 @@ const WeatherCard: React.FC = () => {
|
|||||||
const [city, setCity] = useState("");
|
const [city, setCity] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
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 getAPIKey = () => Cookies.get("apiKey") || "";
|
const getAPIKey = () => Cookies.get("apiKey") || "";
|
||||||
|
const getUnit = () => localStorage.getItem("unit") || "metric";
|
||||||
|
|
||||||
const handleCityChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleCityChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setCity(event.target.value);
|
setCity(event.target.value);
|
||||||
@@ -21,10 +21,11 @@ const WeatherCard: React.FC = () => {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
toast
|
toast
|
||||||
.promise(fetchWeather(city, getAPIKey(), "metric"), {
|
.promise(fetchWeather(city, getAPIKey(), 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")) {
|
||||||
@@ -67,12 +68,40 @@ 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="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="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="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>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{weatherData && <WeatherData />}
|
{weatherData && <WeatherData />}
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,22 +1,23 @@
|
|||||||
export type units = "metric" | "imperial";
|
import { myToast } from "./toastify";
|
||||||
|
|
||||||
export const fetchWeather = async (
|
export const fetchWeather = async (
|
||||||
city: string,
|
city: string,
|
||||||
apiKey: string,
|
apiKey: string,
|
||||||
units: units
|
units: string
|
||||||
) => {
|
) => {
|
||||||
// Get location data
|
// Get location data
|
||||||
const location = await fetch(
|
const location = await fetch(
|
||||||
`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) {
|
||||||
console.error(
|
myToast(
|
||||||
"You are not authorized to access this resource. Please check your API key."
|
"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 {
|
||||||
console.error("Error fetching location data: ", response.statusText);
|
myToast("Error fetching location data. (Error: x32)", "error");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const lat = location[0].lat;
|
const lat = location[0].lat;
|
||||||
|
@@ -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");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user