@jeremyckahn/farmhand 1.23.2 → 1.23.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 (169) hide show
  1. package/README.md +5 -0
  2. package/dist/assets/index-4ynbl-GC.js +392 -0
  3. package/dist/assets/index-4ynbl-GC.js.map +1 -0
  4. package/dist/assets/index-CaYdR-lC.css +1 -0
  5. package/dist/assets/jersey-10-latin-400-normal-DrL73VYW.woff2 +0 -0
  6. package/dist/assets/jersey-10-latin-ext-400-normal-uIKqQSaG.woff2 +0 -0
  7. package/dist/assets/jersey-20-latin-400-normal-bNi78tgO.woff2 +0 -0
  8. package/dist/assets/jersey-20-latin-ext-400-normal-BO3lmWef.woff2 +0 -0
  9. package/dist/assets/jersey-25-latin-400-normal-BuYJnnt7.woff2 +0 -0
  10. package/dist/assets/jersey-25-latin-ext-400-normal-CP5Jl3WN.woff2 +0 -0
  11. package/dist/index.html +2 -2
  12. package/dist/service-worker.js +1 -1
  13. package/dist/service-worker.js.map +1 -1
  14. package/package.json +7 -12
  15. package/src/common/services/randomNumber.test.ts +61 -1
  16. package/src/common/services/randomNumber.ts +36 -1
  17. package/src/common/utils.ts +22 -13
  18. package/src/components/AccountingView/AccountingView.tsx +7 -13
  19. package/src/components/Achievement/Achievement.tsx +2 -15
  20. package/src/components/AchievementsView/AchievementsView.tsx +2 -7
  21. package/src/components/AnimatedNumber/AnimatedNumber.tsx +0 -6
  22. package/src/components/AppBar/AppBar.tsx +21 -17
  23. package/src/components/Cellar/Cellar.tsx +0 -2
  24. package/src/components/Cellar/CellarInventoryTabPanel.tsx +0 -6
  25. package/src/components/Cellar/FermentationTabPanel.tsx +0 -6
  26. package/src/components/Cellar/Keg.tsx +0 -5
  27. package/src/components/Cellar/TabPanel/index.tsx +0 -7
  28. package/src/components/Cellar/WinemakingTabPanel.tsx +0 -6
  29. package/src/components/ContextPane/ContextPane.tsx +0 -6
  30. package/src/components/CowCard/Bloodline/Bloodline.tsx +0 -6
  31. package/src/components/CowCard/CowCard.tsx +7 -39
  32. package/src/components/CowCard/Subheader/Subheader.tsx +21 -44
  33. package/src/components/CowPen/Cow.tsx +5 -11
  34. package/src/components/CowPen/CowPen.test.tsx +4 -4
  35. package/src/components/CowPen/CowPen.tsx +5 -16
  36. package/src/components/CowPenContextMenu/CowPenContextMenu.tsx +2 -18
  37. package/src/components/CowPenContextMenu/TabPanel/index.tsx +0 -7
  38. package/src/components/Farmhand/Farmhand.context.tsx +4 -3
  39. package/src/components/Farmhand/Farmhand.tsx +11 -15
  40. package/src/components/Farmhand/FarmhandReducers.tsx +1 -2
  41. package/src/components/Farmhand/FarmhandService.test.ts +6 -9
  42. package/src/components/Farmhand/hooks/useFarmhandDayAdvancement.ts +2 -6
  43. package/src/components/Farmhand/hooks/useFarmhandNetwork.test.ts +2 -2
  44. package/src/components/Farmhand/hooks/useFarmhandNetwork.ts +15 -31
  45. package/src/components/Farmhand/useFarmhand.test.ts +7 -5
  46. package/src/components/Farmhand/useFarmhand.ts +24 -15
  47. package/src/components/FermentationRecipeList/FermentationRecipe.tsx +0 -5
  48. package/src/components/FermentationRecipeList/FermentationRecipeList.test.tsx +10 -11
  49. package/src/components/FermentationRecipeList/FermentationRecipeList.tsx +2 -3
  50. package/src/components/Field/Field.tsx +15 -50
  51. package/src/components/Forest/Forest.tsx +10 -7
  52. package/src/components/ForestPlot/ForestPlot.tsx +5 -5
  53. package/src/components/ForestQuickSelect/ForestQuickSelect.tsx +0 -12
  54. package/src/components/Home/Home.tsx +2 -7
  55. package/src/components/Home/SnowBackground.tsx +1 -1
  56. package/src/components/IngredientsList/IngredientsList.tsx +0 -7
  57. package/src/components/Inventory/Inventory.test.tsx +4 -4
  58. package/src/components/Inventory/Inventory.tsx +5 -12
  59. package/src/components/Item/Item.tsx +11 -29
  60. package/src/components/ItemList/ItemList.tsx +4 -11
  61. package/src/components/KeybindingsView/KeybindingsView.tsx +0 -2
  62. package/src/components/LogView/LogView.tsx +6 -28
  63. package/src/components/Navigation/DayAndProgressContainer.tsx +0 -8
  64. package/src/components/Navigation/Navigation.tsx +12 -33
  65. package/src/components/NotificationSystem/NotificationSystem.test.tsx +45 -3
  66. package/src/components/NotificationSystem/NotificationSystem.tsx +45 -32
  67. package/src/components/OnlinePeersView/OnlinePeer/OnlinePeer.test.tsx +2 -2
  68. package/src/components/OnlinePeersView/OnlinePeer/OnlinePeer.tsx +0 -11
  69. package/src/components/OnlinePeersView/OnlinePeersView.tsx +0 -10
  70. package/src/components/PixelIcon/createPixelIcon.test.tsx +59 -0
  71. package/src/components/PixelIcon/createPixelIcon.tsx +97 -0
  72. package/src/components/PixelIcon/icons.ts +445 -0
  73. package/src/components/PixelIcon/index.ts +3 -0
  74. package/src/components/Plot/Plot.tsx +3 -3
  75. package/src/components/PriceEventView/PriceEventView.tsx +0 -6
  76. package/src/components/ProgressBar/ProgressBar.tsx +9 -8
  77. package/src/components/QuantityInput/QuantityInput.tsx +6 -11
  78. package/src/components/QuickSelect/QuickSelect.tsx +0 -10
  79. package/src/components/Recipe/Recipe.tsx +0 -9
  80. package/src/components/RecipeList/RecipeList.tsx +0 -6
  81. package/src/components/SearchBar/SearchBar.tsx +4 -10
  82. package/src/components/SettingsView/RandomSeedInput.test.tsx +2 -4
  83. package/src/components/SettingsView/RandomSeedInput.tsx +5 -8
  84. package/src/components/SettingsView/SettingsView.test.tsx +21 -0
  85. package/src/components/SettingsView/SettingsView.tsx +24 -18
  86. package/src/components/Shop/Shop.tsx +0 -20
  87. package/src/components/Shop/TabPanel/index.tsx +0 -7
  88. package/src/components/Stage/Stage.tsx +11 -20
  89. package/src/components/StatsView/StatsView.tsx +0 -18
  90. package/src/components/TierPurchase/TierPurchase.tsx +0 -12
  91. package/src/components/Toolbelt/Toolbelt.tsx +5 -10
  92. package/src/components/UpdateNotifier/UpdateNotifier.tsx +33 -5
  93. package/src/components/UpgradePurchase/UpgradePurchase.tsx +0 -10
  94. package/src/components/WineRecipeList/WineRecipe.test.tsx +2 -2
  95. package/src/components/WineRecipeList/WineRecipe.tsx +0 -5
  96. package/src/components/WineRecipeList/WineRecipeList.tsx +2 -3
  97. package/src/components/Workshop/ForgeTabPanel.tsx +0 -8
  98. package/src/components/Workshop/KitchenTabPanel.tsx +0 -7
  99. package/src/components/Workshop/RecyclingTabPanel.tsx +0 -7
  100. package/src/components/Workshop/TabPanel/index.tsx +0 -7
  101. package/src/components/Workshop/WoodChipperTabPanel.tsx +0 -7
  102. package/src/components/Workshop/Workshop.tsx +0 -9
  103. package/src/components/Workshop/getUpgradesAvailable.ts +1 -1
  104. package/src/constants.ts +18 -13
  105. package/src/data/crops/grape.ts +13 -15
  106. package/src/data/maps.test.ts +3 -4
  107. package/src/data/maps.ts +27 -29
  108. package/src/enums.ts +47 -19
  109. package/src/factories/OreFactory.ts +2 -1
  110. package/src/farmhand.d.ts +6 -3
  111. package/src/game-logic/reducers/applyPrecipitation.ts +4 -4
  112. package/src/game-logic/reducers/generatePriceEvents.ts +6 -3
  113. package/src/game-logic/reducers/harvestForestPlot.ts +1 -2
  114. package/src/game-logic/reducers/makeFermentationRecipe.ts +2 -3
  115. package/src/game-logic/reducers/minePlot.test.ts +2 -2
  116. package/src/game-logic/reducers/minePlot.ts +1 -1
  117. package/src/game-logic/reducers/processCowFertilizerProduction.test.ts +16 -14
  118. package/src/game-logic/reducers/processFeedingCows.test.ts +2 -5
  119. package/src/game-logic/reducers/purchaseCow.ts +2 -7
  120. package/src/game-logic/reducers/showNotification.test.ts +20 -0
  121. package/src/game-logic/reducers/showNotification.ts +4 -2
  122. package/src/game-logic/reducers/updateAchievements.ts +1 -1
  123. package/src/game-logic/reducers/updateFinancialRecords.test.ts +21 -25
  124. package/src/game-logic/reducers/updateFinancialRecords.ts +2 -5
  125. package/src/game-logic/reducers/updateInventoryRecordsForNextDay.test.ts +12 -14
  126. package/src/handlers/ui-events.test.ts +44 -42
  127. package/src/handlers/ui-events.tsx +18 -3
  128. package/src/hooks/usePrefersReducedMotion.ts +10 -6
  129. package/src/index.tsx +0 -2
  130. package/src/mui-theme.ts +258 -37
  131. package/src/scripts/generate-crop-table.ts +3 -2
  132. package/src/setupTests.ts +18 -12
  133. package/src/strings.ts +1 -1
  134. package/src/styles/fonts.ts +73 -0
  135. package/src/styles/pixel.ts +151 -0
  136. package/src/styles/sx.ts +46 -33
  137. package/src/styles/tokens.ts +11 -0
  138. package/src/test-utils/index.ts +1 -0
  139. package/src/utils/chooseRandom.ts +4 -1
  140. package/src/utils/chooseRandomIndex.ts +6 -2
  141. package/src/utils/getCowImage.ts +22 -27
  142. package/src/utils/getMaxYieldOfFermentationRecipe.ts +2 -2
  143. package/src/utils/getPeerMetadata.ts +1 -1
  144. package/src/utils/getPlotImage.test.ts +14 -14
  145. package/src/utils/getPlotImage.ts +10 -10
  146. package/src/utils/getRandomUnlockedCrop.ts +5 -3
  147. package/src/utils/getRandomizedLifespan.test.ts +2 -8
  148. package/src/utils/getValueAdjustmentStream.test.ts +9 -0
  149. package/src/utils/getValueAdjustmentStream.ts +5 -0
  150. package/src/utils/isItemAFarmProduct.ts +2 -2
  151. package/src/utils/shouldPrecipitateToday.ts +3 -1
  152. package/src/utils/shouldStormToday.ts +2 -1
  153. package/src/utils/transformStateDataForImport.ts +3 -5
  154. package/dist/assets/francois-one-latin-400-normal-DD1rFI7H.woff2 +0 -0
  155. package/dist/assets/francois-one-latin-400-normal-DNXs7uOx.woff +0 -0
  156. package/dist/assets/francois-one-latin-ext-400-normal-CwRdWRZD.woff2 +0 -0
  157. package/dist/assets/francois-one-latin-ext-400-normal-JvkpT0Av.woff +0 -0
  158. package/dist/assets/francois-one-vietnamese-400-normal-BLIKeG5Y.woff2 +0 -0
  159. package/dist/assets/francois-one-vietnamese-400-normal-CuWeHlXs.woff +0 -0
  160. package/dist/assets/index-CNcrTYN-.css +0 -1
  161. package/dist/assets/index-DNPchtK9.js +0 -844
  162. package/dist/assets/index-DNPchtK9.js.map +0 -1
  163. package/dist/assets/public-sans-latin-400-normal-8Rpg0ruU.woff2 +0 -0
  164. package/dist/assets/public-sans-latin-400-normal-SBbinRkI.woff +0 -0
  165. package/dist/assets/public-sans-latin-ext-400-normal-B02wwOdZ.woff +0 -0
  166. package/dist/assets/public-sans-latin-ext-400-normal-mk90oQqJ.woff2 +0 -0
  167. package/dist/assets/public-sans-vietnamese-400-normal-BO1meqcx.woff +0 -0
  168. package/dist/assets/public-sans-vietnamese-400-normal-TxZB8OSn.woff2 +0 -0
  169. package/src/fontsource.d.ts +0 -3
@@ -63,6 +63,4 @@ const KeybindingsView = () => (
63
63
  </div>
64
64
  )
65
65
 
66
- KeybindingsView.propTypes = {}
67
-
68
66
  export default KeybindingsView
@@ -1,5 +1,4 @@
1
1
  import React from 'react'
2
- import { array } from 'prop-types'
3
2
  import Alert from '@mui/material/Alert/index.js'
4
3
  import { AlertColor } from '@mui/material'
5
4
  import ReactMarkdown from 'react-markdown'
@@ -7,6 +6,7 @@ import Divider from '@mui/material/Divider/index.js'
7
6
 
8
7
  import FarmhandContext from '../Farmhand/Farmhand.context.js'
9
8
  import { Div } from '../Elements/index.js'
9
+ import { NotificationAlert } from '../NotificationSystem/NotificationSystem.js'
10
10
 
11
11
  export const LogView = ({
12
12
  notificationLog,
@@ -24,28 +24,11 @@ export const LogView = ({
24
24
  >
25
25
  <h3>Today</h3>
26
26
  <ul>
27
- {todaysNotifications.map(
28
- ({
29
- message,
30
- onClick,
31
- severity,
32
- }: farmhand.notification & { onClick?: () => void }) => (
33
- <li {...{ key: message }}>
34
- <Alert
35
- {...{
36
- elevation: 3,
37
- onClick,
38
- severity,
39
- style: {
40
- cursor: onClick ? 'pointer' : 'default',
41
- },
42
- }}
43
- >
44
- <ReactMarkdown {...{ source: message }} />
45
- </Alert>
46
- </li>
47
- )
48
- )}
27
+ {todaysNotifications.map(notification => (
28
+ <li {...{ key: notification.message }}>
29
+ <NotificationAlert {...{ notification }} />
30
+ </li>
31
+ ))}
49
32
  </ul>
50
33
  <Divider />
51
34
  <ul>
@@ -88,11 +71,6 @@ export const LogView = ({
88
71
  </Div>
89
72
  )
90
73
 
91
- LogView.propTypes = {
92
- notificationLog: array.isRequired,
93
- todaysNotifications: array.isRequired,
94
- }
95
-
96
74
  export default function Consumer(
97
75
  props: Partial<Parameters<typeof LogView>[0]>
98
76
  ) {
@@ -1,5 +1,4 @@
1
1
  import React from 'react'
2
- import { number, object } from 'prop-types'
3
2
 
4
3
  import Box from '@mui/material/Box/index.js'
5
4
  import CircularProgress from '@mui/material/CircularProgress/index.js'
@@ -41,7 +40,6 @@ export function DayAndProgressContainer({
41
40
  <span>Day {integerString(dayCount)}, level:</span>
42
41
  <Tooltip
43
42
  {...{
44
- arrow: true,
45
43
  placement: 'top',
46
44
  title: EXPERIENCE_GAUGE_TOOLTIP_LABEL`${experiencePointsToNextLevel}${nextLevel}`,
47
45
  }}
@@ -62,12 +60,6 @@ export function DayAndProgressContainer({
62
60
  )
63
61
  }
64
62
 
65
- DayAndProgressContainer.propTypes = {
66
- dayCount: number.isRequired,
67
- experience: number.isRequired,
68
- itemsSold: object.isRequired,
69
- }
70
-
71
63
  export default function Consumer(
72
64
  props: Partial<Parameters<typeof DayAndProgressContainer>[0]>
73
65
  ) {
@@ -1,12 +1,6 @@
1
1
  import classNames from 'classnames'
2
2
  import React, { useEffect, useState } from 'react'
3
3
 
4
- import AccountBalanceIcon from '@mui/icons-material/AccountBalance.js'
5
- import AssessmentIcon from '@mui/icons-material/Assessment.js'
6
- import BeenhereIcon from '@mui/icons-material/Beenhere.js'
7
- import BookIcon from '@mui/icons-material/Book.js'
8
- import FlashOnIcon from '@mui/icons-material/FlashOn.js'
9
- import SettingsIcon from '@mui/icons-material/Settings.js'
10
4
  import Button from '@mui/material/Button/index.js'
11
5
  import Dialog from '@mui/material/Dialog/index.js'
12
6
  import DialogActions from '@mui/material/DialogActions/index.js'
@@ -22,7 +16,15 @@ import Switch from '@mui/material/Switch/index.js'
22
16
  import TextField from '@mui/material/TextField/index.js'
23
17
  import Tooltip from '@mui/material/Tooltip/index.js'
24
18
  import Typography from '@mui/material/Typography/index.js'
25
- import { array, bool, func, number, string } from 'prop-types'
19
+
20
+ import {
21
+ AccountBalanceIcon,
22
+ AssessmentIcon,
23
+ BeenhereIcon,
24
+ BookIcon,
25
+ FlashOnIcon,
26
+ SettingsIcon,
27
+ } from '../PixelIcon/index.js'
26
28
 
27
29
  import { MAX_ROOM_NAME_LENGTH } from '../../common/constants.js'
28
30
  import {
@@ -36,7 +38,7 @@ import { integerString } from '../../utils/integerString.js'
36
38
  import { inventorySpaceConsumed } from '../../utils/inventorySpaceConsumed.js'
37
39
  import FarmhandContext from '../Farmhand/Farmhand.context.js'
38
40
 
39
- import { breakpoints, colors } from '../../styles/tokens.js'
41
+ import { breakpoints, colors, fonts } from '../../styles/tokens.js'
40
42
  import AccountingView from '../AccountingView/index.js'
41
43
  import AchievementsView from '../AchievementsView/index.js'
42
44
  import { H3, Header } from '../Elements/index.js'
@@ -304,7 +306,7 @@ export const Navigation = ({
304
306
  flexShrink: 0,
305
307
  display: 'flex',
306
308
  '& .version': {
307
- fontFamily: '"Francois One", sans-serif',
309
+ fontFamily: fonts.display,
308
310
  textAlign: 'center',
309
311
  },
310
312
  '& .farm-name': {
@@ -314,7 +316,7 @@ export const Navigation = ({
314
316
  background: 'none',
315
317
  margin: 0,
316
318
  '& input': {
317
- fontFamily: '"Francois One"',
319
+ fontFamily: fonts.display,
318
320
  paddingLeft: '0.5em',
319
321
  paddingRight: '0.5em',
320
322
  textAlign: 'center',
@@ -440,7 +442,6 @@ export const Navigation = ({
440
442
  ].map(({ dialogView: dialogViewType, Icon }) => (
441
443
  <Tooltip
442
444
  {...{
443
- arrow: true,
444
445
  key: dialogViewType,
445
446
  placement: 'top',
446
447
  title: dialogTriggerTextMap[dialogViewType],
@@ -492,28 +493,6 @@ export const Navigation = ({
492
493
  )
493
494
  }
494
495
 
495
- Navigation.propTypes = {
496
- activePlayers: number,
497
- blockInput: bool.isRequired,
498
- farmName: string.isRequired,
499
- handleClickDialogViewButton: func.isRequired,
500
- handleChatRoomOpenStateChange: func.isRequired,
501
- handleActivePlayerButtonClick: func.isRequired,
502
- handleCloseDialogView: func.isRequired,
503
- handleDialogViewExited: func.isRequired,
504
- handleFarmNameUpdate: func.isRequired,
505
- handleOnlineToggleChange: func.isRequired,
506
- handleRoomChange: func.isRequired,
507
- handleViewChange: func.isRequired,
508
- inventory: array.isRequired,
509
- inventoryLimit: number.isRequired,
510
- isChatAvailable: bool.isRequired,
511
- isDialogViewOpen: bool.isRequired,
512
- isOnline: bool.isRequired,
513
- stageFocus: string.isRequired,
514
- viewList: array.isRequired,
515
- }
516
-
517
496
  export default function Consumer(
518
497
  props: Partial<Parameters<typeof Navigation>[0]>
519
498
  ) {
@@ -1,12 +1,20 @@
1
1
  import React from 'react'
2
- import { render } from '@testing-library/react'
2
+ import { render, screen } from '@testing-library/react'
3
+ import userEvent from '@testing-library/user-event'
3
4
  import { SnackbarProvider } from 'notistack'
4
5
 
5
- import { NotificationSystem, getNotificationKey } from './NotificationSystem.js'
6
+ import { NOTIFICATION_DURATION_DEFAULT } from '../../constants.js'
7
+
8
+ import {
9
+ NotificationAlert,
10
+ NotificationSystem,
11
+ getNotificationKey,
12
+ } from './NotificationSystem.js'
6
13
 
7
14
  const defaultProps = {
8
15
  enqueueSnackbar: vitest.fn(),
9
16
  latestNotification: null,
17
+ notificationDuration: NOTIFICATION_DURATION_DEFAULT,
10
18
  }
11
19
 
12
20
  const renderWithSnackbar = (component: React.ReactElement) => {
@@ -44,7 +52,7 @@ test('calls enqueueSnackbar with a content-derived key when latestNotification i
44
52
 
45
53
  expect(enqueueSnackbar).toHaveBeenCalledWith(latestNotification, {
46
54
  key: 'info:Test notification',
47
- autoHideDuration: 1, // NOTIFICATION_DURATION in test mode
55
+ autoHideDuration: 1,
48
56
  preventDuplicate: true,
49
57
  })
50
58
  })
@@ -145,3 +153,37 @@ test('uses the same key for repeated notifications with identical message and se
145
153
  enqueueSnackbar.mock.calls[1][1].key
146
154
  )
147
155
  })
156
+
157
+ describe('NotificationAlert', () => {
158
+ test('renders the message without an action button by default', () => {
159
+ render(
160
+ <NotificationAlert
161
+ notification={{ message: 'Hello', severity: 'info' }}
162
+ />
163
+ )
164
+
165
+ expect(screen.getByText('Hello')).toBeInTheDocument()
166
+ expect(screen.queryByRole('button')).not.toBeInTheDocument()
167
+ })
168
+
169
+ test('renders onClick as a labeled button rather than a clickable alert', async () => {
170
+ const onClick = vitest.fn()
171
+
172
+ render(
173
+ <NotificationAlert
174
+ notification={{
175
+ actionLabel: 'Reload',
176
+ message: 'Update available',
177
+ onClick,
178
+ severity: 'success',
179
+ }}
180
+ />
181
+ )
182
+
183
+ await userEvent.click(screen.getByText('Update available'))
184
+ expect(onClick).not.toHaveBeenCalled()
185
+
186
+ await userEvent.click(screen.getByRole('button', { name: 'Reload' }))
187
+ expect(onClick).toHaveBeenCalledTimes(1)
188
+ })
189
+ })
@@ -1,10 +1,10 @@
1
1
  import React, { useEffect } from 'react'
2
- import { func, shape, string } from 'prop-types'
3
2
  import Alert from '@mui/material/Alert/index.js'
3
+ import Button from '@mui/material/Button/index.js'
4
4
  import ReactMarkdown from 'react-markdown'
5
5
  import { withSnackbar } from 'notistack'
6
6
 
7
- import { NOTIFICATION_DURATION } from '../../constants.js'
7
+ import { getNotificationDuration } from '../../constants.js'
8
8
  import FarmhandContext from '../Farmhand/Farmhand.context.js'
9
9
 
10
10
  export const getNotificationKey = ({
@@ -12,35 +12,56 @@ export const getNotificationKey = ({
12
12
  severity,
13
13
  }: farmhand.notification): string => `${severity}:${message}`
14
14
 
15
+ /**
16
+ * Renders a notification as an Alert. A notification with an onClick gets a
17
+ * button for it rather than making the whole Alert clickable: on Android,
18
+ * clickable alerts rendered with a corrupted, partially dark frame.
19
+ */
20
+ export const NotificationAlert = React.forwardRef<
21
+ HTMLDivElement,
22
+ { notification: farmhand.notification }
23
+ >(function NotificationAlert(
24
+ { notification: { actionLabel = 'OK', message, onClick, severity } },
25
+ ref
26
+ ) {
27
+ return (
28
+ <Alert
29
+ {...{
30
+ ref,
31
+ elevation: 3,
32
+ severity,
33
+ action: onClick ? (
34
+ <Button
35
+ {...{
36
+ color: 'inherit',
37
+ onClick,
38
+ size: 'small',
39
+ variant: 'outlined',
40
+ }}
41
+ >
42
+ {actionLabel}
43
+ </Button>
44
+ ) : undefined,
45
+ }}
46
+ >
47
+ <ReactMarkdown {...{ source: message }} />
48
+ </Alert>
49
+ )
50
+ })
51
+
15
52
  export const snackbarProviderContentCallback = (
16
53
  key: string | number,
17
- {
18
- message,
19
- onClick,
20
- severity,
21
- }: farmhand.notification & { onClick?: () => void }
22
- ) => (
23
- <Alert
24
- {...{
25
- elevation: 3,
26
- key,
27
- onClick,
28
- severity,
29
- style: {
30
- cursor: onClick ? 'pointer' : 'default',
31
- },
32
- }}
33
- >
34
- <ReactMarkdown {...{ source: message }} />
35
- </Alert>
36
- )
54
+ notification: farmhand.notification
55
+ ) => <NotificationAlert {...{ key, notification }} />
37
56
 
38
57
  export const NotificationSystem = ({
39
58
  enqueueSnackbar,
40
59
  latestNotification,
60
+ notificationDuration,
41
61
  }: {
42
62
  enqueueSnackbar: (notification: farmhand.notification, options: any) => void
43
63
  latestNotification: farmhand.notification | null
64
+ notificationDuration: number
44
65
  }) => {
45
66
  useEffect(() => {
46
67
  if (!latestNotification) {
@@ -55,22 +76,14 @@ export const NotificationSystem = ({
55
76
  // once autoHideDuration and a key are set - no onClose needed here.
56
77
  enqueueSnackbar(latestNotification, {
57
78
  key: getNotificationKey(latestNotification),
58
- autoHideDuration: NOTIFICATION_DURATION,
79
+ autoHideDuration: getNotificationDuration(notificationDuration),
59
80
  preventDuplicate: true,
60
81
  })
61
- }, [enqueueSnackbar, latestNotification])
82
+ }, [enqueueSnackbar, latestNotification, notificationDuration])
62
83
 
63
84
  return null
64
85
  }
65
86
 
66
- NotificationSystem.propTypes = {
67
- latestNotification: shape({
68
- message: string.isRequired,
69
- onClick: func,
70
- severity: string.isRequired,
71
- }),
72
- }
73
-
74
87
  export default withSnackbar(function Consumer(props: any) {
75
88
  return (
76
89
  <FarmhandContext.Consumer>
@@ -71,7 +71,7 @@ test('does not render CowCard when no cow is offered for trade', () => {
71
71
  test('renders CowCard when cow is offered for trade', () => {
72
72
  const peerWithCow = {
73
73
  ...defaultPeer,
74
- cowOfferedForTrade: ({
74
+ cowOfferedForTrade: {
75
75
  id: 'test-cow',
76
76
  name: 'Test Cow',
77
77
  color: 'BROWN',
@@ -82,7 +82,7 @@ test('renders CowCard when cow is offered for trade', () => {
82
82
  gender: 'FEMALE',
83
83
  colorsInBloodline: { BROWN: true },
84
84
  originalOwnerId: 'peer-123',
85
- } as unknown) as farmhand.cow,
85
+ } as unknown as farmhand.cow,
86
86
  }
87
87
 
88
88
  render(<OnlinePeer peer={peerWithCow} />)
@@ -1,5 +1,4 @@
1
1
  import React from 'react'
2
- import { number, object, shape, string } from 'prop-types'
3
2
  import Card from '@mui/material/Card/index.js'
4
3
  import CardContent from '@mui/material/CardContent/index.js'
5
4
  import CardHeader from '@mui/material/CardHeader/index.js'
@@ -54,14 +53,4 @@ const OnlinePeer = ({
54
53
  )
55
54
  }
56
55
 
57
- OnlinePeer.propTypes = {
58
- peer: shape({
59
- cowOfferedForTrade: object,
60
- dayCount: number.isRequired,
61
- experience: number.isRequired,
62
- playerId: string.isRequired,
63
- money: number.isRequired,
64
- }).isRequired,
65
- }
66
-
67
56
  export default OnlinePeer
@@ -5,7 +5,6 @@ import Divider from '@mui/material/Divider/index.js'
5
5
  import Card from '@mui/material/Card/index.js'
6
6
  import CardContent from '@mui/material/CardContent/index.js'
7
7
  import sortBy from 'lodash.sortby'
8
- import { array, number, object, string } from 'prop-types'
9
8
 
10
9
  import BailOutErrorBoundary from '../BailOutErrorBoundary/index.js'
11
10
 
@@ -117,15 +116,6 @@ const OnlinePeersView = ({
117
116
  )
118
117
  }
119
118
 
120
- OnlinePeersView.propTypes = {
121
- activePlayers: number.isRequired,
122
- cowIdOfferedForTrade: string.isRequired,
123
- cowInventory: array.isRequired,
124
- playerId: string.isRequired,
125
- latestPeerMessages: array.isRequired,
126
- peers: object.isRequired,
127
- }
128
-
129
119
  export { OnlinePeersView }
130
120
 
131
121
  export default function Consumer(
@@ -0,0 +1,59 @@
1
+ import React, { createRef } from 'react'
2
+ import { render, screen } from '@testing-library/react'
3
+ import Tooltip from '@mui/material/Tooltip/index.js'
4
+
5
+ import { createPixelIcon, flipX, flipY, rotate } from './createPixelIcon.js'
6
+
7
+ const TestIcon = createPixelIcon(['#.', '+#'], 'Test')
8
+
9
+ describe('createPixelIcon', () => {
10
+ test('renders an SvgIcon with a test id derived from its name', () => {
11
+ render(<TestIcon />)
12
+
13
+ const icon = screen.getByTestId('TestIcon')
14
+
15
+ expect(icon.tagName.toLowerCase()).toBe('svg')
16
+ expect(icon).toHaveAttribute('viewBox', '0 0 2 2')
17
+ })
18
+
19
+ test('forwards refs to the underlying svg element', () => {
20
+ const ref = createRef<SVGSVGElement>()
21
+
22
+ render(<TestIcon ref={ref} />)
23
+
24
+ expect(ref.current).toBe(screen.getByTestId('TestIcon'))
25
+ })
26
+
27
+ test('can be the direct child of a Tooltip', () => {
28
+ const consoleError = vitest
29
+ .spyOn(console, 'error')
30
+ .mockImplementation(() => {})
31
+
32
+ render(
33
+ <Tooltip title="Tip">
34
+ <TestIcon />
35
+ </Tooltip>
36
+ )
37
+
38
+ // Tooltip anchors to its child via a ref; without forwardRef React warns
39
+ // that function components cannot be given refs.
40
+ expect(consoleError).not.toHaveBeenCalled()
41
+ consoleError.mockRestore()
42
+ })
43
+ })
44
+
45
+ describe('pixel map transforms', () => {
46
+ const map = ['ab', 'cd']
47
+
48
+ test('flipX mirrors horizontally', () => {
49
+ expect(flipX(map)).toEqual(['ba', 'dc'])
50
+ })
51
+
52
+ test('flipY mirrors vertically', () => {
53
+ expect(flipY(map)).toEqual(['cd', 'ab'])
54
+ })
55
+
56
+ test('rotate turns the map 90 degrees clockwise', () => {
57
+ expect(rotate(map)).toEqual(['ca', 'db'])
58
+ })
59
+ })
@@ -0,0 +1,97 @@
1
+ import React from 'react'
2
+ import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon/index.js'
3
+
4
+ // Pixel art icons are authored as small text pixel maps:
5
+ //
6
+ // `#` is a solid pixel
7
+ // `+` is a half-tone pixel (the same color at reduced opacity)
8
+ // anything else is transparent
9
+ //
10
+ // Every pixel map in an icon set should be the same size so icons line up
11
+ // with each other.
12
+ export type PixelMap = readonly string[]
13
+
14
+ const SHADE_OPACITY = 0.45
15
+
16
+ /**
17
+ * Builds an SVG path that covers every pixel in `rows` that matches `char`.
18
+ * Horizontal runs of matching pixels are merged into a single rectangle to
19
+ * keep the path short.
20
+ */
21
+ const pixelPath = (rows: PixelMap, char: string) => {
22
+ let d = ''
23
+
24
+ rows.forEach((row, y) => {
25
+ let x = 0
26
+
27
+ while (x < row.length) {
28
+ if (row[x] !== char) {
29
+ x++
30
+ continue
31
+ }
32
+
33
+ const start = x
34
+
35
+ while (row[x] === char) x++
36
+
37
+ d += `M${start} ${y}h${x - start}v1h${start - x}z`
38
+ }
39
+ })
40
+
41
+ return d
42
+ }
43
+
44
+ /**
45
+ * Mirrors a pixel map horizontally.
46
+ */
47
+ export const flipX = (rows: PixelMap): PixelMap =>
48
+ rows.map(row => [...row].reverse().join(''))
49
+
50
+ /**
51
+ * Mirrors a pixel map vertically.
52
+ */
53
+ export const flipY = (rows: PixelMap): PixelMap => [...rows].reverse()
54
+
55
+ /**
56
+ * Rotates a square pixel map 90 degrees clockwise.
57
+ */
58
+ export const rotate = (rows: PixelMap): PixelMap =>
59
+ [...rows[0]].map((_, x) =>
60
+ rows
61
+ .map(row => row[x])
62
+ .reverse()
63
+ .join('')
64
+ )
65
+
66
+ /**
67
+ * Creates an MUI SvgIcon component from a pixel map. The result is a drop-in
68
+ * replacement for an @mui/icons-material icon: it takes the same props,
69
+ * forwards refs to the underlying <svg> (which a Tooltip needs to position
70
+ * itself when the icon is its direct child), is colored with `currentColor`,
71
+ * and has a `data-testid` of `${name}Icon`.
72
+ */
73
+ export const createPixelIcon = (rows: PixelMap, name: string) => {
74
+ const width = rows[0].length
75
+ const height = rows.length
76
+ const solid = pixelPath(rows, '#')
77
+ const shade = pixelPath(rows, '+')
78
+
79
+ const PixelIcon = React.forwardRef<SVGSVGElement, SvgIconProps>(
80
+ (props, ref) => (
81
+ <SvgIcon
82
+ ref={ref}
83
+ data-testid={`${name}Icon`}
84
+ viewBox={`0 0 ${width} ${height}`}
85
+ shapeRendering="crispEdges"
86
+ {...props}
87
+ >
88
+ {shade && <path d={shade} opacity={SHADE_OPACITY} />}
89
+ <path d={solid} />
90
+ </SvgIcon>
91
+ )
92
+ )
93
+
94
+ PixelIcon.displayName = `Pixel${name}Icon`
95
+
96
+ return PixelIcon
97
+ }