Merge pull request 'feature/coming_soon' (#3) from feature/coming_soon into main
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
Reviewed-on: #3
This commit is contained in:
commit
00f5d7f92a
|
|
@ -0,0 +1,4 @@
|
|||
node_modules
|
||||
dist
|
||||
npm-debug.log
|
||||
.DS_Store
|
||||
Binary file not shown.
36
Dockerfile
36
Dockerfile
|
|
@ -5,43 +5,19 @@ FROM node:22-bullseye AS builder
|
|||
|
||||
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
|
||||
|
||||
# Copy Yarn 4 files
|
||||
COPY package.json yarn.lock .yarnrc.yml ./
|
||||
COPY .yarn .yarn
|
||||
# Copy package.json and package-lock.json
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies (immutable to match lockfile)
|
||||
RUN yarn install --immutable
|
||||
# Install ALL dependencies (including devDependencies)
|
||||
RUN npm install
|
||||
|
||||
# Copy all source files
|
||||
# Copy source files
|
||||
COPY . .
|
||||
|
||||
# Pass API URL to Vite
|
||||
ENV VITE_API_BASE_URL=$API_BASE_URL
|
||||
|
||||
# Build the app
|
||||
RUN yarn 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"]
|
||||
RUN npm run build
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Vite + React + TS</title>
|
||||
<title>The Kalawati</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -16,7 +16,9 @@
|
|||
"@mui/material": "^7.3.2",
|
||||
"react": "^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": {
|
||||
"@eslint/js": "^9.33.0",
|
||||
|
|
|
|||
31
src/App.tsx
31
src/App.tsx
|
|
@ -1,18 +1,29 @@
|
|||
// import { useState } from 'react'
|
||||
// import reactLogo from './assets/react.svg'
|
||||
// import viteLogo from '/vite.svg'
|
||||
import './App.css'
|
||||
// import VintageComingSoon from './comingsoon/comingsoon'
|
||||
import VintageComingSoonPage from './comingsoon/comingsoon'
|
||||
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
|
||||
import "./App.css";
|
||||
// import VintageComingSoonPage from "./components/comingsoon/comingsoon";
|
||||
// import DarkProductShowcase from "./components/product/product";
|
||||
// import BlogPage from "./components/blogs/BlogPage";
|
||||
// import BlogCard from "./components/blogs/BlogCard";
|
||||
import WeddingGallery from "./WeddingGallery/WeddingGallery";
|
||||
// import OtherPage from "./pages/OtherPage"; // example if you add more pages
|
||||
|
||||
function App() {
|
||||
// const [count, setCount] = useState(0)
|
||||
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
|
@ -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';
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
};
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 243 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 176 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 376 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 258 KiB |
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -7,6 +7,14 @@ import TwitterIcon from '@mui/icons-material/Twitter';
|
|||
import EmailIcon from '@mui/icons-material/Email';
|
||||
import { keyframes } from '@emotion/react';
|
||||
import { styled } from '@mui/system';
|
||||
import image1 from '../../assets/group1.jpg'
|
||||
import image2 from '../../assets/solo1.jpg'
|
||||
import image3 from '../../assets/solo2.jpg'
|
||||
import image4 from '../../assets/bag1.jpg'
|
||||
import image5 from '../../assets/bag2.jpg'
|
||||
import logo from '../../assets/logobgremove.png'
|
||||
|
||||
|
||||
|
||||
// Color palette inspired by Indian heritage
|
||||
const colors = {
|
||||
|
|
@ -57,6 +65,60 @@ const scrollReveal = keyframes`
|
|||
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
|
||||
const VintageBox = styled(Box)({
|
||||
position: 'relative',
|
||||
|
|
@ -158,6 +220,35 @@ const AnimatedSection = styled(Box)({
|
|||
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
|
||||
const storyChapters = [
|
||||
{
|
||||
|
|
@ -165,42 +256,42 @@ const storyChapters = [
|
|||
title: 'Where Curiosity Met Craft',
|
||||
content:
|
||||
'Fresh out of fashion school, I found myself drawn to looms, dye pits, and village homes. I organized workshops to listen, learn, and share space with artisans, forging a bond between design, community, and culture.',
|
||||
image: 'https://images.pexels.com/photos/28382914/pexels-photo-28382914.jpeg?auto=compress&cs=tinysrgb&w=500',
|
||||
image: `${image1}`,
|
||||
},
|
||||
{
|
||||
year: '2020',
|
||||
title: 'A Name Was Born',
|
||||
content:
|
||||
'During the pandemic, I launched a campaign to highlight artisans hit by lockdowns. The Kalawati was born—named after women who hold culture in their palms and creativity in their hearts.',
|
||||
image: 'https://images.pexels.com/photos/31308739/pexels-photo-31308739.jpeg?auto=compress&cs=tinysrgb&w=500',
|
||||
image: `${image2}`,
|
||||
},
|
||||
{
|
||||
year: '2021',
|
||||
title: 'Strengthening the Circle',
|
||||
content:
|
||||
'I spent the year mapping artisan strengths, connecting traditional skills to modern demand, and building trust for a long-term vision of community and craft.',
|
||||
image: 'https://images.pexels.com/photos/3772504/pexels-photo-3772504.jpeg?auto=compress&cs=tinysrgb&w=500',
|
||||
image: `${image3}`,
|
||||
},
|
||||
{
|
||||
year: '2022',
|
||||
title: 'Systems That Serve People',
|
||||
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.',
|
||||
image: 'https://images.pexels.com/photos/163064/pexels-photo-163064.jpeg?auto=compress&cs=tinysrgb&w=500',
|
||||
image: `${image4}`,
|
||||
},
|
||||
{
|
||||
year: '2023',
|
||||
title: 'Quiet Creation',
|
||||
content:
|
||||
'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',
|
||||
title: 'Kalawati Arrives',
|
||||
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.',
|
||||
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',
|
||||
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',
|
||||
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',
|
||||
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',
|
||||
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>) => {
|
||||
e.preventDefault();
|
||||
if (email && email.includes('@')) {
|
||||
console.log('Subscribed with email:', email);
|
||||
setSubscribed(true);
|
||||
setEmail('');
|
||||
setTimeout(() => setSubscribed(false), 3000);
|
||||
}
|
||||
};
|
||||
|
||||
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 */}
|
||||
<Box
|
||||
sx={{
|
||||
|
|
@ -318,14 +444,19 @@ export const VintageComingSoonPage: React.FC = () => {
|
|||
right: '5%',
|
||||
width: '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',
|
||||
opacity: 0.1,
|
||||
backgroundPosition: 'center',
|
||||
opacity: 0.5,
|
||||
zIndex: 0,
|
||||
transform: `rotate(${scrollPosition * 0.02}deg)`,
|
||||
transition: 'transform 0.3s ease-out',
|
||||
borderRadius: '50%', // makes it circular
|
||||
overflow: 'hidden', // ensures image stays inside the circle
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
|
|
@ -333,15 +464,19 @@ export const VintageComingSoonPage: React.FC = () => {
|
|||
left: '5%',
|
||||
width: '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',
|
||||
opacity: 0.1,
|
||||
backgroundPosition: 'center',
|
||||
opacity: 0.4,
|
||||
zIndex: 0,
|
||||
transform: `rotate(${-scrollPosition * 0.03}deg)`,
|
||||
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}>
|
||||
{/* Header */}
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderBottom: `1px solid ${colors.border}`, py: 4, mb: 4 }}>
|
||||
|
|
@ -361,6 +496,7 @@ export const VintageComingSoonPage: React.FC = () => {
|
|||
</AnimatedSection>
|
||||
<AnimatedSection ref={heroRef} style={{ animationDelay: heroInView ? '0.2s' : '0s' }}>
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<BounceBox>
|
||||
<IconButton
|
||||
href="https://instagram.com"
|
||||
target="_blank"
|
||||
|
|
@ -368,6 +504,8 @@ export const VintageComingSoonPage: React.FC = () => {
|
|||
>
|
||||
<InstagramIcon />
|
||||
</IconButton>
|
||||
</BounceBox>
|
||||
<BounceBox sx={{ animationDelay: '0.1s' }}>
|
||||
<IconButton
|
||||
href="https://facebook.com"
|
||||
target="_blank"
|
||||
|
|
@ -375,6 +513,8 @@ export const VintageComingSoonPage: React.FC = () => {
|
|||
>
|
||||
<FacebookIcon />
|
||||
</IconButton>
|
||||
</BounceBox>
|
||||
<BounceBox sx={{ animationDelay: '0.2s' }}>
|
||||
<IconButton
|
||||
href="https://twitter.com"
|
||||
target="_blank"
|
||||
|
|
@ -382,6 +522,7 @@ export const VintageComingSoonPage: React.FC = () => {
|
|||
>
|
||||
<TwitterIcon />
|
||||
</IconButton>
|
||||
</BounceBox>
|
||||
</Box>
|
||||
</AnimatedSection>
|
||||
</Box>
|
||||
|
|
@ -450,7 +591,7 @@ export const VintageComingSoonPage: React.FC = () => {
|
|||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
<GlowingText
|
||||
variant="h1"
|
||||
sx={{
|
||||
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
|
||||
</Typography>
|
||||
</GlowingText>
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{
|
||||
|
|
@ -544,7 +685,7 @@ export const VintageComingSoonPage: React.FC = () => {
|
|||
|
||||
{/* Email Subscription */}
|
||||
<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
|
||||
type="email"
|
||||
value={email}
|
||||
|
|
@ -555,7 +696,7 @@ export const VintageComingSoonPage: React.FC = () => {
|
|||
flex: 1,
|
||||
padding: '16px 24px',
|
||||
borderRadius: '0',
|
||||
border: `1px solid ${colors.border}`,
|
||||
border: 'none',
|
||||
fontSize: '16px',
|
||||
outline: 'none',
|
||||
background: colors.paper,
|
||||
|
|
@ -579,7 +720,7 @@ export const VintageComingSoonPage: React.FC = () => {
|
|||
>
|
||||
Notify Me
|
||||
</VintagePulseButton>
|
||||
</Box>
|
||||
</BorderFlowBox>
|
||||
{subscribed && (
|
||||
<Typography
|
||||
sx={{
|
||||
|
|
@ -613,7 +754,7 @@ export const VintageComingSoonPage: React.FC = () => {
|
|||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
top: "20%",
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
width: '1px',
|
||||
|
|
@ -788,9 +929,10 @@ export const VintageComingSoonPage: React.FC = () => {
|
|||
'&:hover': {
|
||||
transform: 'translateY(-5px)',
|
||||
},
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<VintageImageFrame
|
||||
<FlipInBox
|
||||
sx={{
|
||||
height: '200px',
|
||||
width: '100%',
|
||||
|
|
@ -799,7 +941,6 @@ export const VintageComingSoonPage: React.FC = () => {
|
|||
backgroundPosition: 'center',
|
||||
mb: 3,
|
||||
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' }}>
|
||||
|
|
@ -847,6 +988,7 @@ 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.
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', gap: 3, mb: 4 }}>
|
||||
<BounceBox>
|
||||
<IconButton
|
||||
href="https://instagram.com"
|
||||
target="_blank"
|
||||
|
|
@ -854,6 +996,8 @@ export const VintageComingSoonPage: React.FC = () => {
|
|||
>
|
||||
<InstagramIcon />
|
||||
</IconButton>
|
||||
</BounceBox>
|
||||
<BounceBox sx={{ animationDelay: '0.1s' }}>
|
||||
<IconButton
|
||||
href="https://facebook.com"
|
||||
target="_blank"
|
||||
|
|
@ -861,6 +1005,8 @@ export const VintageComingSoonPage: React.FC = () => {
|
|||
>
|
||||
<FacebookIcon />
|
||||
</IconButton>
|
||||
</BounceBox>
|
||||
<BounceBox sx={{ animationDelay: '0.2s' }}>
|
||||
<IconButton
|
||||
href="https://twitter.com"
|
||||
target="_blank"
|
||||
|
|
@ -868,12 +1014,15 @@ export const VintageComingSoonPage: React.FC = () => {
|
|||
>
|
||||
<TwitterIcon />
|
||||
</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>
|
||||
<Typography
|
||||
variant="caption"
|
||||
|
|
@ -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;
|
||||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
@ -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
479
yarn.lock
|
|
@ -173,7 +173,7 @@ __metadata:
|
|||
languageName: node
|
||||
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
|
||||
resolution: "@babel/runtime@npm:7.28.4"
|
||||
checksum: 10c0/792ce7af9750fb9b93879cc9d1db175701c4689da890e6ced242ea0207c9da411ccf16dc04e689cc01158b28d7898c40d75598f4559109f761c12ce01e959bf7
|
||||
|
|
@ -661,6 +661,22 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 8.0.2
|
||||
resolution: "@isaacs/cliui@npm:8.0.2"
|
||||
|
|
@ -728,49 +744,49 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mui/core-downloads-tracker@npm:^7.3.2":
|
||||
version: 7.3.2
|
||||
resolution: "@mui/core-downloads-tracker@npm:7.3.2"
|
||||
checksum: 10c0/8549ac661e07926e1c1de2664ad50f68fb4f3f6050f3cfe7bf2e8a7ceaefde99c1615f4ab5185dff22a7d72874d1dcc5fdc3651d08ed4eb1abfb798629f3991f
|
||||
"@mui/core-downloads-tracker@npm:^7.3.5":
|
||||
version: 7.3.5
|
||||
resolution: "@mui/core-downloads-tracker@npm:7.3.5"
|
||||
checksum: 10c0/72c71d43b3609ccd5eab5b3bfc5bfc2232b79cfb210cb64a66298de0b2effccb1843aa8cdb6e062bc6f5df91c02d70de84984bb6fab9745ffdf00e81a574dc9b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mui/icons-material@npm:^7.3.2":
|
||||
version: 7.3.2
|
||||
resolution: "@mui/icons-material@npm:7.3.2"
|
||||
version: 7.3.5
|
||||
resolution: "@mui/icons-material@npm:7.3.5"
|
||||
dependencies:
|
||||
"@babel/runtime": "npm:^7.28.3"
|
||||
"@babel/runtime": "npm:^7.28.4"
|
||||
peerDependencies:
|
||||
"@mui/material": ^7.3.2
|
||||
"@mui/material": ^7.3.5
|
||||
"@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
"@types/react":
|
||||
optional: true
|
||||
checksum: 10c0/25d1f0dcbbb4a35f320aac03317657b15631f9d5b9d48d07e34850d61253a2b4d8dd5e5ca624d76f44b5ceda471ea57d2611f6627fec1eef9fce78855ec01cbf
|
||||
checksum: 10c0/5fa551acabc8eddf30113a45a7c7b9923ec0f520608442cb914345493208aaa5b4ba852ad4b766ab58c547a2eb498ab4d6c1a3ebf5d4055a006c2327e397f88c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mui/material@npm:^7.3.2":
|
||||
version: 7.3.2
|
||||
resolution: "@mui/material@npm:7.3.2"
|
||||
version: 7.3.5
|
||||
resolution: "@mui/material@npm:7.3.5"
|
||||
dependencies:
|
||||
"@babel/runtime": "npm:^7.28.3"
|
||||
"@mui/core-downloads-tracker": "npm:^7.3.2"
|
||||
"@mui/system": "npm:^7.3.2"
|
||||
"@mui/types": "npm:^7.4.6"
|
||||
"@mui/utils": "npm:^7.3.2"
|
||||
"@babel/runtime": "npm:^7.28.4"
|
||||
"@mui/core-downloads-tracker": "npm:^7.3.5"
|
||||
"@mui/system": "npm:^7.3.5"
|
||||
"@mui/types": "npm:^7.4.8"
|
||||
"@mui/utils": "npm:^7.3.5"
|
||||
"@popperjs/core": "npm:^2.11.8"
|
||||
"@types/react-transition-group": "npm:^4.4.12"
|
||||
clsx: "npm:^2.1.1"
|
||||
csstype: "npm:^3.1.3"
|
||||
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"
|
||||
peerDependencies:
|
||||
"@emotion/react": ^11.5.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
|
||||
react: ^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
|
||||
"@types/react":
|
||||
optional: true
|
||||
checksum: 10c0/4b82a65af93fe9517991f45c2f9dc127728199921f5c4c5cd7a8cd48e1c89ba17799011440f1b7e32993871c13b3044878a3170ddd9ce0e7cfe5ca0e7e3613f2
|
||||
checksum: 10c0/363ff79d0eaf8044510529bf8d143d7e6f5298297dcfbd0816c5caf49e9514bde6b187b0e135cc1d73ac2ecfec1b06d3fea3bba952e7d8d0bfc997d0510fff8a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mui/private-theming@npm:^7.3.2":
|
||||
version: 7.3.2
|
||||
resolution: "@mui/private-theming@npm:7.3.2"
|
||||
"@mui/private-theming@npm:^7.3.5":
|
||||
version: 7.3.5
|
||||
resolution: "@mui/private-theming@npm:7.3.5"
|
||||
dependencies:
|
||||
"@babel/runtime": "npm:^7.28.3"
|
||||
"@mui/utils": "npm:^7.3.2"
|
||||
"@babel/runtime": "npm:^7.28.4"
|
||||
"@mui/utils": "npm:^7.3.5"
|
||||
prop-types: "npm:^15.8.1"
|
||||
peerDependencies:
|
||||
"@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
|
@ -800,15 +816,15 @@ __metadata:
|
|||
peerDependenciesMeta:
|
||||
"@types/react":
|
||||
optional: true
|
||||
checksum: 10c0/fb6067e92a1bc02d4b2b49fa58901200ccf4b79760a0227bf2859bd2cb99c46ba0f43ece9494eaa220710703eaf309a7c6e732daf4176520c0a16e1407846399
|
||||
checksum: 10c0/1347cf2a1ec1ae93d26134143c20314d53dac61fe5c8c7aa00ab37d9e89f5e6245f787dee9b0bf3d34fc614c9a5da1f5d45759fcd2520ddef4c10e755c4abc5e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mui/styled-engine@npm:^7.3.2":
|
||||
version: 7.3.2
|
||||
resolution: "@mui/styled-engine@npm:7.3.2"
|
||||
"@mui/styled-engine@npm:^7.3.5":
|
||||
version: 7.3.5
|
||||
resolution: "@mui/styled-engine@npm:7.3.5"
|
||||
dependencies:
|
||||
"@babel/runtime": "npm:^7.28.3"
|
||||
"@babel/runtime": "npm:^7.28.4"
|
||||
"@emotion/cache": "npm:^11.14.0"
|
||||
"@emotion/serialize": "npm:^1.3.3"
|
||||
"@emotion/sheet": "npm:^1.4.0"
|
||||
|
|
@ -823,19 +839,19 @@ __metadata:
|
|||
optional: true
|
||||
"@emotion/styled":
|
||||
optional: true
|
||||
checksum: 10c0/d5644b40269a70a1c86844f7301aa6865289994e7835b471f3503e67795010d5334362cfd21d8804f54e8b71d6c9c932ca78bafc2325767e3abbe037f9e8e10b
|
||||
checksum: 10c0/01dc8aefde58d5257564b7fd40f37de0f76d79e6bc6b52738cf41c333a053623baf2648f0557fb4b5ded306fd2b98e94797d7e48ad1c1f297747d2a265e22ad0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mui/system@npm:^7.3.2":
|
||||
version: 7.3.2
|
||||
resolution: "@mui/system@npm:7.3.2"
|
||||
"@mui/system@npm:^7.3.5":
|
||||
version: 7.3.5
|
||||
resolution: "@mui/system@npm:7.3.5"
|
||||
dependencies:
|
||||
"@babel/runtime": "npm:^7.28.3"
|
||||
"@mui/private-theming": "npm:^7.3.2"
|
||||
"@mui/styled-engine": "npm:^7.3.2"
|
||||
"@mui/types": "npm:^7.4.6"
|
||||
"@mui/utils": "npm:^7.3.2"
|
||||
"@babel/runtime": "npm:^7.28.4"
|
||||
"@mui/private-theming": "npm:^7.3.5"
|
||||
"@mui/styled-engine": "npm:^7.3.5"
|
||||
"@mui/types": "npm:^7.4.8"
|
||||
"@mui/utils": "npm:^7.3.5"
|
||||
clsx: "npm:^2.1.1"
|
||||
csstype: "npm:^3.1.3"
|
||||
prop-types: "npm:^15.8.1"
|
||||
|
|
@ -851,41 +867,41 @@ __metadata:
|
|||
optional: true
|
||||
"@types/react":
|
||||
optional: true
|
||||
checksum: 10c0/ed385c37f29a8d4b57bc1c59f8bc06a3e4cc393d86a6e0059229eacc7c96bcb11ae80369de0e459971bde24bdd33078f5578f152f0ac2e796222b269a80833ed
|
||||
checksum: 10c0/12ed6e0f4770848c91c066b6ffb315f2cd31d6281ff12780f8d994d5b677750277812491ba502831601bbe66cbc48812268ae08b7bbc10120e7faf5616807489
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mui/types@npm:^7.4.6":
|
||||
version: 7.4.6
|
||||
resolution: "@mui/types@npm:7.4.6"
|
||||
"@mui/types@npm:^7.4.8":
|
||||
version: 7.4.8
|
||||
resolution: "@mui/types@npm:7.4.8"
|
||||
dependencies:
|
||||
"@babel/runtime": "npm:^7.28.3"
|
||||
"@babel/runtime": "npm:^7.28.4"
|
||||
peerDependencies:
|
||||
"@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
"@types/react":
|
||||
optional: true
|
||||
checksum: 10c0/baa901e410591d0216b3f959cdbf5a1ee2ce560726d2fba1c700b40f64c1be3e63bd799f1b30a7d0bc8cc45a46d782928ea28d9906d64438f21e305884c48a99
|
||||
checksum: 10c0/dfdca47c894da372236f7c209544abd2998a77af646baf28d97a49313064b293b2fb434e45f9e5331e3123f9f8863f7b3e1db8542d7bde2d4f1e5f030d85f0c1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mui/utils@npm:^7.3.2":
|
||||
version: 7.3.2
|
||||
resolution: "@mui/utils@npm:7.3.2"
|
||||
"@mui/utils@npm:^7.3.5":
|
||||
version: 7.3.5
|
||||
resolution: "@mui/utils@npm:7.3.5"
|
||||
dependencies:
|
||||
"@babel/runtime": "npm:^7.28.3"
|
||||
"@mui/types": "npm:^7.4.6"
|
||||
"@babel/runtime": "npm:^7.28.4"
|
||||
"@mui/types": "npm:^7.4.8"
|
||||
"@types/prop-types": "npm:^15.7.15"
|
||||
clsx: "npm:^2.1.1"
|
||||
prop-types: "npm:^15.8.1"
|
||||
react-is: "npm:^19.1.1"
|
||||
react-is: "npm:^19.2.0"
|
||||
peerDependencies:
|
||||
"@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
"@types/react":
|
||||
optional: true
|
||||
checksum: 10c0/5a88ff08a823b976421f8d61098d56d527d95c222800d5b3f71acff795e7b0db6b02e40228773a6ed7ee22d8eaa607d816215b5a4b6497c21aaa9668c2699b56
|
||||
checksum: 10c0/c9f9ce12a5053d7aeafd0c390e7d17d331e0366dec9d993c9ad860f78c1d9410e5a33c40601afa039f4120ea299d2a59e76eff705359c7d96fb09ce636ba72b9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -916,16 +932,16 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@npmcli/agent@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "@npmcli/agent@npm:3.0.0"
|
||||
"@npmcli/agent@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "@npmcli/agent@npm:4.0.0"
|
||||
dependencies:
|
||||
agent-base: "npm:^7.1.0"
|
||||
http-proxy-agent: "npm:^7.0.0"
|
||||
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"
|
||||
checksum: 10c0/efe37b982f30740ee77696a80c196912c274ecd2cb243bc6ae7053a50c733ce0f6c09fda085145f33ecf453be19654acca74b69e81eaad4c90f00ccffe2f9271
|
||||
checksum: 10c0/f7b5ce0f3dd42c3f8c6546e8433573d8049f67ef11ec22aa4704bc41483122f68bf97752e06302c455ead667af5cb753e6a09bff06632bc465c1cfd4c4b75a53
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -938,13 +954,6 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 2.11.8
|
||||
resolution: "@popperjs/core@npm:2.11.8"
|
||||
|
|
@ -1355,10 +1364,10 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"abbrev@npm:^3.0.0":
|
||||
version: 3.0.1
|
||||
resolution: "abbrev@npm:3.0.1"
|
||||
checksum: 10c0/21ba8f574ea57a3106d6d35623f2c4a9111d9ee3e9a5be47baed46ec2457d2eac46e07a5c4a60186f88cb98abbe3e24f2d4cca70bc2b12f1692523e2209a9ccf
|
||||
"abbrev@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "abbrev@npm:4.0.0"
|
||||
checksum: 10c0/b4cc16935235e80702fc90192e349e32f8ef0ed151ef506aa78c81a7c455ec18375c4125414b99f84b2e055199d66383e787675f0bcd87da7a4dbd59f9eac1d5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -1535,6 +1544,13 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 1.0.7
|
||||
resolution: "available-typed-arrays@npm:1.0.7"
|
||||
|
|
@ -1604,23 +1620,22 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cacache@npm:^19.0.1":
|
||||
version: 19.0.1
|
||||
resolution: "cacache@npm:19.0.1"
|
||||
"cacache@npm:^20.0.1":
|
||||
version: 20.0.1
|
||||
resolution: "cacache@npm:20.0.1"
|
||||
dependencies:
|
||||
"@npmcli/fs": "npm:^4.0.0"
|
||||
fs-minipass: "npm:^3.0.0"
|
||||
glob: "npm:^10.2.2"
|
||||
lru-cache: "npm:^10.0.1"
|
||||
glob: "npm:^11.0.3"
|
||||
lru-cache: "npm:^11.1.0"
|
||||
minipass: "npm:^7.0.3"
|
||||
minipass-collect: "npm:^2.0.1"
|
||||
minipass-flush: "npm:^1.0.5"
|
||||
minipass-pipeline: "npm:^1.2.4"
|
||||
p-map: "npm:^7.0.2"
|
||||
ssri: "npm:^12.0.0"
|
||||
tar: "npm:^7.4.3"
|
||||
unique-filename: "npm:^4.0.0"
|
||||
checksum: 10c0/01f2134e1bd7d3ab68be851df96c8d63b492b1853b67f2eecb2c37bb682d37cb70bb858a16f2f0554d3c0071be6dfe21456a1ff6fa4b7eed996570d6a25ffe9c
|
||||
checksum: 10c0/e3efcf3af1c984e6e59e03372d9289861736a572e6e05b620606b87a67e71d04cff6dbc99607801cb21bcaae1fb4fb84d4cc8e3fda725e95881329ef03dac602
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -1731,6 +1746,13 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 7.1.0
|
||||
resolution: "cosmiconfig@npm:7.1.0"
|
||||
|
|
@ -1795,7 +1817,19 @@ __metadata:
|
|||
languageName: node
|
||||
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
|
||||
resolution: "debug@npm:4.4.1"
|
||||
dependencies:
|
||||
|
|
@ -1918,11 +1952,11 @@ __metadata:
|
|||
linkType: hard
|
||||
|
||||
"error-ex@npm:^1.3.1":
|
||||
version: 1.3.2
|
||||
resolution: "error-ex@npm:1.3.2"
|
||||
version: 1.3.4
|
||||
resolution: "error-ex@npm:1.3.4"
|
||||
dependencies:
|
||||
is-arrayish: "npm:^0.2.1"
|
||||
checksum: 10c0/ba827f89369b4c93382cfca5a264d059dfefdaa56ecc5e338ffa58a6471f5ed93b71a20add1d52290a4873d92381174382658c885ac1a2305f7baca363ce9cce
|
||||
checksum: 10c0/b9e34ff4778b8f3b31a8377e1c654456f4c41aeaa3d10a1138c3b7635d8b7b2e03eb2475d46d8ae055c1f180a1063e100bffabf64ea7e7388b37735df5328664
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -2334,9 +2368,9 @@ __metadata:
|
|||
linkType: hard
|
||||
|
||||
"exponential-backoff@npm:^3.1.1":
|
||||
version: 3.1.2
|
||||
resolution: "exponential-backoff@npm:3.1.2"
|
||||
checksum: 10c0/d9d3e1eafa21b78464297df91f1776f7fbaa3d5e3f7f0995648ca5b89c069d17055033817348d9f4a43d1c20b0eab84f75af6991751e839df53e4dfd6f22e844
|
||||
version: 3.1.3
|
||||
resolution: "exponential-backoff@npm:3.1.3"
|
||||
checksum: 10c0/77e3ae682b7b1f4972f563c6dbcd2b0d54ac679e62d5d32f3e5085feba20483cf28bd505543f520e287a56d4d55a28d7874299941faf637e779a1aa5994d1267
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -2404,6 +2438,15 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 7.1.1
|
||||
resolution: "fill-range@npm:7.1.1"
|
||||
|
|
@ -2456,7 +2499,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"foreground-child@npm:^3.1.0":
|
||||
"foreground-child@npm:^3.3.1":
|
||||
version: 3.3.1
|
||||
resolution: "foreground-child@npm:3.3.1"
|
||||
dependencies:
|
||||
|
|
@ -2586,19 +2629,19 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"glob@npm:^10.2.2":
|
||||
version: 10.4.5
|
||||
resolution: "glob@npm:10.4.5"
|
||||
"glob@npm:^11.0.3":
|
||||
version: 11.0.3
|
||||
resolution: "glob@npm:11.0.3"
|
||||
dependencies:
|
||||
foreground-child: "npm:^3.1.0"
|
||||
jackspeak: "npm:^3.1.2"
|
||||
minimatch: "npm:^9.0.4"
|
||||
foreground-child: "npm:^3.3.1"
|
||||
jackspeak: "npm:^4.1.1"
|
||||
minimatch: "npm:^10.0.3"
|
||||
minipass: "npm:^7.1.2"
|
||||
package-json-from-dist: "npm:^1.0.0"
|
||||
path-scurry: "npm:^1.11.1"
|
||||
path-scurry: "npm:^2.0.0"
|
||||
bin:
|
||||
glob: dist/esm/bin.mjs
|
||||
checksum: 10c0/19a9759ea77b8e3ca0a43c2f07ecddc2ad46216b786bb8f993c445aee80d345925a21e5280c7b7c6c59e860a0154b84e4b2b60321fea92cd3c56b4a7489f160e
|
||||
checksum: 10c0/7d24457549ec2903920dfa3d8e76850e7c02aa709122f0164b240c712f5455c0b457e6f2a1eee39344c6148e39895be8094ae8cfef7ccc3296ed30bce250c661
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -2792,9 +2835,9 @@ __metadata:
|
|||
linkType: hard
|
||||
|
||||
"ip-address@npm:^10.0.1":
|
||||
version: 10.0.1
|
||||
resolution: "ip-address@npm:10.0.1"
|
||||
checksum: 10c0/1634d79dae18394004775cb6d699dc46b7c23df6d2083164025a2b15240c1164fccde53d0e08bd5ee4fc53913d033ab6b5e395a809ad4b956a940c446e948843
|
||||
version: 10.1.0
|
||||
resolution: "ip-address@npm:10.1.0"
|
||||
checksum: 10c0/0103516cfa93f6433b3bd7333fa876eb21263912329bfa47010af5e16934eeeff86f3d2ae700a3744a137839ddfad62b900c7a445607884a49b5d1e32a3d7566
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -2855,7 +2898,7 @@ __metadata:
|
|||
languageName: node
|
||||
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
|
||||
resolution: "is-core-module@npm:2.16.1"
|
||||
dependencies:
|
||||
|
|
@ -3079,16 +3122,12 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jackspeak@npm:^3.1.2":
|
||||
version: 3.4.3
|
||||
resolution: "jackspeak@npm:3.4.3"
|
||||
"jackspeak@npm:^4.1.1":
|
||||
version: 4.1.1
|
||||
resolution: "jackspeak@npm:4.1.1"
|
||||
dependencies:
|
||||
"@isaacs/cliui": "npm:^8.0.2"
|
||||
"@pkgjs/parseargs": "npm:^0.11.0"
|
||||
dependenciesMeta:
|
||||
"@pkgjs/parseargs":
|
||||
optional: true
|
||||
checksum: 10c0/6acc10d139eaefdbe04d2f679e6191b3abf073f111edf10b1de5302c97ec93fffeb2fdd8681ed17f16268aa9dd4f8c588ed9d1d3bffbbfa6e8bf897cbb3149b9
|
||||
checksum: 10c0/84ec4f8e21d6514db24737d9caf65361511f75e5e424980eebca4199f400874f45e562ac20fa8aeb1dd20ca2f3f81f0788b6e9c3e64d216a5794fd6f30e0e042
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -3221,10 +3260,10 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0":
|
||||
version: 10.4.3
|
||||
resolution: "lru-cache@npm:10.4.3"
|
||||
checksum: 10c0/ebd04fbca961e6c1d6c0af3799adcc966a1babe798f685bb84e6599266599cd95d94630b10262f5424539bc4640107e8a33aa28585374abf561d30d16f4b39fb
|
||||
"lru-cache@npm:^11.0.0, lru-cache@npm:^11.1.0, lru-cache@npm:^11.2.1":
|
||||
version: 11.2.2
|
||||
resolution: "lru-cache@npm:11.2.2"
|
||||
checksum: 10c0/72d7831bbebc85e2bdefe01047ee5584db69d641c48d7a509e86f66f6ee111b30af7ec3bd68a967d47b69a4b1fa8bbf3872630bd06a63b6735e6f0a5f1c8e83d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -3237,22 +3276,22 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"make-fetch-happen@npm:^14.0.3":
|
||||
version: 14.0.3
|
||||
resolution: "make-fetch-happen@npm:14.0.3"
|
||||
"make-fetch-happen@npm:^15.0.0":
|
||||
version: 15.0.3
|
||||
resolution: "make-fetch-happen@npm:15.0.3"
|
||||
dependencies:
|
||||
"@npmcli/agent": "npm:^3.0.0"
|
||||
cacache: "npm:^19.0.1"
|
||||
"@npmcli/agent": "npm:^4.0.0"
|
||||
cacache: "npm:^20.0.1"
|
||||
http-cache-semantics: "npm:^4.1.1"
|
||||
minipass: "npm:^7.0.2"
|
||||
minipass-fetch: "npm:^4.0.0"
|
||||
minipass-fetch: "npm:^5.0.0"
|
||||
minipass-flush: "npm:^1.0.5"
|
||||
minipass-pipeline: "npm:^1.2.4"
|
||||
negotiator: "npm:^1.0.0"
|
||||
proc-log: "npm:^5.0.0"
|
||||
proc-log: "npm:^6.0.0"
|
||||
promise-retry: "npm:^2.0.1"
|
||||
ssri: "npm:^12.0.0"
|
||||
checksum: 10c0/c40efb5e5296e7feb8e37155bde8eb70bc57d731b1f7d90e35a092fde403d7697c56fb49334d92d330d6f1ca29a98142036d6480a12681133a0a1453164cb2f0
|
||||
ssri: "npm:^13.0.0"
|
||||
checksum: 10c0/525f74915660be60b616bcbd267c4a5b59481b073ba125e45c9c3a041bb1a47a2bd0ae79d028eb6f5f95bf9851a4158423f5068539c3093621abb64027e8e461
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -3280,6 +3319,15 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 3.1.2
|
||||
resolution: "minimatch@npm:3.1.2"
|
||||
|
|
@ -3307,9 +3355,9 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"minipass-fetch@npm:^4.0.0":
|
||||
version: 4.0.1
|
||||
resolution: "minipass-fetch@npm:4.0.1"
|
||||
"minipass-fetch@npm:^5.0.0":
|
||||
version: 5.0.0
|
||||
resolution: "minipass-fetch@npm:5.0.0"
|
||||
dependencies:
|
||||
encoding: "npm:^0.1.13"
|
||||
minipass: "npm:^7.0.3"
|
||||
|
|
@ -3318,7 +3366,7 @@ __metadata:
|
|||
dependenciesMeta:
|
||||
encoding:
|
||||
optional: true
|
||||
checksum: 10c0/a3147b2efe8e078c9bf9d024a0059339c5a09c5b1dded6900a219c218cc8b1b78510b62dae556b507304af226b18c3f1aeb1d48660283602d5b6586c399eed5c
|
||||
checksum: 10c0/9443aab5feab190972f84b64116e54e58dd87a58e62399cae0a4a7461b80568281039b7c3a38ba96453431ebc799d1e26999e548540156216729a4967cd5ef06
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -3358,28 +3406,19 @@ __metadata:
|
|||
languageName: node
|
||||
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
|
||||
resolution: "minipass@npm:7.1.2"
|
||||
checksum: 10c0/b0fd20bb9fb56e5fa9a8bfac539e8915ae07430a619e4b86ff71f5fc757ef3924b23b2c4230393af1eda647ed3d75739e4e0acb250a6b1eb277cf7f8fe449557
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"minizlib@npm:^3.0.1":
|
||||
version: 3.0.2
|
||||
resolution: "minizlib@npm:3.0.2"
|
||||
"minizlib@npm:^3.0.1, minizlib@npm:^3.1.0":
|
||||
version: 3.1.0
|
||||
resolution: "minizlib@npm:3.1.0"
|
||||
dependencies:
|
||||
minipass: "npm:^7.1.2"
|
||||
checksum: 10c0/9f3bd35e41d40d02469cb30470c55ccc21cae0db40e08d1d0b1dff01cc8cc89a6f78e9c5d2b7c844e485ec0a8abc2238111213fdc5b2038e6d1012eacf316f78
|
||||
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
|
||||
checksum: 10c0/5aad75ab0090b8266069c9aabe582c021ae53eb33c6c691054a13a45db3b4f91a7fb1bd79151e6b4e9e9a86727b522527c0a06ec7d45206b745d54cd3097bcec
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -3414,22 +3453,22 @@ __metadata:
|
|||
linkType: hard
|
||||
|
||||
"node-gyp@npm:latest":
|
||||
version: 11.4.2
|
||||
resolution: "node-gyp@npm:11.4.2"
|
||||
version: 12.1.0
|
||||
resolution: "node-gyp@npm:12.1.0"
|
||||
dependencies:
|
||||
env-paths: "npm:^2.2.0"
|
||||
exponential-backoff: "npm:^3.1.1"
|
||||
graceful-fs: "npm:^4.2.6"
|
||||
make-fetch-happen: "npm:^14.0.3"
|
||||
nopt: "npm:^8.0.0"
|
||||
proc-log: "npm:^5.0.0"
|
||||
make-fetch-happen: "npm:^15.0.0"
|
||||
nopt: "npm:^9.0.0"
|
||||
proc-log: "npm:^6.0.0"
|
||||
semver: "npm:^7.3.5"
|
||||
tar: "npm:^7.4.3"
|
||||
tar: "npm:^7.5.2"
|
||||
tinyglobby: "npm:^0.2.12"
|
||||
which: "npm:^5.0.0"
|
||||
which: "npm:^6.0.0"
|
||||
bin:
|
||||
node-gyp: bin/node-gyp.js
|
||||
checksum: 10c0/0bfd3e96770ed70f07798d881dd37b4267708966d868a0e585986baac487d9cf5831285579fd629a83dc4e434f53e6416ce301097f2ee464cb74d377e4d8bdbe
|
||||
checksum: 10c0/f43efea8aaf0beb6b2f6184e533edad779b2ae38062953e21951f46221dd104006cc574154f2ad4a135467a5aae92c49e84ef289311a82e08481c5df0e8dc495
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -3440,14 +3479,14 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"nopt@npm:^8.0.0":
|
||||
version: 8.1.0
|
||||
resolution: "nopt@npm:8.1.0"
|
||||
"nopt@npm:^9.0.0":
|
||||
version: 9.0.0
|
||||
resolution: "nopt@npm:9.0.0"
|
||||
dependencies:
|
||||
abbrev: "npm:^3.0.0"
|
||||
abbrev: "npm:^4.0.0"
|
||||
bin:
|
||||
nopt: bin/nopt.js
|
||||
checksum: 10c0/62e9ea70c7a3eb91d162d2c706b6606c041e4e7b547cbbb48f8b3695af457dd6479904d7ace600856bf923dd8d1ed0696f06195c8c20f02ac87c1da0e1d315ef
|
||||
checksum: 10c0/1822eb6f9b020ef6f7a7516d7b64a8036e09666ea55ac40416c36e4b2b343122c3cff0e2f085675f53de1d2db99a2a89a60ccea1d120bcd6a5347bf6ceb4a7fd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -3566,9 +3605,9 @@ __metadata:
|
|||
linkType: hard
|
||||
|
||||
"p-map@npm:^7.0.2":
|
||||
version: 7.0.3
|
||||
resolution: "p-map@npm:7.0.3"
|
||||
checksum: 10c0/46091610da2b38ce47bcd1d8b4835a6fa4e832848a6682cf1652bc93915770f4617afc844c10a77d1b3e56d2472bb2d5622353fa3ead01a7f42b04fc8e744a5c
|
||||
version: 7.0.4
|
||||
resolution: "p-map@npm:7.0.4"
|
||||
checksum: 10c0/a5030935d3cb2919d7e89454d1ce82141e6f9955413658b8c9403cfe379283770ed3048146b44cde168aa9e8c716505f196d5689db0ae3ce9a71521a2fef3abd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -3621,13 +3660,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"path-scurry@npm:^1.11.1":
|
||||
version: 1.11.1
|
||||
resolution: "path-scurry@npm:1.11.1"
|
||||
"path-scurry@npm:^2.0.0":
|
||||
version: 2.0.1
|
||||
resolution: "path-scurry@npm:2.0.1"
|
||||
dependencies:
|
||||
lru-cache: "npm:^10.2.0"
|
||||
minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0"
|
||||
checksum: 10c0/32a13711a2a505616ae1cc1b5076801e453e7aae6ac40ab55b388bb91b9d0547a52f5aaceff710ea400205f18691120d4431e520afbe4266b836fadede15872d
|
||||
lru-cache: "npm:^11.0.0"
|
||||
minipass: "npm:^7.1.2"
|
||||
checksum: 10c0/2a16ed0e81fbc43513e245aa5763354e25e787dab0d539581a6c3f0f967461a159ed6236b2559de23aa5b88e7dc32b469b6c47568833dd142a4b24b4f5cd2620
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -3684,10 +3723,10 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"proc-log@npm:^5.0.0":
|
||||
version: 5.0.0
|
||||
resolution: "proc-log@npm:5.0.0"
|
||||
checksum: 10c0/bbe5edb944b0ad63387a1d5b1911ae93e05ce8d0f60de1035b218cdcceedfe39dbd2c697853355b70f1a090f8f58fe90da487c85216bf9671f9499d1a897e9e3
|
||||
"proc-log@npm:^6.0.0":
|
||||
version: 6.0.0
|
||||
resolution: "proc-log@npm:6.0.0"
|
||||
checksum: 10c0/40c5e2b4c55e395a3bd72e38cba9c26e58598a1f4844fa6a115716d5231a0919f46aa8e351147035d91583ad39a794593615078c948bc001fe3beb99276be776
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -3737,6 +3776,19 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 9.16.0
|
||||
resolution: "react-intersection-observer@npm:9.16.0"
|
||||
|
|
@ -3757,10 +3809,10 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-is@npm:^19.1.1":
|
||||
version: 19.1.1
|
||||
resolution: "react-is@npm:19.1.1"
|
||||
checksum: 10c0/3dba763fcd69835ae263dcd6727d7ffcc44c1d616f04b7329e67aefdc66a567af4f8dcecdd29454c7a707c968aa1eb85083a83fb616f01675ef25e71cf082f97
|
||||
"react-is@npm:^19.2.0":
|
||||
version: 19.2.0
|
||||
resolution: "react-is@npm:19.2.0"
|
||||
checksum: 10c0/a63cb346aeced8ac0e671b0f9b33720d2906de02a066ca067075d871a5d4c64cdb328f495baf9b5842d5868c0d5edd1ce18465a7358b52f4b6aa983479c9bfa2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -3771,6 +3823,34 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 4.4.5
|
||||
resolution: "react-transition-group@npm:4.4.5"
|
||||
|
|
@ -3831,15 +3911,15 @@ __metadata:
|
|||
linkType: hard
|
||||
|
||||
"resolve@npm:^1.19.0":
|
||||
version: 1.22.10
|
||||
resolution: "resolve@npm:1.22.10"
|
||||
version: 1.22.11
|
||||
resolution: "resolve@npm:1.22.11"
|
||||
dependencies:
|
||||
is-core-module: "npm:^2.16.0"
|
||||
is-core-module: "npm:^2.16.1"
|
||||
path-parse: "npm:^1.0.7"
|
||||
supports-preserve-symlinks-flag: "npm:^1.0.0"
|
||||
bin:
|
||||
resolve: bin/resolve
|
||||
checksum: 10c0/8967e1f4e2cc40f79b7e080b4582b9a8c5ee36ffb46041dccb20e6461161adf69f843b43067b4a375de926a2cd669157e29a29578191def399dd5ef89a1b5203
|
||||
checksum: 10c0/f657191507530f2cbecb5815b1ee99b20741ea6ee02a59c57028e9ec4c2c8d7681afcc35febbd554ac0ded459db6f2d8153382c53a2f266cee2575e512674409
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -3857,15 +3937,15 @@ __metadata:
|
|||
linkType: hard
|
||||
|
||||
"resolve@patch:resolve@npm%3A^1.19.0#optional!builtin<compat/resolve>":
|
||||
version: 1.22.10
|
||||
resolution: "resolve@patch:resolve@npm%3A1.22.10#optional!builtin<compat/resolve>::version=1.22.10&hash=c3c19d"
|
||||
version: 1.22.11
|
||||
resolution: "resolve@patch:resolve@npm%3A1.22.11#optional!builtin<compat/resolve>::version=1.22.11&hash=c3c19d"
|
||||
dependencies:
|
||||
is-core-module: "npm:^2.16.0"
|
||||
is-core-module: "npm:^2.16.1"
|
||||
path-parse: "npm:^1.0.7"
|
||||
supports-preserve-symlinks-flag: "npm:^1.0.0"
|
||||
bin:
|
||||
resolve: bin/resolve
|
||||
checksum: 10c0/52a4e505bbfc7925ac8f4cd91fd8c4e096b6a89728b9f46861d3b405ac9a1ccf4dcbf8befb4e89a2e11370dacd0160918163885cbc669369590f2f31f4c58939
|
||||
checksum: 10c0/ee5b182f2e37cb1165465e58c6abc797fec0a80b5ba3231607beb4677db0c9291ac010c47cf092b6daa2b7f518d69a0e21888e7e2b633f68d501a874212a8c63
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -4040,7 +4120,16 @@ __metadata:
|
|||
languageName: node
|
||||
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
|
||||
resolution: "semver@npm:7.7.2"
|
||||
bin:
|
||||
|
|
@ -4049,6 +4138,13 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 1.2.2
|
||||
resolution: "set-function-length@npm:1.2.2"
|
||||
|
|
@ -4208,6 +4304,15 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 1.1.0
|
||||
resolution: "stop-iteration-iterator@npm:1.1.0"
|
||||
|
|
@ -4357,17 +4462,16 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tar@npm:^7.4.3":
|
||||
version: 7.4.3
|
||||
resolution: "tar@npm:7.4.3"
|
||||
"tar@npm:^7.5.2":
|
||||
version: 7.5.2
|
||||
resolution: "tar@npm:7.5.2"
|
||||
dependencies:
|
||||
"@isaacs/fs-minipass": "npm:^4.0.0"
|
||||
chownr: "npm:^3.0.0"
|
||||
minipass: "npm:^7.1.2"
|
||||
minizlib: "npm:^3.0.1"
|
||||
mkdirp: "npm:^3.0.1"
|
||||
minizlib: "npm:^3.1.0"
|
||||
yallist: "npm:^5.0.0"
|
||||
checksum: 10c0/d4679609bb2a9b48eeaf84632b6d844128d2412b95b6de07d53d8ee8baf4ca0857c9331dfa510390a0727b550fd543d4d1a10995ad86cdf078423fbb8d99831d
|
||||
checksum: 10c0/a7d8b801139b52f93a7e34830db0de54c5aa45487c7cb551f6f3d44a112c67f1cb8ffdae856b05fd4f17b1749911f1c26f1e3a23bbe0279e17fd96077f13f467
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -4392,7 +4496,9 @@ __metadata:
|
|||
globals: "npm:^16.3.0"
|
||||
react: "npm:^19.1.1"
|
||||
react-dom: "npm:^19.1.1"
|
||||
react-dropzone: "npm:^14.3.8"
|
||||
react-intersection-observer: "npm:^9.16.0"
|
||||
react-router-dom: "npm:^7.8.2"
|
||||
typescript: "npm:~5.8.3"
|
||||
typescript-eslint: "npm:^8.39.1"
|
||||
vite: "npm:^7.1.2"
|
||||
|
|
@ -4427,6 +4533,13 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 0.4.0
|
||||
resolution: "type-check@npm:0.4.0"
|
||||
|
|
@ -4704,14 +4817,14 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"which@npm:^5.0.0":
|
||||
version: 5.0.0
|
||||
resolution: "which@npm:5.0.0"
|
||||
"which@npm:^6.0.0":
|
||||
version: 6.0.0
|
||||
resolution: "which@npm:6.0.0"
|
||||
dependencies:
|
||||
isexe: "npm:^3.1.1"
|
||||
bin:
|
||||
node-which: bin/which.js
|
||||
checksum: 10c0/e556e4cd8b7dbf5df52408c9a9dd5ac6518c8c5267c8953f5b0564073c66ed5bf9503b14d876d0e9c7844d4db9725fb0dcf45d6e911e17e26ab363dc3965ae7b
|
||||
checksum: 10c0/fe9d6463fe44a76232bb6e3b3181922c87510a5b250a98f1e43a69c99c079b3f42ddeca7e03d3e5f2241bf2d334f5a7657cfa868b97c109f3870625842f4cc15
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue