six-degrees 0.4.3 → 0.4.5
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 +18 -17
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +18 -17
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -13
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +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.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 +22 -21
- package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +22 -21
- package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +16 -15
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +20 -19
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +16 -15
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +20 -19
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- 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 +22 -21
- package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +22 -21
- package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +16 -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 +22 -21
- package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +22 -21
- package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +16 -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 +22 -21
- package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +22 -21
- package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +16 -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 +22 -21
- package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +22 -21
- package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +16 -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 +22 -21
- package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +22 -21
- package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +16 -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 +22 -21
- package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +22 -21
- package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +16 -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 +22 -21
- package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +22 -21
- package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +16 -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 +1 -1
- 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/_0ezj4e0._.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/_14iv_zy._.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/_212xw8q._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_217nddo._.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_0i3fw_m._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +17 -4
- package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +2 -2
- 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/lib_0i8v-s3._.js +3 -0
- 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/04a3zockahetx.js +1 -0
- package/.next/standalone/.next/static/chunks/{2wwmljnf7yn_5.js → 0a5iadnmt9u9k.js} +1 -1
- package/.next/standalone/.next/static/chunks/0g_p4p7oci18u.js +1 -0
- package/.next/standalone/.next/static/chunks/0y1yms5kez2f3.js +1 -0
- package/.next/standalone/.next/static/chunks/14m1zu-8warte.js +1 -0
- package/.next/standalone/.next/static/chunks/1e1n0kkz-psdb.js +1 -0
- package/.next/standalone/.next/static/chunks/236f0jtp68j_h.js +1 -0
- package/.next/standalone/.next/static/chunks/{1zwjy1mmpkdrg.js → 24m745q_85u34.js} +2 -2
- package/.next/standalone/.next/static/chunks/2_zu7rmb8flzl.js +2 -0
- package/.next/standalone/.next/static/chunks/38wwtni5e1ec1.js +31 -0
- package/.next/standalone/.next/static/chunks/3p_pwu5gx2i9h.js +1 -0
- package/.next/{static/chunks/0_3ubivay59bq.js → standalone/.next/static/chunks/3zw8bv7-mc4n6.js} +1 -1
- package/.next/standalone/CHANGELOG.md +58 -0
- package/.next/standalone/app/api/network/route.js +11 -0
- package/.next/standalone/app/api/scraper/route.js +2 -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 +6 -0
- package/.next/standalone/app/components/ForceGraph.js +257 -39
- package/.next/standalone/app/components/GalaxyLab.js +275 -0
- package/.next/standalone/app/components/ScanStatusBar.js +22 -8
- 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/layout.js +3 -0
- package/.next/standalone/app/page.js +29 -0
- package/.next/standalone/app/scores/page.js +3 -0
- package/.next/standalone/app/setup/page.js +1 -1
- package/.next/standalone/app/social/page.js +281 -0
- package/.next/standalone/lib/experimental-client.js +38 -0
- package/.next/standalone/lib/galaxy-export.js +152 -0
- package/.next/standalone/lib/galaxy-lab.js +296 -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/package-lock.json +2 -2
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/scripts/scrape.py +118 -2
- package/.next/static/chunks/04a3zockahetx.js +1 -0
- package/.next/static/chunks/{2wwmljnf7yn_5.js → 0a5iadnmt9u9k.js} +1 -1
- package/.next/static/chunks/0g_p4p7oci18u.js +1 -0
- package/.next/static/chunks/0y1yms5kez2f3.js +1 -0
- package/.next/static/chunks/14m1zu-8warte.js +1 -0
- package/.next/static/chunks/1e1n0kkz-psdb.js +1 -0
- package/.next/static/chunks/236f0jtp68j_h.js +1 -0
- package/.next/static/chunks/{1zwjy1mmpkdrg.js → 24m745q_85u34.js} +2 -2
- package/.next/static/chunks/2_zu7rmb8flzl.js +2 -0
- package/.next/static/chunks/38wwtni5e1ec1.js +31 -0
- package/.next/static/chunks/3p_pwu5gx2i9h.js +1 -0
- package/.next/{standalone/.next/static/chunks/0_3ubivay59bq.js → static/chunks/3zw8bv7-mc4n6.js} +1 -1
- package/CHANGELOG.md +58 -0
- package/lib/experimental-client.js +38 -0
- package/lib/galaxy-export.js +152 -0
- package/lib/galaxy-lab.js +296 -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/package.json +1 -1
- package/scripts/scrape.py +118 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0d3vjsq._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0m4vk0b._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_012mop3._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_01f38ty._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1163x49._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1av11ws._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1gpbbw9._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1ig7em7._.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/static/chunks/0cuuo2jwc9h94.js +0 -1
- package/.next/standalone/.next/static/chunks/0f4ugd6v3ph0-.js +0 -1
- package/.next/standalone/.next/static/chunks/0l1568qff0efj.js +0 -1
- package/.next/standalone/.next/static/chunks/10ogurbcuf3nq.js +0 -1
- package/.next/standalone/.next/static/chunks/1489t7jrjojk_.js +0 -1
- package/.next/standalone/.next/static/chunks/1zunq4tk-u3ng.js +0 -1
- package/.next/standalone/.next/static/chunks/2p5yt1o9tnxjn.js +0 -2
- package/.next/standalone/.next/static/chunks/2pyo4j84prgp6.js +0 -18
- package/.next/standalone/.next/static/chunks/3kllnkb3hof1s.js +0 -1
- package/.next/static/chunks/0cuuo2jwc9h94.js +0 -1
- package/.next/static/chunks/0f4ugd6v3ph0-.js +0 -1
- package/.next/static/chunks/0l1568qff0efj.js +0 -1
- package/.next/static/chunks/10ogurbcuf3nq.js +0 -1
- package/.next/static/chunks/1489t7jrjojk_.js +0 -1
- package/.next/static/chunks/1zunq4tk-u3ng.js +0 -1
- package/.next/static/chunks/2p5yt1o9tnxjn.js +0 -2
- package/.next/static/chunks/2pyo4j84prgp6.js +0 -18
- package/.next/static/chunks/3kllnkb3hof1s.js +0 -1
- /package/.next/standalone/.next/static/{814ehZBeFO2rUKle7mzX- → 6LINKa6nyesvIEUnLDSck}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{814ehZBeFO2rUKle7mzX- → 6LINKa6nyesvIEUnLDSck}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{814ehZBeFO2rUKle7mzX- → 6LINKa6nyesvIEUnLDSck}/_ssgManifest.js +0 -0
- /package/.next/static/{814ehZBeFO2rUKle7mzX- → 6LINKa6nyesvIEUnLDSck}/_buildManifest.js +0 -0
- /package/.next/static/{814ehZBeFO2rUKle7mzX- → 6LINKa6nyesvIEUnLDSck}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{814ehZBeFO2rUKle7mzX- → 6LINKa6nyesvIEUnLDSck}/_ssgManifest.js +0 -0
|
@@ -20,6 +20,7 @@ export const metadata = {
|
|
|
20
20
|
import UserProvider from './components/UserProvider';
|
|
21
21
|
import StaleServerBanner from './components/StaleServerBanner';
|
|
22
22
|
import ScanStatusBar from './components/ScanStatusBar';
|
|
23
|
+
import SocialAutoSync from './components/SocialAutoSync';
|
|
23
24
|
|
|
24
25
|
export default function RootLayout({ children }) {
|
|
25
26
|
return (
|
|
@@ -33,6 +34,8 @@ export default function RootLayout({ children }) {
|
|
|
33
34
|
<StaleServerBanner />
|
|
34
35
|
{/* While a scan runs: what it's doing and today's budget, with Stop. */}
|
|
35
36
|
<ScanStatusBar />
|
|
37
|
+
{/* The Social tab's once-a-day messages sync, when switched on. */}
|
|
38
|
+
<SocialAutoSync />
|
|
36
39
|
</UserProvider>
|
|
37
40
|
</body>
|
|
38
41
|
</html>
|
|
@@ -8,6 +8,7 @@ import useRequests from './components/useRequests';
|
|
|
8
8
|
import { requestCount } from '../lib/requests-client';
|
|
9
9
|
import { loadNetwork } from '../lib/network';
|
|
10
10
|
import { VIEWS, resolveView } from './components/views';
|
|
11
|
+
import AutoScanButton from './components/AutoScanButton';
|
|
11
12
|
import { peopleByDegree, tierCountsOf } from '../lib/degrees';
|
|
12
13
|
import Sidebar from './components/Sidebar';
|
|
13
14
|
import FilterPanel from './components/FilterPanel';
|
|
@@ -153,6 +154,19 @@ function HomeInner() {
|
|
|
153
154
|
|
|
154
155
|
// Followed once: Bridge Chains has opened it, so leaving that view and coming
|
|
155
156
|
// back, or reloading, starts from the overview.
|
|
157
|
+
// A card's Insights → Separation, filtered to that person's circle ("Show them", "S only").
|
|
158
|
+
const [separationPreset, setSeparationPreset] = useState(null);
|
|
159
|
+
const showInSeparation = useCallback((preset) => {
|
|
160
|
+
setSeparationPreset({ ...preset, id: `${preset.query}|${preset.tier || ''}|${preset.rarity || ''}|${Math.random()}` });
|
|
161
|
+
setMode('degrees');
|
|
162
|
+
setVisualMode('separation');
|
|
163
|
+
}, []);
|
|
164
|
+
// A card's Insights → that person's circle in Bridge Chains.
|
|
165
|
+
const openCircle = useCallback((id) => {
|
|
166
|
+
setChainOpen(id);
|
|
167
|
+
setMode('degrees');
|
|
168
|
+
setVisualMode('chain');
|
|
169
|
+
}, []);
|
|
156
170
|
const chainOpened = useCallback(() => {
|
|
157
171
|
setChainOpen(null);
|
|
158
172
|
try { window.history.replaceState(null, '', window.location.pathname); } catch { /* the link stays */ }
|
|
@@ -284,6 +298,17 @@ function HomeInner() {
|
|
|
284
298
|
>
|
|
285
299
|
Paths
|
|
286
300
|
</Link>}
|
|
301
|
+
{!IS_DEMO && <Link
|
|
302
|
+
href="/social"
|
|
303
|
+
title="Experimental: what your own LinkedIn data says about your relationships"
|
|
304
|
+
style={{
|
|
305
|
+
padding: isMobile ? '6px 10px' : '8px 16px', borderRadius: 6, border: 'none', fontSize: isMobile ? 11 : 13, fontWeight: 600,
|
|
306
|
+
background: 'rgba(255,255,255,0.06)', color: '#FFD700', textDecoration: 'none',
|
|
307
|
+
display: 'flex', alignItems: 'center', gap: 4,
|
|
308
|
+
}}
|
|
309
|
+
>
|
|
310
|
+
Social<span style={{ fontSize: 9, opacity: 0.7, marginLeft: 2 }}>beta</span>
|
|
311
|
+
</Link>}
|
|
287
312
|
{!IS_DEMO && !csvMode && <Link
|
|
288
313
|
href="/scores"
|
|
289
314
|
style={{
|
|
@@ -318,6 +343,7 @@ function HomeInner() {
|
|
|
318
343
|
>
|
|
319
344
|
Scan
|
|
320
345
|
</Link>}
|
|
346
|
+
{!IS_DEMO && !csvMode && <AutoScanButton isMobile={isMobile} />}
|
|
321
347
|
</div>
|
|
322
348
|
{/* Settings — also in CSV/sample mode and on phones, so updates stay reachable */}
|
|
323
349
|
{!IS_DEMO && <Link
|
|
@@ -499,6 +525,7 @@ function HomeInner() {
|
|
|
499
525
|
canScan,
|
|
500
526
|
chainOpen,
|
|
501
527
|
onChainOpened: chainOpened,
|
|
528
|
+
preset: separationPreset,
|
|
502
529
|
};
|
|
503
530
|
return <View {...viewProps} />;
|
|
504
531
|
})()}
|
|
@@ -514,6 +541,8 @@ function HomeInner() {
|
|
|
514
541
|
onToggle={() => setSidebarCollapsed(!sidebarCollapsed)}
|
|
515
542
|
onSelect={(node) => { setSelected(node || null); }}
|
|
516
543
|
onSwitchMode={(newMode) => { setMode(newMode); setFilter('all'); }}
|
|
544
|
+
onOpenCircle={openCircle}
|
|
545
|
+
onShowInSeparation={showInSeparation}
|
|
517
546
|
onFocusNode={(nodeId) => { if (focusNodeRef.current) focusNodeRef.current(nodeId); }}
|
|
518
547
|
csvSource={IS_DEMO ? 'sample' : csvMode ? csvSource : null}
|
|
519
548
|
scanNotes={scanNotes}
|
|
@@ -12,6 +12,7 @@ import Link from 'next/link';
|
|
|
12
12
|
import OnboardingGate from '../components/OnboardingGate';
|
|
13
13
|
import SectorSection from '../components/settings/SectorSection';
|
|
14
14
|
import TierSection from '../components/settings/TierSection';
|
|
15
|
+
import NetworkHealthSection from '../components/settings/NetworkHealthSection';
|
|
15
16
|
import CompanyScores from '../components/CompanyScores';
|
|
16
17
|
import { Body, LINE, FONT } from '../components/ui';
|
|
17
18
|
import { IS_DEMO } from '../../lib/demo';
|
|
@@ -60,7 +61,9 @@ function ScoresInner() {
|
|
|
60
61
|
How someone’s power score is worked out, and the three things you can change about it:{' '}
|
|
61
62
|
<a href="#sector" style={jump}>your field</a>, <a href="#tiers" style={jump}>how tiers are graded</a> and{' '}
|
|
62
63
|
<a href="#companies" style={jump}>any company’s score</a>. A change here rescores everyone.
|
|
64
|
+
{' '}First, <a href="#health" style={jump}>how your network holds together</a>.
|
|
63
65
|
</Body>
|
|
66
|
+
<NetworkHealthSection />
|
|
64
67
|
<SectorSection onSaved={saved} />
|
|
65
68
|
<TierSection onSaved={saved} />
|
|
66
69
|
<div id="companies" style={{ marginTop: 28 }}>
|
|
@@ -444,7 +444,7 @@ function SetupInner() {
|
|
|
444
444
|
</label>
|
|
445
445
|
<label style={{ display: 'flex', gap: 8, alignItems: 'flex-start', fontSize: 12.5, color: '#b8c4c4', cursor: running ? 'default' : 'pointer' }}>
|
|
446
446
|
<input type="checkbox" checked={experimental} disabled={running} style={{ marginTop: 3 }}
|
|
447
|
-
onChange={(e) => setExperimental(e.target.checked)} />
|
|
447
|
+
onChange={(e) => { setExperimental(e.target.checked); window.dispatchEvent(new Event('six-degrees:experimental')); }} />
|
|
448
448
|
<span>
|
|
449
449
|
<b style={{ color: '#FFD700' }}>Experimental:</b> all-day pacing. Up to 10 pages in a sitting,
|
|
450
450
|
then a 45-minute rest; searches only from 9:00 to 19:00; at the daily budget it waits for it to
|
|
@@ -0,0 +1,281 @@
|
|
|
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 { mergeSocial } from '../../lib/galaxy-lab';
|
|
18
|
+
import { keyFor } from '../../lib/separation';
|
|
19
|
+
import { runScrape, watchScanner, scannerNow } from '../../lib/scraper-client';
|
|
20
|
+
import { Body, LINE, FONT } from '../components/ui';
|
|
21
|
+
import { IS_DEMO } from '../../lib/demo';
|
|
22
|
+
|
|
23
|
+
const TIER = { S: '#FFD700', A: '#9B59B6', B: '#3498DB', C: '#95A5A6', D: '#BDC3C7' };
|
|
24
|
+
const WARMTH = { warm: ['Warm', '#ff9f43'], cool: ['Cool', '#3498DB'], dormant: ['Dormant', '#8899aa'], never: ['Never messaged', '#556'] };
|
|
25
|
+
const FILES = { 'connections.csv': 'first name', 'messages.csv': 'conversation id', 'invitations.csv': 'direction', 'positions.csv': 'company name', 'shares.csv': 'date' };
|
|
26
|
+
const day = (t) => (t ? new Date(t).toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric' }) : '');
|
|
27
|
+
const slug = (u) => String(u || '').replace(/\/+$/, '').split('/').pop();
|
|
28
|
+
|
|
29
|
+
export default function SocialPage() {
|
|
30
|
+
return <OnboardingGate><SocialInner /></OnboardingGate>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function SocialInner() {
|
|
34
|
+
const { userId } = useUser();
|
|
35
|
+
const [social, setSocial] = useState(undefined);
|
|
36
|
+
const [net, setNet] = useState(null);
|
|
37
|
+
const [keepEmails, setKeepEmails] = useState(false);
|
|
38
|
+
const [note, setNote] = useState(null);
|
|
39
|
+
const [busy, setBusy] = useState(false);
|
|
40
|
+
const [job, setJob] = useState(() => scannerNow());
|
|
41
|
+
|
|
42
|
+
const reload = () => fetch('/api/social').then((r) => r.json()).then((d) => setSocial(d.social || null)).catch(() => setSocial(null));
|
|
43
|
+
useEffect(() => { reload(); }, []);
|
|
44
|
+
useEffect(() => watchScanner(() => setJob(scannerNow())), []);
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (!userId) return undefined;
|
|
47
|
+
let live = true;
|
|
48
|
+
loadNetwork(userId).then((n) => { if (live) setNet(n); }).catch(() => {});
|
|
49
|
+
return () => { live = false; };
|
|
50
|
+
}, [userId]);
|
|
51
|
+
|
|
52
|
+
// Everyone you know, by profile: name and tier for the lists below.
|
|
53
|
+
const byKey = useMemo(() => {
|
|
54
|
+
const m = new Map();
|
|
55
|
+
for (const r of [...(net?.degree1 || []), ...(net?.degree2 || [])]) if (!m.has(keyFor(r))) m.set(keyFor(r), r);
|
|
56
|
+
return m;
|
|
57
|
+
}, [net]);
|
|
58
|
+
|
|
59
|
+
async function importFolder(list) {
|
|
60
|
+
setBusy(true);
|
|
61
|
+
setNote(null);
|
|
62
|
+
try {
|
|
63
|
+
const found = {};
|
|
64
|
+
for (const f of Array.from(list || [])) {
|
|
65
|
+
const name = f.name.toLowerCase();
|
|
66
|
+
if (FILES[name] && !found[name]) found[name] = readTable(await f.text(), FILES[name]);
|
|
67
|
+
}
|
|
68
|
+
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).');
|
|
69
|
+
const conns = found['connections.csv'] || [];
|
|
70
|
+
const stats = found['messages.csv'] ? messageStats(found['messages.csv']) : { asOf: null, people: new Map() };
|
|
71
|
+
const people = {};
|
|
72
|
+
for (const [url, v] of stats.people) people[url] = { last: v.last, lastFromThem: v.lastFromThem, total: v.total, recent: v.recent };
|
|
73
|
+
if (keepEmails) {
|
|
74
|
+
for (const c of conns) {
|
|
75
|
+
const e = c['email address'];
|
|
76
|
+
if (!e) continue;
|
|
77
|
+
const k = keyFor({ profile_url: c.url });
|
|
78
|
+
people[k] = { ...(people[k] || {}), email: e };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const asOf = stats.asOf || Date.now();
|
|
82
|
+
const body = {
|
|
83
|
+
asOf,
|
|
84
|
+
people,
|
|
85
|
+
chapters: careerChapters(found['positions.csv'] || [], conns, asOf),
|
|
86
|
+
posts: found['shares.csv'] ? postingEffect(found['shares.csv'], conns) : null,
|
|
87
|
+
invites: found['invitations.csv'] ? invitationSplit(found['invitations.csv']) : null,
|
|
88
|
+
emails: keepEmails,
|
|
89
|
+
};
|
|
90
|
+
const r = await fetch('/api/social', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
91
|
+
if (!r.ok) throw new Error((await r.json().catch(() => ({}))).error || 'It couldn’t be saved.');
|
|
92
|
+
setNote({ good: true, text: `Read ${Object.keys(found).length} of LinkedIn’s files: ${stats.people.size.toLocaleString()} people you’ve messaged one to one.` });
|
|
93
|
+
await reload();
|
|
94
|
+
} catch (e) {
|
|
95
|
+
setNote({ good: false, text: e.message });
|
|
96
|
+
} finally {
|
|
97
|
+
setBusy(false);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function syncNow() {
|
|
102
|
+
setNote(null);
|
|
103
|
+
try {
|
|
104
|
+
const end = await runScrape('messages');
|
|
105
|
+
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.' });
|
|
106
|
+
await reload();
|
|
107
|
+
} catch (e) {
|
|
108
|
+
setNote({ good: false, text: e.message });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function setAuto(on) {
|
|
113
|
+
await fetch('/api/social', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ autoSync: on }) });
|
|
114
|
+
reload();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function forget() {
|
|
118
|
+
await fetch('/api/social', { method: 'DELETE' });
|
|
119
|
+
setNote({ good: true, text: 'Forgotten. Nothing from your export or messages is kept here now.' });
|
|
120
|
+
reload();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// What each person's messages say, the export and the live sync together, and
|
|
124
|
+
// "now" for warmth: the newest thing the data knows, so an older export reads as it was.
|
|
125
|
+
const { merged, asOf } = useMemo(() => mergeSocial(social), [social]);
|
|
126
|
+
|
|
127
|
+
const warmth = useMemo(() => {
|
|
128
|
+
const counts = { warm: 0, cool: 0, dormant: 0, never: 0 };
|
|
129
|
+
for (const c of net?.degree1 || []) counts[warmthOf(merged.get(keyFor(c)), asOf)] += 1;
|
|
130
|
+
return counts;
|
|
131
|
+
}, [net, merged, asOf]);
|
|
132
|
+
|
|
133
|
+
const waiting = useMemo(() => {
|
|
134
|
+
const stats = { asOf, people: new Map([...merged].filter(([, v]) => v.lastFromThem != null)) };
|
|
135
|
+
const list = repliesWaiting(stats).map((w) => ({ ...w, person: byKey.get(w.url) }));
|
|
136
|
+
for (const [k, v] of merged) {
|
|
137
|
+
if (v.unread > 0 && !list.some((w) => w.url === k)) list.push({ url: k, unread: v.unread, person: byKey.get(k) });
|
|
138
|
+
}
|
|
139
|
+
const rank = (w) => ({ S: 0, A: 1 }[w.person?.tier] ?? 2);
|
|
140
|
+
return list.sort((a, b) => rank(a) - rank(b) || (a.days ?? 0) - (b.days ?? 0)).slice(0, 15);
|
|
141
|
+
}, [merged, asOf, byKey]);
|
|
142
|
+
|
|
143
|
+
if (IS_DEMO) return <div style={{ minHeight: '100vh', background: '#0a0a1a', color: '#aaa', padding: 40, fontFamily: FONT }}>Not part of the demo.</div>;
|
|
144
|
+
const running = job?.running;
|
|
145
|
+
const box = { padding: '12px 14px', borderRadius: 8, border: LINE, background: 'rgba(255,255,255,0.03)', marginTop: 10 };
|
|
146
|
+
const h2 = { fontSize: 16, margin: '24px 0 6px' };
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<div style={{ minHeight: '100vh', background: '#0a0a1a', color: '#fff', fontFamily: FONT }}>
|
|
150
|
+
<header style={{ padding: '16px 24px', borderBottom: LINE, display: 'flex', alignItems: 'center', gap: 16 }}>
|
|
151
|
+
<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>
|
|
152
|
+
<h1 style={{ fontSize: 22, fontWeight: 700, margin: 0, color: '#FFD700' }}>Social <span style={{ fontSize: 12, color: '#8b9a9a', fontWeight: 600 }}>experimental</span></h1>
|
|
153
|
+
</header>
|
|
154
|
+
<main style={{ maxWidth: 980, margin: '0 auto', padding: '8px 24px 64px' }}>
|
|
155
|
+
<Body style={{ marginTop: 16 }}>
|
|
156
|
+
Who you’re warm with, who’s waiting on a reply, and how each chapter of your career built your network,
|
|
157
|
+
from your own LinkedIn data. Only dates, who wrote last and counts are kept on this computer, never what a
|
|
158
|
+
message says.
|
|
159
|
+
</Body>
|
|
160
|
+
|
|
161
|
+
<h2 style={h2}>Your LinkedIn export</h2>
|
|
162
|
+
<div style={box}>
|
|
163
|
+
<Body style={{ margin: 0 }}>
|
|
164
|
+
On LinkedIn: <i>Settings → Data privacy → Get a copy of your data</i>, pick the larger archive, and unzip the
|
|
165
|
+
file it emails you. Then choose that folder here. It’s read in this window; nothing goes to LinkedIn.
|
|
166
|
+
</Body>
|
|
167
|
+
<label style={{ display: 'flex', gap: 8, alignItems: 'flex-start', fontSize: 12.5, color: '#b8c4c4', marginTop: 10 }}>
|
|
168
|
+
<input type="checkbox" checked={keepEmails} onChange={(e) => setKeepEmails(e.target.checked)} style={{ marginTop: 3 }} />
|
|
169
|
+
<span>Also keep email addresses from it, on this computer only, for one-click email.</span>
|
|
170
|
+
</label>
|
|
171
|
+
<div style={{ marginTop: 10, display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap' }}>
|
|
172
|
+
<label style={{ padding: '7px 14px', borderRadius: 8, background: 'rgba(52,152,219,0.18)', color: '#cfe6f7', cursor: busy ? 'default' : 'pointer', fontSize: 13 }}>
|
|
173
|
+
{busy ? 'Reading…' : social ? 'Choose a newer export' : 'Choose the export folder'}
|
|
174
|
+
<input type="file" webkitdirectory="" directory="" multiple disabled={busy} style={{ display: 'none' }} onChange={(e) => importFolder(e.target.files)} />
|
|
175
|
+
</label>
|
|
176
|
+
{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>}
|
|
177
|
+
{social && <button onClick={forget} style={{ background: 'none', border: 'none', color: '#ff9b9b', cursor: 'pointer', fontSize: 12 }}>Forget it</button>}
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<h2 style={h2}>Live messages sync <span style={{ fontSize: 11, color: '#FFD700' }}>experimental</span></h2>
|
|
182
|
+
<div style={box}>
|
|
183
|
+
<Body style={{ margin: 0 }}>
|
|
184
|
+
Reads your messages list once, in your own Chrome: who each one-to-one conversation is with, when it was last
|
|
185
|
+
active and whether it’s unread. It uses no search budget and never opens or keeps a message’s text.
|
|
186
|
+
</Body>
|
|
187
|
+
<div style={{ marginTop: 10, display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap' }}>
|
|
188
|
+
<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 }}>
|
|
189
|
+
{running ? 'The scanner is busy' : 'Sync messages now'}
|
|
190
|
+
</button>
|
|
191
|
+
<label style={{ display: 'flex', gap: 6, alignItems: 'center', fontSize: 12.5, color: '#b8c4c4' }}>
|
|
192
|
+
<input type="checkbox" checked={social?.autoSync === true} onChange={(e) => setAuto(e.target.checked)} />
|
|
193
|
+
Once a day while the app is open (daytime only)
|
|
194
|
+
</label>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
{note && <div style={{ marginTop: 10, fontSize: 13, color: note.good ? '#00ff88' : '#ff9b9b' }}>{note.text}</div>}
|
|
198
|
+
|
|
199
|
+
{social && (
|
|
200
|
+
<>
|
|
201
|
+
<h2 style={h2}>Warmth</h2>
|
|
202
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', gap: 10 }}>
|
|
203
|
+
{Object.entries(WARMTH).map(([k, [label, color]]) => (
|
|
204
|
+
<div key={k} style={{ ...box, marginTop: 0 }}>
|
|
205
|
+
<div style={{ fontSize: 22, fontWeight: 700, color }}>{(warmth[k] || 0).toLocaleString()}</div>
|
|
206
|
+
<div style={{ fontSize: 12.5, color: '#b8c4c4' }}>{label}</div>
|
|
207
|
+
</div>
|
|
208
|
+
))}
|
|
209
|
+
</div>
|
|
210
|
+
<div style={{ fontSize: 11.5, color: '#778', marginTop: 6 }}>
|
|
211
|
+
Of your connections. Warm: messaged in the last 30 days, or 3+ messages in 90. Cool: within a year. Dormant: longer ago.
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
<h2 style={h2}>Waiting on you</h2>
|
|
215
|
+
<div style={{ ...box, padding: 0 }}>
|
|
216
|
+
{waiting.length === 0 && <div style={{ padding: 12, fontSize: 13, color: '#8b9a9a' }}>No one is waiting on a reply.</div>}
|
|
217
|
+
{waiting.map((w) => (
|
|
218
|
+
<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 }}>
|
|
219
|
+
<span style={{ width: 8, height: 8, borderRadius: '50%', background: TIER[w.person?.tier] || '#556', flexShrink: 0 }} />
|
|
220
|
+
<span style={{ flex: 1, minWidth: 0 }}>
|
|
221
|
+
<b>{w.person?.name || slug(w.url)}</b>
|
|
222
|
+
{w.person?.company ? <span style={{ color: '#8b9a9a' }}> · {w.person.company}</span> : null}
|
|
223
|
+
</span>
|
|
224
|
+
<span style={{ color: '#b8c4c4' }}>{w.unread ? `${w.unread} unread` : `wrote ${w.days} days before your export`}</span>
|
|
225
|
+
<a href={w.url} target="_blank" rel="noreferrer" style={{ color: '#3498DB', textDecoration: 'none' }}>Open ↗</a>
|
|
226
|
+
</div>
|
|
227
|
+
))}
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
{social.chapters?.length > 0 && (
|
|
231
|
+
<>
|
|
232
|
+
<h2 style={h2}>Career chapters</h2>
|
|
233
|
+
<div style={{ ...box, padding: 0 }}>
|
|
234
|
+
{social.chapters.map((c) => {
|
|
235
|
+
const warm = (c.people || []).filter((u) => ['warm', 'cool'].includes(warmthOf(merged.get(keyFor({ profile_url: u })), asOf))).length;
|
|
236
|
+
return (
|
|
237
|
+
<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' }}>
|
|
238
|
+
<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>
|
|
239
|
+
<span style={{ color: '#b8c4c4' }}>{c.made.toLocaleString()} connections made</span>
|
|
240
|
+
<span style={{ color: '#ff9f43', minWidth: 90, textAlign: 'right' }}>{c.made ? `${Math.round((100 * warm) / c.made)}% still in touch` : ''}</span>
|
|
241
|
+
</div>
|
|
242
|
+
);
|
|
243
|
+
})}
|
|
244
|
+
</div>
|
|
245
|
+
</>
|
|
246
|
+
)}
|
|
247
|
+
|
|
248
|
+
{social.posts?.posts?.length > 0 && (
|
|
249
|
+
<>
|
|
250
|
+
<h2 style={h2}>Does posting work?</h2>
|
|
251
|
+
<div style={box}>
|
|
252
|
+
<div style={{ fontSize: 13 }}>
|
|
253
|
+
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>.
|
|
254
|
+
</div>
|
|
255
|
+
<div style={{ fontSize: 11.5, color: '#778', marginTop: 4 }}>What came after a post, not proof it came from it.</div>
|
|
256
|
+
{[...social.posts.posts].sort((a, b) => b.week - a.week).slice(0, 3).map((p) => (
|
|
257
|
+
<div key={p.t} style={{ fontSize: 12.5, color: '#b8c4c4', marginTop: 6 }}>
|
|
258
|
+
{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}
|
|
259
|
+
</div>
|
|
260
|
+
))}
|
|
261
|
+
</div>
|
|
262
|
+
</>
|
|
263
|
+
)}
|
|
264
|
+
|
|
265
|
+
{social.invites?.total > 0 && (
|
|
266
|
+
<>
|
|
267
|
+
<h2 style={h2}>Who reaches out</h2>
|
|
268
|
+
<div style={box}>
|
|
269
|
+
<span style={{ fontSize: 13 }}>
|
|
270
|
+
{social.invites.incoming.toLocaleString()} requests came to you and you sent {social.invites.outgoing.toLocaleString()}
|
|
271
|
+
{' '}({Math.round((100 * social.invites.incoming) / social.invites.total)}% came to you), as far as your export shows.
|
|
272
|
+
</span>
|
|
273
|
+
</div>
|
|
274
|
+
</>
|
|
275
|
+
)}
|
|
276
|
+
</>
|
|
277
|
+
)}
|
|
278
|
+
</main>
|
|
279
|
+
</div>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
@@ -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,152 @@
|
|
|
1
|
+
// Save the Galaxy as a picture, or record its replay as a video (the physics
|
|
2
|
+
// lab, experimental). The Galaxy registers its <svg> here while it's on screen;
|
|
3
|
+
// the lab's buttons call these. Nothing leaves the computer: the file is saved
|
|
4
|
+
// like any other download.
|
|
5
|
+
|
|
6
|
+
import { clockNow, setClock, play, pause } from './galaxy-lab.js';
|
|
7
|
+
|
|
8
|
+
let current = null; // { svg, stamp: () => text or null, legend: () => [[label, colour]] }
|
|
9
|
+
|
|
10
|
+
export function registerGalaxy(entry) {
|
|
11
|
+
current = entry;
|
|
12
|
+
return () => { if (current === entry) current = null; };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const galaxyShown = () => current != null;
|
|
16
|
+
|
|
17
|
+
// The lab's highlight rules live in the page's CSS, which a picture of the
|
|
18
|
+
// <svg> alone doesn't carry, so they ride along inside it.
|
|
19
|
+
const EXPORT_CSS = `
|
|
20
|
+
.lab-focus .gn:not(.lit), .lab-focus .gl:not(.lit) { opacity: 0.06; }
|
|
21
|
+
.lab-focus .gl.lit { stroke-opacity: 0.7; }
|
|
22
|
+
.lab-focus .dot-rings, .lab-focus .catalyst-ring { opacity: 0.15; }
|
|
23
|
+
.lab-find .gn:not(.found), .lab-find .gl:not(.found) { opacity: 0.07; }
|
|
24
|
+
.lab-find .gn.found { stroke: #fff; stroke-width: 2px; }
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
function svgImage(svg) {
|
|
28
|
+
const clone = svg.cloneNode(true);
|
|
29
|
+
clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
|
30
|
+
// The page's font, which the <svg> inherits on screen but not in a file.
|
|
31
|
+
clone.setAttribute('font-family', getComputedStyle(svg).fontFamily || 'system-ui, sans-serif');
|
|
32
|
+
const style = document.createElementNS('http://www.w3.org/2000/svg', 'style');
|
|
33
|
+
style.textContent = EXPORT_CSS;
|
|
34
|
+
clone.insertBefore(style, clone.firstChild);
|
|
35
|
+
const xml = new XMLSerializer().serializeToString(clone);
|
|
36
|
+
const img = new Image();
|
|
37
|
+
img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(xml)}`;
|
|
38
|
+
return img.decode().then(() => img);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// One frame: the Galaxy, its date while a replay runs, the legend, and a small credit.
|
|
42
|
+
async function drawFrame(ctx, w, h) {
|
|
43
|
+
const img = await svgImage(current.svg);
|
|
44
|
+
ctx.fillStyle = '#0a0a1a';
|
|
45
|
+
ctx.fillRect(0, 0, w, h);
|
|
46
|
+
ctx.drawImage(img, 0, 0, w, h);
|
|
47
|
+
const scale = w / current.svg.clientWidth;
|
|
48
|
+
ctx.save();
|
|
49
|
+
ctx.scale(scale, scale);
|
|
50
|
+
const cw = w / scale;
|
|
51
|
+
const ch = h / scale;
|
|
52
|
+
const stamp = current.stamp();
|
|
53
|
+
if (stamp) {
|
|
54
|
+
ctx.font = '600 15px -apple-system, system-ui, sans-serif';
|
|
55
|
+
const tw = ctx.measureText(stamp).width;
|
|
56
|
+
ctx.fillStyle = 'rgba(10,15,30,0.85)';
|
|
57
|
+
ctx.beginPath();
|
|
58
|
+
ctx.roundRect?.(cw / 2 - tw / 2 - 16, ch - 52, tw + 32, 32, 16);
|
|
59
|
+
ctx.fill();
|
|
60
|
+
ctx.fillStyle = '#e6edf5';
|
|
61
|
+
ctx.textAlign = 'center';
|
|
62
|
+
ctx.fillText(stamp, cw / 2, ch - 31);
|
|
63
|
+
}
|
|
64
|
+
ctx.textAlign = 'left';
|
|
65
|
+
ctx.font = '11px -apple-system, system-ui, sans-serif';
|
|
66
|
+
let x = 20;
|
|
67
|
+
for (const [label, colour] of current.legend()) {
|
|
68
|
+
ctx.fillStyle = colour;
|
|
69
|
+
ctx.beginPath();
|
|
70
|
+
ctx.arc(x + 4, ch - 24, 4, 0, Math.PI * 2);
|
|
71
|
+
ctx.fill();
|
|
72
|
+
ctx.fillStyle = '#9aa';
|
|
73
|
+
ctx.fillText(label, x + 12, ch - 20);
|
|
74
|
+
x += 20 + ctx.measureText(label).width;
|
|
75
|
+
}
|
|
76
|
+
ctx.textAlign = 'right';
|
|
77
|
+
ctx.fillStyle = 'rgba(255,255,255,0.35)';
|
|
78
|
+
ctx.fillText('Six Degrees', cw - 16, 24);
|
|
79
|
+
ctx.restore();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function save(blob, name) {
|
|
83
|
+
const url = URL.createObjectURL(blob);
|
|
84
|
+
const a = document.createElement('a');
|
|
85
|
+
a.href = url;
|
|
86
|
+
a.download = name;
|
|
87
|
+
// Anything appended to <body> is position: fixed (TRAPS §29). Gone at once.
|
|
88
|
+
a.style.position = 'fixed';
|
|
89
|
+
a.style.left = '-9999px';
|
|
90
|
+
document.body.appendChild(a);
|
|
91
|
+
a.click();
|
|
92
|
+
a.remove();
|
|
93
|
+
setTimeout(() => URL.revokeObjectURL(url), 60000);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const today = () => new Date().toISOString().slice(0, 10);
|
|
97
|
+
|
|
98
|
+
function canvasFor(scale) {
|
|
99
|
+
const w = Math.round(current.svg.clientWidth * scale);
|
|
100
|
+
const h = Math.round(current.svg.clientHeight * scale);
|
|
101
|
+
const canvas = document.createElement('canvas');
|
|
102
|
+
canvas.width = w;
|
|
103
|
+
canvas.height = h;
|
|
104
|
+
return { canvas, ctx: canvas.getContext('2d'), w, h };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The Galaxy as it is on screen, at twice the size, as a PNG. */
|
|
108
|
+
export async function savePicture() {
|
|
109
|
+
if (!current) throw new Error('Open the Galaxy first.');
|
|
110
|
+
const { canvas, ctx, w, h } = canvasFor(2);
|
|
111
|
+
await drawFrame(ctx, w, h);
|
|
112
|
+
const blob = await new Promise((resolve) => canvas.toBlob(resolve, 'image/png'));
|
|
113
|
+
save(blob, `six-degrees-galaxy-${today()}.png`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export const canRecord = () => typeof window !== 'undefined' && typeof window.MediaRecorder !== 'undefined'
|
|
117
|
+
&& typeof HTMLCanvasElement !== 'undefined' && typeof HTMLCanvasElement.prototype.captureStream === 'function';
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Plays the replay from the start and records it: a video the length of the
|
|
121
|
+
* replay, at 12 frames a second. Resolves when it's saved.
|
|
122
|
+
*/
|
|
123
|
+
export async function recordReplay() {
|
|
124
|
+
if (!current) throw new Error('Open the Galaxy first.');
|
|
125
|
+
if (!canRecord()) throw new Error('This window can’t record video.');
|
|
126
|
+
const { canvas, ctx, w, h } = canvasFor(1);
|
|
127
|
+
const type = ['video/mp4;codecs=avc1', 'video/webm;codecs=vp9', 'video/webm', 'video/mp4'].find((t) => MediaRecorder.isTypeSupported(t));
|
|
128
|
+
if (!type) throw new Error('This window can’t record video.');
|
|
129
|
+
await drawFrame(ctx, w, h);
|
|
130
|
+
const recorder = new MediaRecorder(canvas.captureStream(12), { mimeType: type, videoBitsPerSecond: 6_000_000 });
|
|
131
|
+
const chunks = [];
|
|
132
|
+
recorder.ondataavailable = (e) => { if (e.data.size) chunks.push(e.data); };
|
|
133
|
+
const done = new Promise((resolve) => { recorder.onstop = resolve; });
|
|
134
|
+
recorder.start(250);
|
|
135
|
+
pause();
|
|
136
|
+
setClock({ at: clockNow().min });
|
|
137
|
+
play();
|
|
138
|
+
let drawing = false;
|
|
139
|
+
await new Promise((resolve) => {
|
|
140
|
+
const tick = setInterval(async () => {
|
|
141
|
+
if (drawing) return;
|
|
142
|
+
drawing = true;
|
|
143
|
+
try { await drawFrame(ctx, w, h); } finally { drawing = false; }
|
|
144
|
+
if (!clockNow().playing) { clearInterval(tick); setTimeout(resolve, 600); }
|
|
145
|
+
}, 1000 / 12);
|
|
146
|
+
});
|
|
147
|
+
await drawFrame(ctx, w, h);
|
|
148
|
+
recorder.stop();
|
|
149
|
+
await done;
|
|
150
|
+
const ext = type.startsWith('video/mp4') ? 'mp4' : 'webm';
|
|
151
|
+
save(new Blob(chunks, { type: type.split(';')[0] }), `six-degrees-replay-${today()}.${ext}`);
|
|
152
|
+
}
|