@@ -3,6 +3,7 @@ import ChevronRightIcon from '@mui/icons-material/ChevronRight';
33import CloseIcon from '@mui/icons-material/Close' ;
44import FavoriteIcon from '@mui/icons-material/Favorite' ;
55import HeartBrokenIcon from '@mui/icons-material/HeartBroken' ;
6+ import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined' ;
67import ZoomInIcon from '@mui/icons-material/ZoomIn' ;
78import {
89 Box ,
@@ -12,6 +13,7 @@ import {
1213 Dialog ,
1314 DialogActions ,
1415 DialogContent ,
16+ Divider ,
1517 Tooltip ,
1618 Typography ,
1719} from '@mui/material' ;
@@ -27,6 +29,12 @@ interface PreviewProps {
2729 dateCreated : string ;
2830 mediaType ?: string ;
2931 isFavorite ?: boolean ;
32+ dateMediaTaken ?: string ;
33+ dateMediaCreated ?: string ;
34+ filename ?: string ;
35+ sizeInBytes ?: number ;
36+ width ?: number ;
37+ height ?: number ;
3038 } ;
3139 handlePrev : ( ) => void ;
3240 handleNext : ( ) => void ;
@@ -36,6 +44,56 @@ interface PreviewProps {
3644 handleSingleFavorites ?: ( id : string , actionAdd : boolean ) => void ;
3745}
3846
47+ /** Format bytes → human-readable string (e.g. "3.2 MB") */
48+ const formatBytes = ( bytes : number ) : string => {
49+ if ( bytes < 1024 ) return `${ bytes } B` ;
50+ if ( bytes < 1024 * 1024 ) return `${ ( bytes / 1024 ) . toFixed ( 1 ) } KB` ;
51+ return `${ ( bytes / ( 1024 * 1024 ) ) . toFixed ( 1 ) } MB` ;
52+ } ;
53+
54+ /** Format an ISO date string → readable local date+time */
55+ const formatDate = ( iso : string ) : string => {
56+ try {
57+ return new Date ( iso ) . toLocaleString ( undefined , {
58+ year : 'numeric' ,
59+ month : 'long' ,
60+ day : 'numeric' ,
61+ hour : '2-digit' ,
62+ minute : '2-digit' ,
63+ } ) ;
64+ } catch {
65+ return iso ;
66+ }
67+ } ;
68+
69+ // ── Info pane row ─────────────────────────────────────────────────────────────
70+
71+ interface InfoRowProps {
72+ label : string ;
73+ value : string ;
74+ }
75+
76+ const InfoRow = ( { label, value } : InfoRowProps ) => (
77+ < Box sx = { { py : 1.5 } } >
78+ < Typography
79+ variant = "caption"
80+ sx = { {
81+ color : 'text.secondary' ,
82+ textTransform : 'uppercase' ,
83+ letterSpacing : '0.08em' ,
84+ fontSize : '0.68rem' ,
85+ } }
86+ >
87+ { label }
88+ </ Typography >
89+ < Typography variant = "body2" sx = { { mt : 0.3 , wordBreak : 'break-all' } } >
90+ { value }
91+ </ Typography >
92+ </ Box >
93+ ) ;
94+
95+ // ── Main component ─────────────────────────────────────────────────────────────
96+
3997const Preview = ( {
4098 isOpen,
4199 media,
@@ -51,9 +109,11 @@ const Preview = ({
51109 queryFn : ( ) => getPhoto ( media . id ) ,
52110 } ) ;
53111
112+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
54113 const date = new Date ( media . dateCreated ) . toDateString ( ) ;
55114
56115 const [ zoom , setZoom ] = useState ( false ) ;
116+ const [ infoOpen , setInfoOpen ] = useState ( false ) ;
57117
58118 const handleZoomIn = ( ) => {
59119 setZoom ( true ) ;
@@ -67,6 +127,11 @@ const Preview = ({
67127 handleSingleFavorites ?.( media . id , ! media . isFavorite ) ;
68128 } ;
69129
130+ // Close info pane when media changes
131+ useEffect ( ( ) => {
132+ setInfoOpen ( false ) ;
133+ } , [ media . id ] ) ;
134+
70135 useEffect ( ( ) => {
71136 const handleKeyLeft = ( e : KeyboardEvent ) => {
72137 if ( e . key === 'ArrowLeft' ) {
@@ -89,6 +154,17 @@ const Preview = ({
89154 } ;
90155 } , [ media ] ) ;
91156
157+ // Shared toolbar button style
158+ const toolbarBtnSx = {
159+ minWidth : 32 ,
160+ p : '4px' ,
161+ color : 'gray' ,
162+ '&:hover' : {
163+ backgroundColor : 'transparent' ,
164+ color : 'currentColor' ,
165+ } ,
166+ } ;
167+
92168 return (
93169 < >
94170 { zoom && (
@@ -119,6 +195,7 @@ const Preview = ({
119195 </ Container >
120196 ) }
121197 < Dialog open = { isOpen } onClose = { onClose } fullScreen >
198+ { /* ── Top toolbar ── */ }
122199 < Box
123200 sx = { {
124201 width : '100%' ,
@@ -129,39 +206,9 @@ const Preview = ({
129206 gap : 0.5 , // reduce space between icons
130207 } }
131208 >
132- { media . mediaType !== 'video' && (
133- < Button
134- onClick = { handleZoomIn }
135- disableRipple
136- sx = { {
137- minWidth : 32 ,
138- p : '4px' ,
139- color : 'gray' ,
140- '&:hover' : {
141- backgroundColor : 'transparent' ,
142- color : 'currentColor' ,
143- } ,
144- } }
145- >
146- < Tooltip title = "Zoom In" >
147- < ZoomInIcon />
148- </ Tooltip >
149- </ Button >
150- ) }
209+ { /* HeartBroken / Favorite */ }
151210 { handleSingleFavorites && (
152- < Button
153- onClick = { handleFavorite }
154- disableRipple
155- sx = { {
156- minWidth : 32 ,
157- p : '4px' ,
158- color : 'gray' ,
159- '&:hover' : {
160- backgroundColor : 'transparent' ,
161- color : 'currentColor' ,
162- } ,
163- } }
164- >
211+ < Button onClick = { handleFavorite } disableRipple sx = { toolbarBtnSx } >
165212 { media . isFavorite ? (
166213 < Tooltip title = "Remove from Favorites" >
167214 < HeartBrokenIcon />
@@ -173,31 +220,57 @@ const Preview = ({
173220 ) }
174221 </ Button >
175222 ) }
223+
224+ { /* ── Info button (between HeartBroken and ZoomIn) ── */ }
176225 < Button
177- onClick = { onClose }
226+ onClick = { ( ) => setInfoOpen ( ( prev ) => ! prev ) }
178227 disableRipple
179228 sx = { {
180- minWidth : 32 ,
181- p : '4px' ,
182- color : 'gray' ,
183- '&:hover' : {
184- backgroundColor : 'transparent' ,
185- color : 'currentColor' ,
186- } ,
229+ ...toolbarBtnSx ,
230+ color : infoOpen ? 'primary.main' : 'gray' ,
187231 } }
188232 >
233+ < Tooltip title = "Info" >
234+ < InfoOutlinedIcon />
235+ </ Tooltip >
236+ </ Button >
237+
238+ { /* Zoom In */ }
239+ { media . mediaType !== 'video' && (
240+ < Button onClick = { handleZoomIn } disableRipple sx = { toolbarBtnSx } >
241+ < Tooltip title = "Zoom In" >
242+ < ZoomInIcon />
243+ </ Tooltip >
244+ </ Button >
245+ ) }
246+
247+ { /* Close */ }
248+ < Button onClick = { onClose } disableRipple sx = { toolbarBtnSx } >
189249 < Tooltip title = "Close Preview" >
190250 < CloseIcon />
191251 </ Tooltip >
192252 </ Button >
193253 </ Box >
194- < DialogContent sx = { { p : 0 } } >
254+
255+ { /* ── Main content area ── */ }
256+ < DialogContent
257+ sx = { {
258+ p : 0 ,
259+ overflow : 'hidden' ,
260+ position : 'relative' ,
261+ display : 'flex' ,
262+ } }
263+ >
264+ { /* Media area — shrinks when info pane opens */ }
195265 < Box
196266 sx = { {
197267 display : 'flex' ,
198268 justifyContent : 'space-between' ,
199269 alignItems : 'center' ,
200270 height : '100%' ,
271+ flex : 1 ,
272+ minWidth : 0 ,
273+ transition : 'all 0.3s ease' ,
201274 } }
202275 >
203276 < Button
@@ -231,13 +304,15 @@ const Preview = ({
231304 height : '100%' ,
232305 display : 'flex' ,
233306 justifyContent : 'center' ,
307+ flex : 1 ,
308+ minWidth : 0 ,
234309 } }
235310 >
236311 { isLoading ? (
237312 < CircularProgress sx = { { my : 'auto' } } />
238313 ) : (
239314 < Box
240- onClick = { handleZoomIn }
315+ onClick = { media . mediaType !== 'video' ? handleZoomIn : undefined }
241316 sx = { {
242317 display : 'flex' ,
243318 justifyContent : 'center' ,
@@ -302,6 +377,101 @@ const Preview = ({
302377 </ Tooltip >
303378 </ Button >
304379 </ Box >
380+
381+ { /* ── Sliding Info Pane — absolutely positioned, slides in over the right edge ── */ }
382+ < Box
383+ sx = { {
384+ position : 'absolute' ,
385+ top : 0 ,
386+ right : 0 ,
387+ height : '100%' ,
388+ width : 300 ,
389+ transform : infoOpen ? 'translateX(0)' : 'translateX(100%)' ,
390+ transition : 'transform 0.3s ease' ,
391+ borderLeft : '1px solid' ,
392+ borderColor : 'divider' ,
393+ bgcolor : 'background.paper' ,
394+ boxSizing : 'border-box' ,
395+ px : 2 ,
396+ py : 2 ,
397+ overflowY : 'auto' ,
398+ overflowX : 'hidden' ,
399+ zIndex : 10 ,
400+ } }
401+ >
402+ { /* Pane header */ }
403+ < Box
404+ sx = { {
405+ display : 'flex' ,
406+ alignItems : 'center' ,
407+ justifyContent : 'space-between' ,
408+ mb : 1 ,
409+ } }
410+ >
411+ < Typography variant = "subtitle1" fontWeight = { 600 } >
412+ Info
413+ </ Typography >
414+ < Button
415+ onClick = { ( ) => setInfoOpen ( false ) }
416+ disableRipple
417+ sx = { {
418+ minWidth : 32 ,
419+ p : '4px' ,
420+ color : 'gray' ,
421+ '&:hover' : { backgroundColor : 'transparent' } ,
422+ } }
423+ >
424+ < CloseIcon fontSize = "small" />
425+ </ Button >
426+ </ Box >
427+
428+ < Divider sx = { { mb : 1 } } />
429+
430+ { /* Info rows — only rendered when data is present */ }
431+ { media . filename && (
432+ < InfoRow label = "Filename" value = { media . filename } />
433+ ) }
434+ { media . dateMediaTaken && (
435+ < >
436+ < InfoRow
437+ label = "Date Taken"
438+ value = { formatDate ( media . dateMediaTaken ) }
439+ />
440+ < Divider />
441+ </ >
442+ ) }
443+ { media . dateMediaCreated && (
444+ < >
445+ < InfoRow
446+ label = "Date Created"
447+ value = { formatDate ( media . dateMediaCreated ) }
448+ />
449+ < Divider />
450+ </ >
451+ ) }
452+ { ( media . width || media . height ) && (
453+ < >
454+ < InfoRow
455+ label = "Dimensions"
456+ value = {
457+ [
458+ media . width && `${ media . width } ` ,
459+ media . height && `${ media . height } ` ,
460+ ]
461+ . filter ( Boolean )
462+ . join ( ' × ' ) + ' px'
463+ }
464+ />
465+ < Divider />
466+ </ >
467+ ) }
468+ { media . sizeInBytes != null && (
469+ < InfoRow
470+ label = "File Size"
471+ value = { formatBytes ( media . sizeInBytes ) }
472+ />
473+ ) }
474+ </ Box >
305475 </ DialogContent >
306476 < DialogActions >
307477 < Box
@@ -313,9 +483,9 @@ const Preview = ({
313483 px : 2 ,
314484 } }
315485 >
316- < Typography color = "text.secondary" fontSize = { 'small' } >
486+ { /* <Typography color="text.secondary" fontSize={'small'}>
317487 Date Created: {date}
318- </ Typography >
488+ </Typography> */ }
319489 </ Box >
320490 </ DialogActions >
321491 </ Dialog >
0 commit comments