six-degrees 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +14 -14
- 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 +20 -19
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +20 -19
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +16 -15
- 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.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/crm/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/social/messages/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/social/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/update/route.js +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 +24 -23
- package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +24 -23
- 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 +18 -17
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +22 -21
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +18 -17
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +22 -21
- 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 +24 -23
- package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +24 -23
- 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 +18 -17
- 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 +24 -23
- package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +24 -23
- 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 +18 -17
- 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 +24 -23
- package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +24 -23
- 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 +18 -17
- 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 +24 -23
- package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +24 -23
- 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 +18 -17
- 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 +24 -23
- package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +24 -23
- 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 +18 -17
- 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 +24 -23
- package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +24 -23
- 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 +18 -17
- 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 +24 -23
- package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +24 -23
- 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 +18 -17
- package/.next/standalone/.next/server/app/social/page.js +1 -1
- package/.next/standalone/.next/server/app/social/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/social/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/social.html +1 -1
- package/.next/standalone/.next/server/app/social.rsc +24 -23
- package/.next/standalone/.next/server/app/social.segments/_full.segment.rsc +24 -23
- package/.next/standalone/.next/server/app/social.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/social.segments/social/__PAGE__.segment.rsc +18 -17
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-rnla2._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__00la_y0._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__05_3-22._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__07t47cm._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cj5jqc._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0e0fei3._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljnyho._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o5-tcg._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0qrnous._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t3v1a1._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0um1uey._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0wmexuc._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xu34sz._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__133knx1._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__13dx4sd._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__14ou7j5._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__15-17hl._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__15wc9e-._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__16iu0hv._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__18bcnu4._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__19b7mig._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1bbcwub._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1eris29._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1gjo_cq._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1kbl21f._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qcm08_._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1rphsdk._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tir0vc._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1w25q0m._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__20rwfdw._.js +2 -2
- package/.next/standalone/.next/server/chunks/_1_fynia._.js +7 -7
- package/.next/standalone/.next/server/chunks/_1itd1o_._.js +2 -2
- package/.next/standalone/.next/server/chunks/lib_0851sk0._.js +5 -5
- package/.next/standalone/.next/server/chunks/lib_1q7p2ro._.js +5 -5
- package/.next/standalone/.next/server/chunks/lib_1r6l86l._.js +5 -5
- package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +9 -9
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__08oxiqh._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_03l7vge._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_08g31gm._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0gk_ofx._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0nwsf93._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0vho1lb._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_0jdkqgi._.js → _1-658fq._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_1bc46kz._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1f_n00p._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1fm1ih_._.js +9 -0
- package/.next/standalone/.next/server/chunks/ssr/_1k2nmbw._.js +9 -0
- package/.next/standalone/.next/server/chunks/ssr/_2105jz8._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_141w189._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_components_0t908t-._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_components_0uuff77._.js +13 -0
- package/.next/standalone/.next/server/chunks/ssr/app_components_1j3q-h5._.js +13 -0
- package/.next/standalone/.next/server/chunks/ssr/{_0gtv9jb._.js → app_components_CompanyScores_1g0mytz.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_components_settings_SectorPicker_1c-29qw.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_import_page_1fmu6_l.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +14 -12
- package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +6 -6
- package/.next/standalone/.next/server/chunks/ssr/app_settings_page_1f8mjj_.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +4 -4
- package/.next/standalone/.next/server/chunks/ssr/app_social_page_0h7zxda.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{lib_0i8v-s3._.js → lib_1c4ieuw._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/lib_galaxy-lab_108_lu-.js +1 -1
- 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/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- 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/02pq76u7y4v9-.js +22 -0
- package/.next/standalone/.next/static/chunks/0cmq0exd4d0h9.js +1 -0
- package/.next/{static/chunks/2wgz9ietkqqe6.js → standalone/.next/static/chunks/0i02yxppzds1o.js} +1 -1
- package/.next/standalone/.next/static/chunks/0m_p4k-q2qh--.js +1 -0
- package/.next/standalone/.next/static/chunks/0oyslj62jkubd.js +1 -0
- package/.next/standalone/.next/static/chunks/{1x_ysly42-teq.css → 0vo-l8hxneba6.css} +1 -1
- package/.next/{static/chunks/3sowbvbaj1s6n.js → standalone/.next/static/chunks/0ws7zy0w57ufi.js} +1 -1
- package/.next/standalone/.next/static/chunks/0yh8z2tbrc5i_.js +1 -0
- package/.next/standalone/.next/static/chunks/172e10mi-tpo_.js +34 -0
- package/.next/standalone/.next/static/chunks/1ei5vbe2uw6fl.js +1 -0
- package/.next/standalone/.next/static/chunks/1f7_pl_wq7lh3.js +11 -0
- package/.next/standalone/.next/static/chunks/1jzbe7zdx9vbj.js +2 -0
- package/.next/standalone/.next/static/chunks/1m3017gp790pw.js +45 -0
- package/.next/standalone/.next/static/chunks/1t20e8u0st7ep.js +1 -0
- package/.next/standalone/.next/static/chunks/2-ch9rm27eril.js +11 -0
- package/.next/standalone/.next/static/chunks/2ldv-v3s9yaif.js +1 -0
- package/.next/standalone/.next/static/chunks/3bp284gjf7dyq.js +1 -0
- package/.next/standalone/.next/static/chunks/3k1f6clbn3gj3.js +7 -0
- package/.next/standalone/.next/static/chunks/3l4t8je6iguzc.js +1 -0
- package/.next/standalone/.next/static/chunks/{0muwt9bykzr-1.js → 3tplzuc9jk-tl.js} +1 -1
- package/.next/standalone/.next/static/chunks/42m0bqr8pbovz.js +11 -0
- package/.next/standalone/CHANGELOG.md +61 -0
- package/.next/standalone/README.md +22 -8
- package/.next/standalone/app/api/scraper/route.js +40 -3
- package/.next/standalone/app/api/update/route.js +12 -6
- package/.next/standalone/app/components/AppHeader.js +257 -0
- package/.next/standalone/app/components/Avatar.js +2 -1
- package/.next/standalone/app/components/ChainView.js +12 -12
- package/.next/standalone/app/components/FilterPanel.js +20 -12
- package/.next/standalone/app/components/ForceGraph.js +163 -25
- package/.next/standalone/app/components/GalaxyLab.js +43 -12
- package/.next/standalone/app/components/GridView.js +4 -4
- package/.next/standalone/app/components/LinkedInLimits.js +2 -1
- package/.next/standalone/app/components/ListView.js +3 -3
- package/.next/standalone/app/components/NoteDetail.js +2 -1
- package/.next/standalone/app/components/OnboardingGate.js +2 -2
- package/.next/standalone/app/components/OutlinkQuest.js +2 -1
- package/.next/standalone/app/components/PathsAnalyzer.js +3 -2
- package/.next/standalone/app/components/PeopleMap.js +2 -1
- package/.next/standalone/app/components/RingsView.js +3 -3
- package/.next/standalone/app/components/ScanTrail.js +85 -0
- package/.next/standalone/app/components/SeparationView.js +100 -77
- package/.next/standalone/app/components/Sidebar.js +4 -4
- package/.next/standalone/app/components/TheirCircle.js +2 -1
- package/.next/standalone/app/components/ThemeLoader.js +9 -0
- package/.next/standalone/app/components/UpdatePanel.js +20 -2
- package/.next/standalone/app/components/settings/AppearanceSection.js +226 -0
- package/.next/standalone/app/components/settings/NetworkHealthSection.js +2 -1
- package/.next/standalone/app/components/ui.js +2 -1
- package/.next/standalone/app/globals.css +70 -2
- package/.next/standalone/app/import/page.js +3 -3
- package/.next/standalone/app/layout.js +11 -2
- package/.next/standalone/app/page.js +69 -215
- package/.next/standalone/app/paths/page.js +8 -21
- package/.next/standalone/app/profile/page.js +11 -10
- package/.next/standalone/app/queue/page.js +8 -21
- package/.next/standalone/app/scores/page.js +1 -1
- package/.next/standalone/app/settings/page.js +7 -3
- package/.next/standalone/app/setup/page.js +83 -88
- package/.next/standalone/app/social/Crm.js +2 -1
- package/.next/standalone/app/social/SocialHub.js +2 -2
- package/.next/standalone/app/social/page.js +1 -1
- package/.next/standalone/lib/data-import.js +1 -0
- package/.next/standalone/lib/galaxy-export.js +2 -1
- package/.next/standalone/lib/galaxy-lab.js +24 -8
- package/.next/standalone/lib/level.js +51 -0
- package/.next/standalone/lib/release.js +13 -7
- package/.next/standalone/lib/scanner-python.js +38 -8
- package/.next/standalone/lib/scraper-client.js +2 -1
- package/.next/standalone/lib/separation.js +4 -2
- package/.next/standalone/lib/theme-store.js +61 -0
- package/.next/standalone/lib/themes.js +186 -0
- package/.next/standalone/lib/tiers.js +2 -8
- package/.next/standalone/package-lock.json +2 -2
- package/.next/standalone/package.json +4 -4
- package/.next/standalone/scripts/build-desktop.mjs +325 -0
- package/.next/standalone/scripts/build-site.mjs +85 -15
- package/.next/standalone/scripts/gen-test-network.mjs +93 -0
- package/.next/standalone/scripts/next.mjs +20 -0
- package/.next/standalone/scripts/readme_buttons.py +3 -0
- package/.next/standalone/scripts/requirements.txt +18 -1
- package/.next/standalone/scripts/site-news.mjs +54 -9
- package/.next/standalone/scripts/smoke-desktop.mjs +177 -0
- package/.next/standalone/scripts/windows/six-degrees.iss +100 -0
- package/.next/static/chunks/02pq76u7y4v9-.js +22 -0
- package/.next/static/chunks/0cmq0exd4d0h9.js +1 -0
- package/.next/{standalone/.next/static/chunks/2wgz9ietkqqe6.js → static/chunks/0i02yxppzds1o.js} +1 -1
- package/.next/static/chunks/0m_p4k-q2qh--.js +1 -0
- package/.next/static/chunks/0oyslj62jkubd.js +1 -0
- package/.next/static/chunks/{1x_ysly42-teq.css → 0vo-l8hxneba6.css} +1 -1
- package/.next/{standalone/.next/static/chunks/3sowbvbaj1s6n.js → static/chunks/0ws7zy0w57ufi.js} +1 -1
- package/.next/static/chunks/0yh8z2tbrc5i_.js +1 -0
- package/.next/static/chunks/172e10mi-tpo_.js +34 -0
- package/.next/static/chunks/1ei5vbe2uw6fl.js +1 -0
- package/.next/static/chunks/1f7_pl_wq7lh3.js +11 -0
- package/.next/static/chunks/1jzbe7zdx9vbj.js +2 -0
- package/.next/static/chunks/1m3017gp790pw.js +45 -0
- package/.next/static/chunks/1t20e8u0st7ep.js +1 -0
- package/.next/static/chunks/2-ch9rm27eril.js +11 -0
- package/.next/static/chunks/2ldv-v3s9yaif.js +1 -0
- package/.next/static/chunks/3bp284gjf7dyq.js +1 -0
- package/.next/static/chunks/3k1f6clbn3gj3.js +7 -0
- package/.next/static/chunks/3l4t8je6iguzc.js +1 -0
- package/.next/static/chunks/{0muwt9bykzr-1.js → 3tplzuc9jk-tl.js} +1 -1
- package/.next/static/chunks/42m0bqr8pbovz.js +11 -0
- package/CHANGELOG.md +61 -0
- package/README.md +22 -8
- package/lib/data-import.js +1 -0
- package/lib/galaxy-export.js +2 -1
- package/lib/galaxy-lab.js +24 -8
- package/lib/level.js +51 -0
- package/lib/release.js +13 -7
- package/lib/scanner-python.js +38 -8
- package/lib/scraper-client.js +2 -1
- package/lib/separation.js +4 -2
- package/lib/theme-store.js +61 -0
- package/lib/themes.js +186 -0
- package/lib/tiers.js +2 -8
- package/package.json +4 -4
- package/scripts/requirements.txt +18 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljcioj._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__16hs8kw._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_01kacen._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0xi-fbs._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0xmwmfv._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1-b9hj7._.js +0 -9
- package/.next/standalone/.next/server/chunks/ssr/_112ewem._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_16ab63q._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1fxm0-s._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1ra1d3r._.js +0 -9
- package/.next/standalone/.next/server/chunks/ssr/app_components_ui_0pdmz2z.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/lib_13rapwj._.js +0 -3
- package/.next/standalone/.next/static/chunks/03bziijw5srol.js +0 -1
- package/.next/standalone/.next/static/chunks/0bgq8wmopl_ig.js +0 -7
- package/.next/standalone/.next/static/chunks/0ezus6p2eb-vg.js +0 -1
- package/.next/standalone/.next/static/chunks/0g_fy2034bp0x.js +0 -34
- package/.next/standalone/.next/static/chunks/0q5dru4q6gxl5.js +0 -22
- package/.next/standalone/.next/static/chunks/0w-byiwb2zax8.js +0 -2
- package/.next/standalone/.next/static/chunks/1g7jcovdejc9-.js +0 -1
- package/.next/standalone/.next/static/chunks/1i51yylxdzht1.js +0 -1
- package/.next/standalone/.next/static/chunks/1zf8swaasjf1e.js +0 -43
- package/.next/standalone/.next/static/chunks/2dwtoszeq91us.js +0 -1
- package/.next/standalone/.next/static/chunks/2rqtpccruavf2.js +0 -1
- package/.next/standalone/.next/static/chunks/30x3oq_st7a5r.js +0 -1
- package/.next/standalone/.next/static/chunks/355ryio1b7sm1.js +0 -1
- package/.next/standalone/.next/static/chunks/3ap5hgma7n0o7.js +0 -1
- package/.next/standalone/.next/static/chunks/3fci6qi39-1n5.js +0 -1
- package/.next/static/chunks/03bziijw5srol.js +0 -1
- package/.next/static/chunks/0bgq8wmopl_ig.js +0 -7
- package/.next/static/chunks/0ezus6p2eb-vg.js +0 -1
- package/.next/static/chunks/0g_fy2034bp0x.js +0 -34
- package/.next/static/chunks/0q5dru4q6gxl5.js +0 -22
- package/.next/static/chunks/0w-byiwb2zax8.js +0 -2
- package/.next/static/chunks/1g7jcovdejc9-.js +0 -1
- package/.next/static/chunks/1i51yylxdzht1.js +0 -1
- package/.next/static/chunks/1zf8swaasjf1e.js +0 -43
- package/.next/static/chunks/2dwtoszeq91us.js +0 -1
- package/.next/static/chunks/2rqtpccruavf2.js +0 -1
- package/.next/static/chunks/30x3oq_st7a5r.js +0 -1
- package/.next/static/chunks/355ryio1b7sm1.js +0 -1
- package/.next/static/chunks/3ap5hgma7n0o7.js +0 -1
- package/.next/static/chunks/3fci6qi39-1n5.js +0 -1
- /package/.next/standalone/.next/static/{I-cj79yE2IZ8taX2dZGp3 → dQUvvVSXqbkEFC3-CNcwT}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{I-cj79yE2IZ8taX2dZGp3 → dQUvvVSXqbkEFC3-CNcwT}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{I-cj79yE2IZ8taX2dZGp3 → dQUvvVSXqbkEFC3-CNcwT}/_ssgManifest.js +0 -0
- /package/.next/static/{I-cj79yE2IZ8taX2dZGp3 → dQUvvVSXqbkEFC3-CNcwT}/_buildManifest.js +0 -0
- /package/.next/static/{I-cj79yE2IZ8taX2dZGp3 → dQUvvVSXqbkEFC3-CNcwT}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{I-cj79yE2IZ8taX2dZGp3 → dQUvvVSXqbkEFC3-CNcwT}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
(globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,33821,e=>{"use strict";var t=e.i(43476),n=e.i(71645),r=e.i(22016),o=e.i(18566),i=e.i(26005),l=e.i(76760),a=e.i(30215);let s=["#FFD700","#9B59B6","#3498DB"],c=`
|
|
2
|
+
@keyframes trail-in { from { transform: scale(0); opacity: 0; } }
|
|
3
|
+
@keyframes trail-pulse { 0%, 100% { opacity: 0.45; transform: scale(0.8); } 50% { opacity: 1; transform: scale(1.25); } }
|
|
4
|
+
.scan-trail { transition: opacity 1.2s ease; }
|
|
5
|
+
.scan-trail .dot { animation: trail-in 0.35s cubic-bezier(0.2, 0.9, 0.3, 1.3); }
|
|
6
|
+
.scan-trail .dot.newest { box-shadow: 0 0 6px 1px currentColor; }
|
|
7
|
+
.scan-trail .seed { animation: trail-pulse 1.4s ease-in-out infinite; }
|
|
8
|
+
@media (prefers-reduced-motion: reduce) {
|
|
9
|
+
.scan-trail .dot, .scan-trail .seed { animation: none; }
|
|
10
|
+
.scan-trail { transition: none; }
|
|
11
|
+
}`;function u({inset:e}){let o=(0,a.default)(),i=(0,n.useRef)(null),[l,d]=(0,n.useState)(60),[f,p]=(0,n.useState)({on:!1,pages:0,label:""}),g=o.running&&!o.pending,m=o.target?.name?`Scanning ${o.target.name}`:"Scanning";g&&(!f.on||f.pages!==(o.pages||0)||f.label!==m)&&p({on:!0,pages:o.pages||0,label:m}),(0,n.useEffect)(()=>{if(g||!f.on)return;let e=setTimeout(()=>p(e=>({...e,on:!1})),4e3);return()=>clearTimeout(e)},[g,f.on]),(0,n.useEffect)(()=>{let e=i.current;if(!e)return;let t=new ResizeObserver(()=>d(Math.max(8,Math.floor(e.clientWidth/9)-4)));return t.observe(e),()=>t.disconnect()},[]);let{pages:h}=f,b=Math.max(0,h-l),y=[];for(let e=b;e<h;e++)y.push(e);let x=`${f.label}: ${0===h?"starting":`${h.toLocaleString("en-US")} page${1===h?"":"s"} read`}. Open the Scan page`;return(0,t.jsxs)("div",{ref:i,"aria-hidden":!f.on,style:{position:"absolute",left:e,right:e,bottom:-4,height:7,pointerEvents:"none"},children:[(0,t.jsx)("style",{children:c}),(0,t.jsxs)(r.default,{href:"/setup",title:x,tabIndex:f.on?0:-1,className:"scan-trail",style:{position:"absolute",left:0,top:0,height:7,display:"flex",alignItems:"center",gap:4,pointerEvents:f.on?"auto":"none",opacity:+!!f.on,textDecoration:"none"},children:[b>0&&(0,t.jsxs)("span",{style:{fontSize:9,fontWeight:700,color:"#889",marginRight:2,fontVariantNumeric:"tabular-nums"},children:["+",b.toLocaleString("en-US")]}),y.map(e=>{let n=s[e%s.length];return(0,t.jsx)("span",{className:`dot${e===h-1?" newest":""}`,style:{width:5,height:5,borderRadius:"50%",background:n,color:n,flexShrink:0}},e)}),f.on&&(0,t.jsx)("span",{className:"seed",style:{width:5,height:5,borderRadius:"50%",background:"#00ff88",flexShrink:0}})]})]})}var d=e.i(15812),f=e.i(40996),p=e.i(19821),g=e.i(98658),m=e.i(64745);let h={fontWeight:700,margin:0,whiteSpace:"nowrap",background:"linear-gradient(135deg, #FFD700, #9B59B6, #3498DB)",WebkitBackgroundClip:"text",WebkitTextFillColor:"transparent"};function b({isMobile:e,onOpen:r,openId:i}){let{userId:l}=(0,d.useUser)(),a=(0,o.useRouter)(),[s,c]=(0,n.useState)([]),[u,f]=(0,n.useState)(!1),p=(0,n.useRef)(!1),g=(0,n.useRef)(r);(0,n.useEffect)(()=>{g.current=r}),(0,n.useEffect)(()=>{l&&fetch(`/api/notifications?userId=${l}`).then(e=>e.json()).then(e=>{let t=e.notifications||[];c(t);let n=null!=i&&!p.current&&t.find(e=>String(e.id)===String(i));n&&g.current&&(p.current=!0,g.current(n))}).catch(()=>{})},[l,i]);let m=s.filter(e=>!e.seen).length,h=e=>{e.seen||(fetch("/api/notifications",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({action:"mark-seen",id:e.id})}).catch(()=>{}),c(t=>t.map(t=>t.id===e.id?{...t,seen:!0}:t))),f(!1),r?r(e):a.push(`/?note=${encodeURIComponent(e.id)}`)};return(0,t.jsxs)("div",{style:{position:"relative"},children:[(0,t.jsxs)("button",{onClick:()=>f(!u),title:"Notifications","aria-label":m?`Notifications, ${m} new`:"Notifications",style:{width:e?28:32,height:e?28:32,borderRadius:"50%",border:"none",cursor:"pointer",background:"rgba(255,255,255,0.06)",color:"#888",fontSize:e?12:14,display:"flex",alignItems:"center",justifyContent:"center",position:"relative"},children:["🔔",m>0&&(0,t.jsx)("span",{style:{position:"absolute",top:-2,right:-2,width:16,height:16,borderRadius:"50%",background:"#ff5050",color:"#fff",fontSize:9,fontWeight:700,display:"flex",alignItems:"center",justifyContent:"center"},children:m})]}),u&&(0,t.jsxs)("div",{style:{position:"absolute",top:40,right:0,width:320,maxHeight:400,background:"color-mix(in srgb, var(--sd-bg) 97%, transparent)",border:"1px solid rgba(255,255,255,0.1)",borderRadius:12,overflow:"hidden",zIndex:100,backdropFilter:"blur(12px)",WebkitBackdropFilter:"blur(12px)",boxShadow:"0 8px 32px rgba(0,0,0,0.5)"},children:[(0,t.jsxs)("div",{style:{padding:"12px 16px",borderBottom:"1px solid rgba(255,255,255,0.08)",display:"flex",justifyContent:"space-between",alignItems:"center"},children:[(0,t.jsx)("span",{style:{fontWeight:700,fontSize:14},children:"Notifications"}),m>0&&(0,t.jsx)("button",{onClick:()=>{fetch("/api/notifications",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({action:"mark-all-seen"})}),c(e=>e.map(e=>({...e,seen:!0})))},style:{background:"none",border:"none",color:"#3498DB",fontSize:11,cursor:"pointer",fontWeight:600},children:"Mark all read"})]}),(0,t.jsx)("div",{style:{overflowY:"auto",maxHeight:340},children:0===s.length?(0,t.jsx)("div",{style:{padding:20,textAlign:"center",color:"#555",fontSize:12},children:"No notifications yet"}):s.slice(0,20).map(e=>(0,t.jsx)("div",{role:"button",tabIndex:0,onClick:()=>h(e),onKeyDown:t=>{"Enter"===t.key&&h(e)},onMouseEnter:e=>{e.currentTarget.style.background="rgba(255,255,255,0.06)"},onMouseLeave:t=>{t.currentTarget.style.background=e.seen?"transparent":"rgba(255,215,0,0.04)"},style:{padding:"10px 16px",borderBottom:"1px solid rgba(255,255,255,0.04)",cursor:"pointer",background:e.seen?"transparent":"rgba(255,215,0,0.04)"},children:(0,t.jsxs)("div",{style:{display:"flex",gap:8,alignItems:"flex-start"},children:[(0,t.jsx)("span",{style:{fontSize:16},children:e.icon||"📌"}),(0,t.jsxs)("div",{style:{flex:1},children:[(0,t.jsxs)("div",{style:{fontSize:12,fontWeight:600,color:e.seen?"#888":"#fff"},children:[e.title,(0,t.jsx)("span",{style:{color:"#556",marginLeft:6},children:"›"})]}),e.message&&(0,t.jsx)("div",{style:{fontSize:10,color:"#666",marginTop:2},children:e.message}),(0,t.jsx)("div",{style:{fontSize:9,color:"#444",marginTop:3},children:new Date(e.created_at).toLocaleDateString()})]})]})},e.id))})]})]})}function y({isMobile:e}){let n=(0,a.default)(),r=n.running&&"refresh"===n.action,o=(0,f.busyReason)(n);return(0,t.jsxs)("button",{onClick:async()=>{try{let e=(0,f.notReadyMessage)(await (0,f.scraperStatus)());if(e)return void alert(e);if(!window.confirm("Check for new connections?\n\nThis opens a Chrome window on your LinkedIn connections list and reads it from the newest, stopping once it reaches people already saved. It usually takes under a minute. It reads your own list, not a search, so it doesn’t use your search budget, but like any scan it is LinkedIn traffic from your account."))return;await (0,f.beginScrape)("refresh"),alert("Checking for new connections — watch it on the Scan page.")}catch(e){alert(e.message)}},disabled:!!o,title:o?`${o}. One scan at a time.`:"Check for new connections: reads your LinkedIn connections list from the newest","aria-label":"Check for new connections",style:{height:e?28:32,padding:e?"0 10px":"0 14px",borderRadius:16,border:"1px solid rgba(0,255,136,0.3)",cursor:o?"not-allowed":"pointer",opacity:o?.45:1,background:"rgba(0,255,136,0.08)",color:"#bff5d9",fontSize:e?11:12.5,fontWeight:700,display:"flex",alignItems:"center",gap:6,whiteSpace:"nowrap",flexShrink:0},children:[(0,t.jsx)("span",{"aria-hidden":"true",style:{fontSize:e?12:14},children:"↻"}),r?"Checking…":e?"New":"Check for new"]})}let x=null;function w({isMobile:e,csvMode:r,level:o}){let{userId:i}=(0,d.useUser)(),l=m.IS_DEMO?"sample":r?"csv":"own",a=(0,n.useSyncExternalStore)(g.watchLevel,()=>(0,g.levelNow)(l),()=>null);(0,n.useEffect)(()=>{null!=o?(0,g.rememberLevel)(o,l):null!=a||m.IS_DEMO||r||!i||(x||(x=(0,p.loadNetwork)(i).then(e=>(0,g.networkLevel)(e.degree1,e.degree2)).catch(()=>(x=null,null))),x.then(e=>{null!=e&&(0,g.rememberLevel)(e,l)}))},[o,a,r,l,i]);let s=o??a;return(0,t.jsx)("a",{href:m.IS_DEMO?"/launch":r?"/import":"/profile",title:null!=s?`Your profile \xb7 level ${s}`:"Your profile",style:{width:e?30:36,height:e?30:36,borderRadius:"50%",display:"flex",alignItems:"center",justifyContent:"center",background:"linear-gradient(135deg, #FFD700, #FF6B35)",color:"#000",fontWeight:800,fontSize:e?11:14,textDecoration:"none",marginLeft:e?2:4,flexShrink:0},children:s??""})}e.s(["default",0,function({active:e,isMobile:o,csvMode:a=!1,onMode:s,chips:c,level:d,onOpenNote:f,openNoteId:p=null,brand:g="h1",children:x}){let S=function(){let[e,t]=(0,n.useState)(!1);return(0,n.useEffect)(()=>{let e=()=>t(window.innerWidth<768);return e(),window.addEventListener("resize",e),()=>window.removeEventListener("resize",e)},[]),e}(),k=o??S,v=!m.IS_DEMO&&!a;return(0,t.jsxs)("header",{style:{position:"relative",padding:k?"10px 12px":"20px 30px",borderBottom:"1px solid rgba(255,255,255,0.1)",flexShrink:0,background:"var(--sd-header)",backdropFilter:"var(--sd-header-blur)",WebkitBackdropFilter:"var(--sd-header-blur)"},children:[(0,t.jsxs)("div",{style:k?{display:"flex",alignItems:"center",justifyContent:"space-between",flexWrap:"wrap",gap:6,marginBottom:4}:{display:"grid",gridTemplateColumns:"1fr auto 1fr",alignItems:"center",gap:16,marginBottom:8},children:[(0,t.jsx)(r.default,{href:"/",style:{textDecoration:"none",justifySelf:"start"},children:(0,t.jsx)(g,{style:{...h,fontSize:k?16:28},children:"Six Degrees"})}),(0,t.jsx)(i.default,{active:e,isMobile:k,csvMode:a,onMode:s,after:v?(0,t.jsx)(l.default,{isMobile:k}):null,children:c}),(0,t.jsxs)("div",{style:{display:"flex",alignItems:"center",justifyContent:"flex-end",justifySelf:"end",gap:k?4:8},children:[!m.IS_DEMO&&(0,t.jsx)(r.default,{href:"/settings",title:"Settings","aria-label":"Settings",style:{width:k?28:32,height:k?28:32,borderRadius:"50%",flexShrink:0,background:"rgba(255,255,255,0.06)",color:"#888",fontSize:k?14:16,textDecoration:"none",display:"flex",alignItems:"center",justifyContent:"center"},children:"⚙︎"}),v&&(0,t.jsx)(b,{isMobile:k,onOpen:f,openId:p}),v&&(0,t.jsx)(y,{isMobile:k}),(0,t.jsx)(w,{isMobile:k,csvMode:a,level:d})]})]}),x,v&&(0,t.jsx)(u,{inset:k?12:30})]})}],33821)},26005,76760,e=>{"use strict";var t=e.i(43476),n=e.i(22016),r=e.i(64745);let o=[{key:"network",label:"Network Circle",short:"Circle",on:{background:"#fff",color:"#000"},off:{background:"transparent",color:"#888"}},{key:"degrees",label:"Degrees",on:{background:"linear-gradient(135deg, #FFD700, #FF6B35)",color:"#000"},off:{background:"rgba(255,255,255,0.12)",color:"#fff"}},{key:"separation",label:"Separation",on:{background:"linear-gradient(135deg, #00E5FF, #FFD700 55%, #FF7043)",color:"#000"},off:{background:"rgba(255,255,255,0.12)",color:"#fff"}}],i=[{key:"paths",href:"/paths",label:"Paths",color:"#00ff88",on:"linear-gradient(135deg, #00ff88, #3498DB)",demo:!0},{key:"outlink",href:"/queue",label:"Outlink",color:"#FF6B35",on:"linear-gradient(135deg, #FF6B35, #FFD700)",own:!0},{key:"scan",href:"/setup",label:"Scan",color:"#666",on:"linear-gradient(135deg, #00ff88, #1abc9c)",own:!0}];e.s(["default",0,function({active:e,isMobile:l=!1,csvMode:a=!1,onMode:s,children:c,after:u}){let d={padding:l?"6px 10px":"8px 16px",borderRadius:6,border:"none",fontSize:l?11:13,fontWeight:600,textDecoration:"none"};return(0,t.jsxs)("div",{id:"main-tabs",style:{display:"flex",gap:l?2:4,background:"rgba(255,255,255,0.08)",borderRadius:8,padding:l?2:3,flexWrap:l?"wrap":"nowrap"},children:[o.map(r=>{let o={...d,cursor:"pointer",...e===r.key?r.on:r.off},i=l&&r.short?r.short:r.label;return s?(0,t.jsx)("button",{type:"button",onClick:()=>s(r.key),style:o,children:i},r.key):(0,t.jsx)(n.default,{href:`/?mode=${r.key}`,style:{...o,display:"flex",alignItems:"center"},children:i},r.key)}),c,i.filter(e=>!r.IS_DEMO&&(!e.own||!a)).map(r=>{let o=e===r.key;return(0,t.jsxs)(n.default,{href:r.href,title:r.title,"aria-current":o?"page":void 0,style:{...d,display:"flex",alignItems:"center",gap:4,background:o?r.on:"rgba(255,255,255,0.06)",color:o?"#000":r.color},children:[r.label,r.badge&&(0,t.jsx)("span",{style:{fontSize:9,opacity:.7,marginLeft:2},children:r.badge})]},r.key)}),u]})}],26005);var l=e.i(71645),a=e.i(40996),s=e.i(68792);e.s(["default",0,function({isMobile:e=!1}){let r=(0,l.useSyncExternalStore)(s.watchAllDay,s.allDayNow,()=>!1),[o,i]=(0,l.useState)(()=>(0,a.scannerNow)()),[c,u]=(0,l.useState)(!1);if((0,l.useEffect)(()=>(0,a.watchScanner)(()=>{i((0,a.scannerNow)()),u(!1)}),[]),!r)return null;let d=o?.running&&String(o.action||"").startsWith("auto-bridge"),f={padding:e?"8px 12px":"8px 16px",borderRadius:6,fontSize:13,fontWeight:600,textDecoration:"none",display:"flex",alignItems:"center",gap:6,whiteSpace:"nowrap"};if(d)return(0,t.jsxs)(n.default,{href:"/setup",title:"Auto scan is running: see it on the Scan page",style:{...f,background:"rgba(0,255,136,0.15)",color:"#00ff88",border:"1px solid rgba(0,255,136,0.45)"},children:[(0,t.jsx)("span",{style:{width:7,height:7,borderRadius:"50%",background:"#00ff88"}}),"Auto scan"]});let p=o?.running||c;return(0,t.jsxs)("button",{onClick:()=>{u(!0),(0,s.startAutoScan)().catch(()=>u(!1))},disabled:p,title:p?"The scanner is busy: one scan at a time":"Start the all-day Auto-Bridge (experimental)",style:{...f,cursor:p?"default":"pointer",background:"rgba(255,255,255,0.04)",color:p?"#555":"#8b9a9a",border:"1px solid rgba(255,215,0,0.25)"},children:[(0,t.jsx)("span",{style:{width:7,height:7,borderRadius:"50%",background:"#556"}}),"Auto scan"]})}],76760)},28867,86106,e=>{"use strict";var t=e.i(71645),n=e.i(7214);let r=Object.freeze({known:!1,sent:new Set,mine:new Map,version:0}),o=new Set,i=r,l=null,a=new Map;function s(e=i.sent,t=i.known){for(let n of(i=Object.freeze({known:t,sent:e,mine:new Map(a),version:i.version+1}),[...o]))n()}function c(e){return o.add(e),l||i.known||(l=fetch("/api/outreach").then(e=>e.ok?e.json():null).then(e=>{e&&s(new Set((e.pending||[]).map(n.keyFor)),!0)}).catch(()=>{}).finally(()=>{l=null})),()=>o.delete(e)}function u(){return i}async function d(e){let t=await fetch("/api/outreach",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)}),n=await t.json().catch(()=>({}));if(!t.ok)throw Error(n.error||"Could not save that.");return n}async function f(e,{bridgeId:t}={}){let r=(0,n.keyFor)(e),o=a.has(r)?a.get(r):void 0;a.set(r,!0),s();try{return await d({action:"mark-sent",connectionId:e.id,profileUrl:e.profile_url,bridgeId:t})}catch(e){throw void 0===o?a.delete(r):a.set(r,o),s(),e}}async function p(e){let t=(0,n.keyFor)(e),r=a.has(t)?a.get(t):void 0;a.set(t,!1),s();try{return await d({action:"undo",connectionId:e.id,profileUrl:e.profile_url})}catch(e){throw void 0===r?a.delete(t):a.set(t,r),s(),e}}e.s(["REQUESTS_UNKNOWN",0,r,"hasRequest",0,function(e,t=i){if(!e)return!1;let r=(0,n.keyFor)(e);return t.mine.has(r)?t.mine.get(r):t.known?t.sent.has(r):"pending"===e.unlock_status||"sent"===e.outreach_status},"markRequested",0,f,"requestCount",0,function(e=i){let t=new Set(e.known?e.sent:[]);for(let[n,r]of e.mine)r?t.add(n):t.delete(n);return t.size},"requestsNow",0,u,"undoRequest",0,p,"watchRequests",0,c],86106),e.s(["default",0,function(){return(0,t.useSyncExternalStore)(c,u,()=>r)}],28867)},30215,e=>{"use strict";var t=e.i(71645),n=e.i(40996);e.s(["default",0,function(){return(0,t.useSyncExternalStore)(n.watchScanner,n.scannerNow,()=>n.SCANNER_UNKNOWN)}])},39160,84278,e=>{"use strict";var t=e.i(7214),n=e.i(7837);function r(e){let t=String(e||"").trim();if(!t)return null;let n=t.match(/^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})(?::(\d{2}))?(?:\s*UTC)?$/);if(n)return Date.UTC(+n[1],n[2]-1,+n[3],+n[4],+n[5],+(n[6]||0));if(n=t.match(/^(\d{1,2})\/(\d{1,2})\/(\d{2,4}),?\s+(\d{1,2}):(\d{2})\s*(AM|PM)?$/i)){let e=100>+n[3]?2e3+ +n[3]:+n[3],t=n[4]%12;return"PM"===(n[6]||"").toUpperCase()&&(t+=12),Date.UTC(e,n[1]-1,+n[2],t,+n[5])}if(n=t.match(/^([A-Za-z]{3,9})\s+(\d{4})$/)){let e=Date.parse(`1 ${n[1]} ${n[2]} UTC`);return Number.isNaN(e)?null:e}let r=Date.parse(/\d{4}$/.test(t)?`${t} UTC`:t);return Number.isNaN(r)?null:r}let o=e=>(0,t.keyFor)({profile_url:e});function i(e=[]){let t=new Map;for(let n of e){let e=o(n["sender profile url"]),r=n["conversation id"];r&&e&&(t.has(e)||t.set(e,new Set),t.get(e).add(r))}let n=null;for(let[e,r]of t)(!n||r.size>t.get(n).size)&&(n=e);return n}let l=e=>{let t=f(e);return t?`name:${t}`:null},a=/sponsored/i,s=/in\s?mail/i,c=/linkedin\.com\/(company|showcase|school)\//i,u=e=>/^(yes|true|1)$/i.test(String(e||"").trim());function d(e,t){if(!e||!e.total)return"never";let n=t-e.last;return n<=2592e6||e.recent>=3?"warm":n<=31536e6?"cool":"dormant"}let f=e=>String(e||"").split(",")[0].normalize("NFD").replace(/[̀-ͯ]/g,"").toLowerCase().replace(/[^a-z0-9 ]+/g," ").replace(/\s+/g," ").trim();e.s(["buildConversations",0,function(e=[],{keepText:t=!1}={}){let n=i(e),d=new Map;for(let t of e)n&&o(t["sender profile url"])===n&&t.from&&d.set(t.from,(d.get(t.from)||0)+1);let p=f([...d].sort((e,t)=>t[1]-e[1])[0]?.[0]||""),g=e=>!!e&&(e===n||!!p&&e===`name:${p}`),m={},h=new Map,b=0,y=0;for(let n of e){let e=n["conversation id"],i=r(n.date);if(!e||null==i){y+=1;continue}if(u(n["is message draft"])){b+=1;continue}let d=o(n["sender profile url"])||l(n.from);d&&n.from&&!m[d]&&(m[d]=String(n.from).trim());let f=String(n["recipient profile urls"]||"").split(/[,;\s]+/).filter(Boolean).map(o),p=String(n.to||"").split(",").map(e=>e.trim()).filter(Boolean),x=f.length?f:p.map(l).filter(Boolean);f.length&&f.length===p.length&&f.forEach((e,t)=>{m[e]||(m[e]=p[t])}),f.length||p.forEach(e=>{let t=l(e);t&&!m[t]&&(m[t]=e)}),h.has(e)||h.set(e,{id:e,title:"",everyone:new Set,messages:[],kind:null,folder:"",newest:-1/0});let w=h.get(e);d&&w.everyone.add(d),x.forEach(e=>w.everyone.add(e)),w.kind=w.kind||function(e){let t=String(e.folder||""),n=String(e["conversation title"]||"");return a.test(t)||a.test(n)||c.test(String(e["sender profile url"]||""))?"sponsored":s.test(t)||s.test(n)?"inmail":null}(n),i>=w.newest&&(w.newest=i,w.folder=String(n.folder||"").trim().toLowerCase().slice(0,30));let S={t:i,fromMe:g(d)};t&&(S.text=[n.subject,n.content].map(e=>String(e||"").trim()).filter(Boolean).join("\n\n"),!w.title&&n["conversation title"]&&(w.title=String(n["conversation title"]).trim())),w.messages.push(S)}let x=[];for(let e of h.values()){let n=[...e.everyone].filter(e=>e&&!g(e)).sort();e.messages.sort((e,t)=>e.t-t.t);let r={id:e.id,people:n,group:n.length>1,kind:e.kind,folder:e.folder,messages:e.messages};t&&(r.title=e.title),x.push(r)}for(let e of Object.keys(m))g(e)&&delete m[e];return{self:n,conversations:x,names:m,drafts:b,skipped:y}},"buildInvitations",0,function(e=[],{keepText:t=!1}={}){let n=[];for(let i of e){let e=String(i.direction||"").toUpperCase();if("INCOMING"!==e&&"OUTGOING"!==e)continue;let a="INCOMING"===e?"in":"out",s=String(("in"===a?i.from:i.to)||"").trim(),c=o("in"===a?i.inviterprofileurl:i.inviteeprofileurl),u=(c&&/\/in\//.test(c)?c:null)||l(s);if(!u)continue;let d=r(i["sent at"]),f={id:`${a}|${u}|${d??""}`,dir:a,key:u,name:s.slice(0,120),t:d};t&&String(i.message||"").trim()&&(f.note=String(i.message).trim().slice(0,2e3)),n.push(f)}return n.sort((e,t)=>(t.t||0)-(e.t||0))},"careerChapters",0,function(e=[],t=[],n=Date.now()){let i=e.map(e=>({company:e["company name"]||"",title:e.title||"",from:r(e["started on"]),to:r(e["finished on"])})).filter(e=>e.company&&null!=e.from).sort((e,t)=>e.from-t.from),l=t.map(e=>({url:o(e.url||e.profile_url),t:r(e["connected on"]||e.connected_on)})).filter(e=>null!=e.t);return i.map(e=>{let t=e.to??n,r=l.filter(n=>n.t>=e.from&&n.t<t);return{...e,to:e.to,made:r.length,people:r.map(e=>e.url)}})},"conversationSummary",0,function(e){let t=e.messages[e.messages.length-1];return{id:e.id,people:e.people,group:e.group,kind:e.kind||null,folder:e.folder||"",last:t?t.t:null,lastFromThem:t?!t.fromMe:null,count:e.messages.length,mine:e.messages.filter(e=>e.fromMe).length}},"invitationSplit",0,function(e=[]){let t=0,n=0;for(let r of e){let e=String(r.direction||"").toUpperCase();"INCOMING"===e?t+=1:"OUTGOING"===e&&(n+=1)}return{incoming:t,outgoing:n,total:t+n}},"messageStats",0,function(e=[]){let t=i(e),n=new Map;for(let t of e){let e=o(t["sender profile url"]),i=t["conversation id"];if(!i||!e)continue;let l=String(t["recipient profile urls"]||"").split(/[,;\s]+/).filter(Boolean).map(o),a=r(t.date);if(null==a)continue;n.has(i)||n.set(i,{people:new Set,msgs:[]});let s=n.get(i);s.people.add(e),l.forEach(e=>s.people.add(e)),s.msgs.push({from:e,t:a})}let l=0,a=new Map;for(let e of n.values()){let n=[...e.people].filter(e=>e!==t);if(1!==n.length||!e.people.has(t))continue;let r=n[0],o=a.get(r)||{last:0,lastFromThem:!1,total:0,times:[]};for(let n of e.msgs)o.total+=1,o.times.push(n.t),n.t>=o.last&&(o.last=n.t,o.lastFromThem=n.from!==t),n.t>l&&(l=n.t);a.set(r,o)}for(let e of a.values())e.recent=e.times.filter(e=>e>l-7776e6).length,delete e.times;return{self:t,asOf:l||null,people:a}},"nameKey",0,f,"postingEffect",0,function(e=[],t=[]){let n=e.map(e=>({t:r(e.date),link:e.sharelink||e["share link"]||""})).filter(e=>null!=e.t).sort((e,t)=>e.t-t.t),o=t.map(e=>r(e["connected on"]||e.connected_on)).filter(e=>null!=e).sort((e,t)=>e-t);if(!o.length)return{posts:[],usualWeek:0,afterPosts:0};let i=Math.max(6048e5,o[o.length-1]-o[0]),l=o.length/i*6048e5,a=n.map(e=>({...e,week:o.filter(t=>t>=e.t&&t<e.t+6048e5).length})),s=a.length?a.reduce((e,t)=>e+t.week,0)/a.length:0;return{posts:a,usualWeek:l,afterPosts:s}},"readTable",0,function(e,t){let r=(0,n.parseCsv)(String(e||"")),o=r.findIndex(e=>e.some(e=>e.trim().toLowerCase()===t.toLowerCase()));if(o<0)return[];let i=r[o].map(e=>e.trim().toLowerCase());return r.slice(o+1).filter(e=>e.some(e=>e.trim())).map(e=>Object.fromEntries(i.map((t,n)=>[t,(e[n]??"").trim()])))},"warmthOf",0,d],84278);let p="six-degrees-galaxy-lab",g="six-degrees-galaxy-layouts",m=Object.freeze({on:!0,gravity:0,rings:1,push:15,pull:1,distance:1,dotSize:1,lines:1,sizeBy:"power",orbit:0,names:"key",branch:!1,colourBy:"tier",speed:15,loop:!1,labels:!0,v:2}),h=["gravity","rings","push","pull","distance","orbit","dotSize","lines","sizeBy","names","branch","colourBy","speed","loop"],b=["gravity","rings","push","pull","distance"],y=Object.freeze({gravity:.8,rings:0,push:6,pull:6,distance:.15,sizeBy:"links"}),x=Object.freeze({rings:{lines:1},orbit:{lines:1},clusters:{lines:3}}),w=Object.freeze({gravity:0,rings:3,push:6,pull:4,distance:.22,sizeBy:"power"}),S=new Set,k=null;function v(){return k||(k=function(){try{let e=JSON.parse(localStorage.getItem(p)||"null");if(!e||"object"!=typeof e)return m;return{...m,...e,...e.v===m.v?{}:{branch:!1},v:m.v,on:!0}}catch{return m}}()),k}function j(e){k=null===e?{...m,labels:v().labels}:{...v(),...e,on:!0};try{localStorage.setItem(p,JSON.stringify(k))}catch{}S.forEach(e=>e())}let C=null,F=new Set;function N(){if(!C){try{C=JSON.parse(localStorage.getItem(g)||"[]")}catch{C=[]}Array.isArray(C)||(C=[])}return C}function M(e){C=e;try{localStorage.setItem(g,JSON.stringify(e))}catch{}F.forEach(e=>e())}let O=new Set,E={at:null,playing:!1,min:null,max:null,of:0,find:"",fly:0,found:0,fit:0,social:void 0},I=null;function D(e){let t={...E,...e};Object.keys(e).every(e=>Object.is(t[e],E[e]))||(E=t,O.forEach(e=>e()))}function B(){cancelAnimationFrame(I),I=null,D({playing:!1})}let T=null;function z(e){let n=new Map;for(let[r,o]of Object.entries(e?.people||{}))n.set((0,t.keyFor)({profile_url:r}),{...o});for(let[r,o]of Object.entries(e?.live||{})){let e=(0,t.keyFor)({profile_url:r}),i=n.get(e)||{total:0,recent:0};o.last&&(!i.last||o.last>i.last)&&(i.last=o.last,"boolean"==typeof o.lastFromThem&&(i.lastFromThem=o.lastFromThem)),i.unread=o.unread,i.total=Math.max(i.total||0,1),n.set(e,i)}let r=Math.max(e?.asOf||0,e?.liveAt&&Date.parse(e.liveAt)||0);for(let e of n.values())(e.last||0)>r&&(r=e.last);return{merged:n,asOf:r,chapters:e?.chapters||[],posts:e?.posts?.posts||[]}}let L={1:"#4FC3F7",2:"#BA68C8",3:"#FFB74D"},R=["#FF6B6B","#4ECDC4","#FFD93D","#A29BFE","#00B894","#FD79A8","#74B9FF","#E17055"],A={warm:"#FF7043",cool:"#4FC3F7",dormant:"#8D6E63",never:"#546E7A"},$="#4a5568",U=["#2b1b6b","#6a1b9a","#c2185b","#f4511e","#ffb300","#fff3b0"];function W(e){let t=Math.max(0,Math.min(1,Number(e)||0))*(U.length-1),n=Math.min(U.length-2,Math.floor(t)),r=t-n,o=U[n],i=U[n+1];return`#${[1,3,5].map(e=>Math.round(parseInt(o.slice(e,e+2),16)*(1-r)+parseInt(i.slice(e,e+2),16)*r).toString(16).padStart(2,"0")).join("")}`}function _(e=[],t=()=>0){let n=[...new Set(e.map(t))].sort((e,t)=>e-t),r=new Map(n.map((e,t)=>[e,n.length>1?t/(n.length-1):1]));return e=>r.get(t(e))??0}let q=e=>String(e||"").trim().toLowerCase();e.s(["CLUSTERS",0,y,"FORCE_KEYS",0,b,"LAB_DEFAULTS",0,m,"LAYOUT_LOOKS",0,x,"ORBIT",0,w,"applyLayout",0,function(e){let t=N().find(t=>t.name===e);t&&j(Object.fromEntries(h.filter(e=>e in t.settings).map(e=>[e,t.settings[e]])))},"bornTimes",0,function(e,t){let n=new Map(e.map(e=>[e.id,e])),r=new Map,o=(e,i=0)=>{if(r.has(e.id))return r.get(e.id);let l=-1/0,a=t.get(e.id);if(1===e.degree||null==a||i>6){let t=e.connected_date?Date.parse(e.connected_date):NaN;l=Number.isNaN(t)?-1/0:t}return null!=a&&n.has(a)&&i<=6&&(l=Math.max(l,o(n.get(a),i+1))),r.set(e.id,l),l};return e.map(e=>o(e))},"chapterAt",0,function(e,t){let n=null;for(let r of e?.chapters||[])Number.isFinite(r.from)&&r.from<=t&&(!n||r.from>n.from)&&(n=r);return n&&(!Number.isFinite(n.to)||n.to>t)?n:null},"clockNow",0,()=>E,"colourScheme",0,function(e,n,r,o){if("degree"===e)return{of:e=>L[Math.min(e.degree||1,3)],legend:[["1st",L[1]],["2nd",L[2]],["3rd+",L[3]]]};if("company"===e){let e=new Map;for(let t of n){let n=q(t.company);if(!n)continue;let r=e.get(n)||{name:String(t.company).trim(),n:0};r.n+=1,e.set(n,r)}let t=[...e.entries()].filter(([,e])=>e.n>1).sort((e,t)=>t[1].n-e[1].n).slice(0,R.length),r=new Map(t.map(([e],t)=>[e,R[t]]));return{of:e=>r.get(q(e.company))||$,legend:[...t.map(([,e],t)=>[e.name,R[t]]),["Other",$]]}}if("heat"===e){let e=function(e=[]){return _(e,t.score)}(n);return{of:t=>W(e(t)),heat:e,legend:[["Cold",W(.1)],["Warm",W(.45)],["Hot",W(.75)],["Hottest",W(1)]]}}return"warmth"===e&&o?{of:e=>1===(e.degree||1)?A[d(o.merged.get((0,t.keyFor)(e)),o.asOf)]:"#2d3748",legend:[["Warm",A.warm],["Cool",A.cool],["Dormant",A.dormant],["Never messaged",A.never]]}:{of:e=>r[e.tier]||"#666",legend:Object.entries(r)}},"effectiveLab",0,e=>e.on?e:e.labels===m.labels?m:{...m,labels:e.labels},"findMatches",0,function(e,t){let n=String(t||"").trim().toLowerCase();if(n.length<2)return null;let r=new Set;for(let t of e)[t.name,t.company,t.role,t.headline].some(e=>e&&String(e).toLowerCase().includes(n))&&r.add(t.id);return r},"forgetLayout",0,function(e){M(N().filter(t=>t.name!==e))},"heatBy",0,_,"heatColour",0,W,"labNow",0,v,"layoutOf",0,function(e){let t=t=>Object.entries(t).every(([t,n])=>e[t]===n);return b.every(t=>e[t]===m[t])?"rings":t(y)?"clusters":t(w)?"orbit":null},"layoutsNow",0,N,"loadSocial",0,function(){return T||(T=fetch("/api/social").then(e=>e.ok?e.json():{}).then(e=>{D({social:e.social?z(e.social):null})}).catch(()=>D({social:null}))),T},"mergeSocial",0,z,"milestones",0,function(e){let t=[];for(let n of e?.chapters||[])Number.isFinite(n.from)&&t.push({t:n.from,kind:"job",label:n.company});for(let n of e?.posts||[])Number.isFinite(n.t)&&t.push({t:n.t,kind:"post",label:"You posted"});return t.sort((e,t)=>e.t-t.t)},"pause",0,B,"play",0,function e(){if(null==E.min||null==E.max||E.max<=E.min)return;let t=E.max-E.min,n=1e3*Math.max(1,Number(v().speed)||15),r=null==E.at||E.at>=E.max?E.min:E.at,o=performance.now()-(r-E.min)/t*n;D({at:r,playing:!0});let i=r=>{let l=Math.min(E.max,E.min+(r-o)/n*t);if(D({at:l}),l>=E.max){I=null,D({playing:!1}),v().loop&&setTimeout(()=>{I||E.at!==E.max||(D({at:E.min}),e())},1200);return}I=requestAnimationFrame(i)};cancelAnimationFrame(I),I=requestAnimationFrame(i)},"reachCounts",0,function(e,t){let n=new Map(e.map(e=>[e.id,0]));for(let r of e){let e=t.get(r.id);for(let r=0;null!=e&&r<7;r++)n.set(e,(n.get(e)||0)+1),e=t.get(e)}return n},"saveLayout",0,function(e){let t=String(e||"").trim().slice(0,40);if(!t)return;let n=v(),r=Object.fromEntries(h.map(e=>[e,n[e]]));M([...N().filter(e=>e.name!==t),{name:t,settings:r}])},"setClock",0,D,"setLab",0,j,"stopReplay",0,function(){B(),D({at:null})},"watchClock",0,function(e){return O.add(e),()=>O.delete(e)},"watchLab",0,function(e){S.add(e);let t=t=>{t.key===p&&(k=null,e())};return window.addEventListener("storage",t),()=>{S.delete(e),window.removeEventListener("storage",t)}},"watchLayouts",0,function(e){return F.add(e),()=>F.delete(e)}],39160)}]);
|
|
@@ -6,6 +6,67 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.6.0] - 2026-10-03
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **Themes** (Settings → Appearance). Seven looks: Standard, High contrast, Obsidian, Glass,
|
|
13
|
+
Analyst, Space and Synthwave, each with a small preview of the map in its colours. Then make it
|
|
14
|
+
yours: the background and a second colour, a backdrop behind everything (stars, a grid, a glow,
|
|
15
|
+
a sunset horizon), the accent, each tier's dot and your own, the map's lines (in the colour of
|
|
16
|
+
the person they lead to, or one colour), how see-through the side panels are, and the font.
|
|
17
|
+
Everything changes as you go, on every page, and stays in this browser. Copy a theme code to
|
|
18
|
+
share a look; pasting one only ever sets colours and choices.
|
|
19
|
+
- **Clusters, the way Obsidian draws a graph** (Network Circle → Filters → Layout → Clusters). Your
|
|
20
|
+
network as one round disc: each connection with a scanned circle is a big hub with their circle
|
|
21
|
+
around them, and everyone is sized by their lines. Someone in several circles is linked to each
|
|
22
|
+
of them, so they sit between those hubs and tie them together. The forces have Obsidian's names,
|
|
23
|
+
Center force, Repel force, Link force and Link distance, each saying what it does to your
|
|
24
|
+
network. Names fade as you zoom out, all but the hubs'. Picking a layout fits everyone on screen,
|
|
25
|
+
and so does ⤢ Fit.
|
|
26
|
+
- **Orbit** (Filters → Forces): sets the whole map turning round you, every circle with it, so
|
|
27
|
+
nothing loses its shape. Still under Reduce Motion.
|
|
28
|
+
- **Dots along the header's line while a scan runs**: one for each page the scanner reads,
|
|
29
|
+
collecting from the left, on every tab. A click opens the Scan page.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- **The same header on every tab.** The tabs sit in the exact middle of the window, and Settings,
|
|
33
|
+
notifications, ↻ Check for new and your level are on Paths, Outlink and Scan too. A notification
|
|
34
|
+
picked on another tab opens on the map.
|
|
35
|
+
- **Separation fits a laptop's window.** The header is slimmer: the search sits in the title's row,
|
|
36
|
+
the tier line stays without the sentence under it (it's the title's tooltip now), Only show sits
|
|
37
|
+
beside the tiers, the pill saying where the slider is sits over its middle, and the long
|
|
38
|
+
explanations moved into tooltips. The cards are packed a little closer, so the whole map, ten
|
|
39
|
+
people and every way in, shows at once in the app's normal window wherever the slider is.
|
|
40
|
+
- **The Scan page shows every step at once**, down a line: Get ready, Sign in, Who you know, Who
|
|
41
|
+
they know, each with its label, what it does and its one button, marked Done, Now or Next. The
|
|
42
|
+
line turns green as each is done, so it's plain what's finished and what's left; no more tapping
|
|
43
|
+
dots to see a step.
|
|
44
|
+
- **↻ Check for new**, in words, in the map's header (it was a bare ↻): the same name as the Scan
|
|
45
|
+
page's button, for checking your LinkedIn for new connections in one click.
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
- **Separation's tiers choose who's ranked, not who leads to them.** Hiding a tier in Filters also
|
|
49
|
+
hid your connections of that tier (the purple A-tier lines), so the people they lead to lost a
|
|
50
|
+
way in. In Separation every connection now stays a way in, whatever its tier.
|
|
51
|
+
- The level in the round button at the top right matches the Profile page's: it left out C and D
|
|
52
|
+
tiers and catalysts, so it read lower.
|
|
53
|
+
|
|
54
|
+
## [0.5.1-beta.1] - 2026-10-02 (beta: a pre-release, never installed automatically)
|
|
55
|
+
|
|
56
|
+
### Added
|
|
57
|
+
- **Six Degrees for Windows and Linux (beta).** The same app as on the Mac, with the scanner's
|
|
58
|
+
own Python inside, so nothing needs installing before a scan. **Windows:**
|
|
59
|
+
*Six-Degrees-Windows-Setup.exe* installs it for you with one click (no admin rights), with Start
|
|
60
|
+
Menu and Desktop shortcuts, and opens it; running a newer one over it updates it. It isn't
|
|
61
|
+
signed yet, so SmartScreen may ask the first time (*More info → Run anyway*). **Linux:** a
|
|
62
|
+
`.deb` (`sudo apt install ./Six-Degrees-Linux-x64.deb`) or a `.tar.gz`. Scanning needs Google
|
|
63
|
+
Chrome on both. Your network stays in your home folder's `.six-degrees`, as on the Mac;
|
|
64
|
+
uninstalling never touches it. Settings → Updates points to the new installer there.
|
|
65
|
+
|
|
66
|
+
### Fixed
|
|
67
|
+
- On Windows the scanner would have stopped after its first batch: its progress lines
|
|
68
|
+
("→") can't be written in Windows' default text encoding. Its Python now always writes UTF-8.
|
|
69
|
+
|
|
9
70
|
## [0.5.0] - 2026-10-02
|
|
10
71
|
|
|
11
72
|
### Added
|
|
@@ -41,22 +41,24 @@ know placed on rings by how much they can open up for you, the handful of people
|
|
|
41
41
|
whose own circles reach the furthest, and the shortest chain from you to a stranger
|
|
42
42
|
worth meeting.
|
|
43
43
|
|
|
44
|
-
It's a Mac app
|
|
44
|
+
It's a Mac app, and now a Windows and Linux app too (beta). It runs on your computer, against your own data, with no account and
|
|
45
45
|
no server.
|
|
46
46
|
|
|
47
47
|
## Download
|
|
48
48
|
|
|
49
49
|
<p align="center">
|
|
50
50
|
<a href="https://github.com/blakeb056/six-degrees/releases/latest/download/Six-Degrees-Mac-Apple-Silicon.dmg"><img src="docs/img/download-apple-silicon.png" width="346" alt="Download Six Degrees for a Mac with Apple Silicon (M1 or newer)"></a>
|
|
51
|
-
<a href="https://github.com/blakeb056/six-degrees/releases/latest/download/Six-Degrees-Mac-Intel.dmg"><img src="docs/img/download-intel.png" width="346" alt="Download Six Degrees for a Mac with an Intel chip"></a>
|
|
51
|
+
<a href="https://github.com/blakeb056/six-degrees/releases/latest/download/Six-Degrees-Mac-Intel.dmg"><img src="docs/img/download-intel.png" width="346" alt="Download Six Degrees for a Mac with an Intel chip"></a><br>
|
|
52
|
+
<a href="https://github.com/blakeb056/six-degrees/releases/download/v0.5.1-beta.1/Six-Degrees-Windows-Setup.exe"><img src="docs/img/download-windows.png" width="346" alt="Download Six Degrees for Windows 10 or 11 (beta): one-click Setup.exe"></a>
|
|
53
|
+
<a href="https://github.com/blakeb056/six-degrees/releases/download/v0.5.1-beta.1/Six-Degrees-Linux-x64.deb"><img src="docs/img/download-linux.png" width="346" alt="Download Six Degrees for Linux (beta): .deb for Ubuntu and Debian"></a>
|
|
52
54
|
</p>
|
|
53
55
|
|
|
54
56
|
| Platform | Download | Needs |
|
|
55
57
|
|---|---|---|
|
|
56
58
|
| **macOS**, Apple Silicon (M1 or newer) | [Six-Degrees-Mac-Apple-Silicon.dmg](https://github.com/blakeb056/six-degrees/releases/latest/download/Six-Degrees-Mac-Apple-Silicon.dmg) | macOS 13.5 (Ventura) or later |
|
|
57
59
|
| **macOS**, Intel | [Six-Degrees-Mac-Intel.dmg](https://github.com/blakeb056/six-degrees/releases/latest/download/Six-Degrees-Mac-Intel.dmg) | macOS 13.5 (Ventura) or later |
|
|
58
|
-
| **
|
|
59
|
-
| **
|
|
60
|
+
| **Windows** (beta) | [Six-Degrees-Windows-Setup.exe](https://github.com/blakeb056/six-degrees/releases/download/v0.5.1-beta.1/Six-Degrees-Windows-Setup.exe) | Windows 10 or 11, 64-bit |
|
|
61
|
+
| **Linux** (beta) | [Six-Degrees-Linux-x64.deb](https://github.com/blakeb056/six-degrees/releases/download/v0.5.1-beta.1/Six-Degrees-Linux-x64.deb) or [.tar.gz](https://github.com/blakeb056/six-degrees/releases/download/v0.5.1-beta.1/Six-Degrees-Linux-x64.tar.gz), or from the Terminal: `npx six-degrees` ([how](https://sixdegreesapp.com/download/#linux)) | Ubuntu or Debian, 64-bit (npx: Node 22.13 or later) |
|
|
60
62
|
|
|
61
63
|
<sub>Every release is on [GitHub Releases](https://github.com/blakeb056/six-degrees/releases) with a
|
|
62
64
|
`SHA256SUMS` file ([latest](https://github.com/blakeb056/six-degrees/releases/latest/download/SHA256SUMS)); the Terminal install below checks the download against it.
|
|
@@ -114,7 +116,20 @@ touch it. Each new version also copies your data into its `backups` folder befor
|
|
|
114
116
|
first opens it. To take it to another computer, see
|
|
115
117
|
[Moving to a new computer](#moving-to-a-new-computer).
|
|
116
118
|
|
|
117
|
-
**
|
|
119
|
+
**Windows (beta):** download [Six-Degrees-Windows-Setup.exe](https://github.com/blakeb056/six-degrees/releases/download/v0.5.1-beta.1/Six-Degrees-Windows-Setup.exe)
|
|
120
|
+
and open it. It installs for you alone (no administrator password), adds Start Menu and
|
|
121
|
+
Desktop shortcuts, and opens Six Degrees. It isn't signed with a paid certificate yet, so
|
|
122
|
+
SmartScreen may say it protected your PC: **More info → Run anyway**. A newer Setup.exe
|
|
123
|
+
updates it in place; your network stays in `%USERPROFILE%\.six-degrees`, which
|
|
124
|
+
uninstalling never touches. Scanning needs Google Chrome.
|
|
125
|
+
|
|
126
|
+
**Linux app (beta):** `sudo apt install ./Six-Degrees-Linux-x64.deb` with the
|
|
127
|
+
[.deb](https://github.com/blakeb056/six-degrees/releases/download/v0.5.1-beta.1/Six-Degrees-Linux-x64.deb), then open it from your apps. The
|
|
128
|
+
[.tar.gz](https://github.com/blakeb056/six-degrees/releases/download/v0.5.1-beta.1/Six-Degrees-Linux-x64.tar.gz) runs from its folder; on Ubuntu 24.04 and
|
|
129
|
+
later run `sudo chown root:root chrome-sandbox && sudo chmod 4755 chrome-sandbox` in it
|
|
130
|
+
once (or start it with `--no-sandbox`). Scanning needs Google Chrome (not Chromium).
|
|
131
|
+
|
|
132
|
+
**Linux from the Terminal:** install **Node 22.13 or later** (from nodejs.org or nvm; Ubuntu's own
|
|
118
133
|
`nodejs` package is too old), then run:
|
|
119
134
|
|
|
120
135
|
```bash
|
|
@@ -131,8 +146,7 @@ up the scanner** button downloads a private copy into your data folder (24–33
|
|
|
131
146
|
GitHub, checked against a checksum built into Six Degrees). It's tested on Ubuntu, and
|
|
132
147
|
works on a Mac too if you'd rather not install the app.
|
|
133
148
|
|
|
134
|
-
|
|
135
|
-
Windows yet; npm says so if you try.
|
|
149
|
+
`npx six-degrees` doesn't run on Windows; use the Windows app above.
|
|
136
150
|
|
|
137
151
|
<details>
|
|
138
152
|
<summary>Run it from source (contributors)</summary>
|
|
@@ -248,7 +262,7 @@ two steps away, and 503 of them, 84%, are reached through only one connection.
|
|
|
248
262
|
| Any network, any data, research-grade statistics | — (LinkedIn networks only) | — | any data | ✓ what they're built for |
|
|
249
263
|
| Runs on your computer, with no account | ✓ | — (online, with your account) | varies | ✓ |
|
|
250
264
|
| Price | free, MIT | free, with paid plans | free to paid | free, open source |
|
|
251
|
-
| Windows |
|
|
265
|
+
| Windows | ✓ (beta) | ✓ | ✓ | ✓ |
|
|
252
266
|
|
|
253
267
|
Something out of date, or unfair to another tool? [Open an issue](https://github.com/blakeb056/six-degrees/issues).
|
|
254
268
|
|
|
@@ -46,8 +46,15 @@ const state = registerScanState({
|
|
|
46
46
|
stopping: false,
|
|
47
47
|
stderrTail: [],
|
|
48
48
|
failure: null, // the last lines of stderr from a run that failed — its reason
|
|
49
|
+
pages: 0, // pages the running scan has read: the dots along the header's line (app/components/ScanTrail.js)
|
|
49
50
|
});
|
|
50
51
|
|
|
52
|
+
// A page read, as scripts/scrape.py prints it: " Page 3... " as a circle or a
|
|
53
|
+
// company scan reads each page, and " 350 / 817 collected" for each fifty
|
|
54
|
+
// people of your own connections list. Counted as the lines arrive, since the
|
|
55
|
+
// log only keeps its last MAX_LOG lines.
|
|
56
|
+
const PAGE_READ = /^\s*Page \d+\.\.\.|\d+\s*\/\s*\d+\s+collected/;
|
|
57
|
+
|
|
51
58
|
/** Stop the running job without orphaning the browser it opened.
|
|
52
59
|
*
|
|
53
60
|
* The child is spawned into its own process group, so the negative pid
|
|
@@ -69,6 +76,19 @@ function stopChild() {
|
|
|
69
76
|
state.stopping = true;
|
|
70
77
|
push('Stopping…');
|
|
71
78
|
const pid = child.pid;
|
|
79
|
+
// Windows has no process groups or SIGTERM. taskkill /T walks the tree the
|
|
80
|
+
// scanner started; without /F it asks Chrome's windows to close, so the
|
|
81
|
+
// browser shuts down cleanly and keeps the LinkedIn session. Forced after 5 s.
|
|
82
|
+
if (process.platform === 'win32') {
|
|
83
|
+
spawn('taskkill', ['/pid', String(pid), '/T'], { stdio: 'ignore', windowsHide: true }).on('error', () => {});
|
|
84
|
+
setTimeout(() => {
|
|
85
|
+
if (state.child && state.child.pid === pid) {
|
|
86
|
+
push('Still running — forcing it.');
|
|
87
|
+
spawn('taskkill', ['/pid', String(pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true }).on('error', () => {});
|
|
88
|
+
}
|
|
89
|
+
}, 5000);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
72
92
|
try { process.kill(-pid, 'SIGTERM'); }
|
|
73
93
|
catch { try { child.kill('SIGTERM'); } catch {} }
|
|
74
94
|
|
|
@@ -86,6 +106,7 @@ function push(line) {
|
|
|
86
106
|
const t = part.replace(/\s+$/, '');
|
|
87
107
|
if (!t) continue;
|
|
88
108
|
state.log.push(t);
|
|
109
|
+
if (PAGE_READ.test(t)) state.pages += 1;
|
|
89
110
|
}
|
|
90
111
|
if (state.log.length > MAX_LOG) state.log.splice(0, state.log.length - MAX_LOG);
|
|
91
112
|
}
|
|
@@ -234,13 +255,24 @@ async function look() {
|
|
|
234
255
|
const chrome =
|
|
235
256
|
process.platform === 'darwin' ? existsSync('/Applications/Google Chrome.app')
|
|
236
257
|
: process.platform === 'linux' ? existsSync('/opt/google/chrome/chrome')
|
|
237
|
-
:
|
|
258
|
+
: process.platform === 'win32' ? windowsChrome()
|
|
259
|
+
: true; // elsewhere Playwright resolves the channel itself
|
|
238
260
|
|
|
239
|
-
|
|
261
|
+
// Windows' Chrome keeps its cookies one folder down, in Default\Network.
|
|
262
|
+
const profile = path.join(dataDir(), 'chrome-profile', 'Default');
|
|
263
|
+
const signedIn = existsSync(path.join(profile, 'Cookies')) || existsSync(path.join(profile, 'Network', 'Cookies'));
|
|
240
264
|
|
|
241
265
|
return { root, python, chrome, signedIn };
|
|
242
266
|
}
|
|
243
267
|
|
|
268
|
+
/** Google Chrome on Windows, where Playwright's "chrome" channel looks: for this user, then for everyone. */
|
|
269
|
+
function windowsChrome() {
|
|
270
|
+
const env = process.env;
|
|
271
|
+
return [env.LOCALAPPDATA, env.PROGRAMFILES, env['PROGRAMFILES(X86)']]
|
|
272
|
+
.filter(Boolean)
|
|
273
|
+
.some((root) => existsSync(path.join(root, 'Google', 'Chrome', 'Application', 'chrome.exe')));
|
|
274
|
+
}
|
|
275
|
+
|
|
244
276
|
function machineChecks() {
|
|
245
277
|
if (Date.now() - cached.at < 4000 && cached.value) return Promise.resolve(cached.value);
|
|
246
278
|
if (looking) return looking;
|
|
@@ -342,6 +374,7 @@ function job() {
|
|
|
342
374
|
exitCode: state.exitCode,
|
|
343
375
|
failure: state.running ? null : state.failure,
|
|
344
376
|
progress: state.running ? scanProgress(state.log, state.action) : null,
|
|
377
|
+
pages: state.running ? state.pages : 0,
|
|
345
378
|
log: state.log.slice(-120),
|
|
346
379
|
recent: state.recent,
|
|
347
380
|
budget: state.running ? budgetNow() : null,
|
|
@@ -680,6 +713,7 @@ export async function POST(request) {
|
|
|
680
713
|
state.exitCode = null;
|
|
681
714
|
state.startedAt = Date.now();
|
|
682
715
|
state.log = [spec.label + (target?.name ? ` ${target.name}…` : '…')];
|
|
716
|
+
state.pages = 0;
|
|
683
717
|
state.stderrTail = [];
|
|
684
718
|
state.failure = null;
|
|
685
719
|
forgetChecks();
|
|
@@ -764,7 +798,10 @@ export async function POST(request) {
|
|
|
764
798
|
try {
|
|
765
799
|
// Its own process group, so cancelling reaches the browser as well. A
|
|
766
800
|
// step's env() makes its own environment from this one.
|
|
767
|
-
|
|
801
|
+
// On Windows `detached` means a console of its own, a visible window for
|
|
802
|
+
// Playwright's driver: there taskkill /T reaches the tree instead (stopChild).
|
|
803
|
+
const win = process.platform === 'win32';
|
|
804
|
+
child = spawn(step.cmd, step.args, { cwd: root, env: step.env ? step.env(childEnv) : childEnv, detached: !win, windowsHide: true });
|
|
768
805
|
} catch (err) {
|
|
769
806
|
push(`Could not start: ${err.message}`);
|
|
770
807
|
return finish(-1);
|
|
@@ -3,7 +3,7 @@ import { accessSync, constants, existsSync } from 'node:fs';
|
|
|
3
3
|
import { projectRoot, isGitCheckout, dataDir } from '../../../lib/paths';
|
|
4
4
|
import { homedir, tmpdir } from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
-
import { repoSlug, compareVersions, installKind, updateCommand } from '../../../lib/release';
|
|
6
|
+
import { repoSlug, compareVersions, installKind, updateCommand, INSTALLER_KINDS } from '../../../lib/release';
|
|
7
7
|
import {
|
|
8
8
|
bundleRefusal, dataRefusal, installerTarget, lastUpdateReport, planFromRelease, releaseSource, runningBundle,
|
|
9
9
|
terminalFallback,
|
|
@@ -239,12 +239,18 @@ async function installedPost(action, root) {
|
|
|
239
239
|
// Install installs the version this check offered and no other
|
|
240
240
|
// (lib/updater-job.js rememberCheck); none, when it offered none.
|
|
241
241
|
if (mac) rememberCheck(newer ? latest.version : null);
|
|
242
|
+
// Windows and Linux: the newer release's page, where its installer is. Only
|
|
243
|
+
// ever this project's own releases on GitHub (the page opens it in the browser).
|
|
244
|
+
const releasePage = `https://github.com/${info.slug}/releases/`;
|
|
245
|
+
const download = newer && INSTALLER_KINDS.includes(info.kind) && latest?.url && String(latest.url).startsWith(releasePage)
|
|
246
|
+
? { url: latest.url } : null;
|
|
242
247
|
return Response.json({
|
|
243
248
|
...info,
|
|
244
249
|
latest: latest && { version: latest.version, url: latest.url },
|
|
245
250
|
newer,
|
|
246
251
|
install,
|
|
247
252
|
fallback,
|
|
253
|
+
download,
|
|
248
254
|
});
|
|
249
255
|
} catch (e) {
|
|
250
256
|
return Response.json({ error: `Could not reach GitHub (${e.message}). Try again in a moment.` }, { status: 502 });
|
|
@@ -252,11 +258,11 @@ async function installedPost(action, root) {
|
|
|
252
258
|
}
|
|
253
259
|
|
|
254
260
|
if (action === 'install-release') {
|
|
255
|
-
if (info.kind !== 'mac-app') {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
);
|
|
261
|
+
if (info.kind !== 'mac-app' || process.platform !== 'darwin') {
|
|
262
|
+
const error = INSTALLER_KINDS.includes(info.kind)
|
|
263
|
+
? 'On Windows and Linux an update is the new installer: download it from the release page and run it.'
|
|
264
|
+
: 'Only the Mac app can install an update itself. Use the line below instead.';
|
|
265
|
+
return Response.json({ error, command: info.command }, { status: 400 });
|
|
260
266
|
}
|
|
261
267
|
if (!info.slug) {
|
|
262
268
|
return Response.json({ error: 'This copy does not say where it was published.' }, { status: 400 });
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
// The header on every page that has the app's tabs: the name on the left, the
|
|
4
|
+
// tabs in the exact middle of the window, and the same buttons on the right
|
|
5
|
+
// everywhere — Settings, notifications, ↻ Check for new and your profile.
|
|
6
|
+
// Blake, 2026-10-03: on Paths, Outlink and Scan "the profile settings and
|
|
7
|
+
// refresh buttons aren't there, every section should have the same buttons",
|
|
8
|
+
// and "align the button bar to the exact middle".
|
|
9
|
+
//
|
|
10
|
+
// The row is a grid of 1fr | auto | 1fr: the two sides share what's left
|
|
11
|
+
// equally, so the tabs sit in the middle whatever is beside them. A window too
|
|
12
|
+
// narrow for that lets the sides shrink to fit before anything overlaps.
|
|
13
|
+
|
|
14
|
+
import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
|
|
15
|
+
import Link from 'next/link';
|
|
16
|
+
import { useRouter } from 'next/navigation';
|
|
17
|
+
import AppTabs from './AppTabs';
|
|
18
|
+
import AutoScanButton from './AutoScanButton';
|
|
19
|
+
import ScanTrail from './ScanTrail';
|
|
20
|
+
import useScanner from './useScanner';
|
|
21
|
+
import { useUser } from './UserProvider';
|
|
22
|
+
import { scraperStatus, beginScrape, notReadyMessage, busyReason } from '../../lib/scraper-client';
|
|
23
|
+
import { loadNetwork } from '../../lib/network';
|
|
24
|
+
import { networkLevel, rememberLevel, levelNow, watchLevel } from '../../lib/level';
|
|
25
|
+
import { IS_DEMO } from '../../lib/demo';
|
|
26
|
+
|
|
27
|
+
function useIsMobile() {
|
|
28
|
+
const [m, setM] = useState(false);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const check = () => setM(window.innerWidth < 768);
|
|
31
|
+
check();
|
|
32
|
+
window.addEventListener('resize', check);
|
|
33
|
+
return () => window.removeEventListener('resize', check);
|
|
34
|
+
}, []);
|
|
35
|
+
return m;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const TITLE = { fontWeight: 700, margin: 0, whiteSpace: 'nowrap', background: 'linear-gradient(135deg, #FFD700, #9B59B6, #3498DB)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' };
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {{ active: string, isMobile?: boolean, csvMode?: boolean, onMode?: (key: string) => void,
|
|
42
|
+
* chips?: any, level?: number | null, onOpenNote?: (note: object) => void, openNoteId?: string | null,
|
|
43
|
+
* brand?: string, children?: any }} props
|
|
44
|
+
* `chips` go in the tab row after the map's tabs (the sample or CSV chip);
|
|
45
|
+
* `children` under the row. `level` is your network's level when the page has
|
|
46
|
+
* the network loaded; elsewhere the button finds it. `onOpenNote` opens a
|
|
47
|
+
* notification on this page (the map's side panel); without it, a notification
|
|
48
|
+
* opens there (/?note=…). `brand` is the element the name is drawn in: an
|
|
49
|
+
* <h1> unless the page has its own (the Scan page).
|
|
50
|
+
*/
|
|
51
|
+
export default function AppHeader({ active, isMobile: mobileProp, csvMode = false, onMode, chips, level, onOpenNote, openNoteId = null, brand: Brand = 'h1', children }) {
|
|
52
|
+
const mobileSeen = useIsMobile();
|
|
53
|
+
const isMobile = mobileProp ?? mobileSeen;
|
|
54
|
+
const own = !IS_DEMO && !csvMode; // scanning and notifications are for your own network
|
|
55
|
+
return (
|
|
56
|
+
<header style={{
|
|
57
|
+
position: 'relative', padding: isMobile ? '10px 12px' : '20px 30px', borderBottom: '1px solid rgba(255,255,255,0.1)', flexShrink: 0,
|
|
58
|
+
// Clear, or frosted glass on a glass theme (lib/themes.js).
|
|
59
|
+
background: 'var(--sd-header)', backdropFilter: 'var(--sd-header-blur)', WebkitBackdropFilter: 'var(--sd-header-blur)',
|
|
60
|
+
}}>
|
|
61
|
+
<div style={isMobile
|
|
62
|
+
? { display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 6, marginBottom: 4 }
|
|
63
|
+
: { display: 'grid', gridTemplateColumns: '1fr auto 1fr', alignItems: 'center', gap: 16, marginBottom: 8 }}>
|
|
64
|
+
<Link href="/" style={{ textDecoration: 'none', justifySelf: 'start' }}>
|
|
65
|
+
<Brand style={{ ...TITLE, fontSize: isMobile ? 16 : 28 }}>Six Degrees</Brand>
|
|
66
|
+
</Link>
|
|
67
|
+
<AppTabs active={active} isMobile={isMobile} csvMode={csvMode} onMode={onMode}
|
|
68
|
+
after={own ? <AutoScanButton isMobile={isMobile} /> : null}>
|
|
69
|
+
{chips}
|
|
70
|
+
</AppTabs>
|
|
71
|
+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', justifySelf: 'end', gap: isMobile ? 4 : 8 }}>
|
|
72
|
+
{/* Settings: also in CSV or sample mode and on phones, so updates stay reachable */}
|
|
73
|
+
{!IS_DEMO && <Link href="/settings" title="Settings" aria-label="Settings" style={{
|
|
74
|
+
width: isMobile ? 28 : 32, height: isMobile ? 28 : 32, borderRadius: '50%', flexShrink: 0,
|
|
75
|
+
background: 'rgba(255,255,255,0.06)', color: '#888', fontSize: isMobile ? 14 : 16, textDecoration: 'none',
|
|
76
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
77
|
+
}}>{'⚙︎'}</Link>}
|
|
78
|
+
{own && <NotificationBell isMobile={isMobile} onOpen={onOpenNote} openId={openNoteId} />}
|
|
79
|
+
{own && <RefreshButton isMobile={isMobile} />}
|
|
80
|
+
<ProfileButton isMobile={isMobile} csvMode={csvMode} level={level} />
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
{children}
|
|
84
|
+
{/* While a scan runs: a dot on the line below for each page it reads */}
|
|
85
|
+
{own && <ScanTrail inset={isMobile ? 12 : 30} />}
|
|
86
|
+
</header>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Notifications: the bell, its count, and the list under it. */
|
|
91
|
+
function NotificationBell({ isMobile, onOpen, openId }) {
|
|
92
|
+
const { userId } = useUser();
|
|
93
|
+
const router = useRouter();
|
|
94
|
+
const [notes, setNotes] = useState([]);
|
|
95
|
+
const [open, setOpen] = useState(false);
|
|
96
|
+
// A notification picked on another page arrives as /?note=<id>: opened once, when the list is in.
|
|
97
|
+
const opened = useRef(false);
|
|
98
|
+
const onOpenRef = useRef(onOpen);
|
|
99
|
+
useEffect(() => { onOpenRef.current = onOpen; });
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (!userId) return;
|
|
102
|
+
fetch(`/api/notifications?userId=${userId}`).then((r) => r.json()).then((d) => {
|
|
103
|
+
const list = d.notifications || [];
|
|
104
|
+
setNotes(list);
|
|
105
|
+
const asked = openId != null && !opened.current && list.find((n) => String(n.id) === String(openId));
|
|
106
|
+
if (asked && onOpenRef.current) { opened.current = true; onOpenRef.current(asked); }
|
|
107
|
+
}).catch(() => {});
|
|
108
|
+
}, [userId, openId]);
|
|
109
|
+
const unseen = notes.filter((n) => !n.seen).length;
|
|
110
|
+
const show = (n) => {
|
|
111
|
+
// Open it, and count it as read.
|
|
112
|
+
if (!n.seen) {
|
|
113
|
+
fetch('/api/notifications', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'mark-seen', id: n.id }) }).catch(() => {});
|
|
114
|
+
setNotes((prev) => prev.map((x) => (x.id === n.id ? { ...x, seen: true } : x)));
|
|
115
|
+
}
|
|
116
|
+
setOpen(false);
|
|
117
|
+
if (onOpen) onOpen(n);
|
|
118
|
+
else router.push(`/?note=${encodeURIComponent(n.id)}`);
|
|
119
|
+
};
|
|
120
|
+
return (
|
|
121
|
+
<div style={{ position: 'relative' }}>
|
|
122
|
+
<button onClick={() => setOpen(!open)} title="Notifications" aria-label={unseen ? `Notifications, ${unseen} new` : 'Notifications'} style={{
|
|
123
|
+
width: isMobile ? 28 : 32, height: isMobile ? 28 : 32, borderRadius: '50%', border: 'none', cursor: 'pointer',
|
|
124
|
+
background: 'rgba(255,255,255,0.06)', color: '#888', fontSize: isMobile ? 12 : 14,
|
|
125
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center', position: 'relative',
|
|
126
|
+
}}>
|
|
127
|
+
🔔
|
|
128
|
+
{unseen > 0 && (
|
|
129
|
+
<span style={{
|
|
130
|
+
position: 'absolute', top: -2, right: -2, width: 16, height: 16, borderRadius: '50%',
|
|
131
|
+
background: '#ff5050', color: '#fff', fontSize: 9, fontWeight: 700,
|
|
132
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
133
|
+
}}>{unseen}</span>
|
|
134
|
+
)}
|
|
135
|
+
</button>
|
|
136
|
+
{open && (
|
|
137
|
+
<div style={{
|
|
138
|
+
position: 'absolute', top: 40, right: 0, width: 320, maxHeight: 400,
|
|
139
|
+
background: 'color-mix(in srgb, var(--sd-bg) 97%, transparent)', border: '1px solid rgba(255,255,255,0.1)',
|
|
140
|
+
borderRadius: 12, overflow: 'hidden', zIndex: 100,
|
|
141
|
+
backdropFilter: 'blur(12px)', WebkitBackdropFilter: 'blur(12px)',
|
|
142
|
+
boxShadow: '0 8px 32px rgba(0,0,0,0.5)',
|
|
143
|
+
}}>
|
|
144
|
+
<div style={{ padding: '12px 16px', borderBottom: '1px solid rgba(255,255,255,0.08)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
145
|
+
<span style={{ fontWeight: 700, fontSize: 14 }}>Notifications</span>
|
|
146
|
+
{unseen > 0 && (
|
|
147
|
+
<button onClick={() => {
|
|
148
|
+
fetch('/api/notifications', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'mark-all-seen' }) });
|
|
149
|
+
setNotes((prev) => prev.map((n) => ({ ...n, seen: true })));
|
|
150
|
+
}} style={{ background: 'none', border: 'none', color: '#3498DB', fontSize: 11, cursor: 'pointer', fontWeight: 600 }}>
|
|
151
|
+
Mark all read
|
|
152
|
+
</button>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
<div style={{ overflowY: 'auto', maxHeight: 340 }}>
|
|
156
|
+
{notes.length === 0 ? (
|
|
157
|
+
<div style={{ padding: 20, textAlign: 'center', color: '#555', fontSize: 12 }}>No notifications yet</div>
|
|
158
|
+
) : notes.slice(0, 20).map((n) => (
|
|
159
|
+
<div key={n.id} role="button" tabIndex={0}
|
|
160
|
+
onClick={() => show(n)}
|
|
161
|
+
onKeyDown={(e) => { if (e.key === 'Enter') show(n); }}
|
|
162
|
+
onMouseEnter={(e) => { e.currentTarget.style.background = 'rgba(255,255,255,0.06)'; }}
|
|
163
|
+
onMouseLeave={(e) => { e.currentTarget.style.background = n.seen ? 'transparent' : 'rgba(255,215,0,0.04)'; }}
|
|
164
|
+
style={{
|
|
165
|
+
padding: '10px 16px', borderBottom: '1px solid rgba(255,255,255,0.04)', cursor: 'pointer',
|
|
166
|
+
background: n.seen ? 'transparent' : 'rgba(255,215,0,0.04)',
|
|
167
|
+
}}>
|
|
168
|
+
<div style={{ display: 'flex', gap: 8, alignItems: 'flex-start' }}>
|
|
169
|
+
<span style={{ fontSize: 16 }}>{n.icon || '📌'}</span>
|
|
170
|
+
<div style={{ flex: 1 }}>
|
|
171
|
+
<div style={{ fontSize: 12, fontWeight: 600, color: n.seen ? '#888' : '#fff' }}>{n.title}<span style={{ color: '#556', marginLeft: 6 }}>›</span></div>
|
|
172
|
+
{n.message && <div style={{ fontSize: 10, color: '#666', marginTop: 2 }}>{n.message}</div>}
|
|
173
|
+
<div style={{ fontSize: 9, color: '#444', marginTop: 3 }}>{new Date(n.created_at).toLocaleDateString()}</div>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
))}
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
)}
|
|
181
|
+
</div>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Its own component so the scanner's answer, which changes every second or two
|
|
186
|
+
// while a scan runs, re-renders this button and not the whole page.
|
|
187
|
+
//
|
|
188
|
+
// It asks first, as a company scan does, and says what it will do and what it
|
|
189
|
+
// costs: one click on a small icon used to open a Chrome window on LinkedIn
|
|
190
|
+
// with no word of what it was about to do.
|
|
191
|
+
function RefreshButton({ isMobile }) {
|
|
192
|
+
const scan = useScanner();
|
|
193
|
+
const checking = scan.running && scan.action === 'refresh';
|
|
194
|
+
const busy = busyReason(scan);
|
|
195
|
+
return (
|
|
196
|
+
<button
|
|
197
|
+
onClick={async () => {
|
|
198
|
+
try {
|
|
199
|
+
const blocked = notReadyMessage(await scraperStatus());
|
|
200
|
+
if (blocked) { alert(blocked); return; }
|
|
201
|
+
const ok = window.confirm(
|
|
202
|
+
'Check for new connections?\n\nThis opens a Chrome window on your LinkedIn connections ' +
|
|
203
|
+
'list and reads it from the newest, stopping once it reaches people already saved. It ' +
|
|
204
|
+
'usually takes under a minute. It reads your own list, not a search, so it doesn’t ' +
|
|
205
|
+
'use your search budget, but like any scan it is LinkedIn traffic from your account.',
|
|
206
|
+
);
|
|
207
|
+
if (!ok) return;
|
|
208
|
+
await beginScrape('refresh');
|
|
209
|
+
alert('Checking for new connections — watch it on the Scan page.');
|
|
210
|
+
} catch (e) {
|
|
211
|
+
alert(e.message);
|
|
212
|
+
}
|
|
213
|
+
}}
|
|
214
|
+
disabled={Boolean(busy)}
|
|
215
|
+
title={busy ? `${busy}. One scan at a time.` : 'Check for new connections: reads your LinkedIn connections list from the newest'}
|
|
216
|
+
aria-label="Check for new connections"
|
|
217
|
+
style={{
|
|
218
|
+
// Words, not a bare ↻ (Blake, 2026-10-02: "make it easier to check for new
|
|
219
|
+
// connections"), and the same words as the Scan page's button.
|
|
220
|
+
height: isMobile ? 28 : 32, padding: isMobile ? '0 10px' : '0 14px', borderRadius: 16,
|
|
221
|
+
border: '1px solid rgba(0,255,136,0.3)', cursor: busy ? 'not-allowed' : 'pointer', opacity: busy ? 0.45 : 1,
|
|
222
|
+
background: 'rgba(0,255,136,0.08)', color: '#bff5d9', fontSize: isMobile ? 11 : 12.5, fontWeight: 700,
|
|
223
|
+
display: 'flex', alignItems: 'center', gap: 6, whiteSpace: 'nowrap', flexShrink: 0,
|
|
224
|
+
}}
|
|
225
|
+
>
|
|
226
|
+
<span aria-hidden="true" style={{ fontSize: isMobile ? 12 : 14 }}>↻</span>
|
|
227
|
+
{checking ? 'Checking…' : isMobile ? 'New' : 'Check for new'}
|
|
228
|
+
</button>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Asked once per tab, for the pages that don't load the network themselves.
|
|
233
|
+
let levelLoad = null;
|
|
234
|
+
|
|
235
|
+
/** Your level, opening your Profile (the sample's launch page, or a CSV's import page). */
|
|
236
|
+
function ProfileButton({ isMobile, csvMode, level }) {
|
|
237
|
+
const { userId } = useUser();
|
|
238
|
+
const source = IS_DEMO ? 'sample' : csvMode ? 'csv' : 'own';
|
|
239
|
+
const remembered = useSyncExternalStore(watchLevel, () => levelNow(source), () => null);
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
if (level != null) { rememberLevel(level, source); return; }
|
|
242
|
+
// The sample and a CSV live in the map page's tab: only your own network is loaded here.
|
|
243
|
+
if (remembered != null || IS_DEMO || csvMode || !userId) return;
|
|
244
|
+
if (!levelLoad) levelLoad = loadNetwork(userId).then((n) => networkLevel(n.degree1, n.degree2)).catch(() => { levelLoad = null; return null; });
|
|
245
|
+
levelLoad.then((l) => { if (l != null) rememberLevel(l, source); });
|
|
246
|
+
}, [level, remembered, csvMode, source, userId]);
|
|
247
|
+
const shown = level ?? remembered;
|
|
248
|
+
return (
|
|
249
|
+
<a href={IS_DEMO ? '/launch' : csvMode ? '/import' : '/profile'} title={shown != null ? `Your profile · level ${shown}` : 'Your profile'} style={{
|
|
250
|
+
width: isMobile ? 30 : 36, height: isMobile ? 30 : 36, borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
251
|
+
background: 'linear-gradient(135deg, #FFD700, #FF6B35)', color: '#000', fontWeight: 800, fontSize: isMobile ? 11 : 14,
|
|
252
|
+
textDecoration: 'none', marginLeft: isMobile ? 2 : 4, flexShrink: 0,
|
|
253
|
+
}}>
|
|
254
|
+
{shown ?? ''}
|
|
255
|
+
</a>
|
|
256
|
+
);
|
|
257
|
+
}
|