297 lines
9.2 KiB
TypeScript
297 lines
9.2 KiB
TypeScript
import { useState, ReactNode } from 'react';
|
|
import { X, Maximize2, Minimize2, Pin, PinOff } from 'lucide-react';
|
|
import ComponentSettings from './ComponentSettings';
|
|
import type { TabConfig, LayoutMode, TabCustomization } from '@/types';
|
|
|
|
interface TabPanel {
|
|
config: TabConfig;
|
|
content: ReactNode;
|
|
}
|
|
|
|
interface TabbedContainerProps {
|
|
panels: TabPanel[];
|
|
mode: LayoutMode;
|
|
onTabClose?: (tabId: string) => void;
|
|
onTabPin?: (tabId: string) => void;
|
|
onCustomizationUpdate?: (tabId: string, customization: TabCustomization) => void;
|
|
className?: string;
|
|
}
|
|
|
|
export default function TabbedContainer({
|
|
panels,
|
|
mode,
|
|
onTabClose,
|
|
onTabPin,
|
|
onCustomizationUpdate,
|
|
className = '',
|
|
}: TabbedContainerProps) {
|
|
const [activeTabId, setActiveTabId] = useState<string>(
|
|
panels.find((p) => p.config.visible)?.config.id || panels[0]?.config.id
|
|
);
|
|
const [expandedTab, setExpandedTab] = useState<string | null>(null);
|
|
|
|
const visiblePanels = panels
|
|
.filter((p) => p.config.visible)
|
|
.sort((a, b) => a.config.order - b.config.order);
|
|
|
|
const activePanel = visiblePanels.find((p) => p.config.id === activeTabId);
|
|
|
|
const handleExpand = (tabId: string) => {
|
|
setExpandedTab(expandedTab === tabId ? null : tabId);
|
|
};
|
|
|
|
// Tabs mode - single panel with tab switcher
|
|
if (mode === 'tabs') {
|
|
return (
|
|
<div className={`flex flex-col h-full ${className}`}>
|
|
{/* Tab switcher */}
|
|
<div className="flex items-center gap-1 border-b border-gray-700 bg-dark-card px-2 overflow-x-auto">
|
|
{visiblePanels.map((panel) => (
|
|
<button
|
|
key={panel.config.id}
|
|
onClick={() => setActiveTabId(panel.config.id)}
|
|
className={`flex items-center gap-2 px-4 py-3 border-b-2 transition-colors whitespace-nowrap ${
|
|
activeTabId === panel.config.id
|
|
? 'border-gold-500 text-gold-500 bg-dark-hover'
|
|
: 'border-transparent text-gray-400 hover:text-white hover:bg-dark-hover'
|
|
}`}
|
|
>
|
|
{panel.config.pinned && <Pin className="w-3 h-3" />}
|
|
<span>{panel.config.label}</span>
|
|
{onTabClose && !panel.config.pinned && (
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onTabClose(panel.config.id);
|
|
}}
|
|
className="p-0.5 hover:bg-red-500/20 rounded"
|
|
>
|
|
<X className="w-3 h-3" />
|
|
</button>
|
|
)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Active panel content */}
|
|
<div className="flex-1 overflow-hidden">
|
|
{activePanel && (
|
|
<div className="h-full">{activePanel.content}</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Split mode - two main sections
|
|
if (mode === 'split') {
|
|
const leftPanels = visiblePanels.filter((p) => p.config.position !== 'right');
|
|
const rightPanels = visiblePanels.filter((p) => p.config.position === 'right');
|
|
|
|
return (
|
|
<div className={`grid grid-cols-2 gap-6 h-full ${className}`}>
|
|
{/* Left section */}
|
|
<div className="space-y-6 overflow-y-auto">
|
|
{leftPanels.map((panel) => (
|
|
<PanelCard
|
|
key={panel.config.id}
|
|
panel={panel}
|
|
onExpand={handleExpand}
|
|
onPin={onTabPin}
|
|
onClose={onTabClose}
|
|
onCustomizationUpdate={onCustomizationUpdate}
|
|
isExpanded={expandedTab === panel.config.id}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
{/* Right section */}
|
|
<div className="space-y-6 overflow-y-auto">
|
|
{rightPanels.map((panel) => (
|
|
<PanelCard
|
|
key={panel.config.id}
|
|
panel={panel}
|
|
onExpand={handleExpand}
|
|
onPin={onTabPin}
|
|
onClose={onTabClose}
|
|
onCustomizationUpdate={onCustomizationUpdate}
|
|
isExpanded={expandedTab === panel.config.id}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Grid mode - responsive grid layout
|
|
const getSizeClass = (size: TabConfig['size']) => {
|
|
switch (size) {
|
|
case 'small':
|
|
return 'col-span-1';
|
|
case 'medium':
|
|
return 'col-span-1 lg:col-span-2';
|
|
case 'large':
|
|
return 'col-span-1 lg:col-span-3';
|
|
case 'full':
|
|
return 'col-span-full';
|
|
default:
|
|
return 'col-span-1';
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className={`grid grid-cols-1 lg:grid-cols-6 gap-6 ${className}`}>
|
|
{visiblePanels.map((panel) => (
|
|
<div
|
|
key={panel.config.id}
|
|
className={`${getSizeClass(panel.config.size)} ${
|
|
expandedTab === panel.config.id ? 'fixed inset-4 z-40' : ''
|
|
}`}
|
|
>
|
|
<PanelCard
|
|
panel={panel}
|
|
onExpand={handleExpand}
|
|
onPin={onTabPin}
|
|
onClose={onTabClose}
|
|
onCustomizationUpdate={onCustomizationUpdate}
|
|
isExpanded={expandedTab === panel.config.id}
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Individual panel card component
|
|
interface PanelCardProps {
|
|
panel: TabPanel;
|
|
onExpand: (tabId: string) => void;
|
|
onPin?: (tabId: string) => void;
|
|
onClose?: (tabId: string) => void;
|
|
onCustomizationUpdate?: (tabId: string, customization: TabCustomization) => void;
|
|
isExpanded: boolean;
|
|
}
|
|
|
|
function PanelCard({ panel, onExpand, onPin, onClose, onCustomizationUpdate, isExpanded }: PanelCardProps) {
|
|
const getAvailableSettings = (tabId: string) => {
|
|
// Define available settings per tab
|
|
switch (tabId) {
|
|
case 'news':
|
|
return {
|
|
autoRefresh: true,
|
|
refreshRate: true,
|
|
filters: {
|
|
sentiment: ['ALL', 'POSITIVE', 'NEGATIVE', 'NEUTRAL'],
|
|
impact: ['ALL', 'HIGH', 'MEDIUM', 'LOW'],
|
|
},
|
|
theme: true,
|
|
};
|
|
case 'alerts':
|
|
return {
|
|
autoRefresh: true,
|
|
refreshRate: true,
|
|
filters: {
|
|
severity: ['ALL', 'CRITICAL', 'HIGH', 'MEDIUM', 'LOW'],
|
|
type: ['ALL', 'PRICE_SPIKE', 'NEWS_BREAKING', 'SUPPORT_BREACH'],
|
|
},
|
|
};
|
|
case 'chart':
|
|
return {
|
|
displayMode: ['candlestick', 'line', 'area'],
|
|
theme: true,
|
|
};
|
|
case 'analytics':
|
|
return {
|
|
displayMode: ['detailed', 'compact', 'charts-only'],
|
|
theme: true,
|
|
};
|
|
case 'daily-checklist':
|
|
return {
|
|
autoRefresh: false,
|
|
displayMode: ['all-phases', 'current-phase-only'],
|
|
theme: true,
|
|
};
|
|
case 'trading-journal':
|
|
return {
|
|
displayMode: ['detailed', 'compact', 'list'],
|
|
filters: {
|
|
emotion: ['all', 'confident', 'neutral', 'anxious'],
|
|
result: ['all', 'winners', 'losers'],
|
|
},
|
|
};
|
|
case 'market-summary':
|
|
return {
|
|
autoRefresh: true,
|
|
refreshRate: true,
|
|
displayMode: ['overview', 'levels', 'events', 'ai'],
|
|
};
|
|
default:
|
|
return { theme: true };
|
|
}
|
|
};
|
|
return (
|
|
<div
|
|
className={`card flex flex-col group ${
|
|
isExpanded ? 'h-full' : 'h-auto'
|
|
}`}
|
|
>
|
|
{/* Panel header */}
|
|
<div className="flex items-center justify-between mb-4 pb-3 border-b border-gray-700">
|
|
<h3 className="text-lg font-semibold flex items-center gap-2">
|
|
{panel.config.pinned && <Pin className="w-4 h-4 text-gold-500" />}
|
|
{panel.config.label}
|
|
</h3>
|
|
<div className="flex items-center gap-1">
|
|
{onCustomizationUpdate && (
|
|
<ComponentSettings
|
|
customization={panel.config.customization}
|
|
onUpdate={(customization) =>
|
|
onCustomizationUpdate(panel.config.id, customization)
|
|
}
|
|
availableSettings={getAvailableSettings(panel.config.id)}
|
|
/>
|
|
)}
|
|
{onPin && (
|
|
<button
|
|
onClick={() => onPin(panel.config.id)}
|
|
className="p-1.5 hover:bg-dark-hover rounded transition-colors"
|
|
title={panel.config.pinned ? 'Unpin' : 'Pin'}
|
|
>
|
|
{panel.config.pinned ? (
|
|
<PinOff className="w-4 h-4" />
|
|
) : (
|
|
<Pin className="w-4 h-4" />
|
|
)}
|
|
</button>
|
|
)}
|
|
<button
|
|
onClick={() => onExpand(panel.config.id)}
|
|
className="p-1.5 hover:bg-dark-hover rounded transition-colors"
|
|
title={isExpanded ? 'Minimize' : 'Expand'}
|
|
>
|
|
{isExpanded ? (
|
|
<Minimize2 className="w-4 h-4" />
|
|
) : (
|
|
<Maximize2 className="w-4 h-4" />
|
|
)}
|
|
</button>
|
|
{onClose && !panel.config.pinned && (
|
|
<button
|
|
onClick={() => onClose(panel.config.id)}
|
|
className="p-1.5 hover:bg-red-500/20 rounded transition-colors"
|
|
title="Close"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Panel content */}
|
|
<div className={`flex-1 ${isExpanded ? 'overflow-y-auto' : ''}`}>
|
|
{panel.content}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|