"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): boolean { try { localStorage.setItem(key, JSON.stringify(val)); return true; } catch { return false; } } 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 */} {/* Pas de overflow-hidden ici : le menu déroulant "Restaurer" est en position absolute et serait rogné par un ancêtre overflow-hidden (cf. bug signalé). */}
{/* 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) — les tags de bloc sont convertis en retours à la ligne avant extraction, sinon .textContent colle tous les paragraphes/lignes de liste bout à bout sans séparation. */} {(() => { const withBreaks = a.slot.notes .replace(/<\/(p|div|li|h[1-6])>/gi, "\n") .replace(//gi, "\n"); const doc = new DOMParser().parseFromString(withBreaks, "text/html"); const text = (doc.body.textContent ?? "").replace(/\n{3,}/g, "\n\n").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([]); // Passe à true si une écriture localStorage échoue (ex: quota dépassé à cause // des images en base64 dans les notes archivées) — sans ça l'échec est silencieux // et une suppression/modification peut sembler ne "pas marcher" après rechargement. const [saveError, setSaveError] = useState(false); const persist = useCallback((key: string, val: unknown) => { setSaveError(!saveLS(key, val)); }, []); 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; persist(LS_SLOTS, next); return next; }); }, [persist]); 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); persist(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; persist(LS_SLOTS, n); return n; }); }, [slots, archives, persist]); const deleteArchive = useCallback((id: string) => { const next = archives.filter(a => a.id !== id); setArchives(next); persist(LS_ARCHIVES, next); }, [archives, persist]); 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 }; persist(LS_SLOTS, next); return next; }); }, [archives, persist]); return (
{/* Header */}
Espace Idées · 2 recherches
{saveError && (
⚠ Échec de sauvegarde locale — le quota de stockage du navigateur est probablement dépassé (les archives avec images occupent beaucoup de place). Supprime quelques archives pour libérer de la place, sinon tes changements ne seront pas conservés au rechargement.
)} updateSlot(0, s)} onArchive={() => archiveSlot(0)} /> updateSlot(1, s)} onArchive={() => archiveSlot(1)} />
); }