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.
Files changed (87) hide show
  1. package/App.jsx +95 -0
  2. package/README.md +92 -0
  3. package/components/layout/PortalHeader.jsx +18 -0
  4. package/components/layout/SystemSidebar.jsx +33 -0
  5. package/components/modules/AnalyticsDashboardModule.jsx +17 -0
  6. package/components/modules/ChatMessagingModule.jsx +17 -0
  7. package/components/modules/EcommerceStoreModule.jsx +17 -0
  8. package/components/modules/EventTicketBookingModule.jsx +17 -0
  9. package/components/modules/FlightBookingModule.jsx +17 -0
  10. package/components/modules/FoodOrderingModule.jsx +17 -0
  11. package/components/modules/HospitalAppointmentModule.jsx +17 -0
  12. package/components/modules/HotelBookingModule.jsx +17 -0
  13. package/components/modules/InvoiceBillingModule.jsx +17 -0
  14. package/components/modules/LibraryManagementModule.jsx +17 -0
  15. package/components/modules/ModuleContentDeck.jsx +44 -0
  16. package/components/modules/MovieBookingModule.jsx +17 -0
  17. package/components/modules/QuizExamModule.jsx +17 -0
  18. package/components/modules/StudentRegistrationModule.jsx +17 -0
  19. package/components/modules/SystemModuleRenderer.jsx +19 -0
  20. package/components/modules/SystemModuleTemplate.jsx +62 -0
  21. package/components/modules/SystemVisualWidget.jsx +123 -0
  22. package/components/modules/moduleContentMap.js +238 -0
  23. package/components/modules/moduleEnhancementsMap.js +439 -0
  24. package/components/modules/systemModuleMap.js +31 -0
  25. package/components/system/DynamicSystemForm.jsx +154 -0
  26. package/components/system/SystemHero.jsx +21 -0
  27. package/components/system/SystemSummaryCard.jsx +53 -0
  28. package/data/systems/analyticsDashboard.js +48 -0
  29. package/data/systems/chatMessaging.js +43 -0
  30. package/data/systems/ecommerceStore.js +50 -0
  31. package/data/systems/eventTicketBooking.js +50 -0
  32. package/data/systems/flightBooking.js +38 -0
  33. package/data/systems/foodOrdering.js +48 -0
  34. package/data/systems/hospitalAppointment.js +50 -0
  35. package/data/systems/hotelBooking.js +38 -0
  36. package/data/systems/index.js +31 -0
  37. package/data/systems/invoiceBilling.js +50 -0
  38. package/data/systems/libraryManagement.js +43 -0
  39. package/data/systems/movieBooking.js +48 -0
  40. package/data/systems/quizExam.js +38 -0
  41. package/data/systems/studentRegistration.js +43 -0
  42. package/dist/popsite-ui.es.js +4368 -0
  43. package/dist/popsite-ui.umd.js +60 -0
  44. package/dist/style.css +1 -0
  45. package/index.html +13 -0
  46. package/library/index.js +20 -0
  47. package/main.jsx +15 -0
  48. package/package.json +40 -0
  49. package/src/App.jsx +12 -0
  50. package/src/components/modules/AnalyticsDashboardModule.jsx +224 -0
  51. package/src/components/modules/ChatMessagingModule.jsx +294 -0
  52. package/src/components/modules/EcommerceStoreModule.jsx +405 -0
  53. package/src/components/modules/EventTicketBookingModule.jsx +253 -0
  54. package/src/components/modules/FlightBookingModule.jsx +399 -0
  55. package/src/components/modules/FoodOrderingModule.jsx +316 -0
  56. package/src/components/modules/HospitalAppointmentModule.jsx +267 -0
  57. package/src/components/modules/HotelBookingModule.jsx +317 -0
  58. package/src/components/modules/InvoiceBillingModule.jsx +302 -0
  59. package/src/components/modules/LandingPageModule.jsx +185 -0
  60. package/src/components/modules/LibraryManagementModule.jsx +189 -0
  61. package/src/components/modules/MovieBookingModule.jsx +337 -0
  62. package/src/components/modules/QuizExamModule.jsx +255 -0
  63. package/src/components/modules/StudentRegistrationModule.jsx +292 -0
  64. package/src/components/system/SystemHero.jsx +44 -0
  65. package/src/components/system/SystemSummaryCard.jsx +29 -0
  66. package/src/components/system/Toast.jsx +69 -0
  67. package/src/data/systems/analyticsDashboard.js +32 -0
  68. package/src/data/systems/chatMessaging.js +59 -0
  69. package/src/data/systems/ecommerceStore.js +84 -0
  70. package/src/data/systems/eventBooking.js +33 -0
  71. package/src/data/systems/flightBooking.js +59 -0
  72. package/src/data/systems/foodOrdering.js +48 -0
  73. package/src/data/systems/hospitalAppointment.js +48 -0
  74. package/src/data/systems/hotelBooking.js +59 -0
  75. package/src/data/systems/invoiceBilling.js +19 -0
  76. package/src/data/systems/landingPage.js +29 -0
  77. package/src/data/systems/libraryManagement.js +17 -0
  78. package/src/data/systems/movieBooking.js +49 -0
  79. package/src/data/systems/quizExam.js +31 -0
  80. package/src/data/systems/studentRegistration.js +9 -0
  81. package/src/index.js +22 -0
  82. package/src/main.jsx +10 -0
  83. package/src/styles.css +296 -0
  84. package/styles.css +820 -0
  85. package/utils/systemEngine.js +128 -0
  86. package/vite.config.js +8 -0
  87. 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
+ }