transport – fiszki

import React, { useState, useEffect, useRef } from ‘react’; import { Plane, Volume2, Play, Pause, SkipForward, SkipBack, RefreshCw, Info, CheckCircle2, Settings, Headphones, Languages } from ‘lucide-react’; const COLLOCATIONS = [ // A2 { phrase: “Go by bus”, translation: “Jechać autobusem”, sentence: “I usually go by bus to save money.”, sentenceTrans: “Zazwyczaj jeżdżę autobusem, żeby oszczędzić pieniądze.”, level: “A2” }, { phrase: “Go on foot”, translation: “Iść pieszo”, sentence: “The hotel is close, let’s go on foot.”, sentenceTrans: “Hotel jest blisko, chodźmy pieszo.”, level: “A2” }, { phrase: “Book a flight”, translation: “Zarezerwować lot”, sentence: “We booked our flight three months ago.”, sentenceTrans: “Zarezerwowaliśmy lot trzy miesiące temu.”, level: “A2” }, { phrase: “Get on the train”, translation: “Wsiąść do pociągu”, sentence: “Quick! Get on the train before it leaves.”, sentenceTrans: “Szybko! Wsiadaj do pociągu zanim odjedzie.”, level: “A2” }, { phrase: “Miss a flight”, translation: “Spóźnić się na lot”, sentence: “If we don’t leave now, we will miss our flight.”, sentenceTrans: “Jeśli teraz nie wyjdziemy, spóźnimy się na lot.”, level: “A2” }, // B1 { phrase: “Set off”, translation: “Wyruszyć w drogę”, sentence: “We need to set off at 5 AM to avoid traffic.”, sentenceTrans: “Musimy wyruszyć o 5 rano, żeby uniknąć korków.”, level: “B1” }, { phrase: “Pick someone up”, translation: “Odebrać kogoś”, sentence: “Can you pick me up from the airport?”, sentenceTrans: “Czy możesz odebrać mnie z lotniska?”, level: “B1” }, { phrase: “Drop someone off”, translation: “Podrzucić/wysadzić kogoś”, sentence: “My brother dropped us off at the terminal.”, sentenceTrans: “Mój brat wysadził nas pod terminalem.”, level: “B1” }, { phrase: “Give someone a lift”, translation: “Podwieźć kogoś”, sentence: “I can give you a lift to the station.”, sentenceTrans: “Mogę cię podwieźć na stację.”, level: “B1” }, { phrase: “Take off”, translation: “Startować (samolot)”, sentence: “The plane is ready to take off.”, sentenceTrans: “Samolot jest gotowy do startu.”, level: “B1” }, // Story context { phrase: “Traffic jam”, translation: “Korek uliczny”, sentence: “We got stuck in a huge traffic jam.”, sentenceTrans: “Utknęliśmy w ogromnym korku.”, level: “B1” }, { phrase: “Rush hour”, translation: “Godziny szczytu”, sentence: “Avoid travelling during the rush hour.”, sentenceTrans: “Unikaj podróżowania w godzinach szczytu.”, level: “B1” }, { phrase: “Package holiday”, translation: “Wakacje zorganizowane”, sentence: “We went on a package holiday to Rome.”, sentenceTrans: “Pojechaliśmy na zorganizowane wakacje do Rzymu.”, level: “B1” }, { phrase: “Check into a hotel”, translation: “Zameldować się w hotelu”, sentence: “We checked into our room and went for lunch.”, sentenceTrans: “Zameldowaliśmy się w pokoju i poszliśmy na lunch.”, level: “B1” }, { phrase: “Get lost”, translation: “Zgubić się”, sentence: “We got lost in the narrow streets of Rome.”, sentenceTrans: “Zgubiliśmy się w wąskich uliczkach Rzymu.”, level: “B1” } ]; const apiKey = “”; // Klucz dostarczany przez środowisko const App = () => { const [index, setIndex] = useState(0); const [isAutoPlaying, setIsAutoPlaying] = useState(false); const [isFlipped, setIsFlipped] = useState(false); const [isSpeaking, setIsSpeaking] = useState(false); const [currentLang, setCurrentLang] = useState(‘pl’); // ‘pl’ or ‘en’ const audioRef = useRef(null); const isCanceledRef = useRef(false); const current = COLLOCATIONS[index]; const pcmToWav = (pcmData, sampleRate) => { const buffer = new ArrayBuffer(44 + pcmData.length * 2); const view = new DataView(buffer); const writeString = (offset, string) => { for (let i = 0; i < string.length; i++) { view.setUint8(offset + i, string.charCodeAt(i)); } }; writeString(0, 'RIFF'); view.setUint32(4, 32 + pcmData.length * 2, true); writeString(8, 'WAVE'); writeString(12, 'fmt '); view.setUint32(16, 16, true); view.setUint16(20, 1, true); view.setUint16(22, 1, true); view.setUint32(24, sampleRate, true); view.setUint32(28, sampleRate * 2, true); view.setUint16(32, 2, true); view.setUint16(34, 16, true); writeString(36, 'data'); view.setUint32(40, pcmData.length * 2, true); for (let i = 0; i < pcmData.length; i++) { view.setInt16(44 + i * 2, pcmData[i], true); } return new Blob([buffer], { type: 'audio/wav' }); }; const speak = async (text, langCode = 'en') => { if (isCanceledRef.current) return; setIsSpeaking(true); setCurrentLang(langCode); let retries = 0; const maxRetries = 5; const fetchWithRetry = async (delay) => { try { const prompt = langCode === ‘pl’ ? `Say clearly and naturally in Polish: ${text}` : `Say clearly in a British English accent: ${text}`; const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-tts:generateContent?key=${apiKey}`, { method: ‘POST’, headers: { ‘Content-Type’: ‘application/json’ }, body: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }], generationConfig: { responseModalities: [“AUDIO”], speechConfig: { voiceConfig: { prebuiltVoiceConfig: { voiceName: langCode === ‘pl’ ? “Kore” : “Zephyr” } } } } }) }); if (!response.ok) throw new Error(‘TTS Failed’); const result = await response.json(); const audioData = result.candidates[0].content.parts[0].inlineData.data; const mimeType = result.candidates[0].content.parts[0].inlineData.mimeType; const sampleRate = parseInt(mimeType.match(/rate=(\d+)/)?.[1] || “24000”); const binaryString = atob(audioData); const pcmData = new Int16Array(binaryString.length / 2); for (let i = 0; i < pcmData.length; i++) { pcmData[i] = (binaryString.charCodeAt(i * 2) & 0xFF) | (binaryString.charCodeAt(i * 2 + 1) << 8); } const wavBlob = pcmToWav(pcmData, sampleRate); const url = URL.createObjectURL(wavBlob); return new Promise((resolve) => { if (isCanceledRef.current) { setIsSpeaking(false); resolve(); return; } const audio = new Audio(url); audioRef.current = audio; audio.onended = () => { setIsSpeaking(false); resolve(); }; audio.play(); }); } catch (error) { if (retries < maxRetries && !isCanceledRef.current) { retries++; await new Promise(r => setTimeout(r, delay)); return fetchWithRetry(delay * 2); } setIsSpeaking(false); } }; return fetchWithRetry(1000); }; const runFiszkaSequence = async () => { isCanceledRef.current = false; setIsFlipped(false); // 1. Słowo po polsku await new Promise(r => setTimeout(r, 600)); if (isCanceledRef.current) return; await speak(current.translation, ‘pl’); // 2. Słowo po angielsku (+ obrót karty) await new Promise(r => setTimeout(r, 600)); if (isCanceledRef.current) return; setIsFlipped(true); await new Promise(r => setTimeout(r, 600)); await speak(current.phrase, ‘en’); // 3. Zdanie po polsku (widoczne na przedniej stronie, ale czytamy teraz w sekwencji) await new Promise(r => setTimeout(r, 800)); if (isCanceledRef.current) return; await speak(current.sentenceTrans, ‘pl’); // 4. Zdanie po angielsku await new Promise(r => setTimeout(r, 800)); if (isCanceledRef.current) return; await speak(current.sentence, ‘en’); // 5. Autoodtwarzanie – następna karta if (isAutoPlaying && !isCanceledRef.current) { await new Promise(r => setTimeout(r, 3000)); if (!isCanceledRef.current) { setIndex((prev) => (prev + 1) % COLLOCATIONS.length); } } }; useEffect(() => { if (isAutoPlaying) { runFiszkaSequence(); } return () => { isCanceledRef.current = true; if (audioRef.current) audioRef.current.pause(); }; }, [index, isAutoPlaying]); const toggleAutoPlay = () => { if (!isAutoPlaying) { setIsAutoPlaying(true); } else { setIsAutoPlaying(false); isCanceledRef.current = true; if (audioRef.current) audioRef.current.pause(); setIsSpeaking(false); } }; const handleManualFlip = async () => { if (isAutoPlaying) return; if (!isFlipped) { // Sekwencja polska await speak(current.translation, ‘pl’); await speak(current.sentenceTrans, ‘pl’); setTimeout(() => setIsFlipped(true), 500); } else { // Sekwencja angielska await speak(current.phrase, ‘en’); await speak(current.sentence, ‘en’); } }; return (
{/* Background decorations */}

TRAVEL AUDIO MASTER

Naturalny Polski
British Accent
{/* Control Panel */}
{/* Main Flashcard */}
{/* Front (Polish) */}
Lektor Polski

{current.translation}

{current.sentenceTrans}
{isSpeaking && currentLang === ‘pl’ && (
{[0.4, 0.8, 0.6, 1, 0.7, 0.5, 0.9, 0.4].map((h, i) => (
))}
)}
{/* Back (English) */}
British Accent

{current.phrase}

“{current.sentence}”

{isSpeaking && currentLang === ‘en’ && (
{[0.5, 1, 0.4, 0.8, 0.9, 0.6, 0.7, 0.5].map((h, i) => (
))}
)}
{/* Progress Footer */}
Auto-Play: {isAutoPlaying ? ‘Włączone’ : ‘Wyłączone’}
Kolekcja: {index + 1} / {COLLOCATIONS.length}