fix: vocab worksheet — wider table, show original pages, better layout
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 24s
CI / test-go-edu-search (push) Successful in 24s
CI / test-python-klausur (push) Failing after 1m44s
CI / test-python-agent-core (push) Successful in 15s
CI / test-nodejs-website (push) Successful in 17s

- Swap from 3/5-2/5 grid to 1/3-2/3 flexbox (original left, table right)
- Table uses 3 equal 1fr columns for EN/DE/example instead of cramped 13-col grid
- Full viewport height minus header (calc(100vh - 240px)) for more visible rows
- Show only processed pages in original preview (filtered by selectedPages)
- Remove per-row insert buttons to reduce vertical noise
- Compact row spacing (py-1.5) to fit ~15+ rows without scrolling

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-05 16:07:25 +01:00
parent b7ae36e92b
commit d5be7b6f77

View File

@@ -1417,10 +1417,55 @@ export default function VocabWorksheetPage() {
{/* Vocabulary Tab */}
{session && activeTab === 'vocabulary' && (
<div className="grid grid-cols-1 lg:grid-cols-5 gap-6">
{/* Left: Vocabulary List (3/5) */}
<div className={`${glassCard} rounded-2xl p-6 lg:col-span-3`}>
<div className="flex items-center justify-between mb-4">
<div className="flex flex-col lg:flex-row gap-4" style={{ height: 'calc(100vh - 240px)', minHeight: '500px' }}>
{/* Left: Original pages (scrollable, 1/3 width) */}
<div className={`${glassCard} rounded-2xl p-4 lg:w-1/3 flex flex-col overflow-hidden`}>
<h2 className={`text-sm font-semibold mb-3 flex-shrink-0 ${isDark ? 'text-white/70' : 'text-slate-600'}`}>
Original ({(() => { const pp = selectedPages.length > 0 ? selectedPages : [...new Set(vocabulary.map(v => (v.source_page || 1) - 1))]; return pp.length; })()} Seiten)
</h2>
<div className="flex-1 overflow-y-auto space-y-3">
{(() => {
const processedPageIndices = selectedPages.length > 0
? selectedPages
: [...new Set(vocabulary.map(v => (v.source_page || 1) - 1))].sort((a, b) => a - b)
const thumbsToShow = processedPageIndices
.filter(idx => idx >= 0 && idx < pagesThumbnails.length)
.map(idx => ({ idx, src: pagesThumbnails[idx] }))
if (thumbsToShow.length > 0) {
return thumbsToShow.map(({ idx, src }) => (
<div key={idx} className={`relative rounded-xl overflow-hidden border ${isDark ? 'border-white/10' : 'border-black/10'}`}>
<div className={`absolute top-2 left-2 px-2 py-0.5 rounded-lg text-xs font-medium ${isDark ? 'bg-black/60 text-white' : 'bg-white/90 text-slate-700'}`}>
S. {idx + 1}
</div>
<img src={src} alt={`Seite ${idx + 1}`} className="w-full h-auto" />
</div>
))
}
if (uploadedImage) {
return (
<div className={`relative rounded-xl overflow-hidden border ${isDark ? 'border-white/10' : 'border-black/10'}`}>
<img src={uploadedImage} alt="Arbeitsblatt" className="w-full h-auto" />
</div>
)
}
return (
<div className={`flex-1 flex items-center justify-center py-12 ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
<div className="text-center">
<svg className="w-12 h-12 mx-auto mb-2 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<p className="text-xs">Kein Bild verfuegbar</p>
</div>
</div>
)
})()}
</div>
</div>
{/* Right: Vocabulary table (2/3 width) */}
<div className={`${glassCard} rounded-2xl p-4 lg:w-2/3 flex flex-col overflow-hidden`}>
<div className="flex items-center justify-between mb-3 flex-shrink-0">
<h2 className={`text-lg font-semibold ${isDark ? 'text-white' : 'text-slate-900'}`}>
Vokabeln ({vocabulary.length})
</h2>
@@ -1436,9 +1481,9 @@ export default function VocabWorksheetPage() {
{/* Error messages for failed pages */}
{processingErrors.length > 0 && (
<div className={`rounded-xl p-4 mb-4 ${isDark ? 'bg-orange-500/20 text-orange-200 border border-orange-500/30' : 'bg-orange-100 text-orange-700 border border-orange-200'}`}>
<div className="font-medium mb-2">Einige Seiten konnten nicht verarbeitet werden:</div>
<ul className="text-sm space-y-1">
<div className={`rounded-xl p-3 mb-3 flex-shrink-0 ${isDark ? 'bg-orange-500/20 text-orange-200 border border-orange-500/30' : 'bg-orange-100 text-orange-700 border border-orange-200'}`}>
<div className="font-medium mb-1 text-sm">Einige Seiten konnten nicht verarbeitet werden:</div>
<ul className="text-xs space-y-0.5">
{processingErrors.map((err, idx) => (
<li key={idx}> {err}</li>
))}
@@ -1448,12 +1493,12 @@ export default function VocabWorksheetPage() {
{/* Processing Progress */}
{currentlyProcessingPage && (
<div className={`rounded-xl p-4 mb-4 ${isDark ? 'bg-purple-500/20 border border-purple-500/30' : 'bg-purple-100 border border-purple-200'}`}>
<div className={`rounded-xl p-3 mb-3 flex-shrink-0 ${isDark ? 'bg-purple-500/20 border border-purple-500/30' : 'bg-purple-100 border border-purple-200'}`}>
<div className="flex items-center gap-3">
<div className={`w-5 h-5 border-2 ${isDark ? 'border-purple-300' : 'border-purple-600'} border-t-transparent rounded-full animate-spin`} />
<div className={`w-4 h-4 border-2 ${isDark ? 'border-purple-300' : 'border-purple-600'} border-t-transparent rounded-full animate-spin`} />
<div>
<div className={`font-medium ${isDark ? 'text-purple-200' : 'text-purple-700'}`}>Verarbeite Seite {currentlyProcessingPage}...</div>
<div className={`text-sm ${isDark ? 'text-purple-300/70' : 'text-purple-600'}`}>
<div className={`text-sm font-medium ${isDark ? 'text-purple-200' : 'text-purple-700'}`}>Verarbeite Seite {currentlyProcessingPage}...</div>
<div className={`text-xs ${isDark ? 'text-purple-300/70' : 'text-purple-600'}`}>
{successfulPages.length > 0 && `${successfulPages.length} Seite(n) fertig • `}
{vocabulary.length} Vokabeln bisher
</div>
@@ -1464,14 +1509,14 @@ export default function VocabWorksheetPage() {
{/* Success info */}
{!currentlyProcessingPage && successfulPages.length > 0 && failedPages.length === 0 && (
<div className={`rounded-xl p-3 mb-4 text-sm ${isDark ? 'bg-green-500/20 text-green-200 border border-green-500/30' : 'bg-green-100 text-green-700 border border-green-200'}`}>
<div className={`rounded-xl p-2 mb-3 text-xs flex-shrink-0 ${isDark ? 'bg-green-500/20 text-green-200 border border-green-500/30' : 'bg-green-100 text-green-700 border border-green-200'}`}>
Alle {successfulPages.length} Seite(n) erfolgreich verarbeitet - {vocabulary.length} Vokabeln insgesamt
</div>
)}
{/* Partial success info */}
{!currentlyProcessingPage && successfulPages.length > 0 && failedPages.length > 0 && (
<div className={`rounded-xl p-3 mb-4 text-sm ${isDark ? 'bg-yellow-500/20 text-yellow-200 border border-yellow-500/30' : 'bg-yellow-100 text-yellow-700 border border-yellow-200'}`}>
<div className={`rounded-xl p-2 mb-3 text-xs flex-shrink-0 ${isDark ? 'bg-yellow-500/20 text-yellow-200 border border-yellow-500/30' : 'bg-yellow-100 text-yellow-700 border border-yellow-200'}`}>
{successfulPages.length} Seite(n) erfolgreich, {failedPages.length} fehlgeschlagen - {vocabulary.length} Vokabeln extrahiert
</div>
)}
@@ -1479,10 +1524,10 @@ export default function VocabWorksheetPage() {
{vocabulary.length === 0 ? (
<p className={`text-center py-8 ${isDark ? 'text-white/60' : 'text-slate-500'}`}>Keine Vokabeln gefunden.</p>
) : (
<div className="flex flex-col" style={{ height: 'calc(100vh - 400px)', minHeight: '300px' }}>
<div className="flex flex-col flex-1 overflow-hidden">
{/* Fixed Header */}
<div className={`grid grid-cols-13 gap-2 px-3 py-2 text-sm font-medium border-b ${isDark ? 'border-white/10 text-white/60' : 'border-black/10 text-slate-500'}`} style={{ gridTemplateColumns: 'auto repeat(12, minmax(0, 1fr))' }}>
<div className="flex items-center justify-center w-6">
<div className={`flex-shrink-0 grid gap-2 px-3 py-2 text-sm font-medium border-b ${isDark ? 'border-white/10 text-white/60' : 'border-black/10 text-slate-500'}`} style={{ gridTemplateColumns: '32px 36px 1fr 1fr 1fr 32px' }}>
<div className="flex items-center justify-center">
<input
type="checkbox"
checked={vocabulary.length > 0 && vocabulary.every(v => v.selected)}
@@ -1491,37 +1536,20 @@ export default function VocabWorksheetPage() {
title="Alle auswählen"
/>
</div>
<div className="col-span-1">S.</div>
<div className="col-span-3">Englisch</div>
<div className="col-span-4">Deutsch</div>
<div className="col-span-3">Beispiel</div>
<div className="col-span-1"></div>
<div>S.</div>
<div>Englisch</div>
<div>Deutsch</div>
<div>Beispiel</div>
<div></div>
</div>
{/* Scrollable Content */}
<div className="flex-1 overflow-y-auto py-2">
{/* Insert button at the beginning */}
<div className="flex justify-center py-1 group">
<button
onClick={() => addVocabularyEntry(0)}
className={`px-3 py-0.5 rounded-full text-xs flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity ${
isDark
? 'bg-purple-500/20 text-purple-300 hover:bg-purple-500/30'
: 'bg-purple-100 text-purple-600 hover:bg-purple-200'
}`}
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Zeile einfügen
</button>
</div>
<div className="flex-1 overflow-y-auto py-1">
{vocabulary.map((entry, index) => (
<React.Fragment key={entry.id}>
{/* Vocabulary row */}
<div className={`grid gap-2 px-3 py-2 rounded-xl ${isDark ? 'bg-white/5' : 'bg-black/5'}`} style={{ gridTemplateColumns: 'auto repeat(12, minmax(0, 1fr))' }}>
<div className="flex items-center justify-center w-6">
<div className={`grid gap-2 px-3 py-1.5 rounded-lg mb-0.5 ${isDark ? 'hover:bg-white/5' : 'hover:bg-black/5'}`} style={{ gridTemplateColumns: '32px 36px 1fr 1fr 1fr 32px' }}>
<div className="flex items-center justify-center">
<input
type="checkbox"
checked={entry.selected || false}
@@ -1529,126 +1557,65 @@ export default function VocabWorksheetPage() {
className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500 cursor-pointer"
/>
</div>
<div className={`col-span-1 flex items-center justify-center text-xs font-medium rounded ${isDark ? 'bg-white/10 text-white/60' : 'bg-black/10 text-slate-600'}`}>
<div className={`flex items-center justify-center text-xs font-medium rounded ${isDark ? 'bg-white/10 text-white/60' : 'bg-black/10 text-slate-600'}`}>
{entry.source_page || '-'}
</div>
<input
type="text"
value={entry.english}
onChange={(e) => updateVocabularyEntry(entry.id, 'english', e.target.value)}
className={`col-span-3 px-2 py-1 rounded-lg border ${glassInput} focus:outline-none focus:ring-1 focus:ring-purple-500`}
className={`px-2 py-1 rounded-lg border text-sm ${glassInput} focus:outline-none focus:ring-1 focus:ring-purple-500`}
/>
<input
type="text"
value={entry.german}
onChange={(e) => updateVocabularyEntry(entry.id, 'german', e.target.value)}
className={`col-span-4 px-2 py-1 rounded-lg border ${glassInput} focus:outline-none focus:ring-1 focus:ring-purple-500`}
className={`px-2 py-1 rounded-lg border text-sm ${glassInput} focus:outline-none focus:ring-1 focus:ring-purple-500`}
/>
<input
type="text"
value={entry.example_sentence || ''}
onChange={(e) => updateVocabularyEntry(entry.id, 'example_sentence', e.target.value)}
placeholder="Beispiel"
className={`col-span-3 px-2 py-1 rounded-lg border text-sm ${glassInput} focus:outline-none focus:ring-1 focus:ring-purple-500`}
className={`px-2 py-1 rounded-lg border text-sm ${glassInput} focus:outline-none focus:ring-1 focus:ring-purple-500`}
/>
<button onClick={() => deleteVocabularyEntry(entry.id)} className={`col-span-1 p-1 rounded-lg ${isDark ? 'hover:bg-red-500/20 text-red-400' : 'hover:bg-red-100 text-red-500'}`}>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<button onClick={() => deleteVocabularyEntry(entry.id)} className={`p-1 rounded-lg ${isDark ? 'hover:bg-red-500/20 text-red-400' : 'hover:bg-red-100 text-red-500'}`}>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
{/* Insert button after each row */}
<div className="flex justify-center py-1 group">
<button
onClick={() => addVocabularyEntry(index + 1)}
className={`px-3 py-0.5 rounded-full text-xs flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity ${
isDark
? 'bg-purple-500/20 text-purple-300 hover:bg-purple-500/30'
: 'bg-purple-100 text-purple-600 hover:bg-purple-200'
}`}
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Zeile einfügen
</button>
</div>
</React.Fragment>
))}
</div>
{/* Add new row button at the end */}
<button
onClick={() => addVocabularyEntry()}
className={`w-full py-2 mt-2 rounded-xl border-2 border-dashed flex items-center justify-center gap-2 transition-colors ${
isDark
? 'border-white/20 text-white/60 hover:border-purple-400 hover:text-purple-400 hover:bg-purple-500/10'
: 'border-black/20 text-slate-500 hover:border-purple-500 hover:text-purple-500 hover:bg-purple-50'
}`}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Neue Zeile hinzufügen
</button>
{/* Footer with scroll hint */}
<div className={`pt-2 border-t text-center text-sm ${isDark ? 'border-white/10 text-white/50' : 'border-black/10 text-slate-400'}`}>
{vocabulary.length} Vokabeln insgesamt
{vocabulary.filter(v => v.selected).length > 0 && ` (${vocabulary.filter(v => v.selected).length} ausgewählt)`}
{(() => {
const pages = [...new Set(vocabulary.map(v => v.source_page).filter(Boolean))].sort((a, b) => (a || 0) - (b || 0))
return pages.length > 1 ? ` • Seiten: ${pages.join(', ')}` : ''
})()}
{/* Footer */}
<div className={`flex-shrink-0 pt-2 border-t flex items-center justify-between text-xs ${isDark ? 'border-white/10 text-white/50' : 'border-black/10 text-slate-400'}`}>
<span>
{vocabulary.length} Vokabeln
{vocabulary.filter(v => v.selected).length > 0 && ` (${vocabulary.filter(v => v.selected).length} ausgewaehlt)`}
{(() => {
const pages = [...new Set(vocabulary.map(v => v.source_page).filter(Boolean))].sort((a, b) => (a || 0) - (b || 0))
return pages.length > 1 ? ` • Seiten: ${pages.join(', ')}` : ''
})()}
</span>
<button
onClick={() => addVocabularyEntry()}
className={`px-3 py-1 rounded-lg text-xs flex items-center gap-1 transition-colors ${
isDark
? 'bg-white/10 hover:bg-white/20 text-white/70'
: 'bg-slate-100 hover:bg-slate-200 text-slate-600'
}`}
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Zeile
</button>
</div>
</div>
)}
</div>
{/* Right: Original Worksheet Preview (2/5) */}
<div className={`${glassCard} rounded-2xl p-6 lg:col-span-2`}>
<h2 className={`text-lg font-semibold mb-4 ${isDark ? 'text-white' : 'text-slate-900'}`}>
Original-Arbeitsblatt
</h2>
<div className="flex flex-col" style={{ height: 'calc(100vh - 400px)', minHeight: '300px' }}>
{pagesThumbnails.length > 0 ? (
<div className="flex-1 overflow-y-auto space-y-4">
{pagesThumbnails.map((thumb, idx) => (
<div key={idx} className={`relative rounded-xl overflow-hidden border ${isDark ? 'border-white/10' : 'border-black/10'}`}>
<div className={`absolute top-2 left-2 px-2 py-1 rounded-lg text-xs font-medium ${isDark ? 'bg-black/50 text-white' : 'bg-white/90 text-slate-700'}`}>
Seite {idx + 1}
</div>
<img
src={thumb}
alt={`Seite ${idx + 1}`}
className="w-full h-auto"
/>
</div>
))}
</div>
) : uploadedImage ? (
<div className="flex-1 overflow-y-auto">
<div className={`relative rounded-xl overflow-hidden border ${isDark ? 'border-white/10' : 'border-black/10'}`}>
<img
src={uploadedImage}
alt="Hochgeladenes Arbeitsblatt"
className="w-full h-auto"
/>
</div>
</div>
) : (
<div className={`flex-1 flex items-center justify-center ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
<div className="text-center">
<svg className="w-16 h-16 mx-auto mb-3 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<p className="text-sm">Kein Bild verfügbar</p>
</div>
</div>
)}
</div>
</div>
</div>
)}