popsite-ui 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/App.jsx +95 -0
- package/README.md +92 -0
- package/components/layout/PortalHeader.jsx +18 -0
- package/components/layout/SystemSidebar.jsx +33 -0
- package/components/modules/AnalyticsDashboardModule.jsx +17 -0
- package/components/modules/ChatMessagingModule.jsx +17 -0
- package/components/modules/EcommerceStoreModule.jsx +17 -0
- package/components/modules/EventTicketBookingModule.jsx +17 -0
- package/components/modules/FlightBookingModule.jsx +17 -0
- package/components/modules/FoodOrderingModule.jsx +17 -0
- package/components/modules/HospitalAppointmentModule.jsx +17 -0
- package/components/modules/HotelBookingModule.jsx +17 -0
- package/components/modules/InvoiceBillingModule.jsx +17 -0
- package/components/modules/LibraryManagementModule.jsx +17 -0
- package/components/modules/ModuleContentDeck.jsx +44 -0
- package/components/modules/MovieBookingModule.jsx +17 -0
- package/components/modules/QuizExamModule.jsx +17 -0
- package/components/modules/StudentRegistrationModule.jsx +17 -0
- package/components/modules/SystemModuleRenderer.jsx +19 -0
- package/components/modules/SystemModuleTemplate.jsx +62 -0
- package/components/modules/SystemVisualWidget.jsx +123 -0
- package/components/modules/moduleContentMap.js +238 -0
- package/components/modules/moduleEnhancementsMap.js +439 -0
- package/components/modules/systemModuleMap.js +31 -0
- package/components/system/DynamicSystemForm.jsx +154 -0
- package/components/system/SystemHero.jsx +21 -0
- package/components/system/SystemSummaryCard.jsx +53 -0
- package/data/systems/analyticsDashboard.js +48 -0
- package/data/systems/chatMessaging.js +43 -0
- package/data/systems/ecommerceStore.js +50 -0
- package/data/systems/eventTicketBooking.js +50 -0
- package/data/systems/flightBooking.js +38 -0
- package/data/systems/foodOrdering.js +48 -0
- package/data/systems/hospitalAppointment.js +50 -0
- package/data/systems/hotelBooking.js +38 -0
- package/data/systems/index.js +31 -0
- package/data/systems/invoiceBilling.js +50 -0
- package/data/systems/libraryManagement.js +43 -0
- package/data/systems/movieBooking.js +48 -0
- package/data/systems/quizExam.js +38 -0
- package/data/systems/studentRegistration.js +43 -0
- package/dist/popsite-ui.es.js +4368 -0
- package/dist/popsite-ui.umd.js +60 -0
- package/dist/style.css +1 -0
- package/index.html +13 -0
- package/library/index.js +20 -0
- package/main.jsx +15 -0
- package/package.json +40 -0
- package/src/App.jsx +12 -0
- package/src/components/modules/AnalyticsDashboardModule.jsx +224 -0
- package/src/components/modules/ChatMessagingModule.jsx +294 -0
- package/src/components/modules/EcommerceStoreModule.jsx +405 -0
- package/src/components/modules/EventTicketBookingModule.jsx +253 -0
- package/src/components/modules/FlightBookingModule.jsx +399 -0
- package/src/components/modules/FoodOrderingModule.jsx +316 -0
- package/src/components/modules/HospitalAppointmentModule.jsx +267 -0
- package/src/components/modules/HotelBookingModule.jsx +317 -0
- package/src/components/modules/InvoiceBillingModule.jsx +302 -0
- package/src/components/modules/LandingPageModule.jsx +185 -0
- package/src/components/modules/LibraryManagementModule.jsx +189 -0
- package/src/components/modules/MovieBookingModule.jsx +337 -0
- package/src/components/modules/QuizExamModule.jsx +255 -0
- package/src/components/modules/StudentRegistrationModule.jsx +292 -0
- package/src/components/system/SystemHero.jsx +44 -0
- package/src/components/system/SystemSummaryCard.jsx +29 -0
- package/src/components/system/Toast.jsx +69 -0
- package/src/data/systems/analyticsDashboard.js +32 -0
- package/src/data/systems/chatMessaging.js +59 -0
- package/src/data/systems/ecommerceStore.js +84 -0
- package/src/data/systems/eventBooking.js +33 -0
- package/src/data/systems/flightBooking.js +59 -0
- package/src/data/systems/foodOrdering.js +48 -0
- package/src/data/systems/hospitalAppointment.js +48 -0
- package/src/data/systems/hotelBooking.js +59 -0
- package/src/data/systems/invoiceBilling.js +19 -0
- package/src/data/systems/landingPage.js +29 -0
- package/src/data/systems/libraryManagement.js +17 -0
- package/src/data/systems/movieBooking.js +49 -0
- package/src/data/systems/quizExam.js +31 -0
- package/src/data/systems/studentRegistration.js +9 -0
- package/src/index.js +22 -0
- package/src/main.jsx +10 -0
- package/src/styles.css +296 -0
- package/styles.css +820 -0
- package/utils/systemEngine.js +128 -0
- package/vite.config.js +8 -0
- package/vite.lib.config.js +27 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { movieBookingMockData } from '../../data/systems/movieBooking';
|
|
3
|
+
import { useToast } from '../system/Toast';
|
|
4
|
+
|
|
5
|
+
export function MovieBookingModule() {
|
|
6
|
+
const [currentView, setCurrentView] = useState('grid'); // grid, details, selector, ticket
|
|
7
|
+
const [selectedMovie, setSelectedMovie] = useState(null);
|
|
8
|
+
const [selectedShowtime, setSelectedShowtime] = useState('');
|
|
9
|
+
const [selectedSeats, setSelectedSeats] = useState([]);
|
|
10
|
+
const [bookedTickets, setBookedTickets] = useState([]);
|
|
11
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
12
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
13
|
+
|
|
14
|
+
const { showToast, ToastContainer } = useToast();
|
|
15
|
+
|
|
16
|
+
// Simulated persisted state
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const saved = localStorage.getItem('popsite_movie_tickets');
|
|
19
|
+
if (saved) {
|
|
20
|
+
try { setBookedTickets(JSON.parse(saved)); } catch (e) {}
|
|
21
|
+
}
|
|
22
|
+
}, []);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (bookedTickets.length > 0) {
|
|
26
|
+
localStorage.setItem('popsite_movie_tickets', JSON.stringify(bookedTickets));
|
|
27
|
+
}
|
|
28
|
+
}, [bookedTickets]);
|
|
29
|
+
|
|
30
|
+
const withDelay = (callback, delay = 1500) => {
|
|
31
|
+
setIsLoading(true);
|
|
32
|
+
setTimeout(() => {
|
|
33
|
+
setIsLoading(false);
|
|
34
|
+
callback();
|
|
35
|
+
}, delay);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const handleMovieClick = (movie) => {
|
|
39
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
40
|
+
setSelectedMovie(movie);
|
|
41
|
+
setSelectedShowtime('');
|
|
42
|
+
setSelectedSeats([]);
|
|
43
|
+
setCurrentView('details');
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const handleSeatClick = (seatId) => {
|
|
47
|
+
setSelectedSeats(prev =>
|
|
48
|
+
prev.includes(seatId)
|
|
49
|
+
? prev.filter(s => s !== seatId)
|
|
50
|
+
: [...prev, seatId]
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const calculateTotal = () => {
|
|
55
|
+
return selectedSeats.reduce((total, seatId) => {
|
|
56
|
+
// VIP seats are rows V1, V2, V3
|
|
57
|
+
const isVip = seatId.startsWith('V');
|
|
58
|
+
return total + (isVip ? movieBookingMockData.pricing.vip : movieBookingMockData.pricing.standard);
|
|
59
|
+
}, 0);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const purchaseTickets = () => {
|
|
63
|
+
if (selectedSeats.length === 0) {
|
|
64
|
+
showToast('Please select at least one seat', 'error');
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
withDelay(() => {
|
|
68
|
+
const newTicket = {
|
|
69
|
+
id: `TK-${Math.floor(Math.random() * 100000)}`,
|
|
70
|
+
movie: selectedMovie,
|
|
71
|
+
showtime: selectedShowtime,
|
|
72
|
+
seats: selectedSeats,
|
|
73
|
+
total: calculateTotal()
|
|
74
|
+
};
|
|
75
|
+
setBookedTickets(prev => [...prev, newTicket]);
|
|
76
|
+
showToast('Payment successful! Enjoy the movie.', 'success');
|
|
77
|
+
setCurrentView('ticket');
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// ----- Views -----
|
|
82
|
+
|
|
83
|
+
const renderGrid = () => {
|
|
84
|
+
const filtered = movieBookingMockData.movies.filter(m =>
|
|
85
|
+
m.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
86
|
+
m.genre.toLowerCase().includes(searchQuery.toLowerCase())
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div className="pk-container animate-fade-in">
|
|
91
|
+
<div className="pk-flex pk-justify-between pk-items-center" style={{ marginBottom: '2rem' }}>
|
|
92
|
+
<div>
|
|
93
|
+
<h1 className="pk-heading-lg" style={{ marginBottom: '0.5rem' }}>{movieBookingMockData.cinemaName}</h1>
|
|
94
|
+
<p className="pk-text-body">Experience the magic of cinema.</p>
|
|
95
|
+
</div>
|
|
96
|
+
<button
|
|
97
|
+
className="pk-btn pk-btn-outline"
|
|
98
|
+
onClick={() => {
|
|
99
|
+
if(bookedTickets.length > 0) {
|
|
100
|
+
// Just show the most recent ticket for demo purposes
|
|
101
|
+
setCurrentView('ticket');
|
|
102
|
+
} else {
|
|
103
|
+
showToast("No tickets booked yet.", "info");
|
|
104
|
+
}
|
|
105
|
+
}}
|
|
106
|
+
>
|
|
107
|
+
🎫 My Tickets ({bookedTickets.length})
|
|
108
|
+
</button>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<div className="pk-card pk-glass" style={{ padding: '1.5rem', marginBottom: '2rem' }}>
|
|
112
|
+
<input
|
|
113
|
+
type="text"
|
|
114
|
+
className="pk-input pk-w-full"
|
|
115
|
+
placeholder="Search movies by title or genre..."
|
|
116
|
+
value={searchQuery}
|
|
117
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
118
|
+
/>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
{filtered.length === 0 ? (
|
|
122
|
+
<div className="pk-empty-state">
|
|
123
|
+
<div className="pk-empty-icon">🍿</div>
|
|
124
|
+
<h3 className="pk-heading-md">No movies found</h3>
|
|
125
|
+
</div>
|
|
126
|
+
) : (
|
|
127
|
+
<div className="pk-grid pk-grid-responsive">
|
|
128
|
+
{filtered.map(movie => (
|
|
129
|
+
<div key={movie.id} className="pk-card pk-card-interactive" onClick={() => handleMovieClick(movie)} style={{ cursor: 'pointer' }}>
|
|
130
|
+
<div style={{ height: '350px', background: 'var(--pk-primary-light)' }}>
|
|
131
|
+
<img
|
|
132
|
+
src={movie.image}
|
|
133
|
+
alt={movie.title}
|
|
134
|
+
className="pk-w-full pk-h-full pk-object-cover"
|
|
135
|
+
onError={(e) => { e.target.style.display = 'none'; e.target.nextSibling.style.display = 'flex'; }}
|
|
136
|
+
/>
|
|
137
|
+
<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' }}>
|
|
138
|
+
{movie.title[0]}
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
<div className="pk-card-body">
|
|
142
|
+
<span className="pk-badge" style={{ marginBottom: '0.5rem', display: 'inline-block' }}>{movie.rating}</span>
|
|
143
|
+
<h3 className="pk-heading-md" style={{ marginBottom: '0.25rem' }}>{movie.title}</h3>
|
|
144
|
+
<p className="pk-text-sm" style={{ marginBottom: '0.5rem' }}>{movie.genre} • {movie.runtime}</p>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
))}
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const renderDetails = () => (
|
|
155
|
+
<div className="pk-container animate-fade-in">
|
|
156
|
+
<button className="pk-btn pk-btn-outline" style={{ marginBottom: '2rem' }} onClick={() => setCurrentView('grid')}>← Back</button>
|
|
157
|
+
<div className="pk-card pk-flex" style={{ overflow: 'hidden', flexWrap: 'wrap' }}>
|
|
158
|
+
<div style={{ flex: '1 1 300px', minHeight: '400px' }}>
|
|
159
|
+
<img src={selectedMovie.image} alt={selectedMovie.title} className="pk-w-full pk-h-full pk-object-cover" />
|
|
160
|
+
</div>
|
|
161
|
+
<div style={{ flex: '2 1 400px', padding: '3rem' }}>
|
|
162
|
+
<div className="pk-flex pk-items-center pk-gap-4" style={{ marginBottom: '1rem' }}>
|
|
163
|
+
<span className="pk-badge">{selectedMovie.rating}</span>
|
|
164
|
+
<span className="pk-text-sm">{selectedMovie.genre} • {selectedMovie.runtime}</span>
|
|
165
|
+
</div>
|
|
166
|
+
<h1 className="pk-heading-xl" style={{ marginBottom: '1rem' }}>{selectedMovie.title}</h1>
|
|
167
|
+
<p className="pk-text-body" style={{ fontSize: '1.125rem', marginBottom: '2rem' }}>{selectedMovie.synopsis}</p>
|
|
168
|
+
|
|
169
|
+
<h3 className="pk-heading-md" style={{ marginBottom: '1rem' }}>Select Showtime</h3>
|
|
170
|
+
<div className="pk-flex" style={{ gap: '1rem', flexWrap: 'wrap', marginBottom: '2rem' }}>
|
|
171
|
+
{selectedMovie.showtimes.map(time => (
|
|
172
|
+
<button
|
|
173
|
+
key={time}
|
|
174
|
+
className={selectedShowtime === time ? 'pk-btn pk-btn-primary' : 'pk-btn pk-btn-outline'}
|
|
175
|
+
onClick={() => setSelectedShowtime(time)}
|
|
176
|
+
>
|
|
177
|
+
{time}
|
|
178
|
+
</button>
|
|
179
|
+
))}
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<button
|
|
183
|
+
className="pk-btn pk-btn-primary"
|
|
184
|
+
style={{ fontSize: '1.25rem', padding: '1rem 2rem' }}
|
|
185
|
+
disabled={!selectedShowtime}
|
|
186
|
+
onClick={() => setCurrentView('selector')}
|
|
187
|
+
>
|
|
188
|
+
Continue to Seat Selection
|
|
189
|
+
</button>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
const renderSelector = () => {
|
|
196
|
+
// Generate a mock matrix layout
|
|
197
|
+
const rows = ['A','B','C','D','E','V1','V2'];
|
|
198
|
+
const cols = [1,2,3,4,5,6,7,8,9,10];
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<div className="pk-container animate-fade-in pk-flex pk-flex-col pk-items-center">
|
|
202
|
+
<div className="pk-w-full pk-flex pk-justify-between" style={{ marginBottom: '2rem' }}>
|
|
203
|
+
<button className="pk-btn pk-btn-outline" onClick={() => setCurrentView('details')}>← Back</button>
|
|
204
|
+
<h2 className="pk-heading-md">{selectedMovie.title} - {selectedShowtime}</h2>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
<div className="pk-card" style={{ padding: '3rem', width: '100%', maxWidth: '800px' }}>
|
|
208
|
+
<div style={{ borderBottom: '6px solid var(--pk-primary-light)', paddingBottom: '1rem', textAlign: 'center', marginBottom: '3rem', borderRadius: '50% 50% 0 0 / 20px 20px 0 0' }}>
|
|
209
|
+
<span className="pk-text-muted" style={{ letterSpacing: '0.5em', textTransform: 'uppercase' }}>Screen</span>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<div className="pk-flex pk-flex-col pk-items-center pk-gap-4">
|
|
213
|
+
{rows.map(row => (
|
|
214
|
+
<div key={row} className="pk-flex pk-items-center" style={{ gap: '0.5rem' }}>
|
|
215
|
+
<span style={{ width: '30px', textAlign: 'center', fontWeight: 'bold', color: 'var(--pk-text-muted)' }}>{row}</span>
|
|
216
|
+
<div className="pk-flex" style={{ gap: '0.5rem' }}>
|
|
217
|
+
{cols.map(col => {
|
|
218
|
+
const seatId = `${row}${col}`;
|
|
219
|
+
const isSelected = selectedSeats.includes(seatId);
|
|
220
|
+
const isVip = row.startsWith('V');
|
|
221
|
+
// Mock some random taken seats for realism based on id sum
|
|
222
|
+
const isTaken = (row.charCodeAt(0) + col) % 7 === 0;
|
|
223
|
+
|
|
224
|
+
return (
|
|
225
|
+
<button
|
|
226
|
+
key={seatId}
|
|
227
|
+
disabled={isTaken}
|
|
228
|
+
style={{
|
|
229
|
+
width: '40px', height: '40px',
|
|
230
|
+
borderRadius: '8px 8px 4px 4px',
|
|
231
|
+
border: 'none',
|
|
232
|
+
cursor: isTaken ? 'not-allowed' : 'pointer',
|
|
233
|
+
background: isTaken ? 'var(--pk-border)' :
|
|
234
|
+
isSelected ? 'var(--pk-primary)' :
|
|
235
|
+
isVip ? '#f59e0b' : 'var(--pk-primary-light)',
|
|
236
|
+
color: isSelected ? 'white' : 'transparent',
|
|
237
|
+
transition: 'all 0.2s'
|
|
238
|
+
}}
|
|
239
|
+
onClick={() => handleSeatClick(seatId)}
|
|
240
|
+
>
|
|
241
|
+
{isSelected ? '✓' : ''}
|
|
242
|
+
</button>
|
|
243
|
+
);
|
|
244
|
+
})}
|
|
245
|
+
</div>
|
|
246
|
+
<span style={{ width: '30px' }}></span>
|
|
247
|
+
</div>
|
|
248
|
+
))}
|
|
249
|
+
</div>
|
|
250
|
+
|
|
251
|
+
<div className="pk-flex pk-justify-center" style={{ marginTop: '3rem', gap: '2rem' }}>
|
|
252
|
+
<div className="pk-flex pk-items-center pk-gap-4"><div style={{ width: 20, height: 20, background: 'var(--pk-border)', borderRadius: '4px' }}></div> Unavailable</div>
|
|
253
|
+
<div className="pk-flex pk-items-center pk-gap-4" style={{ gap: '0.5rem' }}><div style={{ width: 20, height: 20, background: 'var(--pk-primary-light)', borderRadius: '4px' }}></div> Standard (${movieBookingMockData.pricing.standard})</div>
|
|
254
|
+
<div className="pk-flex pk-items-center pk-gap-4" style={{ gap: '0.5rem' }}><div style={{ width: 20, height: 20, background: '#f59e0b', borderRadius: '4px' }}></div> VIP (${movieBookingMockData.pricing.vip})</div>
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
{selectedSeats.length > 0 && (
|
|
259
|
+
<div className="pk-card" style={{ position: 'fixed', bottom: '2rem', width: 'calc(100% - 4rem)', maxWidth: '800px', zIndex: 10, padding: '1.5rem', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
260
|
+
<div>
|
|
261
|
+
<div className="pk-text-sm">Selected Seats: <span style={{ fontWeight: 600 }}>{selectedSeats.join(', ')}</span></div>
|
|
262
|
+
<div className="pk-heading-md" style={{ color: 'var(--pk-primary)' }}>Total: ${calculateTotal().toFixed(2)}</div>
|
|
263
|
+
</div>
|
|
264
|
+
<button className="pk-btn pk-btn-primary" style={{ padding: '1rem 2rem', fontSize: '1.125rem' }} onClick={purchaseTickets}>
|
|
265
|
+
Pay Now
|
|
266
|
+
</button>
|
|
267
|
+
</div>
|
|
268
|
+
)}
|
|
269
|
+
</div>
|
|
270
|
+
);
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const renderTicket = () => {
|
|
274
|
+
const ticket = bookedTickets[bookedTickets.length - 1];
|
|
275
|
+
if (!ticket) return null;
|
|
276
|
+
|
|
277
|
+
return (
|
|
278
|
+
<div className="pk-container animate-fade-in pk-flex pk-justify-center pk-items-center" style={{ minHeight: '80vh' }}>
|
|
279
|
+
<div className="pk-card" style={{ width: '100%', maxWidth: '600px', overflow: 'hidden' }}>
|
|
280
|
+
<div className="pk-card-header" style={{ background: 'linear-gradient(135deg, var(--pk-primary), var(--pk-secondary))', color: 'white', padding: '3rem 2rem', textAlign: 'center' }}>
|
|
281
|
+
<h1 className="pk-heading-lg" style={{ marginBottom: '0.5rem' }}>{movieBookingMockData.cinemaName}</h1>
|
|
282
|
+
<p className="pk-text-body" style={{ color: 'rgba(255,255,255,0.8)' }}>Admit {ticket.seats.length}</p>
|
|
283
|
+
</div>
|
|
284
|
+
<div className="pk-card-body" style={{ padding: '3rem 2rem' }}>
|
|
285
|
+
<h2 className="pk-heading-xl" style={{ marginBottom: '1rem' }}>{ticket.movie.title}</h2>
|
|
286
|
+
|
|
287
|
+
<div className="pk-grid" style={{ gridTemplateColumns: '1fr 1fr', gap: '2rem', marginBottom: '2rem' }}>
|
|
288
|
+
<div>
|
|
289
|
+
<p className="pk-label pk-text-muted">Date & Time</p>
|
|
290
|
+
<p className="pk-heading-md">Today, {ticket.showtime}</p>
|
|
291
|
+
</div>
|
|
292
|
+
<div>
|
|
293
|
+
<p className="pk-label pk-text-muted">Seats</p>
|
|
294
|
+
<p className="pk-heading-md" style={{ color: 'var(--pk-primary)' }}>{ticket.seats.join(', ')}</p>
|
|
295
|
+
</div>
|
|
296
|
+
<div>
|
|
297
|
+
<p className="pk-label pk-text-muted">Screen</p>
|
|
298
|
+
<p className="pk-heading-md">IMAX 3</p>
|
|
299
|
+
</div>
|
|
300
|
+
<div>
|
|
301
|
+
<p className="pk-label pk-text-muted">Booking ID</p>
|
|
302
|
+
<p className="pk-heading-md">{ticket.id}</p>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
|
|
306
|
+
<div style={{ borderTop: '2px dashed var(--pk-border)', paddingTop: '2rem', display: 'flex', justifyContent: 'center' }}>
|
|
307
|
+
{/* Mock Barcode */}
|
|
308
|
+
<div style={{ height: '60px', width: '80%', background: 'linear-gradient(90deg, black 0%, black 5%, transparent 5%, transparent 10%, black 10%, black 20%, transparent 20%, transparent 25%, black 25%, black 40%, transparent 40%, transparent 45%, black 45%, black 50%, transparent 50%, transparent 60%, black 60%, black 75%, transparent 75%, transparent 80%, black 80%, black 95%, transparent 95%, transparent 100%)' }}></div>
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
<div className="pk-card-footer pk-flex pk-justify-between">
|
|
312
|
+
<button className="pk-btn pk-btn-outline" onClick={() => setCurrentView('grid')}>Home</button>
|
|
313
|
+
<button className="pk-btn pk-btn-secondary" onClick={() => window.print()}>Print Ticket</button>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
);
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
return (
|
|
321
|
+
<div style={{ position: 'relative', minHeight: '100vh' }}>
|
|
322
|
+
{isLoading && (
|
|
323
|
+
<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)' }}>
|
|
324
|
+
<div className="pk-skeleton" style={{ width: '80px', height: '80px', borderRadius: '50%', marginBottom: '1rem' }}></div>
|
|
325
|
+
<h3 className="pk-heading-md">Processing...</h3>
|
|
326
|
+
</div>
|
|
327
|
+
)}
|
|
328
|
+
|
|
329
|
+
{currentView === 'grid' && renderGrid()}
|
|
330
|
+
{currentView === 'details' && renderDetails()}
|
|
331
|
+
{currentView === 'selector' && renderSelector()}
|
|
332
|
+
{currentView === 'ticket' && renderTicket()}
|
|
333
|
+
|
|
334
|
+
<ToastContainer />
|
|
335
|
+
</div>
|
|
336
|
+
);
|
|
337
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { quizExamMockData } from '../../data/systems/quizExam';
|
|
3
|
+
import { useToast } from '../system/Toast';
|
|
4
|
+
|
|
5
|
+
export function QuizExamModule() {
|
|
6
|
+
const [currentView, setCurrentView] = useState('selection'); // selection, exam, results
|
|
7
|
+
const [activeSubject, setActiveSubject] = useState(null);
|
|
8
|
+
|
|
9
|
+
// Exam State
|
|
10
|
+
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
|
|
11
|
+
const [answers, setAnswers] = useState({});
|
|
12
|
+
const [timeRemaining, setTimeRemaining] = useState(0); // in seconds
|
|
13
|
+
const [score, setScore] = useState(0);
|
|
14
|
+
const [isPassed, setIsPassed] = useState(false);
|
|
15
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
16
|
+
|
|
17
|
+
const { showToast, ToastContainer } = useToast();
|
|
18
|
+
|
|
19
|
+
const withDelay = (callback, delay = 1000) => {
|
|
20
|
+
setIsLoading(true);
|
|
21
|
+
setTimeout(() => {
|
|
22
|
+
setIsLoading(false);
|
|
23
|
+
callback();
|
|
24
|
+
}, delay);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Timer Effect
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
let timer;
|
|
30
|
+
if (currentView === 'exam' && timeRemaining > 0) {
|
|
31
|
+
timer = setInterval(() => setTimeRemaining(prev => prev - 1), 1000);
|
|
32
|
+
} else if (currentView === 'exam' && timeRemaining === 0) {
|
|
33
|
+
// Auto-submit when time reaches 00:00
|
|
34
|
+
handleAutoSubmit();
|
|
35
|
+
}
|
|
36
|
+
return () => clearInterval(timer);
|
|
37
|
+
}, [currentView, timeRemaining]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
38
|
+
|
|
39
|
+
const handleAutoSubmit = () => {
|
|
40
|
+
showToast("Time's up! Auto-submitting exam.", "info");
|
|
41
|
+
calculateResults();
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const handleStartExam = (subject) => {
|
|
45
|
+
withDelay(() => {
|
|
46
|
+
setActiveSubject(subject);
|
|
47
|
+
setAnswers({});
|
|
48
|
+
setCurrentQuestionIndex(0);
|
|
49
|
+
setTimeRemaining(subject.duration * 60);
|
|
50
|
+
setCurrentView('exam');
|
|
51
|
+
showToast(`Started: ${subject.title}`, 'success');
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const calculateResults = () => {
|
|
56
|
+
withDelay(() => {
|
|
57
|
+
let correctCount = 0;
|
|
58
|
+
activeSubject.questions.forEach(q => {
|
|
59
|
+
if (answers[q.id] === q.correct) correctCount++;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const finalScore = (correctCount / activeSubject.questions.length) * 100;
|
|
63
|
+
setScore(finalScore);
|
|
64
|
+
setIsPassed(finalScore >= activeSubject.passingScore);
|
|
65
|
+
setCurrentView('results');
|
|
66
|
+
showToast('Exam submitted successfully.', 'success');
|
|
67
|
+
}, 2000);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const formatTime = (seconds) => {
|
|
71
|
+
const m = Math.floor(seconds / 60).toString().padStart(2, '0');
|
|
72
|
+
const s = (seconds % 60).toString().padStart(2, '0');
|
|
73
|
+
return `${m}:${s}`;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const handleAnswerSelect = (qId, option) => {
|
|
77
|
+
setAnswers(prev => ({ ...prev, [qId]: option }));
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Views
|
|
81
|
+
const renderSelection = () => (
|
|
82
|
+
<div className="pk-container animate-fade-in" style={{ minHeight: '80vh', display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
|
|
83
|
+
<div style={{ textAlign: 'center', marginBottom: '3rem' }}>
|
|
84
|
+
<h1 className="pk-heading-xl" style={{ marginBottom: '1rem' }}>{quizExamMockData.institutionName} Portal</h1>
|
|
85
|
+
<p className="pk-text-body" style={{ fontSize: '1.25rem' }}>Select a subject to begin your assessment.</p>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<div className="pk-grid" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))' }}>
|
|
89
|
+
{quizExamMockData.subjects.map(subject => (
|
|
90
|
+
<div key={subject.id} className="pk-card pk-card-interactive" style={{ padding: '2rem', display: 'flex', flexDirection: 'column' }}>
|
|
91
|
+
<div style={{ fontSize: '3rem', marginBottom: '1rem', opacity: 0.9 }}>{subject.icon}</div>
|
|
92
|
+
<h3 className="pk-heading-md" style={{ marginBottom: '0.5rem' }}>{subject.title}</h3>
|
|
93
|
+
<div style={{ marginBottom: '2rem', flex: 1, display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
|
94
|
+
<p className="pk-text-sm">⏱️ Duration: <span style={{ fontWeight: 600 }}>{subject.duration} Minutes</span></p>
|
|
95
|
+
<p className="pk-text-sm">📝 Questions: <span style={{ fontWeight: 600 }}>{subject.questions.length}</span></p>
|
|
96
|
+
<p className="pk-text-sm">🎯 Passing Score: <span style={{ fontWeight: 600 }}>{subject.passingScore}%</span></p>
|
|
97
|
+
</div>
|
|
98
|
+
<button className="pk-btn pk-btn-primary pk-w-full" onClick={() => handleStartExam(subject)}>
|
|
99
|
+
Start Exam
|
|
100
|
+
</button>
|
|
101
|
+
</div>
|
|
102
|
+
))}
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const renderExam = () => {
|
|
108
|
+
const question = activeSubject.questions[currentQuestionIndex];
|
|
109
|
+
const isLast = currentQuestionIndex === activeSubject.questions.length - 1;
|
|
110
|
+
const progress = ((currentQuestionIndex + 1) / activeSubject.questions.length) * 100;
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div className="pk-container animate-fade-in" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', paddingTop: '4rem' }}>
|
|
114
|
+
{/* Top Info Bar */}
|
|
115
|
+
<div className="pk-card pk-glass" style={{ width: '100%', maxWidth: '800px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '1.5rem', marginBottom: '2rem', position: 'sticky', top: '1rem', zIndex: 10 }}>
|
|
116
|
+
<div>
|
|
117
|
+
<h3 className="pk-heading-md">{activeSubject.title}</h3>
|
|
118
|
+
<p className="pk-text-sm">Question {currentQuestionIndex + 1} of {activeSubject.questions.length}</p>
|
|
119
|
+
</div>
|
|
120
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
|
|
121
|
+
<span className="pk-text-sm">Time Remaining:</span>
|
|
122
|
+
<span className="pk-badge" style={{ fontSize: '1.5rem', background: timeRemaining < 120 ? 'var(--pk-danger)' : 'var(--pk-primary)', color: 'white', padding: '0.5rem 1rem' }}>
|
|
123
|
+
{formatTime(timeRemaining)}
|
|
124
|
+
</span>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
{/* Question Area */}
|
|
129
|
+
<div className="pk-card" style={{ width: '100%', maxWidth: '800px', padding: '3rem', minHeight: '400px', display: 'flex', flexDirection: 'column' }}>
|
|
130
|
+
<h2 className="pk-heading-lg" style={{ marginBottom: '2.5rem' }}>{question.text}</h2>
|
|
131
|
+
|
|
132
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem', flex: 1 }}>
|
|
133
|
+
{question.options.map((opt, idx) => {
|
|
134
|
+
const isSelected = answers[question.id] === opt;
|
|
135
|
+
return (
|
|
136
|
+
<button
|
|
137
|
+
key={idx}
|
|
138
|
+
className={isSelected ? 'pk-btn pk-btn-primary' : 'pk-btn pk-btn-outline'}
|
|
139
|
+
style={{
|
|
140
|
+
justifyContent: 'flex-start',
|
|
141
|
+
padding: '1.5rem',
|
|
142
|
+
fontSize: '1.125rem',
|
|
143
|
+
textAlign: 'left',
|
|
144
|
+
border: isSelected ? '2px solid var(--pk-primary)' : '2px solid var(--pk-border)'
|
|
145
|
+
}}
|
|
146
|
+
onClick={() => handleAnswerSelect(question.id, opt)}
|
|
147
|
+
>
|
|
148
|
+
<span style={{
|
|
149
|
+
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
|
150
|
+
width: '24px', height: '24px', borderRadius: '50%', marginRight: '1rem',
|
|
151
|
+
background: isSelected ? 'white' : 'transparent',
|
|
152
|
+
color: isSelected ? 'var(--pk-primary)' : 'var(--pk-text-muted)',
|
|
153
|
+
border: isSelected ? 'none' : '2px solid var(--pk-border)',
|
|
154
|
+
fontWeight: 'bold', fontSize: '0.875rem'
|
|
155
|
+
}}>
|
|
156
|
+
{String.fromCharCode(65 + idx)}
|
|
157
|
+
</span>
|
|
158
|
+
{opt}
|
|
159
|
+
</button>
|
|
160
|
+
)
|
|
161
|
+
})}
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
{/* Pagination / Submit */}
|
|
165
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: '3rem', paddingTop: '2rem', borderTop: '1px solid var(--pk-border)' }}>
|
|
166
|
+
<button
|
|
167
|
+
className="pk-btn pk-btn-outline"
|
|
168
|
+
disabled={currentQuestionIndex === 0}
|
|
169
|
+
onClick={() => setCurrentQuestionIndex(prev => prev - 1)}
|
|
170
|
+
>
|
|
171
|
+
← Previous
|
|
172
|
+
</button>
|
|
173
|
+
|
|
174
|
+
{/* Visual Progress inside card */}
|
|
175
|
+
<div style={{ width: '150px', background: 'var(--pk-bg-main)', height: '6px', borderRadius: '3px', position: 'relative' }}>
|
|
176
|
+
<div style={{ position: 'absolute', top: 0, left: 0, height: '100%', background: 'var(--pk-primary)', borderRadius: '3px', width: `${progress}%`, transition: 'width 0.3s' }}></div>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
{isLast ? (
|
|
180
|
+
<button className="pk-btn pk-btn-primary" onClick={calculateResults}>Submit Exam ✅</button>
|
|
181
|
+
) : (
|
|
182
|
+
<button className="pk-btn pk-btn-primary" onClick={() => setCurrentQuestionIndex(prev => prev + 1)}>Next →</button>
|
|
183
|
+
)}
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const renderResults = () => {
|
|
191
|
+
// Inline CSS Circle calculation
|
|
192
|
+
const offset = 283 - (283 * score) / 100;
|
|
193
|
+
const strokeColor = isPassed ? 'var(--pk-success)' : 'var(--pk-danger)';
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<div className="pk-container animate-fade-in" style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '80vh' }}>
|
|
197
|
+
<div className="pk-card" style={{ width: '100%', maxWidth: '600px', textAlign: 'center', overflow: 'hidden' }}>
|
|
198
|
+
<div style={{ background: isPassed ? 'var(--pk-success)' : 'var(--pk-danger)', color: 'white', padding: '2rem' }}>
|
|
199
|
+
<h1 className="pk-heading-xl" style={{ marginBottom: '0.5rem' }}>{isPassed ? 'Congratulations!' : 'Exam Failed'}</h1>
|
|
200
|
+
<p style={{ fontSize: '1.25rem', opacity: 0.9 }}>{activeSubject.title}</p>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
<div className="pk-card-body" style={{ padding: '4rem 2rem', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
|
204
|
+
{/* Pure CSS Progress Circle */}
|
|
205
|
+
<div style={{ position: 'relative', width: '160px', height: '160px', marginBottom: '2rem' }}>
|
|
206
|
+
<svg viewBox="0 0 100 100" style={{ width: '100%', height: '100%', transform: 'rotate(-90deg)' }}>
|
|
207
|
+
<circle cx="50" cy="50" r="45" fill="none" stroke="var(--pk-border)" strokeWidth="8" />
|
|
208
|
+
<circle
|
|
209
|
+
cx="50" cy="50" r="45" fill="none" stroke={strokeColor} strokeWidth="8"
|
|
210
|
+
strokeDasharray="283" strokeDashoffset={offset} strokeLinecap="round"
|
|
211
|
+
style={{ transition: 'stroke-dashoffset 1.5s ease-out' }}
|
|
212
|
+
/>
|
|
213
|
+
</svg>
|
|
214
|
+
<div style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
|
|
215
|
+
<span className="pk-heading-lg" style={{ lineHeight: 1 }}>{Math.round(score)}%</span>
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
<div style={{ display: 'flex', gap: '3rem', marginBottom: '2rem' }}>
|
|
220
|
+
<div>
|
|
221
|
+
<p className="pk-text-muted pk-label">Passing Score</p>
|
|
222
|
+
<p className="pk-heading-md">{activeSubject.passingScore}%</p>
|
|
223
|
+
</div>
|
|
224
|
+
<div>
|
|
225
|
+
<p className="pk-text-muted pk-label">Time Spent</p>
|
|
226
|
+
<p className="pk-heading-md">{formatTime((activeSubject.duration * 60) - timeRemaining)}</p>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
<div className="pk-card-footer" style={{ padding: '1.5rem', display: 'flex', justifyContent: 'center', gap: '1rem' }}>
|
|
232
|
+
<button className="pk-btn pk-btn-outline" onClick={() => setCurrentView('selection')}>Return to Dashboard</button>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
);
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
return (
|
|
240
|
+
<div style={{ position: 'relative', minHeight: '100vh', backgroundColor: 'var(--pk-bg-main)' }}>
|
|
241
|
+
{isLoading && (
|
|
242
|
+
<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)' }}>
|
|
243
|
+
<div className="pk-skeleton" style={{ width: '80px', height: '80px', borderRadius: '50%', marginBottom: '1rem' }}></div>
|
|
244
|
+
<h3 className="pk-heading-md">Processing...</h3>
|
|
245
|
+
</div>
|
|
246
|
+
)}
|
|
247
|
+
|
|
248
|
+
{currentView === 'selection' && renderSelection()}
|
|
249
|
+
{currentView === 'exam' && renderExam()}
|
|
250
|
+
{currentView === 'results' && renderResults()}
|
|
251
|
+
|
|
252
|
+
<ToastContainer />
|
|
253
|
+
</div>
|
|
254
|
+
);
|
|
255
|
+
}
|