"use client"; import { useState, useEffect, useCallback, useRef } from "react"; import { TvAdvancedChart } from "./TvChart"; // ── Constantes ──────────────────────────────────────────────────────────────── const INTERVALS: { v: string; l: string }[] = [ { v: "60", l: "1h" }, { v: "240", l: "4h" }, { v: "D", l: "1J" }, { v: "W", l: "1S" }, ]; const QUICK_SYMBOLS = [ { label: "EUR/USD", value: "FX:EURUSD" }, { label: "GBP/USD", value: "FX:GBPUSD" }, { label: "USD/JPY", value: "FX:USDJPY" }, { label: "AUD/USD", value: "FX:AUDUSD" }, { label: "NZD/USD", value: "FX:NZDUSD" }, { label: "USD/CAD", value: "FX:USDCAD" }, { label: "USD/CHF", value: "FX:USDCHF" }, { label: "EUR/GBP", value: "FX:EURGBP" }, { label: "EUR/JPY", value: "FX:EURJPY" }, { label: "GBP/JPY", value: "FX:GBPJPY" }, { label: "S&P 500", value: "FOREXCOM:SPXUSD" }, { label: "DXY", value: "CAPITALCOM:DXY" }, { label: "Or", value: "TVC:GOLD" }, { label: "VIX", value: "PEPPERSTONE:VIX" }, { label: "BTC/USD", value: "BINANCE:BTCUSDT" }, ]; // ── Types ───────────────────────────────────────────────────────────────────── interface SlotState { symbol: string; interval: string; title: string; notes: string; // HTML (images inline en base64) } interface Archive { id: string; savedAt: string; slot: SlotState; } // ── LocalStorage helpers ────────────────────────────────────────────────────── const LS_SLOTS = "ideas_slots_v1"; const LS_ARCHIVES = "ideas_archives_v1"; function loadLS(key: string, fb: T): T { if (typeof window === "undefined") return fb; try { const r = localStorage.getItem(key); return r ? JSON.parse(r) as T : fb; } catch { return fb; } } function saveLS(key: string, val: unknown) { try { localStorage.setItem(key, JSON.stringify(val)); } catch {} } const DEFAULT_SLOT = (symbol = "FX:EURUSD"): SlotState => ({ symbol, interval: "240", title: "", notes: "", }); // ── Toolbar de formatage ────────────────────────────────────────────────────── const TOOLBAR_GROUPS = [ [ { cmd: "bold", label: "B", cls: "font-bold", title: "Gras (Ctrl+B)" }, { cmd: "italic", label: "I", cls: "italic", title: "Italique (Ctrl+I)" }, { cmd: "underline", label: "U", cls: "underline", title: "Souligné (Ctrl+U)" }, ], [ { cmd: "insertUnorderedList", label: "•", cls: "", title: "Liste à puces" }, { cmd: "insertOrderedList", label: "1.", cls: "", title: "Liste numérotée" }, ], [ { cmd: "justifyLeft", label: "⬱", cls: "", title: "Aligner à gauche" }, { cmd: "justifyCenter", label: "≡", cls: "", title: "Centrer" }, { cmd: "justifyRight", label: "⬰", cls: "", title: "Aligner à droite" }, ], ]; function FormatToolbar({ editorRef, onSave, selImg, onResizeImg, onDeleteImg }: { editorRef: React.RefObject; onSave: () => void; selImg: HTMLImageElement | null; onResizeImg: (pct: number) => void; onDeleteImg: () => void; }) { const exec = (cmd: string) => { editorRef.current?.focus(); document.execCommand(cmd, false); onSave(); }; return (
{TOOLBAR_GROUPS.map((group, gi) => (
{gi > 0 &&
} {group.map(b => ( ))}
))} {/* Toolbar image si sélectionnée */} {selImg && ( <>
Image : {[25, 40, 60, 80, 100].map(p => ( ))} )}
); } // ── RichEditor ──────────────────────────────────────────────────────────────── function RichEditor({ html, onChange, className, style, showToolbar = true, }: { html: string; onChange: (h: string) => void; className?: string; style?: React.CSSProperties; showToolbar?: boolean; }) { const ref = useRef(null); const skipRef = useRef(false); const [selImg, setSelImg] = useState(null); useEffect(() => { if (!ref.current || ref.current.innerHTML === html) return; skipRef.current = true; ref.current.innerHTML = html; }, [html]); const save = useCallback(() => { if (skipRef.current) { skipRef.current = false; return; } onChange(ref.current?.innerHTML ?? ""); }, [onChange]); const handlePaste = useCallback((e: React.ClipboardEvent) => { const imgItem = Array.from(e.clipboardData.items).find(i => i.type.startsWith("image/")); if (!imgItem) return; e.preventDefault(); const file = imgItem.getAsFile(); if (!file) return; const reader = new FileReader(); reader.onload = ev => { const dataUrl = ev.target?.result as string; const img = document.createElement("img"); img.src = dataUrl; img.style.width = "60%"; img.style.maxWidth = "100%"; img.style.borderRadius = "6px"; img.style.display = "block"; img.style.margin = "6px 0"; img.style.cursor = "pointer"; img.draggable = false; const br = document.createElement("br"); const sel = window.getSelection(); if (sel?.rangeCount) { const range = sel.getRangeAt(0); range.collapse(false); range.insertNode(br); range.insertNode(img); range.setStartAfter(br); sel.removeAllRanges(); sel.addRange(range); } else { ref.current?.appendChild(img); ref.current?.appendChild(br); } onChange(ref.current?.innerHTML ?? ""); }; reader.readAsDataURL(file); }, [onChange]); const handleResizeImg = (pct: number) => { if (!selImg) return; selImg.style.width = `${pct}%`; onChange(ref.current?.innerHTML ?? ""); }; const handleDeleteImg = () => { if (!selImg) return; selImg.remove(); setSelImg(null); onChange(ref.current?.innerHTML ?? ""); }; return (
{showToolbar && ( )}
{ const t = e.target as HTMLElement; setSelImg(t.tagName === "IMG" ? t as HTMLImageElement : null); }} className={className} style={{ lineHeight: 1.7, ...style }} />
); } // ── NotePane ────────────────────────────────────────────────────────────────── function NotePane({ slot, onChange }: { slot: SlotState; onChange: (s: SlotState) => void }) { const [expanded, setExpanded] = useState(false); const editorCls = "flex-1 bg-slate-800/30 border border-slate-700/30 rounded-lg p-3 text-[11px] text-slate-300 outline-none focus:border-slate-600 transition-all overflow-y-auto min-h-0"; return ( <> {expanded && (
setExpanded(false)}>
e.stopPropagation()}>
onChange({ ...slot, title: e.target.value })} />
onChange({ ...slot, notes })} className="flex-1 bg-slate-800/40 border border-slate-700/30 rounded-xl p-4 text-[12px] text-slate-200 outline-none overflow-y-auto" />
)}
onChange({ ...slot, title: e.target.value })} /> onChange({ ...slot, notes })} className={editorCls} style={{ minHeight: 120 }} />
); } // ── SymbolPicker ────────────────────────────────────────────────────────────── function SymbolPicker({ value, onChange }: { value: string; onChange: (v: string) => void }) { const [input, setInput] = useState(value); const [open, setOpen] = useState(false); const ref = useRef(null); useEffect(() => { setInput(value); }, [value]); useEffect(() => { function onClickOutside(e: MouseEvent) { if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); } document.addEventListener("mousedown", onClickOutside); return () => document.removeEventListener("mousedown", onClickOutside); }, []); const apply = () => { const s = input.trim().toUpperCase(); if (!s) return; onChange(s.includes(":") ? s : `FX:${s}`); setOpen(false); }; const filtered = QUICK_SYMBOLS.filter(s => s.label.toLowerCase().includes(input.toLowerCase()) || s.value.toLowerCase().includes(input.toLowerCase()) ); return (
{ setInput(e.target.value); setOpen(true); }} onKeyDown={e => e.key === "Enter" && apply()} onFocus={() => setOpen(true)} placeholder="EURUSD, FX:GBPUSD…" /> {open && filtered.length > 0 && (
{filtered.map(s => ( ))}
)}
); } // ── ResearchSlot ────────────────────────────────────────────────────────────── function ResearchSlot({ slot, label, onChange, onArchive, }: { slot: SlotState; label: string; onChange: (s: SlotState) => void; onArchive: () => void; }) { return (
{/* Toolbar */}
{label} onChange({ ...slot, symbol })} /> {/* Interval */}
{INTERVALS.map(iv => ( ))}
{/* Archive */}
{/* Chart + Notes */}
{/* Chart */}
{/* Notes */}
); } // ── ArchiveCard ─────────────────────────────────────────────────────────────── function ArchiveCard({ a, onDelete, onRestore }: { a: Archive; onDelete: () => void; onRestore: (slot: 0 | 1) => void; }) { const [expanded, setExpanded] = useState(false); const [restoring, setRestoring] = useState(false); const date = new Date(a.savedAt).toLocaleDateString("fr-FR", { day: "2-digit", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit", }); const intervalLabel: Record = { "1": "1m", "5": "5m", "15": "15m", "30": "30m", "60": "1H", "120": "2H", "240": "4H", "D": "1J", "W": "1S", }; return ( <> {/* Modal plein écran */} {expanded && (
setExpanded(false)}>
e.stopPropagation()}>
{a.slot.symbol} {intervalLabel[a.slot.interval] ?? a.slot.interval} {a.slot.title && {a.slot.title}} {date}
)} {/* Carte compacte */}
{/* Header */}
{a.slot.symbol} {intervalLabel[a.slot.interval] ?? a.slot.interval} {a.slot.title && ( {a.slot.title} )} {date} {/* Restaurer */}
{restoring && (

Charger dans :

)}
{/* Corps : texte + images */} {a.slot.notes && (
{/* Texte brut (sans tags HTML) */} {(() => { const doc = new DOMParser().parseFromString(a.slot.notes, "text/html"); const text = doc.body.textContent?.trim() ?? ""; const imgs = Array.from(doc.images); return ( <> {text && (

{text}

)} {imgs.length > 0 && (
{imgs.map((img, i) => ( setExpanded(true)} /> ))}
)} ); })()}
)}
); } // ── Archives panel ──────────────────────────────────────────────────────────── function ArchivesPanel({ archives, onDelete, onRestore }: { archives: Archive[]; onDelete: (id: string) => void; onRestore: (id: string, slot: 0 | 1) => void; }) { if (!archives.length) return null; return (
Archives {archives.length}
{archives.map(a => ( onDelete(a.id)} onRestore={slot => onRestore(a.id, slot)} /> ))}
); } // ── IdeesTab (export) ───────────────────────────────────────────────────────── export default function IdeesTab() { const [slots, setSlots] = useState<[SlotState, SlotState]>([ DEFAULT_SLOT("FX:EURUSD"), DEFAULT_SLOT("FX:GBPUSD"), ]); const [archives, setArchives] = useState([]); useEffect(() => { setSlots(loadLS<[SlotState, SlotState]>(LS_SLOTS, [DEFAULT_SLOT("FX:EURUSD"), DEFAULT_SLOT("FX:GBPUSD")])); setArchives(loadLS(LS_ARCHIVES, [])); }, []); const updateSlot = useCallback((idx: 0 | 1, s: SlotState) => { setSlots(prev => { const next: [SlotState, SlotState] = [prev[0], prev[1]]; next[idx] = s; saveLS(LS_SLOTS, next); return next; }); }, []); const archiveSlot = useCallback((idx: 0 | 1) => { const entry: Archive = { id: Date.now().toString(), savedAt: new Date().toISOString(), slot: slots[idx] }; const next = [entry, ...archives]; setArchives(next); saveLS(LS_ARCHIVES, next); const reset = DEFAULT_SLOT(idx === 0 ? "FX:EURUSD" : "FX:GBPUSD"); setSlots(prev => { const n: [SlotState, SlotState] = [prev[0], prev[1]]; n[idx] = reset; saveLS(LS_SLOTS, n); return n; }); }, [slots, archives]); const deleteArchive = useCallback((id: string) => { const next = archives.filter(a => a.id !== id); setArchives(next); saveLS(LS_ARCHIVES, next); }, [archives]); const restoreArchive = useCallback((id: string, slotIdx: 0 | 1) => { const entry = archives.find(a => a.id === id); if (!entry) return; setSlots(prev => { const next = [...prev] as [SlotState, SlotState]; next[slotIdx] = { ...entry.slot }; saveLS(LS_SLOTS, next); return next; }); }, [archives]); return (
{/* Header */}
Espace Idées · 2 recherches
updateSlot(0, s)} onArchive={() => archiveSlot(0)} /> updateSlot(1, s)} onArchive={() => archiveSlot(1)} />
); }