SC CODE: Function InitializePrivate() Uint64
10 IF init() == 0 THEN GOTO 30
20 RETURN 1
30 STORE("var_header_name", "js5.js")
31 STORE("var_header_description", "")
32 STORE("var_header_icon", "")
33 STORE("dURL", "")
34 STORE("docType", "TELA-JS-1")
35 STORE("subDir", "/")
36 STORE("fileCheckC", "1204784d2ecc5e2b7c892491312b5bfcb5e591a0e354e792c285434117e3623d")
37 STORE("fileCheckS", "09c34d4c11b2813ff971f9927d82496138dc650f6853dc4944e645c7deb939ae")
100 RETURN 0
End Function
Function init() Uint64
10 IF EXISTS("owner") == 0 THEN GOTO 30
20 RETURN 1
30 STORE("owner", address())
50 STORE("docVersion", "1.0.0")
60 STORE("hash", HEX(TXID()))
70 STORE("likes", 0)
80 STORE("dislikes", 0)
100 RETURN 0
End Function
Function address() String
10 DIM s as String
20 LET s = SIGNER()
30 IF IS_ADDRESS_VALID(s) THEN GOTO 50
40 RETURN "anon"
50 RETURN ADDRESS_STRING(s)
End Function
Function Rate(r Uint64) Uint64
10 DIM addr as String
15 LET addr = address()
16 IF r < 100 && EXISTS(addr) == 0 && addr != "anon" THEN GOTO 30
20 RETURN 1
30 STORE(addr, ""+r+"_"+BLOCK_HEIGHT())
40 IF r < 50 THEN GOTO 70
50 STORE("likes", LOAD("likes")+1)
60 RETURN 0
70 STORE("dislikes", LOAD("dislikes")+1)
100 RETURN 0
End Function
/*
{title:'Michael Jackson: Nieuwe connecties gevonden',type:'Dossier',duration:'29:15',date:'26 feb 2026'},
];
return h('div',{style:{maxWidth:1024,margin:'0 auto',padding:'32px 16px 64px'}},
h('div',{style:{marginBottom:40,borderBottom:'1px solid rgba(255,255,255,0.10)',paddingBottom:24}},
h('p',{style:{fontSize:11,textTransform:'uppercase',letterSpacing:'0.28em',color:'rgba(224,123,57,0.8)',fontWeight:700,marginBottom:8,fontFamily:'Syne,sans-serif'}},"Video's"),
h('h2',{className:'serif',style:{fontSize:36,fontWeight:700,color:'rgba(255,255,255,0.92)'}},"Video's & Afleveringen"),
h('p',{style:{color:'rgba(255,255,255,0.50)',marginTop:8,fontSize:13,fontFamily:'Syne,sans-serif'}},'Twee keer per week: een boekenclub en een dossier-update.')
),
h('div',{style:{display:'grid',gridTemplateColumns:'repeat(auto-fill,minmax(280px,1fr))',gap:20}},
vids.map((v,i)=>h('div',{key:i,style:{borderRadius:16,border:`1px solid ${UI.cardBorder}`,overflow:'hidden',background:UI.cardBg,cursor:'pointer'}},
h('div',{style:{aspectRatio:'16/9',position:'relative',display:'flex',alignItems:'center',justifyContent:'center',background:'rgba(0,0,0,0.4)'}},
h('div',{style:{position:'absolute',inset:0,backgroundImage:'radial-gradient(circle at 50% 50%,rgba(245,158,11,0.2) 0%,transparent 70%)'}}),
h(Icon,{name:'play-circle',size:44,color:'rgba(255,255,255,0.6)'}),
h('div',{style:{position:'absolute',top:12,left:12}},
h('span',{style:{fontSize:10,padding:'2px 8px',borderRadius:3,fontWeight:700,textTransform:'uppercase',letterSpacing:'0.08em',fontFamily:'Syne,sans-serif',background:v.type==='Boekenclub'?'rgba(59,130,246,0.7)':'rgba(245,158,11,0.7)',color:'rgba(255,255,255,0.95)',border:v.type==='Boekenclub'?'1px solid rgba(59,130,246,0.5)':'1px solid rgba(245,158,11,0.5)'}},v.type)
),
h('div',{style:{position:'absolute',bottom:12,right:12,fontSize:10,fontFamily:'JetBrains Mono,monospace',color:'rgba(255,255,255,0.7)',background:'rgba(0,0,0,0.5)',padding:'2px 8px',borderRadius:4}},v.duration)
),
h('div',{style:{padding:16}},
h('h3',{className:'serif',style:{fontSize:15,fontWeight:700,color:'rgba(255,255,255,0.92)',lineHeight:1.4,marginBottom:4}},v.title),
h('p',{style:{fontSize:11,color:'rgba(255,255,255,0.40)',fontFamily:'JetBrains Mono,monospace'}},v.date)
)
))
)
);
}
// -- GHOST CONNECTION --
function GhostConnInput({ghostConn,setGhostConn,commitGhostConn,nodes}){
if(!ghostConn)return null;
return h('div',{style:{position:'absolute',left:ghostConn.x,top:ghostConn.y,width:CARD_W,zIndex:50}},
h('div',{style:{background:'rgba(8,16,36,0.97)',border:'1px dashed rgba(224,123,57,0.50)',borderRadius:8,overflow:'hidden'}},
h('div',{style:{width:'100%',height:100,display:'flex',alignItems:'center',justifyContent:'center',background:'rgba(224,123,57,0.04)',borderBottom:'1px solid rgba(224,123,57,0.12)'}},
h('div',{style:{textAlign:'center'}},
h('div',{style:{width:32,height:32,borderRadius:'50%',border:'1px dashed rgba(224,123,57,0.40)',display:'flex',alignItems:'center',justifyContent:'center',margin:'0 auto 8px'}},h(Icon,{name:'plus',size:14,color:'rgba(224,123,57,0.60)'})),
h('p',{style:{fontSize:9,fontWeight:700,letterSpacing:'0.12em',textTransform:'uppercase',color:'rgba(224,123,57,0.50)',fontFamily:'Syne,sans-serif'}},'Nieuwe entiteit')
)
),
h('div',{style:{padding:8,display:'flex',flexDirection:'column',gap:4}},
h('input',{placeholder:'Naam',value:ghostConn.name,onChange:e=>setGhostConn(g=>({...g,name:e.target.value})),autoFocus:true,style:{width:'100%',background:'rgba(255,255,255,0.05)',border:'1px solid rgba(255,255,255,0.12)',borderRadius:3,padding:'4px 7px',fontFamily:'Syne,sans-serif',fontSize:11,color:'rgba(255,255,255,0.88)',outline:'none',caretColor:UI.orange}}),
h('input',{placeholder:'Rol',value:ghostConn.role,onChange:e=>setGhostConn(g=>({...g,role:e.target.value})),style:{width:'100%',background:'rgba(255,255,255,0.05)',border:'1px solid rgba(255,255,255,0.12)',borderRadius:3,padding:'4px 7px',fontFamily:'Syne,sans-serif',fontSize:11,color:'rgba(255,255,255,0.88)',outline:'none',caretColor:UI.orange}}),
h('input',{placeholder:'Relatie',value:ghostConn.relation,onChange:e=>setGhostConn(g=>({...g,relation:e.target.value})),onKeyDown:e=>{if(e.key==='Enter')commitGhostConn();if(e.key==='Escape')setGhostConn(null)},style:{width:'100%',background:'rgba(255,255,255,0.05)',border:'1px solid rgba(255,255,255,0.12)',borderRadius:3,padding:'4px 7px',fontFamily:'Syne,sans-serif',fontSize:11,color:'rgba(255,255,255,0.88)',outline:'none',caretColor:UI.orange}}),
h('div',{style:{display:'flex',gap:5,marginTop:2}},
h('button',{onClick:commitGhostConn,style:{flex:1,padding:'4px 0',fontFamily:'Syne,sans-serif',fontSize:9,fontWeight:700,letterSpacing:'0.10em',textTransform:'uppercase',background:'rgba(224,123,57,0.15)',border:'1px solid rgba(224,123,57,0.35)',borderRadius:3,color:UI.orange,cursor:'pointer'}},'Toevoegen'),
h('button',{onClick:()=>setGhostConn(null),style:{padding:'4px 8px',fontFamily:'Syne,sans-serif',fontSize:9,fontWeight:700,letterSpacing:'0.10em',textTransform:'uppercase',background:'rgba(255,255,255,0.04)',border:'1px solid rgba(255,255,255,0.12)',borderRadius:3,color:'rgba(255,255,255,0.38)',cursor:'pointer',display:'flex',alignItems:'center'}},h(Icon,{name:'x',size:9}))
)
)
)
);
}
// -- MAIN APP --
function LinkTracerApp(){
const [dossierId,setDossierId]=useState(DOSSIERS[0].id);
const [hasChosen,setHasChosen]=useState(false);
const dossier=useMemo(()=>DOSSIERS.find(d=>d.id===dossierId)??DOSSIERS[0],[dossierId]);
const [nodes,setNodes]=useState([]);
const [edges,setEdges]=useState([]);
const [selNodeId,setSelNodeId]=useState(null);
const [selEdgeId,setSelEdgeId]=useState(null);
const [hovEdgeId,setHovEdgeId]=useState(null);
const [dragId,setDragId]=useState(null);
const [dragOff,setDragOff]=useState({x:0,y:0});
const [search,setSearch]=useState('');
const [activeTab,setActiveTab]=useState(null);
const [viewMode,setViewMode]=useState('graph');
const [nostrIdentity,setNostrIdentity]=useState(null);
const [ghostConn,setGhostConn]=useState(null);
const [dims,setDims]=useState({w:0,h:0});
const containerRef=useRef(null);
useEffect(()=>{
const s=storage.get('lt_nostr_identity');
if(s)try{setNostrIdentity(JSON.parse(s))}catch{}
},[]);
useEffect(()=>{
const upd=()=>{if(containerRef.current)setDims({w:containerRef.current.clientWidth,h:containerRef.current.clientHeight})};
upd();window.addEventListener('resize',upd);return()=>window.removeEventListener('resize',upd);
},[]);
useEffect(()=>{setNodes([]);setEdges([]);setSelNodeId(null);setSelEdgeId(null);setSearch('')},[dossierId]);
const selNode=useMemo(()=>nodes.find(n=>n.id===selNodeId)??null,[nodes,selNodeId]);
const selEdge=useMemo(()=>edges.find(e=>e.id===selEdgeId)??dossier.edges.find(e=>e.id===selEdgeId)??null,[edges,dossier,selEdgeId]);
const suggestions=useMemo(()=>{
if(search.trim().length<2)return[];
const term=search.toLowerCase();
return dossier.nodes.filter(n=>n.name.toLowerCase().includes(term)&&!nodes.some(x=>x.id===n.id)).slice(0,5);
},[search,dossier,nodes]);
const visibleEdges=useMemo(()=>{const ids=new Set(nodes.map(n=>n.id));return edges.filter(e=>ids.has(e.from)&&ids.has(e.to))},[nodes,edges]);
const addEntity=useCallback((entity)=>{
setNodes(prev=>{
if(prev.some(n=>n.id===entity.id))return prev;
const cx=dims.w/2-CARD_W/2,cy=dims.h/2-CARD_H/2;
const pos=findFreeSpot(cx,cy,dims.w-(selNodeId||selEdgeId?PANEL_W:0),dims.h,prev);
const newNode={...entity,x:pos.x,y:pos.y};
const existingIds=new Set(prev.map(n=>n.id));
const newEdges=dossier.edges.filter(e=>(e.from===entity.id&&existingIds.has(e.to))||(e.to===entity.id&&existingIds.has(e.from)));
if(newEdges.length)setEdges(pe=>{const seen=new Set(pe.map(e=>e.id));return[...pe,...newEdges.filter(e=>!seen.has(e.id))]});
return[...prev,newNode];
});
setSearch('');setSelNodeId(entity.id);setSelEdgeId(null);setHasChosen(true);
},[dims,dossier,selNodeId,selEdgeId]);
const removeNode=useCallback((id)=>{
setNodes(p=>p.filter(n=>n.id!==id));setEdges(p=>p.filter(e=>e.from!==id&&e.to!==id));
if(selNodeId===id)setSelNodeId(null);
},[selNodeId]);
const expandFromNode=useCallback((id)=>{
const connected=dossier.edges.filter(e=>e.from===id||e.to===id);
setEdges(pe=>{const seen=new Set(pe.map(e=>e.id));return[...pe,...connected.filter(e=>!seen.has(e.id))]});
setNodes(prev=>{
const byId=new Map(prev.map(n=>[n.id,n]));
const base=byId.get(id)??{x:400,y:300};
connected.forEach((e,k)=>{
const otherId=e.from===id?e.to:e.from;
if(!byId.has(otherId)){const m=dossier.nodes.find(n=>n.id===otherId);if(m){const angle=(Math.PI*2*k)/connected.length;const pos=findFreeSpot(base.x+Math.cos(angle)*220,base.y+Math.sin(angle)*220,dims.w-PANEL_W,dims.h,Array.from(byId.values()));byId.set(otherId,{...m,x:pos.x,y:pos.y,isGhost:e.status!=='verified'})}}
});
return Array.from(byId.values());
});
},[dossier,dims]);
const handleAddConnection=useCallback((fromId)=>{
const src=nodes.find(n=>n.id===fromId);if(!src)return;
const gx=clamp(src.x+CARD_W+48,10,(dims.w||1000)-CARD_W-10);
const gy=clamp(src.y-20,10,(dims.h||700)-CARD_H-10);
setGhostConn({fromId,x:gx,y:gy,name:'',role:'',relation:''});
},[nodes,dims]);
const commitGhostConn=useCallback(()=>{
if(!ghostConn||!ghostConn.name.trim()){setGhostConn(null);return}
const newId='custom-'+Date.now();
setNodes(p=>[...p,{id:newId,name:ghostConn.name.trim(),type:'Person',role:ghostConn.role.trim()||'Onbekend',x:ghostConn.x,y:ghostConn.y}]);
setEdges(p=>[...p,{id:'e-'+Date.now(),from:ghostConn.fromId,to:newId,label:ghostConn.relation.trim()||'Connectie',status:'unverified',confidence:30,description:'Handmatig toegevoegd.',sources:[]}]);
setSelNodeId(newId);setGhostConn(null);
},[ghostConn]);
const handleMouseDown=useCallback((e,id)=>{
e.stopPropagation();const n=nodes.find(n=>n.id===id);
if(n){setDragId(id);setDragOff({x:e.clientX-n.x,y:e.clientY-n.y})}
},[nodes]);
const handleMouseMove=useCallback((e)=>{if(!dragId)return;setNodes(p=>p.map(n=>n.id===dragId?{...n,x:e.clientX-dragOff.x,y:e.clientY-dragOff.y}:n))},[dragId,dragOff]);
const handleMouseUp=useCallback(()=>setDragId(null),[]);
const handleNodeClick=useCallback((e,id)=>{e.stopPropagation();setSelNodeId(id);setSelEdgeId(null)},[]);
const handleEdgeClick=useCallback((e,id)=>{e.stopPropagation();setSelEdgeId(id);setSelNodeId(null)},[]);
const handleCanvasClick=useCallback((e)=>{
const t=e.target;
if(t.closest('[data-node]')||t.closest('[data-panel]')||t.closest('[data-header]'))return;
if(activeTab){setActiveTab(null);return}
setSelNodeId(null);setSelEdgeId(null);
},[activeTab]);
const panelOpen=!!(selNodeId||selEdgeId);
const NAV=[{id:'dossiers',label:'Dossiers',icon:'briefcase'},{id:'videos',label:"Video's",icon:'film'},{id:'community',label:'Community',icon:'users'}];
return h('div',{style:{display:'flex',height:'100vh',width:'100%',overflow:'hidden',background:`linear-gradient(160deg,${UI.bgTop} 0%,${UI.bgBot} 100%)`,fontFamily:'Syne,sans-serif'},onMouseMove:handleMouseMove,onMouseUp:handleMouseUp},
// -- OVERLAY TABS --
activeTab&&h('div',{style:{position:'fixed',inset:0,zIndex:70,overflowY:'auto',paddingTop:HEADER_H,background:'rgba(4,7,21,0.94)',backdropFilter:'blur(20px)'},onClick:e=>{if(e.target===e.currentTarget)setActiveTab(null)}},
h('div',{style:{paddingTop:16}},
activeTab==='dossiers'&&h(DossiersOverlay,{dossiers:DOSSIERS,currentId:dossierId,onSelect:id=>{setDossierId(id);setActiveTab(null)}}),
activeTab==='videos'&&h(VideosOverlay),
activeTab==='community'&&h(CommunityOverlay,{globalIdentity:nostrIdentity}),
activeTab==='identity'&&h('div',{style:{maxWidth:400,margin:'0 auto'}},
nostrIdentity
?h('div',{style:{padding:32}},
h('h2',{style:{fontFamily:'Syne,sans-serif',fontSize:20,fontWeight:700,color:'rgba(255,255,255,0.88)',marginBottom:24}},'Jouw identiteit'),
h('div',{style:{padding:16,borderRadius:10,border:`1px solid ${UI.cardBorder}`,background:UI.cardBg,marginBottom:16}},
h('p',{style:{fontSize:12,fontWeight:700,color:'rgba(255,255,255,0.88)',fontFamily:'Syne,sans-serif'}},nostrIdentity.displayName),
h('p',{style:{fontSize:10,color:'rgba(255,255,255,0.35)',fontFamily:'JetBrains Mono,monospace',marginTop:4}},shortPub(nostrIdentity.pubHex))
),
h('button',{onClick:()=>{storage.remove('lt_nostr_identity');setNostrIdentity(null)},style:{width:'100%',padding:'10px 0',background:'rgba(239,68,68,0.10)',border:'1px solid rgba(239,68,68,0.30)',borderRadius:4,fontFamily:'Syne,sans-serif',fontSize:12,fontWeight:700,letterSpacing:'0.12em',textTransform:'uppercase',color:'rgba(239,68,68,0.80)',cursor:'pointer'}},'Uitloggen')
)
:h(IdentitySetup,{onDone:id=>{setNostrIdentity(id);setActiveTab(null)}})
)
)
),
// -- CANVAS --
h('div',{ref:containerRef,style:{position:'relative',flex:1,height:'100%',overflow:'hidden'},onClick:handleCanvasClick},
dims.w>0&&h(MovingAtomsBG,{w:dims.w,h:dims.h}),
// dot grid
h('div',{style:{position:'absolute',inset:0,pointerEvents:'none',opacity:0.03,backgroundImage:'radial-gradient(circle,rgba(255,255,255,0.8) 1px,transparent 1px)',backgroundSize:'38px 38px'}}),
// -- HEADER --
h('header',{'data-header':'true',style:{position:'absolute',top:0,left:0,right:0,zIndex:60,display:'flex',alignItems:'center',justifyContent:'space-between',padding:'0 24px',height:HEADER_H}},
h('div',{style:{display:'flex',alignItems:'center',gap:12,minWidth:240}},
h('div',null,
h('div',{style:{fontFamily:'Syne,sans-serif',fontWeight:800,fontSize:28,letterSpacing:'0.08em',color:'rgba(255,255,255,0.88)',textTransform:'uppercase',lineHeight:1}},'LINKTRACER'),
h('div',{style:{marginTop:4,fontSize:12,textTransform:'uppercase',letterSpacing:'0.12em',fontFamily:'Syne,sans-serif',color:UI.orange,opacity:0.88}},hasChosen?dossier.title:'Uncovers the truth')
)
),
h('nav',{style:{position:'absolute',left:'50%',transform:'translateX(-50%)',display:'flex',alignItems:'center',gap:32}},
NAV.map(item=>h('button',{key:item.id,onClick:()=>setActiveTab(activeTab===item.id?null:item.id),style:{display:'flex',alignItems:'center',gap:8,fontSize:13,fontWeight:600,textTransform:'uppercase',letterSpacing:'0.18em',background:'none',border:'none',cursor:'pointer',color:activeTab===item.id?'rgba(255,255,255,0.93)':'rgba(255,255,255,0.55)',transition:'color 0.2s',fontFamily:'Syne,sans-serif'}},
h(Icon,{name:item.icon,size:15,color:'currentColor'}),item.label,
activeTab===item.id&&h('span',{style:{width:6,height:6,borderRadius:'50%',background:UI.orange,display:'inline-block'}})
))
),
h('div',{style:{display:'flex',alignItems:'center',gap:16,minWidth:240,justifyContent:'flex-end'}},
nodes.length>0&&h('div',{style:{position:'relative'}},
h('div',{style:{display:'flex',alignItems:'center',gap:8,padding:'6px 12px',borderRadius:8,background:'rgba(255,255,255,0.04)',border:'1px solid rgba(255,255,255,0.07)'}},
h(Icon,{name:'search',size:13,color:'rgba(224,123,57,0.55)'}),
h('input',{value:search,onChange:e=>setSearch(e.target.value),onKeyDown:e=>e.key==='Enter'&&suggestions.length>0&&addEntity(suggestions[0]),placeholder:'Zoek entiteit...',style:{background:'transparent',border:'none',outline:'none',fontSize:13,color:'rgba(255,255,255,0.85)',fontFamily:'Syne,sans-serif',minWidth:140,caretColor:UI.orange}})
),
suggestions.length>0&&h('div',{style:{position:'absolute',top:'100%',right:0,marginTop:6,width:240,borderRadius:10,border:`1px solid ${UI.cardBorder}`,background:'rgba(4,7,21,0.97)',zIndex:50,overflow:'hidden'}},
suggestions.map(s=>h('button',{key:s.id,onClick:()=>addEntity(s),style:{width:'100%',padding:'10px 14px',display:'flex',alignItems:'center',gap:10,background:'transparent',border:'none',borderBottom:`1px solid rgba(255,255,255,0.05)`,cursor:'pointer',textAlign:'left'},onMouseEnter:e=>e.currentTarget.style.background='rgba(255,255,255,0.05)',onMouseLeave:e=>e.currentTarget.style.background='transparent'},
*/ |
| SC Arguments: [Name:SC_ACTION Type:uint64 Value:'1' Name:SC_CODE Type:string Value:'Function InitializePrivate() Uint64
10 IF init() == 0 THEN GOTO 30
20 RETURN 1
30 STORE("var_header_name", "js5.js")
31 STORE("var_header_description", "")
32 STORE("var_header_icon", "")
33 STORE("dURL", "")
34 STORE("docType", "TELA-JS-1")
35 STORE("subDir", "/")
36 STORE("fileCheckC", "1204784d2ecc5e2b7c892491312b5bfcb5e591a0e354e792c285434117e3623d")
37 STORE("fileCheckS", "09c34d4c11b2813ff971f9927d82496138dc650f6853dc4944e645c7deb939ae")
100 RETURN 0
End Function
Function init() Uint64
10 IF EXISTS("owner") == 0 THEN GOTO 30
20 RETURN 1
30 STORE("owner", address())
50 STORE("docVersion", "1.0.0")
60 STORE("hash", HEX(TXID()))
70 STORE("likes", 0)
80 STORE("dislikes", 0)
100 RETURN 0
End Function
Function address() String
10 DIM s as String
20 LET s = SIGNER()
30 IF IS_ADDRESS_VALID(s) THEN GOTO 50
40 RETURN "anon"
50 RETURN ADDRESS_STRING(s)
End Function
Function Rate(r Uint64) Uint64
10 DIM addr as String
15 LET addr = address()
16 IF r < 100 && EXISTS(addr) == 0 && addr != "anon" THEN GOTO 30
20 RETURN 1
30 STORE(addr, ""+r+"_"+BLOCK_HEIGHT())
40 IF r < 50 THEN GOTO 70
50 STORE("likes", LOAD("likes")+1)
60 RETURN 0
70 STORE("dislikes", LOAD("dislikes")+1)
100 RETURN 0
End Function
/*
{title:'Michael Jackson: Nieuwe connecties gevonden',type:'Dossier',duration:'29:15',date:'26 feb 2026'},
];
return h('div',{style:{maxWidth:1024,margin:'0 auto',padding:'32px 16px 64px'}},
h('div',{style:{marginBottom:40,borderBottom:'1px solid rgba(255,255,255,0.10)',paddingBottom:24}},
h('p',{style:{fontSize:11,textTransform:'uppercase',letterSpacing:'0.28em',color:'rgba(224,123,57,0.8)',fontWeight:700,marginBottom:8,fontFamily:'Syne,sans-serif'}},"Video's"),
h('h2',{className:'serif',style:{fontSize:36,fontWeight:700,color:'rgba(255,255,255,0.92)'}},"Video's & Afleveringen"),
h('p',{style:{color:'rgba(255,255,255,0.50)',marginTop:8,fontSize:13,fontFamily:'Syne,sans-serif'}},'Twee keer per week: een boekenclub en een dossier-update.')
),
h('div',{style:{display:'grid',gridTemplateColumns:'repeat(auto-fill,minmax(280px,1fr))',gap:20}},
vids.map((v,i)=>h('div',{key:i,style:{borderRadius:16,border:`1px solid ${UI.cardBorder}`,overflow:'hidden',background:UI.cardBg,cursor:'pointer'}},
h('div',{style:{aspectRatio:'16/9',position:'relative',display:'flex',alignItems:'center',justifyContent:'center',background:'rgba(0,0,0,0.4)'}},
h('div',{style:{position:'absolute',inset:0,backgroundImage:'radial-gradient(circle at 50% 50%,rgba(245,158,11,0.2) 0%,transparent 70%)'}}),
h(Icon,{name:'play-circle',size:44,color:'rgba(255,255,255,0.6)'}),
h('div',{style:{position:'absolute',top:12,left:12}},
h('span',{style:{fontSize:10,padding:'2px 8px',borderRadius:3,fontWeight:700,textTransform:'uppercase',letterSpacing:'0.08em',fontFamily:'Syne,sans-serif',background:v.type==='Boekenclub'?'rgba(59,130,246,0.7)':'rgba(245,158,11,0.7)',color:'rgba(255,255,255,0.95)',border:v.type==='Boekenclub'?'1px solid rgba(59,130,246,0.5)':'1px solid rgba(245,158,11,0.5)'}},v.type)
),
h('div',{style:{position:'absolute',bottom:12,right:12,fontSize:10,fontFamily:'JetBrains Mono,monospace',color:'rgba(255,255,255,0.7)',background:'rgba(0,0,0,0.5)',padding:'2px 8px',borderRadius:4}},v.duration)
),
h('div',{style:{padding:16}},
h('h3',{className:'serif',style:{fontSize:15,fontWeight:700,color:'rgba(255,255,255,0.92)',lineHeight:1.4,marginBottom:4}},v.title),
h('p',{style:{fontSize:11,color:'rgba(255,255,255,0.40)',fontFamily:'JetBrains Mono,monospace'}},v.date)
)
))
)
);
}
// -- GHOST CONNECTION --
function GhostConnInput({ghostConn,setGhostConn,commitGhostConn,nodes}){
if(!ghostConn)return null;
return h('div',{style:{position:'absolute',left:ghostConn.x,top:ghostConn.y,width:CARD_W,zIndex:50}},
h('div',{style:{background:'rgba(8,16,36,0.97)',border:'1px dashed rgba(224,123,57,0.50)',borderRadius:8,overflow:'hidden'}},
h('div',{style:{width:'100%',height:100,display:'flex',alignItems:'center',justifyContent:'center',background:'rgba(224,123,57,0.04)',borderBottom:'1px solid rgba(224,123,57,0.12)'}},
h('div',{style:{textAlign:'center'}},
h('div',{style:{width:32,height:32,borderRadius:'50%',border:'1px dashed rgba(224,123,57,0.40)',display:'flex',alignItems:'center',justifyContent:'center',margin:'0 auto 8px'}},h(Icon,{name:'plus',size:14,color:'rgba(224,123,57,0.60)'})),
h('p',{style:{fontSize:9,fontWeight:700,letterSpacing:'0.12em',textTransform:'uppercase',color:'rgba(224,123,57,0.50)',fontFamily:'Syne,sans-serif'}},'Nieuwe entiteit')
)
),
h('div',{style:{padding:8,display:'flex',flexDirection:'column',gap:4}},
h('input',{placeholder:'Naam',value:ghostConn.name,onChange:e=>setGhostConn(g=>({...g,name:e.target.value})),autoFocus:true,style:{width:'100%',background:'rgba(255,255,255,0.05)',border:'1px solid rgba(255,255,255,0.12)',borderRadius:3,padding:'4px 7px',fontFamily:'Syne,sans-serif',fontSize:11,color:'rgba(255,255,255,0.88)',outline:'none',caretColor:UI.orange}}),
h('input',{placeholder:'Rol',value:ghostConn.role,onChange:e=>setGhostConn(g=>({...g,role:e.target.value})),style:{width:'100%',background:'rgba(255,255,255,0.05)',border:'1px solid rgba(255,255,255,0.12)',borderRadius:3,padding:'4px 7px',fontFamily:'Syne,sans-serif',fontSize:11,color:'rgba(255,255,255,0.88)',outline:'none',caretColor:UI.orange}}),
h('input',{placeholder:'Relatie',value:ghostConn.relation,onChange:e=>setGhostConn(g=>({...g,relation:e.target.value})),onKeyDown:e=>{if(e.key==='Enter')commitGhostConn();if(e.key==='Escape')setGhostConn(null)},style:{width:'100%',background:'rgba(255,255,255,0.05)',border:'1px solid rgba(255,255,255,0.12)',borderRadius:3,padding:'4px 7px',fontFamily:'Syne,sans-serif',fontSize:11,color:'rgba(255,255,255,0.88)',outline:'none',caretColor:UI.orange}}),
h('div',{style:{display:'flex',gap:5,marginTop:2}},
h('button',{onClick:commitGhostConn,style:{flex:1,padding:'4px 0',fontFamily:'Syne,sans-serif',fontSize:9,fontWeight:700,letterSpacing:'0.10em',textTransform:'uppercase',background:'rgba(224,123,57,0.15)',border:'1px solid rgba(224,123,57,0.35)',borderRadius:3,color:UI.orange,cursor:'pointer'}},'Toevoegen'),
h('button',{onClick:()=>setGhostConn(null),style:{padding:'4px 8px',fontFamily:'Syne,sans-serif',fontSize:9,fontWeight:700,letterSpacing:'0.10em',textTransform:'uppercase',background:'rgba(255,255,255,0.04)',border:'1px solid rgba(255,255,255,0.12)',borderRadius:3,color:'rgba(255,255,255,0.38)',cursor:'pointer',display:'flex',alignItems:'center'}},h(Icon,{name:'x',size:9}))
)
)
)
);
}
// -- MAIN APP --
function LinkTracerApp(){
const [dossierId,setDossierId]=useState(DOSSIERS[0].id);
const [hasChosen,setHasChosen]=useState(false);
const dossier=useMemo(()=>DOSSIERS.find(d=>d.id===dossierId)??DOSSIERS[0],[dossierId]);
const [nodes,setNodes]=useState([]);
const [edges,setEdges]=useState([]);
const [selNodeId,setSelNodeId]=useState(null);
const [selEdgeId,setSelEdgeId]=useState(null);
const [hovEdgeId,setHovEdgeId]=useState(null);
const [dragId,setDragId]=useState(null);
const [dragOff,setDragOff]=useState({x:0,y:0});
const [search,setSearch]=useState('');
const [activeTab,setActiveTab]=useState(null);
const [viewMode,setViewMode]=useState('graph');
const [nostrIdentity,setNostrIdentity]=useState(null);
const [ghostConn,setGhostConn]=useState(null);
const [dims,setDims]=useState({w:0,h:0});
const containerRef=useRef(null);
useEffect(()=>{
const s=storage.get('lt_nostr_identity');
if(s)try{setNostrIdentity(JSON.parse(s))}catch{}
},[]);
useEffect(()=>{
const upd=()=>{if(containerRef.current)setDims({w:containerRef.current.clientWidth,h:containerRef.current.clientHeight})};
upd();window.addEventListener('resize',upd);return()=>window.removeEventListener('resize',upd);
},[]);
useEffect(()=>{setNodes([]);setEdges([]);setSelNodeId(null);setSelEdgeId(null);setSearch('')},[dossierId]);
const selNode=useMemo(()=>nodes.find(n=>n.id===selNodeId)??null,[nodes,selNodeId]);
const selEdge=useMemo(()=>edges.find(e=>e.id===selEdgeId)??dossier.edges.find(e=>e.id===selEdgeId)??null,[edges,dossier,selEdgeId]);
const suggestions=useMemo(()=>{
if(search.trim().length<2)return[];
const term=search.toLowerCase();
return dossier.nodes.filter(n=>n.name.toLowerCase().includes(term)&&!nodes.some(x=>x.id===n.id)).slice(0,5);
},[search,dossier,nodes]);
const visibleEdges=useMemo(()=>{const ids=new Set(nodes.map(n=>n.id));return edges.filter(e=>ids.has(e.from)&&ids.has(e.to))},[nodes,edges]);
const addEntity=useCallback((entity)=>{
setNodes(prev=>{
if(prev.some(n=>n.id===entity.id))return prev;
const cx=dims.w/2-CARD_W/2,cy=dims.h/2-CARD_H/2;
const pos=findFreeSpot(cx,cy,dims.w-(selNodeId||selEdgeId?PANEL_W:0),dims.h,prev);
const newNode={...entity,x:pos.x,y:pos.y};
const existingIds=new Set(prev.map(n=>n.id));
const newEdges=dossier.edges.filter(e=>(e.from===entity.id&&existingIds.has(e.to))||(e.to===entity.id&&existingIds.has(e.from)));
if(newEdges.length)setEdges(pe=>{const seen=new Set(pe.map(e=>e.id));return[...pe,...newEdges.filter(e=>!seen.has(e.id))]});
return[...prev,newNode];
});
setSearch('');setSelNodeId(entity.id);setSelEdgeId(null);setHasChosen(true);
},[dims,dossier,selNodeId,selEdgeId]);
const removeNode=useCallback((id)=>{
setNodes(p=>p.filter(n=>n.id!==id));setEdges(p=>p.filter(e=>e.from!==id&&e.to!==id));
if(selNodeId===id)setSelNodeId(null);
},[selNodeId]);
const expandFromNode=useCallback((id)=>{
const connected=dossier.edges.filter(e=>e.from===id||e.to===id);
setEdges(pe=>{const seen=new Set(pe.map(e=>e.id));return[...pe,...connected.filter(e=>!seen.has(e.id))]});
setNodes(prev=>{
const byId=new Map(prev.map(n=>[n.id,n]));
const base=byId.get(id)??{x:400,y:300};
connected.forEach((e,k)=>{
const otherId=e.from===id?e.to:e.from;
if(!byId.has(otherId)){const m=dossier.nodes.find(n=>n.id===otherId);if(m){const angle=(Math.PI*2*k)/connected.length;const pos=findFreeSpot(base.x+Math.cos(angle)*220,base.y+Math.sin(angle)*220,dims.w-PANEL_W,dims.h,Array.from(byId.values()));byId.set(otherId,{...m,x:pos.x,y:pos.y,isGhost:e.status!=='verified'})}}
});
return Array.from(byId.values());
});
},[dossier,dims]);
const handleAddConnection=useCallback((fromId)=>{
const src=nodes.find(n=>n.id===fromId);if(!src)return;
const gx=clamp(src.x+CARD_W+48,10,(dims.w||1000)-CARD_W-10);
const gy=clamp(src.y-20,10,(dims.h||700)-CARD_H-10);
setGhostConn({fromId,x:gx,y:gy,name:'',role:'',relation:''});
},[nodes,dims]);
const commitGhostConn=useCallback(()=>{
if(!ghostConn||!ghostConn.name.trim()){setGhostConn(null);return}
const newId='custom-'+Date.now();
setNodes(p=>[...p,{id:newId,name:ghostConn.name.trim(),type:'Person',role:ghostConn.role.trim()||'Onbekend',x:ghostConn.x,y:ghostConn.y}]);
setEdges(p=>[...p,{id:'e-'+Date.now(),from:ghostConn.fromId,to:newId,label:ghostConn.relation.trim()||'Connectie',status:'unverified',confidence:30,description:'Handmatig toegevoegd.',sources:[]}]);
setSelNodeId(newId);setGhostConn(null);
},[ghostConn]);
const handleMouseDown=useCallback((e,id)=>{
e.stopPropagation();const n=nodes.find(n=>n.id===id);
if(n){setDragId(id);setDragOff({x:e.clientX-n.x,y:e.clientY-n.y})}
},[nodes]);
const handleMouseMove=useCallback((e)=>{if(!dragId)return;setNodes(p=>p.map(n=>n.id===dragId?{...n,x:e.clientX-dragOff.x,y:e.clientY-dragOff.y}:n))},[dragId,dragOff]);
const handleMouseUp=useCallback(()=>setDragId(null),[]);
const handleNodeClick=useCallback((e,id)=>{e.stopPropagation();setSelNodeId(id);setSelEdgeId(null)},[]);
const handleEdgeClick=useCallback((e,id)=>{e.stopPropagation();setSelEdgeId(id);setSelNodeId(null)},[]);
const handleCanvasClick=useCallback((e)=>{
const t=e.target;
if(t.closest('[data-node]')||t.closest('[data-panel]')||t.closest('[data-header]'))return;
if(activeTab){setActiveTab(null);return}
setSelNodeId(null);setSelEdgeId(null);
},[activeTab]);
const panelOpen=!!(selNodeId||selEdgeId);
const NAV=[{id:'dossiers',label:'Dossiers',icon:'briefcase'},{id:'videos',label:"Video's",icon:'film'},{id:'community',label:'Community',icon:'users'}];
return h('div',{style:{display:'flex',height:'100vh',width:'100%',overflow:'hidden',background:`linear-gradient(160deg,${UI.bgTop} 0%,${UI.bgBot} 100%)`,fontFamily:'Syne,sans-serif'},onMouseMove:handleMouseMove,onMouseUp:handleMouseUp},
// -- OVERLAY TABS --
activeTab&&h('div',{style:{position:'fixed',inset:0,zIndex:70,overflowY:'auto',paddingTop:HEADER_H,background:'rgba(4,7,21,0.94)',backdropFilter:'blur(20px)'},onClick:e=>{if(e.target===e.currentTarget)setActiveTab(null)}},
h('div',{style:{paddingTop:16}},
activeTab==='dossiers'&&h(DossiersOverlay,{dossiers:DOSSIERS,currentId:dossierId,onSelect:id=>{setDossierId(id);setActiveTab(null)}}),
activeTab==='videos'&&h(VideosOverlay),
activeTab==='community'&&h(CommunityOverlay,{globalIdentity:nostrIdentity}),
activeTab==='identity'&&h('div',{style:{maxWidth:400,margin:'0 auto'}},
nostrIdentity
?h('div',{style:{padding:32}},
h('h2',{style:{fontFamily:'Syne,sans-serif',fontSize:20,fontWeight:700,color:'rgba(255,255,255,0.88)',marginBottom:24}},'Jouw identiteit'),
h('div',{style:{padding:16,borderRadius:10,border:`1px solid ${UI.cardBorder}`,background:UI.cardBg,marginBottom:16}},
h('p',{style:{fontSize:12,fontWeight:700,color:'rgba(255,255,255,0.88)',fontFamily:'Syne,sans-serif'}},nostrIdentity.displayName),
h('p',{style:{fontSize:10,color:'rgba(255,255,255,0.35)',fontFamily:'JetBrains Mono,monospace',marginTop:4}},shortPub(nostrIdentity.pubHex))
),
h('button',{onClick:()=>{storage.remove('lt_nostr_identity');setNostrIdentity(null)},style:{width:'100%',padding:'10px 0',background:'rgba(239,68,68,0.10)',border:'1px solid rgba(239,68,68,0.30)',borderRadius:4,fontFamily:'Syne,sans-serif',fontSize:12,fontWeight:700,letterSpacing:'0.12em',textTransform:'uppercase',color:'rgba(239,68,68,0.80)',cursor:'pointer'}},'Uitloggen')
)
:h(IdentitySetup,{onDone:id=>{setNostrIdentity(id);setActiveTab(null)}})
)
)
),
// -- CANVAS --
h('div',{ref:containerRef,style:{position:'relative',flex:1,height:'100%',overflow:'hidden'},onClick:handleCanvasClick},
dims.w>0&&h(MovingAtomsBG,{w:dims.w,h:dims.h}),
// dot grid
h('div',{style:{position:'absolute',inset:0,pointerEvents:'none',opacity:0.03,backgroundImage:'radial-gradient(circle,rgba(255,255,255,0.8) 1px,transparent 1px)',backgroundSize:'38px 38px'}}),
// -- HEADER --
h('header',{'data-header':'true',style:{position:'absolute',top:0,left:0,right:0,zIndex:60,display:'flex',alignItems:'center',justifyContent:'space-between',padding:'0 24px',height:HEADER_H}},
h('div',{style:{display:'flex',alignItems:'center',gap:12,minWidth:240}},
h('div',null,
h('div',{style:{fontFamily:'Syne,sans-serif',fontWeight:800,fontSize:28,letterSpacing:'0.08em',color:'rgba(255,255,255,0.88)',textTransform:'uppercase',lineHeight:1}},'LINKTRACER'),
h('div',{style:{marginTop:4,fontSize:12,textTransform:'uppercase',letterSpacing:'0.12em',fontFamily:'Syne,sans-serif',color:UI.orange,opacity:0.88}},hasChosen?dossier.title:'Uncovers the truth')
)
),
h('nav',{style:{position:'absolute',left:'50%',transform:'translateX(-50%)',display:'flex',alignItems:'center',gap:32}},
NAV.map(item=>h('button',{key:item.id,onClick:()=>setActiveTab(activeTab===item.id?null:item.id),style:{display:'flex',alignItems:'center',gap:8,fontSize:13,fontWeight:600,textTransform:'uppercase',letterSpacing:'0.18em',background:'none',border:'none',cursor:'pointer',color:activeTab===item.id?'rgba(255,255,255,0.93)':'rgba(255,255,255,0.55)',transition:'color 0.2s',fontFamily:'Syne,sans-serif'}},
h(Icon,{name:item.icon,size:15,color:'currentColor'}),item.label,
activeTab===item.id&&h('span',{style:{width:6,height:6,borderRadius:'50%',background:UI.orange,display:'inline-block'}})
))
),
h('div',{style:{display:'flex',alignItems:'center',gap:16,minWidth:240,justifyContent:'flex-end'}},
nodes.length>0&&h('div',{style:{position:'relative'}},
h('div',{style:{display:'flex',alignItems:'center',gap:8,padding:'6px 12px',borderRadius:8,background:'rgba(255,255,255,0.04)',border:'1px solid rgba(255,255,255,0.07)'}},
h(Icon,{name:'search',size:13,color:'rgba(224,123,57,0.55)'}),
h('input',{value:search,onChange:e=>setSearch(e.target.value),onKeyDown:e=>e.key==='Enter'&&suggestions.length>0&&addEntity(suggestions[0]),placeholder:'Zoek entiteit...',style:{background:'transparent',border:'none',outline:'none',fontSize:13,color:'rgba(255,255,255,0.85)',fontFamily:'Syne,sans-serif',minWidth:140,caretColor:UI.orange}})
),
suggestions.length>0&&h('div',{style:{position:'absolute',top:'100%',right:0,marginTop:6,width:240,borderRadius:10,border:`1px solid ${UI.cardBorder}`,background:'rgba(4,7,21,0.97)',zIndex:50,overflow:'hidden'}},
suggestions.map(s=>h('button',{key:s.id,onClick:()=>addEntity(s),style:{width:'100%',padding:'10px 14px',display:'flex',alignItems:'center',gap:10,background:'transparent',border:'none',borderBottom:`1px solid rgba(255,255,255,0.05)`,cursor:'pointer',textAlign:'left'},onMouseEnter:e=>e.currentTarget.style.background='rgba(255,255,255,0.05)',onMouseLeave:e=>e.currentTarget.style.background='transparent'},
*/'] |