six-degrees 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/app-path-routes-manifest.json +1 -0
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +35 -7
- package/.next/standalone/.next/required-server-files.json +6 -1
- package/.next/standalone/.next/routes-manifest.json +18 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +11 -11
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +11 -11
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/admin-delete/route.js +1 -1
- package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/admin-update/route.js +1 -1
- package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/bridges/route.js +1 -1
- package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/bulk-import/route.js +1 -1
- package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/connections/route.js +1 -1
- package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/delete-cluster/route.js +1 -1
- package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/ingest/route.js +1 -1
- package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/route.js +1 -1
- package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/notifications/route.js +1 -1
- package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/outreach/route.js +3 -3
- package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/queue/route.js +1 -1
- package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/scraper/route.js +1 -1
- package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/setup-profile/route.js +1 -1
- package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/unlock/route.js +1 -1
- package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/update-images/route.js +1 -1
- package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/users/route.js +1 -1
- package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/import.html +1 -1
- package/.next/standalone/.next/server/app/import.rsc +13 -13
- package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +13 -13
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/launch.html +2 -2
- package/.next/standalone/.next/server/app/launch.rsc +13 -13
- package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/paths.html +1 -1
- package/.next/standalone/.next/server/app/paths.rsc +13 -13
- package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/profile.html +1 -1
- package/.next/standalone/.next/server/app/profile.rsc +13 -13
- package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/queue.html +1 -1
- package/.next/standalone/.next/server/app/queue.rsc +13 -13
- package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/scores/page/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/scores/page/build-manifest.json +20 -0
- package/.next/standalone/.next/server/app/scores/page/next-font-manifest.json +11 -0
- package/.next/standalone/.next/server/app/scores/page/react-loadable-manifest.json +1 -0
- package/.next/standalone/.next/server/app/scores/page/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/scores/page.js +13 -0
- package/.next/standalone/.next/server/app/scores/page.js.map +5 -0
- package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/scores.html +1 -0
- package/.next/standalone/.next/server/app/scores.meta +26 -0
- package/.next/standalone/.next/server/app/scores.rsc +24 -0
- package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +24 -0
- package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +4 -0
- package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +28 -0
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings.html +1 -1
- package/.next/standalone/.next/server/app/settings.rsc +13 -13
- package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/setup.html +1 -1
- package/.next/standalone/.next/server/app/setup.rsc +13 -13
- package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +12 -12
- package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-rnla2._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__00la_y0._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0i5hdu4._.js → [root-of-the-server]__03ydxxh._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__07t47cm._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0bat8nx._.js +8 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cj5jqc._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0e0fei3._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljcioj._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljnyho._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o5-tcg._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0qrnous._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t3v1a1._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0uag-k5._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0um1uey._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0wmexuc._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xu34sz._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__133knx1._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__14ou7j5._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__15-17hl._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__16iu0hv._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__19b7mig._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1bbcwub._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1eris29._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1gjo_cq._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1kbl21f._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qcm08_._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1rphsdk._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1w25q0m._.js +1 -1
- package/.next/standalone/.next/server/chunks/_02twlca._.js +10 -0
- package/.next/standalone/.next/server/chunks/_072jl0y._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0lw8_i0._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1_fynia._.js +8 -6
- package/.next/standalone/.next/server/chunks/_1fn30_l._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1iwkh8t._.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +13 -8
- package/.next/standalone/.next/server/chunks/{lib_rpc_02ndsg2.js → lib_rpc_03i8xef.js} +1 -1
- package/.next/standalone/.next/server/chunks/{lib_rpc_08q5vp6.js → lib_rpc_08fc3rp.js} +1 -1
- package/.next/standalone/.next/server/chunks/{lib_rpc_00tx2sm.js → lib_rpc_08r2t_q.js} +1 -1
- package/.next/standalone/.next/server/chunks/{lib_rpc_02-qa4d.js → lib_rpc_0btpv1t.js} +1 -1
- package/.next/standalone/.next/server/chunks/lib_rpc_0c9d9ti.js +5 -5
- package/.next/standalone/.next/server/chunks/lib_rpc_0cm2ev3.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0iovswz.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0is8flp.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0v8ajtd.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0xgmvvt.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_144mzbf.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1bggi0n.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1dkcu9f.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1o0jb6w.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0an6blh._.js → [root-of-the-server]__0zzcbqg._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1194jps._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1z1j3ek._.js → [root-of-the-server]__19ip1qq._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_01f38ty._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_06w_gl4._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0hw_qnj._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_13kzrwv._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_15xuwkv._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1cyxocj._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1gpbbw9._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1hgd5dp._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1vpfyd-._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_20w06i-._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_scores_page_actions_0x-w58n.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_components_settings_SectorPicker_1c-29qw.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_launch_page_19t0dyd.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +11 -9
- package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +5 -5
- package/.next/standalone/.next/server/chunks/ssr/app_scores_page_09wepvv.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_settings_page_1f8mjj_.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/lib_1bn38gs._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_0x3i8za._.js +131 -0
- package/.next/standalone/.next/server/chunks/ssr/{node_modules_next_dist_1bunoni._.js → node_modules_next_dist_1v8aef8._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_client_components_server-async-storage_0t1dato.js +3 -0
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/middleware-manifest.json +5 -5
- package/.next/standalone/.next/server/next-font-manifest.js +1 -1
- package/.next/standalone/.next/server/next-font-manifest.json +4 -0
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/prefetch-hints.json +14 -0
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/0ax_9zzboyv6d.js +44 -0
- package/.next/standalone/.next/static/chunks/0cuuo2jwc9h94.js +1 -0
- package/.next/standalone/.next/static/chunks/0l1568qff0efj.js +1 -0
- package/.next/standalone/.next/static/chunks/0muwt9bykzr-1.js +1 -0
- package/.next/standalone/.next/static/chunks/0w76iraw7ij3k.js +18 -0
- package/.next/standalone/.next/static/chunks/135n87ydxjrra.js +2 -0
- package/.next/standalone/.next/static/chunks/18xu0fbgo_qz-.js +1 -0
- package/.next/standalone/.next/static/chunks/28bu26ausqzmn.js +1 -0
- package/.next/standalone/.next/static/chunks/{40nvbgfn7-072.css → 2p6o90z0vgcwr.css} +1 -1
- package/.next/standalone/.next/static/chunks/2pnbm1z6j7xab.js +1 -0
- package/.next/standalone/.next/static/chunks/2xjho0lroiwlq.js +1 -0
- package/.next/standalone/.next/static/chunks/3e8zg6ctvvb8p.js +33 -0
- package/.next/standalone/.next/static/chunks/3qh78g2aqhprd.js +2 -0
- package/.next/standalone/CHANGELOG.md +286 -0
- package/.next/standalone/CONTRIBUTING.md +15 -4
- package/.next/standalone/README.md +50 -29
- package/.next/standalone/SECURITY.md +10 -0
- package/.next/standalone/app/api/company-scores/legacy/route.js +1 -1
- package/.next/standalone/app/api/company-scores/route.js +1 -1
- package/.next/standalone/app/api/ingest/route.js +23 -2
- package/.next/standalone/app/api/outreach/route.js +107 -44
- package/.next/standalone/app/api/scraper/route.js +146 -17
- package/.next/standalone/app/api/settings/sector-preview/route.js +3 -2
- package/.next/standalone/app/api/settings/sector-suggestions/route.js +1 -1
- package/.next/standalone/app/api/update-images/route.js +41 -7
- package/.next/standalone/app/avatars/[file]/route.js +6 -1
- package/.next/standalone/app/components/Avatar.js +5 -2
- package/.next/standalone/app/components/ChainView.js +659 -469
- package/.next/standalone/app/components/CompanyScores.js +16 -10
- package/.next/standalone/app/components/EmptyState.js +1 -1
- package/.next/standalone/app/components/FieldStep.js +94 -0
- package/.next/standalone/app/components/ForceGraph.js +183 -768
- package/.next/standalone/app/components/GridView.js +9 -7
- package/.next/standalone/app/components/LegacyScoresCard.js +1 -1
- package/.next/standalone/app/components/LinkedInLimits.js +22 -7
- package/.next/standalone/app/components/ListView.js +6 -4
- package/.next/standalone/app/components/MappingProgress.js +5 -6
- package/.next/standalone/app/components/OnboardingGate.js +1 -1
- package/.next/standalone/app/components/OrbitGraph.js +33 -1
- package/.next/standalone/app/components/OutlinkQuest.js +5 -4
- package/.next/standalone/app/components/PathsAnalyzer.js +1 -1
- package/.next/standalone/app/components/RingsView.js +9 -8
- package/.next/standalone/app/components/SeparationView.js +96 -14
- package/.next/standalone/app/components/Sidebar.js +366 -287
- package/.next/standalone/app/components/TheirCircle.js +300 -0
- package/.next/standalone/app/components/settings/SectorPicker.js +202 -0
- package/.next/standalone/app/components/settings/SectorSection.js +21 -195
- package/.next/standalone/app/components/settings/TierSection.js +99 -0
- package/.next/standalone/app/components/useRequests.js +12 -0
- package/.next/standalone/app/components/useScanner.js +13 -0
- package/.next/standalone/app/components/views.js +3 -5
- package/.next/standalone/app/globals.css +7 -0
- package/.next/standalone/app/launch/page.js +6 -3
- package/.next/standalone/app/page.js +183 -74
- package/.next/standalone/app/paths/page.js +20 -14
- package/.next/standalone/app/profile/page.js +3 -3
- package/.next/standalone/app/queue/page.js +56 -60
- package/.next/standalone/app/scores/page.js +72 -0
- package/.next/standalone/app/settings/page.js +15 -3
- package/.next/standalone/app/setup/page.js +423 -260
- package/.next/standalone/db/schema.js +2 -1
- package/.next/standalone/eslint.config.mjs +3 -3
- package/.next/standalone/install.sh +3 -3
- package/.next/standalone/lib/chain-layout.js +139 -0
- package/.next/standalone/lib/circle.js +169 -0
- package/.next/standalone/lib/companies.js +1 -1
- package/.next/standalone/lib/csv.js +21 -10
- package/.next/standalone/lib/data-export.js +2 -1
- package/.next/standalone/lib/data-folder.js +4 -2
- package/.next/standalone/lib/db-client.js +1 -0
- package/.next/standalone/lib/galaxy.js +12 -0
- package/.next/standalone/lib/ingest.js +27 -8
- package/.next/standalone/lib/legacy-offer.js +7 -6
- package/.next/standalone/lib/legacy-scores.js +2 -2
- package/.next/standalone/lib/linkedin-limits.js +30 -8
- package/.next/standalone/lib/photos.js +47 -0
- package/.next/standalone/lib/quest.js +8 -3
- package/.next/standalone/lib/rarity.js +80 -0
- package/.next/standalone/lib/reach.js +113 -0
- package/.next/standalone/lib/requests-client.js +113 -0
- package/.next/standalone/lib/rpc.js +26 -15
- package/.next/standalone/lib/scan-state.js +1 -0
- package/.next/standalone/lib/scanner-setup.js +20 -1
- package/.next/standalone/lib/score-guess.js +60 -0
- package/.next/standalone/lib/scoring.js +1761 -69
- package/.next/standalone/lib/scraper-client.js +235 -17
- package/.next/standalone/lib/sector-directory.js +1 -1
- package/.next/standalone/lib/sector-focus.js +17 -3
- package/.next/standalone/lib/separation.js +4 -2
- package/.next/standalone/lib/settings-client.js +21 -0
- package/.next/standalone/lib/settings-effects.js +19 -1
- package/.next/standalone/lib/settings.js +7 -2
- package/.next/standalone/next.config.mjs +25 -3
- package/.next/standalone/package-lock.json +2 -2
- package/.next/standalone/package.json +2 -2
- package/.next/standalone/public/demo-data.json +1 -1
- package/.next/standalone/scripts/build-app.mjs +11 -2
- package/.next/standalone/scripts/check-node.mjs +1 -1
- package/.next/standalone/scripts/gen-synthetic.mjs +1 -1
- package/.next/standalone/scripts/image_store.py +77 -21
- package/.next/standalone/scripts/scrape.py +305 -29
- package/.next/standalone/server.js +1 -1
- package/.next/static/chunks/0ax_9zzboyv6d.js +44 -0
- package/.next/static/chunks/0cuuo2jwc9h94.js +1 -0
- package/.next/static/chunks/0l1568qff0efj.js +1 -0
- package/.next/static/chunks/0muwt9bykzr-1.js +1 -0
- package/.next/static/chunks/0w76iraw7ij3k.js +18 -0
- package/.next/static/chunks/135n87ydxjrra.js +2 -0
- package/.next/static/chunks/18xu0fbgo_qz-.js +1 -0
- package/.next/static/chunks/28bu26ausqzmn.js +1 -0
- package/.next/static/chunks/{40nvbgfn7-072.css → 2p6o90z0vgcwr.css} +1 -1
- package/.next/static/chunks/2pnbm1z6j7xab.js +1 -0
- package/.next/static/chunks/2xjho0lroiwlq.js +1 -0
- package/.next/static/chunks/3e8zg6ctvvb8p.js +33 -0
- package/.next/static/chunks/3qh78g2aqhprd.js +2 -0
- package/CHANGELOG.md +286 -0
- package/README.md +50 -29
- package/SECURITY.md +10 -0
- package/db/schema.js +2 -1
- package/lib/chain-layout.js +139 -0
- package/lib/circle.js +169 -0
- package/lib/companies.js +1 -1
- package/lib/csv.js +21 -10
- package/lib/data-export.js +2 -1
- package/lib/data-folder.js +4 -2
- package/lib/db-client.js +1 -0
- package/lib/galaxy.js +12 -0
- package/lib/ingest.js +27 -8
- package/lib/legacy-offer.js +7 -6
- package/lib/legacy-scores.js +2 -2
- package/lib/linkedin-limits.js +30 -8
- package/lib/photos.js +47 -0
- package/lib/quest.js +8 -3
- package/lib/rarity.js +80 -0
- package/lib/reach.js +113 -0
- package/lib/requests-client.js +113 -0
- package/lib/rpc.js +26 -15
- package/lib/scan-state.js +1 -0
- package/lib/scanner-setup.js +20 -1
- package/lib/score-guess.js +60 -0
- package/lib/scoring.js +1761 -69
- package/lib/scraper-client.js +235 -17
- package/lib/sector-directory.js +1 -1
- package/lib/sector-focus.js +17 -3
- package/lib/separation.js +4 -2
- package/lib/settings-client.js +21 -0
- package/lib/settings-effects.js +19 -1
- package/lib/settings.js +7 -2
- package/package.json +2 -2
- package/public/demo-data.json +1 -1
- package/scripts/check-node.mjs +1 -1
- package/scripts/image_store.py +77 -21
- package/scripts/scrape.py +305 -29
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1t3rugi._.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0_8i3ft.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0huvw-3.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0ip2vdh.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0jzisgh.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0yi9xvd.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1jyhd0o.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1kn59-i.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1q2jkza.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1vpm1sk.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1x6p_31.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_07u8byv._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_08r_mlf._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0942v8n._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0hl_t7r._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0tb83dw._.js +0 -132
- package/.next/standalone/.next/server/chunks/ssr/_1-87bs1._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_17gui6a._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/lib_0i8v-s3._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/node_modules_16vay6l._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/node_modules_1kciwn4._.js +0 -3
- package/.next/standalone/.next/static/chunks/06x3jiiz96m5t.js +0 -1
- package/.next/standalone/.next/static/chunks/08k1crid-05dt.js +0 -1
- package/.next/standalone/.next/static/chunks/0ngx9s6-zgshe.js +0 -33
- package/.next/standalone/.next/static/chunks/0q-_khndfxubv.js +0 -2
- package/.next/standalone/.next/static/chunks/1c79veqedvubt.js +0 -1
- package/.next/standalone/.next/static/chunks/1chp3nx45meg5.js +0 -44
- package/.next/standalone/.next/static/chunks/1irqj0lf-fu3d.js +0 -1
- package/.next/standalone/.next/static/chunks/1uadqylrod5_v.js +0 -1
- package/.next/standalone/.next/static/chunks/23u0zclgwpc4d.js +0 -1
- package/.next/standalone/.next/static/chunks/289ktj97di6zg.js +0 -1
- package/.next/standalone/.next/static/chunks/2b3lmf8e6uj8m.js +0 -16
- package/.next/standalone/.next/static/chunks/3npq5yd7c4dmn.js +0 -2
- package/.next/standalone/app/components/BridgeRing.js +0 -753
- package/.next/static/chunks/06x3jiiz96m5t.js +0 -1
- package/.next/static/chunks/08k1crid-05dt.js +0 -1
- package/.next/static/chunks/0ngx9s6-zgshe.js +0 -33
- package/.next/static/chunks/0q-_khndfxubv.js +0 -2
- package/.next/static/chunks/1c79veqedvubt.js +0 -1
- package/.next/static/chunks/1chp3nx45meg5.js +0 -44
- package/.next/static/chunks/1irqj0lf-fu3d.js +0 -1
- package/.next/static/chunks/1uadqylrod5_v.js +0 -1
- package/.next/static/chunks/23u0zclgwpc4d.js +0 -1
- package/.next/static/chunks/289ktj97di6zg.js +0 -1
- package/.next/static/chunks/2b3lmf8e6uj8m.js +0 -16
- package/.next/static/chunks/3npq5yd7c4dmn.js +0 -2
- /package/.next/standalone/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_ssgManifest.js +0 -0
- /package/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_buildManifest.js +0 -0
- /package/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{qqqcxvzBUizkECrT4swyK → WseE_5HuU-1q0ywC9CP3U}/_ssgManifest.js +0 -0
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect, useMemo } from 'react';
|
|
4
|
-
import
|
|
3
|
+
import { useState, useEffect, useMemo, useCallback } from 'react';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import { beginScrape, scraperStatus, notReadyMessage, resumePoint, scansCircleOf, isCircleScan, busyReason } from '../../lib/scraper-client';
|
|
6
|
+
import useScanner from './useScanner';
|
|
7
|
+
import useRequests from './useRequests';
|
|
8
|
+
import TheirCircle from './TheirCircle';
|
|
9
|
+
import { hasRequest, markRequested, undoRequest } from '../../lib/requests-client';
|
|
5
10
|
import { useUser } from './UserProvider';
|
|
6
11
|
import { routeIndex, routesFor } from '../../lib/separation';
|
|
7
12
|
import { topCompanies } from '../../lib/scoring';
|
|
13
|
+
import { reachIndex, reachState, circleState, readyToScan, circleScanCost } from '../../lib/reach';
|
|
8
14
|
import Avatar from './Avatar';
|
|
15
|
+
import { localPhoto } from '../../lib/photos';
|
|
9
16
|
|
|
10
|
-
export default function Sidebar({ selected, stats, tierColors, connections, degree2 = [], mode, collapsed, filter, pending = [], onToggle, onSelect, onSwitchMode, onFocusNode, onMarkSent, onUndoPending }) {
|
|
17
|
+
export default function Sidebar({ selected, stats, tierColors, connections, degree2 = [], mode, collapsed, filter, pending = [], onToggle, onSelect, onSwitchMode, onFocusNode, onMarkSent, onUndoPending, scanNotes, csvSource = null }) {
|
|
11
18
|
const isDegreesMode = mode === 'degrees';
|
|
19
|
+
// 'sample' or 'csv' while the sample or a CSV import is open in this window,
|
|
20
|
+
// null for your own network. Only your own can be scanned: the scanner looks
|
|
21
|
+
// the person up in the database, and these two never reach it.
|
|
22
|
+
const canScan = !csvSource;
|
|
12
23
|
const { userId, userProfile: ctxProfile } = useUser();
|
|
13
24
|
const userProfile = ctxProfile || { name: 'User', sectors: [], goals: [] };
|
|
14
25
|
const [searchQuery, setSearchQuery] = useState('');
|
|
@@ -26,9 +37,13 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
|
|
|
26
37
|
}, [degree2]);
|
|
27
38
|
// Who reaches whom, from the FULL lists this panel is given — so every route
|
|
28
39
|
// shows even while a tier chip is narrowing the view, and a person picked in
|
|
29
|
-
// Chains
|
|
40
|
+
// Chains or Galaxy gets every route too.
|
|
30
41
|
const routeIdx = useMemo(() => routeIndex(degree2), [degree2]);
|
|
31
42
|
const bridgeById = useMemo(() => new Map(connections.map(c => [c.id, c])), [connections]);
|
|
43
|
+
// Who you've asked, shared with every view: a request sent here shows in
|
|
44
|
+
// Separation, the circle and the Outlink queue at once (lib/requests-client.js).
|
|
45
|
+
const requests = useRequests();
|
|
46
|
+
const isRequested = useCallback((row) => hasRequest(row, requests), [requests]);
|
|
32
47
|
|
|
33
48
|
if (selected) {
|
|
34
49
|
const isBridge = selected.degree === 1 && bridgeMap[selected.id];
|
|
@@ -86,9 +101,9 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
|
|
|
86
101
|
</div>
|
|
87
102
|
{/* Profile photo + name */}
|
|
88
103
|
<div style={{ display: 'flex', gap: 12, alignItems: 'center', marginBottom: 16 }}>
|
|
89
|
-
{selected.profile_image_url ? (
|
|
104
|
+
{localPhoto(selected.profile_image_url) ? (
|
|
90
105
|
<img
|
|
91
|
-
src={selected.profile_image_url}
|
|
106
|
+
src={localPhoto(selected.profile_image_url)}
|
|
92
107
|
alt={selected.name}
|
|
93
108
|
style={{ width: 52, height: 52, borderRadius: '50%', objectFit: 'cover', border: `2px solid ${tierColors[selected.tier] || '#555'}`, flexShrink: 0 }}
|
|
94
109
|
onError={(e) => { e.target.style.display = 'none'; e.target.nextSibling.style.display = 'flex'; }}
|
|
@@ -97,7 +112,7 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
|
|
|
97
112
|
<div style={{
|
|
98
113
|
width: 52, height: 52, borderRadius: '50%', flexShrink: 0,
|
|
99
114
|
background: tierColors[selected.tier] || '#555',
|
|
100
|
-
display: selected.profile_image_url ? 'none' : 'flex',
|
|
115
|
+
display: localPhoto(selected.profile_image_url) ? 'none' : 'flex',
|
|
101
116
|
alignItems: 'center', justifyContent: 'center',
|
|
102
117
|
fontSize: 20, fontWeight: 700, color: selected.tier === 'S' ? '#000' : '#fff',
|
|
103
118
|
}}>
|
|
@@ -114,6 +129,16 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
|
|
|
114
129
|
</div>
|
|
115
130
|
</div>
|
|
116
131
|
|
|
132
|
+
{/* Their circle, out to six degrees: who they know, who you added from
|
|
133
|
+
it, and who those people know (TheirCircle.js). Your connections only:
|
|
134
|
+
a 2nd-degree person's circle isn't yours to scan until they accept. */}
|
|
135
|
+
{selected.degree === 1 && (
|
|
136
|
+
<TheirCircle
|
|
137
|
+
person={selected} connections={connections} degree2={degree2}
|
|
138
|
+
tierColors={tierColors} onSelect={onSelect} isRequested={isRequested} canScan={canScan}
|
|
139
|
+
/>
|
|
140
|
+
)}
|
|
141
|
+
|
|
117
142
|
{selected.is_catalyst && (
|
|
118
143
|
<div style={{
|
|
119
144
|
background: 'rgba(0,255,136,0.08)', border: '1px solid rgba(0,255,136,0.3)',
|
|
@@ -239,8 +264,8 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
|
|
|
239
264
|
onMouseEnter={e => e.currentTarget.style.background = 'rgba(255,255,255,0.08)'}
|
|
240
265
|
onMouseLeave={e => e.currentTarget.style.background = 'rgba(255,255,255,0.03)'}
|
|
241
266
|
>
|
|
242
|
-
{m.profile_image_url ? (
|
|
243
|
-
<img src={m.profile_image_url} alt="" style={{
|
|
267
|
+
{localPhoto(m.profile_image_url) ? (
|
|
268
|
+
<img src={localPhoto(m.profile_image_url)} alt="" style={{
|
|
244
269
|
width: 22, height: 22, borderRadius: '50%', objectFit: 'cover', flexShrink: 0,
|
|
245
270
|
border: `1px solid ${tierColors[m.tier] || '#555'}`,
|
|
246
271
|
}} onError={e => { e.target.style.display = 'none'; e.target.nextSibling.style.display = 'flex'; }} />
|
|
@@ -248,7 +273,7 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
|
|
|
248
273
|
<div style={{
|
|
249
274
|
width: 22, height: 22, borderRadius: '50%', flexShrink: 0, fontSize: 9, fontWeight: 700,
|
|
250
275
|
background: tierColors[m.tier] || '#555', color: m.tier === 'S' ? '#000' : '#fff',
|
|
251
|
-
display: m.profile_image_url ? 'none' : 'flex', alignItems: 'center', justifyContent: 'center',
|
|
276
|
+
display: localPhoto(m.profile_image_url) ? 'none' : 'flex', alignItems: 'center', justifyContent: 'center',
|
|
252
277
|
}}>{m.name?.charAt(0)}</div>
|
|
253
278
|
<div style={{ flex: 1, minWidth: 0 }}>
|
|
254
279
|
<div style={{ fontSize: 10, fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{m.name}</div>
|
|
@@ -294,61 +319,67 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
|
|
|
294
319
|
</button>
|
|
295
320
|
)}
|
|
296
321
|
|
|
297
|
-
{/* Connect to Unlock — for ALL degree-2 connections
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}}>
|
|
303
|
-
<div style={{ fontSize: 10, color: '#FFD700', fontWeight: 700, letterSpacing: 1, marginBottom: 6 }}>
|
|
304
|
-
{selected.unlock_status === 'pending' ? 'CONNECTION PENDING' : 'LOCKED PATH'}
|
|
305
|
-
</div>
|
|
306
|
-
<div style={{ fontSize: 13, color: '#ccc', marginBottom: 12 }}>
|
|
307
|
-
{selected.unlock_status === 'pending'
|
|
308
|
-
? `Waiting for ${selected.name} to accept. Run a scan to check.`
|
|
309
|
-
: `Connect with ${selected.name} to unlock their network and extend your 6 degrees.`
|
|
310
|
-
}
|
|
311
|
-
</div>
|
|
322
|
+
{/* Connect to Unlock — for ALL degree-2 connections. A request sent here
|
|
323
|
+
is the person's, everywhere at once (lib/requests-client.js). */}
|
|
324
|
+
{(selected.degree === 2 || selected.source_connection_id) && selected.degree !== 1 && selected.unlock_status !== 'unlocked' && (() => {
|
|
325
|
+
const asked = isRequested(selected);
|
|
326
|
+
return (
|
|
312
327
|
<div style={{
|
|
313
|
-
|
|
314
|
-
borderRadius:
|
|
315
|
-
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
316
|
-
fontSize: 11, color: '#444', marginBottom: 12,
|
|
328
|
+
background: 'rgba(255,215,0,0.06)', border: `1px ${asked ? 'dashed' : 'solid'} rgba(255,215,0,${asked ? 0.45 : 0.25})`,
|
|
329
|
+
borderRadius: 10, padding: 16, marginTop: 16, textAlign: 'center',
|
|
317
330
|
}}>
|
|
318
|
-
{
|
|
331
|
+
<div style={{ fontSize: 10, color: '#FFD700', fontWeight: 700, letterSpacing: 1, marginBottom: 6 }}>
|
|
332
|
+
{asked ? 'REQUEST SENT' : 'LOCKED PATH'}
|
|
333
|
+
</div>
|
|
334
|
+
<div style={{ fontSize: 13, color: '#ccc', marginBottom: 12 }}>
|
|
335
|
+
{asked
|
|
336
|
+
? `Waiting for ${selected.name} to accept. The next scan of your own connections notices when they do.`
|
|
337
|
+
: `Connect with ${selected.name} to unlock their network and extend your six degrees.`
|
|
338
|
+
}
|
|
339
|
+
</div>
|
|
340
|
+
<div style={{
|
|
341
|
+
width: '100%', height: 60, background: 'rgba(255,255,255,0.03)',
|
|
342
|
+
borderRadius: 8, border: '1px dashed rgba(255,255,255,0.1)',
|
|
343
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
344
|
+
fontSize: 11, color: '#444', marginBottom: 12,
|
|
345
|
+
}}>
|
|
346
|
+
{asked ? '[ Cluster Pending... ]' : '[ Hidden Network ]'}
|
|
347
|
+
</div>
|
|
348
|
+
{asked ? (
|
|
349
|
+
<button
|
|
350
|
+
onClick={() => { undoRequest(selected).catch(() => {}); }}
|
|
351
|
+
style={{
|
|
352
|
+
background: 'none', border: 'none', color: '#888', fontSize: 11, fontWeight: 600, cursor: 'pointer',
|
|
353
|
+
}}
|
|
354
|
+
>
|
|
355
|
+
Didn't send it? Undo
|
|
356
|
+
</button>
|
|
357
|
+
) : (
|
|
358
|
+
<a href={selected.profile_url} target="_blank" rel="noopener noreferrer"
|
|
359
|
+
style={{
|
|
360
|
+
display: 'block', padding: '10px 16px',
|
|
361
|
+
background: 'linear-gradient(135deg, #FFD700, #FF6B35)',
|
|
362
|
+
color: '#000', borderRadius: 8, textDecoration: 'none',
|
|
363
|
+
fontWeight: 700, fontSize: 13,
|
|
364
|
+
}}
|
|
365
|
+
onClick={() => {
|
|
366
|
+
// Opens their profile to connect, and marks the request
|
|
367
|
+
// sent, through the bridge whose circle you found them in.
|
|
368
|
+
markRequested(selected, { bridgeId: selected.source_connection_id }).catch(() => {});
|
|
369
|
+
}}
|
|
370
|
+
>
|
|
371
|
+
Connect to Unlock Path
|
|
372
|
+
</a>
|
|
373
|
+
)}
|
|
319
374
|
</div>
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
style={{
|
|
323
|
-
display: 'block', padding: '10px 16px',
|
|
324
|
-
background: 'linear-gradient(135deg, #FFD700, #FF6B35)',
|
|
325
|
-
color: '#000', borderRadius: 8, textDecoration: 'none',
|
|
326
|
-
fontWeight: 700, fontSize: 13,
|
|
327
|
-
}}
|
|
328
|
-
onClick={() => {
|
|
329
|
-
// Mark as pending in unlock system
|
|
330
|
-
fetch('/api/unlock', {
|
|
331
|
-
method: 'POST',
|
|
332
|
-
headers: { 'Content-Type': 'application/json' },
|
|
333
|
-
body: JSON.stringify({ connectionId: selected.id }),
|
|
334
|
-
}).catch(() => {});
|
|
335
|
-
// Sync to outreach/pending system (Outlink)
|
|
336
|
-
fetch('/api/outreach', {
|
|
337
|
-
method: 'POST',
|
|
338
|
-
headers: { 'Content-Type': 'application/json' },
|
|
339
|
-
body: JSON.stringify({ action: 'mark-sent', connectionId: selected.id, profileUrl: selected.profile_url, userId }),
|
|
340
|
-
}).catch(() => {});
|
|
341
|
-
}}
|
|
342
|
-
>
|
|
343
|
-
Connect to Unlock Path
|
|
344
|
-
</a>
|
|
345
|
-
)}
|
|
346
|
-
</div>
|
|
347
|
-
)}
|
|
375
|
+
);
|
|
376
|
+
})()}
|
|
348
377
|
|
|
349
378
|
{/* Create Cluster — for D1 connections OR accepted D2 (promoted, ready to bridge for D3) */}
|
|
350
379
|
{(selected.degree === 1 || selected.outreach_status === 'accepted') && (
|
|
351
|
-
|
|
380
|
+
canScan
|
|
381
|
+
? <CreateClusterCard key={selected.id} selected={selected} degree2={degree2} />
|
|
382
|
+
: <ScansNeedYourNetwork csvSource={csvSource} style={{ marginTop: 16 }} />
|
|
352
383
|
)}
|
|
353
384
|
|
|
354
385
|
{selected.profile_url && (
|
|
@@ -381,8 +412,10 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
|
|
|
381
412
|
Your bridge connections and who they unlock.
|
|
382
413
|
</p>
|
|
383
414
|
|
|
384
|
-
{/*
|
|
385
|
-
|
|
415
|
+
{/* Who to scan next: people you reached, whose own circle is next */}
|
|
416
|
+
{canScan
|
|
417
|
+
? <ReadyToScan connections={connections} degree2={degree2} scanNotes={scanNotes} tierColors={tierColors} />
|
|
418
|
+
: <ScansNeedYourNetwork csvSource={csvSource} style={{ marginBottom: 16 }} />}
|
|
386
419
|
|
|
387
420
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8, marginBottom: 12 }}>
|
|
388
421
|
<StatBox label="Degree-2 Found" value={degree2.length} />
|
|
@@ -435,8 +468,8 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
|
|
|
435
468
|
onMouseEnter={e => e.currentTarget.style.background = 'rgba(255,255,255,0.06)'}
|
|
436
469
|
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
|
|
437
470
|
>
|
|
438
|
-
{m.profile_image_url ? (
|
|
439
|
-
<img src={m.profile_image_url} alt="" style={{ width: 24, height: 24, borderRadius: '50%', objectFit: 'cover' }} />
|
|
471
|
+
{localPhoto(m.profile_image_url) ? (
|
|
472
|
+
<img src={localPhoto(m.profile_image_url)} alt="" style={{ width: 24, height: 24, borderRadius: '50%', objectFit: 'cover' }} />
|
|
440
473
|
) : (
|
|
441
474
|
<div style={{ width: 24, height: 24, borderRadius: '50%', background: tierColors[m.tier] || '#555', fontSize: 9, fontWeight: 700, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff' }}>{m.name?.charAt(0)}</div>
|
|
442
475
|
)}
|
|
@@ -471,8 +504,8 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
|
|
|
471
504
|
border: '1px solid rgba(255,107,53,0.12)',
|
|
472
505
|
}}>
|
|
473
506
|
{/* Photo */}
|
|
474
|
-
{p.profile_image_url ? (
|
|
475
|
-
<img src={p.profile_image_url} alt="" style={{
|
|
507
|
+
{localPhoto(p.profile_image_url) ? (
|
|
508
|
+
<img src={localPhoto(p.profile_image_url)} alt="" style={{
|
|
476
509
|
width: 32, height: 32, borderRadius: '50%', objectFit: 'cover', flexShrink: 0,
|
|
477
510
|
border: `2px solid ${tierColors[p.tier] || '#555'}`,
|
|
478
511
|
}} onError={e => { e.target.style.display = 'none'; }} />
|
|
@@ -653,8 +686,8 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
|
|
|
653
686
|
onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(255,255,255,0.08)'}
|
|
654
687
|
onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}
|
|
655
688
|
>
|
|
656
|
-
{c.profile_image_url ? (
|
|
657
|
-
<img src={c.profile_image_url} alt="" style={{
|
|
689
|
+
{localPhoto(c.profile_image_url) ? (
|
|
690
|
+
<img src={localPhoto(c.profile_image_url)} alt="" style={{
|
|
658
691
|
width: 24, height: 24, borderRadius: '50%', objectFit: 'cover', flexShrink: 0,
|
|
659
692
|
border: `1.5px solid ${tierColors[c.tier] || '#555'}`,
|
|
660
693
|
}} onError={e => { e.target.style.display = 'none'; }} />
|
|
@@ -683,6 +716,11 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
|
|
|
683
716
|
{(() => {
|
|
684
717
|
// Track per-tier rank numbering
|
|
685
718
|
const tierRankCounter = {};
|
|
719
|
+
// Counted once. It used to be counted again for every person in the
|
|
720
|
+
// list: 900 million steps at 30,000 people, about 20 s on every render
|
|
721
|
+
// of the page while nothing was selected (opening the Filter panel, say).
|
|
722
|
+
const tierCounts = {};
|
|
723
|
+
allRanked.forEach(r => { tierCounts[r.tier] = (tierCounts[r.tier] || 0) + 1; });
|
|
686
724
|
return allRanked.map((c, i) => {
|
|
687
725
|
const prevTier = i > 0 ? allRanked[i - 1].tier : null;
|
|
688
726
|
const showHeader = c.tier !== prevTier;
|
|
@@ -690,8 +728,6 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
|
|
|
690
728
|
tierRankCounter[c.tier] = (tierRankCounter[c.tier] || 0) + 1;
|
|
691
729
|
const tierRank = tierRankCounter[c.tier];
|
|
692
730
|
const tierNames = { S: 'S-Tier — Elite', A: 'A-Tier — High Value', B: 'B-Tier — Notable', C: 'C-Tier — Standard', D: 'D-Tier — Entry' };
|
|
693
|
-
const tierCounts = {};
|
|
694
|
-
allRanked.forEach(r => { tierCounts[r.tier] = (tierCounts[r.tier] || 0) + 1; });
|
|
695
731
|
return (
|
|
696
732
|
<div key={c.id}>
|
|
697
733
|
{showHeader && (
|
|
@@ -723,9 +759,9 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
|
|
|
723
759
|
#{tierRank}
|
|
724
760
|
</span>
|
|
725
761
|
{/* Profile photo or initial */}
|
|
726
|
-
{c.profile_image_url ? (
|
|
762
|
+
{localPhoto(c.profile_image_url) ? (
|
|
727
763
|
<img
|
|
728
|
-
src={c.profile_image_url}
|
|
764
|
+
src={localPhoto(c.profile_image_url)}
|
|
729
765
|
alt=""
|
|
730
766
|
style={{ width: 28, height: 28, borderRadius: '50%', objectFit: 'cover', flexShrink: 0,
|
|
731
767
|
border: `1.5px solid ${tierColors[c.tier] || '#555'}` }}
|
|
@@ -735,7 +771,7 @@ export default function Sidebar({ selected, stats, tierColors, connections, degr
|
|
|
735
771
|
<div style={{
|
|
736
772
|
width: 28, height: 28, borderRadius: '50%', flexShrink: 0,
|
|
737
773
|
background: tierColors[c.tier] || '#555',
|
|
738
|
-
display: c.profile_image_url ? 'none' : 'flex',
|
|
774
|
+
display: localPhoto(c.profile_image_url) ? 'none' : 'flex',
|
|
739
775
|
alignItems: 'center', justifyContent: 'center',
|
|
740
776
|
fontSize: 12, fontWeight: 700, color: c.tier === 'S' ? '#000' : '#fff',
|
|
741
777
|
}}>
|
|
@@ -916,146 +952,181 @@ function PathBox({ routes, selected, tierColors, onSelect }) {
|
|
|
916
952
|
);
|
|
917
953
|
}
|
|
918
954
|
|
|
955
|
+
// Scan one person's circle, or carry on with it.
|
|
956
|
+
//
|
|
957
|
+
// LinkedIn lists someone else's connections in its own order, with no dates,
|
|
958
|
+
// so anyone new can turn up on any page: Rescan reads the whole list again from
|
|
959
|
+
// page 1. Resume carries on from the page the last read stopped at, for a list
|
|
960
|
+
// that was only partly read. Both show when there is something to resume.
|
|
961
|
+
//
|
|
962
|
+
// Whether a scan is running comes from the scanner (useScanner), not from this
|
|
963
|
+
// card: close the card mid-scan and open it again and the scan is still here,
|
|
964
|
+
// and every other card greys its buttons out until it ends.
|
|
919
965
|
function CreateClusterCard({ selected, degree2 }) {
|
|
920
|
-
const
|
|
921
|
-
const [
|
|
922
|
-
const [
|
|
923
|
-
const
|
|
966
|
+
const scan = useScanner();
|
|
967
|
+
const [checking, setChecking] = useState(false);
|
|
968
|
+
const [problem, setProblem] = useState(null); // why a scan didn't start: { text, offline }
|
|
969
|
+
const [resume, setResume] = useState(undefined); // where Resume carries on; null: nothing to resume
|
|
924
970
|
const clusterCount = degree2?.filter(d => d.source_connection_id === selected.id).length || 0;
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
background: 'rgba(0,255,136,0.12)', color: '#00ff88', fontWeight: 600, fontSize: 11,
|
|
943
|
-
}}
|
|
944
|
-
>
|
|
945
|
-
Rescan Cluster
|
|
946
|
-
</button>
|
|
947
|
-
</div>
|
|
948
|
-
);
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
async function startScrape() {
|
|
952
|
-
setStatus('checking');
|
|
953
|
-
setLog(['Checking the scanner...']);
|
|
971
|
+
const hasCluster = clusterCount > 0;
|
|
972
|
+
|
|
973
|
+
const mine = scan.running && scansCircleOf(scan, selected);
|
|
974
|
+
const busy = scan.running && !mine ? busyReason(scan) : null;
|
|
975
|
+
const last = scan.finished.find((j) => scansCircleOf(j, selected));
|
|
976
|
+
|
|
977
|
+
// Every read moves where Resume would carry on, so ask again whenever one ends.
|
|
978
|
+
const lastEnded = scan.finished[0]?.startedAt ?? null;
|
|
979
|
+
useEffect(() => {
|
|
980
|
+
let live = true;
|
|
981
|
+
resumePoint(selected.id).then((r) => { if (live) setResume(r); }, () => { if (live) setResume(null); });
|
|
982
|
+
return () => { live = false; };
|
|
983
|
+
}, [selected.id, lastEnded]);
|
|
984
|
+
|
|
985
|
+
async function start(action) {
|
|
986
|
+
setProblem(null);
|
|
987
|
+
setChecking(true);
|
|
954
988
|
const blocked = notReadyMessage(await scraperStatus().catch(() => null));
|
|
989
|
+
setChecking(false);
|
|
955
990
|
if (blocked) {
|
|
956
|
-
|
|
957
|
-
setLog([blocked]);
|
|
991
|
+
setProblem({ text: blocked, offline: true });
|
|
958
992
|
return;
|
|
959
993
|
}
|
|
960
|
-
|
|
961
|
-
setStatus('scraping');
|
|
962
|
-
setLog(['Mapping the circle behind ' + selected.name + '...']);
|
|
963
|
-
|
|
964
994
|
try {
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
setLog(prev => [...prev, 'Done. Refresh to see the cluster.']);
|
|
969
|
-
} else {
|
|
970
|
-
setStatus('error');
|
|
971
|
-
}
|
|
972
|
-
} catch {
|
|
973
|
-
setStatus('error');
|
|
974
|
-
setLog(['Failed to start the scan']);
|
|
995
|
+
await beginScrape(action, action === 'resume' ? { id: selected.id } : { name: selected.name, id: selected.id });
|
|
996
|
+
} catch (e) {
|
|
997
|
+
setProblem({ text: e.message || 'Could not start the scan.' });
|
|
975
998
|
}
|
|
976
999
|
}
|
|
977
1000
|
|
|
1001
|
+
const read = hasCluster || Boolean(resume);
|
|
1002
|
+
const accent = hasCluster ? '#00ff88' : '#9B59B6';
|
|
1003
|
+
const off = Boolean(busy) || checking;
|
|
1004
|
+
const button = (primary) => ({
|
|
1005
|
+
flex: 1, padding: '11px 8px', borderRadius: 8, fontWeight: 700, fontSize: primary ? 13 : 12,
|
|
1006
|
+
cursor: off ? 'not-allowed' : 'pointer', opacity: off ? 0.45 : 1,
|
|
1007
|
+
...(primary
|
|
1008
|
+
? { border: 'none', background: 'linear-gradient(135deg, #9B59B6, #3498DB)', color: '#fff' }
|
|
1009
|
+
: { border: `1px solid ${accent}55`, background: `${accent}14`, color: accent }),
|
|
1010
|
+
});
|
|
1011
|
+
|
|
978
1012
|
return (
|
|
979
|
-
<div style={{
|
|
980
|
-
background: 'rgba(
|
|
1013
|
+
<div id="scan-box" style={{
|
|
1014
|
+
background: hasCluster ? 'rgba(0,255,136,0.06)' : 'rgba(155,89,182,0.08)',
|
|
1015
|
+
border: `1px solid ${hasCluster ? 'rgba(0,255,136,0.2)' : 'rgba(155,89,182,0.3)'}`,
|
|
981
1016
|
borderRadius: 10, padding: 16, marginTop: 16,
|
|
982
1017
|
}}>
|
|
983
|
-
<div style={{ fontSize: 10, color:
|
|
984
|
-
CREATE CLUSTER
|
|
1018
|
+
<div style={{ fontSize: 10, color: accent, fontWeight: 700, letterSpacing: 1, marginBottom: 4, textAlign: 'center' }}>
|
|
1019
|
+
{hasCluster ? 'CLUSTER ACTIVE' : 'CREATE CLUSTER'}
|
|
985
1020
|
</div>
|
|
986
|
-
<div style={{ fontSize: 12, color: '#
|
|
987
|
-
|
|
1021
|
+
<div style={{ fontSize: 12, color: '#aaa', marginBottom: 12, textAlign: 'center' }}>
|
|
1022
|
+
{hasCluster
|
|
1023
|
+
? `${clusterCount} connections mapped in Six Degrees`
|
|
1024
|
+
: <>Scan {selected.name}'s connections to map their network</>}
|
|
988
1025
|
</div>
|
|
989
1026
|
|
|
990
|
-
{
|
|
991
|
-
<
|
|
992
|
-
|
|
993
|
-
style={{
|
|
994
|
-
width: '100%', padding: '12px', borderRadius: 8, border: 'none', cursor: 'pointer',
|
|
995
|
-
background: 'linear-gradient(135deg, #9B59B6, #3498DB)',
|
|
996
|
-
color: '#fff', fontWeight: 700, fontSize: 14,
|
|
997
|
-
}}
|
|
998
|
-
>
|
|
999
|
-
Scan {selected.name}'s Network
|
|
1000
|
-
</button>
|
|
1001
|
-
)}
|
|
1002
|
-
|
|
1003
|
-
{status === 'offline' && (
|
|
1027
|
+
{mine ? (
|
|
1028
|
+
<ScanLog title={scan.pending ? 'Starting…' : `Scanning ${selected.name}’s circle…`} log={scan.log} />
|
|
1029
|
+
) : problem?.offline ? (
|
|
1004
1030
|
<>
|
|
1005
1031
|
<div style={{ padding: '10px', borderRadius: 8, background: 'rgba(255,80,80,0.1)', border: '1px solid rgba(255,80,80,0.3)', color: '#ff5050', fontSize: 12, marginBottom: 8, textAlign: 'center' }}>
|
|
1006
1032
|
Scanner offline
|
|
1007
1033
|
</div>
|
|
1008
1034
|
<div style={{ fontSize: 10, color: '#888', textAlign: 'center', marginBottom: 8 }}>
|
|
1009
|
-
Open the <strong>Scan</strong> page to finish setting the scanner up
|
|
1035
|
+
{problem.text} Open the <strong>Scan</strong> page to finish setting the scanner up.
|
|
1010
1036
|
</div>
|
|
1011
|
-
<button onClick={() =>
|
|
1037
|
+
<button onClick={() => setProblem(null)} style={{
|
|
1012
1038
|
width: '100%', padding: '10px', borderRadius: 8, border: 'none', cursor: 'pointer',
|
|
1013
1039
|
background: 'rgba(255,255,255,0.1)', color: '#aaa', fontWeight: 600, fontSize: 12,
|
|
1014
1040
|
}}>
|
|
1015
1041
|
Retry
|
|
1016
1042
|
</button>
|
|
1017
1043
|
</>
|
|
1018
|
-
)
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1044
|
+
) : (
|
|
1045
|
+
<>
|
|
1046
|
+
{last && <LastRead job={last} />}
|
|
1047
|
+
<div style={{ display: 'flex', gap: 8 }}>
|
|
1048
|
+
{resume && (
|
|
1049
|
+
<button onClick={() => start('resume')} disabled={off} style={button(true)}>
|
|
1050
|
+
Resume from page {resume.nextPage}
|
|
1051
|
+
</button>
|
|
1052
|
+
)}
|
|
1053
|
+
<button onClick={() => start('bridge')} disabled={off} style={button(!resume && !hasCluster)}>
|
|
1054
|
+
{read ? 'Rescan from the start' : <>Scan {selected.name}'s Network</>}
|
|
1055
|
+
</button>
|
|
1056
|
+
</div>
|
|
1057
|
+
{resume && (
|
|
1058
|
+
<div style={{ fontSize: 10, color: '#888', marginTop: 8, lineHeight: 1.45, textAlign: 'center' }}>
|
|
1059
|
+
Read to page {resume.pagesRead} so far. Resume carries on from page {resume.nextPage}. LinkedIn
|
|
1060
|
+
lists their connections in its own order, not by date, so Rescan reads them all again from page 1.
|
|
1061
|
+
</div>
|
|
1062
|
+
)}
|
|
1063
|
+
{hasCluster && resume === null && (
|
|
1064
|
+
<div style={{ fontSize: 10, color: '#888', marginTop: 8, lineHeight: 1.45, textAlign: 'center' }}>
|
|
1065
|
+
Their whole list has been read. LinkedIn doesn't date other people's connections, so a
|
|
1066
|
+
rescan reads it all again to find anyone new.
|
|
1029
1067
|
</div>
|
|
1030
|
-
|
|
1031
|
-
|
|
1068
|
+
)}
|
|
1069
|
+
{busy && (
|
|
1070
|
+
<div style={{ fontSize: 10, color: '#bbb', marginTop: 8, textAlign: 'center' }}>
|
|
1071
|
+
{busy}. One scan at a time: this one can start when it finishes.
|
|
1032
1072
|
</div>
|
|
1033
|
-
|
|
1034
|
-
|
|
1073
|
+
)}
|
|
1074
|
+
{problem && (
|
|
1075
|
+
<div style={{ fontSize: 11, color: '#ff8080', marginTop: 8, textAlign: 'center' }}>{problem.text}</div>
|
|
1076
|
+
)}
|
|
1077
|
+
</>
|
|
1035
1078
|
)}
|
|
1079
|
+
</div>
|
|
1080
|
+
);
|
|
1081
|
+
}
|
|
1036
1082
|
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
)
|
|
1083
|
+
// In place of the Scan buttons while the sample or a CSV import is open. Both
|
|
1084
|
+
// live in this window only, so a scan could only fail: the scanner looks the
|
|
1085
|
+
// person up in the database, and says "Bridge '…' not found in database".
|
|
1086
|
+
function ScansNeedYourNetwork({ csvSource, style }) {
|
|
1087
|
+
return (
|
|
1088
|
+
<div style={{
|
|
1089
|
+
padding: '10px 12px', borderRadius: 8, fontSize: 11, color: '#999', lineHeight: 1.5, textAlign: 'center',
|
|
1090
|
+
background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.08)', ...style,
|
|
1091
|
+
}}>
|
|
1092
|
+
{csvSource === 'sample'
|
|
1093
|
+
? 'Scanning needs your own network: everyone in the sample is invented.'
|
|
1094
|
+
: 'Scanning needs your own network: a CSV import lives only in this window. Close it (× at the top) and open Scan.'}
|
|
1095
|
+
</div>
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1045
1098
|
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1099
|
+
/** A running scan's log, newest at the bottom. */
|
|
1100
|
+
function ScanLog({ title, log, small = false }) {
|
|
1101
|
+
return (
|
|
1102
|
+
<div style={{
|
|
1103
|
+
padding: '10px', borderRadius: 8, background: 'rgba(155,89,182,0.1)', border: '1px solid rgba(155,89,182,0.3)',
|
|
1104
|
+
}}>
|
|
1105
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 }}>
|
|
1106
|
+
<div style={{ width: small ? 6 : 8, height: small ? 6 : 8, borderRadius: '50%', background: '#9B59B6', animation: 'pulse 1s infinite' }} />
|
|
1107
|
+
<span style={{ fontSize: 11, fontWeight: 600, color: '#9B59B6' }}>{title}</span>
|
|
1108
|
+
</div>
|
|
1109
|
+
<div style={{ fontFamily: 'monospace', fontSize: small ? 9 : 10, color: '#888', maxHeight: small ? 80 : 100, overflow: 'auto' }}>
|
|
1110
|
+
{log.slice(-40).map((l, i) => <div key={i}>{l}</div>)}
|
|
1111
|
+
</div>
|
|
1112
|
+
</div>
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
/** How a scan this page watched ended. */
|
|
1117
|
+
function LastRead({ job, who = null }) {
|
|
1118
|
+
const ok = job.exitCode === 0;
|
|
1119
|
+
const why = !ok && Array.isArray(job.failure) && job.failure.length ? job.failure[job.failure.length - 1] : null;
|
|
1120
|
+
return (
|
|
1121
|
+
<div style={{
|
|
1122
|
+
padding: '8px 10px', borderRadius: 8, marginBottom: 10, fontSize: 11, textAlign: 'center', lineHeight: 1.4,
|
|
1123
|
+
background: ok ? 'rgba(0,255,136,0.08)' : 'rgba(255,80,80,0.08)',
|
|
1124
|
+
border: `1px solid ${ok ? 'rgba(0,255,136,0.25)' : 'rgba(255,80,80,0.25)'}`,
|
|
1125
|
+
color: ok ? '#00ff88' : '#ff8080',
|
|
1126
|
+
}}>
|
|
1127
|
+
{ok
|
|
1128
|
+
? `${who ? `${who}’s circle` : 'The scan'} finished. What it found is on the map.`
|
|
1129
|
+
: `${who ? `${who}’s scan` : 'The scan'} stopped before the end${why ? `: ${why}` : '.'}`}
|
|
1059
1130
|
</div>
|
|
1060
1131
|
);
|
|
1061
1132
|
}
|
|
@@ -1076,8 +1147,8 @@ function ClusterCard({ cs, rank, tierColors, onSelect }) {
|
|
|
1076
1147
|
style={{ cursor: 'pointer', display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }}
|
|
1077
1148
|
>
|
|
1078
1149
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
1079
|
-
{cs.bridge.profile_image_url ? (
|
|
1080
|
-
<img src={cs.bridge.profile_image_url} alt="" style={{
|
|
1150
|
+
{localPhoto(cs.bridge.profile_image_url) ? (
|
|
1151
|
+
<img src={localPhoto(cs.bridge.profile_image_url)} alt="" style={{
|
|
1081
1152
|
width: 28, height: 28, borderRadius: '50%', objectFit: 'cover', flexShrink: 0,
|
|
1082
1153
|
border: `1.5px solid ${tierColors[cs.bridge.tier] || '#555'}`,
|
|
1083
1154
|
}} onError={e => { e.target.style.display = 'none'; }} />
|
|
@@ -1120,8 +1191,8 @@ function ClusterCard({ cs, rank, tierColors, onSelect }) {
|
|
|
1120
1191
|
{(cs.sTier.length > 0 || cs.aTier.length > 0) && (
|
|
1121
1192
|
<div style={{ display: 'flex', alignItems: 'center', gap: 4, marginTop: 8 }}>
|
|
1122
1193
|
{[...cs.sTier, ...cs.aTier].slice(0, 4).map(m => (
|
|
1123
|
-
m.profile_image_url ? (
|
|
1124
|
-
<img key={m.id} src={m.profile_image_url} alt={m.name}
|
|
1194
|
+
localPhoto(m.profile_image_url) ? (
|
|
1195
|
+
<img key={m.id} src={localPhoto(m.profile_image_url)} alt={m.name}
|
|
1125
1196
|
style={{ width: 22, height: 22, borderRadius: '50%', objectFit: 'cover',
|
|
1126
1197
|
border: `1.5px solid ${tierColors[m.tier] || '#555'}` }}
|
|
1127
1198
|
onError={(e) => { e.target.style.display = 'none'; }}
|
|
@@ -1171,8 +1242,8 @@ function ClusterCard({ cs, rank, tierColors, onSelect }) {
|
|
|
1171
1242
|
onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(255,255,255,0.06)'}
|
|
1172
1243
|
onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}
|
|
1173
1244
|
>
|
|
1174
|
-
{m.profile_image_url ? (
|
|
1175
|
-
<img src={m.profile_image_url} alt="" style={{
|
|
1245
|
+
{localPhoto(m.profile_image_url) ? (
|
|
1246
|
+
<img src={localPhoto(m.profile_image_url)} alt="" style={{
|
|
1176
1247
|
width: 20, height: 20, borderRadius: '50%', objectFit: 'cover', flexShrink: 0,
|
|
1177
1248
|
border: `1px solid ${tierColors[m.tier] || '#555'}`,
|
|
1178
1249
|
}} onError={(e) => { e.target.style.display = 'none'; e.target.nextSibling.style.display = 'flex'; }} />
|
|
@@ -1180,7 +1251,7 @@ function ClusterCard({ cs, rank, tierColors, onSelect }) {
|
|
|
1180
1251
|
<div style={{
|
|
1181
1252
|
width: 20, height: 20, borderRadius: '50%', flexShrink: 0, fontSize: 8, fontWeight: 700,
|
|
1182
1253
|
background: tierColors[m.tier] || '#555', color: m.tier === 'S' ? '#000' : '#fff',
|
|
1183
|
-
display: m.profile_image_url ? 'none' : 'flex',
|
|
1254
|
+
display: localPhoto(m.profile_image_url) ? 'none' : 'flex',
|
|
1184
1255
|
alignItems: 'center', justifyContent: 'center',
|
|
1185
1256
|
}}>{m.name?.charAt(0)}</div>
|
|
1186
1257
|
<div style={{ minWidth: 0, flex: 1 }}>
|
|
@@ -1277,122 +1348,130 @@ function generateInsights(person, user, allConnections, degree2, routes = []) {
|
|
|
1277
1348
|
return insights.slice(0, 5); // Max 5 insights
|
|
1278
1349
|
}
|
|
1279
1350
|
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
const next = unbridged[0];
|
|
1305
|
-
if (!next) return;
|
|
1306
|
-
setTarget(next);
|
|
1307
|
-
setStatus('checking');
|
|
1308
|
-
setLog([`Scanning ${next.name}'s network...`]);
|
|
1309
|
-
|
|
1310
|
-
const blocked = notReadyMessage(await scraperStatus().catch(() => null));
|
|
1311
|
-
if (blocked) {
|
|
1312
|
-
setStatus('offline');
|
|
1313
|
-
setLog([blocked]);
|
|
1314
|
-
return;
|
|
1315
|
-
}
|
|
1351
|
+
// Who to scan next, in the Degrees panel. It replaced "Auto-Bridge Next", which
|
|
1352
|
+
// started a scan of the top unmapped S/A connection in one click, by name, with
|
|
1353
|
+
// no cost shown, and offered hidden lists again after every run.
|
|
1354
|
+
//
|
|
1355
|
+
// This lists everyone you reached through a circle whose own circle can be
|
|
1356
|
+
// scanned now (lib/reach.js), strongest first, with the circle you found them
|
|
1357
|
+
// in and what a scan asks of LinkedIn. Each opens the Scan page with them
|
|
1358
|
+
// picked, and nothing starts until it's confirmed there, beside the budget and
|
|
1359
|
+
// the cooldown. Hidden lists stay in view, greyed, so nobody seems forgotten.
|
|
1360
|
+
// It's also the way to them from a keyboard or a screen reader: the orbs in
|
|
1361
|
+
// Bridge Chains aren't.
|
|
1362
|
+
function ReadyToScan({ connections, degree2, scanNotes, tierColors }) {
|
|
1363
|
+
const scan = useScanner();
|
|
1364
|
+
const [all, setAll] = useState(false);
|
|
1365
|
+
const reach = useMemo(() => reachIndex(connections, degree2, scanNotes), [connections, degree2, scanNotes]);
|
|
1366
|
+
const ready = useMemo(() => readyToScan(connections, reach), [connections, reach]);
|
|
1367
|
+
const hidden = useMemo(() => connections.filter((c) => reachState(c, reach) === 'hidden'), [connections, reach]);
|
|
1368
|
+
// Everyone else whose circle isn't scanned: the Scan page's batches.
|
|
1369
|
+
const others = useMemo(
|
|
1370
|
+
() => connections.filter((c) => reachState(c, reach) === null && circleState(c, reach) === 'todo').length,
|
|
1371
|
+
[connections, reach],
|
|
1372
|
+
);
|
|
1373
|
+
const byId = useMemo(() => new Map(connections.map((c) => [c.id, c])), [connections]);
|
|
1374
|
+
const cost = circleScanCost();
|
|
1316
1375
|
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1376
|
+
const circle = scan.running && isCircleScan(scan);
|
|
1377
|
+
const busy = scan.running && !circle ? busyReason(scan) : null;
|
|
1378
|
+
const last = scan.finished.find(isCircleScan);
|
|
1379
|
+
const shown = all ? ready : ready.slice(0, 5);
|
|
1380
|
+
const origin = (p) => byId.get(p.unlocked_from_bridge_id)?.name || p.unlocked_from_name;
|
|
1381
|
+
const first = (name) => String(name || '').trim().split(/\s+/)[0];
|
|
1325
1382
|
|
|
1326
1383
|
return (
|
|
1327
1384
|
<div style={{
|
|
1328
|
-
background: 'rgba(
|
|
1385
|
+
background: 'rgba(0,255,136,0.05)', border: '1px solid rgba(0,255,136,0.2)',
|
|
1329
1386
|
borderRadius: 10, padding: 14, marginBottom: 16,
|
|
1330
1387
|
}}>
|
|
1331
|
-
{
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
</div>
|
|
1339
|
-
<button onClick={startAutoBridge} style={{
|
|
1340
|
-
width: '100%', padding: '10px', borderRadius: 8, border: 'none', cursor: 'pointer',
|
|
1341
|
-
background: 'linear-gradient(135deg, #9B59B6, #FF6B35)',
|
|
1342
|
-
color: '#fff', fontWeight: 700, fontSize: 13,
|
|
1343
|
-
}}>
|
|
1344
|
-
Auto-Bridge Next
|
|
1345
|
-
</button>
|
|
1346
|
-
</>
|
|
1347
|
-
)}
|
|
1388
|
+
<div style={{ fontSize: 12, fontWeight: 800, color: '#00ff88', marginBottom: 4 }}>
|
|
1389
|
+
Ready to scan ({ready.length})
|
|
1390
|
+
</div>
|
|
1391
|
+
<div style={{ fontSize: 10.5, color: '#999', lineHeight: 1.5, marginBottom: 10 }}>
|
|
1392
|
+
People you added through a circle whose own circle isn't scanned yet. Scanning one
|
|
1393
|
+
brings in who they know: 3rd degree, through the circle you found them in.
|
|
1394
|
+
</div>
|
|
1348
1395
|
|
|
1349
|
-
{
|
|
1350
|
-
|
|
1351
|
-
<
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1396
|
+
{circle ? (
|
|
1397
|
+
<div style={{ marginBottom: 10 }}>
|
|
1398
|
+
<ScanLog small title={scan.pending ? 'Starting…' : `Scanning ${scan.target?.name || 'a circle'}…`} log={scan.log} />
|
|
1399
|
+
</div>
|
|
1400
|
+
) : last && <LastRead job={last} who={last.target?.name} />}
|
|
1401
|
+
|
|
1402
|
+
{ready.length === 0 ? (
|
|
1403
|
+
<div style={{ fontSize: 11, color: '#aaa', lineHeight: 1.5 }}>
|
|
1404
|
+
Nobody yet. When someone you asked from a circle accepts, ↻ <b>Check for new</b> brings
|
|
1405
|
+
them into your connections, and they show here.
|
|
1406
|
+
</div>
|
|
1407
|
+
) : (
|
|
1408
|
+
<div role="list" style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
1409
|
+
{shown.map((p) => {
|
|
1410
|
+
const now = circle && scansCircleOf(scan, p);
|
|
1411
|
+
const from = origin(p);
|
|
1412
|
+
return (
|
|
1413
|
+
<Link key={p.id} role="listitem" href={`/setup?scan=${encodeURIComponent(p.id)}`}
|
|
1414
|
+
aria-label={`Scan ${p.name}’s circle: opens the Scan page to confirm`}
|
|
1415
|
+
style={{
|
|
1416
|
+
display: 'flex', alignItems: 'center', gap: 8, padding: '8px 10px', borderRadius: 8,
|
|
1417
|
+
textDecoration: 'none', color: '#fff',
|
|
1418
|
+
background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(0,255,136,0.18)',
|
|
1419
|
+
}}>
|
|
1420
|
+
<span style={{ width: 8, height: 8, borderRadius: '50%', flexShrink: 0, background: tierColors[p.tier] || '#888', boxShadow: '0 0 0 2px rgba(0,255,136,0.45)' }} />
|
|
1421
|
+
<span style={{ flex: 1, minWidth: 0 }}>
|
|
1422
|
+
<span style={{ fontSize: 12, fontWeight: 700 }}>{p.name}</span>
|
|
1423
|
+
<span style={{ fontSize: 10, color: tierColors[p.tier] || '#888', fontWeight: 700, marginLeft: 6 }}>{p.tier}</span>
|
|
1424
|
+
{from && (
|
|
1425
|
+
<span style={{ display: 'block', fontSize: 10, color: '#888', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
1426
|
+
was in {first(from)}'s circle
|
|
1427
|
+
</span>
|
|
1428
|
+
)}
|
|
1429
|
+
</span>
|
|
1430
|
+
<span style={{ fontSize: 10.5, fontWeight: 700, color: '#00ff88', whiteSpace: 'nowrap' }}>
|
|
1431
|
+
{now ? 'Scanning…' : 'Scan… →'}
|
|
1432
|
+
</span>
|
|
1433
|
+
</Link>
|
|
1434
|
+
);
|
|
1435
|
+
})}
|
|
1436
|
+
{ready.length > 5 && (
|
|
1437
|
+
<button type="button" onClick={() => setAll((v) => !v)} style={{
|
|
1438
|
+
background: 'none', border: 'none', color: '#888', fontSize: 10.5, fontWeight: 600, cursor: 'pointer', padding: '2px 0', textAlign: 'left',
|
|
1439
|
+
}}>
|
|
1440
|
+
{all ? 'Show fewer' : `Show all ${ready.length}`}
|
|
1441
|
+
</button>
|
|
1442
|
+
)}
|
|
1443
|
+
</div>
|
|
1359
1444
|
)}
|
|
1360
1445
|
|
|
1361
|
-
{
|
|
1362
|
-
<div>
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
</div>
|
|
1367
|
-
<div style={{ fontFamily: 'monospace', fontSize: 9, color: '#777', maxHeight: 80, overflow: 'auto' }}>
|
|
1368
|
-
{log.map((l, i) => <div key={i}>{l}</div>)}
|
|
1369
|
-
</div>
|
|
1446
|
+
{ready.length > 0 && (
|
|
1447
|
+
<div style={{ fontSize: 10, color: '#888', lineHeight: 1.5, marginTop: 8 }}>
|
|
1448
|
+
Each scan: {cost.profileViews} profile view, then one LinkedIn search for every page of their
|
|
1449
|
+
list, up to {cost.searches} pages (about {cost.minutes} min for a whole list). Nothing starts
|
|
1450
|
+
until you confirm it on the Scan page.
|
|
1370
1451
|
</div>
|
|
1371
1452
|
)}
|
|
1372
1453
|
|
|
1373
|
-
{
|
|
1374
|
-
<div style={{
|
|
1375
|
-
|
|
1376
|
-
{
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
color: '#fff', fontWeight: 600, fontSize: 11,
|
|
1382
|
-
}}>Bridge Next</button>
|
|
1454
|
+
{hidden.length > 0 && (
|
|
1455
|
+
<div style={{ marginTop: 10, fontSize: 10.5, color: '#777', lineHeight: 1.6 }}>
|
|
1456
|
+
{hidden.slice(0, 3).map((p) => (
|
|
1457
|
+
<div key={p.id} style={{ opacity: 0.7 }}>
|
|
1458
|
+
<span aria-hidden="true">🔒</span> {p.name} <span style={{ color: '#666' }}>· their list is hidden</span>
|
|
1459
|
+
</div>
|
|
1460
|
+
))}
|
|
1461
|
+
{hidden.length > 3 && <div style={{ opacity: 0.7 }}>🔒 and {hidden.length - 3} more with hidden lists</div>}
|
|
1383
1462
|
</div>
|
|
1384
1463
|
)}
|
|
1385
1464
|
|
|
1386
|
-
{
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1465
|
+
{others > 0 && (
|
|
1466
|
+
<div style={{ fontSize: 10.5, color: '#888', lineHeight: 1.5, marginTop: 10 }}>
|
|
1467
|
+
{others.toLocaleString('en-US')} of your other connections aren't scanned yet.{' '}
|
|
1468
|
+
<Link href="/setup" style={{ color: '#3498DB', textDecoration: 'none' }}>Map them in batches on the Scan page →</Link>
|
|
1469
|
+
</div>
|
|
1470
|
+
)}
|
|
1471
|
+
{busy && (
|
|
1472
|
+
<div style={{ fontSize: 10, color: '#bbb', marginTop: 8 }}>
|
|
1473
|
+
{busy}. One scan at a time.
|
|
1474
|
+
</div>
|
|
1396
1475
|
)}
|
|
1397
1476
|
</div>
|
|
1398
1477
|
);
|