mirror of
https://github.com/kccleoc/seedpgp-web.git
synced 2026-03-07 09:57:50 +08:00
update badges, cosmetic things and UI change
This commit is contained in:
62
src/components/ClipboardDetails.tsx
Normal file
62
src/components/ClipboardDetails.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import React from 'react';
|
||||
|
||||
interface ClipboardEvent {
|
||||
timestamp: Date;
|
||||
field: string;
|
||||
length: number;
|
||||
}
|
||||
|
||||
interface ClipboardDetailsProps {
|
||||
events: ClipboardEvent[];
|
||||
onClear: () => void;
|
||||
}
|
||||
|
||||
export const ClipboardDetails: React.FC<ClipboardDetailsProps> = ({ events, onClear }) => {
|
||||
return (
|
||||
<div>
|
||||
{events.length > 0 && (
|
||||
<div className="mb-3 bg-orange-100 border border-orange-300 rounded-md p-3 text-xs text-orange-900">
|
||||
<strong>⚠️ Clipboard Warning:</strong> Copied data is accessible to other apps,
|
||||
browser tabs, and extensions. Clear clipboard after use.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{events.length > 0 ? (
|
||||
<>
|
||||
<div className="space-y-2 mb-3 max-h-64 overflow-y-auto">
|
||||
{events.map((event, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="bg-white border border-orange-200 rounded-md p-2 text-xs"
|
||||
>
|
||||
<div className="flex justify-between items-start gap-2 mb-1">
|
||||
<span className="font-semibold text-orange-900 break-all">
|
||||
{event.field}
|
||||
</span>
|
||||
<span className="text-gray-400 text-[10px] whitespace-nowrap">
|
||||
{event.timestamp.toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-gray-600 text-[10px]">
|
||||
Copied {event.length} character{event.length !== 1 ? 's' : ''}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={onClear}
|
||||
className="w-full text-xs py-2 px-3 bg-orange-600 hover:bg-orange-700 text-white rounded-md font-medium transition-colors"
|
||||
>
|
||||
🗑️ Clear Clipboard & History
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center py-4">
|
||||
<div className="text-3xl mb-2">✅</div>
|
||||
<p className="text-xs text-gray-500">No clipboard activity detected</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,184 +0,0 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
interface ClipboardEvent {
|
||||
timestamp: Date;
|
||||
field: string;
|
||||
length: number; // Show length without storing actual content
|
||||
}
|
||||
|
||||
export const ClipboardTracker = () => {
|
||||
const [events, setEvents] = useState<ClipboardEvent[]>([]);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleCopy = (e: ClipboardEvent & Event) => {
|
||||
const target = e.target as HTMLElement;
|
||||
|
||||
// Get selection to measure length
|
||||
const selection = window.getSelection()?.toString() || '';
|
||||
const length = selection.length;
|
||||
|
||||
if (length === 0) return; // Nothing copied
|
||||
|
||||
// Detect field name
|
||||
let field = 'Unknown field';
|
||||
|
||||
if (target.tagName === 'TEXTAREA' || target.tagName === 'INPUT') {
|
||||
// Try multiple ways to identify the field
|
||||
field =
|
||||
target.getAttribute('aria-label') ||
|
||||
target.getAttribute('name') ||
|
||||
target.getAttribute('id') ||
|
||||
(target as HTMLInputElement).type ||
|
||||
target.tagName.toLowerCase();
|
||||
|
||||
// Check parent labels
|
||||
const label = target.closest('label') ||
|
||||
document.querySelector(`label[for="${target.id}"]`);
|
||||
if (label) {
|
||||
field = label.textContent?.trim() || field;
|
||||
}
|
||||
|
||||
// Check for data-sensitive attribute
|
||||
const sensitiveAttr = target.getAttribute('data-sensitive') ||
|
||||
target.closest('[data-sensitive]')?.getAttribute('data-sensitive');
|
||||
if (sensitiveAttr) {
|
||||
field = sensitiveAttr;
|
||||
}
|
||||
|
||||
// Detect if it looks like sensitive data
|
||||
const isSensitive = /mnemonic|seed|key|private|password|secret/i.test(
|
||||
target.className + ' ' + field + ' ' + (target.getAttribute('placeholder') || '')
|
||||
);
|
||||
|
||||
if (isSensitive && field === target.tagName.toLowerCase()) {
|
||||
// Try to guess from placeholder
|
||||
const placeholder = target.getAttribute('placeholder');
|
||||
if (placeholder) {
|
||||
field = placeholder.substring(0, 40) + '...';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setEvents(prev => [
|
||||
{ timestamp: new Date(), field, length },
|
||||
...prev.slice(0, 9) // Keep last 10 events
|
||||
]);
|
||||
|
||||
// Auto-expand on first copy
|
||||
if (events.length === 0) {
|
||||
setIsExpanded(true);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('copy', handleCopy as EventListener);
|
||||
return () => document.removeEventListener('copy', handleCopy as EventListener);
|
||||
}, [events.length]);
|
||||
|
||||
const clearClipboard = async () => {
|
||||
try {
|
||||
// Actually clear the system clipboard
|
||||
await navigator.clipboard.writeText('');
|
||||
|
||||
// Clear history
|
||||
setEvents([]);
|
||||
|
||||
// Show success briefly
|
||||
alert('✅ Clipboard cleared and history wiped');
|
||||
} catch (err) {
|
||||
// Fallback for browsers that don't support clipboard API
|
||||
const dummy = document.createElement('textarea');
|
||||
dummy.value = '';
|
||||
document.body.appendChild(dummy);
|
||||
dummy.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(dummy);
|
||||
|
||||
setEvents([]);
|
||||
alert('✅ History cleared (clipboard may require manual clearing)');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-24 right-4 z-50 max-w-sm">
|
||||
<div className={`rounded-lg shadow-lg border-2 transition-all ${events.length > 0
|
||||
? 'bg-orange-50 border-orange-400'
|
||||
: 'bg-gray-50 border-gray-300'
|
||||
}`}>
|
||||
|
||||
{/* Header */}
|
||||
<div
|
||||
className={`px-4 py-3 cursor-pointer flex items-center justify-between rounded-t-lg transition-colors ${events.length > 0 ? 'hover:bg-orange-100' : 'hover:bg-gray-100'
|
||||
}`}
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg">📋</span>
|
||||
<span className="font-semibold text-sm text-gray-700">Clipboard Activity</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{events.length > 0 && (
|
||||
<span className="text-xs bg-orange-500 text-white px-2 py-0.5 rounded-full font-medium">
|
||||
{events.length}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-gray-400 text-sm">{isExpanded ? '▼' : '▶'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expanded Content */}
|
||||
{isExpanded && (
|
||||
<div className="p-4 border-t border-gray-300">
|
||||
{events.length > 0 && (
|
||||
<div className="mb-3 bg-orange-100 border border-orange-300 rounded-md p-3 text-xs text-orange-900">
|
||||
<strong>⚠️ Clipboard Warning:</strong> Copied data is accessible to other apps,
|
||||
browser tabs, and extensions. Clear clipboard after use.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{events.length > 0 ? (
|
||||
<>
|
||||
<div className="space-y-2 mb-3 max-h-64 overflow-y-auto">
|
||||
{events.map((event, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="bg-white border border-orange-200 rounded-md p-2 text-xs"
|
||||
>
|
||||
<div className="flex justify-between items-start gap-2 mb-1">
|
||||
<span className="font-semibold text-orange-900 break-all">
|
||||
{event.field}
|
||||
</span>
|
||||
<span className="text-gray-400 text-[10px] whitespace-nowrap">
|
||||
{event.timestamp.toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-gray-600 text-[10px]">
|
||||
Copied {event.length} character{event.length !== 1 ? 's' : ''}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); // Prevent collapse toggle
|
||||
clearClipboard();
|
||||
}}
|
||||
className="w-full text-xs py-2 px-3 bg-orange-600 hover:bg-orange-700 text-white rounded-md font-medium transition-colors"
|
||||
>
|
||||
🗑️ Clear Clipboard & History
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center py-4">
|
||||
<div className="text-3xl mb-2">✅</div>
|
||||
<p className="text-xs text-gray-500">No clipboard activity detected</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
114
src/components/Header.tsx
Normal file
114
src/components/Header.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import React from 'react';
|
||||
import { Shield, Lock } from 'lucide-react';
|
||||
import SecurityBadge from './badges/SecurityBadge';
|
||||
import StorageBadge from './badges/StorageBadge';
|
||||
import ClipboardBadge from './badges/ClipboardBadge';
|
||||
|
||||
interface StorageItem {
|
||||
key: string;
|
||||
value: string;
|
||||
size: number;
|
||||
isSensitive: boolean;
|
||||
}
|
||||
|
||||
interface ClipboardEvent {
|
||||
timestamp: Date;
|
||||
field: string;
|
||||
length: number;
|
||||
}
|
||||
|
||||
interface HeaderProps {
|
||||
onOpenSecurityModal: () => void;
|
||||
onOpenStorageModal: () => void;
|
||||
localItems: StorageItem[];
|
||||
sessionItems: StorageItem[];
|
||||
events: ClipboardEvent[];
|
||||
onOpenClipboardModal: () => void;
|
||||
activeTab: 'backup' | 'restore';
|
||||
setActiveTab: (tab: 'backup' | 'restore') => void;
|
||||
encryptedMnemonicCache: any;
|
||||
handleLockAndClear: () => void;
|
||||
}
|
||||
|
||||
const Header: React.FC<HeaderProps> = ({
|
||||
onOpenSecurityModal,
|
||||
onOpenStorageModal,
|
||||
localItems,
|
||||
sessionItems,
|
||||
events,
|
||||
onOpenClipboardModal,
|
||||
activeTab,
|
||||
setActiveTab,
|
||||
encryptedMnemonicCache,
|
||||
handleLockAndClear
|
||||
}) => {
|
||||
return (
|
||||
<header className="sticky top-0 z-50 bg-slate-900 border-b border-slate-800 backdrop-blur-sm">
|
||||
<div className="max-w-7xl mx-auto px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
{/* Left: Logo & Title */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-blue-500 rounded-lg flex items-center justify-center">
|
||||
<Shield className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-lg font-semibold text-white">
|
||||
SeedPGP <span className="text-blue-400">v1.4.2</span>
|
||||
</h1>
|
||||
<p className="text-xs text-slate-400">OpenPGP-secured BIP39 backup</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Center: Monitoring Badges */}
|
||||
<div className="hidden md:flex items-center gap-3">
|
||||
<SecurityBadge onClick={onOpenSecurityModal} />
|
||||
<div onClick={onOpenStorageModal} className="cursor-pointer">
|
||||
<StorageBadge localItems={localItems} sessionItems={sessionItems} />
|
||||
</div>
|
||||
<div onClick={onOpenClipboardModal} className="cursor-pointer">
|
||||
<ClipboardBadge events={events} onOpenClipboardModal={onOpenClipboardModal} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: Action Buttons */}
|
||||
<div className="flex items-center gap-3">
|
||||
{encryptedMnemonicCache && (
|
||||
<button
|
||||
onClick={handleLockAndClear}
|
||||
className="flex items-center gap-2 text-sm text-red-400 bg-slate-800/50 px-3 py-1.5 rounded-lg hover:bg-red-900/50 transition-colors"
|
||||
>
|
||||
<Lock size={16} />
|
||||
<span>Lock/Clear</span>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className={`px-4 py-2 rounded-lg ${activeTab === 'backup' ? 'bg-blue-500 hover:bg-blue-600' : 'bg-slate-700 hover:bg-slate-600'}`}
|
||||
onClick={() => setActiveTab('backup')}
|
||||
>
|
||||
Backup
|
||||
</button>
|
||||
<button
|
||||
className={`px-4 py-2 rounded-lg ${activeTab === 'restore' ? 'bg-blue-500 hover:bg-blue-600' : 'bg-slate-700 hover:bg-slate-600'}`}
|
||||
onClick={() => setActiveTab('restore')}
|
||||
>
|
||||
Restore
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile: Stack monitoring badges */}
|
||||
<div className="md:hidden flex items-center gap-3 mt-3 pt-3 border-t border-slate-800">
|
||||
<SecurityBadge onClick={onOpenSecurityModal} />
|
||||
<div onClick={onOpenStorageModal} className="cursor-pointer">
|
||||
<StorageBadge localItems={localItems} sessionItems={sessionItems} />
|
||||
</div>
|
||||
<div onClick={onOpenClipboardModal} className="cursor-pointer">
|
||||
<ClipboardBadge events={events} onOpenClipboardModal={onOpenClipboardModal} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
@@ -4,7 +4,7 @@ export const SecurityWarnings = () => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-4 left-4 z-50 max-w-sm">
|
||||
<div className="fixed top-4 left-4 z-50 max-w-sm">
|
||||
<div className="bg-yellow-50 border-2 border-yellow-400 rounded-lg shadow-lg">
|
||||
|
||||
{/* Header */}
|
||||
|
||||
82
src/components/StorageDetails.tsx
Normal file
82
src/components/StorageDetails.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
|
||||
interface StorageItem {
|
||||
key: string;
|
||||
value: string;
|
||||
size: number;
|
||||
isSensitive: boolean;
|
||||
}
|
||||
|
||||
interface StorageDetailsProps {
|
||||
localItems: StorageItem[];
|
||||
sessionItems: StorageItem[];
|
||||
}
|
||||
|
||||
export const StorageDetails: React.FC<StorageDetailsProps> = ({ localItems, sessionItems }) => {
|
||||
const totalItems = localItems.length + sessionItems.length;
|
||||
const sensitiveCount = [...localItems, ...sessionItems].filter(i => i.isSensitive).length;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{sensitiveCount > 0 && (
|
||||
<div className="mb-3 bg-yellow-50 border border-yellow-300 rounded-md p-3 text-xs">
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-yellow-600 mt-0.5">⚠️</span>
|
||||
<div className="text-yellow-800">
|
||||
<strong>Security Notice:</strong> Sensitive data persists in browser storage
|
||||
(survives refresh/restart). Clear manually if on shared device.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
<StorageSection title="localStorage" items={localItems} icon="💾" />
|
||||
<StorageSection title="sessionStorage" items={sessionItems} icon="⏱️" />
|
||||
</div>
|
||||
|
||||
{totalItems === 0 && (
|
||||
<div className="text-center py-6">
|
||||
<div className="text-4xl mb-2">✅</div>
|
||||
<p className="text-sm text-gray-500">No data in browser storage</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const StorageSection = ({ title, items, icon }: { title: string; items: StorageItem[]; icon: string }) => {
|
||||
if (items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4 className="text-xs font-bold text-gray-500 mb-2 flex items-center gap-1">
|
||||
<span>{icon}</span>
|
||||
<span className="uppercase">{title}</span>
|
||||
<span className="ml-auto text-gray-400 font-normal">({items.length})</span>
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{items.map((item) => (
|
||||
<div
|
||||
key={item.key}
|
||||
className={`text-xs rounded-md border p-2 ${item.isSensitive
|
||||
? 'bg-red-50 border-red-300'
|
||||
: 'bg-gray-50 border-gray-200'
|
||||
}`}
|
||||
>
|
||||
<div className="flex justify-between items-start gap-2 mb-1">
|
||||
<span className={`font-mono font-semibold text-[11px] break-all ${item.isSensitive ? 'text-red-700' : 'text-gray-700'
|
||||
}`}>
|
||||
{item.isSensitive && '🔴 '}{item.key}
|
||||
</span>
|
||||
<span className="text-gray-400 text-[10px] whitespace-nowrap">{item.size}B</span>
|
||||
</div>
|
||||
<div className="text-gray-500 font-mono text-[10px] break-all leading-relaxed opacity-70">
|
||||
{item.value}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,150 +0,0 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
interface StorageItem {
|
||||
key: string;
|
||||
value: string;
|
||||
size: number;
|
||||
isSensitive: boolean;
|
||||
}
|
||||
|
||||
export const StorageIndicator = () => {
|
||||
const [localItems, setLocalItems] = useState<StorageItem[]>([]);
|
||||
const [sessionItems, setSessionItems] = useState<StorageItem[]>([]);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const SENSITIVE_PATTERNS = ['key', 'mnemonic', 'seed', 'private', 'secret', 'pgp', 'password'];
|
||||
|
||||
const isSensitiveKey = (key: string): boolean => {
|
||||
const lowerKey = key.toLowerCase();
|
||||
return SENSITIVE_PATTERNS.some(pattern => lowerKey.includes(pattern));
|
||||
};
|
||||
|
||||
const getStorageItems = (storage: Storage): StorageItem[] => {
|
||||
const items: StorageItem[] = [];
|
||||
for (let i = 0; i < storage.length; i++) {
|
||||
const key = storage.key(i);
|
||||
if (key) {
|
||||
const value = storage.getItem(key) || '';
|
||||
items.push({
|
||||
key,
|
||||
value: value.substring(0, 50) + (value.length > 50 ? '...' : ''),
|
||||
size: new Blob([value]).size,
|
||||
isSensitive: isSensitiveKey(key)
|
||||
});
|
||||
}
|
||||
}
|
||||
return items.sort((a, b) => (b.isSensitive ? 1 : 0) - (a.isSensitive ? 1 : 0));
|
||||
};
|
||||
|
||||
const refreshStorage = () => {
|
||||
setLocalItems(getStorageItems(localStorage));
|
||||
setSessionItems(getStorageItems(sessionStorage));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refreshStorage();
|
||||
const interval = setInterval(refreshStorage, 2000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const totalItems = localItems.length + sessionItems.length;
|
||||
const sensitiveCount = [...localItems, ...sessionItems].filter(i => i.isSensitive).length;
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-4 right-4 z-50 max-w-md">
|
||||
<div className={`bg-white rounded-lg shadow-lg border-2 ${sensitiveCount > 0 ? 'border-red-400' : 'border-gray-300'
|
||||
} transition-all duration-200`}>
|
||||
|
||||
{/* Header Bar */}
|
||||
<div
|
||||
className={`px-4 py-3 rounded-t-lg cursor-pointer flex items-center justify-between ${sensitiveCount > 0 ? 'bg-red-50' : 'bg-gray-50'
|
||||
} hover:opacity-90 transition-opacity`}
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg">🗄️</span>
|
||||
<span className="font-semibold text-sm text-gray-700">Storage Monitor</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{sensitiveCount > 0 && (
|
||||
<span className="text-xs bg-red-500 text-white px-2 py-0.5 rounded-full font-medium">
|
||||
{sensitiveCount}⚠️
|
||||
</span>
|
||||
)}
|
||||
<span className={`text-xs px-2 py-0.5 rounded-full font-medium ${totalItems > 0 ? 'bg-blue-100 text-blue-700' : 'bg-green-100 text-green-700'
|
||||
}`}>
|
||||
{totalItems === 0 ? '✓ Empty' : `${totalItems} item${totalItems !== 1 ? 's' : ''}`}
|
||||
</span>
|
||||
<span className="text-gray-400 text-sm">{isExpanded ? '▼' : '▶'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expanded Content */}
|
||||
{isExpanded && (
|
||||
<div className="p-4 max-h-96 overflow-y-auto">
|
||||
{sensitiveCount > 0 && (
|
||||
<div className="mb-3 bg-yellow-50 border border-yellow-300 rounded-md p-3 text-xs">
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-yellow-600 mt-0.5">⚠️</span>
|
||||
<div className="text-yellow-800">
|
||||
<strong>Security Notice:</strong> Sensitive data persists in browser storage
|
||||
(survives refresh/restart). Clear manually if on shared device.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
<StorageSection title="localStorage" items={localItems} icon="💾" />
|
||||
<StorageSection title="sessionStorage" items={sessionItems} icon="⏱️" />
|
||||
</div>
|
||||
|
||||
{totalItems === 0 && (
|
||||
<div className="text-center py-6">
|
||||
<div className="text-4xl mb-2">✅</div>
|
||||
<p className="text-sm text-gray-500">No data in browser storage</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const StorageSection = ({ title, items, icon }: { title: string; items: StorageItem[]; icon: string }) => {
|
||||
if (items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4 className="text-xs font-bold text-gray-500 mb-2 flex items-center gap-1">
|
||||
<span>{icon}</span>
|
||||
<span className="uppercase">{title}</span>
|
||||
<span className="ml-auto text-gray-400 font-normal">({items.length})</span>
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{items.map((item) => (
|
||||
<div
|
||||
key={item.key}
|
||||
className={`text-xs rounded-md border p-2 ${item.isSensitive
|
||||
? 'bg-red-50 border-red-300'
|
||||
: 'bg-gray-50 border-gray-200'
|
||||
}`}
|
||||
>
|
||||
<div className="flex justify-between items-start gap-2 mb-1">
|
||||
<span className={`font-mono font-semibold text-[11px] break-all ${item.isSensitive ? 'text-red-700' : 'text-gray-700'
|
||||
}`}>
|
||||
{item.isSensitive && '🔴 '}{item.key}
|
||||
</span>
|
||||
<span className="text-gray-400 text-[10px] whitespace-nowrap">{item.size}B</span>
|
||||
</div>
|
||||
<div className="text-gray-500 font-mono text-[10px] break-all leading-relaxed opacity-70">
|
||||
{item.value}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
39
src/components/badges/ClipboardBadge.tsx
Normal file
39
src/components/badges/ClipboardBadge.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import { Clipboard } from 'lucide-react';
|
||||
|
||||
interface ClipboardEvent {
|
||||
timestamp: Date;
|
||||
field: string;
|
||||
length: number;
|
||||
}
|
||||
|
||||
interface ClipboardBadgeProps {
|
||||
events: ClipboardEvent[];
|
||||
onOpenClipboardModal: () => void; // New prop
|
||||
}
|
||||
|
||||
const ClipboardBadge: React.FC<ClipboardBadgeProps> = ({ events, onOpenClipboardModal }) => {
|
||||
const count = events.length;
|
||||
|
||||
// Determine badge style based on clipboard count
|
||||
const badgeStyle =
|
||||
count === 0
|
||||
? "text-green-500 bg-green-500/10 border-green-500/20" // Safe
|
||||
: count < 5
|
||||
? "text-amber-500 bg-amber-500/10 border-amber-500/30 font-semibold" // Warning
|
||||
: "text-red-500 bg-red-500/10 border-red-500/30 font-bold animate-pulse"; // Danger
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onOpenClipboardModal}
|
||||
className={`flex items-center gap-2 px-3 py-1.5 rounded-lg border transition-all hover:scale-105 ${badgeStyle}`}
|
||||
>
|
||||
<Clipboard className="w-3.5 h-3.5" />
|
||||
<span className="text-xs">
|
||||
{count === 0 ? "Empty" : `${count} item${count > 1 ? 's' : ''}`}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClipboardBadge;
|
||||
20
src/components/badges/SecurityBadge.tsx
Normal file
20
src/components/badges/SecurityBadge.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { AlertTriangle } from 'lucide-react';
|
||||
|
||||
interface SecurityBadgeProps {
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
const SecurityBadge: React.FC<SecurityBadgeProps> = ({ onClick }) => {
|
||||
return (
|
||||
<button
|
||||
className="flex items-center gap-2 text-amber-500/80 hover:text-amber-500 transition-colors"
|
||||
onClick={onClick}
|
||||
>
|
||||
<AlertTriangle className="w-4 h-4" />
|
||||
<span className="text-xs font-medium">Security Info</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default SecurityBadge;
|
||||
34
src/components/badges/StorageBadge.tsx
Normal file
34
src/components/badges/StorageBadge.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { HardDrive } from 'lucide-react';
|
||||
|
||||
interface StorageItem {
|
||||
key: string;
|
||||
value: string;
|
||||
size: number;
|
||||
isSensitive: boolean;
|
||||
}
|
||||
|
||||
interface StorageBadgeProps {
|
||||
localItems: StorageItem[];
|
||||
sessionItems: StorageItem[];
|
||||
}
|
||||
|
||||
const StorageBadge: React.FC<StorageBadgeProps> = ({ localItems, sessionItems }) => {
|
||||
const totalItems = localItems.length + sessionItems.length;
|
||||
const sensitiveCount = [...localItems, ...sessionItems].filter(i => i.isSensitive).length;
|
||||
|
||||
const status = sensitiveCount > 0 ? 'Warning' : totalItems > 0 ? 'Active' : 'Empty';
|
||||
const colorClass =
|
||||
status === 'Warning' ? 'text-amber-500/80' :
|
||||
status === 'Active' ? 'text-blue-500/80' :
|
||||
'text-green-500/80';
|
||||
|
||||
return (
|
||||
<div className={`flex items-center gap-2 ${colorClass}`}>
|
||||
<HardDrive className="w-4 h-4" />
|
||||
<span className="text-xs font-medium">{status}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StorageBadge;
|
||||
Reference in New Issue
Block a user