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}
{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}
);
};
export default App;