Import source: src/components/CenterPanel/QRHistory

Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
arsam
2026-09-18 23:16:59 +03:30
parent 65572ef95b
commit 84163ce082
+115
View File
@@ -0,0 +1,115 @@
import { useState } from 'react';
import { useHistory } from '../../context/HistoryContext';
import { useQR } from '../../context/QRContext';
import { QR_TYPES } from '../../config/qrTypes';
import { useTheme } from '../../context/ThemeContext';
function timeAgo(ms: number) {
const s = Math.floor((Date.now() - ms) / 1000);
if (s < 60) return 'just now';
if (s < 3600) return `${Math.floor(s / 60)}m ago`;
if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
return `${Math.floor(s / 86400)}d ago`;
}
export function QRHistory() {
const { history, remove, clear } = useHistory();
const { dispatch } = useQR();
const { isDark } = useTheme();
const [open, setOpen] = useState(false);
if (!history.length) return null;
const restore = (e: (typeof history)[0]) => {
dispatch({ type: 'SET_TYPE', payload: e.type });
dispatch({ type: 'SET_QR_COLOR', payload: e.snapshot.qrColor });
dispatch({ type: 'SET_BG_COLOR', payload: e.snapshot.bgColor });
dispatch({ type: 'SET_PATTERN', payload: e.snapshot.pattern });
dispatch({ type: 'SET_THEME', payload: e.snapshot.theme });
dispatch({ type: 'SET_GRADIENT', payload: e.snapshot.gradientEnabled });
dispatch({ type: 'SET_GRADIENT_COLOR2', payload: e.snapshot.gradientColor2 });
dispatch({ type: 'SET_CORNER_SQUARE', payload: e.snapshot.cornerSquareStyle });
dispatch({ type: 'SET_CORNER_DOT', payload: e.snapshot.cornerDotStyle });
dispatch({ type: 'SET_LOGO', payload: e.snapshot.logoDataUrl });
// restore form data for that type
const fd = e.formData[e.type] as unknown as Record<string, string>;
Object.entries(fd).forEach(([field, value]) =>
dispatch({ type: 'SET_FORM_FIELD', qrType: e.type, field, value })
);
};
return (
<div className="rounded-[16px] overflow-hidden" style={{ border: '1px solid var(--border)' }}>
{/* Header */}
<button
onClick={() => setOpen(o => !o)}
className="w-full flex items-center justify-between px-5 py-3.5 cursor-pointer transition-colors"
style={{ background: 'var(--bg-card)' }}
onMouseEnter={e => (e.currentTarget.style.background = 'var(--hover-bg)')}
onMouseLeave={e => (e.currentTarget.style.background = 'var(--bg-card)')}
>
<div className="flex items-center gap-2">
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" style={{ color: '#7C6FE0' }}>
<path d="M7.5 1a6.5 6.5 0 1 1 0 13A6.5 6.5 0 0 1 7.5 1zm0 3v4l2.5 2" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round"/>
</svg>
<span className="text-[14px] font-semibold" style={{ color: 'var(--text-primary)' }}>
Recent ({history.length})
</span>
</div>
<div className="flex items-center gap-3">
{open && (
<span onClick={e => { e.stopPropagation(); clear(); }} className="text-[12px] cursor-pointer" style={{ color: 'var(--text-muted)' }}
onMouseEnter={e => (e.currentTarget.style.color = '#ef4444')}
onMouseLeave={e => (e.currentTarget.style.color = 'var(--text-muted)')}>
Clear all
</span>
)}
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" style={{ color: 'var(--text-muted)', transform: open ? 'rotate(180deg)' : 'none', transition: 'transform 0.2s' }}>
<path d="M2 4l4 4 4-4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
</svg>
</div>
</button>
{/* List */}
{open && (
<div style={{ background: 'var(--bg-card)', borderTop: '1px solid var(--border)' }}>
{history.map(entry => {
const typeConfig = QR_TYPES.find(t => t.key === entry.type);
const thumb = isDark ? typeConfig?.thumbDark : typeConfig?.thumb;
return (
<div key={entry.id}
className="flex items-center gap-3 px-4 py-3 group transition-colors"
style={{ borderBottom: '1px solid var(--border)' }}
onMouseEnter={e => (e.currentTarget.style.background = 'var(--hover-bg)')}
onMouseLeave={e => (e.currentTarget.style.background = 'transparent')}
>
{thumb && <img src={thumb} alt={entry.type} className="w-10 h-7 rounded-[6px] object-cover flex-shrink-0" />}
<div className="flex-1 min-w-0">
<p className="text-[13px] font-medium truncate" style={{ color: 'var(--text-primary)' }}>{entry.label}</p>
<p className="text-[11px]" style={{ color: 'var(--text-muted)' }}>{entry.type.toUpperCase()} · {timeAgo(entry.savedAt)}</p>
</div>
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<button onClick={() => restore(entry)} title="Restore"
className="w-7 h-7 rounded-[7px] flex items-center justify-center cursor-pointer transition-colors"
style={{ background: 'rgba(124,111,224,0.1)', color: '#7C6FE0' }}>
<svg width="13" height="13" viewBox="0 0 13 13" fill="none">
<path d="M2 6.5A4.5 4.5 0 1 1 6.5 11" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round"/>
<path d="M2 4v2.5h2.5" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</button>
<button onClick={() => remove(entry.id)} title="Delete"
className="w-7 h-7 rounded-[7px] flex items-center justify-center cursor-pointer transition-colors"
style={{ background: 'rgba(239,68,68,0.08)', color: '#ef4444' }}>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
<path d="M2 2l8 8M10 2L2 10" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
</svg>
</button>
</div>
</div>
);
})}
</div>
)}
</div>
);
}