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 (
-
- );
-}
\ 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}
-
-
- }
- sx={{
- color: 'secondary.main',
- fontWeight: 500,
- }}
- >
- View Memories
-
-
-
-
- ))}
-
-
- )}
-
- {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 */}
-
-
- {/* 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 (
-
- );
-};
-
-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)",
+ }}
+ >
+
+
+
+
+
+
+
+ >
+ );
+};
+
+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