Curator: Lokalisierung und einstellbare Paginierung
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
interface Genre {
|
||||
id: number;
|
||||
@@ -56,6 +57,7 @@ function getCuratorUploadHeaders() {
|
||||
}
|
||||
|
||||
export default function CuratorPage() {
|
||||
const t = useTranslations('Curator');
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
@@ -93,7 +95,7 @@ export default function CuratorPage() {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedFilter, setSelectedFilter] = useState<string>('');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const itemsPerPage = 10;
|
||||
const [itemsPerPage, setItemsPerPage] = useState(10);
|
||||
const [playingSongId, setPlayingSongId] = useState<number | null>(null);
|
||||
const [audioElement, setAudioElement] = useState<HTMLAudioElement | null>(null);
|
||||
|
||||
@@ -125,7 +127,7 @@ export default function CuratorPage() {
|
||||
setCuratorInfo(data);
|
||||
localStorage.setItem('hoerdle_curator_is_global', String(data.isGlobalCurator));
|
||||
} else {
|
||||
setMessage('Fehler beim Laden der Kuratoren-Informationen.');
|
||||
setMessage(t('loadCuratorError'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -137,7 +139,7 @@ export default function CuratorPage() {
|
||||
const data: Song[] = await res.json();
|
||||
setSongs(data);
|
||||
} else {
|
||||
setMessage('Fehler beim Laden der Songs.');
|
||||
setMessage(t('loadSongsError'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -176,10 +178,10 @@ export default function CuratorPage() {
|
||||
await bootstrapCuratorData();
|
||||
} else {
|
||||
const err = await res.json().catch(() => null);
|
||||
setMessage(err?.error || 'Login fehlgeschlagen.');
|
||||
setMessage(err?.error || t('loginFailed'));
|
||||
}
|
||||
} catch (e) {
|
||||
setMessage('Netzwerkfehler beim Login.');
|
||||
setMessage(t('loginNetworkError'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -240,13 +242,13 @@ export default function CuratorPage() {
|
||||
if (res.ok) {
|
||||
setEditingId(null);
|
||||
await fetchSongs();
|
||||
setMessage('Song erfolgreich aktualisiert.');
|
||||
setMessage(t('songUpdated'));
|
||||
} else {
|
||||
const errText = await res.text();
|
||||
setMessage(`Fehler beim Speichern: ${errText}`);
|
||||
setMessage(t('saveError', { error: errText }));
|
||||
}
|
||||
} catch (e) {
|
||||
setMessage('Netzwerkfehler beim Speichern.');
|
||||
setMessage(t('saveNetworkError'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -274,10 +276,10 @@ export default function CuratorPage() {
|
||||
|
||||
const handleDelete = async (song: Song) => {
|
||||
if (!canDeleteSong(song)) {
|
||||
setMessage('Du darfst diesen Song nicht löschen.');
|
||||
setMessage(t('noDeletePermission'));
|
||||
return;
|
||||
}
|
||||
if (!confirm(`Möchtest du "${song.title}" wirklich löschen?`)) return;
|
||||
if (!confirm(t('deleteConfirm', { title: song.title }))) return;
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/songs', {
|
||||
@@ -287,13 +289,13 @@ export default function CuratorPage() {
|
||||
});
|
||||
if (res.ok) {
|
||||
await fetchSongs();
|
||||
setMessage('Song gelöscht.');
|
||||
setMessage(t('songDeleted'));
|
||||
} else {
|
||||
const errText = await res.text();
|
||||
setMessage(`Fehler beim Löschen: ${errText}`);
|
||||
setMessage(t('deleteError', { error: errText }));
|
||||
}
|
||||
} catch (e) {
|
||||
setMessage('Netzwerkfehler beim Löschen.');
|
||||
setMessage(t('deleteNetworkError'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -317,7 +319,7 @@ export default function CuratorPage() {
|
||||
audio.onerror = () => {
|
||||
setPlayingSongId(null);
|
||||
setAudioElement(null);
|
||||
alert(`Audio-Datei konnte nicht geladen werden: ${song.filename}`);
|
||||
alert(`Audio file could not be loaded: ${song.filename}`);
|
||||
};
|
||||
|
||||
audio.play()
|
||||
@@ -470,19 +472,19 @@ export default function CuratorPage() {
|
||||
const duplicateCount = results.filter(r => r.isDuplicate).length;
|
||||
const failedCount = results.filter(r => !r.success && !r.isDuplicate).length;
|
||||
|
||||
let msg = `✅ ${successCount}/${results.length} Uploads erfolgreich.`;
|
||||
if (duplicateCount > 0) msg += `\n⚠️ ${duplicateCount} Duplikat(e) übersprungen.`;
|
||||
if (failedCount > 0) msg += `\n❌ ${failedCount} fehlgeschlagen.`;
|
||||
let msg = t('uploadSummary', { success: successCount, total: results.length });
|
||||
if (duplicateCount > 0) msg += `\n` + t('uploadSummaryDuplicates', { count: duplicateCount });
|
||||
if (failedCount > 0) msg += `\n` + t('uploadSummaryFailed', { count: failedCount });
|
||||
setMessage(msg);
|
||||
};
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
<main style={{ maxWidth: '480px', margin: '2rem auto', padding: '1rem' }}>
|
||||
<h1 style={{ fontSize: '1.5rem', marginBottom: '1rem' }}>Kuratoren-Login</h1>
|
||||
<h1 style={{ fontSize: '1.5rem', marginBottom: '1rem' }}>{t('loginTitle')}</h1>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
|
||||
<label>
|
||||
Benutzername
|
||||
{t('loginUsername')}
|
||||
<input
|
||||
type="text"
|
||||
value={username}
|
||||
@@ -491,7 +493,7 @@ export default function CuratorPage() {
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Passwort
|
||||
{t('loginPassword')}
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
@@ -512,7 +514,7 @@ export default function CuratorPage() {
|
||||
marginTop: '0.5rem',
|
||||
}}
|
||||
>
|
||||
Einloggen
|
||||
{t('loginButton')}
|
||||
</button>
|
||||
{message && (
|
||||
<p style={{ color: '#b91c1c', marginTop: '0.5rem', whiteSpace: 'pre-line' }}>{message}</p>
|
||||
@@ -599,8 +601,8 @@ export default function CuratorPage() {
|
||||
<h1 style={{ fontSize: '1.75rem', marginBottom: '0.25rem' }}>Kuratoren-Dashboard</h1>
|
||||
{curatorInfo && (
|
||||
<p style={{ color: '#4b5563', fontSize: '0.9rem' }}>
|
||||
Eingeloggt als <strong>{curatorInfo.username}</strong>
|
||||
{curatorInfo.isGlobalCurator && ' (Globaler Kurator)'}
|
||||
{t('loggedInAs', { username: curatorInfo.username })}
|
||||
{curatorInfo.isGlobalCurator && t('globalCuratorSuffix')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -616,21 +618,19 @@ export default function CuratorPage() {
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
Abmelden
|
||||
{t('logout')}
|
||||
</button>
|
||||
</header>
|
||||
|
||||
{loading && <p>Lade Daten...</p>}
|
||||
{loading && <p>{t('loadingData')}</p>}
|
||||
{message && (
|
||||
<p style={{ marginBottom: '1rem', color: '#b91c1c', whiteSpace: 'pre-line' }}>{message}</p>
|
||||
)}
|
||||
|
||||
<section style={{ marginBottom: '2rem' }}>
|
||||
<h2 style={{ fontSize: '1.25rem', marginBottom: '0.75rem' }}>Titel hochladen</h2>
|
||||
<h2 style={{ fontSize: '1.25rem', marginBottom: '0.75rem' }}>{t('uploadSectionTitle')}</h2>
|
||||
<p style={{ marginBottom: '0.75rem', color: '#4b5563', fontSize: '0.9rem' }}>
|
||||
Ziehe eine oder mehrere MP3-Dateien hierher oder wähle sie aus. Die Titel werden automatisch analysiert
|
||||
(inkl. Erkennung des Erscheinungsjahres) und von der globalen Playlist ausgeschlossen. Wähle mindestens
|
||||
eines deiner Genres aus, um die Titel zuzuordnen.
|
||||
{t('uploadSectionDescription')}
|
||||
</p>
|
||||
<form onSubmit={handleBatchUpload} style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem', maxWidth: '640px' }}>
|
||||
<div
|
||||
@@ -651,9 +651,11 @@ export default function CuratorPage() {
|
||||
>
|
||||
<div style={{ fontSize: '2.5rem', marginBottom: '0.5rem' }}>📁</div>
|
||||
<p style={{ fontWeight: 'bold', marginBottom: '0.25rem' }}>
|
||||
{files.length > 0 ? `${files.length} Datei(en) ausgewählt` : 'MP3-Dateien hierher ziehen'}
|
||||
{files.length > 0
|
||||
? t('dropzoneTitleWithFiles', { count: files.length })
|
||||
: t('dropzoneTitleEmpty')}
|
||||
</p>
|
||||
<p style={{ fontSize: '0.875rem', color: '#666' }}>oder klicken, um Dateien auszuwählen</p>
|
||||
<p style={{ fontSize: '0.875rem', color: '#666' }}>{t('dropzoneSubtitle')}</p>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
@@ -666,7 +668,7 @@ export default function CuratorPage() {
|
||||
|
||||
{files.length > 0 && (
|
||||
<div style={{ marginBottom: '0.5rem' }}>
|
||||
<p style={{ fontWeight: 'bold', marginBottom: '0.25rem' }}>Ausgewählte Dateien:</p>
|
||||
<p style={{ fontWeight: 'bold', marginBottom: '0.25rem' }}>{t('selectedFilesTitle')}</p>
|
||||
<div
|
||||
style={{
|
||||
maxHeight: '160px',
|
||||
@@ -696,7 +698,10 @@ export default function CuratorPage() {
|
||||
}}
|
||||
>
|
||||
<p style={{ fontWeight: 'bold', marginBottom: '0.25rem' }}>
|
||||
Upload: {uploadProgress.current} / {uploadProgress.total}
|
||||
{t('uploadProgress', {
|
||||
current: uploadProgress.current,
|
||||
total: uploadProgress.total,
|
||||
})}
|
||||
</p>
|
||||
<div
|
||||
style={{
|
||||
@@ -723,7 +728,7 @@ export default function CuratorPage() {
|
||||
)}
|
||||
|
||||
<div>
|
||||
<div style={{ fontWeight: 500, marginBottom: '0.25rem' }}>Genres zuordnen</div>
|
||||
<div style={{ fontWeight: 500, marginBottom: '0.25rem' }}>{t('assignGenresLabel')}</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.25rem' }}>
|
||||
{genres
|
||||
.filter(g => curatorInfo?.genreIds.includes(g.id))
|
||||
@@ -751,7 +756,7 @@ export default function CuratorPage() {
|
||||
))}
|
||||
{curatorInfo && curatorInfo.genreIds.length === 0 && (
|
||||
<span style={{ fontSize: '0.8rem', color: '#9ca3af' }}>
|
||||
Dir sind noch keine Genres zugeordnet. Bitte wende dich an den Admin.
|
||||
{t('noAssignedGenres')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -770,7 +775,7 @@ export default function CuratorPage() {
|
||||
alignSelf: 'flex-start',
|
||||
}}
|
||||
>
|
||||
{isUploading ? 'Lade hoch...' : 'Upload starten'}
|
||||
{isUploading ? t('uploadButtonUploading') : t('uploadButtonIdle')}
|
||||
</button>
|
||||
|
||||
{uploadResults.length > 0 && (
|
||||
@@ -784,14 +789,14 @@ export default function CuratorPage() {
|
||||
}}
|
||||
>
|
||||
{uploadResults.map((r, idx) => (
|
||||
<div key={idx} style={{ marginBottom: '0.25rem' }}>
|
||||
<strong>{r.filename}</strong> –{' '}
|
||||
{r.success
|
||||
? '✅ erfolgreich'
|
||||
: r.isDuplicate
|
||||
? `⚠️ Duplikat: ${r.error}`
|
||||
: `❌ Fehler: ${r.error}`}
|
||||
</div>
|
||||
<div key={idx} style={{ marginBottom: '0.25rem' }}>
|
||||
<strong>{r.filename}</strong> –{' '}
|
||||
{r.success
|
||||
? t('uploadResultSuccess')
|
||||
: r.isDuplicate
|
||||
? t('uploadResultDuplicate', { error: r.error })
|
||||
: t('uploadResultError', { error: r.error })}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -800,19 +805,17 @@ export default function CuratorPage() {
|
||||
|
||||
<section style={{ marginBottom: '2rem' }}>
|
||||
<h2 style={{ fontSize: '1.25rem', marginBottom: '0.75rem' }}>
|
||||
Titel in deinen Genres & Specials ({filteredSongs.length} Titel)
|
||||
{t('tracklistTitle', { count: filteredSongs.length })}
|
||||
</h2>
|
||||
<p style={{ marginBottom: '0.75rem', color: '#4b5563', fontSize: '0.9rem' }}>
|
||||
Du kannst Songs bearbeiten, die mindestens einem deiner Genres oder Specials zugeordnet sind.
|
||||
Löschen ist nur erlaubt, wenn ein Song ausschließlich deinen Genres/Specials zugeordnet ist.
|
||||
Genres, Specials, News und politische Statements können nur vom Admin verwaltet werden.
|
||||
{t('tracklistDescription')}
|
||||
</p>
|
||||
|
||||
{/* Suche & Filter */}
|
||||
<div style={{ marginBottom: '0.75rem', display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Nach Titel oder Artist suchen..."
|
||||
placeholder={t('searchPlaceholder')}
|
||||
value={searchQuery}
|
||||
onChange={e => {
|
||||
setSearchQuery(e.target.value);
|
||||
@@ -839,8 +842,8 @@ export default function CuratorPage() {
|
||||
border: '1px solid #d1d5db',
|
||||
}}
|
||||
>
|
||||
<option value="">Alle Inhalte</option>
|
||||
<option value="no-global">🚫 Ohne Global</option>
|
||||
<option value="">{t('filterAll')}</option>
|
||||
<option value="no-global">{t('filterNoGlobal')}</option>
|
||||
<optgroup label="Genres">
|
||||
{genres
|
||||
.filter(g => curatorInfo?.genreIds.includes(g.id))
|
||||
@@ -882,13 +885,13 @@ export default function CuratorPage() {
|
||||
fontSize: '0.85rem',
|
||||
}}
|
||||
>
|
||||
Filter zurücksetzen
|
||||
{t('filterReset')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{visibleSongs.length === 0 ? (
|
||||
<p>Keine passenden Songs in deinen Genres/Specials gefunden.</p>
|
||||
<p>{t('noSongsInScope')}</p>
|
||||
) : (
|
||||
<>
|
||||
<div style={{ overflowX: 'auto' }}>
|
||||
@@ -905,48 +908,48 @@ export default function CuratorPage() {
|
||||
style={{ padding: '0.5rem', cursor: 'pointer' }}
|
||||
onClick={() => handleSort('id')}
|
||||
>
|
||||
ID {sortField === 'id' && (sortDirection === 'asc' ? '↑' : '↓')}
|
||||
{t('columnId')} {sortField === 'id' && (sortDirection === 'asc' ? '↑' : '↓')}
|
||||
</th>
|
||||
<th style={{ padding: '0.5rem' }}>Play</th>
|
||||
<th style={{ padding: '0.5rem' }}>{t('columnPlay')}</th>
|
||||
<th
|
||||
style={{ padding: '0.5rem', cursor: 'pointer' }}
|
||||
onClick={() => handleSort('title')}
|
||||
>
|
||||
Titel {sortField === 'title' && (sortDirection === 'asc' ? '↑' : '↓')}
|
||||
{t('columnTitle')} {sortField === 'title' && (sortDirection === 'asc' ? '↑' : '↓')}
|
||||
</th>
|
||||
<th
|
||||
style={{ padding: '0.5rem', cursor: 'pointer' }}
|
||||
onClick={() => handleSort('artist')}
|
||||
>
|
||||
Artist {sortField === 'artist' && (sortDirection === 'asc' ? '↑' : '↓')}
|
||||
{t('columnArtist')} {sortField === 'artist' && (sortDirection === 'asc' ? '↑' : '↓')}
|
||||
</th>
|
||||
<th
|
||||
style={{ padding: '0.5rem', cursor: 'pointer' }}
|
||||
onClick={() => handleSort('releaseYear')}
|
||||
>
|
||||
Jahr {sortField === 'releaseYear' && (sortDirection === 'asc' ? '↑' : '↓')}
|
||||
{t('columnYear')} {sortField === 'releaseYear' && (sortDirection === 'asc' ? '↑' : '↓')}
|
||||
</th>
|
||||
<th style={{ padding: '0.5rem' }}>Genres / Specials</th>
|
||||
<th style={{ padding: '0.5rem' }}>{t('columnGenresSpecials')}</th>
|
||||
<th
|
||||
style={{ padding: '0.5rem', cursor: 'pointer' }}
|
||||
onClick={() => handleSort('createdAt')}
|
||||
>
|
||||
Hinzugefügt {sortField === 'createdAt' && (sortDirection === 'asc' ? '↑' : '↓')}
|
||||
{t('columnAdded')} {sortField === 'createdAt' && (sortDirection === 'asc' ? '↑' : '↓')}
|
||||
</th>
|
||||
<th
|
||||
style={{ padding: '0.5rem', cursor: 'pointer' }}
|
||||
onClick={() => handleSort('activations')}
|
||||
>
|
||||
Aktivierungen {sortField === 'activations' && (sortDirection === 'asc' ? '↑' : '↓')}
|
||||
{t('columnActivations')} {sortField === 'activations' && (sortDirection === 'asc' ? '↑' : '↓')}
|
||||
</th>
|
||||
<th
|
||||
style={{ padding: '0.5rem', cursor: 'pointer' }}
|
||||
onClick={() => handleSort('averageRating')}
|
||||
>
|
||||
Rating {sortField === 'averageRating' && (sortDirection === 'asc' ? '↑' : '↓')}
|
||||
{t('columnRating')} {sortField === 'averageRating' && (sortDirection === 'asc' ? '↑' : '↓')}
|
||||
</th>
|
||||
<th style={{ padding: '0.5rem' }}>Exclude Global</th>
|
||||
<th style={{ padding: '0.5rem' }}>Aktionen</th>
|
||||
<th style={{ padding: '0.5rem' }}>{t('columnExcludeGlobal')}</th>
|
||||
<th style={{ padding: '0.5rem' }}>{t('columnActions')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -974,8 +977,8 @@ export default function CuratorPage() {
|
||||
}}
|
||||
title={
|
||||
playingSongId === song.id
|
||||
? 'Pause'
|
||||
: 'Abspielen'
|
||||
? t('pause')
|
||||
: t('play')
|
||||
}
|
||||
>
|
||||
{playingSongId === song.id ? '⏸️' : '▶️'}
|
||||
@@ -1150,9 +1153,9 @@ export default function CuratorPage() {
|
||||
disabled={!curatorInfo?.isGlobalCurator}
|
||||
/>
|
||||
) : song.excludeFromGlobal ? (
|
||||
'Ja'
|
||||
t('excludeGlobalYes')
|
||||
) : (
|
||||
'Nein'
|
||||
t('excludeGlobalNo')
|
||||
)}
|
||||
{!curatorInfo?.isGlobalCurator && (
|
||||
<span
|
||||
@@ -1162,7 +1165,7 @@ export default function CuratorPage() {
|
||||
color: '#9ca3af',
|
||||
}}
|
||||
>
|
||||
Nur globale Kuratoren dürfen dieses Flag ändern.
|
||||
{t('excludeGlobalInfo')}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
@@ -1246,50 +1249,73 @@ export default function CuratorPage() {
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div
|
||||
{/* Pagination & Page Size */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginTop: '0.75rem',
|
||||
fontSize: '0.875rem',
|
||||
gap: '0.75rem',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
|
||||
disabled={page === 1}
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginTop: '0.75rem',
|
||||
fontSize: '0.875rem',
|
||||
padding: '0.3rem 0.6rem',
|
||||
borderRadius: '0.25rem',
|
||||
border: '1px solid #d1d5db',
|
||||
background: page === 1 ? '#f3f4f6' : '#fff',
|
||||
cursor: page === 1 ? 'not-allowed' : 'pointer',
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
|
||||
disabled={page === 1}
|
||||
{t('paginationPrev')}
|
||||
</button>
|
||||
<span style={{ color: '#666' }}>
|
||||
{t('paginationLabel', { page, total: totalPages })}
|
||||
</span>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
|
||||
<span>{t('pageSizeLabel')}</span>
|
||||
<select
|
||||
value={itemsPerPage}
|
||||
onChange={e => {
|
||||
const value = parseInt(e.target.value, 10) || 10;
|
||||
const safeValue = Math.min(100, Math.max(1, value));
|
||||
setItemsPerPage(safeValue);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
style={{
|
||||
padding: '0.3rem 0.6rem',
|
||||
padding: '0.25rem 0.5rem',
|
||||
borderRadius: '0.25rem',
|
||||
border: '1px solid #d1d5db',
|
||||
background: page === 1 ? '#f3f4f6' : '#fff',
|
||||
cursor: page === 1 ? 'not-allowed' : 'pointer',
|
||||
}}
|
||||
>
|
||||
Zurück
|
||||
</button>
|
||||
<span style={{ color: '#666' }}>
|
||||
Seite {page} von {totalPages}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
|
||||
disabled={page === totalPages}
|
||||
style={{
|
||||
padding: '0.3rem 0.6rem',
|
||||
borderRadius: '0.25rem',
|
||||
border: '1px solid #d1d5db',
|
||||
background: page === totalPages ? '#f3f4f6' : '#fff',
|
||||
cursor: page === totalPages ? 'not-allowed' : 'pointer',
|
||||
}}
|
||||
>
|
||||
Weiter
|
||||
</button>
|
||||
{[10, 25, 50, 100].map(size => (
|
||||
<option key={size} value={size}>
|
||||
{size}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
|
||||
disabled={page === totalPages}
|
||||
style={{
|
||||
padding: '0.3rem 0.6rem',
|
||||
borderRadius: '0.25rem',
|
||||
border: '1px solid #d1d5db',
|
||||
background: page === totalPages ? '#f3f4f6' : '#fff',
|
||||
cursor: page === totalPages ? 'not-allowed' : 'pointer',
|
||||
}}
|
||||
>
|
||||
{t('paginationNext')}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user