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
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
// Their circle, at the top of a profile card (Blake, 2026-09-28).
|
|
4
|
+
//
|
|
5
|
+
// The person in the middle; their scanned circle round them (D2); the people
|
|
6
|
+
// in it you connected with ringed in green, with their own circles fanning out
|
|
7
|
+
// behind them (D3), and so on out to D6. A request still out is a dotted dot.
|
|
8
|
+
// Each dot is coloured by its tier. Rarity (lib/rarity.js) is a filter beside
|
|
9
|
+
// the tier's rather than a mark on every dot (while it's counted from your
|
|
10
|
+
// scans, most people read as rare, and a ring on nearly everything says
|
|
11
|
+
// nothing): the two work together, so "S" and "Only way in" lights exactly the
|
|
12
|
+
// rare finds in this circle and dims the rest. Tap the map for the large one,
|
|
13
|
+
// where a dot opens that person's card.
|
|
14
|
+
|
|
15
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
16
|
+
import { createPortal } from 'react-dom';
|
|
17
|
+
import { circleIndex, circleRings, circleLayout, requestedByDefault } from '../../lib/circle';
|
|
18
|
+
import { keyFor, routeIndex } from '../../lib/separation';
|
|
19
|
+
import { RARITY, rarityOf, passes, countByRarity, toggle, rarityInfo } from '../../lib/rarity';
|
|
20
|
+
import { scoreGuess } from '../../lib/score-guess';
|
|
21
|
+
|
|
22
|
+
const CLASSIC = { S: '#FFD700', A: '#9B59B6', B: '#3498DB', C: '#95A5A6', D: '#BDC3C7' };
|
|
23
|
+
const TIERS = ['S', 'A', 'B', 'C', 'D'];
|
|
24
|
+
const GREEN = '#00ff88';
|
|
25
|
+
|
|
26
|
+
export default function TheirCircle({ person, connections = [], degree2 = [], tierColors = CLASSIC, onSelect, isRequested = requestedByDefault, canScan = true }) {
|
|
27
|
+
const index = useMemo(() => circleIndex(connections, degree2), [connections, degree2]);
|
|
28
|
+
const ways = useMemo(() => routeIndex(degree2), [degree2]);
|
|
29
|
+
const { rings, totals } = useMemo(() => circleRings(person, index, { isRequested }), [person, index, isRequested]);
|
|
30
|
+
const [tiers, setTiers] = useState(() => new Set());
|
|
31
|
+
const [rarities, setRarities] = useState(() => new Set());
|
|
32
|
+
const [big, setBig] = useState(false);
|
|
33
|
+
|
|
34
|
+
// Each dot's tier and rarity. Your own connections have no rarity: you're
|
|
35
|
+
// already in.
|
|
36
|
+
const people = useMemo(() => rings.flatMap((ring) => ring.people.map((p) => {
|
|
37
|
+
const yours = p.state === 'connected' || p.row.degree === 1;
|
|
38
|
+
const r = yours ? null : rarityOf(p.row, ways.get(keyFor(p.row))?.size || 1);
|
|
39
|
+
return { key: p.key, tier: p.row.tier, rarity: r?.key ?? null, mutuals: r };
|
|
40
|
+
})), [rings, ways]);
|
|
41
|
+
const facts = useMemo(() => new Map(people.map((p) => [p.key, p])), [people]);
|
|
42
|
+
const filter = useMemo(() => ({ tiers, rarities }), [tiers, rarities]);
|
|
43
|
+
const shown = useMemo(() => people.filter((p) => passes(p, filter)).length, [people, filter]);
|
|
44
|
+
|
|
45
|
+
// How complete the ways-in count is: circles scanned out of your connections.
|
|
46
|
+
const scanned = useMemo(() => connections.filter((c) => index.circles.has(c.id)).length, [connections, index]);
|
|
47
|
+
const fromLinkedIn = people.some((p) => p.mutuals?.from === 'linkedin');
|
|
48
|
+
// A score the headline gave nothing to read for: say so, and point at the
|
|
49
|
+
// scan, which is what can firm it up (lib/score-guess.js).
|
|
50
|
+
const guess = useMemo(() => scoreGuess(person), [person]);
|
|
51
|
+
|
|
52
|
+
if (!person) return null;
|
|
53
|
+
// With the sample or a CSV import open there is no scan box below to point
|
|
54
|
+
// at (Sidebar.js says why in its place), so this only says what is known.
|
|
55
|
+
if (!rings.length) {
|
|
56
|
+
const first = person.name?.split(' ')[0] || 'them';
|
|
57
|
+
return (
|
|
58
|
+
<div style={guess ? { ...box, borderColor: 'rgba(255,215,0,0.28)' } : box}>
|
|
59
|
+
<Label>Their circle</Label>
|
|
60
|
+
{guess ? (
|
|
61
|
+
<>
|
|
62
|
+
<div style={{ fontSize: 11.5, color: '#d8ccb0', lineHeight: 1.5, marginBottom: canScan ? 9 : 0 }}>
|
|
63
|
+
<b style={{ color: '#FFD700' }}>Their score is a guess.</b> {guess.reason}
|
|
64
|
+
{canScan && <> Scanning {first}'s circle shows who you can reach through them, and a strong
|
|
65
|
+
circle adds up to +2 to their score.</>}
|
|
66
|
+
</div>
|
|
67
|
+
{canScan && (
|
|
68
|
+
<button type="button" onClick={toScanBox} style={{
|
|
69
|
+
width: '100%', padding: '8px 10px', borderRadius: 8, cursor: 'pointer', fontSize: 12, fontWeight: 700,
|
|
70
|
+
border: '1px solid rgba(255,215,0,0.4)', background: 'rgba(255,215,0,0.08)', color: '#FFD700',
|
|
71
|
+
}}>
|
|
72
|
+
Scan their circle ↓
|
|
73
|
+
</button>
|
|
74
|
+
)}
|
|
75
|
+
</>
|
|
76
|
+
) : canScan ? (
|
|
77
|
+
<div style={{ fontSize: 11, color: '#999', lineHeight: 1.5 }}>
|
|
78
|
+
Not scanned yet. Scan {first}'s circle below to see who they know, and
|
|
79
|
+
everyone you reach through them after that.
|
|
80
|
+
</div>
|
|
81
|
+
) : (
|
|
82
|
+
<div style={{ fontSize: 11, color: '#999', lineHeight: 1.5 }}>Not scanned.</div>
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const byTier = Object.fromEntries(TIERS.map((t) => [t, people.filter((p) => p.tier === t).length]));
|
|
89
|
+
const byRarity = countByRarity(people);
|
|
90
|
+
const filtering = tiers.size > 0 || rarities.size > 0;
|
|
91
|
+
|
|
92
|
+
const chips = (
|
|
93
|
+
<Filters
|
|
94
|
+
tierColors={tierColors} byTier={byTier} byRarity={byRarity}
|
|
95
|
+
tiers={tiers} rarities={rarities}
|
|
96
|
+
onTier={(t) => setTiers((s) => toggle(s, t))} onRarity={(r) => setRarities((s) => toggle(s, r))}
|
|
97
|
+
onClear={() => { setTiers(new Set()); setRarities(new Set()); }}
|
|
98
|
+
/>
|
|
99
|
+
);
|
|
100
|
+
const note = (
|
|
101
|
+
<div style={{ fontSize: 9.5, color: '#777', lineHeight: 1.45, marginTop: 6 }}>
|
|
102
|
+
Rarity is how many mutual connections lead to someone. It never changes a score.{' '}
|
|
103
|
+
{fromLinkedIn
|
|
104
|
+
? 'LinkedIn’s own count where a scan saved it; otherwise the ways in from your scans.'
|
|
105
|
+
: `Counted from the ${scanned} ${scanned === 1 ? 'circle' : 'circles'} you’ve scanned, so it can only go up as you scan more.`}
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<div style={box}>
|
|
111
|
+
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 8 }}>
|
|
112
|
+
<Label>Their circle</Label>
|
|
113
|
+
<button type="button" onClick={() => setBig(true)} style={linkButton} aria-label="Open the large map">
|
|
114
|
+
Enlarge ⤢
|
|
115
|
+
</button>
|
|
116
|
+
</div>
|
|
117
|
+
<button type="button" onClick={() => setBig(true)} aria-label={`${person.name}'s circle: open the large map`} style={{
|
|
118
|
+
display: 'block', width: '100%', padding: 0, border: 'none', background: 'none', cursor: 'zoom-in',
|
|
119
|
+
}}>
|
|
120
|
+
<RingMap person={person} rings={rings} facts={facts} filter={filter} tierColors={tierColors} size={240} />
|
|
121
|
+
</button>
|
|
122
|
+
<RingCounts rings={rings} totals={totals} shown={filtering ? shown : null} />
|
|
123
|
+
{chips}
|
|
124
|
+
{note}
|
|
125
|
+
{big && (
|
|
126
|
+
<Enlarged onClose={() => setBig(false)} title={`${person.name}’s circle`}>
|
|
127
|
+
<RingMap person={person} rings={rings} facts={facts} filter={filter} tierColors={tierColors} size={640} labels
|
|
128
|
+
onPick={(row) => { setBig(false); onSelect?.(row); }} />
|
|
129
|
+
<RingCounts rings={rings} totals={totals} shown={filtering ? shown : null} />
|
|
130
|
+
{chips}
|
|
131
|
+
<Legend />
|
|
132
|
+
{note}
|
|
133
|
+
</Enlarged>
|
|
134
|
+
)}
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function RingMap({ person, rings, facts, filter, tierColors, size, labels = false, onPick }) {
|
|
140
|
+
const layout = useMemo(() => circleLayout(rings, size), [rings, size]);
|
|
141
|
+
const c = layout.center;
|
|
142
|
+
const k = size / 240;
|
|
143
|
+
return (
|
|
144
|
+
<svg viewBox={`0 0 ${size} ${size}`} width="100%" style={{ display: 'block', maxWidth: size, margin: '0 auto' }}
|
|
145
|
+
role="img" aria-label={`${person.name}'s circle, ${rings.length} ${rings.length === 1 ? 'ring' : 'rings'}`}>
|
|
146
|
+
{layout.rings.map((ring) => (
|
|
147
|
+
<g key={ring.degree}>
|
|
148
|
+
<circle cx={c.x} cy={c.y} r={ring.radius} fill="none" stroke="rgba(255,255,255,0.07)" strokeWidth={0.6 * k} />
|
|
149
|
+
<text x={c.x} y={c.y - ring.radius - ring.dotR - 3 * k} textAnchor="middle" fontSize={6.5 * k} fill="#666" fontWeight={700}>
|
|
150
|
+
D{ring.degree}
|
|
151
|
+
</text>
|
|
152
|
+
</g>
|
|
153
|
+
))}
|
|
154
|
+
{layout.spokes.map((s, i) => (
|
|
155
|
+
<line key={i} x1={s.x1} y1={s.y1} x2={s.x2} y2={s.y2} stroke="rgba(0,255,136,0.35)" strokeWidth={0.7 * k} />
|
|
156
|
+
))}
|
|
157
|
+
{layout.rings.map((ring) => ring.items.map((it) => {
|
|
158
|
+
const f = facts.get(it.key);
|
|
159
|
+
const on = passes(f, filter);
|
|
160
|
+
const color = tierColors[it.row.tier] || '#888';
|
|
161
|
+
const r = it.state === 'connected' ? ring.dotR * 1.7 : ring.dotR;
|
|
162
|
+
const tip = [
|
|
163
|
+
`${it.row.name} · ${it.row.tier}-tier${it.row.power_score != null ? ` · ${Number(it.row.power_score).toFixed(1)}` : ''}`,
|
|
164
|
+
it.state === 'connected' ? 'You added them' : it.state === 'requested' ? 'Request sent' : null,
|
|
165
|
+
f?.mutuals ? `${rarityInfo(f.rarity)?.label}: ${f.mutuals.count} mutual${f.mutuals.count === 1 ? '' : 's'}${f.mutuals.from === 'scans' ? ' (from your scans)' : ''}` : null,
|
|
166
|
+
].filter(Boolean).join('\n');
|
|
167
|
+
return (
|
|
168
|
+
<g key={it.key} opacity={on ? 1 : 0.12} style={onPick ? { cursor: 'pointer' } : undefined}
|
|
169
|
+
onClick={onPick ? (e) => { e.stopPropagation(); onPick(it.row); } : undefined}>
|
|
170
|
+
<title>{tip}</title>
|
|
171
|
+
{it.state === 'requested' ? (
|
|
172
|
+
<circle cx={it.x} cy={it.y} r={r * 1.25} fill="none" stroke={color} strokeWidth={Math.max(0.5, r * 0.45)}
|
|
173
|
+
strokeDasharray={`${Math.max(0.6, r * 0.55)} ${Math.max(0.5, r * 0.45)}`} />
|
|
174
|
+
) : (
|
|
175
|
+
<circle cx={it.x} cy={it.y} r={r} fill={color}
|
|
176
|
+
stroke={it.state === 'connected' ? GREEN : 'none'} strokeWidth={Math.max(0.6, r * 0.3)} />
|
|
177
|
+
)}
|
|
178
|
+
{labels && it.state === 'connected' && (
|
|
179
|
+
<text x={it.x} y={it.y - r - 3} textAnchor="middle" fontSize={9} fill={GREEN} fontWeight={700}>
|
|
180
|
+
{String(it.row.name || '').split(' ')[0]}
|
|
181
|
+
</text>
|
|
182
|
+
)}
|
|
183
|
+
</g>
|
|
184
|
+
);
|
|
185
|
+
}))}
|
|
186
|
+
<circle cx={c.x} cy={c.y} r={7 * k} fill={tierColors[person.tier] || '#888'} stroke="#fff" strokeWidth={0.8 * k} />
|
|
187
|
+
<text x={c.x} y={c.y + 2.4 * k} textAnchor="middle" fontSize={6.5 * k} fontWeight={800} fill={person.tier === 'S' ? '#000' : '#fff'}>
|
|
188
|
+
{String(person.name || '?').charAt(0)}
|
|
189
|
+
</text>
|
|
190
|
+
</svg>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function RingCounts({ rings, totals, shown }) {
|
|
195
|
+
return (
|
|
196
|
+
<div style={{ fontSize: 10.5, color: '#aaa', marginTop: 6, lineHeight: 1.5 }}>
|
|
197
|
+
{rings.map((r) => `D${r.degree} ${r.people.length.toLocaleString('en-US')}`).join(' · ')}
|
|
198
|
+
{totals.connected > 0 && <span style={{ color: GREEN }}> · you added {totals.connected}</span>}
|
|
199
|
+
{totals.requested > 0 && <span style={{ color: '#FFD700' }}> · {totals.requested} {totals.requested === 1 ? 'request' : 'requests'} out</span>}
|
|
200
|
+
{shown != null && <span style={{ color: '#fff' }}> · {shown.toLocaleString('en-US')} shown</span>}
|
|
201
|
+
</div>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function Filters({ tierColors, byTier, byRarity, tiers, rarities, onTier, onRarity, onClear }) {
|
|
206
|
+
const chip = (on, color) => ({
|
|
207
|
+
height: 22, padding: '0 7px', borderRadius: 11, cursor: 'pointer', fontSize: 10, fontWeight: 700,
|
|
208
|
+
whiteSpace: 'nowrap', border: `1px solid ${on ? color : 'rgba(255,255,255,0.1)'}`,
|
|
209
|
+
background: on ? `${color}22` : 'rgba(255,255,255,0.03)', color: on ? color : '#999',
|
|
210
|
+
});
|
|
211
|
+
return (
|
|
212
|
+
<div style={{ marginTop: 8, display: 'flex', flexDirection: 'column', gap: 5 }}>
|
|
213
|
+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, alignItems: 'center' }}>
|
|
214
|
+
<span style={rowLabel}>Tier</span>
|
|
215
|
+
{TIERS.map((t) => (
|
|
216
|
+
<button key={t} type="button" aria-pressed={tiers.has(t)} onClick={() => onTier(t)} style={chip(tiers.has(t), tierColors[t] || '#888')}>
|
|
217
|
+
{t} <span style={{ fontWeight: 500, opacity: 0.8 }}>{byTier[t]}</span>
|
|
218
|
+
</button>
|
|
219
|
+
))}
|
|
220
|
+
</div>
|
|
221
|
+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, alignItems: 'center' }}>
|
|
222
|
+
<span style={rowLabel}>Rarity</span>
|
|
223
|
+
{RARITY.map((r) => (
|
|
224
|
+
<button key={r.key} type="button" aria-pressed={rarities.has(r.key)} onClick={() => onRarity(r.key)}
|
|
225
|
+
title={`${r.label}: ${r.range} mutual connection${r.range === '1' ? '' : 's'}`} style={chip(rarities.has(r.key), r.color)}>
|
|
226
|
+
{r.label} <span style={{ fontWeight: 500, opacity: 0.8 }}>{byRarity[r.key]}</span>
|
|
227
|
+
</button>
|
|
228
|
+
))}
|
|
229
|
+
{(tiers.size > 0 || rarities.size > 0) && (
|
|
230
|
+
<button type="button" onClick={onClear} style={linkButton}>Clear</button>
|
|
231
|
+
)}
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function Legend() {
|
|
238
|
+
const item = (swatch, text) => (
|
|
239
|
+
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 5, marginRight: 12, whiteSpace: 'nowrap' }}>{swatch}{text}</span>
|
|
240
|
+
);
|
|
241
|
+
return (
|
|
242
|
+
<div style={{ fontSize: 10.5, color: '#999', marginTop: 8, lineHeight: 1.8 }}>
|
|
243
|
+
{item(<svg width="10" height="10"><circle cx="5" cy="5" r="4" fill="#FFD700" /></svg>, 'colour = tier')}
|
|
244
|
+
{item(<svg width="12" height="12"><circle cx="6" cy="6" r="4" fill="#9B59B6" stroke={GREEN} strokeWidth="1.5" /></svg>, 'you added them')}
|
|
245
|
+
{item(<svg width="12" height="12"><circle cx="6" cy="6" r="4.2" fill="none" stroke="#3498DB" strokeWidth="1.4" strokeDasharray="2 1.6" /></svg>, 'request sent')}
|
|
246
|
+
</div>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Rendered into <body>, not the card: the side panel's backdrop blur makes it
|
|
251
|
+
// the containing block for anything position: fixed inside it, so an overlay
|
|
252
|
+
// there only ever covered the panel. Fixed, so it can't change the page's
|
|
253
|
+
// size (TRAPS §29).
|
|
254
|
+
function Enlarged({ title, onClose, children }) {
|
|
255
|
+
useEffect(() => {
|
|
256
|
+
const onKey = (e) => { if (e.key === 'Escape') onClose(); };
|
|
257
|
+
window.addEventListener('keydown', onKey);
|
|
258
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
259
|
+
}, [onClose]);
|
|
260
|
+
return createPortal(
|
|
261
|
+
<div role="dialog" aria-modal="true" aria-label={title} onClick={onClose} style={{
|
|
262
|
+
position: 'fixed', inset: 0, zIndex: 300, background: 'rgba(0,0,0,0.78)',
|
|
263
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16,
|
|
264
|
+
}}>
|
|
265
|
+
<div onClick={(e) => e.stopPropagation()} style={{
|
|
266
|
+
width: 'min(720px, 100%)', maxHeight: '100%', overflowY: 'auto', boxSizing: 'border-box',
|
|
267
|
+
background: '#0f0c05', border: '1px solid rgba(255,215,0,0.18)', borderRadius: 14, padding: 16,
|
|
268
|
+
}}>
|
|
269
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
|
|
270
|
+
<div style={{ fontSize: 14, fontWeight: 800, color: '#fff' }}>{title}</div>
|
|
271
|
+
<button type="button" onClick={onClose} style={{ ...linkButton, fontSize: 12 }}>Close ✕</button>
|
|
272
|
+
</div>
|
|
273
|
+
<div style={{ fontSize: 10.5, color: '#888', marginBottom: 8 }}>Click a dot to open that person.</div>
|
|
274
|
+
{children}
|
|
275
|
+
</div>
|
|
276
|
+
</div>,
|
|
277
|
+
document.body,
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// The card's scan box (Sidebar.js CreateClusterCard, id "scan-box"): bring it
|
|
282
|
+
// into view, flash it, and put the keyboard on its button.
|
|
283
|
+
function toScanBox() {
|
|
284
|
+
const scanBox = document.getElementById('scan-box');
|
|
285
|
+
if (!scanBox) return;
|
|
286
|
+
scanBox.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
287
|
+
scanBox.animate?.([{ boxShadow: '0 0 0 3px rgba(255,215,0,0.75)' }, { boxShadow: '0 0 0 0 rgba(255,215,0,0)' }], { duration: 1600 });
|
|
288
|
+
scanBox.querySelector('button:not([disabled])')?.focus({ preventScroll: true });
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function Label({ children }) {
|
|
292
|
+
return <div style={{ fontSize: 10, fontWeight: 700, color: '#FF6B35', letterSpacing: 1, textTransform: 'uppercase', marginBottom: 6 }}>{children}</div>;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const box = {
|
|
296
|
+
background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.07)',
|
|
297
|
+
borderRadius: 10, padding: 10, marginBottom: 16,
|
|
298
|
+
};
|
|
299
|
+
const rowLabel = { fontSize: 9, color: '#666', textTransform: 'uppercase', letterSpacing: 0.5, width: 38, flexShrink: 0 };
|
|
300
|
+
const linkButton = { background: 'none', border: 'none', color: '#888', fontSize: 10.5, fontWeight: 600, cursor: 'pointer', padding: 0 };
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
// The sector picks, wherever you choose your field: Scores → Your sector
|
|
4
|
+
// (SectorSection.js) and the Scan page's question before the first scan
|
|
5
|
+
// (app/components/FieldStep.js). Your picks so far, then a search box, or the
|
|
6
|
+
// twelve broad industries, each opening to its narrower sectors from the
|
|
7
|
+
// directory (lib/sector-directory.js). Up to three picks. It only picks:
|
|
8
|
+
// saving is the page's (lib/settings-client.js).
|
|
9
|
+
|
|
10
|
+
import { useMemo, useState } from 'react';
|
|
11
|
+
import { Body, Btn, LINE } from '../ui';
|
|
12
|
+
import { INDUSTRIES } from '../../../lib/companies';
|
|
13
|
+
import { MAX_SECTORS } from '../../../lib/sector-focus';
|
|
14
|
+
import { DIRECTORY, SECTOR_KEYS, sectorByKey } from '../../../lib/sector-directory';
|
|
15
|
+
|
|
16
|
+
const plural = (n, one, many = `${one}s`) => `${n.toLocaleString()} ${n === 1 ? one : many}`;
|
|
17
|
+
|
|
18
|
+
// Each industry's narrower sectors, in the directory's order.
|
|
19
|
+
const SECTORS_OF = new Map(INDUSTRIES.map((g) => [g.key, DIRECTORY.filter((s) => s.group === g.key)]));
|
|
20
|
+
const GROUP_OF = new Map(DIRECTORY.map((s) => [s.key, s.group]));
|
|
21
|
+
// What the search box looks through: each sector's name, its words, jobs and
|
|
22
|
+
// companies, as they read (without the list's "(s)" and "$" marks), so
|
|
23
|
+
// "recruiter" finds HR & Recruiting.
|
|
24
|
+
const SEARCHABLE = DIRECTORY.map((s) => ({
|
|
25
|
+
sector: s,
|
|
26
|
+
label: s.label.toLowerCase(),
|
|
27
|
+
words: [...s.words, ...(s.roles || []), ...(s.names || []), ...s.companies].map((w) => ` ${w.replace(/\(s\)$|\$$/, '').toLowerCase()}`),
|
|
28
|
+
}));
|
|
29
|
+
// A sector of work every company has (HR & Recruiting, Legal…) counts firms in
|
|
30
|
+
// that line, not every company that employs someone doing it.
|
|
31
|
+
const FUNCTION_TITLE = 'Counts firms in this line of work, not every company with someone in the job';
|
|
32
|
+
|
|
33
|
+
/** Sectors (and whole industries) whose name has the query, or a word or company that starts with it. */
|
|
34
|
+
function searchSectors(query) {
|
|
35
|
+
const q = query.trim().toLowerCase().replace(/\s+/g, ' ');
|
|
36
|
+
if (!q) return null;
|
|
37
|
+
return {
|
|
38
|
+
industries: INDUSTRIES.filter((g) => g.label.toLowerCase().includes(q)),
|
|
39
|
+
sectors: SEARCHABLE.filter((s) => s.label.includes(q) || s.words.some((w) => w.includes(` ${q}`))).map((s) => s.sector),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* `sectors` are the picks (keys), `onChange(sectors)` gets the new list.
|
|
45
|
+
* `suggest` is "Suggested from your network" as Scores loads it
|
|
46
|
+
* (/api/settings/sector-suggestions); left out, there are none to show.
|
|
47
|
+
*/
|
|
48
|
+
export default function SectorPicker({ sectors, onChange, disabled, suggest }) {
|
|
49
|
+
const [query, setQuery] = useState('');
|
|
50
|
+
// Industries shown open; those holding a pick start open.
|
|
51
|
+
const [open, setOpen] = useState(() => new Set(sectors.map((k) => GROUP_OF.get(k) || k)));
|
|
52
|
+
|
|
53
|
+
const results = useMemo(() => searchSectors(query), [query]);
|
|
54
|
+
|
|
55
|
+
function toggle(key) {
|
|
56
|
+
const on = sectors.includes(key);
|
|
57
|
+
if (!on && sectors.length >= MAX_SECTORS) return;
|
|
58
|
+
const next = on ? sectors.filter((k) => k !== key) : SECTOR_KEYS.filter((k) => k === key || sectors.includes(k));
|
|
59
|
+
// A sector picked from the suggestions or a search shows up in its industry too.
|
|
60
|
+
if (!on && GROUP_OF.has(key)) setOpen((was) => new Set(was).add(GROUP_OF.get(key)));
|
|
61
|
+
onChange(next);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function flip(group) {
|
|
65
|
+
setOpen((was) => {
|
|
66
|
+
const next = new Set(was);
|
|
67
|
+
if (next.has(group)) next.delete(group); else next.add(group);
|
|
68
|
+
return next;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const full = sectors.length >= MAX_SECTORS;
|
|
73
|
+
// One chip for any pick: an industry or a sector, wherever it's shown.
|
|
74
|
+
const chip = (key, label, { note, title } = {}) => {
|
|
75
|
+
const on = sectors.includes(key);
|
|
76
|
+
return (
|
|
77
|
+
<Chip key={key} color={sectorByKey(key).color} on={on} blocked={!on && full} disabled={disabled} title={title}
|
|
78
|
+
onClick={() => toggle(key)}>
|
|
79
|
+
{label}{note && <span style={{ fontWeight: 400, color: on ? '#dfe7e7' : '#8b9a9a' }}>{note}</span>}
|
|
80
|
+
</Chip>
|
|
81
|
+
);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<>
|
|
86
|
+
<div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: 6 }}>
|
|
87
|
+
<span style={{ fontSize: 13, color: '#8b9a9a' }}>Your picks ({sectors.length} of {MAX_SECTORS}):</span>
|
|
88
|
+
{!sectors.length && <span style={{ fontSize: 13, color: '#667' }}>none yet</span>}
|
|
89
|
+
{sectors.map((k) => {
|
|
90
|
+
const s = sectorByKey(k);
|
|
91
|
+
return (
|
|
92
|
+
<button key={k} type="button" disabled={disabled} onClick={() => toggle(k)} aria-label={`Remove ${s.label}`}
|
|
93
|
+
style={{
|
|
94
|
+
display: 'flex', alignItems: 'center', gap: 6, padding: '4px 10px', borderRadius: 999, fontSize: 12.5,
|
|
95
|
+
fontWeight: 600, color: '#fff', background: `${s.color}33`, border: `1px solid ${s.color}`, cursor: 'pointer',
|
|
96
|
+
}}>
|
|
97
|
+
{s.label} <span aria-hidden="true" style={{ color: '#cfd8d8' }}>×</span>
|
|
98
|
+
</button>
|
|
99
|
+
);
|
|
100
|
+
})}
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
{suggest && <Suggestions state={suggest} chip={chip} />}
|
|
104
|
+
|
|
105
|
+
<label htmlFor="sector-search" style={{ display: 'block', fontSize: 13, color: '#8b9a9a', margin: '16px 0 6px' }}>
|
|
106
|
+
Find a sector
|
|
107
|
+
</label>
|
|
108
|
+
<div style={{ display: 'flex', gap: 8 }}>
|
|
109
|
+
<input id="sector-search" type="search" value={query} onChange={(e) => setQuery(e.target.value)}
|
|
110
|
+
placeholder="Try dentist, bakery or Stripe"
|
|
111
|
+
autoComplete="off" spellCheck={false}
|
|
112
|
+
style={{
|
|
113
|
+
flex: 1, minWidth: 0, padding: '8px 12px', borderRadius: 7, fontSize: 14, color: '#fff',
|
|
114
|
+
background: 'rgba(255,255,255,0.05)', border: LINE, outline: 'none',
|
|
115
|
+
}} />
|
|
116
|
+
{query && <Btn onClick={() => setQuery('')}>Clear</Btn>}
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
{results ? (
|
|
120
|
+
<div style={{ marginTop: 10 }} aria-live="polite">
|
|
121
|
+
{results.industries.length + results.sectors.length === 0 ? (
|
|
122
|
+
<Body style={{ fontSize: 12.5 }}>
|
|
123
|
+
Nothing in the list matches “{query.trim()}”. Try a job title, a kind of business or a company, or clear
|
|
124
|
+
the search and pick one of the twelve broad industries.
|
|
125
|
+
</Body>
|
|
126
|
+
) : (
|
|
127
|
+
<div role="group" aria-label="Sectors that match your search" style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
|
128
|
+
{results.industries.map((g) => chip(g.key, g.label, { note: ' · the whole industry' }))}
|
|
129
|
+
{results.sectors.map((s) => chip(s.key, s.label, { title: s.kind === 'function' ? FUNCTION_TITLE : `In ${sectorByKey(s.group).label}` }))}
|
|
130
|
+
</div>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
) : (
|
|
134
|
+
<div style={{ marginTop: 10 }}>
|
|
135
|
+
{INDUSTRIES.map((g) => {
|
|
136
|
+
const inIndustry = SECTORS_OF.get(g.key);
|
|
137
|
+
const isOpen = open.has(g.key);
|
|
138
|
+
const inside = inIndustry.filter((s) => sectors.includes(s.key)).length;
|
|
139
|
+
return (
|
|
140
|
+
<div key={g.key} style={{ borderTop: LINE, padding: '8px 0' }}>
|
|
141
|
+
<div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: 8 }}>
|
|
142
|
+
{chip(g.key, g.label, { title: 'The whole industry: companies its sectors place, or whose name or the well-known list says so' })}
|
|
143
|
+
<button type="button" aria-expanded={isOpen} aria-controls={`sectors-${g.key}`} onClick={() => flip(g.key)}
|
|
144
|
+
style={{
|
|
145
|
+
padding: '4px 6px', fontSize: 12.5, color: inside ? '#9fd3ff' : '#8b9a9a', background: 'none', border: 'none',
|
|
146
|
+
cursor: 'pointer',
|
|
147
|
+
}}>
|
|
148
|
+
{isOpen ? '▾' : '▸'} {plural(inIndustry.length, 'sector')}{inside ? ` · ${inside} picked` : ''}
|
|
149
|
+
</button>
|
|
150
|
+
</div>
|
|
151
|
+
{isOpen && (
|
|
152
|
+
<div id={`sectors-${g.key}`} role="group" aria-label={`Sectors in ${g.label}`}
|
|
153
|
+
style={{ display: 'flex', flexWrap: 'wrap', gap: 8, margin: '8px 0 2px 14px' }}>
|
|
154
|
+
{inIndustry.map((s) => chip(s.key, s.label, s.kind === 'function' ? { title: FUNCTION_TITLE } : undefined))}
|
|
155
|
+
</div>
|
|
156
|
+
)}
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
})}
|
|
160
|
+
</div>
|
|
161
|
+
)}
|
|
162
|
+
</>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** A pick you can turn on and off: an industry or a sector, with its industry's colour. */
|
|
167
|
+
function Chip({ color, on, blocked, disabled, title, onClick, children }) {
|
|
168
|
+
return (
|
|
169
|
+
<button type="button" aria-pressed={on} disabled={blocked || disabled} onClick={onClick}
|
|
170
|
+
title={blocked ? `Up to ${MAX_SECTORS} sectors. Turn one off first.` : title}
|
|
171
|
+
style={{
|
|
172
|
+
display: 'flex', alignItems: 'center', gap: 7, padding: '6px 12px', borderRadius: 999, maxWidth: '100%',
|
|
173
|
+
fontSize: 12.5, fontWeight: 600, textAlign: 'left', cursor: blocked ? 'not-allowed' : 'pointer',
|
|
174
|
+
color: on ? '#fff' : blocked ? '#556' : '#b8c4c4',
|
|
175
|
+
background: on ? `${color}33` : 'rgba(255,255,255,0.04)',
|
|
176
|
+
border: on ? `1px solid ${color}` : LINE,
|
|
177
|
+
}}>
|
|
178
|
+
<span style={{ width: 8, height: 8, borderRadius: '50%', background: color, opacity: blocked ? 0.4 : 1, flexShrink: 0 }} />
|
|
179
|
+
<span>{children}</span>
|
|
180
|
+
</button>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** "Suggested from your network": the directory's sectors most of your scanned people work in. */
|
|
185
|
+
function Suggestions({ state, chip }) {
|
|
186
|
+
const note = { fontSize: 12.5, marginTop: 10 };
|
|
187
|
+
if (state.loading) return <Body style={note}>Looking through your network for suggestions…</Body>;
|
|
188
|
+
if (state.error) return <Body style={note}>Couldn’t load suggestions ({state.error}). You can still pick below.</Body>;
|
|
189
|
+
if (!state.scored) return <Body style={note}>Suggestions appear here once you’ve scanned your network.</Body>;
|
|
190
|
+
if (!state.list.length) return <Body style={note}>None of the list’s sectors stands out among the people you’ve scanned yet.</Body>;
|
|
191
|
+
return (
|
|
192
|
+
<div style={{ marginTop: 14 }}>
|
|
193
|
+
<div style={{ fontSize: 13, color: '#8b9a9a', marginBottom: 6 }}>Suggested from your network</div>
|
|
194
|
+
<div role="group" aria-label="Suggested from your network" style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
|
195
|
+
{state.list.map((s) => chip(s.key, s.label, {
|
|
196
|
+
note: `: ${plural(s.people, 'person', 'people')} at ${plural(s.companies, 'company', 'companies')}`,
|
|
197
|
+
title: `In ${sectorByKey(s.group).label}`,
|
|
198
|
+
}))}
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
);
|
|
202
|
+
}
|