diff --git a/client/src/api/photos.js b/client/src/api/photos.js index e81aa15..b533f12 100644 --- a/client/src/api/photos.js +++ b/client/src/api/photos.js @@ -183,3 +183,7 @@ export function scanDuplicates() { export function dismissGroup(photoIds) { return api('/api/duplicates/dismiss', { method: 'POST', ...json({ photoIds }) }); } + +export function keepBestDuplicate(photoIds) { + return api('/api/duplicates/keep-best', { method: 'POST', ...json({ photoIds }) }); +} diff --git a/client/src/components/DuplicateGroup.jsx b/client/src/components/DuplicateGroup.jsx index 0c96d79..a40d56e 100644 --- a/client/src/components/DuplicateGroup.jsx +++ b/client/src/components/DuplicateGroup.jsx @@ -9,7 +9,18 @@ function fmtDate(d) { return new Date(d).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); } -export default function DuplicateGroup({ photos, onDismiss, onOpen }) { +function scorePhoto(p) { + const pixels = (p.width || 0) * (p.height || 0); + const size = p.filesize || 0; + const hasExif = p.camera_model ? 1 : 0; + return pixels * 1e6 + size + hasExif * 1e3; +} + +export default function DuplicateGroup({ photos, onDismiss, onKeepBest, onOpen }) { + const scores = photos.map(scorePhoto); + const maxScore = Math.max(...scores); + const bestIdx = scores.indexOf(maxScore); + return (
{/* Photo row */}
- {photos.map((photo, i) => ( -
+ {photos.map((photo, i) => { + const isBest = i === bestIdx; + return (
onOpen(photo)} + key={photo.id} style={{ - aspectRatio: '4/3', - cursor: 'pointer', - background: '#0c0c0c', - overflow: 'hidden', + flex: '1 1 200px', minWidth: 160, maxWidth: 320, + display: 'flex', flexDirection: 'column', + outline: isBest ? '2px solid #22c55e' : 'none', + outlineOffset: '-2px', position: 'relative', }} > - {photo.thumb_url ? ( - - ) : ( -
▪
- )} +
onOpen(photo)} + style={{ + aspectRatio: '4/3', + cursor: 'pointer', + background: '#0c0c0c', + overflow: 'hidden', + position: 'relative', + }} + > + {photo.thumb_url ? ( + + ) : ( +
▪
+ )} - {/* Index badge */} - - {i + 1} - -
- - {/* Meta */} -
- {fmtDate(photo.date_taken)} - - {[fmtSize(photo.filesize), photo.width && `${photo.width}×${photo.height}`].filter(Boolean).join(' · ')} - - {photo.camera_model && ( - - {photo.camera_model} + {/* Index badge */} + + {i + 1} - )} + + {/* Best badge */} + {isBest && ( + + Keep + + )} +
+ + {/* Meta */} +
+ {fmtDate(photo.date_taken)} + + {[fmtSize(photo.filesize), photo.width && `${photo.width}×${photo.height}`].filter(Boolean).join(' · ')} + + {photo.camera_model && ( + + {photo.camera_model} + + )} +
-
- ))} + ); + })}
{/* Footer */}
{photos.length} similar photos +