Skip to content

Commit f36e2bb

Browse files
committed
v0.0.4 released - Timers, final screen and WPM calculation
1 parent 521d222 commit f36e2bb

18 files changed

+1596
-334
lines changed

package-lock.json

Lines changed: 390 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "textcode",
33
"private": true,
4-
"version": "0.0.3",
4+
"version": "0.0.4",
55
"type": "module",
66
"homepage": "https://maek0s.github.io/TextCode",
77
"scripts": {
@@ -21,7 +21,8 @@
2121
"react-color": "^2.19.3",
2222
"react-confetti": "^6.4.0",
2323
"react-dom": "^19.1.0",
24-
"react-icons": "^5.5.0"
24+
"react-icons": "^5.5.0",
25+
"recharts": "^3.1.2"
2526
},
2627
"devDependencies": {
2728
"@eslint/js": "^9.30.1",

src/App.css

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -374,8 +374,7 @@ footer {
374374
gap: 6px;
375375
}
376376

377-
378-
/* Cursor mode code */
377+
/* Cursor */
379378

380379
.cursor {
381380
position: relative;
@@ -398,11 +397,30 @@ footer {
398397
height: 1.1em;
399398
}
400399

400+
.cursor::after {
401+
content: "";
402+
display: block;
403+
width: 2px;
404+
height: 0.9em;
405+
background: var(--cursor-color, #007acc);
406+
animation:
407+
blink 1s steps(2, start) infinite,
408+
pop 0.25s ease-out;
409+
}
410+
411+
/* Animaciones del cursor */
412+
401413
@keyframes blink {
402414
0%, 50% { opacity: 1; }
403415
51%, 100% { opacity: 0; }
404416
}
405417

418+
@keyframes pop {
419+
0% { transform: scaleY(0.9); opacity: 0; }
420+
50% { transform: scaleY(1.2); opacity: 1; }
421+
100% { transform: scaleY(1); opacity: 1; }
422+
}
423+
406424
/* Cambios situacionales */
407425

408426
/* max-width 850px */

src/App.jsx

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,25 @@ import logo from './assets/images/logotextcode.png'
77
import Game from './logic/Game.jsx'
88

99
import Footer from './components/Footer.jsx';
10-
1110
import OptionNav from './components/OptionNav.jsx';
1211

13-
import { FaUser } from "react-icons/fa";
12+
import { FaUser, FaCheckCircle } from "react-icons/fa";
1413
import { IoMdSettings } from "react-icons/io";
1514
import { MdLeaderboard } from "react-icons/md";
1615

1716
import SettingsModal from "./components/SettingsModal.jsx";
17+
import Notification from './components/Notification.jsx';
18+
import UpdatesModal from './components/UpdatesModal.jsx';
1819

1920
import { saveSettings } from "./logic/storage/settings.js"
20-
import UpdatesModal from './components/UpdatesModal.jsx';
2121

2222
function App() {
2323
// Header
2424
const [ isSettingsOpen, setIsSettingsOpen ] = useState(false)
2525

26+
// Notification
27+
const [notification, setNotification] = useState(null);
28+
2629
// Settings
2730
const [ settings, setSettings ] = useState(() => {
2831
const stored = window.localStorage.getItem("settings")
@@ -68,6 +71,18 @@ function App() {
6871
</div>
6972
</header>
7073
<div>
74+
{notification && (
75+
<Notification
76+
message={notification.message}
77+
icon={notification.icon}
78+
backgroundColor={notification.backgroundColor}
79+
textColor={notification.textColor}
80+
duration={notification.duration}
81+
width={notification.width}
82+
position={notification.position}
83+
onClose={() => setNotification(null)}
84+
/>
85+
)}
7186
<SettingsModal
7287
isOpen={isSettingsOpen}
7388
onClose={() => setIsSettingsOpen(false)}
@@ -80,21 +95,12 @@ function App() {
8095
/>
8196
</div>
8297

83-
<Game settings={settings}/>
84-
85-
{
86-
settings.controles && (
87-
<div className="controlsDiv">
88-
<ul>
89-
<li><kbd>Ctrl</kbd> + <kbd>R</kbd> Reiniciar</li>
90-
{/*<li><kbd>Esc</kbd> Parar juego</li>*/}
91-
</ul>
92-
</div>
93-
)
94-
}
98+
<Game settings={settings}
99+
setNotification={setNotification}
100+
notification={notification}
101+
/>
95102

96103
<Footer
97-
isUpdatesOpen={isUpdatesOpen}
98104
setIsUpdatesOpen={setIsUpdatesOpen}
99105
/>
100106
</>

src/components/FinalScreen.jsx

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { useMemo } from "react";
2+
3+
import { FaRedo } from "react-icons/fa";
4+
5+
import { PieChart, Pie, Cell, ResponsiveContainer } from "recharts";
6+
import "../styles/FinalScreen.css";
7+
8+
export default function FinalScreen({ data, gameMode, onClick }) {
9+
const stats = useMemo(() => {
10+
const totalWords = data.length
11+
const correctWords = data.filter(w => w.correctWord).length
12+
const wrongWords = totalWords - correctWords
13+
const totalLetters = data.reduce((acc, w) => acc + w.letterCorrect + w.letterWrong, 0)
14+
const correctLetters = data.reduce((acc, w) => acc + w.letterCorrect, 0)
15+
const wrongLetters = data.reduce((acc, w) => acc + w.letterWrong, 0)
16+
const accuracy = totalLetters > 0 ? Math.round((correctLetters / totalLetters) * 100) : 0
17+
const wpmNeto = data.wpmNeto || 0
18+
const wpmBruto = data.wpmBruto || 0
19+
const time = data.time
20+
21+
return { totalWords, correctWords, wrongWords, totalLetters, correctLetters,
22+
wrongLetters, accuracy, wpmNeto, wpmBruto, time }
23+
}, [data])
24+
25+
const getMotivationalMessage = (wpmNeto) => {
26+
if (wpmNeto >= 100) return "¡Perfecto, eres muy veloz! 🔥"
27+
if (wpmNeto >= 70) return "¡Gran trabajo, lo haces genial! 💪"
28+
if (wpmNeto >= 50) return "¡Bien hecho! Sigue practicando ✨"
29+
if (wpmNeto >= 40 && wpmNeto < 50) return "No está mal, pero puedes mejorar 😉"
30+
if (wpmNeto < 40) return "¡No te rindas! Poco a poco mejorarás 🆙"
31+
return "¡No te rindas! Cada error cuenta 💡"
32+
}
33+
34+
const donutData = [
35+
{ name: "Correctas", value: stats.accuracy },
36+
{ name: "Incorrectas", value: 100 - stats.accuracy }
37+
]
38+
39+
const COLORS = ["#4aff91", "#ff4b6b"]
40+
41+
return (
42+
<div className="final-screen-graph">
43+
44+
<div className="donut-section horizontal">
45+
<div className="donut-left">
46+
<div className="donut-container">
47+
<ResponsiveContainer width={200} height={200}>
48+
<PieChart>
49+
<Pie
50+
data={donutData}
51+
innerRadius={70}
52+
outerRadius={90}
53+
dataKey="value"
54+
startAngle={90}
55+
endAngle={-270}
56+
>
57+
{donutData.map((entry, index) => (
58+
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
59+
))}
60+
</Pie>
61+
</PieChart>
62+
</ResponsiveContainer>
63+
<div className="donut-center">
64+
<span className="donut-value">{stats.accuracy}%</span>
65+
<span className="donut-label">PRECISION</span>
66+
</div>
67+
</div>
68+
</div>
69+
70+
<div className="donut-right">
71+
<div className="motivational-message">
72+
{getMotivationalMessage(stats.wpmNeto)}
73+
</div>
74+
<div className="wpm-panel horizontal">
75+
<div className="metric">
76+
<span className="label">WPM</span>
77+
<span className="value">{stats.wpmNeto}</span>
78+
</div>
79+
<div className="metric">
80+
<span className="label">WPM raw</span>
81+
<span className="value">{stats.wpmBruto}</span>
82+
</div>
83+
</div>
84+
</div>
85+
</div>
86+
87+
<div className="extra-stats">
88+
<div className="stat-card accuracy">
89+
<span className="icon">🎮</span>
90+
<span className="label">Modo de juego</span>
91+
<span className="value">{gameMode}</span>
92+
</div>
93+
<div className="stat-card time">
94+
<span className="icon">⏱️</span>
95+
<span className="label">Tiempo</span>
96+
<span className="value">{stats.time}s</span>
97+
</div>
98+
<div className="stat-card total">
99+
<span className="icon">🔢</span>
100+
<span className="label">Total letras</span>
101+
<span className="value">{stats.totalLetters}</span>
102+
</div>
103+
<div className="stat-card correct">
104+
<span className="icon"></span>
105+
<span className="label">Letras correctas</span>
106+
<span className="value">{stats.correctLetters}</span>
107+
</div>
108+
<div className="stat-card wrong">
109+
<span className="icon"></span>
110+
<span className="label">Letras incorrectas</span>
111+
<span className="value">{stats.wrongLetters}</span>
112+
</div>
113+
</div>
114+
115+
<div className="next-button-container">
116+
<button className="resetButtonFinal"
117+
onClick={(e) => {
118+
e.currentTarget.blur()
119+
onClick()
120+
}}
121+
tabIndex="-1">
122+
<FaRedo className="icon"/>
123+
<span className="text">Reiniciar</span>
124+
</button>
125+
</div>
126+
</div>
127+
);
128+
}

src/components/Footer.jsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ import { MdBrowserUpdated } from "react-icons/md";
44

55
import packageJson from '/package.json';
66

7-
function Footer({ isUpdatesOpen, setIsUpdatesOpen }) {
8-
console.log(isUpdatesOpen)
7+
function Footer({ setIsUpdatesOpen }) {
98
return (
109
<footer>
1110
<span className="githubCreator">

src/components/Notification.jsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { useState, useEffect } from "react";
2+
import "../styles/Notification.css";
3+
4+
const Notification = ({
5+
message,
6+
icon = null, // JSX del icono
7+
backgroundColor = "#333",
8+
textColor = "#fff",
9+
duration = 3000,
10+
onClose,
11+
}) => {
12+
const [visible, setVisible] = useState(false)
13+
14+
useEffect(() => {
15+
setVisible(true)
16+
17+
const timer = setTimeout(() => {
18+
setVisible(false)
19+
setTimeout(() => {
20+
if (onClose) onClose()
21+
}, 300)
22+
}, duration)
23+
24+
return () => clearTimeout(timer)
25+
}, [duration, onClose])
26+
27+
return (
28+
<div
29+
className={`notification ${visible ? "show" : "hide"}`}
30+
style={{ backgroundColor, color: textColor }}
31+
>
32+
{icon && <div className="notification-icon">{icon}</div>}
33+
<div className="notification-message">{message}</div>
34+
</div>
35+
)
36+
}
37+
38+
export default Notification

src/components/SettingsModal.jsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ export default function SettingsModal({ isOpen, onClose, settings, onSave }) {
1818
onSave(updatedSettings)
1919
}
2020

21-
console.log(settings.cursorColor)
22-
2321
return (
2422
<div className="modal-overlay" onClick={onClose}>
2523
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
@@ -84,6 +82,16 @@ export default function SettingsModal({ isOpen, onClose, settings, onSave }) {
8482
)
8583
}
8684

85+
<label className="setting-row">
86+
<span>Mostrar pantalla final</span>
87+
<div
88+
className={`toggle-switch ${settings.enableFinalScreen ? "on" : "off"}`}
89+
onClick={() => toggleSetting("enableFinalScreen")}
90+
>
91+
<div className="toggle-thumb" />
92+
</div>
93+
</label>
94+
8795
<h2>Sonido 🎶</h2>
8896

8997
<label className="setting-row">

src/components/UpdatesModal.jsx

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,9 @@ export default function UpdatesModal({ isOpen, onClose }) {
99
<h2>📢 Próximas actualizaciones</h2>
1010

1111
<div className="updates-list">
12-
<div className="update-block">
13-
<h3>v0.0.4</h3>
14-
<ul>
15-
<li>Temporizador en modo de juego OneWord</li>
16-
<li>Pantalla final</li>
17-
<li>Cálculo de WPM, letras acertadas y más estadísticas</li>
18-
</ul>
19-
</div>
20-
2112
<div className="update-block">
2213
<h3>v0.0.5</h3>
2314
<ul>
24-
<li>Temporizador en modo de juego TimeMode</li>
2515
<li>Mejora en el responsive</li>
2616
<li>Mejora en la estructura del proyecto</li>
2717
</ul>
@@ -46,9 +36,11 @@ export default function UpdatesModal({ isOpen, onClose }) {
4636
<div className="update-block">
4737
<h3>v1.0.1</h3>
4838
<ul>
39+
<li>Compatibilidad con móvil</li>
4940
<li>Tabla de clasificación de mejores tiempos</li>
5041
</ul>
5142
</div>
43+
5244
</div>
5345

5446
<p className="updates-note">

0 commit comments

Comments
 (0)