Skip to content

Commit 2ec8be1

Browse files
authored
Merge branch 'reactplay:main' into main
2 parents fa8abf7 + b434421 commit 2ec8be1

7 files changed

Lines changed: 391 additions & 3 deletions

File tree

‎.github/workflows/contributions.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
name: Add contributors
2+
permissions:
3+
issues: write
24
on:
35
pull_request_target:
46
types:

‎src/common/header/header.css‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -283,8 +283,8 @@
283283

284284
.header-links > li > .app-header-btn.app-header-btn--secondary:hover,
285285
.header-links > li > .app-header-btn.app-header-btn--secondary:focus {
286-
border-color: var(--color-neutral-10);
287-
background-color: var(--color-neutral-10);
286+
border-color: var(--color-brand-primary);
287+
background-color: var(--color-neutral-10);
288288
}
289289

290290
.header-links > li > .app-header-btn.app-header-btn--secondary {

‎src/plays/calculator-by-tea/useCalcalulatorByTeaHook.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ const useCalcalulatorByTeaHook = () => {
5959
}
6060

6161
function onInputRecived(expression: string) {
62-
if (expression.match(/^(?:[0-9]+[-+*/]?)*[0-9]*$/)) {
62+
if (expression.match(/^(?:[0-9]+[-+*/])*[0-9]*$/)) {
6363
setCalculationExpression(expression);
6464
}
6565
}

‎src/plays/text-to-speech/Readme.md‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Text To Speech
2+
3+
Convert text input into spoken audio directly in the browser using the Web Speech API.
4+
5+
## Key Concepts Demonstrated
6+
7+
1. State & Event Handling in React (or Vanilla JS)
8+
2. Web Speech API (`speechSynthesis` and `SpeechSynthesisUtterance`)
9+
3. Conditional Rendering & UI updates
10+
4. Responsive layout with Flexbox and media queries
11+
12+
## How It Works
13+
14+
- User types text into the input box.
15+
- Click **Play** to convert the text into speech.
16+
- Click **Stop** to cancel speech playback.
17+
- Works completely offline (runs client-side).
18+
- Responsive design for different screen sizes.
19+
20+
## Tech Stack
21+
22+
- React (or Vanilla JS)
23+
- CSS
24+
- Web Speech API
25+
26+
## Notes
27+
28+
- Best supported in Chrome, Edge, and Safari; limited support in Firefox.
29+
- Voices vary depending on browser and device.
30+
- Future enhancements: control rate, pitch, and volume.
31+
32+
## Resources
33+
34+
- [MDN – Web Speech API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Speech_API)
35+
- [CSS Tricks – Using the Speech Synthesis API](https://css-tricks.com/using-the-speech-synthesis-api/)
36+
- [W3C Web Speech API Specification](https://www.w3.org/TR/speech-synthesis/)
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import React, { useState, useEffect, useRef } from 'react';
2+
import { FaVolumeUp, FaStop } from 'react-icons/fa';
3+
import PlayHeader from 'common/playlists/PlayHeader';
4+
import './styles.css';
5+
6+
function TextToSpeech(props) {
7+
const [inputText, setInputText] = useState('');
8+
const [convertedText, setConvertedText] = useState('');
9+
const [isSpeaking, setIsSpeaking] = useState(false);
10+
const [rate, setRate] = useState(1);
11+
const [pitch, setPitch] = useState(1);
12+
const [voices, setVoices] = useState([]);
13+
const [selectedVoice, setSelectedVoice] = useState(null);
14+
const [convertClick, setConvertClick] = useState(0);
15+
const [opened, setOpened] = useState(false);
16+
17+
const utteranceRef = useRef(null);
18+
19+
const stopSpeech = () => {
20+
if (window.speechSynthesis.speaking || window.speechSynthesis.paused) {
21+
window.speechSynthesis.cancel();
22+
setIsSpeaking(false);
23+
}
24+
};
25+
26+
useEffect(() => {
27+
setConvertedText(
28+
'Hello there! This feature is powered by the Web Speech API, built by Ritesh. Try generating a few audios to unlock a secret. You can play with rate, pitch, and voice settings. Enjoy experimenting!'
29+
);
30+
setInputText(
31+
'Hello there! This feature is powered by the Web Speech API, built by Ritesh. Try generating a few audios to unlock a secret. You can play with rate, pitch, and voice settings. Enjoy experimenting!'
32+
);
33+
}, []);
34+
35+
useEffect(() => {
36+
const loadVoices = () => {
37+
const availableVoices = window.speechSynthesis.getVoices();
38+
setVoices(availableVoices);
39+
if (!selectedVoice && availableVoices.length > 0) {
40+
setSelectedVoice(availableVoices[0].name);
41+
}
42+
};
43+
44+
loadVoices();
45+
window.speechSynthesis.onvoiceschanged = loadVoices;
46+
}, [selectedVoice]);
47+
48+
useEffect(() => {
49+
if (convertClick > 4 && !opened) {
50+
window.open('https://riteshjs.vercel.app/', '_blank');
51+
setOpened(true);
52+
}
53+
}, [convertClick, opened]);
54+
55+
const handleSpeak = () => {
56+
if (isSpeaking) {
57+
stopSpeech();
58+
59+
return;
60+
}
61+
if (!convertedText.trim()) return;
62+
63+
const utterance = new SpeechSynthesisUtterance(convertedText);
64+
utterance.lang = 'en-US';
65+
utterance.rate = rate;
66+
utterance.pitch = pitch;
67+
68+
const voice = voices.find((v) => v.name === selectedVoice);
69+
if (voice) utterance.voice = voice;
70+
71+
utterance.onend = () => setIsSpeaking(false);
72+
utteranceRef.current = utterance;
73+
74+
window.speechSynthesis.speak(utterance);
75+
setIsSpeaking(true);
76+
};
77+
78+
const handleConvert = () => {
79+
stopSpeech();
80+
if (!inputText.trim()) return;
81+
setConvertedText(inputText.trim());
82+
setConvertClick((prev) => prev + 1);
83+
};
84+
85+
useEffect(() => {
86+
const handleBeforeUnload = () => stopSpeech();
87+
window.addEventListener('beforeunload', handleBeforeUnload);
88+
89+
return () => {
90+
stopSpeech();
91+
window.removeEventListener('beforeunload', handleBeforeUnload);
92+
};
93+
}, []);
94+
95+
return (
96+
<>
97+
<div className="play-details">
98+
<PlayHeader play={props} />
99+
<div className="play-details-body">
100+
<div className="tts-wrapper">
101+
{/* Left side */}
102+
<div className="tts-input-box">
103+
<textarea
104+
className="tts-textarea"
105+
placeholder="Type something here..."
106+
value={inputText}
107+
onChange={(e) => setInputText(e.target.value)}
108+
/>
109+
110+
<div className="tts-sliders">
111+
<div>
112+
<label>Rate: {rate.toFixed(1)}</label>
113+
<input
114+
max="2"
115+
min="0.5"
116+
step="0.1"
117+
type="range"
118+
value={rate}
119+
onChange={(e) => setRate(Number(e.target.value))}
120+
/>
121+
</div>
122+
<div>
123+
<label>Pitch: {pitch.toFixed(1)}</label>
124+
<input
125+
max="2"
126+
min="0"
127+
step="0.1"
128+
type="range"
129+
value={pitch}
130+
onChange={(e) => setPitch(Number(e.target.value))}
131+
/>
132+
</div>
133+
</div>
134+
135+
{/* Voice Selector */}
136+
<div className="tts-voice-selector">
137+
<label>
138+
Voice:{' '}
139+
<select
140+
value={selectedVoice || ''}
141+
onChange={(e) => setSelectedVoice(e.target.value)}
142+
>
143+
{voices.map((voice, idx) => (
144+
<option key={idx} value={voice.name}>
145+
{voice.name} {voice.lang ? `(${voice.lang})` : ''}
146+
</option>
147+
))}
148+
</select>
149+
</label>
150+
</div>
151+
152+
<button className="tts-convert-btn" onClick={handleConvert}>
153+
Convert
154+
</button>
155+
</div>
156+
157+
{/* Right side */}
158+
<div className="tts-output-box">
159+
{convertedText ? (
160+
<>
161+
<p
162+
className="tts-output-text"
163+
dangerouslySetInnerHTML={{ __html: convertedText }}
164+
/>
165+
166+
<button className="tts-speaker-btn" onClick={handleSpeak}>
167+
{isSpeaking ? <FaStop size={28} /> : <FaVolumeUp size={28} />}
168+
</button>
169+
</>
170+
) : (
171+
<p className="tts-placeholder">Converted text will appear here...</p>
172+
)}
173+
</div>
174+
</div>
175+
</div>
176+
</div>
177+
</>
178+
);
179+
}
180+
181+
export default TextToSpeech;

‎src/plays/text-to-speech/cover.png‎

6.21 KB
Loading

0 commit comments

Comments
 (0)