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:
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user