feature/coming_soon #3

Merged
hardik merged 4 commits from feature/coming_soon into main 2025-12-06 19:35:57 +05:30
9 changed files with 1613 additions and 348 deletions
Showing only changes of commit f72539adaa - Show all commits

View File

@ -1,9 +1,9 @@
import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import "./App.css"; import "./App.css";
import VintageComingSoonPage from "./components/comingsoon/comingsoon"; // import VintageComingSoonPage from "./components/comingsoon/comingsoon";
import DarkProductShowcase from "./components/product/product"; // import DarkProductShowcase from "./components/product/product";
import BlogPage from "./components/blogs/BlogPage"; // import BlogPage from "./components/blogs/BlogPage";
import BlogCard from "./components/blogs/BlogCard"; // import BlogCard from "./components/blogs/BlogCard";
import WeddingGallery from "./WeddingGallery/WeddingGallery"; import WeddingGallery from "./WeddingGallery/WeddingGallery";
// import OtherPage from "./pages/OtherPage"; // example if you add more pages // import OtherPage from "./pages/OtherPage"; // example if you add more pages

View File

@ -5,12 +5,17 @@ import {
IconButton, IconButton,
Box, Box,
Typography, Typography,
useTheme, Tooltip,
CircularProgress,
} from '@mui/material'; } from '@mui/material';
import CloseIcon from '@mui/icons-material/Close'; import CloseIcon from '@mui/icons-material/Close';
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew'; import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'; import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
import DownloadIcon from '@mui/icons-material/Download'; import DownloadIcon from '@mui/icons-material/Download';
// import ZoomOutMapIcon from '@mui/icons-material/ZoomOutMap';
import AddIcon from '@mui/icons-material/Add';
import RemoveIcon from '@mui/icons-material/Remove';
import FitScreenIcon from '@mui/icons-material/FitScreen';
export type LightboxPhoto = { export type LightboxPhoto = {
id: number | string; id: number | string;
@ -18,6 +23,8 @@ export type LightboxPhoto = {
title?: string; title?: string;
filename?: string; filename?: string;
created_at?: string; created_at?: string;
naturalWidth?: number;
naturalHeight?: number;
}; };
type Props = { type Props = {
@ -25,191 +32,561 @@ type Props = {
photos: LightboxPhoto[]; photos: LightboxPhoto[];
startIndex?: number; startIndex?: number;
onClose: () => void; onClose: () => void;
startIndexCallback?: (idx: number) => void; // optional: inform parent when slide changes startIndexCallback?: (idx: number) => void;
}; };
const SWIPE_THRESHOLD = 40; // pixels const SWIPE_THRESHOLD = 40;
const MIN_SCALE = 0.1;
const MAX_SCALE = 5;
const SCALE_STEP = 0.25;
export default function LightboxModal({ open, photos, startIndex = 0, onClose, startIndexCallback }: Props) { export default function LightboxModal({
open,
photos,
startIndex = 0,
onClose,
startIndexCallback,
}: Props) {
const [index, setIndex] = useState<number>(startIndex); const [index, setIndex] = useState<number>(startIndex);
const theme = useTheme(); const [scale, setScale] = useState<number>(1);
const [translate, setTranslate] = useState<{ x: number; y: number }>({ x: 0, y: 0 });
const [isPanning, setIsPanning] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [imageDimensions, setImageDimensions] = useState<{ width: number; height: number } | null>(null);
const panStart = useRef<{ x: number; y: number } | null>(null);
const lastTranslate = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
const touchStartX = useRef<number | null>(null); const touchStartX = useRef<number | null>(null);
const touchDeltaX = useRef(0); const touchDeltaX = useRef(0);
useEffect(() => { // For pinch
setIndex(startIndex); const pinchStartDist = useRef<number | null>(null);
}, [startIndex, open]); const pinchStartScale = useRef<number>(1);
const imageRef = useRef<HTMLImageElement>(null);
useEffect(() => setIndex(startIndex), [startIndex, open]);
useEffect(() => { useEffect(() => {
function onKey(e: KeyboardEvent) {
if (!open) return;
if (e.key === 'Escape') onClose();
if (e.key === 'ArrowLeft') prev();
if (e.key === 'ArrowRight') next();
}
window.addEventListener('keydown', onKey); window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey);
}, [open, index]); // eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, index, scale]);
useEffect(() => startIndexCallback?.(index), [index]);
useEffect(() => { useEffect(() => {
// inform parent if needed // reset scale/translate when changing image or closing/opening
startIndexCallback?.(index); setScale(1);
}, [index]); setTranslate({ x: 0, y: 0 });
lastTranslate.current = { x: 0, y: 0 };
setIsLoading(true);
setImageDimensions(null);
}, [index, open]);
const onKey = (e: KeyboardEvent) => {
if (!open) return;
if (e.key === 'Escape') onClose();
if (e.key === 'ArrowLeft') prev();
if (e.key === 'ArrowRight') next();
if (e.key === '+' || (e.ctrlKey && e.key === '=')) zoomIn();
if (e.key === '-') zoomOut();
if (e.key === '0') fitToScreen();
};
const prev = () => setIndex((i) => (i - 1 + photos.length) % photos.length); const prev = () => setIndex((i) => (i - 1 + photos.length) % photos.length);
const next = () => setIndex((i) => (i + 1) % photos.length); const next = () => setIndex((i) => (i + 1) % photos.length);
// Preload neighbors // Preload neighbors
useEffect(() => { useEffect(() => {
if (!photos || photos.length === 0) return; if (!photos?.length) return;
const prevIdx = (index - 1 + photos.length) % photos.length; const a = new Image();
const nextIdx = (index + 1) % photos.length; a.src = photos[(index - 1 + photos.length) % photos.length]?.url;
const p1 = new Image(); const b = new Image();
p1.src = photos[prevIdx]?.url; b.src = photos[(index + 1) % photos.length]?.url;
const p2 = new Image();
p2.src = photos[nextIdx]?.url;
}, [index, photos]); }, [index, photos]);
// Touch handlers for swipe const photo = photos && photos.length ? photos[index] : null;
// Handle image load
const handleImageLoad = (e: React.SyntheticEvent<HTMLImageElement>) => {
setIsLoading(false);
const img = e.currentTarget;
setImageDimensions({
width: img.naturalWidth,
height: img.naturalHeight
});
};
const handleImageError = () => {
setIsLoading(false);
setImageDimensions(null);
};
if (!photo) return null;
/* ============================
Zoom / Pan Controls
============================ */
const setScaleClamped = (s: number) => {
const clamped = Math.max(MIN_SCALE, Math.min(MAX_SCALE, s));
setScale(clamped);
if (clamped === 1) {
// reset translate when fully fit
setTranslate({ x: 0, y: 0 });
lastTranslate.current = { x: 0, y: 0 };
}
};
const zoomIn = () => setScaleClamped(scale + SCALE_STEP);
const zoomOut = () => setScaleClamped(scale - SCALE_STEP);
const fitToScreen = () => setScaleClamped(1);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const handleDoubleClick = (_e: React.MouseEvent) => {
if (scale === 1) setScaleClamped(2);
else setScaleClamped(1);
};
/* ============================
Panning (pointer / touch)
============================ */
const onPointerDown = (e: React.PointerEvent) => {
if (scale <= 1) return;
(e.target as Element).setPointerCapture?.(e.pointerId);
setIsPanning(true);
panStart.current = { x: e.clientX, y: e.clientY };
};
const onPointerMove = (e: React.PointerEvent) => {
if (!isPanning || !panStart.current) return;
const dx = e.clientX - panStart.current.x;
const dy = e.clientY - panStart.current.y;
const TX = lastTranslate.current.x + dx;
const TY = lastTranslate.current.y + dy;
setTranslate({ x: TX, y: TY });
};
const onPointerUp = (e: React.PointerEvent) => {
if (!isPanning) return;
setIsPanning(false);
if (panStart.current) {
const dx = e.clientX - panStart.current.x;
const dy = e.clientY - panStart.current.y;
lastTranslate.current = { x: lastTranslate.current.x + dx, y: lastTranslate.current.y + dy };
panStart.current = null;
}
try {
(e.target as Element).releasePointerCapture?.(e.pointerId);
} catch {
//ignore
}
};
const onWheel = (e: React.WheelEvent) => {
if (!open) return;
e.preventDefault();
const delta = -e.deltaY;
const step = delta > 0 ? SCALE_STEP : -SCALE_STEP;
setScaleClamped(scale + step);
};
/* ============================
Touch handlers for swipe + pinch
Note: use React.Touch types here to satisfy TS
============================ */
const onTouchStart = (e: React.TouchEvent) => { const onTouchStart = (e: React.TouchEvent) => {
touchDeltaX.current = 0; touchDeltaX.current = 0;
touchStartX.current = e.touches?.[0]?.clientX ?? null; if (e.touches.length === 1) {
touchStartX.current = e.touches[0].clientX;
if (scale > 1) {
panStart.current = { x: e.touches[0].clientX, y: e.touches[0].clientY };
setIsPanning(true);
}
} else if (e.touches.length === 2) {
pinchStartDist.current = getDistance(e.touches[0], e.touches[1]);
pinchStartScale.current = scale;
setIsPanning(false);
}
}; };
const onTouchMove = (e: React.TouchEvent) => { const onTouchMove = (e: React.TouchEvent) => {
if (touchStartX.current == null) return; if (e.touches.length === 1 && scale > 1 && isPanning && panStart.current) {
const x = e.touches?.[0]?.clientX ?? 0; const dx = e.touches[0].clientX - panStart.current.x;
touchDeltaX.current = x - (touchStartX.current ?? 0); const dy = e.touches[0].clientY - panStart.current.y;
setTranslate({ x: lastTranslate.current.x + dx, y: lastTranslate.current.y + dy });
e.preventDefault();
} else if (e.touches.length === 2 && pinchStartDist.current != null) {
const dist = getDistance(e.touches[0], e.touches[1]);
const ratio = dist / (pinchStartDist.current || dist);
setScaleClamped(pinchStartScale.current * ratio);
e.preventDefault();
} else if (e.touches.length === 1) {
const x = e.touches[0].clientX;
touchDeltaX.current = x - (touchStartX.current ?? x);
}
}; };
const onTouchEnd = () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars
const delta = touchDeltaX.current; const onTouchEnd = (_e: React.TouchEvent) => {
if (Math.abs(delta) > SWIPE_THRESHOLD) { if (isPanning) {
if (delta < 0) next(); setIsPanning(false);
lastTranslate.current = { ...translate };
panStart.current = null;
}
if (pinchStartDist.current != null) {
pinchStartDist.current = null;
pinchStartScale.current = scale;
}
if (Math.abs(touchDeltaX.current) > SWIPE_THRESHOLD && Math.abs(translate.x) < 10 && !isPanning) {
if (touchDeltaX.current < 0) next();
else prev(); else prev();
} }
touchStartX.current = null;
touchDeltaX.current = 0; touchDeltaX.current = 0;
touchStartX.current = null;
}; };
if (!photos || photos.length === 0) return null; // Helper accepting React.Touch (TS-friendly)
function getDistance(a: React.Touch, b: React.Touch) {
const dx = a.clientX - b.clientX;
const dy = a.clientY - b.clientY;
return Math.sqrt(dx * dx + dy * dy);
}
const photo = photos[index]; // Calculate image display size based on original dimensions
const getImageDisplayStyle = () => {
if (!imageDimensions) return {};
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
const { width: imgWidth, height: imgHeight } = imageDimensions;
// Calculate the maximum dimensions to fit in the viewport
const maxWidth = windowWidth * 0.9; // 90% of window width
const maxHeight = windowHeight * 0.8; // 80% of window height
let displayWidth = imgWidth;
let displayHeight = imgHeight;
// If image is larger than max dimensions, scale it down
if (imgWidth > maxWidth || imgHeight > maxHeight) {
const widthRatio = maxWidth / imgWidth;
const heightRatio = maxHeight / imgHeight;
const ratio = Math.min(widthRatio, heightRatio);
displayWidth = imgWidth * ratio;
displayHeight = imgHeight * ratio;
}
return {
width: `${displayWidth}px`,
height: `${displayHeight}px`,
maxWidth: '90vw',
maxHeight: '80vh',
};
};
/* ============================
Render
============================ */
return ( return (
<Dialog <Dialog
open={open} open={open}
onClose={onClose} onClose={onClose}
fullScreen fullScreen
PaperProps={{ sx: { backgroundColor: 'rgba(0,0,0,0.92)' } }} PaperProps={{
sx: {
m: 0,
p: 0,
height: '100vh',
backgroundColor: 'rgba(0,0,0,0.95)',
overflow: 'hidden',
'& .MuiDialog-container': {
alignItems: 'center',
justifyContent: 'center',
}
},
}}
> >
<Box <Box
sx={{ sx={{
position: 'relative',
width: '100%', width: '100%',
height: '100%', height: '100%',
position: 'relative',
boxSizing: 'border-box',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
userSelect: 'none', userSelect: isPanning ? 'none' : 'auto',
touchAction: 'pan-y', touchAction: 'none',
}} }}
onWheel={onWheel}
onTouchStart={onTouchStart} onTouchStart={onTouchStart}
onTouchMove={onTouchMove} onTouchMove={onTouchMove}
onTouchEnd={onTouchEnd} onTouchEnd={onTouchEnd}
onClick={(e) => {
if (e.target === e.currentTarget) onClose();
}}
> >
{/* Close */} {/* Loading indicator */}
<IconButton {isLoading && (
onClick={onClose} <Box sx={{
sx={{ position: 'absolute', top: 16, right: 16, color: 'white', zIndex: 40 }} position: 'absolute',
aria-label="Close" top: '50%',
> left: '50%',
<CloseIcon /> transform: 'translate(-50%, -50%)',
</IconButton> zIndex: 10
}}>
<CircularProgress sx={{ color: 'rgba(255,255,255,0.7)' }} />
</Box>
)}
{/* Download */} {/* Top right controls */}
<Box sx={{
position: 'absolute',
top: 16,
right: 16,
zIndex: 60,
display: 'flex',
gap: 1,
background: 'rgba(0,0,0,0.5)',
borderRadius: 2,
p: 0.5,
backdropFilter: 'blur(10px)',
}}>
<Tooltip title="Zoom Out">
<IconButton
size="small"
onClick={(ev) => {
ev.stopPropagation();
zoomOut();
}}
sx={{
bgcolor: 'rgba(255,255,255,0.1)',
color: 'white',
'&:hover': { bgcolor: 'rgba(255,255,255,0.2)' }
}}
>
<RemoveIcon />
</IconButton>
</Tooltip>
<Tooltip title="Fit to screen">
<IconButton
size="small"
onClick={(ev) => {
ev.stopPropagation();
fitToScreen();
}}
sx={{
bgcolor: 'rgba(255,255,255,0.1)',
color: 'white',
'&:hover': { bgcolor: 'rgba(255,255,255,0.2)' }
}}
>
<FitScreenIcon />
</IconButton>
</Tooltip>
<Tooltip title="Zoom In">
<IconButton
size="small"
onClick={(ev) => {
ev.stopPropagation();
zoomIn();
}}
sx={{
bgcolor: 'rgba(255,255,255,0.1)',
color: 'white',
'&:hover': { bgcolor: 'rgba(255,255,255,0.2)' }
}}
>
<AddIcon />
</IconButton>
</Tooltip>
<Tooltip title="Download">
<IconButton
size="small"
onClick={(ev) => {
ev.stopPropagation();
try {
const a = document.createElement('a');
a.href = photo.url;
a.download = `${photo.filename ?? photo.title ?? 'image'}`;
document.body.appendChild(a);
a.click();
a.remove();
} catch (err) {
console.error('download', err);
}
}}
sx={{
bgcolor: 'rgba(255,255,255,0.1)',
color: 'white',
'&:hover': { bgcolor: 'rgba(255,255,255,0.2)' }
}}
>
<DownloadIcon />
</IconButton>
</Tooltip>
<Tooltip title="Close (Esc)">
<IconButton
size="small"
onClick={(ev) => {
ev.stopPropagation();
onClose();
}}
sx={{
bgcolor: 'rgba(255,255,255,0.1)',
color: 'white',
'&:hover': { bgcolor: 'rgba(255,255,255,0.2)' }
}}
>
<CloseIcon />
</IconButton>
</Tooltip>
</Box>
{/* Prev / Next arrows */}
<IconButton <IconButton
sx={{ position: 'absolute', top: 16, right: 72, color: 'white', zIndex: 40 }} onClick={(ev) => {
aria-label="Download" ev.stopPropagation();
onClick={() => { prev();
try {
const a = document.createElement('a');
a.href = photo.url;
a.download = `${photo.filename ?? photo.title ?? 'image'}`;
document.body.appendChild(a);
a.click();
a.remove();
} catch (err) {
console.error('download', err);
}
}} }}
>
<DownloadIcon />
</IconButton>
{/* Prev */}
<IconButton
onClick={prev}
sx={{ sx={{
position: 'absolute', position: 'absolute',
left: 8, left: 16,
color: 'rgba(255,255,255,0.9)', zIndex: 50,
zIndex: 40, color: 'white',
bgcolor: 'transparent', bgcolor: 'rgba(0,0,0,0.5)',
backdropFilter: 'blur(10px)',
'&:hover': { bgcolor: 'rgba(0,0,0,0.7)' }
}} }}
aria-label="Previous"
> >
<ArrowBackIosNewIcon /> <ArrowBackIosNewIcon />
</IconButton> </IconButton>
{/* Next */}
<IconButton <IconButton
onClick={next} onClick={(ev) => {
ev.stopPropagation();
next();
}}
sx={{ sx={{
position: 'absolute', position: 'absolute',
right: 8, right: 16,
color: 'rgba(255,255,255,0.9)', zIndex: 50,
zIndex: 40, color: 'white',
bgcolor: 'transparent', bgcolor: 'rgba(0,0,0,0.5)',
backdropFilter: 'blur(10px)',
'&:hover': { bgcolor: 'rgba(0,0,0,0.7)' }
}} }}
aria-label="Next"
> >
<ArrowForwardIosIcon /> <ArrowForwardIosIcon />
</IconButton> </IconButton>
{/* Image */} {/* Image container */}
<Box <Box
sx={{ sx={{
maxWidth: '95%', width: '100%',
maxHeight: '88%', height: '100%',
px: 2,
boxSizing: 'border-box',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
overflow: 'hidden', overflow: 'hidden',
}} }}
onPointerDown={onPointerDown}
onPointerMove={onPointerMove}
onPointerUp={onPointerUp}
onPointerCancel={onPointerUp}
onDoubleClick={handleDoubleClick}
> >
<img <Box
src={photo.url} sx={{
alt={photo.title ?? photo.filename} display: 'flex',
style={{ alignItems: 'center',
maxWidth: '100%', justifyContent: 'center',
maxHeight: '100%', touchAction: 'none',
objectFit: 'contain', position: 'relative',
borderRadius: 6,
boxShadow: '0 10px 30px rgba(0,0,0,0.6)',
}} }}
draggable={false} >
/> <img
ref={imageRef}
src={photo.url}
alt={photo.title ?? photo.filename}
onLoad={handleImageLoad}
onError={handleImageError}
style={{
display: isLoading ? 'none' : 'block',
width: 'auto',
height: 'auto',
maxWidth: '90vw',
maxHeight: '80vh',
objectFit: 'contain',
transform: `translate(${translate.x}px, ${translate.y}px) scale(${scale})`,
transition: isPanning ? 'none' : 'transform 120ms ease',
cursor: scale > 1 ? (isPanning ? 'grabbing' : 'grab') : 'default',
borderRadius: 4,
boxShadow: '0 10px 30px rgba(0,0,0,0.6)',
touchAction: 'none',
...getImageDisplayStyle(),
}}
draggable={false}
/>
</Box>
</Box> </Box>
{/* Caption / Position */} {/* Caption */}
<Box sx={{ position: 'absolute', bottom: 20, left: 20, right: 20, textAlign: 'center', zIndex: 40 }}> <Box sx={{
<Typography variant="subtitle1" color="white" noWrap> position: 'absolute',
bottom: 16,
left: 16,
right: 16,
textAlign: 'center',
zIndex: 50,
background: 'rgba(0,0,0,0.5)',
backdropFilter: 'blur(10px)',
borderRadius: 2,
p: 1.5,
maxWidth: '600px',
mx: 'auto',
}}>
<Typography variant="subtitle1" color="white" noWrap sx={{ fontWeight: 500 }}>
{photo.title ?? photo.filename} {photo.title ?? photo.filename}
</Typography> </Typography>
<Typography variant="caption" color="rgba(255,255,255,0.8)"> <Typography variant="caption" color="rgba(255,255,255,0.85)" sx={{ display: 'block', mt: 0.5 }}>
{index + 1} / {photos.length} {photo.created_at ? `${new Date(photo.created_at).toLocaleDateString()}` : ''} {index + 1} of {photos.length}
{photo.created_at && `${new Date(photo.created_at).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
})}`}
{imageDimensions && `${imageDimensions.width}×${imageDimensions.height}`}
</Typography> </Typography>
</Box> </Box>
{/* Zoom level indicator */}
{scale !== 1 && (
<Box sx={{
position: 'absolute',
top: 16,
left: 16,
zIndex: 50,
background: 'rgba(0,0,0,0.5)',
backdropFilter: 'blur(10px)',
borderRadius: 2,
p: 1,
color: 'white',
fontSize: '0.875rem',
fontWeight: 500,
}}>
{Math.round(scale * 100)}%
</Box>
)}
</Box> </Box>
</Dialog> </Dialog>
); );
} }

View File

@ -24,11 +24,16 @@ export default function PreviewTester() {
useEffect(() => { useEffect(() => {
return () => { return () => {
items.forEach((it) => { items.forEach((it) => {
try { URL.revokeObjectURL(it.previewUrl); } catch { } try {
URL.revokeObjectURL(it.previewUrl);
} catch {
// ignore
}
}); });
}; };
}, [items]); }, [items]);
return ( return (
<Box sx={{ p: 3 }}> <Box sx={{ p: 3 }}>
<Box {...getRootProps()} sx={{ border: '2px dashed', p: 2, textAlign: 'center', mb: 2 }}> <Box {...getRootProps()} sx={{ border: '2px dashed', p: 2, textAlign: 'center', mb: 2 }}>

View File

@ -1,4 +1,5 @@
// SafeGrid.tsx // SafeGrid.tsx
import Grid from '@mui/material/Grid'; import Grid from '@mui/material/Grid';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const SafeGrid = Grid as any; const SafeGrid = Grid as any;
export default SafeGrid; export default SafeGrid;

View File

@ -45,7 +45,7 @@ const UploadQueue: React.FC<Props> = ({ queue, setQueue, newPhotoTitle, onUpload
const uploadAll = useCallback(async () => { const uploadAll = useCallback(async () => {
for (const item of queue.filter((q) => q.status === 'pending' || q.status === 'error')) { for (const item of queue.filter((q) => q.status === 'pending' || q.status === 'error')) {
// eslint-disable-next-line no-await-in-loop
await uploadSingle(item); await uploadSingle(item);
} }
}, [queue, uploadSingle]); }, [queue, uploadSingle]);

View File

@ -1,55 +0,0 @@
// UploadTester.tsx
import React, { useState } from 'react';
import { Box, Button, Input, Typography } from '@mui/material';
export default function UploadTester() {
const [file, setFile] = useState<File | null>(null);
function UploadTester() {
const [file, setFile] = useState<File | null>(null);
const onFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFile(e.target.files?.[0] ?? null);
};
return <input type="file" onChange={onFileChange} />;
}
const handleUpload = async () => {
if (!file) return alert('Choose a file first');
const fd = new FormData();
fd.append('file', file, file.name);
fd.append('title', 'test-upload');
try {
const res = await fetch('https://mfs-api.midastix.com/image-upload/single', {
method: 'POST',
body: fd,
// DO NOT set Content-Type — browser will set multipart boundary
});
const text = await res.text();
console.log('UploadTester: status', res.status, 'body:', text);
alert(`Status: ${res.status} — see console for body`);
} catch (err) {
console.error('UploadTester error', err);
alert('Upload failed — see console');
}
};
return (
<Box sx={{ p: 2 }}>
<Typography variant="subtitle1">Upload Tester (one file)</Typography>
<input
type="file"
onChange={(e) => {
const input = e.target as HTMLInputElement;
const f = input.files && input.files[0];
setFile(f ?? null);
}}
/>
<Box sx={{ mt: 2 }}>
<Button variant="contained" onClick={handleUpload} disabled={!file}>Upload</Button>
</Box>
</Box>
);
}

File diff suppressed because it is too large Load Diff

View File

@ -15,6 +15,7 @@ export const uploadFileXHR = (
file: File, file: File,
onProgress: (pct: number) => void, onProgress: (pct: number) => void,
title?: string title?: string
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): Promise<{ success: boolean; data?: any; error?: string }> => ): Promise<{ success: boolean; data?: any; error?: string }> =>
new Promise((resolve) => { new Promise((resolve) => {
try { try {
@ -34,6 +35,7 @@ export const uploadFileXHR = (
try { try {
const json = JSON.parse(xhr.responseText); const json = JSON.parse(xhr.responseText);
resolve({ success: true, data: json }); resolve({ success: true, data: json });
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) { } catch (e) {
resolve({ success: true, data: xhr.responseText }); resolve({ success: true, data: xhr.responseText });
} }
@ -49,6 +51,7 @@ export const uploadFileXHR = (
fd.append('file', file, file.name); fd.append('file', file, file.name);
if (title) fd.append('title', title); if (title) fd.append('title', title);
xhr.send(fd); xhr.send(fd);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) { } catch (err: any) {
resolve({ success: false, error: err?.message ?? String(err) }); resolve({ success: false, error: err?.message ?? String(err) });
} }
@ -64,14 +67,17 @@ export const fetchImagesApi = async (title = '') => {
throw new Error(txt || `HTTP ${res.status}`); throw new Error(txt || `HTTP ${res.status}`);
} }
const body = await res.json(); const body = await res.json();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let imagesArray: any[] = []; let imagesArray: any[] = [];
if (Array.isArray(body)) imagesArray = body; if (Array.isArray(body)) imagesArray = body;
else if (Array.isArray(body.images)) imagesArray = body.images; else if (Array.isArray(body.images)) imagesArray = body.images;
else if (body.data && Array.isArray(body.data.images)) imagesArray = body.data.images; else if (body.data && Array.isArray(body.data.images)) imagesArray = body.data.images;
else { else {
const firstArray = Object.values(body).find((v) => Array.isArray(v)); const firstArray = Object.values(body).find((v) => Array.isArray(v));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (Array.isArray(firstArray)) imagesArray = firstArray as any[]; if (Array.isArray(firstArray)) imagesArray = firstArray as any[];
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return imagesArray.map((it: any) => ({ return imagesArray.map((it: any) => ({
id: it.id ?? it._id ?? `${it.filename ?? Math.random()}`, id: it.id ?? it._id ?? `${it.filename ?? Math.random()}`,
url: toAbsoluteUrl(it.path ?? it.url), url: toAbsoluteUrl(it.path ?? it.url),
@ -84,17 +90,27 @@ export const fetchImagesApi = async (title = '') => {
}; };
/* Delete photo (by id) */ /* Delete photo (by id) */
export const deletePhotoApi = async (id: number | string) => { export const deletePhotoApi = async (filename: string) => {
const url = `${MFS_API_BASE}/image-upload/${id}`; const url = `${MFS_API_BASE}/image-upload/${filename}`;
const res = await fetch(url, { method: 'DELETE', headers: { accept: 'application/json' } });
const res = await fetch(url, {
method: "DELETE",
headers: { accept: "application/json" },
});
const text = await res.text(); const text = await res.text();
if (!res.ok) throw new Error(text || `HTTP ${res.status}`);
return text || 'deleted'; if (!res.ok) {
throw new Error(text || `HTTP ${res.status}`);
}
return text || "deleted";
}; };
/* Delete folder by title (assumed endpoint) */ /* Delete folder by title (assumed endpoint) */
export const deleteFolderApi = async (title: string) => { export const deleteFolderApi = async (title: string) => {
const url = `${MFS_API_BASE}/image-upload/folder?title=${encodeURIComponent(title)}`; const url = `${MFS_API_BASE}/image-upload/${encodeURIComponent(title)}`;
const res = await fetch(url, { method: 'DELETE', headers: { accept: 'application/json' } }); const res = await fetch(url, { method: 'DELETE', headers: { accept: 'application/json' } });
const text = await res.text(); const text = await res.text();
if (!res.ok) throw new Error(text || `HTTP ${res.status}`); if (!res.ok) throw new Error(text || `HTTP ${res.status}`);

View File

@ -61,10 +61,10 @@ const fadeInUp = keyframes`
} }
`; `;
const fadeIn = keyframes` // const fadeIn = keyframes`
from { opacity: 0; } // from { opacity: 0; }
to { opacity: 1; } // to { opacity: 1; }
`; // `;
const float = keyframes` const float = keyframes`
0% { transform: translateY(0px); } 0% { transform: translateY(0px); }
@ -324,6 +324,7 @@ const BlogCard: React.FC<BlogCardProps> = ({ post, index }) => {
const BlogPage: React.FC = () => { const BlogPage: React.FC = () => {
const [selectedCategory, setSelectedCategory] = useState<string>('all'); const [selectedCategory, setSelectedCategory] = useState<string>('all');
const [searchQuery, setSearchQuery] = useState<string>(''); const [searchQuery, setSearchQuery] = useState<string>('');
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [heroRef, heroInView] = useInView({ threshold: 0.1, triggerOnce: true }); const [heroRef, heroInView] = useInView({ threshold: 0.1, triggerOnce: true });
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm')); const isMobile = useMediaQuery(theme.breakpoints.down('sm'));