Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,5 @@ yarn-debug.log*
yarn-error.log*
.eslintcache
.vscode/settings.json
/.vs
/.vscode
258 changes: 214 additions & 44 deletions src/components/Preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import CloseIcon from '@mui/icons-material/Close';
import FavoriteIcon from '@mui/icons-material/Favorite';
import HeartBrokenIcon from '@mui/icons-material/HeartBroken';
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
import ZoomInIcon from '@mui/icons-material/ZoomIn';
import {
Box,
Expand All @@ -12,6 +13,7 @@ import {
Dialog,
DialogActions,
DialogContent,
Divider,
Tooltip,
Typography,
} from '@mui/material';
Expand All @@ -27,6 +29,12 @@ interface PreviewProps {
dateCreated: string;
mediaType?: string;
isFavorite?: boolean;
dateMediaTaken?: string;
dateMediaCreated?: string;
filename?: string;
sizeInBytes?: number;
width?: number;
height?: number;
};
handlePrev: () => void;
handleNext: () => void;
Expand All @@ -36,6 +44,56 @@ interface PreviewProps {
handleSingleFavorites?: (id: string, actionAdd: boolean) => void;
}

/** Format bytes → human-readable string (e.g. "3.2 MB") */
const formatBytes = (bytes: number): string => {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
};

/** Format an ISO date string → readable local date+time */
const formatDate = (iso: string): string => {
try {
return new Date(iso).toLocaleString(undefined, {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
} catch {
return iso;
}
};

// ── Info pane row ─────────────────────────────────────────────────────────────

interface InfoRowProps {
label: string;
value: string;
}

const InfoRow = ({ label, value }: InfoRowProps) => (
<Box sx={{ py: 1.5 }}>
<Typography
variant="caption"
sx={{
color: 'text.secondary',
textTransform: 'uppercase',
letterSpacing: '0.08em',
fontSize: '0.68rem',
}}
>
{label}
</Typography>
<Typography variant="body2" sx={{ mt: 0.3, wordBreak: 'break-all' }}>
{value}
</Typography>
</Box>
);

// ── Main component ─────────────────────────────────────────────────────────────

const Preview = ({
isOpen,
media,
Expand All @@ -51,9 +109,11 @@ const Preview = ({
queryFn: () => getPhoto(media.id),
});

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const date = new Date(media.dateCreated).toDateString();

const [zoom, setZoom] = useState(false);
const [infoOpen, setInfoOpen] = useState(false);

const handleZoomIn = () => {
setZoom(true);
Expand All @@ -67,6 +127,11 @@ const Preview = ({
handleSingleFavorites?.(media.id, !media.isFavorite);
};

// Close info pane when media changes
useEffect(() => {
setInfoOpen(false);
}, [media.id]);

useEffect(() => {
const handleKeyLeft = (e: KeyboardEvent) => {
if (e.key === 'ArrowLeft') {
Expand All @@ -89,6 +154,17 @@ const Preview = ({
};
}, [media]);

// Shared toolbar button style
const toolbarBtnSx = {
minWidth: 32,
p: '4px',
color: 'gray',
'&:hover': {
backgroundColor: 'transparent',
color: 'currentColor',
},
};

return (
<>
{zoom && (
Expand Down Expand Up @@ -119,6 +195,7 @@ const Preview = ({
</Container>
)}
<Dialog open={isOpen} onClose={onClose} fullScreen>
{/* ── Top toolbar ── */}
<Box
sx={{
width: '100%',
Expand All @@ -129,39 +206,9 @@ const Preview = ({
gap: 0.5, // reduce space between icons
}}
>
{media.mediaType !== 'video' && (
<Button
onClick={handleZoomIn}
disableRipple
sx={{
minWidth: 32,
p: '4px',
color: 'gray',
'&:hover': {
backgroundColor: 'transparent',
color: 'currentColor',
},
}}
>
<Tooltip title="Zoom In">
<ZoomInIcon />
</Tooltip>
</Button>
)}
{/* HeartBroken / Favorite */}
{handleSingleFavorites && (
<Button
onClick={handleFavorite}
disableRipple
sx={{
minWidth: 32,
p: '4px',
color: 'gray',
'&:hover': {
backgroundColor: 'transparent',
color: 'currentColor',
},
}}
>
<Button onClick={handleFavorite} disableRipple sx={toolbarBtnSx}>
{media.isFavorite ? (
<Tooltip title="Remove from Favorites">
<HeartBrokenIcon />
Expand All @@ -173,31 +220,57 @@ const Preview = ({
)}
</Button>
)}

{/* ── Info button (between HeartBroken and ZoomIn) ── */}
<Button
onClick={onClose}
onClick={() => setInfoOpen((prev) => !prev)}
disableRipple
sx={{
minWidth: 32,
p: '4px',
color: 'gray',
'&:hover': {
backgroundColor: 'transparent',
color: 'currentColor',
},
...toolbarBtnSx,
color: infoOpen ? 'primary.main' : 'gray',
}}
>
<Tooltip title="Info">
<InfoOutlinedIcon />
</Tooltip>
</Button>

{/* Zoom In */}
{media.mediaType !== 'video' && (
<Button onClick={handleZoomIn} disableRipple sx={toolbarBtnSx}>
<Tooltip title="Zoom In">
<ZoomInIcon />
</Tooltip>
</Button>
)}

{/* Close */}
<Button onClick={onClose} disableRipple sx={toolbarBtnSx}>
<Tooltip title="Close Preview">
<CloseIcon />
</Tooltip>
</Button>
</Box>
<DialogContent sx={{ p: 0 }}>

{/* ── Main content area ── */}
<DialogContent
sx={{
p: 0,
overflow: 'hidden',
position: 'relative',
display: 'flex',
}}
>
{/* Media area — shrinks when info pane opens */}
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
height: '100%',
flex: 1,
minWidth: 0,
transition: 'all 0.3s ease',
}}
>
<Button
Expand Down Expand Up @@ -231,13 +304,15 @@ const Preview = ({
height: '100%',
display: 'flex',
justifyContent: 'center',
flex: 1,
minWidth: 0,
}}
>
{isLoading ? (
<CircularProgress sx={{ my: 'auto' }} />
) : (
<Box
onClick={handleZoomIn}
onClick={media.mediaType !== 'video' ? handleZoomIn : undefined}
sx={{
display: 'flex',
justifyContent: 'center',
Expand Down Expand Up @@ -302,6 +377,101 @@ const Preview = ({
</Tooltip>
</Button>
</Box>

{/* ── Sliding Info Pane — absolutely positioned, slides in over the right edge ── */}
<Box
sx={{
position: 'absolute',
top: 0,
right: 0,
height: '100%',
width: 300,
transform: infoOpen ? 'translateX(0)' : 'translateX(100%)',
transition: 'transform 0.3s ease',
borderLeft: '1px solid',
borderColor: 'divider',
bgcolor: 'background.paper',
boxSizing: 'border-box',
px: 2,
py: 2,
overflowY: 'auto',
overflowX: 'hidden',
zIndex: 10,
}}
>
{/* Pane header */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
mb: 1,
}}
>
<Typography variant="subtitle1" fontWeight={600}>
Info
</Typography>
<Button
onClick={() => setInfoOpen(false)}
disableRipple
sx={{
minWidth: 32,
p: '4px',
color: 'gray',
'&:hover': { backgroundColor: 'transparent' },
}}
>
<CloseIcon fontSize="small" />
</Button>
</Box>

<Divider sx={{ mb: 1 }} />

{/* Info rows — only rendered when data is present */}
{media.filename && (
<InfoRow label="Filename" value={media.filename} />
)}
{media.dateMediaTaken && (
<>
<InfoRow
label="Date Taken"
value={formatDate(media.dateMediaTaken)}
/>
<Divider />
</>
)}
{media.dateMediaCreated && (
<>
<InfoRow
label="Date Created"
value={formatDate(media.dateMediaCreated)}
/>
<Divider />
</>
)}
{(media.width || media.height) && (
<>
<InfoRow
label="Dimensions"
value={
[
media.width && `${media.width}`,
media.height && `${media.height}`,
]
.filter(Boolean)
.join(' × ') + ' px'
}
/>
<Divider />
</>
)}
{media.sizeInBytes != null && (
<InfoRow
label="File Size"
value={formatBytes(media.sizeInBytes)}
/>
)}
</Box>
</DialogContent>
<DialogActions>
<Box
Expand All @@ -313,9 +483,9 @@ const Preview = ({
px: 2,
}}
>
<Typography color="text.secondary" fontSize={'small'}>
{/* <Typography color="text.secondary" fontSize={'small'}>
Date Created: {date}
</Typography>
</Typography> */}
</Box>
</DialogActions>
</Dialog>
Expand Down
3 changes: 2 additions & 1 deletion src/pages/SettingsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ChangePassword } from '../components/Users/ChangePassword';
import { DeleteAccount } from '../components/Users/DeleteAccount';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { VideoConversionSettings } from '../components/Users/VideoConversionSettings';
import MainLayout from '../layout/MainLayout';

Expand All @@ -8,7 +9,7 @@ const SettingsPage = () => {
<MainLayout title="Settings">
<ChangePassword />
<DeleteAccount />
<VideoConversionSettings />
{/* <VideoConversionSettings /> */}
</MainLayout>
);
};
Expand Down
Loading
Loading