six-degrees 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/app-path-routes-manifest.json +1 -0
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +32 -4
- package/.next/standalone/.next/routes-manifest.json +6 -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.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 +16 -16
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +16 -16
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/admin-delete/route.js +1 -1
- package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/admin-update/route.js +1 -1
- package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/bridges/route.js +1 -1
- package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/bulk-import/route.js +1 -1
- package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/connections/route.js +1 -1
- package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/backup/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/csv/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/restore/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/delete-cluster/route.js +1 -1
- package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/ingest/route.js +1 -1
- package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/route.js +1 -1
- package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/notifications/route.js +1 -1
- package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/outreach/route.js +3 -3
- package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/queue/route.js +1 -1
- package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/scraper/route.js +4 -4
- package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/setup-profile/route.js +1 -1
- package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/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 +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 +1 -1
- package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/update-images/route.js +1 -1
- package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/users/route.js +1 -1
- package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/import.html +1 -1
- package/.next/standalone/.next/server/app/import.rsc +18 -18
- package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +18 -18
- 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 +16 -16
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +19 -19
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +16 -16
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +19 -19
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/insights/page/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/insights/page/build-manifest.json +20 -0
- package/.next/standalone/.next/server/app/insights/page/next-font-manifest.json +11 -0
- package/.next/standalone/.next/server/app/insights/page/react-loadable-manifest.json +1 -0
- package/.next/standalone/.next/server/app/insights/page/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/insights/page.js +13 -0
- package/.next/standalone/.next/server/app/insights/page.js.map +5 -0
- package/.next/standalone/.next/server/app/insights/page.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/insights/page_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/insights.html +1 -0
- package/.next/standalone/.next/server/app/insights.meta +26 -0
- package/.next/standalone/.next/server/app/insights.rsc +35 -0
- package/.next/standalone/.next/server/app/insights.segments/_full.segment.rsc +35 -0
- package/.next/standalone/.next/server/app/insights.segments/_tree.segment.rsc +4 -0
- package/.next/standalone/.next/server/app/insights.segments/insights/__PAGE__.segment.rsc +33 -0
- 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 +18 -18
- package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +18 -18
- 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 +16 -16
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/paths.html +1 -1
- package/.next/standalone/.next/server/app/paths.rsc +19 -19
- package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +19 -19
- 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 +16 -16
- 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 +19 -19
- package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +19 -19
- 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 +16 -16
- 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 +19 -19
- package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +19 -19
- 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 +21 -20
- 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 +18 -18
- package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +18 -18
- 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 +16 -16
- 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 +19 -19
- package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +19 -19
- 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 +16 -16
- 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 +19 -19
- package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +19 -19
- 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 +16 -16
- 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 +18 -18
- package/.next/standalone/.next/server/app/social.segments/_full.segment.rsc +18 -18
- 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 +16 -16
- package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__063s0x7._.js → [root-of-the-server]__0r-0t-9._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ra6xyg._.js +15 -0
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__20rwfdw._.js → [root-of-the-server]__0rcpjad._.js} +5 -2
- package/.next/standalone/.next/server/chunks/_1_fynia._.js +7 -7
- package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_02iz_9q._.js +2 -2
- package/.next/standalone/.next/server/chunks/lib_05agvk3._.js +2 -2
- package/.next/standalone/.next/server/chunks/lib_0v84u66._.js +2 -2
- package/.next/standalone/.next/server/chunks/lib_db-client_0_dch35.js +9 -9
- package/.next/standalone/.next/server/chunks/{lib_rpc_0jnfp2e.js → lib_rpc_00nndai.js} +1 -1
- package/.next/standalone/.next/server/chunks/{lib_rpc_0b17r6v.js → lib_rpc_03ibb-x.js} +1 -1
- package/.next/standalone/.next/server/chunks/{lib_rpc_0doq0t-.js → lib_rpc_03ri3ji.js} +1 -1
- package/.next/standalone/.next/server/chunks/{lib_rpc_0fd2rlw.js → lib_rpc_078ce48.js} +1 -1
- package/.next/standalone/.next/server/chunks/lib_rpc_0a69mgr.js +3 -0
- package/.next/standalone/.next/server/chunks/{lib_rpc_005abye.js → lib_rpc_0cej3t8.js} +1 -1
- package/.next/standalone/.next/server/chunks/lib_rpc_0cmh4g6.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0nx2_pc.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0r79-w1.js +3 -0
- package/.next/standalone/.next/server/chunks/{lib_rpc_0is4qoc.js → lib_rpc_0t552z6.js} +1 -1
- package/.next/standalone/.next/server/chunks/lib_rpc_0yx8ynd.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_12p-o9-.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1466dnb.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1sryxv6.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1trn-x5.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1ygu_mv.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0egamyq._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_04am_h8._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_07zb7lu._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0_79idq._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0gds903._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0klqt9z._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_1k9qfsk._.js → _14jak5s._.js} +6 -2
- package/.next/standalone/.next/server/chunks/ssr/_14nxd3q._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1cj2yog._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1fm1ih_._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1hdqw0-._.js +7 -0
- package/.next/standalone/.next/server/chunks/ssr/_1k2nmbw._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_0gy11ar._.js → _1r62six._.js} +6 -2
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_insights_page_actions_0-0tf75.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_components_0uuff77._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_components_1j3q-h5._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_01o3i5y._.js → app_components_settings_SectorPicker_1c-29qw.js} +2 -6
- package/.next/standalone/.next/server/chunks/ssr/app_components_ui_0pdmz2z.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_insights_page_1d_chja.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +14 -14
- package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +3 -3
- 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 +3 -3
- 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/02wnf8dkxegxg.js +34 -0
- package/.next/standalone/.next/static/chunks/0838_s6w-i_ik.js +1 -0
- package/.next/{static/chunks/2eo9pqq9gay3r.js → standalone/.next/static/chunks/0ka4prpk3w9wn.js} +4 -4
- package/.next/standalone/.next/static/chunks/0s_xxwp7z88vv.js +1 -0
- package/.next/standalone/.next/static/chunks/1-nnl8amfm4yh.js +57 -0
- package/.next/standalone/.next/static/chunks/112mv-com-mqa.js +21 -0
- package/.next/{static/chunks/44yn0tcd9dfk3.js → standalone/.next/static/chunks/1bkt_hhjzvbh8.js} +1 -1
- package/.next/standalone/.next/static/chunks/1cmphb3kh2ers.css +3 -0
- package/.next/{static/chunks/1qbjermbf68xj.js → standalone/.next/static/chunks/1d8yv3k_atka7.js} +1 -1
- package/.next/standalone/.next/static/chunks/23hc51oa4vkhc.js +5 -0
- package/.next/standalone/.next/static/chunks/27n8k9wxlz5-7.js +1 -0
- package/.next/standalone/.next/static/chunks/{0p3g5tfostsep.js → 2q9i7pcxcnph1.js} +2 -2
- package/.next/{static/chunks/2zf4jmz9l3li2.js → standalone/.next/static/chunks/2w-y83mjlpnse.js} +1 -1
- package/.next/standalone/.next/static/chunks/2ws1xf293j-tm.js +5 -0
- package/.next/standalone/.next/static/chunks/3cqn-r-y5rrw1.js +5 -0
- package/.next/standalone/.next/static/chunks/3opgg1efrx8q6.js +1 -0
- package/.next/standalone/.next/static/chunks/3p51fpxxi4d5y.js +34 -0
- package/.next/{static/chunks/0an_b6zi1r_qq.js → standalone/.next/static/chunks/3y40z6of3r9py.js} +1 -5
- package/.next/standalone/.next/static/chunks/403dpflln8ah5.js +1 -0
- package/.next/standalone/CHANGELOG.md +60 -0
- package/.next/standalone/README.md +25 -1
- package/.next/standalone/SECURITY.md +8 -0
- package/.next/standalone/app/api/outreach/route.js +13 -73
- package/.next/standalone/app/api/scraper/route.js +97 -4
- package/.next/standalone/app/components/AppHeader.js +22 -6
- package/.next/standalone/app/components/AppTabs.js +4 -0
- package/.next/standalone/app/components/AutoConnect.js +286 -0
- package/.next/standalone/app/components/ChainView.js +75 -2
- package/.next/standalone/app/components/FilterPanel.js +8 -1
- package/.next/standalone/app/components/LinkedInLimits.js +38 -13
- package/.next/standalone/app/components/OutlinkQuest.js +32 -11
- package/.next/standalone/app/components/ScanStatusBar.js +18 -3
- package/.next/standalone/app/components/ScanTrail.js +13 -3
- package/.next/standalone/app/components/SeparationView.js +6 -22
- package/.next/standalone/app/components/Sidebar.js +147 -38
- package/.next/standalone/app/components/insights/Boards.js +275 -0
- package/.next/standalone/app/components/insights/PeopleView.js +266 -0
- package/.next/standalone/app/components/insights/Rail.js +156 -0
- package/.next/standalone/app/components/insights/Report.js +382 -0
- package/.next/standalone/app/components/insights/parts.js +239 -0
- package/.next/standalone/app/components/settings/UsageSection.js +50 -3
- package/.next/standalone/app/globals.css +2 -3
- package/.next/standalone/app/insights/page.js +220 -0
- package/.next/standalone/app/page.js +1 -17
- package/.next/standalone/app/queue/page.js +30 -18
- package/.next/standalone/app/setup/page.js +16 -9
- package/.next/standalone/lib/auto-connect.js +145 -0
- package/.next/standalone/lib/companies.js +1 -1
- package/.next/standalone/lib/in-progress.js +46 -0
- package/.next/standalone/lib/insights-board.js +613 -0
- package/.next/standalone/lib/linkedin-limits.js +36 -11
- package/.next/standalone/lib/requests-client.js +10 -0
- package/.next/standalone/lib/requests.js +124 -0
- package/.next/standalone/lib/scan-state.js +1 -0
- package/.next/standalone/lib/scraper-client.js +18 -5
- package/.next/standalone/lib/settings.js +3 -0
- package/.next/standalone/lib/usage.js +5 -0
- package/.next/standalone/package-lock.json +2 -2
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/scripts/scrape.py +569 -9
- package/.next/static/chunks/02wnf8dkxegxg.js +34 -0
- package/.next/static/chunks/0838_s6w-i_ik.js +1 -0
- package/.next/{standalone/.next/static/chunks/2eo9pqq9gay3r.js → static/chunks/0ka4prpk3w9wn.js} +4 -4
- package/.next/static/chunks/0s_xxwp7z88vv.js +1 -0
- package/.next/static/chunks/1-nnl8amfm4yh.js +57 -0
- package/.next/static/chunks/112mv-com-mqa.js +21 -0
- package/.next/{standalone/.next/static/chunks/44yn0tcd9dfk3.js → static/chunks/1bkt_hhjzvbh8.js} +1 -1
- package/.next/static/chunks/1cmphb3kh2ers.css +3 -0
- package/.next/{standalone/.next/static/chunks/1qbjermbf68xj.js → static/chunks/1d8yv3k_atka7.js} +1 -1
- package/.next/static/chunks/23hc51oa4vkhc.js +5 -0
- package/.next/static/chunks/27n8k9wxlz5-7.js +1 -0
- package/.next/static/chunks/{0p3g5tfostsep.js → 2q9i7pcxcnph1.js} +2 -2
- package/.next/{standalone/.next/static/chunks/2zf4jmz9l3li2.js → static/chunks/2w-y83mjlpnse.js} +1 -1
- package/.next/static/chunks/2ws1xf293j-tm.js +5 -0
- package/.next/static/chunks/3cqn-r-y5rrw1.js +5 -0
- package/.next/static/chunks/3opgg1efrx8q6.js +1 -0
- package/.next/static/chunks/3p51fpxxi4d5y.js +34 -0
- package/.next/{standalone/.next/static/chunks/0an_b6zi1r_qq.js → static/chunks/3y40z6of3r9py.js} +1 -5
- package/.next/static/chunks/403dpflln8ah5.js +1 -0
- package/CHANGELOG.md +60 -0
- package/README.md +25 -1
- package/SECURITY.md +8 -0
- package/lib/auto-connect.js +145 -0
- package/lib/companies.js +1 -1
- package/lib/in-progress.js +46 -0
- package/lib/insights-board.js +613 -0
- package/lib/linkedin-limits.js +36 -11
- package/lib/requests-client.js +10 -0
- package/lib/requests.js +124 -0
- package/lib/scan-state.js +1 -0
- package/lib/scraper-client.js +18 -5
- package/lib/settings.js +3 -0
- package/lib/usage.js +5 -0
- package/package.json +1 -1
- package/scripts/scrape.py +569 -9
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_4y8z7._.js +0 -8
- package/.next/standalone/.next/server/chunks/_02twlca._.js +0 -10
- package/.next/standalone/.next/server/chunks/lib_rpc_0nef_n7.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0plrzra.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_13a4m0c.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1if17c-.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1j-zw7v.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1sngt-q.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1v2sgme.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1vh-tgx.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1zaubz7.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_02sband._.js +0 -7
- package/.next/standalone/.next/server/chunks/ssr/_189g8lg._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1f_n00p._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/app_components_0t908t-._.js +0 -3
- package/.next/standalone/.next/static/chunks/0e3g0sil79cus.js +0 -21
- package/.next/standalone/.next/static/chunks/0zd0t7e60bn4z.js +0 -1
- package/.next/standalone/.next/static/chunks/163b-20vwa06i.js +0 -34
- package/.next/standalone/.next/static/chunks/1pa-2v1ajvrko.js +0 -1
- package/.next/standalone/.next/static/chunks/23_1z47al92nz.js +0 -57
- package/.next/standalone/.next/static/chunks/260bomtr214m8.js +0 -34
- package/.next/standalone/.next/static/chunks/2xk2ra4piiuod.js +0 -34
- package/.next/standalone/.next/static/chunks/3b0-14cxqsprt.js +0 -1
- package/.next/standalone/.next/static/chunks/3jarus1_38s2z.js +0 -1
- package/.next/standalone/.next/static/chunks/3ssu341hav_sq.css +0 -3
- package/.next/standalone/.next/static/chunks/3v1qd3sqhqn0d.js +0 -1
- package/.next/static/chunks/0e3g0sil79cus.js +0 -21
- package/.next/static/chunks/0zd0t7e60bn4z.js +0 -1
- package/.next/static/chunks/163b-20vwa06i.js +0 -34
- package/.next/static/chunks/1pa-2v1ajvrko.js +0 -1
- package/.next/static/chunks/23_1z47al92nz.js +0 -57
- package/.next/static/chunks/260bomtr214m8.js +0 -34
- package/.next/static/chunks/2xk2ra4piiuod.js +0 -34
- package/.next/static/chunks/3b0-14cxqsprt.js +0 -1
- package/.next/static/chunks/3jarus1_38s2z.js +0 -1
- package/.next/static/chunks/3ssu341hav_sq.css +0 -3
- package/.next/static/chunks/3v1qd3sqhqn0d.js +0 -1
- /package/.next/standalone/.next/static/{ZVpLBK-2vpllv2HxOFs2d → 3NCyJyAPDCEYsYTwF1Ieu}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{ZVpLBK-2vpllv2HxOFs2d → 3NCyJyAPDCEYsYTwF1Ieu}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{ZVpLBK-2vpllv2HxOFs2d → 3NCyJyAPDCEYsYTwF1Ieu}/_ssgManifest.js +0 -0
- /package/.next/static/{ZVpLBK-2vpllv2HxOFs2d → 3NCyJyAPDCEYsYTwF1Ieu}/_buildManifest.js +0 -0
- /package/.next/static/{ZVpLBK-2vpllv2HxOFs2d → 3NCyJyAPDCEYsYTwF1Ieu}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{ZVpLBK-2vpllv2HxOFs2d → 3NCyJyAPDCEYsYTwF1Ieu}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
// Auto, next to every Connect: one connection request, sent for you from the
|
|
4
|
+
// scanner's Chrome, without a note.
|
|
5
|
+
//
|
|
6
|
+
// Blake, 2026-10-03: "auto add and basically adds the person for them in the
|
|
7
|
+
// card or wherever its available", then "i just want it next to connect button
|
|
8
|
+
// but say auto and makes it the better pick visually". So Auto comes first and
|
|
9
|
+
// wears the look Connect had (the gold-to-orange gradient), and the page's own
|
|
10
|
+
// Connect, which opens their profile for you to connect yourself, steps back to
|
|
11
|
+
// an outline beside it.
|
|
12
|
+
//
|
|
13
|
+
// One press, one person: it starts the scanner's `connect` job
|
|
14
|
+
// (app/api/scraper/route.js, scripts/scrape.py connect_person) and says how it
|
|
15
|
+
// went. It waits its turn like every Scan button (greyed out, with the reason,
|
|
16
|
+
// while anything else runs). Before the first one ever it asks once, and the
|
|
17
|
+
// answer is a setting (lib/auto-connect.js); the server refuses until it's
|
|
18
|
+
// given. A request LinkedIn showed as pending is marked as sent by the server,
|
|
19
|
+
// through the same store as Connect (lib/requests.js), and shown everywhere
|
|
20
|
+
// here a moment after "Request sent" (lib/requests-client.js sawRequested).
|
|
21
|
+
|
|
22
|
+
import { useEffect, useRef, useState } from 'react';
|
|
23
|
+
import { createPortal } from 'react-dom';
|
|
24
|
+
import useScanner from './useScanner';
|
|
25
|
+
import { beginScrape, busyReason } from '../../lib/scraper-client';
|
|
26
|
+
import { sawRequested } from '../../lib/requests-client';
|
|
27
|
+
import { AUTO_EXPLAIN, AUTO_CONFIRM, INVITE_CAPS, connectOutcome } from '../../lib/auto-connect';
|
|
28
|
+
|
|
29
|
+
export const AUTO_GRADIENT = 'linear-gradient(135deg, #FFD700, #FF6B35)';
|
|
30
|
+
|
|
31
|
+
// How long "Request sent" shows before the card turns to its pending state.
|
|
32
|
+
const SENT_SHOWN_MS = 2500;
|
|
33
|
+
|
|
34
|
+
// Whether the one-time question has been answered yes, asked of the app once per page.
|
|
35
|
+
let acceptedHere = false;
|
|
36
|
+
|
|
37
|
+
async function autoAccepted() {
|
|
38
|
+
if (acceptedHere) return true;
|
|
39
|
+
try {
|
|
40
|
+
const r = await fetch('/api/settings', { cache: 'no-store' });
|
|
41
|
+
const d = await r.json();
|
|
42
|
+
acceptedHere = Boolean(d?.settings?.autoConnectAccepted);
|
|
43
|
+
} catch {
|
|
44
|
+
acceptedHere = false; // can't tell: ask, which is harmless
|
|
45
|
+
}
|
|
46
|
+
return acceptedHere;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function saveAccepted() {
|
|
50
|
+
const r = await fetch('/api/settings', {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: { 'Content-Type': 'application/json' },
|
|
53
|
+
body: JSON.stringify({ settings: { autoConnectAccepted: new Date().toISOString() } }),
|
|
54
|
+
});
|
|
55
|
+
if (!r.ok) throw new Error((await r.json().catch(() => ({}))).error || 'Your answer couldn’t be saved.');
|
|
56
|
+
acceptedHere = true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Auto's state for one person: press it, the one-time question, how it went. */
|
|
60
|
+
function useAutoConnect(person, onSent) {
|
|
61
|
+
const scan = useScanner();
|
|
62
|
+
const [asking, setAsking] = useState(false);
|
|
63
|
+
const [sending, setSending] = useState(false);
|
|
64
|
+
const [result, setResult] = useState(null); // { ok, text } once a press has an answer
|
|
65
|
+
const ours = useRef(new Set()); // jobs this button started (by startedAt)
|
|
66
|
+
const shownJob = useRef(null);
|
|
67
|
+
|
|
68
|
+
const mine = scan.running && scan.action === 'connect' && Boolean(person?.id) && scan.target?.id === person.id;
|
|
69
|
+
const busy = scan.running && !mine && !sending ? busyReason(scan) : null;
|
|
70
|
+
|
|
71
|
+
const done = (r) => {
|
|
72
|
+
setResult(r);
|
|
73
|
+
if (r.marks) setTimeout(() => { sawRequested(person); onSent?.(person); }, SENT_SHOWN_MS);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// A request to this person started elsewhere (another window, or before a
|
|
77
|
+
// reload) that this page watched end: say how it went here too.
|
|
78
|
+
const last = (scan.finished || []).find((j) => j.action === 'connect' && j.target?.id === person?.id);
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (!last || ours.current.has(last.startedAt) || shownJob.current === last.startedAt) return;
|
|
81
|
+
shownJob.current = last.startedAt;
|
|
82
|
+
done(connectOutcome(last));
|
|
83
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
84
|
+
}, [last?.startedAt]);
|
|
85
|
+
|
|
86
|
+
async function send() {
|
|
87
|
+
setAsking(false);
|
|
88
|
+
setSending(true);
|
|
89
|
+
setResult(null);
|
|
90
|
+
try {
|
|
91
|
+
const { startedAt, ended } = await beginScrape('connect', { id: person.id, name: person.name });
|
|
92
|
+
ours.current.add(startedAt);
|
|
93
|
+
shownJob.current = startedAt;
|
|
94
|
+
done(connectOutcome(await ended));
|
|
95
|
+
} catch (err) {
|
|
96
|
+
if (err.details?.needsAutoAcceptance) {
|
|
97
|
+
acceptedHere = false;
|
|
98
|
+
setAsking(true);
|
|
99
|
+
} else {
|
|
100
|
+
setResult({ ok: false, text: err.message });
|
|
101
|
+
}
|
|
102
|
+
} finally {
|
|
103
|
+
setSending(false);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function press() {
|
|
108
|
+
if (sending || mine || busy) return;
|
|
109
|
+
setResult(null);
|
|
110
|
+
if (await autoAccepted()) send();
|
|
111
|
+
else setAsking(true);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function confirm() {
|
|
115
|
+
try {
|
|
116
|
+
await saveAccepted();
|
|
117
|
+
send();
|
|
118
|
+
} catch (err) {
|
|
119
|
+
setAsking(false);
|
|
120
|
+
setResult({ ok: false, text: err.message });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
press, confirm, cancel: () => setAsking(false), dismiss: () => setResult(null),
|
|
126
|
+
asking, sending: sending || mine, busy, result,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The question before Auto's first request ever. Into <body>: a blurred panel would trap a fixed overlay (TheirCircle.js). */
|
|
131
|
+
function AutoAsk({ onSend, onCancel }) {
|
|
132
|
+
const sendRef = useRef(null);
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
sendRef.current?.focus();
|
|
135
|
+
const onKey = (e) => { if (e.key === 'Escape') onCancel(); };
|
|
136
|
+
window.addEventListener('keydown', onKey);
|
|
137
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
138
|
+
}, [onCancel]);
|
|
139
|
+
return createPortal(
|
|
140
|
+
<div role="dialog" aria-modal="true" aria-label="Auto sends connection requests" onClick={onCancel} style={{
|
|
141
|
+
position: 'fixed', inset: 0, zIndex: 400, background: 'rgba(var(--sd-shade, 0, 0, 0), 0.72)',
|
|
142
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16,
|
|
143
|
+
}}>
|
|
144
|
+
<div data-auto-ask onClick={(e) => e.stopPropagation()} style={{
|
|
145
|
+
width: 'min(420px, 100%)', boxSizing: 'border-box', borderRadius: 14, padding: '20px 20px 16px',
|
|
146
|
+
background: 'var(--sd-surface, #12111c)', border: '1px solid rgba(255,215,0,0.28)',
|
|
147
|
+
boxShadow: '0 18px 60px rgba(0,0,0,0.5)', textAlign: 'left',
|
|
148
|
+
}}>
|
|
149
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 10 }}>
|
|
150
|
+
<span style={{
|
|
151
|
+
padding: '3px 9px', borderRadius: 6, background: AUTO_GRADIENT, color: '#000', fontWeight: 800, fontSize: 12,
|
|
152
|
+
}}>Auto</span>
|
|
153
|
+
<span style={{ fontSize: 15, fontWeight: 750, color: 'var(--sd-fg-1, #fff)' }}>Before the first one</span>
|
|
154
|
+
</div>
|
|
155
|
+
<p style={{ margin: 0, fontSize: 13.5, lineHeight: 1.6, color: 'var(--sd-fg-2, #d6dbe4)' }}>{AUTO_CONFIRM}</p>
|
|
156
|
+
<p style={{ margin: '10px 0 0', fontSize: 12, lineHeight: 1.55, color: 'var(--sd-fg-3, #8b9a9a)' }}>
|
|
157
|
+
It never adds a note, and stops at {INVITE_CAPS.day} in any 24 hours and {INVITE_CAPS.week} in any 7 days.
|
|
158
|
+
This is asked once.
|
|
159
|
+
</p>
|
|
160
|
+
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 16 }}>
|
|
161
|
+
<button type="button" onClick={onCancel} style={{
|
|
162
|
+
padding: '9px 16px', borderRadius: 8, fontSize: 13, fontWeight: 650, cursor: 'pointer',
|
|
163
|
+
background: 'transparent', color: 'var(--sd-fg-2, #ccc)', border: '1px solid rgba(var(--sd-ink, 255, 255, 255), 0.18)',
|
|
164
|
+
}}>Cancel</button>
|
|
165
|
+
<button ref={sendRef} type="button" onClick={onSend} style={{
|
|
166
|
+
padding: '9px 18px', borderRadius: 8, fontSize: 13, fontWeight: 800, cursor: 'pointer',
|
|
167
|
+
background: AUTO_GRADIENT, color: '#000', border: 'none',
|
|
168
|
+
}}>Send it</button>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
</div>,
|
|
172
|
+
document.body,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// minWidth: room for "Sending…", so the row doesn't jump when it changes.
|
|
177
|
+
const SIZES = {
|
|
178
|
+
card: { padding: '10px 18px', fontSize: 13, radius: 8, minWidth: 104 },
|
|
179
|
+
quest: { padding: '7px 14px', fontSize: 12.5, radius: 8, minWidth: 92 },
|
|
180
|
+
row: { padding: '4px 9px', fontSize: 9.5, radius: 4, minWidth: 0 },
|
|
181
|
+
// Insights' Power Index rows: the size of the table's other buttons (Open circle, Scan circle).
|
|
182
|
+
table: { padding: '6px 11px', fontSize: 12, radius: 7, minWidth: 0 },
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/** The Auto button itself: the gradient, the better pick. */
|
|
186
|
+
function AutoButton({ auto, size, label = 'Auto' }) {
|
|
187
|
+
const s = SIZES[size] || SIZES.card;
|
|
188
|
+
// Also while "Request sent" shows, before the card turns to its pending state.
|
|
189
|
+
const off = auto.sending || Boolean(auto.busy) || Boolean(auto.result?.marks);
|
|
190
|
+
const title = auto.busy ? `${auto.busy}. Auto waits for it to finish.` : AUTO_EXPLAIN;
|
|
191
|
+
return (
|
|
192
|
+
<button type="button" data-auto-connect onClick={(e) => { e.stopPropagation(); auto.press(); }} disabled={off} title={title}
|
|
193
|
+
aria-label={auto.sending ? 'Sending the request' : `${label}: ${AUTO_EXPLAIN}`}
|
|
194
|
+
style={{
|
|
195
|
+
padding: s.padding, borderRadius: s.radius, fontSize: s.fontSize, fontWeight: 800, border: 'none', minWidth: s.minWidth,
|
|
196
|
+
background: AUTO_GRADIENT, color: '#000', cursor: off ? 'not-allowed' : 'pointer', opacity: auto.busy ? 0.45 : 1,
|
|
197
|
+
whiteSpace: 'nowrap', flexShrink: 0, boxShadow: off ? 'none' : '0 2px 12px rgba(255,140,50,0.28)',
|
|
198
|
+
}}>
|
|
199
|
+
{auto.sending ? 'Sending…' : label}
|
|
200
|
+
</button>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const resultColor = (r) => (r.ok ? 'var(--sd-green, #00ff88)' : 'var(--sd-coral, #ff8a7a)');
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Auto and the page's Connect, side by side: Auto first, as the better pick.
|
|
208
|
+
* `connect(secondary)` draws the page's own Connect, told whether it now stands
|
|
209
|
+
* second (an outline) or alone (as it always looked). Without Auto (`canAuto`
|
|
210
|
+
* false: the sample, a CSV, someone with no profile on file) Connect stands
|
|
211
|
+
* alone. `size`: 'card' (the person card: a line under it says what Auto does),
|
|
212
|
+
* 'quest' (Outlink's move cards), 'table' (Insights' rows) or 'row' (a list row: the line is the
|
|
213
|
+
* button's tooltip, and how it went shows in a small note under the buttons).
|
|
214
|
+
*/
|
|
215
|
+
export default function ConnectChoice({ person, canAuto = true, size = 'card', connect, onSent, style }) {
|
|
216
|
+
const available = canAuto && Boolean(person?.id) && Boolean(person?.profile_url);
|
|
217
|
+
if (!available) return connect(false);
|
|
218
|
+
// Keyed by person: a card reused for the next person starts fresh, never
|
|
219
|
+
// showing the last one's result as theirs.
|
|
220
|
+
return <WithAuto key={person.id} person={person} size={size} connect={connect} onSent={onSent} style={style} />;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function WithAuto({ person, size, connect, onSent, style }) {
|
|
224
|
+
const auto = useAutoConnect(person, onSent);
|
|
225
|
+
const ask = auto.asking && <AutoAsk onSend={auto.confirm} onCancel={auto.cancel} />;
|
|
226
|
+
|
|
227
|
+
if (size === 'row' || size === 'table') {
|
|
228
|
+
return (
|
|
229
|
+
<span style={{ position: 'relative', display: 'inline-flex', gap: 4, alignItems: 'center', flexShrink: 0, ...style }}
|
|
230
|
+
onClick={(e) => e.stopPropagation()}>
|
|
231
|
+
<AutoButton auto={auto} size={size} />
|
|
232
|
+
{connect(true)}
|
|
233
|
+
{auto.result && (
|
|
234
|
+
<span role="status" style={{
|
|
235
|
+
position: 'absolute', top: 'calc(100% + 4px)', right: 0, zIndex: 20, width: 230, boxSizing: 'border-box',
|
|
236
|
+
padding: '7px 24px 7px 9px', borderRadius: 7, fontSize: 11, lineHeight: 1.45, fontWeight: 600, textAlign: 'left',
|
|
237
|
+
color: resultColor(auto.result), background: 'var(--sd-surface, #14131d)',
|
|
238
|
+
border: `1px solid ${auto.result.ok ? 'rgba(0,255,136,0.35)' : 'rgba(255,138,122,0.4)'}`, boxShadow: '0 8px 24px rgba(0,0,0,0.4)',
|
|
239
|
+
}}>
|
|
240
|
+
{auto.result.text}
|
|
241
|
+
<button type="button" aria-label="Close" onClick={auto.dismiss} style={{
|
|
242
|
+
position: 'absolute', top: 3, right: 4, background: 'none', border: 'none', cursor: 'pointer',
|
|
243
|
+
color: 'var(--sd-fg-3, #888)', fontSize: 12, lineHeight: 1,
|
|
244
|
+
}}>×</button>
|
|
245
|
+
</span>
|
|
246
|
+
)}
|
|
247
|
+
{ask}
|
|
248
|
+
</span>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const card = size === 'card';
|
|
253
|
+
return (
|
|
254
|
+
<div style={style}>
|
|
255
|
+
<div style={{ display: 'flex', gap: 8, alignItems: 'stretch', flexWrap: 'wrap' }}>
|
|
256
|
+
<AutoButton auto={auto} size={size} />
|
|
257
|
+
{connect(true)}
|
|
258
|
+
</div>
|
|
259
|
+
{card && (
|
|
260
|
+
<div style={{ fontSize: 11, lineHeight: 1.5, color: 'var(--sd-fg-3, #8b9a9a)', marginTop: 8 }}>
|
|
261
|
+
{auto.busy ? `${auto.busy}. Auto waits for it to finish.` : AUTO_EXPLAIN}
|
|
262
|
+
</div>
|
|
263
|
+
)}
|
|
264
|
+
{auto.result && (
|
|
265
|
+
<div role="status" style={{ fontSize: 12, lineHeight: 1.5, fontWeight: 650, marginTop: 8, color: resultColor(auto.result) }}>
|
|
266
|
+
{auto.result.text}
|
|
267
|
+
</div>
|
|
268
|
+
)}
|
|
269
|
+
{ask}
|
|
270
|
+
</div>
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* The page's Connect when it stands second to Auto: the same link, as an
|
|
276
|
+
* outline. `color` and `line` keep a page's own colour (Outlink's blue Add).
|
|
277
|
+
*/
|
|
278
|
+
export function secondaryLook(size = 'card', { color = 'var(--sd-gold, #FFD700)', line = 'rgba(255,215,0,0.5)' } = {}) {
|
|
279
|
+
const s = SIZES[size] || SIZES.card;
|
|
280
|
+
return {
|
|
281
|
+
padding: s.padding, borderRadius: s.radius, fontSize: s.fontSize, fontWeight: 700,
|
|
282
|
+
background: 'transparent', color, border: `1px solid ${line}`,
|
|
283
|
+
textDecoration: 'none', cursor: 'pointer', whiteSpace: 'nowrap', boxSizing: 'border-box',
|
|
284
|
+
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
|
285
|
+
};
|
|
286
|
+
}
|
|
@@ -35,6 +35,7 @@ import { useRouter } from 'next/navigation';
|
|
|
35
35
|
import { circleIndex, MAX_DEGREE } from '../../lib/circle';
|
|
36
36
|
import { localPhoto } from '../../lib/photos';
|
|
37
37
|
import { reachIndex, reachState, circleState, readyByCircle, circleScanCost, scanBars } from '../../lib/reach';
|
|
38
|
+
import { stoppedLine } from '../../lib/in-progress';
|
|
38
39
|
import { reachSegments, RING } from '../../lib/dot-rings';
|
|
39
40
|
import { ringLayout, chainTree, dotRadius, previewBand, tierBandLayout, circleActivity } from '../../lib/chain-layout';
|
|
40
41
|
import { noteCircle } from '../../lib/notifications';
|
|
@@ -42,7 +43,7 @@ import { useUser } from './UserProvider';
|
|
|
42
43
|
import { redundancy } from '../../lib/brokerage';
|
|
43
44
|
import { keyFor, score } from '../../lib/separation';
|
|
44
45
|
import { hasRequest } from '../../lib/requests-client';
|
|
45
|
-
import { watchScanner, scannerNow, isCircleScan } from '../../lib/scraper-client';
|
|
46
|
+
import { watchScanner, scannerNow, isCircleScan, beginScrape, scraperStatus, notReadyMessage, busyReason, resumePoint } from '../../lib/scraper-client';
|
|
46
47
|
import useRequests from './useRequests';
|
|
47
48
|
import { TIER_COLORS } from '../../lib/themes';
|
|
48
49
|
|
|
@@ -1111,6 +1112,17 @@ function EmptyCircle({ person, depth, reach, requests, scanning, canScan, onSele
|
|
|
1111
1112
|
const next = ordinal(depth + 1);
|
|
1112
1113
|
const yours = person.degree === 1;
|
|
1113
1114
|
const state = yours ? circleState(person, reach) : null;
|
|
1115
|
+
// Read partway with nothing saved yet (everyone on those pages was already
|
|
1116
|
+
// yours): that isn't a list that has been read, so it says it stopped, with
|
|
1117
|
+
// the card's Resume (Blake, 2026-10-03: "the ones they stopped and would like
|
|
1118
|
+
// to resume"). Where it stopped is the card's own answer, asked of the server
|
|
1119
|
+
// (GET /api/scraper?resume=<id>): the map's scan notes don't say how far a
|
|
1120
|
+
// list got. Undefined while it's asked.
|
|
1121
|
+
const stopped = useStoppedAt(state === 'scanned' && !scanning ? person.id : null);
|
|
1122
|
+
// Why Resume didn't start, kept here: a start greys this to "Scanning…" for a
|
|
1123
|
+
// moment even when it's refused, and the button's own state would go with it.
|
|
1124
|
+
const [problem, setProblem] = useState(null);
|
|
1125
|
+
if (state === 'scanned' && !scanning && stopped === undefined) return null; // never "has been read" by mistake
|
|
1114
1126
|
const cost = circleScanCost();
|
|
1115
1127
|
let title;
|
|
1116
1128
|
let body;
|
|
@@ -1118,6 +1130,10 @@ function EmptyCircle({ person, depth, reach, requests, scanning, canScan, onSele
|
|
|
1118
1130
|
if (scanning) {
|
|
1119
1131
|
title = `Scanning ${first}’s circle now`;
|
|
1120
1132
|
body = 'Their people appear here as the scan saves them, every 10 pages.';
|
|
1133
|
+
} else if (stopped) {
|
|
1134
|
+
title = `${first}’s scan stopped partway`;
|
|
1135
|
+
body = `${stoppedLine(stopped)}. Everyone on those pages was already one of your connections, so nobody new is here yet. Resume carries on from page ${stopped.nextPage}.`;
|
|
1136
|
+
if (canScan) action = <ResumeHere person={person} stopped={stopped} problem={problem} setProblem={setProblem} />;
|
|
1121
1137
|
} else if (state === 'hidden') {
|
|
1122
1138
|
title = `${first} keeps their connections hidden`;
|
|
1123
1139
|
body = 'LinkedIn doesn’t show their list, so there is no circle to scan.';
|
|
@@ -1149,7 +1165,7 @@ function EmptyCircle({ person, depth, reach, requests, scanning, canScan, onSele
|
|
|
1149
1165
|
return (
|
|
1150
1166
|
<div role="status" style={{
|
|
1151
1167
|
position: 'absolute', left: '50%', top, transform: 'translateX(-50%)', width: 'min(360px, calc(100% - 32px))',
|
|
1152
|
-
background: 'color-mix(in srgb, var(--sd-bg) 94%, transparent)', border: `1px solid ${state === 'hidden' ? 'rgba(var(--sd-ink, 255, 255, 255), 0.12)' : 'rgba(0,255,136,0.25)'}`,
|
|
1168
|
+
background: 'color-mix(in srgb, var(--sd-bg) 94%, transparent)', border: `1px solid ${state === 'hidden' ? 'rgba(var(--sd-ink, 255, 255, 255), 0.12)' : stopped && !scanning ? 'rgba(255,215,0,0.4)' : 'rgba(0,255,136,0.25)'}`,
|
|
1153
1169
|
borderRadius: 12, padding: '14px 16px', textAlign: 'center',
|
|
1154
1170
|
}}>
|
|
1155
1171
|
<div style={{ fontSize: 13, fontWeight: 800, color: state === 'hidden' ? 'var(--sd-fg-3, #aaa)' : 'var(--sd-fg-1, #fff)', marginBottom: 6 }}>
|
|
@@ -1161,6 +1177,63 @@ function EmptyCircle({ person, depth, reach, requests, scanning, canScan, onSele
|
|
|
1161
1177
|
);
|
|
1162
1178
|
}
|
|
1163
1179
|
|
|
1180
|
+
/**
|
|
1181
|
+
* Where Resume would carry on with one of your connections ({ nextPage,
|
|
1182
|
+
* pagesRead }), null when there's nothing to carry on with, undefined while
|
|
1183
|
+
* it's asked; and never asked for null. Asked again whenever `id` comes back,
|
|
1184
|
+
* as it does when a scan of theirs ends.
|
|
1185
|
+
*/
|
|
1186
|
+
function useStoppedAt(id) {
|
|
1187
|
+
const [got, setGot] = useState({ id: null, resume: undefined });
|
|
1188
|
+
useEffect(() => {
|
|
1189
|
+
if (!id) return undefined;
|
|
1190
|
+
let live = true;
|
|
1191
|
+
resumePoint(id).then((r) => { if (live) setGot({ id, resume: r }); }, () => { if (live) setGot({ id, resume: null }); });
|
|
1192
|
+
return () => { live = false; };
|
|
1193
|
+
}, [id]);
|
|
1194
|
+
if (!id) return null;
|
|
1195
|
+
return got.id === id ? got.resume : undefined;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
/**
|
|
1199
|
+
* Resume for a circle that stopped partway, from the map: what the card's
|
|
1200
|
+
* Resume does (Sidebar.js CreateClusterCard), the same check first, carrying on
|
|
1201
|
+
* from the page it stopped at, never page 1 again. Once it starts, the empty
|
|
1202
|
+
* circle says it's being scanned. Greyed out, saying why, while another scan runs.
|
|
1203
|
+
*/
|
|
1204
|
+
function ResumeHere({ person, stopped, problem, setProblem }) {
|
|
1205
|
+
const busy = useSyncExternalStore(watchScanner, () => busyReason(scannerNow()), () => null);
|
|
1206
|
+
const [checking, setChecking] = useState(false);
|
|
1207
|
+
async function go() {
|
|
1208
|
+
setProblem(null);
|
|
1209
|
+
setChecking(true);
|
|
1210
|
+
const blocked = notReadyMessage(await scraperStatus().catch(() => null));
|
|
1211
|
+
setChecking(false);
|
|
1212
|
+
if (blocked) {
|
|
1213
|
+
setProblem(`${blocked} Open the Scan page to finish setting the scanner up.`);
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
try {
|
|
1217
|
+
await beginScrape('resume', { id: person.id });
|
|
1218
|
+
} catch (e) {
|
|
1219
|
+
setProblem(e.message || 'Could not start the scan.');
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
const off = Boolean(busy) || checking;
|
|
1223
|
+
return (
|
|
1224
|
+
<>
|
|
1225
|
+
<button type="button" onClick={go} disabled={off} style={{
|
|
1226
|
+
...primary, border: 'none', cursor: off ? 'not-allowed' : 'pointer', opacity: off ? 0.45 : 1,
|
|
1227
|
+
}}>Resume from page {stopped.nextPage}</button>
|
|
1228
|
+
{(busy || problem) && (
|
|
1229
|
+
<div style={{ fontSize: 10.5, color: problem ? '#ff8080' : 'var(--sd-fg-3, #888)', marginTop: 8 }}>
|
|
1230
|
+
{problem || `${busy}. One scan at a time: this one can start when it finishes.`}
|
|
1231
|
+
</div>
|
|
1232
|
+
)}
|
|
1233
|
+
</>
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1164
1237
|
const crumb = { background: 'none', border: 'none', color: '#8fb8d6', fontSize: 11, fontWeight: 600, cursor: 'pointer', padding: 0 };
|
|
1165
1238
|
const note = {
|
|
1166
1239
|
fontSize: 11, padding: '6px 10px', borderRadius: 8, border: '1px solid', background: 'rgba(var(--sd-shade, 0, 0, 0), 0.6)', pointerEvents: 'auto',
|
|
@@ -34,9 +34,16 @@ export function TierGrid({ grid, counts, onChange, mode, isMobile }) {
|
|
|
34
34
|
const what = (d) => DEGREE_IS[mode]?.[d];
|
|
35
35
|
// Separation: your connections are the ways in, and all of them stay, whatever their tier.
|
|
36
36
|
const waysIn = mode === 'separation';
|
|
37
|
+
// Degrees and Separation draw no one past the 2nd degree: the 3rd, the people
|
|
38
|
+
// company scans find, is on Network Circle. Its dot says so instead of "no one
|
|
39
|
+
// yet", which read as locked (Blake, 2026-10-03: "the 3rd one is locked even
|
|
40
|
+
// though … we have 3rd degrees unlocked by now").
|
|
41
|
+
const elsewhere = (d) => d === 3 && (mode === 'degrees' || mode === 'separation');
|
|
37
42
|
const emptyTitle = (d, t) => (waysIn && d === 1
|
|
38
43
|
? 'Every connection stays a way in here, whatever their tier: the tiers choose who is ranked'
|
|
39
|
-
:
|
|
44
|
+
: elsewhere(d)
|
|
45
|
+
? `${t ? `${t}-Tier at 3rd degree` : '3rd degree'}, the people company scans find, isn't drawn here. Network Circle shows ${t ? 'them' : 'it'}.`
|
|
46
|
+
: `No ${t ? `${t}-Tier ` : 'one '}at ${DEGREE_NAMES[d]} degree yet`);
|
|
40
47
|
return (
|
|
41
48
|
<div style={{ marginBottom: 18 }}>
|
|
42
49
|
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 6 }}>
|
|
@@ -10,6 +10,7 @@ import { useState } from 'react';
|
|
|
10
10
|
import Link from 'next/link';
|
|
11
11
|
import { limitQuestion, limitNote, SAFE_LIMITS } from '../../lib/search-risk';
|
|
12
12
|
import { TIER_COLORS as THEME_TIERS } from '../../lib/themes';
|
|
13
|
+
import { inProgressSummary } from '../../lib/in-progress';
|
|
13
14
|
|
|
14
15
|
const LINE = '1px solid rgba(var(--sd-ink, 255, 255, 255), 0.1)';
|
|
15
16
|
const TIER = THEME_TIERS; // the theme's dot colours (lib/themes.js)
|
|
@@ -125,33 +126,57 @@ export function BudgetBox({ li, onSetLimits, disabled }) {
|
|
|
125
126
|
);
|
|
126
127
|
}
|
|
127
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Everyone whose list was only partly read: the Scan page's In progress card,
|
|
131
|
+
* with Resume for each and Resume all. Blake, 2026-10-03: "we need to have some
|
|
132
|
+
* sort of in progress ones in the scanner as well to see all the ones they
|
|
133
|
+
* stopped and would like to resume". It used to be folded away in Fine-tune the
|
|
134
|
+
* scanner, where nobody who had just stopped a scan would look; now it stands
|
|
135
|
+
* on its own right after step 4, and only while someone is stopped partway.
|
|
136
|
+
* Each name opens their circle on the map (/?chain=<id>, the link Watch it fill
|
|
137
|
+
* in uses), to see how far it got.
|
|
138
|
+
*/
|
|
128
139
|
export function PausedList({ paused = [], onResume, onResumeAll, disabled }) {
|
|
129
140
|
const [showAll, setShowAll] = useState(false);
|
|
130
141
|
if (!paused.length) return null;
|
|
131
|
-
const legacy = paused.filter((p) => p.legacy).length;
|
|
132
142
|
const shown = showAll ? paused : paused.slice(0, 8);
|
|
133
143
|
return (
|
|
134
|
-
<
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
144
|
+
<section aria-labelledby="in-progress-title" style={{
|
|
145
|
+
margin: '20px 0 4px', borderRadius: 14, overflow: 'hidden',
|
|
146
|
+
border: '1px solid rgba(255,215,0,0.3)', background: 'rgba(255,215,0,0.035)',
|
|
147
|
+
}}>
|
|
148
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '16px 18px 14px', borderBottom: LINE, flexWrap: 'wrap' }}>
|
|
149
|
+
<div style={{ flex: 1, minWidth: 220 }}>
|
|
150
|
+
<h2 id="in-progress-title" style={{ fontSize: 17, fontWeight: 800, margin: 0, display: 'flex', alignItems: 'center', gap: 9 }}>
|
|
151
|
+
{/* The pause sign the person card shows by a name that stopped partway (Sidebar.js StoppedPartway). */}
|
|
152
|
+
<span aria-hidden="true" style={{ display: 'inline-flex', gap: 2 }}>
|
|
153
|
+
<span style={{ width: 3, height: 12, borderRadius: 1, background: 'var(--sd-gold, #FFD700)' }} />
|
|
154
|
+
<span style={{ width: 3, height: 12, borderRadius: 1, background: 'var(--sd-gold, #FFD700)' }} />
|
|
155
|
+
</span>
|
|
156
|
+
In progress
|
|
157
|
+
</h2>
|
|
158
|
+
<div style={{ fontSize: 13, color: 'var(--sd-fg-3, #8b9a9a)', marginTop: 4, lineHeight: 1.5 }}>
|
|
159
|
+
{inProgressSummary(paused)}
|
|
140
160
|
</div>
|
|
141
161
|
</div>
|
|
142
|
-
<button onClick={onResumeAll} disabled={disabled} style={{ ...btn, opacity: disabled ? 0.4 : 1, cursor: disabled ? 'not-allowed' : 'pointer', background: disabled ? 'rgba(var(--sd-ink, 255, 255, 255), 0.06)' : 'linear-gradient(135deg, #3498DB, #9B59B6)' }}>
|
|
162
|
+
<button onClick={onResumeAll} disabled={disabled} style={{ ...btn, opacity: disabled ? 0.4 : 1, cursor: disabled ? 'not-allowed' : 'pointer', background: disabled ? 'rgba(var(--sd-ink, 255, 255, 255), 0.06)' : 'linear-gradient(135deg, #3498DB, #9B59B6)', ...(disabled ? {} : { color: '#fff' }) }}>
|
|
143
163
|
Resume all
|
|
144
164
|
</button>
|
|
145
165
|
</div>
|
|
146
166
|
<div>
|
|
147
167
|
{shown.map((p) => (
|
|
148
|
-
<div key={p.id} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '
|
|
168
|
+
<div key={p.id} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '9px 18px', borderBottom: '1px solid rgba(var(--sd-ink, 255, 255, 255), 0.04)' }}>
|
|
149
169
|
<span style={{ width: 8, height: 8, borderRadius: '50%', background: TIER[p.tier] || '#667', flexShrink: 0 }} />
|
|
150
170
|
<div style={{ flex: 1, minWidth: 0 }}>
|
|
151
|
-
<div style={{ fontSize: 13, fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
171
|
+
<div style={{ fontSize: 13, fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
172
|
+
<Link href={`/?chain=${encodeURIComponent(p.id)}`} title={`See ${p.name}’s circle on the map`} style={{
|
|
173
|
+
color: 'var(--sd-fg-1, #fff)', textDecoration: 'underline', textDecorationColor: 'rgba(var(--sd-ink, 255, 255, 255), 0.35)', textUnderlineOffset: 3,
|
|
174
|
+
}}>{p.name}</Link>
|
|
175
|
+
</div>
|
|
152
176
|
<div style={{ fontSize: 11.5, color: 'var(--sd-fg-3, #8b9a9a)' }}>
|
|
153
177
|
Read to page {p.pagesRead} · carries on at {p.nextPage}
|
|
154
|
-
{
|
|
178
|
+
{/* Kept whole: on a phone "Oct" and "3" landed on two lines. */}
|
|
179
|
+
{p.at ? <> <span style={{ whiteSpace: 'nowrap' }}>· {day(p.at)}</span></> : ''}
|
|
155
180
|
{p.unclear >= 2 ? ' · came back unclear twice, so it waits at the back of Resume all' : ''}
|
|
156
181
|
</div>
|
|
157
182
|
</div>
|
|
@@ -162,11 +187,11 @@ export function PausedList({ paused = [], onResume, onResumeAll, disabled }) {
|
|
|
162
187
|
))}
|
|
163
188
|
</div>
|
|
164
189
|
{paused.length > 8 && (
|
|
165
|
-
<button onClick={() => setShowAll(!showAll)} style={{ ...linkBtn, padding: '10px
|
|
190
|
+
<button onClick={() => setShowAll(!showAll)} style={{ ...linkBtn, padding: '10px 18px' }}>
|
|
166
191
|
{showAll ? 'Show fewer' : `Show all ${paused.length}`}
|
|
167
192
|
</button>
|
|
168
193
|
)}
|
|
169
|
-
</
|
|
194
|
+
</section>
|
|
170
195
|
);
|
|
171
196
|
}
|
|
172
197
|
|
|
@@ -13,6 +13,7 @@ import { buildQuest, XP_SEND, STAGE_SIZE } from '../../lib/quest';
|
|
|
13
13
|
import { initialsFor } from '../../lib/tiers';
|
|
14
14
|
import { localPhoto } from '../../lib/photos';
|
|
15
15
|
import { TIER_COLORS as THEME_TIERS } from '../../lib/themes';
|
|
16
|
+
import ConnectChoice from './AutoConnect';
|
|
16
17
|
|
|
17
18
|
const TIER = THEME_TIERS; // the theme's dot colours (lib/themes.js)
|
|
18
19
|
const LINE = '1px solid rgba(var(--sd-ink, 255, 255, 255), 0.1)';
|
|
@@ -49,7 +50,8 @@ function Ring({ progress, size = 86, color = '#FF6B35', children }) {
|
|
|
49
50
|
);
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
|
|
53
|
+
// canAuto: Auto beside every invite (components/AutoConnect.js; Blake, 2026-10-03), on your own network.
|
|
54
|
+
export default function OutlinkQuest({ recs, sentIds, added, mappedIds, reach, onSend, onUndo, canAuto = false }) {
|
|
53
55
|
const quest = useMemo(() => buildQuest({ recs, sentIds, added, mappedIds, reach }), [recs, sentIds, added, mappedIds, reach]);
|
|
54
56
|
const [open, setOpen] = useState(null); // bridge id of the expanded cluster
|
|
55
57
|
const [toast, setToast] = useState(null);
|
|
@@ -57,6 +59,11 @@ export default function OutlinkQuest({ recs, sentIds, added, mappedIds, reach, o
|
|
|
57
59
|
|
|
58
60
|
async function send(person, cluster) {
|
|
59
61
|
await onSend(person);
|
|
62
|
+
cheer(person, cluster);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The points and the circle's progress, for an invite sent by hand or by Auto.
|
|
66
|
+
function cheer(person, cluster) {
|
|
60
67
|
const done = cluster.done + 1;
|
|
61
68
|
const first = String(cluster.bridge.name || '').split(/[\s,]+/)[0];
|
|
62
69
|
setToast(done >= cluster.targets.length
|
|
@@ -119,12 +126,17 @@ export default function OutlinkQuest({ recs, sentIds, added, mappedIds, reach, o
|
|
|
119
126
|
<div style={{ fontSize: 11.5, color: 'var(--sd-fg-2, #aab7b7)', margin: '10px 0' }}>
|
|
120
127
|
via <b style={{ color: TIER[cluster.bridge.tier] }}>{cluster.bridge.name}</b> · their circle {cluster.done}/{cluster.targets.length} this stage
|
|
121
128
|
</div>
|
|
122
|
-
<
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
129
|
+
<ConnectChoice person={person} canAuto={canAuto} size="quest" onSent={(p) => cheer(p, cluster)} connect={(second) => {
|
|
130
|
+
const by = (
|
|
131
|
+
<>
|
|
132
|
+
{person.profile_url && (
|
|
133
|
+
<a href={person.profile_url} target="_blank" rel="noopener noreferrer" style={btnGhost}>Open on LinkedIn</a>
|
|
134
|
+
)}
|
|
135
|
+
<button onClick={() => send(person, cluster)} style={second ? btnGhost : btnHot}>I sent an invite · +{XP_SEND[person.tier] || 2}</button>
|
|
136
|
+
</>
|
|
137
|
+
);
|
|
138
|
+
return second ? by : <div style={{ display: 'flex', gap: 8 }}>{by}</div>;
|
|
139
|
+
}} />
|
|
128
140
|
</div>
|
|
129
141
|
))}
|
|
130
142
|
</div>
|
|
@@ -186,10 +198,19 @@ export default function OutlinkQuest({ recs, sentIds, added, mappedIds, reach, o
|
|
|
186
198
|
<div style={{ fontWeight: 600, fontSize: 13 }}>{p.name} <span style={{ color: TIER[p.tier], fontSize: 11 }}>{p.tier}</span></div>
|
|
187
199
|
<div style={{ fontSize: 11.5, color: 'var(--sd-fg-3, #8b9a9a)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{p.headline}</div>
|
|
188
200
|
</div>
|
|
189
|
-
{
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
201
|
+
{sent ? (
|
|
202
|
+
<>
|
|
203
|
+
{p.profile_url && <a href={p.profile_url} target="_blank" rel="noopener noreferrer" style={btnGhost}>LinkedIn</a>}
|
|
204
|
+
<button onClick={() => onUndo(p)} style={{ ...btnGhost, color: 'var(--sd-green, #00ff88)' }}>Sent ✓ (undo)</button>
|
|
205
|
+
</>
|
|
206
|
+
) : (
|
|
207
|
+
<ConnectChoice person={p} canAuto={canAuto} size="quest" onSent={(x) => cheer(x, expanded)} connect={(second) => (
|
|
208
|
+
<>
|
|
209
|
+
{p.profile_url && <a href={p.profile_url} target="_blank" rel="noopener noreferrer" style={btnGhost}>LinkedIn</a>}
|
|
210
|
+
<button onClick={() => send(p, expanded)} style={second ? btnGhost : btnHot}>Sent · +{XP_SEND[p.tier] || 2}</button>
|
|
211
|
+
</>
|
|
212
|
+
)} />
|
|
213
|
+
)}
|
|
193
214
|
</div>
|
|
194
215
|
);
|
|
195
216
|
})}
|