Browse Source

swap speech sounds to tailwind

drawing-pad
Stephanie Gredell 1 month ago
parent
commit
8f216e93c6
  1. 445
      frontend/src/pages/SpeechSoundsApp.css
  2. 146
      frontend/src/pages/SpeechSoundsApp.tsx

445
frontend/src/pages/SpeechSoundsApp.css

@ -1,445 +0,0 @@ @@ -1,445 +0,0 @@
.speech-sounds-app {
min-height: calc(100vh - 60px);
background: var(--background);
padding: 24px;
max-width: 900px;
margin: 0 auto;
}
.app-header {
text-align: center;
margin-bottom: 32px;
}
.app-header h1 {
margin: 0 0 12px 0;
font-size: 42px;
font-weight: 800;
color: var(--primary);
background: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.app-header p {
margin: 0;
font-size: 18px;
color: var(--foreground);
font-weight: 600;
opacity: 0.8;
}
.back-to-groups-button {
margin-bottom: 16px;
padding: 12px 24px;
background: var(--card);
border: 3px solid var(--primary);
border-radius: 20px;
color: var(--primary);
font-size: 16px;
font-weight: 700;
cursor: pointer;
transition: all 0.3s;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.back-to-groups-button:hover {
background: var(--primary);
color: var(--primary-foreground);
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(255, 107, 157, 0.3);
}
.groups-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 24px;
margin-top: 32px;
}
.group-card {
background: var(--card);
border: 4px solid var(--primary);
border-radius: 24px;
padding: 32px 24px;
text-align: center;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
text-decoration: none;
color: var(--foreground);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
position: relative;
overflow: hidden;
}
.group-card::before {
content: "";
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(
circle,
rgba(255, 107, 157, 0.1) 0%,
transparent 70%
);
opacity: 0;
transition: opacity 0.3s;
}
.group-card:hover {
transform: translateY(-8px) scale(1.05);
box-shadow: 0 12px 32px rgba(255, 107, 157, 0.3);
border-color: var(--secondary);
}
.group-card:hover::before {
opacity: 1;
}
.group-card-name {
margin: 0 0 8px 0;
font-size: 28px;
font-weight: 800;
color: var(--primary);
background: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
position: relative;
z-index: 1;
}
.group-card-count {
margin: 0;
font-size: 18px;
color: var(--muted-foreground);
font-weight: 700;
position: relative;
z-index: 1;
}
.practice-area {
background: var(--card);
border-radius: 32px;
padding: 40px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
border: 4px solid var(--primary);
}
.word-display {
text-align: center;
margin-bottom: 40px;
}
.word-text {
font-size: 72px;
font-weight: 900;
color: var(--primary);
background: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin: 0 0 20px 0;
letter-spacing: 4px;
animation: wordBounce 0.5s ease-out;
}
@keyframes wordBounce {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
}
.practice-stats {
display: flex;
justify-content: center;
gap: 20px;
margin-top: 20px;
flex-wrap: wrap;
}
.stat-item {
font-size: 18px;
font-weight: 700;
padding: 12px 20px;
border-radius: 25px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
border: 3px solid;
}
.stat-pass {
background: #10b981;
border-color: #10b981;
color: white;
}
.stat-fail {
background: #ef4444;
border-color: #ef4444;
color: white;
}
.stat-total {
background: var(--secondary);
border-color: var(--secondary);
color: var(--secondary-foreground);
}
.practice-container {
margin-bottom: 32px;
}
.practice-label {
text-align: center;
font-size: 20px;
font-weight: 700;
color: var(--foreground);
margin-bottom: 24px;
}
.practice-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 16px;
margin: 0 auto;
}
.practice-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding: 20px 16px;
border: 3px solid var(--border);
border-radius: 20px;
background: var(--card);
transition: all 0.3s;
}
.practice-item:hover {
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
border-color: var(--primary);
}
.practice-number {
font-size: 16px;
font-weight: 700;
color: var(--primary-foreground);
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: var(--primary);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.practice-buttons {
display: flex;
gap: 10px;
}
.practice-button {
width: 44px;
height: 44px;
border: 3px solid;
border-radius: 12px;
background: white;
font-size: 24px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.practice-button:hover {
transform: scale(1.15);
}
.pass-button {
color: #065f46;
border-color: #10b981;
}
.pass-button:hover {
background: #10b981;
border-color: #10b981;
color: white;
box-shadow: 0 6px 12px rgba(16, 185, 129, 0.3);
}
.pass-button.active {
background: #10b981;
color: white;
border-color: #10b981;
transform: scale(1.1);
box-shadow: 0 6px 16px rgba(16, 185, 129, 0.4);
}
.fail-button {
color: #991b1b;
border-color: #ef4444;
}
.fail-button:hover {
background: #ef4444;
border-color: #ef4444;
color: white;
box-shadow: 0 6px 12px rgba(239, 68, 68, 0.3);
}
.fail-button.active {
background: #ef4444;
color: white;
border-color: #ef4444;
transform: scale(1.1);
box-shadow: 0 6px 16px rgba(239, 68, 68, 0.4);
}
.word-navigation {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding-top: 24px;
border-top: 3px dashed #e0e0e0;
}
.nav-button {
padding: 16px 32px;
background: var(--primary);
color: var(--primary-foreground);
border: 3px solid var(--primary);
border-radius: 25px;
font-size: 18px;
font-weight: 700;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.nav-button:hover:not(:disabled) {
transform: translateY(-4px) scale(1.05);
box-shadow: 0 8px 20px rgba(255, 107, 157, 0.4);
border-color: var(--secondary);
}
.nav-button:disabled {
opacity: 0.4;
cursor: not-allowed;
transform: none;
}
.word-counter {
font-size: 20px;
color: var(--secondary-foreground);
font-weight: 700;
padding: 12px 24px;
background: var(--secondary);
border-radius: 20px;
border: 3px solid var(--secondary);
box-shadow: 0 4px 8px rgba(255, 165, 0, 0.3);
}
.word-actions {
text-align: center;
}
.reset-button {
padding: 12px 24px;
background: var(--card);
color: #ef4444;
border: 3px solid #ef4444;
border-radius: 20px;
font-size: 16px;
font-weight: 700;
cursor: pointer;
transition: all 0.3s;
box-shadow: 0 4px 8px rgba(239, 68, 68, 0.2);
}
.reset-button:hover {
background: #ef4444;
color: white;
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(239, 68, 68, 0.3);
}
.loading-state,
.error-state,
.empty-state {
text-align: center;
padding: 48px 24px;
background: var(--card);
border-radius: 24px;
margin-top: 32px;
border: 4px solid var(--primary);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
}
.empty-state h2 {
margin: 0 0 12px 0;
font-size: 32px;
color: var(--primary);
font-weight: 800;
background: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.empty-state p {
margin: 0;
color: var(--muted-foreground);
font-size: 18px;
font-weight: 600;
}
@media (max-width: 768px) {
.speech-sounds-app {
padding: 16px;
}
.word-text {
font-size: 48px;
}
.practice-grid {
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.practice-stats {
gap: 12px;
}
.stat-item {
font-size: 14px;
padding: 6px 12px;
}
.practice-area {
padding: 24px;
}
.word-navigation {
flex-direction: column;
gap: 16px;
}
.nav-button {
width: 100%;
}
}

146
frontend/src/pages/SpeechSoundsApp.tsx

@ -1,6 +1,5 @@ @@ -1,6 +1,5 @@
import { useState, useEffect } from 'react';
import { wordGroupsApi } from '../services/apiClient';
import './SpeechSoundsApp.css';
interface Word {
id: number;
@ -128,9 +127,9 @@ export function SpeechSoundsApp() { @@ -128,9 +127,9 @@ export function SpeechSoundsApp() {
if (loading) {
return (
<div className="speech-sounds-app">
<div className="loading-state">
<p>Loading word groups...</p>
<div className="min-h-[calc(100vh-60px)] bg-background px-6 py-6 max-w-[900px] mx-auto">
<div className="text-center py-12 px-6 bg-card rounded-3xl mt-8 border-4 border-primary shadow-lg">
<p className="text-foreground">Loading word groups...</p>
</div>
</div>
);
@ -138,9 +137,9 @@ export function SpeechSoundsApp() { @@ -138,9 +137,9 @@ export function SpeechSoundsApp() {
if (error) {
return (
<div className="speech-sounds-app">
<div className="error-state">
<p>Error: {error}</p>
<div className="min-h-[calc(100vh-60px)] bg-background px-6 py-6 max-w-[900px] mx-auto">
<div className="text-center py-12 px-6 bg-card rounded-3xl mt-8 border-4 border-primary shadow-lg">
<p className="text-foreground">Error: {error}</p>
</div>
</div>
);
@ -148,10 +147,12 @@ export function SpeechSoundsApp() { @@ -148,10 +147,12 @@ export function SpeechSoundsApp() {
if (groups.length === 0) {
return (
<div className="speech-sounds-app">
<div className="empty-state">
<h2>No Word Groups Available</h2>
<p>Please add word groups in the admin panel.</p>
<div className="min-h-[calc(100vh-60px)] bg-background px-6 py-6 max-w-[900px] mx-auto">
<div className="text-center py-12 px-6 bg-card rounded-3xl mt-8 border-4 border-primary shadow-lg">
<h2 className="m-0 mb-3 text-[32px] text-primary font-extrabold bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent">
No Word Groups Available
</h2>
<p className="m-0 text-muted-foreground text-lg font-semibold">Please add word groups in the admin panel.</p>
</div>
</div>
);
@ -160,53 +161,80 @@ export function SpeechSoundsApp() { @@ -160,53 +161,80 @@ export function SpeechSoundsApp() {
// Show word practice screen if group is selected
if (showWordPractice && selectedGroup && currentWord) {
return (
<div className="speech-sounds-app">
<div className="app-header">
<button onClick={handleBackToGroups} className="back-to-groups-button">
<div className="min-h-[calc(100vh-60px)] bg-background px-6 py-6 max-w-[900px] mx-auto">
<div className="text-center mb-8">
<button
onClick={handleBackToGroups}
className="mb-4 px-6 py-3 bg-card border-[3px] border-primary rounded-[20px] text-primary text-base font-bold cursor-pointer transition-all shadow-md hover:bg-primary hover:text-primary-foreground hover:-translate-y-0.5 hover:shadow-lg"
>
Back to Groups
</button>
<h1>{selectedGroup.name}</h1>
<p>Practice your speech sounds by checking off each time you say the word</p>
<h1 className="m-0 mb-3 text-[42px] font-extrabold text-primary bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent">
{selectedGroup.name}
</h1>
<p className="m-0 text-lg text-foreground font-semibold opacity-80">
Practice your speech sounds by checking off each time you say the word
</p>
</div>
<div className="practice-area">
<div className="word-display">
<h2 className="word-text">{currentWord.word}</h2>
<div className="practice-stats">
<span className="stat-item stat-pass">
<div className="bg-card rounded-[32px] p-10 shadow-lg border-4 border-primary">
<div className="text-center mb-10">
<h2
className="text-[72px] md:text-[72px] text-[48px] font-black mb-5 tracking-[4px] bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent animate-[wordBounce_0.5s_ease-out]"
style={{
animation: 'wordBounce 0.5s ease-out',
}}
>
{currentWord.word}
</h2>
<div className="flex justify-center gap-5 mt-5 flex-wrap">
<span className="text-lg font-bold py-3 px-5 rounded-[25px] shadow-md border-[3px] bg-[#10b981] border-[#10b981] text-white">
{passCount} Pass
</span>
<span className="stat-item stat-fail">
<span className="text-lg font-bold py-3 px-5 rounded-[25px] shadow-md border-[3px] bg-[#ef4444] border-[#ef4444] text-white">
{failCount} Fail
</span>
<span className="stat-item stat-total">
<span className="text-lg font-bold py-3 px-5 rounded-[25px] shadow-md border-[3px] bg-secondary border-secondary text-secondary-foreground">
{totalCount} / 10 Total
</span>
</div>
</div>
<div className="practice-container">
<div className="practice-label">Mark each practice attempt:</div>
<div className="practice-grid">
<div className="mb-8">
<div className="text-center text-xl font-bold text-foreground mb-6">Mark each practice attempt:</div>
<div className="grid grid-cols-5 md:grid-cols-5 grid-cols-3 gap-4 mx-auto">
{Array.from({ length: 10 }, (_, i) => {
const result = practiceResults[i];
const isPass = result === 'pass';
const isFail = result === 'fail';
return (
<div key={i} className="practice-item">
<span className="practice-number">{i + 1}</span>
<div className="practice-buttons">
<div
key={i}
className="flex flex-col items-center gap-3 p-5 border-[3px] border-border rounded-[20px] bg-card transition-all hover:-translate-y-1 hover:shadow-lg hover:border-primary"
>
<span className="text-base font-bold text-primary-foreground w-10 h-10 flex items-center justify-center rounded-full bg-primary shadow-md">
{i + 1}
</span>
<div className="flex gap-2.5">
<button
onClick={() => togglePractice(currentWord.id, i, 'pass')}
className={`practice-button pass-button ${isPass ? 'active' : ''}`}
className={`w-11 h-11 border-[3px] rounded-xl bg-white text-2xl font-bold cursor-pointer transition-all flex items-center justify-center shadow-md ${
isPass
? 'bg-[#10b981] text-white border-[#10b981] scale-110 shadow-lg'
: 'text-[#065f46] border-[#10b981] hover:bg-[#10b981] hover:border-[#10b981] hover:text-white hover:scale-110 hover:shadow-lg'
}`}
title="Mark as pass"
>
</button>
<button
onClick={() => togglePractice(currentWord.id, i, 'fail')}
className={`practice-button fail-button ${isFail ? 'active' : ''}`}
className={`w-11 h-11 border-[3px] rounded-xl bg-white text-2xl font-bold cursor-pointer transition-all flex items-center justify-center shadow-md ${
isFail
? 'bg-[#ef4444] text-white border-[#ef4444] scale-110 shadow-lg'
: 'text-[#991b1b] border-[#ef4444] hover:bg-[#ef4444] hover:border-[#ef4444] hover:text-white hover:scale-110 hover:shadow-lg'
}`}
title="Mark as fail"
>
@ -218,62 +246,84 @@ export function SpeechSoundsApp() { @@ -218,62 +246,84 @@ export function SpeechSoundsApp() {
</div>
</div>
<div className="word-navigation">
<div className="flex justify-between items-center mb-6 pt-6 border-t-[3px] border-dashed border-border md:flex-row flex-col md:gap-0 gap-4">
<button
onClick={handlePreviousWord}
disabled={currentWordIndex === 0}
className="nav-button prev-button"
className="px-8 py-4 bg-primary text-primary-foreground border-[3px] border-primary rounded-[25px] text-lg font-bold cursor-pointer transition-all shadow-md hover:-translate-y-1 hover:scale-105 hover:shadow-lg hover:border-secondary disabled:opacity-40 disabled:cursor-not-allowed disabled:transform-none"
>
Previous
</button>
<span className="word-counter">
<span className="text-xl text-secondary-foreground font-bold py-3 px-6 bg-secondary rounded-[20px] border-[3px] border-secondary shadow-md">
Word {currentWordIndex + 1} of {selectedGroup.words.length}
</span>
<button
onClick={handleNextWord}
disabled={currentWordIndex === selectedGroup.words.length - 1}
className="nav-button next-button"
className="px-8 py-4 bg-primary text-primary-foreground border-[3px] border-primary rounded-[25px] text-lg font-bold cursor-pointer transition-all shadow-md hover:-translate-y-1 hover:scale-105 hover:shadow-lg hover:border-secondary disabled:opacity-40 disabled:cursor-not-allowed disabled:transform-none md:w-auto w-full"
>
Next
</button>
</div>
<div className="word-actions">
<div className="text-center">
<button
onClick={() => resetWord(currentWord.id)}
className="reset-button"
className="px-6 py-3 bg-card text-[#ef4444] border-[3px] border-[#ef4444] rounded-[20px] text-base font-bold cursor-pointer transition-all shadow-md hover:bg-[#ef4444] hover:text-white hover:-translate-y-0.5 hover:shadow-lg"
>
Reset This Word
</button>
</div>
</div>
<style>{`
@keyframes wordBounce {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
}
`}</style>
</div>
);
}
// Show group selection screen
return (
<div className="speech-sounds-app">
<div className="app-header">
<h1>Speech Sounds Practice</h1>
<p>Choose a word group to start practicing</p>
<div className="min-h-[calc(100vh-60px)] bg-background px-6 py-6 max-w-[900px] mx-auto">
<div className="text-center mb-8">
<h1 className="m-0 mb-3 text-[42px] font-extrabold text-primary bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent">
Speech Sounds Practice
</h1>
<p className="m-0 text-lg text-foreground font-semibold opacity-80">
Choose a word group to start practicing
</p>
</div>
{groups.length === 0 ? (
<div className="empty-state">
<h2>No Word Groups Available</h2>
<p>Please add word groups in the admin panel.</p>
<div className="text-center py-12 px-6 bg-card rounded-3xl mt-8 border-4 border-primary shadow-lg">
<h2 className="m-0 mb-3 text-[32px] text-primary font-extrabold bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent">
No Word Groups Available
</h2>
<p className="m-0 text-muted-foreground text-lg font-semibold">Please add word groups in the admin panel.</p>
</div>
) : (
<div className="groups-grid">
<div className="grid grid-cols-[repeat(auto-fill,minmax(250px,1fr))] gap-6 mt-8">
{groups.map(group => (
<button
key={group.id}
onClick={() => handleSelectGroup(group)}
className="group-card"
className="bg-card border-4 border-primary rounded-3xl py-8 px-6 text-center cursor-pointer transition-all text-foreground shadow-lg relative overflow-hidden hover:-translate-y-2 hover:scale-105 hover:shadow-xl hover:border-secondary group"
>
<h3 className="group-card-name">{group.name}</h3>
<p className="group-card-count">{group.words.length} words</p>
<div className="absolute top-[-50%] left-[-50%] w-[200%] h-[200%] bg-[radial-gradient(circle,rgba(255,107,157,0.1)_0%,transparent_70%)] opacity-0 transition-opacity group-hover:opacity-100"></div>
<h3 className="m-0 mb-2 text-[28px] font-extrabold text-primary bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent relative z-10">
{group.name}
</h3>
<p className="m-0 text-lg text-muted-foreground font-bold relative z-10">
{group.words.length} words
</p>
</button>
))}
</div>

Loading…
Cancel
Save