six-degrees 0.4.10 → 0.5.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 +9 -9
- 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 +13 -13
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +12 -12
- 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 +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 +1 -1
- package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/connections/route.js +1 -1
- package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/delete-cluster/route.js +1 -1
- package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/ingest/route.js +1 -1
- package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/route.js +1 -1
- package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/notifications/route.js +1 -1
- package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/outreach/route.js +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 +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 +3 -2
- package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/route.js +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 +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 +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/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/social/crm/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/social/crm/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/social/crm/route.js +10 -0
- package/.next/standalone/.next/server/app/api/social/crm/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/social/crm/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/social/crm/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/social/messages/route.js +4 -3
- 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 +4 -3
- 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 +15 -15
- package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +15 -15
- 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 +14 -14
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/launch.html +2 -2
- package/.next/standalone/.next/server/app/launch.rsc +15 -15
- package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +15 -15
- 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 +14 -14
- 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 +15 -15
- package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +15 -15
- 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 +14 -14
- 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 +15 -15
- package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +15 -15
- 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 +14 -14
- 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 +15 -15
- package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +15 -15
- 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 +14 -14
- 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 +15 -15
- package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +15 -15
- 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 +14 -14
- 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 +15 -15
- package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +15 -15
- 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 +14 -14
- 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 +15 -15
- package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +15 -15
- 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 +14 -14
- 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 +15 -15
- package/.next/standalone/.next/server/app/social.segments/_full.segment.rsc +15 -15
- 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 +14 -14
- package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0bat8nx._.js → [root-of-the-server]__05_3-22._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t3v1a1._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__06mi9k2._.js → [root-of-the-server]__13dx4sd._.js} +7 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__15wc9e-._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1kbl21f._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tir0vc._.js +8 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__20rwfdw._.js +8 -0
- package/.next/standalone/.next/server/chunks/_072jl0y._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0ea9qwy._.js +1 -1
- 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/_1iwkh8t._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1oqfuh8._.js +3 -0
- package/.next/standalone/.next/server/chunks/_1xh07bp._.js +1 -1
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_social_crm_route_actions_0d1aflv.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_0851sk0._.js +8 -0
- package/.next/standalone/.next/server/chunks/lib_1q7p2ro._.js +8 -0
- package/.next/standalone/.next/server/chunks/lib_1r6l86l._.js +8 -0
- package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +8 -8
- package/.next/standalone/.next/server/chunks/lib_linkedin-export_12imqxg.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_04s8mzr.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_053z7as.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0b_-2te.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0dhbxdm.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0dxlk31.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0f8b-qc.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0pzie7_.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0r76fq_.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0ut7pjp.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_15zyqaz.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_18zpjtq.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1a8xkiz.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1e_55xo.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1oxfd1_.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1s0lod0.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_scoring_1-e4ur0.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_01kacen._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_03l7vge._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_1ap1tu_._.js → _0gtv9jb._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_0jdkqgi._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0xi-fbs._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0xmwmfv._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1-b9hj7._.js +9 -0
- package/.next/standalone/.next/server/chunks/ssr/_112ewem._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_16ab63q._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1fxm0-s._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1ra1d3r._.js +9 -0
- package/.next/standalone/.next/server/chunks/ssr/app_141w189._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +22 -10
- 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 -5
- 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 +19 -3
- 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 +1 -1
- package/.next/standalone/.next/server/chunks/ssr/lib_13rapwj._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_galaxy-lab_108_lu-.js +3 -0
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/middleware-manifest.json +5 -5
- package/.next/standalone/.next/server/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/03bziijw5srol.js +1 -0
- package/.next/standalone/.next/static/chunks/0bgq8wmopl_ig.js +7 -0
- package/.next/standalone/.next/static/chunks/0ezus6p2eb-vg.js +1 -0
- package/.next/standalone/.next/static/chunks/0g_fy2034bp0x.js +34 -0
- package/.next/standalone/.next/static/chunks/0q5dru4q6gxl5.js +22 -0
- package/.next/standalone/.next/static/chunks/0w-byiwb2zax8.js +2 -0
- package/.next/standalone/.next/static/chunks/1g7jcovdejc9-.js +1 -0
- package/.next/standalone/.next/static/chunks/1i51yylxdzht1.js +1 -0
- package/.next/{static/chunks/1xptlvtcz6654.css → standalone/.next/static/chunks/1x_ysly42-teq.css} +1 -1
- package/.next/standalone/.next/static/chunks/1zf8swaasjf1e.js +43 -0
- package/.next/standalone/.next/static/chunks/2rqtpccruavf2.js +1 -0
- package/.next/standalone/.next/static/chunks/2wgz9ietkqqe6.js +1 -0
- package/.next/standalone/.next/static/chunks/30x3oq_st7a5r.js +1 -0
- package/.next/standalone/.next/static/chunks/355ryio1b7sm1.js +1 -0
- package/.next/standalone/.next/static/chunks/3ap5hgma7n0o7.js +1 -0
- package/.next/standalone/.next/static/chunks/3fci6qi39-1n5.js +1 -0
- package/.next/standalone/.next/static/chunks/{24tvdun1cb3g1.js → 3sowbvbaj1s6n.js} +1 -1
- package/.next/standalone/CHANGELOG.md +235 -0
- package/.next/standalone/app/api/data/export/route.js +2 -1
- package/.next/standalone/app/api/ingest/route.js +40 -1
- package/.next/standalone/app/api/scraper/route.js +12 -2
- package/.next/standalone/app/api/settings/sector-preview/route.js +2 -1
- package/.next/standalone/app/api/social/crm/route.js +37 -0
- package/.next/standalone/app/api/social/messages/route.js +44 -8
- package/.next/standalone/app/api/social/route.js +80 -22
- package/.next/standalone/app/components/AppTabs.js +59 -0
- package/.next/standalone/app/components/ChainView.js +303 -89
- package/.next/standalone/app/components/CirclePanel.js +162 -0
- package/.next/standalone/app/components/CompanyScores.js +2 -2
- package/.next/standalone/app/components/FieldStep.js +4 -4
- package/.next/standalone/app/components/FilterPanel.js +121 -183
- package/.next/standalone/app/components/ForceGraph.js +41 -3
- package/.next/standalone/app/components/GalaxyLab.js +26 -31
- package/.next/standalone/app/components/LegacyScoresCard.js +2 -2
- package/.next/standalone/app/components/MapControls.js +154 -0
- package/.next/standalone/app/components/NoteDetail.js +118 -0
- package/.next/standalone/app/components/PathsAnalyzer.js +147 -110
- package/.next/standalone/app/components/PeopleMap.js +308 -0
- package/.next/standalone/app/components/ScanRadar.js +129 -0
- package/.next/standalone/app/components/ScanStatusBar.js +72 -27
- package/.next/standalone/app/components/SeparationView.js +626 -118
- package/.next/standalone/app/components/Sidebar.js +28 -2
- package/.next/standalone/app/components/settings/DataSection.js +12 -3
- package/.next/standalone/app/components/settings/NetworkHealthSection.js +1 -1
- package/.next/standalone/app/components/settings/SectorSection.js +2 -2
- package/.next/standalone/app/components/settings/TierSection.js +1 -1
- package/.next/standalone/app/components/settings/TitleSection.js +149 -0
- package/.next/standalone/app/components/views.js +15 -14
- package/.next/standalone/app/globals.css +11 -0
- package/.next/standalone/app/page.js +89 -156
- package/.next/standalone/app/paths/page.js +39 -27
- package/.next/standalone/app/profile/page.js +33 -5
- package/.next/standalone/app/queue/page.js +56 -34
- package/.next/standalone/app/scores/page.js +16 -66
- package/.next/standalone/app/settings/page.js +27 -13
- package/.next/standalone/app/setup/page.js +298 -205
- package/.next/standalone/app/social/Crm.js +683 -0
- package/.next/standalone/app/social/SocialHub.js +374 -0
- package/.next/standalone/app/social/Thread.js +90 -0
- package/.next/standalone/app/social/page.js +10 -351
- package/.next/standalone/lib/chain-layout.js +105 -35
- package/.next/standalone/lib/data-export.js +10 -4
- package/.next/standalone/lib/data-folder.js +9 -0
- package/.next/standalone/lib/data-import.js +13 -3
- package/.next/standalone/lib/degrees.js +0 -6
- package/.next/standalone/lib/dot-rings.js +25 -0
- package/.next/standalone/lib/galaxy-lab.js +65 -6
- package/.next/standalone/lib/ingest.js +2 -0
- package/.next/standalone/lib/island.js +40 -0
- package/.next/standalone/lib/linkedin-export.js +267 -46
- package/.next/standalone/lib/linkedin-limits.js +6 -2
- package/.next/standalone/lib/map-heat.js +20 -0
- package/.next/standalone/lib/network-health.js +1 -1
- package/.next/standalone/lib/notifications.js +121 -0
- package/.next/standalone/lib/people-map.js +82 -0
- package/.next/standalone/lib/rarity.js +45 -3
- package/.next/standalone/lib/reach.js +7 -6
- package/.next/standalone/lib/rpc.js +15 -4
- package/.next/standalone/lib/scan-pace.js +46 -0
- package/.next/standalone/lib/scoring.js +39 -8
- package/.next/standalone/lib/scraper-client.js +19 -6
- package/.next/standalone/lib/sector-focus.js +4 -4
- package/.next/standalone/lib/separation.js +137 -10
- package/.next/standalone/lib/settings-effects.js +20 -11
- package/.next/standalone/lib/settings.js +3 -0
- package/.next/standalone/lib/social-crm.js +415 -0
- package/.next/standalone/lib/social-store.js +13 -6
- package/.next/standalone/lib/tier-grid.js +99 -0
- package/.next/standalone/lib/title-ranking.js +83 -0
- package/.next/standalone/package-lock.json +2 -2
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/scripts/scrape.py +177 -35
- package/.next/static/chunks/03bziijw5srol.js +1 -0
- package/.next/static/chunks/0bgq8wmopl_ig.js +7 -0
- package/.next/static/chunks/0ezus6p2eb-vg.js +1 -0
- package/.next/static/chunks/0g_fy2034bp0x.js +34 -0
- package/.next/static/chunks/0q5dru4q6gxl5.js +22 -0
- package/.next/static/chunks/0w-byiwb2zax8.js +2 -0
- package/.next/static/chunks/1g7jcovdejc9-.js +1 -0
- package/.next/static/chunks/1i51yylxdzht1.js +1 -0
- package/.next/{standalone/.next/static/chunks/1xptlvtcz6654.css → static/chunks/1x_ysly42-teq.css} +1 -1
- package/.next/static/chunks/1zf8swaasjf1e.js +43 -0
- package/.next/static/chunks/2rqtpccruavf2.js +1 -0
- package/.next/static/chunks/2wgz9ietkqqe6.js +1 -0
- package/.next/static/chunks/30x3oq_st7a5r.js +1 -0
- package/.next/static/chunks/355ryio1b7sm1.js +1 -0
- package/.next/static/chunks/3ap5hgma7n0o7.js +1 -0
- package/.next/static/chunks/3fci6qi39-1n5.js +1 -0
- package/.next/static/chunks/{24tvdun1cb3g1.js → 3sowbvbaj1s6n.js} +1 -1
- package/CHANGELOG.md +235 -0
- package/lib/chain-layout.js +105 -35
- package/lib/data-export.js +10 -4
- package/lib/data-folder.js +9 -0
- package/lib/data-import.js +13 -3
- package/lib/degrees.js +0 -6
- package/lib/dot-rings.js +25 -0
- package/lib/galaxy-lab.js +65 -6
- package/lib/ingest.js +2 -0
- package/lib/island.js +40 -0
- package/lib/linkedin-export.js +267 -46
- package/lib/linkedin-limits.js +6 -2
- package/lib/map-heat.js +20 -0
- package/lib/network-health.js +1 -1
- package/lib/notifications.js +121 -0
- package/lib/people-map.js +82 -0
- package/lib/rarity.js +45 -3
- package/lib/reach.js +7 -6
- package/lib/rpc.js +15 -4
- package/lib/scan-pace.js +46 -0
- package/lib/scoring.js +39 -8
- package/lib/scraper-client.js +19 -6
- package/lib/sector-focus.js +4 -4
- package/lib/separation.js +137 -10
- package/lib/settings-effects.js +20 -11
- package/lib/settings.js +3 -0
- package/lib/social-crm.js +415 -0
- package/lib/social-store.js +13 -6
- package/lib/tier-grid.js +99 -0
- package/lib/title-ranking.js +83 -0
- package/package.json +1 -1
- package/scripts/scrape.py +177 -35
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__08f5awg._.js +0 -8
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__17hjtzl._.js +0 -8
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__20tmem7._.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_18cr1gi._.js +0 -8
- package/.next/standalone/.next/server/chunks/lib_rpc_03i8xef.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_08fc3rp.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_08r2t_q.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0btpv1t.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0c9d9ti.js +0 -8
- package/.next/standalone/.next/server/chunks/lib_rpc_0cm2ev3.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0iovswz.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0is8flp.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0v8ajtd.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0xgmvvt.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_144mzbf.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1bggi0n.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1dkcu9f.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1o0jb6w.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0c_y09g._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0ezj4e0._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0j4-lc3._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0l8cxmo._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0xkur4p._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_14iv_zy._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_15x6q32._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1errbuf._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_212xw8q._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/app_components_19c2d9c._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/app_scores_page_09wepvv.js +0 -3
- package/.next/standalone/.next/static/chunks/07dyt47mp4s9-.js +0 -1
- package/.next/standalone/.next/static/chunks/0a5iadnmt9u9k.js +0 -1
- package/.next/standalone/.next/static/chunks/160wjb73ddwqd.js +0 -6
- package/.next/standalone/.next/static/chunks/1c_n0973t07l0.js +0 -2
- package/.next/standalone/.next/static/chunks/1e1n0kkz-psdb.js +0 -1
- package/.next/standalone/.next/static/chunks/206rm6j42gl_a.js +0 -1
- package/.next/standalone/.next/static/chunks/236f0jtp68j_h.js +0 -1
- package/.next/standalone/.next/static/chunks/2coxzjm_k6ilr.js +0 -1
- package/.next/standalone/.next/static/chunks/2kfhc58df2xyo.js +0 -1
- package/.next/standalone/.next/static/chunks/2pw6_osz1n_te.js +0 -1
- package/.next/standalone/.next/static/chunks/39hr8d-b2ibtw.js +0 -31
- package/.next/standalone/.next/static/chunks/3zw8bv7-mc4n6.js +0 -33
- package/.next/standalone/app/components/OrbitGraph.js +0 -737
- package/.next/standalone/app/social/Conversations.js +0 -246
- package/.next/static/chunks/07dyt47mp4s9-.js +0 -1
- package/.next/static/chunks/0a5iadnmt9u9k.js +0 -1
- package/.next/static/chunks/160wjb73ddwqd.js +0 -6
- package/.next/static/chunks/1c_n0973t07l0.js +0 -2
- package/.next/static/chunks/1e1n0kkz-psdb.js +0 -1
- package/.next/static/chunks/206rm6j42gl_a.js +0 -1
- package/.next/static/chunks/236f0jtp68j_h.js +0 -1
- package/.next/static/chunks/2coxzjm_k6ilr.js +0 -1
- package/.next/static/chunks/2kfhc58df2xyo.js +0 -1
- package/.next/static/chunks/2pw6_osz1n_te.js +0 -1
- package/.next/static/chunks/39hr8d-b2ibtw.js +0 -31
- package/.next/static/chunks/3zw8bv7-mc4n6.js +0 -33
- /package/.next/standalone/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_ssgManifest.js +0 -0
- /package/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_buildManifest.js +0 -0
- /package/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
// The Social tab's CRM: everyone you've been in touch with on LinkedIn, one
|
|
4
|
+
// contact each (lib/social-crm.js), and your own stage, tags, notes and next
|
|
5
|
+
// follow-up for them (app/api/social/crm). A list on the left with the views
|
|
6
|
+
// and a search box, the person on the right; stacked on a phone.
|
|
7
|
+
//
|
|
8
|
+
// What it reads: the Conversations list and requests (GET /api/social/messages,
|
|
9
|
+
// no words), your network (for tier, company and who can introduce you), and
|
|
10
|
+
// the CRM file. What was said shows only while Keep my messages is on (Thread).
|
|
11
|
+
// Your notes are always kept: they're yours. Nothing here goes anywhere but
|
|
12
|
+
// this computer; the CSV is made in this window and saved where you choose.
|
|
13
|
+
|
|
14
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
15
|
+
import {
|
|
16
|
+
buildContacts, inboxOf, awaitingOf, followUpsDue, pipelineOf, sentOf, receivedOf, suggestedStage, matchesContact,
|
|
17
|
+
crmCsv, localDay, emptyCrm, STAGES, stageLabel, AWAIT_DAYS,
|
|
18
|
+
} from '../../lib/social-crm';
|
|
19
|
+
import { keyFor } from '../../lib/separation';
|
|
20
|
+
import { Body, LINE } from '../components/ui';
|
|
21
|
+
import Thread from './Thread';
|
|
22
|
+
|
|
23
|
+
const TIER = { S: '#FFD700', A: '#9B59B6', B: '#3498DB', C: '#95A5A6', D: '#BDC3C7' };
|
|
24
|
+
const STAGE_COLOUR = { new: '#8b9a9a', contacted: '#3498DB', replied: '#00d4aa', meeting: '#ff9f43', won: '#FFD700', 'not-now': '#667' };
|
|
25
|
+
const LIST_PAGE = 100;
|
|
26
|
+
const DAY = 86400000;
|
|
27
|
+
const day = (t) => (t ? new Date(t).toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric' }) : '');
|
|
28
|
+
const ymd = (s) => (s ? new Date(`${s}T12:00:00`).toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric' }) : '');
|
|
29
|
+
// Only LinkedIn's own pages go behind "Open on LinkedIn".
|
|
30
|
+
const linkedin = (u) => (/^https:\/\/(www\.)?linkedin\.com\//.test(String(u || '')) ? u : null);
|
|
31
|
+
const plural = (n, one, many = `${one}s`) => `${n.toLocaleString()} ${n === 1 ? one : many}`;
|
|
32
|
+
|
|
33
|
+
const VIEWS = [
|
|
34
|
+
['inbox', 'Inbox'],
|
|
35
|
+
['awaiting', 'Awaiting reply'],
|
|
36
|
+
['due', 'Follow-ups due'],
|
|
37
|
+
['pipeline', 'Pipeline'],
|
|
38
|
+
['sent', 'Sent'],
|
|
39
|
+
['received', 'Received'],
|
|
40
|
+
['all', 'All'],
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const chip = (bg, color = '#dfe8e8') => ({ fontSize: 10.5, padding: '1px 6px', borderRadius: 4, background: bg, color, whiteSpace: 'nowrap' });
|
|
44
|
+
const btn = { background: 'rgba(255,255,255,0.06)', border: LINE, borderRadius: 6, color: '#cfe6f7', cursor: 'pointer', fontSize: 12, padding: '4px 10px' };
|
|
45
|
+
const field = { padding: '6px 10px', borderRadius: 6, border: LINE, background: 'rgba(0,0,0,0.3)', color: '#fff', fontSize: 13 };
|
|
46
|
+
|
|
47
|
+
/** A download made in this window, as the app's other exports are (lib/galaxy-export.js). */
|
|
48
|
+
function save(text, name) {
|
|
49
|
+
const url = URL.createObjectURL(new Blob([text], { type: 'text/csv;charset=utf-8' }));
|
|
50
|
+
const a = document.createElement('a');
|
|
51
|
+
a.href = url;
|
|
52
|
+
a.download = name;
|
|
53
|
+
// Anything appended to <body> is position: fixed (TRAPS §29). Gone at once.
|
|
54
|
+
a.style.position = 'fixed';
|
|
55
|
+
a.style.left = '-9999px';
|
|
56
|
+
document.body.appendChild(a);
|
|
57
|
+
a.click();
|
|
58
|
+
a.remove();
|
|
59
|
+
setTimeout(() => URL.revokeObjectURL(url), 60000);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export default function Crm({ net, asOf, keep, onKeep, switching, token, crmToken }) {
|
|
63
|
+
const [data, setData] = useState(null);
|
|
64
|
+
const [crm, setCrm] = useState(null);
|
|
65
|
+
const [view, setView] = useState('inbox');
|
|
66
|
+
const [q, setQ] = useState('');
|
|
67
|
+
const [hits, setHits] = useState(null);
|
|
68
|
+
const [picked, setPicked] = useState(null); // { type: 'person', key } | { type: 'group', id }
|
|
69
|
+
const [shown, setShown] = useState(LIST_PAGE);
|
|
70
|
+
const [note, setNote] = useState(null);
|
|
71
|
+
const panel = useRef(null);
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
let live = true;
|
|
75
|
+
fetch('/api/social/messages').then((r) => r.json()).then((d) => { if (live) setData(d); })
|
|
76
|
+
.catch(() => { if (live) setData({ conversations: [], invitations: [], names: {}, inviteNotes: {} }); });
|
|
77
|
+
return () => { live = false; };
|
|
78
|
+
}, [token, keep]);
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
let live = true;
|
|
81
|
+
fetch('/api/social/crm').then((r) => r.json()).then((d) => { if (live) setCrm(d.crm || emptyCrm()); })
|
|
82
|
+
.catch(() => { if (live) setCrm(emptyCrm()); });
|
|
83
|
+
return () => { live = false; };
|
|
84
|
+
}, [crmToken]);
|
|
85
|
+
|
|
86
|
+
// Searching the words happens where they're kept, a moment after typing stops.
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
const words = q.trim();
|
|
89
|
+
if (!keep || words.length < 2) return undefined;
|
|
90
|
+
let live = true;
|
|
91
|
+
const t = setTimeout(() => {
|
|
92
|
+
fetch(`/api/social/messages?q=${encodeURIComponent(words)}`).then((r) => r.json())
|
|
93
|
+
.then((d) => { if (live) setHits({ q: words, ids: new Set(d.ids || []) }); }).catch(() => {});
|
|
94
|
+
}, 250);
|
|
95
|
+
return () => { live = false; clearTimeout(t); };
|
|
96
|
+
}, [q, keep, token]);
|
|
97
|
+
|
|
98
|
+
const pickedKey = picked?.type === 'person' ? picked.key : null;
|
|
99
|
+
const { contacts, groups } = useMemo(() => buildContacts({
|
|
100
|
+
conversations: data?.conversations || [],
|
|
101
|
+
invitations: data?.invitations || [],
|
|
102
|
+
network: net || {},
|
|
103
|
+
crm: crm || emptyCrm(),
|
|
104
|
+
names: data?.names || {},
|
|
105
|
+
also: pickedKey ? [pickedKey] : [],
|
|
106
|
+
}), [data, net, crm, pickedKey]);
|
|
107
|
+
|
|
108
|
+
// Anyone's name: a contact's, else the network's, else as the export or the sync spelt it.
|
|
109
|
+
const nameOf = useMemo(() => {
|
|
110
|
+
const byKey = new Map();
|
|
111
|
+
for (const p of [...(net?.degree1 || []), ...(net?.degree2 || [])]) if (!byKey.has(keyFor(p))) byKey.set(keyFor(p), p);
|
|
112
|
+
const names = data?.names || {};
|
|
113
|
+
return (k) => contacts.get(k)?.name || byKey.get(k)?.name || names[k] || null;
|
|
114
|
+
}, [contacts, net, data]);
|
|
115
|
+
|
|
116
|
+
const awaitDays = crm?.settings?.awaitDays || AWAIT_DAYS;
|
|
117
|
+
// "Now" for a page with no data yet, and today for follow-ups: when the page opened.
|
|
118
|
+
const [opened] = useState(() => Date.now());
|
|
119
|
+
const now = asOf || opened;
|
|
120
|
+
const today = localDay(new Date(opened));
|
|
121
|
+
const people = useMemo(() => [...contacts.values()], [contacts]);
|
|
122
|
+
|
|
123
|
+
// Each view as rows of { type, key | id, sub }: what the list shows under the name.
|
|
124
|
+
const views = useMemo(() => {
|
|
125
|
+
const person = (c, sub) => ({ type: 'person', key: c.key, c, sub, t: c.lastContact || 0 });
|
|
126
|
+
const wrote = (c) => (c.lastFromThem == null ? '' : c.lastFromThem ? `They wrote ${day(c.last)}` : `You wrote ${day(c.last)}`);
|
|
127
|
+
const sentText = (s) => s.items.map((i) => (i.type === 'invite' ? `Request ${day(i.t)} · ${i.status}`
|
|
128
|
+
: i.type === 'request' ? `Tracked request · ${i.status}` : `No reply for ${plural(i.waited, 'day')}`)).join(' · ');
|
|
129
|
+
// In All, the most telling thing about each: the last word, else a request, else your stage.
|
|
130
|
+
const gist = (c) => wrote(c)
|
|
131
|
+
|| (c.invitesOut[0] ? `You asked to connect ${day(c.invitesOut[0].t)}` : '')
|
|
132
|
+
|| (c.invitesIn[0] ? `Asked to connect ${day(c.invitesIn[0].t)}` : '')
|
|
133
|
+
|| (c.request ? `Tracked request · ${c.request.status}` : '')
|
|
134
|
+
|| (c.crm?.stage ? stageLabel(c.crm.stage) : '');
|
|
135
|
+
const all = people.filter((c) => c.lastContact || c.crm || c.request).map((c) => person(c, gist(c)));
|
|
136
|
+
for (const g of groups) all.push({ type: 'group', id: g.id, g, sub: g.last ? `${g.lastFromThem ? 'Someone wrote' : 'You wrote'} ${day(g.last)}` : '', t: g.last || 0 });
|
|
137
|
+
all.sort((a, b) => b.t - a.t);
|
|
138
|
+
return {
|
|
139
|
+
inbox: inboxOf(people).map((c) => person(c, c.unread > 0 ? `${c.unread} unread · ${wrote(c)}` : wrote(c))),
|
|
140
|
+
awaiting: awaitingOf(people, now, awaitDays).map((a) => person(a.contact, `You wrote ${day(a.contact.last)}, no reply for ${plural(a.waited, 'day')}`)),
|
|
141
|
+
due: followUpsDue(people, today).map((c) => person(c, c.crm.followUp === today ? 'Follow up today' : `Follow up was due ${ymd(c.crm.followUp)}`)),
|
|
142
|
+
pipeline: pipelineOf(people),
|
|
143
|
+
sent: sentOf(people, now, awaitDays).map((s) => person(s.contact, sentText(s))),
|
|
144
|
+
received: receivedOf(people).map((r) => person(r.contact, `Asked to connect ${day(r.t)}`)),
|
|
145
|
+
all,
|
|
146
|
+
};
|
|
147
|
+
}, [people, groups, now, awaitDays, today]);
|
|
148
|
+
|
|
149
|
+
const said = keep && hits?.q === q.trim() ? hits.ids : null;
|
|
150
|
+
const words = q.trim().toLowerCase();
|
|
151
|
+
const rows = useMemo(() => {
|
|
152
|
+
const list = view === 'pipeline' ? [] : views[view];
|
|
153
|
+
if (!words) return list;
|
|
154
|
+
const found = list.filter((r) => (r.type === 'person'
|
|
155
|
+
? matchesContact(r.c, words, said)
|
|
156
|
+
: String(r.g.title || '').toLowerCase().includes(words) || r.g.people.some((k) => String(nameOf(k) || '').toLowerCase().includes(words)) || said?.has(r.g.id)));
|
|
157
|
+
// In All, anyone in your network matches too, so you can add a note to
|
|
158
|
+
// someone you haven't been in touch with yet.
|
|
159
|
+
if (view === 'all' && words.length >= 2 && net) {
|
|
160
|
+
const have = new Set(found.map((r) => r.key));
|
|
161
|
+
const seen = new Set();
|
|
162
|
+
for (const p of [...(net.degree1 || []), ...(net.degree2 || [])]) {
|
|
163
|
+
const k = keyFor(p);
|
|
164
|
+
if (have.has(k) || seen.has(k)) continue;
|
|
165
|
+
if (![p.name, p.company, p.role, p.headline].some((x) => String(x || '').toLowerCase().includes(words))) continue;
|
|
166
|
+
seen.add(k);
|
|
167
|
+
found.push({ type: 'person', key: k, c: null, row: p, sub: 'Nothing on file yet', t: 0 });
|
|
168
|
+
if (seen.size >= 50) break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return found;
|
|
172
|
+
}, [views, view, words, said, net, nameOf]);
|
|
173
|
+
|
|
174
|
+
const pipeline = useMemo(() => {
|
|
175
|
+
if (view !== 'pipeline') return null;
|
|
176
|
+
const out = {};
|
|
177
|
+
for (const [id, list] of Object.entries(views.pipeline)) out[id] = words ? list.filter((c) => matchesContact(c, words, said)) : list;
|
|
178
|
+
return out;
|
|
179
|
+
}, [views, view, words, said]);
|
|
180
|
+
|
|
181
|
+
const counts = {
|
|
182
|
+
inbox: views.inbox.length,
|
|
183
|
+
awaiting: views.awaiting.length,
|
|
184
|
+
due: views.due.length,
|
|
185
|
+
pipeline: Object.values(views.pipeline).reduce((n, l) => n + l.length, 0),
|
|
186
|
+
sent: views.sent.length,
|
|
187
|
+
received: views.received.length,
|
|
188
|
+
all: views.all.length,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
function pick(p) {
|
|
192
|
+
setPicked(p);
|
|
193
|
+
// Stacked (a phone): the person's panel is below the list, so go there.
|
|
194
|
+
if (typeof window !== 'undefined' && window.matchMedia?.('(max-width: 760px)').matches) {
|
|
195
|
+
setTimeout(() => panel.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }), 50);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function change(key, set) {
|
|
200
|
+
const r = await fetch('/api/social/crm', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ key, set }) });
|
|
201
|
+
const d = await r.json().catch(() => ({}));
|
|
202
|
+
if (!r.ok) throw new Error(d.error || 'That couldn’t be saved.');
|
|
203
|
+
setCrm((cur) => {
|
|
204
|
+
const next = { ...(cur || emptyCrm()), people: { ...(cur?.people || {}) } };
|
|
205
|
+
if (d.entry) next.people[key] = d.entry;
|
|
206
|
+
else delete next.people[key];
|
|
207
|
+
return next;
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function setAwaitDays(n) {
|
|
212
|
+
const r = await fetch('/api/social/crm', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ settings: { awaitDays: n } }) });
|
|
213
|
+
const d = await r.json().catch(() => ({}));
|
|
214
|
+
if (r.ok) setCrm((cur) => ({ ...(cur || emptyCrm()), settings: d.settings }));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Your notes can outlive Forget it (it asks); this is the way to delete them on their own.
|
|
218
|
+
async function deleteNotes() {
|
|
219
|
+
if (!window.confirm('Delete your CRM: every stage, tag, note and follow-up date you added? This can’t be undone.')) return;
|
|
220
|
+
await fetch('/api/social/crm', { method: 'DELETE' });
|
|
221
|
+
setCrm(emptyCrm());
|
|
222
|
+
setNote('Your CRM notes are deleted.');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function exportCsv() {
|
|
226
|
+
const list = people.filter((c) => c.kind !== 'sponsored').sort((a, b) => (b.lastContact || 0) - (a.lastContact || 0));
|
|
227
|
+
const name = `Six Degrees CRM ${localDay()}.csv`;
|
|
228
|
+
save(crmCsv(list), name);
|
|
229
|
+
// The page can't see where the file went: a browser puts it in its
|
|
230
|
+
// downloads folder, the Mac app asks where to save it.
|
|
231
|
+
setNote(`Made “${name}” (${plural(list.length, 'person', 'people')}). Look for it in your Downloads folder, or wherever you chose to save it. It holds your notes: keep it private.`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const loading = !data || !crm || !net;
|
|
235
|
+
const box = { padding: '12px 14px', borderRadius: 8, border: LINE, background: 'rgba(255,255,255,0.03)', marginTop: 10 };
|
|
236
|
+
const selectedGroup = picked?.type === 'group' ? groups.find((g) => g.id === picked.id) : null;
|
|
237
|
+
const selected = pickedKey ? contacts.get(pickedKey) : null;
|
|
238
|
+
|
|
239
|
+
return (
|
|
240
|
+
<>
|
|
241
|
+
<div style={box}>
|
|
242
|
+
<label style={{ display: 'flex', gap: 8, alignItems: 'center', fontSize: 13.5, color: '#fff', cursor: switching ? 'default' : 'pointer' }}>
|
|
243
|
+
<input type="checkbox" checked={keep} disabled={switching} onChange={(e) => onKeep(e.target.checked)} />
|
|
244
|
+
<b>Keep my messages on this computer</b>
|
|
245
|
+
<span style={{ fontSize: 11.5, color: keep ? '#00ff88' : '#8b9a9a' }}>{keep ? 'on' : 'off'}</span>
|
|
246
|
+
</label>
|
|
247
|
+
<Body style={{ margin: '6px 0 0', fontSize: 12.5 }}>
|
|
248
|
+
Off unless you turn it on. When it’s on, the messages themselves, including what other people wrote to
|
|
249
|
+
you, and the notes sent with connection requests, are saved only in the app’s data folder on this
|
|
250
|
+
computer. They’re never sent anywhere, and they aren’t in “Save a copy of my
|
|
251
|
+
network”. <i>Forget it</i>, or switching this off, deletes them. With it off, the CRM still shows who,
|
|
252
|
+
when and who wrote last. Your own stages, tags, notes and follow-ups are always kept, on this computer only.
|
|
253
|
+
</Body>
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
<div className="crm-grid" style={{ marginTop: 10 }}>
|
|
257
|
+
<div className={view === 'pipeline' ? 'crm-wide' : undefined} style={{ ...box, marginTop: 0, padding: 0, minWidth: 0 }}>
|
|
258
|
+
<div role="tablist" aria-label="CRM views" style={{ display: 'flex', flexWrap: 'wrap', gap: 4, padding: '10px 10px 6px', borderBottom: LINE }}>
|
|
259
|
+
{VIEWS.map(([id, label]) => (
|
|
260
|
+
<button
|
|
261
|
+
key={id}
|
|
262
|
+
role="tab"
|
|
263
|
+
aria-selected={view === id}
|
|
264
|
+
onClick={() => { setView(id); setShown(LIST_PAGE); }}
|
|
265
|
+
style={{
|
|
266
|
+
...btn, padding: '4px 9px', fontSize: 12,
|
|
267
|
+
background: view === id ? 'rgba(52,152,219,0.28)' : 'rgba(255,255,255,0.04)',
|
|
268
|
+
color: view === id ? '#fff' : '#b8c4c4',
|
|
269
|
+
}}
|
|
270
|
+
>
|
|
271
|
+
{label} <span style={{ color: view === id ? '#cfe6f7' : '#778' }}>{loading ? '' : counts[id].toLocaleString()}</span>
|
|
272
|
+
</button>
|
|
273
|
+
))}
|
|
274
|
+
</div>
|
|
275
|
+
<div style={{ padding: '8px 10px', borderBottom: LINE, display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
|
|
276
|
+
<input
|
|
277
|
+
type="search"
|
|
278
|
+
value={q}
|
|
279
|
+
onChange={(e) => { setQ(e.target.value); setShown(LIST_PAGE); }}
|
|
280
|
+
placeholder={keep ? 'Search names, companies, tags, notes, what was said' : 'Search names, companies, tags, notes'}
|
|
281
|
+
aria-label="Search the CRM"
|
|
282
|
+
style={{ ...field, flex: '1 1 180px', minWidth: 0 }}
|
|
283
|
+
/>
|
|
284
|
+
<button onClick={exportCsv} disabled={loading} style={btn} title="A spreadsheet of everyone here, with your stages, tags, follow-ups and notes">Export CSV</button>
|
|
285
|
+
</div>
|
|
286
|
+
{view === 'awaiting' && (
|
|
287
|
+
<div style={{ padding: '8px 12px', borderBottom: LINE, fontSize: 12.5, color: '#b8c4c4', display: 'flex', gap: 6, alignItems: 'center', flexWrap: 'wrap' }}>
|
|
288
|
+
You wrote last and nothing came back for
|
|
289
|
+
<input
|
|
290
|
+
type="number" min={1} max={365} value={awaitDays} aria-label="Days with no reply"
|
|
291
|
+
onChange={(e) => { const n = Number(e.target.value); if (n >= 1 && n <= 365) setAwaitDays(n); }}
|
|
292
|
+
style={{ ...field, width: 64, padding: '3px 6px' }}
|
|
293
|
+
/>
|
|
294
|
+
days or more{asOf ? `, as of ${day(asOf)}` : ''}.
|
|
295
|
+
</div>
|
|
296
|
+
)}
|
|
297
|
+
{note && <div style={{ padding: '8px 12px', fontSize: 12.5, color: '#00ff88', borderBottom: LINE }}>{note}</div>}
|
|
298
|
+
|
|
299
|
+
{loading && <div style={{ padding: 12, fontSize: 13, color: '#8b9a9a' }}>Loading…</div>}
|
|
300
|
+
{!loading && view === 'pipeline' && <Board pipeline={pipeline} picked={pickedKey} onPick={(key) => pick({ type: 'person', key })} />}
|
|
301
|
+
{!loading && view !== 'pipeline' && rows.length === 0 && (
|
|
302
|
+
<div style={{ padding: 12, fontSize: 13, color: '#8b9a9a' }}>{words ? 'Nothing matches that.' : EMPTY[view]}</div>
|
|
303
|
+
)}
|
|
304
|
+
{!loading && view !== 'pipeline' && (
|
|
305
|
+
<div>
|
|
306
|
+
{rows.slice(0, shown).map((r) => (
|
|
307
|
+
<Row
|
|
308
|
+
key={r.type === 'person' ? `p:${r.key}` : `g:${r.id}`}
|
|
309
|
+
r={r}
|
|
310
|
+
nameOf={nameOf}
|
|
311
|
+
active={r.type === 'person' ? pickedKey === r.key : picked?.id === r.id}
|
|
312
|
+
onPick={() => pick(r.type === 'person' ? { type: 'person', key: r.key } : { type: 'group', id: r.id })}
|
|
313
|
+
/>
|
|
314
|
+
))}
|
|
315
|
+
{rows.length > shown && (
|
|
316
|
+
<button onClick={() => setShown(shown + LIST_PAGE)} style={{ width: '100%', padding: 10, background: 'none', border: 'none', color: '#3498DB', cursor: 'pointer', fontSize: 13 }}>
|
|
317
|
+
Show {Math.min(LIST_PAGE, rows.length - shown).toLocaleString()} more of {rows.length.toLocaleString()}
|
|
318
|
+
</button>
|
|
319
|
+
)}
|
|
320
|
+
</div>
|
|
321
|
+
)}
|
|
322
|
+
</div>
|
|
323
|
+
|
|
324
|
+
<div ref={panel} className={view === 'pipeline' ? 'crm-wide' : undefined} style={{ ...box, marginTop: 0, minWidth: 0, scrollMarginTop: 80 }}>
|
|
325
|
+
{!picked && <div style={{ fontSize: 13, color: '#8b9a9a' }}>Pick someone to see everything with them, and to add a stage, tags, notes or a follow-up.</div>}
|
|
326
|
+
{selected && (
|
|
327
|
+
<Person
|
|
328
|
+
key={selected.key}
|
|
329
|
+
c={selected}
|
|
330
|
+
nameOf={nameOf}
|
|
331
|
+
groups={groups}
|
|
332
|
+
keep={keep}
|
|
333
|
+
token={token}
|
|
334
|
+
inviteNotes={data?.inviteNotes || {}}
|
|
335
|
+
onChange={change}
|
|
336
|
+
onPickGroup={(id) => pick({ type: 'group', id })}
|
|
337
|
+
/>
|
|
338
|
+
)}
|
|
339
|
+
{selectedGroup && (
|
|
340
|
+
<Group g={selectedGroup} contacts={contacts} nameOf={nameOf} keep={keep} token={token} onPick={(key) => pick({ type: 'person', key })} />
|
|
341
|
+
)}
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
<div style={{ fontSize: 11.5, color: '#778', marginTop: 6 }}>
|
|
345
|
+
Everyone from your export and the live sync, connections or not; groups are listed and marked. Sponsored
|
|
346
|
+
messages are marked and kept out of the Inbox. Awaiting reply and warmth are judged as of the newest thing
|
|
347
|
+
your data knows.
|
|
348
|
+
{Object.keys(crm?.people || {}).length > 0 && (
|
|
349
|
+
<>
|
|
350
|
+
{' '}Your notes are in the app’s data folder, not in “Save a copy of my network”.{' '}
|
|
351
|
+
<button onClick={deleteNotes} style={{ background: 'none', border: 'none', color: '#ff9b9b', cursor: 'pointer', fontSize: 11.5, padding: 0 }}>Delete my CRM notes</button>
|
|
352
|
+
</>
|
|
353
|
+
)}
|
|
354
|
+
</div>
|
|
355
|
+
</>
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const EMPTY = {
|
|
360
|
+
inbox: 'No one is waiting on a reply.',
|
|
361
|
+
awaiting: 'Nobody you wrote to is overdue a reply.',
|
|
362
|
+
due: 'No follow-ups are due. Set one on anyone, and it shows here on the day.',
|
|
363
|
+
sent: 'Nothing sent yet: choose your export folder above (its Invitations.csv), or mark requests sent in the app.',
|
|
364
|
+
received: 'No requests waiting on you, as far as your export shows.',
|
|
365
|
+
all: 'Nobody here yet. Choose your export folder above, or sync your messages.',
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
function Dot({ tier }) {
|
|
369
|
+
return <span style={{ width: 8, height: 8, borderRadius: '50%', background: TIER[tier] || '#556', flexShrink: 0, display: 'inline-block' }} />;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function Badges({ c }) {
|
|
373
|
+
return (
|
|
374
|
+
<>
|
|
375
|
+
{c && !c.connection && <span style={chip('rgba(255,159,67,0.18)', '#ffc58a')}>{c.degree === 2 ? '2nd degree' : 'Not a connection'}</span>}
|
|
376
|
+
{c?.kind === 'sponsored' && <span style={chip('rgba(255,255,255,0.08)')}>Sponsored</span>}
|
|
377
|
+
{c?.kind === 'inmail' && <span style={chip('rgba(155,89,182,0.25)')}>InMail</span>}
|
|
378
|
+
{c?.crm?.stage && <span style={chip('rgba(255,255,255,0.06)', STAGE_COLOUR[c.crm.stage])}>{stageLabel(c.crm.stage)}</span>}
|
|
379
|
+
</>
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function Row({ r, nameOf, active, onPick }) {
|
|
384
|
+
let title;
|
|
385
|
+
let tier = null;
|
|
386
|
+
let extra = null;
|
|
387
|
+
if (r.type === 'person') {
|
|
388
|
+
const c = r.c;
|
|
389
|
+
title = c?.name || r.row?.name || r.key;
|
|
390
|
+
tier = c?.row?.tier || r.row?.tier;
|
|
391
|
+
extra = (
|
|
392
|
+
<>
|
|
393
|
+
<Badges c={c || { connection: r.row?.degree === 1, degree: r.row?.degree }} />
|
|
394
|
+
{c?.unread > 0 && <span style={chip('#3498DB', '#fff')}>{c.unread} unread</span>}
|
|
395
|
+
</>
|
|
396
|
+
);
|
|
397
|
+
} else {
|
|
398
|
+
title = groupLabel(r.g, nameOf);
|
|
399
|
+
extra = <span style={chip('rgba(255,255,255,0.08)')}>{r.g.unknown ? 'To someone unnamed' : 'Group'}</span>;
|
|
400
|
+
}
|
|
401
|
+
const company = r.type === 'person' ? (r.c?.row?.company || r.row?.company) : null;
|
|
402
|
+
return (
|
|
403
|
+
<div
|
|
404
|
+
role="button"
|
|
405
|
+
tabIndex={0}
|
|
406
|
+
aria-pressed={active}
|
|
407
|
+
onClick={onPick}
|
|
408
|
+
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onPick(); } }}
|
|
409
|
+
style={{ padding: '8px 12px', borderBottom: '1px solid rgba(255,255,255,0.04)', cursor: 'pointer', background: active ? 'rgba(52,152,219,0.14)' : 'none' }}
|
|
410
|
+
>
|
|
411
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 13, minWidth: 0, flexWrap: 'wrap' }}>
|
|
412
|
+
<Dot tier={tier} />
|
|
413
|
+
<b style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: '100%' }}>{title}</b>
|
|
414
|
+
{extra}
|
|
415
|
+
</div>
|
|
416
|
+
<div style={{ fontSize: 11.5, color: '#8b9a9a', marginTop: 2, paddingLeft: 14, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
417
|
+
{[company, r.sub].filter(Boolean).join(' · ')}
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function groupLabel(g, nameOf) {
|
|
424
|
+
if (g.title) return g.title;
|
|
425
|
+
if (g.unknown) return 'A conversation only you wrote in';
|
|
426
|
+
const names = g.people.map(nameOf).filter(Boolean);
|
|
427
|
+
return names.length ? `${names.slice(0, 3).join(', ')}${g.people.length > 3 ? ` +${g.people.length - 3}` : ''}` : `${g.people.length} people`;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function Board({ pipeline, picked, onPick }) {
|
|
431
|
+
return (
|
|
432
|
+
<div className="crm-board">
|
|
433
|
+
{STAGES.map((s) => (
|
|
434
|
+
<div key={s.id} style={{ minWidth: 0 }}>
|
|
435
|
+
<div style={{ fontSize: 12, fontWeight: 650, color: STAGE_COLOUR[s.id], margin: '0 0 6px', display: 'flex', justifyContent: 'space-between' }}>
|
|
436
|
+
<span>{s.label}</span>
|
|
437
|
+
<span style={{ color: '#778' }}>{pipeline[s.id].length.toLocaleString()}</span>
|
|
438
|
+
</div>
|
|
439
|
+
{pipeline[s.id].length === 0 && <div style={{ fontSize: 11.5, color: '#667', padding: '6px 0' }}>No one</div>}
|
|
440
|
+
{pipeline[s.id].slice(0, 200).map((c) => (
|
|
441
|
+
<div
|
|
442
|
+
key={c.key}
|
|
443
|
+
role="button"
|
|
444
|
+
tabIndex={0}
|
|
445
|
+
onClick={() => onPick(c.key)}
|
|
446
|
+
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onPick(c.key); } }}
|
|
447
|
+
style={{
|
|
448
|
+
padding: '7px 9px', marginBottom: 6, borderRadius: 6, border: LINE, cursor: 'pointer', fontSize: 12.5,
|
|
449
|
+
background: picked === c.key ? 'rgba(52,152,219,0.18)' : 'rgba(255,255,255,0.03)',
|
|
450
|
+
}}
|
|
451
|
+
>
|
|
452
|
+
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}><Dot tier={c.row?.tier} /><b style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{c.name}</b></div>
|
|
453
|
+
<div style={{ color: '#8b9a9a', fontSize: 11, marginTop: 2 }}>
|
|
454
|
+
{[c.row?.company, c.crm.followUp ? `Follow up ${ymd(c.crm.followUp)}` : null].filter(Boolean).join(' · ')}
|
|
455
|
+
</div>
|
|
456
|
+
</div>
|
|
457
|
+
))}
|
|
458
|
+
{pipeline[s.id].length > 200 && <div style={{ fontSize: 11, color: '#778' }}>and {(pipeline[s.id].length - 200).toLocaleString()} more (search to find them)</div>}
|
|
459
|
+
</div>
|
|
460
|
+
))}
|
|
461
|
+
</div>
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const h4 = { fontSize: 12, fontWeight: 650, color: '#b8c4c4', margin: '16px 0 6px', textTransform: 'uppercase', letterSpacing: 0.4 };
|
|
466
|
+
|
|
467
|
+
function Person({ c, nameOf, groups, keep, token, inviteNotes, onChange, onPickGroup }) {
|
|
468
|
+
const row = c.row;
|
|
469
|
+
const [open, setOpen] = useState(c.conversations[0]?.id || null);
|
|
470
|
+
const profile = linkedin(c.profileUrl);
|
|
471
|
+
const thread = linkedin(c.threadUrl);
|
|
472
|
+
const how = c.connection
|
|
473
|
+
? `A connection${row?.connected_date ? ` since ${day(Date.parse(row.connected_date))}` : ''}.`
|
|
474
|
+
: c.degree === 2 ? '2nd degree: not a connection yet.' : 'Not a connection.';
|
|
475
|
+
return (
|
|
476
|
+
<div style={{ fontSize: 13 }}>
|
|
477
|
+
<div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
|
|
478
|
+
<Dot tier={row?.tier} />
|
|
479
|
+
<h3 style={{ margin: 0, fontSize: 17 }}>{c.name}</h3>
|
|
480
|
+
{row?.tier && <span style={chip('rgba(255,255,255,0.06)', TIER[row.tier])}>{row.tier} tier</span>}
|
|
481
|
+
<Badges c={c} />
|
|
482
|
+
</div>
|
|
483
|
+
{(row?.headline || row?.company) && (
|
|
484
|
+
<div style={{ color: '#b8c4c4', marginTop: 4 }}>{row.headline || [row.role, row.company].filter(Boolean).join(' at ')}</div>
|
|
485
|
+
)}
|
|
486
|
+
<div style={{ color: '#8b9a9a', marginTop: 4, fontSize: 12.5 }}>
|
|
487
|
+
{how}
|
|
488
|
+
{c.introducers.length > 0 && <> Who can introduce you: <b style={{ color: '#dfe8e8' }}>{c.introducers.slice(0, 6).join(', ')}</b>{c.introducers.length > 6 ? ` and ${c.introducers.length - 6} more` : ''}.</>}
|
|
489
|
+
{row?.unlocked_from_name && c.connection && <> You met through <b style={{ color: '#dfe8e8' }}>{row.unlocked_from_name}</b>.</>}
|
|
490
|
+
</div>
|
|
491
|
+
<div style={{ display: 'flex', gap: 12, marginTop: 6, flexWrap: 'wrap', fontSize: 12.5 }}>
|
|
492
|
+
{profile && <a href={profile} target="_blank" rel="noreferrer" style={{ color: '#3498DB', textDecoration: 'none' }}>Profile on LinkedIn ↗</a>}
|
|
493
|
+
{thread && <a href={thread} target="_blank" rel="noreferrer" style={{ color: '#3498DB', textDecoration: 'none' }}>Conversation on LinkedIn ↗</a>}
|
|
494
|
+
</div>
|
|
495
|
+
|
|
496
|
+
<Yours c={c} onChange={onChange} />
|
|
497
|
+
|
|
498
|
+
<div style={h4}>Conversations</div>
|
|
499
|
+
{c.conversations.length === 0 && <div style={{ color: '#8b9a9a', fontSize: 12.5 }}>None one to one.</div>}
|
|
500
|
+
{c.conversations.map((conv) => (
|
|
501
|
+
<div key={conv.id} style={{ border: LINE, borderRadius: 6, marginBottom: 6, overflow: 'hidden' }}>
|
|
502
|
+
<div
|
|
503
|
+
role="button"
|
|
504
|
+
tabIndex={0}
|
|
505
|
+
aria-expanded={open === conv.id}
|
|
506
|
+
onClick={() => setOpen(open === conv.id ? null : conv.id)}
|
|
507
|
+
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setOpen(open === conv.id ? null : conv.id); } }}
|
|
508
|
+
style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap', padding: '7px 10px', cursor: 'pointer', fontSize: 12.5, background: open === conv.id ? 'rgba(255,255,255,0.04)' : 'none' }}
|
|
509
|
+
>
|
|
510
|
+
<span style={{ color: '#dfe8e8' }}>{day(conv.last) || 'No date'}</span>
|
|
511
|
+
{conv.lastFromThem != null && <span style={{ color: conv.lastFromThem ? '#ff9f43' : '#8b9a9a' }}>{conv.lastFromThem ? 'They wrote last' : 'You wrote last'}</span>}
|
|
512
|
+
{conv.count != null && <span style={{ color: '#8b9a9a' }}>{plural(conv.count, 'message')}</span>}
|
|
513
|
+
{conv.mine != null && conv.count != null && conv.mine === conv.count && <span style={chip('rgba(255,255,255,0.06)')}>Only you wrote</span>}
|
|
514
|
+
{conv.folder && conv.folder !== 'inbox' && <span style={chip('rgba(255,255,255,0.06)')}>{conv.folder}</span>}
|
|
515
|
+
{conv.kind === 'inmail' && <span style={chip('rgba(155,89,182,0.25)')}>InMail</span>}
|
|
516
|
+
{conv.kind === 'sponsored' && <span style={chip('rgba(255,255,255,0.08)')}>Sponsored</span>}
|
|
517
|
+
{conv.unread > 0 && <span style={chip('#3498DB', '#fff')}>{conv.unread} unread</span>}
|
|
518
|
+
</div>
|
|
519
|
+
{open === conv.id && <Thread key={`${conv.id}:${token}:${keep}`} id={conv.id} keep={keep} group={false} token={token} />}
|
|
520
|
+
</div>
|
|
521
|
+
))}
|
|
522
|
+
{c.groups.length > 0 && (
|
|
523
|
+
<div style={{ fontSize: 12.5, color: '#8b9a9a', marginTop: 4 }}>
|
|
524
|
+
Also in {plural(c.groups.length, 'group')}:{' '}
|
|
525
|
+
{c.groups.slice(0, 8).map((id, i) => {
|
|
526
|
+
const g = groups.find((x) => x.id === id);
|
|
527
|
+
return (
|
|
528
|
+
<span key={id}>
|
|
529
|
+
{i > 0 ? ', ' : ''}
|
|
530
|
+
<button onClick={() => onPickGroup(id)} style={{ background: 'none', border: 'none', color: '#3498DB', cursor: 'pointer', padding: 0, fontSize: 12.5 }}>{g ? groupLabel(g, nameOf) : 'a group'}</button>
|
|
531
|
+
</span>
|
|
532
|
+
);
|
|
533
|
+
})}
|
|
534
|
+
</div>
|
|
535
|
+
)}
|
|
536
|
+
|
|
537
|
+
<div style={h4}>Requests</div>
|
|
538
|
+
{c.invitesOut.length + c.invitesIn.length === 0 && !c.request && <div style={{ color: '#8b9a9a', fontSize: 12.5 }}>None on file.</div>}
|
|
539
|
+
{[...c.invitesOut, ...c.invitesIn].sort((a, b) => (b.t || 0) - (a.t || 0)).map((inv) => (
|
|
540
|
+
<div key={inv.id} style={{ fontSize: 12.5, marginBottom: 6 }}>
|
|
541
|
+
<span style={{ color: '#dfe8e8' }}>{inv.dir === 'out' ? 'You asked to connect' : 'They asked to connect'}</span>
|
|
542
|
+
<span style={{ color: '#8b9a9a' }}> · {day(inv.t) || 'no date'} · {c.connection ? 'accepted' : inv.dir === 'out' ? 'pending' : 'not accepted yet'}</span>
|
|
543
|
+
{keep && inviteNotes[inv.id] && <div style={{ marginTop: 3, padding: '6px 9px', borderRadius: 8, background: 'rgba(255,255,255,0.06)', whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' }}>{inviteNotes[inv.id]}</div>}
|
|
544
|
+
</div>
|
|
545
|
+
))}
|
|
546
|
+
{c.request && (
|
|
547
|
+
<div style={{ fontSize: 12.5 }}>
|
|
548
|
+
<span style={{ color: '#dfe8e8' }}>Request tracked in the app</span>
|
|
549
|
+
<span style={{ color: '#8b9a9a' }}> · {c.request.status}</span>
|
|
550
|
+
</div>
|
|
551
|
+
)}
|
|
552
|
+
</div>
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/** Your part: stage, follow-up, tags and notes, each saved as you change it. */
|
|
557
|
+
function Yours({ c, onChange }) {
|
|
558
|
+
const entry = c.crm || {};
|
|
559
|
+
const [notes, setNotes] = useState(entry.notes || '');
|
|
560
|
+
const [tag, setTag] = useState('');
|
|
561
|
+
const [state, setState] = useState(null);
|
|
562
|
+
const timer = useRef(null);
|
|
563
|
+
const suggested = suggestedStage(c);
|
|
564
|
+
|
|
565
|
+
async function put(set) {
|
|
566
|
+
setState('Saving…');
|
|
567
|
+
try {
|
|
568
|
+
await onChange(c.key, set);
|
|
569
|
+
setState('Saved');
|
|
570
|
+
} catch (e) {
|
|
571
|
+
setState(e.message);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
// Notes save a moment after typing stops, and when you leave the box.
|
|
575
|
+
function typed(v) {
|
|
576
|
+
setNotes(v);
|
|
577
|
+
clearTimeout(timer.current);
|
|
578
|
+
timer.current = setTimeout(() => put({ notes: v }), 800);
|
|
579
|
+
}
|
|
580
|
+
function leave() {
|
|
581
|
+
clearTimeout(timer.current);
|
|
582
|
+
if (notes !== (entry.notes || '')) put({ notes });
|
|
583
|
+
}
|
|
584
|
+
useEffect(() => () => clearTimeout(timer.current), []);
|
|
585
|
+
const addDays = (n) => localDay(new Date(Date.now() + n * DAY));
|
|
586
|
+
const tags = entry.tags || [];
|
|
587
|
+
function addTag() {
|
|
588
|
+
const t = tag.trim();
|
|
589
|
+
if (!t) return;
|
|
590
|
+
setTag('');
|
|
591
|
+
put({ tags: [...tags, ...t.split(',')] });
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
return (
|
|
595
|
+
<div style={{ marginTop: 12, padding: '10px 12px', borderRadius: 8, background: 'rgba(52,152,219,0.06)', border: '1px solid rgba(52,152,219,0.18)' }}>
|
|
596
|
+
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', alignItems: 'flex-end' }}>
|
|
597
|
+
<label style={{ fontSize: 11.5, color: '#8b9a9a', display: 'flex', flexDirection: 'column', gap: 3 }}>
|
|
598
|
+
Stage
|
|
599
|
+
<select value={entry.stage || ''} onChange={(e) => put({ stage: e.target.value || null })} style={{ ...field, padding: '5px 8px' }}>
|
|
600
|
+
<option value="">Not set (looks like {stageLabel(suggested)})</option>
|
|
601
|
+
{STAGES.map((s) => <option key={s.id} value={s.id}>{s.label}</option>)}
|
|
602
|
+
</select>
|
|
603
|
+
</label>
|
|
604
|
+
<label style={{ fontSize: 11.5, color: '#8b9a9a', display: 'flex', flexDirection: 'column', gap: 3 }}>
|
|
605
|
+
Next follow-up
|
|
606
|
+
<input type="date" value={entry.followUp || ''} onChange={(e) => put({ followUp: e.target.value || null })} style={{ ...field, padding: '4px 8px', colorScheme: 'dark' }} />
|
|
607
|
+
</label>
|
|
608
|
+
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
|
609
|
+
<button onClick={() => put({ followUp: addDays(3) })} style={btn}>In 3 days</button>
|
|
610
|
+
<button onClick={() => put({ followUp: addDays(7) })} style={btn}>In a week</button>
|
|
611
|
+
{entry.followUp && <button onClick={() => put({ followUp: null })} style={btn}>Clear</button>}
|
|
612
|
+
</div>
|
|
613
|
+
</div>
|
|
614
|
+
{entry.stage == null && suggested !== 'new' && (
|
|
615
|
+
<button onClick={() => put({ stage: suggested })} style={{ ...btn, marginTop: 8 }}>Set to {stageLabel(suggested)}</button>
|
|
616
|
+
)}
|
|
617
|
+
<div style={{ marginTop: 10, display: 'flex', gap: 6, flexWrap: 'wrap', alignItems: 'center' }}>
|
|
618
|
+
{tags.map((t) => (
|
|
619
|
+
<span key={t} style={{ ...chip('rgba(255,255,255,0.08)'), fontSize: 11.5, display: 'inline-flex', gap: 4, alignItems: 'center' }}>
|
|
620
|
+
{t}
|
|
621
|
+
<button aria-label={`Remove ${t}`} onClick={() => put({ tags: tags.filter((x) => x !== t) })} style={{ background: 'none', border: 'none', color: '#8b9a9a', cursor: 'pointer', padding: 0, fontSize: 12 }}>×</button>
|
|
622
|
+
</span>
|
|
623
|
+
))}
|
|
624
|
+
<input
|
|
625
|
+
value={tag}
|
|
626
|
+
onChange={(e) => setTag(e.target.value)}
|
|
627
|
+
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); addTag(); } }}
|
|
628
|
+
onBlur={addTag}
|
|
629
|
+
placeholder="Add a tag"
|
|
630
|
+
aria-label="Add a tag"
|
|
631
|
+
style={{ ...field, padding: '3px 8px', fontSize: 12, width: 120 }}
|
|
632
|
+
/>
|
|
633
|
+
</div>
|
|
634
|
+
<textarea
|
|
635
|
+
value={notes}
|
|
636
|
+
onChange={(e) => typed(e.target.value)}
|
|
637
|
+
onBlur={leave}
|
|
638
|
+
placeholder="Your notes on them: kept on this computer only"
|
|
639
|
+
aria-label="Notes"
|
|
640
|
+
rows={4}
|
|
641
|
+
style={{ ...field, width: '100%', marginTop: 10, resize: 'vertical', fontFamily: 'inherit', lineHeight: 1.45, boxSizing: 'border-box' }}
|
|
642
|
+
/>
|
|
643
|
+
<div style={{ fontSize: 11, color: state && state !== 'Saved' && state !== 'Saving…' ? '#ff9b9b' : '#778', marginTop: 4, minHeight: 14 }}>{state || ''}</div>
|
|
644
|
+
</div>
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function Group({ g, contacts, nameOf, keep, token, onPick }) {
|
|
649
|
+
const href = linkedin(g.threadUrl);
|
|
650
|
+
return (
|
|
651
|
+
<div style={{ fontSize: 13 }}>
|
|
652
|
+
<div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
|
|
653
|
+
<h3 style={{ margin: 0, fontSize: 17 }}>{groupLabel(g, nameOf)}</h3>
|
|
654
|
+
<span style={chip('rgba(255,255,255,0.08)')}>{g.unknown ? 'To someone unnamed' : 'Group'}</span>
|
|
655
|
+
{g.unread > 0 && <span style={chip('#3498DB', '#fff')}>{g.unread} unread</span>}
|
|
656
|
+
</div>
|
|
657
|
+
<div style={{ color: '#8b9a9a', marginTop: 4, fontSize: 12.5 }}>
|
|
658
|
+
{[g.last ? `Last active ${day(g.last)}` : null, g.count != null ? plural(g.count, 'message') : null].filter(Boolean).join(' · ')}
|
|
659
|
+
</div>
|
|
660
|
+
{href && <a href={href} target="_blank" rel="noreferrer" style={{ color: '#3498DB', textDecoration: 'none', fontSize: 12.5, display: 'inline-block', marginTop: 6 }}>Conversation on LinkedIn ↗</a>}
|
|
661
|
+
{g.people.length > 0 && (
|
|
662
|
+
<>
|
|
663
|
+
<div style={h4}>Who’s in it</div>
|
|
664
|
+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
|
|
665
|
+
{g.people.map((k) => {
|
|
666
|
+
const p = contacts.get(k);
|
|
667
|
+
return (
|
|
668
|
+
<button key={k} onClick={() => onPick(k)} style={{ ...btn, display: 'inline-flex', gap: 6, alignItems: 'center' }}>
|
|
669
|
+
<Dot tier={p?.row?.tier} />{nameOf(k) || k.split('/').pop()}
|
|
670
|
+
{p && !p.connection && <span style={{ color: '#ffc58a', fontSize: 10.5 }}>not a connection</span>}
|
|
671
|
+
</button>
|
|
672
|
+
);
|
|
673
|
+
})}
|
|
674
|
+
</div>
|
|
675
|
+
</>
|
|
676
|
+
)}
|
|
677
|
+
<div style={h4}>Messages</div>
|
|
678
|
+
<div style={{ border: LINE, borderRadius: 6, overflow: 'hidden' }}>
|
|
679
|
+
<Thread key={`${g.id}:${token}:${keep}`} id={g.id} keep={keep} group token={token} />
|
|
680
|
+
</div>
|
|
681
|
+
</div>
|
|
682
|
+
);
|
|
683
|
+
}
|