@codesinger0/shared-components 1.1.84 → 1.1.85
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/LargeItemCard.jsx +35 -11
- package/dist/components 2/AccessibilityMenu.jsx +474 -0
- package/dist/components 2/AdvantagesList.jsx +89 -0
- package/dist/components 2/ArticlesList.jsx +269 -0
- package/dist/components 2/DualTextCard.jsx +73 -0
- package/dist/components 2/FloatingWhatsAppButton.jsx +180 -0
- package/dist/components 2/FullscreenCarousel.jsx +292 -0
- package/dist/components 2/Hero.jsx +198 -0
- package/dist/components 2/IconGrid.jsx +144 -0
- package/dist/components 2/IntroSection.jsx +74 -0
- package/dist/components 2/LargeItemCard.jsx +267 -0
- package/dist/components 2/MasonryItemCard.jsx +247 -0
- package/dist/components 2/Menu.d.ts +26 -0
- package/dist/components 2/Menu.jsx +268 -0
- package/dist/components 2/MyOrdersDisplay.jsx +311 -0
- package/dist/components 2/QAAccordion.jsx +212 -0
- package/dist/components 2/SmallItemCard.jsx +152 -0
- package/dist/components 2/SmallItemsGrid.jsx +313 -0
- package/dist/components 2/TextListCards.jsx +107 -0
- package/dist/components 2/ToastProvider.jsx +38 -0
- package/dist/components 2/UnderConstruction.jsx +76 -0
- package/dist/components 2/VideoCard.jsx +88 -0
- package/dist/components 2/cart/CartItem.jsx +101 -0
- package/dist/components 2/cart/FloatingCartButton.jsx +49 -0
- package/dist/components 2/cart/OrderForm.jsx +960 -0
- package/dist/components 2/cart/ShoppingCartModal.jsx +229 -0
- package/dist/components 2/clubMembership/ClubMembershipModal.jsx +289 -0
- package/dist/components 2/clubMembership/ClubPromoModal.jsx +108 -0
- package/dist/components 2/elements/CTAButton.jsx +17 -0
- package/dist/components 2/elements/FixedWidthHeroVideo.jsx +92 -0
- package/dist/components 2/elements/ImageLightbox.jsx +112 -0
- package/dist/components 2/elements/RoundButton.jsx +44 -0
- package/dist/components 2/elements/SmallButton.jsx +35 -0
- package/dist/components 2/elements/Toast.jsx +37 -0
- package/dist/components 2/elements/VideoLightbox.jsx +76 -0
- package/dist/components 2/modals/ItemDetailsModal.jsx +192 -0
- package/dist/components 2/products/CategoryList.jsx +24 -0
- package/dist/components 2/products/PriceRangeSlider.jsx +162 -0
- package/dist/components 2/products/ProductsDisplay.jsx +40 -0
- package/dist/components 2/products/ProductsSidebar.jsx +46 -0
- package/dist/components 2/products/SubcategorySection.jsx +37 -0
- package/dist/context 2/CartContext.jsx +165 -0
- package/dist/context 2/ItemModalContext.jsx +40 -0
- package/dist/hooks 2/useScrollLock.js +52 -0
- package/dist/index 2.js +45 -0
- package/dist/integrations 2/emailService.js +167 -0
- package/dist/styles 2/shared-components.css +29 -0
- package/dist/utils 2/ScrollManager.jsx +85 -0
- package/dist/utils 2/ScrollToTop.jsx +14 -0
- package/package.json +1 -1
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
const FixedWidthHeroVideo = ({
|
|
4
|
+
youtubeVideoId = "dQw4w9WgXcQ",
|
|
5
|
+
useYoutube = false,
|
|
6
|
+
videoUrl = "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"
|
|
7
|
+
}) => {
|
|
8
|
+
const [viewportSize, setViewportSize] = useState({ width: 0, height: 0 });
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
const updateViewportSize = () => {
|
|
12
|
+
setViewportSize({
|
|
13
|
+
width: window.innerWidth,
|
|
14
|
+
height: window.innerHeight
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
updateViewportSize();
|
|
19
|
+
window.addEventListener('resize', updateViewportSize);
|
|
20
|
+
return () => window.removeEventListener('resize', updateViewportSize);
|
|
21
|
+
}, []);
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
const youtubeEmbedUrl = `https://www.youtube.com/embed/${youtubeVideoId}?autoplay=1&mute=1&loop=1&playlist=${youtubeVideoId}&controls=0&showinfo=0&rel=0&iv_load_policy=3&modestbranding=1`;
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="w-full">
|
|
28
|
+
|
|
29
|
+
{/* Hero Section */}
|
|
30
|
+
<section className="relative w-full h-screen overflow-hidden flex items-center justify-center">
|
|
31
|
+
{useYoutube ? (
|
|
32
|
+
/* YouTube Embed */
|
|
33
|
+
<iframe
|
|
34
|
+
className="absolute top-[35%] left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-0 pointer-events-none"
|
|
35
|
+
style={{
|
|
36
|
+
minWidth: '1200px',
|
|
37
|
+
width: 'max(100vw, 1200px)',
|
|
38
|
+
aspectRatio: '16/9',
|
|
39
|
+
minHeight: '120%',
|
|
40
|
+
border: 'none'
|
|
41
|
+
}}
|
|
42
|
+
src={youtubeEmbedUrl}
|
|
43
|
+
title="YouTube video player"
|
|
44
|
+
frameBorder="0"
|
|
45
|
+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
46
|
+
allowFullScreen
|
|
47
|
+
/>
|
|
48
|
+
) : (
|
|
49
|
+
/* Direct Video */
|
|
50
|
+
<video
|
|
51
|
+
className="absolute top-[35%] left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-0 pointer-events-none"
|
|
52
|
+
style={{
|
|
53
|
+
minWidth: '1200px',
|
|
54
|
+
width: 'max(100vw, 1200px)',
|
|
55
|
+
aspectRatio: '16/9',
|
|
56
|
+
minHeight: '120%',
|
|
57
|
+
border: 'none'
|
|
58
|
+
}}
|
|
59
|
+
autoPlay
|
|
60
|
+
muted
|
|
61
|
+
loop
|
|
62
|
+
playsInline
|
|
63
|
+
>
|
|
64
|
+
<source src={videoUrl} type="video/mp4" />
|
|
65
|
+
Your browser does not support the video tag.
|
|
66
|
+
</video>
|
|
67
|
+
)}
|
|
68
|
+
</section>
|
|
69
|
+
|
|
70
|
+
{/* CSS Animation Styles */}
|
|
71
|
+
<style>{`
|
|
72
|
+
@keyframes fadeInUp {
|
|
73
|
+
from {
|
|
74
|
+
opacity: 0;
|
|
75
|
+
transform: translateY(30px);
|
|
76
|
+
}
|
|
77
|
+
to {
|
|
78
|
+
opacity: 1;
|
|
79
|
+
transform: translateY(0);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
`}</style>
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Usage examples:
|
|
88
|
+
// <FixedWidthHeroVideo useYoutube={false} />
|
|
89
|
+
// <FixedWidthHeroVideo useYoutube={false} videoUrl="path/to/your/video.mp4" />
|
|
90
|
+
// <FixedWidthHeroVideo useYoutube={true} youtubeVideoId="dQw4w9WgXcQ" />
|
|
91
|
+
|
|
92
|
+
export default FixedWidthHeroVideo;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
3
|
+
import { X, ChevronLeft, ChevronRight } from 'lucide-react';
|
|
4
|
+
import useScrollLock from '../../hooks/useScrollLock';
|
|
5
|
+
|
|
6
|
+
const ImageLightbox = ({
|
|
7
|
+
images,
|
|
8
|
+
currentIndex,
|
|
9
|
+
isOpen,
|
|
10
|
+
onClose,
|
|
11
|
+
onNavigate
|
|
12
|
+
}) => {
|
|
13
|
+
const currentImage = images[currentIndex];
|
|
14
|
+
|
|
15
|
+
useScrollLock(isOpen);
|
|
16
|
+
|
|
17
|
+
if (!currentImage) return null;
|
|
18
|
+
|
|
19
|
+
// Handle both object format {src, alt, title} and string format
|
|
20
|
+
const imageSrc = typeof currentImage === 'string' ? currentImage : currentImage.src;
|
|
21
|
+
const imageAlt = typeof currentImage === 'string' ? 'Gallery image' : (currentImage.alt || currentImage.title || 'Gallery image');
|
|
22
|
+
const imageTitle = typeof currentImage === 'string' ? null : currentImage.title;
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<AnimatePresence>
|
|
26
|
+
{isOpen && (
|
|
27
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center supports-[height:100dvh]:h-[100dvh]">
|
|
28
|
+
{/* Backdrop */}
|
|
29
|
+
<motion.div
|
|
30
|
+
key="backdrop"
|
|
31
|
+
initial={{ opacity: 0 }}
|
|
32
|
+
animate={{ opacity: 1 }}
|
|
33
|
+
exit={{ opacity: 0 }}
|
|
34
|
+
transition={{ duration: 0.2 }}
|
|
35
|
+
className="absolute inset-0 bg-black bg-opacity-90"
|
|
36
|
+
onClick={onClose}
|
|
37
|
+
/>
|
|
38
|
+
|
|
39
|
+
{/* Image Container */}
|
|
40
|
+
<motion.div
|
|
41
|
+
key="image-container"
|
|
42
|
+
initial={{ opacity: 0, scale: 0.8 }}
|
|
43
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
44
|
+
exit={{ opacity: 0, scale: 0.8 }}
|
|
45
|
+
transition={{ type: "spring", stiffness: 300, damping: 25 }}
|
|
46
|
+
className="relative max-w-[90vw] max-h-[90vh] z-10"
|
|
47
|
+
onClick={(e) => e.stopPropagation()}
|
|
48
|
+
>
|
|
49
|
+
{/* Main Image */}
|
|
50
|
+
<img
|
|
51
|
+
src={imageSrc}
|
|
52
|
+
alt={imageAlt}
|
|
53
|
+
className="w-screen h-screen object-contain rounded-lg shadow-2xl"
|
|
54
|
+
/>
|
|
55
|
+
|
|
56
|
+
{/* Close Button */}
|
|
57
|
+
<button
|
|
58
|
+
onClick={onClose}
|
|
59
|
+
className="absolute top-4 right-4 bg-black bg-opacity-50 hover:bg-opacity-70 text-white p-2 rounded-full transition-all duration-200"
|
|
60
|
+
aria-label="Close lightbox"
|
|
61
|
+
>
|
|
62
|
+
<X size={24} />
|
|
63
|
+
</button>
|
|
64
|
+
|
|
65
|
+
{/* Navigation Arrows */}
|
|
66
|
+
{images.length > 1 && (
|
|
67
|
+
<>
|
|
68
|
+
{currentIndex > 0 && (
|
|
69
|
+
<button
|
|
70
|
+
onClick={() => onNavigate(currentIndex - 1)}
|
|
71
|
+
className="absolute left-4 top-1/2 transform -translate-y-1/2 bg-black bg-opacity-50 hover:bg-opacity-70 text-white p-3 rounded-full transition-all duration-200"
|
|
72
|
+
aria-label="Previous image"
|
|
73
|
+
>
|
|
74
|
+
<ChevronLeft size={24} />
|
|
75
|
+
</button>
|
|
76
|
+
)}
|
|
77
|
+
|
|
78
|
+
{currentIndex < images.length - 1 && (
|
|
79
|
+
<button
|
|
80
|
+
onClick={() => onNavigate(currentIndex + 1)}
|
|
81
|
+
className="absolute right-4 top-1/2 transform -translate-y-1/2 bg-black bg-opacity-50 hover:bg-opacity-70 text-white p-3 rounded-full transition-all duration-200"
|
|
82
|
+
aria-label="Next image"
|
|
83
|
+
>
|
|
84
|
+
<ChevronRight size={24} />
|
|
85
|
+
</button>
|
|
86
|
+
)}
|
|
87
|
+
</>
|
|
88
|
+
)}
|
|
89
|
+
|
|
90
|
+
{/* Image Counter */}
|
|
91
|
+
{images.length > 1 && (
|
|
92
|
+
<div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 bg-black bg-opacity-50 text-white px-3 py-1 rounded-full text-sm">
|
|
93
|
+
{currentIndex + 1} / {images.length}
|
|
94
|
+
</div>
|
|
95
|
+
)}
|
|
96
|
+
|
|
97
|
+
{/* Image Title */}
|
|
98
|
+
{imageTitle && (
|
|
99
|
+
<div className="absolute bottom-4 right-4 bg-black bg-opacity-50 text-white px-3 py-2 rounded-lg max-w-xs">
|
|
100
|
+
<div className="text-sm font-medium" dir="rtl">
|
|
101
|
+
{imageTitle}
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
105
|
+
</motion.div>
|
|
106
|
+
</div>
|
|
107
|
+
)}
|
|
108
|
+
</AnimatePresence>
|
|
109
|
+
);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export default ImageLightbox;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
const RoundButton = ({
|
|
4
|
+
children,
|
|
5
|
+
onClick,
|
|
6
|
+
variant = 'primary',
|
|
7
|
+
size = 'medium',
|
|
8
|
+
className = '',
|
|
9
|
+
...props
|
|
10
|
+
}) => {
|
|
11
|
+
const baseClasses = 'rounded-full font-medium transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2';
|
|
12
|
+
|
|
13
|
+
const variants = {
|
|
14
|
+
primary: 'btn-primary',
|
|
15
|
+
secondary: 'btn-secondary',
|
|
16
|
+
outline: 'border border-gray-300 text-main hover:ring-2 hover:ring-gray-500 hover:opacity-80'
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const sizes = {
|
|
20
|
+
xs: 'px-3 py-1.5 text-sm',
|
|
21
|
+
small: 'px-4 py-2 text-sm',
|
|
22
|
+
medium: 'px-6 py-3 text-base',
|
|
23
|
+
large: 'px-8 py-4 text-lg'
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const buttonClasses = `${baseClasses} ${variants[variant]} ${sizes[size]} ${className}`;
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<button
|
|
30
|
+
className={buttonClasses}
|
|
31
|
+
onClick={(e) => {
|
|
32
|
+
onClick?.(e);
|
|
33
|
+
// remove focus immediately after click
|
|
34
|
+
e.currentTarget.blur();
|
|
35
|
+
}}
|
|
36
|
+
dir="rtl"
|
|
37
|
+
{...props}
|
|
38
|
+
>
|
|
39
|
+
{children}
|
|
40
|
+
</button>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export default RoundButton;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
const SmallButton = ({
|
|
4
|
+
children,
|
|
5
|
+
onClick,
|
|
6
|
+
disabled = false,
|
|
7
|
+
className = '',
|
|
8
|
+
...props
|
|
9
|
+
}) => {
|
|
10
|
+
return (
|
|
11
|
+
<button
|
|
12
|
+
className={`
|
|
13
|
+
btn-primary content-text px-3 py-1
|
|
14
|
+
rounded-md font-medium
|
|
15
|
+
transition-all duration-200
|
|
16
|
+
focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2
|
|
17
|
+
hover:brightness-90
|
|
18
|
+
disabled:opacity-50 disabled:cursor-not-allowed
|
|
19
|
+
${className}
|
|
20
|
+
`}
|
|
21
|
+
onClick={(e) => {
|
|
22
|
+
onClick?.(e);
|
|
23
|
+
// remove focus right after click so the ring disappears
|
|
24
|
+
e.currentTarget.blur();
|
|
25
|
+
}}
|
|
26
|
+
disabled={disabled}
|
|
27
|
+
dir="rtl"
|
|
28
|
+
{...props}
|
|
29
|
+
>
|
|
30
|
+
{children}
|
|
31
|
+
</button>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default SmallButton;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
const variantStyles = {
|
|
4
|
+
success: 'bg-green-500 text-white',
|
|
5
|
+
error: 'bg-red-500 text-white',
|
|
6
|
+
info: 'bg-blue-500 text-white',
|
|
7
|
+
warning: 'bg-yellow-400 text-black',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const Toast = ({ id, message, onClose, duration = 3000, variant = 'info' }) => {
|
|
11
|
+
const [show, setShow] = useState(true);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const timer = setTimeout(() => {
|
|
15
|
+
setShow(false);
|
|
16
|
+
setTimeout(() => onClose(id), 300); // match transition duration
|
|
17
|
+
}, duration);
|
|
18
|
+
|
|
19
|
+
return () => clearTimeout(timer);
|
|
20
|
+
}, [id, onClose, duration]);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div
|
|
24
|
+
className={`fixed bottom-5 left-1/2 transform -translate-x-1/2
|
|
25
|
+
px-4 py-2 rounded-lg shadow-lg
|
|
26
|
+
text-sm sm:text-base md:text-lg
|
|
27
|
+
max-w-xs sm:max-w-sm md:max-w-md
|
|
28
|
+
transition-all duration-300
|
|
29
|
+
${variantStyles[variant]}
|
|
30
|
+
${show ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-5'}`}
|
|
31
|
+
>
|
|
32
|
+
{message}
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export default Toast;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
3
|
+
import { X } from 'lucide-react';
|
|
4
|
+
import useScrollLock from '../../hooks/useScrollLock';
|
|
5
|
+
|
|
6
|
+
const VideoLightbox = ({
|
|
7
|
+
videoUrl,
|
|
8
|
+
isOpen,
|
|
9
|
+
onClose,
|
|
10
|
+
title
|
|
11
|
+
}) => {
|
|
12
|
+
useScrollLock(isOpen);
|
|
13
|
+
|
|
14
|
+
if (!videoUrl) return null;
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<AnimatePresence>
|
|
18
|
+
{isOpen && (
|
|
19
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center supports-[height:100dvh]:h-[100dvh]">
|
|
20
|
+
{/* Backdrop */}
|
|
21
|
+
<motion.div
|
|
22
|
+
key="backdrop"
|
|
23
|
+
initial={{ opacity: 0 }}
|
|
24
|
+
animate={{ opacity: 1 }}
|
|
25
|
+
exit={{ opacity: 0 }}
|
|
26
|
+
transition={{ duration: 0.2 }}
|
|
27
|
+
className="absolute inset-0 bg-black bg-opacity-90"
|
|
28
|
+
onClick={onClose}
|
|
29
|
+
/>
|
|
30
|
+
|
|
31
|
+
{/* Video Container */}
|
|
32
|
+
<motion.div
|
|
33
|
+
key="video-container"
|
|
34
|
+
initial={{ opacity: 0, scale: 0.8 }}
|
|
35
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
36
|
+
exit={{ opacity: 0, scale: 0.8 }}
|
|
37
|
+
transition={{ type: "spring", stiffness: 300, damping: 25 }}
|
|
38
|
+
className="relative max-w-[90vw] max-h-[90vh] z-10"
|
|
39
|
+
onClick={(e) => e.stopPropagation()}
|
|
40
|
+
>
|
|
41
|
+
{/* Main Video */}
|
|
42
|
+
<video
|
|
43
|
+
src={videoUrl}
|
|
44
|
+
controls
|
|
45
|
+
autoPlay
|
|
46
|
+
className="w-screen h-screen object-contain rounded-lg shadow-2xl"
|
|
47
|
+
style={{ maxWidth: '90vw', maxHeight: '90vh' }}
|
|
48
|
+
>
|
|
49
|
+
Your browser does not support the video tag.
|
|
50
|
+
</video>
|
|
51
|
+
|
|
52
|
+
{/* Close Button */}
|
|
53
|
+
<button
|
|
54
|
+
onClick={onClose}
|
|
55
|
+
className="absolute top-4 right-4 bg-black bg-opacity-50 hover:bg-opacity-70 text-white p-2 rounded-full transition-all duration-200"
|
|
56
|
+
aria-label="Close lightbox"
|
|
57
|
+
>
|
|
58
|
+
<X size={24} />
|
|
59
|
+
</button>
|
|
60
|
+
|
|
61
|
+
{/* Video Title */}
|
|
62
|
+
{title && (
|
|
63
|
+
<div className="absolute bottom-4 right-4 bg-black bg-opacity-50 text-white px-3 py-2 rounded-lg max-w-xs">
|
|
64
|
+
<div className="text-sm font-medium" dir="rtl">
|
|
65
|
+
{title}
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
)}
|
|
69
|
+
</motion.div>
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
</AnimatePresence>
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export default VideoLightbox;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
3
|
+
import { X, ShoppingCart } from 'lucide-react';
|
|
4
|
+
import SmallButton from '../elements/SmallButton';
|
|
5
|
+
import useScrollLock from '../../hooks/useScrollLock';
|
|
6
|
+
|
|
7
|
+
const ItemDetailsModal = ({ item, isOpen, onClose, onAddToCart }) => {
|
|
8
|
+
const [localOpen, setLocalOpen] = useState(Boolean(isOpen));
|
|
9
|
+
const closingRequestedRef = useRef(false);
|
|
10
|
+
|
|
11
|
+
// Sync with external isOpen state
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (isOpen) {
|
|
14
|
+
setLocalOpen(true);
|
|
15
|
+
closingRequestedRef.current = false;
|
|
16
|
+
} else {
|
|
17
|
+
setLocalOpen(false);
|
|
18
|
+
}
|
|
19
|
+
}, [isOpen]);
|
|
20
|
+
|
|
21
|
+
useScrollLock(isOpen);
|
|
22
|
+
|
|
23
|
+
// Handle modal close request
|
|
24
|
+
const requestClose = () => {
|
|
25
|
+
closingRequestedRef.current = true;
|
|
26
|
+
setLocalOpen(false);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Handle exit complete
|
|
30
|
+
const handleExitComplete = () => {
|
|
31
|
+
if (closingRequestedRef.current) {
|
|
32
|
+
onClose();
|
|
33
|
+
closingRequestedRef.current = false;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Handle add to cart
|
|
38
|
+
const handleAddToCart = () => {
|
|
39
|
+
onAddToCart(item);
|
|
40
|
+
requestClose();
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
if (!item) return null;
|
|
44
|
+
|
|
45
|
+
// Calculate discount info
|
|
46
|
+
const hasDiscount = item.discountPrice && item.discountPrice < item.price;
|
|
47
|
+
const discountPercentage = hasDiscount
|
|
48
|
+
? Math.round(((item.price - item.discountPrice) / item.price) * 100)
|
|
49
|
+
: 0;
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className={`fixed inset-0 z-50 flex items-center justify-center ${isOpen ? '' : 'pointer-events-none'} supports-[height:100dvh]:h-[100dvh]`}>
|
|
53
|
+
<AnimatePresence initial={false} mode="wait" onExitComplete={handleExitComplete}>
|
|
54
|
+
{localOpen && (
|
|
55
|
+
<>
|
|
56
|
+
{/* Backdrop */}
|
|
57
|
+
<motion.div
|
|
58
|
+
key="backdrop"
|
|
59
|
+
initial={{ opacity: 0 }}
|
|
60
|
+
animate={{ opacity: 1 }}
|
|
61
|
+
exit={{ opacity: 0 }}
|
|
62
|
+
transition={{ duration: 0.18 }}
|
|
63
|
+
className="absolute inset-0 bg-black bg-opacity-30 backdrop-blur-sm pointer-events-auto"
|
|
64
|
+
onClick={requestClose}
|
|
65
|
+
aria-hidden="true"
|
|
66
|
+
/>
|
|
67
|
+
|
|
68
|
+
{/* Modal Panel */}
|
|
69
|
+
<motion.div
|
|
70
|
+
key="modal-panel"
|
|
71
|
+
initial={{ opacity: 0, scale: 0.9, y: 20 }}
|
|
72
|
+
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
73
|
+
exit={{ opacity: 0, scale: 0.9, y: 20 }}
|
|
74
|
+
transition={{
|
|
75
|
+
type: "spring",
|
|
76
|
+
stiffness: 300,
|
|
77
|
+
damping: 30
|
|
78
|
+
}}
|
|
79
|
+
className="relative w-full max-w-2xl mx-4 max-h-[90vh] glass-card overflow-hidden pointer-events-auto"
|
|
80
|
+
dir="rtl"
|
|
81
|
+
role="dialog"
|
|
82
|
+
aria-modal="true"
|
|
83
|
+
onClick={(e) => e.stopPropagation()}
|
|
84
|
+
>
|
|
85
|
+
{/* Header */}
|
|
86
|
+
<div className="flex justify-between items-center p-6 border-b border-white/20">
|
|
87
|
+
<h2 className="subtitle font-bold text-right">{item.name || item.label}</h2>
|
|
88
|
+
<button
|
|
89
|
+
onClick={requestClose}
|
|
90
|
+
className="content-text hover:text-primary hover:bg-gray-100 p-2 rounded-md transition-colors duration-200"
|
|
91
|
+
aria-label="סגור"
|
|
92
|
+
>
|
|
93
|
+
<X size={20} />
|
|
94
|
+
</button>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
{/* Content */}
|
|
98
|
+
<div className="p-6 overflow-y-auto max-h-[calc(90vh-140px)]">
|
|
99
|
+
<div className="grid md:grid-cols-2 gap-6">
|
|
100
|
+
{/* Image Section */}
|
|
101
|
+
<div className="relative">
|
|
102
|
+
<div className="relative w-full h-64 md:h-80 overflow-hidden rounded-xl bg-gray-100">
|
|
103
|
+
{item.imageUrl ? (
|
|
104
|
+
<img
|
|
105
|
+
src={item.imageUrl}
|
|
106
|
+
alt={item.name || item.label}
|
|
107
|
+
className="w-full h-full object-cover"
|
|
108
|
+
onError={(e) => {
|
|
109
|
+
e.target.style.display = 'none';
|
|
110
|
+
e.target.nextElementSibling.style.display = 'flex';
|
|
111
|
+
}}
|
|
112
|
+
/>
|
|
113
|
+
) : null}
|
|
114
|
+
|
|
115
|
+
{/* Image Fallback */}
|
|
116
|
+
<div
|
|
117
|
+
className="w-full h-full bg-gradient-to-br from-gray-100 to-gray-200 flex items-center justify-center"
|
|
118
|
+
style={{ display: item.imageUrl ? 'none' : 'flex' }}
|
|
119
|
+
>
|
|
120
|
+
<div className="text-center text-gray-400">
|
|
121
|
+
<div className="text-4xl mb-2">🍰</div>
|
|
122
|
+
<div className="text-sm font-medium">תמונה</div>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
{/* Discount Badge */}
|
|
127
|
+
{hasDiscount && (
|
|
128
|
+
<div className="absolute top-3 left-3 bg-red-500 text-white px-3 py-1 rounded-full text-sm font-bold shadow-lg">
|
|
129
|
+
-{discountPercentage}%
|
|
130
|
+
</div>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
{/* Details Section */}
|
|
136
|
+
<div className="space-y-4">
|
|
137
|
+
{/* Description */}
|
|
138
|
+
{item.description && (
|
|
139
|
+
<div>
|
|
140
|
+
<h3 className="content-text font-medium mb-2">תיאור</h3>
|
|
141
|
+
<p className="caption-text leading-relaxed">
|
|
142
|
+
{item.description}
|
|
143
|
+
</p>
|
|
144
|
+
</div>
|
|
145
|
+
)}
|
|
146
|
+
|
|
147
|
+
{/* Price Section */}
|
|
148
|
+
<div className="space-y-2">
|
|
149
|
+
<h3 className="content-text font-medium">מחיר</h3>
|
|
150
|
+
{hasDiscount ? (
|
|
151
|
+
<div className="space-y-2">
|
|
152
|
+
<div className="flex items-center gap-3">
|
|
153
|
+
<span className="text-2xl font-bold text-price">
|
|
154
|
+
₪{item.discountPrice}
|
|
155
|
+
</span>
|
|
156
|
+
<span className="text-sm text-gray-400 line-through">
|
|
157
|
+
₪{item.price}
|
|
158
|
+
</span>
|
|
159
|
+
</div>
|
|
160
|
+
<div className="text-sm text-price font-medium">
|
|
161
|
+
חיסכון ₪{item.price - item.discountPrice}
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
) : (
|
|
165
|
+
<div className="text-2xl font-bold text-price">
|
|
166
|
+
₪{item.price}
|
|
167
|
+
</div>
|
|
168
|
+
)}
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
{/* Add to Cart Button */}
|
|
172
|
+
<div className="pt-4">
|
|
173
|
+
<SmallButton
|
|
174
|
+
onClick={handleAddToCart}
|
|
175
|
+
className="w-full flex items-center justify-center gap-2"
|
|
176
|
+
>
|
|
177
|
+
<ShoppingCart size={16} />
|
|
178
|
+
הוסף לעגלה
|
|
179
|
+
</SmallButton>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
</motion.div>
|
|
185
|
+
</>
|
|
186
|
+
)}
|
|
187
|
+
</AnimatePresence>
|
|
188
|
+
</div>
|
|
189
|
+
);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
export default ItemDetailsModal;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
|
|
2
|
+
const CategoryList = ({ categories, selectedCategory, onCategorySelect, className = '' }) => (
|
|
3
|
+
<div className={className}>
|
|
4
|
+
<h3 className="subtitle font-semibold mb-4">קטגוריות</h3>
|
|
5
|
+
<div className="space-y-2">
|
|
6
|
+
{categories.map((category, index) => (
|
|
7
|
+
<button
|
|
8
|
+
key={index}
|
|
9
|
+
onClick={() => onCategorySelect(category)}
|
|
10
|
+
className={`w-full text-right p-3 rounded-lg transition-all duration-200 ${selectedCategory === category
|
|
11
|
+
? 'text-white'
|
|
12
|
+
: 'hover:opacity-80'
|
|
13
|
+
}`}
|
|
14
|
+
style={selectedCategory === category ? { backgroundColor: 'var(--primary)' } : {}}
|
|
15
|
+
dir="rtl"
|
|
16
|
+
>
|
|
17
|
+
<span className="content-text">{category}</span>
|
|
18
|
+
</button>
|
|
19
|
+
))}
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export default CategoryList
|