diff --git a/.codex b/.codex new file mode 100644 index 0000000..e69de29 diff --git a/src/App.tsx b/src/App.tsx index 2d1237d..5622deb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,26 +1,22 @@ 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 VintageComingSoonPage from "./components/comingsoon/comingsoon"; +import DarkProductShowcase from "./components/product/product"; +import BlogPage from "./components/blogs/BlogPage"; +import WebsiteHeader from "./components/header/WebsiteHeader"; // import BlogCard from "./components/blogs/BlogCard"; -import WeddingGallery from "./WeddingGallery/WeddingGallery"; // import OtherPage from "./pages/OtherPage"; // example if you add more pages function App() { return ( + - {/* Default route */} - {/* } /> */} - } /> - - {/* Example extra routes */} - {/* } /> - } /> */} - } /> - - {/* } /> */} + } /> + } /> + } /> + } /> + } /> ); diff --git a/src/WeddingGallery/Gallery.tsx b/src/WeddingGallery/Gallery.tsx deleted file mode 100644 index 480d761..0000000 --- a/src/WeddingGallery/Gallery.tsx +++ /dev/null @@ -1,43 +0,0 @@ -// Gallery.tsx -import React from 'react'; -import SafeGrid from './SafeGrid'; -import { WeddingPhoto } from './types'; -import { Box, Card, CardContent, CardMedia, Chip, IconButton, Typography } from '@mui/material'; -import { Download } from '@mui/icons-material'; - -type Props = { - photos: WeddingPhoto[]; - onDownload?: (url: string, title?: string) => void; -}; - -const Gallery: React.FC = ({ photos, onDownload }) => { - return ( - - {photos.map((p) => ( - - - - - - onDownload?.(p.url, p.title)} sx={{ color: 'white', bgcolor: 'rgba(0,0,0,0.6)' }}> - - - - - - {p.title || 'Untitled'} - - - - {p.created_at ? new Date(p.created_at).toLocaleDateString() : p.date ? new Date(p.date).toLocaleDateString() : ''} - - - - - - ))} - - ); -}; - -export default Gallery; diff --git a/src/WeddingGallery/LightboxModal.tsx b/src/WeddingGallery/LightboxModal.tsx deleted file mode 100644 index 8b0dd39..0000000 --- a/src/WeddingGallery/LightboxModal.tsx +++ /dev/null @@ -1,592 +0,0 @@ -// LightboxModal.tsx -import React, { useEffect, useRef, useState } from 'react'; -import { - Dialog, - IconButton, - Box, - Typography, - 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; - url: string; - title?: string; - filename?: string; - created_at?: string; - naturalWidth?: number; - naturalHeight?: number; -}; - -type Props = { - open: boolean; - photos: LightboxPhoto[]; - startIndex?: number; - onClose: () => void; - startIndexCallback?: (idx: number) => void; -}; - -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) { - const [index, setIndex] = useState(startIndex); - 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); - - // For pinch - const pinchStartDist = useRef(null); - const pinchStartScale = useRef(1); - const imageRef = useRef(null); - - useEffect(() => setIndex(startIndex), [startIndex, open]); - - useEffect(() => { - window.addEventListener('keydown', onKey); - return () => window.removeEventListener('keydown', onKey); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [open, index, scale]); - - useEffect(() => startIndexCallback?.(index), [index]); - - useEffect(() => { - // 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?.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]); - - 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; - 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 (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); - } - }; - // 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(); - } - - touchDeltaX.current = 0; - touchStartX.current = 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); - } - - // 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(); - }} - > - {/* Loading indicator */} - {isLoading && ( - - - - )} - - {/* 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 */} - { - ev.stopPropagation(); - prev(); - }} - sx={{ - position: 'absolute', - left: 16, - zIndex: 50, - color: 'white', - bgcolor: 'rgba(0,0,0,0.5)', - backdropFilter: 'blur(10px)', - '&:hover': { bgcolor: 'rgba(0,0,0,0.7)' } - }} - > - - - - { - ev.stopPropagation(); - next(); - }} - sx={{ - position: 'absolute', - right: 16, - zIndex: 50, - color: 'white', - bgcolor: 'rgba(0,0,0,0.5)', - backdropFilter: 'blur(10px)', - '&:hover': { bgcolor: 'rgba(0,0,0,0.7)' } - }} - > - - - - {/* Image container */} - - - {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 */} - - - {photo.title ?? photo.filename} - - - {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 deleted file mode 100644 index 44b55ee..0000000 --- a/src/WeddingGallery/PreviewTester.tsx +++ /dev/null @@ -1,60 +0,0 @@ -// PreviewTester.tsx -import React, { useCallback, useEffect, useState } from 'react'; -import { Box, Button, Card, CardMedia, CardContent, Typography } from '@mui/material'; -import { useDropzone } from 'react-dropzone'; - -type Item = { id: string; file: File; previewUrl: string }; - -export default function PreviewTester() { - const [items, setItems] = useState([]); - - const onDrop = useCallback((acceptedFiles: File[]) => { - console.log('onDrop called, acceptedFiles:', acceptedFiles); - const now = Date.now(); - const newItems = acceptedFiles.map((f, i) => ({ - id: `${now}-${i}-${f.name}`, - file: f, - previewUrl: URL.createObjectURL(f), - })); - setItems((prev) => [...newItems, ...prev]); - }, []); - - const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop, multiple: true, accept: { 'image/*': [] } }); - - useEffect(() => { - return () => { - items.forEach((it) => { - try { - URL.revokeObjectURL(it.previewUrl); - } catch { - // ignore - } - }); - }; - }, [items]); - - - return ( - - - - {isDragActive ? 'Drop images here' : 'Drag & drop images (PreviewTester)'} - - - - {items.map((it) => ( - - - - {it.file.name} - - - ))} - - - - - - - ); -} diff --git a/src/WeddingGallery/SafeGrid.tsx b/src/WeddingGallery/SafeGrid.tsx deleted file mode 100644 index 18b4b79..0000000 --- a/src/WeddingGallery/SafeGrid.tsx +++ /dev/null @@ -1,5 +0,0 @@ -// 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 deleted file mode 100644 index 3da9481..0000000 --- a/src/WeddingGallery/UploadQueue.tsx +++ /dev/null @@ -1,98 +0,0 @@ -// UploadQueue.tsx -import React, { useCallback } from 'react'; -import { Box, Button, Card, CardContent, CardMedia, IconButton, LinearProgress, Typography } from '@mui/material'; -import { Delete, Replay } from '@mui/icons-material'; -import { FileQueueItem, WeddingPhoto } from './types'; -import { uploadFileXHR, toAbsoluteUrl } from './apis'; - -type Props = { - queue: FileQueueItem[]; - setQueue: React.Dispatch>; - newPhotoTitle?: string; - onUploaded?: (photo: WeddingPhoto) => void; -}; - -const UploadQueue: React.FC = ({ queue, setQueue, newPhotoTitle, onUploaded }) => { - const uploadSingle = useCallback( - async (item: FileQueueItem) => { - setQueue((prev) => prev.map((q) => (q.id === item.id ? { ...q, status: 'uploading', progress: 0, error: undefined } : q))); - const res = await uploadFileXHR( - item.file, - (pct) => setQueue((prev) => prev.map((q) => (q.id === item.id ? { ...q, progress: pct } : q))), - newPhotoTitle - ); - if (res.success) { - const it = res.data; - const uploadedPhoto: WeddingPhoto = { - id: it.id ?? it._id, - url: toAbsoluteUrl(it.path ?? it.url), - title: it.title ?? it.filename ?? item.file.name, - date: it.created_at ?? it.createdAt, - filename: it.filename, - path: it.path, - created_at: it.created_at ?? it.createdAt, - }; - setQueue((prev) => prev.map((q) => (q.id === item.id ? { ...q, status: 'done', progress: 100, uploadedPhoto } : q))); - onUploaded?.(uploadedPhoto); - return true; - } else { - setQueue((prev) => prev.map((q) => (q.id === item.id ? { ...q, status: 'error', error: res.error } : q))); - return false; - } - }, - [newPhotoTitle, setQueue, onUploaded] - ); - - const uploadAll = useCallback(async () => { - for (const item of queue.filter((q) => q.status === 'pending' || q.status === 'error')) { - - await uploadSingle(item); - } - }, [queue, uploadSingle]); - - const retryOne = (id: string) => { - const item = queue.find((q) => q.id === id); - if (item) uploadSingle(item); - }; - - const removeOne = (id: string) => { - const item = queue.find((q) => q.id === id); - if (item) URL.revokeObjectURL(item.previewUrl); - setQueue((prev) => prev.filter((q) => q.id !== id)); - }; - - return ( - - - - - - - {queue.map((q) => ( - - - - - {q.file.name} - - - {q.status === 'uploading' && Uploading — {q.progress}%} - {q.status === 'pending' && Pending} - {q.status === 'done' && Uploaded} - {q.status === 'error' && {q.error ?? 'Upload failed'}} - - - {q.status === 'error' && retryOne(q.id)}>} - removeOne(q.id)}> - - - - ))} - - - ); -}; - -export default UploadQueue; diff --git a/src/WeddingGallery/WeddingGallery.tsx b/src/WeddingGallery/WeddingGallery.tsx deleted file mode 100644 index 03d3a2e..0000000 --- a/src/WeddingGallery/WeddingGallery.tsx +++ /dev/null @@ -1,1544 +0,0 @@ -// WeddingGallery.tsx -import React, { JSX, useCallback, useEffect, useState } from 'react'; -import { - Alert, - Box, - Button, - Card, - CardContent, - CardMedia, - Chip, - CircularProgress, - Container, - CssBaseline, - Grid, - IconButton, - LinearProgress, - Paper, - Snackbar, - TextField, - ThemeProvider, - Toolbar, - Typography, - createTheme, - Dialog, - DialogTitle, - DialogContent, - DialogActions, - alpha, - styled, - AppBar, - Tabs, - Tab, -} from '@mui/material'; -import { - // 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; - title?: string; - filename?: string; - created_at?: string; - date?: string; - path?: string; -}; - -type FileQueueItem = { - id: string; - file: File; - previewUrl: string; - progress: number; - status: 'pending' | 'uploading' | 'done' | 'error'; - error?: string; - uploadedPhoto?: WeddingPhoto; -}; - -type FolderInfo = { - title: string; - count: number; - thumb?: string; - date?: string; - description?: string; -}; - -type StoryStep = { - title: string; - description: string; - icon: React.ReactNode; - photos?: WeddingPhoto[]; -}; - -/* ============================ - Config - ============================ */ -const MFS_API_BASE = 'https://mfs-api.midastix.com'; -const MAX_FILE_SIZE = 10 * 1024 * 1024; -const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; -const MAX_QUEUE_FILES = 30; - -/* ============================ - 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 ''; - if (path.startsWith('http://') || path.startsWith('https://')) return path; - return `${MFS_API_BASE}${path}`; -}; - -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 { - const xhr = new XMLHttpRequest(); - const url = `${MFS_API_BASE}/image-upload/single`; - xhr.open('POST', url, true); - xhr.setRequestHeader('Accept', 'application/json'); - - xhr.upload.onprogress = (ev) => { - if (!ev.lengthComputable) return; - const pct = Math.round((ev.loaded / ev.total) * 100); - onProgress(pct); - }; - - xhr.onload = () => { - if (xhr.status >= 200 && xhr.status < 300) { - 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 }); - } - } else { - const txt = xhr.responseText || `HTTP ${xhr.status}`; - resolve({ success: false, error: txt }); - } - }; - - xhr.onerror = () => resolve({ success: false, error: 'Network error' }); - - const fd = new FormData(); - 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) }); - } - }); - -const fetchImagesApi = async (title = ''): Promise => { - const url = - title && title.trim() !== '' ? `${MFS_API_BASE}/image-upload?title=${encodeURIComponent(title)}` : `${MFS_API_BASE}/image-upload`; - const res = await fetch(url, { headers: { accept: 'application/json' } }); - if (!res.ok) { - const txt = await res.text(); - 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), - title: it.title ?? it.filename ?? '', - filename: it.filename, - created_at: it.created_at ?? it.createdAt, - date: it.date, - path: it.path, - })); -}; - -const deletePhotoApi = async (id: number | string) => { - 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}`); - return txt || 'deleted'; -}; - -const deleteFolderApi = async (title: string) => { - const url = `${MFS_API_BASE}/image-upload/folder?title=${encodeURIComponent(title)}`; - 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}`); - return txt || 'folder deleted'; -}; - -/* ============================ - Theme - ============================ */ -const theme = createTheme({ - palette: { - primary: { main: '#0f766e', light: '#2dd4bf' }, - 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', - }, - }, - }, - }, -}); - -/* ============================ - Component - ============================ */ -export default function WeddingGallery(): JSX.Element { - const [photosAll, setPhotosAll] = useState([]); - const [folders, setFolders] = useState([]); - const [folderOpenTitle, setFolderOpenTitle] = useState(null); - const [folderPhotos, setFolderPhotos] = useState([]); - const [folderModalOpen, setFolderModalOpen] = useState(false); - - const [queue, setQueue] = useState([]); - const [commonTitle, setCommonTitle] = useState(''); - const [snackbar, setSnackbar] = useState<{ open: boolean; severity?: 'success' | 'error' | 'info'; message: string }>({ - open: false, - severity: 'info', - message: '', - }); - - const [deletingId, setDeletingId] = useState(null); - const [deletingFolder, setDeletingFolder] = useState(null); - - 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, - date: it.date || it.created_at, - }); - } - const entry = map.get(title)!; - entry.count += 1; - if (!entry.thumb) entry.thumb = it.url; - }); - - 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([]); - setSnackbar({ open: true, severity: 'error', message: `Failed to load: ${err?.message || err}` }); - } - }, []); - - useEffect(() => { - loadAll(); - return () => { - queue.forEach((q) => { - try { - URL.revokeObjectURL(q.previewUrl); - } catch { - //ignore - } - }); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - /* ============================ - Dropzone - ============================ */ - const onDrop = useCallback((acceptedFiles: File[]) => { - if (!acceptedFiles || acceptedFiles.length === 0) return; - - const currentCount = queue.length; - const available = Math.max(0, MAX_QUEUE_FILES - currentCount); - - if (available <= 0) { - setSnackbar({ open: true, severity: 'error', message: `Maximum ${MAX_QUEUE_FILES} files allowed.` }); - return; - } - - const acceptedToAdd = acceptedFiles.slice(0, available); - const rejectedCount = acceptedFiles.length - acceptedToAdd.length; - - const now = Date.now(); - const items: FileQueueItem[] = acceptedToAdd.map((f, i) => { - const id = `${now}-${currentCount + i}-${f.name}`; - const tooLarge = f.size > MAX_FILE_SIZE; - const invalidType = ALLOWED_TYPES.length > 0 && !ALLOWED_TYPES.includes(f.type); - return { - id, - file: f, - previewUrl: URL.createObjectURL(f), - progress: 0, - status: tooLarge || invalidType ? 'error' : 'pending', - error: tooLarge ? `File too large ${(f.size / (1024 * 1024)).toFixed(1)}MB` : invalidType ? 'Invalid type' : undefined, - } as FileQueueItem; - }); - - setQueue((prev) => [...items, ...prev]); - setShowUploadPanel(true); - - if (rejectedCount > 0) { - setSnackbar({ - open: true, - severity: 'info', - message: `Added ${acceptedToAdd.length} file(s). ${rejectedCount} file(s) were not added.`, - }); - } else { - setSnackbar({ open: true, severity: 'success', message: `Added ${acceptedToAdd.length} file(s)` }); - } - }, [queue.length]); - - const { getRootProps, getInputProps, isDragActive } = useDropzone({ - 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, - }); - - /* ============================ - Upload logic - ============================ */ - const uploadSingle = useCallback( - async (item: FileQueueItem) => { - setQueue((prev) => prev.map((q) => (q.id === item.id ? { ...q, status: 'uploading', progress: 0, error: undefined } : q))); - const res = await uploadFileXHR(item.file, (pct) => { - setQueue((prev) => prev.map((q) => (q.id === item.id ? { ...q, progress: pct } : q))); - }, commonTitle || undefined); - - if (res.success) { - const it = res.data; - const uploadedPhoto: WeddingPhoto = { - id: it.id ?? it._id, - url: toAbsoluteUrl(it.path ?? it.url), - title: it.title ?? it.filename ?? commonTitle ?? 'Untitled', - filename: it.filename, - created_at: it.created_at ?? it.createdAt, - path: it.path, - }; - setQueue((prev) => prev.map((q) => (q.id === item.id ? { ...q, status: 'done', progress: 100, uploadedPhoto } : q))); - setPhotosAll((prev) => [uploadedPhoto, ...prev]); - await loadAll(); - return true; - } else { - setQueue((prev) => prev.map((q) => (q.id === item.id ? { ...q, status: 'error', error: res.error } : q))); - console.error('upload failed', res.error); - return false; - } - }, - [commonTitle, loadAll] - ); - - const uploadAll = useCallback(async () => { - const pending = queue.filter((q) => q.status === 'pending' || q.status === 'error'); - if (!pending.length) { - setSnackbar({ open: true, severity: 'info', message: 'No files to upload' }); - return; - } - for (const item of pending) { - await uploadSingle(item); - } - setSnackbar({ open: true, severity: 'success', message: 'All photos uploaded successfully!' }); - setQueue((prev) => prev.filter((q) => q.status !== 'done')); - }, [queue, uploadSingle]); - - const retryOne = (id: string) => { - const item = queue.find((q) => q.id === id); - if (item) uploadSingle(item); - }; - - const removeOne = (id: string) => { - const item = queue.find((q) => q.id === id); - if (item) { - try { - URL.revokeObjectURL(item.previewUrl); - } catch { - //ignore - } - } - setQueue((prev) => prev.filter((q) => q.id !== id)); - }; - - /* ============================ - Folder Management - ============================ */ - const openFolder = async (title: string) => { - setFolderOpenTitle(title); - try { - 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}` }); - } - }; - - const handleDeletePhoto = async (id: number | string) => { - // if (!confirm('Delete this photo? This cannot be undone.')) return; - setDeletingId(id); - try { - await deletePhotoApi(id); - setFolderPhotos((prev) => prev.filter((p) => p.id !== id)); - 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}` }); - } finally { - setDeletingId(null); - } - }; - - const handleDeleteFolder = async (title: string) => { - 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}` }); - } finally { - setDeletingFolder(null); - } - }; - - /* ============================ - UI - ============================ */ - return ( - - - - - - - - {/* Header */} - - - - - - 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' }} - > - - - - - - - - {/* Tab Content */} - {activeTab === 0 && ( - - {/* Hero Section */} - - - Our Wedding Albums - - - Preserving precious memories of our special day - - - - {/* Upload Section (Collapsible) */} - {showUploadPanel && ( - - - - 📸 Upload Photos - - setShowUploadPanel(false)} size="small"> - - - - - - - - - - - {isDragActive ? 'Drop photos here' : 'Drag & drop photos here'} - - - or click to browse files - - - - - - - - - - - {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' }}> - - - - - - - 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' }, - }} - > - - - - - - - ))} - - )} - - - )} - - - {/* Album Modal */} - setFolderModalOpen(false)} - fullWidth - maxWidth="lg" - PaperProps={{ - sx: { - borderRadius: 3, - maxHeight: '90vh', - }, - }} - > - - - - - - {folderOpenTitle} - - {folderPhotos.length} photos - - - - - - - - {folderPhotos.length === 0 ? ( - - - No photos in this album - - - ) : ( - - {folderPhotos.map((p, idx) => ( - - - - handleDeletePhoto(p.id)} - disabled={deletingId === p.id} - sx={{ - color: 'white', - bgcolor: 'rgba(239, 68, 68, 0.9)', - '&:hover': { bgcolor: '#ef4444' }, - }} - > - - - { - try { - const a = document.createElement('a'); - a.href = p.url; - 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' }); - } - }} - 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)} - /> - - {/* 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 deleted file mode 100644 index 615a03d..0000000 --- a/src/WeddingGallery/apis.ts +++ /dev/null @@ -1,118 +0,0 @@ -// apis.ts -const MFS_API_BASE = 'https://mfs-api.midastix.com'; - -export const toAbsoluteUrl = (path?: string) => { - if (!path) return ''; - if (path.startsWith('http://') || path.startsWith('https://')) return path; - return `${MFS_API_BASE}${path}`; -}; - -/** - * uploadFileXHR(file, onProgress, title?) - * returns { success, data?, error? } - */ -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 { - const xhr = new XMLHttpRequest(); - const url = `${MFS_API_BASE}/image-upload/single`; - xhr.open('POST', url, true); - xhr.setRequestHeader('Accept', 'application/json'); - - xhr.upload.onprogress = (ev) => { - if (!ev.lengthComputable) return; - const pct = Math.round((ev.loaded / ev.total) * 100); - onProgress(pct); - }; - - xhr.onload = () => { - if (xhr.status >= 200 && xhr.status < 300) { - 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 }); - } - } else { - const txt = xhr.responseText || `HTTP ${xhr.status}`; - resolve({ success: false, error: txt }); - } - }; - - xhr.onerror = () => resolve({ success: false, error: 'Network error' }); - - const fd = new FormData(); - 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) }); - } - }); - -/* tolerant fetchImages mapping */ -export const fetchImagesApi = async (title = '') => { - const url = - title && title.trim() !== '' ? `${MFS_API_BASE}/image-upload?title=${encodeURIComponent(title)}` : `${MFS_API_BASE}/image-upload`; - const res = await fetch(url, { headers: { accept: 'application/json' } }); - if (!res.ok) { - const txt = await res.text(); - 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), - title: it.title ?? it.filename ?? '', - date: it.created_at ?? it.createdAt ?? it.date ?? '', - filename: it.filename, - path: it.path, - created_at: it.created_at ?? it.createdAt, - })); -}; - -/* Delete photo (by id) */ -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"; -}; - - -/* Delete folder by title (assumed endpoint) */ -export const deleteFolderApi = async (title: string) => { - 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}`); - return text || 'folder deleted'; -}; diff --git a/src/WeddingGallery/folderModel.tsx b/src/WeddingGallery/folderModel.tsx deleted file mode 100644 index c31bfc3..0000000 --- a/src/WeddingGallery/folderModel.tsx +++ /dev/null @@ -1,53 +0,0 @@ -// FolderModal.tsx -import React from 'react'; -import { Box, Dialog, DialogTitle, DialogContent, DialogActions, Button, Grid, Card, CardMedia, IconButton, Typography } from '@mui/material'; -import DeleteIcon from '@mui/icons-material/Delete'; -import { WeddingPhoto } from './types'; - -type Props = { - open: boolean; - onClose: () => void; - photos: WeddingPhoto[]; - onDeletePhoto: (id: number | string) => Promise; - deletingId?: number | string | null; -}; - -const FolderModal: React.FC = ({ open, onClose, photos, onDeletePhoto, deletingId }) => { - return ( - - Manage Photos ({photos.length}) - - {photos.length === 0 ? ( - No photos in this event. - ) : ( - - {photos.map((p) => ( - - - - - onDeletePhoto(p.id)} - disabled={deletingId === p.id} - > - - - - - {p.filename ?? p.title} - - - - ))} - - )} - - - - - - ); -}; - -export default FolderModal; diff --git a/src/WeddingGallery/foldergrid.tsx b/src/WeddingGallery/foldergrid.tsx deleted file mode 100644 index cb84ea0..0000000 --- a/src/WeddingGallery/foldergrid.tsx +++ /dev/null @@ -1,54 +0,0 @@ -// FolderGrid.tsx -import React from 'react'; -import SafeGrid from './SafeGrid'; -import { FolderInfo } from './types'; -import { Box, Card, CardContent, CardMedia, IconButton, Paper, Typography } from '@mui/material'; -import DeleteIcon from '@mui/icons-material/Delete'; - -type Props = { - folders: FolderInfo[]; - onOpen: (title: string) => void; - onDeleteFolder: (title: string) => void; -}; - -const FolderGrid: React.FC = ({ folders, onOpen, onDeleteFolder }) => { - if (!folders.length) { - return ( - - No events found - Upload photos to create events (folders). - - ); - } - - return ( - - {folders.map((f) => ( - - - onOpen(f.title)}> - - - - - - - {f.title} - {f.count} photos - - - { e.stopPropagation(); onDeleteFolder(f.title); }}> - - - - - - - - - ))} - - ); -}; - -export default FolderGrid; diff --git a/src/WeddingGallery/types.tsx b/src/WeddingGallery/types.tsx deleted file mode 100644 index 15ea034..0000000 --- a/src/WeddingGallery/types.tsx +++ /dev/null @@ -1,26 +0,0 @@ -// types.ts -export type WeddingPhoto = { - id: number | string; - url: string; - title?: string; - date?: string; - filename?: string; - path?: string; - created_at?: string; -}; - -export type FileQueueItem = { - id: string; - file: File; - previewUrl: string; - progress: number; - status: 'pending' | 'uploading' | 'done' | 'error'; - error?: string; - uploadedPhoto?: WeddingPhoto; -}; - -export type FolderInfo = { - title: string; - count: number; - thumb?: string; -}; diff --git a/src/components/blogs/BlogPage.tsx b/src/components/blogs/BlogPage.tsx index 0f93dda..6b119d5 100644 --- a/src/components/blogs/BlogPage.tsx +++ b/src/components/blogs/BlogPage.tsx @@ -1,781 +1,605 @@ -import React, { useState, useMemo } from 'react'; +import React, { useMemo, useState } from "react"; import { - Box, Container, Grid, Typography, TextField, Chip, IconButton, - InputAdornment, Card, CardContent, CardMedia, Button, Avatar, - useTheme, useMediaQuery -} from '@mui/material'; -import { useInView } from 'react-intersection-observer'; -import InstagramIcon from '@mui/icons-material/Instagram'; -import FacebookIcon from '@mui/icons-material/Facebook'; -import TwitterIcon from '@mui/icons-material/Twitter'; -import SearchIcon from '@mui/icons-material/Search'; -import { keyframes } from '@emotion/react'; -import { styled } from '@mui/system'; -import { useNavigate } from 'react-router-dom'; + Avatar, + Box, + Button, + Card, + CardContent, + CardMedia, + Chip, + Container, + Grid, + InputAdornment, + Stack, + TextField, + Typography, +} from "@mui/material"; +import SearchIcon from "@mui/icons-material/Search"; +import ArrowOutwardIcon from "@mui/icons-material/ArrowOutward"; +import AutoStoriesOutlinedIcon from "@mui/icons-material/AutoStoriesOutlined"; +import PaletteOutlinedIcon from "@mui/icons-material/PaletteOutlined"; +import FavoriteBorderOutlinedIcon from "@mui/icons-material/FavoriteBorderOutlined"; +import { keyframes } from "@emotion/react"; +import { styled } from "@mui/system"; +import { useNavigate } from "react-router-dom"; +import blogData from "../../data/blogs.json"; +import type { BlogData, BlogPost } from "../../types/blogs"; -// Define TypeScript interfaces -interface Author { - name: string; - avatar: string; - role: string; -} +const data = blogData as BlogData; -interface BlogPost { - id: string; - title: string; - excerpt: string; - content: string; - image: string; - author: Author; - date: string; - readTime: string; - tags: string[]; - category: string; -} - -interface BlogCardProps { - post: BlogPost; - index: number; -} - -// Color palette const colors = { - paper: '#F5F5EF', - ink: '#1A2526', - accent: '#D4A017', - secondary: '#7B4F3A', - highlight: '#FFF4CC', - border: '#B89A6E', - dark: '#3A1F0F', + ivory: "#fffaf5", + mist: "#f8f1ea", + blush: "#f4ddd4", + peach: "#efb7a2", + coral: "#d9826f", + berry: "#7f4b5d", + plum: "#4f2f42", + gold: "#c99652", + ink: "#2f2430", }; -// Animations -const fadeInUp = keyframes` - from { - opacity: 0; - transform: translateY(30px); - } - to { - opacity: 1; - transform: translateY(0); - } +const rise = keyframes` + from { opacity: 0; transform: translateY(28px); } + to { opacity: 1; transform: translateY(0); } `; -// const fadeIn = keyframes` -// from { opacity: 0; } -// to { opacity: 1; } -// `; - const float = keyframes` 0% { transform: translateY(0px); } - 50% { transform: translateY(-8px); } + 50% { transform: translateY(-10px); } 100% { transform: translateY(0px); } `; -const drawUnderline = keyframes` - 0% { width: 0; } - 100% { width: 100%; } +const glow = keyframes` + 0% { box-shadow: 0 0 0 0 rgba(217, 130, 111, 0.18); } + 70% { box-shadow: 0 0 0 18px rgba(217, 130, 111, 0); } + 100% { box-shadow: 0 0 0 0 rgba(217, 130, 111, 0); } `; -const pulse = keyframes` - 0% { transform: scale(1); } - 50% { transform: scale(1.05); } - 100% { transform: scale(1); } -`; +const PageShell = styled(Box)({ + minHeight: "100vh", + background: + `radial-gradient(circle at top left, ${colors.ivory} 0%, ${colors.mist} 42%, #f7f5f3 100%)`, + color: colors.ink, + position: "relative", + overflow: "hidden", +}); -// Styled Components -interface AnimatedSectionProps { - delay?: string; -} - -const AnimatedSection = styled(Box, { - shouldForwardProp: (prop) => prop !== 'delay', -})(({ delay = '0s' }) => ({ - opacity: 0, - animation: `${fadeInUp} 0.8s forwards`, - animationDelay: delay, +const GlowOrb = styled(Box)<{ + size: number; + top?: string; + right?: string; + left?: string; + bottom?: string; + delay?: string; +}>(({ size, top, right, left, bottom, delay = "0s" }) => ({ + position: "absolute", + width: size, + height: size, + top, + right, + left, + bottom, + borderRadius: "50%", + background: + "radial-gradient(circle at 30% 30%, rgba(255,255,255,0.9), rgba(239,183,162,0.28) 55%, rgba(127,75,93,0.12) 100%)", + filter: "blur(6px)", + animation: `${float} 9s ease-in-out infinite`, + animationDelay: delay, + pointerEvents: "none", })); -const VintageUnderline = styled(Box)({ - position: 'relative', - display: 'inline-block', - '&::after': { - content: '""', - position: 'absolute', - bottom: '-4px', - left: 0, - width: '0', - height: '1.5px', - backgroundColor: colors.accent, - animation: `${drawUnderline} 1.2s forwards`, - animationDelay: '0.3s', - }, +const FadeBlock = styled(Box)<{ delay?: string }>(({ delay = "0s" }) => ({ + opacity: 0, + animation: `${rise} 0.85s ease forwards`, + animationDelay: delay, +})); + +const GlassPanel = styled(Box)({ + background: "rgba(255, 250, 245, 0.72)", + backdropFilter: "blur(16px)", + border: `1px solid ${colors.peach}66`, + boxShadow: "0 22px 50px rgba(79, 47, 66, 0.08)", }); -const FloatingElement = styled(Box)({ - animation: `${float} 6s ease-in-out infinite`, +const EditorialCard = styled(Card)({ + height: "100%", + borderRadius: "28px", + background: "rgba(255,250,245,0.82)", + border: `1px solid ${colors.peach}55`, + boxShadow: "0 18px 36px rgba(79, 47, 66, 0.08)", + transition: "transform 0.35s ease, box-shadow 0.35s ease", + overflow: "hidden", + "&:hover": { + transform: "translateY(-8px)", + boxShadow: "0 28px 52px rgba(79, 47, 66, 0.14)", + }, + "&:hover .journal-card-image": { + transform: "scale(1.06)", + }, }); -// BlogCard Component -const BlogCard: React.FC = ({ post, index }) => { - const navigate = useNavigate(); - const [ref, inView] = useInView({ threshold: 0.2, triggerOnce: true }); - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down('sm')); +const formatDate = (date: string) => + new Date(date).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }); - const handleReadMore = () => { - navigate(`/blog/${post.id}`); - }; +const BlogPage: React.FC = () => { + const navigate = useNavigate(); + const [selectedCategory, setSelectedCategory] = useState("all"); + const [searchQuery, setSearchQuery] = useState(""); - return ( - { + return data.posts.filter((post) => { + const matchesCategory = selectedCategory === "all" || post.category === selectedCategory; + const query = searchQuery.trim().toLowerCase(); + const matchesSearch = + query === "" || + post.title.toLowerCase().includes(query) || + post.excerpt.toLowerCase().includes(query) || + post.tags.some((tag) => tag.toLowerCase().includes(query)); + + return matchesCategory && matchesSearch; + }); + }, [searchQuery, selectedCategory]); + + const featuredPost = filteredPosts[0] ?? data.posts[0]; + const gridPosts = filteredPosts.filter((post) => post.id !== featuredPost?.id); + + const openPost = (post: BlogPost) => { + navigate(`/blog/${post.id}`); + }; + + return ( + + + + + + + + - - + + + + Journal + + + Color-soaked stories from the craft world. + + + The journal now shares the same soft editorial mood as the new site header: warm blush tones, + heirloom plum contrast, and a more premium storytelling feel. + + + + } + label={`${data.posts.length} stories`} sx={{ - transition: 'transform 0.5s ease', - objectFit: 'cover', - width: '100%', + px: 1, + height: 38, + borderRadius: "999px", + background: `${colors.blush}`, + color: colors.plum, + border: `1px solid ${colors.peach}77`, }} - /> - + } + label="Aesthetic journal" sx={{ - position: 'absolute', + px: 1, + height: 38, + borderRadius: "999px", + background: "rgba(255,255,255,0.5)", + color: colors.plum, + border: `1px solid ${colors.peach}77`, + }} + /> + } + label="Slow-made inspiration" + sx={{ + px: 1, + height: 38, + borderRadius: "999px", + background: "rgba(255,255,255,0.5)", + color: colors.plum, + border: `1px solid ${colors.peach}77`, + }} + /> + + + + + + + + + Featured story + + + {featuredPost.title} + + + {featuredPost.excerpt} + + + + + + + + + + + + setSearchQuery(event.target.value)} + InputProps={{ + startAdornment: ( + + + + ), + }} + sx={{ + "& .MuiOutlinedInput-root": { + borderRadius: "18px", + background: "rgba(255,255,255,0.52)", + color: colors.plum, + "& fieldset": { + borderColor: `${colors.peach}88`, + }, + "&:hover fieldset": { + borderColor: colors.coral, + }, + "&.Mui-focused fieldset": { + borderColor: colors.plum, + }, + }, + }} + /> + + + setSelectedCategory("all")} + sx={{ + borderRadius: "999px", + background: + selectedCategory === "all" + ? `linear-gradient(135deg, ${colors.plum}, ${colors.berry})` + : "rgba(255,255,255,0.55)", + color: selectedCategory === "all" ? colors.ivory : colors.plum, + border: `1px solid ${colors.peach}88`, + }} + /> + {data.categories.map((category) => ( + setSelectedCategory(category)} + sx={{ + borderRadius: "999px", + background: + selectedCategory === category + ? `linear-gradient(135deg, ${colors.plum}, ${colors.berry})` + : "rgba(255,255,255,0.55)", + color: selectedCategory === category ? colors.ivory : colors.plum, + border: `1px solid ${colors.peach}88`, + }} + /> + ))} + + + + + + {featuredPost && ( + + + + Featured Reading + + + + + + + + + + + + + + + + {featuredPost.title} + + + + {featuredPost.excerpt} + + + + + + {featuredPost.author.name} + + {featuredPost.author.role} • {formatDate(featuredPost.date)} + + + + + + + + + + + + + + )} + + + + + More Stories + + + Browse the rest of the journal in the refreshed palette. + + + + + + {gridPosts.map((post, index) => ( + + + + + + - + borderRadius: "999px", + background: "rgba(255,250,245,0.88)", + color: colors.plum, + border: `1px solid ${colors.peach}88`, + }} + /> + - - - + + - - - {post.author.name} + sx={{ width: 38, height: 38, border: `2px solid ${colors.blush}` }} + /> + + + {post.author.name} - - {post.author.role} + + {formatDate(post.date)} • {post.readTime} - - + + - - {post.title} - - - - {post.excerpt} - - - - {post.date} • {post.readTime} + {post.title} - - + + {post.excerpt} + - - {post.tags.map((tag: string, i: number) => ( + + {post.tags.map((tag) => ( - ))} - - - - ); + ))} + + + + + + + + ))} + + + {filteredPosts.length === 0 && ( + + + + No stories found + + + Try another search term or switch to a different category. + + + + )} + + + ); }; -// BlogPage Component -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')); - - // Mock data - const blogData = { - posts: [ - { - id: "1", - title: "The Art of Hand Block Printing", - excerpt: "Discover the ancient technique of hand block printing that has been passed down through generations of Indian artisans.", - content: "Full content would be here...", - image: "https://images.pexels.com/photos/6011599/pexels-photo-6011599.jpeg?auto=compress&cs=tinysrgb&w=500", - author: { - name: "Priya Sharma", - avatar: "https://images.pexels.com/photos/3762800/pexels-photo-3762800.jpeg?auto=compress&cs=tinysrgb&w=500", - role: "Textile Conservationist" - }, - date: "2023-10-15", - readTime: "5 min read", - tags: ["Textiles", "Craft", "Heritage"], - category: "Artisan Techniques" - }, - { - id: "2", - title: "Natural Dyes: Colors from Nature", - excerpt: "Explore how traditional Indian artisans extract vibrant colors from plants, minerals, and other natural sources.", - content: "Full content would be here...", - image: "https://images.pexels.com/photos/1375736/pexels-photo-1375736.jpeg?auto=compress&cs=tinysrgb&w=500", - author: { - name: "Rajiv Mehta", - avatar: "https://images.pexels.com/photos/2379004/pexels-photo-2379004.jpeg?auto=compress&cs=tinysrgb&w=500", - role: "Natural Dye Expert" - }, - date: "2023-09-22", - readTime: "7 min read", - tags: ["Eco-friendly", "Sustainability", "Natural"], - category: "Sustainable Practices" - }, - { - id: "3", - title: "The Weavers of Varanasi", - excerpt: "A journey into the world of Varanasi's master weavers who create the exquisite Banarasi silk sarees.", - content: "Full content would be here...", - image: "https://images.pexels.com/photos/942803/pexels-photo-942803.jpeg?auto=compress&cs=tinysrgb&w=500", - author: { - name: "Anjali Patel", - avatar: "https://images.pexels.com/photos/3785077/pexels-photo-3785077.jpeg?auto=compress&cs=tinysrgb&w=500", - role: "Textile Historian" - }, - date: "2023-08-30", - readTime: "8 min read", - tags: ["Weaving", "Silk", "Heritage"], - category: "Artisan Stories" - }, - { - id: "4", - title: "Reviving Ancient Embroidery Techniques", - excerpt: "How contemporary designers are working with artisans to preserve and modernize traditional embroidery methods.", - content: "Full content would be here...", - image: "https://images.pexels.com/photos/6347892/pexels-photo-6347892.jpeg?auto=compress&cs=tinysrgb&w=500", - author: { - name: "Sanjay Kumar", - avatar: "https://images.pexels.com/photos/2182970/pexels-photo-2182970.jpeg?auto=compress&cs=tinysrgb&w=500", - role: "Fashion Designer" - }, - date: "2023-08-15", - readTime: "6 min read", - tags: ["Embroidery", "Design", "Revival"], - category: "Design Innovation" - }, - { - id: "5", - title: "Pottery Traditions of Rajasthan", - excerpt: "Exploring the centuries-old pottery techniques that continue to thrive in the desert state.", - content: "Full content would be here...", - image: "https://images.pexels.com/photos/4110012/pexels-photo-4110012.jpeg?auto=compress&cs=tinysrgb&w=500", - author: { - name: "Vikram Singh", - avatar: "https://images.pexels.com/photos/220453/pexels-photo-220453.jpeg?auto=compress&cs=tinysrgb&w=500", - role: "Cultural Anthropologist" - }, - date: "2023-07-28", - readTime: "9 min read", - tags: ["Pottery", "Tradition", "Rajasthan"], - category: "Cultural Heritage" - }, - { - id: "6", - title: "Sustainable Fashion Revolution", - excerpt: "How traditional Indian textiles are leading the way in sustainable fashion practices worldwide.", - content: "Full content would be here...", - image: "https://images.pexels.com/photos/4947734/pexels-photo-4947734.jpeg?auto=compress&cs=tinysrgb&w=500", - author: { - name: "Meera Desai", - avatar: "https://images.pexels.com/photos/3756678/pexels-photo-3756678.jpeg?auto=compress&cs=tinysrgb&w=500", - role: "Fashion Entrepreneur" - }, - date: "2023-07-10", - readTime: "7 min read", - tags: ["Fashion", "Sustainability", "Innovation"], - category: "Sustainable Practices" - } - ], - categories: [ - "Artisan Techniques", - "Sustainable Practices", - "Artisan Stories", - "Design Innovation", - "Cultural Heritage" - ], - tags: [ - "Textiles", - "Craft", - "Heritage", - "Eco-friendly", - "Sustainability", - "Natural", - "Weaving", - "Silk", - "Embroidery", - "Design", - "Revival" - ] - }; - - const filteredPosts = useMemo(() => { - return blogData.posts.filter(post => { - const matchesCategory = selectedCategory === 'all' || post.category === selectedCategory; - const matchesSearch = searchQuery === '' || - post.title.toLowerCase().includes(searchQuery.toLowerCase()) || - post.excerpt.toLowerCase().includes(searchQuery.toLowerCase()) || - post.tags.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase())); - - return matchesCategory && matchesSearch; - }); - }, [selectedCategory, searchQuery]); - - return ( - - {/* Header Section */} - - - - - - - - Journal - - - - Stories of Craft, Culture & Heritage - - - - - - - - - - - - - - - - - - - - - setSearchQuery(e.target.value)} - InputProps={{ - startAdornment: ( - - - - ), - sx: { - fontFamily: '"Cormorant Garamond", serif', - '& .MuiOutlinedInput-root': { - '& fieldset': { - borderColor: colors.border, - }, - '&:hover fieldset': { - borderColor: colors.accent, - }, - '&.Mui-focused fieldset': { - borderColor: colors.accent, - }, - }, - } - }} - sx={{ mb: 4 }} - /> - - - - - setSelectedCategory('all')} - variant={selectedCategory === 'all' ? "filled" : "outlined"} - sx={{ - color: selectedCategory === 'all' ? colors.ink : colors.secondary, - backgroundColor: selectedCategory === 'all' ? colors.accent : 'transparent', - borderColor: colors.border, - fontFamily: '"Cormorant Garamond", serif', - '&:hover': { - backgroundColor: selectedCategory === 'all' ? colors.accent : `${colors.highlight}80` - } - }} - /> - {blogData.categories.map((category: string) => ( - setSelectedCategory(category)} - variant={selectedCategory === category ? "filled" : "outlined"} - sx={{ - color: selectedCategory === category ? colors.ink : colors.secondary, - backgroundColor: selectedCategory === category ? colors.accent : 'transparent', - borderColor: colors.border, - fontFamily: '"Cormorant Garamond", serif', - '&:hover': { - backgroundColor: selectedCategory === category ? colors.accent : `${colors.highlight}80` - } - }} - /> - ))} - - - - - - {/* Blog Posts Grid */} - - - {filteredPosts.map((post: BlogPost, index: number) => ( - - - - ))} - - - {filteredPosts.length === 0 && ( - - - No articles found - - - Try a different search or category - - - )} - - - {/* Footer */} - - - - - The Craft Chronicle - - - Celebrating the stories, techniques, and people behind traditional crafts and heritage arts. - - - - - - - - - - - - - - © {new Date().getFullYear()} The Craft Chronicle. All rights reserved. - - - - - - ); -}; - -export default BlogPage; \ No newline at end of file +export default BlogPage; diff --git a/src/components/header/StoryHeader.tsx b/src/components/header/StoryHeader.tsx new file mode 100644 index 0000000..42befbf --- /dev/null +++ b/src/components/header/StoryHeader.tsx @@ -0,0 +1,488 @@ +import React, { useEffect, useState } from "react"; +import { Box, Button, Chip, Container, Stack, Typography } from "@mui/material"; +import { keyframes } from "@emotion/react"; +import { styled } from "@mui/system"; +import heroImage from "../../assets/solo1.jpg"; +import accentImage from "../../assets/bag2.jpg"; +import secondHeroImage from "../../assets/solo2.jpg"; +import thirdHeroImage from "../../assets/bag1.jpg"; +import logo from "../../assets/logobgremove.png"; + +const colors = { + mist: "#f8f1ea", + blush: "#f4ddd4", + peach: "#efb7a2", + coral: "#d9826f", + berry: "#7f4b5d", + plum: "#4f2f42", + gold: "#c99652", + ivory: "#fffaf5", +}; + +const drift = keyframes` + 0% { transform: translate3d(0, 0, 0) scale(1); } + 50% { transform: translate3d(0, -18px, 0) scale(1.04); } + 100% { transform: translate3d(0, 0, 0) scale(1); } +`; + +const rise = keyframes` + from { opacity: 0; transform: translateY(28px); } + to { opacity: 1; transform: translateY(0); } +`; + +const scaleIn = keyframes` + from { transform: scale(0.8); opacity: 0; } + to { transform: scale(1); opacity: 1; } +`; + +const rotateIn = keyframes` + from { transform: rotate(5deg) scale(0.9); opacity: 0; } + to { transform: rotate(0deg) scale(1); opacity: 1; } +`; + +const flipIn = keyframes` + 0% { transform: perspective(400px) rotateY(90deg); opacity: 0; } + 100% { transform: perspective(400px) rotateY(0deg); opacity: 1; } +`; + +const fadeInImage = keyframes` + from { opacity: 0; } + to { opacity: 1; } +`; + +const fadeOutImage = keyframes` + from { opacity: 1; } + to { opacity: 0; } +`; + +const float = keyframes` + 0% { transform: translateY(0px); } + 50% { transform: translateY(-10px); } + 100% { transform: translateY(0px); } +`; + +const glow = keyframes` + 0% { box-shadow: 0 0 0 0 rgba(201, 150, 82, 0.35); } + 70% { box-shadow: 0 0 0 18px rgba(201, 150, 82, 0); } + 100% { box-shadow: 0 0 0 0 rgba(201, 150, 82, 0); } +`; + +const shimmer = keyframes` + 0% { transform: translateX(-120%); } + 100% { transform: translateX(120%); } +`; + +const HeroShell = styled(Box)({ + position: "relative", + overflow: "hidden", + borderRadius: "36px", + background: `linear-gradient(135deg, ${colors.ivory} 0%, ${colors.blush} 42%, ${colors.mist} 100%)`, + border: `1px solid ${colors.peach}55`, + boxShadow: "0 30px 70px rgba(79, 47, 66, 0.12)", +}); + +const GlassCard = styled(Box)({ + position: "relative", + borderRadius: "28px", + overflow: "hidden", + background: "rgba(255, 250, 245, 0.72)", + backdropFilter: "blur(14px)", + border: "1px solid rgba(255, 255, 255, 0.55)", + boxShadow: "0 24px 50px rgba(79, 47, 66, 0.12)", +}); + +const VintageImageFrame = styled(Box)({ + border: `12px solid ${colors.ivory}`, + boxShadow: `8px 8px 0 ${colors.berry}45, 0 4px 8px rgba(0,0,0,0.1)`, + position: "relative", + overflow: "hidden", + borderRadius: "28px", + "&::before": { + content: '""', + position: "absolute", + top: "-15px", + left: "-15px", + right: "-15px", + bottom: "-15px", + border: `1px solid ${colors.peach}88`, + zIndex: -1, + opacity: 0.7, + borderRadius: "34px", + }, +}); + +const PrimaryButton = styled(Button)({ + borderRadius: "999px", + padding: "14px 28px", + textTransform: "none", + fontSize: "1rem", + fontWeight: 600, + letterSpacing: "0.02em", + background: `linear-gradient(135deg, ${colors.plum}, ${colors.berry})`, + color: colors.ivory, + boxShadow: "0 16px 34px rgba(79, 47, 66, 0.24)", + animation: `${glow} 3.2s infinite`, + "&:hover": { + background: `linear-gradient(135deg, ${colors.berry}, ${colors.coral})`, + transform: "translateY(-2px)", + }, +}); + +const SecondaryButton = styled(Button)({ + borderRadius: "999px", + padding: "14px 28px", + textTransform: "none", + fontSize: "1rem", + fontWeight: 600, + color: colors.plum, + border: `1px solid ${colors.peach}`, + background: "rgba(255,255,255,0.45)", + "&:hover": { + background: "rgba(255,255,255,0.8)", + borderColor: colors.gold, + }, +}); + +const RevealBox = styled(Box)({ + opacity: 0, + animation: `${rise} 0.9s ease forwards`, +}); + +const FloatingOrb = styled(Box)<{ + delay?: string; + size: number; + top?: string; + left?: string; + right?: string; + bottom?: string; +}>(({ delay = "0s", size, top, left, right, bottom }) => ({ + position: "absolute", + width: size, + height: size, + top, + left, + right, + bottom, + borderRadius: "50%", + background: + "radial-gradient(circle at 30% 30%, rgba(255,255,255,0.8), rgba(217,130,111,0.18) 55%, rgba(127,75,93,0.12) 100%)", + filter: "blur(2px)", + animation: `${drift} 8s ease-in-out infinite`, + animationDelay: delay, +})); + +const StoryHeader: React.FC = () => { + const heroImages = [heroImage, secondHeroImage, thirdHeroImage, accentImage]; + const heroAnimations = [scaleIn, rotateIn, flipIn]; + const [currentHeroImage, setCurrentHeroImage] = useState(0); + const [previousHeroImage, setPreviousHeroImage] = useState(null); + + useEffect(() => { + const interval = setInterval(() => { + setCurrentHeroImage((prev) => { + setPreviousHeroImage(prev); + return (prev + 1) % heroImages.length; + }); + }, 8000); + + return () => clearInterval(interval); + }, [heroImages.length]); + + useEffect(() => { + if (previousHeroImage === null) { + return; + } + + const timeout = setTimeout(() => { + setPreviousHeroImage(null); + }, 2000); + + return () => clearTimeout(timeout); + }, [previousHeroImage]); + + return ( + + + + + + + + + + + + + + + + THE KALAWATI + + + Artisanal Storytelling Goods + + + + + + {["Hand-dyed", "Slow-made", "Soulful color"].map((item) => ( + + ))} + + + + + + + + A poetic storefront for crafted living + + + Stories woven into every beautiful piece. + + + Build desire through color, texture, and memory. This header is designed to feel cinematic, + feminine, and premium for a story-led product brand. + + + + Explore the Collection + Read Our Story + + + + {[ + { value: "24+", label: "curated handcrafted drops" }, + { value: "8", label: "signature color stories" }, + { value: "100%", label: "visual-first storytelling" }, + ].map((item) => ( + + + {item.value} + + {item.label} + + ))} + + + + + + + {previousHeroImage !== null && ( + + + + )} + + + + + + + + {/* */} + + + + + Featured chapter + + + Dusty rose, gold accents, soft heirloom drama + + + Layered cards and floating light create the feeling of a living editorial spread instead of a + standard ecommerce banner. + + + + + + + + + ); +}; + +export default StoryHeader; diff --git a/src/components/header/WebsiteHeader.tsx b/src/components/header/WebsiteHeader.tsx new file mode 100644 index 0000000..6bab3b1 --- /dev/null +++ b/src/components/header/WebsiteHeader.tsx @@ -0,0 +1,275 @@ +import React from "react"; +import { + AppBar, + Box, + Button, + Container, + IconButton, + Menu, + MenuItem, + Toolbar, + Typography, +} from "@mui/material"; +import MenuIcon from "@mui/icons-material/Menu"; +import InstagramIcon from "@mui/icons-material/Instagram"; +import WhatsAppIcon from "@mui/icons-material/WhatsApp"; +import PinterestIcon from "@mui/icons-material/Pinterest"; +import StorefrontIcon from "@mui/icons-material/Storefront"; +import LinkedInIcon from "@mui/icons-material/LinkedIn"; +import { keyframes } from "@emotion/react"; +import { styled } from "@mui/system"; +import { Link as RouterLink, useLocation } from "react-router-dom"; +import logo from "../../assets/logobgremove.png"; + +const colors = { + ivory: "#fffaf5", + blush: "#f4ddd4", + peach: "#efb7a2", + coral: "#d9826f", + berry: "#7f4b5d", + plum: "#4f2f42", + gold: "#c99652", +}; + +const shimmer = keyframes` + 0% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } + 100% { background-position: 0% 50%; } +`; + +const GlassBar = styled(AppBar)({ + background: + "linear-gradient(120deg, rgba(255,250,245,0.94), rgba(244,221,212,0.88), rgba(248,241,234,0.94))", + backdropFilter: "blur(18px)", + borderBottom: `1px solid ${colors.peach}66`, + boxShadow: "0 18px 40px rgba(79, 47, 66, 0.1)", + color: colors.plum, +}); + +const bounce = keyframes` + 0%, 20%, 50%, 80%, 100% { transform: translateY(0); } + 40% { transform: translateY(-8px); } + 60% { transform: translateY(-4px); } +`; + +const SocialDock = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "10px", + padding: "8px 12px", + borderRadius: "999px", + background: "rgba(255,255,255,0.35)", + border: `1px solid ${colors.peach}66`, + boxShadow: "0 12px 28px rgba(79, 47, 66, 0.08)", +}); + +const DesktopNav = styled(Box)({ + display: "flex", + alignItems: "center", + gap: "26px", + padding: "0 10px", +}); + +const navItems = [ + { label: "Home", to: "/" }, + { label: "Shop", to: "/home" }, + { label: "Journal", to: "/journal" }, + { label: "Coming Soon", to: "/coming-soon" }, +]; + +const socialLinks = [ + { label: "Instagram", href: "https://instagram.com/thekalawati", icon: InstagramIcon }, + { label: "WhatsApp", href: "https://wa.me/1234567890", icon: WhatsAppIcon }, + { label: "Pinterest", href: "https://pinterest.com/thekalawati", icon: PinterestIcon }, + { label: "Etsy", href: "https://etsy.com/shop/thekalawati", icon: StorefrontIcon }, + { label: "LinkedIn", href: "https://linkedin.com/company/thekalawati", icon: LinkedInIcon }, +]; + +const isActivePath = (pathname: string, href: string) => { + if (href === "/") { + return pathname === "/"; + } + + return pathname === href || pathname.startsWith(`${href}/`); +}; + +const WebsiteHeader: React.FC = () => { + const location = useLocation(); + const [menuAnchor, setMenuAnchor] = React.useState(null); + + const closeMenu = () => setMenuAnchor(null); + + return ( + <> + + + + + + + + THE KALAWATI + + + Storytelling Store + + + + + + {navItems.map((item) => ( + + ))} + + + + {socialLinks.map((item, index) => { + const Icon = item.icon; + + return ( + + + + ); + })} + + + setMenuAnchor(event.currentTarget)} + sx={{ + display: { xs: "inline-flex", md: "none" }, + color: colors.plum, + border: `1px solid ${colors.peach}88`, + background: "rgba(255,255,255,0.55)", + }} + > + + + + + + + + {navItems.map((item) => ( + + {item.label} + + ))} + + + ); +}; + +export default WebsiteHeader; diff --git a/src/components/product/product.tsx b/src/components/product/product.tsx index d23f0a2..910c2de 100644 --- a/src/components/product/product.tsx +++ b/src/components/product/product.tsx @@ -10,11 +10,10 @@ import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder'; import VisibilityIcon from '@mui/icons-material/Visibility'; import { keyframes } from '@emotion/react'; import { styled } from '@mui/system'; -// import image1 from '../assets/group1.jpg'////// -import image2 from '../../assets/solo1.jpg' -// import image3 from '../assets/solo2.jpg'// import image4 from '../../assets/bag1.jpg' import image5 from '../../assets/bag2.jpg' +import StoryHeader from '../header/StoryHeader'; +import StoryTimeline from '../timeline/StoryTimeline'; // Lighter color palette for product showcase const colors = { lightBase: '#F8F7F4', // Very light cream @@ -33,22 +32,12 @@ const fadeIn = keyframes` to { opacity: 1; transform: translateY(0); } `; -const scaleIn = keyframes` - from { transform: scale(0.95); opacity: 0; } - to { transform: scale(1); opacity: 1; } -`; - const float = keyframes` 0% { transform: translateY(0px); } 50% { transform: translateY(-8px); } 100% { transform: translateY(0px); } `; -const drawUnderline = keyframes` - 0% { width: 0; } - 100% { width: 100%; } -`; - const smoothPulse = keyframes` 0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(193, 154, 107, 0.3); } 70% { transform: scale(1.01); box-shadow: 0 0 0 8px rgba(193, 154, 107, 0); } @@ -117,22 +106,6 @@ const LightVintagePulseButton = styled(LightVintageButton)({ }, }); -const LightVintageUnderline = styled(Box)({ - position: 'relative', - display: 'inline-block', - '&::after': { - content: '""', - position: 'absolute', - bottom: '-4px', - left: 0, - width: '0', - height: '1.5px', - backgroundColor: colors.accent, - animation: `${drawUnderline} 1.2s forwards`, - animationDelay: '0.3s', - }, -}); - const AnimatedSection = styled(Box)({ opacity: 0, transform: 'translateY(20px)', @@ -210,7 +183,6 @@ export const LightProductShowcase: React.FC = () => { const [hoveredProduct, setHoveredProduct] = useState(null); // Intersection Observer for scroll animations - const [heroRef, heroInView] = useInView({ threshold: 0.1, triggerOnce: true }); const [productsRef, productsInView] = useInView({ threshold: 0.1, triggerOnce: true }); const [footerRef, footerInView] = useInView({ threshold: 0.1, triggerOnce: true }); @@ -266,156 +238,13 @@ export const LightProductShowcase: React.FC = () => { }} /> - - {/* Header */} - - - - The Kalawati Collection - - - - - - - - - - - - - - - - + + - {/* Hero Section */} - - - - - - - - - Timeless Craftsmanship, Modern Elegance - - - Each piece tells a story of tradition, skill, and the hands that crafted it with love and dedication. - - - - Explore Collection - - - + {/* Category Filter */} - + { {/* Call to Action */} - + { ); }; -export default LightProductShowcase; \ No newline at end of file +export default LightProductShowcase; diff --git a/src/components/timeline/StoryTimeline.tsx b/src/components/timeline/StoryTimeline.tsx new file mode 100644 index 0000000..328470a --- /dev/null +++ b/src/components/timeline/StoryTimeline.tsx @@ -0,0 +1,331 @@ +import React from "react"; +import { Box, Container, Grid, Typography } from "@mui/material"; +import { keyframes } from "@emotion/react"; +import { styled } from "@mui/system"; +import image1 from "../../assets/group1.jpg"; +import image2 from "../../assets/solo1.jpg"; +import image3 from "../../assets/solo2.jpg"; +import image4 from "../../assets/bag1.jpg"; +import image5 from "../../assets/bag2.jpg"; + +const colors = { + ivory: "#fffaf5", + mist: "#f8f1ea", + blush: "#f4ddd4", + peach: "#efb7a2", + coral: "#d9826f", + berry: "#7f4b5d", + plum: "#4f2f42", +}; + +const riseIn = keyframes` + from { opacity: 0; transform: translateY(36px); } + to { opacity: 1; transform: translateY(0); } +`; + +const rotateIn = keyframes` + from { transform: rotate(5deg) scale(0.9); opacity: 0; } + to { transform: rotate(0deg) scale(1); opacity: 1; } +`; + +const TimelineShell = styled(Box)({ + position: "relative", + paddingTop: "24px", + paddingBottom: "48px", + "&::before": { + content: '""', + position: "absolute", + top: 0, + bottom: 0, + left: "50%", + transform: "translateX(-50%)", + width: "1px", + background: "linear-gradient(to bottom, rgba(217,130,111,0.15), rgba(127,75,93,0.45), rgba(217,130,111,0.15))", + zIndex: 0, + }, + "@media (max-width: 899px)": { + "&::before": { + left: "22px", + transform: "none", + }, + }, +}); + +const FloatingBubble = styled(Box)<{ + size: number; + top?: string; + left?: string; + right?: string; + bottom?: string; + delay?: string; +}>(({ size, top, left, right, bottom, delay = "0s" }) => ({ + position: "absolute", + width: size, + height: size, + top, + left, + right, + bottom, + borderRadius: "50%", + background: + "radial-gradient(circle at 30% 30%, rgba(255,255,255,0.85), rgba(244,221,212,0.55) 45%, rgba(217,130,111,0.16) 75%, rgba(127,75,93,0.08) 100%)", + filter: "blur(3px)", + opacity: 0.9, + pointerEvents: "none", + animation: `${riseIn} 1.2s ease forwards`, + animationDelay: delay, +})); + +const TimelineCard = styled(Box)({ + position: "relative", + borderRadius: "30px", + background: + "linear-gradient(135deg, rgba(255,250,245,0.9) 0%, rgba(244,221,212,0.76) 55%, rgba(248,241,234,0.9) 100%)", + backdropFilter: "blur(14px)", + border: `1px solid ${colors.peach}66`, + boxShadow: "0 18px 38px rgba(79, 47, 66, 0.08)", + overflow: "hidden", + opacity: 0, + animation: `${riseIn} 0.9s ease forwards`, + "&::before": { + content: '""', + position: "absolute", + inset: 0, + background: + "linear-gradient(120deg, rgba(255,255,255,0.18), rgba(255,255,255,0), rgba(255,255,255,0.15))", + pointerEvents: "none", + }, +}); + +const TimelineImage = styled(Box)({ + borderRadius: "24px", + border: `10px solid ${colors.ivory}`, + boxShadow: `10px 10px 0 rgba(127,75,93,0.2), 0 10px 20px rgba(79,47,66,0.08)`, + overflow: "hidden", + position: "relative", + animation: `${rotateIn} 1.1s ease`, + "&::before": { + content: '""', + position: "absolute", + top: "-14px", + left: "-14px", + right: "-14px", + bottom: "-14px", + border: `1px solid ${colors.peach}88`, + borderRadius: "28px", + zIndex: -1, + }, +}); + +const storyChapters = [ + { + year: "2019", + title: "Where Curiosity Met Craft", + content: + "Fresh out of fashion school, I found myself drawn to looms, dye pits, and village homes. I organized workshops to listen, learn, and share space with artisans, forging a bond between design, community, and culture.", + image: image1, + }, + { + year: "2020", + title: "A Name Was Born", + content: + "During the pandemic, I launched a campaign to highlight artisans hit by lockdowns. The Kalawati was born, named after women who hold culture in their palms and creativity in their hearts.", + image: image2, + }, + { + year: "2021", + title: "Strengthening the Circle", + content: + "I spent the year mapping artisan strengths, connecting traditional skills to modern demand, and building trust for a long-term vision of community and craft.", + image: image3, + }, + { + year: "2022", + title: "Systems That Serve People", + content: + "Through fellowship work, systems took shape that blended business and empathy, building stronger brand stories, value chains, and sustainable income pathways.", + image: image4, + }, + { + year: "2023", + title: "Quiet Creation", + content: + "Working with artisan clusters, collections began to take form through handloom, embroidery, and natural dyes, turning process into pieces people could feel.", + image: image1, + }, + { + year: "2024", + title: "Kalawati Arrives", + content: + "The Kalawati opens its doors as a brand and movement, offering ethically made handcrafted apparel and decor with story, memory, and care woven through each release.", + image: image5, + }, +]; + +const StoryTimeline: React.FC = () => { + return ( + + + + + + + + + + Story Timeline + + + Our story woven in time + + + A pink-toned retelling of the same timeline idea from the coming soon page, now designed to sit beneath the main story header as part of the storefront flow. + + + + + + {storyChapters.map((chapter, index) => ( + + + + + + + {chapter.year} + + + {chapter.title} + + + {chapter.content} + + + + + + + + + + + ))} + + + + + ); +}; + +export default StoryTimeline; diff --git a/src/index.css b/src/index.css index ac07061..81b4526 100644 --- a/src/index.css +++ b/src/index.css @@ -1,3 +1,5 @@ +@import url("https://fonts.googleapis.com/css2?family=Playfair+Display:wght@500;600;700&family=Manrope:wght@400;500;600;700&display=swap"); + /* simple defaults (no Tailwind) */ * { box-sizing: border-box; @@ -12,8 +14,13 @@ body, } body { - font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; - background: #f7fafc; + font-family: "Manrope", "Segoe UI", sans-serif; + background: radial-gradient(circle at top, #fff8f1 0%, #f8f1ea 32%, #f7fafc 100%); color: #0f172a; line-height: 1.4; -} \ No newline at end of file +} + +a { + color: inherit; + text-decoration: none; +} diff --git a/tsconfig.app.tsbuildinfo b/tsconfig.app.tsbuildinfo new file mode 100644 index 0000000..9a749f1 --- /dev/null +++ b/tsconfig.app.tsbuildinfo @@ -0,0 +1 @@ +{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/blogs/BlogCard.tsx","./src/components/blogs/BlogPage.tsx","./src/components/comingsoon/comingsoon.tsx","./src/components/header/StoryHeader.tsx","./src/components/header/WebsiteHeader.tsx","./src/components/product/product.tsx","./src/components/timeline/StoryTimeline.tsx","./src/types/blogs.tsx"],"version":"5.8.3"} \ No newline at end of file