402 lines
15 KiB
TypeScript
402 lines
15 KiB
TypeScript
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>
|
|
);
|
|
}
|