Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,16 @@ export default function HomePage() {
<span> Improve your listening with just a few minutes per day!</span>
</p>
<div className="flex gap-4 flex-col w-[280px]">

<Link href={"/game"}>
<MainButton
title={isLessonComplete ? "In Maintenance" : "In Maintenance"}
icon={isLessonComplete ? <TimeIcon /> : <TimeIcon />}
className={`bg-button-main text-white py-3 opacity-50 cursor-not-allowed ${
isLessonComplete ? "opacity-50 cursor-not-allowed" : ""

}`}
disabled={isLessonComplete ? true : false}
/>

</Link>
<div className="w-full"></div>
</div>
</div>
Expand Down
45 changes: 45 additions & 0 deletions src/components/Game/AudioPlayer/Index.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLButtonElement> {
state: AudioStateEnum;
}

const Ready = () => {
return <PlayIcon />;
};

const Pending = () => {
return <TimeIcon />;
};

const Playing = () => {
return <AudioIcon color="#FFF" height={30} />;
};

const StatesMapper = ({ state }) => {
switch (state) {
case "PENDING":
return <Pending />;
case "READY":
return <Ready />;
case "PLAYING":
return <Playing />;
default:
return <Pending />;
}
};

export const AudioPlayer = ({ state, onClick }: ButtonProps) => {
return (
<button
className="w-fit h-fit min-h-12 justify-center bg-button-main flex rounded-2xl font-semibold text-xl gap-2 hover:scale-110 transition-all items-center px-4"
onClick={onClick}
>
<StatesMapper state={state} />
</button>
);
};
20 changes: 13 additions & 7 deletions src/components/Game/GameFeedback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className="flex items-center justify-center flex-col mb-6 ">
Expand All @@ -20,12 +18,21 @@ export const GameFeedback = ({
{feedbackType === "correct" ? <CorrectIcon /> : <WrongIcon />}
<span
className={`font-bold mt-2 text-2xl text-center
${feedbackType === "correct" ? "text-text-correct" : "text-text-wrong"}
${
feedbackType === "correct"
? "text-text-correct"
: "text-text-wrong"
}
`}
>
{feedbackType === "correct" ? "Your phrase is correct!" : "Correct Solution:"} <br />
{feedbackType === "correct"
? "Your phrase is correct!"
: "Correct Solution:"}{" "}
<br />
</span>
<span className="flex flex-wrap justify-center gap-1">
{feedbackType === "incorrect" && feedback}
</span>
<span className="flex flex-wrap justify-center gap-1">{feedbackType === "incorrect" && feedback}</span>
</div>
) : (
<div>
Expand All @@ -39,7 +46,6 @@ export const GameFeedback = ({
</div>
</div>
)}

</div>
);
};
87 changes: 68 additions & 19 deletions src/components/Game/index.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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(() => {
Expand All @@ -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);
Expand All @@ -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 });
Expand All @@ -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 (
<div className="h-full flex flex-col justify-between">
{isLessonComplete ? (
Expand All @@ -95,38 +138,44 @@ const Game = () => {
/>
<div className="h-full flex items-center justify-center flex-col gap-12 p-6">
<GameFeedback
audioSrc={data?.audioBase64}
feedbackType={isCorrect ? "correct" : "incorrect"}
feedback={correctFeedback}
/>
{data?.audioBase64 && !userFeedback && (
<audio controls>
<source src={data?.audioBase64} type="audio/mp3" />
Your browser does not support the audio element.
</audio>
{!userFeedback && (
<AudioPlayer
state={audio.audioState}
onClick={() => handlePlayAudio()}
/>
)}
{userFeedback ? (
<GameInput.Field>
<div className="flex flex-wrap gap-1">{userFeedback}</div>
</GameInput.Field>
) : (
<GameInput.Textarea
onKeyDown={(event) => {
onKeyDown={event => {
if (event.key === "Enter") {
handleButtonPress();
}
}}
onChange={(e) => setUserAnswer(e.target.value)}
onChange={e => setUserAnswer(e.target.value)}
value={userAnswer}
/>
)}
</div>
{canContinue ? (
<FooterButton buttonState={isCorrect} onButtonPress={handleContinue}>
<FooterButton
buttonState={isCorrect}
onButtonPress={handleContinue}
>
Continue
</FooterButton>
) : (
<FooterButton buttonState={isCorrect} disabled={!userAnswer} onButtonPress={handleButtonPress}>
<FooterButton
buttonState={isCorrect}
disabled={!userAnswer}
onButtonPress={handleButtonPress}
>
Submit
</FooterButton>
)}
Expand All @@ -136,4 +185,4 @@ const Game = () => {
);
};

export default Game;
export default Game;
11 changes: 9 additions & 2 deletions src/components/Icons/audio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,22 @@ import * as React from "react";
interface Props {
width?: number;
height?: number;
color?: string;
}
export function AudioIcon(props: Props) {
return (
<svg width="62" height="56" viewBox="0 0 62 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
width={props.width || "62"}
height={props.height || "56"}
viewBox="0 0 62 56"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M25.4 0C26.9464 0 28.2 1.2536 28.2 2.8V53.2C28.2 54.7464 26.9464 56 25.4 56C23.8536 56 22.6 54.7464 22.6 53.2V2.8C22.6 1.2536 23.8536 0 25.4 0ZM47.8 5.6C49.3464 5.6 50.6 6.8536 50.6 8.4V44.8C50.6 46.3464 49.3464 47.6 47.8 47.6C46.2536 47.6 45 46.3464 45 44.8V8.4C45 6.8536 46.2536 5.6 47.8 5.6ZM14.2 8.4C15.7464 8.4 17 9.6536 17 11.2V42C17 43.5464 15.7464 44.8 14.2 44.8C12.6536 44.8 11.4 43.5464 11.4 42V11.2C11.4 9.6536 12.6536 8.4 14.2 8.4ZM36.6 14C38.1464 14 39.4 15.2536 39.4 16.8V36.4C39.4 37.9464 38.1464 39.2 36.6 39.2C35.0536 39.2 33.8 37.9464 33.8 36.4V16.8C33.8 15.2536 35.0536 14 36.6 14ZM3.00001 19.6C4.54641 19.6 5.80001 20.8536 5.80001 22.4V30.8C5.80001 32.3464 4.54641 33.6 3.00001 33.6C1.45361 33.6 0.200012 32.3464 0.200012 30.8V22.4C0.200012 20.8536 1.45361 19.6 3.00001 19.6ZM59 19.6C60.5464 19.6 61.8 20.8536 61.8 22.4V30.8C61.8 32.3464 60.5464 33.6 59 33.6C57.4536 33.6 56.2 32.3464 56.2 30.8V22.4C56.2 20.8536 57.4536 19.6 59 19.6Z"
fill="#4581F4"
fill={props.color || "#4581F4"}
/>
</svg>
);
Expand Down
15 changes: 15 additions & 0 deletions src/hooks/useAudioBrowser.tsx
Original file line number Diff line number Diff line change
@@ -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
};
};
3 changes: 1 addition & 2 deletions src/hooks/useAudioServer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
}
Expand Down