@codesinger0/shared-components 1.0.46 → 1.0.48
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/Menu.jsx +224 -0
- package/dist/index.js +4 -1
- package/dist/utils/ScrollToTop.jsx +14 -0
- package/package.json +3 -1
- package/dist/hooks/useFirestoreCollection.js +0 -243
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
3
|
+
import { Link } from 'react-router-dom';
|
|
4
|
+
import { useMediaQuery } from "react-responsive";
|
|
5
|
+
import useScrollLock from '../hooks/useScrollLock';
|
|
6
|
+
|
|
7
|
+
const Menu = ({
|
|
8
|
+
// Business info
|
|
9
|
+
businessInfo = {},
|
|
10
|
+
|
|
11
|
+
// Navigation items
|
|
12
|
+
navigationItems = [],
|
|
13
|
+
|
|
14
|
+
// Auth/Action buttons component
|
|
15
|
+
AuthButtonsComponent = null,
|
|
16
|
+
|
|
17
|
+
// User context (for admin check)
|
|
18
|
+
isAdmin = false,
|
|
19
|
+
|
|
20
|
+
// Customization
|
|
21
|
+
mobileBreakpoint = 900,
|
|
22
|
+
logoClassName = '',
|
|
23
|
+
menuItemClassName = '',
|
|
24
|
+
sidebarWidth = 'w-80',
|
|
25
|
+
|
|
26
|
+
// Callbacks
|
|
27
|
+
onMenuItemClick = () => {},
|
|
28
|
+
}) => {
|
|
29
|
+
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
|
30
|
+
const isMobile = useMediaQuery({ maxWidth: mobileBreakpoint });
|
|
31
|
+
|
|
32
|
+
// Close sidebar when clicking outside
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const handleClickOutside = (event) => {
|
|
35
|
+
if (isSidebarOpen && !event.target.closest('.sidebar') && !event.target.closest('.hamburger')) {
|
|
36
|
+
setIsSidebarOpen(false);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
if (isSidebarOpen) {
|
|
41
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return () => {
|
|
45
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
46
|
+
};
|
|
47
|
+
}, [isSidebarOpen]);
|
|
48
|
+
|
|
49
|
+
useScrollLock(isSidebarOpen && isMobile);
|
|
50
|
+
|
|
51
|
+
const toggleSidebar = () => {
|
|
52
|
+
setIsSidebarOpen(!isSidebarOpen);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const handleMenuItemClick = (item) => {
|
|
56
|
+
setIsSidebarOpen(false);
|
|
57
|
+
onMenuItemClick(item);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Desktop Menu Component
|
|
61
|
+
const DesktopMenu = () => (
|
|
62
|
+
<nav className="sticky top-0 z-50 bg-menu py-4 border-b-2 border-primary">
|
|
63
|
+
<div className="w-full px-6 flex justify-between items-center">
|
|
64
|
+
{/* Left side - Auth buttons */}
|
|
65
|
+
<div className="flex items-center">
|
|
66
|
+
{AuthButtonsComponent && <AuthButtonsComponent />}
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
{/* Right side - Menu items and logo */}
|
|
70
|
+
<div className="flex items-center space-x-8 space-x-reverse">
|
|
71
|
+
{/* Menu items */}
|
|
72
|
+
<div className="flex items-center space-x-6 space-x-reverse mr-8" dir="rtl">
|
|
73
|
+
{navigationItems.map((item, index) => (
|
|
74
|
+
item.adminRoute && !isAdmin ? null : (
|
|
75
|
+
<Link
|
|
76
|
+
key={index}
|
|
77
|
+
to={item.href}
|
|
78
|
+
onClick={() => handleMenuItemClick(item)}
|
|
79
|
+
className={`subtitle font-normal cursor-pointer menu-item-hover px-4 py-2 rounded-md text transition-colors duration-500 ease-in-out hover:text-primary ${menuItemClassName}`}
|
|
80
|
+
>
|
|
81
|
+
{item.label}
|
|
82
|
+
</Link>
|
|
83
|
+
)
|
|
84
|
+
))}
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
{/* Desktop Logo */}
|
|
88
|
+
<Link to="/" className={`text-primary font-bold text-xl ml-8 ${logoClassName}`}>
|
|
89
|
+
<div className="text-primary font-bold text-xl ml-8">
|
|
90
|
+
<div className="relative w-24 h-12 lg:w-32 lg:h-16 overflow-hidden flex items-center justify-center">
|
|
91
|
+
<img
|
|
92
|
+
src={businessInfo.logo}
|
|
93
|
+
alt={businessInfo.name}
|
|
94
|
+
className="relative z-10 w-full h-full object-contain"
|
|
95
|
+
onError={(e) => {
|
|
96
|
+
e.target.style.display = 'none';
|
|
97
|
+
e.target.nextElementSibling.style.display = 'flex';
|
|
98
|
+
}}
|
|
99
|
+
/>
|
|
100
|
+
{/* Fallback text */}
|
|
101
|
+
<div
|
|
102
|
+
className="relative z-10 w-full h-full flex items-center justify-center text-primary text-xs font-bold"
|
|
103
|
+
style={{ display: 'none' }}
|
|
104
|
+
>
|
|
105
|
+
{businessInfo.name?.slice(0, 2) || ''}
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</Link>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
</nav>
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// Mobile Menu Component
|
|
116
|
+
const MobileMenu = () => (
|
|
117
|
+
<>
|
|
118
|
+
<nav className="sticky top-0 z-50 bg-menu px-4 py-2">
|
|
119
|
+
<div className="flex justify-between items-center">
|
|
120
|
+
{AuthButtonsComponent && <AuthButtonsComponent isMobile={true} />}
|
|
121
|
+
|
|
122
|
+
{/* Mobile Logo */}
|
|
123
|
+
<Link to="/" className={`text-primary font-bold text-xl ${logoClassName}`}>
|
|
124
|
+
<div className="text-primary font-bold text-lg">
|
|
125
|
+
<div className="w-25 h-12 overflow-hidden flex items-center justify-center">
|
|
126
|
+
<img
|
|
127
|
+
src={businessInfo.logo}
|
|
128
|
+
alt={businessInfo.name}
|
|
129
|
+
className="w-full h-full object-contain"
|
|
130
|
+
onError={(e) => {
|
|
131
|
+
e.target.style.display = 'none';
|
|
132
|
+
e.target.nextElementSibling.style.display = 'flex';
|
|
133
|
+
}}
|
|
134
|
+
/>
|
|
135
|
+
{/* Fallback text */}
|
|
136
|
+
<div
|
|
137
|
+
className="w-full h-full flex items-center justify-center text-primary text-xs font-bold"
|
|
138
|
+
style={{ display: 'none' }}
|
|
139
|
+
>
|
|
140
|
+
{businessInfo.name?.slice(0, 2) || ''}
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</Link>
|
|
145
|
+
|
|
146
|
+
{/* Hamburger button */}
|
|
147
|
+
<button
|
|
148
|
+
className="hamburger flex flex-col justify-center items-center w-8 h-8 focus:outline-none text-main"
|
|
149
|
+
onClick={toggleSidebar}
|
|
150
|
+
aria-label="תפריט"
|
|
151
|
+
>
|
|
152
|
+
<span className={`hamburger-line block w-6 h-0.5 mb-1 ${isSidebarOpen ? 'rotate-45 translate-y-1.5' : ''}`} style={{ backgroundColor: 'var(--text)' }}></span>
|
|
153
|
+
<span className={`hamburger-line block w-6 h-0.5 mb-1 ${isSidebarOpen ? 'opacity-0' : ''}`} style={{ backgroundColor: 'var(--text)' }}></span>
|
|
154
|
+
<span className={`hamburger-line block w-6 h-0.5 ${isSidebarOpen ? '-rotate-45 -translate-y-1.5' : ''}`} style={{ backgroundColor: 'var(--text)' }}></span>
|
|
155
|
+
</button>
|
|
156
|
+
</div>
|
|
157
|
+
</nav>
|
|
158
|
+
|
|
159
|
+
{/* Overlay + Sidebar (animated) */}
|
|
160
|
+
<AnimatePresence>
|
|
161
|
+
{isSidebarOpen && (
|
|
162
|
+
<>
|
|
163
|
+
{/* Overlay fade */}
|
|
164
|
+
<motion.div
|
|
165
|
+
key="overlay"
|
|
166
|
+
initial={{ opacity: 0 }}
|
|
167
|
+
animate={{ opacity: 0.5 }}
|
|
168
|
+
exit={{ opacity: 0 }}
|
|
169
|
+
transition={{ duration: 0.2 }}
|
|
170
|
+
className="fixed inset-0 bg-black z-40 supports-[height:100dvh]:h-[100dvh]"
|
|
171
|
+
onClick={() => setIsSidebarOpen(false)}
|
|
172
|
+
aria-hidden="true"
|
|
173
|
+
/>
|
|
174
|
+
|
|
175
|
+
{/* Sidebar spring slide */}
|
|
176
|
+
<motion.aside
|
|
177
|
+
key="sidebar"
|
|
178
|
+
initial={{ x: '100%' }}
|
|
179
|
+
animate={{ x: 0 }}
|
|
180
|
+
exit={{ x: '100%' }}
|
|
181
|
+
transition={{ type: 'spring', stiffness: 320, damping: 28, mass: 0.9 }}
|
|
182
|
+
className={`sidebar fixed top-0 right-0 h-full ${sidebarWidth} bg-white shadow-lg z-50`}
|
|
183
|
+
role="dialog"
|
|
184
|
+
aria-label="תפריט צד"
|
|
185
|
+
>
|
|
186
|
+
<div className="p-6">
|
|
187
|
+
{/* Close button */}
|
|
188
|
+
<div className="flex justify-start mb-8">
|
|
189
|
+
<button
|
|
190
|
+
onClick={() => setIsSidebarOpen(false)}
|
|
191
|
+
className="text-black hover:text-gray-700 text-2xl"
|
|
192
|
+
aria-label="סגור תפריט"
|
|
193
|
+
>
|
|
194
|
+
×
|
|
195
|
+
</button>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
{/* Menu items */}
|
|
199
|
+
<div className="flex flex-col space-y-3">
|
|
200
|
+
{navigationItems.map((item, index) => (
|
|
201
|
+
item.adminRoute && !isAdmin ? null : (
|
|
202
|
+
<Link
|
|
203
|
+
key={index}
|
|
204
|
+
to={item.href}
|
|
205
|
+
onClick={() => handleMenuItemClick(item)}
|
|
206
|
+
className={`text-lg py-3 px-2 text-black text-right ${menuItemClassName}`}
|
|
207
|
+
>
|
|
208
|
+
{item.label}
|
|
209
|
+
</Link>
|
|
210
|
+
)
|
|
211
|
+
))}
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
</motion.aside>
|
|
215
|
+
</>
|
|
216
|
+
)}
|
|
217
|
+
</AnimatePresence>
|
|
218
|
+
</>
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
return isMobile ? <MobileMenu /> : <DesktopMenu />;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
export default Menu;
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ export { default as QAAccordion } from './components/QAAccordion'
|
|
|
9
9
|
export { default as AdvantagesList } from './components/AdvantagesList'
|
|
10
10
|
export { default as ShoppingCartModal } from './components/cart/ShoppingCartModal'
|
|
11
11
|
export { default as FloatingCartButton } from './components/cart/FloatingCartButton'
|
|
12
|
+
export { default as Menu } from './components/Menu'
|
|
12
13
|
|
|
13
14
|
// Modals
|
|
14
15
|
export { default as ItemDetailsModal } from './components/modals/ItemDetailsModal'
|
|
@@ -20,4 +21,6 @@ export { CartProvider, useCart } from './context/CartContext'
|
|
|
20
21
|
|
|
21
22
|
// Hooks
|
|
22
23
|
export { default as useScrollLock } from './hooks/useScrollLock'
|
|
23
|
-
|
|
24
|
+
|
|
25
|
+
// Utils
|
|
26
|
+
export { default as ScrollToTop } from './utils/ScrollToTop'
|
|
@@ -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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codesinger0/shared-components",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.48",
|
|
4
4
|
"description": "Shared React components for customer projects",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"files": [
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
"embla-carousel-react": ">=8.6.0",
|
|
15
15
|
"framer-motion": ">=12.23.12",
|
|
16
16
|
"lucide-react": ">=0.263.1",
|
|
17
|
+
"react-router-dom": ">=7.9.1",
|
|
18
|
+
"react-responsive": ">=10.0.1",
|
|
17
19
|
"react": ">=18.0.0"
|
|
18
20
|
},
|
|
19
21
|
"repository": {
|
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
collection,
|
|
4
|
-
doc,
|
|
5
|
-
getDocs,
|
|
6
|
-
getDoc,
|
|
7
|
-
addDoc,
|
|
8
|
-
setDoc,
|
|
9
|
-
updateDoc,
|
|
10
|
-
deleteDoc,
|
|
11
|
-
orderBy,
|
|
12
|
-
query,
|
|
13
|
-
serverTimestamp
|
|
14
|
-
} from 'firebase/firestore';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Generic Firestore Collection Hook
|
|
18
|
-
*
|
|
19
|
-
* @param {string} collectionName - Name of the Firestore collection
|
|
20
|
-
* @param {Function} ModelClass - Constructor function for the model class
|
|
21
|
-
* @param {Object} options - Configuration options
|
|
22
|
-
* @param {string} options.defaultSortField - Default field to sort by (default: 'createdAt')
|
|
23
|
-
* @param {string} options.defaultSortOrder - Default sort order ('asc' or 'desc', default: 'desc')
|
|
24
|
-
* @param {Object} options.errorMessages - Custom error messages for operations
|
|
25
|
-
* @returns {Object} Hook state and methods
|
|
26
|
-
*/
|
|
27
|
-
export const useFirestoreCollection = (
|
|
28
|
-
collectionName,
|
|
29
|
-
ModelClass,
|
|
30
|
-
options = {},
|
|
31
|
-
db
|
|
32
|
-
) => {
|
|
33
|
-
const {
|
|
34
|
-
defaultSortField = 'createdAt',
|
|
35
|
-
defaultSortOrder = 'desc',
|
|
36
|
-
errorMessages = {}
|
|
37
|
-
} = options;
|
|
38
|
-
|
|
39
|
-
// Default error messages with option to override
|
|
40
|
-
const defaultErrorMessages = {
|
|
41
|
-
get: 'קבלת נתונים',
|
|
42
|
-
getById: 'קבלת פריט',
|
|
43
|
-
create: 'יצירת פריט',
|
|
44
|
-
update: 'עדכון פריט',
|
|
45
|
-
delete: 'מחיקת פריט',
|
|
46
|
-
notFound: 'פריט לא נמצא',
|
|
47
|
-
...errorMessages
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const [items, setItems] = useState([]);
|
|
51
|
-
const [loading, setLoading] = useState(false);
|
|
52
|
-
const [error, setError] = useState(null);
|
|
53
|
-
|
|
54
|
-
// Helper function to clear error
|
|
55
|
-
const clearError = () => setError(null);
|
|
56
|
-
|
|
57
|
-
// Helper function to handle errors
|
|
58
|
-
const handleError = (error, operation) => {
|
|
59
|
-
console.error(`Error in ${operation}:`, error);
|
|
60
|
-
setError(`שגיאה ב${defaultErrorMessages[operation] || operation}: ${error.message}`);
|
|
61
|
-
setLoading(false);
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
// Get all items
|
|
65
|
-
const getItems = async (sortField = defaultSortField, sortOrder = defaultSortOrder) => {
|
|
66
|
-
setLoading(true);
|
|
67
|
-
clearError();
|
|
68
|
-
|
|
69
|
-
try {
|
|
70
|
-
const q = query(
|
|
71
|
-
collection(db, collectionName),
|
|
72
|
-
orderBy(sortField, sortOrder)
|
|
73
|
-
);
|
|
74
|
-
const querySnapshot = await getDocs(q);
|
|
75
|
-
|
|
76
|
-
const itemsData = [];
|
|
77
|
-
querySnapshot.forEach((doc) => {
|
|
78
|
-
itemsData.push(new ModelClass({
|
|
79
|
-
...doc.data(),
|
|
80
|
-
id: doc.id
|
|
81
|
-
}));
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
setItems(itemsData);
|
|
85
|
-
setLoading(false);
|
|
86
|
-
return itemsData;
|
|
87
|
-
} catch (error) {
|
|
88
|
-
handleError(error, 'get');
|
|
89
|
-
return [];
|
|
90
|
-
}
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
// Get single item by ID
|
|
94
|
-
const getItem = async (itemId) => {
|
|
95
|
-
setLoading(true);
|
|
96
|
-
clearError();
|
|
97
|
-
|
|
98
|
-
try {
|
|
99
|
-
const docRef = doc(db, collectionName, itemId);
|
|
100
|
-
const docSnap = await getDoc(docRef);
|
|
101
|
-
|
|
102
|
-
if (docSnap.exists()) {
|
|
103
|
-
const itemData = new ModelClass({
|
|
104
|
-
...docSnap.data(),
|
|
105
|
-
id: docSnap.id
|
|
106
|
-
});
|
|
107
|
-
setLoading(false);
|
|
108
|
-
return itemData;
|
|
109
|
-
} else {
|
|
110
|
-
const errorMsg = defaultErrorMessages.notFound;
|
|
111
|
-
setError(errorMsg);
|
|
112
|
-
setLoading(false);
|
|
113
|
-
throw new Error(errorMsg);
|
|
114
|
-
}
|
|
115
|
-
} catch (error) {
|
|
116
|
-
handleError(error, 'getById');
|
|
117
|
-
return null;
|
|
118
|
-
}
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
// Create new item
|
|
122
|
-
const createItem = async (itemData, customId = null) => {
|
|
123
|
-
setLoading(true);
|
|
124
|
-
clearError();
|
|
125
|
-
|
|
126
|
-
try {
|
|
127
|
-
// Create Model instance and validate
|
|
128
|
-
const item = new ModelClass(itemData);
|
|
129
|
-
const validation = item.validate();
|
|
130
|
-
|
|
131
|
-
if (!validation.isValid) {
|
|
132
|
-
throw new Error(validation.errors.join(', '));
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Prepare item data with timestamps
|
|
136
|
-
const firestoreData = {
|
|
137
|
-
...item.toFirestore(),
|
|
138
|
-
createdAt: serverTimestamp(),
|
|
139
|
-
updatedAt: serverTimestamp()
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
let docRef;
|
|
143
|
-
if (customId) {
|
|
144
|
-
// Use custom ID
|
|
145
|
-
docRef = doc(db, collectionName, customId);
|
|
146
|
-
await setDoc(docRef, firestoreData);
|
|
147
|
-
} else {
|
|
148
|
-
// Auto-generate ID
|
|
149
|
-
docRef = await addDoc(collection(db, collectionName), firestoreData);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Return Model instance
|
|
153
|
-
const newItem = new ModelClass({
|
|
154
|
-
id: docRef.id,
|
|
155
|
-
...item.toFirestore(),
|
|
156
|
-
createdAt: firestoreData.createdAt,
|
|
157
|
-
updatedAt: firestoreData.updatedAt
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
setLoading(false);
|
|
161
|
-
return newItem;
|
|
162
|
-
} catch (error) {
|
|
163
|
-
handleError(error, 'create');
|
|
164
|
-
return null;
|
|
165
|
-
}
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
const updateItem = async (itemId, updates) => {
|
|
169
|
-
setLoading(true);
|
|
170
|
-
clearError();
|
|
171
|
-
|
|
172
|
-
try {
|
|
173
|
-
// Get existing document first
|
|
174
|
-
const docRef = doc(db, collectionName, itemId);
|
|
175
|
-
const existingDoc = await getDoc(docRef);
|
|
176
|
-
|
|
177
|
-
if (!existingDoc.exists()) {
|
|
178
|
-
throw new Error('Document not found');
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Merge existing data with updates
|
|
182
|
-
const existingData = existingDoc.data();
|
|
183
|
-
const mergedData = { ...existingData, ...updates, id: itemId };
|
|
184
|
-
|
|
185
|
-
// Create Model instance and validate
|
|
186
|
-
const item = new ModelClass(mergedData);
|
|
187
|
-
const validation = item.validate();
|
|
188
|
-
|
|
189
|
-
if (!validation.isValid) {
|
|
190
|
-
throw new Error(validation.errors.join(', '));
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Prepare updates with timestamp
|
|
194
|
-
const updateData = {
|
|
195
|
-
...item.toFirestore(),
|
|
196
|
-
updatedAt: serverTimestamp()
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
await updateDoc(docRef, updateData);
|
|
200
|
-
setLoading(false);
|
|
201
|
-
|
|
202
|
-
// Return Model instance
|
|
203
|
-
return new ModelClass({ id: itemId, ...updateData });
|
|
204
|
-
} catch (error) {
|
|
205
|
-
handleError(error, 'update');
|
|
206
|
-
return null;
|
|
207
|
-
}
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
// Delete item
|
|
211
|
-
const deleteItem = async (itemId) => {
|
|
212
|
-
setLoading(true);
|
|
213
|
-
clearError();
|
|
214
|
-
|
|
215
|
-
try {
|
|
216
|
-
const docRef = doc(db, collectionName, itemId);
|
|
217
|
-
await deleteDoc(docRef);
|
|
218
|
-
setLoading(false);
|
|
219
|
-
|
|
220
|
-
return true;
|
|
221
|
-
} catch (error) {
|
|
222
|
-
handleError(error, 'delete');
|
|
223
|
-
return false;
|
|
224
|
-
}
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
return {
|
|
228
|
-
// State
|
|
229
|
-
items,
|
|
230
|
-
loading,
|
|
231
|
-
error,
|
|
232
|
-
|
|
233
|
-
// Actions
|
|
234
|
-
getItems,
|
|
235
|
-
getItem,
|
|
236
|
-
createItem,
|
|
237
|
-
updateItem,
|
|
238
|
-
deleteItem,
|
|
239
|
-
|
|
240
|
-
// Utility
|
|
241
|
-
clearError
|
|
242
|
-
};
|
|
243
|
-
};
|