diff --git a/src/App.tsx b/src/App.tsx index 681138a..2d1237d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,9 +1,9 @@ import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import "./App.css"; -import VintageComingSoonPage from "./components/comingsoon/comingsoon"; -import DarkProductShowcase from "./components/product/product"; -import BlogPage from "./components/blogs/BlogPage"; -import BlogCard from "./components/blogs/BlogCard"; +// import VintageComingSoonPage from "./components/comingsoon/comingsoon"; +// import DarkProductShowcase from "./components/product/product"; +// import BlogPage from "./components/blogs/BlogPage"; +// import BlogCard from "./components/blogs/BlogCard"; import WeddingGallery from "./WeddingGallery/WeddingGallery"; // import OtherPage from "./pages/OtherPage"; // example if you add more pages diff --git a/src/WeddingGallery/LightboxModal.tsx b/src/WeddingGallery/LightboxModal.tsx index b84aa52..8b0dd39 100644 --- a/src/WeddingGallery/LightboxModal.tsx +++ b/src/WeddingGallery/LightboxModal.tsx @@ -5,12 +5,17 @@ import { IconButton, Box, Typography, - useTheme, + Tooltip, + CircularProgress, } from '@mui/material'; import CloseIcon from '@mui/icons-material/Close'; import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew'; import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'; 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 = { id: number | string; @@ -18,6 +23,8 @@ export type LightboxPhoto = { title?: string; filename?: string; created_at?: string; + naturalWidth?: number; + naturalHeight?: number; }; type Props = { @@ -25,191 +32,561 @@ type Props = { photos: LightboxPhoto[]; startIndex?: number; 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(startIndex); - const theme = useTheme(); + const [scale, setScale] = useState(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(null); const touchDeltaX = useRef(0); - useEffect(() => { - setIndex(startIndex); - }, [startIndex, open]); + // For pinch + const pinchStartDist = useRef(null); + const pinchStartScale = useRef(1); + const imageRef = useRef(null); + + useEffect(() => setIndex(startIndex), [startIndex, open]); 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); return () => window.removeEventListener('keydown', onKey); - }, [open, index]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [open, index, scale]); + + useEffect(() => startIndexCallback?.(index), [index]); useEffect(() => { - // inform parent if needed - startIndexCallback?.(index); - }, [index]); + // reset scale/translate when changing image or closing/opening + setScale(1); + 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 next = () => setIndex((i) => (i + 1) % photos.length); // Preload neighbors useEffect(() => { - if (!photos || photos.length === 0) return; - const prevIdx = (index - 1 + photos.length) % photos.length; - const nextIdx = (index + 1) % photos.length; - const p1 = new Image(); - p1.src = photos[prevIdx]?.url; - const p2 = new Image(); - p2.src = photos[nextIdx]?.url; + if (!photos?.length) return; + const a = new Image(); + a.src = photos[(index - 1 + photos.length) % photos.length]?.url; + const b = new Image(); + b.src = photos[(index + 1) % photos.length]?.url; }, [index, photos]); - // Touch handlers for swipe + const photo = photos && photos.length ? photos[index] : null; + + // Handle image load + const handleImageLoad = (e: React.SyntheticEvent) => { + 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) => { 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) => { - if (touchStartX.current == null) return; - const x = e.touches?.[0]?.clientX ?? 0; - touchDeltaX.current = x - (touchStartX.current ?? 0); + if (e.touches.length === 1 && scale > 1 && isPanning && panStart.current) { + const dx = e.touches[0].clientX - panStart.current.x; + 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 = () => { - const delta = touchDeltaX.current; - if (Math.abs(delta) > SWIPE_THRESHOLD) { - if (delta < 0) next(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const onTouchEnd = (_e: React.TouchEvent) => { + if (isPanning) { + 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(); } - touchStartX.current = null; + 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 ( { + if (e.target === e.currentTarget) onClose(); + }} > - {/* Close */} - - - + {/* Loading indicator */} + {isLoading && ( + + + + )} - {/* Download */} + {/* Top right controls */} + + + { + ev.stopPropagation(); + zoomOut(); + }} + sx={{ + bgcolor: 'rgba(255,255,255,0.1)', + color: 'white', + '&:hover': { bgcolor: 'rgba(255,255,255,0.2)' } + }} + > + + + + + + { + ev.stopPropagation(); + fitToScreen(); + }} + sx={{ + bgcolor: 'rgba(255,255,255,0.1)', + color: 'white', + '&:hover': { bgcolor: 'rgba(255,255,255,0.2)' } + }} + > + + + + + + { + ev.stopPropagation(); + zoomIn(); + }} + sx={{ + bgcolor: 'rgba(255,255,255,0.1)', + color: 'white', + '&:hover': { bgcolor: 'rgba(255,255,255,0.2)' } + }} + > + + + + + + { + 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)' } + }} + > + + + + + + { + ev.stopPropagation(); + onClose(); + }} + sx={{ + bgcolor: 'rgba(255,255,255,0.1)', + color: 'white', + '&:hover': { bgcolor: 'rgba(255,255,255,0.2)' } + }} + > + + + + + + {/* Prev / Next arrows */} { - 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); - } + onClick={(ev) => { + ev.stopPropagation(); + prev(); }} - > - - - - {/* Prev */} - - {/* Next */} { + ev.stopPropagation(); + next(); + }} sx={{ position: 'absolute', - right: 8, - color: 'rgba(255,255,255,0.9)', - zIndex: 40, - bgcolor: 'transparent', + right: 16, + zIndex: 50, + color: 'white', + bgcolor: 'rgba(0,0,0,0.5)', + backdropFilter: 'blur(10px)', + '&:hover': { bgcolor: 'rgba(0,0,0,0.7)' } }} - aria-label="Next" > - {/* Image */} + {/* Image container */} - {photo.title + > + {photo.title 1 ? (isPanning ? 'grabbing' : 'grab') : 'default', + borderRadius: 4, + boxShadow: '0 10px 30px rgba(0,0,0,0.6)', + touchAction: 'none', + ...getImageDisplayStyle(), + }} + draggable={false} + /> + - {/* Caption / Position */} - - + {/* Caption */} + + {photo.title ?? photo.filename} - - {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}`} + + {/* Zoom level indicator */} + {scale !== 1 && ( + + {Math.round(scale * 100)}% + + )} ); -} +} \ No newline at end of file diff --git a/src/WeddingGallery/PreviewTester.tsx b/src/WeddingGallery/PreviewTester.tsx index 4ce8246..44b55ee 100644 --- a/src/WeddingGallery/PreviewTester.tsx +++ b/src/WeddingGallery/PreviewTester.tsx @@ -24,11 +24,16 @@ export default function PreviewTester() { useEffect(() => { return () => { items.forEach((it) => { - try { URL.revokeObjectURL(it.previewUrl); } catch { } + try { + URL.revokeObjectURL(it.previewUrl); + } catch { + // ignore + } }); }; }, [items]); + return ( diff --git a/src/WeddingGallery/SafeGrid.tsx b/src/WeddingGallery/SafeGrid.tsx index 63d50ea..18b4b79 100644 --- a/src/WeddingGallery/SafeGrid.tsx +++ b/src/WeddingGallery/SafeGrid.tsx @@ -1,4 +1,5 @@ // SafeGrid.tsx import Grid from '@mui/material/Grid'; +// eslint-disable-next-line @typescript-eslint/no-explicit-any const SafeGrid = Grid as any; export default SafeGrid; diff --git a/src/WeddingGallery/UploadQueue.tsx b/src/WeddingGallery/UploadQueue.tsx index 173834a..3da9481 100644 --- a/src/WeddingGallery/UploadQueue.tsx +++ b/src/WeddingGallery/UploadQueue.tsx @@ -45,7 +45,7 @@ const UploadQueue: React.FC = ({ queue, setQueue, newPhotoTitle, onUpload const uploadAll = useCallback(async () => { for (const item of queue.filter((q) => q.status === 'pending' || q.status === 'error')) { - // eslint-disable-next-line no-await-in-loop + await uploadSingle(item); } }, [queue, uploadSingle]); diff --git a/src/WeddingGallery/UploadTester.tsx b/src/WeddingGallery/UploadTester.tsx deleted file mode 100644 index 2208f70..0000000 --- a/src/WeddingGallery/UploadTester.tsx +++ /dev/null @@ -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(null); - function UploadTester() { - const [file, setFile] = useState(null); - - const onFileChange = (e: React.ChangeEvent) => { - setFile(e.target.files?.[0] ?? null); - }; - - return ; - } - - 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 ( - - Upload Tester (one file) - { - const input = e.target as HTMLInputElement; - const f = input.files && input.files[0]; - setFile(f ?? null); - }} - /> - - - - - ); -} diff --git a/src/WeddingGallery/WeddingGallery.tsx b/src/WeddingGallery/WeddingGallery.tsx index 5fce8e9..03d3a2e 100644 --- a/src/WeddingGallery/WeddingGallery.tsx +++ b/src/WeddingGallery/WeddingGallery.tsx @@ -1,7 +1,6 @@ // WeddingGallery.tsx import React, { JSX, useCallback, useEffect, useState } from 'react'; import { - AppBar, Alert, Box, Button, @@ -26,18 +25,40 @@ import { DialogTitle, DialogContent, DialogActions, + alpha, + styled, + AppBar, + Tabs, + Tab, } from '@mui/material'; import { - CameraAlt, + // CameraAlt, CloudUpload, Delete as DeleteIcon, Download, Replay, + PhotoAlbum, + Celebration, + Diamond, + AddPhotoAlternate, + FolderOpen, + LocalFlorist, Favorite, + Event, + People, + LocationOn, + // DateRange, + Timeline, + ChevronRight, + PhotoCamera, + Videocam, } from '@mui/icons-material'; import { useDropzone } from 'react-dropzone'; import LightboxModal from './LightboxModal'; +/* ============================ + Types + ============================ */ type WeddingPhoto = { id: number | string; url: string; @@ -62,6 +83,15 @@ type FolderInfo = { title: string; count: number; thumb?: string; + date?: string; + description?: string; +}; + +type StoryStep = { + title: string; + description: string; + icon: React.ReactNode; + photos?: WeddingPhoto[]; }; /* ============================ @@ -73,7 +103,171 @@ const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; const MAX_QUEUE_FILES = 30; /* ============================ - Helpers / API functions + Styled Components + ============================ */ +const FloralBackground = styled(Box)(() => ({ + position: 'fixed', + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundImage: ` + radial-gradient(circle at 20% 80%, ${alpha('#ffebee', 0.1)} 0%, transparent 20%), + radial-gradient(circle at 80% 20%, ${alpha('#e8f5e9', 0.1)} 0%, transparent 20%), + radial-gradient(circle at 40% 40%, ${alpha('#f3e5f5', 0.1)} 0%, transparent 30%), + radial-gradient(circle at 60% 60%, ${alpha('#e3f2fd', 0.1)} 0%, transparent 25%) + `, + pointerEvents: 'none', + zIndex: -1, +})); + +const GoldButton = styled(Button)(() => ({ + background: 'linear-gradient(135deg, #d4af37 0%, #ffd700 50%, #d4af37 100%)', + color: '#2c1810', + fontWeight: 600, + padding: '12px 32px', + borderRadius: '50px', + textTransform: 'none', + letterSpacing: '0.5px', + transition: 'all 0.3s ease', + boxShadow: '0 4px 20px rgba(212, 175, 55, 0.3)', + '&:hover': { + transform: 'translateY(-2px)', + boxShadow: '0 8px 25px rgba(212, 175, 55, 0.4)', + }, + '&:disabled': { + background: 'linear-gradient(135deg, #cccccc 0%, #aaaaaa 100%)', + boxShadow: 'none', + }, +})); + +const DropzoneArea = styled(Paper)(({ theme }) => ({ + padding: theme.spacing(4), + border: `2px dashed ${alpha('#d4af37', 0.4)}`, + borderRadius: '20px', + background: `linear-gradient(135deg, ${alpha('#fffaf0', 0.9)} 0%, ${alpha('#fff8e1', 0.9)} 100%)`, + transition: 'all 0.3s ease', + cursor: 'pointer', + textAlign: 'center', + '&:hover': { + borderColor: '#d4af37', + background: `linear-gradient(135deg, ${alpha('#fffaf0', 1)} 0%, ${alpha('#fff8e1', 1)} 100%)`, + transform: 'translateY(-2px)', + boxShadow: '0 12px 24px rgba(212, 175, 55, 0.15)', + }, +})); + +const AlbumCard = styled(Card)(() => ({ + position: 'relative', + borderRadius: '16px', + overflow: 'hidden', + transition: 'all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275)', + background: 'linear-gradient(145deg, #ffffff 0%, #fafafa 100%)', + boxShadow: '0 8px 32px rgba(0, 0, 0, 0.08)', + border: '1px solid rgba(255, 255, 255, 0.3)', + height: '100%', + '&:hover': { + transform: 'translateY(-8px)', + boxShadow: '0 20px 40px rgba(0, 0, 0, 0.15)', + '& .album-overlay': { + opacity: 1, + }, + }, + '&::before': { + content: '""', + position: 'absolute', + top: 0, + left: 0, + right: 0, + height: '4px', + background: 'linear-gradient(90deg, #d4af37 0%, #ffd700 50%, #d4af37 100%)', + zIndex: 1, + }, +})); + +const AlbumOverlay = styled(Box)(({ theme }) => ({ + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + background: 'linear-gradient(0deg, rgba(0,0,0,0.7) 0%, rgba(0,0,0,0.2) 50%, rgba(0,0,0,0) 100%)', + opacity: 0, + transition: 'opacity 0.3s ease', + display: 'flex', + flexDirection: 'column', + justifyContent: 'flex-end', + padding: theme.spacing(3), + color: 'white', +})); + +const PhotoCard = styled(Card)(() => ({ + position: 'relative', + borderRadius: '12px', + overflow: 'hidden', + transition: 'all 0.3s ease', + '&:hover': { + transform: 'scale(1.03)', + boxShadow: '0 12px 28px rgba(0, 0, 0, 0.2)', + '& .photo-actions': { + opacity: 1, + }, + }, +})); + +const PhotoActions = styled(Box)(({ theme }) => ({ + position: 'absolute', + top: 0, + left: 0, + right: 0, + background: 'linear-gradient(to bottom, rgba(0,0,0,0.7) 0%, rgba(0,0,0,0) 100%)', + padding: theme.spacing(1), + opacity: 0, + transition: 'opacity 0.3s ease', + display: 'flex', + justifyContent: 'flex-end', + gap: theme.spacing(1), +})); + +const FloatingActionButton = styled(IconButton)(() => ({ + + position: 'fixed', + bottom: 24, + right: 24, + background: 'linear-gradient(135deg, #d4af37 0%, #ffd700 100%)', + color: '#2c1810', + width: 56, + height: 56, + boxShadow: '0 8px 25px rgba(212, 175, 55, 0.4)', + zIndex: 1000, + '&:hover': { + background: 'linear-gradient(135deg, #c19b2a 0%, #e6c200 100%)', + transform: 'scale(1.1)', + }, +})); + +const StoryStepCard = styled(Paper)(({ theme }) => ({ + position: 'relative', + padding: theme.spacing(3), + borderRadius: '16px', + background: 'rgba(255, 255, 255, 0.95)', + boxShadow: '0 8px 32px rgba(0, 0, 0, 0.08)', + border: '1px solid rgba(255, 255, 255, 0.3)', + '&:hover': { + transform: 'translateY(-4px)', + boxShadow: '0 16px 40px rgba(0, 0, 0, 0.12)', + }, +})); + +const WeddingHeader = styled(AppBar)(() => ({ + background: 'linear-gradient(135deg, rgba(15, 118, 110, 0.95) 0%, rgba(45, 212, 191, 0.9) 100%)', + backdropFilter: 'blur(10px)', + boxShadow: '0 4px 30px rgba(0, 0, 0, 0.1)', + borderBottom: '2px solid rgba(212, 175, 55, 0.3)', +})); + +/* ============================ + Helper Functions ============================ */ const toAbsoluteUrl = (path?: string) => { if (!path) return ''; @@ -85,6 +279,7 @@ const uploadFileXHR = ( file: File, onProgress: (pct: number) => void, title?: string + // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Promise<{ success: boolean; data?: any; error?: string }> => new Promise((resolve) => { try { @@ -104,6 +299,7 @@ const uploadFileXHR = ( try { const json = JSON.parse(xhr.responseText); resolve({ success: true, data: json }); + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { resolve({ success: true, data: xhr.responseText }); } @@ -120,6 +316,7 @@ const uploadFileXHR = ( if (title) fd.append('title', title); xhr.send(fd); + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { resolve({ success: false, error: err?.message ?? String(err) }); } @@ -134,14 +331,17 @@ const fetchImagesApi = async (title = ''): Promise => { throw new Error(txt || `HTTP ${res.status}`); } const body = await res.json(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any let imagesArray: any[] = []; if (Array.isArray(body)) imagesArray = body; else if (Array.isArray(body.images)) imagesArray = body.images; else if (body.data && Array.isArray(body.data.images)) imagesArray = body.data.images; else { 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[]; } + // eslint-disable-next-line @typescript-eslint/no-explicit-any return imagesArray.map((it: any) => ({ id: it.id ?? it._id ?? `${it.filename ?? Math.random()}`, url: toAbsoluteUrl(it.path ?? it.url), @@ -154,7 +354,7 @@ const fetchImagesApi = async (title = ''): Promise => { }; const deletePhotoApi = async (id: number | string) => { - const url = `${MFS_API_BASE}/image-upload/${id}`; + const url = `${MFS_API_BASE}/image-upload/id/${id}`; const res = await fetch(url, { method: 'DELETE', headers: { accept: 'application/json' } }); const txt = await res.text(); if (!res.ok) throw new Error(txt || `HTTP ${res.status}`); @@ -170,19 +370,98 @@ const deleteFolderApi = async (title: string) => { }; /* ============================ - Theme & SafeGrid + Theme ============================ */ const theme = createTheme({ palette: { primary: { main: '#0f766e', light: '#2dd4bf' }, - secondary: { main: '#d4af37' }, - background: { default: '#fbfdfb' }, + secondary: { main: '#d4af37', light: '#ffd700' }, + background: { + default: '#fefefe', + paper: '#fff' + }, + text: { + primary: '#2c1810', + secondary: '#6b7280' + }, + }, + typography: { + fontFamily: '"Poppins", "Inter", -apple-system, BlinkMacSystemFont, sans-serif', + h1: { + fontFamily: '"Great Vibes", cursive', + fontSize: '3.5rem', + fontWeight: 400, + letterSpacing: '1px', + }, + h2: { + fontFamily: '"Great Vibes", cursive', + fontSize: '2.8rem', + fontWeight: 400, + }, + h3: { + fontFamily: '"Playfair Display", serif', + fontWeight: 600, + }, + h4: { + fontFamily: '"Playfair Display", serif', + fontWeight: 500, + }, + h5: { + fontFamily: '"Poppins", sans-serif', + fontWeight: 500, + }, + h6: { + fontFamily: '"Poppins", sans-serif', + fontWeight: 500, + }, + body1: { + fontFamily: '"Inter", sans-serif', + lineHeight: 1.6, + }, + body2: { + fontFamily: '"Inter", sans-serif', + lineHeight: 1.5, + }, + }, + shape: { + borderRadius: 16, + }, + components: { + MuiButton: { + styleOverrides: { + root: { + borderRadius: 12, + textTransform: 'none', + fontWeight: 500, + }, + }, + }, + MuiPaper: { + styleOverrides: { + root: { + borderRadius: 16, + }, + }, + }, + MuiCard: { + styleOverrides: { + root: { + borderRadius: 16, + }, + }, + }, + MuiTab: { + styleOverrides: { + root: { + textTransform: 'none', + fontWeight: 500, + fontSize: '1rem', + }, + }, + }, }, - typography: { fontFamily: '"Inter", "Roboto", sans-serif' }, }); -const SafeGrid = Grid as any; - /* ============================ Component ============================ */ @@ -204,23 +483,84 @@ export default function WeddingGallery(): JSX.Element { const [deletingId, setDeletingId] = useState(null); const [deletingFolder, setDeletingFolder] = useState(null); - // lightbox state const [lightboxOpen, setLightboxOpen] = useState(false); const [lightboxStartIndex, setLightboxStartIndex] = useState(0); + const [showUploadPanel, setShowUploadPanel] = useState(false); + const [activeTab, setActiveTab] = useState(0); + + // Storyline steps + const [storySteps] = useState([ + { + title: 'The Proposal', + description: 'The magical moment when everything began', + icon: , + }, + { + title: 'Engagement Shoot', + description: 'Capturing our love and excitement', + icon: , + }, + { + title: 'Wedding Planning', + description: 'Preparing for our dream day', + icon: , + }, + { + title: 'The Ceremony', + description: 'Saying "I Do" surrounded by loved ones', + icon: , + }, + { + title: 'Reception', + description: 'Celebrating with family and friends', + icon: , + }, + { + title: 'Honeymoon', + description: 'Beginning our journey together', + icon: , + }, + ]); const loadAll = useCallback(async () => { try { const items = await fetchImagesApi(''); setPhotosAll(items); + + // Group photos by title/album const map = new Map(); items.forEach((it) => { - const title = it.title ?? 'untitled'; - if (!map.has(title)) map.set(title, { title, count: 0, thumb: it.url }); + const title = it.title ?? 'Untitled'; + if (!map.has(title)) { + map.set(title, { + title, + count: 0, + thumb: it.url, + date: it.date || it.created_at, + }); + } const entry = map.get(title)!; entry.count += 1; if (!entry.thumb) entry.thumb = it.url; }); - setFolders(Array.from(map.values()).sort((a, b) => b.count - a.count)); + + const folderArray = Array.from(map.values()).sort((a, b) => b.count - a.count); + + // Add sample descriptions for demo + const sampleDescriptions = [ + 'Beautiful moments from our special day', + 'Cherished memories with family and friends', + 'Every glance, every smile, every tear of joy', + 'The beginning of our forever journey', + 'Love, laughter, and happily ever after', + ]; + + folderArray.forEach((folder, index) => { + folder.description = sampleDescriptions[index % sampleDescriptions.length]; + }); + + setFolders(folderArray); + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { console.error('loadAll error', err); setFolders([]); @@ -234,14 +574,16 @@ export default function WeddingGallery(): JSX.Element { queue.forEach((q) => { try { URL.revokeObjectURL(q.previewUrl); - } catch { } + } catch { + //ignore + } }); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); /* ============================ - Drop — enforce MAX_QUEUE_FILES + Dropzone ============================ */ const onDrop = useCallback((acceptedFiles: File[]) => { if (!acceptedFiles || acceptedFiles.length === 0) return; @@ -250,7 +592,7 @@ export default function WeddingGallery(): JSX.Element { const available = Math.max(0, MAX_QUEUE_FILES - currentCount); if (available <= 0) { - setSnackbar({ open: true, severity: 'error', message: `Queue full — maximum ${MAX_QUEUE_FILES} files allowed.` }); + setSnackbar({ open: true, severity: 'error', message: `Maximum ${MAX_QUEUE_FILES} files allowed.` }); return; } @@ -273,15 +615,16 @@ export default function WeddingGallery(): JSX.Element { }); setQueue((prev) => [...items, ...prev]); + setShowUploadPanel(true); if (rejectedCount > 0) { setSnackbar({ open: true, severity: 'info', - message: `Accepted ${acceptedToAdd.length} file(s). Rejected ${rejectedCount} file(s) due to the ${MAX_QUEUE_FILES}-file limit.`, + message: `Added ${acceptedToAdd.length} file(s). ${rejectedCount} file(s) were not added.`, }); } else { - setSnackbar({ open: true, severity: 'success', message: `Added ${acceptedToAdd.length} file(s) to the queue.` }); + setSnackbar({ open: true, severity: 'success', message: `Added ${acceptedToAdd.length} file(s)` }); } }, [queue.length]); @@ -289,6 +632,7 @@ export default function WeddingGallery(): JSX.Element { onDrop, multiple: true, maxSize: MAX_FILE_SIZE, + // eslint-disable-next-line @typescript-eslint/no-explicit-any accept: ALLOWED_TYPES.length ? ALLOWED_TYPES.reduce((acc, t) => ({ ...acc, [t]: [] }), {} as any) : undefined, }); @@ -307,7 +651,7 @@ export default function WeddingGallery(): JSX.Element { const uploadedPhoto: WeddingPhoto = { id: it.id ?? it._id, url: toAbsoluteUrl(it.path ?? it.url), - title: it.title ?? it.filename ?? commonTitle ?? '', + title: it.title ?? it.filename ?? commonTitle ?? 'Untitled', filename: it.filename, created_at: it.created_at ?? it.createdAt, path: it.path, @@ -332,10 +676,9 @@ export default function WeddingGallery(): JSX.Element { return; } for (const item of pending) { - // eslint-disable-next-line no-await-in-loop await uploadSingle(item); } - setSnackbar({ open: true, severity: 'success', message: 'Uploads complete' }); + setSnackbar({ open: true, severity: 'success', message: 'All photos uploaded successfully!' }); setQueue((prev) => prev.filter((q) => q.status !== 'done')); }, [queue, uploadSingle]); @@ -349,13 +692,15 @@ export default function WeddingGallery(): JSX.Element { if (item) { try { URL.revokeObjectURL(item.previewUrl); - } catch { } + } catch { + //ignore + } } setQueue((prev) => prev.filter((q) => q.id !== id)); }; /* ============================ - Folder / modal management + lightbox hookup + Folder Management ============================ */ const openFolder = async (title: string) => { setFolderOpenTitle(title); @@ -363,6 +708,7 @@ export default function WeddingGallery(): JSX.Element { const items = await fetchImagesApi(title); setFolderPhotos(items); setFolderModalOpen(true); + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { console.error('openFolder error', err); setSnackbar({ open: true, severity: 'error', message: `Failed to load folder: ${err?.message || err}` }); @@ -370,7 +716,7 @@ export default function WeddingGallery(): JSX.Element { }; const handleDeletePhoto = async (id: number | string) => { - if (!confirm('Delete this photo? This action cannot be undone.')) return; + // if (!confirm('Delete this photo? This cannot be undone.')) return; setDeletingId(id); try { await deletePhotoApi(id); @@ -378,6 +724,7 @@ export default function WeddingGallery(): JSX.Element { setPhotosAll((prev) => prev.filter((p) => p.id !== id)); setSnackbar({ open: true, severity: 'success', message: 'Photo deleted' }); await loadAll(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { console.error('deletePhoto error', err); setSnackbar({ open: true, severity: 'error', message: `Failed to delete: ${err?.message || err}` }); @@ -387,13 +734,14 @@ export default function WeddingGallery(): JSX.Element { }; const handleDeleteFolder = async (title: string) => { - if (!confirm(`Delete entire folder "${title}" and all its photos? This is irreversible.`)) return; + if (!confirm(`Delete "${title}" and all ${folders.find(f => f.title === title)?.count || 0} photos?`)) return; setDeletingFolder(title); try { await deleteFolderApi(title); setSnackbar({ open: true, severity: 'success', message: `Folder "${title}" deleted` }); await loadAll(); setFolderModalOpen(false); + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { console.error('deleteFolder error', err); setSnackbar({ open: true, severity: 'error', message: `Failed to delete folder: ${err?.message || err}` }); @@ -402,210 +750,736 @@ export default function WeddingGallery(): JSX.Element { } }; - useEffect(() => { - const map = new Map(); - photosAll.forEach((it) => { - const title = it.title ?? 'untitled'; - if (!map.has(title)) map.set(title, { title, count: 0, thumb: it.url }); - const e = map.get(title)!; - e.count += 1; - if (!e.thumb) e.thumb = it.url; - }); - setFolders(Array.from(map.values()).sort((a, b) => b.count - a.count)); - }, [photosAll]); - /* ============================ UI ============================ */ return ( - + + + + + + {/* Header */} + - - - Wedding Gallery — Folders - - } label="Events" sx={{ bgcolor: 'secondary.main', color: '#111' }} /> + + + + Sakashi and Chirags Wedding Memories + + + setActiveTab(newValue)} + sx={{ + display: { xs: 'none', md: 'flex' }, + '& .MuiTabs-indicator': { + backgroundColor: '#ffd700', + }, + }} + > + } + iconPosition="start" + label="Albums" + sx={{ color: 'white' }} + /> + } + iconPosition="start" + label="Our Story" + sx={{ color: 'white' }} + /> + } + iconPosition="start" + label="Moments" + sx={{ color: 'white' }} + /> + + + + + } + label="Love Story" + sx={{ + bgcolor: 'rgba(255, 215, 0, 0.2)', + color: '#ffd700', + border: '1px solid rgba(255, 215, 0, 0.3)', + display: { xs: 'none', sm: 'flex' }, + }} + /> + setShowUploadPanel(true)} + sx={{ color: '#ffd700' }} + > + + + - + - - - Events (Folders) - - Only folders are shown here. Click a folder to manage photos inside. - - - Queue: {queue.length}/{MAX_QUEUE_FILES} - - + + {/* Tab Content */} + {activeTab === 0 && ( + + {/* Hero Section */} + + + Our Wedding Albums + + + Preserving precious memories of our special day + + - - - - - - - {isDragActive ? 'Drop images here' : 'Drag & drop images here, or click to select'} - - Max {(MAX_FILE_SIZE / (1024 * 1024)).toFixed(0)}MB per file. Allowed: jpg, png, gif, webp. Max {MAX_QUEUE_FILES} files in queue. - - - - - - - setCommonTitle(e.target.value)} /> - - - + + + 📸 Upload Photos + + setShowUploadPanel(false)} size="small"> + + - {/* Queue preview thumbnails */} - - {queue.map((q) => ( - - - - {q.file.name} - - {q.status === 'uploading' ? : q.status === 'done' ? Done : q.status === 'error' ? Err : Pending} - retryOne(q.id)} disabled={q.status !== 'error'}> - removeOne(q.id)}> - - - - - ))} - - - - - - - {/* Folders */} - - {folders.length === 0 ? ( - - No events found - Upload photos to create events (folders) - - ) : ( - - {folders.map((f) => ( - - - openFolder(f.title)}> - - + + + + + + + {isDragActive ? 'Drop photos here' : 'Drag & drop photos here'} + + + or click to browse files + + + + + - - - - {f.title} - {f.count} photos + + + + + {queue.length > 0 && ( + + + setCommonTitle(e.target.value)} + sx={{ mb: 2 }} + /> + + + q.status === 'pending' || q.status === 'error')} + startIcon={} + sx={{ flex: 1 }} + > + Upload All ({queue.filter(q => q.status === 'pending' || q.status === 'error').length}) + + + + + + {/* Queue Preview */} + + {queue.map((q) => ( + + + + + {q.file.name} + + + {q.status === 'uploading' && } + {q.status === 'error' && ( + retryOne(q.id)}> + + + )} + removeOne(q.id)}> + + + + + + + ))} + + + )} + + )} + + {/* Albums Grid */} + + + + All Albums ({folders.length}) + + + + + {folders.length === 0 ? ( + + + + No Albums Yet + + + Create your first album to start preserving wedding memories + + setShowUploadPanel(true)} + startIcon={} + > + Create Album + + + ) : ( + + {folders.map((folder) => ( + + + openFolder(folder.title)} sx={{ position: 'relative', cursor: 'pointer' }}> + + - - { e.stopPropagation(); await handleDeleteFolder(f.title); }}> - - + + + + Open Album + + + {folder.count} precious memories + + + + + + ALBUM - + + + + + {folder.title} + + + {folder.description} + + + + + + {folder.count} photos + + + { + e.stopPropagation(); + if (confirm(`Delete album "${folder.title}"?`)) { + await handleDeleteFolder(folder.title); + } + }} + sx={{ + color: '#ef4444', + '&:hover': { + backgroundColor: 'rgba(239, 68, 68, 0.1)', + }, + }} + > + + + + + + + + + ))} + + )} + + + )} + + {activeTab === 1 && ( + + {/* Storyline Hero */} + + + Our Story + + + The beautiful journey that led us here + + + From the first hello to forever, every step of our journey has been magical. + Here's our story in chapters. + + + + {/* Story Steps */} + + {storySteps.map((step) => ( + + + + {step.icon} - - + + {step.title} + + + {step.description} + + + + + + ))} - - )} - + + + )} + + {activeTab === 2 && ( + + {/* Moments Hero */} + + + Special Moments + + + Candid shots and heartfelt moments + + + The most beautiful moments are often the unplanned ones. + These photos capture the true essence of our wedding day. + + + + {/* Photo Grid */} + + {photosAll.length === 0 ? ( + + + + No Photos Yet + + + Upload photos to see your special moments here + + setShowUploadPanel(true)} + startIcon={} + > + Upload Photos + + + ) : ( + + {photosAll.slice(0, 12).map((photo) => ( + + + + { + try { + const a = document.createElement('a'); + a.href = photo.url; + a.download = photo.filename || 'wedding-photo.jpg'; + document.body.appendChild(a); + a.click(); + a.remove(); + } catch (err) { + console.error('download error', err); + } + }} + sx={{ + color: 'white', + bgcolor: 'rgba(15, 118, 110, 0.8)', + '&:hover': { bgcolor: '#0f766e' }, + }} + > + + + + + + + ))} + + )} + + + )} - {/* Folder modal */} - setFolderModalOpen(false)} fullWidth maxWidth="md"> - - Manage — {folderOpenTitle} ({folderPhotos.length}) - - - + {folderPhotos.length === 0 ? ( - No photos in this event. + + + No photos in this album + + ) : ( {folderPhotos.map((p, idx) => ( - - - {/* clicking image opens lightbox at that index */} - { - setLightboxStartIndex(idx); - setLightboxOpen(true); - }} - > - - - + + handleDeletePhoto(p.id)} disabled={deletingId === p.id} + sx={{ + color: 'white', + bgcolor: 'rgba(239, 68, 68, 0.9)', + '&:hover': { bgcolor: '#ef4444' }, + }} > - - - {p.filename ?? p.title} - - { + { try { const a = document.createElement('a'); a.href = p.url; - a.download = `${p.filename ?? p.title ?? 'photo'}.jpg`; + a.download = p.filename || `${p.title || 'wedding-photo'}.jpg`; document.body.appendChild(a); a.click(); a.remove(); + setSnackbar({ open: true, severity: 'success', message: 'Photo downloaded!' }); } catch (err) { console.error('download error', err); setSnackbar({ open: true, severity: 'error', message: 'Download failed' }); } - }}> - - - {p.created_at ? new Date(p.created_at).toLocaleDateString() : ''} - + }} + sx={{ + color: 'white', + bgcolor: 'rgba(15, 118, 110, 0.9)', + '&:hover': { bgcolor: '#0f766e' }, + }} + > + + + + { + setLightboxStartIndex(idx); + setLightboxOpen(true); + }} + > + - + ))} )} - + + {/* Floating Action Button */} + setShowUploadPanel(true)}> + + + {/* Lightbox */} setLightboxOpen(false)} /> - setSnackbar((s) => ({ ...s, open: false }))}> - setSnackbar((s) => ({ ...s, open: false }))}> + {/* Snackbar */} + setSnackbar((s) => ({ ...s, open: false }))} + anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} + > + setSnackbar((s) => ({ ...s, open: false }))} + sx={{ + borderRadius: 2, + boxShadow: '0 8px 32px rgba(0, 0, 0, 0.12)', + fontFamily: '"Inter", sans-serif', + fontWeight: 500, + }} + > {snackbar.message} + + {/* Footer */} + + + + + + + + + Forever Cherished • Always Remembered + + + © {new Date().getFullYear()} Our Wedding Gallery + + + ); -} +} \ No newline at end of file diff --git a/src/WeddingGallery/apis.ts b/src/WeddingGallery/apis.ts index d1fe5de..615a03d 100644 --- a/src/WeddingGallery/apis.ts +++ b/src/WeddingGallery/apis.ts @@ -15,6 +15,7 @@ export const uploadFileXHR = ( file: File, onProgress: (pct: number) => void, title?: string + // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Promise<{ success: boolean; data?: any; error?: string }> => new Promise((resolve) => { try { @@ -34,6 +35,7 @@ export const uploadFileXHR = ( try { const json = JSON.parse(xhr.responseText); resolve({ success: true, data: json }); + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { resolve({ success: true, data: xhr.responseText }); } @@ -49,6 +51,7 @@ export const uploadFileXHR = ( fd.append('file', file, file.name); if (title) fd.append('title', title); xhr.send(fd); + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { resolve({ success: false, error: err?.message ?? String(err) }); } @@ -64,14 +67,17 @@ export const fetchImagesApi = async (title = '') => { throw new Error(txt || `HTTP ${res.status}`); } const body = await res.json(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any let imagesArray: any[] = []; if (Array.isArray(body)) imagesArray = body; else if (Array.isArray(body.images)) imagesArray = body.images; else if (body.data && Array.isArray(body.data.images)) imagesArray = body.data.images; else { 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[]; } + // eslint-disable-next-line @typescript-eslint/no-explicit-any return imagesArray.map((it: any) => ({ id: it.id ?? it._id ?? `${it.filename ?? Math.random()}`, url: toAbsoluteUrl(it.path ?? it.url), @@ -84,17 +90,27 @@ export const fetchImagesApi = async (title = '') => { }; /* Delete photo (by id) */ -export const deletePhotoApi = async (id: number | string) => { - const url = `${MFS_API_BASE}/image-upload/${id}`; - const res = await fetch(url, { method: 'DELETE', headers: { accept: 'application/json' } }); +export const deletePhotoApi = async (filename: string) => { + const url = `${MFS_API_BASE}/image-upload/${filename}`; + + const res = await fetch(url, { + method: "DELETE", + headers: { accept: "application/json" }, + }); + 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) */ 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 text = await res.text(); if (!res.ok) throw new Error(text || `HTTP ${res.status}`); diff --git a/src/components/blogs/BlogPage.tsx b/src/components/blogs/BlogPage.tsx index f7405f0..0f93dda 100644 --- a/src/components/blogs/BlogPage.tsx +++ b/src/components/blogs/BlogPage.tsx @@ -61,10 +61,10 @@ const fadeInUp = keyframes` } `; -const fadeIn = keyframes` - from { opacity: 0; } - to { opacity: 1; } -`; +// const fadeIn = keyframes` +// from { opacity: 0; } +// to { opacity: 1; } +// `; const float = keyframes` 0% { transform: translateY(0px); } @@ -324,6 +324,7 @@ const BlogCard: React.FC = ({ post, index }) => { const BlogPage: React.FC = () => { const [selectedCategory, setSelectedCategory] = useState('all'); const [searchQuery, setSearchQuery] = useState(''); + // eslint-disable-next-line @typescript-eslint/no-unused-vars const [heroRef, heroInView] = useInView({ threshold: 0.1, triggerOnce: true }); const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('sm'));