dev experience
This commit is contained in:
160
apps/website/components/dev/sections/ReplaySection.tsx
Normal file
160
apps/website/components/dev/sections/ReplaySection.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Play, Copy, Trash2, Download, Clock } from 'lucide-react';
|
||||
import { getGlobalReplaySystem } from '@/lib/infrastructure/ErrorReplay';
|
||||
|
||||
interface ReplayEntry {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
error: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export function ReplaySection() {
|
||||
const [replays, setReplays] = useState<ReplayEntry[]>([]);
|
||||
const [selectedReplay, setSelectedReplay] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadReplays();
|
||||
}, []);
|
||||
|
||||
const loadReplays = () => {
|
||||
const system = getGlobalReplaySystem();
|
||||
const index = system.getReplayIndex();
|
||||
setReplays(index);
|
||||
};
|
||||
|
||||
const handleReplay = async (replayId: string) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const system = getGlobalReplaySystem();
|
||||
await system.replay(replayId);
|
||||
} catch (error) {
|
||||
console.error('Replay failed:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExport = (replayId: string) => {
|
||||
const system = getGlobalReplaySystem();
|
||||
const data = system.exportReplay(replayId, 'json');
|
||||
|
||||
const blob = new Blob([data], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `replay_${replayId}.json`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const handleCopy = async (replayId: string) => {
|
||||
const system = getGlobalReplaySystem();
|
||||
const data = system.exportReplay(replayId, 'json');
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(data);
|
||||
console.log('Replay data copied to clipboard');
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = (replayId: string) => {
|
||||
if (confirm('Delete this replay?')) {
|
||||
const system = getGlobalReplaySystem();
|
||||
system.deleteReplay(replayId);
|
||||
loadReplays();
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearAll = () => {
|
||||
if (confirm('Clear all replays?')) {
|
||||
const system = getGlobalReplaySystem();
|
||||
system.clearAll();
|
||||
loadReplays();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-semibold text-gray-400">Error Replay</span>
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
onClick={loadReplays}
|
||||
className="p-1 hover:bg-charcoal-outline rounded"
|
||||
title="Refresh"
|
||||
>
|
||||
<Clock className="w-3 h-3 text-gray-400" />
|
||||
</button>
|
||||
<button
|
||||
onClick={handleClearAll}
|
||||
className="p-1 hover:bg-charcoal-outline rounded"
|
||||
title="Clear All"
|
||||
>
|
||||
<Trash2 className="w-3 h-3 text-red-400" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{replays.length === 0 ? (
|
||||
<div className="text-xs text-gray-500 text-center py-2">
|
||||
No replays available
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1 max-h-48 overflow-auto">
|
||||
{replays.map((replay) => (
|
||||
<div
|
||||
key={replay.id}
|
||||
className="bg-deep-graphite border border-charcoal-outline rounded p-2 text-xs"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2 mb-1">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-mono text-red-400 font-bold truncate">
|
||||
{replay.type}
|
||||
</div>
|
||||
<div className="text-gray-300 truncate">{replay.error}</div>
|
||||
<div className="text-gray-500 text-[10px]">
|
||||
{new Date(replay.timestamp).toLocaleTimeString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-1 mt-1">
|
||||
<button
|
||||
onClick={() => handleReplay(replay.id)}
|
||||
disabled={loading}
|
||||
className="flex items-center gap-1 px-2 py-1 bg-green-600 hover:bg-green-700 text-white rounded"
|
||||
>
|
||||
<Play className="w-3 h-3" />
|
||||
Replay
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleExport(replay.id)}
|
||||
className="flex items-center gap-1 px-2 py-1 bg-iron-gray hover:bg-charcoal-outline text-gray-300 rounded border border-charcoal-outline"
|
||||
>
|
||||
<Download className="w-3 h-3" />
|
||||
Export
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleCopy(replay.id)}
|
||||
className="flex items-center gap-1 px-2 py-1 bg-iron-gray hover:bg-charcoal-outline text-gray-300 rounded border border-charcoal-outline"
|
||||
>
|
||||
<Copy className="w-3 h-3" />
|
||||
Copy
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(replay.id)}
|
||||
className="flex items-center gap-1 px-2 py-1 bg-iron-gray hover:bg-charcoal-outline text-gray-300 rounded border border-charcoal-outline"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user