Uncategorised
- Details
- Geschrieben von: admin
- Kategorie: Uncategorised
- Zugriffe: 12
Pokémon
Ruf-Galerie
Finde und höre jedes Pokémon.
Lade Pokémon...
- Details
- Geschrieben von: admin
- Kategorie: Uncategorised
- Zugriffe: 5
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title id="pageTitle">Pokémon-Ruf-Galerie</title>
<!-- Lade Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Lade Lucide Icons -->
<script type="module">
// Import der Lucide Icons. 'Download' wurde für Kompatibilität verwendet.
import { createIcons, Zap, Volume2, Search, Loader, Globe, Download, BellRing } from 'https://unpkg.com/lucide@latest/dist/umd/lucide.js';
createIcons({ icons: { Zap, Volume2, Search, Loader, Globe, Download, BellRing } });
</script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&family=Press+Start+2P&display=swap');
body {
font-family: 'Inter', sans-serif;
background-color: #f7f7f7;
}
/* Eigene Schriftart für das Logo */
.logo-font {
font-family: 'Press Start 2P', cursive;
text-shadow:
-2px -2px 0 #3b82f6,
2px -2px 0 #3b82f6,
-2px 2px 0 #3b82f6,
2px 2px 0 #3b82f6,
4px 4px 0 #a5b4fc;
-webkit-text-stroke: 1px #3b82f6;
}
/* Custom scrollbar für besseren Look */
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-thumb { background: #ef4444; border-radius: 4px; }
::-webkit-scrollbar-track { background: #fca5a5; }
.pokemon-card {
transition: transform 0.2s, box-shadow 0.2s;
cursor: pointer;
position: relative;
user-select: none;
-webkit-user-select: none;
touch-action: manipulation;
}
.pokemon-card:hover {
transform: translateY(-4px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
/* Visuelles Feedback für den Langen Druck */
.pokemon-card.long-press-active {
box-shadow: 0 0 0 5px rgba(255, 193, 7, 0.5); /* Gelber Rahmen als Halte-Indikator */
}
/* Animation für den Sound-Indikator */
.cry-indicator i[data-lucide="volume-2"]:hover {
animation: pulse-ring 1.5s infinite;
}
@keyframes pulse-ring {
0% { transform: scale(0.9); opacity: 1; }
70% { transform: scale(1.1); opacity: 0.5; }
100% { transform: scale(0.9); opacity: 1; }
}
/* Icon-Platzierung für visuelles Feedback */
.action-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0);
opacity: 0;
transition: all 0.2s ease-out;
pointer-events: none;
z-index: 10;
background: rgba(255, 255, 255, 0.8);
border-radius: 50%;
padding: 8px;
}
.action-icon.show-play {
transform: translate(-50%, -50%) scale(1);
opacity: 1;
color: #ef4444; /* Rot für Play */
animation: none;
}
</style>
</head>
<body class="p-4 sm:p-8">
<div class="max-w-6xl mx-auto">
<!-- Header und Banner im Pokémon-Stil -->
<header class="mb-8 pb-4 border-b-4 border-red-500 relative overflow-hidden rounded-xl bg-gradient-to-r from-red-600 to-red-400 shadow-xl p-6 sm:p-8 text-white">
<!-- Hintergrundmuster -->
<div class="absolute inset-0 opacity-10 bg-[url('https://www.transparenttextures.com/patterns/cubes.png')]"></div>
<!-- Sprachauswahl -->
<div class="absolute top-4 right-4 z-20 flex items-center bg-white/20 backdrop-blur-sm rounded-full pr-3">
<i data-lucide="globe" class="w-5 h-5 ml-3 mr-2"></i>
<select id="languageSelector" onchange="changeLanguage(this.value)" class="bg-transparent text-white text-sm font-semibold focus:outline-none cursor-pointer">
<option value="de" class="text-gray-800">Deutsch</option>
<option value="en" class="text-gray-800">English</option>
<option value="es" class="text-gray-800">Español</option>
<option value="fr" class="text-gray-800">Français</option>
<option value="it" class="text-gray-800">Italiano</option>
<option value="ja" class="text-gray-800">日本語</option>
</select>
</div>
<div class="relative z-10 flex flex-col sm:flex-row items-center justify-center sm:justify-start pt-6 sm:pt-0">
<!-- Animierter Pokéball -->
<img src="https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/poke-ball.png" alt="Pokéball" class="w-16 h-16 sm:w-20 sm:h-20 mr-0 sm:mr-4 mb-4 sm:mb-0 animate-bounce-slow">
<div>
<!-- Logo Text mit Spezialschnitt (Pixel-Font) -->
<h1 class="text-4xl sm:text-5xl font-extrabold logo-font text-white mb-2 text-center sm:text-left">
Pokémon
</h1>
<p id="headerSubtitle" class="text-lg sm:text-xl font-semibold text-white text-center sm:text-left">
Ruf-Galerie
</p>
<p id="headerDescription" class="text-sm opacity-90 mt-2 text-center sm:text-left">
Finde und höre jedes Pokémon.
</p>
</div>
</div>
</header>
<!-- Suchleiste (Sticky) -->
<div class="mb-6 sticky top-0 bg-[#f7f7f7] py-3 z-10 -mx-4 px-4 sm:mx-0 sm:px-0 shadow-md rounded-xl">
<div class="relative">
<i data-lucide="search" class="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400"></i>
<input
type="text"
id="searchInput"
placeholder="Pokémon-Namen oder ID suchen (z.B. Glurak, 6)..."
class="w-full p-3 pl-10 border border-gray-300 rounded-xl focus:ring-red-500 focus:border-red-500 transition shadow-inner"
oninput="filterAndFetchPokemons()"
/>
</div>
</div>
<!-- Lade- und Fehlerstatus -->
<div id="statusMessage" class="text-center p-8 text-lg font-semibold text-gray-700">
<i data-lucide="loader" class="w-6 h-6 animate-spin text-red-500 mx-auto mb-2"></i>
Lade Pokémon...
</div>
<!-- Pokémon-Karten Container -->
<main id="pokemonList" class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4"></main>
<!-- Mehr-laden Button -->
<div class="text-center mt-8">
<button
id="loadMoreButton"
onclick="loadMorePokemons()"
class="bg-red-500 hover:bg-red-600 text-white font-bold py-3 px-6 rounded-full shadow-lg transition duration-200 transform hover:scale-[1.02] active:scale-[0.98] focus:outline-none focus:ring-4 focus:ring-red-300"
style="display: none;"
>
<i data-lucide="loader" class="w-4 h-4 mr-2 inline-block"></i>
Nächste 200 Pokémon laden
</button>
</div>
</div>
<!-- DOWNLOAD BESTÄTIGUNGS-MODAL -->
<div id="downloadModal" class="fixed inset-0 bg-gray-900 bg-opacity-75 hidden flex items-center justify-center z-50 p-4" onclick="closeModal()">
<div
id="modalContent"
class="bg-white rounded-xl shadow-2xl w-full max-w-sm p-6 transform transition-all duration-300"
onclick="event.stopPropagation()"
>
<div class="text-center mb-4">
<!-- Icon für den Download-Button im Modal -->
<i data-lucide="download" class="w-10 h-10 text-blue-500 mx-auto mb-3"></i>
<h3 id="modalTitle" class="text-xl font-bold text-gray-800">Download bestätigen</h3>
</div>
<p id="modalMessage" class="text-gray-600 mb-6 text-center">Möchtest du den Ruf von Glurak (#006) wirklich als MP3-Datei herunterladen?</p>
<div class="flex justify-end space-x-3">
<button
onclick="closeModal()"
id="modalCancelButton"
class="px-4 py-2 bg-gray-200 text-gray-700 font-semibold rounded-lg hover:bg-gray-300 transition"
>
Abbrechen
</button>
<button
id="modalConfirmButton"
class="px-4 py-2 bg-red-500 text-white font-semibold rounded-lg hover:bg-red-600 transition"
>
Bestätigen und herunterladen
</button>
</div>
</div>
</div>
<script>
// --- TRANSLATIONS (Übersetzungen) ---
const translations = {
de: {
title: 'Pokémon-Ruf-Galerie',
subtitle: 'Ruf-Galerie',
description: 'Finde und höre jedes Pokémon.',
searchPlaceholder: 'Pokémon-Namen oder ID suchen (z.B. Glurak, 6)...',
loading: 'Lade Pokémon...',
loadingBlock: 'Lade Pokémon-Block...',
loadMore: (count) => `Nächste ${count} Pokémon laden`,
notFound: (term) => `Pokémon "${term}" nicht gefunden.`,
fetchError: 'Fehler beim Laden der Pokémon. Bitte versuchen Sie es später erneut.',
// Modal
modalTitle: 'Download bestätigen',
modalMessage: (name, id) => `Möchtest du den Ruf von ${name} (#${String(id).padStart(3, '0')}) wirklich als MP3-Datei herunterladen?`,
modalCancel: 'Abbrechen',
modalConfirm: 'Bestätigen und herunterladen',
},
en: {
title: 'Pokémon Cry Gallery',
subtitle: 'Cry Gallery',
description: 'Find and listen to every Pokémon.',
searchPlaceholder: 'Search Pokémon name or ID (e.g. Charizard, 6)...',
loading: 'Loading Pokémon...',
loadingBlock: 'Loading Pokémon block...',
loadMore: (count) => `Load next ${count} Pokémon`,
notFound: (term) => `Pokémon "${term}" not found.`,
fetchError: 'Error loading Pokémon. Please try again later.',
// Modal
modalTitle: 'Confirm Download',
modalMessage: (name, id) => `Do you really want to download the cry for ${name} (#${String(id).padStart(3, '0')}) as an MP3 file?`,
modalCancel: 'Cancel',
modalConfirm: 'Confirm and Download',
},
es: {
title: 'Galería de Gritos Pokémon',
subtitle: 'Galería de Gritos',
description: 'Encuentra y escucha a todos los Pokémon.',
searchPlaceholder: 'Buscar nombre o ID de Pokémon (ej. Charizard, 6)...',
loading: 'Cargando Pokémon...',
loadingBlock: 'Cargando bloque de Pokémon...',
loadMore: (count) => `Cargar ${count} Pokémon más`,
notFound: (term) => `Pokémon "${term}" no encontrado.`,
fetchError: 'Error al cargar Pokémon. Por favor, inténtelo de nuevo más tarde.',
// Modal
modalTitle: 'Confirmar Descarga',
modalMessage: (name, id) => `¿Realmente quieres descargar el grito de ${name} (#${String(id).padStart(3, '0')}) como archivo MP3?`,
modalCancel: 'Cancelar',
modalConfirm: 'Confirmar y Descargar',
},
fr: {
title: 'Galerie des Cris Pokémon',
subtitle: 'Galerie des Cris',
description: 'Trouvez et écoutez tous les Pokémon.',
searchPlaceholder: 'Chercher nom ou ID de Pokémon (ex. Dracaufeu, 6)...',
loading: 'Chargement des Pokémon...',
loadingBlock: 'Chargement du bloc Pokémon...',
loadMore: (count) => `Charger les ${count} prochains Pokémon`,
notFound: (term) => `Pokémon "${term}" non trouvé.`,
fetchError: 'Erreur lors du chargement des Pokémon. Veuillez réessayer plus tard.',
// Modal
modalTitle: 'Confirmer le Téléchargement',
modalMessage: (name, id) => `Voulez-vous vraiment télécharger le cri de ${name} (#${String(id).padStart(3, '0')}) au format MP3 ?`,
modalCancel: 'Annuler',
modalConfirm: 'Confirmer et Télécharger',
},
it: {
title: 'Galleria dei Versi Pokémon',
subtitle: 'Galleria dei Versi',
description: 'Trova e ascolta ogni Pokémon.',
searchPlaceholder: 'Cerca nome o ID Pokémon (es. Charizard, 6)...',
loading: 'Caricamento Pokémon...',
loadingBlock: 'Caricamento blocco Pokémon...',
loadMore: (count) => `Carica i prossimi ${count} Pokémon`,
notFound: (term) => `Pokémon "${term}" non trovato.`,
fetchError: 'Errore durante il caricamento dei Pokémon. Riprova più tardi.',
// Modal
modalTitle: 'Conferma Download',
modalMessage: (name, id) => `Vuoi davvero scaricare il verso di ${name} (#${String(id).padStart(3, '0')}) come file MP3?`,
modalCancel: 'Annulla',
modalConfirm: 'Conferma e Scarica',
},
ja: {
title: 'ポケモン鳴き声ギャラリー',
subtitle: '鳴き声ギャラリー',
description: 'すべてのポケモンを見つけて聞く。',
searchPlaceholder: 'ポケモンの名前またはIDを検索 (例: リザードン, 6)...',
loading: 'ポケモンをロード中...',
loadingBlock: 'ポケモンブロックをロード中...',
loadMore: (count) => `次の${count}匹のポケモンをロード`,
notFound: (term) => `ポケモン"${term}"は見つかりませんでした。`,
fetchError: 'ポケモンのロード中にエラーが発生しました。後でもう一度お試しください。',
// Modal
modalTitle: 'ダウンロード確認',
modalMessage: (name, id) => `${name}(#${String(id).padStart(3, '0')})の鳴き声をMP3ファイルとしてダウンロードしますか?`,
modalCancel: 'キャンセル',
modalConfirm: '確認してダウンロード',
}
};
// --- GLOBALE KONSTANTEN UND ZUSTAND ---
const POKEMON_LIST_ELEMENT = document.getElementById('pokemonList');
const STATUS_MESSAGE = document.getElementById('statusMessage');
const LOAD_MORE_BUTTON = document.getElementById('loadMoreButton');
const CRY_BASE_URL = 'https://play.pokemonshowdown.com/audio/cries/';
const POKEAPI_BASE_URL = 'https://pokeapi.co/api/v2/pokemon/';
const POKEAPI_SPECIES_URL = 'https://pokeapi.co/api/v2/pokemon-species/';
const DOWNLOAD_MODAL = document.getElementById('downloadModal');
// Mappt die Sprachcodes zu den IDs der PokeAPI (für lokalisierte Namen)
const POKEAPI_LANG_MAP = {
'de': 'de', 'en': 'en', 'es': 'es', 'fr': 'fr', 'it': 'it', 'ja': 'ja'
};
// Konfiguration für die Ladestrategie
const INITIAL_LIMIT = 251;
const LOAD_INCREMENT = 200;
let currentLanguage = 'de';
let allPokemonData = new Map();
let currentOffset = 0;
let isLoading = false;
let pokemonToDownload = null; // Speichert das Pokémon-Objekt für den Modal-Download
// --- INTERAKTIONS-LOGIK (KURZ-/LANGER DRUCK) ---
let pressTimer = null;
const LONG_PRESS_DURATION = 500; // 0.5 Sekunden
let isLongPress = false;
/**
* Startet den Timer für den langen Druck.
*/
window.startPress = (englishName, id, event) => {
if (event.type === 'mousedown') { event.preventDefault(); }
if (event.type === 'touchstart') { event.stopPropagation(); }
clearTimeout(pressTimer);
isLongPress = false;
// Füge visuelles Feedback hinzu
const card = event.currentTarget;
card.classList.add('long-press-active');
// Starte den Download-Timer
pressTimer = setTimeout(() => {
isLongPress = true;
card.classList.remove('long-press-active'); // Entferne Halte-Feedback
showConfirmationModal(id);
}, LONG_PRESS_DURATION);
};
/**
* Beendet den Druck und führt bei kurzem Druck die Play-Aktion aus.
*/
window.endPress = (englishName, id, event) => {
const card = event.currentTarget;
clearTimeout(pressTimer);
card.classList.remove('long-press-active');
// Wenn es kein langer Druck war, ist es ein kurzer Klick/Tipp
if (!isLongPress) {
playCry(englishName, card);
}
// Reset state
isLongPress = false;
};
/**
* Bricht den langen Druck ab (z.B. bei Scrollen/Bewegen des Fingers).
*/
window.cancelPress = (event) => {
clearTimeout(pressTimer);
isLongPress = true; // Setze auf true, um versehentliches Abspielen zu verhindern
const card = event.currentTarget;
card.classList.remove('long-press-active');
}
// --- MODAL LOGIK ---
/**
* Zeigt das Bestätigungs-Modal an.
*/
const showConfirmationModal = (pokemonId) => {
const pokemon = allPokemonData.get(parseInt(pokemonId));
if (!pokemon) return;
pokemonToDownload = pokemon;
const t = translations[currentLanguage];
const localizedName = pokemon.localizedNames[currentLanguage];
// Inhalt lokalisieren
document.getElementById('modalTitle').textContent = t.modalTitle;
document.getElementById('modalMessage').innerHTML = t.modalMessage(localizedName, pokemon.id);
document.getElementById('modalCancelButton').textContent = t.modalCancel;
document.getElementById('modalConfirmButton').textContent = t.modalConfirm;
// Download-Funktion an den Button binden
document.getElementById('modalConfirmButton').onclick = () => {
triggerDownload(pokemonToDownload);
closeModal();
};
DOWNLOAD_MODAL.classList.remove('hidden');
if (typeof createIcons !== 'undefined') { createIcons(); } // Icons im Modal aktualisieren
};
/**
* Schließt das Bestätigungs-Modal.
*/
window.closeModal = () => {
DOWNLOAD_MODAL.classList.add('hidden');
pokemonToDownload = null;
};
/**
* Löst den Download der MP3-Datei aus.
*/
const triggerDownload = (pokemon) => {
const paddedId = String(pokemon.id).padStart(3, '0');
const downloadFileName = `${paddedId}_${pokemon.englishName}.mp3`;
const downloadUrl = `${CRY_BASE_URL}${sanitizeName(pokemon.englishName)}.mp3`;
// Verwende einen temporären Anker-Tag für den Download
const a = document.createElement('a');
a.href = downloadUrl;
a.download = downloadFileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
// --- SOUND LOGIK ---
/**
* Konvertiert den ENGLISCHEN Namen für die Sound-URL.
*/
const sanitizeName = (name) => {
return name.toLowerCase().replace(/[^a-z0-9-]/g, '');
};
/**
* Spielt den Ruf-Sound des Pokémons ab und zeigt Feedback.
*/
window.playCry = (englishName, card) => {
const sanitizedName = sanitizeName(englishName);
const audio = new Audio(`${CRY_BASE_URL}${sanitizedName}.mp3`);
audio.play().catch(e => console.error("Fehler beim Abspielen des Rufs:", e));
// Visuelles Feedback für Play
showActionIcon(card, 'play');
};
/**
* Zeigt ein temporäres Icon über der Karte (Play).
*/
const showActionIcon = (card, type) => {
const playIcon = card.querySelector('.action-icon-play');
// Setze alle zurück
playIcon.classList.remove('show-play');
// Zeige das korrekte Icon
if (type === 'play') {
playIcon.classList.add('show-play');
}
// Verstecke das Icon nach einer kurzen Zeit
setTimeout(() => {
playIcon.classList.remove('show-play');
}, 800);
};
// --- LOKALISIERUNGS- UND UI-LOGIK ---
/**
* Ruft den lokalisierten Namen aus den Species-Daten ab.
*/
const getLocalizedName = (speciesData, langCode) => {
if (!speciesData || !speciesData.names) return speciesData?.name || 'Unknown';
const apiLangCode = POKEAPI_LANG_MAP[langCode];
const nameEntry = speciesData.names.find(
name => name.language.name === apiLangCode
);
if (nameEntry) {
return nameEntry.name;
} else {
const englishNameEntry = speciesData.names.find(name => name.language.name === 'en');
return englishNameEntry ? englishNameEntry.name : speciesData.name;
}
};
/**
* Aktualisiert die Anzeige aller Pokémon-Namen, wenn die Sprache gewechselt wird.
*/
const updateAllPokemonNames = () => {
allPokemonData.forEach(pokemon => {
const card = document.getElementById(`card-${pokemon.id}`);
if (card) {
const displayLocalizedName = pokemon.localizedNames[currentLanguage] || pokemon.englishName;
const paddedId = String(pokemon.id).padStart(3, '0');
const nameElement = card.querySelector('h3');
if (nameElement) {
nameElement.innerHTML = `${displayLocalizedName}
<span class="text-gray-500 text-sm font-normal">#${paddedId}</span>`;
}
}
});
// Aktualisiere das Modal, falls es sichtbar ist
if (!DOWNLOAD_MODAL.classList.contains('hidden') && pokemonToDownload) {
const t = translations[currentLanguage];
const localizedName = pokemonToDownload.localizedNames[currentLanguage];
document.getElementById('modalTitle').textContent = t.modalTitle;
document.getElementById('modalMessage').innerHTML = t.modalMessage(localizedName, pokemonToDownload.id);
document.getElementById('modalCancelButton').textContent = t.modalCancel;
document.getElementById('modalConfirmButton').textContent = t.modalConfirm;
}
};
/**
* Ändert die Sprache der gesamten Benutzeroberfläche.
*/
window.changeLanguage = (langCode) => {
if (!translations[langCode]) return;
currentLanguage = langCode;
const t = translations[langCode];
// 1. UI-Texte aktualisieren
document.getElementById('pageTitle').textContent = t.title;
document.getElementById('headerSubtitle').textContent = t.subtitle;
document.getElementById('headerDescription').textContent = t.description;
document.getElementById('searchInput').setAttribute('placeholder', t.searchPlaceholder);
LOAD_MORE_BUTTON.innerHTML = `<i data-lucide="loader" class="w-4 h-4 mr-2 inline-block"></i> ${t.loadMore(LOAD_INCREMENT)}`;
// 2. Namen der Pokémon und Modal-Texte aktualisieren
updateAllPokemonNames();
// 3. Statusmeldung aktualisieren
if (isLoading) {
showStatus(t.loadingBlock, true);
} else if (POKEMON_LIST_ELEMENT.children.length === 0 && document.getElementById('searchInput').value.trim() === '') {
showStatus(t.loading, true);
} else if (POKEMON_LIST_ELEMENT.children.length === 0 && document.getElementById('searchInput').value.trim() !== '') {
showStatus(t.notFound(document.getElementById('searchInput').value.trim()));
} else {
STATUS_MESSAGE.style.display = 'none';
}
};
/**
* Erstellt das HTML-Element für eine Pokémon-Karte.
*/
const createPokemonCard = (pokemon) => {
const paddedId = String(pokemon.id).padStart(3, '0');
const displayLocalizedName = pokemon.localizedNames[currentLanguage] || pokemon.englishName;
return `
<div
id="card-${pokemon.id}"
class="pokemon-card bg-white rounded-xl shadow-lg p-4 text-center border-4 border-transparent"
data-name="${pokemon.englishName.toLowerCase()}"
data-id="${pokemon.id}"
onmousedown="startPress('${pokemon.englishName}', '${pokemon.id}', event)"
onmouseup="endPress('${pokemon.englishName}', '${pokemon.id}', event)"
ontouchstart="startPress('${pokemon.englishName}', '${pokemon.id}', event)"
ontouchend="endPress('${pokemon.englishName}', '${pokemon.id}', event)"
ontouchmove="cancelPress(event)"
>
<div class="cry-button flex justify-center items-center mb-2 relative">
<!-- Icon für visuelles Feedback beim Abspielen (overlay) -->
<i data-lucide="volume-2" class="action-icon action-icon-play w-16 h-16 text-red-500"></i>
<img
src="/${pokemon.sprites.front_default}"
alt="${pokemon.englishName}"
onerror="this.onerror=null;this.src='https://placehold.co/100x100/CCCCCC/333333?text=${paddedId}'"
class="w-24 h-24 object-contain transition duration-300"
>
</div>
<h3 class="text-lg font-extrabold text-gray-800">${displayLocalizedName}
<span class="text-gray-500 text-sm font-normal">#${paddedId}</span>
</h3>
</div>
`;
};
/**
* Blendet die Statusmeldung ein/aus.
*/
const showStatus = (message, showLoader = false) => {
STATUS_MESSAGE.innerHTML = showLoader
? `<i data-lucide="loader" class="w-6 h-6 animate-spin text-red-500 mx-auto mb-2"></i> ${message}`
: message;
STATUS_MESSAGE.style.display = 'block';
if (typeof createIcons !== 'undefined') { createIcons(); }
};
/**
* Ruft ein einzelnes Pokémon (Daten und Species) ab und gibt das kombinierte Objekt zurück.
*/
const fetchSinglePokemon = async (idOrName) => {
try {
// 1. Normale Pokémon-Daten abrufen
const pokemonResponse = await fetch(`${POKEAPI_BASE_URL}${idOrName.toLowerCase()}`);
if (!pokemonResponse.ok) return null;
const pokemonData = await pokemonResponse.json();
// 2. Species-Daten abrufen (für lokalisierte Namen)
const speciesResponse = await fetch(`${POKEAPI_SPECIES_URL}${pokemonData.id}`);
if (!speciesResponse.ok) return null;
const speciesData = await speciesResponse.json();
// 3. Lokalisierte Namen extrahieren
const localizedNames = {};
for (const langCode in POKEAPI_LANG_MAP) {
localizedNames[langCode] = getLocalizedName(speciesData, langCode);
}
// 4. Kombiniertes Objekt zurückgeben
return {
...pokemonData,
englishName: pokemonData.name, // Speichere den Originalnamen für Cry-Wiedergabe
localizedNames: localizedNames
};
} catch (error) {
console.error("Fehler beim Abrufen des Pokémons:", error);
return null;
}
};
/**
* Filtert die geladenen Pokémon oder sucht ein einzelnes Pokémon von der API.
*/
window.filterAndFetchPokemons = async () => {
const searchTerm = document.getElementById('searchInput').value.toLowerCase().trim();
const t = translations[currentLanguage];
// 1. Wenn die Suche leer ist, zeige alle geladenen Karten und den Button an
if (searchTerm === '') {
POKEMON_LIST_ELEMENT.innerHTML = '';
allPokemonData.forEach(pokemon => {
POKEMON_LIST_ELEMENT.insertAdjacentHTML('beforeend', createPokemonCard(pokemon));
});
updateAllPokemonNames();
STATUS_MESSAGE.style.display = 'none';
LOAD_MORE_BUTTON.style.display = 'block';
return;
}
// 2. Suche lokal nach ID (einfachste und schnellste Methode)
const foundById = allPokemonData.get(parseInt(searchTerm));
if (foundById) {
// Nur das gefundene Pokémon anzeigen
POKEMON_LIST_ELEMENT.innerHTML = createPokemonCard(foundById);
updateAllPokemonNames();
STATUS_MESSAGE.style.display = 'none';
LOAD_MORE_BUTTON.style.display = 'none';
return;
}
// 3. Wenn lokal nichts gefunden, versuche API-Fetch (funktioniert am besten mit EN Namen oder ID)
if (searchTerm.length >= 2 && !isLoading) {
showStatus(`${t.loadingBlock.replace('Block', 'Pokémon')} "${searchTerm}"...`, true);
const singlePokemon = await fetchSinglePokemon(searchTerm);
if (singlePokemon) {
POKEMON_LIST_ELEMENT.innerHTML = createPokemonCard(singlePokemon);
updateAllPokemonNames();
STATUS_MESSAGE.style.display = 'none';
} else {
showStatus(t.notFound(searchTerm));
POKEMON_LIST_ELEMENT.innerHTML = ''; // Leere die Liste bei Nicht-Fund
}
} else {
showStatus(t.notFound(searchTerm));
POKEMON_LIST_ELEMENT.innerHTML = '';
}
LOAD_MORE_BUTTON.style.display = 'none';
};
/**
* Lädt eine Reihe von Pokémon von der API.
*/
const fetchPokemonBatch = async (offset, count) => {
if (isLoading) return;
isLoading = true;
const t = translations[currentLanguage];
showStatus(t.loadingBlock, true);
LOAD_MORE_BUTTON.disabled = true;
try {
// Schritt 1: Hole die Liste der Pokémon-Namen/URLs
const listResponse = await fetch(`${POKEAPI_BASE_URL}?offset=${offset}&limit=${count}`);
if (!listResponse.ok) throw new Error(t.fetchError);
const listData = await listResponse.json();
// Setze den "Mehr laden" Text auf die korrekte nächste Zahl
LOAD_MORE_BUTTON.innerHTML = `<i data-lucide="loader" class="w-4 h-4 mr-2 inline-block"></i> ${t.loadMore(LOAD_INCREMENT)}`;
// Schritt 2: Hole die Details (inkl. Species für Namen) für jedes Pokémon
const detailPromises = listData.results.map(p => fetchSinglePokemon(p.name));
const detailedPokemons = await Promise.all(detailPromises);
// Schritt 3: Aktualisiere den globalen Cache und die UI
let newCardsHtml = '';
detailedPokemons.forEach(p => {
if (p && !allPokemonData.has(p.id)) {
allPokemonData.set(p.id, p);
newCardsHtml += createPokemonCard(p);
}
});
POKEMON_LIST_ELEMENT.insertAdjacentHTML('beforeend', newCardsHtml);
updateAllPokemonNames();
currentOffset += detailedPokemons.length;
// Verstecke die Statusmeldung und zeige den Button an, wenn die Liste nicht leer ist
if (POKEMON_LIST_ELEMENT.children.length > 0) {
STATUS_MESSAGE.style.display = 'none';
LOAD_MORE_BUTTON.style.display = 'block';
}
} catch (error) {
console.error("Gesamtfehler beim Laden der Pokémon:", error);
showStatus(t.fetchError);
LOAD_MORE_BUTTON.style.display = 'none';
} finally {
isLoading = false;
LOAD_MORE_BUTTON.disabled = false;
// Re-initialisiere Lucide Icons nach dem Einfügen neuer Karten
if (typeof createIcons !== 'undefined') { createIcons(); }
}
};
/**
* Lädt die nächste Staffel von Pokémon (für den "Mehr laden"-Button).
*/
window.loadMorePokemons = () => {
fetchPokemonBatch(currentOffset, LOAD_INCREMENT);
};
/**
* Initialisiert die Anwendung.
*/
window.onload = () => {
// Setze die Standard-Sprache und lade den ersten Block
const defaultLang = document.getElementById('languageSelector').value;
changeLanguage(defaultLang);
fetchPokemonBatch(currentOffset, INITIAL_LIMIT);
};
</script>
</body>
</html>
- Details
- Geschrieben von: admin
- Kategorie: Uncategorised
- Zugriffe: 3
Pokémon
Ruf-Galerie
Finde und höre jedes Pokémon.
Lade Pokémon...