feature/coming_soon #3

Merged
hardik merged 4 commits from feature/coming_soon into main 2025-12-06 19:35:57 +05:30
30 changed files with 5886 additions and 344 deletions

4
.dockerignore Normal file
View File

@ -0,0 +1,4 @@
node_modules
dist
npm-debug.log
.DS_Store

Binary file not shown.

View File

@ -5,43 +5,19 @@ FROM node:22-bullseye AS builder
ARG API_BASE_URL="https://navigolabs.com/api" ARG API_BASE_URL="https://navigolabs.com/api"
# Enable Corepack and Yarn 4
RUN corepack enable && corepack prepare yarn@4.9.2 --activate
WORKDIR /app WORKDIR /app
# Copy Yarn 4 files # Copy package.json and package-lock.json
COPY package.json yarn.lock .yarnrc.yml ./ COPY package*.json ./
COPY .yarn .yarn
# Install dependencies (immutable to match lockfile) # Install ALL dependencies (including devDependencies)
RUN yarn install --immutable RUN npm install
# Copy all source files # Copy source files
COPY . . COPY . .
# Pass API URL to Vite # Pass API URL to Vite
ENV VITE_API_BASE_URL=$API_BASE_URL ENV VITE_API_BASE_URL=$API_BASE_URL
# Build the app # Build the app
RUN yarn build RUN npm run build
# ------------------------
# Production stage
# ------------------------
FROM node:22-alpine AS production
WORKDIR /app
# Copy built files from builder stage
COPY --from=builder /app/dist ./dist
# Install a lightweight static server
RUN npm install -g serve@14
# Expose port (optional but recommended)
EXPOSE 3000
# Start the static server
CMD ["serve", "-s", "dist", "-l", "3000"]

View File

@ -1,12 +1,15 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head>
<meta charset="utf-8" /> <head>
<meta name="viewport" content="width=device-width,initial-scale=1" /> <meta charset="utf-8" />
<title>Vite + React + TS</title> <meta name="viewport" content="width=device-width,initial-scale=1" />
</head> <title>The Kalawati</title>
<body> </head>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script> <body>
</body> <div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html> </html>

787
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -16,7 +16,9 @@
"@mui/material": "^7.3.2", "@mui/material": "^7.3.2",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-intersection-observer": "^9.16.0" "react-dropzone": "^14.3.8",
"react-intersection-observer": "^9.16.0",
"react-router-dom": "^7.8.2"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.33.0", "@eslint/js": "^9.33.0",

View File

@ -1,18 +1,29 @@
// import { useState } from 'react' import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
// import reactLogo from './assets/react.svg' import "./App.css";
// import viteLogo from '/vite.svg' // import VintageComingSoonPage from "./components/comingsoon/comingsoon";
import './App.css' // import DarkProductShowcase from "./components/product/product";
// import VintageComingSoon from './comingsoon/comingsoon' // import BlogPage from "./components/blogs/BlogPage";
import VintageComingSoonPage from './comingsoon/comingsoon' // import BlogCard from "./components/blogs/BlogCard";
import WeddingGallery from "./WeddingGallery/WeddingGallery";
// import OtherPage from "./pages/OtherPage"; // example if you add more pages
function App() { function App() {
// const [count, setCount] = useState(0)
return ( return (
<Router>
<Routes>
{/* Default route */}
{/* <Route path="/" element={<VintageComingSoonPage />} /> */}
<Route path="/" element={<WeddingGallery />} />
<VintageComingSoonPage /> {/* Example extra routes */}
{/* <Route path="/home" element={<DarkProductShowcase />} />
<Route path="/blog" element={<BlogPage />} /> */}
<Route path="/weddingGallery" element={<WeddingGallery />} />
{/* <Route path="/blog" element={<BlogCard post={undefined} index={0} />} /> */}
</Routes>
</Router>
); );
} }
export default App export default App;

View File

@ -0,0 +1,43 @@
// 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<Props> = ({ photos, onDownload }) => {
return (
<SafeGrid container spacing={3}>
{photos.map((p) => (
<SafeGrid item xs={12} sm={6} md={4} key={p.id}>
<Card sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<Box sx={{ position: 'relative' }}>
<CardMedia component="img" height="250" image={p.url} alt={p.title} sx={{ objectFit: 'cover' }} />
<Box sx={{ position: 'absolute', top: 8, right: 8 }}>
<IconButton size="small" onClick={() => onDownload?.(p.url, p.title)} sx={{ color: 'white', bgcolor: 'rgba(0,0,0,0.6)' }}>
<Download fontSize="small" />
</IconButton>
</Box>
</Box>
<CardContent sx={{ bgcolor: '#fffaf0', flexGrow: 1 }}>
<Typography variant="h6">{p.title || 'Untitled'}</Typography>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Chip label={p.title ?? '—'} size="small" color="primary" variant="outlined" />
<Typography variant="caption" color="text.secondary">
{p.created_at ? new Date(p.created_at).toLocaleDateString() : p.date ? new Date(p.date).toLocaleDateString() : ''}
</Typography>
</Box>
</CardContent>
</Card>
</SafeGrid>
))}
</SafeGrid>
);
};
export default Gallery;

View File

@ -0,0 +1,592 @@
// 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<number>(startIndex);
const [scale, setScale] = useState<number>(1);
const [translate, setTranslate] = useState<{ x: number; y: number }>({ x: 0, y: 0 });
const [isPanning, setIsPanning] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [imageDimensions, setImageDimensions] = useState<{ width: number; height: number } | null>(null);
const panStart = useRef<{ x: number; y: number } | null>(null);
const lastTranslate = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
const touchStartX = useRef<number | null>(null);
const touchDeltaX = useRef(0);
// For pinch
const pinchStartDist = useRef<number | null>(null);
const pinchStartScale = useRef<number>(1);
const imageRef = useRef<HTMLImageElement>(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<HTMLImageElement>) => {
setIsLoading(false);
const img = e.currentTarget;
setImageDimensions({
width: img.naturalWidth,
height: img.naturalHeight
});
};
const handleImageError = () => {
setIsLoading(false);
setImageDimensions(null);
};
if (!photo) return null;
/* ============================
Zoom / Pan Controls
============================ */
const setScaleClamped = (s: number) => {
const clamped = Math.max(MIN_SCALE, Math.min(MAX_SCALE, s));
setScale(clamped);
if (clamped === 1) {
// reset translate when fully fit
setTranslate({ x: 0, y: 0 });
lastTranslate.current = { x: 0, y: 0 };
}
};
const zoomIn = () => setScaleClamped(scale + SCALE_STEP);
const zoomOut = () => setScaleClamped(scale - SCALE_STEP);
const fitToScreen = () => setScaleClamped(1);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const handleDoubleClick = (_e: React.MouseEvent) => {
if (scale === 1) setScaleClamped(2);
else setScaleClamped(1);
};
/* ============================
Panning (pointer / touch)
============================ */
const onPointerDown = (e: React.PointerEvent) => {
if (scale <= 1) return;
(e.target as Element).setPointerCapture?.(e.pointerId);
setIsPanning(true);
panStart.current = { x: e.clientX, y: e.clientY };
};
const onPointerMove = (e: React.PointerEvent) => {
if (!isPanning || !panStart.current) return;
const dx = e.clientX - panStart.current.x;
const dy = e.clientY - panStart.current.y;
const TX = lastTranslate.current.x + dx;
const TY = lastTranslate.current.y + dy;
setTranslate({ x: TX, y: TY });
};
const onPointerUp = (e: React.PointerEvent) => {
if (!isPanning) return;
setIsPanning(false);
if (panStart.current) {
const dx = e.clientX - panStart.current.x;
const dy = e.clientY - panStart.current.y;
lastTranslate.current = { x: lastTranslate.current.x + dx, y: lastTranslate.current.y + dy };
panStart.current = null;
}
try {
(e.target as Element).releasePointerCapture?.(e.pointerId);
} catch {
//ignore
}
};
const onWheel = (e: React.WheelEvent) => {
if (!open) return;
e.preventDefault();
const delta = -e.deltaY;
const step = delta > 0 ? SCALE_STEP : -SCALE_STEP;
setScaleClamped(scale + step);
};
/* ============================
Touch handlers for swipe + pinch
Note: use React.Touch types here to satisfy TS
============================ */
const onTouchStart = (e: React.TouchEvent) => {
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 (
<Dialog
open={open}
onClose={onClose}
fullScreen
PaperProps={{
sx: {
m: 0,
p: 0,
height: '100vh',
backgroundColor: 'rgba(0,0,0,0.95)',
overflow: 'hidden',
'& .MuiDialog-container': {
alignItems: 'center',
justifyContent: 'center',
}
},
}}
>
<Box
sx={{
width: '100%',
height: '100%',
position: 'relative',
boxSizing: 'border-box',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
userSelect: isPanning ? 'none' : 'auto',
touchAction: 'none',
}}
onWheel={onWheel}
onTouchStart={onTouchStart}
onTouchMove={onTouchMove}
onTouchEnd={onTouchEnd}
onClick={(e) => {
if (e.target === e.currentTarget) onClose();
}}
>
{/* Loading indicator */}
{isLoading && (
<Box sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
zIndex: 10
}}>
<CircularProgress sx={{ color: 'rgba(255,255,255,0.7)' }} />
</Box>
)}
{/* Top right controls */}
<Box sx={{
position: 'absolute',
top: 16,
right: 16,
zIndex: 60,
display: 'flex',
gap: 1,
background: 'rgba(0,0,0,0.5)',
borderRadius: 2,
p: 0.5,
backdropFilter: 'blur(10px)',
}}>
<Tooltip title="Zoom Out">
<IconButton
size="small"
onClick={(ev) => {
ev.stopPropagation();
zoomOut();
}}
sx={{
bgcolor: 'rgba(255,255,255,0.1)',
color: 'white',
'&:hover': { bgcolor: 'rgba(255,255,255,0.2)' }
}}
>
<RemoveIcon />
</IconButton>
</Tooltip>
<Tooltip title="Fit to screen">
<IconButton
size="small"
onClick={(ev) => {
ev.stopPropagation();
fitToScreen();
}}
sx={{
bgcolor: 'rgba(255,255,255,0.1)',
color: 'white',
'&:hover': { bgcolor: 'rgba(255,255,255,0.2)' }
}}
>
<FitScreenIcon />
</IconButton>
</Tooltip>
<Tooltip title="Zoom In">
<IconButton
size="small"
onClick={(ev) => {
ev.stopPropagation();
zoomIn();
}}
sx={{
bgcolor: 'rgba(255,255,255,0.1)',
color: 'white',
'&:hover': { bgcolor: 'rgba(255,255,255,0.2)' }
}}
>
<AddIcon />
</IconButton>
</Tooltip>
<Tooltip title="Download">
<IconButton
size="small"
onClick={(ev) => {
ev.stopPropagation();
try {
const a = document.createElement('a');
a.href = photo.url;
a.download = `${photo.filename ?? photo.title ?? 'image'}`;
document.body.appendChild(a);
a.click();
a.remove();
} catch (err) {
console.error('download', err);
}
}}
sx={{
bgcolor: 'rgba(255,255,255,0.1)',
color: 'white',
'&:hover': { bgcolor: 'rgba(255,255,255,0.2)' }
}}
>
<DownloadIcon />
</IconButton>
</Tooltip>
<Tooltip title="Close (Esc)">
<IconButton
size="small"
onClick={(ev) => {
ev.stopPropagation();
onClose();
}}
sx={{
bgcolor: 'rgba(255,255,255,0.1)',
color: 'white',
'&:hover': { bgcolor: 'rgba(255,255,255,0.2)' }
}}
>
<CloseIcon />
</IconButton>
</Tooltip>
</Box>
{/* Prev / Next arrows */}
<IconButton
onClick={(ev) => {
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)' }
}}
>
<ArrowBackIosNewIcon />
</IconButton>
<IconButton
onClick={(ev) => {
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)' }
}}
>
<ArrowForwardIosIcon />
</IconButton>
{/* Image container */}
<Box
sx={{
width: '100%',
height: '100%',
px: 2,
boxSizing: 'border-box',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
}}
onPointerDown={onPointerDown}
onPointerMove={onPointerMove}
onPointerUp={onPointerUp}
onPointerCancel={onPointerUp}
onDoubleClick={handleDoubleClick}
>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
touchAction: 'none',
position: 'relative',
}}
>
<img
ref={imageRef}
src={photo.url}
alt={photo.title ?? photo.filename}
onLoad={handleImageLoad}
onError={handleImageError}
style={{
display: isLoading ? 'none' : 'block',
width: 'auto',
height: 'auto',
maxWidth: '90vw',
maxHeight: '80vh',
objectFit: 'contain',
transform: `translate(${translate.x}px, ${translate.y}px) scale(${scale})`,
transition: isPanning ? 'none' : 'transform 120ms ease',
cursor: scale > 1 ? (isPanning ? 'grabbing' : 'grab') : 'default',
borderRadius: 4,
boxShadow: '0 10px 30px rgba(0,0,0,0.6)',
touchAction: 'none',
...getImageDisplayStyle(),
}}
draggable={false}
/>
</Box>
</Box>
{/* Caption */}
<Box sx={{
position: 'absolute',
bottom: 16,
left: 16,
right: 16,
textAlign: 'center',
zIndex: 50,
background: 'rgba(0,0,0,0.5)',
backdropFilter: 'blur(10px)',
borderRadius: 2,
p: 1.5,
maxWidth: '600px',
mx: 'auto',
}}>
<Typography variant="subtitle1" color="white" noWrap sx={{ fontWeight: 500 }}>
{photo.title ?? photo.filename}
</Typography>
<Typography variant="caption" color="rgba(255,255,255,0.85)" sx={{ display: 'block', mt: 0.5 }}>
{index + 1} of {photos.length}
{photo.created_at && `${new Date(photo.created_at).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
})}`}
{imageDimensions && `${imageDimensions.width}×${imageDimensions.height}`}
</Typography>
</Box>
{/* Zoom level indicator */}
{scale !== 1 && (
<Box sx={{
position: 'absolute',
top: 16,
left: 16,
zIndex: 50,
background: 'rgba(0,0,0,0.5)',
backdropFilter: 'blur(10px)',
borderRadius: 2,
p: 1,
color: 'white',
fontSize: '0.875rem',
fontWeight: 500,
}}>
{Math.round(scale * 100)}%
</Box>
)}
</Box>
</Dialog>
);
}

View File

@ -0,0 +1,60 @@
// 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<Item[]>([]);
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 (
<Box sx={{ p: 3 }}>
<Box {...getRootProps()} sx={{ border: '2px dashed', p: 2, textAlign: 'center', mb: 2 }}>
<input {...getInputProps()} />
<Typography>{isDragActive ? 'Drop images here' : 'Drag & drop images (PreviewTester)'}</Typography>
</Box>
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
{items.map((it) => (
<Card key={it.id} sx={{ width: 160 }}>
<CardMedia component="img" height="100" image={it.previewUrl} alt={it.file.name} />
<CardContent>
<Typography variant="caption" noWrap>{it.file.name}</Typography>
</CardContent>
</Card>
))}
</Box>
<Box sx={{ mt: 2 }}>
<Button onClick={() => { console.log('items state:', items); alert(`Items: ${items.length}`); }}>Console.log items</Button>
</Box>
</Box>
);
}

View File

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

View File

@ -0,0 +1,98 @@
// 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<React.SetStateAction<FileQueueItem[]>>;
newPhotoTitle?: string;
onUploaded?: (photo: WeddingPhoto) => void;
};
const UploadQueue: React.FC<Props> = ({ 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 (
<Box>
<Box sx={{ display: 'flex', gap: 1, mb: 1 }}>
<Button onClick={uploadAll} disabled={!queue.some((q) => q.status === 'pending' || q.status === 'error')} variant="contained">
Upload All
</Button>
</Box>
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
{queue.map((q) => (
<Card key={q.id} sx={{ width: 180 }}>
<CardMedia component="img" height={110} image={q.previewUrl} />
<CardContent>
<Typography variant="body2" noWrap>
{q.file.name}
</Typography>
{q.status === 'uploading' && <Typography variant="caption">Uploading {q.progress}%</Typography>}
{q.status === 'pending' && <Typography variant="caption">Pending</Typography>}
{q.status === 'done' && <Typography variant="caption" color="success.main">Uploaded</Typography>}
{q.status === 'error' && <Typography variant="caption" color="error.main">{q.error ?? 'Upload failed'}</Typography>}
<LinearProgress variant="determinate" value={q.progress} sx={{ mt: 1 }} />
<Box sx={{ display: 'flex', gap: 1, mt: 1 }}>
{q.status === 'error' && <IconButton size="small" onClick={() => retryOne(q.id)}><Replay fontSize="small" /></IconButton>}
<IconButton size="small" onClick={() => removeOne(q.id)}><Delete fontSize="small" /></IconButton>
</Box>
</CardContent>
</Card>
))}
</Box>
</Box>
);
};
export default UploadQueue;

File diff suppressed because it is too large Load Diff

118
src/WeddingGallery/apis.ts Normal file
View File

@ -0,0 +1,118 @@
// 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';
};

View File

@ -0,0 +1,53 @@
// 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<void>;
deletingId?: number | string | null;
};
const FolderModal: React.FC<Props> = ({ open, onClose, photos, onDeletePhoto, deletingId }) => {
return (
<Dialog open={open} onClose={onClose} fullWidth maxWidth="md">
<DialogTitle>Manage Photos ({photos.length})</DialogTitle>
<DialogContent dividers>
{photos.length === 0 ? (
<Typography variant="body2">No photos in this event.</Typography>
) : (
<Grid container spacing={2}>
{photos.map((p) => (
<Grid key={p.id}>
<Card>
<Box sx={{ position: 'relative' }}>
<CardMedia component="img" height="140" image={p.url} />
<IconButton
sx={{ position: 'absolute', top: 6, right: 6, bgcolor: 'rgba(255,255,255,0.8)' }}
onClick={() => onDeletePhoto(p.id)}
disabled={deletingId === p.id}
>
<DeleteIcon fontSize="small" />
</IconButton>
</Box>
<Box sx={{ p: 1 }}>
<Typography variant="caption" noWrap>{p.filename ?? p.title}</Typography>
</Box>
</Card>
</Grid>
))}
</Grid>
)}
</DialogContent>
<DialogActions>
<Button onClick={onClose} variant="outlined">Close</Button>
</DialogActions>
</Dialog>
);
};
export default FolderModal;

View File

@ -0,0 +1,54 @@
// 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<Props> = ({ folders, onOpen, onDeleteFolder }) => {
if (!folders.length) {
return (
<Paper sx={{ p: 6, textAlign: 'center' }}>
<Typography variant="h6" color="text.secondary">No events found</Typography>
<Typography variant="body2" color="text.secondary">Upload photos to create events (folders).</Typography>
</Paper>
);
}
return (
<SafeGrid container spacing={3}>
{folders.map((f) => (
<SafeGrid item xs={12} sm={6} md={4} key={f.title}>
<Card sx={{ cursor: 'pointer', transition: 'transform .15s', '&:hover': { transform: 'translateY(-6px)' } }}>
<Box onClick={() => onOpen(f.title)}>
<Box sx={{ height: 200, overflow: 'hidden' }}>
<CardMedia component="img" image={f.thumb ?? ''} alt={f.title} sx={{ width: '100%', height: 200, objectFit: 'cover', backgroundColor: '#f4f6f7' }} />
</Box>
<CardContent>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Box sx={{ minWidth: 0 }}>
<Typography variant="subtitle1" noWrap>{f.title}</Typography>
<Typography variant="caption" color="text.secondary">{f.count} photos</Typography>
</Box>
<Box>
<IconButton edge="end" size="small" aria-label="delete folder" onClick={(e) => { e.stopPropagation(); onDeleteFolder(f.title); }}>
<DeleteIcon />
</IconButton>
</Box>
</Box>
</CardContent>
</Box>
</Card>
</SafeGrid>
))}
</SafeGrid>
);
};
export default FolderGrid;

View File

@ -0,0 +1,26 @@
// 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;
};

BIN
src/assets/bag1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

BIN
src/assets/bag2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

BIN
src/assets/group1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
src/assets/logobgremove.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 KiB

BIN
src/assets/solo1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
src/assets/solo2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

View File

@ -0,0 +1,359 @@
import React from 'react';
import { Card, CardContent, CardMedia, Typography, Box, Chip, Button, Avatar } from '@mui/material';
import { BlogPost } from '../../types/blogs';
import { keyframes } from '@emotion/react';
import { styled } from '@mui/system';
import { useNavigate } from 'react-router-dom';
// Use your existing color palette
const colors = {
paper: '#F5F5EF',
ink: '#1A2526',
accent: '#D4A017',
secondary: '#7B4F3A',
highlight: '#FFF4CC',
border: '#B89A6E',
dark: '#3A1F0F',
};
// Storytelling Animations
const storyReveal = keyframes`
0% {
opacity: 0;
transform: translateY(30px) rotate(2deg);
filter: blur(4px);
}
100% {
opacity: 1;
transform: translateY(0) rotate(0);
filter: blur(0);
}
`;
const imageFocus = keyframes`
0% { transform: scale(1.1); filter: brightness(0.7) sepia(0.3); }
100% { transform: scale(1); filter: brightness(1) sepia(0); }
`;
const contentAppear = keyframes`
0% {
opacity: 0;
transform: translateY(20px);
}
100% {
opacity: 1;
transform: translateY(0);
}
`;
const underlineDraw = keyframes`
0% { width: 0; }
100% { width: 100%; }
`;
const tagFloat = keyframes`
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-3px); }
`;
// Styled Components
const StoryCard = styled(Card)({
animation: `${storyReveal} 1.2s forwards`,
opacity: 0,
transform: 'translateY(30px) rotate(2deg)',
position: 'relative',
overflow: 'hidden',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'linear-gradient(to bottom, transparent 60%, rgba(0,0,0,0.7) 100%)',
opacity: 0,
transition: 'opacity 0.5s ease',
zIndex: 1,
},
'&:hover::before': {
opacity: 1,
},
});
const StoryImage = styled(CardMedia)({
animation: `${imageFocus} 1.5s forwards`,
transform: 'scale(1.1)',
filter: 'brightness(0.7) sepia(0.3)',
transition: 'all 0.7s ease',
position: 'relative',
'&:hover': {
transform: 'scale(1.05)',
},
});
const StoryContent = styled(CardContent)({
animation: `${contentAppear} 1s forwards`,
animationDelay: '0.5s',
opacity: 0,
position: 'relative',
zIndex: 2,
});
const StoryTitle = styled(Typography)({
position: 'relative',
display: 'inline-block',
'&::after': {
content: '""',
position: 'absolute',
bottom: '-4px',
left: 0,
width: 0,
height: '2px',
backgroundColor: colors.accent,
animation: `${underlineDraw} 1s forwards`,
animationDelay: '1s',
},
});
const StoryTag = styled(Chip)({
animation: `${tagFloat} 3s ease-in-out infinite`,
animationDelay: '0.3s',
});
interface BlogCardProps {
post: BlogPost;
index: number;
}
const BlogCard: React.FC<BlogCardProps> = ({ post, index }) => {
const navigate = useNavigate();
const handleReadMore = () => {
navigate(`/blog/${post.id}`);
};
// Calculate animation delays for staggered effect
const animationDelay = `${index * 0.15}s`;
return (
<StoryCard
sx={{
background: colors.paper,
border: `1px solid ${colors.border}`,
borderRadius: '4px',
overflow: 'hidden',
transition: 'all 0.4s ease',
height: '100%',
display: 'flex',
flexDirection: 'column',
boxShadow: '0 4px 20px rgba(0,0,0,0.08)',
animationDelay: animationDelay,
'&:hover': {
transform: 'translateY(-8px) rotate(0)',
boxShadow: '0 12px 30px rgba(0,0,0,0.15)',
},
}}
>
<Box sx={{ position: 'relative', overflow: 'hidden' }}>
<StoryImage
// component="img"
// height="260"
image={post.image}
// alt={post.title}
// sx={{
// animationDelay: animationDelay,
// }}
/>
<Box
sx={{
position: 'absolute',
top: 16,
right: 16,
zIndex: 3,
}}
>
<StoryTag
label={post.category}
size="small"
sx={{
background: colors.accent,
color: colors.ink,
fontSize: '0.7rem',
fontWeight: 600,
animationDelay: `${index * 0.15 + 0.2}s`,
}}
/>
</Box>
</Box>
<StoryContent
sx={{
p: 3,
flexGrow: 1,
display: 'flex',
flexDirection: 'column',
animationDelay: `${index * 0.15 + 0.3}s`,
}}
>
<Box sx={{ mb: 2, display: 'flex', alignItems: 'center' }}>
<Avatar
src={post.author.avatar}
alt={post.author.name}
sx={{
width: 32,
height: 32,
mr: 1.5,
border: `2px solid ${colors.border}`,
}}
/>
<Box>
<Typography
variant="caption"
sx={{
color: colors.dark,
fontFamily: '"Cormorant Garamond", serif',
fontWeight: 600,
display: 'block',
lineHeight: 1.2,
}}
>
{post.author.name}
</Typography>
<Typography
variant="caption"
sx={{
color: colors.secondary,
fontFamily: '"Cormorant Garamond", serif',
fontSize: '0.7rem',
fontStyle: 'italic',
}}
>
{post.author.role}
</Typography>
</Box>
</Box>
<StoryTitle
variant="h6"
sx={{
fontWeight: 600,
color: colors.ink,
fontFamily: '"Playfair Display", serif',
mb: 2,
lineHeight: 1.3,
fontSize: '1.25rem',
animationDelay: `${index * 0.15 + 0.4}s`,
}}
>
{post.title}
</StoryTitle>
<Typography
variant="body2"
sx={{
color: colors.secondary,
fontFamily: '"Cormorant Garamond", serif',
mb: 2,
flexGrow: 1,
fontSize: '1rem',
lineHeight: 1.6,
animationDelay: `${index * 0.15 + 0.5}s`,
}}
>
{post.excerpt}
</Typography>
<Box sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
mt: 'auto',
animationDelay: `${index * 0.15 + 0.6}s`,
}}>
<Typography
variant="caption"
sx={{
color: colors.secondary,
fontFamily: '"Cormorant Garamond", serif',
display: 'flex',
alignItems: 'center',
}}
>
<Box
component="span"
sx={{
display: 'inline-block',
width: '4px',
height: '4px',
borderRadius: '50%',
bgcolor: colors.accent,
mx: 1,
}}
/>
{post.date} {post.readTime}
</Typography>
<Button
onClick={handleReadMore}
sx={{
color: colors.accent,
fontFamily: '"Cormorant Garamond", serif',
fontWeight: 600,
fontSize: '0.9rem',
position: 'relative',
padding: '4px 8px',
'&::after': {
content: '""',
position: 'absolute',
bottom: 0,
left: 0,
width: '0',
height: '1px',
backgroundColor: colors.accent,
transition: 'width 0.3s ease',
},
'&:hover': {
color: colors.dark,
background: 'transparent',
'&::after': {
width: '100%',
},
}
}}
>
Read Story
</Button>
</Box>
<Box
sx={{
mt: 2,
display: 'flex',
flexWrap: 'wrap',
gap: 0.5,
animationDelay: `${index * 0.15 + 0.7}s`,
}}
>
{post.tags.map((tag, i) => (
<Chip
key={i}
label={tag}
size="small"
variant="outlined"
sx={{
color: colors.secondary,
borderColor: colors.border,
fontSize: '0.6rem',
height: '20px',
animation: `${tagFloat} 3s ease-in-out infinite`,
animationDelay: `${i * 0.2}s`,
}}
/>
))}
</Box>
</StoryContent>
</StoryCard>
);
};
export default BlogCard;

View File

@ -0,0 +1,781 @@
import React, { useState, useMemo } 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';
// Define TypeScript interfaces
interface Author {
name: string;
avatar: string;
role: string;
}
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',
};
// Animations
const fadeInUp = keyframes`
from {
opacity: 0;
transform: translateY(30px);
}
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); }
100% { transform: translateY(0px); }
`;
const drawUnderline = keyframes`
0% { width: 0; }
100% { width: 100%; }
`;
const pulse = keyframes`
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
`;
// Styled Components
interface AnimatedSectionProps {
delay?: string;
}
const AnimatedSection = styled(Box, {
shouldForwardProp: (prop) => prop !== 'delay',
})<AnimatedSectionProps>(({ delay = '0s' }) => ({
opacity: 0,
animation: `${fadeInUp} 0.8s forwards`,
animationDelay: delay,
}));
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 FloatingElement = styled(Box)({
animation: `${float} 6s ease-in-out infinite`,
});
// BlogCard Component
const BlogCard: React.FC<BlogCardProps> = ({ 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 handleReadMore = () => {
navigate(`/blog/${post.id}`);
};
return (
<Card
ref={ref}
sx={{
opacity: 0,
animation: inView ? `${fadeInUp} 0.8s forwards` : 'none',
animationDelay: inView ? `${index * 0.1}s` : '0s',
height: '100%',
display: 'flex',
flexDirection: 'column',
background: colors.paper,
border: `1px solid ${colors.border}`,
borderRadius: '8px',
overflow: 'hidden',
transition: 'all 0.3s ease',
boxShadow: '0 4px 12px rgba(0,0,0,0.05)',
'&:hover': {
transform: 'translateY(-5px)',
boxShadow: '0 8px 24px rgba(0,0,0,0.1)',
'& .blog-image': {
transform: 'scale(1.05)',
}
},
}}
>
<Box sx={{ position: 'relative', overflow: 'hidden' }}>
<CardMedia
component="img"
height="240"
image={post.image}
alt={post.title}
className="blog-image"
sx={{
transition: 'transform 0.5s ease',
objectFit: 'cover',
width: '100%',
}}
/>
<Chip
label={post.category}
size="small"
sx={{
position: 'absolute',
top: 16,
right: 16,
background: colors.accent,
color: colors.ink,
fontWeight: 600,
fontSize: '0.7rem',
animation: `${pulse} 2s infinite`,
}}
/>
</Box>
<CardContent sx={{ p: 3, flexGrow: 1, display: 'flex', flexDirection: 'column' }}>
<Box sx={{ mb: 2, display: 'flex', alignItems: 'center' }}>
<Avatar
src={post.author.avatar}
alt={post.author.name}
sx={{
width: 32,
height: 32,
mr: 1.5,
border: `2px solid ${colors.border}`,
}}
/>
<Box>
<Typography
variant="caption"
sx={{
color: colors.dark,
fontFamily: '"Cormorant Garamond", serif',
fontWeight: 600,
display: 'block',
lineHeight: 1.2,
}}
>
{post.author.name}
</Typography>
<Typography
variant="caption"
sx={{
color: colors.secondary,
fontFamily: '"Cormorant Garamond", serif',
fontSize: '0.7rem',
fontStyle: 'italic',
}}
>
{post.author.role}
</Typography>
</Box>
</Box>
<Typography
variant="h6"
sx={{
fontWeight: 600,
color: colors.ink,
fontFamily: '"Playfair Display", serif',
mb: 2,
lineHeight: 1.3,
fontSize: isMobile ? '1.1rem' : '1.25rem',
minHeight: isMobile ? 'auto' : '64px',
}}
>
{post.title}
</Typography>
<Typography
variant="body2"
sx={{
color: colors.secondary,
fontFamily: '"Cormorant Garamond", serif',
mb: 2,
flexGrow: 1,
fontSize: '1rem',
lineHeight: 1.6,
}}
>
{post.excerpt}
</Typography>
<Box sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
mt: 'auto',
}}>
<Typography
variant="caption"
sx={{
color: colors.secondary,
fontFamily: '"Cormorant Garamond", serif',
display: 'flex',
alignItems: 'center',
}}
>
{post.date} {post.readTime}
</Typography>
<Button
onClick={handleReadMore}
sx={{
color: colors.accent,
fontFamily: '"Cormorant Garamond", serif',
fontWeight: 600,
fontSize: '0.9rem',
position: 'relative',
padding: '4px 8px',
'&::after': {
content: '""',
position: 'absolute',
bottom: 0,
left: 0,
width: '0',
height: '1px',
backgroundColor: colors.accent,
transition: 'width 0.3s ease',
},
'&:hover': {
color: colors.dark,
background: 'transparent',
'&::after': {
width: '100%',
},
}
}}
>
Read Story
</Button>
</Box>
<Box sx={{ mt: 2, display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{post.tags.map((tag: string, i: number) => (
<Chip
key={i}
label={tag}
size="small"
variant="outlined"
sx={{
color: colors.secondary,
borderColor: colors.border,
fontSize: '0.6rem',
height: '20px',
}}
/>
))}
</Box>
</CardContent>
</Card>
);
};
// BlogPage Component
const BlogPage: React.FC = () => {
const [selectedCategory, setSelectedCategory] = useState<string>('all');
const [searchQuery, setSearchQuery] = useState<string>('');
// 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 (
<Box sx={{
minHeight: '100vh',
background: colors.paper,
color: colors.ink,
pt: isMobile ? 4 : 0,
}}>
{/* Header Section */}
<Box sx={{
background: `linear-gradient(to bottom, ${colors.paper} 0%, ${colors.highlight}40 100%)`,
py: isMobile ? 4 : 8,
borderBottom: `1px solid ${colors.border}30`,
}}>
<Container maxWidth="lg">
<Box sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: isMobile ? 'center' : 'flex-start',
flexDirection: isMobile ? 'column' : 'row',
gap: isMobile ? 2 : 0,
mb: 4
}}>
<AnimatedSection delay={heroInView ? '0.1s' : '0s'}>
<Box>
<FloatingElement>
<Typography
variant={isMobile ? "h4" : "h3"}
sx={{
fontWeight: 300,
color: colors.ink,
letterSpacing: '2px',
fontFamily: '"Playfair Display", serif',
}}
>
<VintageUnderline>Journal</VintageUnderline>
</Typography>
</FloatingElement>
<Typography
variant={isMobile ? "body1" : "h6"}
sx={{
mt: 1,
color: colors.secondary,
fontWeight: 300,
fontStyle: 'italic',
fontFamily: '"Cormorant Garamond", serif',
}}
>
Stories of Craft, Culture & Heritage
</Typography>
</Box>
</AnimatedSection>
<AnimatedSection delay={heroInView ? '0.2s' : '0s'}>
<Box sx={{ display: 'flex', gap: 1, mt: isMobile ? 1 : 0 }}>
<IconButton
href="https://instagram.com"
target="_blank"
sx={{
color: colors.secondary,
'&:hover': {
color: colors.accent,
transform: 'scale(1.1)',
background: `${colors.highlight}80`
},
transition: 'all 0.3s ease'
}}
>
<InstagramIcon />
</IconButton>
<IconButton
href="https://facebook.com"
target="_blank"
sx={{
color: colors.secondary,
'&:hover': {
color: colors.accent,
transform: 'scale(1.1)',
background: `${colors.highlight}80`
},
transition: 'all 0.3s ease'
}}
>
<FacebookIcon />
</IconButton>
<IconButton
href="https://twitter.com"
target="_blank"
sx={{
color: colors.secondary,
'&:hover': {
color: colors.accent,
transform: 'scale(1.1)',
background: `${colors.highlight}80`
},
transition: 'all 0.3s ease'
}}
>
<TwitterIcon />
</IconButton>
</Box>
</AnimatedSection>
</Box>
<AnimatedSection delay={heroInView ? '0.3s' : '0s'}>
<TextField
fullWidth
placeholder="Search articles..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon sx={{ color: colors.secondary }} />
</InputAdornment>
),
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 }}
/>
</AnimatedSection>
<AnimatedSection delay={heroInView ? '0.4s' : '0s'}>
<Box sx={{
display: 'flex',
flexWrap: 'wrap',
gap: 1,
justifyContent: isMobile ? 'center' : 'flex-start'
}}>
<Chip
label="All"
onClick={() => 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) => (
<Chip
key={category}
label={category}
onClick={() => 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`
}
}}
/>
))}
</Box>
</AnimatedSection>
</Container>
</Box>
{/* Blog Posts Grid */}
<Container maxWidth="lg" sx={{ py: isMobile ? 4 : 8 }}>
<Grid container spacing={isMobile ? 2 : 4}>
{filteredPosts.map((post: BlogPost, index: number) => (
<Grid key={post.id}>
<BlogCard post={post} index={index} />
</Grid>
))}
</Grid>
{filteredPosts.length === 0 && (
<Box sx={{ textAlign: 'center', py: 10 }}>
<Typography
variant="h5"
sx={{
color: colors.secondary,
fontFamily: '"Cormorant Garamond", serif',
fontStyle: 'italic',
mb: 2
}}
>
No articles found
</Typography>
<Typography
variant="body1"
sx={{
color: colors.secondary,
fontFamily: '"Cormorant Garamond", serif',
}}
>
Try a different search or category
</Typography>
</Box>
)}
</Container>
{/* Footer */}
<Box sx={{
py: 5,
textAlign: 'center',
borderTop: `1px solid ${colors.border}30`,
background: colors.paper,
mt: 4
}}>
<Container maxWidth="lg">
<AnimatedSection delay={'0.1s'}>
<Typography
variant="h6"
sx={{
mb: 2,
fontWeight: 300,
color: colors.ink,
letterSpacing: '3px',
fontFamily: '"Playfair Display", serif',
textTransform: 'uppercase',
}}
>
The Craft Chronicle
</Typography>
<Typography
variant="body2"
sx={{
color: colors.secondary,
mb: 3,
maxWidth: '600px',
margin: '0 auto',
fontSize: '0.95rem',
fontFamily: '"Cormorant Garamond", serif',
lineHeight: 1.6,
}}
>
Celebrating the stories, techniques, and people behind traditional crafts and heritage arts.
</Typography>
<Box sx={{ display: 'flex', justifyContent: 'center', gap: 2, mb: 3 }}>
<IconButton
href="https://instagram.com"
target="_blank"
sx={{
color: colors.secondary,
'&:hover': {
color: colors.accent,
transform: 'scale(1.1)',
background: `${colors.highlight}80`
},
transition: 'all 0.3s ease'
}}
>
<InstagramIcon />
</IconButton>
<IconButton
href="https://facebook.com"
target="_blank"
sx={{
color: colors.secondary,
'&:hover': {
color: colors.accent,
transform: 'scale(1.1)',
background: `${colors.highlight}80`
},
transition: 'all 0.3s ease'
}}
>
<FacebookIcon />
</IconButton>
<IconButton
href="https://twitter.com"
target="_blank"
sx={{
color: colors.secondary,
'&:hover': {
color: colors.accent,
transform: 'scale(1.1)',
background: `${colors.highlight}80`
},
transition: 'all 0.3s ease'
}}
>
<TwitterIcon />
</IconButton>
</Box>
<Typography
variant="caption"
sx={{
color: colors.secondary,
opacity: 0.7,
fontSize: '0.8rem',
fontFamily: '"Cormorant Garamond", serif'
}}
>
© {new Date().getFullYear()} The Craft Chronicle. All rights reserved.
</Typography>
</AnimatedSection>
</Container>
</Box>
</Box>
);
};
export default BlogPage;

View File

@ -7,6 +7,14 @@ import TwitterIcon from '@mui/icons-material/Twitter';
import EmailIcon from '@mui/icons-material/Email'; import EmailIcon from '@mui/icons-material/Email';
import { keyframes } from '@emotion/react'; import { keyframes } from '@emotion/react';
import { styled } from '@mui/system'; 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 logo from '../../assets/logobgremove.png'
// Color palette inspired by Indian heritage // Color palette inspired by Indian heritage
const colors = { const colors = {
@ -57,6 +65,60 @@ const scrollReveal = keyframes`
to { opacity: 1; transform: translateY(0); } to { opacity: 1; transform: translateY(0); }
`; `;
const shimmer = keyframes`
0% { background-position: -200px 0; }
100% { background-position: calc(200px + 100%) 0; }
`;
const textGlow = keyframes`
0% { text-shadow: 0 0 5px rgba(212, 160, 23, 0.3); }
50% { text-shadow: 0 0 15px rgba(212, 160, 23, 0.6); }
100% { text-shadow: 0 0 5px rgba(212, 160, 23, 0.3); }
`;
const borderFlow = keyframes`
0% { border-color: ${colors.accent}; }
33% { border-color: ${colors.secondary}; }
66% { border-color: ${colors.border}; }
100% { border-color: ${colors.accent}; }
`;
// const typewriter = keyframes`
// from { width: 0; }
// to { width: 100%; }
// `;
// const fadeInLeft = keyframes`
// from { opacity: 0; transform: translateX(-50px); }
// to { opacity: 1; transform: translateX(0); }
// `;
// const fadeInRight = keyframes`
// from { opacity: 0; transform: translateX(50px); }
// to { opacity: 1; transform: translateX(0); }
// `;
const bounce = keyframes`
0%, 20%, 50%, 80%, 100% { transform: translateY(0); }
40% { transform: translateY(-15px); }
60% { transform: translateY(-7px); }
`;
const flipIn = keyframes`
0% { transform: perspective(400px) rotateY(90deg); opacity: 0; }
100% { transform: perspective(400px) rotateY(0deg); opacity: 1; }
`;
// const zoomIn = keyframes`
// from { transform: scale(0.95); opacity: 0; }
// to { transform: scale(1); opacity: 1; }
// `;
const spinSlow = keyframes`
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
`;
// Styled components // Styled components
const VintageBox = styled(Box)({ const VintageBox = styled(Box)({
position: 'relative', position: 'relative',
@ -158,6 +220,35 @@ const AnimatedSection = styled(Box)({
animation: `${scrollReveal} 1s forwards`, animation: `${scrollReveal} 1s forwards`,
}); });
const GlowingText = styled(Typography)({
animation: `${textGlow} 3s ease-in-out infinite`,
});
const BorderFlowBox = styled(Box)({
border: `1px solid ${colors.accent}`,
animation: `${borderFlow} 6s infinite linear`,
});
// const TypewriterText = styled(Typography)({
// overflow: 'hidden',
// whiteSpace: 'nowrap',
// margin: '0 auto',
// animation: `${typewriter} 4s steps(40, end)`,
// });
const BounceBox = styled(Box)({
animation: `${bounce} 2s infinite`,
});
const FlipInBox = styled(Box)({
animation: `${flipIn} 1s forwards`,
transformOrigin: 'center',
});
// const ZoomInBox = styled(Box)({
// animation: `${zoomIn} 1s forwards`,
// });
// Kalawati story sections with updated Pexels images // Kalawati story sections with updated Pexels images
const storyChapters = [ const storyChapters = [
{ {
@ -165,42 +256,42 @@ const storyChapters = [
title: 'Where Curiosity Met Craft', title: 'Where Curiosity Met Craft',
content: 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.', '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: 'https://images.pexels.com/photos/28382914/pexels-photo-28382914.jpeg?auto=compress&cs=tinysrgb&w=500', image: `${image1}`,
}, },
{ {
year: '2020', year: '2020',
title: 'A Name Was Born', title: 'A Name Was Born',
content: 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.', '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: 'https://images.pexels.com/photos/31308739/pexels-photo-31308739.jpeg?auto=compress&cs=tinysrgb&w=500', image: `${image2}`,
}, },
{ {
year: '2021', year: '2021',
title: 'Strengthening the Circle', title: 'Strengthening the Circle',
content: 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.', '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: 'https://images.pexels.com/photos/3772504/pexels-photo-3772504.jpeg?auto=compress&cs=tinysrgb&w=500', image: `${image3}`,
}, },
{ {
year: '2022', year: '2022',
title: 'Systems That Serve People', title: 'Systems That Serve People',
content: content:
'Through the JSW Foundation Fellowship, I built systems blending business and empathy—brand building for rural women, value chain development, and sustainable income pathways.', 'Through the JSW Foundation Fellowship, I built systems blending business and empathy—brand building for rural women, value chain development, and sustainable income pathways.',
image: 'https://images.pexels.com/photos/163064/pexels-photo-163064.jpeg?auto=compress&cs=tinysrgb&w=500', image: `${image4}`,
}, },
{ {
year: '2023', year: '2023',
title: 'Quiet Creation', title: 'Quiet Creation',
content: content:
'Working with artisan clusters, we prototyped collections with handloom, embroidery, and natural dyes—crafting stories you could wear.', 'Working with artisan clusters, we prototyped collections with handloom, embroidery, and natural dyes—crafting stories you could wear.',
image: 'https://images.pexels.com/photos/145939/pexels-photo-145939.jpeg?auto=compress&cs=tinysrgb&w=500', image: `${image1}`,
}, },
{ {
year: '2024', year: '2024',
title: 'Kalawati Arrives', title: 'Kalawati Arrives',
content: content:
'The Kalawati opens its doors as a brand and movement, offering ethically made handcrafted apparel and decor. Join us to co-create, support heritage, and connect to roots.', 'The Kalawati opens its doors as a brand and movement, offering ethically made handcrafted apparel and decor. Join us to co-create, support heritage, and connect to roots.',
image: 'https://images.pexels.com/photos/774859/pexels-photo-774859.jpeg?auto=compress&cs=tinysrgb&w=500', image: `${image5}`,
}, },
]; ];
@ -209,22 +300,22 @@ const processSteps = [
{ {
title: 'Design Inspiration', title: 'Design Inspiration',
description: 'Drawing from traditional Indian motifs and contemporary aesthetics', description: 'Drawing from traditional Indian motifs and contemporary aesthetics',
image: 'https://images.pexels.com/photos/28382914/pexels-photo-28382914.jpeg?auto=compress&cs=tinysrgb&w=500', image: `${image4}`,
}, },
{ {
title: 'Material Selection', title: 'Material Selection',
description: 'Choosing the finest natural fibers and dyes', description: 'Choosing the finest natural fibers and dyes',
image: 'https://images.pexels.com/photos/145939/pexels-photo-145939.jpeg?auto=compress&cs=tinysrgb&w=500', image: `${image5}`,
}, },
{ {
title: 'Artisan Crafting', title: 'Artisan Crafting',
description: 'Skilled hands weaving stories into fabric', description: 'Skilled hands weaving stories into fabric',
image: 'https://images.pexels.com/photos/163064/pexels-photo-163064.jpeg?auto=compress&cs=tinysrgb&w=500', image: `${image4}`,
}, },
{ {
title: 'Quality Assurance', title: 'Quality Assurance',
description: 'Meticulous inspection ensuring perfection', description: 'Meticulous inspection ensuring perfection',
image: 'https://images.pexels.com/photos/3772504/pexels-photo-3772504.jpeg?auto=compress&cs=tinysrgb&w=500', image: `${image5}`,
}, },
]; ];
@ -287,10 +378,12 @@ export const VintageComingSoonPage: React.FC = () => {
const handleSubscribe = (e: React.MouseEvent<HTMLButtonElement>) => { const handleSubscribe = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault(); e.preventDefault();
console.log('Subscribed with email:', email); if (email && email.includes('@')) {
setSubscribed(true); console.log('Subscribed with email:', email);
setEmail(''); setSubscribed(true);
setTimeout(() => setSubscribed(false), 3000); setEmail('');
setTimeout(() => setSubscribed(false), 3000);
}
}; };
return ( return (
@ -310,6 +403,39 @@ export const VintageComingSoonPage: React.FC = () => {
}} }}
/> />
{/* Animated decorative elements */}
<Box
sx={{
position: 'fixed',
top: '10%',
right: '5%',
width: '100px',
height: '100px',
backgroundImage: `linear-gradient(90deg, ${colors.accent}20, ${colors.secondary}30, ${colors.accent}20)`,
backgroundSize: '200px 100px',
borderRadius: '50%',
animation: `${shimmer} 2s infinite linear, ${spinSlow} 20s infinite linear`,
zIndex: 0,
opacity: 0.3,
}}
/>
<Box
sx={{
position: 'fixed',
bottom: '15%',
left: '5%',
width: '80px',
height: '80px',
backgroundImage: `linear-gradient(90deg, ${colors.secondary}20, ${colors.accent}30, ${colors.secondary}20)`,
backgroundSize: '200px 100px',
borderRadius: '50%',
animation: `${shimmer} 3s infinite linear, ${spinSlow} 25s infinite linear reverse`,
zIndex: 0,
opacity: 0.3,
}}
/>
{/* Parallax background elements */} {/* Parallax background elements */}
<Box <Box
sx={{ sx={{
@ -318,14 +444,19 @@ export const VintageComingSoonPage: React.FC = () => {
right: '5%', right: '5%',
width: '200px', width: '200px',
height: '200px', height: '200px',
backgroundImage: 'url(https://images.pexels.com/photos/28382914/pexels-photo-28382914.jpeg?auto=compress&cs=tinysrgb&w=500)', backgroundImage: `url(${logo})`,
backgroundSize: 'cover', backgroundSize: 'cover',
opacity: 0.1, backgroundPosition: 'center',
opacity: 0.5,
zIndex: 0, zIndex: 0,
transform: `rotate(${scrollPosition * 0.02}deg)`, transform: `rotate(${scrollPosition * 0.02}deg)`,
transition: 'transform 0.3s ease-out', transition: 'transform 0.3s ease-out',
borderRadius: '50%', // makes it circular
overflow: 'hidden', // ensures image stays inside the circle
}} }}
/> />
<Box <Box
sx={{ sx={{
position: 'fixed', position: 'fixed',
@ -333,15 +464,19 @@ export const VintageComingSoonPage: React.FC = () => {
left: '5%', left: '5%',
width: '150px', width: '150px',
height: '150px', height: '150px',
backgroundImage: 'url(https://images.pexels.com/photos/145939/pexels-photo-145939.jpeg?auto=compress&cs=tinysrgb&w=500)', backgroundImage: `url(${logo})`,
backgroundSize: 'cover', backgroundSize: 'cover',
opacity: 0.1, backgroundPosition: 'center',
opacity: 0.4,
zIndex: 0, zIndex: 0,
transform: `rotate(${-scrollPosition * 0.03}deg)`, transform: `rotate(${-scrollPosition * 0.03}deg)`,
transition: 'transform 0.3s ease-out', transition: 'transform 0.3s ease-out',
borderRadius: '50%', // circular shape
overflow: 'hidden', // ensures the image fits inside the circle
}} }}
/> />
<Container maxWidth="lg" sx={{ position: 'relative', zIndex: 1, py: 6 }} ref={containerRef}> <Container maxWidth="lg" sx={{ position: 'relative', zIndex: 1, py: 6 }} ref={containerRef}>
{/* Header */} {/* Header */}
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderBottom: `1px solid ${colors.border}`, py: 4, mb: 4 }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderBottom: `1px solid ${colors.border}`, py: 4, mb: 4 }}>
@ -361,27 +496,33 @@ export const VintageComingSoonPage: React.FC = () => {
</AnimatedSection> </AnimatedSection>
<AnimatedSection ref={heroRef} style={{ animationDelay: heroInView ? '0.2s' : '0s' }}> <AnimatedSection ref={heroRef} style={{ animationDelay: heroInView ? '0.2s' : '0s' }}>
<Box sx={{ display: 'flex', gap: 2 }}> <Box sx={{ display: 'flex', gap: 2 }}>
<IconButton <BounceBox>
href="https://instagram.com" <IconButton
target="_blank" href="https://instagram.com"
sx={{ color: colors.secondary, '&:hover': { color: colors.accent, transform: 'scale(1.1)' }, transition: 'all 0.3s ease' }} target="_blank"
> sx={{ color: colors.secondary, '&:hover': { color: colors.accent, transform: 'scale(1.1)' }, transition: 'all 0.3s ease' }}
<InstagramIcon /> >
</IconButton> <InstagramIcon />
<IconButton </IconButton>
href="https://facebook.com" </BounceBox>
target="_blank" <BounceBox sx={{ animationDelay: '0.1s' }}>
sx={{ color: colors.secondary, '&:hover': { color: colors.accent, transform: 'scale(1.1)' }, transition: 'all 0.3s ease' }} <IconButton
> href="https://facebook.com"
<FacebookIcon /> target="_blank"
</IconButton> sx={{ color: colors.secondary, '&:hover': { color: colors.accent, transform: 'scale(1.1)' }, transition: 'all 0.3s ease' }}
<IconButton >
href="https://twitter.com" <FacebookIcon />
target="_blank" </IconButton>
sx={{ color: colors.secondary, '&:hover': { color: colors.accent, transform: 'scale(1.1)' }, transition: 'all 0.3s ease' }} </BounceBox>
> <BounceBox sx={{ animationDelay: '0.2s' }}>
<TwitterIcon /> <IconButton
</IconButton> href="https://twitter.com"
target="_blank"
sx={{ color: colors.secondary, '&:hover': { color: colors.accent, transform: 'scale(1.1)' }, transition: 'all 0.3s ease' }}
>
<TwitterIcon />
</IconButton>
</BounceBox>
</Box> </Box>
</AnimatedSection> </AnimatedSection>
</Box> </Box>
@ -450,7 +591,7 @@ export const VintageComingSoonPage: React.FC = () => {
flex: 1, flex: 1,
}} }}
> >
<Typography <GlowingText
variant="h1" variant="h1"
sx={{ sx={{
fontWeight: 300, fontWeight: 300,
@ -462,7 +603,7 @@ export const VintageComingSoonPage: React.FC = () => {
}} }}
> >
<span style={{ fontWeight: 400, color: colors.dark }}>A Journey</span> Woven with Stories, People & Purpose <span style={{ fontWeight: 400, color: colors.dark }}>A Journey</span> Woven with Stories, People & Purpose
</Typography> </GlowingText>
<Typography <Typography
variant="h5" variant="h5"
sx={{ sx={{
@ -544,7 +685,7 @@ export const VintageComingSoonPage: React.FC = () => {
{/* Email Subscription */} {/* Email Subscription */}
<Box> <Box>
<Box sx={{ display: 'flex', gap: 3, mb: 3, flexDirection: { xs: 'column', sm: 'row' } }}> <BorderFlowBox sx={{ display: 'flex', gap: 3, mb: 3, flexDirection: { xs: 'column', sm: 'row' }, p: '1px', borderRadius: '0' }}>
<input <input
type="email" type="email"
value={email} value={email}
@ -555,7 +696,7 @@ export const VintageComingSoonPage: React.FC = () => {
flex: 1, flex: 1,
padding: '16px 24px', padding: '16px 24px',
borderRadius: '0', borderRadius: '0',
border: `1px solid ${colors.border}`, border: 'none',
fontSize: '16px', fontSize: '16px',
outline: 'none', outline: 'none',
background: colors.paper, background: colors.paper,
@ -579,7 +720,7 @@ export const VintageComingSoonPage: React.FC = () => {
> >
Notify Me Notify Me
</VintagePulseButton> </VintagePulseButton>
</Box> </BorderFlowBox>
{subscribed && ( {subscribed && (
<Typography <Typography
sx={{ sx={{
@ -613,7 +754,7 @@ export const VintageComingSoonPage: React.FC = () => {
'&::before': { '&::before': {
content: '""', content: '""',
position: 'absolute', position: 'absolute',
top: 0, top: "20%",
left: '50%', left: '50%',
transform: 'translateX(-50%)', transform: 'translateX(-50%)',
width: '1px', width: '1px',
@ -788,9 +929,10 @@ export const VintageComingSoonPage: React.FC = () => {
'&:hover': { '&:hover': {
transform: 'translateY(-5px)', transform: 'translateY(-5px)',
}, },
height: '100%',
}} }}
> >
<VintageImageFrame <FlipInBox
sx={{ sx={{
height: '200px', height: '200px',
width: '100%', width: '100%',
@ -799,7 +941,6 @@ export const VintageComingSoonPage: React.FC = () => {
backgroundPosition: 'center', backgroundPosition: 'center',
mb: 3, mb: 3,
filter: 'sepia(0.2) contrast(1.05)', filter: 'sepia(0.2) contrast(1.05)',
animation: `${scaleIn} 1s ease`,
}} }}
/> />
<Typography variant="h6" sx={{ fontWeight: 400, color: colors.ink, mb: 2, fontFamily: '"Playfair Display", serif' }}> <Typography variant="h6" sx={{ fontWeight: 400, color: colors.ink, mb: 2, fontFamily: '"Playfair Display", serif' }}>
@ -847,33 +988,41 @@ export const VintageComingSoonPage: React.FC = () => {
Join our journey to weave stories, empower artisans, and celebrate Indian heritage. Be the first to experience our handcrafted collections. Join our journey to weave stories, empower artisans, and celebrate Indian heritage. Be the first to experience our handcrafted collections.
</Typography> </Typography>
<Box sx={{ display: 'flex', justifyContent: 'center', gap: 3, mb: 4 }}> <Box sx={{ display: 'flex', justifyContent: 'center', gap: 3, mb: 4 }}>
<IconButton <BounceBox>
href="https://instagram.com" <IconButton
target="_blank" href="https://instagram.com"
sx={{ color: colors.secondary, '&:hover': { color: colors.accent, transform: 'scale(1.1)' }, transition: 'all 0.3s ease' }} target="_blank"
> sx={{ color: colors.secondary, '&:hover': { color: colors.accent, transform: 'scale(1.1)' }, transition: 'all 0.3s ease' }}
<InstagramIcon /> >
</IconButton> <InstagramIcon />
<IconButton </IconButton>
href="https://facebook.com" </BounceBox>
target="_blank" <BounceBox sx={{ animationDelay: '0.1s' }}>
sx={{ color: colors.secondary, '&:hover': { color: colors.accent, transform: 'scale(1.1)' }, transition: 'all 0.3s ease' }} <IconButton
> href="https://facebook.com"
<FacebookIcon /> target="_blank"
</IconButton> sx={{ color: colors.secondary, '&:hover': { color: colors.accent, transform: 'scale(1.1)' }, transition: 'all 0.3s ease' }}
<IconButton >
href="https://twitter.com" <FacebookIcon />
target="_blank" </IconButton>
sx={{ color: colors.secondary, '&:hover': { color: colors.accent, transform: 'scale(1.1)' }, transition: 'all 0.3s ease' }} </BounceBox>
> <BounceBox sx={{ animationDelay: '0.2s' }}>
<TwitterIcon /> <IconButton
</IconButton> href="https://twitter.com"
<IconButton target="_blank"
href="mailto:contact@thekalawati.com" sx={{ color: colors.secondary, '&:hover': { color: colors.accent, transform: 'scale(1.1)' }, transition: 'all 0.3s ease' }}
sx={{ color: colors.secondary, '&:hover': { color: colors.accent, transform: 'scale(1.1)' }, transition: 'all 0.3s ease' }} >
> <TwitterIcon />
<EmailIcon /> </IconButton>
</IconButton> </BounceBox>
<BounceBox sx={{ animationDelay: '0.3s' }}>
<IconButton
href="mailto:contact@thekalawati.com"
sx={{ color: colors.secondary, '&:hover': { color: colors.accent, transform: 'scale(1.1)' }, transition: 'all 0.3s ease' }}
>
<EmailIcon />
</IconButton>
</BounceBox>
</Box> </Box>
<Typography <Typography
variant="caption" variant="caption"

View File

@ -0,0 +1,708 @@
import React, { useState } from 'react';
import { Box, Typography, Button, Container, IconButton, Grid, Card, Chip, CardMedia } 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 EmailIcon from '@mui/icons-material/Email';
import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
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'
// Lighter color palette for product showcase
const colors = {
lightBase: '#F8F7F4', // Very light cream
lightPaper: '#FFFFFF', // White
lightInk: '#3E3E3E', // Dark gray for text
accent: '#C19A6B', // Muted gold
secondary: '#8E7D6D', // Muted taupe
highlight: '#E8D9C7', // Light sand
border: '#D1C6B5', // Light tan border
dark: '#5C4B3A', // Dark brown for contrast
};
// Smooth animations
const fadeIn = keyframes`
from { opacity: 0; transform: translateY(20px); }
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); }
100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(193, 154, 107, 0); }
`;
// Styled components
const LightVintageBox = styled(Box)({
position: 'relative',
backgroundColor: colors.lightBase,
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: `url("data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z' fill='%235C4B3A' fill-opacity='0.05' fill-rule='evenodd'/%3E%3C/svg%3E")`,
opacity: 0.1,
zIndex: 0,
},
});
const LightVintageButton = styled(Button)({
position: 'relative',
overflow: 'hidden',
border: `1px solid ${colors.border}`,
color: colors.lightInk,
backgroundColor: colors.lightPaper,
fontWeight: 400,
letterSpacing: '0.5px',
textTransform: 'uppercase',
padding: '10px 22px',
borderRadius: '2px',
transition: 'all 0.3s ease',
boxShadow: '0 2px 8px rgba(0,0,0,0.05)',
'&:hover': {
backgroundColor: colors.highlight,
color: colors.dark,
borderColor: colors.accent,
transform: 'translateY(-2px)',
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
},
'&::after': {
content: '""',
position: 'absolute',
bottom: 0,
left: 0,
width: '100%',
height: '1px',
backgroundColor: colors.accent,
transform: 'scaleX(0)',
transformOrigin: 'right',
transition: 'transform 0.3s ease',
},
'&:hover::after': {
transform: 'scaleX(1)',
transformOrigin: 'left',
},
});
const LightVintagePulseButton = styled(LightVintageButton)({
animation: `${smoothPulse} 3s infinite`,
'&:hover': {
animation: 'none',
},
});
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)',
animation: `${fadeIn} 0.8s forwards`,
});
// Product data
const products = [
{
id: 1,
name: 'Handwoven Silk Saree',
price: '₹12,999',
description: 'Traditional Banarasi silk with zari work',
image: `${image5}`,
category: 'Clothing',
tags: ['Handmade', 'Premium'],
},
{
id: 2,
name: 'Block Print Cotton Kurta',
price: '₹3,499',
description: 'Hand-block printed with natural dyes',
image: `${image4}`,
category: 'Clothing',
tags: ['Eco-friendly', 'Handmade'],
},
{
id: 3,
name: 'Brass Pooja Thali',
price: '₹2,899',
description: 'Handcrafted brass prayer set with intricate designs',
image: `${image4}`,
category: 'Home Decor',
tags: ['Ritual', 'Handcrafted'],
},
{
id: 4,
name: 'Embroidered Jutti',
price: '₹1,899',
description: 'Traditional Punjabi footwear with phulkari work',
image: `${image5}`,
category: 'Footwear',
tags: ['Handmade', 'Comfort'],
},
{
id: 5,
name: 'Handloom Cushion Covers',
price: '₹1,299',
description: 'Set of 2 handwoven cushion covers',
image: `${image4}`,
category: 'Home Decor',
tags: ['Handmade', 'Set'],
},
{
id: 6,
name: 'Silver Tribal Jewelry Set',
price: '₹5,999',
description: 'Traditional tribal necklace and earrings set',
image: `${image5}`,
category: 'Jewelry',
tags: ['Handcrafted', 'Silver'],
},
];
const categories = [
{ name: 'All', value: 'all' },
{ name: 'Clothing', value: 'Clothing' },
{ name: 'Home Decor', value: 'Home Decor' },
{ name: 'Jewelry', value: 'Jewelry' },
{ name: 'Footwear', value: 'Footwear' },
];
export const LightProductShowcase: React.FC = () => {
const [selectedCategory, setSelectedCategory] = useState('all');
const [hoveredProduct, setHoveredProduct] = useState<number | null>(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 });
const filteredProducts = selectedCategory === 'all'
? products
: products.filter(product => product.category === selectedCategory);
return (
<LightVintageBox sx={{ minHeight: '100vh', color: colors.lightInk, overflow: 'hidden', position: 'relative' }}>
{/* Paper texture overlay */}
<Box
sx={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
backgroundImage:
'url("data:image/svg+xml,%3Csvg width=\'600\' height=\'600\' viewBox=\'0 0 600 600\' xmlns=\'http://www.w3.org/2000/svg\'%3E%3Cfilter id=\'noiseFilter\'%3E%3CfeTurbulence type=\'fractalNoise\' baseFrequency=\'0.65\' numOctaves=\'3\' stitchTiles=\'stitch\'/%3E%3C/filter%3E%3Crect width=\'100%25\' height=\'100%25\' filter=\'url(%23noiseFilter)\' opacity=\'0.03\'/%3E%3C/svg%3E")',
opacity: 0.3,
zIndex: 0,
}}
/>
{/* Animated decorative elements */}
<Box
sx={{
position: 'fixed',
top: '15%',
right: '5%',
width: '80px',
height: '80px',
background: `linear-gradient(135deg, ${colors.accent}15, ${colors.secondary}20, ${colors.accent}15)`,
borderRadius: '50%',
animation: `${float} 8s ease-in-out infinite`,
zIndex: 0,
opacity: 0.4,
}}
/>
<Box
sx={{
position: 'fixed',
bottom: '20%',
left: '5%',
width: '60px',
height: '60px',
background: `linear-gradient(135deg, ${colors.secondary}15, ${colors.accent}20, ${colors.secondary}15)`,
borderRadius: '50%',
animation: `${float} 7s ease-in-out infinite reverse`,
zIndex: 0,
opacity: 0.4,
}}
/>
<Container maxWidth="lg" sx={{ position: 'relative', zIndex: 1, py: 5 }}>
{/* Header */}
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderBottom: `1px solid ${colors.border}`, py: 3, mb: 4 }}>
<AnimatedSection ref={heroRef} style={{ animationDelay: heroInView ? '0.1s' : '0s' }}>
<Typography
variant="h3"
sx={{
fontWeight: 500,
color: colors.lightInk,
letterSpacing: '2px',
fontFamily: '"Playfair Display", serif',
}}
>
<LightVintageUnderline>The Kalawati Collection</LightVintageUnderline>
</Typography>
</AnimatedSection>
<AnimatedSection ref={heroRef} style={{ animationDelay: heroInView ? '0.2s' : '0s' }}>
<Box sx={{ display: 'flex', gap: 1 }}>
<IconButton
href="https://instagram.com"
target="_blank"
sx={{
color: colors.secondary,
'&:hover': { color: colors.accent, transform: 'scale(1.1)' },
transition: 'all 0.3s ease'
}}
>
<InstagramIcon />
</IconButton>
<IconButton
href="https://facebook.com"
target="_blank"
sx={{
color: colors.secondary,
'&:hover': { color: colors.accent, transform: 'scale(1.1)' },
transition: 'all 0.3s ease'
}}
>
<FacebookIcon />
</IconButton>
<IconButton
href="https://twitter.com"
target="_blank"
sx={{
color: colors.secondary,
'&:hover': { color: colors.accent, transform: 'scale(1.1)' },
transition: 'all 0.3s ease'
}}
>
<TwitterIcon />
</IconButton>
</Box>
</AnimatedSection>
</Box>
{/* Hero Section */}
<Box
sx={{
display: 'flex',
flexDirection: { xs: 'column', md: 'row' },
alignItems: 'center',
gap: 6,
py: 6,
minHeight: '50vh',
}}
>
<AnimatedSection
ref={heroRef}
style={{
animationDelay: heroInView ? '0.3s' : '0s',
flex: 1,
}}
>
<Box
sx={{
height: { xs: '350px', md: '450px' },
backgroundImage: `url(${image2})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
borderRadius: '4px',
boxShadow: '0 4px 20px rgba(0,0,0,0.08)',
animation: `${scaleIn} 1s ease`,
}}
/>
</AnimatedSection>
<AnimatedSection
ref={heroRef}
style={{
animationDelay: heroInView ? '0.4s' : '0s',
flex: 1,
}}
>
<Typography
variant="h3"
sx={{
fontWeight: 300,
mb: 3,
fontSize: { xs: '2.2rem', md: '2.8rem' },
lineHeight: 1.3,
color: colors.lightInk,
fontFamily: '"Playfair Display", serif',
}}
>
<span style={{ fontWeight: 400, color: colors.dark }}>Timeless</span> Craftsmanship, Modern Elegance
</Typography>
<Typography
variant="h6"
sx={{
mb: 4,
color: colors.secondary,
fontWeight: 300,
fontStyle: 'italic',
fontFamily: '"Cormorant Garamond", serif',
pl: 3,
position: 'relative',
'&::before': {
content: '""',
position: 'absolute',
left: 0,
top: 0,
height: '100%',
width: '2px',
background: colors.accent,
},
}}
>
Each piece tells a story of tradition, skill, and the hands that crafted it with love and dedication.
</Typography>
<LightVintagePulseButton
variant="contained"
sx={{
px: 4,
py: 1.5,
fontWeight: 300,
fontFamily: '"Cormorant Garamond", serif',
letterSpacing: '1.5px',
color: colors.lightInk,
'&:hover': { background: colors.highlight },
}}
>
Explore Collection
</LightVintagePulseButton>
</AnimatedSection>
</Box>
{/* Category Filter */}
<Box sx={{ my: 6, textAlign: 'center' }}>
<AnimatedSection style={{ animationDelay: '0.1s' }}>
<Typography
variant="h5"
sx={{
mb: 3,
fontWeight: 300,
color: colors.lightInk,
fontFamily: '"Playfair Display", serif',
}}
>
Browse Categories
</Typography>
</AnimatedSection>
<Box sx={{ display: 'flex', justifyContent: 'center', flexWrap: 'wrap', gap: 1.5, mb: 5 }}>
{categories.map((category) => (
<Chip
key={category.value}
label={category.name}
onClick={() => setSelectedCategory(category.value)}
variant={selectedCategory === category.value ? "filled" : "outlined"}
sx={{
color: selectedCategory === category.value ? colors.lightPaper : colors.lightInk,
backgroundColor: selectedCategory === category.value ? colors.accent : 'transparent',
borderColor: colors.border,
fontFamily: '"Cormorant Garamond", serif',
fontSize: '1rem',
padding: '8px 16px',
'&:hover': {
backgroundColor: selectedCategory === category.value ? colors.accent : colors.highlight,
},
}}
/>
))}
</Box>
</Box>
{/* Products Grid */}
<Box ref={productsRef} sx={{ my: 8 }}>
<Grid container spacing={3}>
{filteredProducts.map((product, index) => (
<Grid key={product.id}>
<AnimatedSection
style={{
animationDelay: productsInView ? `${index * 0.1}s` : '0s',
}}
>
<Card
sx={{
background: colors.lightPaper,
border: `1px solid ${colors.border}`,
borderRadius: '4px',
overflow: 'hidden',
transition: 'all 0.3s ease',
height: '100%',
display: 'flex',
flexDirection: 'column',
boxShadow: '0 4px 12px rgba(0,0,0,0.05)',
'&:hover': {
transform: 'translateY(-4px)',
boxShadow: `0 8px 24px rgba(0,0,0,0.1)`,
},
}}
onMouseEnter={() => setHoveredProduct(product.id)}
onMouseLeave={() => setHoveredProduct(null)}
>
<Box sx={{ position: 'relative', overflow: 'hidden' }}>
<CardMedia
component="img"
height="300"
image={product.image}
alt={product.name}
sx={{
transition: 'all 0.5s ease',
transform: hoveredProduct === product.id ? 'scale(1.05)' : 'scale(1)',
filter: 'brightness(0.98)',
}}
/>
<Box
sx={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: `linear-gradient(to bottom, transparent 0%, ${colors.lightBase} 100%)`,
opacity: hoveredProduct === product.id ? 0.8 : 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 2,
transition: 'all 0.3s ease',
}}
>
<IconButton sx={{ color: colors.lightInk, background: `${colors.lightPaper}`, '&:hover': { background: colors.accent, color: colors.lightPaper } }}>
<FavoriteBorderIcon />
</IconButton>
<IconButton sx={{ color: colors.lightInk, background: `${colors.lightPaper}`, '&:hover': { background: colors.accent, color: colors.lightPaper } }}>
<ShoppingCartIcon />
</IconButton>
<IconButton sx={{ color: colors.lightInk, background: `${colors.lightPaper}`, '&:hover': { background: colors.accent, color: colors.lightPaper } }}>
<VisibilityIcon />
</IconButton>
</Box>
<Box sx={{ position: 'absolute', top: 10, right: 10 }}>
{product.tags.map((tag, i) => (
<Chip
key={i}
label={tag}
size="small"
sx={{
background: colors.accent,
color: colors.lightPaper,
fontSize: '0.7rem',
height: '22px',
mb: 0.5,
display: 'block',
}}
/>
))}
</Box>
</Box>
<Box sx={{ p: 2.5, flexGrow: 1, display: 'flex', flexDirection: 'column' }}>
<Typography
variant="h6"
sx={{
fontWeight: 400,
color: colors.lightInk,
fontFamily: '"Playfair Display", serif',
mb: 1,
}}
>
{product.name}
</Typography>
<Typography
variant="body2"
sx={{
color: colors.secondary,
fontFamily: '"Cormorant Garamond", serif',
mb: 2,
flexGrow: 1,
fontSize: '0.95rem',
}}
>
{product.description}
</Typography>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography
variant="h6"
sx={{
color: colors.accent,
fontFamily: '"Playfair Display", serif',
}}
>
{product.price}
</Typography>
<LightVintageButton size="small">
Add to Cart
</LightVintageButton>
</Box>
</Box>
</Card>
</AnimatedSection>
</Grid>
))}
</Grid>
</Box>
{/* Call to Action */}
<Box sx={{ my: 8, py: 8, textAlign: 'center', background: colors.highlight, borderRadius: '4px' }}>
<Container maxWidth="md">
<AnimatedSection style={{ animationDelay: '0.1s' }}>
<Typography
variant="h4"
sx={{
mb: 3,
fontWeight: 300,
color: colors.lightInk,
fontFamily: '"Playfair Display", serif',
}}
>
Join Our Artisan Community
</Typography>
</AnimatedSection>
<AnimatedSection style={{ animationDelay: '0.2s' }}>
<Typography
variant="body1"
sx={{
color: colors.secondary,
mb: 5,
fontSize: '1.05rem',
fontFamily: '"Cormorant Garamond", serif',
lineHeight: 1.7,
}}
>
By purchasing our products, you're not just buying beautiful handcrafted items—you're supporting traditional artisans and helping preserve India's rich cultural heritage for future generations.
</Typography>
</AnimatedSection>
<AnimatedSection style={{ animationDelay: '0.3s' }}>
<LightVintagePulseButton
variant="contained"
sx={{
px: 5,
py: 1.5,
fontWeight: 300,
fontFamily: '"Cormorant Garamond", serif',
letterSpacing: '1.5px',
color: colors.lightInk,
'&:hover': { background: colors.highlight },
}}
>
Discover Our Story
</LightVintagePulseButton>
</AnimatedSection>
</Container>
</Box>
{/* Footer */}
<Box ref={footerRef} sx={{ py: 5, textAlign: 'center', borderTop: `1px solid ${colors.border}` }}>
<AnimatedSection style={{ animationDelay: footerInView ? '0.1s' : '0s' }}>
<Typography
variant="h6"
sx={{
mb: 2,
fontWeight: 300,
color: colors.lightInk,
letterSpacing: '3px',
fontFamily: '"Playfair Display", serif',
textTransform: 'uppercase',
}}
>
Handcrafted Heritage
</Typography>
<Typography
variant="body2"
sx={{
color: colors.secondary,
mb: 3,
maxWidth: '600px',
margin: '0 auto',
fontSize: '0.95rem',
fontFamily: '"Cormorant Garamond", serif',
lineHeight: 1.6,
}}
>
Each piece tells a story of India's rich heritage and skilled artisanship.
</Typography>
<Box sx={{ display: 'flex', justifyContent: 'center', gap: 2, mb: 3 }}>
<IconButton
href="https://instagram.com"
target="_blank"
sx={{ color: colors.secondary, '&:hover': { color: colors.accent, transform: 'scale(1.1)' }, transition: 'all 0.3s ease' }}
>
<InstagramIcon />
</IconButton>
<IconButton
href="https://facebook.com"
target="_blank"
sx={{ color: colors.secondary, '&:hover': { color: colors.accent, transform: 'scale(1.1)' }, transition: 'all 0.3s ease' }}
>
<FacebookIcon />
</IconButton>
<IconButton
href="https://twitter.com"
target="_blank"
sx={{ color: colors.secondary, '&:hover': { color: colors.accent, transform: 'scale(1.1)' }, transition: 'all 0.3s ease' }}
>
<TwitterIcon />
</IconButton>
<IconButton
href="mailto:contact@thekalawati.com"
sx={{ color: colors.secondary, '&:hover': { color: colors.accent, transform: 'scale(1.1)' }, transition: 'all 0.3s ease' }}
>
<EmailIcon />
</IconButton>
</Box>
<Typography
variant="caption"
sx={{ color: colors.secondary, opacity: 0.7, fontSize: '0.8rem', fontFamily: '"Cormorant Garamond", serif' }}
>
© {new Date().getFullYear()} Handcrafted Heritage. All rights reserved.
</Typography>
</AnimatedSection>
</Box>
</Container>
</LightVintageBox>
);
};
export default LightProductShowcase;

104
src/data/blogs.json Normal file
View File

@ -0,0 +1,104 @@
{
"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"
}
],
"categories": [
"Artisan Techniques",
"Sustainable Practices",
"Artisan Stories",
"Design Innovation",
"Cultural Heritage"
],
"tags": [
"Textiles",
"Craft",
"Heritage",
"Eco-friendly",
"Sustainability",
"Natural",
"Weaving",
"Silk",
"Embroidery",
"Design",
"Revival"
]
}

24
src/types/blogs.tsx Normal file
View File

@ -0,0 +1,24 @@
export interface BlogAuthor {
name: string;
avatar: string;
role: string;
}
export interface BlogPost {
id: string;
title: string;
excerpt: string;
content: string;
image: string;
author: BlogAuthor;
date: string;
readTime: string;
tags: string[];
category: string;
}
export interface BlogData {
posts: BlogPost[];
categories: string[];
tags: string[];
}

479
yarn.lock
View File

@ -173,7 +173,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.28.3, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.7": "@babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.28.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.7":
version: 7.28.4 version: 7.28.4
resolution: "@babel/runtime@npm:7.28.4" resolution: "@babel/runtime@npm:7.28.4"
checksum: 10c0/792ce7af9750fb9b93879cc9d1db175701c4689da890e6ced242ea0207c9da411ccf16dc04e689cc01158b28d7898c40d75598f4559109f761c12ce01e959bf7 checksum: 10c0/792ce7af9750fb9b93879cc9d1db175701c4689da890e6ced242ea0207c9da411ccf16dc04e689cc01158b28d7898c40d75598f4559109f761c12ce01e959bf7
@ -661,6 +661,22 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@isaacs/balanced-match@npm:^4.0.1":
version: 4.0.1
resolution: "@isaacs/balanced-match@npm:4.0.1"
checksum: 10c0/7da011805b259ec5c955f01cee903da72ad97c5e6f01ca96197267d3f33103d5b2f8a1af192140f3aa64526c593c8d098ae366c2b11f7f17645d12387c2fd420
languageName: node
linkType: hard
"@isaacs/brace-expansion@npm:^5.0.0":
version: 5.0.0
resolution: "@isaacs/brace-expansion@npm:5.0.0"
dependencies:
"@isaacs/balanced-match": "npm:^4.0.1"
checksum: 10c0/b4d4812f4be53afc2c5b6c545001ff7a4659af68d4484804e9d514e183d20269bb81def8682c01a22b17c4d6aed14292c8494f7d2ac664e547101c1a905aa977
languageName: node
linkType: hard
"@isaacs/cliui@npm:^8.0.2": "@isaacs/cliui@npm:^8.0.2":
version: 8.0.2 version: 8.0.2
resolution: "@isaacs/cliui@npm:8.0.2" resolution: "@isaacs/cliui@npm:8.0.2"
@ -728,49 +744,49 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@mui/core-downloads-tracker@npm:^7.3.2": "@mui/core-downloads-tracker@npm:^7.3.5":
version: 7.3.2 version: 7.3.5
resolution: "@mui/core-downloads-tracker@npm:7.3.2" resolution: "@mui/core-downloads-tracker@npm:7.3.5"
checksum: 10c0/8549ac661e07926e1c1de2664ad50f68fb4f3f6050f3cfe7bf2e8a7ceaefde99c1615f4ab5185dff22a7d72874d1dcc5fdc3651d08ed4eb1abfb798629f3991f checksum: 10c0/72c71d43b3609ccd5eab5b3bfc5bfc2232b79cfb210cb64a66298de0b2effccb1843aa8cdb6e062bc6f5df91c02d70de84984bb6fab9745ffdf00e81a574dc9b
languageName: node languageName: node
linkType: hard linkType: hard
"@mui/icons-material@npm:^7.3.2": "@mui/icons-material@npm:^7.3.2":
version: 7.3.2 version: 7.3.5
resolution: "@mui/icons-material@npm:7.3.2" resolution: "@mui/icons-material@npm:7.3.5"
dependencies: dependencies:
"@babel/runtime": "npm:^7.28.3" "@babel/runtime": "npm:^7.28.4"
peerDependencies: peerDependencies:
"@mui/material": ^7.3.2 "@mui/material": ^7.3.5
"@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta: peerDependenciesMeta:
"@types/react": "@types/react":
optional: true optional: true
checksum: 10c0/25d1f0dcbbb4a35f320aac03317657b15631f9d5b9d48d07e34850d61253a2b4d8dd5e5ca624d76f44b5ceda471ea57d2611f6627fec1eef9fce78855ec01cbf checksum: 10c0/5fa551acabc8eddf30113a45a7c7b9923ec0f520608442cb914345493208aaa5b4ba852ad4b766ab58c547a2eb498ab4d6c1a3ebf5d4055a006c2327e397f88c
languageName: node languageName: node
linkType: hard linkType: hard
"@mui/material@npm:^7.3.2": "@mui/material@npm:^7.3.2":
version: 7.3.2 version: 7.3.5
resolution: "@mui/material@npm:7.3.2" resolution: "@mui/material@npm:7.3.5"
dependencies: dependencies:
"@babel/runtime": "npm:^7.28.3" "@babel/runtime": "npm:^7.28.4"
"@mui/core-downloads-tracker": "npm:^7.3.2" "@mui/core-downloads-tracker": "npm:^7.3.5"
"@mui/system": "npm:^7.3.2" "@mui/system": "npm:^7.3.5"
"@mui/types": "npm:^7.4.6" "@mui/types": "npm:^7.4.8"
"@mui/utils": "npm:^7.3.2" "@mui/utils": "npm:^7.3.5"
"@popperjs/core": "npm:^2.11.8" "@popperjs/core": "npm:^2.11.8"
"@types/react-transition-group": "npm:^4.4.12" "@types/react-transition-group": "npm:^4.4.12"
clsx: "npm:^2.1.1" clsx: "npm:^2.1.1"
csstype: "npm:^3.1.3" csstype: "npm:^3.1.3"
prop-types: "npm:^15.8.1" prop-types: "npm:^15.8.1"
react-is: "npm:^19.1.1" react-is: "npm:^19.2.0"
react-transition-group: "npm:^4.4.5" react-transition-group: "npm:^4.4.5"
peerDependencies: peerDependencies:
"@emotion/react": ^11.5.0 "@emotion/react": ^11.5.0
"@emotion/styled": ^11.3.0 "@emotion/styled": ^11.3.0
"@mui/material-pigment-css": ^7.3.2 "@mui/material-pigment-css": ^7.3.5
"@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
@ -783,16 +799,16 @@ __metadata:
optional: true optional: true
"@types/react": "@types/react":
optional: true optional: true
checksum: 10c0/4b82a65af93fe9517991f45c2f9dc127728199921f5c4c5cd7a8cd48e1c89ba17799011440f1b7e32993871c13b3044878a3170ddd9ce0e7cfe5ca0e7e3613f2 checksum: 10c0/363ff79d0eaf8044510529bf8d143d7e6f5298297dcfbd0816c5caf49e9514bde6b187b0e135cc1d73ac2ecfec1b06d3fea3bba952e7d8d0bfc997d0510fff8a
languageName: node languageName: node
linkType: hard linkType: hard
"@mui/private-theming@npm:^7.3.2": "@mui/private-theming@npm:^7.3.5":
version: 7.3.2 version: 7.3.5
resolution: "@mui/private-theming@npm:7.3.2" resolution: "@mui/private-theming@npm:7.3.5"
dependencies: dependencies:
"@babel/runtime": "npm:^7.28.3" "@babel/runtime": "npm:^7.28.4"
"@mui/utils": "npm:^7.3.2" "@mui/utils": "npm:^7.3.5"
prop-types: "npm:^15.8.1" prop-types: "npm:^15.8.1"
peerDependencies: peerDependencies:
"@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0
@ -800,15 +816,15 @@ __metadata:
peerDependenciesMeta: peerDependenciesMeta:
"@types/react": "@types/react":
optional: true optional: true
checksum: 10c0/fb6067e92a1bc02d4b2b49fa58901200ccf4b79760a0227bf2859bd2cb99c46ba0f43ece9494eaa220710703eaf309a7c6e732daf4176520c0a16e1407846399 checksum: 10c0/1347cf2a1ec1ae93d26134143c20314d53dac61fe5c8c7aa00ab37d9e89f5e6245f787dee9b0bf3d34fc614c9a5da1f5d45759fcd2520ddef4c10e755c4abc5e
languageName: node languageName: node
linkType: hard linkType: hard
"@mui/styled-engine@npm:^7.3.2": "@mui/styled-engine@npm:^7.3.5":
version: 7.3.2 version: 7.3.5
resolution: "@mui/styled-engine@npm:7.3.2" resolution: "@mui/styled-engine@npm:7.3.5"
dependencies: dependencies:
"@babel/runtime": "npm:^7.28.3" "@babel/runtime": "npm:^7.28.4"
"@emotion/cache": "npm:^11.14.0" "@emotion/cache": "npm:^11.14.0"
"@emotion/serialize": "npm:^1.3.3" "@emotion/serialize": "npm:^1.3.3"
"@emotion/sheet": "npm:^1.4.0" "@emotion/sheet": "npm:^1.4.0"
@ -823,19 +839,19 @@ __metadata:
optional: true optional: true
"@emotion/styled": "@emotion/styled":
optional: true optional: true
checksum: 10c0/d5644b40269a70a1c86844f7301aa6865289994e7835b471f3503e67795010d5334362cfd21d8804f54e8b71d6c9c932ca78bafc2325767e3abbe037f9e8e10b checksum: 10c0/01dc8aefde58d5257564b7fd40f37de0f76d79e6bc6b52738cf41c333a053623baf2648f0557fb4b5ded306fd2b98e94797d7e48ad1c1f297747d2a265e22ad0
languageName: node languageName: node
linkType: hard linkType: hard
"@mui/system@npm:^7.3.2": "@mui/system@npm:^7.3.5":
version: 7.3.2 version: 7.3.5
resolution: "@mui/system@npm:7.3.2" resolution: "@mui/system@npm:7.3.5"
dependencies: dependencies:
"@babel/runtime": "npm:^7.28.3" "@babel/runtime": "npm:^7.28.4"
"@mui/private-theming": "npm:^7.3.2" "@mui/private-theming": "npm:^7.3.5"
"@mui/styled-engine": "npm:^7.3.2" "@mui/styled-engine": "npm:^7.3.5"
"@mui/types": "npm:^7.4.6" "@mui/types": "npm:^7.4.8"
"@mui/utils": "npm:^7.3.2" "@mui/utils": "npm:^7.3.5"
clsx: "npm:^2.1.1" clsx: "npm:^2.1.1"
csstype: "npm:^3.1.3" csstype: "npm:^3.1.3"
prop-types: "npm:^15.8.1" prop-types: "npm:^15.8.1"
@ -851,41 +867,41 @@ __metadata:
optional: true optional: true
"@types/react": "@types/react":
optional: true optional: true
checksum: 10c0/ed385c37f29a8d4b57bc1c59f8bc06a3e4cc393d86a6e0059229eacc7c96bcb11ae80369de0e459971bde24bdd33078f5578f152f0ac2e796222b269a80833ed checksum: 10c0/12ed6e0f4770848c91c066b6ffb315f2cd31d6281ff12780f8d994d5b677750277812491ba502831601bbe66cbc48812268ae08b7bbc10120e7faf5616807489
languageName: node languageName: node
linkType: hard linkType: hard
"@mui/types@npm:^7.4.6": "@mui/types@npm:^7.4.8":
version: 7.4.6 version: 7.4.8
resolution: "@mui/types@npm:7.4.6" resolution: "@mui/types@npm:7.4.8"
dependencies: dependencies:
"@babel/runtime": "npm:^7.28.3" "@babel/runtime": "npm:^7.28.4"
peerDependencies: peerDependencies:
"@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta: peerDependenciesMeta:
"@types/react": "@types/react":
optional: true optional: true
checksum: 10c0/baa901e410591d0216b3f959cdbf5a1ee2ce560726d2fba1c700b40f64c1be3e63bd799f1b30a7d0bc8cc45a46d782928ea28d9906d64438f21e305884c48a99 checksum: 10c0/dfdca47c894da372236f7c209544abd2998a77af646baf28d97a49313064b293b2fb434e45f9e5331e3123f9f8863f7b3e1db8542d7bde2d4f1e5f030d85f0c1
languageName: node languageName: node
linkType: hard linkType: hard
"@mui/utils@npm:^7.3.2": "@mui/utils@npm:^7.3.5":
version: 7.3.2 version: 7.3.5
resolution: "@mui/utils@npm:7.3.2" resolution: "@mui/utils@npm:7.3.5"
dependencies: dependencies:
"@babel/runtime": "npm:^7.28.3" "@babel/runtime": "npm:^7.28.4"
"@mui/types": "npm:^7.4.6" "@mui/types": "npm:^7.4.8"
"@types/prop-types": "npm:^15.7.15" "@types/prop-types": "npm:^15.7.15"
clsx: "npm:^2.1.1" clsx: "npm:^2.1.1"
prop-types: "npm:^15.8.1" prop-types: "npm:^15.8.1"
react-is: "npm:^19.1.1" react-is: "npm:^19.2.0"
peerDependencies: peerDependencies:
"@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta: peerDependenciesMeta:
"@types/react": "@types/react":
optional: true optional: true
checksum: 10c0/5a88ff08a823b976421f8d61098d56d527d95c222800d5b3f71acff795e7b0db6b02e40228773a6ed7ee22d8eaa607d816215b5a4b6497c21aaa9668c2699b56 checksum: 10c0/c9f9ce12a5053d7aeafd0c390e7d17d331e0366dec9d993c9ad860f78c1d9410e5a33c40601afa039f4120ea299d2a59e76eff705359c7d96fb09ce636ba72b9
languageName: node languageName: node
linkType: hard linkType: hard
@ -916,16 +932,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@npmcli/agent@npm:^3.0.0": "@npmcli/agent@npm:^4.0.0":
version: 3.0.0 version: 4.0.0
resolution: "@npmcli/agent@npm:3.0.0" resolution: "@npmcli/agent@npm:4.0.0"
dependencies: dependencies:
agent-base: "npm:^7.1.0" agent-base: "npm:^7.1.0"
http-proxy-agent: "npm:^7.0.0" http-proxy-agent: "npm:^7.0.0"
https-proxy-agent: "npm:^7.0.1" https-proxy-agent: "npm:^7.0.1"
lru-cache: "npm:^10.0.1" lru-cache: "npm:^11.2.1"
socks-proxy-agent: "npm:^8.0.3" socks-proxy-agent: "npm:^8.0.3"
checksum: 10c0/efe37b982f30740ee77696a80c196912c274ecd2cb243bc6ae7053a50c733ce0f6c09fda085145f33ecf453be19654acca74b69e81eaad4c90f00ccffe2f9271 checksum: 10c0/f7b5ce0f3dd42c3f8c6546e8433573d8049f67ef11ec22aa4704bc41483122f68bf97752e06302c455ead667af5cb753e6a09bff06632bc465c1cfd4c4b75a53
languageName: node languageName: node
linkType: hard linkType: hard
@ -938,13 +954,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@pkgjs/parseargs@npm:^0.11.0":
version: 0.11.0
resolution: "@pkgjs/parseargs@npm:0.11.0"
checksum: 10c0/5bd7576bb1b38a47a7fc7b51ac9f38748e772beebc56200450c4a817d712232b8f1d3ef70532c80840243c657d491cf6a6be1e3a214cff907645819fdc34aadd
languageName: node
linkType: hard
"@popperjs/core@npm:^2.11.8": "@popperjs/core@npm:^2.11.8":
version: 2.11.8 version: 2.11.8
resolution: "@popperjs/core@npm:2.11.8" resolution: "@popperjs/core@npm:2.11.8"
@ -1355,10 +1364,10 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"abbrev@npm:^3.0.0": "abbrev@npm:^4.0.0":
version: 3.0.1 version: 4.0.0
resolution: "abbrev@npm:3.0.1" resolution: "abbrev@npm:4.0.0"
checksum: 10c0/21ba8f574ea57a3106d6d35623f2c4a9111d9ee3e9a5be47baed46ec2457d2eac46e07a5c4a60186f88cb98abbe3e24f2d4cca70bc2b12f1692523e2209a9ccf checksum: 10c0/b4cc16935235e80702fc90192e349e32f8ef0ed151ef506aa78c81a7c455ec18375c4125414b99f84b2e055199d66383e787675f0bcd87da7a4dbd59f9eac1d5
languageName: node languageName: node
linkType: hard linkType: hard
@ -1535,6 +1544,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"attr-accept@npm:^2.2.4":
version: 2.2.5
resolution: "attr-accept@npm:2.2.5"
checksum: 10c0/9b4cb82213925cab2d568f71b3f1c7a7778f9192829aac39a281e5418cd00c04a88f873eb89f187e0bf786fa34f8d52936f178e62cbefb9254d57ecd88ada99b
languageName: node
linkType: hard
"available-typed-arrays@npm:^1.0.7": "available-typed-arrays@npm:^1.0.7":
version: 1.0.7 version: 1.0.7
resolution: "available-typed-arrays@npm:1.0.7" resolution: "available-typed-arrays@npm:1.0.7"
@ -1604,23 +1620,22 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"cacache@npm:^19.0.1": "cacache@npm:^20.0.1":
version: 19.0.1 version: 20.0.1
resolution: "cacache@npm:19.0.1" resolution: "cacache@npm:20.0.1"
dependencies: dependencies:
"@npmcli/fs": "npm:^4.0.0" "@npmcli/fs": "npm:^4.0.0"
fs-minipass: "npm:^3.0.0" fs-minipass: "npm:^3.0.0"
glob: "npm:^10.2.2" glob: "npm:^11.0.3"
lru-cache: "npm:^10.0.1" lru-cache: "npm:^11.1.0"
minipass: "npm:^7.0.3" minipass: "npm:^7.0.3"
minipass-collect: "npm:^2.0.1" minipass-collect: "npm:^2.0.1"
minipass-flush: "npm:^1.0.5" minipass-flush: "npm:^1.0.5"
minipass-pipeline: "npm:^1.2.4" minipass-pipeline: "npm:^1.2.4"
p-map: "npm:^7.0.2" p-map: "npm:^7.0.2"
ssri: "npm:^12.0.0" ssri: "npm:^12.0.0"
tar: "npm:^7.4.3"
unique-filename: "npm:^4.0.0" unique-filename: "npm:^4.0.0"
checksum: 10c0/01f2134e1bd7d3ab68be851df96c8d63b492b1853b67f2eecb2c37bb682d37cb70bb858a16f2f0554d3c0071be6dfe21456a1ff6fa4b7eed996570d6a25ffe9c checksum: 10c0/e3efcf3af1c984e6e59e03372d9289861736a572e6e05b620606b87a67e71d04cff6dbc99607801cb21bcaae1fb4fb84d4cc8e3fda725e95881329ef03dac602
languageName: node languageName: node
linkType: hard linkType: hard
@ -1731,6 +1746,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"cookie@npm:^1.0.1":
version: 1.0.2
resolution: "cookie@npm:1.0.2"
checksum: 10c0/fd25fe79e8fbcfcaf6aa61cd081c55d144eeeba755206c058682257cb38c4bd6795c6620de3f064c740695bb65b7949ebb1db7a95e4636efb8357a335ad3f54b
languageName: node
linkType: hard
"cosmiconfig@npm:^7.0.0": "cosmiconfig@npm:^7.0.0":
version: 7.1.0 version: 7.1.0
resolution: "cosmiconfig@npm:7.1.0" resolution: "cosmiconfig@npm:7.1.0"
@ -1795,7 +1817,19 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4": "debug@npm:4":
version: 4.4.3
resolution: "debug@npm:4.4.3"
dependencies:
ms: "npm:^2.1.3"
peerDependenciesMeta:
supports-color:
optional: true
checksum: 10c0/d79136ec6c83ecbefd0f6a5593da6a9c91ec4d7ddc4b54c883d6e71ec9accb5f67a1a5e96d00a328196b5b5c86d365e98d8a3a70856aaf16b4e7b1985e67f5a6
languageName: node
linkType: hard
"debug@npm:^4.1.0, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4":
version: 4.4.1 version: 4.4.1
resolution: "debug@npm:4.4.1" resolution: "debug@npm:4.4.1"
dependencies: dependencies:
@ -1918,11 +1952,11 @@ __metadata:
linkType: hard linkType: hard
"error-ex@npm:^1.3.1": "error-ex@npm:^1.3.1":
version: 1.3.2 version: 1.3.4
resolution: "error-ex@npm:1.3.2" resolution: "error-ex@npm:1.3.4"
dependencies: dependencies:
is-arrayish: "npm:^0.2.1" is-arrayish: "npm:^0.2.1"
checksum: 10c0/ba827f89369b4c93382cfca5a264d059dfefdaa56ecc5e338ffa58a6471f5ed93b71a20add1d52290a4873d92381174382658c885ac1a2305f7baca363ce9cce checksum: 10c0/b9e34ff4778b8f3b31a8377e1c654456f4c41aeaa3d10a1138c3b7635d8b7b2e03eb2475d46d8ae055c1f180a1063e100bffabf64ea7e7388b37735df5328664
languageName: node languageName: node
linkType: hard linkType: hard
@ -2334,9 +2368,9 @@ __metadata:
linkType: hard linkType: hard
"exponential-backoff@npm:^3.1.1": "exponential-backoff@npm:^3.1.1":
version: 3.1.2 version: 3.1.3
resolution: "exponential-backoff@npm:3.1.2" resolution: "exponential-backoff@npm:3.1.3"
checksum: 10c0/d9d3e1eafa21b78464297df91f1776f7fbaa3d5e3f7f0995648ca5b89c069d17055033817348d9f4a43d1c20b0eab84f75af6991751e839df53e4dfd6f22e844 checksum: 10c0/77e3ae682b7b1f4972f563c6dbcd2b0d54ac679e62d5d32f3e5085feba20483cf28bd505543f520e287a56d4d55a28d7874299941faf637e779a1aa5994d1267
languageName: node languageName: node
linkType: hard linkType: hard
@ -2404,6 +2438,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"file-selector@npm:^2.1.0":
version: 2.1.2
resolution: "file-selector@npm:2.1.2"
dependencies:
tslib: "npm:^2.7.0"
checksum: 10c0/fe827e0e95410aacfcc3eabc38c29cc36055257f03c1c06b631a2b5af9730c142ad2c52f5d64724d02231709617bda984701f52bd1f4b7aca50fb6585a27c1d2
languageName: node
linkType: hard
"fill-range@npm:^7.1.1": "fill-range@npm:^7.1.1":
version: 7.1.1 version: 7.1.1
resolution: "fill-range@npm:7.1.1" resolution: "fill-range@npm:7.1.1"
@ -2456,7 +2499,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"foreground-child@npm:^3.1.0": "foreground-child@npm:^3.3.1":
version: 3.3.1 version: 3.3.1
resolution: "foreground-child@npm:3.3.1" resolution: "foreground-child@npm:3.3.1"
dependencies: dependencies:
@ -2586,19 +2629,19 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"glob@npm:^10.2.2": "glob@npm:^11.0.3":
version: 10.4.5 version: 11.0.3
resolution: "glob@npm:10.4.5" resolution: "glob@npm:11.0.3"
dependencies: dependencies:
foreground-child: "npm:^3.1.0" foreground-child: "npm:^3.3.1"
jackspeak: "npm:^3.1.2" jackspeak: "npm:^4.1.1"
minimatch: "npm:^9.0.4" minimatch: "npm:^10.0.3"
minipass: "npm:^7.1.2" minipass: "npm:^7.1.2"
package-json-from-dist: "npm:^1.0.0" package-json-from-dist: "npm:^1.0.0"
path-scurry: "npm:^1.11.1" path-scurry: "npm:^2.0.0"
bin: bin:
glob: dist/esm/bin.mjs glob: dist/esm/bin.mjs
checksum: 10c0/19a9759ea77b8e3ca0a43c2f07ecddc2ad46216b786bb8f993c445aee80d345925a21e5280c7b7c6c59e860a0154b84e4b2b60321fea92cd3c56b4a7489f160e checksum: 10c0/7d24457549ec2903920dfa3d8e76850e7c02aa709122f0164b240c712f5455c0b457e6f2a1eee39344c6148e39895be8094ae8cfef7ccc3296ed30bce250c661
languageName: node languageName: node
linkType: hard linkType: hard
@ -2792,9 +2835,9 @@ __metadata:
linkType: hard linkType: hard
"ip-address@npm:^10.0.1": "ip-address@npm:^10.0.1":
version: 10.0.1 version: 10.1.0
resolution: "ip-address@npm:10.0.1" resolution: "ip-address@npm:10.1.0"
checksum: 10c0/1634d79dae18394004775cb6d699dc46b7c23df6d2083164025a2b15240c1164fccde53d0e08bd5ee4fc53913d033ab6b5e395a809ad4b956a940c446e948843 checksum: 10c0/0103516cfa93f6433b3bd7333fa876eb21263912329bfa47010af5e16934eeeff86f3d2ae700a3744a137839ddfad62b900c7a445607884a49b5d1e32a3d7566
languageName: node languageName: node
linkType: hard linkType: hard
@ -2855,7 +2898,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"is-core-module@npm:^2.13.0, is-core-module@npm:^2.16.0": "is-core-module@npm:^2.13.0, is-core-module@npm:^2.16.1":
version: 2.16.1 version: 2.16.1
resolution: "is-core-module@npm:2.16.1" resolution: "is-core-module@npm:2.16.1"
dependencies: dependencies:
@ -3079,16 +3122,12 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"jackspeak@npm:^3.1.2": "jackspeak@npm:^4.1.1":
version: 3.4.3 version: 4.1.1
resolution: "jackspeak@npm:3.4.3" resolution: "jackspeak@npm:4.1.1"
dependencies: dependencies:
"@isaacs/cliui": "npm:^8.0.2" "@isaacs/cliui": "npm:^8.0.2"
"@pkgjs/parseargs": "npm:^0.11.0" checksum: 10c0/84ec4f8e21d6514db24737d9caf65361511f75e5e424980eebca4199f400874f45e562ac20fa8aeb1dd20ca2f3f81f0788b6e9c3e64d216a5794fd6f30e0e042
dependenciesMeta:
"@pkgjs/parseargs":
optional: true
checksum: 10c0/6acc10d139eaefdbe04d2f679e6191b3abf073f111edf10b1de5302c97ec93fffeb2fdd8681ed17f16268aa9dd4f8c588ed9d1d3bffbbfa6e8bf897cbb3149b9
languageName: node languageName: node
linkType: hard linkType: hard
@ -3221,10 +3260,10 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0": "lru-cache@npm:^11.0.0, lru-cache@npm:^11.1.0, lru-cache@npm:^11.2.1":
version: 10.4.3 version: 11.2.2
resolution: "lru-cache@npm:10.4.3" resolution: "lru-cache@npm:11.2.2"
checksum: 10c0/ebd04fbca961e6c1d6c0af3799adcc966a1babe798f685bb84e6599266599cd95d94630b10262f5424539bc4640107e8a33aa28585374abf561d30d16f4b39fb checksum: 10c0/72d7831bbebc85e2bdefe01047ee5584db69d641c48d7a509e86f66f6ee111b30af7ec3bd68a967d47b69a4b1fa8bbf3872630bd06a63b6735e6f0a5f1c8e83d
languageName: node languageName: node
linkType: hard linkType: hard
@ -3237,22 +3276,22 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"make-fetch-happen@npm:^14.0.3": "make-fetch-happen@npm:^15.0.0":
version: 14.0.3 version: 15.0.3
resolution: "make-fetch-happen@npm:14.0.3" resolution: "make-fetch-happen@npm:15.0.3"
dependencies: dependencies:
"@npmcli/agent": "npm:^3.0.0" "@npmcli/agent": "npm:^4.0.0"
cacache: "npm:^19.0.1" cacache: "npm:^20.0.1"
http-cache-semantics: "npm:^4.1.1" http-cache-semantics: "npm:^4.1.1"
minipass: "npm:^7.0.2" minipass: "npm:^7.0.2"
minipass-fetch: "npm:^4.0.0" minipass-fetch: "npm:^5.0.0"
minipass-flush: "npm:^1.0.5" minipass-flush: "npm:^1.0.5"
minipass-pipeline: "npm:^1.2.4" minipass-pipeline: "npm:^1.2.4"
negotiator: "npm:^1.0.0" negotiator: "npm:^1.0.0"
proc-log: "npm:^5.0.0" proc-log: "npm:^6.0.0"
promise-retry: "npm:^2.0.1" promise-retry: "npm:^2.0.1"
ssri: "npm:^12.0.0" ssri: "npm:^13.0.0"
checksum: 10c0/c40efb5e5296e7feb8e37155bde8eb70bc57d731b1f7d90e35a092fde403d7697c56fb49334d92d330d6f1ca29a98142036d6480a12681133a0a1453164cb2f0 checksum: 10c0/525f74915660be60b616bcbd267c4a5b59481b073ba125e45c9c3a041bb1a47a2bd0ae79d028eb6f5f95bf9851a4158423f5068539c3093621abb64027e8e461
languageName: node languageName: node
linkType: hard linkType: hard
@ -3280,6 +3319,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"minimatch@npm:^10.0.3":
version: 10.1.1
resolution: "minimatch@npm:10.1.1"
dependencies:
"@isaacs/brace-expansion": "npm:^5.0.0"
checksum: 10c0/c85d44821c71973d636091fddbfbffe62370f5ee3caf0241c5b60c18cd289e916200acb2361b7e987558cd06896d153e25d505db9fc1e43e6b4b6752e2702902
languageName: node
linkType: hard
"minimatch@npm:^3.1.2": "minimatch@npm:^3.1.2":
version: 3.1.2 version: 3.1.2
resolution: "minimatch@npm:3.1.2" resolution: "minimatch@npm:3.1.2"
@ -3307,9 +3355,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"minipass-fetch@npm:^4.0.0": "minipass-fetch@npm:^5.0.0":
version: 4.0.1 version: 5.0.0
resolution: "minipass-fetch@npm:4.0.1" resolution: "minipass-fetch@npm:5.0.0"
dependencies: dependencies:
encoding: "npm:^0.1.13" encoding: "npm:^0.1.13"
minipass: "npm:^7.0.3" minipass: "npm:^7.0.3"
@ -3318,7 +3366,7 @@ __metadata:
dependenciesMeta: dependenciesMeta:
encoding: encoding:
optional: true optional: true
checksum: 10c0/a3147b2efe8e078c9bf9d024a0059339c5a09c5b1dded6900a219c218cc8b1b78510b62dae556b507304af226b18c3f1aeb1d48660283602d5b6586c399eed5c checksum: 10c0/9443aab5feab190972f84b64116e54e58dd87a58e62399cae0a4a7461b80568281039b7c3a38ba96453431ebc799d1e26999e548540156216729a4967cd5ef06
languageName: node languageName: node
linkType: hard linkType: hard
@ -3358,28 +3406,19 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4, minipass@npm:^7.1.2": "minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4, minipass@npm:^7.1.2":
version: 7.1.2 version: 7.1.2
resolution: "minipass@npm:7.1.2" resolution: "minipass@npm:7.1.2"
checksum: 10c0/b0fd20bb9fb56e5fa9a8bfac539e8915ae07430a619e4b86ff71f5fc757ef3924b23b2c4230393af1eda647ed3d75739e4e0acb250a6b1eb277cf7f8fe449557 checksum: 10c0/b0fd20bb9fb56e5fa9a8bfac539e8915ae07430a619e4b86ff71f5fc757ef3924b23b2c4230393af1eda647ed3d75739e4e0acb250a6b1eb277cf7f8fe449557
languageName: node languageName: node
linkType: hard linkType: hard
"minizlib@npm:^3.0.1": "minizlib@npm:^3.0.1, minizlib@npm:^3.1.0":
version: 3.0.2 version: 3.1.0
resolution: "minizlib@npm:3.0.2" resolution: "minizlib@npm:3.1.0"
dependencies: dependencies:
minipass: "npm:^7.1.2" minipass: "npm:^7.1.2"
checksum: 10c0/9f3bd35e41d40d02469cb30470c55ccc21cae0db40e08d1d0b1dff01cc8cc89a6f78e9c5d2b7c844e485ec0a8abc2238111213fdc5b2038e6d1012eacf316f78 checksum: 10c0/5aad75ab0090b8266069c9aabe582c021ae53eb33c6c691054a13a45db3b4f91a7fb1bd79151e6b4e9e9a86727b522527c0a06ec7d45206b745d54cd3097bcec
languageName: node
linkType: hard
"mkdirp@npm:^3.0.1":
version: 3.0.1
resolution: "mkdirp@npm:3.0.1"
bin:
mkdirp: dist/cjs/src/bin.js
checksum: 10c0/9f2b975e9246351f5e3a40dcfac99fcd0baa31fbfab615fe059fb11e51f10e4803c63de1f384c54d656e4db31d000e4767e9ef076a22e12a641357602e31d57d
languageName: node languageName: node
linkType: hard linkType: hard
@ -3414,22 +3453,22 @@ __metadata:
linkType: hard linkType: hard
"node-gyp@npm:latest": "node-gyp@npm:latest":
version: 11.4.2 version: 12.1.0
resolution: "node-gyp@npm:11.4.2" resolution: "node-gyp@npm:12.1.0"
dependencies: dependencies:
env-paths: "npm:^2.2.0" env-paths: "npm:^2.2.0"
exponential-backoff: "npm:^3.1.1" exponential-backoff: "npm:^3.1.1"
graceful-fs: "npm:^4.2.6" graceful-fs: "npm:^4.2.6"
make-fetch-happen: "npm:^14.0.3" make-fetch-happen: "npm:^15.0.0"
nopt: "npm:^8.0.0" nopt: "npm:^9.0.0"
proc-log: "npm:^5.0.0" proc-log: "npm:^6.0.0"
semver: "npm:^7.3.5" semver: "npm:^7.3.5"
tar: "npm:^7.4.3" tar: "npm:^7.5.2"
tinyglobby: "npm:^0.2.12" tinyglobby: "npm:^0.2.12"
which: "npm:^5.0.0" which: "npm:^6.0.0"
bin: bin:
node-gyp: bin/node-gyp.js node-gyp: bin/node-gyp.js
checksum: 10c0/0bfd3e96770ed70f07798d881dd37b4267708966d868a0e585986baac487d9cf5831285579fd629a83dc4e434f53e6416ce301097f2ee464cb74d377e4d8bdbe checksum: 10c0/f43efea8aaf0beb6b2f6184e533edad779b2ae38062953e21951f46221dd104006cc574154f2ad4a135467a5aae92c49e84ef289311a82e08481c5df0e8dc495
languageName: node languageName: node
linkType: hard linkType: hard
@ -3440,14 +3479,14 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"nopt@npm:^8.0.0": "nopt@npm:^9.0.0":
version: 8.1.0 version: 9.0.0
resolution: "nopt@npm:8.1.0" resolution: "nopt@npm:9.0.0"
dependencies: dependencies:
abbrev: "npm:^3.0.0" abbrev: "npm:^4.0.0"
bin: bin:
nopt: bin/nopt.js nopt: bin/nopt.js
checksum: 10c0/62e9ea70c7a3eb91d162d2c706b6606c041e4e7b547cbbb48f8b3695af457dd6479904d7ace600856bf923dd8d1ed0696f06195c8c20f02ac87c1da0e1d315ef checksum: 10c0/1822eb6f9b020ef6f7a7516d7b64a8036e09666ea55ac40416c36e4b2b343122c3cff0e2f085675f53de1d2db99a2a89a60ccea1d120bcd6a5347bf6ceb4a7fd
languageName: node languageName: node
linkType: hard linkType: hard
@ -3566,9 +3605,9 @@ __metadata:
linkType: hard linkType: hard
"p-map@npm:^7.0.2": "p-map@npm:^7.0.2":
version: 7.0.3 version: 7.0.4
resolution: "p-map@npm:7.0.3" resolution: "p-map@npm:7.0.4"
checksum: 10c0/46091610da2b38ce47bcd1d8b4835a6fa4e832848a6682cf1652bc93915770f4617afc844c10a77d1b3e56d2472bb2d5622353fa3ead01a7f42b04fc8e744a5c checksum: 10c0/a5030935d3cb2919d7e89454d1ce82141e6f9955413658b8c9403cfe379283770ed3048146b44cde168aa9e8c716505f196d5689db0ae3ce9a71521a2fef3abd
languageName: node languageName: node
linkType: hard linkType: hard
@ -3621,13 +3660,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"path-scurry@npm:^1.11.1": "path-scurry@npm:^2.0.0":
version: 1.11.1 version: 2.0.1
resolution: "path-scurry@npm:1.11.1" resolution: "path-scurry@npm:2.0.1"
dependencies: dependencies:
lru-cache: "npm:^10.2.0" lru-cache: "npm:^11.0.0"
minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0" minipass: "npm:^7.1.2"
checksum: 10c0/32a13711a2a505616ae1cc1b5076801e453e7aae6ac40ab55b388bb91b9d0547a52f5aaceff710ea400205f18691120d4431e520afbe4266b836fadede15872d checksum: 10c0/2a16ed0e81fbc43513e245aa5763354e25e787dab0d539581a6c3f0f967461a159ed6236b2559de23aa5b88e7dc32b469b6c47568833dd142a4b24b4f5cd2620
languageName: node languageName: node
linkType: hard linkType: hard
@ -3684,10 +3723,10 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"proc-log@npm:^5.0.0": "proc-log@npm:^6.0.0":
version: 5.0.0 version: 6.0.0
resolution: "proc-log@npm:5.0.0" resolution: "proc-log@npm:6.0.0"
checksum: 10c0/bbe5edb944b0ad63387a1d5b1911ae93e05ce8d0f60de1035b218cdcceedfe39dbd2c697853355b70f1a090f8f58fe90da487c85216bf9671f9499d1a897e9e3 checksum: 10c0/40c5e2b4c55e395a3bd72e38cba9c26e58598a1f4844fa6a115716d5231a0919f46aa8e351147035d91583ad39a794593615078c948bc001fe3beb99276be776
languageName: node languageName: node
linkType: hard linkType: hard
@ -3737,6 +3776,19 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"react-dropzone@npm:^14.3.8":
version: 14.3.8
resolution: "react-dropzone@npm:14.3.8"
dependencies:
attr-accept: "npm:^2.2.4"
file-selector: "npm:^2.1.0"
prop-types: "npm:^15.8.1"
peerDependencies:
react: ">= 16.8 || 18.0.0"
checksum: 10c0/e17b1832783cda7b8824fe9370e99185d1abbdd5e4980b2985d6321c5768c8de18ff7b9ad550c809ee9743269dea608ff74d5208062754ce8377ad022897b278
languageName: node
linkType: hard
"react-intersection-observer@npm:^9.16.0": "react-intersection-observer@npm:^9.16.0":
version: 9.16.0 version: 9.16.0
resolution: "react-intersection-observer@npm:9.16.0" resolution: "react-intersection-observer@npm:9.16.0"
@ -3757,10 +3809,10 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"react-is@npm:^19.1.1": "react-is@npm:^19.2.0":
version: 19.1.1 version: 19.2.0
resolution: "react-is@npm:19.1.1" resolution: "react-is@npm:19.2.0"
checksum: 10c0/3dba763fcd69835ae263dcd6727d7ffcc44c1d616f04b7329e67aefdc66a567af4f8dcecdd29454c7a707c968aa1eb85083a83fb616f01675ef25e71cf082f97 checksum: 10c0/a63cb346aeced8ac0e671b0f9b33720d2906de02a066ca067075d871a5d4c64cdb328f495baf9b5842d5868c0d5edd1ce18465a7358b52f4b6aa983479c9bfa2
languageName: node languageName: node
linkType: hard linkType: hard
@ -3771,6 +3823,34 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"react-router-dom@npm:^7.8.2":
version: 7.9.6
resolution: "react-router-dom@npm:7.9.6"
dependencies:
react-router: "npm:7.9.6"
peerDependencies:
react: ">=18"
react-dom: ">=18"
checksum: 10c0/63984c46385da232655b9e3a8a99f6dd7b94c36827be6e954f246c362f83740b5f59b1de99cae81da3b0cef2220d701dcc22e4fafb4a84600541e1c0450b9d57
languageName: node
linkType: hard
"react-router@npm:7.9.6":
version: 7.9.6
resolution: "react-router@npm:7.9.6"
dependencies:
cookie: "npm:^1.0.1"
set-cookie-parser: "npm:^2.6.0"
peerDependencies:
react: ">=18"
react-dom: ">=18"
peerDependenciesMeta:
react-dom:
optional: true
checksum: 10c0/2a177bbe19021e3b8211df849ea5b3f3a4f482327e6de3341aaeaa4f1406dc9be7b675b229eefea6761e04a59a40ccaaf8188f2ee88eb2d0b2a6b6448daea368
languageName: node
linkType: hard
"react-transition-group@npm:^4.4.5": "react-transition-group@npm:^4.4.5":
version: 4.4.5 version: 4.4.5
resolution: "react-transition-group@npm:4.4.5" resolution: "react-transition-group@npm:4.4.5"
@ -3831,15 +3911,15 @@ __metadata:
linkType: hard linkType: hard
"resolve@npm:^1.19.0": "resolve@npm:^1.19.0":
version: 1.22.10 version: 1.22.11
resolution: "resolve@npm:1.22.10" resolution: "resolve@npm:1.22.11"
dependencies: dependencies:
is-core-module: "npm:^2.16.0" is-core-module: "npm:^2.16.1"
path-parse: "npm:^1.0.7" path-parse: "npm:^1.0.7"
supports-preserve-symlinks-flag: "npm:^1.0.0" supports-preserve-symlinks-flag: "npm:^1.0.0"
bin: bin:
resolve: bin/resolve resolve: bin/resolve
checksum: 10c0/8967e1f4e2cc40f79b7e080b4582b9a8c5ee36ffb46041dccb20e6461161adf69f843b43067b4a375de926a2cd669157e29a29578191def399dd5ef89a1b5203 checksum: 10c0/f657191507530f2cbecb5815b1ee99b20741ea6ee02a59c57028e9ec4c2c8d7681afcc35febbd554ac0ded459db6f2d8153382c53a2f266cee2575e512674409
languageName: node languageName: node
linkType: hard linkType: hard
@ -3857,15 +3937,15 @@ __metadata:
linkType: hard linkType: hard
"resolve@patch:resolve@npm%3A^1.19.0#optional!builtin<compat/resolve>": "resolve@patch:resolve@npm%3A^1.19.0#optional!builtin<compat/resolve>":
version: 1.22.10 version: 1.22.11
resolution: "resolve@patch:resolve@npm%3A1.22.10#optional!builtin<compat/resolve>::version=1.22.10&hash=c3c19d" resolution: "resolve@patch:resolve@npm%3A1.22.11#optional!builtin<compat/resolve>::version=1.22.11&hash=c3c19d"
dependencies: dependencies:
is-core-module: "npm:^2.16.0" is-core-module: "npm:^2.16.1"
path-parse: "npm:^1.0.7" path-parse: "npm:^1.0.7"
supports-preserve-symlinks-flag: "npm:^1.0.0" supports-preserve-symlinks-flag: "npm:^1.0.0"
bin: bin:
resolve: bin/resolve resolve: bin/resolve
checksum: 10c0/52a4e505bbfc7925ac8f4cd91fd8c4e096b6a89728b9f46861d3b405ac9a1ccf4dcbf8befb4e89a2e11370dacd0160918163885cbc669369590f2f31f4c58939 checksum: 10c0/ee5b182f2e37cb1165465e58c6abc797fec0a80b5ba3231607beb4677db0c9291ac010c47cf092b6daa2b7f518d69a0e21888e7e2b633f68d501a874212a8c63
languageName: node languageName: node
linkType: hard linkType: hard
@ -4040,7 +4120,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"semver@npm:^7.3.5, semver@npm:^7.6.0": "semver@npm:^7.3.5":
version: 7.7.3
resolution: "semver@npm:7.7.3"
bin:
semver: bin/semver.js
checksum: 10c0/4afe5c986567db82f44c8c6faef8fe9df2a9b1d98098fc1721f57c696c4c21cebd572f297fc21002f81889492345b8470473bc6f4aff5fb032a6ea59ea2bc45e
languageName: node
linkType: hard
"semver@npm:^7.6.0":
version: 7.7.2 version: 7.7.2
resolution: "semver@npm:7.7.2" resolution: "semver@npm:7.7.2"
bin: bin:
@ -4049,6 +4138,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"set-cookie-parser@npm:^2.6.0":
version: 2.7.2
resolution: "set-cookie-parser@npm:2.7.2"
checksum: 10c0/4381a9eb7ee951dfe393fe7aacf76b9a3b4e93a684d2162ab35594fa4053cc82a4d7d7582bf397718012c9adcf839b8cd8f57c6c42901ea9effe33c752da4a45
languageName: node
linkType: hard
"set-function-length@npm:^1.2.2": "set-function-length@npm:^1.2.2":
version: 1.2.2 version: 1.2.2
resolution: "set-function-length@npm:1.2.2" resolution: "set-function-length@npm:1.2.2"
@ -4208,6 +4304,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"ssri@npm:^13.0.0":
version: 13.0.0
resolution: "ssri@npm:13.0.0"
dependencies:
minipass: "npm:^7.0.3"
checksum: 10c0/405f3a531cd98b013cecb355d63555dca42fd12c7bc6671738aaa9a82882ff41cdf0ef9a2b734ca4f9a760338f114c29d01d9238a65db3ccac27929bd6e6d4b2
languageName: node
linkType: hard
"stop-iteration-iterator@npm:^1.1.0": "stop-iteration-iterator@npm:^1.1.0":
version: 1.1.0 version: 1.1.0
resolution: "stop-iteration-iterator@npm:1.1.0" resolution: "stop-iteration-iterator@npm:1.1.0"
@ -4357,17 +4462,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"tar@npm:^7.4.3": "tar@npm:^7.5.2":
version: 7.4.3 version: 7.5.2
resolution: "tar@npm:7.4.3" resolution: "tar@npm:7.5.2"
dependencies: dependencies:
"@isaacs/fs-minipass": "npm:^4.0.0" "@isaacs/fs-minipass": "npm:^4.0.0"
chownr: "npm:^3.0.0" chownr: "npm:^3.0.0"
minipass: "npm:^7.1.2" minipass: "npm:^7.1.2"
minizlib: "npm:^3.0.1" minizlib: "npm:^3.1.0"
mkdirp: "npm:^3.0.1"
yallist: "npm:^5.0.0" yallist: "npm:^5.0.0"
checksum: 10c0/d4679609bb2a9b48eeaf84632b6d844128d2412b95b6de07d53d8ee8baf4ca0857c9331dfa510390a0727b550fd543d4d1a10995ad86cdf078423fbb8d99831d checksum: 10c0/a7d8b801139b52f93a7e34830db0de54c5aa45487c7cb551f6f3d44a112c67f1cb8ffdae856b05fd4f17b1749911f1c26f1e3a23bbe0279e17fd96077f13f467
languageName: node languageName: node
linkType: hard linkType: hard
@ -4392,7 +4496,9 @@ __metadata:
globals: "npm:^16.3.0" globals: "npm:^16.3.0"
react: "npm:^19.1.1" react: "npm:^19.1.1"
react-dom: "npm:^19.1.1" react-dom: "npm:^19.1.1"
react-dropzone: "npm:^14.3.8"
react-intersection-observer: "npm:^9.16.0" react-intersection-observer: "npm:^9.16.0"
react-router-dom: "npm:^7.8.2"
typescript: "npm:~5.8.3" typescript: "npm:~5.8.3"
typescript-eslint: "npm:^8.39.1" typescript-eslint: "npm:^8.39.1"
vite: "npm:^7.1.2" vite: "npm:^7.1.2"
@ -4427,6 +4533,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"tslib@npm:^2.7.0":
version: 2.8.1
resolution: "tslib@npm:2.8.1"
checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62
languageName: node
linkType: hard
"type-check@npm:^0.4.0, type-check@npm:~0.4.0": "type-check@npm:^0.4.0, type-check@npm:~0.4.0":
version: 0.4.0 version: 0.4.0
resolution: "type-check@npm:0.4.0" resolution: "type-check@npm:0.4.0"
@ -4704,14 +4817,14 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"which@npm:^5.0.0": "which@npm:^6.0.0":
version: 5.0.0 version: 6.0.0
resolution: "which@npm:5.0.0" resolution: "which@npm:6.0.0"
dependencies: dependencies:
isexe: "npm:^3.1.1" isexe: "npm:^3.1.1"
bin: bin:
node-which: bin/which.js node-which: bin/which.js
checksum: 10c0/e556e4cd8b7dbf5df52408c9a9dd5ac6518c8c5267c8953f5b0564073c66ed5bf9503b14d876d0e9c7844d4db9725fb0dcf45d6e911e17e26ab363dc3965ae7b checksum: 10c0/fe9d6463fe44a76232bb6e3b3181922c87510a5b250a98f1e43a69c99c079b3f42ddeca7e03d3e5f2241bf2d334f5a7657cfa868b97c109f3870625842f4cc15
languageName: node languageName: node
linkType: hard linkType: hard