@codesinger0/shared-components 1.1.58 → 1.1.61
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/AdvantagesList.jsx +89 -0
- package/dist/components/ArticlesList.jsx +269 -0
- package/dist/components/DualTextCard.jsx +73 -0
- package/dist/components/FloatingWhatsAppButton.jsx +180 -0
- package/dist/components/IconGrid.jsx +144 -0
- package/dist/components/IntroSection.jsx +74 -0
- package/dist/components/Menu.jsx +268 -0
- package/dist/components/{QAAccordion 2.jsx → QAAccordion.jsx} +4 -3
- package/dist/components/SmallItemsGrid.jsx +308 -0
- package/dist/components/TextListCards.jsx +107 -0
- package/dist/components/UnderConstruction.jsx +76 -0
- package/dist/components/VideoCard.jsx +88 -0
- package/dist/context/CartContext.jsx +165 -0
- package/dist/context/ItemModalContext.jsx +40 -0
- package/dist/hooks/useScrollLock.js +52 -0
- package/dist/integrations/emailService.js +167 -0
- package/dist/styles/shared-components.css +29 -0
- package/dist/utils/ScrollManager.jsx +85 -0
- package/dist/utils/ScrollToTop.jsx +14 -0
- package/package.json +1 -1
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import React, { createContext, useContext, useState, useEffect } from 'react';
|
|
2
|
+
import { useToast } from '../components/ToastProvider';
|
|
3
|
+
|
|
4
|
+
const CART_STORAGE_KEY = 'digishop_shopping_cart';
|
|
5
|
+
|
|
6
|
+
const CartContext = createContext();
|
|
7
|
+
|
|
8
|
+
export const useCart = () => {
|
|
9
|
+
const context = useContext(CartContext);
|
|
10
|
+
if (!context) {
|
|
11
|
+
throw new Error('useCart must be used within a CartProvider');
|
|
12
|
+
}
|
|
13
|
+
return context;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const CartProvider = ({ children, user = null }) => {
|
|
17
|
+
const [cartItems, setCartItems] = useState([]);
|
|
18
|
+
const [isCartOpen, setIsCartOpen] = useState(false);
|
|
19
|
+
const [previousUser, setPreviousUser] = useState(null);
|
|
20
|
+
|
|
21
|
+
const addToast = useToast();
|
|
22
|
+
|
|
23
|
+
const totalItems = cartItems.reduce((sum, item) => sum + item.quantity, 0);
|
|
24
|
+
const totalPrice = cartItems.reduce((sum, item) => sum + (item.price * item.quantity), 0);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
// If we had a user before and now we don't = logout happened
|
|
28
|
+
if (previousUser && !user) {
|
|
29
|
+
clearCart(); // Auto-clear cart on logout
|
|
30
|
+
}
|
|
31
|
+
setPreviousUser(user);
|
|
32
|
+
}, [user, previousUser]);
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
const savedCart = loadCartFromStorage();
|
|
36
|
+
if (savedCart.length > 0) {
|
|
37
|
+
setCartItems(savedCart);
|
|
38
|
+
}
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
saveCartToStorage(cartItems);
|
|
43
|
+
}, [cartItems]);
|
|
44
|
+
|
|
45
|
+
const saveCartToStorage = (cartItems) => {
|
|
46
|
+
try {
|
|
47
|
+
localStorage.setItem(CART_STORAGE_KEY, JSON.stringify(cartItems));
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error('Failed to save cart to localStorage:', error);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const loadCartFromStorage = () => {
|
|
54
|
+
try {
|
|
55
|
+
const stored = localStorage.getItem(CART_STORAGE_KEY);
|
|
56
|
+
return stored ? JSON.parse(stored) : [];
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error('Failed to load cart from localStorage:', error);
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const clearCartFromStorage = () => {
|
|
64
|
+
try {
|
|
65
|
+
localStorage.removeItem(CART_STORAGE_KEY);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error('Failed to clear cart from localStorage:', error);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const addToCart = (product) => {
|
|
72
|
+
setCartItems(prevItems => {
|
|
73
|
+
const existingItem = prevItems.find(item => item.id === product.id);
|
|
74
|
+
|
|
75
|
+
if (existingItem) {
|
|
76
|
+
return prevItems.map(item =>
|
|
77
|
+
item.id === product.id
|
|
78
|
+
? { ...item, quantity: item.quantity + 1 }
|
|
79
|
+
: item
|
|
80
|
+
);
|
|
81
|
+
} else {
|
|
82
|
+
return [...prevItems, {
|
|
83
|
+
id: product.id,
|
|
84
|
+
name: product.name || product.title,
|
|
85
|
+
price: product.discountPrice || product.price,
|
|
86
|
+
quantity: 1,
|
|
87
|
+
imageUrl: product.imageUrl || ''
|
|
88
|
+
}];
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
addToast("המוצר נוסף לעגלה", "success");
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const removeFromCart = (itemId) => {
|
|
95
|
+
setCartItems(prevItems => prevItems.filter(item => item.id !== itemId));
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Update item quantity
|
|
99
|
+
const updateQuantity = (itemId, quantity) => {
|
|
100
|
+
if (quantity <= 0) {
|
|
101
|
+
removeFromCart(itemId);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
setCartItems(prevItems =>
|
|
106
|
+
prevItems.map(item =>
|
|
107
|
+
item.id === itemId
|
|
108
|
+
? { ...item, quantity }
|
|
109
|
+
: item
|
|
110
|
+
)
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const clearCart = () => {
|
|
115
|
+
setCartItems([]);
|
|
116
|
+
clearCartFromStorage();
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const clearCartOnLogout = () => {
|
|
120
|
+
setCartItems([]);
|
|
121
|
+
clearCartFromStorage();
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Toggle cart visibility
|
|
125
|
+
const toggleCart = () => {
|
|
126
|
+
setIsCartOpen(prev => !prev);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const openCart = () => setIsCartOpen(true);
|
|
130
|
+
const closeCart = () => setIsCartOpen(false);
|
|
131
|
+
|
|
132
|
+
// Checkout handler
|
|
133
|
+
const handleCheckout = () => {
|
|
134
|
+
// TODO: Implement checkout logic
|
|
135
|
+
console.log('Proceeding to checkout with items:', cartItems);
|
|
136
|
+
console.log('Total:', totalPrice);
|
|
137
|
+
// For now, just close cart
|
|
138
|
+
closeCart();
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const value = {
|
|
142
|
+
// State
|
|
143
|
+
cartItems,
|
|
144
|
+
totalItems,
|
|
145
|
+
totalPrice,
|
|
146
|
+
isCartOpen,
|
|
147
|
+
|
|
148
|
+
// Actions
|
|
149
|
+
addToCart,
|
|
150
|
+
removeFromCart,
|
|
151
|
+
updateQuantity,
|
|
152
|
+
clearCart,
|
|
153
|
+
clearCartOnLogout,
|
|
154
|
+
toggleCart,
|
|
155
|
+
openCart,
|
|
156
|
+
closeCart,
|
|
157
|
+
handleCheckout
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<CartContext.Provider value={value}>
|
|
162
|
+
{children}
|
|
163
|
+
</CartContext.Provider>
|
|
164
|
+
);
|
|
165
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// src/context/ItemModalContext.js
|
|
2
|
+
import React, { createContext, useContext, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
const ItemModalContext = createContext();
|
|
5
|
+
|
|
6
|
+
export const useItemModal = () => {
|
|
7
|
+
const context = useContext(ItemModalContext);
|
|
8
|
+
if (!context) {
|
|
9
|
+
throw new Error('useItemModal must be used within an ItemModalProvider');
|
|
10
|
+
}
|
|
11
|
+
return context;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const ItemModalProvider = ({ children }) => {
|
|
15
|
+
const [selectedItem, setSelectedItem] = useState(null);
|
|
16
|
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
17
|
+
|
|
18
|
+
const openModal = (item) => {
|
|
19
|
+
console.log('Opening modal for item:', item);
|
|
20
|
+
setSelectedItem(item);
|
|
21
|
+
setIsModalOpen(true);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const closeModal = () => {
|
|
25
|
+
setIsModalOpen(false);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const value = {
|
|
29
|
+
selectedItem,
|
|
30
|
+
isModalOpen,
|
|
31
|
+
openModal,
|
|
32
|
+
closeModal
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<ItemModalContext.Provider value={value}>
|
|
37
|
+
{children}
|
|
38
|
+
</ItemModalContext.Provider>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// src/hooks/useScrollLock.js
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
|
|
4
|
+
const useScrollLock = (isOpen) => {
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
if (isOpen) {
|
|
7
|
+
// Store original styles
|
|
8
|
+
const originalStyle = window.getComputedStyle(document.body);
|
|
9
|
+
const originalOverflow = originalStyle.overflow;
|
|
10
|
+
const originalPosition = originalStyle.position;
|
|
11
|
+
|
|
12
|
+
// Get current scroll position
|
|
13
|
+
const scrollY = window.scrollY;
|
|
14
|
+
|
|
15
|
+
// Apply scroll lock with Mobile Safari fixes
|
|
16
|
+
document.body.style.overflow = 'hidden';
|
|
17
|
+
document.body.style.position = 'fixed';
|
|
18
|
+
document.body.style.top = `-${scrollY}px`;
|
|
19
|
+
document.body.style.left = '0';
|
|
20
|
+
document.body.style.right = '0';
|
|
21
|
+
document.body.style.width = '100%';
|
|
22
|
+
|
|
23
|
+
// Mobile Safari specific: prevent viewport changes
|
|
24
|
+
const viewport = document.querySelector('meta[name="viewport"]');
|
|
25
|
+
const originalViewport = viewport?.content;
|
|
26
|
+
if (viewport) {
|
|
27
|
+
viewport.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Cleanup function
|
|
31
|
+
return () => {
|
|
32
|
+
// Restore original styles
|
|
33
|
+
document.body.style.overflow = originalOverflow;
|
|
34
|
+
document.body.style.position = originalPosition;
|
|
35
|
+
document.body.style.top = '';
|
|
36
|
+
document.body.style.left = '';
|
|
37
|
+
document.body.style.right = '';
|
|
38
|
+
document.body.style.width = '';
|
|
39
|
+
|
|
40
|
+
// Restore scroll position
|
|
41
|
+
window.scrollTo(0, scrollY);
|
|
42
|
+
|
|
43
|
+
// Restore viewport
|
|
44
|
+
if (viewport && originalViewport) {
|
|
45
|
+
viewport.content = originalViewport;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}, [isOpen]);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export default useScrollLock;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// src/services/emailService.js
|
|
2
|
+
import emailjs from '@emailjs/browser';
|
|
3
|
+
|
|
4
|
+
// EmailJS Configuration
|
|
5
|
+
const EMAILJS_CONFIG = {
|
|
6
|
+
serviceId: 'service_w3pkjf6',
|
|
7
|
+
templateId: 'template_lwf20nq',
|
|
8
|
+
publicKey: 'Sgy1Do0paUmA8QyPR'
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// Rate limiting configuration
|
|
12
|
+
const RATE_LIMIT = {
|
|
13
|
+
maxAttempts: 3,
|
|
14
|
+
timeWindow: 60000, // 1 minute in milliseconds
|
|
15
|
+
storageKey: 'emailjs_rate_limit'
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Initialize EmailJS
|
|
19
|
+
emailjs.init(EMAILJS_CONFIG.publicKey);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check rate limiting
|
|
23
|
+
* @returns {boolean} - true if rate limit exceeded, false otherwise
|
|
24
|
+
*/
|
|
25
|
+
const checkRateLimit = () => {
|
|
26
|
+
try {
|
|
27
|
+
const rateLimitData = localStorage.getItem(RATE_LIMIT.storageKey);
|
|
28
|
+
|
|
29
|
+
if (!rateLimitData) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const { attempts, timestamp } = JSON.parse(rateLimitData);
|
|
34
|
+
const now = Date.now();
|
|
35
|
+
|
|
36
|
+
// Reset if time window has passed
|
|
37
|
+
if (now - timestamp > RATE_LIMIT.timeWindow) {
|
|
38
|
+
localStorage.removeItem(RATE_LIMIT.storageKey);
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Check if rate limit exceeded
|
|
43
|
+
if (attempts >= RATE_LIMIT.maxAttempts) {
|
|
44
|
+
const timeLeft = Math.ceil((RATE_LIMIT.timeWindow - (now - timestamp)) / 1000);
|
|
45
|
+
throw new Error(`יותר מדי ניסיונות. אנא נסה שוב בעוד ${timeLeft} שניות.`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return false;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (error.message.includes('יותר מדי ניסיונות')) {
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
// If there's an error reading from localStorage, allow the request
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Update rate limit counter
|
|
60
|
+
*/
|
|
61
|
+
const updateRateLimit = () => {
|
|
62
|
+
try {
|
|
63
|
+
const rateLimitData = localStorage.getItem(RATE_LIMIT.storageKey);
|
|
64
|
+
const now = Date.now();
|
|
65
|
+
|
|
66
|
+
if (!rateLimitData) {
|
|
67
|
+
localStorage.setItem(RATE_LIMIT.storageKey, JSON.stringify({
|
|
68
|
+
attempts: 1,
|
|
69
|
+
timestamp: now
|
|
70
|
+
}));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const { attempts, timestamp } = JSON.parse(rateLimitData);
|
|
75
|
+
|
|
76
|
+
// Reset if time window has passed
|
|
77
|
+
if (now - timestamp > RATE_LIMIT.timeWindow) {
|
|
78
|
+
localStorage.setItem(RATE_LIMIT.storageKey, JSON.stringify({
|
|
79
|
+
attempts: 1,
|
|
80
|
+
timestamp: now
|
|
81
|
+
}));
|
|
82
|
+
} else {
|
|
83
|
+
// Increment attempts
|
|
84
|
+
localStorage.setItem(RATE_LIMIT.storageKey, JSON.stringify({
|
|
85
|
+
attempts: attempts + 1,
|
|
86
|
+
timestamp
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.error('Error updating rate limit:', error);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Send email using EmailJS
|
|
96
|
+
* @param {Object} params - Email parameters
|
|
97
|
+
* @param {string} params.name - Sender's name
|
|
98
|
+
* @param {string} params.email - Sender's email
|
|
99
|
+
* @param {string} params.phone - Sender's phone
|
|
100
|
+
* @param {string} params.message - Email message
|
|
101
|
+
* @returns {Promise<Object>} - EmailJS response
|
|
102
|
+
*/
|
|
103
|
+
export const sendEmail = async ({ name, email, phone, business, message }) => {
|
|
104
|
+
try {
|
|
105
|
+
// Check rate limiting
|
|
106
|
+
checkRateLimit();
|
|
107
|
+
|
|
108
|
+
// Prepare template parameters matching your EmailJS template
|
|
109
|
+
const templateParams = {
|
|
110
|
+
from_name: name,
|
|
111
|
+
from_email: email,
|
|
112
|
+
from_phone: phone,
|
|
113
|
+
business: business,
|
|
114
|
+
message: message,
|
|
115
|
+
timestamp: new Date().toLocaleString('he-IL', {
|
|
116
|
+
timeZone: 'Asia/Jerusalem',
|
|
117
|
+
dateStyle: 'full',
|
|
118
|
+
timeStyle: 'short'
|
|
119
|
+
})
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// Send email using EmailJS
|
|
123
|
+
const response = await emailjs.send(
|
|
124
|
+
EMAILJS_CONFIG.serviceId,
|
|
125
|
+
EMAILJS_CONFIG.templateId,
|
|
126
|
+
templateParams,
|
|
127
|
+
EMAILJS_CONFIG.publicKey
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// Update rate limit counter on successful send
|
|
131
|
+
updateRateLimit();
|
|
132
|
+
|
|
133
|
+
console.log('Email sent successfully:', response);
|
|
134
|
+
return {
|
|
135
|
+
success: true,
|
|
136
|
+
response
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
} catch (error) {
|
|
140
|
+
console.error('Error sending email:', error);
|
|
141
|
+
|
|
142
|
+
// Handle specific error types
|
|
143
|
+
if (error.message.includes('יותר מדי ניסיונות')) {
|
|
144
|
+
throw new Error(error.message);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (error.text) {
|
|
148
|
+
throw new Error(`שגיאה בשליחת האימייל: ${error.text}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
throw new Error('שגיאה בשליחת האימייל. אנא נסה שוב מאוחר יותר.');
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Clear rate limit (useful for testing or admin purposes)
|
|
157
|
+
*/
|
|
158
|
+
export const clearRateLimit = () => {
|
|
159
|
+
try {
|
|
160
|
+
localStorage.removeItem(RATE_LIMIT.storageKey);
|
|
161
|
+
console.log('Rate limit cleared');
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.error('Error clearing rate limit:', error);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export default sendEmail;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/* Glass Card - Glassmorphism Effect */
|
|
2
|
+
.glass-card {
|
|
3
|
+
@apply rounded-xl backdrop-blur-md;
|
|
4
|
+
background-color: var(--card-bg);
|
|
5
|
+
background: color-mix(in srgb, var(--card-bg) 75%, transparent);
|
|
6
|
+
backdrop-filter: blur(10px);
|
|
7
|
+
box-shadow: 0 8px 32px 0 color-mix(in srgb, var(--primary) 20%, transparent);
|
|
8
|
+
border: 1px solid color-mix(in srgb, var(--card-bg) 30%, transparent);
|
|
9
|
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.glass-button {
|
|
13
|
+
background: rgba(255, 107, 53, 0.2);
|
|
14
|
+
backdrop-filter: blur(10px);
|
|
15
|
+
border: 1px solid rgba(255, 107, 53, 0.3);
|
|
16
|
+
transition: all 0.3s ease;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.glass-button:hover {
|
|
20
|
+
background: rgba(255, 107, 53, 0.3);
|
|
21
|
+
transform: translateY(-2px);
|
|
22
|
+
box-shadow: 0 12px 24px rgba(255, 107, 53, 0.2);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.glass-card:hover {
|
|
26
|
+
background: color-mix(in srgb, var(--card-bg) 55%, transparent);
|
|
27
|
+
box-shadow: 0 12px 40px 0 color-mix(in srgb, var(--primary) 30%, transparent);
|
|
28
|
+
transform: translateY(-2px);
|
|
29
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Custom hook for smooth scrolling to anchors
|
|
5
|
+
* @param {Object} options - Configuration options
|
|
6
|
+
* @param {string} options.behavior - Scroll behavior ('smooth', 'instant', 'auto')
|
|
7
|
+
* @param {string} options.block - Vertical alignment ('start', 'center', 'end', 'nearest')
|
|
8
|
+
* @param {number} options.offset - Additional offset in pixels (useful for fixed headers)
|
|
9
|
+
* @returns {Function} scrollToAnchor function
|
|
10
|
+
*/
|
|
11
|
+
export const useScrollToAnchor = (options = {}) => {
|
|
12
|
+
const {
|
|
13
|
+
behavior = 'smooth',
|
|
14
|
+
block = 'start',
|
|
15
|
+
offset = 0
|
|
16
|
+
} = options;
|
|
17
|
+
|
|
18
|
+
const scrollToAnchor = useCallback((anchorId) => {
|
|
19
|
+
// Remove # if provided
|
|
20
|
+
const cleanAnchorId = anchorId.replace('#', '');
|
|
21
|
+
|
|
22
|
+
// Find the element
|
|
23
|
+
const element = document.getElementById(cleanAnchorId);
|
|
24
|
+
|
|
25
|
+
if (!element) {
|
|
26
|
+
console.warn(`Element with id "${cleanAnchorId}" not found`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// If offset is needed, calculate position manually
|
|
31
|
+
if (offset !== 0) {
|
|
32
|
+
const elementPosition = element.getBoundingClientRect().top;
|
|
33
|
+
const offsetPosition = elementPosition + window.pageYOffset - offset;
|
|
34
|
+
|
|
35
|
+
window.scrollTo({
|
|
36
|
+
top: offsetPosition,
|
|
37
|
+
behavior: behavior
|
|
38
|
+
});
|
|
39
|
+
} else {
|
|
40
|
+
// Use native scrollIntoView
|
|
41
|
+
element.scrollIntoView({
|
|
42
|
+
behavior: behavior,
|
|
43
|
+
block: block,
|
|
44
|
+
inline: 'nearest'
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}, [behavior, block, offset]);
|
|
48
|
+
|
|
49
|
+
return scrollToAnchor;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Alternative hook that also handles URL hash updates
|
|
53
|
+
export const useScrollToAnchorWithHash = (options = {}) => {
|
|
54
|
+
const scrollToAnchor = useScrollToAnchor(options);
|
|
55
|
+
|
|
56
|
+
const scrollToAnchorWithHash = useCallback((anchorId) => {
|
|
57
|
+
const cleanAnchorId = anchorId.replace('#', '');
|
|
58
|
+
|
|
59
|
+
// Update URL hash without triggering page reload
|
|
60
|
+
history.pushState(null, null, `#${cleanAnchorId}`);
|
|
61
|
+
|
|
62
|
+
// Scroll to the element
|
|
63
|
+
scrollToAnchor(cleanAnchorId);
|
|
64
|
+
}, [scrollToAnchor]);
|
|
65
|
+
|
|
66
|
+
return scrollToAnchorWithHash;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Hook that automatically scrolls to hash on page load
|
|
70
|
+
export const useScrollToHashOnLoad = (options = {}) => {
|
|
71
|
+
const scrollToAnchor = useScrollToAnchor(options);
|
|
72
|
+
|
|
73
|
+
React.useEffect(() => {
|
|
74
|
+
// Check if there's a hash in the URL when component mounts
|
|
75
|
+
const hash = window.location.hash;
|
|
76
|
+
if (hash) {
|
|
77
|
+
// Small delay to ensure page is fully loaded
|
|
78
|
+
setTimeout(() => {
|
|
79
|
+
scrollToAnchor(hash);
|
|
80
|
+
}, 100);
|
|
81
|
+
}
|
|
82
|
+
}, [scrollToAnchor]);
|
|
83
|
+
|
|
84
|
+
return scrollToAnchor;
|
|
85
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// ScrollToTop.jsx
|
|
2
|
+
import { useLayoutEffect } from 'react';
|
|
3
|
+
import { useLocation } from 'react-router-dom';
|
|
4
|
+
|
|
5
|
+
export default function ScrollToTop({ behavior = 'auto' }) {
|
|
6
|
+
const { pathname } = useLocation();
|
|
7
|
+
|
|
8
|
+
// useLayoutEffect prevents visible jump because it runs before paint
|
|
9
|
+
useLayoutEffect(() => {
|
|
10
|
+
window.scrollTo({ top: 0, left: 0, behavior });
|
|
11
|
+
}, [pathname, behavior]);
|
|
12
|
+
|
|
13
|
+
return null;
|
|
14
|
+
}
|