onairos 2.2.0 → 2.3.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/LARAVEL_INTEGRATION_GUIDE.md +643 -0
- package/LARAVEL_TECHNICAL_EXPLANATION.md +465 -0
- package/README.md +122 -43
- package/dist/onairos-laravel.js +2 -0
- package/dist/onairos-laravel.js.map +1 -0
- package/dist/onairos.bundle.js +1 -1
- package/dist/onairos.bundle.js.map +1 -1
- package/dist/onairos.esm.js +1 -1
- package/dist/onairos.esm.js.map +1 -1
- package/laravel.txt +430 -0
- package/package.json +31 -2
- package/src/components/DataRequest.js +35 -97
- package/src/laravel/OnairosVue.vue +398 -0
- package/src/laravel/blade-helpers.js +263 -0
- package/src/laravel/vite-plugin.js +179 -0
- package/src/overlay/CheckBox.jsx +83 -23
- package/src/overlay/IndividualConnection.js +145 -15
- package/src/overlay/IndividualConnectionNew.jsx +101 -22
- package/tests/laravel/examples/blade-example.test.js +283 -0
- package/tests/laravel/laravel-integration.test.js +647 -0
- package/tests/laravel/setup.js +84 -0
- package/tests/laravel/vitest.config.js +20 -0
- package/webpack.config.js +46 -13
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onairos Laravel Blade Integration Helpers
|
|
3
|
+
*
|
|
4
|
+
* This file provides utilities for integrating Onairos components
|
|
5
|
+
* directly in PHP Blade templates without requiring React setup.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Global Onairos instance for Blade templates
|
|
9
|
+
let onairosInstance = null;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Initialize Onairos for Blade template usage
|
|
13
|
+
* @param {Object} config - Global configuration
|
|
14
|
+
*/
|
|
15
|
+
export function initializeOnairosForBlade(config = {}) {
|
|
16
|
+
const defaultConfig = {
|
|
17
|
+
apiKey: null,
|
|
18
|
+
baseUrl: 'https://api2.onairos.uk',
|
|
19
|
+
testMode: false,
|
|
20
|
+
autoDetectMobile: true,
|
|
21
|
+
globalStyles: true
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
window.OnairosConfig = { ...defaultConfig, ...config };
|
|
25
|
+
|
|
26
|
+
// Inject global styles if enabled
|
|
27
|
+
if (window.OnairosConfig.globalStyles) {
|
|
28
|
+
injectOnairosStyles();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Initialize mobile detection
|
|
32
|
+
window.OnairosUtils = {
|
|
33
|
+
isMobile: detectMobileDevice(),
|
|
34
|
+
detectMobile: detectMobileDevice
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
console.log('🔥 Onairos initialized for Laravel Blade templates');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create an Onairos button element that can be used in Blade templates
|
|
42
|
+
* @param {string} targetElementId - ID of the element to mount to
|
|
43
|
+
* @param {Object} options - Onairos button options
|
|
44
|
+
*/
|
|
45
|
+
export function createOnairosButton(targetElementId, options = {}) {
|
|
46
|
+
const element = document.getElementById(targetElementId);
|
|
47
|
+
if (!element) {
|
|
48
|
+
console.error(`❌ Element with ID "${targetElementId}" not found`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const defaultOptions = {
|
|
53
|
+
requestData: ['email', 'profile'],
|
|
54
|
+
webpageName: 'Laravel App',
|
|
55
|
+
testMode: window.OnairosConfig?.testMode || false,
|
|
56
|
+
autoFetch: true,
|
|
57
|
+
buttonType: 'pill',
|
|
58
|
+
textColor: 'black'
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const config = { ...defaultOptions, ...options };
|
|
62
|
+
|
|
63
|
+
// Create button HTML
|
|
64
|
+
element.innerHTML = `
|
|
65
|
+
<div class="onairos-button-container">
|
|
66
|
+
<button
|
|
67
|
+
id="${targetElementId}-btn"
|
|
68
|
+
class="onairos-btn onairos-btn-${config.buttonType}"
|
|
69
|
+
data-onairos-config='${JSON.stringify(config)}'
|
|
70
|
+
>
|
|
71
|
+
<span class="onairos-btn-text" style="color: ${config.textColor}">
|
|
72
|
+
Connect with Onairos
|
|
73
|
+
</span>
|
|
74
|
+
<span class="onairos-btn-loading" style="display: none;">
|
|
75
|
+
Loading...
|
|
76
|
+
</span>
|
|
77
|
+
</button>
|
|
78
|
+
</div>
|
|
79
|
+
`;
|
|
80
|
+
|
|
81
|
+
// Add event listener
|
|
82
|
+
const button = document.getElementById(`${targetElementId}-btn`);
|
|
83
|
+
button.addEventListener('click', () => handleOnairosButtonClick(config));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Handle Onairos button click in Blade context
|
|
88
|
+
* @param {Object} config - Button configuration
|
|
89
|
+
*/
|
|
90
|
+
function handleOnairosButtonClick(config) {
|
|
91
|
+
// For Blade templates, we'll use the popup/iframe approach
|
|
92
|
+
if (window.OnairosUtils.isMobile) {
|
|
93
|
+
handleMobileFlow(config);
|
|
94
|
+
} else {
|
|
95
|
+
handleDesktopFlow(config);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Handle mobile OAuth flow (redirect-based)
|
|
101
|
+
* @param {Object} config - Configuration
|
|
102
|
+
*/
|
|
103
|
+
function handleMobileFlow(config) {
|
|
104
|
+
const authUrl = buildAuthUrl(config);
|
|
105
|
+
window.location.href = authUrl;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Handle desktop OAuth flow (popup-based)
|
|
110
|
+
* @param {Object} config - Configuration
|
|
111
|
+
*/
|
|
112
|
+
function handleDesktopFlow(config) {
|
|
113
|
+
const authUrl = buildAuthUrl(config);
|
|
114
|
+
|
|
115
|
+
const popup = window.open(
|
|
116
|
+
authUrl,
|
|
117
|
+
'onairosAuth',
|
|
118
|
+
'width=450,height=700,scrollbars=yes,resizable=yes'
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
if (popup) {
|
|
122
|
+
const checkClosed = setInterval(() => {
|
|
123
|
+
if (popup.closed) {
|
|
124
|
+
clearInterval(checkClosed);
|
|
125
|
+
handleAuthComplete(config);
|
|
126
|
+
}
|
|
127
|
+
}, 1000);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Build authentication URL for OAuth flow
|
|
133
|
+
* @param {Object} config - Configuration
|
|
134
|
+
* @returns {string} Auth URL
|
|
135
|
+
*/
|
|
136
|
+
function buildAuthUrl(config) {
|
|
137
|
+
const baseUrl = window.OnairosConfig.baseUrl;
|
|
138
|
+
const params = new URLSearchParams({
|
|
139
|
+
webpageName: config.webpageName,
|
|
140
|
+
requestData: JSON.stringify(config.requestData),
|
|
141
|
+
testMode: config.testMode,
|
|
142
|
+
returnUrl: window.location.href
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
return `${baseUrl}/auth/laravel?${params.toString()}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Handle authentication completion
|
|
150
|
+
* @param {Object} config - Configuration
|
|
151
|
+
*/
|
|
152
|
+
function handleAuthComplete(config) {
|
|
153
|
+
if (config.onComplete && typeof config.onComplete === 'function') {
|
|
154
|
+
config.onComplete({
|
|
155
|
+
success: true,
|
|
156
|
+
timestamp: new Date().toISOString()
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Trigger custom event for Laravel apps to listen to
|
|
161
|
+
const event = new CustomEvent('onairosAuthComplete', {
|
|
162
|
+
detail: { config, success: true }
|
|
163
|
+
});
|
|
164
|
+
window.dispatchEvent(event);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Detect if device is mobile
|
|
169
|
+
* @returns {boolean}
|
|
170
|
+
*/
|
|
171
|
+
function detectMobileDevice() {
|
|
172
|
+
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
|
|
173
|
+
(window.innerWidth <= 768);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Inject Onairos styles into the page
|
|
178
|
+
*/
|
|
179
|
+
function injectOnairosStyles() {
|
|
180
|
+
if (document.getElementById('onairos-styles')) return;
|
|
181
|
+
|
|
182
|
+
const styles = `
|
|
183
|
+
<style id="onairos-styles">
|
|
184
|
+
.onairos-button-container {
|
|
185
|
+
display: inline-block;
|
|
186
|
+
margin: 10px 0;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.onairos-btn {
|
|
190
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
191
|
+
border: none;
|
|
192
|
+
border-radius: 25px;
|
|
193
|
+
padding: 12px 24px;
|
|
194
|
+
color: white;
|
|
195
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
196
|
+
font-size: 14px;
|
|
197
|
+
font-weight: 600;
|
|
198
|
+
cursor: pointer;
|
|
199
|
+
transition: all 0.3s ease;
|
|
200
|
+
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.onairos-btn:hover {
|
|
204
|
+
transform: translateY(-2px);
|
|
205
|
+
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.onairos-btn-pill {
|
|
209
|
+
border-radius: 25px;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.onairos-btn-icon {
|
|
213
|
+
border-radius: 50%;
|
|
214
|
+
width: 45px;
|
|
215
|
+
height: 45px;
|
|
216
|
+
padding: 0;
|
|
217
|
+
display: flex;
|
|
218
|
+
align-items: center;
|
|
219
|
+
justify-content: center;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.onairos-btn-loading {
|
|
223
|
+
color: #888;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
@media (max-width: 768px) {
|
|
227
|
+
.onairos-btn {
|
|
228
|
+
width: 100%;
|
|
229
|
+
padding: 15px 20px;
|
|
230
|
+
font-size: 16px;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
</style>
|
|
234
|
+
`;
|
|
235
|
+
|
|
236
|
+
document.head.insertAdjacentHTML('beforeend', styles);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Laravel Blade template directive helper
|
|
241
|
+
* Usage in Blade: @onairos(['requestData' => ['email'], 'webpageName' => 'My App'])
|
|
242
|
+
*/
|
|
243
|
+
export function renderOnairosDirective(options = {}) {
|
|
244
|
+
const elementId = `onairos-${Math.random().toString(36).substr(2, 9)}`;
|
|
245
|
+
|
|
246
|
+
return `
|
|
247
|
+
<div id="${elementId}"></div>
|
|
248
|
+
<script>
|
|
249
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
250
|
+
if (window.createOnairosButton) {
|
|
251
|
+
window.createOnairosButton('${elementId}', ${JSON.stringify(options)});
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
</script>
|
|
255
|
+
`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Expose functions globally for Blade template access
|
|
259
|
+
if (typeof window !== 'undefined') {
|
|
260
|
+
window.initializeOnairosForBlade = initializeOnairosForBlade;
|
|
261
|
+
window.createOnairosButton = createOnairosButton;
|
|
262
|
+
window.renderOnairosDirective = renderOnairosDirective;
|
|
263
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onairos Vite Plugin for Laravel
|
|
3
|
+
*
|
|
4
|
+
* This plugin provides seamless integration of Onairos components
|
|
5
|
+
* within Laravel Vite applications, handling both development and
|
|
6
|
+
* production builds.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { resolve } from 'path';
|
|
10
|
+
|
|
11
|
+
export function onairosLaravelPlugin(options = {}) {
|
|
12
|
+
const defaultOptions = {
|
|
13
|
+
autoImport: true,
|
|
14
|
+
injectGlobals: true,
|
|
15
|
+
optimizeDeps: true,
|
|
16
|
+
enableHMR: true,
|
|
17
|
+
bladeSupport: true
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const config = { ...defaultOptions, ...options };
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
name: 'onairos-laravel',
|
|
24
|
+
config(viteConfig, { command }) {
|
|
25
|
+
// Optimize dependencies for faster dev server startup
|
|
26
|
+
if (config.optimizeDeps) {
|
|
27
|
+
viteConfig.optimizeDeps = viteConfig.optimizeDeps || {};
|
|
28
|
+
viteConfig.optimizeDeps.include = viteConfig.optimizeDeps.include || [];
|
|
29
|
+
viteConfig.optimizeDeps.include.push('onairos');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Add alias for easier imports
|
|
33
|
+
viteConfig.resolve = viteConfig.resolve || {};
|
|
34
|
+
viteConfig.resolve.alias = viteConfig.resolve.alias || {};
|
|
35
|
+
viteConfig.resolve.alias['@onairos'] = resolve('node_modules/onairos');
|
|
36
|
+
|
|
37
|
+
// Configure for Laravel Blade support
|
|
38
|
+
if (config.bladeSupport && command === 'serve') {
|
|
39
|
+
viteConfig.server = viteConfig.server || {};
|
|
40
|
+
viteConfig.server.watch = viteConfig.server.watch || {};
|
|
41
|
+
viteConfig.server.watch.include = viteConfig.server.watch.include || [];
|
|
42
|
+
viteConfig.server.watch.include.push('resources/views/**/*.blade.php');
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
configureServer(server) {
|
|
47
|
+
if (config.enableHMR) {
|
|
48
|
+
// Add custom HMR handling for Onairos components
|
|
49
|
+
server.ws.on('onairos:reload', () => {
|
|
50
|
+
server.ws.send({
|
|
51
|
+
type: 'full-reload'
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
transformIndexHtml: {
|
|
58
|
+
enforce: 'pre',
|
|
59
|
+
transform(html, context) {
|
|
60
|
+
if (config.injectGlobals && context.server) {
|
|
61
|
+
// Inject Onairos initialization script for development
|
|
62
|
+
const initScript = `
|
|
63
|
+
<script type="module">
|
|
64
|
+
import { initializeOnairosForBlade } from '/node_modules/onairos/src/laravel/blade-helpers.js';
|
|
65
|
+
|
|
66
|
+
// Initialize Onairos for Laravel Blade templates
|
|
67
|
+
initializeOnairosForBlade({
|
|
68
|
+
testMode: true,
|
|
69
|
+
autoDetectMobile: true,
|
|
70
|
+
globalStyles: true
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Enable HMR for Onairos components
|
|
74
|
+
if (import.meta.hot) {
|
|
75
|
+
import.meta.hot.on('onairos:update', () => {
|
|
76
|
+
console.log('🔥 Onairos components updated');
|
|
77
|
+
window.location.reload();
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
</script>
|
|
81
|
+
`;
|
|
82
|
+
|
|
83
|
+
return html.replace('<head>', `<head>${initScript}`);
|
|
84
|
+
}
|
|
85
|
+
return html;
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
generateBundle(options, bundle) {
|
|
90
|
+
// Add Onairos assets to the build output
|
|
91
|
+
if (config.autoImport) {
|
|
92
|
+
// Create a separate chunk for Laravel integration
|
|
93
|
+
this.emitFile({
|
|
94
|
+
type: 'chunk',
|
|
95
|
+
id: 'onairos-laravel-integration',
|
|
96
|
+
fileName: 'onairos-laravel.js'
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
writeBundle(options, bundle) {
|
|
102
|
+
// Create Laravel-specific integration files
|
|
103
|
+
const laravelIntegrationCode = `
|
|
104
|
+
// Onairos Laravel Integration
|
|
105
|
+
import { initializeOnairosForBlade, createOnairosButton } from 'onairos/blade';
|
|
106
|
+
|
|
107
|
+
// Auto-initialize for production
|
|
108
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
109
|
+
initializeOnairosForBlade({
|
|
110
|
+
testMode: false,
|
|
111
|
+
autoDetectMobile: true,
|
|
112
|
+
globalStyles: true
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Export for manual usage
|
|
117
|
+
window.Onairos = {
|
|
118
|
+
init: initializeOnairosForBlade,
|
|
119
|
+
createButton: createOnairosButton
|
|
120
|
+
};
|
|
121
|
+
`;
|
|
122
|
+
|
|
123
|
+
// Write the integration file
|
|
124
|
+
this.emitFile({
|
|
125
|
+
type: 'asset',
|
|
126
|
+
fileName: 'onairos-laravel-integration.js',
|
|
127
|
+
source: laravelIntegrationCode
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Vue.js specific plugin for Laravel
|
|
134
|
+
export function onairosVuePlugin(options = {}) {
|
|
135
|
+
return {
|
|
136
|
+
name: 'onairos-vue-laravel',
|
|
137
|
+
config(viteConfig) {
|
|
138
|
+
// Add Vue-specific optimizations for Onairos
|
|
139
|
+
viteConfig.optimizeDeps = viteConfig.optimizeDeps || {};
|
|
140
|
+
viteConfig.optimizeDeps.include = viteConfig.optimizeDeps.include || [];
|
|
141
|
+
viteConfig.optimizeDeps.include.push('onairos', 'vue');
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
transform(code, id) {
|
|
145
|
+
// Auto-import Onairos in Vue components
|
|
146
|
+
if (id.endsWith('.vue') && options.autoImport) {
|
|
147
|
+
if (code.includes('OnairosButton') && !code.includes('import.*OnairosButton')) {
|
|
148
|
+
return `import { OnairosButton } from 'onairos';\n${code}`;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// React specific plugin for Laravel
|
|
157
|
+
export function onairosReactPlugin(options = {}) {
|
|
158
|
+
return {
|
|
159
|
+
name: 'onairos-react-laravel',
|
|
160
|
+
config(viteConfig) {
|
|
161
|
+
// Add React-specific optimizations for Onairos
|
|
162
|
+
viteConfig.optimizeDeps = viteConfig.optimizeDeps || {};
|
|
163
|
+
viteConfig.optimizeDeps.include = viteConfig.optimizeDeps.include || [];
|
|
164
|
+
viteConfig.optimizeDeps.include.push('onairos', 'react', 'react-dom');
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
transform(code, id) {
|
|
168
|
+
// Auto-import Onairos in React components
|
|
169
|
+
if ((id.endsWith('.jsx') || id.endsWith('.tsx')) && options.autoImport) {
|
|
170
|
+
if (code.includes('OnairosButton') && !code.includes('import.*OnairosButton')) {
|
|
171
|
+
return `import { OnairosButton } from 'onairos';\n${code}`;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export default onairosLaravelPlugin;
|
package/src/overlay/CheckBox.jsx
CHANGED
|
@@ -8,46 +8,106 @@ function Box({
|
|
|
8
8
|
setSelected,
|
|
9
9
|
number,
|
|
10
10
|
type,
|
|
11
|
-
title
|
|
11
|
+
title
|
|
12
|
+
}) {
|
|
12
13
|
const [isChecked, setIsChecked] = useState(false);
|
|
13
14
|
|
|
14
15
|
const handleCheckboxChange = (newState) => {
|
|
15
16
|
setIsChecked(newState);
|
|
16
|
-
console.log("This data request is active
|
|
17
|
-
console.log("With new Checked state now being:
|
|
17
|
+
console.log("This data request is active:", active);
|
|
18
|
+
console.log("With new Checked state now being:", newState);
|
|
19
|
+
|
|
18
20
|
// Update the counter
|
|
19
21
|
if (newState) {
|
|
20
22
|
changeGranted(1);
|
|
21
23
|
} else {
|
|
22
24
|
changeGranted(-1);
|
|
23
25
|
}
|
|
24
|
-
|
|
26
|
+
|
|
27
|
+
// Call parent callback if provided
|
|
28
|
+
if (onSelectionChange) {
|
|
29
|
+
onSelectionChange(newState);
|
|
30
|
+
}
|
|
25
31
|
};
|
|
26
32
|
|
|
27
|
-
return (
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
33
|
+
return (
|
|
34
|
+
<div className="group">
|
|
35
|
+
<div className="relative">
|
|
36
|
+
{/* Custom styled checkbox with clear visual feedback */}
|
|
37
|
+
<label className="flex items-center cursor-pointer">
|
|
38
|
+
<div className="relative">
|
|
39
|
+
<input
|
|
40
|
+
type="checkbox"
|
|
41
|
+
checked={isChecked}
|
|
42
|
+
disabled={!active}
|
|
43
|
+
onChange={(e) => handleCheckboxChange(e.target.checked)}
|
|
44
|
+
className="sr-only" // Hide default checkbox
|
|
45
|
+
/>
|
|
46
|
+
|
|
47
|
+
{/* Custom checkbox appearance */}
|
|
48
|
+
<div className={`
|
|
49
|
+
w-6 h-6 border-2 rounded-md flex items-center justify-center transition-all duration-200
|
|
50
|
+
${!active
|
|
51
|
+
? 'border-gray-300 bg-gray-100 cursor-not-allowed opacity-50'
|
|
52
|
+
: isChecked
|
|
53
|
+
? 'border-green-500 bg-green-500 shadow-md transform scale-105'
|
|
54
|
+
: 'border-gray-400 bg-white hover:border-green-400 hover:bg-green-50'
|
|
55
|
+
}
|
|
56
|
+
`}>
|
|
57
|
+
{/* Checkmark icon */}
|
|
58
|
+
{isChecked && (
|
|
59
|
+
<svg
|
|
60
|
+
className="w-4 h-4 text-white animate-pulse"
|
|
61
|
+
fill="currentColor"
|
|
62
|
+
viewBox="0 0 20 20"
|
|
63
|
+
>
|
|
64
|
+
<path
|
|
65
|
+
fillRule="evenodd"
|
|
66
|
+
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
|
67
|
+
clipRule="evenodd"
|
|
68
|
+
/>
|
|
69
|
+
</svg>
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
{/* Selection status text */}
|
|
75
|
+
<span className={`ml-3 text-sm font-medium transition-colors duration-200 ${
|
|
76
|
+
!active
|
|
77
|
+
? 'text-gray-400'
|
|
78
|
+
: isChecked
|
|
79
|
+
? 'text-green-600 font-semibold'
|
|
80
|
+
: 'text-gray-700'
|
|
81
|
+
}`}>
|
|
82
|
+
{!active
|
|
83
|
+
? 'Not available'
|
|
84
|
+
: isChecked
|
|
85
|
+
? '✓ Selected'
|
|
86
|
+
: 'Click to select'
|
|
87
|
+
}
|
|
88
|
+
</span>
|
|
89
|
+
</label>
|
|
90
|
+
|
|
91
|
+
{/* Selection indicator badge */}
|
|
92
|
+
{isChecked && active && (
|
|
93
|
+
<div className="absolute -top-2 -right-2 w-4 h-4 bg-green-500 rounded-full flex items-center justify-center animate-bounce">
|
|
94
|
+
<svg className="w-2 h-2 text-white" fill="currentColor" viewBox="0 0 20 20">
|
|
95
|
+
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
|
96
|
+
</svg>
|
|
97
|
+
</div>
|
|
98
|
+
)}
|
|
99
|
+
</div>
|
|
42
100
|
</div>
|
|
43
|
-
|
|
44
|
-
);
|
|
101
|
+
);
|
|
45
102
|
}
|
|
46
103
|
|
|
47
104
|
Box.propTypes = {
|
|
48
105
|
active: PropTypes.bool.isRequired,
|
|
49
|
-
onSelectionChange: PropTypes.func
|
|
50
|
-
|
|
106
|
+
onSelectionChange: PropTypes.func,
|
|
107
|
+
changeGranted: PropTypes.func.isRequired,
|
|
108
|
+
setSelected: PropTypes.func,
|
|
109
|
+
number: PropTypes.number,
|
|
110
|
+
type: PropTypes.string,
|
|
51
111
|
title: PropTypes.string.isRequired
|
|
52
112
|
};
|
|
53
113
|
|