Skip to content
Open
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
3 changes: 3 additions & 0 deletions .Jules/palette.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 2025-05-14 - [Clear Button for Range Inputs]
**Learning:** Since native HTML range inputs (`type="range"`) cannot represent a `null` or "unset" state, a separate interactive element is required to allow users to clear numeric values (like personal ratings) once they have been set.
**Action:** Always provide a 'Clear' or 'Reset' button next to range inputs if the underlying data field is nullable, ensuring it is accessible with proper ARIA labels and focus states.
22 changes: 18 additions & 4 deletions src/components/Library/GameDetailHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export function GameDetailHeader({ game, onGameUpdated, onPlatformChange, onFilt
alt={game.display_name}
className="w-full rounded-lg"
/>
<div className="svg-text-mask">
<div className="svg-text-mask" aria-hidden="true">
<span className="text-4xl font-black text-white drop-shadow-lg" style={{textShadow: '0 2px 8px rgba(0,0,0,0.4)'}}>PPGM</span>
</div>
</div>
Expand All @@ -148,11 +148,12 @@ export function GameDetailHeader({ game, onGameUpdated, onPlatformChange, onFilt
onClick={(e) => { e.stopPropagation(); onFavoriteToggle(); }}
className="absolute -top-2 -right-2 w-10 h-10 flex items-center justify-center text-3xl transition-transform hover:scale-110"
title={game.is_favorite ? t('removeFromFavorites') : t('addToFavorites')}
aria-label={game.is_favorite ? t('removeFromFavorites') : t('addToFavorites')}
>
{game.is_favorite ? (
<span className="text-yellow-400 drop-shadow-lg">★</span>
<span className="text-yellow-400 drop-shadow-lg" aria-hidden="true">★</span>
) : (
<span className="text-gray-400 hover:text-yellow-300">☆</span>
<span className="text-gray-400 hover:text-yellow-300" aria-hidden="true">☆</span>
)}
</button>
)}
Expand Down Expand Up @@ -364,7 +365,20 @@ export function GameDetailHeader({ game, onGameUpdated, onPlatformChange, onFilt
{/* Personal Rating */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="theme-text-muted text-sm">{t('personalRating')} (0-100): <span className="text-purple-500 font-semibold">{game.personal_rating !== null && game.personal_rating !== undefined ? `${game.personal_rating}/100` : '-'}</span></span>
<div className="flex items-center gap-2">
<span className="theme-text-muted text-sm">{t('personalRating')} (0-100): <span className="text-purple-500 font-semibold">{game.personal_rating !== null && game.personal_rating !== undefined ? `${game.personal_rating}/100` : '-'}</span></span>
{game.personal_rating !== null && game.personal_rating !== undefined && (
<button
type="button"
onClick={() => onRatingChange?.(null)}
className="text-gray-500 hover:text-red-500 transition-colors cursor-pointer focus-visible:ring-2 focus-visible:ring-indigo-500 rounded px-1"
title={t('clearRating')}
aria-label={t('clearRating')}
>
<span aria-hidden="true">✕</span>
</button>
)}
</div>
</div>
<div className="flex items-center gap-3">
<span className="text-xs theme-text-muted">0</span>
Expand Down
2 changes: 0 additions & 2 deletions src/components/Library/GameScreenshotsCarousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ export function GameScreenshotsCarousel({ gameId }: GameScreenshotsCarouselProps
const [igdbScreenshots, setIgdbScreenshots] = useState<string[]>([]);
const [currentIndex, setCurrentIndex] = useState(0);
const [loading, setLoading] = useState(true);
const [hasIgdbId, setHasIgdbId] = useState(false);

useEffect(() => {
const loadScreenshots = async () => {
Expand All @@ -29,7 +28,6 @@ export function GameScreenshotsCarousel({ gameId }: GameScreenshotsCarouselProps
// Load game data to check IGDB ID
try {
const game = await invoke<{ igdb_id: number | null }>("get_game_by_id", { id: gameId });
setHasIgdbId(!!game?.igdb_id);

// Load IGDB screenshots if available
if (game?.igdb_id) {
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export const translations = {

// Game Detail
personalRating: 'Personal Rating',
clearRating: 'Clear Rating',
notes: 'Notes',
saveNotes: 'Save Notes',
deleteGame: 'Delete Game',
Expand Down Expand Up @@ -435,6 +436,7 @@ export const translations = {

// Game Detail
personalRating: 'Note personnelle',
clearRating: 'Effacer la note',
notes: 'Notes',
saveNotes: 'Sauvegarder les notes',
deleteGame: 'Supprimer le jeu',
Expand Down