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,316 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { foodOrderingMockData } from '../../data/systems/foodOrdering';
3
+ import { useToast } from '../system/Toast';
4
+
5
+ export function FoodOrderingModule() {
6
+ const [currentView, setCurrentView] = useState('directory'); // directory, menu, tracker
7
+ const [selectedRestaurant, setSelectedRestaurant] = useState(null);
8
+ const [activeCategory, setActiveCategory] = useState('All');
9
+ const [searchQuery, setSearchQuery] = useState('');
10
+
11
+ const [cart, setCart] = useState([]);
12
+ const [orderState, setOrderState] = useState(0); // 0: Preparing, 1: Out for Delivery, 2: Arrived
13
+ const [activeOrder, setActiveOrder] = useState(null);
14
+
15
+ const { showToast, ToastContainer } = useToast();
16
+
17
+ useEffect(() => {
18
+ const savedCart = localStorage.getItem('popsite_food_cart');
19
+ if (savedCart) {
20
+ try { setCart(JSON.parse(savedCart)); } catch (e) {}
21
+ }
22
+ }, []);
23
+
24
+ useEffect(() => {
25
+ localStorage.setItem('popsite_food_cart', JSON.stringify(cart));
26
+ }, [cart]);
27
+
28
+ const handleAddToCart = (item) => {
29
+ const existing = cart.find(c => c.item.id === item.id);
30
+ if (existing) {
31
+ setCart(cart.map(c => c.item.id === item.id ? { ...c, qty: c.qty + 1 } : c));
32
+ } else {
33
+ setCart([...cart, { item, qty: 1 }]);
34
+ }
35
+ showToast(`Added ${item.name} to cart`, 'success');
36
+ };
37
+
38
+ const handleUpdateQty = (itemId, delta) => {
39
+ setCart(cart.map(c => {
40
+ if (c.item.id === itemId) return { ...c, qty: Math.max(0, c.qty + delta) };
41
+ return c;
42
+ }).filter(c => c.qty > 0));
43
+ };
44
+
45
+ const handleCheckout = () => {
46
+ if (cart.length === 0) return;
47
+ const total = cart.reduce((acc, c) => acc + (c.item.price * c.qty), 0) + selectedRestaurant.deliveryFee;
48
+
49
+ setActiveOrder({
50
+ id: `ORD-${Math.floor(Math.random()*10000)}`,
51
+ restaurant: selectedRestaurant,
52
+ items: [...cart],
53
+ total
54
+ });
55
+
56
+ setCart([]);
57
+ setOrderState(0);
58
+ setCurrentView('tracker');
59
+ showToast('Order confirmed!', 'success');
60
+
61
+ // Simulate delivery progress sequentially using setTimeout
62
+ setTimeout(() => {
63
+ setOrderState(1);
64
+ showToast('Your order is out for delivery!', 'info');
65
+
66
+ setTimeout(() => {
67
+ setOrderState(2);
68
+ showToast('Your order has arrived. Enjoy!', 'success');
69
+ }, 5000);
70
+
71
+ }, 4000);
72
+ };
73
+
74
+ const renderCartSidebar = () => {
75
+ if (!selectedRestaurant) return null;
76
+
77
+ const subtotal = cart.reduce((acc, c) => acc + (c.item.price * c.qty), 0);
78
+ const total = subtotal + selectedRestaurant.deliveryFee;
79
+
80
+ return (
81
+ <div className="pk-card" style={{ position: 'sticky', top: '20px', display: 'flex', flexDirection: 'column', height: 'calc(100vh - 40px)' }}>
82
+ <div className="pk-card-header" style={{ paddingBottom: '1rem', borderBottom: '1px solid var(--pk-border)' }}>
83
+ <h3 className="pk-heading-md">Your Order</h3>
84
+ <p className="pk-text-sm">{selectedRestaurant.name}</p>
85
+ </div>
86
+ <div className="pk-card-body" style={{ flex: 1, overflowY: 'auto', padding: '1rem' }}>
87
+ {cart.length === 0 ? (
88
+ <div className="pk-empty-state" style={{ padding: '2rem 1rem', border: 'none' }}>
89
+ <div style={{ fontSize: '3rem', opacity: 0.5, marginBottom: '1rem' }}>🛒</div>
90
+ <p className="pk-text-body">Cart is empty.</p>
91
+ </div>
92
+ ) : (
93
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
94
+ {cart.map((c, i) => (
95
+ <div key={i} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
96
+ <div style={{ flex: 1 }}>
97
+ <p className="pk-label" style={{ marginBottom: '0.25rem' }}>{c.item.name}</p>
98
+ <p className="pk-text-sm">${(c.item.price * c.qty).toFixed(2)}</p>
99
+ </div>
100
+ <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', background: 'var(--pk-bg-main)', borderRadius: 'var(--pk-radius-full)', padding: '0.25rem' }}>
101
+ <button className="pk-btn pk-btn-outline" style={{ borderRadius: '50%', width: 28, height: 28, padding: 0, display: 'flex', alignItems: 'center', justifyItems: 'center', justifyContent: 'center' }} onClick={() => handleUpdateQty(c.item.id, -1)}>-</button>
102
+ <span style={{ width: '20px', textAlign: 'center', fontWeight: 'bold' }}>{c.qty}</span>
103
+ <button className="pk-btn pk-btn-outline" style={{ borderRadius: '50%', width: 28, height: 28, padding: 0, display: 'flex', alignItems: 'center', justifyItems: 'center', justifyContent: 'center' }} onClick={() => handleUpdateQty(c.item.id, 1)}>+</button>
104
+ </div>
105
+ </div>
106
+ ))}
107
+ </div>
108
+ )}
109
+ </div>
110
+ <div className="pk-card-footer" style={{ padding: '1.5rem', background: 'var(--pk-bg-main)' }}>
111
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.5rem' }}>
112
+ <span className="pk-text-body">Subtotal</span>
113
+ <span>${subtotal.toFixed(2)}</span>
114
+ </div>
115
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '1rem' }}>
116
+ <span className="pk-text-body">Delivery Fee</span>
117
+ <span>${selectedRestaurant.deliveryFee.toFixed(2)}</span>
118
+ </div>
119
+ <button
120
+ className="pk-btn pk-btn-primary"
121
+ style={{ width: '100%', padding: '1rem', fontSize: '1.125rem' }}
122
+ disabled={cart.length === 0}
123
+ onClick={handleCheckout}
124
+ >
125
+ Checkout • ${total.toFixed(2)}
126
+ </button>
127
+ </div>
128
+ </div>
129
+ );
130
+ };
131
+
132
+ const renderDirectory = () => {
133
+ const filtered = foodOrderingMockData.restaurants.filter(r => {
134
+ const matchCat = activeCategory === 'All' || r.category === activeCategory;
135
+ const matchSearch = r.name.toLowerCase().includes(searchQuery.toLowerCase());
136
+ return matchCat && matchSearch;
137
+ });
138
+
139
+ return (
140
+ <div className="pk-container animate-fade-in">
141
+ <div style={{ marginBottom: '2rem' }}>
142
+ <h1 className="pk-heading-xl" style={{ marginBottom: '0.5rem' }}>{foodOrderingMockData.platformName}</h1>
143
+ <p className="pk-text-body">Cravings delivered to your door.</p>
144
+ </div>
145
+
146
+ <div className="pk-card pk-glass" style={{ padding: '1.5rem', marginBottom: '2rem', display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
147
+ <input
148
+ type="text"
149
+ className="pk-input"
150
+ placeholder="Search restaurants..."
151
+ style={{ width: '100%', maxWidth: '500px' }}
152
+ value={searchQuery}
153
+ onChange={e => setSearchQuery(e.target.value)}
154
+ />
155
+ <div style={{ display: 'flex', gap: '0.75rem', overflowX: 'auto', paddingBottom: '0.5rem' }}>
156
+ {foodOrderingMockData.categories.map(cat => (
157
+ <button
158
+ key={cat}
159
+ className={activeCategory === cat ? 'pk-btn pk-btn-primary' : 'pk-btn pk-btn-outline'}
160
+ style={{ borderRadius: 'var(--pk-radius-full)', padding: '0.5rem 1.25rem', whiteSpace: 'nowrap' }}
161
+ onClick={() => setActiveCategory(cat)}
162
+ >
163
+ {cat}
164
+ </button>
165
+ ))}
166
+ </div>
167
+ </div>
168
+
169
+ <div className="pk-grid" style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))' }}>
170
+ {filtered.map(restaurant => (
171
+ <div key={restaurant.id} className="pk-card pk-card-interactive" style={{ cursor: 'pointer', display: 'flex', flexDirection: 'column' }} onClick={() => { setSelectedRestaurant(restaurant); setCurrentView('menu'); }}>
172
+ <div style={{ height: '200px', width: '100%' }}>
173
+ <img src={restaurant.image} alt={restaurant.name} style={{ width: '100%', height: '100%', objectFit: 'cover' }} onError={(e) => {e.target.style.display='none'; e.target.nextSibling.style.display='flex';}} />
174
+ <div style={{ display: 'none', width: '100%', height: '100%', background: 'var(--pk-secondary)', color: 'white', alignItems: 'center', justifyContent: 'center', fontSize: '2rem' }}>{restaurant.name[0]}</div>
175
+ </div>
176
+ <div className="pk-card-body" style={{ flex: 1 }}>
177
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '0.5rem' }}>
178
+ <h3 className="pk-heading-md">{restaurant.name}</h3>
179
+ <span style={{ background: '#f59e0b', color: 'white', padding: '0.25rem 0.5rem', borderRadius: 'var(--pk-radius-sm)', fontWeight: 'bold', fontSize: '0.875rem' }}>★ {restaurant.rating}</span>
180
+ </div>
181
+ <p className="pk-text-sm" style={{ marginBottom: '1rem' }}>{restaurant.category} • {restaurant.deliveryTime}</p>
182
+ <span className="pk-badge" style={{ background: 'var(--pk-primary-light)', color: 'var(--pk-primary)' }}>Delivery: ${restaurant.deliveryFee}</span>
183
+ </div>
184
+ </div>
185
+ ))}
186
+ </div>
187
+ </div>
188
+ );
189
+ };
190
+
191
+ const renderMenu = () => (
192
+ <div className="pk-container animate-fade-in" style={{ display: 'flex', gap: '2rem', alignItems: 'flex-start' }}>
193
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: '2rem' }}>
194
+ <button className="pk-btn pk-btn-outline" style={{ alignSelf: 'flex-start' }} onClick={() => setCurrentView('directory')}>← Back</button>
195
+
196
+ <div style={{ height: '300px', width: '100%', borderRadius: 'var(--pk-radius-lg)', overflow: 'hidden', position: 'relative' }}>
197
+ <img src={selectedRestaurant.image} alt={selectedRestaurant.name} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
198
+ <div style={{ position: 'absolute', bottom: 0, left: 0, width: '100%', padding: '2rem', background: 'linear-gradient(transparent, rgba(0,0,0,0.8))', color: 'white' }}>
199
+ <h1 className="pk-heading-xl" style={{ marginBottom: '0.5rem' }}>{selectedRestaurant.name}</h1>
200
+ <p style={{ fontSize: '1.125rem', opacity: 0.9 }}>{selectedRestaurant.category} • {selectedRestaurant.rating} Rating</p>
201
+ </div>
202
+ </div>
203
+
204
+ <div>
205
+ <h2 className="pk-heading-lg" style={{ marginBottom: '1.5rem' }}>Menu</h2>
206
+ <div className="pk-grid" style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))' }}>
207
+ {selectedRestaurant.menu.map(item => (
208
+ <div key={item.id} className="pk-card" style={{ padding: '1.5rem', display: 'flex', flexDirection: 'column' }}>
209
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '0.5rem' }}>
210
+ <h3 className="pk-heading-md">{item.name}</h3>
211
+ <span className="pk-heading-md" style={{ color: 'var(--pk-primary)' }}>${item.price.toFixed(2)}</span>
212
+ </div>
213
+ <p className="pk-text-body" style={{ marginBottom: '1rem', flex: 1 }}>{item.description}</p>
214
+ <div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap', marginBottom: '1.5rem' }}>
215
+ {item.dietary.map(tag => (
216
+ <span key={tag} className="pk-badge" style={{ background: 'rgba(16, 185, 129, 0.1)', color: 'var(--pk-success)' }}>{tag}</span>
217
+ ))}
218
+ </div>
219
+ <button className="pk-btn pk-btn-outline" style={{ marginTop: 'auto' }} onClick={() => handleAddToCart(item)}>
220
+ + Add to Cart
221
+ </button>
222
+ </div>
223
+ ))}
224
+ </div>
225
+ </div>
226
+ </div>
227
+
228
+ {/* Sidebar Space */}
229
+ <div style={{ width: '350px', flexShrink: 0 }} className="pk-hidden-mobile">
230
+ {renderCartSidebar()}
231
+ </div>
232
+ </div>
233
+ );
234
+
235
+ const renderTracker = () => {
236
+ if (!activeOrder) return null;
237
+
238
+ const progressPercent = orderState === 0 ? 33 : orderState === 1 ? 66 : 100;
239
+ const steps = ['Preparing', 'Out for Delivery', 'Arrived'];
240
+
241
+ return (
242
+ <div className="pk-container animate-fade-in" style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '80vh' }}>
243
+ <div className="pk-card" style={{ width: '100%', maxWidth: '600px', overflow: 'hidden' }}>
244
+ {/* Map Placeholder */}
245
+ <div style={{ height: '250px', background: 'linear-gradient(135deg, #e2e8f0, #cbd5e1)', position: 'relative', overflow: 'hidden', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
246
+ <div style={{ position: 'absolute', width: '200%', height: '200%', background: 'radial-gradient(circle, transparent 20%, rgba(255,255,255,0.4) 20%, rgba(255,255,255,0.4) 80%, transparent 80%, transparent) 0 0, radial-gradient(circle, transparent 20%, rgba(255,255,255,0.4) 20%, rgba(255,255,255,0.4) 80%, transparent 80%, transparent) 50px 50px', backgroundSize: '100px 100px', opacity: 0.5 }}></div>
247
+ <div style={{ position: 'relative', fontSize: '4rem', filter: 'drop-shadow(0 10px 10px rgba(0,0,0,0.2))', animation: orderState === 1 ? 'bounce 2s infinite' : 'none' }}>
248
+ {orderState === 0 ? '🧑‍🍳' : orderState === 1 ? '🛵' : '🎉'}
249
+ </div>
250
+ </div>
251
+
252
+ <div className="pk-card-body" style={{ padding: '3rem 2rem' }}>
253
+ <h2 className="pk-heading-xl" style={{ textAlign: 'center', marginBottom: '1rem' }}>Order {steps[orderState]}</h2>
254
+ <p className="pk-text-body" style={{ textAlign: 'center', marginBottom: '3rem' }}>{activeOrder.id} • {activeOrder.restaurant.name}</p>
255
+
256
+ {/* Progress Bar UI */}
257
+ <div style={{ position: 'relative', marginBottom: '3rem' }}>
258
+ <div style={{ position: 'absolute', top: '50%', left: 0, width: '100%', height: '4px', background: 'var(--pk-bg-main)', transform: 'translateY(-50%)', zIndex: 0 }}></div>
259
+ <div style={{ position: 'absolute', top: '50%', left: 0, width: `${progressPercent}%`, height: '4px', background: 'var(--pk-primary)', transform: 'translateY(-50%)', zIndex: 1, transition: 'width 1s cubic-bezier(0.4, 0, 0.2, 1)' }}></div>
260
+
261
+ <div style={{ display: 'flex', justifyContent: 'space-between', position: 'relative', zIndex: 2 }}>
262
+ {[0, 1, 2].map((step) => (
263
+ <div key={step} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.5rem', width: '33.33%' }}>
264
+ <div style={{
265
+ width: '24px', height: '24px', borderRadius: '50%',
266
+ background: step <= orderState ? 'var(--pk-primary)' : 'var(--pk-bg-main)',
267
+ border: `4px solid ${step <= orderState ? 'var(--pk-primary-light)' : 'var(--pk-border)'}`,
268
+ transition: 'all 0.5s'
269
+ }}></div>
270
+ <span className="pk-text-sm" style={{ fontWeight: step === orderState ? 600 : 400, color: step <= orderState ? 'var(--pk-text-main)' : 'var(--pk-text-muted)' }}>{steps[step]}</span>
271
+ </div>
272
+ ))}
273
+ </div>
274
+ </div>
275
+
276
+ <div style={{ background: 'var(--pk-bg-main)', padding: '1.5rem', borderRadius: 'var(--pk-radius-md)' }}>
277
+ <h4 className="pk-label" style={{ marginBottom: '1rem' }}>Order Summary</h4>
278
+ {activeOrder.items.map((c, i) => (
279
+ <div key={i} style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.5rem' }}>
280
+ <span className="pk-text-sm">{c.qty}x {c.item.name}</span>
281
+ <span className="pk-text-sm">${(c.item.price * c.qty).toFixed(2)}</span>
282
+ </div>
283
+ ))}
284
+ <div style={{ borderTop: '1px solid var(--pk-border)', marginTop: '1rem', paddingTop: '1rem', display: 'flex', justifyContent: 'space-between' }}>
285
+ <span className="pk-heading-md">Total</span>
286
+ <span className="pk-heading-md">${activeOrder.total.toFixed(2)}</span>
287
+ </div>
288
+ </div>
289
+ </div>
290
+
291
+ {orderState === 2 && (
292
+ <div className="pk-card-footer" style={{ display: 'flex', justifyContent: 'center' }}>
293
+ <button className="pk-btn pk-btn-primary" onClick={() => { setActiveOrder(null); setCurrentView('directory'); }}>
294
+ Back to Restaurants
295
+ </button>
296
+ </div>
297
+ )}
298
+ </div>
299
+
300
+ <style>{`
301
+ @keyframes bounce { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-15px); } }
302
+ `}</style>
303
+ </div>
304
+ );
305
+ };
306
+
307
+ return (
308
+ <div style={{ position: 'relative', minHeight: '100vh', backgroundColor: 'var(--pk-bg-main)' }}>
309
+ {currentView === 'directory' && renderDirectory()}
310
+ {currentView === 'menu' && renderMenu()}
311
+ {currentView === 'tracker' && renderTracker()}
312
+
313
+ <ToastContainer />
314
+ </div>
315
+ );
316
+ }
@@ -0,0 +1,267 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { hospitalAppointmentMockData } from '../../data/systems/hospitalAppointment';
3
+ import { useToast } from '../system/Toast';
4
+
5
+ export function HospitalAppointmentModule() {
6
+ const [currentView, setCurrentView] = useState('directory'); // directory, booking, history
7
+ const [doctors, setDoctors] = useState(hospitalAppointmentMockData.doctors);
8
+ const [history, setHistory] = useState(hospitalAppointmentMockData.patientHistory);
9
+
10
+ const [searchQuery, setSearchQuery] = useState('');
11
+ const [activeSpecialty, setActiveSpecialty] = useState('All');
12
+
13
+ const [selectedDoctor, setSelectedDoctor] = useState(null);
14
+ const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]);
15
+ const [isLoading, setIsLoading] = useState(false);
16
+
17
+ const { showToast, ToastContainer } = useToast();
18
+
19
+ useEffect(() => {
20
+ const savedDocs = localStorage.getItem('popsite_hosp_docs');
21
+ const savedHist = localStorage.getItem('popsite_hosp_hist');
22
+ if (savedDocs) try { setDoctors(JSON.parse(savedDocs)); } catch(e){}
23
+ if (savedHist) try { setHistory(JSON.parse(savedHist)); } catch(e){}
24
+ }, []);
25
+
26
+ useEffect(() => {
27
+ localStorage.setItem('popsite_hosp_docs', JSON.stringify(doctors));
28
+ localStorage.setItem('popsite_hosp_hist', JSON.stringify(history));
29
+ }, [doctors, history]);
30
+
31
+ const withDelay = (callback, delay = 1000) => {
32
+ setIsLoading(true);
33
+ setTimeout(() => {
34
+ setIsLoading(false);
35
+ callback();
36
+ }, delay);
37
+ };
38
+
39
+ const handleBookSlot = (timeSlot) => {
40
+ withDelay(() => {
41
+ // Create appointment
42
+ const newApt = {
43
+ id: `APT-${Math.floor(Math.random()*10000)}`,
44
+ doctorName: selectedDoctor.name,
45
+ doctorId: selectedDoctor.id,
46
+ date: selectedDate,
47
+ time: timeSlot,
48
+ status: 'Scheduled',
49
+ diagnosis: '--'
50
+ };
51
+
52
+ setHistory(prev => [newApt, ...prev]);
53
+
54
+ // Remove the slot from doctor's availability (simulating DB atomic lock)
55
+ setDoctors(prev => prev.map(doc => {
56
+ if (doc.id === selectedDoctor.id) {
57
+ return { ...doc, availableTimeSlots: doc.availableTimeSlots.filter(t => t !== timeSlot) };
58
+ }
59
+ return doc;
60
+ }));
61
+
62
+ showToast(`Appointment confirmed for ${timeSlot}`, 'success');
63
+ setCurrentView('history');
64
+ }, 1500);
65
+ };
66
+
67
+ const handleCancelAppointment = (apt) => {
68
+ withDelay(() => {
69
+ // Mark as cancelled
70
+ setHistory(prev => prev.map(a => a.id === apt.id ? { ...a, status: 'Cancelled' } : a));
71
+
72
+ // Free up the slot (mocking releasing the lock back to doctor)
73
+ setDoctors(prev => prev.map(doc => {
74
+ if (doc.id === apt.doctorId) {
75
+ // Re-insert slot and sort them (simplified string sort for demo)
76
+ const newSlots = [...doc.availableTimeSlots, apt.time].sort();
77
+ return { ...doc, availableTimeSlots: newSlots };
78
+ }
79
+ return doc;
80
+ }));
81
+
82
+ showToast(`Appointment ${apt.id} cancelled successfully`, 'info');
83
+ });
84
+ };
85
+
86
+ // Views
87
+ const renderNav = () => (
88
+ <div className="pk-card" style={{ marginBottom: '2rem', padding: '1rem', display: 'flex', gap: '1rem', overflowX: 'auto' }}>
89
+ <button className={currentView === 'directory' ? 'pk-btn pk-btn-primary' : 'pk-btn pk-btn-outline'} onClick={() => setCurrentView('directory')}>Find a Doctor</button>
90
+ <button className={currentView === 'history' ? 'pk-btn pk-btn-primary' : 'pk-btn pk-btn-outline'} onClick={() => setCurrentView('history')}>My Appointments</button>
91
+ </div>
92
+ );
93
+
94
+ const renderDirectory = () => {
95
+ const filtered = doctors.filter(doc => {
96
+ const matchSpec = activeSpecialty === 'All' || doc.specialty === activeSpecialty;
97
+ const matchSearch = doc.name.toLowerCase().includes(searchQuery.toLowerCase());
98
+ return matchSpec && matchSearch;
99
+ });
100
+
101
+ return (
102
+ <div className="animate-fade-in">
103
+ <div className="pk-card pk-glass" style={{ padding: '1.5rem', marginBottom: '2rem', display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
104
+ <input
105
+ type="text"
106
+ className="pk-input"
107
+ placeholder="Search doctors by name..."
108
+ style={{ width: '100%', maxWidth: '500px' }}
109
+ value={searchQuery}
110
+ onChange={e => setSearchQuery(e.target.value)}
111
+ />
112
+ <div style={{ display: 'flex', gap: '0.75rem', overflowX: 'auto', paddingBottom: '0.5rem' }}>
113
+ {hospitalAppointmentMockData.specialties.map(spec => (
114
+ <button
115
+ key={spec}
116
+ className={activeSpecialty === spec ? 'pk-btn pk-btn-primary' : 'pk-btn pk-btn-outline'}
117
+ style={{ borderRadius: 'var(--pk-radius-full)', padding: '0.5rem 1.25rem', whiteSpace: 'nowrap' }}
118
+ onClick={() => setActiveSpecialty(spec)}
119
+ >
120
+ {spec}
121
+ </button>
122
+ ))}
123
+ </div>
124
+ </div>
125
+
126
+ <div className="pk-grid" style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))' }}>
127
+ {filtered.map(doc => (
128
+ <div key={doc.id} className="pk-card pk-card-interactive" style={{ padding: '2rem', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
129
+ <img src={doc.image} alt={doc.name} style={{ width: '120px', height: '120px', borderRadius: '50%', objectFit: 'cover', marginBottom: '1.5rem', boxShadow: 'var(--pk-shadow-md)' }} onError={(e) => {e.target.style.display='none'; e.target.nextSibling.style.display='flex'}} />
130
+ <div style={{ display: 'none', width: '120px', height: '120px', borderRadius: '50%', background: 'var(--pk-primary-light)', color: 'var(--pk-primary)', alignItems: 'center', justifyContent: 'center', fontSize: '2rem', fontWeight: 'bold', marginBottom: '1.5rem' }}>
131
+ {doc.name.split(' ')[1]?.[0] || 'D'}
132
+ </div>
133
+
134
+ <h3 className="pk-heading-md" style={{ marginBottom: '0.5rem' }}>{doc.name}</h3>
135
+ <span className="pk-badge" style={{ marginBottom: '1rem', background: 'var(--pk-bg-main)', color: 'var(--pk-text-muted)' }}>{doc.specialty}</span>
136
+ <span style={{ fontSize: '0.875rem', color: '#f59e0b', fontWeight: 600, marginBottom: '1.5rem' }}>★ {doc.rating} Rating</span>
137
+
138
+ <button
139
+ className="pk-btn pk-btn-primary pk-w-full"
140
+ onClick={() => { setSelectedDoctor(doc); setCurrentView('booking'); }}
141
+ >
142
+ Book Appointment
143
+ </button>
144
+ </div>
145
+ ))}
146
+ </div>
147
+ </div>
148
+ );
149
+ };
150
+
151
+ const renderBooking = () => {
152
+ if (!selectedDoctor) return null;
153
+ // Re-fetch doctor from state to ensure slots are accurate
154
+ const currentDoc = doctors.find(d => d.id === selectedDoctor.id);
155
+
156
+ return (
157
+ <div className="animate-fade-in" style={{ display: 'flex', justifyContent: 'center' }}>
158
+ <div className="pk-card" style={{ width: '100%', maxWidth: '800px', padding: '3rem' }}>
159
+ <button className="pk-btn pk-btn-outline" style={{ marginBottom: '2rem' }} onClick={() => setCurrentView('directory')}>← Back</button>
160
+
161
+ <div style={{ display: 'flex', gap: '2rem', alignItems: 'center', marginBottom: '3rem', borderBottom: '1px solid var(--pk-border)', paddingBottom: '2rem' }}>
162
+ <img src={currentDoc.image} alt="" style={{ width: '100px', height: '100px', borderRadius: '50%', objectFit: 'cover' }} onError={(e)=>{e.target.style.display='none'}} />
163
+ <div>
164
+ <h2 className="pk-heading-lg">{currentDoc.name}</h2>
165
+ <p className="pk-text-body">{currentDoc.specialty}</p>
166
+ </div>
167
+ </div>
168
+
169
+ <div className="pk-input-group" style={{ marginBottom: '3rem', maxWidth: '300px' }}>
170
+ <label className="pk-label">Select Date: </label>
171
+ <input type="date" className="pk-input" value={selectedDate} onChange={e => setSelectedDate(e.target.value)} />
172
+ </div>
173
+
174
+ <h3 className="pk-heading-md" style={{ marginBottom: '1.5rem' }}>Available Time Slots</h3>
175
+
176
+ {currentDoc.availableTimeSlots.length === 0 ? (
177
+ <div className="pk-empty-state" style={{ padding: '2rem', border: '1px solid var(--pk-border)' }}>
178
+ <h4 className="pk-heading-md">Fully Booked</h4>
179
+ <p className="pk-text-body">No remaining slots available for this doctor.</p>
180
+ </div>
181
+ ) : (
182
+ <div className="pk-grid" style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))' }}>
183
+ {currentDoc.availableTimeSlots.map(slot => (
184
+ <button
185
+ key={slot}
186
+ className="pk-btn pk-btn-outline"
187
+ style={{ padding: '1rem', fontSize: '1.125rem' }}
188
+ onClick={() => handleBookSlot(slot)}
189
+ >
190
+ {slot}
191
+ </button>
192
+ ))}
193
+ </div>
194
+ )}
195
+ </div>
196
+ </div>
197
+ );
198
+ };
199
+
200
+ const renderHistory = () => (
201
+ <div className="animate-fade-in">
202
+ <h2 className="pk-heading-lg" style={{ marginBottom: '2rem' }}>Patient History</h2>
203
+
204
+ {history.length === 0 ? (
205
+ <div className="pk-empty-state">
206
+ <div className="pk-empty-icon" style={{ fontSize: '4rem' }}>🏥</div>
207
+ <h3 className="pk-heading-md">No Appointments Found</h3>
208
+ <p className="pk-text-body">You have not scheduled any appointments yet.</p>
209
+ </div>
210
+ ) : (
211
+ <div className="pk-grid pk-flex-col pk-gap-4">
212
+ {history.map(apt => (
213
+ <div key={apt.id} className="pk-card" style={{ padding: '1.5rem', display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '1rem', borderLeft: apt.status === 'Cancelled' ? '4px solid var(--pk-danger)' : apt.status === 'Completed' ? '4px solid var(--pk-success)' : '4px solid var(--pk-primary)' }}>
214
+ <div>
215
+ <h3 className="pk-heading-md">{apt.doctorName}</h3>
216
+ <p className="pk-text-sm" style={{ color: 'var(--pk-primary)', fontWeight: 600, marginBottom: '0.5rem' }}>{apt.date} • {apt.time}</p>
217
+ <p className="pk-text-sm">ID: {apt.id}</p>
218
+ </div>
219
+
220
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: '0.5rem' }}>
221
+ <span className="pk-badge" style={{ background: apt.status === 'Scheduled' ? 'var(--pk-primary-light)' : 'var(--pk-bg-main)', color: apt.status === 'Cancelled' ? 'var(--pk-danger)' : 'var(--pk-text-main)' }}>
222
+ {apt.status}
223
+ </span>
224
+
225
+ {apt.status === 'Scheduled' && (
226
+ <button
227
+ className="pk-btn pk-btn-outline"
228
+ style={{ color: 'var(--pk-danger)', border: 'none', padding: '0.5rem 1rem' }}
229
+ onClick={() => handleCancelAppointment(apt)}
230
+ >
231
+ Cancel Appointment
232
+ </button>
233
+ )}
234
+ </div>
235
+ </div>
236
+ ))}
237
+ </div>
238
+ )}
239
+ </div>
240
+ );
241
+
242
+ return (
243
+ <div className="pk-container" style={{ minHeight: '100vh' }}>
244
+ {isLoading && (
245
+ <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)' }}>
246
+ <div className="pk-skeleton" style={{ width: '80px', height: '80px', borderRadius: '50%', marginBottom: '1rem' }}></div>
247
+ <h3 className="pk-heading-md">Processing...</h3>
248
+ </div>
249
+ )}
250
+
251
+ <div style={{ marginBottom: '2rem' }}>
252
+ <h1 className="pk-heading-xl">{hospitalAppointmentMockData.hospitalName}</h1>
253
+ <p className="pk-text-body">Patient portal and reservation network.</p>
254
+ </div>
255
+
256
+ {renderNav()}
257
+
258
+ <div style={{ position: 'relative' }}>
259
+ {currentView === 'directory' && renderDirectory()}
260
+ {currentView === 'booking' && renderBooking()}
261
+ {currentView === 'history' && renderHistory()}
262
+ </div>
263
+
264
+ <ToastContainer />
265
+ </div>
266
+ );
267
+ }