Initial commit: Gold Trading Simulator with AI-powered analysis
This commit is contained in:
@@ -0,0 +1,401 @@
|
||||
import { useState } from 'react';
|
||||
import { Settings, Layout, Grid, Columns, Save, RotateCcw, Eye, EyeOff, GripVertical } from 'lucide-react';
|
||||
import type { DashboardConfig, TabConfig, LayoutMode, LayoutPreset } from '@/types';
|
||||
|
||||
interface DashboardCustomizerProps {
|
||||
config: DashboardConfig;
|
||||
onConfigChange: (config: DashboardConfig) => void;
|
||||
onSavePreset: (name: string, description: string) => void;
|
||||
onLoadPreset: (presetId: string) => void;
|
||||
onResetToDefault: () => void;
|
||||
}
|
||||
|
||||
const DEFAULT_PRESETS: LayoutPreset[] = [
|
||||
{
|
||||
id: 'trading-focus',
|
||||
name: 'Trading Focus',
|
||||
description: 'Optimized for active trading with chart and controls prominent',
|
||||
mode: 'grid',
|
||||
tabs: [],
|
||||
},
|
||||
{
|
||||
id: 'analysis-focus',
|
||||
name: 'Analysis Focus',
|
||||
description: 'Full-screen analytics and AI insights',
|
||||
mode: 'tabs',
|
||||
tabs: [],
|
||||
},
|
||||
{
|
||||
id: 'news-focus',
|
||||
name: 'News Focus',
|
||||
description: 'News and market updates at the forefront',
|
||||
mode: 'split',
|
||||
tabs: [],
|
||||
},
|
||||
{
|
||||
id: 'balanced',
|
||||
name: 'Balanced View',
|
||||
description: 'Equal emphasis on all components',
|
||||
mode: 'grid',
|
||||
tabs: [],
|
||||
},
|
||||
];
|
||||
|
||||
export default function DashboardCustomizer({
|
||||
config,
|
||||
onConfigChange,
|
||||
onSavePreset,
|
||||
onLoadPreset,
|
||||
onResetToDefault,
|
||||
}: DashboardCustomizerProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<'layout' | 'tabs' | 'presets'>('layout');
|
||||
const [draggedTab, setDraggedTab] = useState<TabConfig | null>(null);
|
||||
const [presetName, setPresetName] = useState('');
|
||||
const [presetDescription, setPresetDescription] = useState('');
|
||||
|
||||
const handleLayoutModeChange = (mode: LayoutMode) => {
|
||||
onConfigChange({ ...config, mode });
|
||||
};
|
||||
|
||||
const handleTabToggle = (tabId: string) => {
|
||||
const updatedTabs = config.tabs.map((tab) =>
|
||||
tab.id === tabId ? { ...tab, visible: !tab.visible } : tab
|
||||
);
|
||||
onConfigChange({ ...config, tabs: updatedTabs });
|
||||
};
|
||||
|
||||
const handleTabPin = (tabId: string) => {
|
||||
const updatedTabs = config.tabs.map((tab) =>
|
||||
tab.id === tabId ? { ...tab, pinned: !tab.pinned } : tab
|
||||
);
|
||||
onConfigChange({ ...config, tabs: updatedTabs });
|
||||
};
|
||||
|
||||
const handleTabSizeChange = (tabId: string, size: TabConfig['size']) => {
|
||||
const updatedTabs = config.tabs.map((tab) =>
|
||||
tab.id === tabId ? { ...tab, size } : tab
|
||||
);
|
||||
onConfigChange({ ...config, tabs: updatedTabs });
|
||||
};
|
||||
|
||||
const handleDragStart = (tab: TabConfig) => {
|
||||
setDraggedTab(tab);
|
||||
};
|
||||
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const handleDrop = (targetTab: TabConfig) => {
|
||||
if (!draggedTab) return;
|
||||
|
||||
const updatedTabs = [...config.tabs];
|
||||
const draggedIndex = updatedTabs.findIndex((t) => t.id === draggedTab.id);
|
||||
const targetIndex = updatedTabs.findIndex((t) => t.id === targetTab.id);
|
||||
|
||||
if (draggedIndex !== -1 && targetIndex !== -1) {
|
||||
updatedTabs.splice(draggedIndex, 1);
|
||||
updatedTabs.splice(targetIndex, 0, draggedTab);
|
||||
|
||||
// Update order numbers
|
||||
const reorderedTabs = updatedTabs.map((tab, index) => ({
|
||||
...tab,
|
||||
order: index,
|
||||
}));
|
||||
|
||||
onConfigChange({ ...config, tabs: reorderedTabs });
|
||||
}
|
||||
|
||||
setDraggedTab(null);
|
||||
};
|
||||
|
||||
const handleSavePreset = () => {
|
||||
if (presetName.trim()) {
|
||||
onSavePreset(presetName, presetDescription);
|
||||
setPresetName('');
|
||||
setPresetDescription('');
|
||||
setActiveTab('presets');
|
||||
}
|
||||
};
|
||||
|
||||
const allPresets = [...DEFAULT_PRESETS, ...config.customPresets];
|
||||
|
||||
if (!isOpen) {
|
||||
return (
|
||||
<button
|
||||
onClick={() => setIsOpen(true)}
|
||||
className="btn-secondary flex items-center gap-2"
|
||||
title="Customize Dashboard"
|
||||
>
|
||||
<Settings className="w-4 h-4" />
|
||||
<span>Customize</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
|
||||
<div className="bg-dark-card rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-hidden flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="p-6 border-b border-gray-700">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Settings className="w-6 h-6 text-gold-500" />
|
||||
<div>
|
||||
<h2 className="text-xl font-bold">Dashboard Customization</h2>
|
||||
<p className="text-sm text-gray-400">Personalize your trading interface</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="text-gray-400 hover:text-white transition-colors"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex border-b border-gray-700">
|
||||
<button
|
||||
onClick={() => setActiveTab('layout')}
|
||||
className={`px-6 py-3 font-medium transition-colors ${
|
||||
activeTab === 'layout'
|
||||
? 'text-gold-500 border-b-2 border-gold-500'
|
||||
: 'text-gray-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<Layout className="w-4 h-4 inline mr-2" />
|
||||
Layout Mode
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('tabs')}
|
||||
className={`px-6 py-3 font-medium transition-colors ${
|
||||
activeTab === 'tabs'
|
||||
? 'text-gold-500 border-b-2 border-gold-500'
|
||||
: 'text-gray-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<Columns className="w-4 h-4 inline mr-2" />
|
||||
Tabs & Order
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('presets')}
|
||||
className={`px-6 py-3 font-medium transition-colors ${
|
||||
activeTab === 'presets'
|
||||
? 'text-gold-500 border-b-2 border-gold-500'
|
||||
: 'text-gray-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<Grid className="w-4 h-4 inline mr-2" />
|
||||
Presets
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
{activeTab === 'layout' && (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">Choose Layout Mode</h3>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<button
|
||||
onClick={() => handleLayoutModeChange('grid')}
|
||||
className={`p-4 rounded-lg border-2 transition-all ${
|
||||
config.mode === 'grid'
|
||||
? 'border-gold-500 bg-gold-500/10'
|
||||
: 'border-gray-700 hover:border-gray-600'
|
||||
}`}
|
||||
>
|
||||
<Grid className="w-8 h-8 mx-auto mb-2" />
|
||||
<div className="font-medium">Grid</div>
|
||||
<div className="text-xs text-gray-400 mt-1">
|
||||
Multiple panels visible
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => handleLayoutModeChange('tabs')}
|
||||
className={`p-4 rounded-lg border-2 transition-all ${
|
||||
config.mode === 'tabs'
|
||||
? 'border-gold-500 bg-gold-500/10'
|
||||
: 'border-gray-700 hover:border-gray-600'
|
||||
}`}
|
||||
>
|
||||
<Columns className="w-8 h-8 mx-auto mb-2" />
|
||||
<div className="font-medium">Tabs</div>
|
||||
<div className="text-xs text-gray-400 mt-1">
|
||||
One panel at a time
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => handleLayoutModeChange('split')}
|
||||
className={`p-4 rounded-lg border-2 transition-all ${
|
||||
config.mode === 'split'
|
||||
? 'border-gold-500 bg-gold-500/10'
|
||||
: 'border-gray-700 hover:border-gray-600'
|
||||
}`}
|
||||
>
|
||||
<Layout className="w-8 h-8 mx-auto mb-2" />
|
||||
<div className="font-medium">Split</div>
|
||||
<div className="text-xs text-gray-400 mt-1">
|
||||
Two main sections
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'tabs' && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold">Manage Tabs</h3>
|
||||
<p className="text-sm text-gray-400">Drag to reorder, toggle visibility</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{config.tabs
|
||||
.sort((a, b) => a.order - b.order)
|
||||
.map((tab) => (
|
||||
<div
|
||||
key={tab.id}
|
||||
draggable
|
||||
onDragStart={() => handleDragStart(tab)}
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={() => handleDrop(tab)}
|
||||
className={`p-4 rounded-lg border-2 transition-all cursor-move ${
|
||||
tab.visible
|
||||
? 'border-gray-700 bg-dark-hover'
|
||||
: 'border-gray-800 bg-gray-900/50 opacity-60'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<GripVertical className="w-5 h-5 text-gray-500" />
|
||||
<div>
|
||||
<div className="font-medium">{tab.label}</div>
|
||||
<div className="text-xs text-gray-400">Order: {tab.order + 1}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Size selector */}
|
||||
<select
|
||||
value={tab.size}
|
||||
onChange={(e) =>
|
||||
handleTabSizeChange(tab.id, e.target.value as TabConfig['size'])
|
||||
}
|
||||
className="px-2 py-1 bg-dark-bg border border-gray-700 rounded text-sm"
|
||||
disabled={!tab.visible}
|
||||
>
|
||||
<option value="small">Small</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="large">Large</option>
|
||||
<option value="full">Full</option>
|
||||
</select>
|
||||
|
||||
{/* Pin button */}
|
||||
<button
|
||||
onClick={() => handleTabPin(tab.id)}
|
||||
className={`p-2 rounded ${
|
||||
tab.pinned ? 'text-gold-500' : 'text-gray-400 hover:text-white'
|
||||
}`}
|
||||
title="Pin tab"
|
||||
disabled={!tab.visible}
|
||||
>
|
||||
📌
|
||||
</button>
|
||||
|
||||
{/* Visibility toggle */}
|
||||
<button
|
||||
onClick={() => handleTabToggle(tab.id)}
|
||||
className="p-2 rounded hover:bg-dark-hover"
|
||||
title={tab.visible ? 'Hide' : 'Show'}
|
||||
>
|
||||
{tab.visible ? (
|
||||
<Eye className="w-4 h-4" />
|
||||
) : (
|
||||
<EyeOff className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'presets' && (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">Quick Presets</h3>
|
||||
<div className="grid grid-cols-2 gap-4 mb-6">
|
||||
{allPresets.map((preset) => (
|
||||
<button
|
||||
key={preset.id}
|
||||
onClick={() => onLoadPreset(preset.id)}
|
||||
className={`p-4 rounded-lg border-2 text-left transition-all ${
|
||||
config.activePreset === preset.id
|
||||
? 'border-gold-500 bg-gold-500/10'
|
||||
: 'border-gray-700 hover:border-gray-600'
|
||||
}`}
|
||||
>
|
||||
<div className="font-medium mb-1">{preset.name}</div>
|
||||
<div className="text-xs text-gray-400">{preset.description}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-700 pt-6">
|
||||
<h3 className="text-lg font-semibold mb-4">Save Current Layout</h3>
|
||||
<div className="space-y-3">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Preset name..."
|
||||
value={presetName}
|
||||
onChange={(e) => setPresetName(e.target.value)}
|
||||
className="w-full px-4 py-2 bg-dark-bg border border-gray-700 rounded-lg focus:border-gold-500 focus:outline-none"
|
||||
/>
|
||||
<textarea
|
||||
placeholder="Description (optional)..."
|
||||
value={presetDescription}
|
||||
onChange={(e) => setPresetDescription(e.target.value)}
|
||||
className="w-full px-4 py-2 bg-dark-bg border border-gray-700 rounded-lg focus:border-gold-500 focus:outline-none"
|
||||
rows={2}
|
||||
/>
|
||||
<button
|
||||
onClick={handleSavePreset}
|
||||
disabled={!presetName.trim()}
|
||||
className="btn-primary w-full flex items-center justify-center gap-2"
|
||||
>
|
||||
<Save className="w-4 h-4" />
|
||||
Save as New Preset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="p-6 border-t border-gray-700 flex items-center justify-between">
|
||||
<button
|
||||
onClick={onResetToDefault}
|
||||
className="btn-secondary flex items-center gap-2 text-red-500 hover:text-red-400"
|
||||
>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
Reset to Default
|
||||
</button>
|
||||
<button onClick={() => setIsOpen(false)} className="btn-primary">
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user