@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.
- package/README.md +5 -0
- package/dist/assets/index-4ynbl-GC.js +392 -0
- package/dist/assets/index-4ynbl-GC.js.map +1 -0
- package/dist/assets/index-CaYdR-lC.css +1 -0
- package/dist/assets/jersey-10-latin-400-normal-DrL73VYW.woff2 +0 -0
- package/dist/assets/jersey-10-latin-ext-400-normal-uIKqQSaG.woff2 +0 -0
- package/dist/assets/jersey-20-latin-400-normal-bNi78tgO.woff2 +0 -0
- package/dist/assets/jersey-20-latin-ext-400-normal-BO3lmWef.woff2 +0 -0
- package/dist/assets/jersey-25-latin-400-normal-BuYJnnt7.woff2 +0 -0
- package/dist/assets/jersey-25-latin-ext-400-normal-CP5Jl3WN.woff2 +0 -0
- package/dist/index.html +2 -2
- package/dist/service-worker.js +1 -1
- package/dist/service-worker.js.map +1 -1
- package/package.json +7 -12
- package/src/common/services/randomNumber.test.ts +61 -1
- package/src/common/services/randomNumber.ts +36 -1
- package/src/common/utils.ts +22 -13
- package/src/components/AccountingView/AccountingView.tsx +7 -13
- package/src/components/Achievement/Achievement.tsx +2 -15
- package/src/components/AchievementsView/AchievementsView.tsx +2 -7
- package/src/components/AnimatedNumber/AnimatedNumber.tsx +0 -6
- package/src/components/AppBar/AppBar.tsx +21 -17
- package/src/components/Cellar/Cellar.tsx +0 -2
- package/src/components/Cellar/CellarInventoryTabPanel.tsx +0 -6
- package/src/components/Cellar/FermentationTabPanel.tsx +0 -6
- package/src/components/Cellar/Keg.tsx +0 -5
- package/src/components/Cellar/TabPanel/index.tsx +0 -7
- package/src/components/Cellar/WinemakingTabPanel.tsx +0 -6
- package/src/components/ContextPane/ContextPane.tsx +0 -6
- package/src/components/CowCard/Bloodline/Bloodline.tsx +0 -6
- package/src/components/CowCard/CowCard.tsx +7 -39
- package/src/components/CowCard/Subheader/Subheader.tsx +21 -44
- package/src/components/CowPen/Cow.tsx +5 -11
- package/src/components/CowPen/CowPen.test.tsx +4 -4
- package/src/components/CowPen/CowPen.tsx +5 -16
- package/src/components/CowPenContextMenu/CowPenContextMenu.tsx +2 -18
- package/src/components/CowPenContextMenu/TabPanel/index.tsx +0 -7
- package/src/components/Farmhand/Farmhand.context.tsx +4 -3
- package/src/components/Farmhand/Farmhand.tsx +11 -15
- package/src/components/Farmhand/FarmhandReducers.tsx +1 -2
- package/src/components/Farmhand/FarmhandService.test.ts +6 -9
- package/src/components/Farmhand/hooks/useFarmhandDayAdvancement.ts +2 -6
- package/src/components/Farmhand/hooks/useFarmhandNetwork.test.ts +2 -2
- package/src/components/Farmhand/hooks/useFarmhandNetwork.ts +15 -31
- package/src/components/Farmhand/useFarmhand.test.ts +7 -5
- package/src/components/Farmhand/useFarmhand.ts +24 -15
- package/src/components/FermentationRecipeList/FermentationRecipe.tsx +0 -5
- package/src/components/FermentationRecipeList/FermentationRecipeList.test.tsx +10 -11
- package/src/components/FermentationRecipeList/FermentationRecipeList.tsx +2 -3
- package/src/components/Field/Field.tsx +15 -50
- package/src/components/Forest/Forest.tsx +10 -7
- package/src/components/ForestPlot/ForestPlot.tsx +5 -5
- package/src/components/ForestQuickSelect/ForestQuickSelect.tsx +0 -12
- package/src/components/Home/Home.tsx +2 -7
- package/src/components/Home/SnowBackground.tsx +1 -1
- package/src/components/IngredientsList/IngredientsList.tsx +0 -7
- package/src/components/Inventory/Inventory.test.tsx +4 -4
- package/src/components/Inventory/Inventory.tsx +5 -12
- package/src/components/Item/Item.tsx +11 -29
- package/src/components/ItemList/ItemList.tsx +4 -11
- package/src/components/KeybindingsView/KeybindingsView.tsx +0 -2
- package/src/components/LogView/LogView.tsx +6 -28
- package/src/components/Navigation/DayAndProgressContainer.tsx +0 -8
- package/src/components/Navigation/Navigation.tsx +12 -33
- package/src/components/NotificationSystem/NotificationSystem.test.tsx +45 -3
- package/src/components/NotificationSystem/NotificationSystem.tsx +45 -32
- package/src/components/OnlinePeersView/OnlinePeer/OnlinePeer.test.tsx +2 -2
- package/src/components/OnlinePeersView/OnlinePeer/OnlinePeer.tsx +0 -11
- package/src/components/OnlinePeersView/OnlinePeersView.tsx +0 -10
- package/src/components/PixelIcon/createPixelIcon.test.tsx +59 -0
- package/src/components/PixelIcon/createPixelIcon.tsx +97 -0
- package/src/components/PixelIcon/icons.ts +445 -0
- package/src/components/PixelIcon/index.ts +3 -0
- package/src/components/Plot/Plot.tsx +3 -3
- package/src/components/PriceEventView/PriceEventView.tsx +0 -6
- package/src/components/ProgressBar/ProgressBar.tsx +9 -8
- package/src/components/QuantityInput/QuantityInput.tsx +6 -11
- package/src/components/QuickSelect/QuickSelect.tsx +0 -10
- package/src/components/Recipe/Recipe.tsx +0 -9
- package/src/components/RecipeList/RecipeList.tsx +0 -6
- package/src/components/SearchBar/SearchBar.tsx +4 -10
- package/src/components/SettingsView/RandomSeedInput.test.tsx +2 -4
- package/src/components/SettingsView/RandomSeedInput.tsx +5 -8
- package/src/components/SettingsView/SettingsView.test.tsx +21 -0
- package/src/components/SettingsView/SettingsView.tsx +24 -18
- package/src/components/Shop/Shop.tsx +0 -20
- package/src/components/Shop/TabPanel/index.tsx +0 -7
- package/src/components/Stage/Stage.tsx +11 -20
- package/src/components/StatsView/StatsView.tsx +0 -18
- package/src/components/TierPurchase/TierPurchase.tsx +0 -12
- package/src/components/Toolbelt/Toolbelt.tsx +5 -10
- package/src/components/UpdateNotifier/UpdateNotifier.tsx +33 -5
- package/src/components/UpgradePurchase/UpgradePurchase.tsx +0 -10
- package/src/components/WineRecipeList/WineRecipe.test.tsx +2 -2
- package/src/components/WineRecipeList/WineRecipe.tsx +0 -5
- package/src/components/WineRecipeList/WineRecipeList.tsx +2 -3
- package/src/components/Workshop/ForgeTabPanel.tsx +0 -8
- package/src/components/Workshop/KitchenTabPanel.tsx +0 -7
- package/src/components/Workshop/RecyclingTabPanel.tsx +0 -7
- package/src/components/Workshop/TabPanel/index.tsx +0 -7
- package/src/components/Workshop/WoodChipperTabPanel.tsx +0 -7
- package/src/components/Workshop/Workshop.tsx +0 -9
- package/src/components/Workshop/getUpgradesAvailable.ts +1 -1
- package/src/constants.ts +18 -13
- package/src/data/crops/grape.ts +13 -15
- package/src/data/maps.test.ts +3 -4
- package/src/data/maps.ts +27 -29
- package/src/enums.ts +47 -19
- package/src/factories/OreFactory.ts +2 -1
- package/src/farmhand.d.ts +6 -3
- package/src/game-logic/reducers/applyPrecipitation.ts +4 -4
- package/src/game-logic/reducers/generatePriceEvents.ts +6 -3
- package/src/game-logic/reducers/harvestForestPlot.ts +1 -2
- package/src/game-logic/reducers/makeFermentationRecipe.ts +2 -3
- package/src/game-logic/reducers/minePlot.test.ts +2 -2
- package/src/game-logic/reducers/minePlot.ts +1 -1
- package/src/game-logic/reducers/processCowFertilizerProduction.test.ts +16 -14
- package/src/game-logic/reducers/processFeedingCows.test.ts +2 -5
- package/src/game-logic/reducers/purchaseCow.ts +2 -7
- package/src/game-logic/reducers/showNotification.test.ts +20 -0
- package/src/game-logic/reducers/showNotification.ts +4 -2
- package/src/game-logic/reducers/updateAchievements.ts +1 -1
- package/src/game-logic/reducers/updateFinancialRecords.test.ts +21 -25
- package/src/game-logic/reducers/updateFinancialRecords.ts +2 -5
- package/src/game-logic/reducers/updateInventoryRecordsForNextDay.test.ts +12 -14
- package/src/handlers/ui-events.test.ts +44 -42
- package/src/handlers/ui-events.tsx +18 -3
- package/src/hooks/usePrefersReducedMotion.ts +10 -6
- package/src/index.tsx +0 -2
- package/src/mui-theme.ts +258 -37
- package/src/scripts/generate-crop-table.ts +3 -2
- package/src/setupTests.ts +18 -12
- package/src/strings.ts +1 -1
- package/src/styles/fonts.ts +73 -0
- package/src/styles/pixel.ts +151 -0
- package/src/styles/sx.ts +46 -33
- package/src/styles/tokens.ts +11 -0
- package/src/test-utils/index.ts +1 -0
- package/src/utils/chooseRandom.ts +4 -1
- package/src/utils/chooseRandomIndex.ts +6 -2
- package/src/utils/getCowImage.ts +22 -27
- package/src/utils/getMaxYieldOfFermentationRecipe.ts +2 -2
- package/src/utils/getPeerMetadata.ts +1 -1
- package/src/utils/getPlotImage.test.ts +14 -14
- package/src/utils/getPlotImage.ts +10 -10
- package/src/utils/getRandomUnlockedCrop.ts +5 -3
- package/src/utils/getRandomizedLifespan.test.ts +2 -8
- package/src/utils/getValueAdjustmentStream.test.ts +9 -0
- package/src/utils/getValueAdjustmentStream.ts +5 -0
- package/src/utils/isItemAFarmProduct.ts +2 -2
- package/src/utils/shouldPrecipitateToday.ts +3 -1
- package/src/utils/shouldStormToday.ts +2 -1
- package/src/utils/transformStateDataForImport.ts +3 -5
- package/dist/assets/francois-one-latin-400-normal-DD1rFI7H.woff2 +0 -0
- package/dist/assets/francois-one-latin-400-normal-DNXs7uOx.woff +0 -0
- package/dist/assets/francois-one-latin-ext-400-normal-CwRdWRZD.woff2 +0 -0
- package/dist/assets/francois-one-latin-ext-400-normal-JvkpT0Av.woff +0 -0
- package/dist/assets/francois-one-vietnamese-400-normal-BLIKeG5Y.woff2 +0 -0
- package/dist/assets/francois-one-vietnamese-400-normal-CuWeHlXs.woff +0 -0
- package/dist/assets/index-CNcrTYN-.css +0 -1
- package/dist/assets/index-DNPchtK9.js +0 -844
- package/dist/assets/index-DNPchtK9.js.map +0 -1
- package/dist/assets/public-sans-latin-400-normal-8Rpg0ruU.woff2 +0 -0
- package/dist/assets/public-sans-latin-400-normal-SBbinRkI.woff +0 -0
- package/dist/assets/public-sans-latin-ext-400-normal-B02wwOdZ.woff +0 -0
- package/dist/assets/public-sans-latin-ext-400-normal-mk90oQqJ.woff2 +0 -0
- package/dist/assets/public-sans-vietnamese-400-normal-BO1meqcx.woff +0 -0
- package/dist/assets/public-sans-vietnamese-400-normal-TxZB8OSn.woff2 +0 -0
- package/src/fontsource.d.ts +0 -3
|
@@ -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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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 {
|
|
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,
|
|
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 {
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
+
}
|