JavaScript Fortgeschritten

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

  1. Gehe zu openweathermap.org
  2. Erstelle einen kostenlosen Account
  3. 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

  1. API-Integration

    • Fetch API verwenden
    • Query-Parameter bauen
    • Response verarbeiten
  2. Async/Await

    • Asynchronen Code schreiben
    • Fehler mit try/catch behandeln
    • Loading-States verwalten
  3. DOM-Manipulation

    • Elemente dynamisch aktualisieren
    • Klassen togglen
    • Event Handler
  4. Browser APIs

    • Geolocation API
    • LocalStorage
  5. 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!