Browser extension for SecureBit Chat — a P2P messenger with military-grade cryptography.
This commit is contained in:
3636
src/app.jsx
Normal file
3636
src/app.jsx
Normal file
File diff suppressed because it is too large
Load Diff
492
src/components/QRScanner.js
Normal file
492
src/components/QRScanner.js
Normal file
@@ -0,0 +1,492 @@
|
||||
// Simple QR Scanner Component using only Html5Qrcode
|
||||
const QRScanner = ({ onScan, onClose, isVisible, continuous = false }) => {
|
||||
const videoRef = React.useRef(null);
|
||||
const qrScannerRef = React.useRef(null);
|
||||
const [error, setError] = React.useState(null);
|
||||
const [isScanning, setIsScanning] = React.useState(false);
|
||||
const [progress, setProgress] = React.useState({ id: null, seq: 0, total: 0 });
|
||||
const [showFocusHint, setShowFocusHint] = React.useState(false);
|
||||
const [manualMode, setManualMode] = React.useState(false);
|
||||
const [scannedParts, setScannedParts] = React.useState(new Set());
|
||||
const [currentQRId, setCurrentQRId] = React.useState(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isVisible) {
|
||||
startScanner();
|
||||
} else {
|
||||
stopScanner();
|
||||
}
|
||||
|
||||
return () => {
|
||||
stopScanner();
|
||||
};
|
||||
}, [isVisible]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const onProgress = (e) => {
|
||||
const { id, seq, total } = e.detail || {};
|
||||
if (!id || !total) return;
|
||||
setProgress({ id, seq, total });
|
||||
|
||||
// Обновляем ID текущего QR кода
|
||||
if (id !== currentQRId) {
|
||||
setCurrentQRId(id);
|
||||
setScannedParts(new Set()); // Сбрасываем сканированные части для нового ID
|
||||
}
|
||||
|
||||
// Добавляем отсканированную часть
|
||||
setScannedParts(prev => new Set([...prev, seq]));
|
||||
};
|
||||
const onComplete = () => {
|
||||
// Close scanner once app signals completion
|
||||
if (!continuous) return;
|
||||
try { stopScanner(); } catch {}
|
||||
};
|
||||
document.addEventListener('qr-scan-progress', onProgress, { passive: true });
|
||||
document.addEventListener('qr-scan-complete', onComplete, { passive: true });
|
||||
return () => {
|
||||
document.removeEventListener('qr-scan-progress', onProgress, { passive: true });
|
||||
document.removeEventListener('qr-scan-complete', onComplete, { passive: true });
|
||||
};
|
||||
}, [currentQRId]);
|
||||
|
||||
// Функция для tap-to-focus
|
||||
const handleTapToFocus = (event, html5Qrcode) => {
|
||||
try {
|
||||
// Показываем подсказку о фокусировке
|
||||
setShowFocusHint(true);
|
||||
setTimeout(() => setShowFocusHint(false), 2000);
|
||||
|
||||
// Получаем координаты клика относительно видео элемента
|
||||
const rect = event.target.getBoundingClientRect();
|
||||
const x = event.clientX - rect.left;
|
||||
const y = event.clientY - rect.top;
|
||||
|
||||
// Нормализуем координаты (0-1)
|
||||
const normalizedX = x / rect.width;
|
||||
const normalizedY = y / rect.height;
|
||||
|
||||
console.log('Tap to focus at:', { x, y, normalizedX, normalizedY });
|
||||
|
||||
// Попытка программной фокусировки (если поддерживается браузером)
|
||||
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
||||
// Это может не работать во всех браузерах, но попробуем
|
||||
console.log('Attempting programmatic focus...');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.warn('Tap to focus error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Функции для ручного управления
|
||||
const toggleManualMode = () => {
|
||||
setManualMode(!manualMode);
|
||||
if (!manualMode) {
|
||||
// При включении ручного режима останавливаем автопрокрутку
|
||||
console.log('Manual mode enabled - auto-scroll disabled');
|
||||
} else {
|
||||
// При выключении ручного режима возобновляем автопрокрутку
|
||||
console.log('Manual mode disabled - auto-scroll enabled');
|
||||
}
|
||||
};
|
||||
|
||||
const resetProgress = () => {
|
||||
setScannedParts(new Set());
|
||||
setCurrentQRId(null);
|
||||
setProgress({ id: null, seq: 0, total: 0 });
|
||||
};
|
||||
|
||||
const startScanner = async () => {
|
||||
try {
|
||||
console.log('Starting QR scanner...');
|
||||
setError(null);
|
||||
setIsScanning(true);
|
||||
|
||||
// Allow camera on HTTP as well; rely on browser permission prompts
|
||||
|
||||
// Check if Html5Qrcode is available
|
||||
if (!window.Html5Qrcode) {
|
||||
setError('QR scanner library not loaded');
|
||||
setIsScanning(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get available cameras first
|
||||
console.log('Getting available cameras...');
|
||||
const cameras = await window.Html5Qrcode.getCameras();
|
||||
console.log('Available cameras:', cameras);
|
||||
|
||||
if (!cameras || cameras.length === 0) {
|
||||
setError('No cameras found on this device');
|
||||
setIsScanning(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear any existing scanner
|
||||
if (qrScannerRef.current) {
|
||||
try {
|
||||
qrScannerRef.current.stop();
|
||||
} catch (e) {
|
||||
console.log('Stopping previous scanner:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Create video element if it doesn't exist
|
||||
if (!videoRef.current) {
|
||||
console.log('Video element not found');
|
||||
setError('Video element not found');
|
||||
setIsScanning(false);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Video element found:', videoRef.current);
|
||||
console.log('Video element ID:', videoRef.current.id);
|
||||
|
||||
// Create Html5Qrcode instance
|
||||
console.log('Creating Html5Qrcode instance...');
|
||||
const html5Qrcode = new window.Html5Qrcode(videoRef.current.id || 'qr-reader');
|
||||
|
||||
// Find back camera (environment facing)
|
||||
let cameraId = cameras[0].id; // Default to first camera
|
||||
let selectedCamera = cameras[0];
|
||||
|
||||
// Look for back camera
|
||||
for (const camera of cameras) {
|
||||
if (camera.label.toLowerCase().includes('back') ||
|
||||
camera.label.toLowerCase().includes('rear') ||
|
||||
camera.label.toLowerCase().includes('environment')) {
|
||||
cameraId = camera.id;
|
||||
selectedCamera = camera;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Available cameras:');
|
||||
cameras.forEach((cam, index) => {
|
||||
console.log(`${index + 1}. ${cam.label} (${cam.id})`);
|
||||
});
|
||||
console.log('Selected camera:', selectedCamera.label, 'ID:', cameraId);
|
||||
|
||||
// Start camera
|
||||
console.log('Starting camera with Html5Qrcode...');
|
||||
const isDesktop = (typeof window !== 'undefined') && ((window.innerWidth || 0) >= 1024);
|
||||
const qrboxSize = isDesktop ? 560 : 360;
|
||||
await html5Qrcode.start(
|
||||
cameraId, // Use specific camera ID
|
||||
{
|
||||
fps: /iPhone|iPad|iPod/i.test(navigator.userAgent) ? 2 : 3,
|
||||
qrbox: { width: qrboxSize, height: qrboxSize },
|
||||
// Улучшенные настройки для мобильных устройств
|
||||
aspectRatio: 1.0,
|
||||
videoConstraints: {
|
||||
focusMode: "continuous", // Непрерывная автофокусировка
|
||||
exposureMode: "continuous", // Непрерывная экспозиция
|
||||
whiteBalanceMode: "continuous", // Непрерывный баланс белого
|
||||
torch: false, // Вспышка выключена по умолчанию
|
||||
facingMode: "environment" // Используем заднюю камеру
|
||||
}
|
||||
},
|
||||
(decodedText, decodedResult) => {
|
||||
console.log('QR Code detected:', decodedText);
|
||||
try {
|
||||
const res = onScan(decodedText);
|
||||
const handleResult = (val) => {
|
||||
const shouldClose = val === true || !continuous;
|
||||
if (shouldClose) {
|
||||
stopScanner();
|
||||
}
|
||||
};
|
||||
if (res && typeof res.then === 'function') {
|
||||
res.then(handleResult).catch((e) => {
|
||||
console.warn('onScan async handler error:', e);
|
||||
if (!continuous) stopScanner();
|
||||
});
|
||||
} else {
|
||||
handleResult(res);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('onScan handler threw:', e);
|
||||
if (!continuous) {
|
||||
stopScanner();
|
||||
}
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
// Ignore decode errors, they're normal during scanning
|
||||
console.log('QR decode error:', error);
|
||||
}
|
||||
);
|
||||
|
||||
// Store scanner reference
|
||||
qrScannerRef.current = html5Qrcode;
|
||||
console.log('QR scanner started successfully');
|
||||
|
||||
// Добавляем обработчик tap-to-focus для мобильных устройств
|
||||
if (videoRef.current) {
|
||||
videoRef.current.addEventListener('click', (event) => {
|
||||
handleTapToFocus(event, html5Qrcode);
|
||||
});
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error starting QR scanner:', err);
|
||||
let errorMessage = 'Failed to start camera';
|
||||
|
||||
if (err.name === 'NotAllowedError') {
|
||||
errorMessage = 'Camera access denied. Please allow camera access and try again.';
|
||||
} else if (err.name === 'NotFoundError') {
|
||||
errorMessage = 'No camera found on this device.';
|
||||
} else if (err.name === 'NotSupportedError') {
|
||||
errorMessage = 'Camera not supported on this device.';
|
||||
} else if (err.name === 'NotReadableError') {
|
||||
errorMessage = 'Camera is already in use by another application.';
|
||||
} else if (err.message) {
|
||||
errorMessage = err.message;
|
||||
}
|
||||
|
||||
setError(errorMessage);
|
||||
setIsScanning(false);
|
||||
}
|
||||
};
|
||||
|
||||
const stopScanner = () => {
|
||||
if (qrScannerRef.current) {
|
||||
try {
|
||||
qrScannerRef.current.stop().then(() => {
|
||||
console.log('QR scanner stopped');
|
||||
}).catch((err) => {
|
||||
console.log('Error stopping scanner:', err);
|
||||
});
|
||||
} catch (err) {
|
||||
console.log('Error stopping scanner:', err);
|
||||
}
|
||||
qrScannerRef.current = null;
|
||||
}
|
||||
setIsScanning(false);
|
||||
try {
|
||||
// iOS Safari workaround: small delay before closing modal to release camera
|
||||
if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) {
|
||||
setTimeout(() => {
|
||||
// no-op; allow camera to settle
|
||||
}, 150);
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
stopScanner();
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return React.createElement('div', {
|
||||
className: "fixed inset-0 bg-black/80 flex items-center justify-center z-50"
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'scanner-modal',
|
||||
className: "bg-gray-800 rounded-lg p-6 w-full mx-4 max-w-2xl"
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'scanner-header',
|
||||
className: "flex items-center justify-between mb-4"
|
||||
}, [
|
||||
React.createElement('h3', {
|
||||
key: 'title',
|
||||
className: "text-lg font-medium text-white"
|
||||
}, 'Scan QR Code'),
|
||||
React.createElement('button', {
|
||||
key: 'close-btn',
|
||||
onClick: handleClose,
|
||||
className: "text-gray-400 hover:text-white transition-colors"
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
className: 'fas fa-times text-xl'
|
||||
})
|
||||
])
|
||||
]),
|
||||
|
||||
// Индикатор прогресса сканирования
|
||||
progress.total > 1 && React.createElement('div', {
|
||||
key: 'progress-indicator',
|
||||
className: "mb-4 p-3 bg-gray-800/50 border border-gray-600/30 rounded-lg"
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'progress-header',
|
||||
className: "flex items-center justify-between mb-2"
|
||||
}, [
|
||||
React.createElement('span', {
|
||||
key: 'progress-title',
|
||||
className: "text-sm text-gray-300"
|
||||
}, `QR ID: ${currentQRId ? currentQRId.substring(0, 8) + '...' : 'N/A'}`),
|
||||
React.createElement('span', {
|
||||
key: 'progress-count',
|
||||
className: "text-sm text-blue-400"
|
||||
}, `${scannedParts.size}/${progress.total} scanned`)
|
||||
]),
|
||||
React.createElement('div', {
|
||||
key: 'progress-numbers',
|
||||
className: "flex flex-wrap gap-1"
|
||||
}, Array.from({ length: progress.total }, (_, i) => {
|
||||
const partNumber = i + 1;
|
||||
const isScanned = scannedParts.has(partNumber);
|
||||
return React.createElement('div', {
|
||||
key: `part-${partNumber}`,
|
||||
className: `w-8 h-8 rounded-full flex items-center justify-center text-xs font-medium transition-colors ${
|
||||
isScanned
|
||||
? 'bg-green-500 text-white'
|
||||
: 'bg-gray-600 text-gray-300'
|
||||
}`
|
||||
}, partNumber);
|
||||
}))
|
||||
]),
|
||||
|
||||
// Панель управления
|
||||
progress.total > 1 && React.createElement('div', {
|
||||
key: 'control-panel',
|
||||
className: "mb-4 flex gap-2"
|
||||
}, [
|
||||
React.createElement('button', {
|
||||
key: 'manual-toggle',
|
||||
onClick: toggleManualMode,
|
||||
className: `px-3 py-1 rounded text-xs font-medium transition-colors ${
|
||||
manualMode
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-gray-600 text-gray-300 hover:bg-gray-500'
|
||||
}`
|
||||
}, manualMode ? 'Manual Mode' : 'Auto Mode'),
|
||||
React.createElement('button', {
|
||||
key: 'reset-progress',
|
||||
onClick: resetProgress,
|
||||
className: "px-3 py-1 bg-red-500/20 text-red-400 border border-red-500/20 rounded text-xs font-medium hover:bg-red-500/30"
|
||||
}, 'Reset'),
|
||||
React.createElement('span', {
|
||||
key: 'mode-hint',
|
||||
className: "text-xs text-gray-400 self-center"
|
||||
}, manualMode ? 'Tap to focus, scan manually' : 'Auto-scrolling enabled')
|
||||
]),
|
||||
|
||||
React.createElement('div', {
|
||||
key: 'scanner-content',
|
||||
className: "relative"
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'video-container',
|
||||
id: 'qr-reader',
|
||||
ref: videoRef,
|
||||
className: "w-full h-80 md:h-[32rem] bg-gray-700 rounded-lg"
|
||||
}),
|
||||
|
||||
error && React.createElement('div', {
|
||||
key: 'error',
|
||||
className: "absolute inset-0 flex items-center justify-center bg-red-900/50 rounded-lg"
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'error-content',
|
||||
className: "text-center text-white p-4"
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'error-icon',
|
||||
className: 'fas fa-exclamation-triangle text-2xl mb-2'
|
||||
}),
|
||||
React.createElement('p', {
|
||||
key: 'error-text',
|
||||
className: "text-sm"
|
||||
}, error)
|
||||
])
|
||||
]),
|
||||
|
||||
!error && !isScanning && React.createElement('div', {
|
||||
key: 'loading',
|
||||
className: "absolute inset-0 flex items-center justify-center bg-gray-700/50 rounded-lg"
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'loading-content',
|
||||
className: "text-center text-white"
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'loading-icon',
|
||||
className: 'fas fa-spinner fa-spin text-2xl mb-2'
|
||||
}),
|
||||
React.createElement('p', {
|
||||
key: 'loading-text',
|
||||
className: "text-sm"
|
||||
}, 'Starting camera...')
|
||||
])
|
||||
]),
|
||||
|
||||
!error && isScanning && React.createElement('div', {
|
||||
key: 'scanning-overlay',
|
||||
className: "absolute inset-0 flex items-center justify-center"
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'scanning-content',
|
||||
className: "text-center text-white bg-black/50 rounded-lg px-4 py-2"
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'scanning-icon',
|
||||
className: 'fas fa-qrcode text-xl mb-1'
|
||||
}),
|
||||
React.createElement('p', {
|
||||
key: 'scanning-text',
|
||||
className: "text-xs"
|
||||
}, progress && progress.total > 1 ? `Frames: ${Math.min(progress.seq, progress.total)}/${progress.total}` : 'Point camera at QR code'),
|
||||
React.createElement('p', {
|
||||
key: 'tap-hint',
|
||||
className: "text-xs text-blue-300 mt-1"
|
||||
}, 'Tap screen to focus')
|
||||
])
|
||||
]),
|
||||
|
||||
// Подсказка о фокусировке
|
||||
showFocusHint && React.createElement('div', {
|
||||
key: 'focus-hint',
|
||||
className: "absolute top-4 left-1/2 transform -translate-x-1/2 bg-green-500/90 text-white px-3 py-1 rounded-full text-xs font-medium z-10"
|
||||
}, 'Focusing...'),
|
||||
// Bottom overlay kept simple on mobile
|
||||
]),
|
||||
|
||||
// Дополнительные подсказки для улучшения сканирования
|
||||
React.createElement('div', {
|
||||
key: 'scanning-tips',
|
||||
className: "mt-4 p-3 bg-blue-500/10 border border-blue-500/20 rounded-lg"
|
||||
}, [
|
||||
React.createElement('h4', {
|
||||
key: 'tips-title',
|
||||
className: "text-blue-400 text-sm font-medium mb-2 flex items-center"
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'tips-icon',
|
||||
className: 'fas fa-lightbulb mr-2'
|
||||
}),
|
||||
'Tips for better scanning:'
|
||||
]),
|
||||
React.createElement('ul', {
|
||||
key: 'tips-list',
|
||||
className: "text-xs text-blue-300 space-y-1"
|
||||
}, [
|
||||
React.createElement('li', {
|
||||
key: 'tip-1'
|
||||
}, '• Ensure good lighting'),
|
||||
React.createElement('li', {
|
||||
key: 'tip-2'
|
||||
}, '• Hold phone steady'),
|
||||
React.createElement('li', {
|
||||
key: 'tip-3'
|
||||
}, '• Tap screen to focus'),
|
||||
React.createElement('li', {
|
||||
key: 'tip-4'
|
||||
}, '• Keep QR code in frame')
|
||||
])
|
||||
])
|
||||
|
||||
])
|
||||
]);
|
||||
};
|
||||
|
||||
// Export for use in other files
|
||||
window.QRScanner = QRScanner;
|
||||
console.log('QRScanner component loaded and available on window.QRScanner');
|
||||
1
src/components/QRScanner.jsx
Normal file
1
src/components/QRScanner.jsx
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
320
src/components/ui/ComparisonTable.jsx
Normal file
320
src/components/ui/ComparisonTable.jsx
Normal file
@@ -0,0 +1,320 @@
|
||||
|
||||
|
||||
|
||||
const ComparisonTable = () => {
|
||||
const [selectedFeature, setSelectedFeature] = React.useState(null);
|
||||
|
||||
const messengers = [
|
||||
{
|
||||
name: "SecureBit.chat",
|
||||
logo: <div className="w-8 h-8 bg-orange-500/10 border border-orange-500/20 rounded-lg flex items-center justify-center">
|
||||
<i className="fas fa-shield-halved text-orange-400" />
|
||||
</div>,
|
||||
type: "P2P WebRTC",
|
||||
version: "Latest",
|
||||
color: "orange",
|
||||
},
|
||||
{
|
||||
name: "Signal",
|
||||
logo: (
|
||||
<svg className="w-8 h-8" viewBox="0 0 122.88 122.31" xmlns="http://www.w3.org/2000/svg">
|
||||
<path className="fill-blue-500" d="M27.75,0H95.13a27.83,27.83,0,0,1,27.75,27.75V94.57a27.83,27.83,0,0,1-27.75,27.74H27.75A27.83,27.83,0,0,1,0,94.57V27.75A27.83,27.83,0,0,1,27.75,0Z" />
|
||||
<path className="fill-white" d="M61.44,25.39A35.76,35.76,0,0,0,31.18,80.18L27.74,94.86l14.67-3.44a35.77,35.77,0,1,0,19-66Z" />
|
||||
</svg>
|
||||
),
|
||||
type: "Centralized",
|
||||
version: "Latest",
|
||||
color: "blue",
|
||||
},
|
||||
{
|
||||
name: "Threema",
|
||||
logo: (
|
||||
<svg className="w-8 h-8" viewBox="0 0 122.88 122.88" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="122.88" height="122.88" rx="18.43" fill="#474747" />
|
||||
<path fill="#FFFFFF" d="M44.26,78.48l-19.44,4.8l4.08-16.56c-4.08-5.28-6.48-12-6.48-18.96c0-18.96,17.52-34.32,39.12-34.32c21.6,0,39.12,15.36,39.12,34.32c0,18.96-17.52,34.32-39.12,34.32c-6,0-12-1.2-17.04-3.36L44.26,78.48z M50.26,44.64h-0.48c-0.96,0-1.68,0.72-1.44,1.68v15.6c0,0.96,0.72,1.68,1.68,1.68l23.04,0c0.96,0,1.68-0.72,1.68-1.68v-15.6c0-0.96-0.72-1.68-1.68-1.68h-0.48v-4.32c0-6-5.04-11.04-11.04-11.04S50.5,34.32,50.5,40.32v4.32H50.26z M68.02,44.64h-13.2v-4.32c0-3.6,2.88-6.72,6.72-6.72c3.6,0,6.72,2.88,6.72,6.72v4.32H68.02z" />
|
||||
<circle cx="37.44" cy="97.44" r="6.72" fill="#3fe669" />
|
||||
<circle cx="61.44" cy="97.44" r="6.72" fill="#3fe669" />
|
||||
<circle cx="85.44" cy="97.44" r="6.72" fill="#3fe669" />
|
||||
</svg>
|
||||
),
|
||||
type: "Centralized",
|
||||
version: "Latest",
|
||||
color: "green",
|
||||
},
|
||||
{
|
||||
name: "Session",
|
||||
logo: (
|
||||
<svg className="w-8 h-8" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="1024" height="1024" fill="#333132" />
|
||||
<path fill="#00f782" d="M431 574.8c-.8-7.4-6.7-8.2-10.8-10.6-13.6-7.9-27.5-15.4-41.3-23l-22.5-12.3c-8.5-4.7-17.1-9.2-25.6-14.1-10.5-6-21-11.9-31.1-18.6-18.9-12.5-33.8-29.1-46.3-48.1-8.3-12.6-14.8-26.1-19.2-40.4-6.7-21.7-10.8-44.1-7.8-66.8 1.8-14 4.6-28 9.7-41.6 7.8-20.8 19.3-38.8 34.2-54.8 9.8-10.6 21.2-19.1 33.4-26.8 14.7-9.3 30.7-15.4 47.4-19 13.8-3 28.1-4.3 42.2-4.4 89.9-.4 179.7-.3 269.6 0 12.6 0 25.5 1 37.7 4.1 24.3 6.2 45.7 18.2 63 37 11.2 12.2 20.4 25.8 25.8 41.2 7.3 20.7 12.3 42.1 6.7 64.4-2.1 8.5-2.7 17.5-6.1 25.4-4.7 10.9-10.8 21.2-17.2 31.2-8.7 13.5-20.5 24.3-34.4 32.2-10.1 5.7-21 10.2-32 14.3-18.1 6.7-37.2 5-56.1 5.2-17.2.2-34.5 0-51.7.1-1.7 0-3.4 1.2-5.1 1.9 1.3 1.8 2.1 4.3 3.9 5.3 13.5 7.8 27.2 15.4 40.8 22.9 11 6 22.3 11.7 33.2 17.9 15.2 8.5 30.2 17.4 45.3 26.1 19.3 11.1 34.8 26.4 47.8 44.3 9.7 13.3 17.2 27.9 23 43.5 6.1 16.6 9.2 33.8 10.4 51.3.6 9.1-.7 18.5-1.9 27.6-1.2 9.1-2.7 18.4-5.6 27.1-3.3 10.2-7.4 20.2-12.4 29.6-8.4 15.7-19.6 29.4-32.8 41.4-12.7 11.5-26.8 20.6-42.4 27.6-22.9 10.3-46.9 14.4-71.6 14.5-89.7.3-179.4.2-269.1-.1-12.6 0-25.5-1-37.7-3.9-24.5-5.7-45.8-18-63.3-36.4-11.6-12.3-20.2-26.5-26.6-41.9-2.7-6.4-4.1-13.5-5.4-20.4-1.5-8.1-2.8-16.3-3.1-24.5-.6-15.7 2.8-30.9 8.2-45.4 8.2-22 21.7-40.6 40.2-55.2 10-7.9 21.3-13.7 33.1-18.8 16.6-7.2 34-8.1 51.4-8.5 21.9-.5 43.9-.1 65.9-.1 1.9-.1 3.9-.3 6.2-.4zm96.3-342.4c0 .1 0 .1 0 0-48.3.1-96.6-.6-144.9.5-13.5.3-27.4 3.9-40.1 8.7-14.9 5.6-28.1 14.6-39.9 25.8-20.2 19-32.2 42.2-37.2 68.9-3.6 19-1.4 38.1 4.1 56.5 4.1 13.7 10.5 26.4 18.5 38.4 14.8 22.2 35.7 36.7 58.4 49.2 11 6.1 22.2 11.9 33.2 18 13.5 7.5 26.9 15.1 40.4 22.6 13.1 7.3 26.2 14.5 39.2 21.7 9.7 5.3 19.4 10.7 29.1 16.1 2.9 1.6 4.1.2 4.5-2.4.3-2 .3-4 .3-6.1v-58.8c0-19.9.1-39.9 0-59.8 0-6.6 1.7-12.8 7.6-16.1 3.5-2 8.2-2.8 12.4-2.8 50.3-.2 100.7-.2 151-.1 19.8 0 38.3-4.4 55.1-15.1 23.1-14.8 36.3-36.3 40.6-62.9 3.4-20.8-1-40.9-12.4-58.5-17.8-27.5-43.6-43-76.5-43.6-47.8-.8-95.6-.2-143.4-.2zm-30.6 559.7c45.1 0 90.2-.2 135.3.1 18.9.1 36.6-3.9 53.9-11.1 18.4-7.7 33.6-19.8 46.3-34.9 9.1-10.8 16.2-22.9 20.8-36.5 4.2-12.4 7.4-24.7 7.3-37.9-.1-10.3.2-20.5-3.4-30.5-2.6-7.2-3.4-15.2-6.4-22.1-3.9-8.9-8.9-17.3-14-25.5-12.9-20.8-31.9-34.7-52.8-46.4-10.6-5.9-21.2-11.6-31.8-17.5-10.3-5.7-20.4-11.7-30.7-17.4-11.2-6.1-22.5-11.9-33.7-18-16.6-9.1-33.1-18.4-49.8-27.5-4.9-2.7-6.1-1.9-6.4 3.9-.1 2-.1 4.1-.1 6.1v114.5c0 14.8-5.6 20.4-20.4 20.4-47.6.1-95.3-.1-142.9.2-10.5.1-21.1 1.4-31.6 2.8-16.5 2.2-30.5 9.9-42.8 21-17 15.5-27 34.7-29.4 57.5-1.1 10.9-.4 21.7 2.9 32.5 3.7 12.3 9.2 23.4 17.5 33 19.2 22.1 43.4 33.3 72.7 33.3 46.6.1 93 0 139.5 0z" />
|
||||
</svg>
|
||||
),
|
||||
type: "Onion Network",
|
||||
version: "Latest",
|
||||
color: "cyan",
|
||||
},
|
||||
];
|
||||
|
||||
const features = [
|
||||
{
|
||||
name: "Security Architecture",
|
||||
lockbit: { status: "trophy", detail: "18-layer military-grade defense system with complete ASN.1 validation" },
|
||||
signal: { status: "check", detail: "Signal Protocol with double ratchet" },
|
||||
threema: { status: "check", detail: "Standard security implementation" },
|
||||
session: { status: "check", detail: "Modified Signal Protocol + Onion routing" },
|
||||
},
|
||||
{
|
||||
name: "Cryptography",
|
||||
lockbit: { status: "trophy", detail: "ECDH P-384 + AES-GCM 256 + ECDSA P-384" },
|
||||
signal: { status: "check", detail: "Signal Protocol + Double Ratchet" },
|
||||
threema: { status: "check", detail: "NaCl + XSalsa20 + Poly1305" },
|
||||
session: { status: "check", detail: "Modified Signal Protocol" },
|
||||
},
|
||||
{
|
||||
name: "Perfect Forward Secrecy",
|
||||
lockbit: { status: "trophy", detail: "Auto rotation every 5 minutes or 100 messages" },
|
||||
signal: { status: "check", detail: "Double Ratchet algorithm" },
|
||||
threema: { status: "warning", detail: "Partial (group chats)" },
|
||||
session: { status: "check", detail: "Session Ratchet algorithm" },
|
||||
},
|
||||
{
|
||||
name: "Architecture",
|
||||
lockbit: { status: "trophy", detail: "Pure P2P WebRTC without servers" },
|
||||
signal: { status: "times", detail: "Centralized Signal servers" },
|
||||
threema: { status: "times", detail: "Threema servers in Switzerland" },
|
||||
session: { status: "warning", detail: "Onion routing via network nodes" },
|
||||
},
|
||||
{
|
||||
name: "Registration Anonymity",
|
||||
lockbit: { status: "trophy", detail: "No registration required, instant anonymous channels" },
|
||||
signal: { status: "times", detail: "Phone number required" },
|
||||
threema: { status: "check", detail: "ID generated locally" },
|
||||
session: { status: "check", detail: "Random session ID" },
|
||||
},
|
||||
{
|
||||
name: "Payment Integration",
|
||||
lockbit: { status: "trophy", detail: "Lightning Network satoshis per session + WebLN" },
|
||||
signal: { status: "times", detail: "No payment system" },
|
||||
threema: { status: "times", detail: "No payment system" },
|
||||
session: { status: "times", detail: "No payment system" },
|
||||
},
|
||||
{
|
||||
name: "Metadata Protection",
|
||||
lockbit: { status: "trophy", detail: "Full metadata encryption + traffic obfuscation" },
|
||||
signal: { status: "warning", detail: "Sealed Sender (partial)" },
|
||||
threema: { status: "warning", detail: "Minimal metadata" },
|
||||
session: { status: "check", detail: "Onion routing hides metadata" },
|
||||
},
|
||||
{
|
||||
name: "Traffic Obfuscation",
|
||||
lockbit: { status: "trophy", detail: "Fake traffic + pattern masking + packet padding" },
|
||||
signal: { status: "times", detail: "No traffic obfuscation" },
|
||||
threema: { status: "times", detail: "No traffic obfuscation" },
|
||||
session: { status: "check", detail: "Onion routing provides obfuscation" },
|
||||
},
|
||||
{
|
||||
name: "Open Source",
|
||||
lockbit: { status: "trophy", detail: "100% open + auditable + MIT license" },
|
||||
signal: { status: "check", detail: "Fully open" },
|
||||
threema: { status: "warning", detail: "Only clients open" },
|
||||
session: { status: "check", detail: "Fully open" },
|
||||
},
|
||||
{
|
||||
name: "MITM Protection",
|
||||
lockbit: { status: "trophy", detail: "Out-of-band verification + mutual auth + ECDSA" },
|
||||
signal: { status: "check", detail: "Safety numbers verification" },
|
||||
threema: { status: "check", detail: "QR code scanning" },
|
||||
session: { status: "warning", detail: "Basic key verification" },
|
||||
},
|
||||
{
|
||||
name: "Economic Model",
|
||||
lockbit: { status: "trophy", detail: "Sustainable pay-per-session model" },
|
||||
signal: { status: "warning", detail: "Donations and grants dependency" },
|
||||
threema: { status: "check", detail: "One-time app purchase" },
|
||||
session: { status: "warning", detail: "Donations dependency" },
|
||||
},
|
||||
{
|
||||
name: "Censorship Resistance",
|
||||
lockbit: { status: "trophy", detail: "Impossible to block P2P + no servers to target" },
|
||||
signal: { status: "warning", detail: "Blocked in authoritarian countries" },
|
||||
threema: { status: "warning", detail: "May be blocked" },
|
||||
session: { status: "check", detail: "Onion routing bypasses blocks" },
|
||||
},
|
||||
{
|
||||
name: "Data Storage",
|
||||
lockbit: { status: "trophy", detail: "Zero data storage - only in browser memory" },
|
||||
signal: { status: "warning", detail: "Local database storage" },
|
||||
threema: { status: "warning", detail: "Local + optional backup" },
|
||||
session: { status: "warning", detail: "Local database storage" },
|
||||
},
|
||||
{
|
||||
name: "Key Security",
|
||||
lockbit: { status: "trophy", detail: "Non-extractable keys + hardware protection" },
|
||||
signal: { status: "check", detail: "Secure key storage" },
|
||||
threema: { status: "check", detail: "Local key storage" },
|
||||
session: { status: "check", detail: "Secure key storage" },
|
||||
},
|
||||
{
|
||||
name: "Post-Quantum Roadmap",
|
||||
lockbit: { status: "check", detail: "Planned v5.0 - CRYSTALS-Kyber/Dilithium" },
|
||||
signal: { status: "warning", detail: "PQXDH in development" },
|
||||
threema: { status: "times", detail: "Not announced" },
|
||||
session: { status: "times", detail: "Not announced" },
|
||||
},
|
||||
];
|
||||
|
||||
const getStatusIcon = (status) => {
|
||||
const statusMap = {
|
||||
"trophy": { icon: "fa-trophy", color: "accent-orange" },
|
||||
"check": { icon: "fa-check", color: "text-green-300" },
|
||||
"warning": { icon: "fa-exclamation-triangle", color: "text-yellow-300" },
|
||||
"times": { icon: "fa-times", color: "text-red-300" },
|
||||
};
|
||||
return statusMap[status] || { icon: "fa-question", color: "text-gray-400" };
|
||||
};
|
||||
|
||||
const toggleFeatureDetail = (index) => {
|
||||
setSelectedFeature(selectedFeature === index ? null : index);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-16">
|
||||
{/* Title */}
|
||||
<div className="text-center mb-8">
|
||||
<h3 className="text-3xl font-bold text-white mb-3">
|
||||
Enhanced Security Edition Comparison
|
||||
</h3>
|
||||
<p className="text-gray-400 max-w-2xl mx-auto mb-4">
|
||||
Enhanced Security Edition vs leading secure messengers
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Table container */}
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Mobile Alert */}
|
||||
<div className="md:hidden p-4 bg-yellow-500/10 border border-yellow-500/20 rounded-lg mb-4">
|
||||
<p className="text-yellow-400 text-sm text-center">
|
||||
<i className="fas fa-lightbulb mr-2"></i>
|
||||
Rotate your device horizontally for better viewing
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="overflow-x-auto">
|
||||
<table
|
||||
className="w-full border-collapse rounded-xl overflow-hidden shadow-2xl"
|
||||
style={{ backgroundColor: "rgba(42, 43, 42, 0.9)" }}
|
||||
>
|
||||
{/* Table Header */}
|
||||
<thead>
|
||||
<tr className="bg-black-table">
|
||||
<th className="text-left p-4 border-b border-gray-600 text-white font-bold min-w-[240px]">
|
||||
Security Criterion
|
||||
</th>
|
||||
{messengers.map((messenger, index) => (
|
||||
<th key={`messenger-${index}`} className="text-center p-4 border-b border-gray-600 min-w-[160px]">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="mb-2">{messenger.logo}</div>
|
||||
<div className={`text-sm font-bold ${
|
||||
messenger.color === 'orange' ? 'text-orange-400' :
|
||||
messenger.color === 'blue' ? 'text-blue-400' :
|
||||
messenger.color === 'green' ? 'text-green-400' :
|
||||
'text-cyan-400'
|
||||
}`}>
|
||||
{messenger.name}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">{messenger.type}</div>
|
||||
<div className="text-xs text-gray-500 mt-1">{messenger.version}</div>
|
||||
</div>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
{/* Table body */}
|
||||
<tbody>
|
||||
{features.map((feature, featureIndex) => (
|
||||
<React.Fragment key={`feature-${featureIndex}`}>
|
||||
<tr
|
||||
className={`border-b border-gray-700/30 transition-all duration-200 cursor-pointer hover:bg-[rgb(20_20_20_/30%)] ${
|
||||
selectedFeature === featureIndex ? 'bg-[rgb(20_20_20_/50%)]' : ''
|
||||
}`}
|
||||
onClick={() => toggleFeatureDetail(featureIndex)}
|
||||
>
|
||||
<td className="p-4 text-white font-semibold">
|
||||
<div className="flex items-center justify-between">
|
||||
<span>{feature.name}</span>
|
||||
<i className={`fas fa-chevron-${selectedFeature === featureIndex ? 'up' : 'down'} text-xs text-gray-400 opacity-60 transition-all duration-200`} />
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-4 text-center">
|
||||
<i className={`fas ${getStatusIcon(feature.lockbit.status).icon} ${getStatusIcon(feature.lockbit.status).color} text-2xl`} />
|
||||
</td>
|
||||
<td className="p-4 text-center">
|
||||
<i className={`fas ${getStatusIcon(feature.signal.status).icon} ${getStatusIcon(feature.signal.status).color} text-2xl`} />
|
||||
</td>
|
||||
<td className="p-4 text-center">
|
||||
<i className={`fas ${getStatusIcon(feature.threema.status).icon} ${getStatusIcon(feature.threema.status).color} text-2xl`} />
|
||||
</td>
|
||||
<td className="p-4 text-center">
|
||||
<i className={`fas ${getStatusIcon(feature.session.status).icon} ${getStatusIcon(feature.session.status).color} text-2xl`} />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{/* Details */}
|
||||
{selectedFeature === featureIndex && (
|
||||
<tr className="border-b border-gray-700/30 bg-gradient-to-r from-gray-800/20 to-gray-900/20">
|
||||
<td className="p-4 text-xs text-gray-400 font-medium">Technical Details:</td>
|
||||
<td className="p-4 text-center">
|
||||
<div className="text-xs text-orange-300 font-medium leading-relaxed">
|
||||
{feature.lockbit.detail}
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-4 text-center">
|
||||
<div className="text-xs text-blue-300 leading-relaxed">
|
||||
{feature.signal.detail}
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-4 text-center">
|
||||
<div className="text-xs text-green-300 leading-relaxed">
|
||||
{feature.threema.detail}
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-4 text-center">
|
||||
<div className="text-xs text-cyan-300 leading-relaxed">
|
||||
{feature.session.detail}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Legend */}
|
||||
<div className="mt-8 grid grid-cols-2 md:grid-cols-4 gap-4 max-w-5xl mx-auto">
|
||||
<div className="flex items-center justify-center p-4 bg-orange-500/10 rounded-xl hover:bg-orange-500/40 transition-colors">
|
||||
<i className="fas fa-trophy text-orange-400 mr-2 text-xl"></i>
|
||||
<span className="text-orange-300 text-sm font-bold">Category Leader</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center p-4 bg-green-500/10 rounded-xl hover:bg-green-600/40 transition-colors">
|
||||
<i className="fas fa-check text-green-300 mr-2 text-xl"></i>
|
||||
<span className="text-green-200 text-sm font-bold">Excellent</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-center p-4 bg-yellow-500/10 rounded-xl hover:bg-yellow-600/40 transition-colors">
|
||||
<i className="fas fa-exclamation-triangle text-yellow-300 mr-2 text-xl"></i>
|
||||
<span className="text-yellow-200 text-sm font-bold">Partial/Limited</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-center p-4 bg-red-500/10 rounded-xl hover:bg-red-600/40 transition-colors">
|
||||
<i className="fas fa-times text-red-300 mr-2 text-xl"></i>
|
||||
<span className="text-red-200 text-sm font-bold">Not Available</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
window.ComparisonTable = ComparisonTable;
|
||||
86
src/components/ui/DownloadApps.jsx
Normal file
86
src/components/ui/DownloadApps.jsx
Normal file
@@ -0,0 +1,86 @@
|
||||
const DownloadApps = () => {
|
||||
const apps = [
|
||||
{ id: 'web', name: 'Web App', subtitle: 'Browser Version', icon: 'fas fa-globe', platform: 'Web', isActive: true, url: 'https://securebit.chat/', color: 'green' },
|
||||
{ id: 'windows', name: 'Windows', subtitle: 'Desktop App', icon: 'fab fa-windows', platform: 'Desktop', isActive: false, url: 'https://securebit.chat/download/windows/SecureBit%20Chat%20Setup%204.1.222.exe', color: 'blue' },
|
||||
{ id: 'macos', name: 'macOS', subtitle: 'Desktop App', icon: 'fab fa-safari', platform: 'Desktop', isActive: false, url: '#', color: 'gray' },
|
||||
{ id: 'linux', name: 'Linux', subtitle: 'Desktop App', icon: 'fab fa-linux', platform: 'Desktop', isActive: false, url: '#', color: 'orange' },
|
||||
{ id: 'ios', name: 'iOS', subtitle: 'iPhone & iPad', icon: 'fab fa-apple', platform: 'Mobile', isActive: false, url: 'https://apps.apple.com/app/securebit-chat/', color: 'white' },
|
||||
{ id: 'android', name: 'Android', subtitle: 'Google Play', icon: 'fab fa-android', platform: 'Mobile', isActive: false, url: 'https://play.google.com/store/apps/details?id=com.securebit.chat', color: 'green' },
|
||||
{ id: 'chrome', name: 'Chrome', subtitle: 'Browser Extension', icon: 'fab fa-chrome', platform: 'Browser', isActive: false, url: '#', color: 'yellow' },
|
||||
{ id: 'edge', name: 'Edge', subtitle: 'Browser Extension', icon: 'fab fa-edge', platform: 'Browser', isActive: false, url: '#', color: 'blue' },
|
||||
{ id: 'opera', name: 'Opera', subtitle: 'Browser Extension', icon: 'fab fa-opera', platform: 'Browser', isActive: false, url: '#', color: 'red' },
|
||||
{ id: 'firefox', name: 'Firefox', subtitle: 'Browser Extension', icon: 'fab fa-firefox-browser', platform: 'Browser', isActive: false, url: '#', color: 'orange' },
|
||||
];
|
||||
|
||||
const handleDownload = (app) => {
|
||||
if (app.isActive) window.open(app.url, '_blank');
|
||||
};
|
||||
|
||||
const desktopApps = apps.filter(a => a.platform === 'Desktop' || a.platform === 'Web');
|
||||
const mobileApps = apps.filter(a => a.platform === 'Mobile');
|
||||
const browserApps = apps.filter(a => a.platform === 'Browser');
|
||||
|
||||
const cardSize = "w-28 h-28";
|
||||
|
||||
const colorClasses = {
|
||||
green: 'text-green-500',
|
||||
blue: 'text-blue-500',
|
||||
gray: 'text-gray-500',
|
||||
orange: 'text-orange-500',
|
||||
red: 'text-red-500',
|
||||
white: 'text-white',
|
||||
yellow: 'text-yellow-400',
|
||||
};
|
||||
|
||||
const renderAppCard = (app) => (
|
||||
React.createElement('div', {
|
||||
key: app.id,
|
||||
className: `group relative ${cardSize} rounded-2xl overflow-hidden card-minimal cursor-pointer`
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'bg-icon',
|
||||
className: `${app.icon} absolute text-[3rem] ${app.isActive ? colorClasses[app.color] : 'text-white/10'} top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 pointer-events-none transition-all duration-500 group-hover:scale-105`
|
||||
}),
|
||||
React.createElement('div', {
|
||||
key: 'overlay',
|
||||
className: "absolute inset-0 bg-black/30 backdrop-blur-md flex flex-col items-center justify-center text-center opacity-0 transition-opacity duration-300 group-hover:opacity-100"
|
||||
}, [
|
||||
React.createElement('h4', { key: 'name', className: `text-sm font-semibold text-primary mb-1` }, app.name),
|
||||
React.createElement('p', { key: 'subtitle', className: `text-xs text-secondary mb-2` }, app.subtitle),
|
||||
app.isActive ?
|
||||
React.createElement('button', {
|
||||
key: 'btn',
|
||||
onClick: () => handleDownload(app),
|
||||
className: `px-2 py-1 rounded-xl bg-emerald-500 text-black font-medium hover:bg-emerald-600 transition-colors text-xs`
|
||||
}, app.id === "web" ? "Launch" : "Download")
|
||||
:
|
||||
React.createElement('span', { key: 'coming', className: "text-gray-400 font-medium text-xs" }, "Coming Soon")
|
||||
])
|
||||
])
|
||||
);
|
||||
|
||||
return React.createElement('div', { className: "mt-20 px-6" }, [
|
||||
// Header
|
||||
React.createElement('div', { key: 'header', className: "text-center max-w-3xl mx-auto mb-12" }, [
|
||||
React.createElement('h3', { key: 'title', className: "text-3xl font-bold text-primary mb-3" }, 'Download SecureBit.chat'),
|
||||
React.createElement('p', { key: 'subtitle', className: "text-secondary text-lg mb-5" }, 'Stay secure on every device. Choose your platform and start chatting privately.')
|
||||
]),
|
||||
|
||||
// Desktop Apps
|
||||
React.createElement('div', { key: 'desktop-row', className: "hidden sm:flex justify-center flex-wrap gap-6 mb-6" },
|
||||
desktopApps.map(renderAppCard)
|
||||
),
|
||||
|
||||
// Mobile Apps
|
||||
React.createElement('div', { key: 'mobile-row', className: "flex justify-center gap-6 mb-6" },
|
||||
mobileApps.map(renderAppCard)
|
||||
),
|
||||
|
||||
// Browser Extensions
|
||||
React.createElement('div', { key: 'browser-row', className: "flex justify-center gap-6" },
|
||||
browserApps.map(renderAppCard)
|
||||
)
|
||||
]);
|
||||
};
|
||||
|
||||
window.DownloadApps = DownloadApps;
|
||||
423
src/components/ui/FileTransfer.jsx
Normal file
423
src/components/ui/FileTransfer.jsx
Normal file
@@ -0,0 +1,423 @@
|
||||
// File Transfer Component for Chat Interface - Fixed Version
|
||||
const FileTransferComponent = ({ webrtcManager, isConnected }) => {
|
||||
const [dragOver, setDragOver] = React.useState(false);
|
||||
const [transfers, setTransfers] = React.useState({ sending: [], receiving: [] });
|
||||
const [readyFiles, setReadyFiles] = React.useState([]); // файлы, готовые к скачиванию
|
||||
const fileInputRef = React.useRef(null);
|
||||
|
||||
// Update transfers periodically
|
||||
React.useEffect(() => {
|
||||
if (!isConnected || !webrtcManager) return;
|
||||
|
||||
const updateTransfers = () => {
|
||||
const currentTransfers = webrtcManager.getFileTransfers();
|
||||
setTransfers(currentTransfers);
|
||||
};
|
||||
|
||||
const interval = setInterval(updateTransfers, 500);
|
||||
return () => clearInterval(interval);
|
||||
}, [isConnected, webrtcManager]);
|
||||
|
||||
// Setup file transfer callbacks - ИСПРАВЛЕНИЕ: НЕ отправляем промежуточные сообщения в чат
|
||||
React.useEffect(() => {
|
||||
if (!webrtcManager) return;
|
||||
|
||||
webrtcManager.setFileTransferCallbacks(
|
||||
// Progress callback - ТОЛЬКО обновляем UI, НЕ отправляем в чат
|
||||
(progress) => {
|
||||
// Обновляем только локальное состояние
|
||||
const currentTransfers = webrtcManager.getFileTransfers();
|
||||
setTransfers(currentTransfers);
|
||||
|
||||
// НЕ отправляем сообщения в чат!
|
||||
},
|
||||
|
||||
// File received callback - добавляем кнопку скачивания в UI
|
||||
(fileData) => {
|
||||
// Добавляем в список готовых к скачиванию
|
||||
setReadyFiles(prev => {
|
||||
// избегаем дублей по fileId
|
||||
if (prev.some(f => f.fileId === fileData.fileId)) return prev;
|
||||
return [...prev, {
|
||||
fileId: fileData.fileId,
|
||||
fileName: fileData.fileName,
|
||||
fileSize: fileData.fileSize,
|
||||
mimeType: fileData.mimeType,
|
||||
getBlob: fileData.getBlob,
|
||||
getObjectURL: fileData.getObjectURL,
|
||||
revokeObjectURL: fileData.revokeObjectURL
|
||||
}];
|
||||
});
|
||||
|
||||
// Обновляем список активных передач
|
||||
const currentTransfers = webrtcManager.getFileTransfers();
|
||||
setTransfers(currentTransfers);
|
||||
},
|
||||
|
||||
// Error callback
|
||||
(error) => {
|
||||
const currentTransfers = webrtcManager.getFileTransfers();
|
||||
setTransfers(currentTransfers);
|
||||
|
||||
// ИСПРАВЛЕНИЕ: НЕ дублируем сообщения об ошибках
|
||||
// Уведомления об ошибках уже отправляются в WebRTC менеджере
|
||||
}
|
||||
);
|
||||
}, [webrtcManager]);
|
||||
|
||||
const handleFileSelect = async (files) => {
|
||||
if (!isConnected || !webrtcManager) {
|
||||
alert('Соединение не установлено. Сначала установите соединение.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Дополнительная проверка состояния соединения
|
||||
if (!webrtcManager.isConnected() || !webrtcManager.isVerified) {
|
||||
alert('Соединение не готово для передачи файлов. Дождитесь завершения установки соединения.');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
try {
|
||||
// КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ: Валидация файла перед отправкой
|
||||
const validation = webrtcManager.validateFile(file);
|
||||
if (!validation.isValid) {
|
||||
const errorMessage = validation.errors.join('. ');
|
||||
alert(`Файл ${file.name} не может быть отправлен: ${errorMessage}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
await webrtcManager.sendFile(file);
|
||||
} catch (error) {
|
||||
// Более мягкая обработка ошибок - не закрываем сессию
|
||||
|
||||
// Показываем пользователю ошибку, но не закрываем соединение
|
||||
if (error.message.includes('Connection not ready')) {
|
||||
alert(`Файл ${file.name} не может быть отправлен сейчас. Проверьте соединение и попробуйте снова.`);
|
||||
} else if (error.message.includes('File too large') || error.message.includes('exceeds maximum')) {
|
||||
alert(`Файл ${file.name} слишком большой: ${error.message}`);
|
||||
} else if (error.message.includes('Maximum concurrent transfers')) {
|
||||
alert(`Достигнут лимит одновременных передач. Дождитесь завершения текущих передач.`);
|
||||
} else if (error.message.includes('File type not allowed')) {
|
||||
alert(`Тип файла ${file.name} не поддерживается: ${error.message}`);
|
||||
} else {
|
||||
alert(`Ошибка отправки файла ${file.name}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDrop = (e) => {
|
||||
e.preventDefault();
|
||||
setDragOver(false);
|
||||
|
||||
const files = Array.from(e.dataTransfer.files);
|
||||
handleFileSelect(files);
|
||||
};
|
||||
|
||||
const handleDragOver = (e) => {
|
||||
e.preventDefault();
|
||||
setDragOver(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = (e) => {
|
||||
e.preventDefault();
|
||||
setDragOver(false);
|
||||
};
|
||||
|
||||
const handleFileInputChange = (e) => {
|
||||
const files = Array.from(e.target.files);
|
||||
handleFileSelect(files);
|
||||
e.target.value = ''; // Reset input
|
||||
};
|
||||
|
||||
const formatFileSize = (bytes) => {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
const getStatusIcon = (status) => {
|
||||
switch (status) {
|
||||
case 'metadata_sent':
|
||||
case 'preparing':
|
||||
return 'fas fa-cog fa-spin';
|
||||
case 'transmitting':
|
||||
case 'receiving':
|
||||
return 'fas fa-exchange-alt fa-pulse';
|
||||
case 'assembling':
|
||||
return 'fas fa-puzzle-piece fa-pulse';
|
||||
case 'completed':
|
||||
return 'fas fa-check text-green-400';
|
||||
case 'failed':
|
||||
return 'fas fa-times text-red-400';
|
||||
default:
|
||||
return 'fas fa-circle';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusText = (status) => {
|
||||
switch (status) {
|
||||
case 'metadata_sent':
|
||||
return 'Подготовка...';
|
||||
case 'transmitting':
|
||||
return 'Отправка...';
|
||||
case 'receiving':
|
||||
return 'Получение...';
|
||||
case 'assembling':
|
||||
return 'Сборка файла...';
|
||||
case 'completed':
|
||||
return 'Завершено';
|
||||
case 'failed':
|
||||
return 'Ошибка';
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
};
|
||||
|
||||
if (!isConnected) {
|
||||
return React.createElement('div', {
|
||||
className: "p-4 text-center text-muted"
|
||||
}, 'Передача файлов доступна только при установленном соединении');
|
||||
}
|
||||
|
||||
// Проверяем дополнительное состояние соединения
|
||||
const isConnectionReady = webrtcManager && webrtcManager.isConnected() && webrtcManager.isVerified;
|
||||
|
||||
if (!isConnectionReady) {
|
||||
return React.createElement('div', {
|
||||
className: "p-4 text-center text-yellow-600"
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'icon',
|
||||
className: 'fas fa-exclamation-triangle mr-2'
|
||||
}),
|
||||
'Соединение устанавливается... Передача файлов будет доступна после завершения установки.'
|
||||
]);
|
||||
}
|
||||
|
||||
return React.createElement('div', {
|
||||
className: "file-transfer-component"
|
||||
}, [
|
||||
// File Drop Zone
|
||||
React.createElement('div', {
|
||||
key: 'drop-zone',
|
||||
className: `file-drop-zone ${dragOver ? 'drag-over' : ''}`,
|
||||
onDrop: handleDrop,
|
||||
onDragOver: handleDragOver,
|
||||
onDragLeave: handleDragLeave,
|
||||
onClick: () => fileInputRef.current?.click()
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'drop-content',
|
||||
className: "drop-content"
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'icon',
|
||||
className: 'fas fa-cloud-upload-alt text-2xl mb-2 text-blue-400'
|
||||
}),
|
||||
React.createElement('p', {
|
||||
key: 'text',
|
||||
className: "text-primary font-medium"
|
||||
}, 'Drag files here or click to select'),
|
||||
React.createElement('p', {
|
||||
key: 'subtext',
|
||||
className: "text-muted text-sm"
|
||||
}, 'Maximum size: 100 MB per file')
|
||||
])
|
||||
]),
|
||||
|
||||
// Hidden file input
|
||||
React.createElement('input', {
|
||||
key: 'file-input',
|
||||
ref: fileInputRef,
|
||||
type: 'file',
|
||||
multiple: true,
|
||||
className: 'hidden',
|
||||
onChange: handleFileInputChange
|
||||
}),
|
||||
|
||||
// Active Transfers
|
||||
(transfers.sending.length > 0 || transfers.receiving.length > 0) && React.createElement('div', {
|
||||
key: 'transfers',
|
||||
className: "active-transfers mt-4"
|
||||
}, [
|
||||
React.createElement('h4', {
|
||||
key: 'title',
|
||||
className: "text-primary font-medium mb-3 flex items-center"
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'icon',
|
||||
className: 'fas fa-exchange-alt mr-2'
|
||||
}),
|
||||
'Передача файлов'
|
||||
]),
|
||||
|
||||
// Sending files
|
||||
...transfers.sending.map(transfer =>
|
||||
React.createElement('div', {
|
||||
key: `send-${transfer.fileId}`,
|
||||
className: "transfer-item bg-blue-500/10 border border-blue-500/20 rounded-lg p-3 mb-2"
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'header',
|
||||
className: "flex items-center justify-between mb-2"
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'info',
|
||||
className: "flex items-center"
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'icon',
|
||||
className: 'fas fa-upload text-blue-400 mr-2'
|
||||
}),
|
||||
React.createElement('span', {
|
||||
key: 'name',
|
||||
className: "text-primary font-medium text-sm"
|
||||
}, transfer.fileName),
|
||||
React.createElement('span', {
|
||||
key: 'size',
|
||||
className: "text-muted text-xs ml-2"
|
||||
}, formatFileSize(transfer.fileSize))
|
||||
]),
|
||||
React.createElement('button', {
|
||||
key: 'cancel',
|
||||
onClick: () => webrtcManager.cancelFileTransfer(transfer.fileId),
|
||||
className: "text-red-400 hover:text-red-300 text-xs"
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
className: 'fas fa-times'
|
||||
})
|
||||
])
|
||||
]),
|
||||
React.createElement('div', {
|
||||
key: 'progress',
|
||||
className: "progress-bar"
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'fill',
|
||||
className: "progress-fill bg-blue-400",
|
||||
style: { width: `${transfer.progress}%` }
|
||||
}),
|
||||
React.createElement('div', {
|
||||
key: 'text',
|
||||
className: "progress-text text-xs flex items-center justify-between"
|
||||
}, [
|
||||
React.createElement('span', {
|
||||
key: 'status',
|
||||
className: "flex items-center"
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'icon',
|
||||
className: `${getStatusIcon(transfer.status)} mr-1`
|
||||
}),
|
||||
getStatusText(transfer.status)
|
||||
]),
|
||||
React.createElement('span', {
|
||||
key: 'percent'
|
||||
}, `${transfer.progress.toFixed(1)}%`)
|
||||
])
|
||||
])
|
||||
])
|
||||
),
|
||||
|
||||
// Receiving files
|
||||
...transfers.receiving.map(transfer =>
|
||||
React.createElement('div', {
|
||||
key: `recv-${transfer.fileId}`,
|
||||
className: "transfer-item bg-green-500/10 border border-green-500/20 rounded-lg p-3 mb-2"
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'header',
|
||||
className: "flex items-center justify-between mb-2"
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'info',
|
||||
className: "flex items-center"
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'icon',
|
||||
className: 'fas fa-download text-green-400 mr-2'
|
||||
}),
|
||||
React.createElement('span', {
|
||||
key: 'name',
|
||||
className: "text-primary font-medium text-sm"
|
||||
}, transfer.fileName),
|
||||
React.createElement('span', {
|
||||
key: 'size',
|
||||
className: "text-muted text-xs ml-2"
|
||||
}, formatFileSize(transfer.fileSize))
|
||||
]),
|
||||
React.createElement('div', { key: 'actions', className: 'flex items-center space-x-2' }, [
|
||||
(() => {
|
||||
const rf = readyFiles.find(f => f.fileId === transfer.fileId);
|
||||
if (!rf || transfer.status !== 'completed') return null;
|
||||
return React.createElement('button', {
|
||||
key: 'download',
|
||||
className: 'text-green-400 hover:text-green-300 text-xs flex items-center',
|
||||
onClick: async () => {
|
||||
try {
|
||||
const url = await rf.getObjectURL();
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = rf.fileName || 'file';
|
||||
a.click();
|
||||
rf.revokeObjectURL(url);
|
||||
} catch (e) {
|
||||
alert('Failed to start download: ' + e.message);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
React.createElement('i', { key: 'i', className: 'fas fa-download mr-1' }),
|
||||
'Download'
|
||||
]);
|
||||
})(),
|
||||
React.createElement('button', {
|
||||
key: 'cancel',
|
||||
onClick: () => webrtcManager.cancelFileTransfer(transfer.fileId),
|
||||
className: "text-red-400 hover:text-red-300 text-xs"
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
className: 'fas fa-times'
|
||||
})
|
||||
])
|
||||
])
|
||||
]),
|
||||
React.createElement('div', {
|
||||
key: 'progress',
|
||||
className: "progress-bar"
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'fill',
|
||||
className: "progress-fill bg-green-400",
|
||||
style: { width: `${transfer.progress}%` }
|
||||
}),
|
||||
React.createElement('div', {
|
||||
key: 'text',
|
||||
className: "progress-text text-xs flex items-center justify-between"
|
||||
}, [
|
||||
React.createElement('span', {
|
||||
key: 'status',
|
||||
className: "flex items-center"
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'icon',
|
||||
className: `${getStatusIcon(transfer.status)} mr-1`
|
||||
}),
|
||||
getStatusText(transfer.status)
|
||||
]),
|
||||
React.createElement('span', {
|
||||
key: 'percent'
|
||||
}, `${transfer.progress.toFixed(1)}%`)
|
||||
])
|
||||
])
|
||||
])
|
||||
)
|
||||
])
|
||||
]);
|
||||
};
|
||||
|
||||
// Export
|
||||
window.FileTransferComponent = FileTransferComponent;
|
||||
708
src/components/ui/Header.jsx
Normal file
708
src/components/ui/Header.jsx
Normal file
@@ -0,0 +1,708 @@
|
||||
const EnhancedMinimalHeader = ({
|
||||
status,
|
||||
fingerprint,
|
||||
verificationCode,
|
||||
onDisconnect,
|
||||
isConnected,
|
||||
securityLevel,
|
||||
sessionManager,
|
||||
sessionTimeLeft,
|
||||
webrtcManager
|
||||
}) => {
|
||||
const [currentTimeLeft, setCurrentTimeLeft] = React.useState(sessionTimeLeft || 0);
|
||||
const [hasActiveSession, setHasActiveSession] = React.useState(false);
|
||||
const [sessionType, setSessionType] = React.useState('unknown');
|
||||
const [realSecurityLevel, setRealSecurityLevel] = React.useState(null);
|
||||
const [lastSecurityUpdate, setLastSecurityUpdate] = React.useState(0);
|
||||
|
||||
// ============================================
|
||||
// FIXED SECURITY UPDATE LOGIC
|
||||
// ============================================
|
||||
|
||||
React.useEffect(() => {
|
||||
let isUpdating = false;
|
||||
let lastUpdateAttempt = 0;
|
||||
|
||||
const updateRealSecurityStatus = async () => {
|
||||
const now = Date.now();
|
||||
if (now - lastUpdateAttempt < 10000) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isUpdating) {
|
||||
return;
|
||||
}
|
||||
|
||||
isUpdating = true;
|
||||
lastUpdateAttempt = now;
|
||||
|
||||
try {
|
||||
if (!webrtcManager || !isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeWebrtcManager = webrtcManager;
|
||||
|
||||
let realSecurityData = null;
|
||||
|
||||
if (typeof activeWebrtcManager.getRealSecurityLevel === 'function') {
|
||||
realSecurityData = await activeWebrtcManager.getRealSecurityLevel();
|
||||
} else if (typeof activeWebrtcManager.calculateAndReportSecurityLevel === 'function') {
|
||||
realSecurityData = await activeWebrtcManager.calculateAndReportSecurityLevel();
|
||||
} else {
|
||||
realSecurityData = await window.EnhancedSecureCryptoUtils.calculateSecurityLevel(activeWebrtcManager);
|
||||
}
|
||||
|
||||
if (realSecurityData && realSecurityData.isRealData !== false) {
|
||||
const currentScore = realSecurityLevel?.score || 0;
|
||||
const newScore = realSecurityData.score || 0;
|
||||
|
||||
if (currentScore !== newScore || !realSecurityLevel) {
|
||||
setRealSecurityLevel(realSecurityData);
|
||||
setLastSecurityUpdate(now);
|
||||
|
||||
} else if (window.DEBUG_MODE) {
|
||||
}
|
||||
} else {
|
||||
console.warn(' Security calculation returned invalid data');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(' Error in real security calculation:', error);
|
||||
} finally {
|
||||
isUpdating = false;
|
||||
}
|
||||
};
|
||||
|
||||
if (isConnected) {
|
||||
updateRealSecurityStatus();
|
||||
|
||||
if (!realSecurityLevel || realSecurityLevel.score < 50) {
|
||||
const retryInterval = setInterval(() => {
|
||||
if (!realSecurityLevel || realSecurityLevel.score < 50) {
|
||||
updateRealSecurityStatus();
|
||||
} else {
|
||||
clearInterval(retryInterval);
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
setTimeout(() => clearInterval(retryInterval), 30000);
|
||||
}
|
||||
}
|
||||
|
||||
const interval = setInterval(updateRealSecurityStatus, 30000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [webrtcManager, isConnected]);
|
||||
|
||||
// ============================================
|
||||
// FIXED EVENT HANDLERS
|
||||
// ============================================
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleSecurityUpdate = (event) => {
|
||||
|
||||
setTimeout(() => {
|
||||
setLastSecurityUpdate(0);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const handleRealSecurityCalculated = (event) => {
|
||||
|
||||
if (event.detail && event.detail.securityData) {
|
||||
setRealSecurityLevel(event.detail.securityData);
|
||||
setLastSecurityUpdate(Date.now());
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('security-level-updated', handleSecurityUpdate);
|
||||
document.addEventListener('real-security-calculated', handleRealSecurityCalculated);
|
||||
|
||||
window.forceHeaderSecurityUpdate = (webrtcManager) => {
|
||||
|
||||
if (webrtcManager && window.EnhancedSecureCryptoUtils) {
|
||||
window.EnhancedSecureCryptoUtils.calculateSecurityLevel(webrtcManager)
|
||||
.then(securityData => {
|
||||
if (securityData && securityData.isRealData !== false) {
|
||||
setRealSecurityLevel(securityData);
|
||||
setLastSecurityUpdate(Date.now());
|
||||
console.log('✅ Header security level force-updated');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('❌ Force update failed:', error);
|
||||
});
|
||||
} else {
|
||||
setLastSecurityUpdate(0);
|
||||
}
|
||||
};
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('security-level-updated', handleSecurityUpdate);
|
||||
document.removeEventListener('real-security-calculated', handleRealSecurityCalculated);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// ============================================
|
||||
// REST of the component logic
|
||||
// ============================================
|
||||
|
||||
React.useEffect(() => {
|
||||
// All security features are enabled by default - no session management needed
|
||||
setHasActiveSession(true);
|
||||
setCurrentTimeLeft(0);
|
||||
setSessionType('premium'); // All features enabled
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
// All security features are enabled by default
|
||||
setHasActiveSession(true);
|
||||
setCurrentTimeLeft(0);
|
||||
setSessionType('premium'); // All features enabled
|
||||
}, [sessionTimeLeft]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleForceUpdate = (event) => {
|
||||
// All security features are enabled by default
|
||||
setHasActiveSession(true);
|
||||
setCurrentTimeLeft(0);
|
||||
setSessionType('premium'); // All features enabled
|
||||
};
|
||||
|
||||
// Connection cleanup handler (use existing event from module)
|
||||
const handleConnectionCleaned = () => {
|
||||
if (window.DEBUG_MODE) {
|
||||
console.log('🧹 Connection cleaned - clearing security data in header');
|
||||
}
|
||||
|
||||
setRealSecurityLevel(null);
|
||||
setLastSecurityUpdate(0);
|
||||
|
||||
setHasActiveSession(false);
|
||||
setCurrentTimeLeft(0);
|
||||
setSessionType('unknown');
|
||||
};
|
||||
|
||||
const handlePeerDisconnect = () => {
|
||||
if (window.DEBUG_MODE) {
|
||||
console.log('👋 Peer disconnect detected - clearing security data in header');
|
||||
}
|
||||
|
||||
setRealSecurityLevel(null);
|
||||
setLastSecurityUpdate(0);
|
||||
};
|
||||
|
||||
const handleDisconnected = () => {
|
||||
|
||||
setRealSecurityLevel(null);
|
||||
setLastSecurityUpdate(0);
|
||||
setHasActiveSession(false);
|
||||
setCurrentTimeLeft(0);
|
||||
setSessionType('unknown');
|
||||
};
|
||||
|
||||
document.addEventListener('force-header-update', handleForceUpdate);
|
||||
document.addEventListener('peer-disconnect', handlePeerDisconnect);
|
||||
document.addEventListener('connection-cleaned', handleConnectionCleaned);
|
||||
document.addEventListener('disconnected', handleDisconnected);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('force-header-update', handleForceUpdate);
|
||||
document.removeEventListener('peer-disconnect', handlePeerDisconnect);
|
||||
document.removeEventListener('connection-cleaned', handleConnectionCleaned);
|
||||
document.removeEventListener('disconnected', handleDisconnected);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// ============================================
|
||||
// SECURITY INDICATOR CLICK HANDLER
|
||||
// ============================================
|
||||
|
||||
const handleSecurityClick = async (event) => {
|
||||
// Check if it's a right-click or Ctrl+click to disconnect
|
||||
if (event && (event.button === 2 || event.ctrlKey || event.metaKey)) {
|
||||
if (onDisconnect && typeof onDisconnect === 'function') {
|
||||
onDisconnect();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent default behavior
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
|
||||
// Run real security tests if webrtcManager is available
|
||||
let realTestResults = null;
|
||||
if (webrtcManager && window.EnhancedSecureCryptoUtils) {
|
||||
try {
|
||||
realTestResults = await window.EnhancedSecureCryptoUtils.calculateSecurityLevel(webrtcManager);
|
||||
console.log('✅ Real security tests completed:', realTestResults);
|
||||
} catch (error) {
|
||||
console.error('❌ Real security tests failed:', error);
|
||||
}
|
||||
} else {
|
||||
console.log('⚠️ Cannot run security tests:', {
|
||||
webrtcManager: !!webrtcManager,
|
||||
cryptoUtils: !!window.EnhancedSecureCryptoUtils
|
||||
});
|
||||
}
|
||||
|
||||
// If no real test results and no existing security level, show progress message
|
||||
if (!realTestResults && !realSecurityLevel) {
|
||||
alert('Security verification in progress...\nPlease wait for real-time cryptographic verification to complete.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Use real test results if available, otherwise fall back to current data
|
||||
let securityData = realTestResults || realSecurityLevel;
|
||||
|
||||
// If still no security data, create a basic fallback
|
||||
if (!securityData) {
|
||||
securityData = {
|
||||
level: 'UNKNOWN',
|
||||
score: 0,
|
||||
color: 'gray',
|
||||
verificationResults: {},
|
||||
timestamp: Date.now(),
|
||||
details: 'Security verification not available',
|
||||
isRealData: false,
|
||||
passedChecks: 0,
|
||||
totalChecks: 0,
|
||||
sessionType: 'unknown'
|
||||
};
|
||||
console.log('Using fallback security data:', securityData);
|
||||
}
|
||||
|
||||
// Detailed information about the REAL security check
|
||||
let message = `REAL-TIME SECURITY VERIFICATION\n\n`;
|
||||
message += `Security Level: ${securityData.level} (${securityData.score}%)\n`;
|
||||
message += `Session Type: ${securityData.sessionType || 'premium'}\n`;
|
||||
message += `Verification Time: ${new Date(securityData.timestamp).toLocaleTimeString()}\n`;
|
||||
message += `Data Source: ${securityData.isRealData ? 'Real Cryptographic Tests' : 'Simulated Data'}\n\n`;
|
||||
|
||||
if (securityData.verificationResults) {
|
||||
message += 'DETAILED CRYPTOGRAPHIC TESTS:\n';
|
||||
message += '=' + '='.repeat(40) + '\n';
|
||||
|
||||
const passedTests = Object.entries(securityData.verificationResults).filter(([key, result]) => result.passed);
|
||||
const failedTests = Object.entries(securityData.verificationResults).filter(([key, result]) => !result.passed);
|
||||
|
||||
if (passedTests.length > 0) {
|
||||
message += 'PASSED TESTS:\n';
|
||||
passedTests.forEach(([key, result]) => {
|
||||
const testName = key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase());
|
||||
message += ` ${testName}: ${result.details || 'Test passed'}\n`;
|
||||
});
|
||||
message += '\n';
|
||||
}
|
||||
|
||||
if (failedTests.length > 0) {
|
||||
message += 'FAILED/UNAVAILABLE TESTS:\n';
|
||||
failedTests.forEach(([key, result]) => {
|
||||
const testName = key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase());
|
||||
message += ` ${testName}: ${result.details || 'Test failed or unavailable'}\n`;
|
||||
});
|
||||
message += '\n';
|
||||
}
|
||||
|
||||
message += `SUMMARY:\n`;
|
||||
message += `Passed: ${securityData.passedChecks}/${securityData.totalChecks} tests\n`;
|
||||
message += `Score: ${securityData.score}/${securityData.maxPossibleScore || 100} points\n\n`;
|
||||
}
|
||||
|
||||
// Real security features status
|
||||
message += `SECURITY FEATURES STATUS:\n`;
|
||||
message += '=' + '='.repeat(40) + '\n';
|
||||
|
||||
if (securityData.verificationResults) {
|
||||
const features = {
|
||||
'ECDSA Digital Signatures': securityData.verificationResults.verifyECDSASignatures?.passed || false,
|
||||
'ECDH Key Exchange': securityData.verificationResults.verifyECDHKeyExchange?.passed || false,
|
||||
'AES-GCM Encryption': securityData.verificationResults.verifyEncryption?.passed || false,
|
||||
'Message Integrity (HMAC)': securityData.verificationResults.verifyMessageIntegrity?.passed || false,
|
||||
'Perfect Forward Secrecy': securityData.verificationResults.verifyPerfectForwardSecrecy?.passed || false,
|
||||
'Replay Protection': securityData.verificationResults.verifyReplayProtection?.passed || false,
|
||||
'DTLS Fingerprint': securityData.verificationResults.verifyDTLSFingerprint?.passed || false,
|
||||
'SAS Verification': securityData.verificationResults.verifySASVerification?.passed || false,
|
||||
'Metadata Protection': securityData.verificationResults.verifyMetadataProtection?.passed || false,
|
||||
'Traffic Obfuscation': securityData.verificationResults.verifyTrafficObfuscation?.passed || false
|
||||
};
|
||||
|
||||
Object.entries(features).forEach(([feature, isEnabled]) => {
|
||||
message += `${isEnabled ? '✅' : '❌'} ${feature}\n`;
|
||||
});
|
||||
} else {
|
||||
// Fallback if no verification results
|
||||
message += `✅ ECDSA Digital Signatures\n`;
|
||||
message += `✅ ECDH Key Exchange\n`;
|
||||
message += `✅ AES-GCM Encryption\n`;
|
||||
message += `✅ Message Integrity (HMAC)\n`;
|
||||
message += `✅ Perfect Forward Secrecy\n`;
|
||||
message += `✅ Replay Protection\n`;
|
||||
message += `✅ DTLS Fingerprint\n`;
|
||||
message += `✅ SAS Verification\n`;
|
||||
message += `✅ Metadata Protection\n`;
|
||||
message += `✅ Traffic Obfuscation\n`;
|
||||
}
|
||||
|
||||
message += `\n${securityData.details || 'Real cryptographic verification completed'}`;
|
||||
|
||||
if (securityData.isRealData) {
|
||||
message += '\n\n✅ This is REAL-TIME verification using actual cryptographic functions.';
|
||||
} else {
|
||||
message += '\n\n⚠️ Warning: This data may be simulated. Connection may not be fully established.';
|
||||
}
|
||||
|
||||
// Show in a more user-friendly way
|
||||
const modal = document.createElement('div');
|
||||
modal.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.8);
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: monospace;
|
||||
`;
|
||||
|
||||
const content = document.createElement('div');
|
||||
content.style.cssText = `
|
||||
background: #1a1a1a;
|
||||
color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
max-width: 80%;
|
||||
max-height: 80%;
|
||||
overflow-y: auto;
|
||||
white-space: pre-line;
|
||||
border: 1px solid #333;
|
||||
`;
|
||||
|
||||
content.textContent = message;
|
||||
modal.appendChild(content);
|
||||
|
||||
// Close on click outside
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) {
|
||||
document.body.removeChild(modal);
|
||||
}
|
||||
});
|
||||
|
||||
// Close on Escape key
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
document.body.removeChild(modal);
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
document.body.appendChild(modal);
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// DISPLAY UTILITIES
|
||||
// ============================================
|
||||
|
||||
const getStatusConfig = () => {
|
||||
switch (status) {
|
||||
case 'connected':
|
||||
return {
|
||||
text: 'Connected',
|
||||
className: 'status-connected',
|
||||
badgeClass: 'bg-green-500/10 text-green-400 border-green-500/20'
|
||||
};
|
||||
case 'verifying':
|
||||
return {
|
||||
text: 'Verifying...',
|
||||
className: 'status-verifying',
|
||||
badgeClass: 'bg-purple-500/10 text-purple-400 border-purple-500/20'
|
||||
};
|
||||
case 'connecting':
|
||||
return {
|
||||
text: 'Connecting...',
|
||||
className: 'status-connecting',
|
||||
badgeClass: 'bg-blue-500/10 text-blue-400 border-blue-500/20'
|
||||
};
|
||||
case 'retrying':
|
||||
return {
|
||||
text: 'Retrying...',
|
||||
className: 'status-connecting',
|
||||
badgeClass: 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20'
|
||||
};
|
||||
case 'failed':
|
||||
return {
|
||||
text: 'Error',
|
||||
className: 'status-failed',
|
||||
badgeClass: 'bg-red-500/10 text-red-400 border-red-500/20'
|
||||
};
|
||||
case 'reconnecting':
|
||||
return {
|
||||
text: 'Reconnecting...',
|
||||
className: 'status-connecting',
|
||||
badgeClass: 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20'
|
||||
};
|
||||
case 'peer_disconnected':
|
||||
return {
|
||||
text: 'Peer disconnected',
|
||||
className: 'status-failed',
|
||||
badgeClass: 'bg-orange-500/10 text-orange-400 border-orange-500/20'
|
||||
};
|
||||
default:
|
||||
return {
|
||||
text: 'Not connected',
|
||||
className: 'status-disconnected',
|
||||
badgeClass: 'bg-gray-500/10 text-gray-400 border-gray-500/20'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const config = getStatusConfig();
|
||||
const displaySecurityLevel = isConnected ? (realSecurityLevel || securityLevel) : null;
|
||||
|
||||
const shouldShowTimer = hasActiveSession && currentTimeLeft > 0 && window.SessionTimer;
|
||||
|
||||
// ============================================
|
||||
// DATA RELIABILITY INDICATOR
|
||||
// ============================================
|
||||
|
||||
const getSecurityIndicatorDetails = () => {
|
||||
if (!displaySecurityLevel) {
|
||||
return {
|
||||
tooltip: 'Security verification in progress...',
|
||||
isVerified: false,
|
||||
dataSource: 'loading'
|
||||
};
|
||||
}
|
||||
|
||||
const isRealData = displaySecurityLevel.isRealData !== false;
|
||||
const baseTooltip = `${displaySecurityLevel.level} (${displaySecurityLevel.score}%)`;
|
||||
|
||||
if (isRealData) {
|
||||
return {
|
||||
tooltip: `${baseTooltip} - Real-time verification ✅\nRight-click or Ctrl+click to disconnect`,
|
||||
isVerified: true,
|
||||
dataSource: 'real'
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
tooltip: `${baseTooltip} - Estimated (connection establishing...)\nRight-click or Ctrl+click to disconnect`,
|
||||
isVerified: false,
|
||||
dataSource: 'estimated'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const securityDetails = getSecurityIndicatorDetails();
|
||||
|
||||
// ============================================
|
||||
// ADDING global methods for debugging
|
||||
// ============================================
|
||||
|
||||
React.useEffect(() => {
|
||||
window.debugHeaderSecurity = () => {
|
||||
console.log('🔍 Header Security Debug:', {
|
||||
realSecurityLevel,
|
||||
lastSecurityUpdate,
|
||||
isConnected,
|
||||
webrtcManagerProp: !!webrtcManager,
|
||||
windowWebrtcManager: !!window.webrtcManager,
|
||||
cryptoUtils: !!window.EnhancedSecureCryptoUtils,
|
||||
displaySecurityLevel: displaySecurityLevel,
|
||||
securityDetails: securityDetails
|
||||
});
|
||||
};
|
||||
|
||||
return () => {
|
||||
delete window.debugHeaderSecurity;
|
||||
};
|
||||
}, [realSecurityLevel, lastSecurityUpdate, isConnected, webrtcManager, displaySecurityLevel, securityDetails]);
|
||||
|
||||
// ============================================
|
||||
// RENDER
|
||||
// ============================================
|
||||
|
||||
return React.createElement('header', {
|
||||
className: 'header-minimal sticky top-0 z-50'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'container',
|
||||
className: 'max-w-7xl mx-auto px-4 sm:px-6 lg:px-8'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'content',
|
||||
className: 'flex items-center justify-between h-16'
|
||||
}, [
|
||||
// Logo and Title
|
||||
React.createElement('div', {
|
||||
key: 'logo-section',
|
||||
className: 'flex items-center space-x-2 sm:space-x-3'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'logo',
|
||||
className: 'icon-container w-8 h-8 sm:w-10 sm:h-10'
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
className: 'fas fa-shield-halved accent-orange text-sm sm:text-base'
|
||||
})
|
||||
]),
|
||||
React.createElement('div', {
|
||||
key: 'title-section'
|
||||
}, [
|
||||
React.createElement('h1', {
|
||||
key: 'title',
|
||||
className: 'text-lg sm:text-xl font-semibold text-primary'
|
||||
}, 'SecureBit.chat'),
|
||||
React.createElement('p', {
|
||||
key: 'subtitle',
|
||||
className: 'text-xs sm:text-sm text-muted hidden sm:block'
|
||||
}, 'End-to-end freedom v4.3.120')
|
||||
])
|
||||
]),
|
||||
|
||||
// Status and Controls - Responsive
|
||||
React.createElement('div', {
|
||||
key: 'status-section',
|
||||
className: 'flex items-center space-x-2 sm:space-x-3'
|
||||
}, [
|
||||
// Session Timer - all features enabled by default
|
||||
shouldShowTimer && React.createElement(window.SessionTimer, {
|
||||
key: 'session-timer',
|
||||
timeLeft: currentTimeLeft,
|
||||
sessionType: sessionType,
|
||||
onDisconnect: onDisconnect
|
||||
}),
|
||||
|
||||
displaySecurityLevel && React.createElement('div', {
|
||||
key: 'security-level',
|
||||
className: 'hidden md:flex items-center space-x-2 cursor-pointer hover:opacity-80 transition-opacity duration-200',
|
||||
onClick: handleSecurityClick,
|
||||
onContextMenu: (e) => {
|
||||
e.preventDefault();
|
||||
if (onDisconnect && typeof onDisconnect === 'function') {
|
||||
onDisconnect();
|
||||
}
|
||||
},
|
||||
title: securityDetails.tooltip
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'security-icon',
|
||||
className: `w-6 h-6 rounded-full flex items-center justify-center relative ${
|
||||
displaySecurityLevel.color === 'green' ? 'bg-green-500/20' :
|
||||
displaySecurityLevel.color === 'orange' ? 'bg-orange-500/20' :
|
||||
displaySecurityLevel.color === 'yellow' ? 'bg-yellow-500/20' : 'bg-red-500/20'
|
||||
} ${securityDetails.isVerified ? '' : 'animate-pulse'}`
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
className: `fas fa-shield-alt text-xs ${
|
||||
displaySecurityLevel.color === 'green' ? 'text-green-400' :
|
||||
displaySecurityLevel.color === 'orange' ? 'text-orange-400' :
|
||||
displaySecurityLevel.color === 'yellow' ? 'text-yellow-400' : 'text-red-400'
|
||||
}`
|
||||
})
|
||||
]),
|
||||
React.createElement('div', {
|
||||
key: 'security-info',
|
||||
className: 'flex flex-col'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'security-level-text',
|
||||
className: 'text-xs font-medium text-primary flex items-center space-x-1'
|
||||
}, [
|
||||
React.createElement('span', {}, `${displaySecurityLevel.level} (${displaySecurityLevel.score}%)`)
|
||||
]),
|
||||
React.createElement('div', {
|
||||
key: 'security-details',
|
||||
className: 'text-xs text-muted mt-1 hidden lg:block'
|
||||
}, securityDetails.dataSource === 'real' ?
|
||||
`${displaySecurityLevel.passedChecks || 0}/${displaySecurityLevel.totalChecks || 0} tests` :
|
||||
(displaySecurityLevel.details || `Stage ${displaySecurityLevel.stage || 1}`)
|
||||
),
|
||||
React.createElement('div', {
|
||||
key: 'security-progress',
|
||||
className: 'w-16 h-1 bg-gray-600 rounded-full overflow-hidden'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'progress-bar',
|
||||
className: `h-full transition-all duration-500 ${
|
||||
displaySecurityLevel.color === 'green' ? 'bg-green-400' :
|
||||
displaySecurityLevel.color === 'orange' ? 'bg-orange-400' :
|
||||
displaySecurityLevel.color === 'yellow' ? 'bg-yellow-400' : 'bg-red-400'
|
||||
}`,
|
||||
style: { width: `${displaySecurityLevel.score}%` }
|
||||
})
|
||||
])
|
||||
])
|
||||
]),
|
||||
|
||||
// Mobile Security Indicator
|
||||
displaySecurityLevel && React.createElement('div', {
|
||||
key: 'mobile-security',
|
||||
className: 'md:hidden flex items-center'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'mobile-security-icon',
|
||||
className: `w-8 h-8 rounded-full flex items-center justify-center cursor-pointer hover:opacity-80 transition-opacity duration-200 relative ${
|
||||
displaySecurityLevel.color === 'green' ? 'bg-green-500/20' :
|
||||
displaySecurityLevel.color === 'orange' ? 'bg-orange-500/20' :
|
||||
displaySecurityLevel.color === 'yellow' ? 'bg-yellow-500/20' : 'bg-red-500/20'
|
||||
} ${securityDetails.isVerified ? '' : 'animate-pulse'}`,
|
||||
title: securityDetails.tooltip,
|
||||
onClick: handleSecurityClick,
|
||||
onContextMenu: (e) => {
|
||||
e.preventDefault();
|
||||
if (onDisconnect && typeof onDisconnect === 'function') {
|
||||
onDisconnect();
|
||||
}
|
||||
}
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
className: `fas fa-shield-alt text-sm ${
|
||||
displaySecurityLevel.color === 'green' ? 'text-green-400' :
|
||||
displaySecurityLevel.color === 'orange' ? 'text-orange-400' :
|
||||
displaySecurityLevel.color === 'yellow' ? 'text-yellow-400' : 'text-red-400'
|
||||
}`
|
||||
})
|
||||
])
|
||||
]),
|
||||
|
||||
// Status Badge
|
||||
React.createElement('div', {
|
||||
key: 'status-badge',
|
||||
className: `px-2 sm:px-3 py-1.5 rounded-lg border ${config.badgeClass} flex items-center space-x-1 sm:space-x-2`
|
||||
}, [
|
||||
React.createElement('span', {
|
||||
key: 'status-dot',
|
||||
className: `status-dot ${config.className}`
|
||||
}),
|
||||
React.createElement('span', {
|
||||
key: 'status-text',
|
||||
className: 'text-xs sm:text-sm font-medium'
|
||||
}, config.text),
|
||||
]),
|
||||
|
||||
// Disconnect Button
|
||||
isConnected && React.createElement('button', {
|
||||
key: 'disconnect-btn',
|
||||
onClick: onDisconnect,
|
||||
className: 'p-1.5 sm:px-3 sm:py-1.5 bg-red-500/10 hover:bg-red-500/20 text-red-400 border border-red-500/20 rounded-lg transition-all duration-200 text-sm'
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
className: 'fas fa-power-off sm:mr-2'
|
||||
}),
|
||||
React.createElement('span', {
|
||||
className: 'hidden sm:inline'
|
||||
}, 'Disconnect')
|
||||
])
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
};
|
||||
|
||||
window.EnhancedMinimalHeader = EnhancedMinimalHeader;
|
||||
91
src/components/ui/PasswordModal.jsx
Normal file
91
src/components/ui/PasswordModal.jsx
Normal file
@@ -0,0 +1,91 @@
|
||||
const React = window.React;
|
||||
|
||||
const PasswordModal = ({ isOpen, onClose, onSubmit, action, password, setPassword }) => {
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
if (password.trim()) {
|
||||
onSubmit(password.trim());
|
||||
setPassword('');
|
||||
}
|
||||
};
|
||||
|
||||
const getActionText = () => {
|
||||
return action === 'offer' ? 'invitation' : 'response';
|
||||
};
|
||||
|
||||
return React.createElement('div', {
|
||||
className: 'fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'modal',
|
||||
className: 'card-minimal rounded-xl p-6 max-w-md w-full border-purple-500/20'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'header',
|
||||
className: 'flex items-center mb-4'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'icon',
|
||||
className: 'w-10 h-10 bg-purple-500/10 border border-purple-500/20 rounded-lg flex items-center justify-center mr-3'
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
className: 'fas fa-key accent-purple'
|
||||
})
|
||||
]),
|
||||
React.createElement('h3', {
|
||||
key: 'title',
|
||||
className: 'text-lg font-medium text-primary'
|
||||
}, 'Password input')
|
||||
]),
|
||||
React.createElement('form', {
|
||||
key: 'form',
|
||||
onSubmit: handleSubmit,
|
||||
className: 'space-y-4'
|
||||
}, [
|
||||
React.createElement('p', {
|
||||
key: 'description',
|
||||
className: 'text-secondary text-sm'
|
||||
}, `Enter password for decryption ${getActionText()}:`),
|
||||
React.createElement('input', {
|
||||
key: 'password-input',
|
||||
type: 'password',
|
||||
value: password,
|
||||
onChange: (e) => setPassword(e.target.value),
|
||||
placeholder: 'Enter password...',
|
||||
className: 'w-full p-3 bg-gray-900/30 border border-gray-500/20 rounded-lg text-primary placeholder-gray-500 focus:border-purple-500/40 focus:outline-none transition-all',
|
||||
autoFocus: true
|
||||
}),
|
||||
React.createElement('div', {
|
||||
key: 'buttons',
|
||||
className: 'flex space-x-3'
|
||||
}, [
|
||||
React.createElement('button', {
|
||||
key: 'submit',
|
||||
type: 'submit',
|
||||
className: 'flex-1 btn-primary text-white py-3 px-4 rounded-lg font-medium transition-all duration-200'
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
className: 'fas fa-unlock-alt mr-2'
|
||||
}),
|
||||
'Decrypt'
|
||||
]),
|
||||
React.createElement('button', {
|
||||
key: 'cancel',
|
||||
type: 'button',
|
||||
onClick: onClose,
|
||||
className: 'flex-1 btn-secondary text-white py-3 px-4 rounded-lg font-medium transition-all duration-200'
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
className: 'fas fa-times mr-2'
|
||||
}),
|
||||
'Cancel'
|
||||
])
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
};
|
||||
|
||||
window.PasswordModal = PasswordModal;
|
||||
432
src/components/ui/Roadmap.jsx
Normal file
432
src/components/ui/Roadmap.jsx
Normal file
@@ -0,0 +1,432 @@
|
||||
function Roadmap() {
|
||||
const [selectedPhase, setSelectedPhase] = React.useState(null);
|
||||
const phases = [
|
||||
{
|
||||
version: "v1.0",
|
||||
title: "Start of Development",
|
||||
status: "done",
|
||||
date: "Early 2025",
|
||||
description: "Idea, prototype, and infrastructure setup",
|
||||
features: [
|
||||
"Concept and requirements formation",
|
||||
"Stack selection: WebRTC, P2P, cryptography",
|
||||
"First messaging prototypes",
|
||||
"Repository creation and CI",
|
||||
"Basic encryption architecture",
|
||||
"UX/UI design"
|
||||
]
|
||||
},
|
||||
{
|
||||
version: "v1.5",
|
||||
title: "Alpha Release",
|
||||
status: "done",
|
||||
date: "Spring 2025",
|
||||
description: "First public alpha: basic chat and key exchange",
|
||||
features: [
|
||||
"Basic P2P messaging via WebRTC",
|
||||
"Simple E2E encryption (demo scheme)",
|
||||
"Stable signaling and reconnection",
|
||||
"Minimal UX for testing",
|
||||
"Feedback collection from early testers"
|
||||
]
|
||||
},
|
||||
{
|
||||
version: "v2.0",
|
||||
title: "Security Hardened",
|
||||
status: "done",
|
||||
date: "Summer 2025",
|
||||
description: "Security strengthening and stable branch release",
|
||||
features: [
|
||||
"ECDH/ECDSA implementation in production",
|
||||
"Perfect Forward Secrecy and key rotation",
|
||||
"Improved authentication checks",
|
||||
"File encryption and large payload transfers",
|
||||
"Audit of basic cryptoprocesses"
|
||||
]
|
||||
},
|
||||
{
|
||||
version: "v3.0",
|
||||
title: "Scaling & Stability",
|
||||
status: "done",
|
||||
date: "Fall 2025",
|
||||
description: "Network scaling and stability improvements",
|
||||
features: [
|
||||
"Optimization of P2P connections and NAT traversal",
|
||||
"Reconnection mechanisms and message queues",
|
||||
"Reduced battery consumption on mobile",
|
||||
"Support for multi-device synchronization",
|
||||
"Monitoring and logging tools for developers"
|
||||
]
|
||||
},
|
||||
{
|
||||
version: "v3.5",
|
||||
title: "Privacy-first Release",
|
||||
status: "done",
|
||||
date: "Winter 2025",
|
||||
description: "Focus on privacy: minimizing metadata",
|
||||
features: [
|
||||
"Metadata protection and fingerprint reduction",
|
||||
"Experiments with onion routing and DHT",
|
||||
"Options for anonymous connections",
|
||||
"Preparation for open code audit",
|
||||
"Improved user verification processes"
|
||||
]
|
||||
},
|
||||
|
||||
// current and future phases
|
||||
{
|
||||
version: "v4.3.120",
|
||||
title: "Enhanced Security Edition",
|
||||
status: "current",
|
||||
date: "Now",
|
||||
description: "Current version with ECDH + DTLS + SAS security, 18-layer military-grade cryptography and complete ASN.1 validation",
|
||||
features: [
|
||||
"ECDH + DTLS + SAS triple-layer security",
|
||||
"ECDH P-384 + AES-GCM 256-bit encryption",
|
||||
"DTLS fingerprint verification",
|
||||
"SAS (Short Authentication String) verification",
|
||||
"Perfect Forward Secrecy with key rotation",
|
||||
"Enhanced MITM attack prevention",
|
||||
"Complete ASN.1 DER validation",
|
||||
"OID and EC point verification",
|
||||
"SPKI structure validation",
|
||||
"P2P WebRTC architecture",
|
||||
"Metadata protection",
|
||||
"100% open source code"
|
||||
]
|
||||
},
|
||||
{
|
||||
version: "v4.5",
|
||||
title: "Mobile & Desktop Edition",
|
||||
status: "development",
|
||||
date: "Q2 2025",
|
||||
description: "Native apps for all platforms",
|
||||
features: [
|
||||
"PWA app for mobile",
|
||||
"Electron app for desktop",
|
||||
"Real-time notifications",
|
||||
"Automatic reconnection",
|
||||
"Battery optimization",
|
||||
"Cross-device synchronization",
|
||||
"Improved UX/UI",
|
||||
"Support for files up to 100MB"
|
||||
]
|
||||
},
|
||||
{
|
||||
version: "v5.0",
|
||||
title: "Quantum-Resistant Edition",
|
||||
status: "planned",
|
||||
date: "Q4 2025",
|
||||
description: "Protection against quantum computers",
|
||||
features: [
|
||||
"Post-quantum cryptography CRYSTALS-Kyber",
|
||||
"SPHINCS+ digital signatures",
|
||||
"Hybrid scheme: classic + PQ",
|
||||
"Quantum-safe key exchange",
|
||||
"Updated hashing algorithms",
|
||||
"Migration of existing sessions",
|
||||
"Compatibility with v4.x",
|
||||
"Quantum-resistant protocols"
|
||||
]
|
||||
},
|
||||
{
|
||||
version: "v5.5",
|
||||
title: "Group Communications",
|
||||
status: "planned",
|
||||
date: "Q2 2026",
|
||||
description: "Group chats with preserved privacy",
|
||||
features: [
|
||||
"P2P group connections up to 8 participants",
|
||||
"Mesh networking for groups",
|
||||
"Signal Double Ratchet for groups",
|
||||
"Anonymous groups without metadata",
|
||||
"Ephemeral groups (disappear after session)",
|
||||
"Cryptographic group administration",
|
||||
"Group member auditing"
|
||||
]
|
||||
},
|
||||
{
|
||||
version: "v6.0",
|
||||
title: "Decentralized Network",
|
||||
status: "research",
|
||||
date: "2027",
|
||||
description: "Fully decentralized network",
|
||||
features: [
|
||||
"LockBit node mesh network",
|
||||
"DHT for peer discovery",
|
||||
"Built-in onion routing",
|
||||
"Tokenomics and node incentives",
|
||||
"Governance via DAO",
|
||||
"Interoperability with other networks",
|
||||
"Cross-platform compatibility",
|
||||
"Self-healing network"
|
||||
]
|
||||
},
|
||||
{
|
||||
version: "v7.0",
|
||||
title: "AI Privacy Assistant",
|
||||
status: "research",
|
||||
date: "2028+",
|
||||
description: "AI for privacy and security",
|
||||
features: [
|
||||
"Local AI threat analysis",
|
||||
"Automatic MITM detection",
|
||||
"Adaptive cryptography",
|
||||
"Personalized security recommendations",
|
||||
"Zero-knowledge machine learning",
|
||||
"Private AI assistant",
|
||||
"Predictive security",
|
||||
"Autonomous attack protection"
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
const getStatusConfig = (status) => {
|
||||
switch (status) {
|
||||
case 'current':
|
||||
return {
|
||||
color: 'green',
|
||||
bgClass: 'bg-green-500/10 border-green-500/20',
|
||||
textClass: 'text-green-400',
|
||||
icon: 'fas fa-check-circle',
|
||||
label: 'Current Version'
|
||||
};
|
||||
case 'development':
|
||||
return {
|
||||
color: 'orange',
|
||||
bgClass: 'bg-orange-500/10 border-orange-500/20',
|
||||
textClass: 'text-orange-400',
|
||||
icon: 'fas fa-code',
|
||||
label: 'In Development'
|
||||
};
|
||||
case 'planned':
|
||||
return {
|
||||
color: 'blue',
|
||||
bgClass: 'bg-blue-500/10 border-blue-500/20',
|
||||
textClass: 'text-blue-400',
|
||||
icon: 'fas fa-calendar-alt',
|
||||
label: 'Planned'
|
||||
};
|
||||
case 'research':
|
||||
return {
|
||||
color: 'purple',
|
||||
bgClass: 'bg-purple-500/10 border-purple-500/20',
|
||||
textClass: 'text-purple-400',
|
||||
icon: 'fas fa-flask',
|
||||
label: 'Research'
|
||||
};
|
||||
case 'done':
|
||||
return {
|
||||
color: 'gray',
|
||||
bgClass: 'bg-gray-500/10 border-gray-500/20',
|
||||
textClass: 'text-gray-300',
|
||||
icon: 'fas fa-flag-checkered',
|
||||
label: 'Released'
|
||||
};
|
||||
default:
|
||||
return {
|
||||
color: 'gray',
|
||||
bgClass: 'bg-gray-500/10 border-gray-500/20',
|
||||
textClass: 'text-gray-400',
|
||||
icon: 'fas fa-question',
|
||||
label: 'Unknown'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const togglePhaseDetail = (index) => {
|
||||
setSelectedPhase(selectedPhase === index ? null : index);
|
||||
};
|
||||
return (
|
||||
<div key="roadmap-section" className="mt-16 px-4 sm:px-0">
|
||||
<div key="section-header" className="text-center mb-12">
|
||||
<h3 key="title" className="text-2xl font-semibold text-primary mb-3">
|
||||
Development Roadmap
|
||||
</h3>
|
||||
<p key="subtitle" className="text-secondary max-w-2xl mx-auto mb-6">
|
||||
Evolution of SecureBit.chat : from initial development to quantum-resistant decentralized network with complete ASN.1 validation
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div key="roadmap-container" className="max-w-6xl mx-auto">
|
||||
<div key="timeline" className="relative">
|
||||
{/* The line has been removed */}
|
||||
|
||||
<div key="phases" className="space-y-8">
|
||||
{phases.map((phase, index) => {
|
||||
const statusConfig = getStatusConfig(phase.status);
|
||||
const isExpanded = selectedPhase === index;
|
||||
|
||||
return (
|
||||
<div key={`phase-${index}`} className="relative">
|
||||
{/* The dots are visible only on sm and larger screens */}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={isExpanded}
|
||||
onClick={() => togglePhaseDetail(index)}
|
||||
key={`phase-button-${index}`}
|
||||
className={`card-minimal rounded-xl p-4 text-left w-full transition-all duration-300 ${
|
||||
isExpanded
|
||||
? "ring-2 ring-" + statusConfig.color + "-500/30"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
key="phase-header"
|
||||
className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-4 space-y-2 sm:space-y-0"
|
||||
>
|
||||
<div
|
||||
key="phase-info"
|
||||
className="flex flex-col sm:flex-row sm:items-center sm:space-x-4"
|
||||
>
|
||||
<div
|
||||
key="version-badge"
|
||||
className={`px-3 py-1 ${statusConfig.bgClass} border rounded-lg mb-2 sm:mb-0`}
|
||||
>
|
||||
<span
|
||||
key="version"
|
||||
className={`${statusConfig.textClass} font-bold text-sm`}
|
||||
>
|
||||
{phase.version}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div key="title-section">
|
||||
<h4
|
||||
key="title"
|
||||
className="text-lg font-semibold text-primary"
|
||||
>
|
||||
{phase.title}
|
||||
</h4>
|
||||
<p
|
||||
key="description"
|
||||
className="text-secondary text-sm"
|
||||
>
|
||||
{phase.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
key="phase-meta"
|
||||
className="flex items-center space-x-3 text-sm text-gray-400 font-medium"
|
||||
>
|
||||
<div
|
||||
key="status-badge"
|
||||
className={`flex items-center px-3 py-1 ${statusConfig.bgClass} border rounded-lg`}
|
||||
>
|
||||
<i
|
||||
key="status-icon"
|
||||
className={`${statusConfig.icon} ${statusConfig.textClass} mr-2 text-xs`}
|
||||
/>
|
||||
<span
|
||||
key="status-text"
|
||||
className={`${statusConfig.textClass} text-xs font-medium`}
|
||||
>
|
||||
{statusConfig.label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div key="date">{phase.date}</div>
|
||||
<i
|
||||
key="expand-icon"
|
||||
className={`fas fa-chevron-${
|
||||
isExpanded ? "up" : "down"
|
||||
} text-gray-400 text-sm`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
<div
|
||||
key="features-section"
|
||||
className="mt-6 pt-6 border-t border-gray-700/30"
|
||||
>
|
||||
<h5
|
||||
key="features-title"
|
||||
className="text-primary font-medium mb-4 flex items-center"
|
||||
>
|
||||
<i
|
||||
key="features-icon"
|
||||
className="fas fa-list-ul mr-2 text-sm"
|
||||
/>
|
||||
Key features:
|
||||
</h5>
|
||||
|
||||
<div
|
||||
key="features-grid"
|
||||
className="grid md:grid-cols-2 gap-3"
|
||||
>
|
||||
{phase.features.map((feature, featureIndex) => (
|
||||
<div
|
||||
key={`feature-${featureIndex}`}
|
||||
className="flex items-center space-x-3 p-3 bg-custom-bg rounded-lg"
|
||||
>
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full ${statusConfig.textClass.replace(
|
||||
"text-",
|
||||
"bg-"
|
||||
)}`}
|
||||
/>
|
||||
<span className="text-secondary text-sm">
|
||||
{feature}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div key="cta-section" className="mt-12 text-center">
|
||||
<div
|
||||
key="cta-card"
|
||||
className="card-minimal rounded-xl p-8 max-w-2xl mx-auto"
|
||||
>
|
||||
<h4
|
||||
key="cta-title"
|
||||
className="text-xl font-semibold text-primary mb-3"
|
||||
>
|
||||
Join the future of privacy
|
||||
</h4>
|
||||
<p key="cta-description" className="text-secondary mb-6">
|
||||
SecureBit.chat grows thanks to the community. Your ideas and feedback help shape the future of secure communication with complete ASN.1 validation.
|
||||
</p>
|
||||
|
||||
<div
|
||||
key="cta-buttons"
|
||||
className="flex flex-col sm:flex-row gap-4 justify-center"
|
||||
>
|
||||
<a
|
||||
key="github-link"
|
||||
href="https://github.com/SecureBitChat/SecureBitChatBrowserExtension"
|
||||
className="btn-primary text-white py-3 px-6 rounded-lg font-medium transition-all duration-200 flex items-center justify-center"
|
||||
>
|
||||
<i key="github-icon" className="fab fa-github mr-2" />
|
||||
GitHub Repository
|
||||
</a>
|
||||
|
||||
<a
|
||||
key="feedback-link"
|
||||
href="mailto:lockbitchat@tutanota.com"
|
||||
className="btn-secondary text-white py-3 px-6 rounded-lg font-medium transition-all duration-200 flex items-center justify-center"
|
||||
>
|
||||
<i key="feedback-icon" className="fas fa-comments mr-2" />
|
||||
Feedback
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
window.Roadmap = Roadmap;
|
||||
59
src/components/ui/SecurityFeatures.jsx
Normal file
59
src/components/ui/SecurityFeatures.jsx
Normal file
@@ -0,0 +1,59 @@
|
||||
const SecurityFeatures = () => {
|
||||
const features = [
|
||||
{ id: 'feature1', color: '#00ff88', icon: 'fas fa-key accent-green', title: 'ECDH P-384 Key Exchange', desc: 'Military-grade elliptic curve key exchange' },
|
||||
{ id: 'feature2', color: '#a78bfa', icon: 'fas fa-user-shield accent-purple', title: 'MITM Protection', desc: 'Out-of-band verification against attacks' },
|
||||
{ id: 'feature3', color: '#ff8800', icon: 'fas fa-lock accent-orange', title: 'AES-GCM 256 Encryption', desc: 'Authenticated encryption standard' },
|
||||
{ id: 'feature4', color: '#00ffff', icon: 'fas fa-sync-alt accent-cyan', title: 'Perfect Forward Secrecy', desc: 'Automatic key rotation every 5 minutes' },
|
||||
{ id: 'feature5', color: '#0088ff', icon: 'fas fa-signature accent-blue', title: 'ECDSA P-384 Signatures', desc: 'Digital signatures for message integrity' },
|
||||
{ id: 'feature6', color: '#f87171', icon: 'fas fa-shield-alt accent-red', title: 'SAS Security', desc: 'Revolutionary key exchange & MITM protection' }
|
||||
];
|
||||
|
||||
React.useEffect(() => {
|
||||
const cards = document.querySelectorAll(".card");
|
||||
const radius = 200;
|
||||
|
||||
const handleMove = (e) => {
|
||||
cards.forEach((card) => {
|
||||
const rect = card.getBoundingClientRect();
|
||||
const cx = rect.left + rect.width / 2;
|
||||
const cy = rect.top + rect.height / 2;
|
||||
|
||||
const dx = e.clientX - cx;
|
||||
const dy = e.clientY - cy;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (dist < radius) {
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
card.style.setProperty("--x", `${x}px`);
|
||||
card.style.setProperty("--y", `${y}px`);
|
||||
card.classList.add("active-glow");
|
||||
} else {
|
||||
card.classList.remove("active-glow");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener("mousemove", handleMove);
|
||||
return () => window.removeEventListener("mousemove", handleMove);
|
||||
}, []);
|
||||
|
||||
const renderFeature = (f) =>
|
||||
React.createElement('div', {
|
||||
key: f.id,
|
||||
className: "card p-3 sm:p-4 text-center",
|
||||
style: { "--color": f.color }
|
||||
}, [
|
||||
React.createElement('div', { key: 'icon', className: "w-10 h-10 sm:w-12 sm:h-12 flex items-center justify-center mx-auto mb-2 sm:mb-3 relative z-10" }, [
|
||||
React.createElement('i', { className: f.icon })
|
||||
]),
|
||||
React.createElement('h4', { key: 'title', className: "text-xs sm:text-sm font-medium text-primary mb-1 relative z-10" }, f.title),
|
||||
React.createElement('p', { key: 'desc', className: "text-xs text-muted leading-tight relative z-10" }, f.desc)
|
||||
]);
|
||||
|
||||
return React.createElement('div', {
|
||||
className: "grid grid-cols-2 md:grid-cols-2 lg:grid-cols-3 gap-3 sm:gap-4 max-w-6xl mx-auto mt-8"
|
||||
}, features.map(renderFeature));
|
||||
};
|
||||
|
||||
window.SecurityFeatures = SecurityFeatures;
|
||||
334
src/components/ui/SessionTimer.jsx
Normal file
334
src/components/ui/SessionTimer.jsx
Normal file
@@ -0,0 +1,334 @@
|
||||
// SessionTimer Component - v4.3.120 - ECDH + DTLS + SAS
|
||||
const SessionTimer = ({ timeLeft, sessionType, sessionManager, onDisconnect }) => {
|
||||
const [currentTime, setCurrentTime] = React.useState(timeLeft || 0);
|
||||
const [showExpiredMessage, setShowExpiredMessage] = React.useState(false);
|
||||
const [initialized, setInitialized] = React.useState(false);
|
||||
const [connectionBroken, setConnectionBroken] = React.useState(false);
|
||||
|
||||
|
||||
const [loggedHidden, setLoggedHidden] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (connectionBroken) {
|
||||
if (!loggedHidden) {
|
||||
console.log('⏱️ SessionTimer initialization skipped - connection broken');
|
||||
setLoggedHidden(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let initialTime = 0;
|
||||
|
||||
if (sessionManager?.hasActiveSession()) {
|
||||
initialTime = sessionManager.getTimeLeft();
|
||||
} else if (timeLeft && timeLeft > 0) {
|
||||
initialTime = timeLeft;
|
||||
}
|
||||
|
||||
if (initialTime <= 0) {
|
||||
setCurrentTime(0);
|
||||
setInitialized(false);
|
||||
setLoggedHidden(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (connectionBroken) {
|
||||
setCurrentTime(0);
|
||||
setInitialized(false);
|
||||
setLoggedHidden(true);
|
||||
return;
|
||||
}
|
||||
setCurrentTime(initialTime);
|
||||
setInitialized(true);
|
||||
setLoggedHidden(false);
|
||||
}, [sessionManager, connectionBroken]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (connectionBroken) {
|
||||
if (!loggedHidden) {
|
||||
setLoggedHidden(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (timeLeft && timeLeft > 0) {
|
||||
setCurrentTime(timeLeft);
|
||||
}
|
||||
setLoggedHidden(false);
|
||||
}, [timeLeft, connectionBroken]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (connectionBroken) {
|
||||
if (!loggedHidden) {
|
||||
setLoggedHidden(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentTime || currentTime <= 0 || !sessionManager) {
|
||||
return;
|
||||
}
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (connectionBroken) {
|
||||
setCurrentTime(0);
|
||||
clearInterval(interval);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sessionManager?.hasActiveSession()) {
|
||||
const newTime = sessionManager.getTimeLeft();
|
||||
setCurrentTime(newTime);
|
||||
|
||||
if (window.DEBUG_MODE && Math.floor(Date.now() / 30000) !== Math.floor((Date.now() - 1000) / 30000)) {
|
||||
console.log('⏱️ Timer tick:', Math.floor(newTime / 1000) + 's');
|
||||
}
|
||||
|
||||
if (newTime <= 0) {
|
||||
setShowExpiredMessage(true);
|
||||
setTimeout(() => setShowExpiredMessage(false), 5000);
|
||||
clearInterval(interval);
|
||||
}
|
||||
} else {
|
||||
setCurrentTime(0);
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [initialized, currentTime, sessionManager, connectionBroken]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleSessionTimerUpdate = (event) => {
|
||||
if (connectionBroken) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.detail.timeLeft && event.detail.timeLeft > 0) {
|
||||
setCurrentTime(event.detail.timeLeft);
|
||||
}
|
||||
};
|
||||
|
||||
const handleForceHeaderUpdate = (event) => {
|
||||
if (connectionBroken) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sessionManager && sessionManager.hasActiveSession()) {
|
||||
const newTime = sessionManager.getTimeLeft();
|
||||
setCurrentTime(newTime);
|
||||
} else {
|
||||
setCurrentTime(event.detail.timeLeft);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePeerDisconnect = (event) => {
|
||||
setConnectionBroken(true);
|
||||
setCurrentTime(0);
|
||||
setShowExpiredMessage(false);
|
||||
setLoggedHidden(false);
|
||||
};
|
||||
|
||||
const handleNewConnection = (event) => {
|
||||
setConnectionBroken(false);
|
||||
setLoggedHidden(false);
|
||||
};
|
||||
|
||||
const handleConnectionCleaned = (event) => {
|
||||
setConnectionBroken(true);
|
||||
setCurrentTime(0);
|
||||
setShowExpiredMessage(false);
|
||||
setInitialized(false);
|
||||
setLoggedHidden(false);
|
||||
};
|
||||
|
||||
const handleSessionReset = (event) => {
|
||||
setConnectionBroken(true);
|
||||
setCurrentTime(0);
|
||||
setShowExpiredMessage(false);
|
||||
setInitialized(false);
|
||||
setLoggedHidden(false);
|
||||
};
|
||||
|
||||
const handleSessionCleanup = (event) => {
|
||||
setConnectionBroken(true);
|
||||
setCurrentTime(0);
|
||||
setShowExpiredMessage(false);
|
||||
setInitialized(false);
|
||||
setLoggedHidden(false);
|
||||
};
|
||||
|
||||
const handleDisconnected = (event) => {
|
||||
setConnectionBroken(true);
|
||||
setCurrentTime(0);
|
||||
setShowExpiredMessage(false);
|
||||
setInitialized(false);
|
||||
setLoggedHidden(false);
|
||||
};
|
||||
|
||||
document.addEventListener('session-timer-update', handleSessionTimerUpdate);
|
||||
document.addEventListener('force-header-update', handleForceHeaderUpdate);
|
||||
document.addEventListener('peer-disconnect', handlePeerDisconnect);
|
||||
document.addEventListener('new-connection', handleNewConnection);
|
||||
document.addEventListener('connection-cleaned', handleConnectionCleaned);
|
||||
document.addEventListener('session-reset', handleSessionReset);
|
||||
document.addEventListener('session-cleanup', handleSessionCleanup);
|
||||
document.addEventListener('disconnected', handleDisconnected);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('session-timer-update', handleSessionTimerUpdate);
|
||||
document.removeEventListener('force-header-update', handleForceHeaderUpdate);
|
||||
document.removeEventListener('peer-disconnect', handlePeerDisconnect);
|
||||
document.removeEventListener('new-connection', handleNewConnection);
|
||||
document.removeEventListener('connection-cleaned', handleConnectionCleaned);
|
||||
document.removeEventListener('session-reset', handleSessionReset);
|
||||
document.removeEventListener('session-cleanup', handleSessionCleanup);
|
||||
document.removeEventListener('disconnected', handleDisconnected);
|
||||
};
|
||||
}, [sessionManager]);
|
||||
|
||||
if (showExpiredMessage) {
|
||||
return React.createElement('div', {
|
||||
className: 'session-timer expired flex items-center space-x-2 px-3 py-1.5 rounded-lg animate-pulse',
|
||||
style: { background: 'linear-gradient(135deg, rgba(239, 68, 68, 0.2) 0%, rgba(220, 38, 38, 0.2) 100%)' }
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'icon',
|
||||
className: 'fas fa-exclamation-triangle text-red-400'
|
||||
}),
|
||||
React.createElement('span', {
|
||||
key: 'message',
|
||||
className: 'text-red-400 text-sm font-medium'
|
||||
}, 'Session Expired!')
|
||||
]);
|
||||
}
|
||||
|
||||
if (!sessionManager) {
|
||||
if (!loggedHidden) {
|
||||
console.log('⏱️ SessionTimer hidden - no sessionManager');
|
||||
setLoggedHidden(true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (connectionBroken) {
|
||||
if (!loggedHidden) {
|
||||
console.log('⏱️ SessionTimer hidden - connection broken');
|
||||
setLoggedHidden(true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!currentTime || currentTime <= 0) {
|
||||
if (!loggedHidden) {
|
||||
console.log('⏱️ SessionTimer hidden - no time left, currentTime:', currentTime);
|
||||
setLoggedHidden(true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (loggedHidden) {
|
||||
setLoggedHidden(false);
|
||||
}
|
||||
|
||||
const totalMinutes = Math.floor(currentTime / (60 * 1000));
|
||||
const totalSeconds = Math.floor(currentTime / 1000);
|
||||
|
||||
const isDemo = sessionType === 'demo';
|
||||
const isWarning = isDemo ? totalMinutes <= 2 : totalMinutes <= 10;
|
||||
const isCritical = isDemo ? totalSeconds <= 60 : totalMinutes <= 5;
|
||||
|
||||
const formatTime = (ms) => {
|
||||
const hours = Math.floor(ms / (60 * 60 * 1000));
|
||||
const minutes = Math.floor((ms % (60 * 60 * 1000)) / (60 * 1000));
|
||||
const seconds = Math.floor((ms % (60 * 1000)) / 1000);
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||
} else {
|
||||
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
};
|
||||
|
||||
const getTimerStyle = () => {
|
||||
const totalDuration = sessionType === 'demo' ? 6 * 60 * 1000 : 60 * 60 * 1000;
|
||||
const timeProgress = (totalDuration - currentTime) / totalDuration;
|
||||
|
||||
let backgroundColor, textColor, iconColor, iconClass, shouldPulse;
|
||||
|
||||
if (timeProgress <= 0.33) {
|
||||
backgroundColor = 'linear-gradient(135deg, rgba(34, 197, 94, 0.15) 0%, rgba(22, 163, 74, 0.15) 100%)';
|
||||
textColor = 'text-green-400';
|
||||
iconColor = 'text-green-400';
|
||||
iconClass = 'fas fa-clock';
|
||||
shouldPulse = false;
|
||||
} else if (timeProgress <= 0.66) {
|
||||
backgroundColor = 'linear-gradient(135deg, rgba(234, 179, 8, 0.15) 0%, rgba(202, 138, 4, 0.15) 100%)';
|
||||
textColor = 'text-yellow-400';
|
||||
iconColor = 'text-yellow-400';
|
||||
iconClass = 'fas fa-clock';
|
||||
shouldPulse = false;
|
||||
} else {
|
||||
backgroundColor = 'linear-gradient(135deg, rgba(239, 68, 68, 0.15) 0%, rgba(220, 38, 38, 0.15) 100%)';
|
||||
textColor = 'text-red-400';
|
||||
iconColor = 'text-red-400';
|
||||
iconClass = 'fas fa-exclamation-triangle';
|
||||
shouldPulse = true;
|
||||
}
|
||||
|
||||
return { backgroundColor, textColor, iconColor, iconClass, shouldPulse };
|
||||
};
|
||||
|
||||
const timerStyle = getTimerStyle();
|
||||
|
||||
const handleTimerClick = () => {
|
||||
if (onDisconnect && typeof onDisconnect === 'function') {
|
||||
onDisconnect();
|
||||
}
|
||||
};
|
||||
|
||||
return React.createElement('div', {
|
||||
className: `session-timer flex items-center space-x-2 px-3 py-1.5 rounded-lg transition-all duration-500 cursor-pointer hover:opacity-80 ${
|
||||
isDemo ? 'demo-session' : ''
|
||||
} ${timerStyle.shouldPulse ? 'animate-pulse' : ''}`,
|
||||
style: { background: timerStyle.backgroundColor },
|
||||
onClick: handleTimerClick,
|
||||
title: 'Click to disconnect and clear session'
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'icon',
|
||||
className: `${timerStyle.iconClass} ${timerStyle.iconColor}`
|
||||
}),
|
||||
React.createElement('span', {
|
||||
key: 'time',
|
||||
className: `text-sm font-mono font-semibold ${timerStyle.textColor}`
|
||||
}, formatTime(currentTime)),
|
||||
React.createElement('div', {
|
||||
key: 'progress',
|
||||
className: 'ml-2 w-16 h-1 bg-gray-700 rounded-full overflow-hidden'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'progress-bar',
|
||||
className: `${timerStyle.textColor.replace('text-', 'bg-')} h-full rounded-full transition-all duration-500`,
|
||||
style: {
|
||||
width: `${Math.max(0, Math.min(100, (currentTime / (sessionType === 'demo' ? 6 * 60 * 1000 : 60 * 60 * 1000)) * 100))}%`
|
||||
}
|
||||
})
|
||||
])
|
||||
]);
|
||||
};
|
||||
|
||||
window.SessionTimer = SessionTimer;
|
||||
|
||||
window.updateSessionTimer = (newTimeLeft, newSessionType) => {
|
||||
document.dispatchEvent(new CustomEvent('session-timer-update', {
|
||||
detail: { timeLeft: newTimeLeft, sessionType: newSessionType }
|
||||
}));
|
||||
};
|
||||
|
||||
123
src/components/ui/Testimonials.jsx
Normal file
123
src/components/ui/Testimonials.jsx
Normal file
@@ -0,0 +1,123 @@
|
||||
const Testimonials = () => {
|
||||
const testimonials = [
|
||||
{ id: "t1", rating: 5.0, text: "The interface feels modern and smooth. It saves me at least 2 hours every day when managing design tasks."},
|
||||
{ id: "t2", rating: 5.0, text: "Finally, a solution that blends speed with simplicity. My team adopted it within a week without training."},
|
||||
{ id: "t3", rating: 5.0, text: "I can track progress in real time and get a clear overview of our workflow. It feels empowering."},
|
||||
{ id: "t4", rating: 5.0, text: "Our pipeline visibility improved dramatically. I no longer need to manually track updates."},
|
||||
{ id: "t5", rating: 5.0, text: "The security-first approach gives me peace of mind. We handle sensitive data with confidence now."},
|
||||
{ id: "t6", rating: 5.0, text: "User feedback cycles are now twice as fast. It helps us test and ship features quickly."}
|
||||
];
|
||||
|
||||
React.useEffect(() => {
|
||||
const colUp = document.querySelector(".col-up");
|
||||
const colDown = document.querySelector(".col-down");
|
||||
const wrapper = document.querySelector(".testimonials-wrapper");
|
||||
|
||||
if (!colUp || !colDown || !wrapper) return;
|
||||
|
||||
let paused = false;
|
||||
const speed = 0.5;
|
||||
let animationId;
|
||||
|
||||
const cloneCards = (container) => {
|
||||
const cards = Array.from(container.children);
|
||||
cards.forEach(card => {
|
||||
const clone = card.cloneNode(true);
|
||||
container.appendChild(clone);
|
||||
});
|
||||
};
|
||||
|
||||
cloneCards(colUp);
|
||||
cloneCards(colDown);
|
||||
|
||||
const getHalfHeight = (el) => {
|
||||
const children = Array.from(el.children);
|
||||
const halfCount = children.length / 2;
|
||||
let height = 0;
|
||||
for (let i = 0; i < halfCount; i++) {
|
||||
height += children[i].offsetHeight;
|
||||
if (i < halfCount - 1) height += 24;
|
||||
}
|
||||
return height;
|
||||
};
|
||||
|
||||
let y1 = 0;
|
||||
const maxScroll1 = getHalfHeight(colUp);
|
||||
const maxScroll2 = getHalfHeight(colDown);
|
||||
let y2 = -maxScroll2;
|
||||
|
||||
function animate() {
|
||||
if (!paused) {
|
||||
y1 -= speed;
|
||||
y2 += speed;
|
||||
|
||||
if (Math.abs(y1) >= maxScroll1) {
|
||||
y1 = 0;
|
||||
}
|
||||
|
||||
if (y2 >= 0) {
|
||||
y2 = -maxScroll2;
|
||||
}
|
||||
|
||||
colUp.style.transform = `translateY(${y1}px)`;
|
||||
colDown.style.transform = `translateY(${y2}px)`;
|
||||
}
|
||||
animationId = requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
animate();
|
||||
|
||||
const handleMouseEnter = () => { paused = true; };
|
||||
const handleMouseLeave = () => { paused = false; };
|
||||
|
||||
wrapper.addEventListener("mouseenter", handleMouseEnter);
|
||||
wrapper.addEventListener("mouseleave", handleMouseLeave);
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(animationId);
|
||||
wrapper.removeEventListener("mouseenter", handleMouseEnter);
|
||||
wrapper.removeEventListener("mouseleave", handleMouseLeave);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const renderCard = (t, index) => (
|
||||
<div key={`${t.id}-${index}`} className="card bg-neutral-900 rounded-xl p-5 shadow-md w-72 text-sm text-white flex-shrink-0">
|
||||
<div className="flex items-center mb-2 text-yellow-400">
|
||||
{"★".repeat(Math.floor(t.rating))}
|
||||
<span className="ml-2 text-secondary">{t.rating.toFixed(1)}</span>
|
||||
</div>
|
||||
<p className="text-secondary mb-3">{t.text}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="py-14 px-6 bg-transparent">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-5 gap-12 max-w-7xl mx-auto items-center">
|
||||
<div className="lg:col-span-2 flex flex-col justify-center">
|
||||
<p className="text-sm text-secondary mb-2">Testimonials</p>
|
||||
<h2 className="text-2xl sm:text-3xl font-bold text-white mb-4 leading-snug">
|
||||
What our users are saying
|
||||
</h2>
|
||||
<p className="text-secondary text-sm">
|
||||
We continuously listen to our community and improve every day.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-3 testimonials-wrapper flex gap-6 overflow-hidden relative h-[420px]">
|
||||
<div className="pointer-events-none absolute top-0 left-0 w-full h-16 bg-gradient-to-b from-[#1f1f1f]/90 to-transparent z-20"></div>
|
||||
<div className="pointer-events-none absolute bottom-0 left-0 w-full h-16 bg-gradient-to-t from-[#1f1f1f]/90 to-transparent z-20"></div>
|
||||
|
||||
<div className="col-up flex flex-col gap-6">
|
||||
{testimonials.map((t, i) => renderCard(t, i))}
|
||||
</div>
|
||||
|
||||
<div className="col-down flex flex-col gap-6">
|
||||
{testimonials.map((t, i) => renderCard(t, i))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
window.Testimonials = Testimonials;
|
||||
209
src/components/ui/UniqueFeatureSlider.jsx
Normal file
209
src/components/ui/UniqueFeatureSlider.jsx
Normal file
@@ -0,0 +1,209 @@
|
||||
// Enhanced Modern Slider Component with Loading Protection
|
||||
const UniqueFeatureSlider = () => {
|
||||
const trackRef = React.useRef(null);
|
||||
const wrapRef = React.useRef(null);
|
||||
const [current, setCurrent] = React.useState(0);
|
||||
const [isReady, setIsReady] = React.useState(false);
|
||||
|
||||
const slides = [
|
||||
{
|
||||
icon: "🛡️",
|
||||
bgImage: "linear-gradient(135deg, rgb(255 107 53 / 6%) 0%, rgb(255 140 66 / 45%) 100%)",
|
||||
thumbIcon: "🔒",
|
||||
title: "18-Layer Military Security",
|
||||
description: "Revolutionary defense system with ECDH P-384 + AES-GCM 256 + ECDSA + Complete ASN.1 Validation."
|
||||
},
|
||||
{
|
||||
icon: "🌐",
|
||||
bgImage: "linear-gradient(135deg, rgb(147 51 234 / 6%) 0%, rgb(168 85 247 / 45%) 100%)",
|
||||
thumbIcon: "🔗",
|
||||
title: "Pure P2P WebRTC",
|
||||
description: "Direct peer-to-peer connections without any servers. Complete decentralization with zero infrastructure."
|
||||
},
|
||||
{
|
||||
icon: "🔄",
|
||||
bgImage: "linear-gradient(135deg, rgb(16 185 129 / 6%) 0%, rgb(52 211 153 / 45%) 100%)",
|
||||
thumbIcon: "⚡",
|
||||
title: "Perfect Forward Secrecy",
|
||||
description: "Automatic key rotation every 5 minutes. Non-extractable keys with hardware protection."
|
||||
},
|
||||
{
|
||||
icon: "🎭",
|
||||
bgImage: "linear-gradient(135deg, rgb(6 182 212 / 6%) 0%, rgb(34 211 238 / 45%) 100%)",
|
||||
thumbIcon: "🌫️",
|
||||
title: "Traffic Obfuscation",
|
||||
description: "Fake traffic generation and pattern masking make communication indistinguishable from noise."
|
||||
},
|
||||
{
|
||||
icon: "👁️",
|
||||
bgImage: "linear-gradient(135deg, rgb(37 99 235 / 6%) 0%, rgb(59 130 246 / 45%) 100%)",
|
||||
thumbIcon: "🚫",
|
||||
title: "Zero Data Collection",
|
||||
description: "No registration, no servers, no logs. Complete anonymity with instant channels."
|
||||
}
|
||||
];
|
||||
|
||||
// Проверка готовности компонента
|
||||
React.useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setIsReady(true);
|
||||
}, 100);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
const isMobile = () => window.matchMedia("(max-width:767px)").matches;
|
||||
|
||||
const center = React.useCallback((i) => {
|
||||
if (!trackRef.current || !wrapRef.current) return;
|
||||
const card = trackRef.current.children[i];
|
||||
if (!card) return;
|
||||
|
||||
const axis = isMobile() ? "top" : "left";
|
||||
const size = isMobile() ? "clientHeight" : "clientWidth";
|
||||
const start = isMobile() ? card.offsetTop : card.offsetLeft;
|
||||
|
||||
wrapRef.current.scrollTo({
|
||||
[axis]: start - (wrapRef.current[size] / 2 - card[size] / 2),
|
||||
behavior: "smooth"
|
||||
});
|
||||
}, []);
|
||||
|
||||
const activate = React.useCallback((i, scroll = false) => {
|
||||
if (i === current) return;
|
||||
setCurrent(i);
|
||||
if (scroll) {
|
||||
setTimeout(() => center(i), 50);
|
||||
}
|
||||
}, [current, center]);
|
||||
|
||||
const go = (step) => {
|
||||
const newIndex = Math.min(Math.max(current + step, 0), slides.length - 1);
|
||||
activate(newIndex, true);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleKeydown = (e) => {
|
||||
if (["ArrowRight", "ArrowDown"].includes(e.key)) go(1);
|
||||
if (["ArrowLeft", "ArrowUp"].includes(e.key)) go(-1);
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeydown, { passive: true });
|
||||
return () => window.removeEventListener("keydown", handleKeydown);
|
||||
}, [current]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isReady) {
|
||||
center(current);
|
||||
}
|
||||
}, [current, center, isReady]);
|
||||
// Render loading state if not ready
|
||||
if (!isReady) {
|
||||
return React.createElement('section', {
|
||||
style: {
|
||||
background: 'transparent',
|
||||
minHeight: '400px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}
|
||||
},
|
||||
React.createElement('div', {
|
||||
style: {
|
||||
opacity: 0.5,
|
||||
fontSize: '14px',
|
||||
color: '#fff'
|
||||
}
|
||||
}, 'Loading...')
|
||||
);
|
||||
}
|
||||
|
||||
return React.createElement('section', { style: { background: 'transparent' } }, [
|
||||
// Header
|
||||
React.createElement('div', {
|
||||
key: 'head',
|
||||
className: 'head'
|
||||
}, [
|
||||
React.createElement('h2', {
|
||||
key: 'title',
|
||||
className: 'text-2xl sm:text-3xl font-bold text-white mb-4 leading-snug'
|
||||
}, 'Why SecureBit.chat is unique'),
|
||||
React.createElement('div', {
|
||||
key: 'controls',
|
||||
className: 'controls'
|
||||
}, [
|
||||
React.createElement('button', {
|
||||
key: 'prev',
|
||||
id: 'prev-slider',
|
||||
className: 'nav-btn',
|
||||
'aria-label': 'Prev',
|
||||
disabled: current === 0,
|
||||
onClick: () => go(-1)
|
||||
}, '‹'),
|
||||
React.createElement('button', {
|
||||
key: 'next',
|
||||
id: 'next-slider',
|
||||
className: 'nav-btn',
|
||||
'aria-label': 'Next',
|
||||
disabled: current === slides.length - 1,
|
||||
onClick: () => go(1)
|
||||
}, '›')
|
||||
])
|
||||
]),
|
||||
|
||||
// Slider
|
||||
React.createElement('div', {
|
||||
key: 'slider',
|
||||
className: 'slider',
|
||||
ref: wrapRef
|
||||
},
|
||||
React.createElement('div', {
|
||||
className: 'track',
|
||||
ref: trackRef
|
||||
}, slides.map((slide, index) =>
|
||||
React.createElement('article', {
|
||||
key: index,
|
||||
className: 'project-card',
|
||||
...(index === current ? { active: '' } : {}),
|
||||
onMouseEnter: () => {
|
||||
if (window.matchMedia("(hover:hover)").matches) {
|
||||
activate(index, true);
|
||||
}
|
||||
},
|
||||
onClick: () => activate(index, true)
|
||||
}, [
|
||||
// Background
|
||||
React.createElement('div', {
|
||||
key: 'bg',
|
||||
className: 'project-card__bg',
|
||||
style: {
|
||||
background: slide.bgImage,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center'
|
||||
}
|
||||
}),
|
||||
|
||||
// Content
|
||||
React.createElement('div', {
|
||||
key: 'content',
|
||||
className: 'project-card__content'
|
||||
}, [
|
||||
// Text container
|
||||
React.createElement('div', { key: 'text' }, [
|
||||
React.createElement('h3', {
|
||||
key: 'title',
|
||||
className: 'project-card__title'
|
||||
}, slide.title),
|
||||
React.createElement('p', {
|
||||
key: 'desc',
|
||||
className: 'project-card__desc'
|
||||
}, slide.description)
|
||||
])
|
||||
])
|
||||
])
|
||||
))
|
||||
),
|
||||
]);
|
||||
};
|
||||
|
||||
// Export for use in your app
|
||||
window.UniqueFeatureSlider = UniqueFeatureSlider;
|
||||
2674
src/crypto/EnhancedSecureCryptoUtils.js
Normal file
2674
src/crypto/EnhancedSecureCryptoUtils.js
Normal file
File diff suppressed because it is too large
Load Diff
481
src/crypto/cose-qr.js
Normal file
481
src/crypto/cose-qr.js
Normal file
@@ -0,0 +1,481 @@
|
||||
/**
|
||||
* COSE-based QR Code Compression and Encryption
|
||||
* Implements secure payload packing with CBOR, compression, and chunking
|
||||
*/
|
||||
|
||||
import * as cbor from 'cbor-js';
|
||||
import * as pako from 'pako';
|
||||
import * as base64 from 'base64-js';
|
||||
|
||||
// Base64URL encoding/decoding helpers
|
||||
function toBase64Url(uint8) {
|
||||
let b64 = base64.fromByteArray(uint8);
|
||||
return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
||||
}
|
||||
|
||||
function fromBase64Url(str) {
|
||||
str = str.replace(/-/g, '+').replace(/_/g, '/');
|
||||
while (str.length % 4) str += '=';
|
||||
return base64.toByteArray(str);
|
||||
}
|
||||
|
||||
// Generate UUID for chunking
|
||||
function generateUUID() {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
||||
const r = Math.random() * 16 | 0;
|
||||
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Pack secure payload using COSE-like structure with compression
|
||||
* @param {Object} payloadObj - The data to pack
|
||||
* @param {CryptoKey} senderEcdsaPrivKey - Sender's signing key (optional)
|
||||
* @param {CryptoKey} recipientEcdhPubKey - Recipient's ECDH key (optional, null for broadcast)
|
||||
* @returns {Array<string>} Array of QR code strings (chunks)
|
||||
*/
|
||||
export async function packSecurePayload(payloadObj, senderEcdsaPrivKey = null, recipientEcdhPubKey = null) {
|
||||
try {
|
||||
console.log('🔐 Starting COSE packing...');
|
||||
|
||||
// 1. Canonicalize payload (minified JSON)
|
||||
const payloadJson = JSON.stringify(payloadObj);
|
||||
console.log(`📊 Original payload size: ${payloadJson.length} characters`);
|
||||
|
||||
// 2. Create ephemeral ECDH keypair (P-384) for encryption
|
||||
let ciphertextCose;
|
||||
let ephemeralRaw = null;
|
||||
|
||||
if (recipientEcdhPubKey) {
|
||||
console.log('🔐 Encrypting for specific recipient...');
|
||||
|
||||
// Generate ephemeral ECDH keypair
|
||||
const ecdhPair = await crypto.subtle.generateKey(
|
||||
{ name: "ECDH", namedCurve: "P-384" },
|
||||
true,
|
||||
["deriveKey", "deriveBits"]
|
||||
);
|
||||
|
||||
// Export ephemeral public key as raw bytes
|
||||
ephemeralRaw = new Uint8Array(await crypto.subtle.exportKey('raw', ecdhPair.publicKey));
|
||||
|
||||
// Derive shared secret
|
||||
const sharedBits = await crypto.subtle.deriveBits(
|
||||
{ name: "ECDH", public: recipientEcdhPubKey },
|
||||
ecdhPair.privateKey,
|
||||
384
|
||||
);
|
||||
|
||||
// HKDF-SHA384: derive AES-256-GCM key
|
||||
const hkdfKey = await crypto.subtle.importKey('raw', sharedBits, 'HKDF', false, ['deriveKey']);
|
||||
const cek = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'HKDF',
|
||||
hash: 'SHA-384',
|
||||
salt: new Uint8Array(0),
|
||||
info: new TextEncoder().encode('SecureBit QR ECDH AES key')
|
||||
},
|
||||
hkdfKey,
|
||||
{ name: 'AES-GCM', length: 256 },
|
||||
true,
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
|
||||
// AES-GCM encrypt payload
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
const enc = await crypto.subtle.encrypt(
|
||||
{ name: 'AES-GCM', iv },
|
||||
cek,
|
||||
new TextEncoder().encode(payloadJson)
|
||||
);
|
||||
|
||||
// Build COSE_Encrypt-like structure
|
||||
ciphertextCose = {
|
||||
protected: { alg: 'A256GCM' },
|
||||
unprotected: { epk: ephemeralRaw },
|
||||
ciphertext: new Uint8Array(enc),
|
||||
iv: iv
|
||||
};
|
||||
} else {
|
||||
console.log('🔐 Using broadcast mode (no encryption)...');
|
||||
// Broadcast mode: not encrypted, include ephemeral key for future use
|
||||
ephemeralRaw = crypto.getRandomValues(new Uint8Array(97)); // P-384 uncompressed point size
|
||||
ciphertextCose = {
|
||||
plaintext: new TextEncoder().encode(payloadJson),
|
||||
epk: ephemeralRaw
|
||||
};
|
||||
}
|
||||
|
||||
// 3. Wrap in COSE_Sign1 structure (sign if key provided)
|
||||
let coseSign1;
|
||||
const toSign = cbor.encode(ciphertextCose);
|
||||
|
||||
if (senderEcdsaPrivKey) {
|
||||
console.log('🔐 Signing payload...');
|
||||
// Sign using ECDSA P-384 SHA-384
|
||||
const signature = new Uint8Array(await crypto.subtle.sign(
|
||||
{ name: 'ECDSA', hash: 'SHA-384' },
|
||||
senderEcdsaPrivKey,
|
||||
toSign
|
||||
));
|
||||
|
||||
// COSE_Sign1 as array: [protected, unprotected, payload, signature]
|
||||
const protectedHeader = cbor.encode({ alg: 'ES384' });
|
||||
const unprotectedHeader = { kid: 'securebit-sender' };
|
||||
coseSign1 = [protectedHeader, unprotectedHeader, toSign, signature];
|
||||
} else {
|
||||
console.log('🔐 No signing key provided, using unsigned structure...');
|
||||
// COSE_Sign1 as array: [protected, unprotected, payload, signature]
|
||||
const protectedHeader = cbor.encode({ alg: 'none' });
|
||||
const unprotectedHeader = {};
|
||||
coseSign1 = [protectedHeader, unprotectedHeader, toSign, new Uint8Array(0)];
|
||||
}
|
||||
|
||||
// 4. Final encode: CBOR -> deflate -> base64url
|
||||
const cborFinal = cbor.encode(coseSign1);
|
||||
const compressed = pako.deflate(cborFinal);
|
||||
const encoded = toBase64Url(compressed);
|
||||
|
||||
console.log(`📊 Compressed size: ${encoded.length} characters (${Math.round((1 - encoded.length/payloadJson.length) * 100)}% reduction)`);
|
||||
|
||||
// 5. Chunking for QR codes - улучшенное разбиение для лучшего сканирования
|
||||
const TARGET_CHUNKS = 10; // Целевое количество частей
|
||||
const QR_MAX = Math.max(200, Math.floor(encoded.length / TARGET_CHUNKS)); // Динамический размер части
|
||||
const chunks = [];
|
||||
|
||||
if (encoded.length <= QR_MAX) {
|
||||
// Single chunk
|
||||
chunks.push(JSON.stringify({
|
||||
hdr: { v: 1, id: generateUUID(), seq: 1, total: 1 },
|
||||
body: encoded
|
||||
}));
|
||||
} else {
|
||||
// Multiple chunks - разбиваем на больше частей для лучшего сканирования
|
||||
const id = generateUUID();
|
||||
const totalChunks = Math.ceil(encoded.length / QR_MAX);
|
||||
|
||||
console.log(`📊 COSE: Splitting ${encoded.length} chars into ${totalChunks} chunks (max ${QR_MAX} chars per chunk)`);
|
||||
|
||||
for (let i = 0, seq = 1; i < encoded.length; i += QR_MAX, seq++) {
|
||||
const part = encoded.slice(i, i + QR_MAX);
|
||||
chunks.push(JSON.stringify({
|
||||
hdr: { v: 1, id, seq, total: totalChunks },
|
||||
body: part
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`📊 Generated ${chunks.length} QR chunk(s)`);
|
||||
return chunks;
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error in packSecurePayload:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive and process COSE-packed QR data
|
||||
* @param {Array<string>} qrStrings - Array of QR code strings
|
||||
* @param {CryptoKey} recipientEcdhPrivKey - Recipient's ECDH private key (optional)
|
||||
* @param {CryptoKey} trustedSenderPubKey - Trusted sender's public key (optional)
|
||||
* @returns {Array<Object>} Array of processed payloads
|
||||
*/
|
||||
export async function receiveAndProcess(qrStrings, recipientEcdhPrivKey = null, trustedSenderPubKey = null) {
|
||||
try {
|
||||
console.log('🔓 Starting COSE processing...');
|
||||
|
||||
// 1. Assemble chunks by ID
|
||||
console.log(`📊 Processing ${qrStrings.length} QR string(s)`);
|
||||
const assembled = await assembleFromQrStrings(qrStrings);
|
||||
if (!assembled.length) {
|
||||
console.error('❌ No complete packets found after assembly');
|
||||
throw new Error('No complete packets found');
|
||||
}
|
||||
|
||||
console.log(`📊 Assembled ${assembled.length} complete packet(s)`);
|
||||
console.log('📊 First assembled packet:', assembled[0]);
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const pack of assembled) {
|
||||
try {
|
||||
const encoded = pack.jsonObj;
|
||||
|
||||
// 2. Decode: base64url -> decompress -> CBOR decode
|
||||
const compressed = fromBase64Url(encoded.body || encoded);
|
||||
const cborBytes = pako.inflate(compressed);
|
||||
console.log('🔓 Decompressed CBOR bytes length:', cborBytes.length);
|
||||
console.log('🔓 CBOR bytes type:', typeof cborBytes, cborBytes.constructor.name);
|
||||
|
||||
// Convert Uint8Array to ArrayBuffer for cbor-js
|
||||
const cborArrayBuffer = cborBytes.buffer.slice(cborBytes.byteOffset, cborBytes.byteOffset + cborBytes.byteLength);
|
||||
console.log('🔓 Converted to ArrayBuffer, length:', cborArrayBuffer.byteLength);
|
||||
|
||||
const coseSign1 = cbor.decode(cborArrayBuffer);
|
||||
|
||||
console.log('🔓 Decoded COSE structure');
|
||||
|
||||
// Handle both array and object formats
|
||||
let protectedHeader, unprotectedHeader, payload, signature;
|
||||
if (Array.isArray(coseSign1)) {
|
||||
// Array format: [protected, unprotected, payload, signature]
|
||||
[protectedHeader, unprotectedHeader, payload, signature] = coseSign1;
|
||||
console.log('🔓 COSE structure is array format');
|
||||
} else {
|
||||
// Object format (legacy)
|
||||
protectedHeader = coseSign1.protected;
|
||||
unprotectedHeader = coseSign1.unprotected;
|
||||
payload = coseSign1.payload;
|
||||
signature = coseSign1.signature;
|
||||
console.log('🔓 COSE structure is object format (legacy)');
|
||||
}
|
||||
|
||||
// 3. Verify signature (if key provided)
|
||||
if (trustedSenderPubKey && signature && signature.length > 0) {
|
||||
const toVerify = cbor.encode([protectedHeader, unprotectedHeader, payload]);
|
||||
const isValid = await crypto.subtle.verify(
|
||||
{ name: 'ECDSA', hash: 'SHA-384' },
|
||||
trustedSenderPubKey,
|
||||
signature,
|
||||
toVerify
|
||||
);
|
||||
|
||||
if (!isValid) {
|
||||
console.warn('⚠️ Signature verification failed');
|
||||
continue;
|
||||
}
|
||||
console.log('✅ Signature verified');
|
||||
}
|
||||
|
||||
// 4. Decrypt payload
|
||||
let inner;
|
||||
if (payload instanceof Uint8Array) {
|
||||
// Payload is still encoded
|
||||
const innerArrayBuffer = payload.buffer.slice(payload.byteOffset, payload.byteOffset + payload.byteLength);
|
||||
inner = cbor.decode(innerArrayBuffer);
|
||||
} else {
|
||||
// Payload is already decoded
|
||||
inner = payload;
|
||||
}
|
||||
console.log('🔓 Inner payload type:', typeof inner, inner.constructor.name);
|
||||
console.log('🔓 Inner payload keys:', Object.keys(inner));
|
||||
console.log('🔓 Inner payload full object:', inner);
|
||||
|
||||
let payloadObj;
|
||||
|
||||
if (inner.ciphertext && recipientEcdhPrivKey) {
|
||||
console.log('🔓 Decrypting encrypted payload...');
|
||||
|
||||
// Get ephemeral public key
|
||||
const epkRaw = inner.unprotected?.epk || inner.epk;
|
||||
|
||||
// Import ephemeral public key
|
||||
const ephemeralPub = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
epkRaw,
|
||||
{ name: 'ECDH', namedCurve: 'P-384' },
|
||||
true,
|
||||
[]
|
||||
);
|
||||
|
||||
// Derive shared secret
|
||||
const sharedBits = await crypto.subtle.deriveBits(
|
||||
{ name: 'ECDH', public: ephemeralPub },
|
||||
recipientEcdhPrivKey,
|
||||
384
|
||||
);
|
||||
|
||||
// HKDF-SHA384 -> AES key
|
||||
const hkdfKey = await crypto.subtle.importKey('raw', sharedBits, 'HKDF', false, ['deriveKey']);
|
||||
const cek = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'HKDF',
|
||||
hash: 'SHA-384',
|
||||
salt: new Uint8Array(0),
|
||||
info: new TextEncoder().encode('SecureBit QR ECDH AES key')
|
||||
},
|
||||
hkdfKey,
|
||||
{ name: 'AES-GCM', length: 256 },
|
||||
true,
|
||||
['decrypt']
|
||||
);
|
||||
|
||||
// Decrypt
|
||||
const plaintext = await crypto.subtle.decrypt(
|
||||
{ name: 'AES-GCM', iv: inner.iv },
|
||||
cek,
|
||||
inner.ciphertext
|
||||
);
|
||||
|
||||
const payloadJson = new TextDecoder().decode(plaintext);
|
||||
payloadObj = JSON.parse(payloadJson);
|
||||
|
||||
} else if (inner.plaintext) {
|
||||
console.log('🔓 Processing plaintext payload...');
|
||||
// Broadcast mode
|
||||
payloadObj = JSON.parse(new TextDecoder().decode(inner.plaintext));
|
||||
} else if (Object.keys(inner).length === 0) {
|
||||
console.log('🔓 Empty inner payload, using alternative approach...');
|
||||
|
||||
// Alternative: try to use the original assembled body
|
||||
try {
|
||||
const originalBody = encoded.body || encoded;
|
||||
console.log('🔓 Trying to decode original body:', originalBody.substring(0, 50) + '...');
|
||||
|
||||
// Decode base64url -> decompress -> CBOR decode -> extract JSON
|
||||
const compressed = fromBase64Url(originalBody);
|
||||
const decompressed = pako.inflate(compressed);
|
||||
console.log('🔓 Decompressed length:', decompressed.length);
|
||||
|
||||
// Convert to ArrayBuffer for CBOR decoding
|
||||
const decompressedArrayBuffer = decompressed.buffer.slice(decompressed.byteOffset, decompressed.byteOffset + decompressed.byteLength);
|
||||
const cborDecoded = cbor.decode(decompressedArrayBuffer);
|
||||
console.log('🔓 CBOR decoded structure:', cborDecoded);
|
||||
|
||||
// Handle both array and object formats
|
||||
let payload;
|
||||
if (Array.isArray(cborDecoded)) {
|
||||
// Array format: [protected, unprotected, payload, signature]
|
||||
console.log('🔓 Alternative: COSE structure is array format');
|
||||
console.log('🔓 Array length:', cborDecoded.length);
|
||||
console.log('🔓 Array elements:', cborDecoded.map((el, i) => `${i}: ${typeof el} ${el.constructor.name}`));
|
||||
|
||||
// Payload is at index 2
|
||||
payload = cborDecoded[2];
|
||||
console.log('🔓 Payload at index 2:', payload);
|
||||
} else {
|
||||
// Object format (legacy)
|
||||
payload = cborDecoded.payload;
|
||||
console.log('🔓 Alternative: COSE structure is object format (legacy)');
|
||||
}
|
||||
|
||||
// Extract the actual payload from CBOR structure
|
||||
if (payload && payload instanceof Uint8Array) {
|
||||
const payloadArrayBuffer = payload.buffer.slice(payload.byteOffset, payload.byteOffset + payload.byteLength);
|
||||
const innerCbor = cbor.decode(payloadArrayBuffer);
|
||||
console.log('🔓 Inner CBOR structure:', innerCbor);
|
||||
|
||||
if (innerCbor.plaintext) {
|
||||
const jsonString = new TextDecoder().decode(innerCbor.plaintext);
|
||||
payloadObj = JSON.parse(jsonString);
|
||||
console.log('🔓 Successfully decoded via alternative approach');
|
||||
console.log('🔓 Alternative payloadObj:', payloadObj);
|
||||
} else {
|
||||
console.error('❌ No plaintext found in inner CBOR structure');
|
||||
continue;
|
||||
}
|
||||
} else if (payload && typeof payload === 'object' && Object.keys(payload).length > 0) {
|
||||
// Payload is already a decoded object
|
||||
console.log('🔓 Payload is already decoded object:', payload);
|
||||
if (payload.plaintext) {
|
||||
const jsonString = new TextDecoder().decode(payload.plaintext);
|
||||
payloadObj = JSON.parse(jsonString);
|
||||
console.log('🔓 Successfully decoded from payload object');
|
||||
console.log('🔓 Alternative payloadObj:', payloadObj);
|
||||
} else {
|
||||
console.error('❌ No plaintext found in payload object');
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
console.error('❌ No payload found in CBOR structure');
|
||||
console.log('🔓 CBOR structure keys:', Object.keys(cborDecoded));
|
||||
console.log('🔓 Payload type:', typeof payload);
|
||||
console.log('🔓 Payload value:', payload);
|
||||
continue;
|
||||
}
|
||||
} catch (altError) {
|
||||
console.error('❌ Alternative approach failed:', altError);
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
console.warn('⚠️ Unknown payload format:', inner);
|
||||
continue;
|
||||
}
|
||||
|
||||
results.push({
|
||||
payloadObj,
|
||||
senderVerified: !!trustedSenderPubKey,
|
||||
encrypted: !!inner.ciphertext
|
||||
});
|
||||
|
||||
} catch (packError) {
|
||||
console.error('❌ Error processing packet:', packError);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ Successfully processed ${results.length} payload(s)`);
|
||||
return results;
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error in receiveAndProcess:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assemble QR chunks into complete packets
|
||||
* @param {Array<string>} qrStrings - Array of QR code strings
|
||||
* @returns {Array<Object>} Array of assembled packets
|
||||
*/
|
||||
async function assembleFromQrStrings(qrStrings) {
|
||||
const packets = new Map();
|
||||
|
||||
console.log('🔧 Starting assembly of QR strings...');
|
||||
|
||||
for (const qrString of qrStrings) {
|
||||
try {
|
||||
console.log('🔧 Parsing QR string:', qrString.substring(0, 100) + '...');
|
||||
const parsed = JSON.parse(qrString);
|
||||
console.log('🔧 Parsed structure:', parsed);
|
||||
|
||||
if (parsed.hdr && parsed.body) {
|
||||
const id = parsed.hdr.id;
|
||||
console.log(`🔧 Processing packet ID: ${id}, seq: ${parsed.hdr.seq}, total: ${parsed.hdr.total}`);
|
||||
|
||||
if (!packets.has(id)) {
|
||||
packets.set(id, {
|
||||
id: id,
|
||||
chunks: new Map(),
|
||||
total: parsed.hdr.total
|
||||
});
|
||||
console.log(`🔧 Created new packet for ID: ${id}`);
|
||||
}
|
||||
|
||||
const packet = packets.get(id);
|
||||
packet.chunks.set(parsed.hdr.seq, parsed.body);
|
||||
console.log(`🔧 Added chunk ${parsed.hdr.seq} to packet ${id}. Current chunks: ${packet.chunks.size}/${packet.total}`);
|
||||
|
||||
// Check if complete
|
||||
if (packet.chunks.size === packet.total) {
|
||||
console.log(`🔧 Packet ${id} is complete! Assembling body...`);
|
||||
// Assemble body
|
||||
let assembledBody = '';
|
||||
for (let i = 1; i <= packet.total; i++) {
|
||||
assembledBody += packet.chunks.get(i);
|
||||
}
|
||||
|
||||
packet.jsonObj = { body: assembledBody };
|
||||
packet.complete = true;
|
||||
console.log(`🔧 Assembled body length: ${assembledBody.length} characters`);
|
||||
}
|
||||
} else {
|
||||
console.warn('⚠️ QR string missing hdr or body:', parsed);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Failed to parse QR string:', error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Return only complete packets
|
||||
const completePackets = Array.from(packets.values()).filter(p => p.complete);
|
||||
console.log(`🔧 Assembly complete. Found ${completePackets.length} complete packets`);
|
||||
return completePackets;
|
||||
}
|
||||
|
||||
// Export for global use
|
||||
window.packSecurePayload = packSecurePayload;
|
||||
window.receiveAndProcess = receiveAndProcess;
|
||||
13408
src/network/EnhancedSecureWebRTCManager.js
Normal file
13408
src/network/EnhancedSecureWebRTCManager.js
Normal file
File diff suppressed because it is too large
Load Diff
34
src/scripts/app-boot.js
Normal file
34
src/scripts/app-boot.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import { EnhancedSecureCryptoUtils } from '../crypto/EnhancedSecureCryptoUtils.js';
|
||||
import { EnhancedSecureWebRTCManager } from '../network/EnhancedSecureWebRTCManager.js';
|
||||
import { EnhancedSecureFileTransfer } from '../transfer/EnhancedSecureFileTransfer.js';
|
||||
|
||||
// Import UI components (side-effect: they attach themselves to window.*)
|
||||
import '../components/ui/SessionTimer.jsx';
|
||||
import '../components/ui/Header.jsx';
|
||||
import '../components/ui/DownloadApps.jsx';
|
||||
import '../components/ui/UniqueFeatureSlider.jsx';
|
||||
import '../components/ui/SecurityFeatures.jsx';
|
||||
import '../components/ui/Testimonials.jsx';
|
||||
import '../components/ui/ComparisonTable.jsx';
|
||||
import '../components/ui/Roadmap.jsx';
|
||||
import '../components/ui/FileTransfer.jsx';
|
||||
|
||||
// Expose to global for legacy usage inside app code
|
||||
window.EnhancedSecureCryptoUtils = EnhancedSecureCryptoUtils;
|
||||
window.EnhancedSecureWebRTCManager = EnhancedSecureWebRTCManager;
|
||||
window.EnhancedSecureFileTransfer = EnhancedSecureFileTransfer;
|
||||
|
||||
// Mount application once DOM and modules are ready
|
||||
const start = () => {
|
||||
if (typeof window.initializeApp === 'function') {
|
||||
window.initializeApp();
|
||||
} else if (window.DEBUG_MODE) {
|
||||
console.error('initializeApp is not defined on window');
|
||||
}
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', start);
|
||||
} else {
|
||||
start();
|
||||
}
|
||||
55
src/scripts/bootstrap-modules.js
vendored
Normal file
55
src/scripts/bootstrap-modules.js
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
// Temporary bootstrap that still uses eval for JSX components fetched as text.
|
||||
// Next step is to replace this with proper ESM imports of prebuilt JS.
|
||||
(async () => {
|
||||
try {
|
||||
const timestamp = Date.now();
|
||||
const [cryptoModule, webrtcModule, paymentModule, fileTransferModule] = await Promise.all([
|
||||
import(`../crypto/EnhancedSecureCryptoUtils.js?v=${timestamp}`),
|
||||
import(`../network/EnhancedSecureWebRTCManager.js?v=${timestamp}`),
|
||||
import(`../session/PayPerSessionManager.js?v=${timestamp}`),
|
||||
import(`../transfer/EnhancedSecureFileTransfer.js?v=${timestamp}`),
|
||||
]);
|
||||
|
||||
const { EnhancedSecureCryptoUtils } = cryptoModule;
|
||||
window.EnhancedSecureCryptoUtils = EnhancedSecureCryptoUtils;
|
||||
const { EnhancedSecureWebRTCManager } = webrtcModule;
|
||||
window.EnhancedSecureWebRTCManager = EnhancedSecureWebRTCManager;
|
||||
const { PayPerSessionManager } = paymentModule;
|
||||
window.PayPerSessionManager = PayPerSessionManager;
|
||||
const { EnhancedSecureFileTransfer } = fileTransferModule;
|
||||
window.EnhancedSecureFileTransfer = EnhancedSecureFileTransfer;
|
||||
|
||||
async function loadReactComponent(path) {
|
||||
const response = await fetch(`${path}?v=${timestamp}`);
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
const code = await response.text();
|
||||
// eslint-disable-next-line no-eval
|
||||
eval(code);
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
loadReactComponent('../components/ui/SessionTimer.jsx'),
|
||||
loadReactComponent('../components/ui/Header.jsx'),
|
||||
loadReactComponent('../components/ui/SessionTypeSelector.jsx'),
|
||||
loadReactComponent('../components/ui/LightningPayment.jsx'),
|
||||
loadReactComponent('../components/ui/PaymentModal.jsx'),
|
||||
loadReactComponent('../components/ui/DownloadApps.jsx'),
|
||||
loadReactComponent('../components/ui/ComparisonTable.jsx'),
|
||||
loadReactComponent('../components/ui/UniqueFeatureSlider.jsx'),
|
||||
loadReactComponent('../components/ui/SecurityFeatures.jsx'),
|
||||
loadReactComponent('../components/ui/Testimonials.jsx'),
|
||||
loadReactComponent('../components/ui/Roadmap.jsx'),
|
||||
loadReactComponent('../components/ui/FileTransfer.jsx'),
|
||||
]);
|
||||
|
||||
if (typeof window.initializeApp === 'function') {
|
||||
window.initializeApp();
|
||||
} else {
|
||||
console.error('❌ Function initializeApp not found');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Module loading error:', error);
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
44
src/scripts/fa-check.js
Normal file
44
src/scripts/fa-check.js
Normal file
@@ -0,0 +1,44 @@
|
||||
// Global logging and function settings
|
||||
window.DEBUG_MODE = true;
|
||||
|
||||
// Fake function settings (for stability)
|
||||
window.DISABLE_FAKE_TRAFFIC = false; // Set true to disable fake messages
|
||||
window.DISABLE_DECOY_CHANNELS = false; // Set true to disable decoy channels
|
||||
|
||||
// Enhanced icon loading fallback
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Check if Font Awesome loaded properly
|
||||
function checkFontAwesome() {
|
||||
const testIcon = document.createElement('i');
|
||||
testIcon.className = 'fas fa-shield-halved';
|
||||
testIcon.style.position = 'absolute';
|
||||
testIcon.style.left = '-9999px';
|
||||
testIcon.style.visibility = 'hidden';
|
||||
document.body.appendChild(testIcon);
|
||||
|
||||
const computedStyle = window.getComputedStyle(testIcon, '::before');
|
||||
const content = computedStyle.content;
|
||||
const fontFamily = computedStyle.fontFamily;
|
||||
|
||||
document.body.removeChild(testIcon);
|
||||
|
||||
if (!content || content === 'none' || content === 'normal' || (!fontFamily.includes('Font Awesome') && !fontFamily.includes('fa-solid'))) {
|
||||
console.warn('Font Awesome not loaded properly, using fallback icons');
|
||||
document.body.classList.add('fa-fallback');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!checkFontAwesome()) {
|
||||
setTimeout(function () {
|
||||
if (!checkFontAwesome()) {
|
||||
console.warn('Font Awesome still not loaded, using fallback icons');
|
||||
document.body.classList.add('fa-fallback');
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
193
src/scripts/qr-local.js
Normal file
193
src/scripts/qr-local.js
Normal file
@@ -0,0 +1,193 @@
|
||||
// Local QR generator and scanner with COSE compression (no external CDNs)
|
||||
// Exposes:
|
||||
// - window.generateQRCode(text, { size?: number, margin?: number, errorCorrectionLevel?: 'L'|'M'|'Q'|'H' })
|
||||
// - window.generateCOSEQRCode(data, senderKey?, recipientKey?) - COSE-based compression
|
||||
// - window.Html5Qrcode (for scanning QR codes)
|
||||
// - window.packSecurePayload, window.receiveAndProcess (COSE functions)
|
||||
|
||||
import * as QRCode from 'qrcode';
|
||||
import { Html5Qrcode } from 'html5-qrcode';
|
||||
import { gzip, ungzip, deflate, inflate } from 'pako';
|
||||
import * as cbor from 'cbor-js';
|
||||
import { packSecurePayload, receiveAndProcess } from '../crypto/cose-qr.js';
|
||||
|
||||
// Compact payload prefix to signal gzip+base64 content
|
||||
const COMPRESSION_PREFIX = 'SB1:gz:';
|
||||
const BINARY_PREFIX = 'SB1:bin:'; // CBOR + deflate + base64url
|
||||
|
||||
function uint8ToBase64(bytes) {
|
||||
let binary = '';
|
||||
const chunkSize = 0x8000;
|
||||
for (let i = 0; i < bytes.length; i += chunkSize) {
|
||||
const chunk = bytes.subarray(i, i + chunkSize);
|
||||
binary += String.fromCharCode.apply(null, chunk);
|
||||
}
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
function base64ToUint8(b64) {
|
||||
const binary = atob(b64);
|
||||
const len = binary.length;
|
||||
const bytes = new Uint8Array(len);
|
||||
for (let i = 0; i < len; i++) bytes[i] = binary.charCodeAt(i);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function compressStringToBase64Gzip(text) {
|
||||
const utf8 = new TextEncoder().encode(text);
|
||||
const gz = gzip(utf8);
|
||||
return uint8ToBase64(gz);
|
||||
}
|
||||
|
||||
function decompressBase64GzipToString(b64) {
|
||||
const gz = base64ToUint8(b64);
|
||||
const out = ungzip(gz);
|
||||
return new TextDecoder().decode(out);
|
||||
}
|
||||
|
||||
async function generateQRCode(text, opts = {}) {
|
||||
const size = opts.size || 512;
|
||||
const margin = opts.margin ?? 2;
|
||||
const errorCorrectionLevel = opts.errorCorrectionLevel || 'M';
|
||||
return await QRCode.toDataURL(text, { width: size, margin, errorCorrectionLevel });
|
||||
}
|
||||
|
||||
// Generate QR with gzip+base64 payload and recognizable prefix for scanners
|
||||
async function generateCompressedQRCode(text, opts = {}) {
|
||||
try {
|
||||
const compressedB64 = compressStringToBase64Gzip(text);
|
||||
const payload = COMPRESSION_PREFIX + compressedB64;
|
||||
return await generateQRCode(payload, opts);
|
||||
} catch (e) {
|
||||
console.warn('generateCompressedQRCode failed, falling back to plain:', e?.message || e);
|
||||
return await generateQRCode(text, opts);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Binary (CBOR) encode/decode helpers ----
|
||||
function base64ToBase64Url(b64) {
|
||||
return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
||||
}
|
||||
function base64UrlToBase64(b64url) {
|
||||
let b64 = b64url.replace(/-/g, '+').replace(/_/g, '/');
|
||||
const pad = b64.length % 4;
|
||||
if (pad) b64 += '='.repeat(4 - pad);
|
||||
return b64;
|
||||
}
|
||||
|
||||
function encodeObjectToBinaryBase64Url(obj) {
|
||||
const cborBytes = cbor.encode(obj);
|
||||
const compressed = deflate(new Uint8Array(cborBytes));
|
||||
const b64 = uint8ToBase64(compressed);
|
||||
return base64ToBase64Url(b64);
|
||||
}
|
||||
|
||||
function decodeBinaryBase64UrlToObject(b64url) {
|
||||
const b64 = base64UrlToBase64(b64url);
|
||||
const compressed = base64ToUint8(b64);
|
||||
const decompressed = inflate(compressed);
|
||||
const ab = decompressed.buffer.slice(decompressed.byteOffset, decompressed.byteOffset + decompressed.byteLength);
|
||||
return cbor.decode(ab);
|
||||
}
|
||||
|
||||
async function generateBinaryQRCodeFromObject(obj, opts = {}) {
|
||||
try {
|
||||
const b64url = encodeObjectToBinaryBase64Url(obj);
|
||||
const payload = BINARY_PREFIX + b64url;
|
||||
return await generateQRCode(payload, opts);
|
||||
} catch (e) {
|
||||
console.warn('generateBinaryQRCodeFromObject failed, falling back to JSON compressed:', e?.message || e);
|
||||
const text = JSON.stringify(obj);
|
||||
return await generateCompressedQRCode(text, opts);
|
||||
}
|
||||
}
|
||||
|
||||
// COSE-based QR generation for large data
|
||||
async function generateCOSEQRCode(data, senderKey = null, recipientKey = null) {
|
||||
try {
|
||||
console.log('🔐 Generating COSE-based QR code...');
|
||||
|
||||
// Pack data using COSE
|
||||
const chunks = await packSecurePayload(data, senderKey, recipientKey);
|
||||
|
||||
if (chunks.length === 1) {
|
||||
// Single QR code
|
||||
return await generateQRCode(chunks[0]);
|
||||
} else {
|
||||
// Enforce single-QR policy: let caller fallback to template/reference
|
||||
console.warn(`📊 COSE packing produced ${chunks.length} chunks; falling back to non-COSE strategy`);
|
||||
throw new Error('COSE QR would require multiple chunks');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating COSE QR code:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Expose functions to global scope
|
||||
window.generateQRCode = generateQRCode;
|
||||
window.generateCompressedQRCode = generateCompressedQRCode;
|
||||
window.generateBinaryQRCodeFromObject = generateBinaryQRCodeFromObject;
|
||||
window.generateCOSEQRCode = generateCOSEQRCode;
|
||||
window.Html5Qrcode = Html5Qrcode;
|
||||
window.packSecurePayload = packSecurePayload;
|
||||
window.receiveAndProcess = receiveAndProcess;
|
||||
|
||||
// Expose helper to transparently decompress scanner payloads
|
||||
window.decompressIfNeeded = function (scannedText) {
|
||||
try {
|
||||
if (typeof scannedText === 'string' && scannedText.startsWith(COMPRESSION_PREFIX)) {
|
||||
const b64 = scannedText.slice(COMPRESSION_PREFIX.length);
|
||||
return decompressBase64GzipToString(b64);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('decompressIfNeeded failed:', e?.message || e);
|
||||
}
|
||||
return scannedText;
|
||||
};
|
||||
|
||||
// Expose helper to get compressed string with prefix for copy/paste flows
|
||||
window.compressToPrefixedGzip = function (text) {
|
||||
try {
|
||||
const payload = String(text || '');
|
||||
const compressedB64 = compressStringToBase64Gzip(payload);
|
||||
return COMPRESSION_PREFIX + compressedB64;
|
||||
} catch (e) {
|
||||
console.warn('compressToPrefixedGzip failed:', e?.message || e);
|
||||
return String(text || '');
|
||||
}
|
||||
};
|
||||
|
||||
// Expose helpers for binary payloads in copy/paste
|
||||
window.encodeBinaryToPrefixed = function (objOrJson) {
|
||||
try {
|
||||
const obj = typeof objOrJson === 'string' ? JSON.parse(objOrJson) : objOrJson;
|
||||
const b64url = encodeObjectToBinaryBase64Url(obj);
|
||||
return BINARY_PREFIX + b64url;
|
||||
} catch (e) {
|
||||
console.warn('encodeBinaryToPrefixed failed:', e?.message || e);
|
||||
return typeof objOrJson === 'string' ? objOrJson : JSON.stringify(objOrJson);
|
||||
}
|
||||
};
|
||||
|
||||
window.decodeAnyPayload = function (scannedText) {
|
||||
try {
|
||||
if (typeof scannedText === 'string') {
|
||||
if (scannedText.startsWith(BINARY_PREFIX)) {
|
||||
const b64url = scannedText.slice(BINARY_PREFIX.length);
|
||||
return decodeBinaryBase64UrlToObject(b64url); // returns object
|
||||
}
|
||||
if (scannedText.startsWith(COMPRESSION_PREFIX)) {
|
||||
const s = window.decompressIfNeeded(scannedText);
|
||||
return s; // returns JSON string
|
||||
}
|
||||
// Not prefixed: return as-is
|
||||
return scannedText;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('decodeAnyPayload failed:', e?.message || e);
|
||||
}
|
||||
return scannedText;
|
||||
};
|
||||
|
||||
console.log('QR libraries loaded: generateQRCode, generateCompressedQRCode, generateBinaryQRCodeFromObject, Html5Qrcode, COSE functions');
|
||||
29
src/styles/animations.css
Normal file
29
src/styles/animations.css
Normal file
@@ -0,0 +1,29 @@
|
||||
/* Smooth Message Scrolling/Appearance*/
|
||||
@keyframes messageSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Icon pulsation */
|
||||
@keyframes iconPulse {
|
||||
0%, 100% { opacity: 0.7; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
/* Pulse for the timer */
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
}
|
||||
|
||||
/* Scroll of logos */
|
||||
@keyframes walletLogosScroll {
|
||||
0% { transform: translateX(0); }
|
||||
100% { transform: translateX(-50%); }
|
||||
}
|
||||
575
src/styles/components.css
Normal file
575
src/styles/components.css
Normal file
@@ -0,0 +1,575 @@
|
||||
|
||||
.minimal-bg {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.hide-scrollbar {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
.hide-scrollbar::-webkit-scrollbar {
|
||||
display: none; /* Chrome, Safari, Opera */
|
||||
}
|
||||
|
||||
/* ============================================ */
|
||||
/* FILE TRANSFER STYLES */
|
||||
/* ============================================ */
|
||||
|
||||
.file-transfer-component {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
/* Используем dvh для динамической высоты на мобильных */
|
||||
.mobile-chat-height {
|
||||
height: calc(100dvh - 64px) !important;
|
||||
}
|
||||
|
||||
/* Кнопка прокрутки на мобильном */
|
||||
.scroll-to-bottom-mobile {
|
||||
bottom: 140px !important;
|
||||
right: 16px !important;
|
||||
width: 40px !important;
|
||||
height: 40px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.header-minimal {
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.mobile-chat-height {
|
||||
height: calc(100dvh - 56px) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.file-drop-zone {
|
||||
border: 2px dashed #4b5563;
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
background: rgba(55, 65, 81, 0.1);
|
||||
}
|
||||
|
||||
.file-drop-zone:hover {
|
||||
border-color: #3b82f6;
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.file-drop-zone.drag-over {
|
||||
border-color: #10b981;
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.drop-content {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.active-transfers {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.transfer-item {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.transfer-item:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
position: relative;
|
||||
height: 6px;
|
||||
background: rgba(75, 85, 99, 0.3);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
transition: width 0.3s ease;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
position: absolute;
|
||||
top: -20px;
|
||||
right: 0;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.file-transfer-section {
|
||||
border-top: 1px solid rgba(75, 85, 99, 0.1);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.file-drop-zone {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.transfer-item {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.header-minimal {
|
||||
background: rgb(35 36 35 / 13%);
|
||||
backdrop-filter: blur(5px);
|
||||
-webkit-backdrop-filter: blur(5px);
|
||||
flex-shrink: 0;
|
||||
height: 64px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 40;
|
||||
}
|
||||
|
||||
.header-minimal .cursor-pointer:hover {
|
||||
transform: scale(1.05);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
.header-minimal .cursor-pointer:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* The main chat container takes up the rest of the height. */
|
||||
.chat-container {
|
||||
/* display: flex; */
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 64px); /* 64px - header height */
|
||||
min-height: 0;
|
||||
/* flex: 1; */
|
||||
}
|
||||
|
||||
.chat-messages-area {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chat-input-area {
|
||||
flex-shrink: 0;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
background: rgba(42, 43, 42, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-top: 1px solid rgba(75, 85, 99, 0.2);
|
||||
}
|
||||
|
||||
/* The message container must occupy the entire available height. */
|
||||
.chat-messages-area > div:first-child {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
scroll-behavior: smooth;
|
||||
scroll-padding-bottom: 20px;
|
||||
scroll-margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* For mobile devices, take into account the height of the virtual keyboard */
|
||||
@media (max-width: 768px) {
|
||||
.chat-container {
|
||||
height: calc(100vh - 64px);
|
||||
height: calc(100dvh - 64px); /* dvh to support dynamic height on mobile */
|
||||
}
|
||||
}
|
||||
|
||||
/* Fix for main application container */
|
||||
main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.card-minimal {
|
||||
background: rgba(42, 43, 42, 0.8);
|
||||
backdrop-filter: blur(16px);
|
||||
border: 1px solid rgba(75, 85, 99, 0.2);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
/* Selected state override (higher precedence than Tailwind bg-*) */
|
||||
.card-minimal--selected {
|
||||
background: rgba(249, 115, 22, 0.15) !important; /* orange-500 @ 0.15 */
|
||||
border-color: rgba(249, 115, 22, 1) !important; /* border-orange-500 */
|
||||
box-shadow: 0 0 0 2px rgba(249, 115, 22, 0.3) !important; /* soft ring */
|
||||
}
|
||||
/* .card-minimal:hover {
|
||||
border-color: rgba(249, 115, 22, 0.3);
|
||||
transform: translateY(-1px);
|
||||
transition: all 0.2s ease;
|
||||
} */
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.status-connected { background: #10b981; }
|
||||
.status-connecting { background: #6b7280; }
|
||||
.status-failed { background: #ef4444; }
|
||||
.status-disconnected { background: #6b7280; }
|
||||
.status-verifying { background: #9ca3af; }
|
||||
|
||||
.security-shield {
|
||||
background: linear-gradient(135deg, #2A2B2A 0%, #3a3b3a 100%);
|
||||
border: 1px solid rgba(75, 85, 99, 0.3);
|
||||
}
|
||||
|
||||
.verification-code {
|
||||
background: rgba(42, 43, 42, 0.8);
|
||||
border: 1px solid rgba(75, 85, 99, 0.3);
|
||||
color: #f1f5f9;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
letter-spacing: 0.1em;
|
||||
font-size: 1.2em;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: rgba(42, 43, 42, 0.8);
|
||||
border: 1px solid rgba(75, 85, 99, 0.3);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.icon-container i {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.icon-sm { font-size: 0.875rem; }
|
||||
.icon-md { font-size: 1rem; }
|
||||
.icon-lg { font-size: 1.125rem; }
|
||||
.icon-xl { font-size: 1.25rem; }
|
||||
.icon-2xl { font-size: 1.5rem; }
|
||||
|
||||
.step-number {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: linear-gradient(135deg, #f97316, #ea580c);
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.icon-fallback {
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.fas, .far, .fab {
|
||||
display: inline-block;
|
||||
font-style: normal;
|
||||
font-variant: normal;
|
||||
text-rendering: auto;
|
||||
line-height: 1;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Improve icon rendering */
|
||||
.fas::before, .far::before, .fab::before {
|
||||
display: inline-block;
|
||||
font-style: normal;
|
||||
font-variant: normal;
|
||||
text-rendering: auto;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Icon loading fallback */
|
||||
.icon-loading {
|
||||
opacity: 0.7;
|
||||
animation: iconPulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Fallback icons content */
|
||||
.fa-fallback .fas.fa-shield-halved::before { content: "🛡️"; }
|
||||
.fa-fallback .fas.fa-shield-alt::before { content: "🛡️"; }
|
||||
.fa-fallback .fas.fa-lock::before { content: "🔒"; }
|
||||
.fa-fallback .fas.fa-unlock-alt::before { content: "🔓"; }
|
||||
.fa-fallback .fas.fa-key::before { content: "🔑"; }
|
||||
.fa-fallback .fas.fa-fingerprint::before { content: "👆"; }
|
||||
.fa-fallback .fas.fa-exchange-alt::before { content: "🔄"; }
|
||||
.fa-fallback .fas.fa-plus::before { content: "➕"; }
|
||||
.fa-fallback .fas.fa-link::before { content: "🔗"; }
|
||||
.fa-fallback .fas.fa-paste::before { content: "📋"; }
|
||||
.fa-fallback .fas.fa-check-circle::before { content: "✅"; }
|
||||
.fa-fallback .fas.fa-cogs::before { content: "⚙️"; }
|
||||
.fa-fallback .fas.fa-rocket::before { content: "🚀"; }
|
||||
.fa-fallback .fas.fa-copy::before { content: "📄"; }
|
||||
.fa-fallback .fas.fa-check::before { content: "✓"; }
|
||||
.fa-fallback .fas.fa-times::before { content: "✗"; }
|
||||
.fa-fallback .fas.fa-exclamation-triangle::before { content: "⚠️"; }
|
||||
.fa-fallback .fas.fa-info-circle::before { content: "ℹ️"; }
|
||||
.fa-fallback .fas.fa-circle::before { content: "●"; }
|
||||
.fa-fallback .fas.fa-paper-plane::before { content: "📤"; }
|
||||
.fa-fallback .fas.fa-comments::before { content: "💬"; }
|
||||
.fa-fallback .fas.fa-signature::before { content: "✍️"; }
|
||||
.fa-fallback .fas.fa-power-off::before { content: "⏻"; }
|
||||
.fa-fallback .fas.fa-arrow-left::before { content: "←"; }
|
||||
.fa-fallback .fas.fa-chevron-down::before { content: "↓"; }
|
||||
|
||||
/* Ensure fallback icons are properly sized & use emoji font */
|
||||
.fa-fallback .fas::before {
|
||||
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
/* Icon alignment in buttons */
|
||||
button i {
|
||||
vertical-align: middle;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
/* Pay-per-session UI - Обновленный трехцветный таймер */
|
||||
.session-timer {
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.session-timer:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Анимация пульсации для красной зоны */
|
||||
@keyframes timer-pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
.session-timer.animate-pulse {
|
||||
animation: timer-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Lightning button */
|
||||
.lightning-button {
|
||||
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
|
||||
border: 1px solid rgba(245, 158, 11, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.lightning-button:hover {
|
||||
background: linear-gradient(135deg, #d97706 0%, #b45309 100%);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
|
||||
border: 1px solid rgba(249, 115, 22, 0.3);
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background: linear-gradient(135deg, #ea580c 0%, #dc2626 100%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: linear-gradient(135deg, #2A2B2A 0%, #3a3b3a 100%);
|
||||
border: 1px solid rgba(75, 85, 99, 0.3);
|
||||
}
|
||||
.btn-secondary:hover {
|
||||
background: linear-gradient(135deg, #3a3b3a 0%, #4a4b4a 100%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-verify {
|
||||
background: linear-gradient(135deg, #2A2B2A 0%, #3a3b3a 100%);
|
||||
border: 1px solid rgba(75, 85, 99, 0.3);
|
||||
}
|
||||
.btn-verify:hover {
|
||||
background: linear-gradient(135deg, #3a3b3a 0%, #4a4b4a 100%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Wallet logos container & per-wallet filters */
|
||||
.wallet-logos-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
height: 64px;
|
||||
margin: 20px 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wallet-logos-track {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
animation: walletLogosScroll 30s linear infinite;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.wallet-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100px;
|
||||
height: 48px;
|
||||
background: rgba(42, 43, 42, 0.8);
|
||||
border: 1px solid rgba(75, 85, 99, 0.3);
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #f1f5f9;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.wallet-logo:hover {
|
||||
border-color: rgba(249, 115, 22, 0.3);
|
||||
background: rgba(249, 115, 22, 0.1);
|
||||
transform: translateY(-2px) scale(1.05);
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.wallet-logo.bitcoin-lightning { background: transparent; padding: 4px; }
|
||||
.wallet-logo.bitcoin-lightning img { width: 48px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); }
|
||||
|
||||
.wallet-logo.impervious { background: transparent; padding: 4px; }
|
||||
.wallet-logo.impervious img { width: 48px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); }
|
||||
|
||||
.wallet-logo.strike { background: transparent; padding: 4px; }
|
||||
.wallet-logo.strike img { width: 48px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); }
|
||||
|
||||
.wallet-logo.lnbits { background: transparent; padding: 4px; }
|
||||
.wallet-logo.lnbits img { width: 48px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); }
|
||||
|
||||
.wallet-logo.lightning-labs { background: transparent; padding: 4px; }
|
||||
.wallet-logo.lightning-labs img { width: 48px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); }
|
||||
|
||||
.wallet-logo.atomic { background: transparent; padding: 4px; }
|
||||
.wallet-logo.atomic img { width: 48px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); }
|
||||
|
||||
.wallet-logo.breez { background: transparent; padding: 4px; }
|
||||
.wallet-logo.breez img { width: 48px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); }
|
||||
|
||||
.wallet-logo.alby { background: transparent; padding: 4px; }
|
||||
.wallet-logo.alby img { width: 48px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); }
|
||||
|
||||
.wallet-logo.phoenix { background: transparent; }
|
||||
.wallet-logo.blixt { background: transparent; }
|
||||
.wallet-logo.zeus { background: transparent; padding: 4px; }
|
||||
.wallet-logo.zeus img { width: 48px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); }
|
||||
|
||||
.wallet-logo.wos { background: transparent; padding: 4px; }
|
||||
.wallet-logo.wos img { width: 80px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); }
|
||||
|
||||
.wallet-logo.muun { background: transparent; padding: 4px; }
|
||||
.wallet-logo.muun img { width: 48px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); }
|
||||
|
||||
/* Pause animation on hover for logos */
|
||||
.wallet-logos-container:hover .wallet-logos-track {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
.message-slide {
|
||||
animation: messageSlideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
.chat-messages-area .message:last-child {
|
||||
scroll-margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.chat-messages-area {
|
||||
scroll-behavior: smooth;
|
||||
scroll-padding-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes iconPulse {
|
||||
0%, 100% { opacity: 0.7; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes messageSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes walletLogosScroll {
|
||||
from { transform: translateX(0); }
|
||||
to { transform: translateX(-50%); }
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
.custom-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(156, 163, 175, 0.5);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(156, 163, 175, 0.7);
|
||||
}
|
||||
|
||||
.accent-orange { color: #fb923c !important; }
|
||||
.accent-green { color: #34d399 !important; }
|
||||
.accent-red { color: #f87171 !important; }
|
||||
.accent-yellow { color: #fbbf24 !important; }
|
||||
.accent-purple { color: #a78bfa !important; }
|
||||
.accent-gray { color: #9ca3af !important; }
|
||||
.accent-cyan { color: #22d3ee !important; }
|
||||
.accent-blue { color: #60a5fa !important; }
|
||||
|
||||
/* Ensure icons visible in dark backgrounds */
|
||||
.text-secondary i {
|
||||
opacity: 0.8;
|
||||
}
|
||||
467
src/styles/main.css
Normal file
467
src/styles/main.css
Normal file
@@ -0,0 +1,467 @@
|
||||
/* Basic fonts and colors */
|
||||
* {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #2A2B2A;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Basic backgrounds */
|
||||
.bg-custom-bg {
|
||||
background-color: rgb(37 38 37) !important;
|
||||
}
|
||||
|
||||
.bg-my{
|
||||
background-color: rgb(26 26 26);
|
||||
}
|
||||
|
||||
.bg-header {
|
||||
background-color: rgb(35 35 35) !important;
|
||||
}
|
||||
|
||||
.minimal-bg {
|
||||
background: linear-gradient(135deg, #1a1a1a 0%, #2A2B2A 100%);
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.card-minimal.rounded-xl.p-6.cursor-pointer.group.flex-1.create {
|
||||
background-color: #60a5fa08;
|
||||
}
|
||||
|
||||
.card-minimal.rounded-xl.p-6.cursor-pointer.group.flex-1.join {
|
||||
background-color: #34d39908;
|
||||
}
|
||||
|
||||
tr.bg-black-table {
|
||||
background-color: #1c1c1c;
|
||||
}
|
||||
|
||||
|
||||
/* Text styles */
|
||||
.text-primary { color: #f1f5f9; }
|
||||
.text-secondary { color: #b6b6b6; }
|
||||
.text-muted { color: #818080; }
|
||||
|
||||
/* Accent colors */
|
||||
.accent-orange { color: #fb923c; }
|
||||
.accent-green { color: #34d399; }
|
||||
.accent-red { color: #f87171; }
|
||||
.accent-yellow { color: #fbbf24; }
|
||||
.accent-purple { color: #a78bfa; }
|
||||
.accent-blue { color: #60a5fa; }
|
||||
.accent-gray { color: #9ca3af; }
|
||||
.accent-cyan { color: #22d3ee; }
|
||||
|
||||
/* Custom scroll */
|
||||
.custom-scrollbar::-webkit-scrollbar { width: 6px; }
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: rgba(42, 43, 42, 0.3);
|
||||
border-radius: 3px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: rgba(75, 85, 99, 0.5);
|
||||
border-radius: 3px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(75, 85, 99, 0.7);
|
||||
}
|
||||
|
||||
/* Smooth scrolling */
|
||||
.scroll-smooth {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Improved scrolling for messages */
|
||||
.messages-container {
|
||||
scroll-behavior: smooth;
|
||||
scroll-padding-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Enhanced autoscroll for chat */
|
||||
.chat-messages-area {
|
||||
scroll-behavior: smooth;
|
||||
scroll-padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.chat-messages-area > div:first-child {
|
||||
scroll-behavior: smooth;
|
||||
scroll-padding-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Smooth scrolling for all message containers */
|
||||
[class*="chat"] {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Media Queries (Mobile/Tablet) */
|
||||
@media (max-width: 640px) {
|
||||
.header-minimal { padding: 0 8px; }
|
||||
|
||||
.icon-container {
|
||||
min-width: 32px;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.verification-code {
|
||||
font-size: 0.875rem;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.header-minimal .max-w-7xl {
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.header-minimal button {
|
||||
min-width: 32px;
|
||||
min-height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 641px) and (max-width: 1024px) {
|
||||
.header-minimal .max-w-7xl {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
position: relative;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
|
||||
.card::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 12px;
|
||||
padding: 2px;
|
||||
background: radial-gradient(
|
||||
circle at var(--x, 50%) var(--y, 50%),
|
||||
var(--color),
|
||||
transparent 80%
|
||||
);
|
||||
opacity: 0;
|
||||
transition: opacity 0.4s;
|
||||
-webkit-mask:
|
||||
linear-gradient(#fff 0 0) content-box,
|
||||
linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
}
|
||||
|
||||
.card.active-glow::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.card::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 3px;
|
||||
border-radius: 10px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
:root {
|
||||
--gap: 1.25rem;
|
||||
--speed: 0.55s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
--closed: 5rem;
|
||||
--open: 30rem;
|
||||
--accent: #fb923c;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
font-family: Inter, sans-serif;
|
||||
background: #07090d;
|
||||
color: #c5c7ce;
|
||||
}
|
||||
|
||||
.head {
|
||||
max-width: 1400px;
|
||||
margin: auto;
|
||||
padding: 70px 20px 40px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
|
||||
.nav-btn {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
color: #fff;
|
||||
font-size: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: 0.3s;
|
||||
}
|
||||
.nav-btn:hover {
|
||||
background: var(--accent);
|
||||
}
|
||||
.nav-btn:disabled {
|
||||
opacity: 0.3;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.slider {
|
||||
max-width: 1400px;
|
||||
margin: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.track {
|
||||
display: flex;
|
||||
gap: var(--gap);
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
scroll-behavior: smooth;
|
||||
scroll-snap-type: x mandatory;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
.track::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.project-card {
|
||||
position: relative;
|
||||
flex: 0 0 var(--closed);
|
||||
height: 26rem;
|
||||
border-radius: 1rem;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: flex-basis var(--speed), transform var(--speed);
|
||||
}
|
||||
.project-card[active] {
|
||||
flex-basis: var(--open);
|
||||
transform: translateY(-6px);
|
||||
box-shadow: 0 15px 12px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
.project-card__bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
filter: brightness(0.75) saturate(75%);
|
||||
transition: filter 0.3s, transform var(--speed);
|
||||
}
|
||||
.project-card:hover .project-card__bg {
|
||||
filter: brightness(0.9) saturate(100%);
|
||||
transform: scale(1.06);
|
||||
}
|
||||
|
||||
.project-card__content {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.7rem;
|
||||
padding: 0;
|
||||
background: linear-gradient(transparent 40%, rgba(0, 0, 0, 0.35) 100%);
|
||||
z-index: 2;
|
||||
}
|
||||
.project-card__title {
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
font-size: 1.35rem;
|
||||
writing-mode: vertical-rl;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.project-card__thumb,
|
||||
.project-card__desc,
|
||||
.project-card__btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.project-card[active] .project-card__content {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 1.2rem 2rem;
|
||||
gap: 1.1rem;
|
||||
}
|
||||
.project-card[active] .project-card__title {
|
||||
writing-mode: horizontal-tb;
|
||||
transform: none;
|
||||
font-size: 2.4rem;
|
||||
}
|
||||
.project-card[active] .project-card__thumb,
|
||||
.project-card[active] .project-card__desc,
|
||||
.project-card[active] .project-card__btn {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.project-card__thumb {
|
||||
width: 133px;
|
||||
height: 269px;
|
||||
border-radius: 0.45rem;
|
||||
object-fit: cover;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
.project-card__desc {
|
||||
color: #b6b6b6;
|
||||
font-size: 1rem;
|
||||
line-height: 1.4;
|
||||
max-width: 16rem;
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 767px) {
|
||||
:root {
|
||||
--closed: 4rem;
|
||||
--open: 22rem;
|
||||
}
|
||||
.head {
|
||||
padding: 50px 20px 30px;
|
||||
}
|
||||
.track {
|
||||
flex-direction: column;
|
||||
scroll-snap-type: y mandatory;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.project-card {
|
||||
height: 20rem;
|
||||
}
|
||||
.project-card__title {
|
||||
font-size: 1.1rem;
|
||||
writing-mode: horizontal-tb;
|
||||
transform: none;
|
||||
text-align: center;
|
||||
padding-inline: 0.3rem;
|
||||
}
|
||||
.nav-btn {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
:root {
|
||||
--closed: 100%;
|
||||
--open: 100%;
|
||||
--gap: 0.8rem;
|
||||
}
|
||||
|
||||
.head {
|
||||
padding: 30px 15px 20px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.slider {
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.track {
|
||||
flex-direction: column;
|
||||
scroll-snap-type: y mandatory;
|
||||
gap: 0.8rem;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.project-card {
|
||||
height: auto;
|
||||
min-height: 80px;
|
||||
flex: 0 0 auto;
|
||||
width: 100%;
|
||||
scroll-snap-align: start;
|
||||
}
|
||||
|
||||
.project-card[active] {
|
||||
min-height: 300px;
|
||||
transform: none;
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.project-card__content {
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
padding: 1rem;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.project-card__title {
|
||||
writing-mode: horizontal-tb;
|
||||
transform: none;
|
||||
font-size: 1.2rem;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.project-card__thumb,
|
||||
.project-card__desc,
|
||||
.project-card__btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.project-card[active] .project-card__content {
|
||||
align-items: flex-start;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.project-card[active] .project-card__title {
|
||||
font-size: 1.8rem;
|
||||
margin-bottom: 1rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.project-card[active] .project-card__thumb {
|
||||
width: 200px;
|
||||
height: 267px;
|
||||
border-radius: 0.35rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.project-card[active] .project-card__desc {
|
||||
font-size: 0.95rem;
|
||||
max-width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.project-card[active] .project-card__btn {
|
||||
align-self: center;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding: 0.7rem;
|
||||
}
|
||||
|
||||
.dots {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.controls {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
padding: 0 15px 20px;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
position: static;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
4
src/styles/tw-input.css
Normal file
4
src/styles/tw-input.css
Normal file
@@ -0,0 +1,4 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
2029
src/transfer/EnhancedSecureFileTransfer.js
Normal file
2029
src/transfer/EnhancedSecureFileTransfer.js
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user