feat(ui): migrate settings and entries controls
This commit is contained in:
parent
324e34d537
commit
7f5f12509a
@ -5,24 +5,25 @@ import { Pencil, Trash2, Copy as CopyIcon, Search, Plus, Filter, LayoutGrid, Clo
|
|||||||
import type { BotState } from '../hooks/useWebSocket';
|
import type { BotState } from '../hooks/useWebSocket';
|
||||||
import { DEFAULT_BOT_STATE } from '../hooks/useWebSocket';
|
import { DEFAULT_BOT_STATE } from '../hooks/useWebSocket';
|
||||||
import { createManualEntry, deleteManualEntry, fetchManualEntries, type ManualEntryPayload } from '../lib/manualEntriesApi';
|
import { createManualEntry, deleteManualEntry, fetchManualEntries, type ManualEntryPayload } from '../lib/manualEntriesApi';
|
||||||
|
import { Button } from '../components/ui/Primitives';
|
||||||
|
|
||||||
interface Entry {
|
interface Entry {
|
||||||
stock_instance_id: string;
|
stock_instance_id: string;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
user_id: string;
|
user_id: string;
|
||||||
buy_price?: string;
|
buy_price?: string;
|
||||||
sell_price?: string;
|
sell_price?: string;
|
||||||
buy_time?: string;
|
buy_time?: string;
|
||||||
sell_time?: string;
|
sell_time?: string;
|
||||||
quantity?: string;
|
quantity?: string;
|
||||||
filled_quantity?: string;
|
filled_quantity?: string;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
status: string;
|
status: string;
|
||||||
is_crypto: boolean;
|
is_crypto: boolean;
|
||||||
is_real_trade: boolean;
|
is_real_trade: boolean;
|
||||||
label?: string;
|
label?: string;
|
||||||
entry_price?: string;
|
entry_price?: string;
|
||||||
gain_threshold_for_sell?: string;
|
gain_threshold_for_sell?: string;
|
||||||
drop_threshold_for_buy?: string;
|
drop_threshold_for_buy?: string;
|
||||||
workflow_type?: string;
|
workflow_type?: string;
|
||||||
@ -36,80 +37,80 @@ export const filterEntriesByTab = (entries: Entry[], activeTab: string) =>
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
switch (activeTab) {
|
switch (activeTab) {
|
||||||
case 'paperActive':
|
case 'paperActive':
|
||||||
return !entry.is_real_trade && entry.active && entry.status !== 'sellCompleted';
|
return !entry.is_real_trade && entry.active && entry.status !== 'sellCompleted';
|
||||||
case 'paperCompleted':
|
case 'paperCompleted':
|
||||||
return !entry.is_real_trade && entry.status === 'sellCompleted';
|
return !entry.is_real_trade && entry.status === 'sellCompleted';
|
||||||
case 'realActive':
|
case 'realActive':
|
||||||
return entry.is_real_trade && entry.active && entry.status !== 'sellCompleted';
|
return entry.is_real_trade && entry.active && entry.status !== 'sellCompleted';
|
||||||
case 'realCompleted':
|
case 'realCompleted':
|
||||||
return entry.is_real_trade && entry.status === 'sellCompleted';
|
return entry.is_real_trade && entry.status === 'sellCompleted';
|
||||||
case 'inactive':
|
case 'inactive':
|
||||||
return !entry.active;
|
return !entry.active;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const buildClonedEntryPayload = (entry: Entry, generatedId: string) => {
|
export const buildClonedEntryPayload = (entry: Entry, generatedId: string) => {
|
||||||
const rest = { ...entry } as Record<string, unknown>;
|
const rest = { ...entry } as Record<string, unknown>;
|
||||||
delete rest.stock_instance_id;
|
delete rest.stock_instance_id;
|
||||||
delete rest.id;
|
delete rest.id;
|
||||||
delete rest.created_at;
|
delete rest.created_at;
|
||||||
delete rest.updated_at;
|
delete rest.updated_at;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...rest,
|
...rest,
|
||||||
stock_instance_id: generatedId,
|
stock_instance_id: generatedId,
|
||||||
active: false,
|
active: false,
|
||||||
status: 'active'
|
status: 'active'
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
interface EntriesTabProps {
|
interface EntriesTabProps {
|
||||||
botState?: BotState;
|
botState?: BotState;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ENTRY_STATE_MAP = [
|
const ENTRY_STATE_MAP = [
|
||||||
{ label: 'LOCKED', test: (entry: Entry) => entry.status?.toLowerCase().includes('lock') || entry.active },
|
{ label: 'LOCKED', test: (entry: Entry) => entry.status?.toLowerCase().includes('lock') || entry.active },
|
||||||
{ label: 'BLOCKED', test: (entry: Entry) => ['blocked', 'cooldown', 'waiting'].some(term => entry.status?.toLowerCase().includes(term)) },
|
{ label: 'BLOCKED', test: (entry: Entry) => ['blocked', 'cooldown', 'waiting'].some(term => entry.status?.toLowerCase().includes(term)) },
|
||||||
{ label: 'ORPHAN', test: (entry: Entry) => ['orphan', 'stale', 'reconcile'].some(term => entry.status?.toLowerCase().includes(term)) },
|
{ label: 'ORPHAN', test: (entry: Entry) => ['orphan', 'stale', 'reconcile'].some(term => entry.status?.toLowerCase().includes(term)) },
|
||||||
];
|
];
|
||||||
|
|
||||||
const deriveEntryState = (entry: Entry, botState: BotState): string => {
|
const deriveEntryState = (entry: Entry, botState: BotState): string => {
|
||||||
const orderMatch = botState.orders.find(o => o.symbol === entry.symbol);
|
const orderMatch = botState.orders.find(o => o.symbol === entry.symbol);
|
||||||
const status = (entry.status || '').toLowerCase();
|
const status = (entry.status || '').toLowerCase();
|
||||||
const orderStatus = (orderMatch?.status || '').toLowerCase();
|
const orderStatus = (orderMatch?.status || '').toLowerCase();
|
||||||
|
|
||||||
if (ENTRY_STATE_MAP[0].test(entry)) return 'LOCKED';
|
if (ENTRY_STATE_MAP[0].test(entry)) return 'LOCKED';
|
||||||
if (ENTRY_STATE_MAP[1].test(entry)) return 'BLOCKED';
|
if (ENTRY_STATE_MAP[1].test(entry)) return 'BLOCKED';
|
||||||
if (orderStatus.includes('pending') || orderStatus.includes('submitted') || status.includes('submitted')) return 'SUBMITTED';
|
if (orderStatus.includes('pending') || orderStatus.includes('submitted') || status.includes('submitted')) return 'SUBMITTED';
|
||||||
if (orderStatus.includes('filled') || orderStatus.includes('confirmed') || status.includes('filled') || status.includes('confirmed')) return 'CONFIRMED';
|
if (orderStatus.includes('filled') || orderStatus.includes('confirmed') || status.includes('filled') || status.includes('confirmed')) return 'CONFIRMED';
|
||||||
if (ENTRY_STATE_MAP[2].test(entry)) return 'ORPHAN';
|
if (ENTRY_STATE_MAP[2].test(entry)) return 'ORPHAN';
|
||||||
return 'Unknown';
|
return 'Unknown';
|
||||||
};
|
};
|
||||||
|
|
||||||
const NAV_TABS = [
|
const NAV_TABS = [
|
||||||
{ id: "paperActive", label: "Paper Active", icon: <Activity size={14} /> },
|
{ id: "paperActive", label: "Paper Active", icon: <Activity size={14} /> },
|
||||||
{ id: "realActive", label: "Real Execution", icon: <ShieldCheck size={14} /> },
|
{ id: "realActive", label: "Real Execution", icon: <ShieldCheck size={14} /> },
|
||||||
{ id: "paperCompleted", label: "Paper Archive", icon: <Clock size={14} /> },
|
{ id: "paperCompleted", label: "Paper Archive", icon: <Clock size={14} /> },
|
||||||
{ id: "realCompleted", label: "Real Archive", icon: <ShieldCheck size={14} /> },
|
{ id: "realCompleted", label: "Real Archive", icon: <ShieldCheck size={14} /> },
|
||||||
{ id: "inactive", label: "Suspended", icon: <Filter size={14} /> }
|
{ id: "inactive", label: "Suspended", icon: <Filter size={14} /> }
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const tabClass = (isActive: boolean) =>
|
const tabClass = (isActive: boolean) =>
|
||||||
`px-6 py-3 rounded-[1.5rem] text-[10px] font-black uppercase tracking-widest transition-all flex items-center gap-3 whitespace-nowrap ${isActive ? "bg-white text-black shadow-xl" : "text-gray-500 hover:text-gray-300"}`;
|
`px-6 py-3 rounded-[1.5rem] text-[10px] font-black uppercase tracking-widest transition-all flex items-center gap-3 whitespace-nowrap ${isActive ? "bg-white text-black shadow-xl" : "text-gray-500 hover:text-gray-300"}`;
|
||||||
|
|
||||||
export const EntriesTab = ({ botState = DEFAULT_BOT_STATE }: EntriesTabProps) => {
|
export const EntriesTab = ({ botState = DEFAULT_BOT_STATE }: EntriesTabProps) => {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const [entries, setEntries] = useState<Entry[]>([]);
|
const [entries, setEntries] = useState<Entry[]>([]);
|
||||||
const [editingEntry, setEditingEntry] = useState<Entry | null>(null);
|
const [editingEntry, setEditingEntry] = useState<Entry | null>(null);
|
||||||
const [activeTab, setActiveTab] = useState("paperActive");
|
const [activeTab, setActiveTab] = useState("paperActive");
|
||||||
const [isAdding, setIsAdding] = useState(false);
|
const [isAdding, setIsAdding] = useState(false);
|
||||||
|
|
||||||
// Filter Logic
|
// Filter Logic
|
||||||
const filteredEntries = filterEntriesByTab(entries, activeTab);
|
const filteredEntries = filterEntriesByTab(entries, activeTab);
|
||||||
|
|
||||||
const fetchEntries = async () => {
|
const fetchEntries = async () => {
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
try {
|
try {
|
||||||
@ -118,13 +119,13 @@ export const EntriesTab = ({ botState = DEFAULT_BOT_STATE }: EntriesTabProps) =>
|
|||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
console.error("Error fetching entries:", message);
|
console.error("Error fetching entries:", message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchEntries();
|
fetchEntries();
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
const handleDelete = async (id: string) => {
|
const handleDelete = async (id: string) => {
|
||||||
if (!confirm('⚠️ CRITICAL: Permanently delete this entry from neural watchlist?')) return;
|
if (!confirm('⚠️ CRITICAL: Permanently delete this entry from neural watchlist?')) return;
|
||||||
await deleteManualEntry(id);
|
await deleteManualEntry(id);
|
||||||
@ -135,159 +136,163 @@ export const EntriesTab = ({ botState = DEFAULT_BOT_STATE }: EntriesTabProps) =>
|
|||||||
await createManualEntry(buildClonedEntryPayload(entry, crypto.randomUUID()) as unknown as ManualEntryPayload);
|
await createManualEntry(buildClonedEntryPayload(entry, crypto.randomUUID()) as unknown as ManualEntryPayload);
|
||||||
fetchEntries();
|
fetchEntries();
|
||||||
};
|
};
|
||||||
|
|
||||||
const entryCards = filteredEntries.map((entry) => {
|
const entryCards = filteredEntries.map((entry) => {
|
||||||
const entryState = deriveEntryState(entry, botState);
|
const entryState = deriveEntryState(entry, botState);
|
||||||
return (
|
return (
|
||||||
<div key={entry.stock_instance_id} className="group relative rounded-[2.5rem] p-1 transition-all duration-500 bg-white/[0.01] hover:bg-gradient-to-br hover:from-green-500/10 hover:to-transparent">
|
<div key={entry.stock_instance_id} className="group relative rounded-[2.5rem] p-1 transition-all duration-500 bg-white/[0.01] hover:bg-gradient-to-br hover:from-green-500/10 hover:to-transparent">
|
||||||
<div className="bg-[#0a0b0d] border border-white/5 rounded-[2.4rem] p-8 h-full flex flex-col transition-all group-hover:border-white/10 group-hover:shadow-2xl">
|
<div className="bg-[#0a0b0d] border border-white/5 rounded-[2.4rem] p-8 h-full flex flex-col transition-all group-hover:border-white/10 group-hover:shadow-2xl">
|
||||||
|
|
||||||
{/* Card Top */}
|
{/* Card Top */}
|
||||||
<div className="flex items-start justify-between mb-8">
|
<div className="flex items-start justify-between mb-8">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h4 className="text-2xl font-black text-white tracking-tight group-hover:text-green-400 transition-colors uppercase">{entry.symbol}</h4>
|
<h4 className="text-2xl font-black text-white tracking-tight group-hover:text-green-400 transition-colors uppercase">{entry.symbol}</h4>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className={`px-2 py-0.5 rounded text-[9px] font-black tracking-widest uppercase ${entry.is_real_trade ? 'bg-blue-500/20 text-blue-400' : 'bg-purple-500/20 text-purple-400'}`}>
|
<span className={`px-2 py-0.5 rounded text-[9px] font-black tracking-widest uppercase ${entry.is_real_trade ? 'bg-blue-500/20 text-blue-400' : 'bg-purple-500/20 text-purple-400'}`}>
|
||||||
{entry.is_real_trade ? 'REAL-UNIT' : 'PAPER-VOID'}
|
{entry.is_real_trade ? 'REAL-UNIT' : 'PAPER-VOID'}
|
||||||
</span>
|
</span>
|
||||||
{entry.label && (
|
{entry.label && (
|
||||||
<span className="px-2 py-0.5 bg-white/5 rounded text-[9px] font-black text-gray-500 uppercase tracking-widest border border-white/5">
|
<span className="px-2 py-0.5 bg-white/5 rounded text-[9px] font-black text-gray-500 uppercase tracking-widest border border-white/5">
|
||||||
{entry.label}
|
{entry.label}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 opacity-0 group-hover:opacity-100 transition-all translate-y-2 group-hover:translate-y-0">
|
<div className="flex gap-2 opacity-0 group-hover:opacity-100 transition-all translate-y-2 group-hover:translate-y-0">
|
||||||
<button onClick={() => setEditingEntry(entry)} className="p-3 bg-white/5 rounded-2xl text-gray-400 hover:text-white hover:bg-white/10 transition-all"><Pencil size={16} /></button>
|
<Button type="button" onClick={() => setEditingEntry(entry)} variant="ghost" className="p-3 bg-white/5 rounded-2xl text-gray-400 hover:text-white hover:bg-white/10 transition-all"><Pencil size={16} /></Button>
|
||||||
<button onClick={() => handleClone(entry)} className="p-3 bg-white/5 rounded-2xl text-gray-400 hover:text-green-400 hover:bg-green-500/10 transition-all"><CopyIcon size={16} /></button>
|
<Button type="button" onClick={() => handleClone(entry)} variant="ghost" className="p-3 bg-white/5 rounded-2xl text-gray-400 hover:text-green-400 hover:bg-green-500/10 transition-all"><CopyIcon size={16} /></Button>
|
||||||
<button onClick={() => handleDelete(entry.stock_instance_id)} className="p-3 bg-red-500/10 rounded-2xl text-red-500 hover:bg-red-500/20 transition-all"><Trash2 size={16} /></button>
|
<Button type="button" onClick={() => handleDelete(entry.stock_instance_id)} variant="ghost" className="p-3 bg-red-500/10 rounded-2xl text-red-500 hover:bg-red-500/20 transition-all"><Trash2 size={16} /></Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Metrics Grid */}
|
{/* Metrics Grid */}
|
||||||
<div className="grid grid-cols-2 gap-4 mb-8">
|
<div className="grid grid-cols-2 gap-4 mb-8">
|
||||||
<div className="bg-white/[0.02] p-5 rounded-3xl border border-white/5">
|
<div className="bg-white/[0.02] p-5 rounded-3xl border border-white/5">
|
||||||
<span className="block text-[8px] text-gray-600 font-black uppercase tracking-[0.2em] mb-2">Entry Price</span>
|
<span className="block text-[8px] text-gray-600 font-black uppercase tracking-[0.2em] mb-2">Entry Price</span>
|
||||||
<span className="text-xl font-black text-white font-mono">${entry.buy_price || '0.00'}</span>
|
<span className="text-xl font-black text-white font-mono">${entry.buy_price || '0.00'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white/[0.02] p-5 rounded-3xl border border-white/5">
|
<div className="bg-white/[0.02] p-5 rounded-3xl border border-white/5">
|
||||||
<span className="block text-[8px] text-gray-600 font-black uppercase tracking-[0.2em] mb-2">Target Exit</span>
|
<span className="block text-[8px] text-gray-600 font-black uppercase tracking-[0.2em] mb-2">Target Exit</span>
|
||||||
<span className="text-xl font-black text-white font-mono">${entry.sell_price || '---'}</span>
|
<span className="text-xl font-black text-white font-mono">${entry.sell_price || '---'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white/[0.02] p-4 rounded-3xl border border-white/5">
|
<div className="bg-white/[0.02] p-4 rounded-3xl border border-white/5">
|
||||||
<span className="block text-[8px] text-gray-600 font-black uppercase tracking-[0.2em] mb-1">Quantity/Lot</span>
|
<span className="block text-[8px] text-gray-600 font-black uppercase tracking-[0.2em] mb-1">Quantity/Lot</span>
|
||||||
<span className="text-xs font-black text-gray-400 font-mono">{entry.quantity} UNITS</span>
|
<span className="text-xs font-black text-gray-400 font-mono">{entry.quantity} UNITS</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white/[0.02] p-4 rounded-3xl border border-white/5">
|
<div className="bg-white/[0.02] p-4 rounded-3xl border border-white/5">
|
||||||
<span className="block text-[8px] text-gray-600 font-black uppercase tracking-[0.2em] mb-1">Status</span>
|
<span className="block text-[8px] text-gray-600 font-black uppercase tracking-[0.2em] mb-1">Status</span>
|
||||||
<span className={`text-[10px] font-black tracking-widest uppercase ${entry.active ? 'text-green-400 animate-pulse' : 'text-gray-600'}`}>
|
<span className={`text-[10px] font-black tracking-widest uppercase ${entry.active ? 'text-green-400 animate-pulse' : 'text-gray-600'}`}>
|
||||||
{entry.active ? 'SCANNING' : 'INACTIVE'}
|
{entry.active ? 'SCANNING' : 'INACTIVE'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="entry-state-row mb-6">
|
<div className="entry-state-row mb-6">
|
||||||
<span className="text-[9px] text-gray-500 font-black uppercase tracking-[0.2em]">Entry State</span>
|
<span className="text-[9px] text-gray-500 font-black uppercase tracking-[0.2em]">Entry State</span>
|
||||||
<span className={`entry-state-pill state-${entryState.toLowerCase().replace(/[^a-z]/g, '')}`}>
|
<span className={`entry-state-pill state-${entryState.toLowerCase().replace(/[^a-z]/g, '')}`}>
|
||||||
{entryState}
|
{entryState}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Entry Notes/Context */}
|
{/* Entry Notes/Context */}
|
||||||
<div className="flex-grow">
|
<div className="flex-grow">
|
||||||
<span className="block text-[8px] text-gray-600 font-black uppercase tracking-[0.2em] mb-3">Neural Context</span>
|
<span className="block text-[8px] text-gray-600 font-black uppercase tracking-[0.2em] mb-3">Neural Context</span>
|
||||||
<p className="text-[10px] font-bold text-gray-500 leading-relaxed uppercase italic">
|
<p className="text-[10px] font-bold text-gray-500 leading-relaxed uppercase italic">
|
||||||
{entry.notes || 'No manual notes injection provided for this asset cluster.'}
|
{entry.notes || 'No manual notes injection provided for this asset cluster.'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ID Context */}
|
{/* ID Context */}
|
||||||
<div className="mt-8 pt-6 border-t border-white/5 flex items-center justify-between">
|
<div className="mt-8 pt-6 border-t border-white/5 flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-6 h-6 rounded-full bg-white/5 flex items-center justify-center text-[10px] text-gray-600 font-black">
|
<div className="w-6 h-6 rounded-full bg-white/5 flex items-center justify-center text-[10px] text-gray-600 font-black">
|
||||||
ID
|
ID
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[8px] text-gray-700 font-mono select-all truncate max-w-[150px]">{entry.stock_instance_id}</span>
|
<span className="text-[8px] text-gray-700 font-mono select-all truncate max-w-[150px]">{entry.stock_instance_id}</span>
|
||||||
</div>
|
</div>
|
||||||
<ShieldCheck size={14} className="text-gray-800" />
|
<ShieldCheck size={14} className="text-gray-800" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const navTabs = NAV_TABS;
|
const navTabs = NAV_TABS;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="entries-tab space-y-10 animate-in fade-in duration-500">
|
<div className="entries-tab space-y-10 animate-in fade-in duration-500">
|
||||||
|
|
||||||
{/* 1. HEADER & GLOBAL ACTIONS */}
|
{/* 1. HEADER & GLOBAL ACTIONS */}
|
||||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 px-4">
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 px-4">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="w-14 h-14 bg-green-500/10 border border-green-500/20 rounded-2xl flex items-center justify-center text-green-400 shadow-2xl">
|
<div className="w-14 h-14 bg-green-500/10 border border-green-500/20 rounded-2xl flex items-center justify-center text-green-400 shadow-2xl">
|
||||||
<Search size={30} />
|
<Search size={30} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-3xl font-black text-white tracking-tighter uppercase">Watchlist & Entries</h2>
|
<h2 className="text-3xl font-black text-white tracking-tighter uppercase">Watchlist & Entries</h2>
|
||||||
<p className="text-gray-500 text-[10px] font-black uppercase tracking-[0.2em] opacity-60">Neural Opportunity Cluster</p>
|
<p className="text-gray-500 text-[10px] font-black uppercase tracking-[0.2em] opacity-60">Neural Opportunity Cluster</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
onClick={() => { setIsAdding(!isAdding); setEditingEntry(null); }}
|
type="button"
|
||||||
className={`flex items-center gap-3 px-8 py-4 rounded-[2rem] text-xs font-black uppercase tracking-widest transition-all ${isAdding ? 'bg-red-500/10 text-red-400 border border-red-500/20' : 'bg-white text-black shadow-2xl hover:scale-105'
|
onClick={() => { setIsAdding(!isAdding); setEditingEntry(null); }}
|
||||||
}`}
|
variant="ghost"
|
||||||
>
|
className={`flex items-center gap-3 px-8 py-4 rounded-[2rem] text-xs font-black uppercase tracking-widest transition-all ${isAdding ? 'bg-red-500/10 text-red-400 border border-red-500/20' : 'bg-white text-black shadow-2xl hover:scale-105'
|
||||||
{isAdding ? <X size={18} /> : <Plus size={18} />}
|
}`}
|
||||||
{isAdding ? 'Cancel Entry' : 'Manual Entry Injection'}
|
>
|
||||||
</button>
|
{isAdding ? <X size={18} /> : <Plus size={18} />}
|
||||||
</div>
|
{isAdding ? 'Cancel Entry' : 'Manual Entry Injection'}
|
||||||
|
</Button>
|
||||||
{/* 2. FORM DRAWER (IF ADDING/EDITING) */}
|
</div>
|
||||||
{(isAdding || editingEntry) && (
|
|
||||||
<div className="bg-[#0a0b0d] border border-white/10 rounded-[3rem] p-10 shadow-2xl animate-in zoom-in-95 duration-300">
|
{/* 2. FORM DRAWER (IF ADDING/EDITING) */}
|
||||||
<div className="flex items-center justify-between mb-8">
|
{(isAdding || editingEntry) && (
|
||||||
<h3 className="text-lg font-black text-white uppercase tracking-tight flex items-center gap-3">
|
<div className="bg-[#0a0b0d] border border-white/10 rounded-[3rem] p-10 shadow-2xl animate-in zoom-in-95 duration-300">
|
||||||
<Plus size={20} className="text-green-400" />
|
<div className="flex items-center justify-between mb-8">
|
||||||
{editingEntry ? `Update Identity: ${editingEntry.symbol}` : 'New Opportunity Initialization'}
|
<h3 className="text-lg font-black text-white uppercase tracking-tight flex items-center gap-3">
|
||||||
</h3>
|
<Plus size={20} className="text-green-400" />
|
||||||
<button onClick={() => { setIsAdding(false); setEditingEntry(null); }} className="text-gray-500 hover:text-white"><X size={20} /></button>
|
{editingEntry ? `Update Identity: ${editingEntry.symbol}` : 'New Opportunity Initialization'}
|
||||||
</div>
|
</h3>
|
||||||
<div className="max-w-4xl">
|
<Button type="button" onClick={() => { setIsAdding(false); setEditingEntry(null); }} variant="ghost" className="text-gray-500 hover:text-white"><X size={20} /></Button>
|
||||||
<EntryForm
|
</div>
|
||||||
onSuccess={() => { fetchEntries(); setEditingEntry(null); setIsAdding(false); }}
|
<div className="max-w-4xl">
|
||||||
initialData={editingEntry}
|
<EntryForm
|
||||||
/>
|
onSuccess={() => { fetchEntries(); setEditingEntry(null); setIsAdding(false); }}
|
||||||
</div>
|
initialData={editingEntry}
|
||||||
</div>
|
/>
|
||||||
)}
|
</div>
|
||||||
|
</div>
|
||||||
{/* 3. NAVIGATION CLUSTERS */}
|
)}
|
||||||
|
|
||||||
|
{/* 3. NAVIGATION CLUSTERS */}
|
||||||
<div className="flex items-center p-1.5 bg-black/40 border border-white/5 rounded-[2rem] self-start shadow-2xl overflow-x-auto no-scrollbar max-w-full">
|
<div className="flex items-center p-1.5 bg-black/40 border border-white/5 rounded-[2rem] self-start shadow-2xl overflow-x-auto no-scrollbar max-w-full">
|
||||||
{navTabs.map((tab) => (
|
{navTabs.map((tab) => (
|
||||||
<button
|
<Button
|
||||||
key={tab.id}
|
type="button"
|
||||||
onClick={() => setActiveTab(tab.id)}
|
key={tab.id}
|
||||||
className={tabClass(activeTab === tab.id)}
|
onClick={() => setActiveTab(tab.id)}
|
||||||
>
|
variant="ghost"
|
||||||
{tab.icon} {tab.label}
|
className={tabClass(activeTab === tab.id)}
|
||||||
</button>
|
>
|
||||||
))}
|
{tab.icon} {tab.label}
|
||||||
</div>
|
</Button>
|
||||||
{/* 4. CARDS GRID */}
|
))}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 gap-8">
|
</div>
|
||||||
{entryCards}
|
{/* 4. CARDS GRID */}
|
||||||
{filteredEntries.length === 0 && (
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 gap-8">
|
||||||
<div className="col-span-full py-32 text-center bg-white/[0.02] border border-dashed border-white/5 rounded-[3rem]">
|
{entryCards}
|
||||||
<LayoutGrid size={48} className="mx-auto mb-6 text-gray-800 opacity-40" />
|
{filteredEntries.length === 0 && (
|
||||||
<p className="text-sm font-black text-gray-700 uppercase tracking-[0.3em] opacity-40">Cluster Context Null</p>
|
<div className="col-span-full py-32 text-center bg-white/[0.02] border border-dashed border-white/5 rounded-[3rem]">
|
||||||
</div>
|
<LayoutGrid size={48} className="mx-auto mb-6 text-gray-800 opacity-40" />
|
||||||
)}
|
<p className="text-sm font-black text-gray-700 uppercase tracking-[0.3em] opacity-40">Cluster Context Null</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
);
|
</div>
|
||||||
};
|
</div>
|
||||||
|
);
|
||||||
const X = ({ size }: { size: number }) => (
|
};
|
||||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg>
|
|
||||||
|
const X = ({ size }: { size: number }) => (
|
||||||
|
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
|
|||||||
import { useAuth } from '../components/AuthContext';
|
import { useAuth } from '../components/AuthContext';
|
||||||
import type { BotState } from '../hooks/useWebSocket';
|
import type { BotState } from '../hooks/useWebSocket';
|
||||||
import { updateCurrentUserProfile } from '../lib/profileApi';
|
import { updateCurrentUserProfile } from '../lib/profileApi';
|
||||||
|
import { Button, Input } from '../components/ui/Primitives';
|
||||||
|
|
||||||
interface SettingsTabProps {
|
interface SettingsTabProps {
|
||||||
botState: BotState;
|
botState: BotState;
|
||||||
@ -129,13 +130,13 @@ export const SettingsTab = ({ botState }: SettingsTabProps) => {
|
|||||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '20px' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '20px' }}>
|
||||||
<h3>User Configuration</h3>
|
<h3>User Configuration</h3>
|
||||||
{!editing ? (
|
{!editing ? (
|
||||||
<button onClick={() => setEditing(true)} className="primary-button">Edit Configuration</button>
|
<Button type="button" onClick={() => setEditing(true)} className="primary-button">Edit Configuration</Button>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ display: 'flex', gap: '10px' }}>
|
<div style={{ display: 'flex', gap: '10px' }}>
|
||||||
<button onClick={() => setEditing(false)} className="secondary-button" disabled={saving}>Cancel</button>
|
<Button type="button" onClick={() => setEditing(false)} className="secondary-button" disabled={saving}>Cancel</Button>
|
||||||
<button onClick={handleSave} className="primary-button" disabled={saving}>
|
<Button type="button" onClick={handleSave} className="primary-button" disabled={saving}>
|
||||||
{saving ? 'Saving...' : 'Save Changes'}
|
{saving ? 'Saving...' : 'Save Changes'}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -143,7 +144,7 @@ export const SettingsTab = ({ botState }: SettingsTabProps) => {
|
|||||||
<div className="form-grid">
|
<div className="form-grid">
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>First Name</label>
|
<label>First Name</label>
|
||||||
<input
|
<Input
|
||||||
name="first_name"
|
name="first_name"
|
||||||
value={formData.first_name}
|
value={formData.first_name}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
@ -152,7 +153,7 @@ export const SettingsTab = ({ botState }: SettingsTabProps) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>Last Name</label>
|
<label>Last Name</label>
|
||||||
<input
|
<Input
|
||||||
name="last_name"
|
name="last_name"
|
||||||
value={formData.last_name}
|
value={formData.last_name}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
@ -162,7 +163,7 @@ export const SettingsTab = ({ botState }: SettingsTabProps) => {
|
|||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>Your Role</label>
|
<label>Your Role</label>
|
||||||
<input
|
<Input
|
||||||
value={profile?.role || 'user'}
|
value={profile?.role || 'user'}
|
||||||
disabled={true}
|
disabled={true}
|
||||||
style={{ background: 'rgba(255, 255, 255, 0.02)', color: '#888', fontStyle: 'italic' }}
|
style={{ background: 'rgba(255, 255, 255, 0.02)', color: '#888', fontStyle: 'italic' }}
|
||||||
@ -172,7 +173,7 @@ export const SettingsTab = ({ botState }: SettingsTabProps) => {
|
|||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>Trading Enabled</label>
|
<label>Trading Enabled</label>
|
||||||
<div className="toggle-wrapper">
|
<div className="toggle-wrapper">
|
||||||
<input
|
<Input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name="trade_enable"
|
name="trade_enable"
|
||||||
checked={formData.trade_enable}
|
checked={formData.trade_enable}
|
||||||
@ -188,7 +189,7 @@ export const SettingsTab = ({ botState }: SettingsTabProps) => {
|
|||||||
{/* Threshold Settings */}
|
{/* Threshold Settings */}
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>Buy Threshold (e.g. 0.02 for 2%)</label>
|
<label>Buy Threshold (e.g. 0.02 for 2%)</label>
|
||||||
<input
|
<Input
|
||||||
name="drop_threshold_for_buy"
|
name="drop_threshold_for_buy"
|
||||||
value={formData.drop_threshold_for_buy}
|
value={formData.drop_threshold_for_buy}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
@ -198,7 +199,7 @@ export const SettingsTab = ({ botState }: SettingsTabProps) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>Sell Threshold (e.g. 0.01 for 1%)</label>
|
<label>Sell Threshold (e.g. 0.01 for 1%)</label>
|
||||||
<input
|
<Input
|
||||||
name="gain_threshold_for_sell"
|
name="gain_threshold_for_sell"
|
||||||
value={formData.gain_threshold_for_sell}
|
value={formData.gain_threshold_for_sell}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
@ -209,7 +210,7 @@ export const SettingsTab = ({ botState }: SettingsTabProps) => {
|
|||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>Market Poll Interval (ms)</label>
|
<label>Market Poll Interval (ms)</label>
|
||||||
<input
|
<Input
|
||||||
name="market_poll_interval_in_seconds"
|
name="market_poll_interval_in_seconds"
|
||||||
value={formData.market_poll_interval_in_seconds}
|
value={formData.market_poll_interval_in_seconds}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
@ -225,7 +226,7 @@ export const SettingsTab = ({ botState }: SettingsTabProps) => {
|
|||||||
|
|
||||||
<div className="form-group full-width">
|
<div className="form-group full-width">
|
||||||
<label>Financial Modeling Prep API Key</label>
|
<label>Financial Modeling Prep API Key</label>
|
||||||
<input
|
<Input
|
||||||
name="FMP_API_KEY"
|
name="FMP_API_KEY"
|
||||||
value={formData.FMP_API_KEY}
|
value={formData.FMP_API_KEY}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
@ -255,7 +256,7 @@ export const SettingsTab = ({ botState }: SettingsTabProps) => {
|
|||||||
|
|
||||||
<div className="form-group full-width">
|
<div className="form-group full-width">
|
||||||
<label>Paper ALPACA API Key</label>
|
<label>Paper ALPACA API Key</label>
|
||||||
<input
|
<Input
|
||||||
name="ALPACA_API_KEY"
|
name="ALPACA_API_KEY"
|
||||||
value={formData.ALPACA_API_KEY}
|
value={formData.ALPACA_API_KEY}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
@ -265,7 +266,7 @@ export const SettingsTab = ({ botState }: SettingsTabProps) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="form-group full-width">
|
<div className="form-group full-width">
|
||||||
<label>Paper ALPACA Secret Key</label>
|
<label>Paper ALPACA Secret Key</label>
|
||||||
<input
|
<Input
|
||||||
name="ALPACA_SECRET_KEY"
|
name="ALPACA_SECRET_KEY"
|
||||||
value={formData.ALPACA_SECRET_KEY}
|
value={formData.ALPACA_SECRET_KEY}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
@ -276,7 +277,7 @@ export const SettingsTab = ({ botState }: SettingsTabProps) => {
|
|||||||
|
|
||||||
<div className="form-group full-width">
|
<div className="form-group full-width">
|
||||||
<label>Real ALPACA API Key</label>
|
<label>Real ALPACA API Key</label>
|
||||||
<input
|
<Input
|
||||||
name="REAL_ALPACA_API_KEY"
|
name="REAL_ALPACA_API_KEY"
|
||||||
value={formData.REAL_ALPACA_API_KEY}
|
value={formData.REAL_ALPACA_API_KEY}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
@ -286,7 +287,7 @@ export const SettingsTab = ({ botState }: SettingsTabProps) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="form-group full-width">
|
<div className="form-group full-width">
|
||||||
<label>Real ALPACA Secret Key</label>
|
<label>Real ALPACA Secret Key</label>
|
||||||
<input
|
<Input
|
||||||
name="REAL_ALPACA_SECRET_KEY"
|
name="REAL_ALPACA_SECRET_KEY"
|
||||||
value={formData.REAL_ALPACA_SECRET_KEY}
|
value={formData.REAL_ALPACA_SECRET_KEY}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user