feat: enhance ChangeAPI and ChangePreferences components; add "API key update" functionality and save preferences feature
This commit is contained in:
@@ -4,19 +4,25 @@ import { changeAPIcookie } from "../utils/changeAPIcookie";
|
||||
|
||||
interface Props {
|
||||
currentAPIKey: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const ChangeAPI: React.FC<Props> = ({ currentAPIKey }) => {
|
||||
const ChangeAPI: React.FC<Props> = ({ currentAPIKey, onClose }) => {
|
||||
const [apiKey, setApiKey] = useState(currentAPIKey);
|
||||
|
||||
const handleUpdate = () => {
|
||||
changeAPIcookie(apiKey);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<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.
|
||||
</p>
|
||||
<div className="mb-6 flex items-center gap-2 text-gray-600">
|
||||
<span>We are using</span>
|
||||
</h4>
|
||||
<div className="mb-6 flex items-center gap-2 font-style: italic text-gray-500 flex-wrap text-">
|
||||
<p>We are using</p>
|
||||
<a
|
||||
href="https://openweathermap.org/api"
|
||||
className="text-blue-600 font-semibold underline hover:text-blue-800"
|
||||
@@ -25,26 +31,42 @@ const ChangeAPI: React.FC<Props> = ({ currentAPIKey }) => {
|
||||
>
|
||||
OpenWeatherMap
|
||||
</a>
|
||||
<span>API for fetching weather data.</span>
|
||||
<p>API for fetching weather data.</p>
|
||||
</div>
|
||||
<form className="flex flex-col gap-4">
|
||||
<label htmlFor="apiKey" className="font-medium text-gray-700">
|
||||
API Key:
|
||||
</label>
|
||||
<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-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 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>
|
||||
<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
|
||||
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"
|
||||
onClick={() => changeAPIcookie(apiKey)}
|
||||
onClick={handleUpdate}
|
||||
>
|
||||
Update API Key
|
||||
</button>
|
||||
|
@@ -1,11 +1,124 @@
|
||||
import React from "react";
|
||||
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" },
|
||||
];
|
||||
|
||||
const ChangePreferences: React.FC = () => {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<h2 className="text-2xl font-bold mb-2 text-blue-700">
|
||||
Change Preferences
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
@@ -75,6 +75,31 @@ const Header: React.FC = () => {
|
||||
Preferences
|
||||
</span>
|
||||
</button>
|
||||
<a
|
||||
href="https://git.the1s.de/theis.gaedigk/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>
|
||||
{apiCard && (
|
||||
@@ -87,7 +112,10 @@ const Header: React.FC = () => {
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<ChangeAPI currentAPIKey={apiKey} />
|
||||
<ChangeAPI
|
||||
currentAPIKey={apiKey}
|
||||
onClose={() => setApiCard(false)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -102,7 +130,7 @@ const Header: React.FC = () => {
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<ChangePreferences />
|
||||
<ChangePreferences onClose={() => setPreferencesCard(false)} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
@@ -19,7 +19,25 @@ const WeatherData: React.FC = () => {
|
||||
{weatherData?.name}
|
||||
</h2>
|
||||
<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"
|
||||
: "K"}
|
||||
</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 className="text-lg mb-4 capitalize text-blue-600">
|
||||
{weatherData?.weather?.[0]?.description}
|
||||
|
@@ -10,8 +10,8 @@ const WeatherCard: React.FC = () => {
|
||||
const [city, setCity] = useState("");
|
||||
const [loading, setLoading] = 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 getUnit = () => localStorage.getItem("unit") || "metric";
|
||||
|
||||
const handleCityChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setCity(event.target.value);
|
||||
@@ -21,10 +21,11 @@ const WeatherCard: React.FC = () => {
|
||||
event.preventDefault();
|
||||
setLoading(true);
|
||||
toast
|
||||
.promise(fetchWeather(city, getAPIKey(), "metric"), {
|
||||
.promise(fetchWeather(city, getAPIKey(), getUnit()), {
|
||||
pending: "Fetching weather data...",
|
||||
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.",
|
||||
})
|
||||
.then(() => {
|
||||
if (localStorage.getItem("weather")) {
|
||||
@@ -67,12 +68,40 @@ 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"
|
||||
/>
|
||||
<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"
|
||||
>
|
||||
Get Weather
|
||||
</button>
|
||||
<div className="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"
|
||||
>
|
||||
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>
|
||||
{weatherData && <WeatherData />}
|
||||
</div>
|
||||
|
@@ -1,22 +1,20 @@
|
||||
export type units = "metric" | "imperial";
|
||||
import { myToast } from "./toastify";
|
||||
|
||||
export const fetchWeather = async (
|
||||
city: string,
|
||||
apiKey: string,
|
||||
units: units
|
||||
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) {
|
||||
console.error(
|
||||
"You are not authorized to access this resource. Please check your API key."
|
||||
);
|
||||
myToast("You are not authorized to access this resource. Please check your API key.", "error");
|
||||
} else if (response.ok) {
|
||||
return response.json();
|
||||
} else {
|
||||
console.error("Error fetching location data: ", response.statusText);
|
||||
myToast("Error fetching location data: " + response.statusText, "error");
|
||||
}
|
||||
});
|
||||
const lat = location[0].lat;
|
||||
|
@@ -2,6 +2,7 @@ import Cookies from "js-cookie";
|
||||
import { myToast } from "./toastify";
|
||||
|
||||
export const changeAPIcookie = (newApiKey: string) => {
|
||||
let apiKey15 = newApiKey.slice(0, 15);
|
||||
Cookies.set("apiKey", newApiKey);
|
||||
myToast("API key updated successfully!", "success");
|
||||
myToast("API key updated successfully!" + " " + "Your new API key: " + apiKey15 + "...", "success");
|
||||
};
|
||||
|
Reference in New Issue
Block a user