popsite-ui 1.0.0
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/App.jsx +95 -0
- package/README.md +92 -0
- package/components/layout/PortalHeader.jsx +18 -0
- package/components/layout/SystemSidebar.jsx +33 -0
- package/components/modules/AnalyticsDashboardModule.jsx +17 -0
- package/components/modules/ChatMessagingModule.jsx +17 -0
- package/components/modules/EcommerceStoreModule.jsx +17 -0
- package/components/modules/EventTicketBookingModule.jsx +17 -0
- package/components/modules/FlightBookingModule.jsx +17 -0
- package/components/modules/FoodOrderingModule.jsx +17 -0
- package/components/modules/HospitalAppointmentModule.jsx +17 -0
- package/components/modules/HotelBookingModule.jsx +17 -0
- package/components/modules/InvoiceBillingModule.jsx +17 -0
- package/components/modules/LibraryManagementModule.jsx +17 -0
- package/components/modules/ModuleContentDeck.jsx +44 -0
- package/components/modules/MovieBookingModule.jsx +17 -0
- package/components/modules/QuizExamModule.jsx +17 -0
- package/components/modules/StudentRegistrationModule.jsx +17 -0
- package/components/modules/SystemModuleRenderer.jsx +19 -0
- package/components/modules/SystemModuleTemplate.jsx +62 -0
- package/components/modules/SystemVisualWidget.jsx +123 -0
- package/components/modules/moduleContentMap.js +238 -0
- package/components/modules/moduleEnhancementsMap.js +439 -0
- package/components/modules/systemModuleMap.js +31 -0
- package/components/system/DynamicSystemForm.jsx +154 -0
- package/components/system/SystemHero.jsx +21 -0
- package/components/system/SystemSummaryCard.jsx +53 -0
- package/data/systems/analyticsDashboard.js +48 -0
- package/data/systems/chatMessaging.js +43 -0
- package/data/systems/ecommerceStore.js +50 -0
- package/data/systems/eventTicketBooking.js +50 -0
- package/data/systems/flightBooking.js +38 -0
- package/data/systems/foodOrdering.js +48 -0
- package/data/systems/hospitalAppointment.js +50 -0
- package/data/systems/hotelBooking.js +38 -0
- package/data/systems/index.js +31 -0
- package/data/systems/invoiceBilling.js +50 -0
- package/data/systems/libraryManagement.js +43 -0
- package/data/systems/movieBooking.js +48 -0
- package/data/systems/quizExam.js +38 -0
- package/data/systems/studentRegistration.js +43 -0
- package/dist/popsite-ui.es.js +4368 -0
- package/dist/popsite-ui.umd.js +60 -0
- package/dist/style.css +1 -0
- package/index.html +13 -0
- package/library/index.js +20 -0
- package/main.jsx +15 -0
- package/package.json +40 -0
- package/src/App.jsx +12 -0
- package/src/components/modules/AnalyticsDashboardModule.jsx +224 -0
- package/src/components/modules/ChatMessagingModule.jsx +294 -0
- package/src/components/modules/EcommerceStoreModule.jsx +405 -0
- package/src/components/modules/EventTicketBookingModule.jsx +253 -0
- package/src/components/modules/FlightBookingModule.jsx +399 -0
- package/src/components/modules/FoodOrderingModule.jsx +316 -0
- package/src/components/modules/HospitalAppointmentModule.jsx +267 -0
- package/src/components/modules/HotelBookingModule.jsx +317 -0
- package/src/components/modules/InvoiceBillingModule.jsx +302 -0
- package/src/components/modules/LandingPageModule.jsx +185 -0
- package/src/components/modules/LibraryManagementModule.jsx +189 -0
- package/src/components/modules/MovieBookingModule.jsx +337 -0
- package/src/components/modules/QuizExamModule.jsx +255 -0
- package/src/components/modules/StudentRegistrationModule.jsx +292 -0
- package/src/components/system/SystemHero.jsx +44 -0
- package/src/components/system/SystemSummaryCard.jsx +29 -0
- package/src/components/system/Toast.jsx +69 -0
- package/src/data/systems/analyticsDashboard.js +32 -0
- package/src/data/systems/chatMessaging.js +59 -0
- package/src/data/systems/ecommerceStore.js +84 -0
- package/src/data/systems/eventBooking.js +33 -0
- package/src/data/systems/flightBooking.js +59 -0
- package/src/data/systems/foodOrdering.js +48 -0
- package/src/data/systems/hospitalAppointment.js +48 -0
- package/src/data/systems/hotelBooking.js +59 -0
- package/src/data/systems/invoiceBilling.js +19 -0
- package/src/data/systems/landingPage.js +29 -0
- package/src/data/systems/libraryManagement.js +17 -0
- package/src/data/systems/movieBooking.js +49 -0
- package/src/data/systems/quizExam.js +31 -0
- package/src/data/systems/studentRegistration.js +9 -0
- package/src/index.js +22 -0
- package/src/main.jsx +10 -0
- package/src/styles.css +296 -0
- package/styles.css +820 -0
- package/utils/systemEngine.js +128 -0
- package/vite.config.js +8 -0
- package/vite.lib.config.js +27 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { ecommerceMockData } from '../../data/systems/ecommerceStore';
|
|
3
|
+
import { useToast } from '../system/Toast';
|
|
4
|
+
|
|
5
|
+
export function EcommerceStoreModule() {
|
|
6
|
+
const [currentView, setCurrentView] = useState('grid'); // grid, details, checkout, success
|
|
7
|
+
const [selectedProduct, setSelectedProduct] = useState(null);
|
|
8
|
+
const [cart, setCart] = useState([]);
|
|
9
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
10
|
+
const [activeCategory, setActiveCategory] = useState('All');
|
|
11
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
12
|
+
|
|
13
|
+
const { showToast, ToastContainer } = useToast();
|
|
14
|
+
|
|
15
|
+
// Load cart from localStorage
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const savedCart = localStorage.getItem('popsite_cart');
|
|
18
|
+
if (savedCart) {
|
|
19
|
+
try {
|
|
20
|
+
setCart(JSON.parse(savedCart));
|
|
21
|
+
} catch (e) {
|
|
22
|
+
console.error('Failed to parse cart');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
// Save cart to localStorage
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
localStorage.setItem('popsite_cart', JSON.stringify(cart));
|
|
30
|
+
}, [cart]);
|
|
31
|
+
|
|
32
|
+
// Simulated Network Delay Utility
|
|
33
|
+
const withDelay = (callback, delay = 1500) => {
|
|
34
|
+
setIsLoading(true);
|
|
35
|
+
setTimeout(() => {
|
|
36
|
+
setIsLoading(false);
|
|
37
|
+
callback();
|
|
38
|
+
}, delay);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const handleProductClick = (product) => {
|
|
42
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
43
|
+
setSelectedProduct({ ...product, selectedSize: product.sizes[0] || null, selectedColor: product.colors[0] || null });
|
|
44
|
+
setCurrentView('details');
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const handleAddToCart = () => {
|
|
48
|
+
if (!selectedProduct.inStock) {
|
|
49
|
+
showToast('Product is currently out of stock', 'error');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check if exactly this item exists
|
|
54
|
+
const existingIndex = cart.findIndex(
|
|
55
|
+
item => item.id === selectedProduct.id && item.selectedSize === selectedProduct.selectedSize && item.selectedColor === selectedProduct.selectedColor
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const updatedCart = [...cart];
|
|
59
|
+
if (existingIndex >= 0) {
|
|
60
|
+
updatedCart[existingIndex].quantity += 1;
|
|
61
|
+
} else {
|
|
62
|
+
updatedCart.push({ ...selectedProduct, quantity: 1 });
|
|
63
|
+
}
|
|
64
|
+
setCart(updatedCart);
|
|
65
|
+
showToast(`${selectedProduct.name} added to cart!`, 'success');
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const handleRemoveFromCart = (index) => {
|
|
69
|
+
const updated = [...cart];
|
|
70
|
+
updated.splice(index, 1);
|
|
71
|
+
setCart(updated);
|
|
72
|
+
showToast('Item removed from cart', 'info');
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const handleCheckout = () => {
|
|
76
|
+
if (cart.length === 0) {
|
|
77
|
+
showToast('Your cart is empty', 'error');
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
withDelay(() => {
|
|
81
|
+
setCurrentView('success');
|
|
82
|
+
setCart([]);
|
|
83
|
+
showToast('Order placed successfully!', 'success');
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const handleExportCSV = () => {
|
|
88
|
+
const headers = "ID,Name,Category,Price,InStock,Rating\n";
|
|
89
|
+
const rows = ecommerceMockData.products.map(p =>
|
|
90
|
+
`${p.id},"${p.name}","${p.category}",${p.price},${p.inStock},${p.rating}`
|
|
91
|
+
).join("\n");
|
|
92
|
+
const blob = new Blob([headers + rows], { type: 'text/csv' });
|
|
93
|
+
const url = URL.createObjectURL(blob);
|
|
94
|
+
const a = document.createElement('a');
|
|
95
|
+
a.href = url;
|
|
96
|
+
a.download = `products_export_${Date.now()}.csv`;
|
|
97
|
+
a.click();
|
|
98
|
+
showToast('Catalog exported to CSV', 'success');
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const cartTotal = cart.reduce((total, item) => total + (item.price * item.quantity), 0);
|
|
102
|
+
|
|
103
|
+
// VIEWS
|
|
104
|
+
const renderGrid = () => {
|
|
105
|
+
const filteredProducts = ecommerceMockData.products.filter(p => {
|
|
106
|
+
const matchSearch = p.name.toLowerCase().includes(searchQuery.toLowerCase());
|
|
107
|
+
const matchCategory = activeCategory === 'All' || p.category === activeCategory;
|
|
108
|
+
return matchSearch && matchCategory;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<div className="pk-container animate-fade-in">
|
|
113
|
+
<div className="pk-flex pk-justify-between pk-items-center" style={{ marginBottom: '2rem', flexWrap: 'wrap', gap: '1rem' }}>
|
|
114
|
+
<div>
|
|
115
|
+
<h1 className="pk-heading-lg" style={{ marginBottom: '0.5rem' }}>{ecommerceMockData.storeName}</h1>
|
|
116
|
+
<p className="pk-text-body">Discover premium gear for modern living.</p>
|
|
117
|
+
</div>
|
|
118
|
+
<div className="pk-flex pk-items-center" style={{ gap: '1rem' }}>
|
|
119
|
+
<button className="pk-btn pk-btn-outline" onClick={handleExportCSV}>
|
|
120
|
+
⬇️ Export CSV
|
|
121
|
+
</button>
|
|
122
|
+
<button className="pk-btn pk-btn-primary" onClick={() => setCurrentView('checkout')}>
|
|
123
|
+
🛒 Cart ({cart.length})
|
|
124
|
+
</button>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<div className="pk-card pk-glass" style={{ padding: '1.5rem', marginBottom: '2rem', display: 'flex', gap: '1rem', flexWrap: 'wrap', alignItems: 'center' }}>
|
|
129
|
+
<div className="pk-input-group" style={{ margin: 0, flex: 1, minWidth: '250px' }}>
|
|
130
|
+
<input
|
|
131
|
+
type="text"
|
|
132
|
+
className="pk-input"
|
|
133
|
+
placeholder="Search products..."
|
|
134
|
+
value={searchQuery}
|
|
135
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
136
|
+
/>
|
|
137
|
+
</div>
|
|
138
|
+
<div className="pk-flex" style={{ gap: '0.5rem', overflowX: 'auto', paddingBottom: '4px' }}>
|
|
139
|
+
{ecommerceMockData.categories.map(cat => (
|
|
140
|
+
<button
|
|
141
|
+
key={cat}
|
|
142
|
+
className={activeCategory === cat ? 'pk-badge' : 'pk-btn pk-btn-outline'}
|
|
143
|
+
style={activeCategory !== cat ? { padding: '0.25rem 0.75rem', borderRadius: 'var(--pk-radius-full)', border: '1px solid var(--pk-border)' } : { cursor: 'pointer' }}
|
|
144
|
+
onClick={() => setActiveCategory(cat)}
|
|
145
|
+
>
|
|
146
|
+
{cat}
|
|
147
|
+
</button>
|
|
148
|
+
))}
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
{filteredProducts.length === 0 ? (
|
|
153
|
+
<div className="pk-empty-state">
|
|
154
|
+
<div className="pk-empty-icon">🏜️</div>
|
|
155
|
+
<h3 className="pk-heading-md">No products found</h3>
|
|
156
|
+
<p className="pk-text-body">Try adjusting your filters or search query.</p>
|
|
157
|
+
<button className="pk-btn pk-btn-secondary" style={{ marginTop: '1rem' }} onClick={() => { setSearchQuery(''); setActiveCategory('All'); }}>Clear Filters</button>
|
|
158
|
+
</div>
|
|
159
|
+
) : (
|
|
160
|
+
<div className="pk-grid pk-grid-responsive">
|
|
161
|
+
{filteredProducts.map(product => (
|
|
162
|
+
<div key={product.id} className="pk-card pk-card-interactive" onClick={() => handleProductClick(product)} style={{ cursor: 'pointer', display: 'flex', flexDirection: 'column' }}>
|
|
163
|
+
<div style={{ height: '250px', width: '100%', overflow: 'hidden', backgroundColor: 'var(--pk-primary-light)' }}>
|
|
164
|
+
<img
|
|
165
|
+
src={product.image}
|
|
166
|
+
alt={product.name}
|
|
167
|
+
className="pk-object-cover pk-w-full pk-h-full"
|
|
168
|
+
style={{ transition: 'transform 0.5s' }}
|
|
169
|
+
onError={(e) => {
|
|
170
|
+
e.target.style.display = 'none';
|
|
171
|
+
e.target.nextSibling.style.display = 'flex';
|
|
172
|
+
}}
|
|
173
|
+
/>
|
|
174
|
+
{/* Image Fallback */}
|
|
175
|
+
<div style={{ display: 'none', width: '100%', height: '100%', background: 'linear-gradient(135deg, var(--pk-primary), var(--pk-secondary))', color: 'white', alignItems: 'center', justifyContent: 'center', fontSize: '2rem', fontWeight: 'bold' }}>
|
|
176
|
+
{product.name.substring(0, 2).toUpperCase()}
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
<div className="pk-card-body" style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
|
|
180
|
+
<div className="pk-flex pk-justify-between pk-items-center" style={{ marginBottom: '0.5rem' }}>
|
|
181
|
+
<span className="pk-badge">{product.category}</span>
|
|
182
|
+
<span style={{ fontSize: '0.875rem', color: '#f59e0b', fontWeight: 600 }}>★ {product.rating}</span>
|
|
183
|
+
</div>
|
|
184
|
+
<h3 className="pk-heading-md" style={{ marginBottom: '0.5rem' }}>{product.name}</h3>
|
|
185
|
+
<div className="pk-text-body" style={{ display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden', marginBottom: '1rem', flex: 1 }}>{product.description}</div>
|
|
186
|
+
<div className="pk-flex pk-justify-between pk-items-center" style={{ marginTop: 'auto' }}>
|
|
187
|
+
<span className="pk-heading-md" style={{ color: 'var(--pk-primary)' }}>${product.price.toFixed(2)}</span>
|
|
188
|
+
{!product.inStock && <span className="pk-text-sm" style={{ color: 'var(--pk-danger)' }}>Out of Stock</span>}
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
))}
|
|
193
|
+
</div>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const renderDetails = () => {
|
|
200
|
+
if (!selectedProduct) return null;
|
|
201
|
+
const p = selectedProduct;
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<div className="pk-container animate-fade-in">
|
|
205
|
+
<button className="pk-btn pk-btn-outline" style={{ marginBottom: '2rem' }} onClick={() => setCurrentView('grid')}>
|
|
206
|
+
← Back to Catalog
|
|
207
|
+
</button>
|
|
208
|
+
|
|
209
|
+
<div className="pk-card" style={{ display: 'flex', flexWrap: 'wrap' }}>
|
|
210
|
+
<div style={{ flex: '1 1 400px', height: '500px', backgroundColor: 'var(--pk-primary-light)', padding: '2rem' }}>
|
|
211
|
+
<img
|
|
212
|
+
src={p.image}
|
|
213
|
+
alt={p.name}
|
|
214
|
+
className="pk-object-cover pk-w-full pk-h-full"
|
|
215
|
+
style={{ borderRadius: 'var(--pk-radius-lg)', boxShadow: 'var(--pk-shadow-lg)' }}
|
|
216
|
+
onError={(e) => {
|
|
217
|
+
e.target.style.display = 'none';
|
|
218
|
+
e.target.nextSibling.style.display = 'flex';
|
|
219
|
+
}}
|
|
220
|
+
/>
|
|
221
|
+
<div style={{ display: 'none', width: '100%', height: '100%', background: 'linear-gradient(135deg, var(--pk-primary), var(--pk-secondary))', color: 'white', alignItems: 'center', justifyContent: 'center', fontSize: '4rem', fontWeight: 'bold', borderRadius: 'var(--pk-radius-lg)', boxShadow: 'var(--pk-shadow-lg)' }}>
|
|
222
|
+
{p.name.substring(0, 2).toUpperCase()}
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
<div style={{ flex: '1 1 400px', padding: '3rem' }}>
|
|
227
|
+
<div className="pk-flex pk-items-center" style={{ gap: '1rem', marginBottom: '1rem' }}>
|
|
228
|
+
<span className="pk-badge">{p.category}</span>
|
|
229
|
+
<span style={{ fontSize: '1rem', color: '#f59e0b', fontWeight: 600 }}>★ {p.rating} ({p.reviews} reviews)</span>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
<h1 className="pk-heading-xl" style={{ marginBottom: '1rem' }}>{p.name}</h1>
|
|
233
|
+
<p className="pk-heading-lg" style={{ color: 'var(--pk-primary)', marginBottom: '1.5rem' }}>${p.price.toFixed(2)}</p>
|
|
234
|
+
|
|
235
|
+
<p className="pk-text-body" style={{ fontSize: '1.125rem', marginBottom: '2rem' }}>{p.description}</p>
|
|
236
|
+
|
|
237
|
+
{p.sizes && p.sizes.length > 0 && (
|
|
238
|
+
<div style={{ marginBottom: '1.5rem' }}>
|
|
239
|
+
<h4 className="pk-label" style={{ marginBottom: '0.5rem' }}>Select Size</h4>
|
|
240
|
+
<div className="pk-flex" style={{ gap: '0.5rem', flexWrap: 'wrap' }}>
|
|
241
|
+
{p.sizes.map(size => (
|
|
242
|
+
<button
|
|
243
|
+
key={size}
|
|
244
|
+
className={p.selectedSize === size ? 'pk-btn pk-btn-primary' : 'pk-btn pk-btn-outline'}
|
|
245
|
+
style={{ padding: '0.5rem 1rem' }}
|
|
246
|
+
onClick={() => setSelectedProduct({ ...p, selectedSize: size })}
|
|
247
|
+
>
|
|
248
|
+
{size}
|
|
249
|
+
</button>
|
|
250
|
+
))}
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
)}
|
|
254
|
+
|
|
255
|
+
{p.colors && p.colors.length > 0 && (
|
|
256
|
+
<div style={{ marginBottom: '2.5rem' }}>
|
|
257
|
+
<h4 className="pk-label" style={{ marginBottom: '0.5rem' }}>Select Color</h4>
|
|
258
|
+
<div className="pk-flex" style={{ gap: '0.5rem', flexWrap: 'wrap' }}>
|
|
259
|
+
{p.colors.map(color => (
|
|
260
|
+
<button
|
|
261
|
+
key={color}
|
|
262
|
+
className={p.selectedColor === color ? 'pk-btn pk-btn-secondary' : 'pk-btn pk-btn-outline'}
|
|
263
|
+
onClick={() => setSelectedProduct({ ...p, selectedColor: color })}
|
|
264
|
+
>
|
|
265
|
+
{color}
|
|
266
|
+
</button>
|
|
267
|
+
))}
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
)}
|
|
271
|
+
|
|
272
|
+
<button
|
|
273
|
+
className={`pk-btn pk-btn-primary pk-w-full ${!p.inStock ? 'pk-btn-outline' : ''}`}
|
|
274
|
+
style={{ fontSize: '1.25rem', padding: '1rem' }}
|
|
275
|
+
disabled={!p.inStock}
|
|
276
|
+
onClick={handleAddToCart}
|
|
277
|
+
>
|
|
278
|
+
{p.inStock ? '🛒 Add to Cart' : 'Out of Stock'}
|
|
279
|
+
</button>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
);
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const renderCheckout = () => (
|
|
287
|
+
<div className="pk-container animate-fade-in">
|
|
288
|
+
<div className="pk-flex pk-justify-between pk-items-center" style={{ marginBottom: '2rem' }}>
|
|
289
|
+
<h1 className="pk-heading-lg">Your Cart</h1>
|
|
290
|
+
<button className="pk-btn pk-btn-outline" onClick={() => setCurrentView('grid')}>
|
|
291
|
+
Keep Shopping
|
|
292
|
+
</button>
|
|
293
|
+
</div>
|
|
294
|
+
|
|
295
|
+
{cart.length === 0 ? (
|
|
296
|
+
<div className="pk-empty-state">
|
|
297
|
+
<div className="pk-empty-icon">🛍️</div>
|
|
298
|
+
<h3 className="pk-heading-md">Your cart is empty</h3>
|
|
299
|
+
<p className="pk-text-body" style={{ marginBottom: '2rem' }}>Looks like you haven't added anything yet.</p>
|
|
300
|
+
<button className="pk-btn pk-btn-primary" onClick={() => setCurrentView('grid')}>Start Shopping</button>
|
|
301
|
+
</div>
|
|
302
|
+
) : (
|
|
303
|
+
<div className="pk-grid" style={{ gridTemplateColumns: 'minmax(300px, 2fr) minmax(300px, 1fr)' }}>
|
|
304
|
+
<div className="pk-flex pk-flex-col pk-gap-4">
|
|
305
|
+
{cart.map((item, idx) => (
|
|
306
|
+
<div key={idx} className="pk-card pk-flex pk-items-center" style={{ padding: '1rem', gap: '1.5rem' }}>
|
|
307
|
+
<div style={{ width: '100px', height: '100px', borderRadius: 'var(--pk-radius-md)', overflow: 'hidden', background: 'var(--pk-primary-light)' }}>
|
|
308
|
+
<img src={item.image} alt={item.name} className="pk-object-cover pk-w-full pk-h-full" />
|
|
309
|
+
</div>
|
|
310
|
+
<div style={{ flex: 1 }}>
|
|
311
|
+
<h3 className="pk-heading-md" style={{ fontSize: '1.25rem', marginBottom: '0.25rem' }}>{item.name}</h3>
|
|
312
|
+
<p className="pk-text-sm">
|
|
313
|
+
{item.selectedColor && `Color: ${item.selectedColor}`}
|
|
314
|
+
{item.selectedColor && item.selectedSize && ` | `}
|
|
315
|
+
{item.selectedSize && `Size: ${item.selectedSize}`}
|
|
316
|
+
</p>
|
|
317
|
+
<p className="pk-text-body" style={{ fontWeight: 600, marginTop: '0.5rem' }}>Qty: {item.quantity}</p>
|
|
318
|
+
</div>
|
|
319
|
+
<div className="pk-flex pk-flex-col pk-items-center" style={{ gap: '1rem', minWidth: '80px', alignItems: 'flex-end' }}>
|
|
320
|
+
<span className="pk-heading-md">${(item.price * item.quantity).toFixed(2)}</span>
|
|
321
|
+
<button className="pk-btn pk-btn-outline" style={{ padding: '0.25rem 0.5rem', color: 'var(--pk-danger)', border: 'none', background: 'var(--pk-danger-light)' }} onClick={() => handleRemoveFromCart(idx)}>
|
|
322
|
+
Remove
|
|
323
|
+
</button>
|
|
324
|
+
</div>
|
|
325
|
+
</div>
|
|
326
|
+
))}
|
|
327
|
+
</div>
|
|
328
|
+
|
|
329
|
+
<div>
|
|
330
|
+
<div className="pk-card" style={{ position: 'sticky', top: '100px' }}>
|
|
331
|
+
<div className="pk-card-header">
|
|
332
|
+
<h3 className="pk-heading-md">Order Summary</h3>
|
|
333
|
+
</div>
|
|
334
|
+
<div className="pk-card-body pk-flex pk-flex-col pk-gap-4">
|
|
335
|
+
<div className="pk-flex pk-justify-between">
|
|
336
|
+
<span className="pk-text-body">Subtotal</span>
|
|
337
|
+
<span style={{ fontWeight: 600 }}>${cartTotal.toFixed(2)}</span>
|
|
338
|
+
</div>
|
|
339
|
+
<div className="pk-flex pk-justify-between">
|
|
340
|
+
<span className="pk-text-body">Shipping</span>
|
|
341
|
+
<span style={{ fontWeight: 600 }}>Free</span>
|
|
342
|
+
</div>
|
|
343
|
+
<div className="pk-flex pk-justify-between">
|
|
344
|
+
<span className="pk-text-body">Tax (Estimated)</span>
|
|
345
|
+
<span style={{ fontWeight: 600 }}>${(cartTotal * 0.08).toFixed(2)}</span>
|
|
346
|
+
</div>
|
|
347
|
+
<hr style={{ border: 0, borderTop: '1px solid var(--pk-border)', margin: '0.5rem 0' }} />
|
|
348
|
+
<div className="pk-flex pk-justify-between pk-items-center">
|
|
349
|
+
<span className="pk-heading-md">Total</span>
|
|
350
|
+
<span className="pk-heading-lg" style={{ color: 'var(--pk-primary)' }}>${(cartTotal * 1.08).toFixed(2)}</span>
|
|
351
|
+
</div>
|
|
352
|
+
<button
|
|
353
|
+
className="pk-btn pk-btn-primary pk-w-full"
|
|
354
|
+
style={{ marginTop: '1rem', fontSize: '1.25rem', padding: '1rem' }}
|
|
355
|
+
onClick={handleCheckout}
|
|
356
|
+
>
|
|
357
|
+
Proceed to Payment
|
|
358
|
+
</button>
|
|
359
|
+
</div>
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
</div>
|
|
363
|
+
)}
|
|
364
|
+
</div>
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
const renderSuccess = () => (
|
|
368
|
+
<div className="pk-container animate-fade-in" style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '60vh' }}>
|
|
369
|
+
<div className="pk-card pk-text-center" style={{ padding: '4rem 2rem', maxWidth: '600px' }}>
|
|
370
|
+
<div style={{ fontSize: '5rem', marginBottom: '1rem' }}>🎉</div>
|
|
371
|
+
<h1 className="pk-heading-xl" style={{ marginBottom: '1rem' }}>Order Confirmed!</h1>
|
|
372
|
+
<p className="pk-text-body" style={{ fontSize: '1.25rem', marginBottom: '2rem' }}>
|
|
373
|
+
Thank you for your purchase. We've sent a receipt to your email and your items will ship shortly.
|
|
374
|
+
</p>
|
|
375
|
+
<button className="pk-btn pk-btn-primary" onClick={() => setCurrentView('grid')}>
|
|
376
|
+
Return to Store
|
|
377
|
+
</button>
|
|
378
|
+
</div>
|
|
379
|
+
</div>
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
return (
|
|
383
|
+
<div style={{ position: 'relative', minHeight: '100vh', backgroundColor: 'var(--pk-bg-main)' }}>
|
|
384
|
+
{isLoading && (
|
|
385
|
+
<div style={{ position: 'fixed', inset: 0, backgroundColor: 'rgba(255,255,255,0.7)', zIndex: 100, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', backdropFilter: 'blur(4px)' }}>
|
|
386
|
+
<div className="pk-skeleton" style={{ width: '80px', height: '80px', borderRadius: '50%', marginBottom: '1rem' }}></div>
|
|
387
|
+
<h3 className="pk-heading-md">Processing...</h3>
|
|
388
|
+
</div>
|
|
389
|
+
)}
|
|
390
|
+
|
|
391
|
+
{/* Global CSS for fade animations inline for portability if app doesnt import it directly */}
|
|
392
|
+
<style>{`
|
|
393
|
+
.animate-fade-in { animation: popsiteFadeIn 0.4s ease-out; }
|
|
394
|
+
@keyframes popsiteFadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
|
|
395
|
+
`}</style>
|
|
396
|
+
|
|
397
|
+
{currentView === 'grid' && renderGrid()}
|
|
398
|
+
{currentView === 'details' && renderDetails()}
|
|
399
|
+
{currentView === 'checkout' && renderCheckout()}
|
|
400
|
+
{currentView === 'success' && renderSuccess()}
|
|
401
|
+
|
|
402
|
+
<ToastContainer />
|
|
403
|
+
</div>
|
|
404
|
+
);
|
|
405
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { eventBookingMockData } from '../../data/systems/eventBooking';
|
|
3
|
+
import { useToast } from '../system/Toast';
|
|
4
|
+
|
|
5
|
+
export function EventTicketBookingModule() {
|
|
6
|
+
const [currentView, setCurrentView] = useState('feed'); // feed, details
|
|
7
|
+
const [selectedEvent, setSelectedEvent] = useState(null);
|
|
8
|
+
const [showModal, setShowModal] = useState(false);
|
|
9
|
+
const [activeTier, setActiveTier] = useState(null);
|
|
10
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
11
|
+
const [bookedEvents, setBookedEvents] = useState([]);
|
|
12
|
+
|
|
13
|
+
const { showToast, ToastContainer } = useToast();
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const saved = localStorage.getItem('popsite_events');
|
|
17
|
+
if (saved) {
|
|
18
|
+
try { setBookedEvents(JSON.parse(saved)); } catch (e) {}
|
|
19
|
+
}
|
|
20
|
+
}, []);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
localStorage.setItem('popsite_events', JSON.stringify(bookedEvents));
|
|
24
|
+
}, [bookedEvents]);
|
|
25
|
+
|
|
26
|
+
// Global Countdown Timer Hook
|
|
27
|
+
const useCountdown = (targetDateStr) => {
|
|
28
|
+
const [timeLeft, setTimeLeft] = useState({ days: 0, hours: 0, minutes: 0, seconds: 0 });
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
const target = new Date(targetDateStr).getTime();
|
|
32
|
+
const interval = setInterval(() => {
|
|
33
|
+
const now = new Date().getTime();
|
|
34
|
+
const difference = target - now;
|
|
35
|
+
|
|
36
|
+
if (difference <= 0) {
|
|
37
|
+
clearInterval(interval);
|
|
38
|
+
setTimeLeft({ days: 0, hours: 0, minutes: 0, seconds: 0 });
|
|
39
|
+
} else {
|
|
40
|
+
setTimeLeft({
|
|
41
|
+
days: Math.floor(difference / (1000 * 60 * 60 * 24)),
|
|
42
|
+
hours: Math.floor((difference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)),
|
|
43
|
+
minutes: Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60)),
|
|
44
|
+
seconds: Math.floor((difference % (1000 * 60)) / 1000)
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}, 1000);
|
|
48
|
+
return () => clearInterval(interval);
|
|
49
|
+
}, [targetDateStr]);
|
|
50
|
+
|
|
51
|
+
return timeLeft;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const withDelay = (callback, delay = 1500) => {
|
|
55
|
+
setIsLoading(true);
|
|
56
|
+
setTimeout(() => {
|
|
57
|
+
setIsLoading(false);
|
|
58
|
+
callback();
|
|
59
|
+
}, delay);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const handlePurchase = () => {
|
|
63
|
+
withDelay(() => {
|
|
64
|
+
const newTicket = {
|
|
65
|
+
eventId: selectedEvent.id,
|
|
66
|
+
ticketId: `TKT-${Math.floor(Math.random()*10000)}-${Date.now().toString().slice(-4)}`,
|
|
67
|
+
tier: activeTier
|
|
68
|
+
};
|
|
69
|
+
setBookedEvents(prev => [...prev, newTicket]);
|
|
70
|
+
setShowModal(false);
|
|
71
|
+
setActiveTier(null);
|
|
72
|
+
showToast('Payment successful! Your ticket is secured.', 'success');
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const renderFeed = () => (
|
|
77
|
+
<div className="pk-container animate-fade-in">
|
|
78
|
+
<div style={{ textAlign: 'center', marginBottom: '3rem' }}>
|
|
79
|
+
<h1 className="pk-heading-xl" style={{ marginBottom: '1rem' }}>{eventBookingMockData.platformName}</h1>
|
|
80
|
+
<p className="pk-text-body" style={{ fontSize: '1.25rem' }}>Secure your spot at the world's most exclusive events.</p>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<div className="pk-grid pk-flex-col pk-gap-6">
|
|
84
|
+
{eventBookingMockData.upcomingEvents.map(ev => {
|
|
85
|
+
const dateObj = new Date(ev.date);
|
|
86
|
+
return (
|
|
87
|
+
<div key={ev.id} className="pk-card pk-card-interactive" style={{ display: 'flex', flexWrap: 'wrap' }}>
|
|
88
|
+
<div style={{ flex: '1 1 300px', height: '250px' }}>
|
|
89
|
+
<img src={ev.image} alt={ev.title} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
|
|
90
|
+
</div>
|
|
91
|
+
<div className="pk-card-body" style={{ flex: '2 1 400px', display: 'flex', flexDirection: 'column' }}>
|
|
92
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.5rem' }}>
|
|
93
|
+
<span className="pk-badge">{dateObj.toLocaleDateString(undefined, {month: 'long', day: 'numeric', year: 'numeric'})}</span>
|
|
94
|
+
{bookedEvents.find(b => b.eventId === ev.id) && (
|
|
95
|
+
<span style={{ color: 'var(--pk-success)', fontWeight: 'bold' }}>✓ Ticket Owned</span>
|
|
96
|
+
)}
|
|
97
|
+
</div>
|
|
98
|
+
<h2 className="pk-heading-lg" style={{ marginBottom: '0.5rem' }}>{ev.title}</h2>
|
|
99
|
+
<p className="pk-text-sm pk-text-muted" style={{ marginBottom: '1rem' }}>📍 {ev.location}</p>
|
|
100
|
+
<p className="pk-text-body" style={{ flex: 1 }}>{ev.description}</p>
|
|
101
|
+
|
|
102
|
+
<button className="pk-btn pk-btn-primary" style={{ alignSelf: 'flex-start', marginTop: '1.5rem' }} onClick={() => { setSelectedEvent(ev); setCurrentView('details'); }}>
|
|
103
|
+
View Details & Tickets
|
|
104
|
+
</button>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
})}
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const renderDetails = () => {
|
|
114
|
+
if (!selectedEvent) return null;
|
|
115
|
+
return (
|
|
116
|
+
<div className="pk-container animate-fade-in">
|
|
117
|
+
<button className="pk-btn pk-btn-outline" style={{ marginBottom: '2rem' }} onClick={() => setCurrentView('feed')}>← Back to Events</button>
|
|
118
|
+
|
|
119
|
+
<div style={{ position: 'relative', height: '400px', borderRadius: 'var(--pk-radius-lg)', overflow: 'hidden', marginBottom: '3rem' }}>
|
|
120
|
+
<img src={selectedEvent.image} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
|
|
121
|
+
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(rgba(0,0,0,0), rgba(0,0,0,0.8))', display: 'flex', flexDirection: 'column', justifyContent: 'flex-end', padding: '3rem' }}>
|
|
122
|
+
<h1 className="pk-heading-xl" style={{ color: 'white', marginBottom: '0.5rem' }}>{selectedEvent.title}</h1>
|
|
123
|
+
<p style={{ color: 'rgba(255,255,255,0.8)', fontSize: '1.25rem' }}>📍 {selectedEvent.location}</p>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<div className="pk-grid" style={{ gridTemplateColumns: 'minmax(300px, 2fr) minmax(300px, 1fr)' }}>
|
|
128
|
+
<div>
|
|
129
|
+
<h2 className="pk-heading-lg" style={{ marginBottom: '1.5rem' }}>About this Event</h2>
|
|
130
|
+
<p className="pk-text-body" style={{ fontSize: '1.125rem', marginBottom: '3rem' }}>{selectedEvent.description}</p>
|
|
131
|
+
|
|
132
|
+
<h3 className="pk-heading-md" style={{ marginBottom: '1rem' }}>Who's Speaking</h3>
|
|
133
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem', marginBottom: '3rem' }}>
|
|
134
|
+
{selectedEvent.speakers.map((s, i) => <div key={i} className="pk-card" style={{ padding: '1rem' }}>👤 {s}</div>)}
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<h3 className="pk-heading-md" style={{ marginBottom: '1rem' }}>Agenda</h3>
|
|
138
|
+
<div style={{ borderLeft: '2px solid var(--pk-border)', paddingLeft: '1.5rem', display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
|
139
|
+
{selectedEvent.agenda.map((a, i) => (
|
|
140
|
+
<div key={i} style={{ position: 'relative' }}>
|
|
141
|
+
<div style={{ position: 'absolute', left: '-1.5rem', top: '5px', transform: 'translateX(-50%)', width: '12px', height: '12px', borderRadius: '50%', background: 'var(--pk-primary)' }}></div>
|
|
142
|
+
<p className="pk-text-body" style={{ fontWeight: 600 }}>{a}</p>
|
|
143
|
+
</div>
|
|
144
|
+
))}
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<div>
|
|
149
|
+
<div className="pk-card" style={{ position: 'sticky', top: '20px', padding: '2rem' }}>
|
|
150
|
+
<EventCountdownTimer targetDate={selectedEvent.date} />
|
|
151
|
+
<button className="pk-btn pk-btn-primary pk-w-full" style={{ padding: '1.25rem', fontSize: '1.125rem' }} onClick={() => setShowModal(true)}>
|
|
152
|
+
Get Tickets
|
|
153
|
+
</button>
|
|
154
|
+
{bookedEvents.find(b => b.eventId === selectedEvent.id) && (
|
|
155
|
+
<p className="pk-text-sm pk-text-center" style={{ marginTop: '1rem', color: 'var(--pk-success)', fontWeight: 'bold' }}>You have a ticket for this event!</p>
|
|
156
|
+
)}
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
{/* Modal Implementation natively */}
|
|
162
|
+
{showModal && (
|
|
163
|
+
<div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', zIndex: 100, display: 'flex', alignItems: 'center', justifyContent: 'center', backdropFilter: 'blur(5px)' }}>
|
|
164
|
+
<div className="pk-card animate-fade-in" style={{ width: '100%', maxWidth: '600px', padding: '2rem' }}>
|
|
165
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '2rem' }}>
|
|
166
|
+
<h2 className="pk-heading-lg">Select Tier</h2>
|
|
167
|
+
<button className="pk-btn pk-btn-outline" style={{ border: 'none' }} onClick={() => setShowModal(false)}>✕</button>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem', marginBottom: '2rem' }}>
|
|
171
|
+
{selectedEvent.tiers.map(tier => {
|
|
172
|
+
const isSoldOut = tier.capacity <= 0;
|
|
173
|
+
return (
|
|
174
|
+
<button
|
|
175
|
+
key={tier.id}
|
|
176
|
+
disabled={isSoldOut}
|
|
177
|
+
className={activeTier?.id === tier.id ? 'pk-btn pk-btn-primary' : 'pk-btn pk-btn-outline'}
|
|
178
|
+
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '1.5rem', textAlign: 'left', opacity: isSoldOut ? 0.5 : 1 }}
|
|
179
|
+
onClick={() => setActiveTier(tier)}
|
|
180
|
+
>
|
|
181
|
+
<div>
|
|
182
|
+
<h3 className="pk-heading-md" style={{ color: activeTier?.id === tier.id ? 'white' : 'var(--pk-text-main)' }}>{tier.name}</h3>
|
|
183
|
+
{isSoldOut ? (
|
|
184
|
+
<span style={{ color: 'var(--pk-danger)', fontWeight: 'bold', fontSize: '0.875rem' }}>SOLD OUT</span>
|
|
185
|
+
) : (
|
|
186
|
+
<span style={{ fontSize: '0.875rem', opacity: 0.8 }}>Available</span>
|
|
187
|
+
)}
|
|
188
|
+
</div>
|
|
189
|
+
<span className="pk-heading-lg" style={{ color: activeTier?.id === tier.id ? 'white' : 'var(--pk-primary)' }}>${tier.price}</span>
|
|
190
|
+
</button>
|
|
191
|
+
);
|
|
192
|
+
})}
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
<button
|
|
196
|
+
className="pk-btn pk-btn-primary pk-w-full"
|
|
197
|
+
style={{ padding: '1.25rem' }}
|
|
198
|
+
disabled={!activeTier}
|
|
199
|
+
onClick={handlePurchase}
|
|
200
|
+
>
|
|
201
|
+
Confirm Checkout
|
|
202
|
+
</button>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
)}
|
|
206
|
+
</div>
|
|
207
|
+
);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// Helper component to use hook cleanly
|
|
211
|
+
const EventCountdownTimer = ({ targetDate }) => {
|
|
212
|
+
const timeLeft = useCountdown(targetDate);
|
|
213
|
+
return (
|
|
214
|
+
<div style={{ textAlign: 'center', marginBottom: '2rem' }}>
|
|
215
|
+
<p className="pk-text-muted pk-label" style={{ marginBottom: '1rem' }}>Event Starts In</p>
|
|
216
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '0.5rem' }}>
|
|
217
|
+
<div style={{ background: 'var(--pk-bg-main)', padding: '0.75rem', borderRadius: 'var(--pk-radius-md)', flex: 1 }}>
|
|
218
|
+
<div className="pk-heading-md">{timeLeft.days}</div>
|
|
219
|
+
<div className="pk-text-sm">Days</div>
|
|
220
|
+
</div>
|
|
221
|
+
<div style={{ background: 'var(--pk-bg-main)', padding: '0.75rem', borderRadius: 'var(--pk-radius-md)', flex: 1 }}>
|
|
222
|
+
<div className="pk-heading-md">{timeLeft.hours}</div>
|
|
223
|
+
<div className="pk-text-sm">Hrs</div>
|
|
224
|
+
</div>
|
|
225
|
+
<div style={{ background: 'var(--pk-bg-main)', padding: '0.75rem', borderRadius: 'var(--pk-radius-md)', flex: 1 }}>
|
|
226
|
+
<div className="pk-heading-md">{timeLeft.minutes}</div>
|
|
227
|
+
<div className="pk-text-sm">Min</div>
|
|
228
|
+
</div>
|
|
229
|
+
<div style={{ background: 'var(--pk-bg-main)', padding: '0.75rem', borderRadius: 'var(--pk-radius-md)', flex: 1 }}>
|
|
230
|
+
<div className="pk-heading-md">{timeLeft.seconds}</div>
|
|
231
|
+
<div className="pk-text-sm">Sec</div>
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
);
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
return (
|
|
239
|
+
<div style={{ position: 'relative', minHeight: '100vh', backgroundColor: 'var(--pk-bg-main)' }}>
|
|
240
|
+
{isLoading && (
|
|
241
|
+
<div style={{ position: 'fixed', inset: 0, backgroundColor: 'rgba(255,255,255,0.8)', zIndex: 100, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', backdropFilter: 'blur(4px)' }}>
|
|
242
|
+
<div className="pk-skeleton" style={{ width: '80px', height: '80px', borderRadius: '50%', marginBottom: '1rem' }}></div>
|
|
243
|
+
<h3 className="pk-heading-md">Processing Reservation...</h3>
|
|
244
|
+
</div>
|
|
245
|
+
)}
|
|
246
|
+
|
|
247
|
+
{currentView === 'feed' && renderFeed()}
|
|
248
|
+
{currentView === 'details' && renderDetails()}
|
|
249
|
+
|
|
250
|
+
<ToastContainer />
|
|
251
|
+
</div>
|
|
252
|
+
);
|
|
253
|
+
}
|