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,317 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { hotelBookingMockData } from '../../data/systems/hotelBooking';
3
+ import { useToast } from '../system/Toast';
4
+
5
+ export function HotelBookingModule() {
6
+ const [currentView, setCurrentView] = useState('search'); // search, listing, gallery, summary
7
+
8
+ // Search State
9
+ const defaultCheckIn = new Date().toISOString().split('T')[0];
10
+ const defaultCheckOut = new Date(Date.now() + 86400000 * 3).toISOString().split('T')[0];
11
+ const [destination, setDestination] = useState('');
12
+ const [checkIn, setCheckIn] = useState(defaultCheckIn);
13
+ const [checkOut, setCheckOut] = useState(defaultCheckOut);
14
+
15
+ // Filter State
16
+ const [activeRating, setActiveRating] = useState(0); // 0 means all
17
+ const [selectedAmenities, setSelectedAmenities] = useState([]);
18
+
19
+ const [selectedHotel, setSelectedHotel] = useState(null);
20
+ const [nights, setNights] = useState(3);
21
+ const [isLoading, setIsLoading] = useState(false);
22
+
23
+ const { showToast, ToastContainer } = useToast();
24
+
25
+ useEffect(() => {
26
+ // Calculate nights
27
+ const start = new Date(checkIn);
28
+ const end = new Date(checkOut);
29
+ const diff = Math.ceil((end - start) / (1000 * 60 * 60 * 24));
30
+ if (diff > 0) {
31
+ setNights(diff);
32
+ } else {
33
+ setNights(0);
34
+ }
35
+ }, [checkIn, checkOut]);
36
+
37
+ const withDelay = (callback, delay = 1500) => {
38
+ setIsLoading(true);
39
+ setTimeout(() => {
40
+ setIsLoading(false);
41
+ callback();
42
+ }, delay);
43
+ };
44
+
45
+ const handleSearch = (e) => {
46
+ e.preventDefault();
47
+ if (nights <= 0) {
48
+ showToast('Check-out must be after Check-in', 'error');
49
+ return;
50
+ }
51
+ withDelay(() => {
52
+ setCurrentView('listing');
53
+ });
54
+ };
55
+
56
+ const toggleAmenity = (amenity) => {
57
+ if (selectedAmenities.includes(amenity)) {
58
+ setSelectedAmenities(prev => prev.filter(a => a !== amenity));
59
+ } else {
60
+ setSelectedAmenities(prev => [...prev, amenity]);
61
+ }
62
+ };
63
+
64
+ const handleBook = () => {
65
+ withDelay(() => {
66
+ showToast(`Success! Your booking for ${selectedHotel.name} is confirmed.`, 'success');
67
+ setCurrentView('search');
68
+ }, 2000);
69
+ };
70
+
71
+ const renderSearch = () => (
72
+ <div className="pk-container animate-fade-in" style={{ minHeight: '80vh', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center' }}>
73
+ <div style={{ textAlign: 'center', marginBottom: '3rem' }}>
74
+ <h1 className="pk-heading-xl" style={{ marginBottom: '1rem' }}>{hotelBookingMockData.platformName}</h1>
75
+ <p className="pk-text-body" style={{ fontSize: '1.25rem' }}>Find your perfect escape anywhere in the world.</p>
76
+ </div>
77
+
78
+ <div className="pk-card pk-glass" style={{ width: '100%', maxWidth: '800px', padding: '3rem' }}>
79
+ <form onSubmit={handleSearch} style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
80
+ <div className="pk-input-group">
81
+ <label className="pk-label">Where are you going?</label>
82
+ <input type="text" className="pk-input" placeholder="e.g. Tokyo, Swiss Alps, Maldives" value={destination} onChange={e => setDestination(e.target.value)} required />
83
+ </div>
84
+ <div style={{ display: 'flex', gap: '1.5rem' }}>
85
+ <div className="pk-input-group" style={{ flex: 1 }}>
86
+ <label className="pk-label">Check In</label>
87
+ <input type="date" className="pk-input" value={checkIn} onChange={e => setCheckIn(e.target.value)} required />
88
+ </div>
89
+ <div className="pk-input-group" style={{ flex: 1 }}>
90
+ <label className="pk-label">Check Out</label>
91
+ <input type="date" className="pk-input" value={checkOut} onChange={e => setCheckOut(e.target.value)} required />
92
+ </div>
93
+ </div>
94
+ {nights > 0 && <p className="pk-text-sm pk-text-center" style={{ color: 'var(--pk-primary)' }}>Duration: {nights} nights</p>}
95
+ <button type="submit" className="pk-btn pk-btn-primary pk-w-full" style={{ padding: '1.25rem', fontSize: '1.125rem', marginTop: '1rem' }}>
96
+ Search Accommodations
97
+ </button>
98
+ </form>
99
+ </div>
100
+ </div>
101
+ );
102
+
103
+ const renderListing = () => {
104
+ const filtered = hotelBookingMockData.hotels.filter(h => {
105
+ const matchDest = !destination || h.destination.toLowerCase().includes(destination.toLowerCase());
106
+ const matchRating = activeRating === 0 || h.rating >= activeRating;
107
+ const matchAmenities = selectedAmenities.every(a => h.amenities.includes(a));
108
+ return matchDest && matchRating && matchAmenities;
109
+ });
110
+
111
+ return (
112
+ <div className="pk-container animate-fade-in" style={{ display: 'flex', gap: '2rem', alignItems: 'flex-start' }}>
113
+ {/* Filters Sidebar */}
114
+ <div className="pk-card pk-hidden-mobile" style={{ width: '300px', padding: '1.5rem', flexShrink: 0 }}>
115
+ <button className="pk-btn pk-btn-outline pk-w-full" style={{ marginBottom: '2rem' }} onClick={() => setCurrentView('search')}>← Modifiy Search</button>
116
+
117
+ <h3 className="pk-heading-md" style={{ marginBottom: '1rem' }}>Star Rating</h3>
118
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem', marginBottom: '2rem' }}>
119
+ {[5,4,3].map(stars => (
120
+ <label key={stars} style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer' }}>
121
+ <input type="radio" name="rating" checked={activeRating === stars} onChange={() => setActiveRating(stars)} style={{ accentColor: 'var(--pk-primary)' }} />
122
+ <span style={{ color: '#f59e0b' }}>{'★'.repeat(stars)}</span>
123
+ <span className="pk-text-muted">& up</span>
124
+ </label>
125
+ ))}
126
+ <label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer', marginTop: '0.5rem' }}>
127
+ <input type="radio" name="rating" checked={activeRating === 0} onChange={() => setActiveRating(0)} style={{ accentColor: 'var(--pk-primary)' }} />
128
+ <span className="pk-text-main">Show All</span>
129
+ </label>
130
+ </div>
131
+
132
+ <h3 className="pk-heading-md" style={{ marginBottom: '1rem' }}>Amenities</h3>
133
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem' }}>
134
+ {hotelBookingMockData.amenities.map(a => (
135
+ <button
136
+ key={a}
137
+ className={selectedAmenities.includes(a) ? 'pk-btn pk-btn-primary' : 'pk-btn pk-btn-outline'}
138
+ style={{ padding: '0.25rem 0.5rem', borderRadius: '4px', fontSize: '0.75rem' }}
139
+ onClick={() => toggleAmenity(a)}
140
+ >
141
+ {a}
142
+ </button>
143
+ ))}
144
+ </div>
145
+ </div>
146
+
147
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: '2rem' }}>
148
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
149
+ <h2 className="pk-heading-lg">{destination || 'All Destinations'}</h2>
150
+ <span className="pk-text-muted">{filtered.length} properties found</span>
151
+ </div>
152
+
153
+ {filtered.length === 0 ? (
154
+ <div className="pk-empty-state">
155
+ <h3 className="pk-heading-md">No properties found</h3>
156
+ <p className="pk-text-body">Try removing some filters.</p>
157
+ </div>
158
+ ) : (
159
+ filtered.map(hotel => (
160
+ <div key={hotel.id} className="pk-card pk-card-interactive" style={{ display: 'flex', flexWrap: 'wrap', overflow: 'hidden' }}>
161
+ <div style={{ flex: '1 1 300px', height: '250px' }}>
162
+ <img src={hotel.image} alt={hotel.name} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
163
+ </div>
164
+ <div style={{ flex: '2 1 300px', padding: '2rem', display: 'flex', flexDirection: 'column' }}>
165
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '0.5rem' }}>
166
+ <div>
167
+ <h3 className="pk-heading-lg">{hotel.name}</h3>
168
+ <p className="pk-text-sm" style={{ color: 'var(--pk-primary)', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
169
+ 📍 {hotel.destination}
170
+ </p>
171
+ </div>
172
+ <span style={{ background: '#f59e0b', color: 'white', padding: '0.25rem 0.5rem', borderRadius: '4px', fontWeight: 'bold' }}>
173
+ {'★'.repeat(hotel.rating)}
174
+ </span>
175
+ </div>
176
+ <p className="pk-text-body" style={{ margin: '1rem 0', flex: 1 }}>{hotel.description}</p>
177
+
178
+ <div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap', marginBottom: '1.5rem' }}>
179
+ {hotel.amenities.map(a => <span key={a} className="pk-badge">{a}</span>)}
180
+ </div>
181
+
182
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', borderTop: '1px solid var(--pk-border)', paddingTop: '1rem' }}>
183
+ <div>
184
+ <span className="pk-heading-lg" style={{ color: 'var(--pk-primary)' }}>${hotel.pricePerNight}</span>
185
+ <span className="pk-text-muted"> / night</span>
186
+ </div>
187
+ <button
188
+ className="pk-btn pk-btn-primary"
189
+ onClick={() => { setSelectedHotel(hotel); setCurrentView('gallery'); }}
190
+ >
191
+ View Deal
192
+ </button>
193
+ </div>
194
+ </div>
195
+ </div>
196
+ ))
197
+ )}
198
+ </div>
199
+ </div>
200
+ );
201
+ };
202
+
203
+ const renderGallery = () => {
204
+ if (!selectedHotel) return null;
205
+ return (
206
+ <div className="pk-container animate-fade-in">
207
+ <button className="pk-btn pk-btn-outline" style={{ marginBottom: '2rem' }} onClick={() => setCurrentView('listing')}>← Back</button>
208
+
209
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', marginBottom: '2rem' }}>
210
+ <div>
211
+ <h1 className="pk-heading-xl" style={{ marginBottom: '0.5rem' }}>{selectedHotel.name}</h1>
212
+ <p className="pk-text-body" style={{ fontSize: '1.125rem' }}>📍 {selectedHotel.destination}</p>
213
+ </div>
214
+ <button className="pk-btn pk-btn-primary" style={{ padding: '1rem 2rem', fontSize: '1.25rem' }} onClick={() => setCurrentView('summary')}>
215
+ Reserve • ${selectedHotel.pricePerNight * nights} Total
216
+ </button>
217
+ </div>
218
+
219
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: '1rem' }}>
220
+ <div style={{ flex: '2 1 500px', height: '400px', borderRadius: 'var(--pk-radius-lg)', overflow: 'hidden' }}>
221
+ <img src={selectedHotel.image} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
222
+ </div>
223
+ <div style={{ flex: '1 1 300px', display: 'flex', flexDirection: 'column', gap: '1rem' }}>
224
+ {selectedHotel.gallery.map((img, i) => (
225
+ <div key={i} style={{ flex: 1, height: '192px', borderRadius: 'var(--pk-radius-lg)', overflow: 'hidden' }}>
226
+ <img src={img} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
227
+ </div>
228
+ ))}
229
+ </div>
230
+ </div>
231
+
232
+ <div className="pk-card" style={{ marginTop: '2rem', padding: '2rem' }}>
233
+ <h3 className="pk-heading-md" style={{ marginBottom: '1rem' }}>About this property</h3>
234
+ <p className="pk-text-body">{selectedHotel.description}</p>
235
+ <h4 className="pk-label" style={{ marginTop: '2rem', marginBottom: '1rem' }}>Popular Amenities</h4>
236
+ <div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap' }}>
237
+ {selectedHotel.amenities.map(a => <span key={a} className="pk-badge" style={{ fontSize: '1rem', padding: '0.5rem 1rem' }}>✓ {a}</span>)}
238
+ </div>
239
+ </div>
240
+ </div>
241
+ );
242
+ };
243
+
244
+ const renderSummary = () => {
245
+ if (!selectedHotel) return null;
246
+ const total = selectedHotel.pricePerNight * nights;
247
+
248
+ return (
249
+ <div className="pk-container animate-fade-in" style={{ display: 'flex', justifyContent: 'center' }}>
250
+ <div className="pk-card" style={{ width: '100%', maxWidth: '600px', padding: '3rem' }}>
251
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem' }}>
252
+ <h2 className="pk-heading-lg">Review Booking</h2>
253
+ <button className="pk-btn pk-btn-outline" style={{ border: 'none' }} onClick={() => setCurrentView('gallery')}>Cancel</button>
254
+ </div>
255
+
256
+ <div style={{ display: 'flex', gap: '1.5rem', marginBottom: '2rem', background: 'var(--pk-bg-main)', padding: '1rem', borderRadius: 'var(--pk-radius-md)' }}>
257
+ <img src={selectedHotel.image} alt="" style={{ width: '80px', height: '80px', borderRadius: 'var(--pk-radius-sm)', objectFit: 'cover' }} />
258
+ <div>
259
+ <h3 className="pk-heading-md">{selectedHotel.name}</h3>
260
+ <p className="pk-text-sm" style={{ color: 'var(--pk-primary)' }}>{selectedHotel.destination}</p>
261
+ </div>
262
+ </div>
263
+
264
+ <div style={{ display: 'flex', gap: '1rem', marginBottom: '2rem' }}>
265
+ <div className="pk-card" style={{ flex: 1, padding: '1rem', textAlign: 'center' }}>
266
+ <p className="pk-label pk-text-muted">Check-In</p>
267
+ <p className="pk-heading-sm">{checkIn}</p>
268
+ </div>
269
+ <div className="pk-card" style={{ flex: 1, padding: '1rem', textAlign: 'center' }}>
270
+ <p className="pk-label pk-text-muted">Check-Out</p>
271
+ <p className="pk-heading-sm">{checkOut}</p>
272
+ </div>
273
+ </div>
274
+
275
+ <div style={{ borderTop: '1px solid var(--pk-border)', borderBottom: '1px solid var(--pk-border)', padding: '1.5rem 0', marginBottom: '2rem' }}>
276
+ <h3 className="pk-heading-md" style={{ marginBottom: '1rem' }}>Price Details</h3>
277
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.5rem' }}>
278
+ <span className="pk-text-body">${selectedHotel.pricePerNight} x {nights} nights</span>
279
+ <span>${total}</span>
280
+ </div>
281
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.5rem' }}>
282
+ <span className="pk-text-body">Taxes and fees</span>
283
+ <span>${(total * 0.1).toFixed(0)}</span>
284
+ </div>
285
+ </div>
286
+
287
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem' }}>
288
+ <span className="pk-heading-lg">Total (USD)</span>
289
+ <span className="pk-heading-xl" style={{ color: 'var(--pk-primary)' }}>${(total * 1.1).toFixed(0)}</span>
290
+ </div>
291
+
292
+ <button className="pk-btn pk-btn-primary pk-w-full" style={{ padding: '1.25rem', fontSize: '1.125rem' }} onClick={handleBook}>
293
+ Confirm & Pay
294
+ </button>
295
+ </div>
296
+ </div>
297
+ );
298
+ };
299
+
300
+ return (
301
+ <div style={{ position: 'relative', minHeight: '100vh', backgroundColor: 'var(--pk-bg-main)' }}>
302
+ {isLoading && (
303
+ <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)' }}>
304
+ <div className="pk-skeleton" style={{ width: '80px', height: '80px', borderRadius: '50%', marginBottom: '1rem' }}></div>
305
+ <h3 className="pk-heading-md">Searching Global Availability...</h3>
306
+ </div>
307
+ )}
308
+
309
+ {currentView === 'search' && renderSearch()}
310
+ {currentView === 'listing' && renderListing()}
311
+ {currentView === 'gallery' && renderGallery()}
312
+ {currentView === 'summary' && renderSummary()}
313
+
314
+ <ToastContainer />
315
+ </div>
316
+ );
317
+ }
@@ -0,0 +1,302 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { invoiceBillingMockData } from '../../data/systems/invoiceBilling';
3
+ import { useToast } from '../system/Toast';
4
+
5
+ export function InvoiceBillingModule() {
6
+ const [currentView, setCurrentView] = useState('dashboard'); // dashboard, creator, clients
7
+ const [invoices, setInvoices] = useState(invoiceBillingMockData.recentInvoices);
8
+ const [searchQuery, setSearchQuery] = useState('');
9
+
10
+ // Invoice Creator State
11
+ const [selectedClient, setSelectedClient] = useState('');
12
+ const [taxRate, setTaxRate] = useState(invoiceBillingMockData.companyInfo.taxRate);
13
+ const [lineItems, setLineItems] = useState([
14
+ { id: 1, description: 'Website Redesign', qty: 1, rate: 2500 },
15
+ { id: 2, description: 'Hosting (Annual)', qty: 1, rate: 150 }
16
+ ]);
17
+
18
+ const { showToast, ToastContainer } = useToast();
19
+
20
+ useEffect(() => {
21
+ const saved = localStorage.getItem('popsite_invoices');
22
+ if (saved) {
23
+ try { setInvoices(JSON.parse(saved)); } catch(e){}
24
+ }
25
+ }, []);
26
+
27
+ useEffect(() => {
28
+ localStorage.setItem('popsite_invoices', JSON.stringify(invoices));
29
+ }, [invoices]);
30
+
31
+ // Math calculated dynamically
32
+ const subTotal = lineItems.reduce((acc, item) => acc + (item.qty * item.rate), 0);
33
+ const taxAmount = subTotal * (taxRate / 100);
34
+ const grandTotal = subTotal + taxAmount;
35
+
36
+ const handleAddItem = () => {
37
+ setLineItems([...lineItems, { id: Date.now(), description: '', qty: 1, rate: 0 }]);
38
+ };
39
+
40
+ const handleRemoveItem = (id) => {
41
+ setLineItems(lineItems.filter(item => item.id !== id));
42
+ };
43
+
44
+ const handleChangeItem = (id, field, value) => {
45
+ setLineItems(lineItems.map(item =>
46
+ item.id === id ? { ...item, [field]: value } : item
47
+ ));
48
+ };
49
+
50
+ const handleSaveInvoice = () => {
51
+ if (!selectedClient) {
52
+ showToast('Please select a client', 'error');
53
+ return;
54
+ }
55
+ const newInvoice = {
56
+ id: `INV-${new Date().getFullYear()}-${Math.floor(Math.random()*1000)}`,
57
+ client: invoiceBillingMockData.clients.find(c => c.id === selectedClient)?.name || 'Unknown',
58
+ date: new Date().toISOString().split('T')[0],
59
+ amount: grandTotal,
60
+ status: 'Pending'
61
+ };
62
+ setInvoices([newInvoice, ...invoices]);
63
+ showToast(`Invoice ${newInvoice.id} generated!`, 'success');
64
+ setCurrentView('dashboard');
65
+ };
66
+
67
+ const renderNav = () => (
68
+ <div className="pk-card no-print" style={{ marginBottom: '2rem', padding: '1rem', display: 'flex', gap: '1rem', overflowX: 'auto' }}>
69
+ <button className={currentView === 'dashboard' ? 'pk-btn pk-btn-primary' : 'pk-btn pk-btn-outline'} onClick={() => setCurrentView('dashboard')}>Overview</button>
70
+ <button className={currentView === 'creator' ? 'pk-btn pk-btn-primary' : 'pk-btn pk-btn-outline'} onClick={() => setCurrentView('creator')}>Create Invoice</button>
71
+ <button className={currentView === 'clients' ? 'pk-btn pk-btn-primary' : 'pk-btn pk-btn-outline'} onClick={() => setCurrentView('clients')}>Client Directory</button>
72
+ </div>
73
+ );
74
+
75
+ const renderDashboard = () => (
76
+ <div className="animate-fade-in no-print">
77
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem' }}>
78
+ <h2 className="pk-heading-lg">Recent Invoices</h2>
79
+ <button className="pk-btn pk-btn-primary" onClick={() => setCurrentView('creator')}>+ New Invoice</button>
80
+ </div>
81
+
82
+ <div className="pk-card" style={{ overflowX: 'auto' }}>
83
+ <table style={{ width: '100%', borderCollapse: 'collapse', textAlign: 'left' }}>
84
+ <thead>
85
+ <tr style={{ background: 'var(--pk-bg-main)', borderBottom: '1px solid var(--pk-border)' }}>
86
+ <th style={{ padding: '1rem 1.5rem', color: 'var(--pk-text-muted)' }}>Invoice ID</th>
87
+ <th style={{ padding: '1rem 1.5rem', color: 'var(--pk-text-muted)' }}>Client</th>
88
+ <th style={{ padding: '1rem 1.5rem', color: 'var(--pk-text-muted)' }}>Date</th>
89
+ <th style={{ padding: '1rem 1.5rem', color: 'var(--pk-text-muted)' }}>Amount</th>
90
+ <th style={{ padding: '1rem 1.5rem', color: 'var(--pk-text-muted)' }}>Status</th>
91
+ </tr>
92
+ </thead>
93
+ <tbody>
94
+ {invoices.map((inv, i) => (
95
+ <tr key={i} style={{ borderBottom: '1px solid var(--pk-border)' }}>
96
+ <td style={{ padding: '1rem 1.5rem', fontWeight: 600 }}>{inv.id}</td>
97
+ <td style={{ padding: '1rem 1.5rem' }}>{inv.client}</td>
98
+ <td style={{ padding: '1rem 1.5rem' }}>{inv.date}</td>
99
+ <td style={{ padding: '1rem 1.5rem', fontWeight: 'bold' }}>${inv.amount.toLocaleString(undefined, {minimumFractionDigits: 2})}</td>
100
+ <td style={{ padding: '1rem 1.5rem' }}>
101
+ <span className="pk-badge" style={{
102
+ background: inv.status === 'Paid' ? 'var(--pk-success)' : inv.status === 'Overdue' ? 'var(--pk-danger)' : 'var(--pk-warning)',
103
+ color: 'white'
104
+ }}>
105
+ {inv.status}
106
+ </span>
107
+ </td>
108
+ </tr>
109
+ ))}
110
+ </tbody>
111
+ </table>
112
+ </div>
113
+ </div>
114
+ );
115
+
116
+ const renderClients = () => {
117
+ const filtered = invoiceBillingMockData.clients.filter(c => c.name.toLowerCase().includes(searchQuery.toLowerCase()));
118
+
119
+ return (
120
+ <div className="animate-fade-in no-print">
121
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem' }}>
122
+ <h2 className="pk-heading-lg">Client Directory</h2>
123
+ <input
124
+ type="text"
125
+ className="pk-input"
126
+ placeholder="Search clients..."
127
+ value={searchQuery}
128
+ onChange={e => setSearchQuery(e.target.value)}
129
+ />
130
+ </div>
131
+
132
+ <div className="pk-grid" style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))' }}>
133
+ {filtered.map(client => (
134
+ <div key={client.id} className="pk-card" style={{ padding: '1.5rem' }}>
135
+ <h3 className="pk-heading-md" style={{ marginBottom: '1rem' }}>{client.name}</h3>
136
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
137
+ <a href={`mailto:${client.email}`} className="pk-text-body" style={{ color: 'var(--pk-primary)', textDecoration: 'none' }}>✉️ {client.email}</a>
138
+ <a href={`tel:${client.phone}`} className="pk-text-body" style={{ color: 'var(--pk-text-main)', textDecoration: 'none' }}>📞 {client.phone}</a>
139
+ </div>
140
+ <button
141
+ className="pk-btn pk-btn-outline"
142
+ style={{ width: '100%', marginTop: '1.5rem' }}
143
+ onClick={() => { setSelectedClient(client.id); setCurrentView('creator'); }}
144
+ >
145
+ Invoice Client
146
+ </button>
147
+ </div>
148
+ ))}
149
+ </div>
150
+ </div>
151
+ );
152
+ };
153
+
154
+ const renderCreator = () => (
155
+ <div className="animate-fade-in print-container" style={{ background: 'white', padding: '2rem', borderRadius: 'var(--pk-radius-lg)', boxShadow: 'var(--pk-shadow-lg)' }}>
156
+ {/* Print Setup */}
157
+ <style>{`
158
+ @media print {
159
+ body * { visibility: hidden; }
160
+ .print-container, .print-container * { visibility: visible; }
161
+ .print-container { position: absolute; left: 0; top: 0; width: 100%; box-shadow: none; padding: 0; }
162
+ .no-print { display: none !important; }
163
+ .pk-container { padding: 0 !important; max-width: 100% !important; margin: 0 !important; }
164
+ @page { margin: 1cm; }
165
+ }
166
+ `}</style>
167
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '3rem', borderBottom: '2px solid var(--pk-border)', paddingBottom: '2rem' }}>
168
+ <div>
169
+ <h1 className="pk-heading-xl" style={{ color: 'var(--pk-primary)', marginBottom: '0.5rem' }}>INVOICE</h1>
170
+ <p className="pk-text-body">ID: INV-DRAFT</p>
171
+ <p className="pk-text-body">Date: {new Date().toISOString().split('T')[0]}</p>
172
+ </div>
173
+ <div style={{ textAlign: 'right' }}>
174
+ <h3 className="pk-heading-md">{invoiceBillingMockData.companyInfo.name}</h3>
175
+ <p className="pk-text-body" style={{ whiteSpace: 'pre-line' }}>{invoiceBillingMockData.companyInfo.address}</p>
176
+ <p className="pk-text-body">{invoiceBillingMockData.companyInfo.email}</p>
177
+ </div>
178
+ </div>
179
+
180
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '3rem' }}>
181
+ <div style={{ flex: 1 }}>
182
+ <h4 className="pk-label" style={{ marginBottom: '0.5rem', color: 'var(--pk-text-muted)' }}>Bill To:</h4>
183
+ <select
184
+ className="pk-input no-print"
185
+ value={selectedClient}
186
+ onChange={e => setSelectedClient(e.target.value)}
187
+ style={{ width: '100%', maxWidth: '300px', marginBottom: '0.5rem' }}
188
+ >
189
+ <option value="">-- Select Client --</option>
190
+ {invoiceBillingMockData.clients.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
191
+ </select>
192
+ {/* Print friendly view */}
193
+ <div style={{ display: 'none', display: selectedClient ? 'block' : 'none' }}>
194
+ <h3 className="pk-heading-md">{invoiceBillingMockData.clients.find(c => c.id === selectedClient)?.name}</h3>
195
+ <p className="pk-text-body">{invoiceBillingMockData.clients.find(c => c.id === selectedClient)?.email}</p>
196
+ </div>
197
+ {!selectedClient && <div style={{ display: 'none' }}>_____________</div>}
198
+ </div>
199
+ </div>
200
+
201
+ <table style={{ width: '100%', borderCollapse: 'collapse', marginBottom: '2rem' }}>
202
+ <thead>
203
+ <tr style={{ background: 'var(--pk-primary-light)', color: 'var(--pk-primary)' }}>
204
+ <th style={{ padding: '1rem', textAlign: 'left' }}>Description</th>
205
+ <th style={{ padding: '1rem', textAlign: 'center', width: '100px' }}>Qty</th>
206
+ <th style={{ padding: '1rem', textAlign: 'right', width: '150px' }}>Rate</th>
207
+ <th style={{ padding: '1rem', textAlign: 'right', width: '150px' }}>Total</th>
208
+ <th className="no-print" style={{ width: '50px' }}></th>
209
+ </tr>
210
+ </thead>
211
+ <tbody>
212
+ {lineItems.map((item, idx) => (
213
+ <tr key={item.id} style={{ borderBottom: '1px solid var(--pk-border)' }}>
214
+ <td style={{ padding: '1rem' }}>
215
+ <input
216
+ type="text"
217
+ className="pk-input"
218
+ style={{ width: '100%', border: 'none', background: 'transparent' }}
219
+ value={item.description}
220
+ onChange={e => handleChangeItem(item.id, 'description', e.target.value)}
221
+ placeholder="Item description"
222
+ />
223
+ </td>
224
+ <td style={{ padding: '1rem', textAlign: 'center' }}>
225
+ <input
226
+ type="number"
227
+ className="pk-input"
228
+ style={{ width: '60px', textAlign: 'center' }}
229
+ value={item.qty}
230
+ onChange={e => handleChangeItem(item.id, 'qty', parseFloat(e.target.value) || 0)}
231
+ />
232
+ </td>
233
+ <td style={{ padding: '1rem', textAlign: 'right' }}>
234
+ <input
235
+ type="number"
236
+ className="pk-input"
237
+ style={{ width: '100px', textAlign: 'right' }}
238
+ value={item.rate}
239
+ onChange={e => handleChangeItem(item.id, 'rate', parseFloat(e.target.value) || 0)}
240
+ />
241
+ </td>
242
+ <td style={{ padding: '1rem', textAlign: 'right', fontWeight: 600 }}>
243
+ ${(item.qty * item.rate).toFixed(2)}
244
+ </td>
245
+ <td className="no-print" style={{ padding: '1rem', textAlign: 'center' }}>
246
+ <button className="pk-btn pk-btn-outline" style={{ padding: '0.25rem 0.5rem', color: 'var(--pk-danger)', border: 'none' }} onClick={() => handleRemoveItem(item.id)}>×</button>
247
+ </td>
248
+ </tr>
249
+ ))}
250
+ </tbody>
251
+ </table>
252
+
253
+ <button className="pk-btn pk-btn-outline no-print" onClick={handleAddItem} style={{ marginBottom: '2rem' }}>+ Add Line Item</button>
254
+
255
+ <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
256
+ <div style={{ width: '300px' }}>
257
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '1rem' }}>
258
+ <span className="pk-text-body">Subtotal</span>
259
+ <span>${subTotal.toFixed(2)}</span>
260
+ </div>
261
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
262
+ <span className="pk-text-body">Tax Rate (%)</span>
263
+ <input type="number" className="pk-input no-print" style={{ width: '80px', textAlign: 'right' }} value={taxRate} onChange={e => setTaxRate(parseFloat(e.target.value) || 0)} />
264
+ <span style={{ display: 'none' }}>{taxRate}%</span>
265
+ </div>
266
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '1rem', color: 'var(--pk-text-muted)' }}>
267
+ <span className="pk-text-body">Tax Amount</span>
268
+ <span>${taxAmount.toFixed(2)}</span>
269
+ </div>
270
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: '1rem', borderTop: '2px solid var(--pk-border)', paddingTop: '1rem' }}>
271
+ <span className="pk-heading-md">Total Due</span>
272
+ <span className="pk-heading-lg" style={{ color: 'var(--pk-primary)' }}>${grandTotal.toFixed(2)}</span>
273
+ </div>
274
+ </div>
275
+ </div>
276
+
277
+ <div className="no-print" style={{ display: 'flex', justifyContent: 'flex-end', gap: '1rem', marginTop: '3rem' }}>
278
+ <button className="pk-btn pk-btn-secondary" onClick={() => window.print()}>🖨️ Print to PDF</button>
279
+ <button className="pk-btn pk-btn-primary" onClick={handleSaveInvoice}>✅ Save Invoice</button>
280
+ </div>
281
+ </div>
282
+ );
283
+
284
+ return (
285
+ <div className="pk-container" style={{ minHeight: '100vh' }}>
286
+ <div className="no-print" style={{ marginBottom: '2rem' }}>
287
+ <h1 className="pk-heading-xl">Billing & Invoicing</h1>
288
+ <p className="pk-text-body">Manage clients and generate dynamic invoices.</p>
289
+ </div>
290
+
291
+ {renderNav()}
292
+
293
+ <div style={{ position: 'relative' }}>
294
+ {currentView === 'dashboard' && renderDashboard()}
295
+ {currentView === 'clients' && renderClients()}
296
+ {currentView === 'creator' && renderCreator()}
297
+ </div>
298
+
299
+ <ToastContainer />
300
+ </div>
301
+ );
302
+ }