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
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:
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user