diff --git a/src/app/page.tsx b/src/app/page.tsx index 648e234..3a4f32f 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -40,17 +40,16 @@ export default function HomePage() { Improve your listening with just a few minutes per day!

- + : } className={`bg-button-main text-white py-3 opacity-50 cursor-not-allowed ${ isLessonComplete ? "opacity-50 cursor-not-allowed" : "" - }`} disabled={isLessonComplete ? true : false} /> - +
diff --git a/src/components/Game/AudioPlayer/Index.tsx b/src/components/Game/AudioPlayer/Index.tsx new file mode 100644 index 0000000..6a4d857 --- /dev/null +++ b/src/components/Game/AudioPlayer/Index.tsx @@ -0,0 +1,45 @@ +import { PlayIcon } from "@/components/Icons/playIcon"; +import { TimeIcon } from "@/components/Icons/time"; +import { AudioIcon } from "@/components/Icons/audio"; + +export type AudioStateEnum = "PENDING" | "READY" | "PLAYING"; + +interface ButtonProps extends React.ButtonHTMLAttributes { + state: AudioStateEnum; +} + +const Ready = () => { + return ; +}; + +const Pending = () => { + return ; +}; + +const Playing = () => { + return ; +}; + +const StatesMapper = ({ state }) => { + switch (state) { + case "PENDING": + return ; + case "READY": + return ; + case "PLAYING": + return ; + default: + return ; + } +}; + +export const AudioPlayer = ({ state, onClick }: ButtonProps) => { + return ( + + ); +}; diff --git a/src/components/Game/GameFeedback.tsx b/src/components/Game/GameFeedback.tsx index eeee668..323c97f 100644 --- a/src/components/Game/GameFeedback.tsx +++ b/src/components/Game/GameFeedback.tsx @@ -5,13 +5,11 @@ import { WrongIcon } from "../Icons/wrong"; interface GameFeedbackProps { feedback: React.ReactNode; feedbackType?: "correct" | "incorrect"; - audioSrc?: string; } export const GameFeedback = ({ feedback, - feedbackType = "correct", - audioSrc + feedbackType = "correct" }: GameFeedbackProps) => { return (
@@ -20,12 +18,21 @@ export const GameFeedback = ({ {feedbackType === "correct" ? : } - {feedbackType === "correct" ? "Your phrase is correct!" : "Correct Solution:"}
+ {feedbackType === "correct" + ? "Your phrase is correct!" + : "Correct Solution:"}{" "} +
+
+ + {feedbackType === "incorrect" && feedback} - {feedbackType === "incorrect" && feedback}
) : (
@@ -39,7 +46,6 @@ export const GameFeedback = ({
)} - ); }; diff --git a/src/components/Game/index.tsx b/src/components/Game/index.tsx index 502fc1b..9a0109a 100644 --- a/src/components/Game/index.tsx +++ b/src/components/Game/index.tsx @@ -1,9 +1,10 @@ import { GameInput } from "./Input"; import { GameFeedback } from "./GameFeedback"; -import { useEffect, useState } from "react"; +import { AudioPlayer, AudioStateEnum } from "./AudioPlayer/Index"; +import { useEffect, useState, useRef } from "react"; import FooterButton from "@/components/Footer/FooterButton"; -import { AudioServer } from "@/service/AudioServer"; import { useAudioServer } from "@/hooks/useAudioServer"; +import { useAudioBrowser } from "@/hooks/useAudioBrowser"; import { useQuery } from "@tanstack/react-query"; import ProgressBar from "@/components/ProgressBar"; import Cookies from "js-cookie"; @@ -20,15 +21,28 @@ const Game = () => { const [isLessonComplete, setIsLessonComplete] = useState(false); const [correctCount, setCorrectCount] = useState(0); const [canContinue, setCanContinue] = useState(false); // Novo estado para controle de continuação + const [audio, setAudio] = useState<{ + audioSynthesis: SpeechSynthesisUtterance; + audioState: AudioStateEnum; + }>({ + audioSynthesis: null, + audioState: "PENDING" + }); // Novo objeto de audio const totalSteps = 5; - const { fetchRandomPhrase, verifyAnswer, compareCorrectAnswer, compareUserAnswer } = - useAudioServer(); + const { + fetchRandomPhrase, + verifyAnswer, + compareCorrectAnswer, + compareUserAnswer + } = useAudioServer(); + + const { getAudio } = useAudioBrowser(); const { data, error, isLoading, refetch } = useQuery({ queryKey: ["randomPhrase", currentStep], queryFn: fetchRandomPhrase, - enabled: currentStep <= totalSteps, + enabled: currentStep <= totalSteps }); useEffect(() => { @@ -43,19 +57,36 @@ const Game = () => { } }, [currentStep, refetch]); + useEffect(() => { + if (!data) return; + + async function fetchAudio() { + const audio = await getAudio(data.phrase); + setAudio(old => ({ + audioState: "READY", + audioSynthesis: audio + })); + } + + fetchAudio(); + }, [data]); + async function handleButtonPress() { const isCorrect = verifyAnswer(userAnswer, data?.phrase); setIsCorrect(isCorrect); if (isCorrect) { - setCorrectCount((prevCount) => { + setCorrectCount(prevCount => { const newCount = prevCount + 1; Cookies.set("correctCount", newCount.toString(), { expires: 1 }); return newCount; }); } - const correctFeedbackComponent = compareCorrectAnswer(userAnswer, data?.phrase); + const correctFeedbackComponent = compareCorrectAnswer( + userAnswer, + data?.phrase + ); const userFeedbackComponent = compareUserAnswer(userAnswer, data?.phrase); setCorrectFeedback(correctFeedbackComponent); setUserFeedback(userFeedbackComponent); @@ -64,7 +95,7 @@ const Game = () => { function handleContinue() { if (currentStep < totalSteps) { - setCurrentStep((prevStep) => prevStep + 1); + setCurrentStep(prevStep => prevStep + 1); } else { setIsLessonComplete(true); Cookies.set("lessonComplete", "true", { expires: 1 }); @@ -78,6 +109,18 @@ const Game = () => { setCanContinue(false); // Resetar o estado de continuação } + async function handlePlayAudio() { + audio.audioSynthesis.onstart = () => { + setAudio(old => ({ ...old, audioState: "PLAYING" })); + }; + + audio.audioSynthesis.onend = () => { + setAudio(old => ({ ...old, audioState: "READY" })); + }; + + window.speechSynthesis.speak(audio.audioSynthesis); + } + return (
{isLessonComplete ? ( @@ -95,15 +138,14 @@ const Game = () => { />
- {data?.audioBase64 && !userFeedback && ( - + {!userFeedback && ( + handlePlayAudio()} + /> )} {userFeedback ? ( @@ -111,22 +153,29 @@ const Game = () => { ) : ( { + onKeyDown={event => { if (event.key === "Enter") { handleButtonPress(); } }} - onChange={(e) => setUserAnswer(e.target.value)} + onChange={e => setUserAnswer(e.target.value)} value={userAnswer} /> )}
{canContinue ? ( - + Continue ) : ( - + Submit )} @@ -136,4 +185,4 @@ const Game = () => { ); }; -export default Game; \ No newline at end of file +export default Game; diff --git a/src/components/Icons/audio.tsx b/src/components/Icons/audio.tsx index a1984f0..697547e 100644 --- a/src/components/Icons/audio.tsx +++ b/src/components/Icons/audio.tsx @@ -2,15 +2,22 @@ import * as React from "react"; interface Props { width?: number; height?: number; + color?: string; } export function AudioIcon(props: Props) { return ( - + ); diff --git a/src/hooks/useAudioBrowser.tsx b/src/hooks/useAudioBrowser.tsx new file mode 100644 index 0000000..b299517 --- /dev/null +++ b/src/hooks/useAudioBrowser.tsx @@ -0,0 +1,15 @@ +export const useAudioBrowser = () => { + const getAudio = async (phrase: string) => { + try { + const synthesis = new SpeechSynthesisUtterance(phrase); + synthesis.lang = "en-US"; + return synthesis; + } catch (e) { + throw Error("Unable to load audio synthesizer"); + } + }; + + return { + getAudio + }; +}; diff --git a/src/hooks/useAudioServer.tsx b/src/hooks/useAudioServer.tsx index 16893a6..4e77fd5 100644 --- a/src/hooks/useAudioServer.tsx +++ b/src/hooks/useAudioServer.tsx @@ -21,8 +21,7 @@ export const useAudioServer = () => { } try { - const audioBase64 = await textToSpeechAction(phrase); - return { phrase, audioBase64 }; + return { phrase }; } catch (error) { throw new Error(`Failed to load audio: ${error.message}`); }