Files
robinhood/frontend/src/components/DashboardCustomizer.tsx
T

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>
);
}