@sudobility/sudojo_lib 0.0.201 → 0.0.203

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 (263) hide show
  1. package/dist/admin/adminApi.d.ts +34 -0
  2. package/dist/admin/adminApi.d.ts.map +1 -0
  3. package/dist/admin/adminApi.js +59 -0
  4. package/dist/admin/adminApi.js.map +1 -0
  5. package/dist/admin/boardRules.d.ts +75 -0
  6. package/dist/admin/boardRules.d.ts.map +1 -0
  7. package/dist/admin/boardRules.js +127 -0
  8. package/dist/admin/boardRules.js.map +1 -0
  9. package/dist/admin/boardStrings.d.ts +55 -0
  10. package/dist/admin/boardStrings.d.ts.map +1 -0
  11. package/dist/admin/boardStrings.js +99 -0
  12. package/dist/admin/boardStrings.js.map +1 -0
  13. package/dist/admin/examples.d.ts +58 -0
  14. package/dist/admin/examples.d.ts.map +1 -0
  15. package/dist/admin/examples.js +104 -0
  16. package/dist/admin/examples.js.map +1 -0
  17. package/dist/admin/extraction.d.ts +87 -0
  18. package/dist/admin/extraction.d.ts.map +1 -0
  19. package/dist/admin/extraction.js +141 -0
  20. package/dist/admin/extraction.js.map +1 -0
  21. package/dist/admin/index.d.ts +32 -0
  22. package/dist/admin/index.d.ts.map +1 -0
  23. package/dist/admin/index.js +22 -0
  24. package/dist/admin/index.js.map +1 -0
  25. package/dist/admin/jobs.d.ts +108 -0
  26. package/dist/admin/jobs.d.ts.map +1 -0
  27. package/dist/admin/jobs.js +435 -0
  28. package/dist/admin/jobs.js.map +1 -0
  29. package/dist/admin/regenerateHints.d.ts +16 -0
  30. package/dist/admin/regenerateHints.d.ts.map +1 -0
  31. package/dist/admin/regenerateHints.js +43 -0
  32. package/dist/admin/regenerateHints.js.map +1 -0
  33. package/dist/admin/types.d.ts +42 -0
  34. package/dist/admin/types.d.ts.map +1 -0
  35. package/dist/admin/types.js +21 -0
  36. package/dist/admin/types.js.map +1 -0
  37. package/dist/admin/useAdminJob.d.ts +28 -0
  38. package/dist/admin/useAdminJob.d.ts.map +1 -0
  39. package/dist/admin/useAdminJob.js +99 -0
  40. package/dist/admin/useAdminJob.js.map +1 -0
  41. package/dist/admin/useAdminStats.d.ts +46 -0
  42. package/dist/admin/useAdminStats.d.ts.map +1 -0
  43. package/dist/admin/useAdminStats.js +91 -0
  44. package/dist/admin/useAdminStats.js.map +1 -0
  45. package/dist/admin/useBoardGenerator.d.ts +29 -0
  46. package/dist/admin/useBoardGenerator.d.ts.map +1 -0
  47. package/dist/admin/useBoardGenerator.js +38 -0
  48. package/dist/admin/useBoardGenerator.js.map +1 -0
  49. package/dist/admin/useBoardTechniquesUpdater.d.ts +14 -0
  50. package/dist/admin/useBoardTechniquesUpdater.d.ts.map +1 -0
  51. package/dist/admin/useBoardTechniquesUpdater.js +28 -0
  52. package/dist/admin/useBoardTechniquesUpdater.js.map +1 -0
  53. package/dist/admin/useExampleCreator.d.ts +29 -0
  54. package/dist/admin/useExampleCreator.d.ts.map +1 -0
  55. package/dist/admin/useExampleCreator.js +58 -0
  56. package/dist/admin/useExampleCreator.js.map +1 -0
  57. package/dist/admin/useSingleBoardExtractor.d.ts +18 -0
  58. package/dist/admin/useSingleBoardExtractor.d.ts.map +1 -0
  59. package/dist/admin/useSingleBoardExtractor.js +19 -0
  60. package/dist/admin/useSingleBoardExtractor.js.map +1 -0
  61. package/dist/admin/useTechniqueExtractor.d.ts +26 -0
  62. package/dist/admin/useTechniqueExtractor.d.ts.map +1 -0
  63. package/dist/admin/useTechniqueExtractor.js +37 -0
  64. package/dist/admin/useTechniqueExtractor.js.map +1 -0
  65. package/dist/context/SudojoApiContext.d.ts +58 -0
  66. package/dist/context/SudojoApiContext.d.ts.map +1 -0
  67. package/dist/context/SudojoApiContext.js +52 -0
  68. package/dist/context/SudojoApiContext.js.map +1 -0
  69. package/dist/context/SudojoApiProvider.d.ts +21 -0
  70. package/dist/context/SudojoApiProvider.d.ts.map +1 -0
  71. package/dist/context/SudojoApiProvider.js +15 -0
  72. package/dist/context/SudojoApiProvider.js.map +1 -0
  73. package/dist/context/index.d.ts +3 -0
  74. package/dist/context/index.d.ts.map +1 -1
  75. package/dist/context/index.js +2 -0
  76. package/dist/context/index.js.map +1 -1
  77. package/dist/hooks/index.d.ts +24 -1
  78. package/dist/hooks/index.d.ts.map +1 -1
  79. package/dist/hooks/index.js +22 -0
  80. package/dist/hooks/index.js.map +1 -1
  81. package/dist/hooks/useBoardEntry.d.ts +65 -5
  82. package/dist/hooks/useBoardEntry.d.ts.map +1 -1
  83. package/dist/hooks/useBoardEntry.js +124 -27
  84. package/dist/hooks/useBoardEntry.js.map +1 -1
  85. package/dist/hooks/useBoardScan.d.ts +27 -0
  86. package/dist/hooks/useBoardScan.d.ts.map +1 -0
  87. package/dist/hooks/useBoardScan.js +27 -0
  88. package/dist/hooks/useBoardScan.js.map +1 -0
  89. package/dist/hooks/useCommunities.d.ts +6 -4
  90. package/dist/hooks/useCommunities.d.ts.map +1 -1
  91. package/dist/hooks/useCommunities.js +6 -1
  92. package/dist/hooks/useCommunities.js.map +1 -1
  93. package/dist/hooks/useCompletionTrigger.d.ts +23 -0
  94. package/dist/hooks/useCompletionTrigger.d.ts.map +1 -0
  95. package/dist/hooks/useCompletionTrigger.js +39 -0
  96. package/dist/hooks/useCompletionTrigger.js.map +1 -0
  97. package/dist/hooks/useDailyGame.d.ts +11 -6
  98. package/dist/hooks/useDailyGame.d.ts.map +1 -1
  99. package/dist/hooks/useDailyGame.js +20 -61
  100. package/dist/hooks/useDailyGame.js.map +1 -1
  101. package/dist/hooks/useDeleteAccount.d.ts +72 -0
  102. package/dist/hooks/useDeleteAccount.d.ts.map +1 -0
  103. package/dist/hooks/useDeleteAccount.js +71 -0
  104. package/dist/hooks/useDeleteAccount.js.map +1 -0
  105. package/dist/hooks/useEnteredGameSession.d.ts +73 -0
  106. package/dist/hooks/useEnteredGameSession.d.ts.map +1 -0
  107. package/dist/hooks/useEnteredGameSession.js +155 -0
  108. package/dist/hooks/useEnteredGameSession.js.map +1 -0
  109. package/dist/hooks/useGameSession.d.ts +8 -4
  110. package/dist/hooks/useGameSession.d.ts.map +1 -1
  111. package/dist/hooks/useGameSession.js +4 -1
  112. package/dist/hooks/useGameSession.js.map +1 -1
  113. package/dist/hooks/useGameTeaching.d.ts.map +1 -1
  114. package/dist/hooks/useGameTeaching.js +11 -13
  115. package/dist/hooks/useGameTeaching.js.map +1 -1
  116. package/dist/hooks/useHint.d.ts +7 -7
  117. package/dist/hooks/useHint.d.ts.map +1 -1
  118. package/dist/hooks/useHint.js +18 -9
  119. package/dist/hooks/useHint.js.map +1 -1
  120. package/dist/hooks/useIsSiteAdmin.d.ts +39 -0
  121. package/dist/hooks/useIsSiteAdmin.d.ts.map +1 -0
  122. package/dist/hooks/useIsSiteAdmin.js +31 -0
  123. package/dist/hooks/useIsSiteAdmin.js.map +1 -0
  124. package/dist/hooks/useLearning.d.ts +8 -8
  125. package/dist/hooks/useLearning.d.ts.map +1 -1
  126. package/dist/hooks/useLearning.js +5 -2
  127. package/dist/hooks/useLearning.js.map +1 -1
  128. package/dist/hooks/useLevelGame.d.ts +7 -7
  129. package/dist/hooks/useLevelGame.d.ts.map +1 -1
  130. package/dist/hooks/useLevelGame.js +13 -49
  131. package/dist/hooks/useLevelGame.js.map +1 -1
  132. package/dist/hooks/useLevels.d.ts +8 -8
  133. package/dist/hooks/useLevels.d.ts.map +1 -1
  134. package/dist/hooks/useLevels.js +7 -3
  135. package/dist/hooks/useLevels.js.map +1 -1
  136. package/dist/hooks/usePracticeGame.d.ts +53 -0
  137. package/dist/hooks/usePracticeGame.d.ts.map +1 -0
  138. package/dist/hooks/usePracticeGame.js +72 -0
  139. package/dist/hooks/usePracticeGame.js.map +1 -0
  140. package/dist/hooks/usePuzzleDistribution.d.ts +2 -0
  141. package/dist/hooks/usePuzzleDistribution.d.ts.map +1 -1
  142. package/dist/hooks/usePuzzleDistribution.js +4 -14
  143. package/dist/hooks/usePuzzleDistribution.js.map +1 -1
  144. package/dist/hooks/usePuzzleProgress.d.ts +37 -0
  145. package/dist/hooks/usePuzzleProgress.d.ts.map +1 -0
  146. package/dist/hooks/usePuzzleProgress.js +48 -0
  147. package/dist/hooks/usePuzzleProgress.js.map +1 -0
  148. package/dist/hooks/usePuzzleSession.d.ts +215 -0
  149. package/dist/hooks/usePuzzleSession.d.ts.map +1 -0
  150. package/dist/hooks/usePuzzleSession.js +358 -0
  151. package/dist/hooks/usePuzzleSession.js.map +1 -0
  152. package/dist/hooks/useResumeGame.d.ts +70 -0
  153. package/dist/hooks/useResumeGame.d.ts.map +1 -0
  154. package/dist/hooks/useResumeGame.js +123 -0
  155. package/dist/hooks/useResumeGame.js.map +1 -0
  156. package/dist/hooks/useStrategies.d.ts +39 -0
  157. package/dist/hooks/useStrategies.d.ts.map +1 -0
  158. package/dist/hooks/useStrategies.js +34 -0
  159. package/dist/hooks/useStrategies.js.map +1 -0
  160. package/dist/hooks/useSudoku.d.ts +5 -0
  161. package/dist/hooks/useSudoku.d.ts.map +1 -1
  162. package/dist/hooks/useSudoku.js +7 -1
  163. package/dist/hooks/useSudoku.js.map +1 -1
  164. package/dist/hooks/useTechniqueByPath.d.ts +46 -0
  165. package/dist/hooks/useTechniqueByPath.d.ts.map +1 -0
  166. package/dist/hooks/useTechniqueByPath.js +47 -0
  167. package/dist/hooks/useTechniqueByPath.js.map +1 -0
  168. package/dist/hooks/useTechniqueExample.d.ts +60 -0
  169. package/dist/hooks/useTechniqueExample.d.ts.map +1 -0
  170. package/dist/hooks/useTechniqueExample.js +63 -0
  171. package/dist/hooks/useTechniqueExample.js.map +1 -0
  172. package/dist/hooks/useTechniques.d.ts +8 -8
  173. package/dist/hooks/useTechniques.d.ts.map +1 -1
  174. package/dist/hooks/useTechniques.js +9 -21
  175. package/dist/hooks/useTechniques.js.map +1 -1
  176. package/dist/index.d.ts +418 -11
  177. package/dist/index.d.ts.map +1 -1
  178. package/dist/index.js +276 -10
  179. package/dist/index.js.map +1 -1
  180. package/dist/types/currentGame.d.ts +9 -1
  181. package/dist/types/currentGame.d.ts.map +1 -1
  182. package/dist/types/currentGame.js.map +1 -1
  183. package/dist/utils/auth.d.ts +6 -0
  184. package/dist/utils/auth.d.ts.map +1 -1
  185. package/dist/utils/auth.js +8 -0
  186. package/dist/utils/auth.js.map +1 -1
  187. package/dist/utils/community.d.ts +64 -0
  188. package/dist/utils/community.d.ts.map +1 -0
  189. package/dist/utils/community.js +66 -0
  190. package/dist/utils/community.js.map +1 -0
  191. package/dist/utils/date.d.ts +37 -0
  192. package/dist/utils/date.d.ts.map +1 -0
  193. package/dist/utils/date.js +68 -0
  194. package/dist/utils/date.js.map +1 -0
  195. package/dist/utils/entityTranslate.d.ts +120 -0
  196. package/dist/utils/entityTranslate.d.ts.map +1 -0
  197. package/dist/utils/entityTranslate.js +146 -0
  198. package/dist/utils/entityTranslate.js.map +1 -0
  199. package/dist/utils/gameFetchStatus.d.ts +74 -0
  200. package/dist/utils/gameFetchStatus.d.ts.map +1 -0
  201. package/dist/utils/gameFetchStatus.js +118 -0
  202. package/dist/utils/gameFetchStatus.js.map +1 -0
  203. package/dist/utils/gamification.d.ts +36 -0
  204. package/dist/utils/gamification.d.ts.map +1 -0
  205. package/dist/utils/gamification.js +33 -0
  206. package/dist/utils/gamification.js.map +1 -0
  207. package/dist/utils/hintAccess.d.ts +64 -0
  208. package/dist/utils/hintAccess.d.ts.map +1 -0
  209. package/dist/utils/hintAccess.js +57 -0
  210. package/dist/utils/hintAccess.js.map +1 -0
  211. package/dist/utils/index.d.ts +31 -10
  212. package/dist/utils/index.d.ts.map +1 -1
  213. package/dist/utils/index.js +20 -9
  214. package/dist/utils/index.js.map +1 -1
  215. package/dist/utils/language.d.ts +123 -0
  216. package/dist/utils/language.d.ts.map +1 -0
  217. package/dist/utils/language.js +95 -0
  218. package/dist/utils/language.js.map +1 -0
  219. package/dist/utils/localizedHint.d.ts +60 -0
  220. package/dist/utils/localizedHint.d.ts.map +1 -1
  221. package/dist/utils/localizedHint.js +90 -0
  222. package/dist/utils/localizedHint.js.map +1 -1
  223. package/dist/utils/progress.d.ts +10 -1
  224. package/dist/utils/progress.d.ts.map +1 -1
  225. package/dist/utils/progress.js +24 -0
  226. package/dist/utils/progress.js.map +1 -1
  227. package/dist/utils/scannedBoard.d.ts +79 -0
  228. package/dist/utils/scannedBoard.d.ts.map +1 -0
  229. package/dist/utils/scannedBoard.js +96 -0
  230. package/dist/utils/scannedBoard.js.map +1 -0
  231. package/dist/utils/shareUrl.d.ts +38 -15
  232. package/dist/utils/shareUrl.d.ts.map +1 -1
  233. package/dist/utils/shareUrl.js +75 -26
  234. package/dist/utils/shareUrl.js.map +1 -1
  235. package/dist/utils/subscription.d.ts +118 -0
  236. package/dist/utils/subscription.d.ts.map +1 -1
  237. package/dist/utils/subscription.js +212 -0
  238. package/dist/utils/subscription.js.map +1 -1
  239. package/dist/utils/sudokuPresenter.d.ts +42 -0
  240. package/dist/utils/sudokuPresenter.d.ts.map +1 -1
  241. package/dist/utils/sudokuPresenter.js +114 -1
  242. package/dist/utils/sudokuPresenter.js.map +1 -1
  243. package/dist/utils/sudokuScrambler.d.ts +8 -0
  244. package/dist/utils/sudokuScrambler.d.ts.map +1 -1
  245. package/dist/utils/sudokuScrambler.js +12 -0
  246. package/dist/utils/sudokuScrambler.js.map +1 -1
  247. package/dist/utils/technique.d.ts +112 -1
  248. package/dist/utils/technique.d.ts.map +1 -1
  249. package/dist/utils/technique.js +293 -1
  250. package/dist/utils/technique.js.map +1 -1
  251. package/dist/utils/techniqueBitmask.d.ts +31 -0
  252. package/dist/utils/techniqueBitmask.d.ts.map +1 -0
  253. package/dist/utils/techniqueBitmask.js +53 -0
  254. package/dist/utils/techniqueBitmask.js.map +1 -0
  255. package/dist/utils/techniqueExample.d.ts +42 -0
  256. package/dist/utils/techniqueExample.d.ts.map +1 -0
  257. package/dist/utils/techniqueExample.js +57 -0
  258. package/dist/utils/techniqueExample.js.map +1 -0
  259. package/dist/utils/theme.d.ts +2 -0
  260. package/dist/utils/theme.d.ts.map +1 -1
  261. package/dist/utils/theme.js +2 -0
  262. package/dist/utils/theme.js.map +1 -1
  263. package/package.json +7 -7
@@ -3,31 +3,18 @@
3
3
  * Allows users to enter given clues for a custom puzzle
4
4
  */
5
5
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
6
+ import { createEmptyBoard } from '../types';
6
7
  import { useSolverValidate } from '@sudobility/sudojo_client';
8
+ import { MIN_CLUES } from '@sudobility/sudojo_types';
9
+ import { cellsToPuzzleString, countClues } from '../utils/sudokuScrambler';
10
+ import { useResolvedSudojoApi } from '../context/SudojoApiContext';
11
+ import { hasScannedInput } from '../utils/scannedBoard';
12
+ import { parsePencilmarksString, pencilmarksToString, } from '../utils/techniqueWalkthrough';
7
13
  /**
8
14
  * Create an empty 81-cell board for entry mode
9
15
  */
10
- function createEmptyBoard() {
11
- return Array.from({ length: 81 }, (_, index) => ({
12
- index,
13
- solution: null,
14
- given: null,
15
- input: null,
16
- pencilmarks: null,
17
- }));
18
- }
19
- /**
20
- * Convert cells to 81-character puzzle string
21
- * '0' for empty, '1'-'9' for given values
22
- */
23
- function cellsToPuzzleString(cells) {
24
- return cells.map(cell => cell.given?.toString() ?? '0').join('');
25
- }
26
- /**
27
- * Count the number of non-empty cells (clues)
28
- */
29
- function countClues(cells) {
30
- return cells.filter(cell => cell.given !== null).length;
16
+ function createEmptyCells() {
17
+ return createEmptyBoard().cells;
31
18
  }
32
19
  /**
33
20
  * Create a board from an 81-character puzzle string
@@ -35,7 +22,7 @@ function countClues(cells) {
35
22
  */
36
23
  function puzzleStringToCells(puzzle) {
37
24
  if (puzzle.length !== 81) {
38
- return createEmptyBoard();
25
+ return createEmptyCells();
39
26
  }
40
27
  return Array.from({ length: 81 }, (_, index) => {
41
28
  const char = puzzle.charAt(index);
@@ -63,8 +50,17 @@ function classifyValidationError(errorMsg) {
63
50
  }
64
51
  return 'enter.errors.validationFailed';
65
52
  }
66
- export function useBoardEntry({ networkClient, baseUrl, token, }) {
67
- const [cells, setCells] = useState(createEmptyBoard);
53
+ function createEmptyPencilmarks() {
54
+ return Array.from({ length: 81 }, () => []);
55
+ }
56
+ const EMPTY_INPUT = '0'.repeat(81);
57
+ export function useBoardEntry(options = {}) {
58
+ const { networkClient, baseUrl, token } = useResolvedSudojoApi(options, 'useBoardEntry');
59
+ const [cells, setCells] = useState(createEmptyCells);
60
+ const [pencilmarks, setPencilmarks] = useState(createEmptyPencilmarks);
61
+ const [autopencil, setAutopencil] = useState(false);
62
+ const [scannedInput, setScannedInput] = useState(null);
63
+ const [isPencilMode, setIsPencilMode] = useState(false);
68
64
  const [selectedIndex, setSelectedIndex] = useState(null);
69
65
  const [validationError, setValidationError] = useState(null);
70
66
  const [validatedPuzzle, setValidatedPuzzle] = useState(null);
@@ -74,7 +70,7 @@ export function useBoardEntry({ networkClient, baseUrl, token, }) {
74
70
  const clueCount = useMemo(() => countClues(cells), [cells]);
75
71
  // Use the solver validate hook
76
72
  const { data: validateData, error: validateError, isLoading: isValidating, } = useSolverValidate(networkClient, baseUrl, token, { original: puzzleString }, {
77
- enabled: shouldValidate && clueCount >= 17,
73
+ enabled: shouldValidate && clueCount >= MIN_CLUES,
78
74
  retry: false,
79
75
  });
80
76
  // Track whether we've already processed this validation result
@@ -149,6 +145,14 @@ export function useBoardEntry({ networkClient, baseUrl, token, }) {
149
145
  }
150
146
  return newCells;
151
147
  });
148
+ // A given replaces the cell's pencilmarks
149
+ setPencilmarks(prev => {
150
+ if (!prev[selectedIndex]?.length)
151
+ return prev;
152
+ const next = [...prev];
153
+ next[selectedIndex] = [];
154
+ return next;
155
+ });
152
156
  // Clear validation state when board changes
153
157
  setValidationError(null);
154
158
  setValidatedPuzzle(null);
@@ -176,7 +180,7 @@ export function useBoardEntry({ networkClient, baseUrl, token, }) {
176
180
  // Trigger validation
177
181
  const validate = useCallback(() => {
178
182
  // Check minimum clues
179
- if (clueCount < 17) {
183
+ if (clueCount < MIN_CLUES) {
180
184
  setValidationError('enter.errors.notEnoughClues');
181
185
  return;
182
186
  }
@@ -190,7 +194,11 @@ export function useBoardEntry({ networkClient, baseUrl, token, }) {
190
194
  }, [clueCount]);
191
195
  // Reset to empty board
192
196
  const reset = useCallback(() => {
193
- setCells(createEmptyBoard());
197
+ setCells(createEmptyCells());
198
+ setPencilmarks(createEmptyPencilmarks());
199
+ setAutopencil(false);
200
+ setScannedInput(null);
201
+ setIsPencilMode(false);
194
202
  setSelectedIndex(null);
195
203
  setValidationError(null);
196
204
  setValidatedPuzzle(null);
@@ -201,6 +209,9 @@ export function useBoardEntry({ networkClient, baseUrl, token, }) {
201
209
  // Set cells from puzzle string
202
210
  const setCellsFromPuzzle = useCallback((puzzle) => {
203
211
  setCells(puzzleStringToCells(puzzle));
212
+ setPencilmarks(createEmptyPencilmarks());
213
+ setAutopencil(false);
214
+ setScannedInput(null);
204
215
  setSelectedIndex(null);
205
216
  setValidationError(null);
206
217
  setValidatedPuzzle(null);
@@ -208,6 +219,77 @@ export function useBoardEntry({ networkClient, baseUrl, token, }) {
208
219
  processedDataRef.current = null;
209
220
  processedErrorRef.current = null;
210
221
  }, []);
222
+ // Toggle a pencilmark digit on the selected (non-given) cell
223
+ const togglePencilmark = useCallback((digit) => {
224
+ if (selectedIndex === null || digit < 1 || digit > 9)
225
+ return;
226
+ if (cells[selectedIndex]?.given !== null)
227
+ return;
228
+ setPencilmarks(prev => {
229
+ const next = [...prev];
230
+ const current = next[selectedIndex] ?? [];
231
+ next[selectedIndex] = current.includes(digit)
232
+ ? current.filter(d => d !== digit)
233
+ : [...current, digit].sort((a, b) => a - b);
234
+ return next;
235
+ });
236
+ }, [selectedIndex, cells]);
237
+ const togglePencilMode = useCallback(() => {
238
+ setIsPencilMode(prev => !prev);
239
+ }, []);
240
+ const setPencilMode = useCallback((on) => {
241
+ setIsPencilMode(on);
242
+ }, []);
243
+ const enterDigit = useCallback((value) => {
244
+ if (isPencilMode) {
245
+ togglePencilmark(value);
246
+ }
247
+ else {
248
+ setGiven(value);
249
+ }
250
+ }, [isPencilMode, togglePencilmark, setGiven]);
251
+ const toggleGiven = useCallback((value) => {
252
+ if (selectedIndex !== null && cells[selectedIndex]?.given === value) {
253
+ erase();
254
+ }
255
+ else {
256
+ setGiven(value);
257
+ }
258
+ }, [selectedIndex, cells, erase, setGiven]);
259
+ // Load a scanned board (givens, pencilmarks, player digits)
260
+ const applyScan = useCallback((scanned) => {
261
+ setCellsFromPuzzle(scanned.original);
262
+ const parsed = parsePencilmarksString(scanned.pencilmarks);
263
+ setPencilmarks(parsed.length === 81
264
+ ? parsed.map(pm => (pm ? [...pm].sort((a, b) => a - b) : []))
265
+ : createEmptyPencilmarks());
266
+ setAutopencil(scanned.autopencil);
267
+ setScannedInput(hasScannedInput(scanned) ? scanned.user : null);
268
+ }, [setCellsFromPuzzle]);
269
+ // Entry cells with pencilmarks merged in (givens never show pencilmarks)
270
+ const displayCells = useMemo(() => cells.map((cell, i) => {
271
+ if (cell.given !== null)
272
+ return cell;
273
+ const pm = pencilmarks[i];
274
+ return pm && pm.length > 0 ? { ...cell, pencilmarks: pm } : cell;
275
+ }), [cells, pencilmarks]);
276
+ const hasPencilmarks = useMemo(() => pencilmarks.some(pm => pm.length > 0), [pencilmarks]);
277
+ const pencilmarksString = useMemo(() => pencilmarksToString(pencilmarks.map(pm => [...pm])), [pencilmarks]);
278
+ const initialPlayState = useMemo(() => {
279
+ if (!scannedInput && !hasPencilmarks)
280
+ return undefined;
281
+ // A given corrected after the scan wins over a scanned digit there.
282
+ const input = scannedInput
283
+ ? Array.from(scannedInput, (ch, i) => cells[i]?.given !== null ? '0' : ch).join('')
284
+ : EMPTY_INPUT;
285
+ const state = { input };
286
+ if (hasPencilmarks) {
287
+ state.pencilmarks = pencilmarksString;
288
+ state.autopencil = autopencil;
289
+ }
290
+ return state;
291
+ }, [scannedInput, hasPencilmarks, pencilmarksString, autopencil, cells]);
292
+ const canValidate = clueCount >= MIN_CLUES && !isValidating;
211
293
  return {
212
294
  cells,
213
295
  selectedIndex,
@@ -222,6 +304,21 @@ export function useBoardEntry({ networkClient, baseUrl, token, }) {
222
304
  reset,
223
305
  clueCount,
224
306
  setCellsFromPuzzle,
307
+ pencilmarks,
308
+ autopencil,
309
+ hasPencilmarks,
310
+ pencilmarksString,
311
+ togglePencilmark,
312
+ displayCells,
313
+ isPencilMode,
314
+ togglePencilMode,
315
+ setPencilMode,
316
+ enterDigit,
317
+ toggleGiven,
318
+ canValidate,
319
+ applyScan,
320
+ scannedInput,
321
+ initialPlayState,
225
322
  };
226
323
  }
227
324
  //# sourceMappingURL=useBoardEntry.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useBoardEntry.js","sourceRoot":"","sources":["../../src/hooks/useBoardEntry.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Hook for managing Sudoku board entry mode state\n * Allows users to enter given clues for a custom puzzle\n */\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport type { SudokuCell } from '../types';\nimport { useSolverValidate } from '@sudobility/sudojo_client';\nimport type { NetworkClient } from '@sudobility/types';\n\nexport interface ValidatedPuzzle {\n puzzle: string;\n solution: string;\n /** Difficulty level (1-12) the solver rated the puzzle at */\n level?: number;\n /** Solver's difficulty_score for the puzzle, when the API sends it */\n difficultyScore?: number | undefined;\n}\n\nexport interface UseBoardEntryOptions {\n networkClient: NetworkClient;\n baseUrl: string;\n token: string;\n}\n\nexport interface UseBoardEntryReturn {\n /** 81 cells with given values only */\n cells: SudokuCell[];\n /** Currently selected cell index */\n selectedIndex: number | null;\n /** Whether validation is in progress */\n isValidating: boolean;\n /** Validation error message key */\n validationError: string | null;\n /** Validated puzzle data (puzzle + solution + solver rating) */\n validatedPuzzle: ValidatedPuzzle | null;\n /** Select a cell by index */\n selectCell: (index: number) => void;\n /** Set a given value at the selected cell */\n setGiven: (value: number) => void;\n /** Erase the selected cell */\n erase: () => void;\n /** Get the puzzle as an 81-character string */\n getPuzzleString: () => string;\n /** Trigger validation */\n validate: () => void;\n /** Reset to empty board */\n reset: () => void;\n /** Number of clues entered */\n clueCount: number;\n /** Set cells from an 81-character puzzle string */\n setCellsFromPuzzle: (puzzle: string) => void;\n}\n\n/**\n * Create an empty 81-cell board for entry mode\n */\nfunction createEmptyBoard(): SudokuCell[] {\n return Array.from({ length: 81 }, (_, index) => ({\n index,\n solution: null,\n given: null,\n input: null,\n pencilmarks: null,\n }));\n}\n\n/**\n * Convert cells to 81-character puzzle string\n * '0' for empty, '1'-'9' for given values\n */\nfunction cellsToPuzzleString(cells: SudokuCell[]): string {\n return cells.map(cell => cell.given?.toString() ?? '0').join('');\n}\n\n/**\n * Count the number of non-empty cells (clues)\n */\nfunction countClues(cells: SudokuCell[]): number {\n return cells.filter(cell => cell.given !== null).length;\n}\n\n/**\n * Create a board from an 81-character puzzle string\n * '0' or '.' for empty, '1'-'9' for given values\n */\nfunction puzzleStringToCells(puzzle: string): SudokuCell[] {\n if (puzzle.length !== 81) {\n return createEmptyBoard();\n }\n\n return Array.from({ length: 81 }, (_, index) => {\n const char = puzzle.charAt(index);\n const value = parseInt(char, 10);\n const given = value >= 1 && value <= 9 ? value : null;\n\n return {\n index,\n solution: null,\n given,\n input: null,\n pencilmarks: null,\n };\n });\n}\n\n/**\n * Classify a validation error message into a translation key\n */\nfunction classifyValidationError(errorMsg: string): string {\n const lower = errorMsg.toLowerCase();\n if (lower.includes('multiple') || lower.includes('not unique')) {\n return 'enter.errors.multipleSolutions';\n }\n if (lower.includes('cannot solve') || lower.includes('no solution')) {\n return 'enter.errors.noSolution';\n }\n return 'enter.errors.validationFailed';\n}\n\nexport function useBoardEntry({\n networkClient,\n baseUrl,\n token,\n}: UseBoardEntryOptions): UseBoardEntryReturn {\n const [cells, setCells] = useState<SudokuCell[]>(createEmptyBoard);\n const [selectedIndex, setSelectedIndex] = useState<number | null>(null);\n const [validationError, setValidationError] = useState<string | null>(null);\n const [validatedPuzzle, setValidatedPuzzle] =\n useState<ValidatedPuzzle | null>(null);\n const [shouldValidate, setShouldValidate] = useState(false);\n\n // Compute puzzle string for validation\n const puzzleString = useMemo(() => cellsToPuzzleString(cells), [cells]);\n const clueCount = useMemo(() => countClues(cells), [cells]);\n\n // Use the solver validate hook\n const {\n data: validateData,\n error: validateError,\n isLoading: isValidating,\n } = useSolverValidate(\n networkClient,\n baseUrl,\n token,\n { original: puzzleString },\n {\n enabled: shouldValidate && clueCount >= 17,\n retry: false,\n }\n );\n\n // Track whether we've already processed this validation result\n const processedDataRef = useRef<typeof validateData | null>(null);\n const processedErrorRef = useRef<typeof validateError | null>(null);\n\n // Handle validation result\n useEffect(() => {\n if (!shouldValidate || isValidating) return;\n if (!validateData) return;\n // Avoid re-processing the same result\n if (processedDataRef.current === validateData) return;\n processedDataRef.current = validateData;\n\n // ValidateData has board.solution\n const board = validateData.data?.board;\n const solution = board?.solution;\n if (validateData.success && solution) {\n setValidatedPuzzle({\n puzzle: puzzleString,\n solution,\n level: board.level,\n difficultyScore: board.difficulty_score,\n });\n setValidationError(null);\n } else if (validateData.error) {\n // Validation failed with specific error\n setValidationError(classifyValidationError(validateData.error));\n setValidatedPuzzle(null);\n } else {\n // Unexpected response - success but no solution\n console.error('Unexpected validation response:', validateData);\n setValidationError('enter.errors.validationFailed');\n setValidatedPuzzle(null);\n }\n setShouldValidate(false);\n }, [validateData, shouldValidate, isValidating, puzzleString]);\n\n // Handle network/query errors (e.g. 400 responses throw NetworkError)\n useEffect(() => {\n if (!shouldValidate || isValidating) return;\n if (!validateError) return;\n if (processedErrorRef.current === validateError) return;\n processedErrorRef.current = validateError;\n\n // Extract error message from NetworkError response body or message\n const networkErr = validateError as {\n response?: { error?: string };\n message?: string;\n };\n const errorMsg = networkErr.response?.error ?? networkErr.message ?? '';\n setValidationError(classifyValidationError(errorMsg));\n setValidatedPuzzle(null);\n setShouldValidate(false);\n }, [validateError, shouldValidate, isValidating]);\n\n // Select a cell\n const selectCell = useCallback((index: number) => {\n if (index >= 0 && index < 81) {\n setSelectedIndex(index);\n }\n }, []);\n\n // Set a given value at the selected cell\n const setGiven = useCallback(\n (value: number) => {\n if (selectedIndex === null || value < 1 || value > 9) return;\n\n setCells(prevCells => {\n const newCells = [...prevCells];\n const existing = newCells[selectedIndex];\n if (existing) {\n newCells[selectedIndex] = { ...existing, given: value };\n }\n return newCells;\n });\n\n // Clear validation state when board changes\n setValidationError(null);\n setValidatedPuzzle(null);\n },\n [selectedIndex]\n );\n\n // Erase the selected cell\n const erase = useCallback(() => {\n if (selectedIndex === null) return;\n\n setCells(prevCells => {\n const newCells = [...prevCells];\n const existing = newCells[selectedIndex];\n if (existing) {\n newCells[selectedIndex] = { ...existing, given: null };\n }\n return newCells;\n });\n\n // Clear validation state when board changes\n setValidationError(null);\n setValidatedPuzzle(null);\n }, [selectedIndex]);\n\n // Get puzzle string\n const getPuzzleString = useCallback(() => {\n return puzzleString;\n }, [puzzleString]);\n\n // Trigger validation\n const validate = useCallback(() => {\n // Check minimum clues\n if (clueCount < 17) {\n setValidationError('enter.errors.notEnoughClues');\n return;\n }\n\n // Clear previous state and trigger validation\n setValidationError(null);\n setValidatedPuzzle(null);\n // Clear processed refs so cached React Query results are re-processed\n processedDataRef.current = null;\n processedErrorRef.current = null;\n setShouldValidate(true);\n }, [clueCount]);\n\n // Reset to empty board\n const reset = useCallback(() => {\n setCells(createEmptyBoard());\n setSelectedIndex(null);\n setValidationError(null);\n setValidatedPuzzle(null);\n setShouldValidate(false);\n processedDataRef.current = null;\n processedErrorRef.current = null;\n }, []);\n\n // Set cells from puzzle string\n const setCellsFromPuzzle = useCallback((puzzle: string) => {\n setCells(puzzleStringToCells(puzzle));\n setSelectedIndex(null);\n setValidationError(null);\n setValidatedPuzzle(null);\n setShouldValidate(false);\n processedDataRef.current = null;\n processedErrorRef.current = null;\n }, []);\n\n return {\n cells,\n selectedIndex,\n isValidating,\n validationError,\n validatedPuzzle,\n selectCell,\n setGiven,\n erase,\n getPuzzleString,\n validate,\n reset,\n clueCount,\n setCellsFromPuzzle,\n };\n}\n"]}
1
+ {"version":3,"file":"useBoardEntry.js","sourceRoot":"","sources":["../../src/hooks/useBoardEntry.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Hook for managing Sudoku board entry mode state\n * Allows users to enter given clues for a custom puzzle\n */\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport type { SudokuCell } from '../types';\nimport { createEmptyBoard } from '../types';\nimport { useSolverValidate } from '@sudobility/sudojo_client';\nimport { MIN_CLUES } from '@sudobility/sudojo_types';\nimport { cellsToPuzzleString, countClues } from '../utils/sudokuScrambler';\nimport type { NetworkClient } from '@sudobility/types';\nimport { useResolvedSudojoApi } from '../context/SudojoApiContext';\nimport { hasScannedInput, type ScannedBoard } from '../utils/scannedBoard';\nimport {\n parsePencilmarksString,\n pencilmarksToString,\n} from '../utils/techniqueWalkthrough';\n\nexport interface ValidatedPuzzle {\n puzzle: string;\n solution: string;\n /** Difficulty level (1-12) the solver rated the puzzle at */\n level?: number;\n /** Solver's difficulty_score for the puzzle, when the API sends it */\n difficultyScore?: number | undefined;\n}\n\nexport interface UseBoardEntryOptions {\n /** Network client for API calls (default: SudojoApiProvider) */\n networkClient?: NetworkClient | undefined;\n /** Base URL for the Sudojo API (default: SudojoApiProvider) */\n baseUrl?: string | undefined;\n /** Access token (default: SudojoApiProvider) */\n token?: string | null | undefined;\n}\n\n/**\n * What the game should apply once the entered puzzle is validated: the\n * scanned player digits and/or the entry pencilmarks. Pass the fields to the\n * game as initialInput / initialPencilmarks / initialAutoPencilmarks (or to\n * useSudoku's applyHintData).\n */\nexport interface BoardEntryPlayState {\n /** 81-char player input ('0' = none). '0'.repeat(81) when only pencilmarks exist */\n input?: string;\n /** 81 comma-separated pencilmark entries */\n pencilmarks?: string;\n /** Whether the pencilmarks were auto-filled */\n autopencil?: boolean;\n}\n\nexport interface UseBoardEntryReturn {\n /** 81 cells with given values only */\n cells: SudokuCell[];\n /** Currently selected cell index */\n selectedIndex: number | null;\n /** Whether validation is in progress */\n isValidating: boolean;\n /** Validation error message key */\n validationError: string | null;\n /** Validated puzzle data (puzzle + solution + solver rating) */\n validatedPuzzle: ValidatedPuzzle | null;\n /** Select a cell by index */\n selectCell: (index: number) => void;\n /** Set a given value at the selected cell */\n setGiven: (value: number) => void;\n /** Erase the selected cell */\n erase: () => void;\n /** Get the puzzle as an 81-character string */\n getPuzzleString: () => string;\n /** Trigger validation */\n validate: () => void;\n /** Reset to empty board */\n reset: () => void;\n /** Number of clues entered */\n clueCount: number;\n /** Set cells from an 81-character puzzle string (clears pencilmarks and scanned input) */\n setCellsFromPuzzle: (puzzle: string) => void;\n\n // --- Entry pencilmarks ---------------------------------------------------\n /** Per-cell entry pencilmarks (81 sorted digit arrays; [] = none) */\n pencilmarks: number[][];\n /** Whether the pencilmarks were auto-filled (from a scan) */\n autopencil: boolean;\n /** Whether any cell has pencilmarks */\n hasPencilmarks: boolean;\n /** Pencilmarks as 81 comma-separated entries (e.g. \"12,,9,...\") */\n pencilmarksString: string;\n /** Toggle a pencilmark digit on the selected cell (ignored on givens) */\n togglePencilmark: (digit: number) => void;\n /** `cells` with the entry pencilmarks merged in, for display */\n displayCells: SudokuCell[];\n /** Whether digit input goes to pencilmarks instead of givens */\n isPencilMode: boolean;\n /** Toggle pencil mode */\n togglePencilMode: () => void;\n /** Set pencil mode */\n setPencilMode: (on: boolean) => void;\n /**\n * Route a digit: in pencil mode toggle a pencilmark, otherwise set a given\n * (web/RN entry keypad behavior).\n */\n enterDigit: (value: number) => void;\n\n // --- Givens --------------------------------------------------------------\n /**\n * Set a given at the selected cell, or erase it when the cell already holds\n * that digit (extension keypad behavior).\n */\n toggleGiven: (value: number) => void;\n /** Whether Validate may run (enough clues and not already validating) */\n canValidate: boolean;\n\n // --- Scans ---------------------------------------------------------------\n /**\n * Load a scanned board: givens from `original`, pencilmarks and autopencil\n * from the scan, and the player's digits as `scannedInput`.\n */\n applyScan: (scanned: ScannedBoard) => void;\n /** The scanned player digits (81 chars), or null when the scan had none */\n scannedInput: string | null;\n /**\n * What the game should apply once validated (scanned input and/or entry\n * pencilmarks), or undefined when there is nothing to restore.\n */\n initialPlayState: BoardEntryPlayState | undefined;\n}\n\n/**\n * Create an empty 81-cell board for entry mode\n */\nfunction createEmptyCells(): SudokuCell[] {\n return createEmptyBoard().cells;\n}\n\n/**\n * Create a board from an 81-character puzzle string\n * '0' or '.' for empty, '1'-'9' for given values\n */\nfunction puzzleStringToCells(puzzle: string): SudokuCell[] {\n if (puzzle.length !== 81) {\n return createEmptyCells();\n }\n\n return Array.from({ length: 81 }, (_, index) => {\n const char = puzzle.charAt(index);\n const value = parseInt(char, 10);\n const given = value >= 1 && value <= 9 ? value : null;\n\n return {\n index,\n solution: null,\n given,\n input: null,\n pencilmarks: null,\n };\n });\n}\n\n/**\n * Classify a validation error message into a translation key\n */\nfunction classifyValidationError(errorMsg: string): string {\n const lower = errorMsg.toLowerCase();\n if (lower.includes('multiple') || lower.includes('not unique')) {\n return 'enter.errors.multipleSolutions';\n }\n if (lower.includes('cannot solve') || lower.includes('no solution')) {\n return 'enter.errors.noSolution';\n }\n return 'enter.errors.validationFailed';\n}\n\nfunction createEmptyPencilmarks(): number[][] {\n return Array.from({ length: 81 }, () => []);\n}\n\nconst EMPTY_INPUT = '0'.repeat(81);\n\nexport function useBoardEntry(\n options: UseBoardEntryOptions = {}\n): UseBoardEntryReturn {\n const { networkClient, baseUrl, token } = useResolvedSudojoApi(\n options,\n 'useBoardEntry'\n );\n const [cells, setCells] = useState<SudokuCell[]>(createEmptyCells);\n const [pencilmarks, setPencilmarks] = useState<number[][]>(\n createEmptyPencilmarks\n );\n const [autopencil, setAutopencil] = useState(false);\n const [scannedInput, setScannedInput] = useState<string | null>(null);\n const [isPencilMode, setIsPencilMode] = useState(false);\n const [selectedIndex, setSelectedIndex] = useState<number | null>(null);\n const [validationError, setValidationError] = useState<string | null>(null);\n const [validatedPuzzle, setValidatedPuzzle] =\n useState<ValidatedPuzzle | null>(null);\n const [shouldValidate, setShouldValidate] = useState(false);\n\n // Compute puzzle string for validation\n const puzzleString = useMemo(() => cellsToPuzzleString(cells), [cells]);\n const clueCount = useMemo(() => countClues(cells), [cells]);\n\n // Use the solver validate hook\n const {\n data: validateData,\n error: validateError,\n isLoading: isValidating,\n } = useSolverValidate(\n networkClient,\n baseUrl,\n token,\n { original: puzzleString },\n {\n enabled: shouldValidate && clueCount >= MIN_CLUES,\n retry: false,\n }\n );\n\n // Track whether we've already processed this validation result\n const processedDataRef = useRef<typeof validateData | null>(null);\n const processedErrorRef = useRef<typeof validateError | null>(null);\n\n // Handle validation result\n useEffect(() => {\n if (!shouldValidate || isValidating) return;\n if (!validateData) return;\n // Avoid re-processing the same result\n if (processedDataRef.current === validateData) return;\n processedDataRef.current = validateData;\n\n // ValidateData has board.solution\n const board = validateData.data?.board;\n const solution = board?.solution;\n if (validateData.success && solution) {\n setValidatedPuzzle({\n puzzle: puzzleString,\n solution,\n level: board.level,\n difficultyScore: board.difficulty_score,\n });\n setValidationError(null);\n } else if (validateData.error) {\n // Validation failed with specific error\n setValidationError(classifyValidationError(validateData.error));\n setValidatedPuzzle(null);\n } else {\n // Unexpected response - success but no solution\n console.error('Unexpected validation response:', validateData);\n setValidationError('enter.errors.validationFailed');\n setValidatedPuzzle(null);\n }\n setShouldValidate(false);\n }, [validateData, shouldValidate, isValidating, puzzleString]);\n\n // Handle network/query errors (e.g. 400 responses throw NetworkError)\n useEffect(() => {\n if (!shouldValidate || isValidating) return;\n if (!validateError) return;\n if (processedErrorRef.current === validateError) return;\n processedErrorRef.current = validateError;\n\n // Extract error message from NetworkError response body or message\n const networkErr = validateError as {\n response?: { error?: string };\n message?: string;\n };\n const errorMsg = networkErr.response?.error ?? networkErr.message ?? '';\n setValidationError(classifyValidationError(errorMsg));\n setValidatedPuzzle(null);\n setShouldValidate(false);\n }, [validateError, shouldValidate, isValidating]);\n\n // Select a cell\n const selectCell = useCallback((index: number) => {\n if (index >= 0 && index < 81) {\n setSelectedIndex(index);\n }\n }, []);\n\n // Set a given value at the selected cell\n const setGiven = useCallback(\n (value: number) => {\n if (selectedIndex === null || value < 1 || value > 9) return;\n\n setCells(prevCells => {\n const newCells = [...prevCells];\n const existing = newCells[selectedIndex];\n if (existing) {\n newCells[selectedIndex] = { ...existing, given: value };\n }\n return newCells;\n });\n // A given replaces the cell's pencilmarks\n setPencilmarks(prev => {\n if (!prev[selectedIndex]?.length) return prev;\n const next = [...prev];\n next[selectedIndex] = [];\n return next;\n });\n\n // Clear validation state when board changes\n setValidationError(null);\n setValidatedPuzzle(null);\n },\n [selectedIndex]\n );\n\n // Erase the selected cell\n const erase = useCallback(() => {\n if (selectedIndex === null) return;\n\n setCells(prevCells => {\n const newCells = [...prevCells];\n const existing = newCells[selectedIndex];\n if (existing) {\n newCells[selectedIndex] = { ...existing, given: null };\n }\n return newCells;\n });\n\n // Clear validation state when board changes\n setValidationError(null);\n setValidatedPuzzle(null);\n }, [selectedIndex]);\n\n // Get puzzle string\n const getPuzzleString = useCallback(() => {\n return puzzleString;\n }, [puzzleString]);\n\n // Trigger validation\n const validate = useCallback(() => {\n // Check minimum clues\n if (clueCount < MIN_CLUES) {\n setValidationError('enter.errors.notEnoughClues');\n return;\n }\n\n // Clear previous state and trigger validation\n setValidationError(null);\n setValidatedPuzzle(null);\n // Clear processed refs so cached React Query results are re-processed\n processedDataRef.current = null;\n processedErrorRef.current = null;\n setShouldValidate(true);\n }, [clueCount]);\n\n // Reset to empty board\n const reset = useCallback(() => {\n setCells(createEmptyCells());\n setPencilmarks(createEmptyPencilmarks());\n setAutopencil(false);\n setScannedInput(null);\n setIsPencilMode(false);\n setSelectedIndex(null);\n setValidationError(null);\n setValidatedPuzzle(null);\n setShouldValidate(false);\n processedDataRef.current = null;\n processedErrorRef.current = null;\n }, []);\n\n // Set cells from puzzle string\n const setCellsFromPuzzle = useCallback((puzzle: string) => {\n setCells(puzzleStringToCells(puzzle));\n setPencilmarks(createEmptyPencilmarks());\n setAutopencil(false);\n setScannedInput(null);\n setSelectedIndex(null);\n setValidationError(null);\n setValidatedPuzzle(null);\n setShouldValidate(false);\n processedDataRef.current = null;\n processedErrorRef.current = null;\n }, []);\n\n // Toggle a pencilmark digit on the selected (non-given) cell\n const togglePencilmark = useCallback(\n (digit: number) => {\n if (selectedIndex === null || digit < 1 || digit > 9) return;\n if (cells[selectedIndex]?.given !== null) return;\n setPencilmarks(prev => {\n const next = [...prev];\n const current = next[selectedIndex] ?? [];\n next[selectedIndex] = current.includes(digit)\n ? current.filter(d => d !== digit)\n : [...current, digit].sort((a, b) => a - b);\n return next;\n });\n },\n [selectedIndex, cells]\n );\n\n const togglePencilMode = useCallback(() => {\n setIsPencilMode(prev => !prev);\n }, []);\n\n const setPencilMode = useCallback((on: boolean) => {\n setIsPencilMode(on);\n }, []);\n\n const enterDigit = useCallback(\n (value: number) => {\n if (isPencilMode) {\n togglePencilmark(value);\n } else {\n setGiven(value);\n }\n },\n [isPencilMode, togglePencilmark, setGiven]\n );\n\n const toggleGiven = useCallback(\n (value: number) => {\n if (selectedIndex !== null && cells[selectedIndex]?.given === value) {\n erase();\n } else {\n setGiven(value);\n }\n },\n [selectedIndex, cells, erase, setGiven]\n );\n\n // Load a scanned board (givens, pencilmarks, player digits)\n const applyScan = useCallback(\n (scanned: ScannedBoard) => {\n setCellsFromPuzzle(scanned.original);\n const parsed = parsePencilmarksString(scanned.pencilmarks);\n setPencilmarks(\n parsed.length === 81\n ? parsed.map(pm => (pm ? [...pm].sort((a, b) => a - b) : []))\n : createEmptyPencilmarks()\n );\n setAutopencil(scanned.autopencil);\n setScannedInput(hasScannedInput(scanned) ? scanned.user : null);\n },\n [setCellsFromPuzzle]\n );\n\n // Entry cells with pencilmarks merged in (givens never show pencilmarks)\n const displayCells = useMemo(\n () =>\n cells.map((cell, i) => {\n if (cell.given !== null) return cell;\n const pm = pencilmarks[i];\n return pm && pm.length > 0 ? { ...cell, pencilmarks: pm } : cell;\n }),\n [cells, pencilmarks]\n );\n\n const hasPencilmarks = useMemo(\n () => pencilmarks.some(pm => pm.length > 0),\n [pencilmarks]\n );\n\n const pencilmarksString = useMemo(\n () => pencilmarksToString(pencilmarks.map(pm => [...pm])),\n [pencilmarks]\n );\n\n const initialPlayState = useMemo((): BoardEntryPlayState | undefined => {\n if (!scannedInput && !hasPencilmarks) return undefined;\n // A given corrected after the scan wins over a scanned digit there.\n const input = scannedInput\n ? Array.from(scannedInput, (ch, i) =>\n cells[i]?.given !== null ? '0' : ch\n ).join('')\n : EMPTY_INPUT;\n const state: BoardEntryPlayState = { input };\n if (hasPencilmarks) {\n state.pencilmarks = pencilmarksString;\n state.autopencil = autopencil;\n }\n return state;\n }, [scannedInput, hasPencilmarks, pencilmarksString, autopencil, cells]);\n\n const canValidate = clueCount >= MIN_CLUES && !isValidating;\n\n return {\n cells,\n selectedIndex,\n isValidating,\n validationError,\n validatedPuzzle,\n selectCell,\n setGiven,\n erase,\n getPuzzleString,\n validate,\n reset,\n clueCount,\n setCellsFromPuzzle,\n pencilmarks,\n autopencil,\n hasPencilmarks,\n pencilmarksString,\n togglePencilmark,\n displayCells,\n isPencilMode,\n togglePencilMode,\n setPencilMode,\n enterDigit,\n toggleGiven,\n canValidate,\n applyScan,\n scannedInput,\n initialPlayState,\n };\n}\n"]}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Hook for scanning a Sudoku board from an image.
3
+ *
4
+ * The request goes through sudojo_client's `useSudojoOcrExtract`; this hook
5
+ * turns the response into a ScannedBoard (givens, the player's digits and
6
+ * pencilmarks) with `scannedBoardFromResponse`.
7
+ */
8
+ import type { OcrSource } from '@sudobility/sudojo_types';
9
+ import type { NetworkClient } from '@sudobility/types';
10
+ import { type ScannedBoard } from '../utils/scannedBoard';
11
+ export interface UseBoardScanOptions {
12
+ /** Network client for API calls (default: SudojoApiProvider) */
13
+ networkClient?: NetworkClient | undefined;
14
+ /** Base URL for the Sudojo API (default: SudojoApiProvider) */
15
+ baseUrl?: string | undefined;
16
+ }
17
+ export interface UseBoardScanReturn {
18
+ /**
19
+ * Read the board in an image.
20
+ * @throws ScanBoardError
21
+ */
22
+ scan: (token: string, image: string, source?: OcrSource) => Promise<ScannedBoard>;
23
+ /** Whether a scan is in flight */
24
+ isScanning: boolean;
25
+ }
26
+ export declare function useBoardScan(options?: UseBoardScanOptions): UseBoardScanReturn;
27
+ //# sourceMappingURL=useBoardScan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useBoardScan.d.ts","sourceRoot":"","sources":["../../src/hooks/useBoardScan.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD,OAAO,EAEL,KAAK,YAAY,EAElB,MAAM,uBAAuB,CAAC;AAE/B,MAAM,WAAW,mBAAmB;IAClC,gEAAgE;IAChE,aAAa,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IAC1C,+DAA+D;IAC/D,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC9B;AAED,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,IAAI,EAAE,CACJ,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,MAAM,CAAC,EAAE,SAAS,KACf,OAAO,CAAC,YAAY,CAAC,CAAC;IAC3B,kCAAkC;IAClC,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,wBAAgB,YAAY,CAC1B,OAAO,GAAE,mBAAwB,GAChC,kBAAkB,CA4BpB"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Hook for scanning a Sudoku board from an image.
3
+ *
4
+ * The request goes through sudojo_client's `useSudojoOcrExtract`; this hook
5
+ * turns the response into a ScannedBoard (givens, the player's digits and
6
+ * pencilmarks) with `scannedBoardFromResponse`.
7
+ */
8
+ import { useCallback } from 'react';
9
+ import { useSudojoOcrExtract } from '@sudobility/sudojo_client';
10
+ import { useResolvedSudojoApi } from '../context/SudojoApiContext';
11
+ import { ScanBoardError, scannedBoardFromResponse, } from '../utils/scannedBoard';
12
+ export function useBoardScan(options = {}) {
13
+ const { networkClient, baseUrl } = useResolvedSudojoApi(options, 'useBoardScan');
14
+ const { mutateAsync, isPending } = useSudojoOcrExtract(networkClient, baseUrl);
15
+ const scan = useCallback(async (token, image, source = 'library') => {
16
+ let response;
17
+ try {
18
+ response = await mutateAsync({ token, image, source });
19
+ }
20
+ catch (error) {
21
+ throw new ScanBoardError('REQUEST_FAILED', error instanceof Error ? error.message : String(error), { cause: error });
22
+ }
23
+ return scannedBoardFromResponse(response);
24
+ }, [mutateAsync]);
25
+ return { scan, isScanning: isPending };
26
+ }
27
+ //# sourceMappingURL=useBoardScan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useBoardScan.js","sourceRoot":"","sources":["../../src/hooks/useBoardScan.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AACpC,OAAO,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAGhE,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,EACL,cAAc,EAEd,wBAAwB,GACzB,MAAM,uBAAuB,CAAC;AAuB/B,MAAM,UAAU,YAAY,CAC1B,UAA+B,EAAE;IAEjC,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,GAAG,oBAAoB,CACrD,OAAO,EACP,cAAc,CACf,CAAC;IACF,MAAM,EAAE,WAAW,EAAE,SAAS,EAAE,GAAG,mBAAmB,CACpD,aAAa,EACb,OAAO,CACR,CAAC;IAEF,MAAM,IAAI,GAAG,WAAW,CACtB,KAAK,EAAE,KAAa,EAAE,KAAa,EAAE,SAAoB,SAAS,EAAE,EAAE;QACpE,IAAI,QAAQ,CAAC;QACb,IAAI,CAAC;YACH,QAAQ,GAAG,MAAM,WAAW,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;QACzD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,cAAc,CACtB,gBAAgB,EAChB,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EACtD,EAAE,KAAK,EAAE,KAAK,EAAE,CACjB,CAAC;QACJ,CAAC;QACD,OAAO,wBAAwB,CAAC,QAAQ,CAAC,CAAC;IAC5C,CAAC,EACD,CAAC,WAAW,CAAC,CACd,CAAC;IAEF,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,CAAC;AACzC,CAAC","sourcesContent":["/**\n * Hook for scanning a Sudoku board from an image.\n *\n * The request goes through sudojo_client's `useSudojoOcrExtract`; this hook\n * turns the response into a ScannedBoard (givens, the player's digits and\n * pencilmarks) with `scannedBoardFromResponse`.\n */\n\nimport { useCallback } from 'react';\nimport { useSudojoOcrExtract } from '@sudobility/sudojo_client';\nimport type { OcrSource } from '@sudobility/sudojo_types';\nimport type { NetworkClient } from '@sudobility/types';\nimport { useResolvedSudojoApi } from '../context/SudojoApiContext';\nimport {\n ScanBoardError,\n type ScannedBoard,\n scannedBoardFromResponse,\n} from '../utils/scannedBoard';\n\nexport interface UseBoardScanOptions {\n /** Network client for API calls (default: SudojoApiProvider) */\n networkClient?: NetworkClient | undefined;\n /** Base URL for the Sudojo API (default: SudojoApiProvider) */\n baseUrl?: string | undefined;\n}\n\nexport interface UseBoardScanReturn {\n /**\n * Read the board in an image.\n * @throws ScanBoardError\n */\n scan: (\n token: string,\n image: string,\n source?: OcrSource\n ) => Promise<ScannedBoard>;\n /** Whether a scan is in flight */\n isScanning: boolean;\n}\n\nexport function useBoardScan(\n options: UseBoardScanOptions = {}\n): UseBoardScanReturn {\n const { networkClient, baseUrl } = useResolvedSudojoApi(\n options,\n 'useBoardScan'\n );\n const { mutateAsync, isPending } = useSudojoOcrExtract(\n networkClient,\n baseUrl\n );\n\n const scan = useCallback(\n async (token: string, image: string, source: OcrSource = 'library') => {\n let response;\n try {\n response = await mutateAsync({ token, image, source });\n } catch (error) {\n throw new ScanBoardError(\n 'REQUEST_FAILED',\n error instanceof Error ? error.message : String(error),\n { cause: error }\n );\n }\n return scannedBoardFromResponse(response);\n },\n [mutateAsync]\n );\n\n return { scan, isScanning: isPending };\n}\n"]}
@@ -4,10 +4,10 @@
4
4
  import type { Community } from '@sudobility/sudojo_types';
5
5
  import type { NetworkClient } from '@sudobility/types';
6
6
  export interface UseCommunitiesOptions {
7
- /** Network client for API calls */
8
- networkClient: NetworkClient;
9
- /** Base URL for the Sudojo API */
10
- baseUrl: string;
7
+ /** Network client for API calls (default: SudojoApiProvider) */
8
+ networkClient?: NetworkClient | undefined;
9
+ /** Base URL for the Sudojo API (default: SudojoApiProvider) */
10
+ baseUrl?: string | undefined;
11
11
  /** Access token for authentication (optional for this public endpoint) */
12
12
  token?: string;
13
13
  /** Language code to filter communities (e.g., "en", "ja") */
@@ -26,6 +26,8 @@ export interface UseCommunitiesResult {
26
26
  refetch: () => void;
27
27
  /** Communities grouped by platform type */
28
28
  communitiesByPlatform: Map<string, Community[]>;
29
+ /** Platform ids present in communitiesByPlatform, in display order (sortCommunityPlatforms) */
30
+ sortedPlatforms: string[];
29
31
  }
30
32
  /**
31
33
  * Hook for fetching communities filtered by language.
@@ -1 +1 @@
1
- {"version":3,"file":"useCommunities.d.ts","sourceRoot":"","sources":["../../src/hooks/useCommunities.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAGvD,MAAM,WAAW,qBAAqB;IACpC,mCAAmC;IACnC,aAAa,EAAE,aAAa,CAAC;IAC7B,kCAAkC;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,0EAA0E;IAC1E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kCAAkC;IAClC,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,oBAAoB;IACnC,iDAAiD;IACjD,WAAW,EAAE,SAAS,EAAE,CAAC;IACzB,sCAAsC;IACtC,SAAS,EAAE,OAAO,CAAC;IACnB,8BAA8B;IAC9B,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,0BAA0B;IAC1B,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,2CAA2C;IAC3C,qBAAqB,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;CACjD;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,qBAAqB,GAC7B,oBAAoB,CA+CtB"}
1
+ {"version":3,"file":"useCommunities.d.ts","sourceRoot":"","sources":["../../src/hooks/useCommunities.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAKvD,MAAM,WAAW,qBAAqB;IACpC,gEAAgE;IAChE,aAAa,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IAC1C,+DAA+D;IAC/D,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,0EAA0E;IAC1E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kCAAkC;IAClC,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,oBAAoB;IACnC,iDAAiD;IACjD,WAAW,EAAE,SAAS,EAAE,CAAC;IACzB,sCAAsC;IACtC,SAAS,EAAE,OAAO,CAAC;IACnB,8BAA8B;IAC9B,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,0BAA0B;IAC1B,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,2CAA2C;IAC3C,qBAAqB,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;IAChD,+FAA+F;IAC/F,eAAe,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,qBAAqB,GAC7B,oBAAoB,CAmDtB"}
@@ -2,7 +2,9 @@
2
2
  * Hook for fetching communities filtered by language
3
3
  */
4
4
  import { useMemo } from 'react';
5
+ import { useResolvedSudojoApi } from '../context/SudojoApiContext';
5
6
  import { useSudojoCommunities } from '@sudobility/sudojo_client';
7
+ import { sortCommunityPlatforms } from '../utils/community';
6
8
  /**
7
9
  * Hook for fetching communities filtered by language.
8
10
  *
@@ -14,7 +16,8 @@ import { useSudojoCommunities } from '@sudobility/sudojo_client';
14
16
  * @returns Communities data and utilities
15
17
  */
16
18
  export function useCommunities(options) {
17
- const { networkClient, baseUrl, token = '', language, enabled = true, } = options;
19
+ const { language, enabled = true } = options;
20
+ const { networkClient, baseUrl, token } = useResolvedSudojoApi(options, 'useCommunities');
18
21
  const queryParams = useMemo(() => {
19
22
  if (language === undefined)
20
23
  return undefined;
@@ -36,6 +39,7 @@ export function useCommunities(options) {
36
39
  }
37
40
  return byPlatform;
38
41
  }, [communities]);
42
+ const sortedPlatforms = useMemo(() => sortCommunityPlatforms(communitiesByPlatform.keys()), [communitiesByPlatform]);
39
43
  return {
40
44
  communities,
41
45
  isLoading,
@@ -44,6 +48,7 @@ export function useCommunities(options) {
44
48
  refetch();
45
49
  },
46
50
  communitiesByPlatform,
51
+ sortedPlatforms,
47
52
  };
48
53
  }
49
54
  //# sourceMappingURL=useCommunities.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useCommunities.js","sourceRoot":"","sources":["../../src/hooks/useCommunities.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AAGhC,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AA4BjE;;;;;;;;;GASG;AACH,MAAM,UAAU,cAAc,CAC5B,OAA8B;IAE9B,MAAM,EACJ,aAAa,EACb,OAAO,EACP,KAAK,GAAG,EAAE,EACV,QAAQ,EACR,OAAO,GAAG,IAAI,GACf,GAAG,OAAO,CAAC;IAEZ,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,EAAE;QAC/B,IAAI,QAAQ,KAAK,SAAS;YAAE,OAAO,SAAS,CAAC;QAC7C,OAAO,EAAE,QAAQ,EAAE,CAAC;IACtB,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;IAEf,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,oBAAoB,CAC9D,aAAa,EACb,OAAO,EACP,KAAK,EACL,WAAW,EACX,EAAE,OAAO,EAAE,CACZ,CAAC;IAEF,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,EAAE;QAC/B,IAAI,CAAC,IAAI,EAAE,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI;YAAE,OAAO,EAAE,CAAC;QAC5C,OAAO,IAAI,CAAC,IAAI,CAAC;IACnB,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;IAEX,MAAM,qBAAqB,GAAG,OAAO,CAAC,GAAG,EAAE;QACzC,MAAM,UAAU,GAAG,IAAI,GAAG,EAAuB,CAAC;QAClD,KAAK,MAAM,SAAS,IAAI,WAAW,EAAE,CAAC;YACpC,MAAM,GAAG,GAAG,SAAS,CAAC,QAAQ,CAAC;YAC/B,MAAM,QAAQ,GAAG,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;YAC3C,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YACzB,UAAU,CAAC,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;QAChC,CAAC;QACD,OAAO,UAAU,CAAC;IACpB,CAAC,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC;IAElB,OAAO;QACL,WAAW;QACX,SAAS;QACT,KAAK,EAAE,KAAK,IAAI,IAAI;QACpB,OAAO,EAAE,GAAG,EAAE;YACZ,OAAO,EAAE,CAAC;QACZ,CAAC;QACD,qBAAqB;KACtB,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Hook for fetching communities filtered by language\n */\n\nimport { useMemo } from 'react';\nimport type { Community } from '@sudobility/sudojo_types';\nimport type { NetworkClient } from '@sudobility/types';\nimport { useSudojoCommunities } from '@sudobility/sudojo_client';\n\nexport interface UseCommunitiesOptions {\n /** Network client for API calls */\n networkClient: NetworkClient;\n /** Base URL for the Sudojo API */\n baseUrl: string;\n /** Access token for authentication (optional for this public endpoint) */\n token?: string;\n /** Language code to filter communities (e.g., \"en\", \"ja\") */\n language?: string;\n /** Whether to enable the query */\n enabled?: boolean;\n}\n\nexport interface UseCommunitiesResult {\n /** All communities for the specified language */\n communities: Community[];\n /** Whether communities are loading */\n isLoading: boolean;\n /** Error if loading failed */\n error: Error | null;\n /** Refetch communities */\n refetch: () => void;\n /** Communities grouped by platform type */\n communitiesByPlatform: Map<string, Community[]>;\n}\n\n/**\n * Hook for fetching communities filtered by language.\n *\n * Unlike techniques/levels where the same records are translated,\n * communities are entirely different records per language.\n * Switching language triggers a new fetch with different results.\n *\n * @param options - Hook options\n * @returns Communities data and utilities\n */\nexport function useCommunities(\n options: UseCommunitiesOptions\n): UseCommunitiesResult {\n const {\n networkClient,\n baseUrl,\n token = '',\n language,\n enabled = true,\n } = options;\n\n const queryParams = useMemo(() => {\n if (language === undefined) return undefined;\n return { language };\n }, [language]);\n\n const { data, isLoading, error, refetch } = useSudojoCommunities(\n networkClient,\n baseUrl,\n token,\n queryParams,\n { enabled }\n );\n\n const communities = useMemo(() => {\n if (!data?.success || !data.data) return [];\n return data.data;\n }, [data]);\n\n const communitiesByPlatform = useMemo(() => {\n const byPlatform = new Map<string, Community[]>();\n for (const community of communities) {\n const key = community.platform;\n const existing = byPlatform.get(key) ?? [];\n existing.push(community);\n byPlatform.set(key, existing);\n }\n return byPlatform;\n }, [communities]);\n\n return {\n communities,\n isLoading,\n error: error ?? null,\n refetch: () => {\n refetch();\n },\n communitiesByPlatform,\n };\n}\n"]}
1
+ {"version":3,"file":"useCommunities.js","sourceRoot":"","sources":["../../src/hooks/useCommunities.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AAGhC,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AA8B5D;;;;;;;;;GASG;AACH,MAAM,UAAU,cAAc,CAC5B,OAA8B;IAE9B,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,IAAI,EAAE,GAAG,OAAO,CAAC;IAC7C,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,oBAAoB,CAC5D,OAAO,EACP,gBAAgB,CACjB,CAAC;IAEF,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,EAAE;QAC/B,IAAI,QAAQ,KAAK,SAAS;YAAE,OAAO,SAAS,CAAC;QAC7C,OAAO,EAAE,QAAQ,EAAE,CAAC;IACtB,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;IAEf,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,oBAAoB,CAC9D,aAAa,EACb,OAAO,EACP,KAAK,EACL,WAAW,EACX,EAAE,OAAO,EAAE,CACZ,CAAC;IAEF,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,EAAE;QAC/B,IAAI,CAAC,IAAI,EAAE,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI;YAAE,OAAO,EAAE,CAAC;QAC5C,OAAO,IAAI,CAAC,IAAI,CAAC;IACnB,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;IAEX,MAAM,qBAAqB,GAAG,OAAO,CAAC,GAAG,EAAE;QACzC,MAAM,UAAU,GAAG,IAAI,GAAG,EAAuB,CAAC;QAClD,KAAK,MAAM,SAAS,IAAI,WAAW,EAAE,CAAC;YACpC,MAAM,GAAG,GAAG,SAAS,CAAC,QAAQ,CAAC;YAC/B,MAAM,QAAQ,GAAG,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;YAC3C,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YACzB,UAAU,CAAC,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;QAChC,CAAC;QACD,OAAO,UAAU,CAAC;IACpB,CAAC,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC;IAElB,MAAM,eAAe,GAAG,OAAO,CAC7B,GAAG,EAAE,CAAC,sBAAsB,CAAC,qBAAqB,CAAC,IAAI,EAAE,CAAC,EAC1D,CAAC,qBAAqB,CAAC,CACxB,CAAC;IAEF,OAAO;QACL,WAAW;QACX,SAAS;QACT,KAAK,EAAE,KAAK,IAAI,IAAI;QACpB,OAAO,EAAE,GAAG,EAAE;YACZ,OAAO,EAAE,CAAC;QACZ,CAAC;QACD,qBAAqB;QACrB,eAAe;KAChB,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Hook for fetching communities filtered by language\n */\n\nimport { useMemo } from 'react';\nimport type { Community } from '@sudobility/sudojo_types';\nimport type { NetworkClient } from '@sudobility/types';\nimport { useResolvedSudojoApi } from '../context/SudojoApiContext';\nimport { useSudojoCommunities } from '@sudobility/sudojo_client';\nimport { sortCommunityPlatforms } from '../utils/community';\n\nexport interface UseCommunitiesOptions {\n /** Network client for API calls (default: SudojoApiProvider) */\n networkClient?: NetworkClient | undefined;\n /** Base URL for the Sudojo API (default: SudojoApiProvider) */\n baseUrl?: string | undefined;\n /** Access token for authentication (optional for this public endpoint) */\n token?: string;\n /** Language code to filter communities (e.g., \"en\", \"ja\") */\n language?: string;\n /** Whether to enable the query */\n enabled?: boolean;\n}\n\nexport interface UseCommunitiesResult {\n /** All communities for the specified language */\n communities: Community[];\n /** Whether communities are loading */\n isLoading: boolean;\n /** Error if loading failed */\n error: Error | null;\n /** Refetch communities */\n refetch: () => void;\n /** Communities grouped by platform type */\n communitiesByPlatform: Map<string, Community[]>;\n /** Platform ids present in communitiesByPlatform, in display order (sortCommunityPlatforms) */\n sortedPlatforms: string[];\n}\n\n/**\n * Hook for fetching communities filtered by language.\n *\n * Unlike techniques/levels where the same records are translated,\n * communities are entirely different records per language.\n * Switching language triggers a new fetch with different results.\n *\n * @param options - Hook options\n * @returns Communities data and utilities\n */\nexport function useCommunities(\n options: UseCommunitiesOptions\n): UseCommunitiesResult {\n const { language, enabled = true } = options;\n const { networkClient, baseUrl, token } = useResolvedSudojoApi(\n options,\n 'useCommunities'\n );\n\n const queryParams = useMemo(() => {\n if (language === undefined) return undefined;\n return { language };\n }, [language]);\n\n const { data, isLoading, error, refetch } = useSudojoCommunities(\n networkClient,\n baseUrl,\n token,\n queryParams,\n { enabled }\n );\n\n const communities = useMemo(() => {\n if (!data?.success || !data.data) return [];\n return data.data;\n }, [data]);\n\n const communitiesByPlatform = useMemo(() => {\n const byPlatform = new Map<string, Community[]>();\n for (const community of communities) {\n const key = community.platform;\n const existing = byPlatform.get(key) ?? [];\n existing.push(community);\n byPlatform.set(key, existing);\n }\n return byPlatform;\n }, [communities]);\n\n const sortedPlatforms = useMemo(\n () => sortCommunityPlatforms(communitiesByPlatform.keys()),\n [communitiesByPlatform]\n );\n\n return {\n communities,\n isLoading,\n error: error ?? null,\n refetch: () => {\n refetch();\n },\n communitiesByPlatform,\n sortedPlatforms,\n };\n}\n"]}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Fire a callback once each time a puzzle becomes completed.
3
+ *
4
+ * Shared latch from the web and RN SudokuGame (and the extension side panel):
5
+ * `onComplete` runs on the false -> true transition of `isCompleted`, never
6
+ * again while it stays true, and the latch re-arms when `isCompleted` goes
7
+ * back to false (a new board, an undo), as in the apps' game components.
8
+ */
9
+ /**
10
+ * @param isCompleted - Whether the puzzle is currently completed
11
+ * @param onComplete - Called once per completion (latest callback is used)
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * useCompletionTrigger(isCompleted, () => {
16
+ * const finalTime = stopTimer();
17
+ * setShowCelebration(true);
18
+ * onComplete?.(finalTime);
19
+ * });
20
+ * ```
21
+ */
22
+ export declare function useCompletionTrigger(isCompleted: boolean, onComplete: () => void): void;
23
+ //# sourceMappingURL=useCompletionTrigger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useCompletionTrigger.d.ts","sourceRoot":"","sources":["../../src/hooks/useCompletionTrigger.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAClC,WAAW,EAAE,OAAO,EACpB,UAAU,EAAE,MAAM,IAAI,GACrB,IAAI,CAeN"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Fire a callback once each time a puzzle becomes completed.
3
+ *
4
+ * Shared latch from the web and RN SudokuGame (and the extension side panel):
5
+ * `onComplete` runs on the false -> true transition of `isCompleted`, never
6
+ * again while it stays true, and the latch re-arms when `isCompleted` goes
7
+ * back to false (a new board, an undo), as in the apps' game components.
8
+ */
9
+ import { useEffect, useRef } from 'react';
10
+ /**
11
+ * @param isCompleted - Whether the puzzle is currently completed
12
+ * @param onComplete - Called once per completion (latest callback is used)
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * useCompletionTrigger(isCompleted, () => {
17
+ * const finalTime = stopTimer();
18
+ * setShowCelebration(true);
19
+ * onComplete?.(finalTime);
20
+ * });
21
+ * ```
22
+ */
23
+ export function useCompletionTrigger(isCompleted, onComplete) {
24
+ const completedRef = useRef(false);
25
+ const onCompleteRef = useRef(onComplete);
26
+ useEffect(() => {
27
+ onCompleteRef.current = onComplete;
28
+ });
29
+ useEffect(() => {
30
+ if (isCompleted && !completedRef.current) {
31
+ completedRef.current = true;
32
+ onCompleteRef.current();
33
+ }
34
+ else if (!isCompleted) {
35
+ completedRef.current = false;
36
+ }
37
+ }, [isCompleted]);
38
+ }
39
+ //# sourceMappingURL=useCompletionTrigger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useCompletionTrigger.js","sourceRoot":"","sources":["../../src/hooks/useCompletionTrigger.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AAE1C;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,oBAAoB,CAClC,WAAoB,EACpB,UAAsB;IAEtB,MAAM,YAAY,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IACnC,MAAM,aAAa,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC;IACzC,SAAS,CAAC,GAAG,EAAE;QACb,aAAa,CAAC,OAAO,GAAG,UAAU,CAAC;IACrC,CAAC,CAAC,CAAC;IAEH,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,WAAW,IAAI,CAAC,YAAY,CAAC,OAAO,EAAE,CAAC;YACzC,YAAY,CAAC,OAAO,GAAG,IAAI,CAAC;YAC5B,aAAa,CAAC,OAAO,EAAE,CAAC;QAC1B,CAAC;aAAM,IAAI,CAAC,WAAW,EAAE,CAAC;YACxB,YAAY,CAAC,OAAO,GAAG,KAAK,CAAC;QAC/B,CAAC;IACH,CAAC,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC;AACpB,CAAC","sourcesContent":["/**\n * Fire a callback once each time a puzzle becomes completed.\n *\n * Shared latch from the web and RN SudokuGame (and the extension side panel):\n * `onComplete` runs on the false -> true transition of `isCompleted`, never\n * again while it stays true, and the latch re-arms when `isCompleted` goes\n * back to false (a new board, an undo), as in the apps' game components.\n */\n\nimport { useEffect, useRef } from 'react';\n\n/**\n * @param isCompleted - Whether the puzzle is currently completed\n * @param onComplete - Called once per completion (latest callback is used)\n *\n * @example\n * ```tsx\n * useCompletionTrigger(isCompleted, () => {\n * const finalTime = stopTimer();\n * setShowCelebration(true);\n * onComplete?.(finalTime);\n * });\n * ```\n */\nexport function useCompletionTrigger(\n isCompleted: boolean,\n onComplete: () => void\n): void {\n const completedRef = useRef(false);\n const onCompleteRef = useRef(onComplete);\n useEffect(() => {\n onCompleteRef.current = onComplete;\n });\n\n useEffect(() => {\n if (isCompleted && !completedRef.current) {\n completedRef.current = true;\n onCompleteRef.current();\n } else if (!isCompleted) {\n completedRef.current = false;\n }\n }, [isCompleted]);\n}\n"]}
@@ -4,14 +4,14 @@
4
4
  */
5
5
  import type { Daily } from '@sudobility/sudojo_types';
6
6
  import type { NetworkClient } from '@sudobility/types';
7
- import type { GameFetchStatus } from './useLevelGame';
7
+ import { type GameFetchStatus } from '../utils/gameFetchStatus';
8
8
  export interface UseDailyGameOptions {
9
- /** Network client for API calls */
10
- networkClient: NetworkClient;
11
- /** Base URL for the Sudojo API */
12
- baseUrl: string;
9
+ /** Network client for API calls (default: SudojoApiProvider) */
10
+ networkClient?: NetworkClient | undefined;
11
+ /** Base URL for the Sudojo API (default: SudojoApiProvider) */
12
+ baseUrl?: string | undefined;
13
13
  /** Access token for authentication */
14
- token: string;
14
+ token?: string | undefined;
15
15
  /** Whether subscription is currently active */
16
16
  subscriptionActive?: boolean;
17
17
  /** Whether to enable the query */
@@ -37,6 +37,11 @@ export interface UseDailyGameResult {
37
37
  * Automatically refetches when auth token or subscription status changes.
38
38
  * Returns status indicating whether auth or subscription is required.
39
39
  *
40
+ * "Today" is the **UTC** date (getUtcDateString), the same day the API's
41
+ * `/dailies/today` serves, computed once per mount. (It used to be the local
42
+ * date, which asked for tomorrow's or yesterday's daily near midnight.)
43
+ * `dailyDate` is the daily's own `YYYY-MM-DD`, never timezone-shifted.
44
+ *
40
45
  * @param options - Hook options
41
46
  * @returns Daily puzzle data and status
42
47
  *
@@ -1 +1 @@
1
- {"version":3,"file":"useDailyGame.d.ts","sourceRoot":"","sources":["../../src/hooks/useDailyGame.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAC;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAgEtD,MAAM,WAAW,mBAAmB;IAClC,mCAAmC;IACnC,aAAa,EAAE,aAAa,CAAC;IAC7B,kCAAkC;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,sCAAsC;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,+CAA+C;IAC/C,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,kCAAkC;IAClC,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IACjC,2BAA2B;IAC3B,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,kDAAkD;IAClD,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,uCAAuC;IACvC,MAAM,EAAE,eAAe,CAAC;IACxB,8BAA8B;IAC9B,SAAS,EAAE,OAAO,CAAC;IACnB,4BAA4B;IAC5B,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,+BAA+B;IAC/B,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,kBAAkB,CA0E7E"}
1
+ {"version":3,"file":"useDailyGame.d.ts","sourceRoot":"","sources":["../../src/hooks/useDailyGame.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAC;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAIvD,OAAO,EAEL,KAAK,eAAe,EAErB,MAAM,0BAA0B,CAAC;AAElC,MAAM,WAAW,mBAAmB;IAClC,gEAAgE;IAChE,aAAa,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IAC1C,+DAA+D;IAC/D,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,sCAAsC;IACtC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,+CAA+C;IAC/C,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,kCAAkC;IAClC,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IACjC,2BAA2B;IAC3B,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,kDAAkD;IAClD,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,uCAAuC;IACvC,MAAM,EAAE,eAAe,CAAC;IACxB,8BAA8B;IAC9B,SAAS,EAAE,OAAO,CAAC;IACnB,4BAA4B;IAC5B,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,+BAA+B;IAC/B,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,kBAAkB,CAsE7E"}