Projekt: Wetter-App mit API
Projekt: Wetter-App mit API
In diesem Projekt baust du eine vollständige Wetter-App, die echte Wetterdaten von einer API lädt. Du lernst dabei:
- API-Integration mit fetch
- Async/await in der Praxis
- Fehlerbehandlung
- DOM-Manipulation
- Geolocation API
Das Endergebnis
+------------------------------------------+
| 🌤️ Wetter-App |
+------------------------------------------+
| [Berlin________________] [🔍 Suchen] |
| [📍 Standort verwenden] |
+------------------------------------------+
| |
| ☀️ |
| 25°C |
| Berlin, DE |
| "Klarer Himmel" |
| |
| 💨 Wind: 12 km/h | 💧 Feucht.: 45% |
| |
+------------------------------------------+
Projekt-Setup
1. API-Key holen
- Gehe zu openweathermap.org
- Erstelle einen kostenlosen Account
- Kopiere deinen API-Key
2. Dateien erstellen
wetter-app/
├── index.html
├── style.css
└── app.js
HTML-Struktur
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Wetter-App</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="app">
<header class="header">
<h1>🌤️ Wetter-App</h1>
</header>
<main class="main">
<!-- Suche -->
<div class="search-container">
<form class="search-form" id="search-form">
<input
type="text"
class="search-input"
id="city-input"
placeholder="Stadt eingeben..."
autocomplete="off"
>
<button type="submit" class="search-btn">🔍</button>
</form>
<button class="location-btn" id="location-btn">
📍 Meinen Standort verwenden
</button>
</div>
<!-- Loading -->
<div class="loading hidden" id="loading">
<div class="spinner"></div>
<p>Lade Wetterdaten...</p>
</div>
<!-- Error -->
<div class="error hidden" id="error">
<p class="error-message" id="error-message"></p>
</div>
<!-- Wetter-Anzeige -->
<div class="weather-container hidden" id="weather-container">
<div class="weather-main">
<img class="weather-icon" id="weather-icon" src="" alt="Wetter">
<div class="temperature" id="temperature"></div>
<div class="location" id="location"></div>
<div class="description" id="description"></div>
</div>
<div class="weather-details">
<div class="detail">
<span class="detail-icon">💨</span>
<span class="detail-label">Wind</span>
<span class="detail-value" id="wind"></span>
</div>
<div class="detail">
<span class="detail-icon">💧</span>
<span class="detail-label">Feuchtigkeit</span>
<span class="detail-value" id="humidity"></span>
</div>
<div class="detail">
<span class="detail-icon">🌡️</span>
<span class="detail-label">Gefühlt</span>
<span class="detail-value" id="feels-like"></span>
</div>
<div class="detail">
<span class="detail-icon">👁️</span>
<span class="detail-label">Sicht</span>
<span class="detail-value" id="visibility"></span>
</div>
</div>
</div>
</main>
</div>
<script src="app.js"></script>
</body>
</html>
CSS-Styling
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--primary: #6366f1;
--primary-dark: #4f46e5;
--bg: #0f172a;
--card: #1e293b;
--border: #334155;
--text: #f1f5f9;
--text-muted: #94a3b8;
--success: #22c55e;
--error: #ef4444;
}
body {
font-family: 'Segoe UI', system-ui, sans-serif;
background: linear-gradient(135deg, var(--bg) 0%, #1e1b4b 100%);
color: var(--text);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 1rem;
}
.app {
width: 100%;
max-width: 400px;
background: var(--card);
border-radius: 24px;
overflow: hidden;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
}
.header {
padding: 1.5rem;
text-align: center;
background: linear-gradient(135deg, var(--primary), #8b5cf6);
}
.header h1 {
font-size: 1.5rem;
font-weight: 700;
}
.main {
padding: 1.5rem;
}
/* Search */
.search-container {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-bottom: 1.5rem;
}
.search-form {
display: flex;
gap: 0.5rem;
}
.search-input {
flex: 1;
padding: 0.875rem 1rem;
background: var(--bg);
border: 2px solid var(--border);
border-radius: 12px;
color: var(--text);
font-size: 1rem;
}
.search-input:focus {
outline: none;
border-color: var(--primary);
}
.search-btn {
padding: 0.875rem 1.25rem;
background: var(--primary);
border: none;
border-radius: 12px;
cursor: pointer;
font-size: 1rem;
}
.search-btn:hover {
background: var(--primary-dark);
}
.location-btn {
padding: 0.75rem;
background: transparent;
border: 1px solid var(--border);
border-radius: 10px;
color: var(--text-muted);
cursor: pointer;
transition: all 0.2s;
}
.location-btn:hover {
border-color: var(--primary);
color: var(--text);
}
/* Loading */
.loading {
text-align: center;
padding: 2rem;
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid var(--border);
border-top-color: var(--primary);
border-radius: 50%;
margin: 0 auto 1rem;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Error */
.error {
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3);
border-radius: 12px;
padding: 1rem;
text-align: center;
color: var(--error);
}
/* Weather Display */
.weather-container {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.weather-main {
text-align: center;
padding: 1rem 0 2rem;
}
.weather-icon {
width: 120px;
height: 120px;
}
.temperature {
font-size: 4rem;
font-weight: 700;
line-height: 1;
margin-bottom: 0.5rem;
}
.location {
font-size: 1.25rem;
color: var(--text);
margin-bottom: 0.25rem;
}
.description {
font-size: 1rem;
color: var(--text-muted);
text-transform: capitalize;
}
/* Details Grid */
.weather-details {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
padding-top: 1.5rem;
border-top: 1px solid var(--border);
}
.detail {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
}
.detail-icon {
font-size: 1.5rem;
}
.detail-label {
font-size: 0.75rem;
color: var(--text-muted);
}
.detail-value {
font-weight: 600;
}
/* Utility */
.hidden {
display: none !important;
}
JavaScript - Die App-Logik
// === Configuration ===
const API_KEY = "DEIN_API_KEY_HIER"; // Ersetze mit deinem Key!
const API_BASE = "https://api.openweathermap.org/data/2.5";
// === DOM Elements ===
const elements = {
searchForm: document.getElementById("search-form"),
cityInput: document.getElementById("city-input"),
locationBtn: document.getElementById("location-btn"),
loading: document.getElementById("loading"),
error: document.getElementById("error"),
errorMessage: document.getElementById("error-message"),
weatherContainer: document.getElementById("weather-container"),
weatherIcon: document.getElementById("weather-icon"),
temperature: document.getElementById("temperature"),
location: document.getElementById("location"),
description: document.getElementById("description"),
wind: document.getElementById("wind"),
humidity: document.getElementById("humidity"),
feelsLike: document.getElementById("feels-like"),
visibility: document.getElementById("visibility")
};
// === State ===
let currentCity = localStorage.getItem("lastCity") || "";
// === API Functions ===
async function fetchWeather(city) {
const url = `${API_BASE}/weather?q=${encodeURIComponent(city)}&appid=${API_KEY}&units=metric&lang=de`;
const response = await fetch(url);
if (!response.ok) {
if (response.status === 404) {
throw new Error("Stadt nicht gefunden. Bitte überprüfe die Eingabe.");
}
if (response.status === 401) {
throw new Error("API-Key ungültig. Bitte überprüfe deine Konfiguration.");
}
throw new Error("Wetterdaten konnten nicht geladen werden.");
}
return response.json();
}
async function fetchWeatherByCoords(lat, lon) {
const url = `${API_BASE}/weather?lat=${lat}&lon=${lon}&appid=${API_KEY}&units=metric&lang=de`;
const response = await fetch(url);
if (!response.ok) {
throw new Error("Wetterdaten konnten nicht geladen werden.");
}
return response.json();
}
// === UI Functions ===
function showLoading() {
elements.loading.classList.remove("hidden");
elements.error.classList.add("hidden");
elements.weatherContainer.classList.add("hidden");
}
function hideLoading() {
elements.loading.classList.add("hidden");
}
function showError(message) {
elements.errorMessage.textContent = message;
elements.error.classList.remove("hidden");
elements.weatherContainer.classList.add("hidden");
}
function displayWeather(data) {
// Icon URL
const iconUrl = `https://openweathermap.org/img/wn/${data.weather[0].icon}@4x.png`;
elements.weatherIcon.src = iconUrl;
elements.weatherIcon.alt = data.weather[0].description;
// Hauptdaten
elements.temperature.textContent = `${Math.round(data.main.temp)}°C`;
elements.location.textContent = `${data.name}, ${data.sys.country}`;
elements.description.textContent = data.weather[0].description;
// Details
elements.wind.textContent = `${Math.round(data.wind.speed * 3.6)} km/h`;
elements.humidity.textContent = `${data.main.humidity}%`;
elements.feelsLike.textContent = `${Math.round(data.main.feels_like)}°C`;
elements.visibility.textContent = `${(data.visibility / 1000).toFixed(1)} km`;
// Anzeigen
elements.error.classList.add("hidden");
elements.weatherContainer.classList.remove("hidden");
// Letzte Stadt speichern
localStorage.setItem("lastCity", data.name);
}
// === Event Handlers ===
async function handleSearch(city) {
if (!city.trim()) {
showError("Bitte gib eine Stadt ein.");
return;
}
showLoading();
try {
const data = await fetchWeather(city);
displayWeather(data);
} catch (error) {
showError(error.message);
} finally {
hideLoading();
}
}
async function handleGeolocation() {
if (!navigator.geolocation) {
showError("Geolocation wird von deinem Browser nicht unterstützt.");
return;
}
showLoading();
try {
const position = await new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject, {
enableHighAccuracy: true,
timeout: 10000
});
});
const { latitude, longitude } = position.coords;
const data = await fetchWeatherByCoords(latitude, longitude);
displayWeather(data);
} catch (error) {
if (error.code === 1) {
showError("Standortzugriff verweigert. Bitte erlaube den Zugriff oder suche manuell.");
} else if (error.code === 2) {
showError("Standort konnte nicht ermittelt werden.");
} else if (error.code === 3) {
showError("Standortabfrage hat zu lange gedauert.");
} else {
showError(error.message);
}
} finally {
hideLoading();
}
}
// === Event Listeners ===
elements.searchForm.addEventListener("submit", (e) => {
e.preventDefault();
handleSearch(elements.cityInput.value);
});
elements.locationBtn.addEventListener("click", () => {
handleGeolocation();
});
// === Initialization ===
function init() {
// Letzte Stadt laden
if (currentCity) {
elements.cityInput.value = currentCity;
handleSearch(currentCity);
}
// Fokus auf Input
elements.cityInput.focus();
}
// App starten
init();
Features erweitern
5-Tage-Vorhersage hinzufügen
async function fetchForecast(city) {
const url = `${API_BASE}/forecast?q=${encodeURIComponent(city)}&appid=${API_KEY}&units=metric&lang=de`;
const response = await fetch(url);
if (!response.ok) throw new Error("Vorhersage konnte nicht geladen werden");
return response.json();
}
function displayForecast(data) {
// Tägliche Vorhersage extrahieren (12:00 Uhr jeden Tag)
const daily = data.list.filter(item =>
item.dt_txt.includes("12:00:00")
).slice(0, 5);
const forecastHTML = daily.map(day => {
const date = new Date(day.dt * 1000);
const dayName = date.toLocaleDateString("de-DE", { weekday: "short" });
const icon = `https://openweathermap.org/img/wn/${day.weather[0].icon}.png`;
return `
<div class="forecast-day">
<span class="forecast-name">${dayName}</span>
<img src="${icon}" alt="${day.weather[0].description}">
<span class="forecast-temp">${Math.round(day.main.temp)}°</span>
</div>
`;
}).join("");
document.getElementById("forecast").textContent = "";
const div = document.createElement("div");
div.className = "forecast-container";
const parser = new DOMParser();
daily.forEach(day => {
const date = new Date(day.dt * 1000);
const dayName = date.toLocaleDateString("de-DE", { weekday: "short" });
const icon = `https://openweathermap.org/img/wn/${day.weather[0].icon}.png`;
const dayEl = document.createElement("div");
dayEl.className = "forecast-day";
const nameSpan = document.createElement("span");
nameSpan.className = "forecast-name";
nameSpan.textContent = dayName;
const img = document.createElement("img");
img.src = icon;
img.alt = day.weather[0].description;
const tempSpan = document.createElement("span");
tempSpan.className = "forecast-temp";
tempSpan.textContent = `${Math.round(day.main.temp)}°`;
dayEl.appendChild(nameSpan);
dayEl.appendChild(img);
dayEl.appendChild(tempSpan);
div.appendChild(dayEl);
});
document.getElementById("forecast").appendChild(div);
}
Letzte Suchanfragen speichern
function saveRecentSearch(city) {
let recent = JSON.parse(localStorage.getItem("recentCities") || "[]");
recent = recent.filter(c => c.toLowerCase() !== city.toLowerCase());
recent.unshift(city);
recent = recent.slice(0, 5); // Max 5 speichern
localStorage.setItem("recentCities", JSON.stringify(recent));
displayRecentSearches();
}
function displayRecentSearches() {
const recent = JSON.parse(localStorage.getItem("recentCities") || "[]");
const container = document.getElementById("recent-searches");
if (recent.length === 0) {
container.classList.add("hidden");
return;
}
container.classList.remove("hidden");
const list = container.querySelector(".recent-list");
list.textContent = "";
recent.forEach(city => {
const btn = document.createElement("button");
btn.className = "recent-btn";
btn.textContent = city;
btn.addEventListener("click", () => handleSearch(city));
list.appendChild(btn);
});
}
Was du gelernt hast
-
API-Integration
- Fetch API verwenden
- Query-Parameter bauen
- Response verarbeiten
-
Async/Await
- Asynchronen Code schreiben
- Fehler mit try/catch behandeln
- Loading-States verwalten
-
DOM-Manipulation
- Elemente dynamisch aktualisieren
- Klassen togglen
- Event Handler
-
Browser APIs
- Geolocation API
- LocalStorage
-
UX Best Practices
- Loading-Spinner
- Fehler-Anzeige
- Letzte Suche speichern
Challenge: Erweitere die App um einen Dark/Light Mode Toggle und zeige Sonnenaufgang/Sonnenuntergang an!