@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.
- package/dist/admin/adminApi.d.ts +34 -0
- package/dist/admin/adminApi.d.ts.map +1 -0
- package/dist/admin/adminApi.js +59 -0
- package/dist/admin/adminApi.js.map +1 -0
- package/dist/admin/boardRules.d.ts +75 -0
- package/dist/admin/boardRules.d.ts.map +1 -0
- package/dist/admin/boardRules.js +127 -0
- package/dist/admin/boardRules.js.map +1 -0
- package/dist/admin/boardStrings.d.ts +55 -0
- package/dist/admin/boardStrings.d.ts.map +1 -0
- package/dist/admin/boardStrings.js +99 -0
- package/dist/admin/boardStrings.js.map +1 -0
- package/dist/admin/examples.d.ts +58 -0
- package/dist/admin/examples.d.ts.map +1 -0
- package/dist/admin/examples.js +104 -0
- package/dist/admin/examples.js.map +1 -0
- package/dist/admin/extraction.d.ts +87 -0
- package/dist/admin/extraction.d.ts.map +1 -0
- package/dist/admin/extraction.js +141 -0
- package/dist/admin/extraction.js.map +1 -0
- package/dist/admin/index.d.ts +32 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +22 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/jobs.d.ts +108 -0
- package/dist/admin/jobs.d.ts.map +1 -0
- package/dist/admin/jobs.js +435 -0
- package/dist/admin/jobs.js.map +1 -0
- package/dist/admin/regenerateHints.d.ts +16 -0
- package/dist/admin/regenerateHints.d.ts.map +1 -0
- package/dist/admin/regenerateHints.js +43 -0
- package/dist/admin/regenerateHints.js.map +1 -0
- package/dist/admin/types.d.ts +42 -0
- package/dist/admin/types.d.ts.map +1 -0
- package/dist/admin/types.js +21 -0
- package/dist/admin/types.js.map +1 -0
- package/dist/admin/useAdminJob.d.ts +28 -0
- package/dist/admin/useAdminJob.d.ts.map +1 -0
- package/dist/admin/useAdminJob.js +99 -0
- package/dist/admin/useAdminJob.js.map +1 -0
- package/dist/admin/useAdminStats.d.ts +46 -0
- package/dist/admin/useAdminStats.d.ts.map +1 -0
- package/dist/admin/useAdminStats.js +91 -0
- package/dist/admin/useAdminStats.js.map +1 -0
- package/dist/admin/useBoardGenerator.d.ts +29 -0
- package/dist/admin/useBoardGenerator.d.ts.map +1 -0
- package/dist/admin/useBoardGenerator.js +38 -0
- package/dist/admin/useBoardGenerator.js.map +1 -0
- package/dist/admin/useBoardTechniquesUpdater.d.ts +14 -0
- package/dist/admin/useBoardTechniquesUpdater.d.ts.map +1 -0
- package/dist/admin/useBoardTechniquesUpdater.js +28 -0
- package/dist/admin/useBoardTechniquesUpdater.js.map +1 -0
- package/dist/admin/useExampleCreator.d.ts +29 -0
- package/dist/admin/useExampleCreator.d.ts.map +1 -0
- package/dist/admin/useExampleCreator.js +58 -0
- package/dist/admin/useExampleCreator.js.map +1 -0
- package/dist/admin/useSingleBoardExtractor.d.ts +18 -0
- package/dist/admin/useSingleBoardExtractor.d.ts.map +1 -0
- package/dist/admin/useSingleBoardExtractor.js +19 -0
- package/dist/admin/useSingleBoardExtractor.js.map +1 -0
- package/dist/admin/useTechniqueExtractor.d.ts +26 -0
- package/dist/admin/useTechniqueExtractor.d.ts.map +1 -0
- package/dist/admin/useTechniqueExtractor.js +37 -0
- package/dist/admin/useTechniqueExtractor.js.map +1 -0
- package/dist/context/SudojoApiContext.d.ts +58 -0
- package/dist/context/SudojoApiContext.d.ts.map +1 -0
- package/dist/context/SudojoApiContext.js +52 -0
- package/dist/context/SudojoApiContext.js.map +1 -0
- package/dist/context/SudojoApiProvider.d.ts +21 -0
- package/dist/context/SudojoApiProvider.d.ts.map +1 -0
- package/dist/context/SudojoApiProvider.js +15 -0
- package/dist/context/SudojoApiProvider.js.map +1 -0
- package/dist/context/index.d.ts +3 -0
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +2 -0
- package/dist/context/index.js.map +1 -1
- package/dist/hooks/index.d.ts +24 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +22 -0
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/useBoardEntry.d.ts +65 -5
- package/dist/hooks/useBoardEntry.d.ts.map +1 -1
- package/dist/hooks/useBoardEntry.js +124 -27
- package/dist/hooks/useBoardEntry.js.map +1 -1
- package/dist/hooks/useBoardScan.d.ts +27 -0
- package/dist/hooks/useBoardScan.d.ts.map +1 -0
- package/dist/hooks/useBoardScan.js +27 -0
- package/dist/hooks/useBoardScan.js.map +1 -0
- package/dist/hooks/useCommunities.d.ts +6 -4
- package/dist/hooks/useCommunities.d.ts.map +1 -1
- package/dist/hooks/useCommunities.js +6 -1
- package/dist/hooks/useCommunities.js.map +1 -1
- package/dist/hooks/useCompletionTrigger.d.ts +23 -0
- package/dist/hooks/useCompletionTrigger.d.ts.map +1 -0
- package/dist/hooks/useCompletionTrigger.js +39 -0
- package/dist/hooks/useCompletionTrigger.js.map +1 -0
- package/dist/hooks/useDailyGame.d.ts +11 -6
- package/dist/hooks/useDailyGame.d.ts.map +1 -1
- package/dist/hooks/useDailyGame.js +20 -61
- package/dist/hooks/useDailyGame.js.map +1 -1
- package/dist/hooks/useDeleteAccount.d.ts +72 -0
- package/dist/hooks/useDeleteAccount.d.ts.map +1 -0
- package/dist/hooks/useDeleteAccount.js +71 -0
- package/dist/hooks/useDeleteAccount.js.map +1 -0
- package/dist/hooks/useEnteredGameSession.d.ts +73 -0
- package/dist/hooks/useEnteredGameSession.d.ts.map +1 -0
- package/dist/hooks/useEnteredGameSession.js +155 -0
- package/dist/hooks/useEnteredGameSession.js.map +1 -0
- package/dist/hooks/useGameSession.d.ts +8 -4
- package/dist/hooks/useGameSession.d.ts.map +1 -1
- package/dist/hooks/useGameSession.js +4 -1
- package/dist/hooks/useGameSession.js.map +1 -1
- package/dist/hooks/useGameTeaching.d.ts.map +1 -1
- package/dist/hooks/useGameTeaching.js +11 -13
- package/dist/hooks/useGameTeaching.js.map +1 -1
- package/dist/hooks/useHint.d.ts +7 -7
- package/dist/hooks/useHint.d.ts.map +1 -1
- package/dist/hooks/useHint.js +18 -9
- package/dist/hooks/useHint.js.map +1 -1
- package/dist/hooks/useIsSiteAdmin.d.ts +39 -0
- package/dist/hooks/useIsSiteAdmin.d.ts.map +1 -0
- package/dist/hooks/useIsSiteAdmin.js +31 -0
- package/dist/hooks/useIsSiteAdmin.js.map +1 -0
- package/dist/hooks/useLearning.d.ts +8 -8
- package/dist/hooks/useLearning.d.ts.map +1 -1
- package/dist/hooks/useLearning.js +5 -2
- package/dist/hooks/useLearning.js.map +1 -1
- package/dist/hooks/useLevelGame.d.ts +7 -7
- package/dist/hooks/useLevelGame.d.ts.map +1 -1
- package/dist/hooks/useLevelGame.js +13 -49
- package/dist/hooks/useLevelGame.js.map +1 -1
- package/dist/hooks/useLevels.d.ts +8 -8
- package/dist/hooks/useLevels.d.ts.map +1 -1
- package/dist/hooks/useLevels.js +7 -3
- package/dist/hooks/useLevels.js.map +1 -1
- package/dist/hooks/usePracticeGame.d.ts +53 -0
- package/dist/hooks/usePracticeGame.d.ts.map +1 -0
- package/dist/hooks/usePracticeGame.js +72 -0
- package/dist/hooks/usePracticeGame.js.map +1 -0
- package/dist/hooks/usePuzzleDistribution.d.ts +2 -0
- package/dist/hooks/usePuzzleDistribution.d.ts.map +1 -1
- package/dist/hooks/usePuzzleDistribution.js +4 -14
- package/dist/hooks/usePuzzleDistribution.js.map +1 -1
- package/dist/hooks/usePuzzleProgress.d.ts +37 -0
- package/dist/hooks/usePuzzleProgress.d.ts.map +1 -0
- package/dist/hooks/usePuzzleProgress.js +48 -0
- package/dist/hooks/usePuzzleProgress.js.map +1 -0
- package/dist/hooks/usePuzzleSession.d.ts +215 -0
- package/dist/hooks/usePuzzleSession.d.ts.map +1 -0
- package/dist/hooks/usePuzzleSession.js +358 -0
- package/dist/hooks/usePuzzleSession.js.map +1 -0
- package/dist/hooks/useResumeGame.d.ts +70 -0
- package/dist/hooks/useResumeGame.d.ts.map +1 -0
- package/dist/hooks/useResumeGame.js +123 -0
- package/dist/hooks/useResumeGame.js.map +1 -0
- package/dist/hooks/useStrategies.d.ts +39 -0
- package/dist/hooks/useStrategies.d.ts.map +1 -0
- package/dist/hooks/useStrategies.js +34 -0
- package/dist/hooks/useStrategies.js.map +1 -0
- package/dist/hooks/useSudoku.d.ts +5 -0
- package/dist/hooks/useSudoku.d.ts.map +1 -1
- package/dist/hooks/useSudoku.js +7 -1
- package/dist/hooks/useSudoku.js.map +1 -1
- package/dist/hooks/useTechniqueByPath.d.ts +46 -0
- package/dist/hooks/useTechniqueByPath.d.ts.map +1 -0
- package/dist/hooks/useTechniqueByPath.js +47 -0
- package/dist/hooks/useTechniqueByPath.js.map +1 -0
- package/dist/hooks/useTechniqueExample.d.ts +60 -0
- package/dist/hooks/useTechniqueExample.d.ts.map +1 -0
- package/dist/hooks/useTechniqueExample.js +63 -0
- package/dist/hooks/useTechniqueExample.js.map +1 -0
- package/dist/hooks/useTechniques.d.ts +8 -8
- package/dist/hooks/useTechniques.d.ts.map +1 -1
- package/dist/hooks/useTechniques.js +9 -21
- package/dist/hooks/useTechniques.js.map +1 -1
- package/dist/index.d.ts +418 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +276 -10
- package/dist/index.js.map +1 -1
- package/dist/types/currentGame.d.ts +9 -1
- package/dist/types/currentGame.d.ts.map +1 -1
- package/dist/types/currentGame.js.map +1 -1
- package/dist/utils/auth.d.ts +6 -0
- package/dist/utils/auth.d.ts.map +1 -1
- package/dist/utils/auth.js +8 -0
- package/dist/utils/auth.js.map +1 -1
- package/dist/utils/community.d.ts +64 -0
- package/dist/utils/community.d.ts.map +1 -0
- package/dist/utils/community.js +66 -0
- package/dist/utils/community.js.map +1 -0
- package/dist/utils/date.d.ts +37 -0
- package/dist/utils/date.d.ts.map +1 -0
- package/dist/utils/date.js +68 -0
- package/dist/utils/date.js.map +1 -0
- package/dist/utils/entityTranslate.d.ts +120 -0
- package/dist/utils/entityTranslate.d.ts.map +1 -0
- package/dist/utils/entityTranslate.js +146 -0
- package/dist/utils/entityTranslate.js.map +1 -0
- package/dist/utils/gameFetchStatus.d.ts +74 -0
- package/dist/utils/gameFetchStatus.d.ts.map +1 -0
- package/dist/utils/gameFetchStatus.js +118 -0
- package/dist/utils/gameFetchStatus.js.map +1 -0
- package/dist/utils/gamification.d.ts +36 -0
- package/dist/utils/gamification.d.ts.map +1 -0
- package/dist/utils/gamification.js +33 -0
- package/dist/utils/gamification.js.map +1 -0
- package/dist/utils/hintAccess.d.ts +64 -0
- package/dist/utils/hintAccess.d.ts.map +1 -0
- package/dist/utils/hintAccess.js +57 -0
- package/dist/utils/hintAccess.js.map +1 -0
- package/dist/utils/index.d.ts +31 -10
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +20 -9
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/language.d.ts +123 -0
- package/dist/utils/language.d.ts.map +1 -0
- package/dist/utils/language.js +95 -0
- package/dist/utils/language.js.map +1 -0
- package/dist/utils/localizedHint.d.ts +60 -0
- package/dist/utils/localizedHint.d.ts.map +1 -1
- package/dist/utils/localizedHint.js +90 -0
- package/dist/utils/localizedHint.js.map +1 -1
- package/dist/utils/progress.d.ts +10 -1
- package/dist/utils/progress.d.ts.map +1 -1
- package/dist/utils/progress.js +24 -0
- package/dist/utils/progress.js.map +1 -1
- package/dist/utils/scannedBoard.d.ts +79 -0
- package/dist/utils/scannedBoard.d.ts.map +1 -0
- package/dist/utils/scannedBoard.js +96 -0
- package/dist/utils/scannedBoard.js.map +1 -0
- package/dist/utils/shareUrl.d.ts +38 -15
- package/dist/utils/shareUrl.d.ts.map +1 -1
- package/dist/utils/shareUrl.js +75 -26
- package/dist/utils/shareUrl.js.map +1 -1
- package/dist/utils/subscription.d.ts +118 -0
- package/dist/utils/subscription.d.ts.map +1 -1
- package/dist/utils/subscription.js +212 -0
- package/dist/utils/subscription.js.map +1 -1
- package/dist/utils/sudokuPresenter.d.ts +42 -0
- package/dist/utils/sudokuPresenter.d.ts.map +1 -1
- package/dist/utils/sudokuPresenter.js +114 -1
- package/dist/utils/sudokuPresenter.js.map +1 -1
- package/dist/utils/sudokuScrambler.d.ts +8 -0
- package/dist/utils/sudokuScrambler.d.ts.map +1 -1
- package/dist/utils/sudokuScrambler.js +12 -0
- package/dist/utils/sudokuScrambler.js.map +1 -1
- package/dist/utils/technique.d.ts +112 -1
- package/dist/utils/technique.d.ts.map +1 -1
- package/dist/utils/technique.js +293 -1
- package/dist/utils/technique.js.map +1 -1
- package/dist/utils/techniqueBitmask.d.ts +31 -0
- package/dist/utils/techniqueBitmask.d.ts.map +1 -0
- package/dist/utils/techniqueBitmask.js +53 -0
- package/dist/utils/techniqueBitmask.js.map +1 -0
- package/dist/utils/techniqueExample.d.ts +42 -0
- package/dist/utils/techniqueExample.d.ts.map +1 -0
- package/dist/utils/techniqueExample.js +57 -0
- package/dist/utils/techniqueExample.js.map +1 -0
- package/dist/utils/theme.d.ts +2 -0
- package/dist/utils/theme.d.ts.map +1 -1
- package/dist/utils/theme.js +2 -0
- package/dist/utils/theme.js.map +1 -1
- package/package.json +7 -7
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"date.js","sourceRoot":"","sources":["../../src/utils/date.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,YAAY,GAAG,0BAA0B,CAAC;AAEhD,SAAS,IAAI,CAAC,CAAS;IACrB,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AACpC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAa,IAAI,IAAI,EAAE;IACtD,OAAO,GAAG,IAAI,CAAC,cAAc,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,IAAI,IAAI,CACrE,IAAI,CAAC,UAAU,EAAE,CAClB,EAAE,CAAC;AACN,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAChC,KAAgC;IAEhC,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACtD,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,KAAK,CAAC;IAC1B,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3D,IACE,IAAI,CAAC,WAAW,EAAE,KAAK,MAAM,CAAC,CAAC,CAAC;QAChC,IAAI,CAAC,QAAQ,EAAE,KAAK,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC;QACjC,IAAI,CAAC,OAAO,EAAE,KAAK,MAAM,CAAC,CAAC,CAAC,EAC5B,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;AAC1B,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,eAAe,CAC7B,QAAmC,EACnC,MAA0B,EAC1B,UAAsC;IACpC,OAAO,EAAE,OAAO;IAChB,KAAK,EAAE,OAAO;IACd,GAAG,EAAE,SAAS;CACf;IAED,MAAM,UAAU,GAAG,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IAChD,IAAI,CAAC,UAAU;QAAE,OAAO,IAAI,CAAC;IAC7B,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAIjD,CAAC;IACF,OAAO,IAAI,IAAI,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,kBAAkB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AACnE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAC1B,SAAoC,EACpC,MAAY,IAAI,IAAI,EAAE;IAEtB,MAAM,UAAU,GAAG,kBAAkB,CAAC,SAAS,CAAC,CAAC;IACjD,OAAO,UAAU,KAAK,IAAI,IAAI,UAAU,KAAK,gBAAgB,CAAC,GAAG,CAAC,CAAC;AACrE,CAAC","sourcesContent":["/**\n * Calendar-date helpers for daily puzzles.\n *\n * A daily's `date` is a calendar date (`YYYY-MM-DD`), not an instant. The API's\n * `/dailies/today` picks the daily by the **UTC** date, so \"today\" for the\n * daily is the UTC date everywhere in the clients.\n */\n\nconst DATE_PATTERN = /^(\\d{4})-(\\d{2})-(\\d{2})/;\n\nfunction pad2(n: number): string {\n return String(n).padStart(2, '0');\n}\n\n/**\n * The UTC calendar date of `date` (default: now) as `YYYY-MM-DD`, the date\n * the API's `/dailies/today` uses.\n */\nexport function getUtcDateString(date: Date = new Date()): string {\n return `${date.getUTCFullYear()}-${pad2(date.getUTCMonth() + 1)}-${pad2(\n date.getUTCDate()\n )}`;\n}\n\n/**\n * Normalize a daily date (`YYYY-MM-DD`, or an ISO timestamp starting with\n * one) to `YYYY-MM-DD`, without any timezone shift. Returns null when it does\n * not start with a valid date.\n */\nexport function normalizeDailyDate(\n value: string | null | undefined\n): string | null {\n const match = value ? DATE_PATTERN.exec(value) : null;\n if (!match) return null;\n const [, y, m, d] = match;\n const date = new Date(Number(y), Number(m) - 1, Number(d));\n if (\n date.getFullYear() !== Number(y) ||\n date.getMonth() !== Number(m) - 1 ||\n date.getDate() !== Number(d)\n ) {\n return null;\n }\n return `${y}-${m}-${d}`;\n}\n\n/**\n * Format a daily's `YYYY-MM-DD` date for display as that LOCAL calendar date\n * (never shifted by the viewer's UTC offset, which `new Date('2026-10-05')`\n * would do west of UTC).\n *\n * @param yyyyMmDd - The daily's date\n * @param locale - BCP 47 locale (default: the runtime's)\n * @param options - Intl options (default: short weekday, short month, day,\n * e.g. \"Mon, Oct 5\")\n * @returns The formatted date, or null for an invalid date\n */\nexport function formatDailyDate(\n yyyyMmDd: string | null | undefined,\n locale?: string | string[],\n options: Intl.DateTimeFormatOptions = {\n weekday: 'short',\n month: 'short',\n day: 'numeric',\n }\n): string | null {\n const normalized = normalizeDailyDate(yyyyMmDd);\n if (!normalized) return null;\n const [y, m, d] = normalized.split('-').map(Number) as [\n number,\n number,\n number,\n ];\n return new Date(y, m - 1, d).toLocaleDateString(locale, options);\n}\n\n/**\n * Whether a saved daily game is from another day than today's daily\n * (UTC, like the API), so it should be cleared rather than resumed.\n * A missing date counts as stale.\n */\nexport function isStaleDaily(\n dailyDate: string | null | undefined,\n now: Date = new Date()\n): boolean {\n const normalized = normalizeDailyDate(dailyDate);\n return normalized === null || normalized !== getUtcDateString(now);\n}\n"]}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translation of API entities (levels, techniques, badges) in the apps.
|
|
3
|
+
*
|
|
4
|
+
* Two key schemes are in use, and this file documents both:
|
|
5
|
+
*
|
|
6
|
+
* 1. **API localization keys** (`level.localization.title.stringKey`): dotted
|
|
7
|
+
* paths whose first segment names the entity family, e.g. `levels.3.title`,
|
|
8
|
+
* `techniques.full-house.title`, `badges.level_1.title`. Resolve them with
|
|
9
|
+
* `localizedField(tEntity, ...)` where `tEntity` comes from
|
|
10
|
+
* {@link createNamespacedTranslate}.
|
|
11
|
+
* 2. **Namespace keys** the apps look up directly in an i18next namespace:
|
|
12
|
+
* `${level}.title` / `${level}.text` in `levels`, `${path}.title` in
|
|
13
|
+
* `techniques`, `${level}.name` / `${level}.label` in `belts`.
|
|
14
|
+
*
|
|
15
|
+
* The older helpers in `i18nKeys.ts` (`getLevelKey` → `levels.N`,
|
|
16
|
+
* `getBeltKey` → `belts.N.name`) build keys for a single merged namespace and
|
|
17
|
+
* keep that behavior; the display helpers here match what the apps render.
|
|
18
|
+
*/
|
|
19
|
+
import type { LocalizedHint } from '@sudobility/sudojo_types';
|
|
20
|
+
import { type TranslateFunction } from './localizedHint';
|
|
21
|
+
/**
|
|
22
|
+
* Route entity keys to the i18next namespace that holds them.
|
|
23
|
+
*
|
|
24
|
+
* Some families live in their own namespace whose file is NOT nested under the
|
|
25
|
+
* family segment (`levels.json` is `{ "3": { "title": ... } }`), so a key like
|
|
26
|
+
* `levels.3.title` only resolves once the prefix is stripped and the lookup is
|
|
27
|
+
* routed to that namespace. Others (`badges.*`) live nested inside the default
|
|
28
|
+
* namespace and resolve as-is.
|
|
29
|
+
*
|
|
30
|
+
* On a miss the returned function defers to `fallback` with the untouched key,
|
|
31
|
+
* so callers such as `localizedField` still get the original key back, detect
|
|
32
|
+
* the miss, and fall back to the raw API value.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* const tEntity = createNamespacedTranslate(tApp, { levels: tLevels, techniques: tTechniques });
|
|
36
|
+
* localizedField(tEntity, level.localization?.title, level.title);
|
|
37
|
+
*/
|
|
38
|
+
export declare function createNamespacedTranslate(fallback: TranslateFunction, routes: Record<string, TranslateFunction>): TranslateFunction;
|
|
39
|
+
/** The fields of a Level the display helpers read. */
|
|
40
|
+
export interface LevelDisplaySource {
|
|
41
|
+
level: number;
|
|
42
|
+
title?: string | null;
|
|
43
|
+
text?: string | null;
|
|
44
|
+
localization?: {
|
|
45
|
+
title?: LocalizedHint;
|
|
46
|
+
text?: LocalizedHint;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** The fields of a Technique the display helpers read. */
|
|
50
|
+
export interface TechniqueDisplaySource {
|
|
51
|
+
title?: string | null;
|
|
52
|
+
path?: string | null;
|
|
53
|
+
localization?: {
|
|
54
|
+
title?: LocalizedHint;
|
|
55
|
+
text?: LocalizedHint;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Display title for a level, the way both apps build it:
|
|
60
|
+
* `localizedField(tEntity, level.localization?.title, '')`, then
|
|
61
|
+
* `tLevels(`${level}.title`, level.title)`, then the raw title.
|
|
62
|
+
*
|
|
63
|
+
* @param tEntity - Entity translate function (see createNamespacedTranslate)
|
|
64
|
+
* @param tLevels - Optional translate function for the `levels` namespace
|
|
65
|
+
* @returns '' when nothing is available (apps may then show "Level N")
|
|
66
|
+
*/
|
|
67
|
+
export declare function getLevelDisplayTitle(level: LevelDisplaySource, tEntity: TranslateFunction, tLevels?: TranslateFunction): string;
|
|
68
|
+
/**
|
|
69
|
+
* Display description for a level: `localization.text`, then
|
|
70
|
+
* `tLevels(`${level}.text`, level.text)`, then the raw text ('' if none).
|
|
71
|
+
*/
|
|
72
|
+
export declare function getLevelDisplayText(level: LevelDisplaySource, tEntity: TranslateFunction, tLevels?: TranslateFunction): string;
|
|
73
|
+
/**
|
|
74
|
+
* Display title for a technique: `localization.title`, then
|
|
75
|
+
* `tTechniques(`${canonicalPath}.title`, technique.title)`, then the raw title.
|
|
76
|
+
*
|
|
77
|
+
* The namespace key uses the canonical path (`3d-medusa` → `medusa-coloring`),
|
|
78
|
+
* which is how `techniques.json` is keyed.
|
|
79
|
+
*
|
|
80
|
+
* @param tEntity - Entity translate function (see createNamespacedTranslate)
|
|
81
|
+
* @param tTechniques - Optional translate function for the `techniques` namespace
|
|
82
|
+
*/
|
|
83
|
+
export declare function getTechniqueDisplayTitle(technique: TechniqueDisplaySource, tEntity: TranslateFunction, tTechniques?: TranslateFunction): string;
|
|
84
|
+
/**
|
|
85
|
+
* Display name of the belt for a level (e.g. "Yellow"), as AchievementModal
|
|
86
|
+
* shows it: `tBelts(`${level}.name`, belt.name)` in the `belts` namespace,
|
|
87
|
+
* falling back to the belt's English name ('' for an unknown level).
|
|
88
|
+
*
|
|
89
|
+
* @param tBelts - Optional translate function for the `belts` namespace
|
|
90
|
+
*/
|
|
91
|
+
export declare function getBeltDisplayName(level: number, tBelts?: TranslateFunction): string;
|
|
92
|
+
/**
|
|
93
|
+
* Display label of the belt for a level (e.g. "Yellow Belt"):
|
|
94
|
+
* `tBelts(`${level}.label`, `${belt.name} Belt`)` in the `belts` namespace,
|
|
95
|
+
* falling back to `"<name> Belt"` ('' for an unknown level).
|
|
96
|
+
*
|
|
97
|
+
* @param tBelts - Optional translate function for the `belts` namespace
|
|
98
|
+
*/
|
|
99
|
+
export declare function getBeltDisplayLabel(level: number, tBelts?: TranslateFunction): string;
|
|
100
|
+
/**
|
|
101
|
+
* The fields of a Strategy the display helper reads. The API's Strategy has
|
|
102
|
+
* only `stub` today; `title` / `localization` are read when present.
|
|
103
|
+
*/
|
|
104
|
+
export interface StrategyDisplaySource {
|
|
105
|
+
stub: string;
|
|
106
|
+
title?: string | null;
|
|
107
|
+
localization?: {
|
|
108
|
+
title?: LocalizedHint;
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Display title for a strategy: `localization.title`, then
|
|
113
|
+
* `tStrategies(`${stub}.title`, title ?? stub)`, then the raw title, then the
|
|
114
|
+
* stub (the apps' `tStrat(`${stub}.title`, stub)`).
|
|
115
|
+
*
|
|
116
|
+
* @param tEntity - Entity translate function (see createNamespacedTranslate)
|
|
117
|
+
* @param tStrategies - Optional translate function for the `strategies` namespace
|
|
118
|
+
*/
|
|
119
|
+
export declare function getStrategyDisplayTitle(strategy: StrategyDisplaySource, tEntity: TranslateFunction, tStrategies?: TranslateFunction): string;
|
|
120
|
+
//# sourceMappingURL=entityTranslate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entityTranslate.d.ts","sourceRoot":"","sources":["../../src/utils/entityTranslate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAE9D,OAAO,EAAkB,KAAK,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAGzE;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,iBAAiB,EAC3B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,GACxC,iBAAiB,CAenB;AAeD,sDAAsD;AACtD,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,YAAY,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,aAAa,CAAC;QAAC,IAAI,CAAC,EAAE,aAAa,CAAA;KAAE,CAAC;CAChE;AAED,0DAA0D;AAC1D,MAAM,WAAW,sBAAsB;IACrC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,YAAY,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,aAAa,CAAC;QAAC,IAAI,CAAC,EAAE,aAAa,CAAA;KAAE,CAAC;CAChE;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,kBAAkB,EACzB,OAAO,EAAE,iBAAiB,EAC1B,OAAO,CAAC,EAAE,iBAAiB,GAC1B,MAAM,CAOR;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,kBAAkB,EACzB,OAAO,EAAE,iBAAiB,EAC1B,OAAO,CAAC,EAAE,iBAAiB,GAC1B,MAAM,CAOR;AAED;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,sBAAsB,EACjC,OAAO,EAAE,iBAAiB,EAC1B,WAAW,CAAC,EAAE,iBAAiB,GAC9B,MAAM,CAUR;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,MAAM,EACb,MAAM,CAAC,EAAE,iBAAiB,GACzB,MAAM,CAGR;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EACb,MAAM,CAAC,EAAE,iBAAiB,GACzB,MAAM,CAIR;AAED;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,YAAY,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,aAAa,CAAA;KAAE,CAAC;CAC1C;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,qBAAqB,EAC/B,OAAO,EAAE,iBAAiB,EAC1B,WAAW,CAAC,EAAE,iBAAiB,GAC9B,MAAM,CASR"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translation of API entities (levels, techniques, badges) in the apps.
|
|
3
|
+
*
|
|
4
|
+
* Two key schemes are in use, and this file documents both:
|
|
5
|
+
*
|
|
6
|
+
* 1. **API localization keys** (`level.localization.title.stringKey`): dotted
|
|
7
|
+
* paths whose first segment names the entity family, e.g. `levels.3.title`,
|
|
8
|
+
* `techniques.full-house.title`, `badges.level_1.title`. Resolve them with
|
|
9
|
+
* `localizedField(tEntity, ...)` where `tEntity` comes from
|
|
10
|
+
* {@link createNamespacedTranslate}.
|
|
11
|
+
* 2. **Namespace keys** the apps look up directly in an i18next namespace:
|
|
12
|
+
* `${level}.title` / `${level}.text` in `levels`, `${path}.title` in
|
|
13
|
+
* `techniques`, `${level}.name` / `${level}.label` in `belts`.
|
|
14
|
+
*
|
|
15
|
+
* The older helpers in `i18nKeys.ts` (`getLevelKey` → `levels.N`,
|
|
16
|
+
* `getBeltKey` → `belts.N.name`) build keys for a single merged namespace and
|
|
17
|
+
* keep that behavior; the display helpers here match what the apps render.
|
|
18
|
+
*/
|
|
19
|
+
import { getBeltForLevel } from '@sudobility/sudojo_types';
|
|
20
|
+
import { localizedField } from './localizedHint';
|
|
21
|
+
import { toCanonicalTechniquePath } from './technique';
|
|
22
|
+
/**
|
|
23
|
+
* Route entity keys to the i18next namespace that holds them.
|
|
24
|
+
*
|
|
25
|
+
* Some families live in their own namespace whose file is NOT nested under the
|
|
26
|
+
* family segment (`levels.json` is `{ "3": { "title": ... } }`), so a key like
|
|
27
|
+
* `levels.3.title` only resolves once the prefix is stripped and the lookup is
|
|
28
|
+
* routed to that namespace. Others (`badges.*`) live nested inside the default
|
|
29
|
+
* namespace and resolve as-is.
|
|
30
|
+
*
|
|
31
|
+
* On a miss the returned function defers to `fallback` with the untouched key,
|
|
32
|
+
* so callers such as `localizedField` still get the original key back, detect
|
|
33
|
+
* the miss, and fall back to the raw API value.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* const tEntity = createNamespacedTranslate(tApp, { levels: tLevels, techniques: tTechniques });
|
|
37
|
+
* localizedField(tEntity, level.localization?.title, level.title);
|
|
38
|
+
*/
|
|
39
|
+
export function createNamespacedTranslate(fallback, routes) {
|
|
40
|
+
return (key, values) => {
|
|
41
|
+
for (const [prefix, translate] of Object.entries(routes)) {
|
|
42
|
+
const scope = `${prefix}.`;
|
|
43
|
+
if (!key.startsWith(scope))
|
|
44
|
+
continue;
|
|
45
|
+
const scopedKey = key.slice(scope.length);
|
|
46
|
+
const translated = translate(scopedKey, values);
|
|
47
|
+
// i18next returns the key itself when the translation is missing.
|
|
48
|
+
if (translated !== scopedKey)
|
|
49
|
+
return translated;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
return fallback(key, values);
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Translate `key` with i18next-style `defaultValue`, treating a result equal to
|
|
57
|
+
* the key (a `t` that ignores defaultValue) or '' as a miss.
|
|
58
|
+
*/
|
|
59
|
+
function translateOr(t, key, fallback) {
|
|
60
|
+
const translated = t(key, { defaultValue: fallback });
|
|
61
|
+
return translated && translated !== key ? translated : fallback;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Display title for a level, the way both apps build it:
|
|
65
|
+
* `localizedField(tEntity, level.localization?.title, '')`, then
|
|
66
|
+
* `tLevels(`${level}.title`, level.title)`, then the raw title.
|
|
67
|
+
*
|
|
68
|
+
* @param tEntity - Entity translate function (see createNamespacedTranslate)
|
|
69
|
+
* @param tLevels - Optional translate function for the `levels` namespace
|
|
70
|
+
* @returns '' when nothing is available (apps may then show "Level N")
|
|
71
|
+
*/
|
|
72
|
+
export function getLevelDisplayTitle(level, tEntity, tLevels) {
|
|
73
|
+
const raw = level.title ?? '';
|
|
74
|
+
return (localizedField(tEntity, level.localization?.title, '') ||
|
|
75
|
+
(tLevels ? translateOr(tLevels, `${level.level}.title`, raw) : '') ||
|
|
76
|
+
raw);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Display description for a level: `localization.text`, then
|
|
80
|
+
* `tLevels(`${level}.text`, level.text)`, then the raw text ('' if none).
|
|
81
|
+
*/
|
|
82
|
+
export function getLevelDisplayText(level, tEntity, tLevels) {
|
|
83
|
+
const raw = level.text ?? '';
|
|
84
|
+
return (localizedField(tEntity, level.localization?.text, '') ||
|
|
85
|
+
(tLevels ? translateOr(tLevels, `${level.level}.text`, raw) : '') ||
|
|
86
|
+
raw);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Display title for a technique: `localization.title`, then
|
|
90
|
+
* `tTechniques(`${canonicalPath}.title`, technique.title)`, then the raw title.
|
|
91
|
+
*
|
|
92
|
+
* The namespace key uses the canonical path (`3d-medusa` → `medusa-coloring`),
|
|
93
|
+
* which is how `techniques.json` is keyed.
|
|
94
|
+
*
|
|
95
|
+
* @param tEntity - Entity translate function (see createNamespacedTranslate)
|
|
96
|
+
* @param tTechniques - Optional translate function for the `techniques` namespace
|
|
97
|
+
*/
|
|
98
|
+
export function getTechniqueDisplayTitle(technique, tEntity, tTechniques) {
|
|
99
|
+
const raw = technique.title ?? '';
|
|
100
|
+
const path = toCanonicalTechniquePath(technique.path);
|
|
101
|
+
return (localizedField(tEntity, technique.localization?.title, '') ||
|
|
102
|
+
(tTechniques && path
|
|
103
|
+
? translateOr(tTechniques, `${path}.title`, raw)
|
|
104
|
+
: '') ||
|
|
105
|
+
raw);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Display name of the belt for a level (e.g. "Yellow"), as AchievementModal
|
|
109
|
+
* shows it: `tBelts(`${level}.name`, belt.name)` in the `belts` namespace,
|
|
110
|
+
* falling back to the belt's English name ('' for an unknown level).
|
|
111
|
+
*
|
|
112
|
+
* @param tBelts - Optional translate function for the `belts` namespace
|
|
113
|
+
*/
|
|
114
|
+
export function getBeltDisplayName(level, tBelts) {
|
|
115
|
+
const raw = getBeltForLevel(level)?.name ?? '';
|
|
116
|
+
return tBelts ? translateOr(tBelts, `${level}.name`, raw) : raw;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Display label of the belt for a level (e.g. "Yellow Belt"):
|
|
120
|
+
* `tBelts(`${level}.label`, `${belt.name} Belt`)` in the `belts` namespace,
|
|
121
|
+
* falling back to `"<name> Belt"` ('' for an unknown level).
|
|
122
|
+
*
|
|
123
|
+
* @param tBelts - Optional translate function for the `belts` namespace
|
|
124
|
+
*/
|
|
125
|
+
export function getBeltDisplayLabel(level, tBelts) {
|
|
126
|
+
const name = getBeltForLevel(level)?.name;
|
|
127
|
+
const raw = name ? `${name} Belt` : '';
|
|
128
|
+
return tBelts ? translateOr(tBelts, `${level}.label`, raw) : raw;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Display title for a strategy: `localization.title`, then
|
|
132
|
+
* `tStrategies(`${stub}.title`, title ?? stub)`, then the raw title, then the
|
|
133
|
+
* stub (the apps' `tStrat(`${stub}.title`, stub)`).
|
|
134
|
+
*
|
|
135
|
+
* @param tEntity - Entity translate function (see createNamespacedTranslate)
|
|
136
|
+
* @param tStrategies - Optional translate function for the `strategies` namespace
|
|
137
|
+
*/
|
|
138
|
+
export function getStrategyDisplayTitle(strategy, tEntity, tStrategies) {
|
|
139
|
+
const raw = strategy.title || strategy.stub;
|
|
140
|
+
return (localizedField(tEntity, strategy.localization?.title, '') ||
|
|
141
|
+
(tStrategies
|
|
142
|
+
? translateOr(tStrategies, `${strategy.stub}.title`, raw)
|
|
143
|
+
: '') ||
|
|
144
|
+
raw);
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=entityTranslate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entityTranslate.js","sourceRoot":"","sources":["../../src/utils/entityTranslate.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Translation of API entities (levels, techniques, badges) in the apps.\n *\n * Two key schemes are in use, and this file documents both:\n *\n * 1. **API localization keys** (`level.localization.title.stringKey`): dotted\n * paths whose first segment names the entity family, e.g. `levels.3.title`,\n * `techniques.full-house.title`, `badges.level_1.title`. Resolve them with\n * `localizedField(tEntity, ...)` where `tEntity` comes from\n * {@link createNamespacedTranslate}.\n * 2. **Namespace keys** the apps look up directly in an i18next namespace:\n * `${level}.title` / `${level}.text` in `levels`, `${path}.title` in\n * `techniques`, `${level}.name` / `${level}.label` in `belts`.\n *\n * The older helpers in `i18nKeys.ts` (`getLevelKey` → `levels.N`,\n * `getBeltKey` → `belts.N.name`) build keys for a single merged namespace and\n * keep that behavior; the display helpers here match what the apps render.\n */\n\nimport type { LocalizedHint } from '@sudobility/sudojo_types';\nimport { getBeltForLevel } from '@sudobility/sudojo_types';\nimport { localizedField, type TranslateFunction } from './localizedHint';\nimport { toCanonicalTechniquePath } from './technique';\n\n/**\n * Route entity keys to the i18next namespace that holds them.\n *\n * Some families live in their own namespace whose file is NOT nested under the\n * family segment (`levels.json` is `{ \"3\": { \"title\": ... } }`), so a key like\n * `levels.3.title` only resolves once the prefix is stripped and the lookup is\n * routed to that namespace. Others (`badges.*`) live nested inside the default\n * namespace and resolve as-is.\n *\n * On a miss the returned function defers to `fallback` with the untouched key,\n * so callers such as `localizedField` still get the original key back, detect\n * the miss, and fall back to the raw API value.\n *\n * @example\n * const tEntity = createNamespacedTranslate(tApp, { levels: tLevels, techniques: tTechniques });\n * localizedField(tEntity, level.localization?.title, level.title);\n */\nexport function createNamespacedTranslate(\n fallback: TranslateFunction,\n routes: Record<string, TranslateFunction>\n): TranslateFunction {\n return (key, values) => {\n for (const [prefix, translate] of Object.entries(routes)) {\n const scope = `${prefix}.`;\n if (!key.startsWith(scope)) continue;\n\n const scopedKey = key.slice(scope.length);\n const translated = translate(scopedKey, values);\n // i18next returns the key itself when the translation is missing.\n if (translated !== scopedKey) return translated;\n break;\n }\n\n return fallback(key, values);\n };\n}\n\n/**\n * Translate `key` with i18next-style `defaultValue`, treating a result equal to\n * the key (a `t` that ignores defaultValue) or '' as a miss.\n */\nfunction translateOr(\n t: TranslateFunction,\n key: string,\n fallback: string\n): string {\n const translated = t(key, { defaultValue: fallback });\n return translated && translated !== key ? translated : fallback;\n}\n\n/** The fields of a Level the display helpers read. */\nexport interface LevelDisplaySource {\n level: number;\n title?: string | null;\n text?: string | null;\n localization?: { title?: LocalizedHint; text?: LocalizedHint };\n}\n\n/** The fields of a Technique the display helpers read. */\nexport interface TechniqueDisplaySource {\n title?: string | null;\n path?: string | null;\n localization?: { title?: LocalizedHint; text?: LocalizedHint };\n}\n\n/**\n * Display title for a level, the way both apps build it:\n * `localizedField(tEntity, level.localization?.title, '')`, then\n * `tLevels(`${level}.title`, level.title)`, then the raw title.\n *\n * @param tEntity - Entity translate function (see createNamespacedTranslate)\n * @param tLevels - Optional translate function for the `levels` namespace\n * @returns '' when nothing is available (apps may then show \"Level N\")\n */\nexport function getLevelDisplayTitle(\n level: LevelDisplaySource,\n tEntity: TranslateFunction,\n tLevels?: TranslateFunction\n): string {\n const raw = level.title ?? '';\n return (\n localizedField(tEntity, level.localization?.title, '') ||\n (tLevels ? translateOr(tLevels, `${level.level}.title`, raw) : '') ||\n raw\n );\n}\n\n/**\n * Display description for a level: `localization.text`, then\n * `tLevels(`${level}.text`, level.text)`, then the raw text ('' if none).\n */\nexport function getLevelDisplayText(\n level: LevelDisplaySource,\n tEntity: TranslateFunction,\n tLevels?: TranslateFunction\n): string {\n const raw = level.text ?? '';\n return (\n localizedField(tEntity, level.localization?.text, '') ||\n (tLevels ? translateOr(tLevels, `${level.level}.text`, raw) : '') ||\n raw\n );\n}\n\n/**\n * Display title for a technique: `localization.title`, then\n * `tTechniques(`${canonicalPath}.title`, technique.title)`, then the raw title.\n *\n * The namespace key uses the canonical path (`3d-medusa` → `medusa-coloring`),\n * which is how `techniques.json` is keyed.\n *\n * @param tEntity - Entity translate function (see createNamespacedTranslate)\n * @param tTechniques - Optional translate function for the `techniques` namespace\n */\nexport function getTechniqueDisplayTitle(\n technique: TechniqueDisplaySource,\n tEntity: TranslateFunction,\n tTechniques?: TranslateFunction\n): string {\n const raw = technique.title ?? '';\n const path = toCanonicalTechniquePath(technique.path);\n return (\n localizedField(tEntity, technique.localization?.title, '') ||\n (tTechniques && path\n ? translateOr(tTechniques, `${path}.title`, raw)\n : '') ||\n raw\n );\n}\n\n/**\n * Display name of the belt for a level (e.g. \"Yellow\"), as AchievementModal\n * shows it: `tBelts(`${level}.name`, belt.name)` in the `belts` namespace,\n * falling back to the belt's English name ('' for an unknown level).\n *\n * @param tBelts - Optional translate function for the `belts` namespace\n */\nexport function getBeltDisplayName(\n level: number,\n tBelts?: TranslateFunction\n): string {\n const raw = getBeltForLevel(level)?.name ?? '';\n return tBelts ? translateOr(tBelts, `${level}.name`, raw) : raw;\n}\n\n/**\n * Display label of the belt for a level (e.g. \"Yellow Belt\"):\n * `tBelts(`${level}.label`, `${belt.name} Belt`)` in the `belts` namespace,\n * falling back to `\"<name> Belt\"` ('' for an unknown level).\n *\n * @param tBelts - Optional translate function for the `belts` namespace\n */\nexport function getBeltDisplayLabel(\n level: number,\n tBelts?: TranslateFunction\n): string {\n const name = getBeltForLevel(level)?.name;\n const raw = name ? `${name} Belt` : '';\n return tBelts ? translateOr(tBelts, `${level}.label`, raw) : raw;\n}\n\n/**\n * The fields of a Strategy the display helper reads. The API's Strategy has\n * only `stub` today; `title` / `localization` are read when present.\n */\nexport interface StrategyDisplaySource {\n stub: string;\n title?: string | null;\n localization?: { title?: LocalizedHint };\n}\n\n/**\n * Display title for a strategy: `localization.title`, then\n * `tStrategies(`${stub}.title`, title ?? stub)`, then the raw title, then the\n * stub (the apps' `tStrat(`${stub}.title`, stub)`).\n *\n * @param tEntity - Entity translate function (see createNamespacedTranslate)\n * @param tStrategies - Optional translate function for the `strategies` namespace\n */\nexport function getStrategyDisplayTitle(\n strategy: StrategyDisplaySource,\n tEntity: TranslateFunction,\n tStrategies?: TranslateFunction\n): string {\n const raw = strategy.title || strategy.stub;\n return (\n localizedField(tEntity, strategy.localization?.title, '') ||\n (tStrategies\n ? translateOr(tStrategies, `${strategy.stub}.title`, raw)\n : '') ||\n raw\n );\n}\n"]}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status of a game fetch (useLevelGame / useDailyGame): which screen to show.
|
|
3
|
+
*/
|
|
4
|
+
/** Game fetch status indicating what screen to show */
|
|
5
|
+
export type GameFetchStatus = 'loading' | 'success' | 'auth_required' | 'subscription_required' | 'entitlement_required' | 'error';
|
|
6
|
+
/**
|
|
7
|
+
* API response with the optional `action` field older Sudojo API deployments
|
|
8
|
+
* attached to gated responses.
|
|
9
|
+
*/
|
|
10
|
+
export interface GameFetchResponse {
|
|
11
|
+
success: boolean;
|
|
12
|
+
data?: unknown;
|
|
13
|
+
action?: {
|
|
14
|
+
type: string;
|
|
15
|
+
options?: string[];
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Whether a response or error says the user must sign in.
|
|
20
|
+
*
|
|
21
|
+
* Legacy: the current sudojo_api never sends `action: { type: 'auth_required' }`
|
|
22
|
+
* or an "Account required" message (its puzzle and daily GETs are public), so
|
|
23
|
+
* this is false against it. Kept for older deployments; not provably
|
|
24
|
+
* unreachable, because `error` can be anything the network layer throws.
|
|
25
|
+
*/
|
|
26
|
+
export declare function isAuthRequiredResponse(response: GameFetchResponse | null | undefined, error: unknown): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Whether a response or error says a subscription is required.
|
|
29
|
+
*
|
|
30
|
+
* Legacy: the current sudojo_api never sends
|
|
31
|
+
* `action: { type: 'subscription_required' }` or "Daily limit reached" (its
|
|
32
|
+
* daily-limit gate was deleted). The `'subscription'` substring check still
|
|
33
|
+
* matches any error message containing that word, so it is kept.
|
|
34
|
+
*/
|
|
35
|
+
export declare function isSubscriptionRequiredResponse(response: GameFetchResponse | null | undefined, error: unknown): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Derive the fetch status for a game query, in priority order:
|
|
38
|
+
* entitlement denied, loading, auth required, subscription required, error,
|
|
39
|
+
* success (a successful response with data), otherwise still loading.
|
|
40
|
+
*/
|
|
41
|
+
export declare function getGameFetchStatus(state: {
|
|
42
|
+
isLoading: boolean;
|
|
43
|
+
response: GameFetchResponse | null | undefined;
|
|
44
|
+
error: unknown;
|
|
45
|
+
entitlementDenied?: boolean;
|
|
46
|
+
}): GameFetchStatus;
|
|
47
|
+
/**
|
|
48
|
+
* HTTP status carried by an error, when it has one. The apps' network
|
|
49
|
+
* clients throw a NetworkError (`@sudobility/types`) with a numeric `status`
|
|
50
|
+
* for non-2xx responses.
|
|
51
|
+
*/
|
|
52
|
+
export declare function getErrorHttpStatus(error: unknown): number | undefined;
|
|
53
|
+
/** Status of a practice fetch (usePracticeGame): which screen to show. */
|
|
54
|
+
export type PracticeFetchStatus = 'loading' | 'ready' | 'auth_required' | 'subscription_required' | 'no_practices' | 'error';
|
|
55
|
+
/**
|
|
56
|
+
* Derive the practice fetch status (web PracticePage / RN PracticeScreen):
|
|
57
|
+
* - error: HTTP 401 -> auth_required, 402 -> subscription_required, any other
|
|
58
|
+
* status -> error; an error without a status falls back to its message
|
|
59
|
+
* ('401'/'auth' -> auth_required, '402'/'subscription' ->
|
|
60
|
+
* subscription_required)
|
|
61
|
+
* - `success: false` with "No practices found" -> no_practices, else error
|
|
62
|
+
* - a practice -> ready; a successful response without one -> no_practices
|
|
63
|
+
* - otherwise loading
|
|
64
|
+
*/
|
|
65
|
+
export declare function getPracticeFetchStatus(state: {
|
|
66
|
+
isLoading: boolean;
|
|
67
|
+
response: {
|
|
68
|
+
success: boolean;
|
|
69
|
+
data?: unknown;
|
|
70
|
+
error?: string | null | undefined;
|
|
71
|
+
} | null | undefined;
|
|
72
|
+
error: unknown;
|
|
73
|
+
}): PracticeFetchStatus;
|
|
74
|
+
//# sourceMappingURL=gameFetchStatus.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gameFetchStatus.d.ts","sourceRoot":"","sources":["../../src/utils/gameFetchStatus.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,uDAAuD;AACvD,MAAM,MAAM,eAAe,GACvB,SAAS,GACT,SAAS,GACT,eAAe,GACf,uBAAuB,GACvB,sBAAsB,GACtB,OAAO,CAAC;AAEZ;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE;QACP,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;KACpB,CAAC;CACH;AASD;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,iBAAiB,GAAG,IAAI,GAAG,SAAS,EAC9C,KAAK,EAAE,OAAO,GACb,OAAO,CAQT;AAED;;;;;;;GAOG;AACH,wBAAgB,8BAA8B,CAC5C,QAAQ,EAAE,iBAAiB,GAAG,IAAI,GAAG,SAAS,EAC9C,KAAK,EAAE,OAAO,GACb,OAAO,CAaT;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE;IACxC,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,iBAAiB,GAAG,IAAI,GAAG,SAAS,CAAC;IAC/C,KAAK,EAAE,OAAO,CAAC;IACf,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B,GAAG,eAAe,CAWlB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAMrE;AAED,0EAA0E;AAC1E,MAAM,MAAM,mBAAmB,GAC3B,SAAS,GACT,OAAO,GACP,eAAe,GACf,uBAAuB,GACvB,cAAc,GACd,OAAO,CAAC;AAEZ;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE;IAC5C,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,EACJ;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;KAAE,GACvE,IAAI,GACJ,SAAS,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;CAChB,GAAG,mBAAmB,CAyBtB"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status of a game fetch (useLevelGame / useDailyGame): which screen to show.
|
|
3
|
+
*/
|
|
4
|
+
function errorMessage(error) {
|
|
5
|
+
if (error && typeof error === 'object') {
|
|
6
|
+
return error.message;
|
|
7
|
+
}
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Whether a response or error says the user must sign in.
|
|
12
|
+
*
|
|
13
|
+
* Legacy: the current sudojo_api never sends `action: { type: 'auth_required' }`
|
|
14
|
+
* or an "Account required" message (its puzzle and daily GETs are public), so
|
|
15
|
+
* this is false against it. Kept for older deployments; not provably
|
|
16
|
+
* unreachable, because `error` can be anything the network layer throws.
|
|
17
|
+
*/
|
|
18
|
+
export function isAuthRequiredResponse(response, error) {
|
|
19
|
+
if (response?.success === false &&
|
|
20
|
+
response.action?.type === 'auth_required') {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
return errorMessage(error)?.includes('Account required') ?? false;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Whether a response or error says a subscription is required.
|
|
27
|
+
*
|
|
28
|
+
* Legacy: the current sudojo_api never sends
|
|
29
|
+
* `action: { type: 'subscription_required' }` or "Daily limit reached" (its
|
|
30
|
+
* daily-limit gate was deleted). The `'subscription'` substring check still
|
|
31
|
+
* matches any error message containing that word, so it is kept.
|
|
32
|
+
*/
|
|
33
|
+
export function isSubscriptionRequiredResponse(response, error) {
|
|
34
|
+
if (response?.success === false &&
|
|
35
|
+
response.action?.type === 'subscription_required') {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
const message = errorMessage(error);
|
|
39
|
+
return ((message?.includes('Daily limit reached') ||
|
|
40
|
+
message?.includes('subscription')) ??
|
|
41
|
+
false);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Derive the fetch status for a game query, in priority order:
|
|
45
|
+
* entitlement denied, loading, auth required, subscription required, error,
|
|
46
|
+
* success (a successful response with data), otherwise still loading.
|
|
47
|
+
*/
|
|
48
|
+
export function getGameFetchStatus(state) {
|
|
49
|
+
const { isLoading, response, error, entitlementDenied = false } = state;
|
|
50
|
+
if (entitlementDenied)
|
|
51
|
+
return 'entitlement_required';
|
|
52
|
+
if (isLoading)
|
|
53
|
+
return 'loading';
|
|
54
|
+
if (isAuthRequiredResponse(response, error))
|
|
55
|
+
return 'auth_required';
|
|
56
|
+
if (isSubscriptionRequiredResponse(response, error)) {
|
|
57
|
+
return 'subscription_required';
|
|
58
|
+
}
|
|
59
|
+
if (error)
|
|
60
|
+
return 'error';
|
|
61
|
+
if (response?.success && response.data)
|
|
62
|
+
return 'success';
|
|
63
|
+
return 'loading';
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* HTTP status carried by an error, when it has one. The apps' network
|
|
67
|
+
* clients throw a NetworkError (`@sudobility/types`) with a numeric `status`
|
|
68
|
+
* for non-2xx responses.
|
|
69
|
+
*/
|
|
70
|
+
export function getErrorHttpStatus(error) {
|
|
71
|
+
if (error && typeof error === 'object') {
|
|
72
|
+
const status = error.status;
|
|
73
|
+
if (typeof status === 'number')
|
|
74
|
+
return status;
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Derive the practice fetch status (web PracticePage / RN PracticeScreen):
|
|
80
|
+
* - error: HTTP 401 -> auth_required, 402 -> subscription_required, any other
|
|
81
|
+
* status -> error; an error without a status falls back to its message
|
|
82
|
+
* ('401'/'auth' -> auth_required, '402'/'subscription' ->
|
|
83
|
+
* subscription_required)
|
|
84
|
+
* - `success: false` with "No practices found" -> no_practices, else error
|
|
85
|
+
* - a practice -> ready; a successful response without one -> no_practices
|
|
86
|
+
* - otherwise loading
|
|
87
|
+
*/
|
|
88
|
+
export function getPracticeFetchStatus(state) {
|
|
89
|
+
const { isLoading, response, error } = state;
|
|
90
|
+
if (isLoading)
|
|
91
|
+
return 'loading';
|
|
92
|
+
if (error) {
|
|
93
|
+
const httpStatus = getErrorHttpStatus(error);
|
|
94
|
+
if (httpStatus === 401)
|
|
95
|
+
return 'auth_required';
|
|
96
|
+
if (httpStatus === 402)
|
|
97
|
+
return 'subscription_required';
|
|
98
|
+
if (httpStatus === undefined) {
|
|
99
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
100
|
+
if (message.includes('401') || message.includes('auth')) {
|
|
101
|
+
return 'auth_required';
|
|
102
|
+
}
|
|
103
|
+
if (message.includes('402') || message.includes('subscription')) {
|
|
104
|
+
return 'subscription_required';
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return 'error';
|
|
108
|
+
}
|
|
109
|
+
if (response?.success === false) {
|
|
110
|
+
return response.error?.includes('No practices found')
|
|
111
|
+
? 'no_practices'
|
|
112
|
+
: 'error';
|
|
113
|
+
}
|
|
114
|
+
if (response?.success)
|
|
115
|
+
return response.data ? 'ready' : 'no_practices';
|
|
116
|
+
return 'loading';
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=gameFetchStatus.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gameFetchStatus.js","sourceRoot":"","sources":["../../src/utils/gameFetchStatus.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Status of a game fetch (useLevelGame / useDailyGame): which screen to show.\n */\n\n/** Game fetch status indicating what screen to show */\nexport type GameFetchStatus =\n | 'loading'\n | 'success'\n | 'auth_required'\n | 'subscription_required'\n | 'entitlement_required'\n | 'error';\n\n/**\n * API response with the optional `action` field older Sudojo API deployments\n * attached to gated responses.\n */\nexport interface GameFetchResponse {\n success: boolean;\n data?: unknown;\n action?: {\n type: string;\n options?: string[];\n };\n}\n\nfunction errorMessage(error: unknown): string | undefined {\n if (error && typeof error === 'object') {\n return (error as { message?: string }).message;\n }\n return undefined;\n}\n\n/**\n * Whether a response or error says the user must sign in.\n *\n * Legacy: the current sudojo_api never sends `action: { type: 'auth_required' }`\n * or an \"Account required\" message (its puzzle and daily GETs are public), so\n * this is false against it. Kept for older deployments; not provably\n * unreachable, because `error` can be anything the network layer throws.\n */\nexport function isAuthRequiredResponse(\n response: GameFetchResponse | null | undefined,\n error: unknown\n): boolean {\n if (\n response?.success === false &&\n response.action?.type === 'auth_required'\n ) {\n return true;\n }\n return errorMessage(error)?.includes('Account required') ?? false;\n}\n\n/**\n * Whether a response or error says a subscription is required.\n *\n * Legacy: the current sudojo_api never sends\n * `action: { type: 'subscription_required' }` or \"Daily limit reached\" (its\n * daily-limit gate was deleted). The `'subscription'` substring check still\n * matches any error message containing that word, so it is kept.\n */\nexport function isSubscriptionRequiredResponse(\n response: GameFetchResponse | null | undefined,\n error: unknown\n): boolean {\n if (\n response?.success === false &&\n response.action?.type === 'subscription_required'\n ) {\n return true;\n }\n const message = errorMessage(error);\n return (\n (message?.includes('Daily limit reached') ||\n message?.includes('subscription')) ??\n false\n );\n}\n\n/**\n * Derive the fetch status for a game query, in priority order:\n * entitlement denied, loading, auth required, subscription required, error,\n * success (a successful response with data), otherwise still loading.\n */\nexport function getGameFetchStatus(state: {\n isLoading: boolean;\n response: GameFetchResponse | null | undefined;\n error: unknown;\n entitlementDenied?: boolean;\n}): GameFetchStatus {\n const { isLoading, response, error, entitlementDenied = false } = state;\n if (entitlementDenied) return 'entitlement_required';\n if (isLoading) return 'loading';\n if (isAuthRequiredResponse(response, error)) return 'auth_required';\n if (isSubscriptionRequiredResponse(response, error)) {\n return 'subscription_required';\n }\n if (error) return 'error';\n if (response?.success && response.data) return 'success';\n return 'loading';\n}\n\n/**\n * HTTP status carried by an error, when it has one. The apps' network\n * clients throw a NetworkError (`@sudobility/types`) with a numeric `status`\n * for non-2xx responses.\n */\nexport function getErrorHttpStatus(error: unknown): number | undefined {\n if (error && typeof error === 'object') {\n const status = (error as { status?: unknown }).status;\n if (typeof status === 'number') return status;\n }\n return undefined;\n}\n\n/** Status of a practice fetch (usePracticeGame): which screen to show. */\nexport type PracticeFetchStatus =\n | 'loading'\n | 'ready'\n | 'auth_required'\n | 'subscription_required'\n | 'no_practices'\n | 'error';\n\n/**\n * Derive the practice fetch status (web PracticePage / RN PracticeScreen):\n * - error: HTTP 401 -> auth_required, 402 -> subscription_required, any other\n * status -> error; an error without a status falls back to its message\n * ('401'/'auth' -> auth_required, '402'/'subscription' ->\n * subscription_required)\n * - `success: false` with \"No practices found\" -> no_practices, else error\n * - a practice -> ready; a successful response without one -> no_practices\n * - otherwise loading\n */\nexport function getPracticeFetchStatus(state: {\n isLoading: boolean;\n response:\n | { success: boolean; data?: unknown; error?: string | null | undefined }\n | null\n | undefined;\n error: unknown;\n}): PracticeFetchStatus {\n const { isLoading, response, error } = state;\n if (isLoading) return 'loading';\n if (error) {\n const httpStatus = getErrorHttpStatus(error);\n if (httpStatus === 401) return 'auth_required';\n if (httpStatus === 402) return 'subscription_required';\n if (httpStatus === undefined) {\n const message = error instanceof Error ? error.message : String(error);\n if (message.includes('401') || message.includes('auth')) {\n return 'auth_required';\n }\n if (message.includes('402') || message.includes('subscription')) {\n return 'subscription_required';\n }\n }\n return 'error';\n }\n if (response?.success === false) {\n return response.error?.includes('No practices found')\n ? 'no_practices'\n : 'error';\n }\n if (response?.success) return response.data ? 'ready' : 'no_practices';\n return 'loading';\n}\n"]}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gamification helpers: badge progress for the profile / statistics screens.
|
|
3
|
+
*/
|
|
4
|
+
import type { BadgeDefinition } from '@sudobility/sudojo_types';
|
|
5
|
+
/** `badgeType` of the per-level mastery badges (`level_<n>`). */
|
|
6
|
+
export declare const LEVEL_MASTERY_BADGE_TYPE = "level_mastery";
|
|
7
|
+
/** `badgeType` of the games-played milestone badges (`games_<n>`). */
|
|
8
|
+
export declare const GAMES_PLAYED_BADGE_TYPE = "games_played";
|
|
9
|
+
/** Badge definitions split for display, with earned lookup. */
|
|
10
|
+
export interface BadgeProgress<B extends BadgeDefinitionLike = BadgeDefinition> {
|
|
11
|
+
/** `level_mastery` badges, by requirementValue (the level) ascending */
|
|
12
|
+
levelBadges: B[];
|
|
13
|
+
/** `games_played` badges, by requirementValue (games) ascending */
|
|
14
|
+
gameBadges: B[];
|
|
15
|
+
/** Whether the user has earned the badge with this key */
|
|
16
|
+
isEarned: (badgeKey: string) => boolean;
|
|
17
|
+
/** Number of earned badges (distinct keys) */
|
|
18
|
+
earnedCount: number;
|
|
19
|
+
/** Number of badge definitions */
|
|
20
|
+
totalCount: number;
|
|
21
|
+
}
|
|
22
|
+
/** The fields of a badge definition groupBadgeProgress reads. */
|
|
23
|
+
export type BadgeDefinitionLike = Pick<BadgeDefinition, 'badgeKey' | 'badgeType' | 'requirementValue'>;
|
|
24
|
+
/**
|
|
25
|
+
* Split badge definitions into level-mastery and games-played lists, each
|
|
26
|
+
* sorted by requirementValue, as the web ProfilePage and the RN settings /
|
|
27
|
+
* statistics screens show them. Other badge types are left out of both lists
|
|
28
|
+
* but still count in `totalCount`.
|
|
29
|
+
*
|
|
30
|
+
* @param definitions - All badge definitions (GET /gamification/badges)
|
|
31
|
+
* @param earned - The user's earned badges (`stats.badges`)
|
|
32
|
+
*/
|
|
33
|
+
export declare function groupBadgeProgress<B extends BadgeDefinitionLike>(definitions: readonly B[] | null | undefined, earned: ReadonlyArray<{
|
|
34
|
+
badgeKey: string;
|
|
35
|
+
}> | null | undefined): BadgeProgress<B>;
|
|
36
|
+
//# sourceMappingURL=gamification.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gamification.d.ts","sourceRoot":"","sources":["../../src/utils/gamification.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAEhE,iEAAiE;AACjE,eAAO,MAAM,wBAAwB,kBAAkB,CAAC;AAExD,sEAAsE;AACtE,eAAO,MAAM,uBAAuB,iBAAiB,CAAC;AAEtD,+DAA+D;AAC/D,MAAM,WAAW,aAAa,CAC5B,CAAC,SAAS,mBAAmB,GAAG,eAAe;IAE/C,wEAAwE;IACxE,WAAW,EAAE,CAAC,EAAE,CAAC;IACjB,mEAAmE;IACnE,UAAU,EAAE,CAAC,EAAE,CAAC;IAChB,0DAA0D;IAC1D,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;IACxC,8CAA8C;IAC9C,WAAW,EAAE,MAAM,CAAC;IACpB,kCAAkC;IAClC,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,iEAAiE;AACjE,MAAM,MAAM,mBAAmB,GAAG,IAAI,CACpC,eAAe,EACf,UAAU,GAAG,WAAW,GAAG,kBAAkB,CAC9C,CAAC;AAKF;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,mBAAmB,EAC9D,WAAW,EAAE,SAAS,CAAC,EAAE,GAAG,IAAI,GAAG,SAAS,EAC5C,MAAM,EAAE,aAAa,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,IAAI,GAAG,SAAS,GAC7D,aAAa,CAAC,CAAC,CAAC,CAclB"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gamification helpers: badge progress for the profile / statistics screens.
|
|
3
|
+
*/
|
|
4
|
+
/** `badgeType` of the per-level mastery badges (`level_<n>`). */
|
|
5
|
+
export const LEVEL_MASTERY_BADGE_TYPE = 'level_mastery';
|
|
6
|
+
/** `badgeType` of the games-played milestone badges (`games_<n>`). */
|
|
7
|
+
export const GAMES_PLAYED_BADGE_TYPE = 'games_played';
|
|
8
|
+
const byRequirement = (a, b) => (a.requirementValue ?? 0) - (b.requirementValue ?? 0);
|
|
9
|
+
/**
|
|
10
|
+
* Split badge definitions into level-mastery and games-played lists, each
|
|
11
|
+
* sorted by requirementValue, as the web ProfilePage and the RN settings /
|
|
12
|
+
* statistics screens show them. Other badge types are left out of both lists
|
|
13
|
+
* but still count in `totalCount`.
|
|
14
|
+
*
|
|
15
|
+
* @param definitions - All badge definitions (GET /gamification/badges)
|
|
16
|
+
* @param earned - The user's earned badges (`stats.badges`)
|
|
17
|
+
*/
|
|
18
|
+
export function groupBadgeProgress(definitions, earned) {
|
|
19
|
+
const all = definitions ?? [];
|
|
20
|
+
const earnedKeys = new Set((earned ?? []).map(b => b.badgeKey));
|
|
21
|
+
return {
|
|
22
|
+
levelBadges: all
|
|
23
|
+
.filter(b => b.badgeType === LEVEL_MASTERY_BADGE_TYPE)
|
|
24
|
+
.sort(byRequirement),
|
|
25
|
+
gameBadges: all
|
|
26
|
+
.filter(b => b.badgeType === GAMES_PLAYED_BADGE_TYPE)
|
|
27
|
+
.sort(byRequirement),
|
|
28
|
+
isEarned: badgeKey => earnedKeys.has(badgeKey),
|
|
29
|
+
earnedCount: earnedKeys.size,
|
|
30
|
+
totalCount: all.length,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=gamification.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gamification.js","sourceRoot":"","sources":["../../src/utils/gamification.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,iEAAiE;AACjE,MAAM,CAAC,MAAM,wBAAwB,GAAG,eAAe,CAAC;AAExD,sEAAsE;AACtE,MAAM,CAAC,MAAM,uBAAuB,GAAG,cAAc,CAAC;AAwBtD,MAAM,aAAa,GAAG,CAAC,CAAsB,EAAE,CAAsB,EAAE,EAAE,CACvE,CAAC,CAAC,CAAC,gBAAgB,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,gBAAgB,IAAI,CAAC,CAAC,CAAC;AAExD;;;;;;;;GAQG;AACH,MAAM,UAAU,kBAAkB,CAChC,WAA4C,EAC5C,MAA8D;IAE9D,MAAM,GAAG,GAAG,WAAW,IAAI,EAAE,CAAC;IAC9B,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;IAChE,OAAO;QACL,WAAW,EAAE,GAAG;aACb,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,wBAAwB,CAAC;aACrD,IAAI,CAAC,aAAa,CAAC;QACtB,UAAU,EAAE,GAAG;aACZ,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,uBAAuB,CAAC;aACpD,IAAI,CAAC,aAAa,CAAC;QACtB,QAAQ,EAAE,QAAQ,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC;QAC9C,WAAW,EAAE,UAAU,CAAC,IAAI;QAC5B,UAAU,EAAE,GAAG,CAAC,MAAM;KACvB,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Gamification helpers: badge progress for the profile / statistics screens.\n */\n\nimport type { BadgeDefinition } from '@sudobility/sudojo_types';\n\n/** `badgeType` of the per-level mastery badges (`level_<n>`). */\nexport const LEVEL_MASTERY_BADGE_TYPE = 'level_mastery';\n\n/** `badgeType` of the games-played milestone badges (`games_<n>`). */\nexport const GAMES_PLAYED_BADGE_TYPE = 'games_played';\n\n/** Badge definitions split for display, with earned lookup. */\nexport interface BadgeProgress<\n B extends BadgeDefinitionLike = BadgeDefinition,\n> {\n /** `level_mastery` badges, by requirementValue (the level) ascending */\n levelBadges: B[];\n /** `games_played` badges, by requirementValue (games) ascending */\n gameBadges: B[];\n /** Whether the user has earned the badge with this key */\n isEarned: (badgeKey: string) => boolean;\n /** Number of earned badges (distinct keys) */\n earnedCount: number;\n /** Number of badge definitions */\n totalCount: number;\n}\n\n/** The fields of a badge definition groupBadgeProgress reads. */\nexport type BadgeDefinitionLike = Pick<\n BadgeDefinition,\n 'badgeKey' | 'badgeType' | 'requirementValue'\n>;\n\nconst byRequirement = (a: BadgeDefinitionLike, b: BadgeDefinitionLike) =>\n (a.requirementValue ?? 0) - (b.requirementValue ?? 0);\n\n/**\n * Split badge definitions into level-mastery and games-played lists, each\n * sorted by requirementValue, as the web ProfilePage and the RN settings /\n * statistics screens show them. Other badge types are left out of both lists\n * but still count in `totalCount`.\n *\n * @param definitions - All badge definitions (GET /gamification/badges)\n * @param earned - The user's earned badges (`stats.badges`)\n */\nexport function groupBadgeProgress<B extends BadgeDefinitionLike>(\n definitions: readonly B[] | null | undefined,\n earned: ReadonlyArray<{ badgeKey: string }> | null | undefined\n): BadgeProgress<B> {\n const all = definitions ?? [];\n const earnedKeys = new Set((earned ?? []).map(b => b.badgeKey));\n return {\n levelBadges: all\n .filter(b => b.badgeType === LEVEL_MASTERY_BADGE_TYPE)\n .sort(byRequirement),\n gameBadges: all\n .filter(b => b.badgeType === GAMES_PLAYED_BADGE_TYPE)\n .sort(byRequirement),\n isEarned: badgeKey => earnedKeys.has(badgeKey),\n earnedCount: earnedKeys.size,\n totalCount: all.length,\n };\n}\n"]}
|