scrabble-solver 2.18.3 → 2.18.4

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 (161) hide show
  1. package/.nx/workspace-data/d/server-process.json +3 -3
  2. package/.nx/workspace-data/file-map.json +737 -704
  3. package/.nx/workspace-data/lockfile-dependencies.hash +1 -1
  4. package/.nx/workspace-data/lockfile-nodes.hash +1 -1
  5. package/.nx/workspace-data/nx_files.nxt +0 -0
  6. package/.nx/workspace-data/parsed-lock-file.dependencies.json +528 -713
  7. package/.nx/workspace-data/parsed-lock-file.nodes.json +965 -1370
  8. package/.nx/workspace-data/project-graph.json +1559 -2155
  9. package/.nx/workspace-data/source-maps.json +44 -8
  10. package/.oxlintrc.json +0 -1
  11. package/CLAUDE.md +9 -7
  12. package/bun.lock +230 -279
  13. package/lerna.json +1 -1
  14. package/package.json +5 -5
  15. package/packages/configs/package.json +3 -3
  16. package/packages/constants/package.json +1 -1
  17. package/packages/dictionaries/bin/update-dictionaries.js +2 -5
  18. package/packages/dictionaries/build/Dictionaries.d.ts +1 -1
  19. package/packages/dictionaries/build/Dictionaries.js +9 -4
  20. package/packages/dictionaries/build/lib/DiskCache.js +1 -1
  21. package/packages/dictionaries/build/lib/downloadDictionary.d.ts +1 -1
  22. package/packages/dictionaries/build/lib/downloadDictionary.js +12 -7
  23. package/packages/dictionaries/package.json +4 -4
  24. package/packages/dictionaries/src/Dictionaries.ts +13 -6
  25. package/packages/dictionaries/src/lib/DiskCache.ts +2 -2
  26. package/packages/dictionaries/src/lib/downloadDictionary.ts +14 -6
  27. package/packages/logger/README.md +18 -0
  28. package/packages/logger/build/constants.d.ts +3 -0
  29. package/packages/logger/build/constants.js +4 -1
  30. package/packages/logger/build/describeError.d.ts +5 -0
  31. package/packages/logger/build/describeError.js +23 -0
  32. package/packages/logger/build/events.d.ts +92 -0
  33. package/packages/logger/build/events.js +13 -0
  34. package/packages/logger/build/formatCsvRow.d.ts +2 -0
  35. package/packages/logger/build/formatCsvRow.js +20 -0
  36. package/packages/logger/build/index.d.ts +5 -1
  37. package/packages/logger/build/index.js +13 -3
  38. package/packages/logger/build/logError.d.ts +4 -0
  39. package/packages/logger/build/logError.js +17 -0
  40. package/packages/logger/build/logEvent.d.ts +3 -0
  41. package/packages/logger/build/logEvent.js +29 -0
  42. package/packages/logger/package.json +7 -8
  43. package/packages/logger/scripts/export-csv.ts +113 -0
  44. package/packages/logger/scripts/readEvents.ts +34 -0
  45. package/packages/logger/scripts/tsconfig.json +10 -0
  46. package/packages/logger/src/constants.ts +9 -0
  47. package/packages/logger/src/describeError.test.ts +44 -0
  48. package/packages/logger/src/describeError.ts +30 -0
  49. package/packages/logger/src/events.ts +103 -0
  50. package/packages/logger/src/formatCsvRow.test.ts +41 -0
  51. package/packages/logger/src/formatCsvRow.ts +24 -0
  52. package/packages/logger/src/index.ts +13 -1
  53. package/packages/logger/src/logError.ts +24 -0
  54. package/packages/logger/src/logEvent.test.ts +59 -0
  55. package/packages/logger/src/logEvent.ts +26 -0
  56. package/packages/scrabble-solver/.next/BUILD_ID +1 -1
  57. package/packages/scrabble-solver/.next/build-manifest.json +15 -15
  58. package/packages/scrabble-solver/.next/cache/.previewinfo +1 -1
  59. package/packages/scrabble-solver/.next/cache/.rscinfo +1 -1
  60. package/packages/scrabble-solver/.next/cache/.tsbuildinfo +1 -1
  61. package/packages/scrabble-solver/.next/cache/webpack/client-production/0.pack +0 -0
  62. package/packages/scrabble-solver/.next/cache/webpack/client-production/index.pack +0 -0
  63. package/packages/scrabble-solver/.next/cache/webpack/client-production/index.pack.old +0 -0
  64. package/packages/scrabble-solver/.next/cache/webpack/edge-server-production/index.pack +0 -0
  65. package/packages/scrabble-solver/.next/cache/webpack/edge-server-production/index.pack.old +0 -0
  66. package/packages/scrabble-solver/.next/cache/webpack/server-production/0.pack +0 -0
  67. package/packages/scrabble-solver/.next/cache/webpack/server-production/index.pack +0 -0
  68. package/packages/scrabble-solver/.next/cache/webpack/server-production/index.pack.old +0 -0
  69. package/packages/scrabble-solver/.next/diagnostics/framework.json +1 -1
  70. package/packages/scrabble-solver/.next/dynamic-css-manifest.json +1 -1
  71. package/packages/scrabble-solver/.next/prerender-manifest.json +4 -4
  72. package/packages/scrabble-solver/.next/react-loadable-manifest.json +6 -6
  73. package/packages/scrabble-solver/.next/routes-manifest.json +1 -1
  74. package/packages/scrabble-solver/.next/server/chunks/546.js +1 -1
  75. package/packages/scrabble-solver/.next/server/chunks/612.js +5 -0
  76. package/packages/scrabble-solver/.next/server/chunks/712.js +1 -1
  77. package/packages/scrabble-solver/.next/server/chunks/715.js +1 -0
  78. package/packages/scrabble-solver/.next/server/chunks/961.js +1 -1
  79. package/packages/scrabble-solver/.next/server/dynamic-css-manifest.js +1 -1
  80. package/packages/scrabble-solver/.next/server/middleware-build-manifest.js +1 -1
  81. package/packages/scrabble-solver/.next/server/middleware-react-loadable-manifest.js +1 -1
  82. package/packages/scrabble-solver/.next/server/pages/404.html +2 -2
  83. package/packages/scrabble-solver/.next/server/pages/500.html +2 -2
  84. package/packages/scrabble-solver/.next/server/pages/_document.js +2 -2
  85. package/packages/scrabble-solver/.next/server/pages/api/dictionary/[locale]/[word].js +2 -2
  86. package/packages/scrabble-solver/.next/server/pages/api/dictionary/[locale]/[word].js.nft.json +1 -1
  87. package/packages/scrabble-solver/.next/server/pages/api/dictionary/[locale].js +1 -1
  88. package/packages/scrabble-solver/.next/server/pages/api/dictionary/[locale].js.nft.json +1 -1
  89. package/packages/scrabble-solver/.next/server/pages/api/solve.js +1 -1
  90. package/packages/scrabble-solver/.next/server/pages/api/solve.js.nft.json +1 -1
  91. package/packages/scrabble-solver/.next/server/pages/api/verify.js +1 -1
  92. package/packages/scrabble-solver/.next/server/pages/api/verify.js.nft.json +1 -1
  93. package/packages/scrabble-solver/.next/server/pages/api/visit.js +1 -1
  94. package/packages/scrabble-solver/.next/server/pages/api/visit.js.nft.json +1 -1
  95. package/packages/scrabble-solver/.next/server/pages/index.html +2 -2
  96. package/packages/scrabble-solver/.next/server/pages/index.js +1 -1
  97. package/packages/scrabble-solver/.next/server/pages/index.json +1 -1
  98. package/packages/scrabble-solver/.next/server/pages/not-found.html +2 -2
  99. package/packages/scrabble-solver/.next/static/{8WnIUTe-yJvPD6stltvYI → TAqjh9l5VJQOfJ4SzgPk2}/_buildManifest.js +1 -1
  100. package/packages/scrabble-solver/.next/static/chunks/101-f341f70c10d71212.js +3 -0
  101. package/packages/scrabble-solver/.next/static/chunks/693.d295da7c7b873986.js +1 -0
  102. package/packages/scrabble-solver/.next/static/chunks/980.dd35e807b18063c1.js +1 -0
  103. package/packages/scrabble-solver/.next/static/chunks/{main-7e52da35a4beea0b.js → main-6e9ac37e28d2b899.js} +1 -1
  104. package/packages/scrabble-solver/.next/static/chunks/pages/_app-9340d51f5a6787fc.js +1 -0
  105. package/packages/scrabble-solver/.next/static/chunks/pages/index-ea21d96a88f25000.js +1 -0
  106. package/packages/scrabble-solver/.next/static/chunks/{webpack-d7138f1ef6b8c9d6.js → webpack-d73aa4aae49cd800.js} +1 -1
  107. package/packages/scrabble-solver/.next/static/css/{92f85ce358a0a468.css → 4dbcacc1122ac6b9.css} +1 -1
  108. package/packages/scrabble-solver/.next/static/css/8b8087461767360a.css +1 -0
  109. package/packages/scrabble-solver/.next/static/css/{65bcc9440e067113.css → 98051ed25076b896.css} +1 -1
  110. package/packages/scrabble-solver/.next/trace +22 -22
  111. package/packages/scrabble-solver/.next/trace-build +1 -1
  112. package/packages/scrabble-solver/package.json +10 -10
  113. package/packages/scrabble-solver/public/service-worker.js +1 -1
  114. package/packages/scrabble-solver/src/api/BadRequestError.ts +6 -0
  115. package/packages/scrabble-solver/src/api/index.ts +2 -1
  116. package/packages/scrabble-solver/src/api/withApiLog.ts +85 -0
  117. package/packages/scrabble-solver/src/components/Button/Button.module.scss +1 -1
  118. package/packages/scrabble-solver/src/components/Solver/Solver.module.scss +4 -4
  119. package/packages/scrabble-solver/src/components/Solver/Solver.tsx +2 -2
  120. package/packages/scrabble-solver/src/hooks/useColumns.ts +5 -7
  121. package/packages/scrabble-solver/src/modals/ResultsModal/ResultsModal.tsx +2 -2
  122. package/packages/scrabble-solver/src/modals/WordsModal/WordsModal.module.scss +1 -1
  123. package/packages/scrabble-solver/src/modals/WordsModal/WordsModal.tsx +2 -2
  124. package/packages/scrabble-solver/src/modals/WordsModal/components/WordsTable/WordsTable.tsx +2 -2
  125. package/packages/scrabble-solver/src/pages/api/dictionary/[locale]/[word].ts +22 -30
  126. package/packages/scrabble-solver/src/pages/api/dictionary/[locale]/index.ts +36 -42
  127. package/packages/scrabble-solver/src/pages/api/solve.ts +32 -42
  128. package/packages/scrabble-solver/src/pages/api/verify.ts +35 -43
  129. package/packages/scrabble-solver/src/pages/api/visit.ts +38 -13
  130. package/packages/scrabble-solver/src/pages/index.tsx +2 -2
  131. package/packages/scrabble-solver/src/parameters/index.ts +0 -2
  132. package/packages/scrabble-solver/src/sdk/solve.ts +3 -9
  133. package/packages/scrabble-solver/src/sdk/verify.ts +2 -10
  134. package/packages/scrabble-solver/src/sdk/visit.ts +8 -3
  135. package/packages/scrabble-solver/src/state/sagas.ts +7 -1
  136. package/packages/scrabble-solver/src/styles/_tokens.scss +0 -2
  137. package/packages/scrabble-solver/src/styles/mixins.scss +0 -5
  138. package/packages/scrabble-solver/src/styles/variables.module.scss +0 -1
  139. package/packages/scrabble-solver/src/styles/variables.module.scss.d.ts +0 -1
  140. package/packages/scrabble-solver/src/styles/variables.scss +1 -1
  141. package/packages/scrabble-solver/src/types/api.ts +7 -0
  142. package/packages/scrabble-solver/tsconfig.tsbuildinfo +1 -1
  143. package/packages/solver/package.json +5 -5
  144. package/packages/types/package.json +2 -2
  145. package/packages/word-definitions/package.json +2 -2
  146. package/packages/word-lists/package.json +2 -2
  147. package/playwright-report/index.html +1 -1
  148. package/packages/logger/build/logger.d.ts +0 -1
  149. package/packages/logger/build/logger.js +0 -28
  150. package/packages/logger/scripts/stats.js +0 -73
  151. package/packages/logger/src/logger.ts +0 -30
  152. package/packages/scrabble-solver/.next/server/chunks/392.js +0 -1
  153. package/packages/scrabble-solver/.next/static/chunks/101-829ab8a8d310e7a0.js +0 -3
  154. package/packages/scrabble-solver/.next/static/chunks/693.677c88e0eb1340b5.js +0 -1
  155. package/packages/scrabble-solver/.next/static/chunks/980.7ce17464fc56648a.js +0 -1
  156. package/packages/scrabble-solver/.next/static/chunks/pages/_app-e3fe880e24c024ab.js +0 -1
  157. package/packages/scrabble-solver/.next/static/chunks/pages/index-b073a1686818dd26.js +0 -1
  158. package/packages/scrabble-solver/.next/static/css/a691348b788f8bc0.css +0 -1
  159. package/packages/scrabble-solver/src/api/getServerLoggingData.ts +0 -17
  160. package/packages/scrabble-solver/src/hooks/useIsCompactLayout.ts +0 -14
  161. /package/packages/scrabble-solver/.next/static/{8WnIUTe-yJvPD6stltvYI → TAqjh9l5VJQOfJ4SzgPk2}/_ssgManifest.js +0 -0
@@ -1,7 +1,7 @@
1
1
  import { getConfig, hasConfig } from '@scrabble-solver/configs';
2
2
  import { BLANK } from '@scrabble-solver/constants';
3
3
  import { dictionaries } from '@scrabble-solver/dictionaries';
4
- import { logger } from '@scrabble-solver/logger';
4
+ import { logEvent } from '@scrabble-solver/logger';
5
5
  import { solve as solveScrabble } from '@scrabble-solver/solver';
6
6
  import {
7
7
  Board,
@@ -15,7 +15,7 @@ import {
15
15
  } from '@scrabble-solver/types';
16
16
  import { type NextApiRequest, type NextApiResponse } from 'next';
17
17
 
18
- import { getServerLoggingData, isBoardValid, isCharacterValid } from '@/api';
18
+ import { type ApiContext, BadRequestError, isBoardValid, isCharacterValid, withApiLog } from '@/api';
19
19
  import { isStringArray } from '@/lib/isStringArray';
20
20
 
21
21
  interface RequestData {
@@ -26,71 +26,63 @@ interface RequestData {
26
26
  locale: Locale;
27
27
  }
28
28
 
29
- const solve = async (request: NextApiRequest, response: NextApiResponse): Promise<void> => {
30
- const meta = getServerLoggingData(request);
31
-
32
- try {
33
- const { board, characters, config, game, locale } = parseRequest(request);
34
-
35
- logger.info('solve - request', {
36
- meta,
37
- payload: {
38
- board: board.toString(),
39
- configId: config.game,
40
- boardBlanksCount: board.getBlanksCount(),
41
- boardTilesCount: board.getTilesCount(),
42
- characters: characters.join(''),
43
- game,
44
- locale,
45
- },
46
- });
47
-
48
- const gaddag = await dictionaries.get(locale);
49
- const tiles = characters.map((character) => new Tile({ character, isBlank: character === BLANK }));
50
- const results = solveScrabble(gaddag, config, board, tiles);
51
- response.status(200).send(results);
52
- } catch (error) {
53
- const message = error instanceof Error ? error.message : 'Unknown error';
54
- logger.error('solve - error', { error, meta });
55
- response.status(500).send({ error: 'Server error', message });
56
- }
57
- };
29
+ export default withApiLog('solve', solve);
30
+
31
+ async function solve(request: NextApiRequest, response: NextApiResponse, { ip, getElapsedMs }: ApiContext) {
32
+ const { board, characters, config, game, locale } = parseRequest(request);
33
+ const gaddag = await dictionaries.get(locale);
34
+ const tiles = characters.map((character) => new Tile({ character, isBlank: character === BLANK }));
35
+ const results = solveScrabble(gaddag, config, board, tiles);
36
+ response.status(200).send(results);
37
+
38
+ logEvent({
39
+ type: 'solve',
40
+ ip,
41
+ ms: getElapsedMs(),
42
+ locale,
43
+ game,
44
+ tiles: board.getTilesCount(),
45
+ blanks: board.getBlanksCount(),
46
+ rack: characters.join(','),
47
+ results: results.length,
48
+ });
49
+ }
58
50
 
59
- const parseRequest = (request: NextApiRequest): RequestData => {
51
+ function parseRequest(request: NextApiRequest): RequestData {
60
52
  const { board: boardJson, characters, game, locale } = request.body;
61
53
 
62
54
  if (!isLocale(locale)) {
63
- throw new Error('Invalid "locale" parameter');
55
+ throw new BadRequestError('Invalid "locale" parameter');
64
56
  }
65
57
 
66
58
  if (!isGame(game)) {
67
- throw new Error('Invalid "game" parameter');
59
+ throw new BadRequestError('Invalid "game" parameter');
68
60
  }
69
61
 
70
62
  if (!isStringArray(characters) || characters.length === 0) {
71
- throw new Error('Invalid "characters" parameter');
63
+ throw new BadRequestError('Invalid "characters" parameter');
72
64
  }
73
65
 
74
66
  if (!hasConfig(game, locale)) {
75
- throw new Error(`No game "${game}" in "${locale}"`);
67
+ throw new BadRequestError(`No game "${game}" in "${locale}"`);
76
68
  }
77
69
 
78
70
  const config = getConfig(game, locale);
79
71
 
80
72
  for (const character of characters) {
81
73
  if (!isCharacterValid(character)) {
82
- throw new Error('Invalid "characters" parameter');
74
+ throw new BadRequestError('Invalid "characters" parameter');
83
75
  }
84
76
  }
85
77
 
86
78
  const blanksCount = characters.filter((character) => character === BLANK).length;
87
79
 
88
80
  if (blanksCount > config.blanksCount) {
89
- throw new Error('Too many blank tiles passed');
81
+ throw new BadRequestError('Too many blank tiles passed');
90
82
  }
91
83
 
92
84
  if (!isBoardJson(boardJson) || !isBoardValid(boardJson, config)) {
93
- throw new Error('Invalid "board" parameter');
85
+ throw new BadRequestError('Invalid "board" parameter');
94
86
  }
95
87
 
96
88
  const board = Board.fromJson(boardJson);
@@ -102,12 +94,10 @@ const parseRequest = (request: NextApiRequest): RequestData => {
102
94
  game,
103
95
  locale,
104
96
  };
105
- };
97
+ }
106
98
 
107
99
  export const config = {
108
100
  api: {
109
101
  responseLimit: '25mb',
110
102
  },
111
103
  };
112
-
113
- export default solve;
@@ -1,10 +1,10 @@
1
1
  import { getConfig, hasConfig } from '@scrabble-solver/configs';
2
2
  import { dictionaries } from '@scrabble-solver/dictionaries';
3
- import { logger } from '@scrabble-solver/logger';
3
+ import { logEvent } from '@scrabble-solver/logger';
4
4
  import { Board, type Config, type Game, type Locale, isBoardJson, isGame, isLocale } from '@scrabble-solver/types';
5
5
  import { type NextApiRequest, type NextApiResponse } from 'next';
6
6
 
7
- import { getServerLoggingData, isBoardValid } from '@/api';
7
+ import { type ApiContext, BadRequestError, isBoardValid, withApiLog } from '@/api';
8
8
  import { type VerifiedWord } from '@/types';
9
9
 
10
10
  interface RequestData {
@@ -14,63 +14,57 @@ interface RequestData {
14
14
  locale: Locale;
15
15
  }
16
16
 
17
- const verify = async (request: NextApiRequest, response: NextApiResponse): Promise<void> => {
18
- const meta = getServerLoggingData(request);
17
+ export default withApiLog('verify', verify);
19
18
 
20
- try {
21
- const { board, game, locale } = parseRequest(request);
19
+ async function verify(request: NextApiRequest, response: NextApiResponse, { ip, getElapsedMs }: ApiContext) {
20
+ const { board, game, locale } = parseRequest(request);
21
+ const gaddag = await dictionaries.get(locale);
22
+ const words = board.getWords().sort((a, b) => a.word.localeCompare(b.word, locale));
23
+ const invalidWords: VerifiedWord[] = [];
24
+ const validWords: VerifiedWord[] = [];
22
25
 
23
- logger.info('verify - request', {
24
- meta,
25
- payload: {
26
- board: board.toString(),
27
- boardBlanksCount: board.getBlanksCount(),
28
- boardTilesCount: board.getTilesCount(),
29
- game,
30
- locale,
31
- },
32
- });
33
-
34
- const gaddag = await dictionaries.get(locale);
35
- const words = board.getWords().sort((a, b) => a.word.localeCompare(b.word, locale));
36
- const invalidWords: VerifiedWord[] = [];
37
- const validWords: VerifiedWord[] = [];
38
-
39
- for (const word of words) {
40
- if (gaddag.has(word.word)) {
41
- validWords.push({ ...word, isValid: true });
42
- } else {
43
- invalidWords.push({ ...word, isValid: false });
44
- }
26
+ for (const word of words) {
27
+ if (gaddag.has(word.word)) {
28
+ validWords.push({ ...word, isValid: true });
29
+ } else {
30
+ invalidWords.push({ ...word, isValid: false });
45
31
  }
46
-
47
- response.status(200).send({ invalidWords, validWords });
48
- } catch (error) {
49
- const message = error instanceof Error ? error.message : 'Unknown error';
50
- logger.error('verify - error', { error, meta });
51
- response.status(500).send({ error: 'Server error', message });
52
32
  }
53
- };
54
33
 
55
- const parseRequest = (request: NextApiRequest): RequestData => {
34
+ response.status(200).send({ invalidWords, validWords });
35
+
36
+ logEvent({
37
+ type: 'verify',
38
+ ip,
39
+ ms: getElapsedMs(),
40
+ locale,
41
+ game,
42
+ tiles: board.getTilesCount(),
43
+ blanks: board.getBlanksCount(),
44
+ valid: validWords.length,
45
+ invalid: invalidWords.length,
46
+ });
47
+ }
48
+
49
+ function parseRequest(request: NextApiRequest): RequestData {
56
50
  const { board: boardJson, game, locale } = request.body;
57
51
 
58
52
  if (!isLocale(locale)) {
59
- throw new Error('Invalid "locale" parameter');
53
+ throw new BadRequestError('Invalid "locale" parameter');
60
54
  }
61
55
 
62
56
  if (!isGame(game)) {
63
- throw new Error('Invalid "game" parameter');
57
+ throw new BadRequestError('Invalid "game" parameter');
64
58
  }
65
59
 
66
60
  if (!hasConfig(game, locale)) {
67
- throw new Error(`No game "${game}" in "${locale}"`);
61
+ throw new BadRequestError(`No game "${game}" in "${locale}"`);
68
62
  }
69
63
 
70
64
  const config = getConfig(game, locale);
71
65
 
72
66
  if (!isBoardJson(boardJson) || !isBoardValid(boardJson, config)) {
73
- throw new Error('Invalid "board" parameter');
67
+ throw new BadRequestError('Invalid "board" parameter');
74
68
  }
75
69
 
76
70
  const board = Board.fromJson(boardJson);
@@ -81,6 +75,4 @@ const parseRequest = (request: NextApiRequest): RequestData => {
81
75
  game,
82
76
  locale,
83
77
  };
84
- };
85
-
86
- export default verify;
78
+ }
@@ -1,19 +1,44 @@
1
- import { logger } from '@scrabble-solver/logger';
1
+ import { logEvent } from '@scrabble-solver/logger';
2
+ import { isGame, isLocale, isObject } from '@scrabble-solver/types';
2
3
  import { type NextApiRequest, type NextApiResponse } from 'next';
3
4
 
4
- import { getServerLoggingData } from '@/api';
5
+ import { type ApiContext, withApiLog } from '@/api';
5
6
 
6
- const visit = (request: NextApiRequest, response: NextApiResponse): void => {
7
- const meta = getServerLoggingData(request);
7
+ interface RequestData {
8
+ game: string | undefined;
9
+ locale: string | undefined;
10
+ referrer: string | undefined;
11
+ url: string | undefined;
12
+ }
8
13
 
9
- try {
10
- logger.info('visit - request', { meta });
11
- response.status(200).send(true);
12
- } catch (error) {
13
- const message = error instanceof Error ? error.message : 'Unknown error';
14
- logger.error('visit - error', { error, meta });
15
- response.status(500).send({ error: 'Server error', message });
14
+ const URL_MAX_LENGTH = 256;
15
+
16
+ export default withApiLog('visit', visit);
17
+
18
+ function visit(request: NextApiRequest, response: NextApiResponse, { ip, ua }: ApiContext) {
19
+ const { game, locale, referrer, url } = parseRequest(request);
20
+ response.status(200).send(true);
21
+ logEvent({ type: 'visit', ip, ua, referrer, locale, game, url });
22
+ }
23
+
24
+ // A visit is only a ping - unexpected fields are ignored, never rejected.
25
+ function parseRequest(request: NextApiRequest): RequestData {
26
+ const body: unknown = request.body;
27
+
28
+ if (!isObject(body)) {
29
+ return { game: undefined, locale: undefined, referrer: undefined, url: undefined };
16
30
  }
17
- };
18
31
 
19
- export default visit;
32
+ const { game, locale, referrer, url } = body;
33
+
34
+ return {
35
+ game: isGame(game) ? game : undefined,
36
+ locale: isLocale(locale) ? locale : undefined,
37
+ referrer: parseUrl(referrer),
38
+ url: parseUrl(url),
39
+ };
40
+ }
41
+
42
+ function parseUrl(value: unknown): string | undefined {
43
+ return typeof value === 'string' && value.length > 0 ? value.slice(0, URL_MAX_LENGTH) : undefined;
44
+ }
@@ -14,9 +14,9 @@ import { NavButtons } from '@/components/NavButtons';
14
14
  import { Solver } from '@/components/Solver';
15
15
  import { useDirection } from '@/hooks/useDirection';
16
16
  import { useEffectOnce } from '@/hooks/useEffectOnce';
17
- import { useIsCompactLayout } from '@/hooks/useIsCompactLayout';
18
17
  import { useLanguage } from '@/hooks/useLanguage';
19
18
  import { useLocalStorage } from '@/hooks/useLocalStorage';
19
+ import { useMediaQuery } from '@/hooks/useMediaQuery';
20
20
  import { LOCALE_FEATURES } from '@/i18n/constants';
21
21
  import { schedulePreloadModals } from '@/modals/preload';
22
22
  import { CONFIG_PENDING_CLASS, GITHUB_PROJECT_URL, NPM_PACKAGE_URL, SITE_DESCRIPTION, SITE_URL } from '@/parameters';
@@ -94,7 +94,7 @@ const Index: FunctionComponent<Props> = ({ version }) => {
94
94
  const config = useTypedSelector(selectConfig);
95
95
  const locale = useTypedSelector(selectLocale);
96
96
  const isHydrated = useTypedSelector(selectIsHydrated);
97
- const isCompactLayout = useIsCompactLayout();
97
+ const isCompactLayout = useMediaQuery('<l');
98
98
  const previousIsCompactLayout = useRef(isCompactLayout);
99
99
  const [modals, setModals] = useState<Record<Modal, boolean>>({
100
100
  dictionary: false,
@@ -8,8 +8,6 @@ export const BREAKPOINTS = {
8
8
  xl: Number(variables.breakpointXl),
9
9
  };
10
10
 
11
- export const BREAKPOINT_HEIGHT_L = Number(variables.breakpointHeightL);
12
-
13
11
  export const GITHUB_PROJECT_URL = 'https://github.com/kamilmielnik/scrabble-solver';
14
12
 
15
13
  export const NPM_PACKAGE_URL = 'https://www.npmjs.com/package/scrabble-solver';
@@ -1,17 +1,11 @@
1
- import { Board, type BoardJson, type Game, type Locale, Result, type ResultJson } from '@scrabble-solver/types';
1
+ import { Board, Result, type ResultJson } from '@scrabble-solver/types';
2
2
 
3
3
  import { solveLocally } from '@/solver-worker';
4
+ import { type SolveRequestPayload } from '@/types';
4
5
 
5
6
  import { fetchJson } from './fetchJson';
6
7
 
7
- interface Payload {
8
- board: BoardJson;
9
- characters: string[];
10
- game: Game;
11
- locale: Locale;
12
- }
13
-
14
- export const solve = async ({ board, characters, game, locale }: Payload): Promise<Result[]> => {
8
+ export const solve = async ({ board, characters, game, locale }: SolveRequestPayload): Promise<Result[]> => {
15
9
  const payload = { board, characters, game, locale };
16
10
  const json =
17
11
  (await solveLocally(payload)) ??
@@ -1,22 +1,14 @@
1
- import { type BoardJson, type Game, type Locale } from '@scrabble-solver/types';
2
-
3
1
  import { verifyLocally } from '@/solver-worker';
4
- import { type VerifiedWord } from '@/types';
2
+ import { type VerifiedWord, type VerifyRequestPayload } from '@/types';
5
3
 
6
4
  import { fetchJson } from './fetchJson';
7
5
 
8
- interface Payload {
9
- board: BoardJson;
10
- game: Game;
11
- locale: Locale;
12
- }
13
-
14
6
  interface Response {
15
7
  invalidWords: VerifiedWord[];
16
8
  validWords: VerifiedWord[];
17
9
  }
18
10
 
19
- export const verify = async ({ board, game, locale }: Payload): Promise<Response> => {
11
+ export const verify = async ({ board, game, locale }: VerifyRequestPayload): Promise<Response> => {
20
12
  const payload = { board, game, locale };
21
13
  return (
22
14
  (await verifyLocally(payload)) ??
@@ -1,5 +1,10 @@
1
- export const visit = (): Promise<Response> => {
2
- return fetch('/api/visit', {
1
+ import { type VisitRequestPayload } from '@/types';
2
+
3
+ import { fetchJson } from './fetchJson';
4
+
5
+ export function visit(payload: VisitRequestPayload): Promise<boolean> {
6
+ return fetchJson<boolean>('/api/visit', {
3
7
  method: 'PUT',
8
+ body: JSON.stringify(payload),
4
9
  });
5
- };
10
+ }
@@ -204,7 +204,13 @@ function* hydratePersistedTranslations(locale: Locale, version: string): AnyGene
204
204
 
205
205
  function* visitWhenIdle(): AnyGenerator {
206
206
  yield call(waitForIdleOrFirstIntent);
207
- yield call(visit);
207
+ const locale = yield select(selectLocale);
208
+ const game = yield select(selectGame);
209
+
210
+ try {
211
+ yield call(visit, { game, locale, referrer: document.referrer, url: location.href });
212
+ // oxlint-disable-next-line no-empty
213
+ } catch {}
208
214
  }
209
215
 
210
216
  function* prefetchDictionaryOnFirstIntent(): AnyGenerator {
@@ -10,8 +10,6 @@ $breakpoint-m: 992px;
10
10
  $breakpoint-l: 1200px;
11
11
  $breakpoint-xl: 1400px;
12
12
 
13
- $breakpoint-height-l: 860px;
14
-
15
13
  $border-color: #cdcdcd;
16
14
  $border-color-light: #d9d9d9;
17
15
  $border-radius: 5px;
@@ -10,11 +10,6 @@
10
10
  ),
11
11
  $media-expressions: (
12
12
  touch: '(hover: none)',
13
- /**
14
- * Keep in sync with useIsCompactLayout
15
- */
16
- compact: '(max-width: #{$breakpoint-l - 1px}), (max-height: #{$breakpoint-height-l - 1px})',
17
- desktop: '(min-width: #{$breakpoint-l}) and (min-height: #{$breakpoint-height-l})',
18
13
  )
19
14
  );
20
15
 
@@ -6,7 +6,6 @@
6
6
  breakpointM: strip-unit($breakpoint-m);
7
7
  breakpointL: strip-unit($breakpoint-l);
8
8
  breakpointXl: strip-unit($breakpoint-xl);
9
- breakpointHeightL: strip-unit($breakpoint-height-l);
10
9
 
11
10
  borderColorLight: $border-color-light;
12
11
  borderWidth: strip-unit($border-width);
@@ -2,7 +2,6 @@
2
2
  declare const styles: {
3
3
  readonly borderColorLight: string;
4
4
  readonly borderWidth: string;
5
- readonly breakpointHeightL: string;
6
5
  readonly breakpointL: string;
7
6
  readonly breakpointM: string;
8
7
  readonly breakpointS: string;
@@ -153,7 +153,7 @@ $easeOutSine: cubic-bezier(0.61, 1, 0.88, 1);
153
153
  --components-spacing: var(--spacing--l);
154
154
  }
155
155
 
156
- @include media('compact') {
156
+ @include media('<l') {
157
157
  --dictionary--height: var(--dictionary--height--mobile);
158
158
  --logo--height: #{$logo-height-small};
159
159
  --solver-column-total: 0px;
@@ -12,3 +12,10 @@ export interface VerifyRequestPayload {
12
12
  game: Game;
13
13
  locale: Locale;
14
14
  }
15
+
16
+ export interface VisitRequestPayload {
17
+ game: Game;
18
+ locale: Locale;
19
+ referrer: string;
20
+ url: string;
21
+ }