Skip to content
Merged
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
4 changes: 4 additions & 0 deletions .Jules/palette.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@
**Learning:** Centralizing the visual and semantic treatment of required fields (e.g., a red asterisk `*` and `aria-required="true"`) in base components like `CustomTextInput` ensures a consistent and accessible experience across all forms without duplicating logic. Additionally, meticulous attention to Spanish orthography (e.g., 'Clínico' vs 'Clinico', 'Ocurrió' vs 'Ocurrio') significantly elevates the professional feel of the application for native speakers.
**Action:** Always provide standardized visual cues for mandatory fields in base components and maintain strict linguistic standards for localized interfaces.

## 2025-06-26 - [Flashcard Keyboard Shortcuts and Accessibility Hints]
**Learning:** Adding keyboard shortcuts (Space/Enter, 1-4) for high-repetition tasks like flashcard study significantly reduces friction. Including visual hints in brackets (e.g., `[Espacio]`, `[1]`) within button labels ensures discoverability without cluttering the UI. Crucially, when using `useCallback` in global listeners, the `useEffect` must be defined *after* the callbacks to avoid initialization `ReferenceError` due to lack of hoisting.
**Action:** Always implement keyboard shortcuts for study interfaces and provide clear visual and ARIA hints for discovery. Ensure correct hook ordering to avoid initialization errors.

## 2025-06-12 - [Maximizing Clickable Areas and Robust Input Grouping]
**Learning:** In collection-based form inputs (like exam questions), moving padding from the `<li>` to a full-width block `label` maximizes the interactive hit area, significantly improving the user experience on both desktop and touch devices. Furthermore, using indices (e.g., `questionIndex`) instead of text for input `name` and `id` attributes ensures robust grouping and accessibility even when multiple questions share identical text.
**Action:** Always wrap collection-item inputs in full-width block labels and use stable, unique indices for input grouping.
Expand Down
43 changes: 35 additions & 8 deletions src/v2/pages/V2FlashcardStudy.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -236,10 +236,10 @@ const Flashcard = ({ card, isFlipped, onFlip }) => (
// Quality rating buttons (SM-2 mapped to UI)
const QualityButtons = ({ onRate, disabled }) => {
const buttons = [
{ quality: 1, label: 'Otra vez', sublabel: '< 1 día', color: '#ba1a1a', icon: 'replay' },
{ quality: 3, label: 'Difícil', sublabel: '2-3 días', color: '#9c4247', icon: 'sentiment_dissatisfied' },
{ quality: 4, label: 'Bien', sublabel: '4-6 días', color: '#0fa397', icon: 'sentiment_satisfied' },
{ quality: 5, label: 'Fácil', sublabel: '7+ días', color: '#4a6360', icon: 'sentiment_very_satisfied' }
{ quality: 1, label: 'Otra vez', sublabel: '< 1 día', color: '#ba1a1a', icon: 'replay', hint: '1' },
{ quality: 3, label: 'Difícil', sublabel: '2-3 días', color: '#9c4247', icon: 'sentiment_dissatisfied', hint: '2' },
{ quality: 4, label: 'Bien', sublabel: '4-6 días', color: '#0fa397', icon: 'sentiment_satisfied', hint: '3' },
{ quality: 5, label: 'Fácil', sublabel: '7+ días', color: '#4a6360', icon: 'sentiment_very_satisfied', hint: '4' }
];

return (
Expand All @@ -255,13 +255,14 @@ const QualityButtons = ({ onRate, disabled }) => {
opacity: disabled ? 0.5 : 1,
border: 'none'
}}
aria-label={`Calificar como ${btn.label}`}
aria-label={`Atajo: ${btn.hint}. Calificar como ${btn.label}`}
title={`Atajo: ${btn.hint}`}
>
<i className='material-icons' style={{ fontSize: '28px', color: btn.color }} aria-hidden='true'>
{btn.icon}
</i>
<span className='v2-label-large v2-text-semibold' style={{ color: btn.color }}>
{btn.label}
{btn.label} <span className='v2-opacity-50'>[{btn.hint}]</span>
</span>
<span className='v2-label-small v2-opacity-70'>
{btn.sublabel}
Expand Down Expand Up @@ -348,7 +349,7 @@ const V2FlashcardStudy = () => {
useEffect(() => {
fetchDueFlashcards();
}, [fetchDueFlashcards]);

// Handle card flip
const handleFlip = useCallback(() => {
if (!isFlipped) {
Expand Down Expand Up @@ -386,6 +387,30 @@ const V2FlashcardStudy = () => {
setIsSubmitting(false);
}
}, [currentCard, currentIndex, flashcards.length, isSubmitting]);

// Keyboard shortcuts
useEffect(() => {
const handleKeyDown = (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;

if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
if (!isFlipped) {
handleFlip();
} else if (!isSubmitting) {
handleRate(4); // "Bien" is default
}
} else if (isFlipped && !isSubmitting) {
if (e.key === '1') handleRate(1);
if (e.key === '2') handleRate(3);
if (e.key === '3') handleRate(4);
if (e.key === '4') handleRate(5);
}
};

window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isFlipped, isSubmitting, handleFlip, handleRate]);

// Restart session
const handleRestartSession = useCallback(() => {
Expand Down Expand Up @@ -501,9 +526,11 @@ const V2FlashcardStudy = () => {
<button
className='v2-btn-tonal'
onClick={handleFlip}
aria-label='Atajo: Espacio. Mostrar Respuesta'
title='Atajo: Espacio'
>
<i className='material-icons' aria-hidden='true'>visibility</i>
Mostrar Respuesta
Mostrar Respuesta <span className='v2-opacity-50 v2-ml-8'>[Espacio]</span>
</button>
</div>
)}
Expand Down
62 changes: 62 additions & 0 deletions src/v2/pages/V2FlashcardStudy.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,4 +143,66 @@ describe('V2FlashcardStudy', () => {
expect(screen.getByText('¡Todo al día!')).toBeTruthy();
});
});

it('flips the card when pressing Space', async () => {
render(
<MemoryRouter>
<V2FlashcardStudy />
</MemoryRouter>
);

await waitFor(() => screen.getByText('¿Cuál es la tríada de Virchow?'));

fireEvent.keyDown(window, { key: ' ' });

await waitFor(() => {
expect(screen.getByText(/Estasis venosa/)).toBeTruthy();
});
});

it('rates the card when pressing numeric keys when flipped', async () => {
render(
<MemoryRouter>
<V2FlashcardStudy />
</MemoryRouter>
);

await waitFor(() => screen.getByText('¿Cuál es la tríada de Virchow?'));

// Flip first
fireEvent.keyDown(window, { key: 'Enter' });
await waitFor(() => screen.getByText(/Estasis venosa/));

// Rate with '4' (Fácil)
fireEvent.keyDown(window, { key: '4' });

await waitFor(() => {
expect(screen.getByText('Agente causal más común de epiglotitis')).toBeTruthy();
});

expect(FlashcardService.reviewFlashcard).toHaveBeenCalledWith(1, 5); // 4 maps to quality 5
});

it('rates the card as "Bien" when pressing Space when flipped', async () => {
render(
<MemoryRouter>
<V2FlashcardStudy />
</MemoryRouter>
);

await waitFor(() => screen.getByText('¿Cuál es la tríada de Virchow?'));

// Flip first
fireEvent.keyDown(window, { key: ' ' });
await waitFor(() => screen.getByText(/Estasis venosa/));

// Rate with Space
fireEvent.keyDown(window, { key: ' ' });

await waitFor(() => {
expect(screen.getByText('Agente causal más común de epiglotitis')).toBeTruthy();
});

expect(FlashcardService.reviewFlashcard).toHaveBeenCalledWith(1, 4); // Space maps to quality 4
});
});
Loading