six-degrees 0.4.0-beta.1 → 0.4.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.
Files changed (355) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +5 -5
  4. package/.next/standalone/.next/required-server-files.json +6 -1
  5. package/.next/standalone/.next/routes-manifest.json +12 -1
  6. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  11. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  12. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  13. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  14. package/.next/standalone/.next/server/app/_not-found.rsc +11 -11
  15. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +11 -11
  16. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +10 -10
  17. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  18. package/.next/standalone/.next/server/app/api/admin-delete/route.js +1 -1
  19. package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -1
  20. package/.next/standalone/.next/server/app/api/admin-update/route.js +1 -1
  21. package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/bridges/route.js +1 -1
  23. package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/bulk-import/route.js +1 -1
  25. package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/connections/route.js +1 -1
  29. package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/delete-cluster/route.js +1 -1
  36. package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/ingest/route.js +1 -1
  38. package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/api/network/route.js +1 -1
  40. package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/api/notifications/route.js +1 -1
  42. package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/api/outreach/route.js +1 -1
  44. package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/api/queue/route.js +1 -1
  46. package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
  47. package/.next/standalone/.next/server/app/api/scraper/route.js +1 -1
  48. package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
  49. package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -1
  51. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/api/setup-profile/route.js +1 -1
  53. package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
  54. package/.next/standalone/.next/server/app/api/unlock/route.js +1 -1
  55. package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/api/update-images/route.js +1 -1
  58. package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
  59. package/.next/standalone/.next/server/app/api/users/route.js +1 -1
  60. package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
  61. package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
  62. package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
  63. package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
  64. package/.next/standalone/.next/server/app/import.html +1 -1
  65. package/.next/standalone/.next/server/app/import.rsc +13 -13
  66. package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +13 -13
  67. package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +2 -2
  68. package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +12 -12
  69. package/.next/standalone/.next/server/app/index.html +1 -1
  70. package/.next/standalone/.next/server/app/index.rsc +13 -13
  71. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +12 -12
  72. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +13 -13
  73. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  74. package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
  75. package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
  76. package/.next/standalone/.next/server/app/launch.html +2 -2
  77. package/.next/standalone/.next/server/app/launch.rsc +13 -13
  78. package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +13 -13
  79. package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +2 -2
  80. package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +12 -12
  81. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  82. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  83. package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
  84. package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
  85. package/.next/standalone/.next/server/app/paths.html +1 -1
  86. package/.next/standalone/.next/server/app/paths.rsc +13 -13
  87. package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +13 -13
  88. package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +2 -2
  89. package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +12 -12
  90. package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
  91. package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
  92. package/.next/standalone/.next/server/app/profile.html +1 -1
  93. package/.next/standalone/.next/server/app/profile.rsc +13 -13
  94. package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +13 -13
  95. package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +2 -2
  96. package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +12 -12
  97. package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
  98. package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
  99. package/.next/standalone/.next/server/app/queue.html +1 -1
  100. package/.next/standalone/.next/server/app/queue.rsc +13 -13
  101. package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +13 -13
  102. package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +2 -2
  103. package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +12 -12
  104. package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -1
  105. package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +1 -1
  106. package/.next/standalone/.next/server/app/scores.html +1 -1
  107. package/.next/standalone/.next/server/app/scores.rsc +13 -13
  108. package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +13 -13
  109. package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +2 -2
  110. package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +12 -12
  111. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  112. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  113. package/.next/standalone/.next/server/app/settings.html +1 -1
  114. package/.next/standalone/.next/server/app/settings.rsc +13 -13
  115. package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +13 -13
  116. package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  117. package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +12 -12
  118. package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
  119. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  120. package/.next/standalone/.next/server/app/setup.html +1 -1
  121. package/.next/standalone/.next/server/app/setup.rsc +13 -13
  122. package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +13 -13
  123. package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +2 -2
  124. package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +12 -12
  125. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-rnla2._.js +1 -1
  126. package/.next/standalone/.next/server/chunks/[root-of-the-server]__00la_y0._.js +1 -1
  127. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0i5hdu4._.js → [root-of-the-server]__03ydxxh._.js} +2 -2
  128. package/.next/standalone/.next/server/chunks/[root-of-the-server]__07t47cm._.js +1 -1
  129. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0rc27za._.js → [root-of-the-server]__0bat8nx._.js} +2 -2
  130. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cj5jqc._.js +1 -1
  131. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0e0fei3._.js +1 -1
  132. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljcioj._.js +1 -1
  133. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljnyho._.js +1 -1
  134. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o5-tcg._.js +1 -1
  135. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0qrnous._.js +1 -1
  136. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t3v1a1._.js +1 -1
  137. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0uag-k5._.js +1 -1
  138. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0um1uey._.js +1 -1
  139. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0wmexuc._.js +1 -1
  140. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xu34sz._.js +1 -1
  141. package/.next/standalone/.next/server/chunks/[root-of-the-server]__133knx1._.js +1 -1
  142. package/.next/standalone/.next/server/chunks/[root-of-the-server]__14ou7j5._.js +1 -1
  143. package/.next/standalone/.next/server/chunks/[root-of-the-server]__15-17hl._.js +1 -1
  144. package/.next/standalone/.next/server/chunks/[root-of-the-server]__16iu0hv._.js +1 -1
  145. package/.next/standalone/.next/server/chunks/[root-of-the-server]__19b7mig._.js +1 -1
  146. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1bbcwub._.js +2 -2
  147. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1eris29._.js +1 -1
  148. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1gjo_cq._.js +1 -1
  149. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1kbl21f._.js +1 -1
  150. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qcm08_._.js +1 -1
  151. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1rphsdk._.js +1 -1
  152. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1w25q0m._.js +1 -1
  153. package/.next/standalone/.next/server/chunks/_1_fynia._.js +7 -6
  154. package/.next/standalone/.next/server/chunks/_1fn30_l._.js +1 -1
  155. package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
  156. package/.next/standalone/.next/server/chunks/_1iwkh8t._.js +1 -1
  157. package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +12 -8
  158. package/.next/standalone/.next/server/chunks/{lib_rpc_02ndsg2.js → lib_rpc_03i8xef.js} +1 -1
  159. package/.next/standalone/.next/server/chunks/{lib_rpc_08q5vp6.js → lib_rpc_08fc3rp.js} +1 -1
  160. package/.next/standalone/.next/server/chunks/{lib_rpc_00tx2sm.js → lib_rpc_08r2t_q.js} +1 -1
  161. package/.next/standalone/.next/server/chunks/{lib_rpc_02-qa4d.js → lib_rpc_0btpv1t.js} +1 -1
  162. package/.next/standalone/.next/server/chunks/lib_rpc_0c9d9ti.js +5 -5
  163. package/.next/standalone/.next/server/chunks/lib_rpc_0cm2ev3.js +3 -0
  164. package/.next/standalone/.next/server/chunks/lib_rpc_0iovswz.js +3 -0
  165. package/.next/standalone/.next/server/chunks/lib_rpc_0is8flp.js +3 -0
  166. package/.next/standalone/.next/server/chunks/lib_rpc_0v8ajtd.js +3 -0
  167. package/.next/standalone/.next/server/chunks/lib_rpc_0xgmvvt.js +3 -0
  168. package/.next/standalone/.next/server/chunks/lib_rpc_144mzbf.js +3 -0
  169. package/.next/standalone/.next/server/chunks/lib_rpc_1bggi0n.js +3 -0
  170. package/.next/standalone/.next/server/chunks/lib_rpc_1dkcu9f.js +3 -0
  171. package/.next/standalone/.next/server/chunks/lib_rpc_1o0jb6w.js +3 -0
  172. package/.next/standalone/.next/server/chunks/ssr/_01f38ty._.js +1 -1
  173. package/.next/standalone/.next/server/chunks/ssr/_06w_gl4._.js +3 -0
  174. package/.next/standalone/.next/server/chunks/ssr/_0hw_qnj._.js +3 -0
  175. package/.next/standalone/.next/server/chunks/ssr/_13kzrwv._.js +3 -0
  176. package/.next/standalone/.next/server/chunks/ssr/_15xuwkv._.js +3 -0
  177. package/.next/standalone/.next/server/chunks/ssr/_1gpbbw9._.js +1 -1
  178. package/.next/standalone/.next/server/chunks/ssr/_1hgd5dp._.js +3 -0
  179. package/.next/standalone/.next/server/chunks/ssr/{_0cu6-an._.js → _1vpfyd-._.js} +2 -2
  180. package/.next/standalone/.next/server/chunks/ssr/_20w06i-._.js +3 -0
  181. package/.next/standalone/.next/server/chunks/ssr/app_components_settings_SectorPicker_1c-29qw.js +3 -0
  182. package/.next/standalone/.next/server/chunks/ssr/app_launch_page_19t0dyd.js +1 -1
  183. package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +11 -9
  184. package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +1 -1
  185. package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +1 -1
  186. package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +5 -5
  187. package/.next/standalone/.next/server/chunks/ssr/app_scores_page_09wepvv.js +1 -1
  188. package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +2 -2
  189. package/.next/standalone/.next/server/chunks/ssr/lib_1bn38gs._.js +1 -1
  190. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_client_components_server-async-storage_0t1dato.js +3 -0
  191. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  192. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  193. package/.next/standalone/.next/server/pages/404.html +1 -1
  194. package/.next/standalone/.next/server/pages/500.html +1 -1
  195. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  196. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  197. package/.next/standalone/.next/static/chunks/0ax_9zzboyv6d.js +44 -0
  198. package/.next/standalone/.next/static/chunks/0cuuo2jwc9h94.js +1 -0
  199. package/.next/standalone/.next/static/chunks/0l1568qff0efj.js +1 -0
  200. package/.next/standalone/.next/static/chunks/0muwt9bykzr-1.js +1 -0
  201. package/.next/standalone/.next/static/chunks/0w76iraw7ij3k.js +18 -0
  202. package/.next/standalone/.next/static/chunks/135n87ydxjrra.js +2 -0
  203. package/.next/standalone/.next/static/chunks/18xu0fbgo_qz-.js +1 -0
  204. package/.next/standalone/.next/static/chunks/28bu26ausqzmn.js +1 -0
  205. package/.next/standalone/.next/static/chunks/{40nvbgfn7-072.css → 2p6o90z0vgcwr.css} +1 -1
  206. package/.next/standalone/.next/static/chunks/2pnbm1z6j7xab.js +1 -0
  207. package/.next/standalone/.next/static/chunks/2xjho0lroiwlq.js +1 -0
  208. package/.next/standalone/.next/static/chunks/3e8zg6ctvvb8p.js +33 -0
  209. package/.next/standalone/.next/static/chunks/3qh78g2aqhprd.js +2 -0
  210. package/.next/standalone/CHANGELOG.md +152 -0
  211. package/.next/standalone/CONTRIBUTING.md +15 -4
  212. package/.next/standalone/README.md +21 -15
  213. package/.next/standalone/SECURITY.md +10 -0
  214. package/.next/standalone/app/api/ingest/route.js +6 -1
  215. package/.next/standalone/app/api/scraper/route.js +79 -11
  216. package/.next/standalone/app/api/update-images/route.js +41 -7
  217. package/.next/standalone/app/avatars/[file]/route.js +6 -1
  218. package/.next/standalone/app/components/Avatar.js +5 -2
  219. package/.next/standalone/app/components/ChainView.js +659 -469
  220. package/.next/standalone/app/components/EmptyState.js +1 -1
  221. package/.next/standalone/app/components/FieldStep.js +94 -0
  222. package/.next/standalone/app/components/ForceGraph.js +183 -768
  223. package/.next/standalone/app/components/GridView.js +9 -7
  224. package/.next/standalone/app/components/LinkedInLimits.js +22 -7
  225. package/.next/standalone/app/components/ListView.js +6 -4
  226. package/.next/standalone/app/components/MappingProgress.js +5 -6
  227. package/.next/standalone/app/components/OnboardingGate.js +1 -1
  228. package/.next/standalone/app/components/OrbitGraph.js +3 -1
  229. package/.next/standalone/app/components/OutlinkQuest.js +5 -4
  230. package/.next/standalone/app/components/RingsView.js +9 -8
  231. package/.next/standalone/app/components/Sidebar.js +172 -112
  232. package/.next/standalone/app/components/TheirCircle.js +18 -11
  233. package/.next/standalone/app/components/settings/SectorPicker.js +202 -0
  234. package/.next/standalone/app/components/settings/SectorSection.js +17 -191
  235. package/.next/standalone/app/components/settings/TierSection.js +7 -12
  236. package/.next/standalone/app/components/views.js +2 -2
  237. package/.next/standalone/app/globals.css +7 -0
  238. package/.next/standalone/app/launch/page.js +2 -2
  239. package/.next/standalone/app/page.js +130 -25
  240. package/.next/standalone/app/paths/page.js +3 -2
  241. package/.next/standalone/app/queue/page.js +18 -10
  242. package/.next/standalone/app/setup/page.js +423 -260
  243. package/.next/standalone/db/schema.js +1 -1
  244. package/.next/standalone/eslint.config.mjs +3 -3
  245. package/.next/standalone/install.sh +3 -3
  246. package/.next/standalone/lib/chain-layout.js +139 -0
  247. package/.next/standalone/lib/csv.js +4 -6
  248. package/.next/standalone/lib/data-export.js +2 -1
  249. package/.next/standalone/lib/data-folder.js +4 -2
  250. package/.next/standalone/lib/galaxy.js +12 -0
  251. package/.next/standalone/lib/ingest.js +15 -8
  252. package/.next/standalone/lib/linkedin-limits.js +30 -8
  253. package/.next/standalone/lib/photos.js +47 -0
  254. package/.next/standalone/lib/quest.js +8 -3
  255. package/.next/standalone/lib/reach.js +113 -0
  256. package/.next/standalone/lib/scan-state.js +1 -0
  257. package/.next/standalone/lib/scanner-setup.js +20 -1
  258. package/.next/standalone/lib/scoring.js +769 -18
  259. package/.next/standalone/lib/scraper-client.js +27 -0
  260. package/.next/standalone/lib/sector-focus.js +13 -0
  261. package/.next/standalone/lib/separation.js +4 -2
  262. package/.next/standalone/lib/settings-client.js +21 -0
  263. package/.next/standalone/lib/settings.js +3 -1
  264. package/.next/standalone/next.config.mjs +25 -3
  265. package/.next/standalone/package-lock.json +2 -2
  266. package/.next/standalone/package.json +1 -1
  267. package/.next/standalone/scripts/build-app.mjs +11 -2
  268. package/.next/standalone/scripts/check-node.mjs +1 -1
  269. package/.next/standalone/scripts/image_store.py +77 -21
  270. package/.next/standalone/scripts/scrape.py +243 -29
  271. package/.next/standalone/server.js +1 -1
  272. package/.next/static/chunks/0ax_9zzboyv6d.js +44 -0
  273. package/.next/static/chunks/0cuuo2jwc9h94.js +1 -0
  274. package/.next/static/chunks/0l1568qff0efj.js +1 -0
  275. package/.next/static/chunks/0muwt9bykzr-1.js +1 -0
  276. package/.next/static/chunks/0w76iraw7ij3k.js +18 -0
  277. package/.next/static/chunks/135n87ydxjrra.js +2 -0
  278. package/.next/static/chunks/18xu0fbgo_qz-.js +1 -0
  279. package/.next/static/chunks/28bu26ausqzmn.js +1 -0
  280. package/.next/static/chunks/{40nvbgfn7-072.css → 2p6o90z0vgcwr.css} +1 -1
  281. package/.next/static/chunks/2pnbm1z6j7xab.js +1 -0
  282. package/.next/static/chunks/2xjho0lroiwlq.js +1 -0
  283. package/.next/static/chunks/3e8zg6ctvvb8p.js +33 -0
  284. package/.next/static/chunks/3qh78g2aqhprd.js +2 -0
  285. package/CHANGELOG.md +152 -0
  286. package/README.md +21 -15
  287. package/SECURITY.md +10 -0
  288. package/db/schema.js +1 -1
  289. package/lib/chain-layout.js +139 -0
  290. package/lib/csv.js +4 -6
  291. package/lib/data-export.js +2 -1
  292. package/lib/data-folder.js +4 -2
  293. package/lib/galaxy.js +12 -0
  294. package/lib/ingest.js +15 -8
  295. package/lib/linkedin-limits.js +30 -8
  296. package/lib/photos.js +47 -0
  297. package/lib/quest.js +8 -3
  298. package/lib/reach.js +113 -0
  299. package/lib/scan-state.js +1 -0
  300. package/lib/scanner-setup.js +20 -1
  301. package/lib/scoring.js +769 -18
  302. package/lib/scraper-client.js +27 -0
  303. package/lib/sector-focus.js +13 -0
  304. package/lib/separation.js +4 -2
  305. package/lib/settings-client.js +21 -0
  306. package/lib/settings.js +3 -1
  307. package/package.json +1 -1
  308. package/scripts/check-node.mjs +1 -1
  309. package/scripts/image_store.py +77 -21
  310. package/scripts/scrape.py +243 -29
  311. package/.next/standalone/.next/server/chunks/lib_rpc_0_8i3ft.js +0 -3
  312. package/.next/standalone/.next/server/chunks/lib_rpc_0huvw-3.js +0 -3
  313. package/.next/standalone/.next/server/chunks/lib_rpc_0ip2vdh.js +0 -3
  314. package/.next/standalone/.next/server/chunks/lib_rpc_0yi9xvd.js +0 -3
  315. package/.next/standalone/.next/server/chunks/lib_rpc_1jyhd0o.js +0 -3
  316. package/.next/standalone/.next/server/chunks/lib_rpc_1kn59-i.js +0 -3
  317. package/.next/standalone/.next/server/chunks/lib_rpc_1q2jkza.js +0 -3
  318. package/.next/standalone/.next/server/chunks/lib_rpc_1vpm1sk.js +0 -3
  319. package/.next/standalone/.next/server/chunks/lib_rpc_1x6p_31.js +0 -3
  320. package/.next/standalone/.next/server/chunks/ssr/_0223v9s._.js +0 -3
  321. package/.next/standalone/.next/server/chunks/ssr/_0bcf13g._.js +0 -3
  322. package/.next/standalone/.next/server/chunks/ssr/_0wxbq78._.js +0 -3
  323. package/.next/standalone/.next/server/chunks/ssr/_0z88zcl._.js +0 -3
  324. package/.next/standalone/.next/server/chunks/ssr/_1-87bs1._.js +0 -3
  325. package/.next/standalone/.next/server/chunks/ssr/_17gui6a._.js +0 -3
  326. package/.next/standalone/.next/static/chunks/0_yurn4q91vq8.js +0 -16
  327. package/.next/standalone/.next/static/chunks/0d_mh-06srh2k.js +0 -1
  328. package/.next/standalone/.next/static/chunks/0x3oadfikvb1c.js +0 -1
  329. package/.next/standalone/.next/static/chunks/1d5hl1io798n2.js +0 -1
  330. package/.next/standalone/.next/static/chunks/1qt67cvj9sjwi.js +0 -1
  331. package/.next/standalone/.next/static/chunks/1roznk7hkjes1.js +0 -44
  332. package/.next/standalone/.next/static/chunks/2_bf2n4gstad9.js +0 -33
  333. package/.next/standalone/.next/static/chunks/36gbwek52x_w-.js +0 -2
  334. package/.next/standalone/.next/static/chunks/3a55suosthntc.js +0 -1
  335. package/.next/standalone/.next/static/chunks/3fq1zq-6cfwbc.js +0 -1
  336. package/.next/standalone/.next/static/chunks/3nbnio971zlji.js +0 -2
  337. package/.next/standalone/.next/static/chunks/3s6awp3qq9s7w.js +0 -1
  338. package/.next/static/chunks/0_yurn4q91vq8.js +0 -16
  339. package/.next/static/chunks/0d_mh-06srh2k.js +0 -1
  340. package/.next/static/chunks/0x3oadfikvb1c.js +0 -1
  341. package/.next/static/chunks/1d5hl1io798n2.js +0 -1
  342. package/.next/static/chunks/1qt67cvj9sjwi.js +0 -1
  343. package/.next/static/chunks/1roznk7hkjes1.js +0 -44
  344. package/.next/static/chunks/2_bf2n4gstad9.js +0 -33
  345. package/.next/static/chunks/36gbwek52x_w-.js +0 -2
  346. package/.next/static/chunks/3a55suosthntc.js +0 -1
  347. package/.next/static/chunks/3fq1zq-6cfwbc.js +0 -1
  348. package/.next/static/chunks/3nbnio971zlji.js +0 -2
  349. package/.next/static/chunks/3s6awp3qq9s7w.js +0 -1
  350. /package/.next/standalone/.next/static/{NnWlxbDF24KhrU1eHiEnv → WseE_5HuU-1q0ywC9CP3U}/_buildManifest.js +0 -0
  351. /package/.next/standalone/.next/static/{NnWlxbDF24KhrU1eHiEnv → WseE_5HuU-1q0ywC9CP3U}/_clientMiddlewareManifest.js +0 -0
  352. /package/.next/standalone/.next/static/{NnWlxbDF24KhrU1eHiEnv → WseE_5HuU-1q0ywC9CP3U}/_ssgManifest.js +0 -0
  353. /package/.next/static/{NnWlxbDF24KhrU1eHiEnv → WseE_5HuU-1q0ywC9CP3U}/_buildManifest.js +0 -0
  354. /package/.next/static/{NnWlxbDF24KhrU1eHiEnv → WseE_5HuU-1q0ywC9CP3U}/_clientMiddlewareManifest.js +0 -0
  355. /package/.next/static/{NnWlxbDF24KhrU1eHiEnv → WseE_5HuU-1q0ywC9CP3U}/_ssgManifest.js +0 -0
@@ -0,0 +1,202 @@
1
+ 'use client';
2
+
3
+ // The sector picks, wherever you choose your field: Scores → Your sector
4
+ // (SectorSection.js) and the Scan page's question before the first scan
5
+ // (app/components/FieldStep.js). Your picks so far, then a search box, or the
6
+ // twelve broad industries, each opening to its narrower sectors from the
7
+ // directory (lib/sector-directory.js). Up to three picks. It only picks:
8
+ // saving is the page's (lib/settings-client.js).
9
+
10
+ import { useMemo, useState } from 'react';
11
+ import { Body, Btn, LINE } from '../ui';
12
+ import { INDUSTRIES } from '../../../lib/companies';
13
+ import { MAX_SECTORS } from '../../../lib/sector-focus';
14
+ import { DIRECTORY, SECTOR_KEYS, sectorByKey } from '../../../lib/sector-directory';
15
+
16
+ const plural = (n, one, many = `${one}s`) => `${n.toLocaleString()} ${n === 1 ? one : many}`;
17
+
18
+ // Each industry's narrower sectors, in the directory's order.
19
+ const SECTORS_OF = new Map(INDUSTRIES.map((g) => [g.key, DIRECTORY.filter((s) => s.group === g.key)]));
20
+ const GROUP_OF = new Map(DIRECTORY.map((s) => [s.key, s.group]));
21
+ // What the search box looks through: each sector's name, its words, jobs and
22
+ // companies, as they read (without the list's "(s)" and "$" marks), so
23
+ // "recruiter" finds HR & Recruiting.
24
+ const SEARCHABLE = DIRECTORY.map((s) => ({
25
+ sector: s,
26
+ label: s.label.toLowerCase(),
27
+ words: [...s.words, ...(s.roles || []), ...(s.names || []), ...s.companies].map((w) => ` ${w.replace(/\(s\)$|\$$/, '').toLowerCase()}`),
28
+ }));
29
+ // A sector of work every company has (HR & Recruiting, Legal…) counts firms in
30
+ // that line, not every company that employs someone doing it.
31
+ const FUNCTION_TITLE = 'Counts firms in this line of work, not every company with someone in the job';
32
+
33
+ /** Sectors (and whole industries) whose name has the query, or a word or company that starts with it. */
34
+ function searchSectors(query) {
35
+ const q = query.trim().toLowerCase().replace(/\s+/g, ' ');
36
+ if (!q) return null;
37
+ return {
38
+ industries: INDUSTRIES.filter((g) => g.label.toLowerCase().includes(q)),
39
+ sectors: SEARCHABLE.filter((s) => s.label.includes(q) || s.words.some((w) => w.includes(` ${q}`))).map((s) => s.sector),
40
+ };
41
+ }
42
+
43
+ /**
44
+ * `sectors` are the picks (keys), `onChange(sectors)` gets the new list.
45
+ * `suggest` is "Suggested from your network" as Scores loads it
46
+ * (/api/settings/sector-suggestions); left out, there are none to show.
47
+ */
48
+ export default function SectorPicker({ sectors, onChange, disabled, suggest }) {
49
+ const [query, setQuery] = useState('');
50
+ // Industries shown open; those holding a pick start open.
51
+ const [open, setOpen] = useState(() => new Set(sectors.map((k) => GROUP_OF.get(k) || k)));
52
+
53
+ const results = useMemo(() => searchSectors(query), [query]);
54
+
55
+ function toggle(key) {
56
+ const on = sectors.includes(key);
57
+ if (!on && sectors.length >= MAX_SECTORS) return;
58
+ const next = on ? sectors.filter((k) => k !== key) : SECTOR_KEYS.filter((k) => k === key || sectors.includes(k));
59
+ // A sector picked from the suggestions or a search shows up in its industry too.
60
+ if (!on && GROUP_OF.has(key)) setOpen((was) => new Set(was).add(GROUP_OF.get(key)));
61
+ onChange(next);
62
+ }
63
+
64
+ function flip(group) {
65
+ setOpen((was) => {
66
+ const next = new Set(was);
67
+ if (next.has(group)) next.delete(group); else next.add(group);
68
+ return next;
69
+ });
70
+ }
71
+
72
+ const full = sectors.length >= MAX_SECTORS;
73
+ // One chip for any pick: an industry or a sector, wherever it's shown.
74
+ const chip = (key, label, { note, title } = {}) => {
75
+ const on = sectors.includes(key);
76
+ return (
77
+ <Chip key={key} color={sectorByKey(key).color} on={on} blocked={!on && full} disabled={disabled} title={title}
78
+ onClick={() => toggle(key)}>
79
+ {label}{note && <span style={{ fontWeight: 400, color: on ? '#dfe7e7' : '#8b9a9a' }}>{note}</span>}
80
+ </Chip>
81
+ );
82
+ };
83
+
84
+ return (
85
+ <>
86
+ <div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: 6 }}>
87
+ <span style={{ fontSize: 13, color: '#8b9a9a' }}>Your picks ({sectors.length} of {MAX_SECTORS}):</span>
88
+ {!sectors.length && <span style={{ fontSize: 13, color: '#667' }}>none yet</span>}
89
+ {sectors.map((k) => {
90
+ const s = sectorByKey(k);
91
+ return (
92
+ <button key={k} type="button" disabled={disabled} onClick={() => toggle(k)} aria-label={`Remove ${s.label}`}
93
+ style={{
94
+ display: 'flex', alignItems: 'center', gap: 6, padding: '4px 10px', borderRadius: 999, fontSize: 12.5,
95
+ fontWeight: 600, color: '#fff', background: `${s.color}33`, border: `1px solid ${s.color}`, cursor: 'pointer',
96
+ }}>
97
+ {s.label} <span aria-hidden="true" style={{ color: '#cfd8d8' }}>×</span>
98
+ </button>
99
+ );
100
+ })}
101
+ </div>
102
+
103
+ {suggest && <Suggestions state={suggest} chip={chip} />}
104
+
105
+ <label htmlFor="sector-search" style={{ display: 'block', fontSize: 13, color: '#8b9a9a', margin: '16px 0 6px' }}>
106
+ Find a sector
107
+ </label>
108
+ <div style={{ display: 'flex', gap: 8 }}>
109
+ <input id="sector-search" type="search" value={query} onChange={(e) => setQuery(e.target.value)}
110
+ placeholder="Try dentist, bakery or Stripe"
111
+ autoComplete="off" spellCheck={false}
112
+ style={{
113
+ flex: 1, minWidth: 0, padding: '8px 12px', borderRadius: 7, fontSize: 14, color: '#fff',
114
+ background: 'rgba(255,255,255,0.05)', border: LINE, outline: 'none',
115
+ }} />
116
+ {query && <Btn onClick={() => setQuery('')}>Clear</Btn>}
117
+ </div>
118
+
119
+ {results ? (
120
+ <div style={{ marginTop: 10 }} aria-live="polite">
121
+ {results.industries.length + results.sectors.length === 0 ? (
122
+ <Body style={{ fontSize: 12.5 }}>
123
+ Nothing in the list matches &ldquo;{query.trim()}&rdquo;. Try a job title, a kind of business or a company, or clear
124
+ the search and pick one of the twelve broad industries.
125
+ </Body>
126
+ ) : (
127
+ <div role="group" aria-label="Sectors that match your search" style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
128
+ {results.industries.map((g) => chip(g.key, g.label, { note: ' · the whole industry' }))}
129
+ {results.sectors.map((s) => chip(s.key, s.label, { title: s.kind === 'function' ? FUNCTION_TITLE : `In ${sectorByKey(s.group).label}` }))}
130
+ </div>
131
+ )}
132
+ </div>
133
+ ) : (
134
+ <div style={{ marginTop: 10 }}>
135
+ {INDUSTRIES.map((g) => {
136
+ const inIndustry = SECTORS_OF.get(g.key);
137
+ const isOpen = open.has(g.key);
138
+ const inside = inIndustry.filter((s) => sectors.includes(s.key)).length;
139
+ return (
140
+ <div key={g.key} style={{ borderTop: LINE, padding: '8px 0' }}>
141
+ <div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: 8 }}>
142
+ {chip(g.key, g.label, { title: 'The whole industry: companies its sectors place, or whose name or the well-known list says so' })}
143
+ <button type="button" aria-expanded={isOpen} aria-controls={`sectors-${g.key}`} onClick={() => flip(g.key)}
144
+ style={{
145
+ padding: '4px 6px', fontSize: 12.5, color: inside ? '#9fd3ff' : '#8b9a9a', background: 'none', border: 'none',
146
+ cursor: 'pointer',
147
+ }}>
148
+ {isOpen ? '▾' : '▸'} {plural(inIndustry.length, 'sector')}{inside ? ` · ${inside} picked` : ''}
149
+ </button>
150
+ </div>
151
+ {isOpen && (
152
+ <div id={`sectors-${g.key}`} role="group" aria-label={`Sectors in ${g.label}`}
153
+ style={{ display: 'flex', flexWrap: 'wrap', gap: 8, margin: '8px 0 2px 14px' }}>
154
+ {inIndustry.map((s) => chip(s.key, s.label, s.kind === 'function' ? { title: FUNCTION_TITLE } : undefined))}
155
+ </div>
156
+ )}
157
+ </div>
158
+ );
159
+ })}
160
+ </div>
161
+ )}
162
+ </>
163
+ );
164
+ }
165
+
166
+ /** A pick you can turn on and off: an industry or a sector, with its industry's colour. */
167
+ function Chip({ color, on, blocked, disabled, title, onClick, children }) {
168
+ return (
169
+ <button type="button" aria-pressed={on} disabled={blocked || disabled} onClick={onClick}
170
+ title={blocked ? `Up to ${MAX_SECTORS} sectors. Turn one off first.` : title}
171
+ style={{
172
+ display: 'flex', alignItems: 'center', gap: 7, padding: '6px 12px', borderRadius: 999, maxWidth: '100%',
173
+ fontSize: 12.5, fontWeight: 600, textAlign: 'left', cursor: blocked ? 'not-allowed' : 'pointer',
174
+ color: on ? '#fff' : blocked ? '#556' : '#b8c4c4',
175
+ background: on ? `${color}33` : 'rgba(255,255,255,0.04)',
176
+ border: on ? `1px solid ${color}` : LINE,
177
+ }}>
178
+ <span style={{ width: 8, height: 8, borderRadius: '50%', background: color, opacity: blocked ? 0.4 : 1, flexShrink: 0 }} />
179
+ <span>{children}</span>
180
+ </button>
181
+ );
182
+ }
183
+
184
+ /** "Suggested from your network": the directory's sectors most of your scanned people work in. */
185
+ function Suggestions({ state, chip }) {
186
+ const note = { fontSize: 12.5, marginTop: 10 };
187
+ if (state.loading) return <Body style={note}>Looking through your network for suggestions…</Body>;
188
+ if (state.error) return <Body style={note}>Couldn&rsquo;t load suggestions ({state.error}). You can still pick below.</Body>;
189
+ if (!state.scored) return <Body style={note}>Suggestions appear here once you&rsquo;ve scanned your network.</Body>;
190
+ if (!state.list.length) return <Body style={note}>None of the list&rsquo;s sectors stands out among the people you&rsquo;ve scanned yet.</Body>;
191
+ return (
192
+ <div style={{ marginTop: 14 }}>
193
+ <div style={{ fontSize: 13, color: '#8b9a9a', marginBottom: 6 }}>Suggested from your network</div>
194
+ <div role="group" aria-label="Suggested from your network" style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
195
+ {state.list.map((s) => chip(s.key, s.label, {
196
+ note: `: ${plural(s.people, 'person', 'people')} at ${plural(s.companies, 'company', 'companies')}`,
197
+ title: `In ${sectorByKey(s.group).label}`,
198
+ }))}
199
+ </div>
200
+ </div>
201
+ );
202
+ }
@@ -10,16 +10,19 @@
10
10
  // Before you save, the page asks the app what would change (a dry run that
11
11
  // writes nothing). Saving stores the choice with your network and rescores
12
12
  // everyone, the way a company score change does. Suggestions come from the
13
- // people you've scanned (/api/settings/sector-suggestions).
13
+ // people you've scanned (/api/settings/sector-suggestions). The picks
14
+ // themselves are SectorPicker.js, which the Scan page's question about your
15
+ // field, before the first scan, shows too.
14
16
 
15
- import { useEffect, useMemo, useRef, useState } from 'react';
17
+ import { useEffect, useRef, useState } from 'react';
16
18
  import { Section, Body, Status, Btn, LINE } from '../ui';
17
- import { INDUSTRIES } from '../../../lib/companies';
18
- import { MAX_SECTORS, sameFocus, focusFingerprint } from '../../../lib/sector-focus';
19
- import { DIRECTORY, SECTOR_KEYS, sectorByKey } from '../../../lib/sector-directory';
19
+ import SectorPicker from './SectorPicker';
20
+ import { sameFocus, focusFingerprint } from '../../../lib/sector-focus';
21
+ import { DIRECTORY, sectorByKey } from '../../../lib/sector-directory';
20
22
  import { SECTOR_BONUS } from '../../../lib/scoring';
21
23
  import { TIER_COLORS_CLASSIC } from '../../../lib/tiers';
22
24
  import { csvNetworkSource } from '../../../lib/csv';
25
+ import { saveSettings } from '../../../lib/settings-client';
23
26
 
24
27
  const STRENGTH_LABEL = { lean: 'Lean', strong: 'Strong' };
25
28
  // What says the reader has taken over the page (see the jump to #sector).
@@ -27,33 +30,10 @@ const READER_INPUT = ['wheel', 'touchstart', 'keydown', 'pointerdown'];
27
30
  const DEGREE = { 1: '1st', 2: '2nd', 3: '3rd' };
28
31
  const plural = (n, one, many = `${one}s`) => `${n.toLocaleString()} ${n === 1 ? one : many}`;
29
32
 
30
- // Each industry's narrower sectors, in the directory's order.
31
- const SECTORS_OF = new Map(INDUSTRIES.map((g) => [g.key, DIRECTORY.filter((s) => s.group === g.key)]));
32
- const GROUP_OF = new Map(DIRECTORY.map((s) => [s.key, s.group]));
33
- // What the search box looks through: each sector's name, its words, jobs and
34
- // companies, as they read (without the list's "(s)" and "$" marks), so
35
- // "recruiter" finds HR & Recruiting.
36
- const SEARCHABLE = DIRECTORY.map((s) => ({
37
- sector: s,
38
- label: s.label.toLowerCase(),
39
- words: [...s.words, ...(s.roles || []), ...(s.names || []), ...s.companies].map((w) => ` ${w.replace(/\(s\)$|\$$/, '').toLowerCase()}`),
40
- }));
41
- // A sector of work every company has (HR & Recruiting, Legal…) counts firms in
42
- // that line, not every company that employs someone doing it.
43
- const FUNCTION_TITLE = 'Counts firms in this line of work, not every company with someone in the job';
33
+ // The sectors that are work every kind of company has (HR & Recruiting, Legal…), for the notes below.
44
34
  const FUNCTIONS = DIRECTORY.filter((s) => s.kind === 'function').map((s) => s.label);
45
35
  const listed = (labels) => (labels.length > 1 ? `${labels.slice(0, -1).join(', ')} and ${labels.at(-1)}` : labels.join(''));
46
36
 
47
- /** Sectors (and whole industries) whose name has the query, or a word or company that starts with it. */
48
- function searchSectors(query) {
49
- const q = query.trim().toLowerCase().replace(/\s+/g, ' ');
50
- if (!q) return null;
51
- return {
52
- industries: INDUSTRIES.filter((g) => g.label.toLowerCase().includes(q)),
53
- sectors: SEARCHABLE.filter((s) => s.label.includes(q) || s.words.some((w) => w.includes(` ${q}`))).map((s) => s.sector),
54
- };
55
- }
56
-
57
37
  /** "120 people move up a tier and 4 move down." (or "moved", after a save) */
58
38
  function tierLine(up, down, past) {
59
39
  const [goes, go] = past ? ['moved', 'moved'] : ['moves', 'move'];
@@ -71,9 +51,6 @@ export default function SectorSection({ onSaved } = {}) {
71
51
  const [result, setResult] = useState(null);
72
52
  const [loadError, setLoadError] = useState(null);
73
53
  const [suggest, setSuggest] = useState({ loading: true });
74
- const [query, setQuery] = useState('');
75
- // Industries shown open; those holding a saved pick start open.
76
- const [open, setOpen] = useState(() => new Set());
77
54
  // A CSV import or the sample network held in this window. This section only
78
55
  // renders in the browser (the page waits in OnboardingGate), so reading
79
56
  // sessionStorage here can't disagree with a server render.
@@ -91,7 +68,6 @@ export default function SectorSection({ onSaved } = {}) {
91
68
  const focus = d.settings?.sectorFocus || { sectors: [], strength: 'lean' };
92
69
  setSaved(focus);
93
70
  setDraft(focus);
94
- setOpen(new Set(focus.sectors.map((k) => GROUP_OF.get(k) || k)));
95
71
  })
96
72
  .catch((e) => { if (!off) setLoadError(e.message); });
97
73
  // Suggestions read every scanned row, so they load on their own and never hold up the rest.
@@ -127,8 +103,6 @@ export default function SectorSection({ onSaved } = {}) {
127
103
  return stop;
128
104
  }, [draft, loadError]);
129
105
 
130
- const results = useMemo(() => searchSectors(query), [query]);
131
-
132
106
  // Every change asks for a fresh dry run, a moment after the last click.
133
107
  // Only the newest answer is kept.
134
108
  function change(next) {
@@ -153,56 +127,20 @@ export default function SectorSection({ onSaved } = {}) {
153
127
  }, 250);
154
128
  }
155
129
 
156
- function toggle(key) {
157
- const on = draft.sectors.includes(key);
158
- if (!on && draft.sectors.length >= MAX_SECTORS) return;
159
- const sectors = on ? draft.sectors.filter((k) => k !== key) : SECTOR_KEYS.filter((k) => k === key || draft.sectors.includes(k));
160
- // A sector picked from the suggestions or a search shows up in its industry too.
161
- if (!on && GROUP_OF.has(key)) setOpen((was) => new Set(was).add(GROUP_OF.get(key)));
162
- change({ ...draft, sectors });
163
- }
164
-
165
- function flip(group) {
166
- setOpen((was) => {
167
- const next = new Set(was);
168
- if (next.has(group)) next.delete(group); else next.add(group);
169
- return next;
170
- });
171
- }
172
-
173
130
  async function save() {
174
131
  setSaving(true);
175
132
  setResult(null);
176
133
  clearTimeout(timer.current);
177
134
  ticket.current++;
178
- try {
179
- const r = await fetch('/api/settings', {
180
- method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ settings: { sectorFocus: draft } }),
181
- });
182
- const d = await r.json();
183
- if (d.settings) { setSaved(d.settings.sectorFocus); setDraft(d.settings.sectorFocus); setPreview(null); }
184
- if (!r.ok) { setResult({ tone: 'bad', text: d.error || 'Could not save.' }); return; }
185
- setResult({ tone: 'ok', text: savedLine(d.settings.sectorFocus, d.effects?.sectorFocus) });
186
- onSaved?.();
187
- } catch {
188
- setResult({ tone: 'bad', text: 'Could not reach the app. Reload this page to see what is saved.' });
189
- } finally {
190
- setSaving(false);
191
- }
135
+ const d = await saveSettings({ sectorFocus: draft });
136
+ setSaving(false);
137
+ if (d.settings) { setSaved(d.settings.sectorFocus); setDraft(d.settings.sectorFocus); setPreview(null); }
138
+ if (d.error) { setResult({ tone: 'bad', text: d.error }); return; }
139
+ setResult({ tone: 'ok', text: savedLine(d.settings.sectorFocus, d.effects?.sectorFocus) });
140
+ onSaved?.();
192
141
  }
193
142
 
194
143
  const changed = draft && saved && !sameFocus(draft, saved);
195
- const full = draft && draft.sectors.length >= MAX_SECTORS;
196
- // One chip for any pick: an industry or a sector, wherever it's shown.
197
- const chip = (key, label, { note, title } = {}) => {
198
- const on = draft.sectors.includes(key);
199
- return (
200
- <Chip key={key} color={sectorByKey(key).color} on={on} blocked={!on && full} disabled={saving} title={title}
201
- onClick={() => toggle(key)}>
202
- {label}{note && <span style={{ fontWeight: 400, color: on ? '#dfe7e7' : '#8b9a9a' }}>{note}</span>}
203
- </Chip>
204
- );
205
- };
206
144
 
207
145
  return (
208
146
  <Section id="sector" title="Your sector"
@@ -212,82 +150,8 @@ export default function SectorSection({ onSaved } = {}) {
212
150
 
213
151
  {draft && (
214
152
  <>
215
- <div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: 6 }}>
216
- <span style={{ fontSize: 13, color: '#8b9a9a' }}>Your picks ({draft.sectors.length} of {MAX_SECTORS}):</span>
217
- {!draft.sectors.length && <span style={{ fontSize: 13, color: '#667' }}>none yet</span>}
218
- {draft.sectors.map((k) => {
219
- const s = sectorByKey(k);
220
- return (
221
- <button key={k} type="button" disabled={saving} onClick={() => toggle(k)} aria-label={`Remove ${s.label}`}
222
- style={{
223
- display: 'flex', alignItems: 'center', gap: 6, padding: '4px 10px', borderRadius: 999, fontSize: 12.5,
224
- fontWeight: 600, color: '#fff', background: `${s.color}33`, border: `1px solid ${s.color}`, cursor: 'pointer',
225
- }}>
226
- {s.label} <span aria-hidden="true" style={{ color: '#cfd8d8' }}>×</span>
227
- </button>
228
- );
229
- })}
230
- </div>
231
-
232
- <Suggestions state={suggest} chip={chip} />
233
-
234
- <label htmlFor="sector-search" style={{ display: 'block', fontSize: 13, color: '#8b9a9a', margin: '16px 0 6px' }}>
235
- Find a sector
236
- </label>
237
- <div style={{ display: 'flex', gap: 8 }}>
238
- <input id="sector-search" type="search" value={query} onChange={(e) => setQuery(e.target.value)}
239
- placeholder="Try dentist, bakery or Stripe"
240
- autoComplete="off" spellCheck={false}
241
- style={{
242
- flex: 1, minWidth: 0, padding: '8px 12px', borderRadius: 7, fontSize: 14, color: '#fff',
243
- background: 'rgba(255,255,255,0.05)', border: LINE, outline: 'none',
244
- }} />
245
- {query && <Btn onClick={() => setQuery('')}>Clear</Btn>}
246
- </div>
247
-
248
- {results ? (
249
- <div style={{ marginTop: 10 }} aria-live="polite">
250
- {results.industries.length + results.sectors.length === 0 ? (
251
- <Body style={{ fontSize: 12.5 }}>
252
- Nothing in the list matches &ldquo;{query.trim()}&rdquo;. Try a job title, a kind of business or a company, or clear
253
- the search and pick one of the twelve broad industries.
254
- </Body>
255
- ) : (
256
- <div role="group" aria-label="Sectors that match your search" style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
257
- {results.industries.map((g) => chip(g.key, g.label, { note: ' · the whole industry' }))}
258
- {results.sectors.map((s) => chip(s.key, s.label, { title: s.kind === 'function' ? FUNCTION_TITLE : `In ${sectorByKey(s.group).label}` }))}
259
- </div>
260
- )}
261
- </div>
262
- ) : (
263
- <div style={{ marginTop: 10 }}>
264
- {INDUSTRIES.map((g) => {
265
- const sectors = SECTORS_OF.get(g.key);
266
- const isOpen = open.has(g.key);
267
- const inside = sectors.filter((s) => draft.sectors.includes(s.key)).length;
268
- return (
269
- <div key={g.key} style={{ borderTop: LINE, padding: '8px 0' }}>
270
- <div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: 8 }}>
271
- {chip(g.key, g.label, { title: 'The whole industry: companies its sectors place, or whose name or the well-known list says so' })}
272
- <button type="button" aria-expanded={isOpen} aria-controls={`sectors-${g.key}`} onClick={() => flip(g.key)}
273
- style={{
274
- padding: '4px 6px', fontSize: 12.5, color: inside ? '#9fd3ff' : '#8b9a9a', background: 'none', border: 'none',
275
- cursor: 'pointer',
276
- }}>
277
- {isOpen ? '▾' : '▸'} {plural(sectors.length, 'sector')}{inside ? ` · ${inside} picked` : ''}
278
- </button>
279
- </div>
280
- {isOpen && (
281
- <div id={`sectors-${g.key}`} role="group" aria-label={`Sectors in ${g.label}`}
282
- style={{ display: 'flex', flexWrap: 'wrap', gap: 8, margin: '8px 0 2px 14px' }}>
283
- {sectors.map((s) => chip(s.key, s.label, s.kind === 'function' ? { title: FUNCTION_TITLE } : undefined))}
284
- </div>
285
- )}
286
- </div>
287
- );
288
- })}
289
- </div>
290
- )}
153
+ <SectorPicker sectors={draft.sectors} onChange={(sectors) => change({ ...draft, sectors })} disabled={saving}
154
+ suggest={suggest} />
291
155
 
292
156
  <div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: 8, marginTop: 14 }}>
293
157
  <span style={{ fontSize: 13, color: '#8b9a9a' }}>How much:</span>
@@ -352,44 +216,6 @@ export default function SectorSection({ onSaved } = {}) {
352
216
  );
353
217
  }
354
218
 
355
- /** A pick you can turn on and off: an industry or a sector, with its industry's colour. */
356
- function Chip({ color, on, blocked, disabled, title, onClick, children }) {
357
- return (
358
- <button type="button" aria-pressed={on} disabled={blocked || disabled} onClick={onClick}
359
- title={blocked ? `Up to ${MAX_SECTORS} sectors. Turn one off first.` : title}
360
- style={{
361
- display: 'flex', alignItems: 'center', gap: 7, padding: '6px 12px', borderRadius: 999, maxWidth: '100%',
362
- fontSize: 12.5, fontWeight: 600, textAlign: 'left', cursor: blocked ? 'not-allowed' : 'pointer',
363
- color: on ? '#fff' : blocked ? '#556' : '#b8c4c4',
364
- background: on ? `${color}33` : 'rgba(255,255,255,0.04)',
365
- border: on ? `1px solid ${color}` : LINE,
366
- }}>
367
- <span style={{ width: 8, height: 8, borderRadius: '50%', background: color, opacity: blocked ? 0.4 : 1, flexShrink: 0 }} />
368
- <span>{children}</span>
369
- </button>
370
- );
371
- }
372
-
373
- /** "Suggested from your network": the directory's sectors most of your scanned people work in. */
374
- function Suggestions({ state, chip }) {
375
- const note = { fontSize: 12.5, marginTop: 10 };
376
- if (state.loading) return <Body style={note}>Looking through your network for suggestions…</Body>;
377
- if (state.error) return <Body style={note}>Couldn&rsquo;t load suggestions ({state.error}). You can still pick below.</Body>;
378
- if (!state.scored) return <Body style={note}>Suggestions appear here once you&rsquo;ve scanned your network.</Body>;
379
- if (!state.list.length) return <Body style={note}>None of the list&rsquo;s sectors stands out among the people you&rsquo;ve scanned yet.</Body>;
380
- return (
381
- <div style={{ marginTop: 14 }}>
382
- <div style={{ fontSize: 13, color: '#8b9a9a', marginBottom: 6 }}>Suggested from your network</div>
383
- <div role="group" aria-label="Suggested from your network" style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
384
- {state.list.map((s) => chip(s.key, s.label, {
385
- note: `: ${plural(s.people, 'person', 'people')} at ${plural(s.companies, 'company', 'companies')}`,
386
- title: `In ${sectorByKey(s.group).label}`,
387
- }))}
388
- </div>
389
- </div>
390
- );
391
- }
392
-
393
219
  /** "Saved: …. Rescored your network (N people). …" */
394
220
  function savedLine(focus, effect) {
395
221
  // The labels have commas of their own ("Tech, Software & AI"), so a dot
@@ -11,6 +11,7 @@ import { useEffect, useState } from 'react';
11
11
  import { Section, Body, Status, LINE } from '../ui';
12
12
  import { CURVE, CURVE_FLOOR } from '../../../lib/scoring';
13
13
  import { csvNetworkSource } from '../../../lib/csv';
14
+ import { saveSettings } from '../../../lib/settings-client';
14
15
 
15
16
  const pct = (x) => `${Math.round(x * 100)}%`;
16
17
  const [S, A, B] = CURVE.map(([, share]) => share);
@@ -53,21 +54,15 @@ export default function TierSection({ onSaved } = {}) {
53
54
  if (saving || key === saved) return;
54
55
  setSaving(true);
55
56
  setResult(null);
56
- try {
57
- const r = await fetch('/api/settings', {
58
- method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ settings: { tierScale: key } }),
59
- });
60
- const d = await r.json();
61
- if (d.settings) setSaved(d.settings.tierScale);
62
- if (!r.ok) { setResult({ tone: 'bad', text: d.error || 'Could not save.' }); return; }
63
- const e = d.effects?.tierScale;
57
+ const { settings, effects, error } = await saveSettings({ tierScale: key });
58
+ if (settings) setSaved(settings.tierScale);
59
+ if (error) setResult({ tone: 'bad', text: error });
60
+ else {
61
+ const e = effects?.tierScale;
64
62
  setResult({ tone: 'ok', text: e ? `Saved. ${plural(e.moved, 'person', 'people')} changed tier.` : 'Saved.' });
65
63
  onSaved?.();
66
- } catch {
67
- setResult({ tone: 'bad', text: 'Could not reach the app. Reload this page to see what is saved.' });
68
- } finally {
69
- setSaving(false);
70
64
  }
65
+ setSaving(false);
71
66
  }
72
67
 
73
68
  return (
@@ -36,8 +36,8 @@ export const VIEWS = {
36
36
  };
37
37
 
38
38
  /**
39
- * The order they appear in the menu. Separation is the first Degrees view, so
40
- * it is also where resolveView lands when the chosen view isn't a Degrees one
39
+ * The order they appear in the menu. Orbit is the first Degrees view, so it is
40
+ * also where resolveView lands when the chosen view isn't a Degrees one
41
41
  * (Galaxy, say, carried over from Network Circle).
42
42
  */
43
43
  const ORDER = ['galaxy', 'orbit', 'separation', 'chain', 'rings', 'list'];
@@ -19,6 +19,13 @@ body:has(> div > [data-map]) {
19
19
  overflow: hidden;
20
20
  }
21
21
 
22
+ /* The Scan page's field question holds Continue and Skip for now at the foot
23
+ of the window (app/components/FieldStep.js). Without this, a pick reached
24
+ with Tab can stop behind them. */
25
+ html:has([data-field-question]) {
26
+ scroll-padding-bottom: 80px;
27
+ }
28
+
22
29
  ::-webkit-scrollbar {
23
30
  width: 6px;
24
31
  }
@@ -125,7 +125,7 @@ function HeroSection() {
125
125
  transition: 'all 1s cubic-bezier(0.16, 1, 0.3, 1) 0.15s',
126
126
  letterSpacing: '-0.03em',
127
127
  }}>
128
- 6 Degrees of<br />Separation
128
+ Six Degrees of<br />Separation
129
129
  </h1>
130
130
 
131
131
  {/* Subtitle */}
@@ -315,7 +315,7 @@ function GalaxySection() {
315
315
  }
316
316
 
317
317
  // ═══════════════════════════════════════════════════════════════
318
- // BRIDGE CHAINS — 6 degrees visualization
318
+ // BRIDGE CHAINS — six degrees visualization
319
319
  // ═══════════════════════════════════════════════════════════════
320
320
  function ChainSection() {
321
321
  const [ref, inView] = useInView();