Bonus: fetch & API – JSONPlaceholder Users, DOM-Rendering, Fehlerbehandlung, Filter

This commit is contained in:
David Kertzscher 2026-05-20 13:03:38 +00:00
parent 21d6e4e451
commit d2b2132605
12 changed files with 593 additions and 6 deletions

143
api.html Normal file
View file

@ -0,0 +1,143 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="API Demo fetch & JSON David & Karo EIS Projekt">
<title>API Demo David & Karo</title>
<link rel="stylesheet" href="css/style.css">
<link rel="icon" href="img/logo.png" type="image/png">
</head>
<body>
<header>
<div class="header-container">
<div class="logo">
<img src="img/logo.png" alt="Logo">
<div class="logo-text">
<span class="institution">David & Karo</span>
<span class="project">EIS Projekt</span>
</div>
</div>
<!-- Dark Mode Toggle Button -->
<button id="dark-mode-toggle" type="button" class="dark-mode-toggle" title="Dark Mode aktivieren">
<span class="dark-mode-icon">🌙</span>
</button>
<!-- Hamburger Menu Button (nur Mobile) -->
<button id="nav-toggle" type="button" class="nav-toggle" aria-label="Menü öffnen" aria-expanded="false">
<span></span><span></span><span></span>
</button>
<nav id="main-nav">
<a href="index.html">Start</a>
<a href="ueber_uns.html">Über uns</a>
<a href="eis_projekt.html">Projekt</a>
<a href="team.html">Team</a>
<a href="galerie.html">Galerie</a>
<a href="notenrechner.html">Notenrechner</a>
<a href="textanalyse.html">Textanalyse</a>
<a href="api.html" class="active">API Demo</a>
<a href="kontakt.html">Kontakt</a>
<a href="impressum.html">Impressum</a>
</nav>
</div>
</header>
<main>
<section class="hero">
<h1>🌐 fetch & API Demo</h1>
<p>Live-Daten von <code>jsonplaceholder.typicode.com/users</code> geladen per JavaScript fetch(). ✨</p>
</section>
<section>
<h2>💡 Wie funktioniert's?</h2>
<div class="info-cards">
<div class="info-card">
<h4>1⃣ fetch(URL)</h4>
<p>JavaScript sendet einen HTTP-GET Request an die API-URL und erhält eine Promise zurück.</p>
</div>
<div class="info-card">
<h4>2⃣ .then(r =&gt; r.json())</h4>
<p>Die Antwort wird als JSON geparst aus Text werden echte JavaScript-Objekte.</p>
</div>
<div class="info-card">
<h4>3⃣ DOM rendern</h4>
<p>Die Daten werden mit <code>createElement</code> in den DOM eingefügt kein Reload nötig!</p>
</div>
<div class="info-card">
<h4>4⃣ .catch(err)</h4>
<p>Fehlerbehandlung: Falls die API nicht erreichbar ist, wird eine Fehlermeldung angezeigt.</p>
</div>
</div>
</section>
<section>
<h2>👥 Benutzer von der API</h2>
<p>Die Liste wird live beim Laden der Seite von <strong>jsonplaceholder.typicode.com</strong> abgerufen:</p>
<div class="api-controls">
<button type="button" id="btn-reload" class="btn">🔄 Neu laden</button>
<input type="text" id="api-search" placeholder="🔍 Name oder Stadt filtern…" aria-label="Benutzer filtern">
</div>
<!-- Lade-Indikator -->
<div id="api-loading" class="api-loading">
<div class="api-spinner"></div>
<p>Daten werden geladen…</p>
</div>
<!-- Fehlermeldung -->
<div id="api-error" class="api-error" hidden>
<span></span>
<div>
<strong>Fehler beim Laden der Daten.</strong><br>
<span id="api-error-msg"></span>
</div>
<button type="button" id="btn-retry" class="btn">Erneut versuchen</button>
</div>
<!-- Benutzer-Liste -->
<ul id="user-list" class="user-list" aria-live="polite"></ul>
<!-- Anzahl -->
<p id="user-count" class="api-count"></p>
</section>
<section>
<h2>📄 Quellcode-Erklärung</h2>
<pre class="code-block"><code>// 1. fetch aufrufen
fetch('https://jsonplaceholder.typicode.com/users')
// 2. Antwort als JSON parsen
.then(response => {
if (!response.ok) {
throw new Error('HTTP ' + response.status);
}
return response.json();
})
// 3. Daten in console.log prüfen & im DOM rendern
.then(users => {
console.log('API-Daten:', users);
renderUsers(users);
})
// 4. Fehlerbehandlung
.catch(error => {
console.error('Fetch-Fehler:', error);
showApiError(error.message);
});</code></pre>
</section>
</main>
<footer>
<p>&copy; 2026 Made with 💕 & David & Karo</p>
<div class="footer-links">
<a href="impressum.html">Impressum</a>
<a href="kontakt.html">Kontakt</a>
<a href="#">Datenschutz</a>
</div>
</footer>
<script src="js/script.js"></script>
<script src="js/api-demo.js"></script>
</body>
</html>

View file

@ -2926,3 +2926,264 @@ body.dark .nav-toggle span {
text-align: center;
}
}
/*
API Demo api.html
*/
/* Info-Cards (Erklärung wie fetch funktioniert) */
.info-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.25rem;
margin: 1.5rem 0;
}
.info-card {
background: linear-gradient(135deg, #fff0f5, #fce7f3);
border: 1px solid #f9a8d4;
border-radius: 12px;
padding: 1.25rem;
text-align: center;
}
.info-card h4 {
color: #be185d;
margin-bottom: 0.5rem;
font-size: 1rem;
}
.info-card p {
font-size: 0.9rem;
color: #555;
margin: 0;
}
.info-card code {
background: #fce7f3;
padding: 0.1em 0.35em;
border-radius: 4px;
font-size: 0.85em;
color: #be185d;
}
/* API Controls (Button + Suchfeld) */
.api-controls {
display: flex;
gap: 1rem;
align-items: center;
flex-wrap: wrap;
margin: 1.25rem 0;
}
#api-search {
flex: 1 1 220px;
padding: 0.6rem 1rem;
border: 2px solid #f9a8d4;
border-radius: 25px;
font-size: 0.95rem;
outline: none;
transition: border-color 0.2s;
}
#api-search:focus {
border-color: #ec4899;
}
/* Lade-Indikator */
.api-loading {
display: flex;
align-items: center;
gap: 1rem;
padding: 1.5rem;
color: #be185d;
}
.api-loading[hidden] { display: none; }
.api-spinner {
width: 32px;
height: 32px;
border: 4px solid #fce7f3;
border-top-color: #ec4899;
border-radius: 50%;
animation: spin 0.8s linear infinite;
flex-shrink: 0;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Fehlermeldung */
.api-error {
display: flex;
align-items: flex-start;
gap: 1rem;
background: #fff0f0;
border: 1px solid #fca5a5;
border-radius: 10px;
padding: 1rem 1.25rem;
color: #b91c1c;
margin: 1rem 0;
}
.api-error[hidden] { display: none; }
.api-error span:first-child {
font-size: 1.5rem;
flex-shrink: 0;
}
/* Benutzer-Liste */
.user-list {
list-style: none;
padding: 0;
margin: 0;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.25rem;
}
/* User-Karte */
.api-user-card {
background: #fff;
border: 1px solid #f9a8d4;
border-radius: 14px;
padding: 1.25rem;
display: flex;
gap: 1rem;
align-items: flex-start;
box-shadow: 0 2px 8px rgba(236,72,153,0.08);
transition: transform 0.2s, box-shadow 0.2s;
}
.api-user-card:hover {
transform: translateY(-3px);
box-shadow: 0 6px 18px rgba(236,72,153,0.15);
}
.api-user-avatar {
width: 48px;
height: 48px;
background: linear-gradient(135deg, #ec4899, #be185d);
color: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.3rem;
font-weight: 700;
flex-shrink: 0;
}
.api-user-name {
font-size: 1rem;
font-weight: 700;
color: #be185d;
margin: 0 0 0.15rem;
}
.api-user-username {
font-size: 0.85rem;
color: #888;
margin: 0 0 0.5rem;
}
.api-user-details {
list-style: none;
padding: 0;
margin: 0;
font-size: 0.85rem;
color: #444;
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.api-user-details a {
color: #ec4899;
text-decoration: none;
}
.api-user-details a:hover {
text-decoration: underline;
}
/* Ergebnis-Zähler */
.api-count {
text-align: right;
font-size: 0.9rem;
color: #888;
margin-top: 0.75rem;
}
/* Keine Ergebnisse */
.api-no-results {
grid-column: 1 / -1;
text-align: center;
padding: 2rem;
color: #aaa;
font-size: 1rem;
}
/* Code-Block */
.code-block {
background: #1e1e2e;
color: #cdd6f4;
border-radius: 12px;
padding: 1.5rem;
overflow-x: auto;
font-size: 0.88rem;
line-height: 1.65;
border: 1px solid #313244;
}
.code-block code {
font-family: 'Courier New', Courier, monospace;
}
/* ── Dark Mode ── */
body.dark .info-card {
background: linear-gradient(135deg, #1e1e2e, #2d2d3f);
border-color: #7c3aed44;
}
body.dark .info-card h4 { color: #f9a8d4; }
body.dark .info-card p { color: #aaa; }
body.dark .info-card code {
background: #2d2d3f;
color: #f9a8d4;
}
body.dark #api-search {
background: #1e1e2e;
color: #f0f0f0;
border-color: #7c3aed55;
}
body.dark .api-loading { color: #f9a8d4; }
body.dark .api-spinner { border-color: #333; border-top-color: #f9a8d4; }
body.dark .api-error {
background: #2d1515;
border-color: #7f1d1d;
color: #fca5a5;
}
body.dark .api-user-card {
background: #1e1e2e;
border-color: #7c3aed44;
}
body.dark .api-user-name { color: #f9a8d4; }
body.dark .api-user-username { color: #777; }
body.dark .api-user-details { color: #ccc; }
body.dark .api-user-details a { color: #f9a8d4; }
/* ── Responsive ── */
@media (max-width: 600px) {
.api-controls { flex-direction: column; align-items: stretch; }
#api-search { flex: unset; width: 100%; }
.user-list { grid-template-columns: 1fr; }
}

View file

@ -34,6 +34,7 @@
<a href="galerie.html">Galerie</a>
<a href="notenrechner.html">Notenrechner</a>
<a href="textanalyse.html">Textanalyse</a>
<a href="api.html">API Demo</a>
<a href="kontakt.html">Kontakt</a>
<a href="impressum.html">Impressum</a>
</nav>
@ -175,7 +176,8 @@
<p>&copy; 2026 Made with 💕 & David & Karo</p>
<div class="footer-links">
<a href="impressum.html">Impressum</a>
<a href="kontakt.html">Kontakt</a>
<a href="api.html">API Demo</a>
<a href="kontakt.html">Kontakt</a>
<a href="#">Datenschutz</a>
</div>
</footer>

View file

@ -34,6 +34,7 @@
<a href="galerie.html" class="active">Galerie</a>
<a href="notenrechner.html">Notenrechner</a>
<a href="textanalyse.html">Textanalyse</a>
<a href="api.html">API Demo</a>
<a href="kontakt.html">Kontakt</a>
<a href="impressum.html">Impressum</a>
</nav>
@ -87,7 +88,8 @@
<p>&copy; 2026 Made with 💕 & David & Karo</p>
<div class="footer-links">
<a href="impressum.html">Impressum</a>
<a href="kontakt.html">Kontakt</a>
<a href="api.html">API Demo</a>
<a href="kontakt.html">Kontakt</a>
<a href="#">Datenschutz</a>
</div>
</footer>

View file

@ -34,6 +34,7 @@
<a href="galerie.html">Galerie</a>
<a href="notenrechner.html">Notenrechner</a>
<a href="textanalyse.html">Textanalyse</a>
<a href="api.html">API Demo</a>
<a href="kontakt.html">Kontakt</a>
<a href="impressum.html" class="active">Impressum</a>
</nav>
@ -117,7 +118,8 @@
<p>&copy; 2026 Made with 💕 & David & Karo</p>
<div class="footer-links">
<a href="impressum.html">Impressum</a>
<a href="kontakt.html">Kontakt</a>
<a href="api.html">API Demo</a>
<a href="kontakt.html">Kontakt</a>
<a href="#">Datenschutz</a>
</div>
</footer>

View file

@ -34,6 +34,7 @@
<a href="galerie.html">Galerie</a>
<a href="notenrechner.html">Notenrechner</a>
<a href="textanalyse.html">Textanalyse</a>
<a href="api.html">API Demo</a>
<a href="kontakt.html">Kontakt</a>
<a href="impressum.html">Impressum</a>
</nav>
@ -87,7 +88,8 @@
<p>&copy; 2026 Made with 💕 & David & Karo</p>
<div class="footer-links">
<a href="impressum.html">Impressum</a>
<a href="kontakt.html">Kontakt</a>
<a href="api.html">API Demo</a>
<a href="kontakt.html">Kontakt</a>
<a href="#">Datenschutz</a>
</div>
</footer>

169
js/api-demo.js Normal file
View file

@ -0,0 +1,169 @@
/**
* api-demo.js
* Fetch & API Demo jsonplaceholder.typicode.com/users
* Lädt Benutzer per fetch(), rendert sie im DOM, mit Fehlerbehandlung & Filterung.
*/
document.addEventListener('DOMContentLoaded', function () {
// ── DOM-Referenzen ────────────────────────────────────────────────────────
const userList = document.getElementById('user-list');
const loadingEl = document.getElementById('api-loading');
const errorEl = document.getElementById('api-error');
const errorMsgEl = document.getElementById('api-error-msg');
const countEl = document.getElementById('user-count');
const searchInput = document.getElementById('api-search');
const btnReload = document.getElementById('btn-reload');
const btnRetry = document.getElementById('btn-retry');
// Nur auf der API-Demo-Seite ausführen
if (!userList) return;
// Alle geladenen User merken (für Client-seitigen Filter)
let allUsers = [];
// ── Hilfsfunktionen ───────────────────────────────────────────────────────
/** Zeigt den Lade-Spinner */
function showLoading () {
loadingEl.removeAttribute('hidden');
errorEl.setAttribute('hidden', '');
userList.innerHTML = '';
countEl.textContent = '';
if (btnReload) btnReload.disabled = true;
}
/** Versteckt den Lade-Spinner */
function hideLoading () {
loadingEl.setAttribute('hidden', '');
if (btnReload) btnReload.disabled = false;
}
/** Zeigt eine Fehlermeldung an */
function showApiError (message) {
hideLoading();
errorMsgEl.textContent = message;
errorEl.removeAttribute('hidden');
countEl.textContent = '';
}
/**
* Erstellt eine User-Karte als <li>-Element
* @param {Object} user JSON-Objekt aus der API
* @returns {HTMLLIElement}
*/
function createUserCard (user) {
const li = document.createElement('li');
li.className = 'api-user-card';
li.innerHTML = `
<div class="api-user-avatar" aria-hidden="true">
${user.name.charAt(0).toUpperCase()}
</div>
<div class="api-user-info">
<h3 class="api-user-name">${user.name}</h3>
<p class="api-user-username">@${user.username}</p>
<ul class="api-user-details">
<li>📧 <a href="mailto:${user.email}">${user.email}</a></li>
<li>🏙 ${user.address.city}</li>
<li>💼 ${user.company.name}</li>
<li>🌐 <a href="https://${user.website}" target="_blank" rel="noopener noreferrer">${user.website}</a></li>
</ul>
</div>
`;
return li;
}
/**
* Rendert eine Liste von Usern in den DOM
* @param {Array} users
*/
function renderUsers (users) {
userList.innerHTML = '';
if (users.length === 0) {
userList.innerHTML = '<li class="api-no-results">Keine Benutzer gefunden. 🔍</li>';
countEl.textContent = '';
return;
}
// DocumentFragment für performantes Einfügen (ein Reflow statt n)
const fragment = document.createDocumentFragment();
users.forEach(function (user) {
fragment.appendChild(createUserCard(user));
});
userList.appendChild(fragment);
countEl.textContent = users.length + ' Benutzer geladen ✅';
}
// ── Haupt-Fetch-Funktion ──────────────────────────────────────────────────
function loadUsers () {
showLoading();
// ① fetch() aufrufen
fetch('https://jsonplaceholder.typicode.com/users')
// ② Antwort prüfen & als JSON parsen
.then(function (response) {
if (!response.ok) {
throw new Error('HTTP-Fehler: ' + response.status + ' ' + response.statusText);
}
return response.json();
})
// ③ Daten verarbeiten & im DOM rendern
.then(function (users) {
console.log('✅ API-Antwort erhalten:', users);
console.table(users.map(function (u) {
return { Name: u.name, 'E-Mail': u.email, Stadt: u.address.city, Firma: u.company.name };
}));
allUsers = users;
hideLoading();
renderUsers(allUsers);
// Suchfeld zurücksetzen
if (searchInput) searchInput.value = '';
})
// ④ Fehlerbehandlung
.catch(function (err) {
console.error('❌ Fetch-Fehler:', err);
showApiError(err.message || 'Unbekannter Fehler. Bitte Internetverbindung prüfen.');
});
}
// ── Client-seitiger Filter ────────────────────────────────────────────────
function filterUsers (query) {
if (!query.trim()) {
renderUsers(allUsers);
return;
}
const q = query.toLowerCase();
const filtered = allUsers.filter(function (user) {
return (
user.name.toLowerCase().includes(q) ||
user.username.toLowerCase().includes(q) ||
user.address.city.toLowerCase().includes(q) ||
user.company.name.toLowerCase().includes(q) ||
user.email.toLowerCase().includes(q)
);
});
renderUsers(filtered);
}
// ── Event Listener ────────────────────────────────────────────────────────
if (btnReload) btnReload.addEventListener('click', loadUsers);
if (btnRetry) btnRetry.addEventListener('click', loadUsers);
if (searchInput) {
searchInput.addEventListener('input', function () {
filterUsers(this.value);
});
}
// ── Beim Seitenaufruf automatisch laden ───────────────────────────────────
loadUsers();
});

View file

@ -127,7 +127,8 @@
<p>&copy; 2026 Made with 💕 & David & Karo</p>
<div class="footer-links">
<a href="impressum.html">Impressum</a>
<a href="kontakt.html">Kontakt</a>
<a href="api.html">API Demo</a>
<a href="kontakt.html">Kontakt</a>
<a href="#">Datenschutz</a>
</div>
</footer>

View file

@ -34,6 +34,7 @@
<a href="galerie.html">Galerie</a>
<a href="notenrechner.html" class="active">Notenrechner</a>
<a href="textanalyse.html">Textanalyse</a>
<a href="api.html">API Demo</a>
<a href="kontakt.html">Kontakt</a>
<a href="impressum.html">Impressum</a>
</nav>

View file

@ -34,6 +34,7 @@
<a href="galerie.html">Galerie</a>
<a href="notenrechner.html">Notenrechner</a>
<a href="textanalyse.html">Textanalyse</a>
<a href="api.html">API Demo</a>
<a href="kontakt.html">Kontakt</a>
<a href="impressum.html">Impressum</a>
</nav>

View file

@ -34,6 +34,7 @@
<a href="galerie.html">Galerie</a>
<a href="notenrechner.html">Notenrechner</a>
<a href="textanalyse.html" class="active">Textanalyse</a>
<a href="api.html">API Demo</a>
<a href="kontakt.html">Kontakt</a>
<a href="impressum.html">Impressum</a>
</nav>

View file

@ -34,6 +34,7 @@
<a href="galerie.html">Galerie</a>
<a href="notenrechner.html">Notenrechner</a>
<a href="textanalyse.html">Textanalyse</a>
<a href="api.html">API Demo</a>
<a href="kontakt.html">Kontakt</a>
<a href="impressum.html">Impressum</a>
</nav>
@ -108,7 +109,8 @@
<p>&copy; 2026 Made with 💕 & David & Karo</p>
<div class="footer-links">
<a href="impressum.html">Impressum</a>
<a href="kontakt.html">Kontakt</a>
<a href="api.html">API Demo</a>
<a href="kontakt.html">Kontakt</a>
<a href="#">Datenschutz</a>
</div>
</footer>