Skip to content

Commit 198e55d

Browse files
committed
Optimize learning experience with mobile-first responsive layout
MAJOR IMPROVEMENTS: - Responsive sticky layout: Side-by-side on desktop (passage left, question right), sticky passage on mobile - Enhanced reading flow: Context banners, improved typography, better visual hierarchy - Improved feedback system: 2-second think timer before showing correct answer, immediate result indicator - Touch-friendly design: 44px minimum touch targets, optimized for mobile interaction - Visual polish: Enhanced spacing, card elevation, gradient improvements FEATURES: - Mobile: Sticky passage header stays visible while scrolling through questions - Desktop: Split view with passage always visible alongside questions - Reading context banner: Clear instruction to read carefully before answering - Question context banner: Visual separator and prompt for question section - Enhanced option buttons: Better spacing, hover effects, responsive text sizing - Think timer: 2-second delay encourages reflection before showing explanation - Immediate feedback: Shows correct/incorrect immediately, detailed explanation after delay MOBILE OPTIMIZATIONS: - Touch-friendly buttons with min-height 44px - Prevents iOS zoom on input focus (16px font-size) - Responsive typography: Scales from mobile to desktop - Optimized spacing and padding for all screen sizes TRANSLATIONS: - Added 4 new translation keys to all 16 languages: - readingInstruction: Guides user to read carefully - questionPrompt: Contextualizes the question - readComplete: Reading status indicator - reading: Reading in progress indicator DESIGN ENHANCEMENTS: - Upgraded container to rounded-2xl with shadow-2xl - Gradient backgrounds from-gray-800 to-gray-900 - Better visual hierarchy with improved spacing - Enhanced borders and visual feedback All E2E tests passing. Optimized for learning effectiveness and mobile experience.
1 parent 503381e commit 198e55d

File tree

21 files changed

+202
-67
lines changed

21 files changed

+202
-67
lines changed

app/components/TextGenerator/LanguageSelector.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const LanguageSelector = () => {
1212
useTextGeneratorStore();
1313

1414
return (
15-
<div className="bg-gradient-to-r from-gray-800 to-gray-900 rounded-xl p-6 border border-gray-700 shadow-lg mb-8">
15+
<div className="bg-gradient-to-r from-gray-800 via-gray-800 to-gray-900 rounded-2xl p-6 md:p-8 border border-gray-700 shadow-xl mb-6">
1616
<div className="grid grid-cols-2 gap-x-4 gap-y-2">
1717
<label
1818
htmlFor="passage-language-select"

app/components/TextGenerator/QuizSection.tsx

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
'use client';
22

3-
import React, { useCallback } from 'react';
3+
import React, { useCallback, useState, useEffect } from 'react';
44
import { useTranslation } from 'react-i18next';
55
import { getTextDirection, useLanguage } from 'app/hooks/useLanguage';
66
import useTextGeneratorStore from 'app/store/textGeneratorStore';
7-
import { InformationCircleIcon } from '@heroicons/react/24/outline';
7+
import {
8+
InformationCircleIcon,
9+
QuestionMarkCircleIcon,
10+
CheckCircleIcon,
11+
XCircleIcon,
12+
} from '@heroicons/react/24/outline';
813
import type { QuizData } from 'app/domain/schemas';
914
import type { Language } from 'app/domain/language';
1015
import { LanguageSchema } from 'app/domain/language';
@@ -45,7 +50,9 @@ const QuizOptionButton: React.FC<QuizOptionButtonProps> = ({
4550
key={optionKey}
4651
onClick={handleAsyncClick(optionKey)}
4752
disabled={isAnswered || isSubmittingAnswer}
48-
className={`w-full text-left p-3 rounded-md border transition-colors relative ${
53+
className={`w-full text-left min-h-[44px] p-4 md:p-5 rounded-lg border-2 transition-all duration-200 transform relative text-base md:text-lg touch-manipulation ${
54+
!isAnswered && !isSubmittingAnswer ? 'hover:scale-[1.02] hover:shadow-lg' : ''
55+
} ${
4956
isAnswered && feedback.correctAnswer
5057
? optionKey === feedback.correctAnswer
5158
? 'bg-green-900/50 border-green-700 text-green-100'
@@ -164,6 +171,16 @@ const QuizSection = () => {
164171
isSubmittingFeedback,
165172
} = useTextGeneratorStore();
166173
const questionLanguage = contextQuestionLanguage;
174+
const [showCorrectAnswer, setShowCorrectAnswer] = useState(false);
175+
176+
useEffect(() => {
177+
if (isAnswered) {
178+
const timer = setTimeout(() => { setShowCorrectAnswer(true); }, 2000);
179+
return () => { clearTimeout(timer); };
180+
} else {
181+
setShowCorrectAnswer(false);
182+
}
183+
}, [isAnswered]);
167184
const handleAsyncClick = useCallback(
168185
(answer: string) => (event: React.MouseEvent<HTMLButtonElement>) => {
169186
event.preventDefault();
@@ -175,9 +192,18 @@ const QuizSection = () => {
175192
return null;
176193
}
177194
return (
178-
<div className="mt-6 space-y-4" data-testid="quiz-section">
195+
<div
196+
className="pt-6 mt-6 border-t border-gray-700 lg:border-0 lg:pt-0 lg:mt-0 space-y-4"
197+
data-testid="quiz-section"
198+
>
199+
<div className="mb-4 p-3 bg-purple-900/20 border border-purple-700/50 rounded-lg">
200+
<p className="text-sm text-purple-200 flex items-center gap-2">
201+
<QuestionMarkCircleIcon className="w-4 h-4" />
202+
{t('practice.questionPrompt')}
203+
</p>
204+
</div>
179205
<h3
180-
className="text-lg font-semibold text-white"
206+
className="text-lg md:text-xl lg:text-2xl font-semibold text-white"
181207
data-testid="question-text"
182208
dir={getTextDirection(questionLanguage)}
183209
>
@@ -200,14 +226,37 @@ const QuizSection = () => {
200226
/>
201227
))}
202228
</div>
203-
<FeedbackExplanation
204-
isAnswered={isAnswered}
205-
showExplanation={showExplanation}
206-
feedback={feedback}
207-
t={t}
208-
questionLanguage={questionLanguage}
209-
generatedPassageLanguage={generatedPassageLanguage}
210-
/>
229+
{isAnswered && (
230+
<div className="mt-6 space-y-4">
231+
{/* Immediate result indicator */}
232+
<div
233+
className={`p-4 rounded-lg border-2 ${feedback.isCorrect ? 'bg-green-900/30 border-green-600' : 'bg-red-900/30 border-red-600'}`}
234+
>
235+
<div className="flex items-center gap-3">
236+
{feedback.isCorrect ? (
237+
<CheckCircleIcon className="w-6 h-6 text-green-400" />
238+
) : (
239+
<XCircleIcon className="w-6 h-6 text-red-400" />
240+
)}
241+
<span className="text-lg font-semibold text-white">
242+
{feedback.isCorrect ? t('practice.correct') : t('practice.incorrect')}
243+
</span>
244+
</div>
245+
</div>
246+
247+
{/* Detailed explanation (shows after delay) */}
248+
{showCorrectAnswer && (
249+
<FeedbackExplanation
250+
isAnswered={isAnswered}
251+
showExplanation={showExplanation}
252+
feedback={feedback}
253+
t={t}
254+
questionLanguage={questionLanguage}
255+
generatedPassageLanguage={generatedPassageLanguage}
256+
/>
257+
)}
258+
</div>
259+
)}
211260
<ProgressionFeedback />
212261
{isSubmittingFeedback && (
213262
<div className="mt-4 p-4 bg-gray-700/50 border border-gray-600 rounded-lg shadow">

app/components/TextGenerator/ReadingPassage.tsx

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -36,30 +36,39 @@ const ReadingPassage = () => {
3636
}
3737

3838
return (
39-
<div data-testid="reading-passage" className="mb-6">
40-
<div className="flex items-center justify-between mb-4">
41-
<div className="flex items-center text-blue-400">
42-
<BookOpenIcon className="w-5 h-5 mr-2 hidden sm:inline-flex" />
43-
<span className="text-lg font-medium hidden sm:inline" data-testid="passage-title">
44-
{t('practice.passageTitle')}
45-
</span>
46-
</div>
47-
<div className="flex items-center space-x-4">
48-
{hover.progressionPhase === 'credits' && (
49-
<div
50-
className="flex items-center justify-center w-8 h-8 rounded-full text-sm font-medium text-yellow-400 bg-gray-700/50"
51-
data-testid="hover-credits-display"
52-
title={t('practice.hoverCreditsTooltip') || 'Hover Credits'}
53-
>
54-
<span>{hover.creditsAvailable}</span>
55-
</div>
56-
)}
57-
<AudioControls />
39+
<div data-testid="reading-passage" className="mb-6 lg:mb-0">
40+
<div className="sticky top-0 z-10 bg-gray-900/95 backdrop-blur-sm lg:relative lg:bg-transparent lg:backdrop-blur-none pb-4 mb-4 border-b border-gray-700 lg:border-0">
41+
<div className="flex items-center justify-between mb-4">
42+
<div className="flex items-center text-blue-400">
43+
<BookOpenIcon className="w-5 h-5 mr-2 hidden sm:inline-flex" />
44+
<span className="text-lg font-medium hidden sm:inline" data-testid="passage-title">
45+
{t('practice.passageTitle')}
46+
</span>
47+
</div>
48+
<div className="flex items-center space-x-4">
49+
{hover.progressionPhase === 'credits' && (
50+
<div
51+
className="flex items-center justify-center w-8 h-8 rounded-full text-sm font-medium text-yellow-400 bg-gray-700/50"
52+
data-testid="hover-credits-display"
53+
title={t('practice.hoverCreditsTooltip') || 'Hover Credits'}
54+
>
55+
<span>{hover.creditsAvailable}</span>
56+
</div>
57+
)}
58+
<AudioControls />
59+
</div>
5860
</div>
5961
</div>
6062

63+
<div className="mb-3 p-3 bg-blue-900/20 border border-blue-700/50 rounded-lg">
64+
<p className="text-sm text-blue-200 flex items-center gap-2">
65+
<BookOpenIcon className="w-4 h-4" />
66+
{t('practice.readingInstruction')}
67+
</p>
68+
</div>
69+
6170
<div
62-
className="prose prose-xl prose-invert max-w-none text-gray-300 leading-relaxed"
71+
className="prose prose-lg md:prose-xl prose-invert max-w-none text-gray-200 leading-relaxed md:leading-loose tracking-wide"
6372
data-testid="passage-text"
6473
>
6574
<div dir={getTextDirection(generatedPassageLanguage)}>

app/components/TextGenerator/TextGeneratorContainer.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,18 @@ const TextGeneratorContainer = () => {
7676

7777
{isContentVisible && (
7878
<div
79-
className="bg-gray-800 rounded-xl p-4 md:p-6 border border-gray-700 shadow-lg mt-6"
79+
className="bg-gradient-to-br from-gray-800 to-gray-900 rounded-2xl p-6 md:p-8 border border-gray-700 shadow-2xl mt-6"
8080
data-testid="generated-content"
8181
ref={generatedContentRef}
8282
>
83-
<ReadingPassage />
84-
<QuizSection />
83+
<div className="flex flex-col lg:flex-row lg:gap-6 lg:items-start">
84+
<div className="lg:w-2/5 lg:sticky lg:top-4 lg:self-start">
85+
<ReadingPassage />
86+
</div>
87+
<div className="lg:w-3/5">
88+
<QuizSection />
89+
</div>
90+
</div>
8591
</div>
8692
)}
8793

app/globals.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@ body {
4040
outline-offset: 2px;
4141
}
4242

43+
/* Prevent zoom on focus (iOS) */
44+
input,
45+
select,
46+
textarea {
47+
font-size: 16px;
48+
}
49+
4350
/* Fade-in animation */
4451
@keyframes fadeIn {
4552
from {

public/locales/de/common.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@
8181
"hoverCreditsTooltip": "Fahren Sie mit der Maus über Wörter oder klicken Sie darauf, um sie zu übersetzen. Credits sind begrenzt.",
8282
"relevantText": "Relevanter Text",
8383
"showExplanation": "Erklärung anzeigen",
84+
"startStreak": "Starten Sie Ihre Serie mit richtigen Antworten!",
85+
"readingInstruction": "Lesen Sie den Text sorgfältig, um die folgende Frage zu beantworten",
86+
"questionPrompt": "Beantworten Sie diese Frage basierend auf dem obigen Text:",
87+
"readComplete": "Text gelesen",
88+
"reading": "Lesen...",
8489
"progressionFeedback": {
8590
"correct": "Richtig! Serie: {{streak}}/5",
8691
"incorrect": "Serie zurückgesetzt. Versuchen Sie es erneut!",
@@ -92,8 +97,7 @@
9297
"keepLearning": "Machen Sie weiter so!",
9398
"signInToTrack": "Melden Sie sich an, um Ihren Fortschritt zu verfolgen und Progression freizuschalten!",
9499
"signInCta": "Jetzt anmelden"
95-
},
96-
"startStreak": "Starten Sie Ihre Serie mit richtigen Antworten!"
100+
}
97101
},
98102
"languages": {
99103
"learning": {

public/locales/el/common.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@
8181
"hoverCreditsTooltip": "Περάστε το ποντίκι πάνω ή κάντε κλικ στις λέξεις για μετάφραση. Οι πιστώσεις είναι περιορισμένες.",
8282
"relevantText": "Σχετικό Κείμενο",
8383
"showExplanation": "Εμφάνιση Εξήγησης",
84+
"startStreak": "Ξεκινήστε τη σειρά σας απαντώντας σωστά!",
85+
"readingInstruction": "Διαβάστε προσεκτικά το κείμενο για να απαντήσετε στην παρακάτω ερώτηση",
86+
"questionPrompt": "Με βάση το παραπάνω κείμενο, απαντήστε σε αυτή την ερώτηση:",
87+
"readComplete": "Ανάγνωση ολοκληρώθηκε",
88+
"reading": "Ανάγνωση...",
8489
"progressionFeedback": {
8590
"correct": "Σωστό! Σειρά: {{streak}}/5",
8691
"incorrect": "Η σειρά επαναφέρθηκε. Δοκιμάστε ξανά!",
@@ -92,8 +97,7 @@
9297
"keepLearning": "Συνεχίστε την εξαιρετική δουλειά!",
9398
"signInToTrack": "Συνδεθείτε για να παρακολουθήσετε την πρόοδό σας και να ξεκλειδώσετε την πρόοδο!",
9499
"signInCta": "Συνδεθείτε τώρα"
95-
},
96-
"startStreak": "Ξεκινήστε τη σειρά σας απαντώντας σωστά!"
100+
}
97101
},
98102
"languages": {
99103
"learning": {

public/locales/en/common.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@
8282
"relevantText": "Relevant Text",
8383
"showExplanation": "Show Explanation",
8484
"hideExplanation": "Hide Explanation",
85+
"readingInstruction": "Read the passage carefully to answer the question below",
86+
"questionPrompt": "Based on the passage above, answer this question:",
87+
"readComplete": "Passage read",
88+
"reading": "Reading...",
8589
"progressionFeedback": {
8690
"correct": "Correct! Streak: {{streak}}/5",
8791
"incorrect": "Streak reset. Try again!",

public/locales/es/common.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@
8181
"hoverCreditsTooltip": "Pasa el cursor o haz clic en las palabras para traducirlas. Los créditos son limitados.",
8282
"relevantText": "Texto Relevante",
8383
"showExplanation": "Mostrar Explicación",
84+
"startStreak": "¡Comienza tu racha respondiendo correctamente!",
85+
"readingInstruction": "Lee el pasaje cuidadosamente para responder la pregunta a continuación",
86+
"questionPrompt": "Basándote en el pasaje anterior, responde esta pregunta:",
87+
"readComplete": "Pasaje leído",
88+
"reading": "Leyendo...",
8489
"progressionFeedback": {
8590
"correct": "¡Correcto! Racha: {{streak}}/5",
8691
"incorrect": "Racha reiniciada. ¡Inténtalo de nuevo!",
@@ -92,8 +97,7 @@
9297
"keepLearning": "¡Sigue con el excelente trabajo!",
9398
"signInToTrack": "¡Inicia sesión para rastrear tu progreso y desbloquear la progresión!",
9499
"signInCta": "Iniciar sesión ahora"
95-
},
96-
"startStreak": "¡Comienza tu racha respondiendo correctamente!"
100+
}
97101
},
98102
"languages": {
99103
"learning": {

public/locales/fil/common.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@
8181
"hoverCreditsTooltip": "Mag-hover o mag-click sa mga salita para sa pagsasalin. Limitado ang mga kredito.",
8282
"relevantText": "Kaugnay na Teksto",
8383
"showExplanation": "Ipakita ang Paliwanag",
84+
"startStreak": "Simulan ang serye mo sa pamamagitan ng pagsagot nang tama!",
85+
"readingInstruction": "Basahin nang mabuti ang talata upang masagot ang tanong sa ibaba",
86+
"questionPrompt": "Batay sa talata sa itaas, sagutin ang tanong na ito:",
87+
"readComplete": "Nabasa na ang talata",
88+
"reading": "Nagbabasa...",
8489
"progressionFeedback": {
8590
"correct": "Tama! Serye: {{streak}}/5",
8691
"incorrect": "Na-reset ang serye. Subukan ulit!",
@@ -92,8 +97,7 @@
9297
"keepLearning": "Magpatuloy sa mahusay na trabaho!",
9398
"signInToTrack": "Mag-sign in para i-track ang progress at i-unlock ang progression!",
9499
"signInCta": "Mag-sign in ngayon"
95-
},
96-
"startStreak": "Simulan ang serye mo sa pamamagitan ng pagsagot nang tama!"
100+
}
97101
},
98102
"languages": {
99103
"learning": {

0 commit comments

Comments
 (0)