six-degrees 0.4.2 → 0.4.4
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 +3 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +41 -13
- package/.next/standalone/.next/routes-manifest.json +12 -0
- 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 +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 +19 -17
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +19 -17
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +15 -13
- 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.nft.json +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.nft.json +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.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.nft.json +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.nft.json +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.nft.json +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.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/social/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/social/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/social/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/social/route.js +8 -0
- package/.next/standalone/.next/server/app/api/social/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/social/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/social/route_client-reference-manifest.js +3 -0
- 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.nft.json +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 +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 +23 -21
- package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +23 -21
- 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 +17 -15
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +21 -19
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +17 -15
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +21 -19
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/launch/page.js +1 -1
- 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 +23 -21
- package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +23 -21
- 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 +17 -15
- package/.next/standalone/.next/server/app/page.js +1 -1
- 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 +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 +23 -21
- package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +23 -21
- 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 +17 -15
- package/.next/standalone/.next/server/app/profile/page.js +1 -1
- 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 +23 -21
- package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +23 -21
- 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 +17 -15
- package/.next/standalone/.next/server/app/queue/page.js +1 -1
- 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 +23 -21
- package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +23 -21
- 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 +17 -15
- package/.next/standalone/.next/server/app/scores/page.js +1 -1
- package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/scores.html +1 -1
- package/.next/standalone/.next/server/app/scores.rsc +23 -21
- package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +23 -21
- package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +17 -15
- package/.next/standalone/.next/server/app/settings/page.js +1 -1
- 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 +23 -21
- package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +23 -21
- 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 +17 -15
- package/.next/standalone/.next/server/app/setup/page.js +1 -1
- 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 +23 -21
- package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +23 -21
- 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 +17 -15
- package/.next/standalone/.next/server/app/social/page/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/social/page/build-manifest.json +20 -0
- package/.next/standalone/.next/server/app/social/page/next-font-manifest.json +11 -0
- package/.next/standalone/.next/server/app/social/page/react-loadable-manifest.json +1 -0
- package/.next/standalone/.next/server/app/social/page/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/social/page.js +13 -0
- package/.next/standalone/.next/server/app/social/page.js.map +5 -0
- package/.next/standalone/.next/server/app/social/page.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/social/page_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/social.html +1 -0
- package/.next/standalone/.next/server/app/social.meta +26 -0
- package/.next/standalone/.next/server/app/social.rsc +26 -0
- package/.next/standalone/.next/server/app/social.segments/_full.segment.rsc +26 -0
- package/.next/standalone/.next/server/app/social.segments/_tree.segment.rsc +4 -0
- package/.next/standalone/.next/server/app/social.segments/social/__PAGE__.segment.rsc +30 -0
- package/.next/standalone/.next/server/app-paths-manifest.json +3 -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]__17kn1mk._.js +8 -0
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0uag-k5._.js → [root-of-the-server]__1dmc7-t._.js} +1 -1
- package/.next/standalone/.next/server/chunks/_1_fynia._.js +5 -5
- package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1xh07bp._.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_social_route_actions_1_3c6qn.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__16hs8kw._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1feicqq._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1jeztd-._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_005q9n1._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_08_03wh._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_08cfj2b._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0j4-lc3._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_0hw_qnj._.js → _0wxbq78._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0xkur4p._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_15x6q32._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_1cyxocj._.js → _1ap1tu_._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_1mj739k._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_social_page_actions_176i1kl.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +3 -3
- package/.next/standalone/.next/server/chunks/ssr/app_scores_page_09wepvv.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_social_page_0h7zxda.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_0i8v-s3._.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/04a3zockahetx.js +1 -0
- package/.next/standalone/.next/static/chunks/0a5iadnmt9u9k.js +1 -0
- package/.next/standalone/.next/static/chunks/{2p6o90z0vgcwr.css → 0bo1dxn5gwzpk.css} +1 -1
- package/.next/standalone/.next/static/chunks/0g_p4p7oci18u.js +1 -0
- package/.next/standalone/.next/static/chunks/{0ax_9zzboyv6d.js → 17-rhp8sg4m1b.js} +2 -2
- package/.next/standalone/.next/static/chunks/1ra4cwcp75tfa.js +1 -0
- package/.next/standalone/.next/static/chunks/236f0jtp68j_h.js +1 -0
- package/.next/standalone/.next/static/chunks/24m745q_85u34.js +6 -0
- package/.next/standalone/.next/static/chunks/2_wcobfqry2uj.js +1 -0
- package/.next/standalone/.next/static/chunks/2dwtoszeq91us.js +1 -0
- package/.next/standalone/.next/static/chunks/2hvcevue9aro4.js +1 -0
- package/.next/standalone/.next/static/chunks/2uo9i03_05rqi.js +18 -0
- package/.next/standalone/.next/static/chunks/3p_pwu5gx2i9h.js +1 -0
- package/.next/standalone/.next/static/chunks/3yc3rzp4dxvyo.js +1 -0
- package/.next/standalone/.next/static/chunks/3zw8bv7-mc4n6.js +33 -0
- package/.next/standalone/CHANGELOG.md +63 -0
- package/.next/standalone/app/api/network/route.js +11 -0
- package/.next/standalone/app/api/scraper/route.js +19 -0
- package/.next/standalone/app/api/social/route.js +96 -0
- package/.next/standalone/app/components/AutoScanButton.js +41 -0
- package/.next/standalone/app/components/ChainView.js +7 -0
- package/.next/standalone/app/components/FilterPanel.js +37 -5
- package/.next/standalone/app/components/OrbitGraph.js +17 -4
- package/.next/standalone/app/components/ScanStatusBar.js +96 -0
- package/.next/standalone/app/components/SeparationView.js +9 -1
- package/.next/standalone/app/components/Sidebar.js +88 -26
- package/.next/standalone/app/components/SocialAutoSync.js +34 -0
- package/.next/standalone/app/components/settings/NetworkHealthSection.js +85 -0
- package/.next/standalone/app/globals.css +4 -0
- package/.next/standalone/app/layout.js +6 -0
- package/.next/standalone/app/page.js +37 -4
- package/.next/standalone/app/scores/page.js +3 -0
- package/.next/standalone/app/setup/page.js +13 -1
- package/.next/standalone/app/social/page.js +293 -0
- package/.next/standalone/lib/experimental-client.js +38 -0
- package/.next/standalone/lib/insights.js +54 -0
- package/.next/standalone/lib/linkedin-export.js +156 -0
- package/.next/standalone/lib/network-health.js +42 -0
- package/.next/standalone/lib/network.js +1 -0
- package/.next/standalone/lib/scraper-client.js +1 -0
- package/.next/standalone/package-lock.json +2 -2
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/scripts/scrape.py +335 -5
- package/.next/static/chunks/04a3zockahetx.js +1 -0
- package/.next/static/chunks/0a5iadnmt9u9k.js +1 -0
- package/.next/static/chunks/{2p6o90z0vgcwr.css → 0bo1dxn5gwzpk.css} +1 -1
- package/.next/static/chunks/0g_p4p7oci18u.js +1 -0
- package/.next/static/chunks/{0ax_9zzboyv6d.js → 17-rhp8sg4m1b.js} +2 -2
- package/.next/static/chunks/1ra4cwcp75tfa.js +1 -0
- package/.next/static/chunks/236f0jtp68j_h.js +1 -0
- package/.next/static/chunks/24m745q_85u34.js +6 -0
- package/.next/static/chunks/2_wcobfqry2uj.js +1 -0
- package/.next/static/chunks/2dwtoszeq91us.js +1 -0
- package/.next/static/chunks/2hvcevue9aro4.js +1 -0
- package/.next/static/chunks/2uo9i03_05rqi.js +18 -0
- package/.next/static/chunks/3p_pwu5gx2i9h.js +1 -0
- package/.next/static/chunks/3yc3rzp4dxvyo.js +1 -0
- package/.next/static/chunks/3zw8bv7-mc4n6.js +33 -0
- package/CHANGELOG.md +63 -0
- package/lib/experimental-client.js +38 -0
- package/lib/insights.js +54 -0
- package/lib/linkedin-export.js +156 -0
- package/lib/network-health.js +42 -0
- package/lib/network.js +1 -0
- package/lib/scraper-client.js +1 -0
- package/package.json +1 -1
- package/scripts/scrape.py +335 -5
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h1_f_2._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19ip1qq._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_06w_gl4._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_13kzrwv._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_15xuwkv._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1hgd5dp._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1vpfyd-._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_20w06i-._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/lib_1bn38gs._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/node_modules_1f8t_rp._.js +0 -23
- package/.next/standalone/.next/static/chunks/049vavjsmqgmn.js +0 -33
- package/.next/standalone/.next/static/chunks/0cuuo2jwc9h94.js +0 -1
- package/.next/standalone/.next/static/chunks/0ljxa-wk_iu__.js +0 -18
- package/.next/standalone/.next/static/chunks/0mp9b72fm-jp6.js +0 -6
- package/.next/standalone/.next/static/chunks/1pre1cotntk6-.js +0 -1
- package/.next/standalone/.next/static/chunks/22x2_r768-u5i.js +0 -1
- package/.next/standalone/.next/static/chunks/28bu26ausqzmn.js +0 -1
- package/.next/standalone/.next/static/chunks/2jz311xl1pp_8.js +0 -1
- package/.next/standalone/.next/static/chunks/2ot61qsv6_0eq.js +0 -1
- package/.next/standalone/.next/static/chunks/2xjho0lroiwlq.js +0 -1
- package/.next/standalone/.next/static/chunks/3zs_6-nn0dapb.js +0 -1
- package/.next/static/chunks/049vavjsmqgmn.js +0 -33
- package/.next/static/chunks/0cuuo2jwc9h94.js +0 -1
- package/.next/static/chunks/0ljxa-wk_iu__.js +0 -18
- package/.next/static/chunks/0mp9b72fm-jp6.js +0 -6
- package/.next/static/chunks/1pre1cotntk6-.js +0 -1
- package/.next/static/chunks/22x2_r768-u5i.js +0 -1
- package/.next/static/chunks/28bu26ausqzmn.js +0 -1
- package/.next/static/chunks/2jz311xl1pp_8.js +0 -1
- package/.next/static/chunks/2ot61qsv6_0eq.js +0 -1
- package/.next/static/chunks/2xjho0lroiwlq.js +0 -1
- package/.next/static/chunks/3zs_6-nn0dapb.js +0 -1
- /package/.next/standalone/.next/static/{wGVrXBkFLmEB4snfhtOcO → VE8LeJXfD2H0_OgJxuJcm}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{wGVrXBkFLmEB4snfhtOcO → VE8LeJXfD2H0_OgJxuJcm}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{wGVrXBkFLmEB4snfhtOcO → VE8LeJXfD2H0_OgJxuJcm}/_ssgManifest.js +0 -0
- /package/.next/static/{wGVrXBkFLmEB4snfhtOcO → VE8LeJXfD2H0_OgJxuJcm}/_buildManifest.js +0 -0
- /package/.next/static/{wGVrXBkFLmEB4snfhtOcO → VE8LeJXfD2H0_OgJxuJcm}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{wGVrXBkFLmEB4snfhtOcO → VE8LeJXfD2H0_OgJxuJcm}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
// Social (experimental): what your own LinkedIn data says about your
|
|
4
|
+
// relationships (lib/linkedin-export.js). Two sources, both yours:
|
|
5
|
+
// - the export LinkedIn emails you (Settings → Data privacy → Get a copy of
|
|
6
|
+
// your data), read here in the browser: full history, no traffic to LinkedIn;
|
|
7
|
+
// - an experimental live sync (the scanner's --messages): your messages list,
|
|
8
|
+
// read once in your own Chrome, by hand or once a day.
|
|
9
|
+
// Kept on this computer: dates, who wrote last and counts. Never message text.
|
|
10
|
+
|
|
11
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
12
|
+
import Link from 'next/link';
|
|
13
|
+
import OnboardingGate from '../components/OnboardingGate';
|
|
14
|
+
import { useUser } from '../components/UserProvider';
|
|
15
|
+
import { loadNetwork } from '../../lib/network';
|
|
16
|
+
import { readTable, messageStats, careerChapters, postingEffect, invitationSplit, warmthOf, repliesWaiting } from '../../lib/linkedin-export';
|
|
17
|
+
import { keyFor } from '../../lib/separation';
|
|
18
|
+
import { runScrape, watchScanner, scannerNow } from '../../lib/scraper-client';
|
|
19
|
+
import { Body, LINE, FONT } from '../components/ui';
|
|
20
|
+
import { IS_DEMO } from '../../lib/demo';
|
|
21
|
+
|
|
22
|
+
const TIER = { S: '#FFD700', A: '#9B59B6', B: '#3498DB', C: '#95A5A6', D: '#BDC3C7' };
|
|
23
|
+
const WARMTH = { warm: ['Warm', '#ff9f43'], cool: ['Cool', '#3498DB'], dormant: ['Dormant', '#8899aa'], never: ['Never messaged', '#556'] };
|
|
24
|
+
const FILES = { 'connections.csv': 'first name', 'messages.csv': 'conversation id', 'invitations.csv': 'direction', 'positions.csv': 'company name', 'shares.csv': 'date' };
|
|
25
|
+
const day = (t) => (t ? new Date(t).toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric' }) : '');
|
|
26
|
+
const slug = (u) => String(u || '').replace(/\/+$/, '').split('/').pop();
|
|
27
|
+
|
|
28
|
+
export default function SocialPage() {
|
|
29
|
+
return <OnboardingGate><SocialInner /></OnboardingGate>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function SocialInner() {
|
|
33
|
+
const { userId } = useUser();
|
|
34
|
+
const [social, setSocial] = useState(undefined);
|
|
35
|
+
const [net, setNet] = useState(null);
|
|
36
|
+
const [keepEmails, setKeepEmails] = useState(false);
|
|
37
|
+
const [note, setNote] = useState(null);
|
|
38
|
+
const [busy, setBusy] = useState(false);
|
|
39
|
+
const [job, setJob] = useState(() => scannerNow());
|
|
40
|
+
|
|
41
|
+
const reload = () => fetch('/api/social').then((r) => r.json()).then((d) => setSocial(d.social || null)).catch(() => setSocial(null));
|
|
42
|
+
useEffect(() => { reload(); }, []);
|
|
43
|
+
useEffect(() => watchScanner(() => setJob(scannerNow())), []);
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (!userId) return undefined;
|
|
46
|
+
let live = true;
|
|
47
|
+
loadNetwork(userId).then((n) => { if (live) setNet(n); }).catch(() => {});
|
|
48
|
+
return () => { live = false; };
|
|
49
|
+
}, [userId]);
|
|
50
|
+
|
|
51
|
+
// Everyone you know, by profile: name and tier for the lists below.
|
|
52
|
+
const byKey = useMemo(() => {
|
|
53
|
+
const m = new Map();
|
|
54
|
+
for (const r of [...(net?.degree1 || []), ...(net?.degree2 || [])]) if (!m.has(keyFor(r))) m.set(keyFor(r), r);
|
|
55
|
+
return m;
|
|
56
|
+
}, [net]);
|
|
57
|
+
|
|
58
|
+
async function importFolder(list) {
|
|
59
|
+
setBusy(true);
|
|
60
|
+
setNote(null);
|
|
61
|
+
try {
|
|
62
|
+
const found = {};
|
|
63
|
+
for (const f of Array.from(list || [])) {
|
|
64
|
+
const name = f.name.toLowerCase();
|
|
65
|
+
if (FILES[name] && !found[name]) found[name] = readTable(await f.text(), FILES[name]);
|
|
66
|
+
}
|
|
67
|
+
if (!Object.keys(found).length) throw new Error('That folder doesn’t have LinkedIn’s export files in it (Connections.csv, messages.csv and so on).');
|
|
68
|
+
const conns = found['connections.csv'] || [];
|
|
69
|
+
const stats = found['messages.csv'] ? messageStats(found['messages.csv']) : { asOf: null, people: new Map() };
|
|
70
|
+
const people = {};
|
|
71
|
+
for (const [url, v] of stats.people) people[url] = { last: v.last, lastFromThem: v.lastFromThem, total: v.total, recent: v.recent };
|
|
72
|
+
if (keepEmails) {
|
|
73
|
+
for (const c of conns) {
|
|
74
|
+
const e = c['email address'];
|
|
75
|
+
if (!e) continue;
|
|
76
|
+
const k = keyFor({ profile_url: c.url });
|
|
77
|
+
people[k] = { ...(people[k] || {}), email: e };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const asOf = stats.asOf || Date.now();
|
|
81
|
+
const body = {
|
|
82
|
+
asOf,
|
|
83
|
+
people,
|
|
84
|
+
chapters: careerChapters(found['positions.csv'] || [], conns, asOf),
|
|
85
|
+
posts: found['shares.csv'] ? postingEffect(found['shares.csv'], conns) : null,
|
|
86
|
+
invites: found['invitations.csv'] ? invitationSplit(found['invitations.csv']) : null,
|
|
87
|
+
emails: keepEmails,
|
|
88
|
+
};
|
|
89
|
+
const r = await fetch('/api/social', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
90
|
+
if (!r.ok) throw new Error((await r.json().catch(() => ({}))).error || 'It couldn’t be saved.');
|
|
91
|
+
setNote({ good: true, text: `Read ${Object.keys(found).length} of LinkedIn’s files: ${stats.people.size.toLocaleString()} people you’ve messaged one to one.` });
|
|
92
|
+
await reload();
|
|
93
|
+
} catch (e) {
|
|
94
|
+
setNote({ good: false, text: e.message });
|
|
95
|
+
} finally {
|
|
96
|
+
setBusy(false);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function syncNow() {
|
|
101
|
+
setNote(null);
|
|
102
|
+
try {
|
|
103
|
+
const end = await runScrape('messages');
|
|
104
|
+
setNote(end?.exitCode === 0 ? { good: true, text: 'Your messages list is read.' } : { good: false, text: 'The sync stopped before it finished. The Scan page has the log.' });
|
|
105
|
+
await reload();
|
|
106
|
+
} catch (e) {
|
|
107
|
+
setNote({ good: false, text: e.message });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function setAuto(on) {
|
|
112
|
+
await fetch('/api/social', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ autoSync: on }) });
|
|
113
|
+
reload();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function forget() {
|
|
117
|
+
await fetch('/api/social', { method: 'DELETE' });
|
|
118
|
+
setNote({ good: true, text: 'Forgotten. Nothing from your export or messages is kept here now.' });
|
|
119
|
+
reload();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// What each person's messages say, the export and the live sync together.
|
|
123
|
+
const merged = useMemo(() => {
|
|
124
|
+
const out = new Map();
|
|
125
|
+
for (const [url, v] of Object.entries(social?.people || {})) out.set(keyFor({ profile_url: url }), { ...v });
|
|
126
|
+
for (const [url, v] of Object.entries(social?.live || {})) {
|
|
127
|
+
const k = keyFor({ profile_url: url });
|
|
128
|
+
const cur = out.get(k) || { total: 0, recent: 0 };
|
|
129
|
+
if (v.last && (!cur.last || v.last > cur.last)) cur.last = v.last;
|
|
130
|
+
cur.unread = v.unread;
|
|
131
|
+
cur.total = Math.max(cur.total || 0, 1);
|
|
132
|
+
out.set(k, cur);
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}, [social]);
|
|
136
|
+
// "Now" for warmth is the newest thing the data knows, not the clock: an older export reads as it was.
|
|
137
|
+
const asOf = Math.max(social?.asOf || 0, social?.liveAt ? Date.parse(social.liveAt) : 0, ...[...merged.values()].map((v) => v.last || 0));
|
|
138
|
+
|
|
139
|
+
const warmth = useMemo(() => {
|
|
140
|
+
const counts = { warm: 0, cool: 0, dormant: 0, never: 0 };
|
|
141
|
+
for (const c of net?.degree1 || []) counts[warmthOf(merged.get(keyFor(c)), asOf)] += 1;
|
|
142
|
+
return counts;
|
|
143
|
+
}, [net, merged, asOf]);
|
|
144
|
+
|
|
145
|
+
const waiting = useMemo(() => {
|
|
146
|
+
const stats = { asOf, people: new Map([...merged].filter(([, v]) => v.lastFromThem != null)) };
|
|
147
|
+
const list = repliesWaiting(stats).map((w) => ({ ...w, person: byKey.get(w.url) }));
|
|
148
|
+
for (const [k, v] of merged) {
|
|
149
|
+
if (v.unread > 0 && !list.some((w) => w.url === k)) list.push({ url: k, unread: v.unread, person: byKey.get(k) });
|
|
150
|
+
}
|
|
151
|
+
const rank = (w) => ({ S: 0, A: 1 }[w.person?.tier] ?? 2);
|
|
152
|
+
return list.sort((a, b) => rank(a) - rank(b) || (a.days ?? 0) - (b.days ?? 0)).slice(0, 15);
|
|
153
|
+
}, [merged, asOf, byKey]);
|
|
154
|
+
|
|
155
|
+
if (IS_DEMO) return <div style={{ minHeight: '100vh', background: '#0a0a1a', color: '#aaa', padding: 40, fontFamily: FONT }}>Not part of the demo.</div>;
|
|
156
|
+
const running = job?.running;
|
|
157
|
+
const box = { padding: '12px 14px', borderRadius: 8, border: LINE, background: 'rgba(255,255,255,0.03)', marginTop: 10 };
|
|
158
|
+
const h2 = { fontSize: 16, margin: '24px 0 6px' };
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<div style={{ minHeight: '100vh', background: '#0a0a1a', color: '#fff', fontFamily: FONT }}>
|
|
162
|
+
<header style={{ padding: '16px 24px', borderBottom: LINE, display: 'flex', alignItems: 'center', gap: 16 }}>
|
|
163
|
+
<Link href="/" style={{ color: '#888', textDecoration: 'none', fontSize: 13, fontWeight: 600, padding: '6px 14px', borderRadius: 6, background: 'rgba(255,255,255,0.06)', border: LINE }}>← Back to Map</Link>
|
|
164
|
+
<h1 style={{ fontSize: 22, fontWeight: 700, margin: 0, color: '#FFD700' }}>Social <span style={{ fontSize: 12, color: '#8b9a9a', fontWeight: 600 }}>experimental</span></h1>
|
|
165
|
+
</header>
|
|
166
|
+
<main style={{ maxWidth: 980, margin: '0 auto', padding: '8px 24px 64px' }}>
|
|
167
|
+
<Body style={{ marginTop: 16 }}>
|
|
168
|
+
Who you’re warm with, who’s waiting on a reply, and how each chapter of your career built your network,
|
|
169
|
+
from your own LinkedIn data. Only dates, who wrote last and counts are kept on this computer, never what a
|
|
170
|
+
message says.
|
|
171
|
+
</Body>
|
|
172
|
+
|
|
173
|
+
<h2 style={h2}>Your LinkedIn export</h2>
|
|
174
|
+
<div style={box}>
|
|
175
|
+
<Body style={{ margin: 0 }}>
|
|
176
|
+
On LinkedIn: <i>Settings → Data privacy → Get a copy of your data</i>, pick the larger archive, and unzip the
|
|
177
|
+
file it emails you. Then choose that folder here. It’s read in this window; nothing goes to LinkedIn.
|
|
178
|
+
</Body>
|
|
179
|
+
<label style={{ display: 'flex', gap: 8, alignItems: 'flex-start', fontSize: 12.5, color: '#b8c4c4', marginTop: 10 }}>
|
|
180
|
+
<input type="checkbox" checked={keepEmails} onChange={(e) => setKeepEmails(e.target.checked)} style={{ marginTop: 3 }} />
|
|
181
|
+
<span>Also keep email addresses from it, on this computer only, for one-click email.</span>
|
|
182
|
+
</label>
|
|
183
|
+
<div style={{ marginTop: 10, display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap' }}>
|
|
184
|
+
<label style={{ padding: '7px 14px', borderRadius: 8, background: 'rgba(52,152,219,0.18)', color: '#cfe6f7', cursor: busy ? 'default' : 'pointer', fontSize: 13 }}>
|
|
185
|
+
{busy ? 'Reading…' : social ? 'Choose a newer export' : 'Choose the export folder'}
|
|
186
|
+
<input type="file" webkitdirectory="" directory="" multiple disabled={busy} style={{ display: 'none' }} onChange={(e) => importFolder(e.target.files)} />
|
|
187
|
+
</label>
|
|
188
|
+
{social && <span style={{ fontSize: 12, color: '#8b9a9a' }}>From an export up to {day(social.asOf)}{social.liveAt ? ` · messages synced ${day(Date.parse(social.liveAt))}` : ''}</span>}
|
|
189
|
+
{social && <button onClick={forget} style={{ background: 'none', border: 'none', color: '#ff9b9b', cursor: 'pointer', fontSize: 12 }}>Forget it</button>}
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<h2 style={h2}>Live messages sync <span style={{ fontSize: 11, color: '#FFD700' }}>experimental</span></h2>
|
|
194
|
+
<div style={box}>
|
|
195
|
+
<Body style={{ margin: 0 }}>
|
|
196
|
+
Reads your messages list once, in your own Chrome: who each one-to-one conversation is with, when it was last
|
|
197
|
+
active and whether it’s unread. It uses no search budget and never opens or keeps a message’s text.
|
|
198
|
+
</Body>
|
|
199
|
+
<div style={{ marginTop: 10, display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap' }}>
|
|
200
|
+
<button onClick={syncNow} disabled={running} style={{ padding: '7px 14px', borderRadius: 8, border: 'none', background: running ? '#333' : 'rgba(0,255,136,0.15)', color: running ? '#777' : '#00ff88', cursor: running ? 'default' : 'pointer', fontSize: 13 }}>
|
|
201
|
+
{running ? 'The scanner is busy' : 'Sync messages now'}
|
|
202
|
+
</button>
|
|
203
|
+
<label style={{ display: 'flex', gap: 6, alignItems: 'center', fontSize: 12.5, color: '#b8c4c4' }}>
|
|
204
|
+
<input type="checkbox" checked={social?.autoSync === true} onChange={(e) => setAuto(e.target.checked)} />
|
|
205
|
+
Once a day while the app is open (daytime only)
|
|
206
|
+
</label>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
{note && <div style={{ marginTop: 10, fontSize: 13, color: note.good ? '#00ff88' : '#ff9b9b' }}>{note.text}</div>}
|
|
210
|
+
|
|
211
|
+
{social && (
|
|
212
|
+
<>
|
|
213
|
+
<h2 style={h2}>Warmth</h2>
|
|
214
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', gap: 10 }}>
|
|
215
|
+
{Object.entries(WARMTH).map(([k, [label, color]]) => (
|
|
216
|
+
<div key={k} style={{ ...box, marginTop: 0 }}>
|
|
217
|
+
<div style={{ fontSize: 22, fontWeight: 700, color }}>{(warmth[k] || 0).toLocaleString()}</div>
|
|
218
|
+
<div style={{ fontSize: 12.5, color: '#b8c4c4' }}>{label}</div>
|
|
219
|
+
</div>
|
|
220
|
+
))}
|
|
221
|
+
</div>
|
|
222
|
+
<div style={{ fontSize: 11.5, color: '#778', marginTop: 6 }}>
|
|
223
|
+
Of your connections. Warm: messaged in the last 30 days, or 3+ messages in 90. Cool: within a year. Dormant: longer ago.
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
<h2 style={h2}>Waiting on you</h2>
|
|
227
|
+
<div style={{ ...box, padding: 0 }}>
|
|
228
|
+
{waiting.length === 0 && <div style={{ padding: 12, fontSize: 13, color: '#8b9a9a' }}>No one is waiting on a reply.</div>}
|
|
229
|
+
{waiting.map((w) => (
|
|
230
|
+
<div key={w.url} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '8px 14px', borderBottom: '1px solid rgba(255,255,255,0.04)', fontSize: 13 }}>
|
|
231
|
+
<span style={{ width: 8, height: 8, borderRadius: '50%', background: TIER[w.person?.tier] || '#556', flexShrink: 0 }} />
|
|
232
|
+
<span style={{ flex: 1, minWidth: 0 }}>
|
|
233
|
+
<b>{w.person?.name || slug(w.url)}</b>
|
|
234
|
+
{w.person?.company ? <span style={{ color: '#8b9a9a' }}> · {w.person.company}</span> : null}
|
|
235
|
+
</span>
|
|
236
|
+
<span style={{ color: '#b8c4c4' }}>{w.unread ? `${w.unread} unread` : `wrote ${w.days} days before your export`}</span>
|
|
237
|
+
<a href={w.url} target="_blank" rel="noreferrer" style={{ color: '#3498DB', textDecoration: 'none' }}>Open ↗</a>
|
|
238
|
+
</div>
|
|
239
|
+
))}
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
{social.chapters?.length > 0 && (
|
|
243
|
+
<>
|
|
244
|
+
<h2 style={h2}>Career chapters</h2>
|
|
245
|
+
<div style={{ ...box, padding: 0 }}>
|
|
246
|
+
{social.chapters.map((c) => {
|
|
247
|
+
const warm = (c.people || []).filter((u) => ['warm', 'cool'].includes(warmthOf(merged.get(keyFor({ profile_url: u })), asOf))).length;
|
|
248
|
+
return (
|
|
249
|
+
<div key={`${c.company}-${c.from}`} style={{ display: 'flex', gap: 10, padding: '8px 14px', borderBottom: '1px solid rgba(255,255,255,0.04)', fontSize: 13, alignItems: 'baseline' }}>
|
|
250
|
+
<span style={{ flex: 1 }}><b>{c.company}</b> <span style={{ color: '#8b9a9a' }}>{c.title} · {new Date(c.from).getFullYear()}–{c.to ? new Date(c.to).getFullYear() : 'now'}</span></span>
|
|
251
|
+
<span style={{ color: '#b8c4c4' }}>{c.made.toLocaleString()} connections made</span>
|
|
252
|
+
<span style={{ color: '#ff9f43', minWidth: 90, textAlign: 'right' }}>{c.made ? `${Math.round((100 * warm) / c.made)}% still in touch` : ''}</span>
|
|
253
|
+
</div>
|
|
254
|
+
);
|
|
255
|
+
})}
|
|
256
|
+
</div>
|
|
257
|
+
</>
|
|
258
|
+
)}
|
|
259
|
+
|
|
260
|
+
{social.posts?.posts?.length > 0 && (
|
|
261
|
+
<>
|
|
262
|
+
<h2 style={h2}>Does posting work?</h2>
|
|
263
|
+
<div style={box}>
|
|
264
|
+
<div style={{ fontSize: 13 }}>
|
|
265
|
+
In the week after a post you made <b>{social.posts.afterPosts.toFixed(1)}</b> new connections on average; a usual week, <b>{social.posts.usualWeek.toFixed(1)}</b>.
|
|
266
|
+
</div>
|
|
267
|
+
<div style={{ fontSize: 11.5, color: '#778', marginTop: 4 }}>What came after a post, not proof it came from it.</div>
|
|
268
|
+
{[...social.posts.posts].sort((a, b) => b.week - a.week).slice(0, 3).map((p) => (
|
|
269
|
+
<div key={p.t} style={{ fontSize: 12.5, color: '#b8c4c4', marginTop: 6 }}>
|
|
270
|
+
{day(p.t)}: {p.week} new connections that week {p.link ? <a href={p.link} target="_blank" rel="noreferrer" style={{ color: '#3498DB', textDecoration: 'none' }}>Post ↗</a> : null}
|
|
271
|
+
</div>
|
|
272
|
+
))}
|
|
273
|
+
</div>
|
|
274
|
+
</>
|
|
275
|
+
)}
|
|
276
|
+
|
|
277
|
+
{social.invites?.total > 0 && (
|
|
278
|
+
<>
|
|
279
|
+
<h2 style={h2}>Who reaches out</h2>
|
|
280
|
+
<div style={box}>
|
|
281
|
+
<span style={{ fontSize: 13 }}>
|
|
282
|
+
{social.invites.incoming.toLocaleString()} requests came to you and you sent {social.invites.outgoing.toLocaleString()}
|
|
283
|
+
{' '}({Math.round((100 * social.invites.incoming) / social.invites.total)}% came to you), as far as your export shows.
|
|
284
|
+
</span>
|
|
285
|
+
</div>
|
|
286
|
+
</>
|
|
287
|
+
)}
|
|
288
|
+
</>
|
|
289
|
+
)}
|
|
290
|
+
</main>
|
|
291
|
+
</div>
|
|
292
|
+
);
|
|
293
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// The experimental all-day Auto-Bridge (Auto scan), as the header button and
|
|
2
|
+
// the status bar see it. The switch is the Scan page's checkbox, kept in this
|
|
3
|
+
// browser (its useRemembered: JSON under six-degrees-experimental-auto); the page
|
|
4
|
+
// announces a change with the 'six-degrees:experimental' event.
|
|
5
|
+
|
|
6
|
+
import { beginScrape } from './scraper-client';
|
|
7
|
+
|
|
8
|
+
const KEY = 'six-degrees-experimental-auto';
|
|
9
|
+
|
|
10
|
+
export function watchAllDay(change) {
|
|
11
|
+
window.addEventListener('storage', change);
|
|
12
|
+
window.addEventListener('six-degrees:experimental', change);
|
|
13
|
+
return () => {
|
|
14
|
+
window.removeEventListener('storage', change);
|
|
15
|
+
window.removeEventListener('six-degrees:experimental', change);
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function allDayNow() {
|
|
20
|
+
try { return localStorage.getItem(KEY) === 'true'; } catch { return false; }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const remembered = (key, fallback) => {
|
|
24
|
+
try { const v = localStorage.getItem(key); return v === null ? fallback : JSON.parse(v); } catch { return fallback; }
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Start Auto scan with the Scan page's remembered choices (order, pages, finish), 10 people, S and A first. */
|
|
28
|
+
export function startAutoScan() {
|
|
29
|
+
const order = remembered('six-degrees-bridge-order', 'newest');
|
|
30
|
+
return beginScrape('auto-bridge', {
|
|
31
|
+
experimental: true,
|
|
32
|
+
maxBridges: 10,
|
|
33
|
+
order,
|
|
34
|
+
tiers: order === 'score' ? ['S', 'A'] : [],
|
|
35
|
+
maxPages: remembered('six-degrees-bridge-pages-v2', 100),
|
|
36
|
+
deeper: remembered('six-degrees-bridge-finish', true),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Insights on a connection's card (Graph Study §10, items 16 and 19): six plain
|
|
2
|
+
// numbers from what the app already keeps, no new scanning. For one of your
|
|
3
|
+
// connections; null for a 2nd-degree person.
|
|
4
|
+
//
|
|
5
|
+
// circle how many people are in their scanned circle
|
|
6
|
+
// only how many of them none of your other connections reach (lib/brokerage.js)
|
|
7
|
+
// twin whose circle overlaps theirs most, when it's real (15%+, 3+ people)
|
|
8
|
+
// mix their circle by tier
|
|
9
|
+
// bars how much of their circle is scanned, 0–5 (lib/reach.js scanBars)
|
|
10
|
+
// rank where they stand among your connections by who only they reach
|
|
11
|
+
// companies where the most people in their circle work, the top two
|
|
12
|
+
//
|
|
13
|
+
// Plain functions, no React; tests/insights.test.mjs.
|
|
14
|
+
|
|
15
|
+
import { keyFor } from './separation.js';
|
|
16
|
+
import { scanBars } from './reach.js';
|
|
17
|
+
|
|
18
|
+
const TIERS = ['S', 'A', 'B', 'C', 'D'];
|
|
19
|
+
|
|
20
|
+
export function profileInsights(person, { degree2 = [], exclusive, overlap, reach } = {}) {
|
|
21
|
+
if (!person || person.degree === 2) return null;
|
|
22
|
+
const seen = new Set();
|
|
23
|
+
const mix = { S: 0, A: 0, B: 0, C: 0, D: 0 };
|
|
24
|
+
const at = new Map();
|
|
25
|
+
for (const row of degree2) {
|
|
26
|
+
if (row?.source_connection_id !== person.id) continue;
|
|
27
|
+
const key = keyFor(row);
|
|
28
|
+
if (seen.has(key)) continue;
|
|
29
|
+
seen.add(key);
|
|
30
|
+
mix[TIERS.includes(row.tier) ? row.tier : 'D'] += 1;
|
|
31
|
+
const company = String(row.company || '').trim();
|
|
32
|
+
if (company) at.set(company, (at.get(company) || 0) + 1);
|
|
33
|
+
}
|
|
34
|
+
const companies = [...at.entries()].filter(([, n]) => n >= 2)
|
|
35
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 2).map(([name, count]) => ({ name, count }));
|
|
36
|
+
const circle = seen.size;
|
|
37
|
+
const ex = exclusive?.get(person.id) || { reach: 0, only: 0, total: 0 };
|
|
38
|
+
const ov = overlap?.get(person.id);
|
|
39
|
+
const twin = ov && ov.share >= 0.15 && ov.shared >= 3 ? { id: ov.with, share: ov.share, shared: ov.shared } : null;
|
|
40
|
+
const ranked = [...(exclusive?.entries() || [])]
|
|
41
|
+
.sort((a, b) => b[1].reach - a[1].reach || String(a[0]).localeCompare(String(b[0])));
|
|
42
|
+
const place = ranked.findIndex(([id]) => id === person.id);
|
|
43
|
+
return {
|
|
44
|
+
circle,
|
|
45
|
+
only: ex.only,
|
|
46
|
+
onlyShare: circle ? ex.only / circle : 0,
|
|
47
|
+
twin,
|
|
48
|
+
mix,
|
|
49
|
+
bars: reach ? scanBars(person, reach) : null,
|
|
50
|
+
rank: place >= 0 ? place + 1 : null,
|
|
51
|
+
of: ranked.length,
|
|
52
|
+
companies,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// The Social tab (experimental): what your own LinkedIn data export says about
|
|
2
|
+
// your relationships. The export is the folder LinkedIn emails you from
|
|
3
|
+
// Settings → Data privacy → Get a copy of your data; nothing here talks to
|
|
4
|
+
// LinkedIn. It runs in the browser on the files you pick, and what it keeps is
|
|
5
|
+
// numbers and dates only:
|
|
6
|
+
//
|
|
7
|
+
// people per profile: when you last messaged, who wrote last, how many
|
|
8
|
+
// messages (1:1 conversations only). Never a message's text.
|
|
9
|
+
// chapters your jobs (Positions.csv) and the connections made in each
|
|
10
|
+
// posts your posts (Shares.csv) and the connections that came the week after
|
|
11
|
+
// invites requests to and from you (Invitations.csv), when the export has them
|
|
12
|
+
//
|
|
13
|
+
// Plain functions, no React; tests/linkedin-export.test.mjs.
|
|
14
|
+
|
|
15
|
+
import { parseCsv } from './csv.js';
|
|
16
|
+
import { keyFor } from './separation.js';
|
|
17
|
+
|
|
18
|
+
const DAY = 86400000;
|
|
19
|
+
|
|
20
|
+
/** The rows of a CSV as objects, from the first line that has `mustHave` among its columns (exports put notes above the header). */
|
|
21
|
+
export function readTable(text, mustHave) {
|
|
22
|
+
const rows = parseCsv(String(text || ''));
|
|
23
|
+
const at = rows.findIndex((r) => r.some((c) => c.trim().toLowerCase() === mustHave.toLowerCase()));
|
|
24
|
+
if (at < 0) return [];
|
|
25
|
+
const head = rows[at].map((c) => c.trim().toLowerCase());
|
|
26
|
+
return rows.slice(at + 1)
|
|
27
|
+
.filter((r) => r.some((c) => c.trim()))
|
|
28
|
+
.map((r) => Object.fromEntries(head.map((h, i) => [h, (r[i] ?? '').trim()])));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A date in any of the export's formats, as ms; null when it can't be read. */
|
|
32
|
+
export function exportDate(s) {
|
|
33
|
+
const v = String(s || '').trim();
|
|
34
|
+
if (!v) return null;
|
|
35
|
+
let m = v.match(/^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})(?::(\d{2}))?(?:\s*UTC)?$/);
|
|
36
|
+
if (m) return Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +(m[6] || 0));
|
|
37
|
+
m = v.match(/^(\d{1,2})\/(\d{1,2})\/(\d{2,4}),?\s+(\d{1,2}):(\d{2})\s*(AM|PM)?$/i);
|
|
38
|
+
if (m) {
|
|
39
|
+
const y = +m[3] < 100 ? 2000 + +m[3] : +m[3];
|
|
40
|
+
let h = +m[4] % 12;
|
|
41
|
+
if ((m[6] || '').toUpperCase() === 'PM') h += 12;
|
|
42
|
+
return Date.UTC(y, +m[1] - 1, +m[2], h, +m[5]);
|
|
43
|
+
}
|
|
44
|
+
m = v.match(/^([A-Za-z]{3,9})\s+(\d{4})$/); // "Jan 2020"
|
|
45
|
+
if (m) { const t = Date.parse(`1 ${m[1]} ${m[2]} UTC`); return Number.isNaN(t) ? null : t; }
|
|
46
|
+
const t = Date.parse(/\d{4}$/.test(v) ? `${v} UTC` : v); // "01 May 2024"
|
|
47
|
+
return Number.isNaN(t) ? null : t;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const urlKey = (u) => keyFor({ profile_url: u });
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* From messages.csv, per person (by profile): { last, lastFromThem, total, recent }.
|
|
54
|
+
* `self` is you: the sender in the most conversations. Only 1:1 conversations
|
|
55
|
+
* count; group threads say little about one relationship. No text is read.
|
|
56
|
+
*/
|
|
57
|
+
export function messageStats(rows = []) {
|
|
58
|
+
const convos = new Map();
|
|
59
|
+
const senders = new Map();
|
|
60
|
+
for (const r of rows) {
|
|
61
|
+
const from = urlKey(r['sender profile url']);
|
|
62
|
+
const id = r['conversation id'];
|
|
63
|
+
if (!id || !from) continue;
|
|
64
|
+
const to = String(r['recipient profile urls'] || '').split(/[,;\s]+/).filter(Boolean).map(urlKey);
|
|
65
|
+
const t = exportDate(r.date);
|
|
66
|
+
if (t == null) continue;
|
|
67
|
+
if (!convos.has(id)) convos.set(id, { people: new Set(), msgs: [] });
|
|
68
|
+
const c = convos.get(id);
|
|
69
|
+
c.people.add(from);
|
|
70
|
+
to.forEach((p) => c.people.add(p));
|
|
71
|
+
c.msgs.push({ from, t });
|
|
72
|
+
if (!senders.has(from)) senders.set(from, new Set());
|
|
73
|
+
senders.get(from).add(id);
|
|
74
|
+
}
|
|
75
|
+
let self = null;
|
|
76
|
+
for (const [who, ids] of senders) if (!self || ids.size > senders.get(self).size) self = who;
|
|
77
|
+
let asOf = 0;
|
|
78
|
+
const people = new Map();
|
|
79
|
+
for (const c of convos.values()) {
|
|
80
|
+
const others = [...c.people].filter((p) => p !== self);
|
|
81
|
+
if (others.length !== 1 || !c.people.has(self)) continue;
|
|
82
|
+
const other = others[0];
|
|
83
|
+
const s = people.get(other) || { last: 0, lastFromThem: false, total: 0, times: [] };
|
|
84
|
+
for (const m of c.msgs) {
|
|
85
|
+
s.total += 1;
|
|
86
|
+
s.times.push(m.t);
|
|
87
|
+
if (m.t >= s.last) { s.last = m.t; s.lastFromThem = m.from !== self; }
|
|
88
|
+
if (m.t > asOf) asOf = m.t;
|
|
89
|
+
}
|
|
90
|
+
people.set(other, s);
|
|
91
|
+
}
|
|
92
|
+
for (const s of people.values()) {
|
|
93
|
+
s.recent = s.times.filter((t) => t > asOf - 90 * DAY).length;
|
|
94
|
+
delete s.times;
|
|
95
|
+
}
|
|
96
|
+
return { self, asOf: asOf || null, people };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Warm (in the last 30 days, or 3+ in 90), cool (within a year), dormant (longer ago), or never. */
|
|
100
|
+
export function warmthOf(stat, asOf) {
|
|
101
|
+
if (!stat || !stat.total) return 'never';
|
|
102
|
+
const age = asOf - stat.last;
|
|
103
|
+
if (age <= 30 * DAY || stat.recent >= 3) return 'warm';
|
|
104
|
+
return age <= 365 * DAY ? 'cool' : 'dormant';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** People who wrote last, 3 to 30 days before the export: replies you might owe. */
|
|
108
|
+
export function repliesWaiting(stats) {
|
|
109
|
+
const out = [];
|
|
110
|
+
for (const [url, s] of stats.people) {
|
|
111
|
+
const age = stats.asOf - s.last;
|
|
112
|
+
if (s.lastFromThem && age >= 3 * DAY && age <= 30 * DAY) out.push({ url, days: Math.round(age / DAY), total: s.total });
|
|
113
|
+
}
|
|
114
|
+
return out.sort((a, b) => a.days - b.days);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Your jobs, oldest first, each with the connections made while you were there. */
|
|
118
|
+
export function careerChapters(positions = [], connections = [], asOf = Date.now()) {
|
|
119
|
+
const jobs = positions
|
|
120
|
+
.map((p) => ({ company: p['company name'] || '', title: p.title || '', from: exportDate(p['started on']), to: exportDate(p['finished on']) }))
|
|
121
|
+
.filter((j) => j.company && j.from != null)
|
|
122
|
+
.sort((a, b) => a.from - b.from);
|
|
123
|
+
const made = connections.map((c) => ({ url: urlKey(c.url || c.profile_url), t: exportDate(c['connected on'] || c.connected_on) })).filter((c) => c.t != null);
|
|
124
|
+
return jobs.map((j) => {
|
|
125
|
+
const end = j.to ?? asOf;
|
|
126
|
+
const inside = made.filter((c) => c.t >= j.from && c.t < end);
|
|
127
|
+
return { ...j, to: j.to, made: inside.length, people: inside.map((c) => c.url) };
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Your posts and the connections made in the 7 days after each, against your
|
|
133
|
+
* usual week. Something that happened after a post, not proof it came from it.
|
|
134
|
+
*/
|
|
135
|
+
export function postingEffect(shares = [], connections = []) {
|
|
136
|
+
const posts = shares.map((s) => ({ t: exportDate(s.date), link: s.sharelink || s['share link'] || '' })).filter((p) => p.t != null).sort((a, b) => a.t - b.t);
|
|
137
|
+
const made = connections.map((c) => exportDate(c['connected on'] || c.connected_on)).filter((t) => t != null).sort((a, b) => a - b);
|
|
138
|
+
if (!made.length) return { posts: [], usualWeek: 0, afterPosts: 0 };
|
|
139
|
+
const span = Math.max(DAY * 7, made[made.length - 1] - made[0]);
|
|
140
|
+
const usualWeek = (made.length / span) * 7 * DAY;
|
|
141
|
+
const counted = posts.map((p) => ({ ...p, week: made.filter((t) => t >= p.t && t < p.t + 7 * DAY).length }));
|
|
142
|
+
const afterPosts = counted.length ? counted.reduce((n, p) => n + p.week, 0) / counted.length : 0;
|
|
143
|
+
return { posts: counted, usualWeek, afterPosts };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Requests to and from you, when the export has them. */
|
|
147
|
+
export function invitationSplit(rows = []) {
|
|
148
|
+
let incoming = 0;
|
|
149
|
+
let outgoing = 0;
|
|
150
|
+
for (const r of rows) {
|
|
151
|
+
const d = String(r.direction || '').toUpperCase();
|
|
152
|
+
if (d === 'INCOMING') incoming += 1;
|
|
153
|
+
else if (d === 'OUTGOING') outgoing += 1;
|
|
154
|
+
}
|
|
155
|
+
return { incoming, outgoing, total: incoming + outgoing };
|
|
156
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Network health for the Scores tab (item 41, Graph Study §8.5/§10.2), from
|
|
2
|
+
// what the app already keeps. No percentiles against other people: there is no
|
|
3
|
+
// data to compare with, so every number is about your own network.
|
|
4
|
+
//
|
|
5
|
+
// circles how many of your connections have a scanned circle
|
|
6
|
+
// early fewer than 5: say the numbers are an early estimate
|
|
7
|
+
// twoWays of everyone you reach through a circle, the share reached two or
|
|
8
|
+
// more ways (lib/brokerage.js redundancy)
|
|
9
|
+
// effective Burt's effective size of your own network, N − 2t/N, with t the ties
|
|
10
|
+
// between your connections that scans have kept (lib/ties.js), and
|
|
11
|
+
// efficiency ES/N. Honest only as far as those ties go.
|
|
12
|
+
// top your five connections by who only they reach
|
|
13
|
+
//
|
|
14
|
+
// Plain functions, no React; tests/network-health.test.mjs.
|
|
15
|
+
|
|
16
|
+
import { exclusiveReach, redundancy } from './brokerage.js';
|
|
17
|
+
|
|
18
|
+
export function networkHealth({ degree1 = [], degree2 = [], ties = 0 } = {}) {
|
|
19
|
+
const circleOf = new Map();
|
|
20
|
+
for (const row of degree2) {
|
|
21
|
+
if (row?.source_connection_id != null) circleOf.set(row.source_connection_id, (circleOf.get(row.source_connection_id) || 0) + 1);
|
|
22
|
+
}
|
|
23
|
+
const only = exclusiveReach(degree2, degree1);
|
|
24
|
+
const n = degree1.length;
|
|
25
|
+
const t = Math.max(0, Number(ties) || 0);
|
|
26
|
+
const es = n ? n - (2 * t) / n : 0;
|
|
27
|
+
const byId = new Map(degree1.map((c) => [c.id, c]));
|
|
28
|
+
const top = [...only.entries()]
|
|
29
|
+
.sort((a, b) => b[1].reach - a[1].reach || String(a[0]).localeCompare(String(b[0])))
|
|
30
|
+
.slice(0, 5)
|
|
31
|
+
.map(([id, r]) => {
|
|
32
|
+
const c = byId.get(id) || {};
|
|
33
|
+
return { id, name: c.name || '', company: c.company || '', tier: c.tier || '', circle: circleOf.get(id) || 0, only: r.only, share: r.total ? r.only / r.total : 0 };
|
|
34
|
+
});
|
|
35
|
+
return {
|
|
36
|
+
circles: circleOf.size,
|
|
37
|
+
early: circleOf.size < 5,
|
|
38
|
+
twoWays: redundancy(degree2, degree1),
|
|
39
|
+
effective: { n, ties: t, size: es, efficiency: n ? es / n : 0 },
|
|
40
|
+
top,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -148,6 +148,7 @@ function take(s) {
|
|
|
148
148
|
target: running ? s.target ?? null : starting ? now.target : null,
|
|
149
149
|
startedAt: running ? s.startedAt ?? null : null,
|
|
150
150
|
progress: running ? s.progress ?? null : null,
|
|
151
|
+
budget: running ? s.budget ?? null : null,
|
|
151
152
|
log: running && Array.isArray(s.log) ? s.log : [],
|
|
152
153
|
finished,
|
|
153
154
|
});
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "six-degrees",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "six-degrees",
|
|
9
|
-
"version": "0.4.
|
|
9
|
+
"version": "0.4.4",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"os": [
|
|
12
12
|
"darwin",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "six-degrees",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"description": "Map your professional network as a galaxy — leverage tiers, bridges into new circles, and the shortest path to anyone. Runs locally on your own data.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Blake Burford",
|