feat: improve UX with modern skip/solve buttons, ID sorting, and URL-safe filenames
This commit is contained in:
@@ -11,7 +11,7 @@ interface Song {
|
||||
activations: number;
|
||||
}
|
||||
|
||||
type SortField = 'title' | 'artist' | 'createdAt';
|
||||
type SortField = 'id' | 'title' | 'artist' | 'createdAt';
|
||||
type SortDirection = 'asc' | 'desc';
|
||||
|
||||
export default function AdminPage() {
|
||||
@@ -210,8 +210,14 @@ export default function AdminPage() {
|
||||
);
|
||||
|
||||
const sortedSongs = [...filteredSongs].sort((a, b) => {
|
||||
const valA = a[sortField].toLowerCase();
|
||||
const valB = b[sortField].toLowerCase();
|
||||
// Handle numeric sorting for ID
|
||||
if (sortField === 'id') {
|
||||
return sortDirection === 'asc' ? a.id - b.id : b.id - a.id;
|
||||
}
|
||||
|
||||
// String sorting for other fields
|
||||
const valA = String(a[sortField]).toLowerCase();
|
||||
const valB = String(b[sortField]).toLowerCase();
|
||||
|
||||
if (valA < valB) return sortDirection === 'asc' ? -1 : 1;
|
||||
if (valA > valB) return sortDirection === 'asc' ? 1 : -1;
|
||||
@@ -302,7 +308,12 @@ export default function AdminPage() {
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.875rem' }}>
|
||||
<thead>
|
||||
<tr style={{ borderBottom: '2px solid #e5e7eb', textAlign: 'left' }}>
|
||||
<th style={{ padding: '0.75rem' }}>ID</th>
|
||||
<th
|
||||
style={{ padding: '0.75rem', cursor: 'pointer', userSelect: 'none' }}
|
||||
onClick={() => handleSort('id')}
|
||||
>
|
||||
ID {sortField === 'id' && (sortDirection === 'asc' ? '↑' : '↓')}
|
||||
</th>
|
||||
<th
|
||||
style={{ padding: '0.75rem', cursor: 'pointer', userSelect: 'none' }}
|
||||
onClick={() => handleSort('title')}
|
||||
|
||||
@@ -103,7 +103,19 @@ export async function POST(request: Request) {
|
||||
if (!title) title = 'Unknown Title';
|
||||
if (!artist) artist = 'Unknown Artist';
|
||||
|
||||
const filename = `${Date.now()}-${file.name.replace(/[^a-zA-Z0-9.]/g, '_')}`;
|
||||
// Create URL-safe filename
|
||||
const originalName = file.name.replace(/\.mp3$/i, '');
|
||||
const sanitizedName = originalName
|
||||
.replace(/[^a-zA-Z0-9]/g, '-') // Replace special chars with dash
|
||||
.replace(/-+/g, '-') // Replace multiple dashes with single dash
|
||||
.replace(/^-|-$/g, ''); // Remove leading/trailing dashes
|
||||
|
||||
// Warn if filename was changed
|
||||
if (originalName !== sanitizedName) {
|
||||
validationInfo.warnings.push(`Filename sanitized: "${originalName}" → "${sanitizedName}"`);
|
||||
}
|
||||
|
||||
const filename = `${Date.now()}-${sanitizedName}.mp3`;
|
||||
const uploadDir = path.join(process.cwd(), 'public/uploads');
|
||||
|
||||
await writeFile(path.join(uploadDir, filename), buffer);
|
||||
|
||||
@@ -194,17 +194,30 @@ body {
|
||||
}
|
||||
|
||||
.skip-button {
|
||||
background: none;
|
||||
width: 100%;
|
||||
padding: 1rem 1.5rem;
|
||||
margin-top: 1rem;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
color: #666;
|
||||
text-decoration: underline;
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.skip-button:hover {
|
||||
color: #000;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
|
||||
}
|
||||
|
||||
.skip-button:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 10px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
/* Messages */
|
||||
|
||||
@@ -136,12 +136,28 @@ export default function Game({ dailyPuzzle }: GameProps) {
|
||||
{!hasWon && !hasLost && (
|
||||
<>
|
||||
<GuessInput onGuess={handleGuess} disabled={false} />
|
||||
{gameState.guesses.length < 6 ? (
|
||||
<button
|
||||
onClick={() => addGuess("SKIPPED", false)}
|
||||
className="skip-button"
|
||||
>
|
||||
Skip (+{UNLOCK_STEPS[Math.min(gameState.guesses.length + 1, 6)] - unlockedSeconds}s)
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => {
|
||||
addGuess("SKIPPED", false);
|
||||
setHasLost(true);
|
||||
}}
|
||||
className="skip-button"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
|
||||
boxShadow: '0 4px 15px rgba(245, 87, 108, 0.4)'
|
||||
}}
|
||||
>
|
||||
Solve (Give Up)
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user