diff --git a/views/components/etc/webview.tsx b/views/components/etc/webview.tsx index 7200fe1d8..044f0657e 100644 --- a/views/components/etc/webview.tsx +++ b/views/components/etc/webview.tsx @@ -127,7 +127,7 @@ const ElectronWebView = forwardRef( const errorScript = `document.write('
Webview load error
Error Code: ${e.errorCode}
Description: ${e.errorDescription}
URL: ${e.validatedURL}')\ndocument.body.style.backgroundColor = "white"` const target = e.target if (target && 'executeJavaScript' in target) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- target is narrowed by 'executeJavaScript' in check above const webviewTarget = target as WebviewTag webviewTarget.executeJavaScript(errorScript) } diff --git a/views/components/ship/aaci-indicator.es b/views/components/ship/aaci-indicator.tsx similarity index 52% rename from views/components/ship/aaci-indicator.es rename to views/components/ship/aaci-indicator.tsx index 66c112652..d57ffb9af 100644 --- a/views/components/ship/aaci-indicator.es +++ b/views/components/ship/aaci-indicator.tsx @@ -1,7 +1,7 @@ import { Tooltip, Tag, Position, Intent } from '@blueprintjs/core' import { memoize, get } from 'lodash' import React from 'react' -import { withNamespaces, Trans } from 'react-i18next' +import { useTranslation, Trans } from 'react-i18next' import { connect } from 'react-redux' import { createSelector } from 'reselect' import { @@ -14,7 +14,7 @@ import i18next from 'views/env-parts/i18next' import { getShipAACIs, getShipAllAACIs, AACITable } from 'views/utils/aaci' import { shipDataSelectorFactory, shipEquipDataSelectorFactory } from 'views/utils/selectors' -const getAvailableTranslation = memoize((str) => +const getAvailableTranslation = memoize((str: string) => i18next.translator.exists(`main:${str}`) ? ( main:{str} ) : i18next.translator.exists(`resources:${str}`) ? ( @@ -24,14 +24,20 @@ const getAvailableTranslation = memoize((str) => ), ) -const __t = (name) => +const __t = (name: string[]) => name.map((n, i) => ( {getAvailableTranslation(n)} )) -const AACISelectorFactory = memoize((shipId) => +interface AACISelectorResult { + AACIs: number[] + maxShotdown: number +} + + +const AACISelectorFactory = memoize((shipId: number) => createSelector( [shipDataSelectorFactory(shipId), shipEquipDataSelectorFactory(shipId)], ([_ship = {}, $ship = {}] = [], _equips = []) => { @@ -45,42 +51,38 @@ const AACISelectorFactory = memoize((shipId) => ), ) -const maxAACIShotdownSelectorFactory = memoize((shipId) => +const maxAACIShotdownSelectorFactory = memoize((shipId: number) => createSelector([shipDataSelectorFactory(shipId)], ([_ship = {}, $ship = {}] = []) => { const AACIs = getShipAllAACIs({ ...$ship, ..._ship }) return Math.max(...AACIs.map((id) => AACITable[id].fixed || 0)) }), ) -export const AACIIndicator = withNamespaces(['main'])( - connect((state, { shipId }) => ({ - AACIs: AACISelectorFactory(shipId)(state) || [], - maxShotdown: maxAACIShotdownSelectorFactory(shipId)(state), - }))(({ AACIs, maxShotdown, shipId, t }) => { - const currentMax = Math.max(...AACIs.map((id) => AACITable[id].fixed || 0)) +const AACIIndicatorComponent: React.FC = ({ AACIs, maxShotdown }) => { + const { t } = useTranslation(['main']) + const currentMax = Math.max(...AACIs.map((id) => AACITable[id].fixed || 0)) - const tooltip = AACIs.length && ( - - {AACIs.map((id) => ( - - - {t('main:AACIType', { count: id })} - - {get(AACITable, `${id}.name.length`, 0) > 0 ? __t(AACITable[id].name) : ''} - - - {t('main:Shot down', { count: AACITable[id].fixed })} - - {t('main:Modifier', { count: AACITable[id].modifier })} - - - ))} - {currentMax < maxShotdown && {t('main:Max shot down not reached')}} - - ) + const tooltip = AACIs.length > 0 && ( + + {AACIs.map((id) => ( + + + {t('main:AACIType', { count: id })} + {get(AACITable, `${id}.name.length`, 0) > 0 ? __t(AACITable[id].name) : ''} + + {t('main:Shot down', { count: get(AACITable, `${id}.fixed`, 0) })} + + {t('main:Modifier', { count: get(AACITable, `${id}.modifier`, 0) })} + + + ))} + {currentMax < maxShotdown && {t('main:Max shot down not reached')}} + + ) - return ( - !!AACIs.length && ( + return ( + <> + {AACIs.length > 0 && ( @@ -88,7 +90,14 @@ export const AACIIndicator = withNamespaces(['main'])( - ) - ) - }), -) + )} + + ) +} + +const mapStateToProps = (state: unknown, { shipId }: { shipId: number }): AACISelectorResult => ({ + AACIs: AACISelectorFactory(shipId)(state) || [], + maxShotdown: maxAACIShotdownSelectorFactory(shipId)(state), +}) + +export const AACIIndicator = connect(mapStateToProps)(AACIIndicatorComponent) diff --git a/views/components/ship/aapb-indicator.es b/views/components/ship/aapb-indicator.es deleted file mode 100644 index dea1c5543..000000000 --- a/views/components/ship/aapb-indicator.es +++ /dev/null @@ -1,44 +0,0 @@ -import { Tooltip, Tag, Position, Intent } from '@blueprintjs/core' -import { memoize, compact, isFinite } from 'lodash' -import React from 'react' -import { withNamespaces } from 'react-i18next' -import { connect } from 'react-redux' -import { compose } from 'redux' -import { createSelector } from 'reselect' -import { ShipLabel } from 'views/components/ship-parts/styled-components' -import { getShipAAPB } from 'views/utils/aapb' -import { shipDataSelectorFactory, shipEquipDataSelectorFactory } from 'views/utils/selectors' - -const AAPBSelectorFactory = memoize((shipId) => - createSelector( - [shipDataSelectorFactory(shipId), shipEquipDataSelectorFactory(shipId)], - (shipInfo, equipsInfo) => { - if (!shipInfo || !equipsInfo) return 0 - /* - equipment position is irrelevant with regard to AAPB trigger rate, - so we might as well remove all `undefined` for getShipAAPB to - have a uniform structure to work with. - */ - return getShipAAPB(shipInfo, compact(equipsInfo)) - }, - ), -) - -export const AAPBIndicator = compose( - withNamespaces(['main']), - connect((state, { shipId }) => ({ - AAPB: AAPBSelectorFactory(shipId)(state) || 0, - })), -)( - ({ AAPB, shipId, t }) => - isFinite(AAPB) && - AAPB > 0 && ( - - {`${AAPB.toFixed(2)}%`}}> - - {t('main:AAPB')} - - - - ), -) diff --git a/views/components/ship/aapb-indicator.tsx b/views/components/ship/aapb-indicator.tsx new file mode 100644 index 000000000..a101c8337 --- /dev/null +++ b/views/components/ship/aapb-indicator.tsx @@ -0,0 +1,66 @@ +import { Tag, Intent, Position, Tooltip } from '@blueprintjs/core' +import { compact, isFinite, memoize } from 'lodash' +import React from 'react' +import { useTranslation } from 'react-i18next' +import { connect } from 'react-redux' +import { createSelector } from 'reselect' +import { ShipLabel } from 'views/components/ship-parts/styled-components' +import { getShipAAPB } from 'views/utils/aapb' +import { shipDataSelectorFactory, shipEquipDataSelectorFactory } from 'views/utils/selectors' + +interface ShipInfo { + [key: string]: unknown +} + +interface EquipInfo { + [key: string]: unknown +} + +interface AAPBSelectorProps { + shipId: number +} + +interface AAPBStateProps { + AAPB: number +} + +interface AAPBIndicatorProps extends AAPBSelectorProps, AAPBStateProps {} + +const AAPBSelectorFactory = memoize((shipId: number) => + createSelector( + [shipDataSelectorFactory(shipId), shipEquipDataSelectorFactory(shipId)], + (shipInfo: [ShipInfo, ShipInfo] | undefined, equipsInfo: (EquipInfo[] | undefined)[]) => { + if (!shipInfo || !equipsInfo) return 0 + /* + equipment position is irrelevant with regard to AAPB trigger rate, + so we might as well remove all `undefined` for getShipAAPB to + have a uniform structure to work with. + */ + return getShipAAPB(shipInfo, compact(equipsInfo)) + }, + ), +) + +const AAPBIndicatorComponent: React.FC = ({ AAPB }) => { + const { t } = useTranslation(['main']) + + if (!isFinite(AAPB) || AAPB <= 0) { + return null + } + + return ( + + {`${AAPB.toFixed(2)}%`}}> + + {t('main:AAPB')} + + + + ) +} + +const mapStateToProps = (state: unknown, ownProps: AAPBSelectorProps): AAPBStateProps => ({ + AAPB: AAPBSelectorFactory(ownProps.shipId)(state) || 0, +}) + +export const AAPBIndicator = connect(mapStateToProps)(AAPBIndicatorComponent) diff --git a/views/components/ship/index.es b/views/components/ship/index.tsx similarity index 62% rename from views/components/ship/index.es rename to views/components/ship/index.tsx index c637317e7..2ed596a83 100644 --- a/views/components/ship/index.es +++ b/views/components/ship/index.tsx @@ -1,10 +1,8 @@ import { Button, ResizeSensor } from '@blueprintjs/core' import { get, memoize, times } from 'lodash' -import PropTypes from 'prop-types' import React, { Component } from 'react' import FontAwesome from 'react-fontawesome' -import { withNamespaces, Trans } from 'react-i18next' -/* global getStore */ +import { Trans, withTranslation, type WithTranslation } from 'react-i18next' import { connect } from 'react-redux' import { compose } from 'redux' import { createSelector } from 'reselect' @@ -33,9 +31,84 @@ import { LandbaseButton } from '../ship-parts/landbase-button' import { SquardRow } from './lbac-view' import { ShipRow } from './ship-item' -const shipRowWidthSelector = (state) => get(state, 'layout.shippane.width', 450) +interface StateData { + const?: { + $mapareas?: Record + } + info?: { + fleets?: unknown[] + airbase?: unknown[] + } + sortie?: { + spAttackCount?: number + combinedFlag?: unknown + } + layout?: { + shippane?: { + width?: number + height?: number + } + } + ui?: { + activeFleetId?: number + } + config?: { + poi?: { + transition?: { + enable?: boolean + } + appearance?: { + avatar?: boolean + } + } + } + [key: string]: unknown +} + +interface ShipViewSwitchButtonProps { + fleetId: number + activeFleetId: number + fleetName: string + fleetState: number + onClick: (e: React.MouseEvent) => void + disabled: boolean +} + +interface FleetShipViewProps { + fleetId: number + shipsId: number[] + enableAvatar: boolean + width: number + isSpAttack: boolean +} + +interface LBViewProps extends WithTranslation { + areaIds: number[] + mapareas: Record + enableAvatar: boolean + width: number +} + +interface ReactClassProps extends WithTranslation { + enableTransition: boolean + fleetCount: number + activeFleetId: number + airBaseCnt: number + enableAvatar: boolean + width: number + dispatch: (action: { type: string; [key: string]: unknown }) => void +} + +interface ReactClassState { + activeFleetId: number + prevFleetId: number | null +} -const shipViewSwitchButtonDataSelectorFactory = memoize((fleetId) => +/* global getStore */ + +const shipRowWidthSelector = (state: StateData) => get(state, 'layout.shippane.width', 450) + +const shipViewSwitchButtonDataSelectorFactory = memoize((fleetId: number) => createSelector( [fleetNameSelectorFactory(fleetId), fleetStateSelectorFactory(fleetId)], (fleetName, fleetState) => ({ @@ -45,27 +118,37 @@ const shipViewSwitchButtonDataSelectorFactory = memoize((fleetId) => ), ) -const ShipViewSwitchButton = connect((state, { fleetId }) => - shipViewSwitchButtonDataSelectorFactory(fleetId)(state), -)(({ fleetId, activeFleetId, fleetName, fleetState, onClick, disabled }) => ( +const ShipViewSwitchButton: React.FC = ({ + fleetId, + activeFleetId, + fleetName, + fleetState, + onClick, + disabled, +}) => ( -)) +) + +const ConnectedShipViewSwitchButton = connect( + (state: StateData, { fleetId }: { fleetId: number }) => + shipViewSwitchButtonDataSelectorFactory(fleetId)(state), +)(ShipViewSwitchButton) -const fleetShipViewDataSelectorFactory = memoize((fleetId) => +const fleetShipViewDataSelectorFactory = memoize((fleetId: number) => createSelector( [ fleetShipsIdSelectorFactory(fleetId), fleetShipsDataWithEscapeSelectorFactory(fleetId), - (state) => get(state.sortie, 'spAttackCount'), - (state) => get(state.info, 'useitems.95.api_count'), - (state) => get(state.sortie, 'combinedFlag'), + (state: StateData) => get(state, 'sortie.spAttackCount'), + (state: StateData) => get(state, 'info.useitems.95.api_count'), + (state: StateData) => get(state, 'sortie.combinedFlag'), ], (shipsId, shipsData, spAttackCount, submarineSupplyCount, combinedFlag) => ({ shipsId, @@ -79,9 +162,13 @@ const fleetShipViewDataSelectorFactory = memoize((fleetId) => ), ) -const FleetShipView = connect((state, { fleetId }) => - fleetShipViewDataSelectorFactory(fleetId)(state), -)(({ fleetId, shipsId, enableAvatar, width, isSpAttack }) => ( +const FleetShipView: React.FC = ({ + fleetId, + shipsId, + enableAvatar, + width, + isSpAttack, +}) => ( <>
@@ -98,15 +185,13 @@ const FleetShipView = connect((state, { fleetId }) => ))} -)) +) -const LBView = compose( - withNamespaces(['resources']), - connect((state) => ({ - areaIds: get(state, 'info.airbase', []).map((a) => a.api_area_id), - mapareas: get(state, 'const.$mapareas', {}), - })), -)(({ areaIds, mapareas, t, enableAvatar, width }) => ( +const ConnectedFleetShipView = connect((state: StateData, { fleetId }: { fleetId: number }) => + fleetShipViewDataSelectorFactory(fleetId)(state), +)(FleetShipView) + +const LBView: React.FC = ({ areaIds, mapareas, t, enableAvatar, width }) => ( {areaIds.map( (id, i) => @@ -123,27 +208,31 @@ const LBView = compose( )), )} -)) +) -@connect((state, props) => ({ - enableTransition: get(state, 'config.poi.transition.enable', true), - fleetCount: get(state, 'info.fleets.length', 4), - activeFleetId: get(state, 'ui.activeFleetId', 0), - airBaseCnt: get(state, 'info.airbase.length', 0), - enableAvatar: get(state, 'config.poi.appearance.avatar', true), - width: shipRowWidthSelector(state), -})) -export class reactClass extends Component { - static propTypes = { - enableTransition: PropTypes.bool.isRequired, - fleetCount: PropTypes.number.isRequired, - activeFleetId: PropTypes.number.isRequired, - airBaseCnt: PropTypes.number.isRequired, - enableAvatar: PropTypes.bool, - width: PropTypes.number, +const ConnectedLBView = compose( + withTranslation(['resources']), + connect((state: StateData) => ({ + areaIds: get(state, 'info.airbase', []).map((a: { api_area_id: number }) => a.api_area_id), + mapareas: get(state, 'const.$mapareas', {}), + })), +)(LBView) + +class ReactClassComponent extends Component { + static displayName = 'ShipView' + + constructor(props: ReactClassProps) { + super(props) + this.state = { + activeFleetId: props.activeFleetId, + prevFleetId: null, + } } - static getDerivedStateFromProps(props, state) { + static getDerivedStateFromProps( + props: ReactClassProps, + state: ReactClassState, + ): Partial | null { if (props.activeFleetId !== state.activeFleetId) { return { prevFleetId: state.activeFleetId, @@ -153,24 +242,14 @@ export class reactClass extends Component { return null } - constructor(props) { - super(props) - this.nowTime = 0 - } - - state = { - activeFleetId: this.props.activeFleetId, - prevFleetId: null, - } - - handleTransitionEnd = (i) => { + handleTransitionEnd = (i: number) => { if (i === this.state.prevFleetId) { this.setState({ prevFleetId: null }) } } - handleClick = (idx) => { - if (idx != this.state.activeFleetId) { + handleClick = (idx: number) => { + if (idx !== this.state.activeFleetId) { this.props.dispatch({ type: '@@TabSwitch', tabInfo: { @@ -189,14 +268,16 @@ export class reactClass extends Component { }) } - handleResize = (entries) => { + handleResize = (entries: ResizeObserverEntry[]) => { entries.forEach((entry) => { const { width, height } = entry.contentRect if ( width !== 0 && height !== 0 && - (width !== getStore('layout.shippane.width') || - height !== getStore('layout.shippane.height')) + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- getStore returns unknown, layout values are known to be numbers + (width !== (getStore('layout.shippane.width') as number) || + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- getStore returns unknown, layout values are known to be numbers + height !== (getStore('layout.shippane.height') as number)) ) { this.props.dispatch({ type: '@@LayoutUpdate', @@ -219,11 +300,11 @@ export class reactClass extends Component { {times(4).map((i) => ( - this.props.fleetCount} - onClick={(e) => this.handleClick(i)} + onClick={() => this.handleClick(i)} activeFleetId={activeFleetId} /> ))} @@ -232,7 +313,7 @@ export class reactClass extends Component { key={4} fleetId={4} disabled={this.props.airBaseCnt === 0} - onClick={(e) => this.handleClick(4)} + onClick={() => this.handleClick(4)} activeFleetId={activeFleetId} isMini={false} /> @@ -252,7 +333,7 @@ export class reactClass extends Component { left={activeFleetId > i} right={activeFleetId < i} > - 4} right={activeFleetId < 4} > - + @@ -281,6 +365,17 @@ export class reactClass extends Component { } } +const mapStateToProps = (state: StateData) => ({ + enableTransition: get(state, 'config.poi.transition.enable', true), + fleetCount: get(state, 'info.fleets.length', 4), + activeFleetId: get(state, 'ui.activeFleetId', 0), + airBaseCnt: get(state, 'info.airbase.length', 0), + enableAvatar: get(state, 'config.poi.appearance.avatar', true), + width: shipRowWidthSelector(state), +}) + +export const reactClass = connect(mapStateToProps)(ReactClassComponent) + export const displayName = ( main:Fleet @@ -288,3 +383,5 @@ export const displayName = ( ) export const icon = + +export const name = 'ship-view' diff --git a/views/components/ship/lbac-view.es b/views/components/ship/lbac-view.tsx similarity index 71% rename from views/components/ship/lbac-view.es rename to views/components/ship/lbac-view.tsx index d9da34a70..d5ed37308 100644 --- a/views/components/ship/lbac-view.es +++ b/views/components/ship/lbac-view.tsx @@ -2,9 +2,8 @@ import { Tag, ProgressBar, Tooltip, Position } from '@blueprintjs/core' import memoize from 'fast-memoize' import { get } from 'lodash' import React from 'react' -import { withNamespaces } from 'react-i18next' +import { useTranslation } from 'react-i18next' import { connect } from 'react-redux' -import { compose } from 'redux' import { createSelector } from 'reselect' import { ShipItem, @@ -29,26 +28,65 @@ import { landbaseSelectorFactory, landbaseEquipDataSelectorFactory } from 'views import { LandbaseSlotitems } from './slotitems' -const SquadSelectorFactory = memoize((squardId) => +interface EquipData { + [key: string]: unknown +} + +interface Landbase { + api_action_kind: number + api_distance: { + api_base: number + api_bonus: number + } + api_name: string + api_nowhp?: number + api_maxhp?: number + [key: string]: unknown +} + +interface SquadSelectorProps { + squardId: number +} + +interface SquadStateProps { + landbase: Landbase + equipsData: EquipData[] + squardId: number +} + +interface SquadRowProps extends SquadSelectorProps { + enableAvatar: boolean + compact: boolean +} + +const SquadSelectorFactory = memoize((squardId: number) => createSelector( [landbaseSelectorFactory(squardId), landbaseEquipDataSelectorFactory(squardId)], - (landbase, equipsData) => ({ - landbase, - equipsData, + (landbase: Landbase | undefined, equipsData: EquipData[] | undefined): SquadStateProps => ({ + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- default empty Landbase when selector returns undefined + landbase: landbase ?? ({} as Landbase), + equipsData: equipsData ?? [], squardId, }), ), ) -export const SquardRow = compose( - withNamespaces(['main']), - connect((state, { squardId }) => SquadSelectorFactory(squardId)), -)(({ landbase, equipsData, squardId, t, enableAvatar, compact }) => { +const SquadRowComponent: React.FC = ({ + landbase, + equipsData, + squardId, + enableAvatar, + compact, +}) => { + const { t } = useTranslation(['main']) + const { api_action_kind, api_distance, api_name, api_nowhp = 200, api_maxhp = 200 } = landbase - const { api_base, api_bonus } = api_distance + + const { api_base = 0, api_bonus = 0 } = api_distance || {} const tyku = getTyku([equipsData], api_action_kind) const hpPercentage = (api_nowhp / api_maxhp) * 100 const hideLBACName = enableAvatar && compact + return ( ) +} + +const mapStateToProps = (state: unknown, ownProps: SquadSelectorProps): SquadStateProps => ({ + ...SquadSelectorFactory(ownProps.squardId)(state), }) + +export const SquardRow = connect(mapStateToProps)(SquadRowComponent) diff --git a/views/components/ship/oasw-indicator.es b/views/components/ship/oasw-indicator.tsx similarity index 63% rename from views/components/ship/oasw-indicator.es rename to views/components/ship/oasw-indicator.tsx index 3110d93ab..829c33f7e 100644 --- a/views/components/ship/oasw-indicator.es +++ b/views/components/ship/oasw-indicator.tsx @@ -1,14 +1,19 @@ import { Tag, Intent } from '@blueprintjs/core' import { memoize } from 'lodash' import React from 'react' -import { withNamespaces } from 'react-i18next' +import { useTranslation } from 'react-i18next' import { connect } from 'react-redux' import { createSelector } from 'reselect' import { ShipLabel } from 'views/components/ship-parts/styled-components' import { isOASW } from 'views/utils/oasw' import { shipDataSelectorFactory, shipEquipDataSelectorFactory } from 'views/utils/selectors' -const OASWSelectorFactory = memoize((shipId) => +interface OASWSelectorResult { + isOASW: boolean +} + + +const OASWSelectorFactory = memoize((shipId: number) => createSelector( [shipDataSelectorFactory(shipId), shipEquipDataSelectorFactory(shipId)], ([_ship = {}, $ship = {}] = [], _equips = []) => { @@ -22,17 +27,23 @@ const OASWSelectorFactory = memoize((shipId) => ), ) -export const OASWIndicator = withNamespaces(['main'])( - connect((state, { shipId }) => ({ - isOASW: OASWSelectorFactory(shipId)(state), - }))( - ({ isOASW, shipId, t }) => - isOASW && ( +const OASWIndicatorComponent: React.FC = ({ isOASW }) => { + const { t } = useTranslation(['main']) + return ( + <> + {isOASW && ( {t('main:OASW')} - ), - ), -) + )} + + ) +} + +const mapStateToProps = (state: unknown, { shipId }: { shipId: number }): OASWSelectorResult => ({ + isOASW: OASWSelectorFactory(shipId)(state), +}) + +export const OASWIndicator = connect(mapStateToProps)(OASWIndicatorComponent) diff --git a/views/components/ship/ship-item.es b/views/components/ship/ship-item.tsx similarity index 63% rename from views/components/ship/ship-item.es rename to views/components/ship/ship-item.tsx index 9b9369e8c..2ffa7a5ab 100644 --- a/views/components/ship/ship-item.es +++ b/views/components/ship/ship-item.tsx @@ -1,9 +1,8 @@ import { ProgressBar, Tooltip, Position, Tag, Intent } from '@blueprintjs/core' import shallowEqual from 'fbjs/lib/shallowEqual' import { isEqual, pick, omit, memoize, get } from 'lodash' -import PropTypes from 'prop-types' import React, { Component } from 'react' -import { withNamespaces } from 'react-i18next' +import { type WithTranslation, withTranslation } from 'react-i18next' import { connect } from 'react-redux' import { createSelector } from 'reselect' import { MaterialIcon } from 'views/components/etc/icon' @@ -49,7 +48,56 @@ import { AAPBIndicator } from './aapb-indicator' import { OASWIndicator } from './oasw-indicator' import { Slotitems } from './slotitems' -const shipRowDataSelectorFactory = memoize((shipId) => +interface Ship { + api_lv?: number + api_exp?: [number, number, number] + api_id: number + api_ship_id?: number + api_nowhp: number + api_maxhp: number + api_cond: number + api_fuel: number + api_bull: number + api_soku: number + api_ndock_time: number + [key: string]: unknown +} + +interface ShipType { + api_name?: string + [key: string]: unknown +} + +interface ConstData { + $shipTypes: Record + [key: string]: unknown +} + +interface RepairDock { + [key: string]: unknown +} + +interface ShipRowData { + ship: Ship + $ship: Ship + $shipTypes: Record + labelStatus: number + shipAvatarColor: string +} + +interface ShipRowProps extends WithTranslation { + shipId: number + ship?: Ship + $ship?: Ship + $shipTypes?: Record + labelStatus?: number + enableAvatar?: boolean + compact?: boolean + shipAvatarColor?: string + showSpAttackLabel?: boolean +} + +const shipRowDataSelectorFactory = memoize((shipId: number) => createSelector( [ shipDataSelectorFactory(shipId), @@ -57,32 +105,33 @@ const shipRowDataSelectorFactory = memoize((shipId) => constSelector, escapeStatusSelectorFactory(shipId), fcdShipTagColorSelector, - (state) => get(state, 'config.poi.appearance.avatarType'), + (state: Record) => get(state, 'config.poi.appearance.avatarType'), ], - ([ship, $ship] = [], repairDock, { $shipTypes }, escaped, shipTagColor, avatarType) => ({ - ship: ship || {}, - $ship: $ship || {}, - $shipTypes, - labelStatus: getShipLabelStatus(ship, $ship, repairDock, escaped), - shipAvatarColor: selectShipAvatarColor(ship, $ship, shipTagColor, avatarType), - }), + ( + shipData: [Ship, Ship] | undefined, + repairDock: RepairDock | undefined, + { $shipTypes }: ConstData, + escaped: boolean, + shipTagColor: string[], + avatarType: string, + ): ShipRowData => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- default empty Ship when shipData is undefined + const [ship, $ship] = shipData ?? [{} as Ship, {} as Ship] + return { + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- fallback empty Ship when ship is falsy + ship: ship || ({} as Ship), + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- fallback empty Ship when $ship is falsy + $ship: $ship || ({} as Ship), + $shipTypes, + labelStatus: getShipLabelStatus(ship, $ship, repairDock, escaped), + shipAvatarColor: selectShipAvatarColor(ship, $ship, shipTagColor, avatarType), + } + }, ), ) -@withNamespaces(['main', 'resources']) -@connect((state, { shipId }) => shipRowDataSelectorFactory(shipId)(state)) -export class ShipRow extends Component { - static propTypes = { - ship: PropTypes.object, - $ship: PropTypes.object, - $shipTypes: PropTypes.object, - labelStatus: PropTypes.number, - enableAvatar: PropTypes.bool, - compact: PropTypes.bool, - shipAvatarColor: PropTypes.string, - } - - shouldComponentUpdate(nextProps) { +class ShipRowComponent extends Component { + shouldComponentUpdate(nextProps: ShipRowProps): boolean { // Remember to expand the list in case you add new properties to display const shipPickProps = [ 'api_lv', @@ -101,55 +150,70 @@ export class ShipRow extends Component { ) } - render() { + render(): React.ReactNode { const { - ship, - $ship, - $shipTypes, - labelStatus, + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- default empty Ship for destructuring default value + ship = {} as Ship, + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- default empty Ship for destructuring default value + $ship = {} as Ship, + $shipTypes = {}, + labelStatus = 0, enableAvatar, shipAvatarColor, showSpAttackLabel, compact, t, } = this.props + const hideShipName = enableAvatar && compact const labelStatusStyle = getStatusStyle(labelStatus) - const hpPercentage = (ship.api_nowhp / ship.api_maxhp) * 100 - const fuelPercentage = (ship.api_fuel / $ship.api_fuel_max) * 100 - const ammoPercentage = (ship.api_bull / $ship.api_bull_max) * 100 + const hpPercentage = ship.api_maxhp ? (ship.api_nowhp / ship.api_maxhp) * 100 : 0 + const fuelPercentage = ((ship.api_fuel ?? 0) / ($ship?.api_fuel_max || 1)) * 100 + const ammoPercentage = ((ship.api_bull ?? 0) / ($ship?.api_bull_max || 1)) * 100 + const fuelTip = ( - {ship.api_fuel} / {$ship.api_fuel_max} + {ship.api_fuel ?? 0} / {$ship?.api_fuel_max} {fuelPercentage < 100 && ` (-${Math.max( 1, - Math.floor(($ship.api_fuel_max - ship.api_fuel) * (ship.api_lv > 99 ? 0.85 : 1)), + Math.floor( + (($ship?.api_fuel_max || 0) - (ship.api_fuel ?? 0)) * + (ship.api_lv && ship.api_lv > 99 ? 0.85 : 1), + ), )})`} ) + const ammoTip = ( - {ship.api_bull} / {$ship.api_bull_max} + {ship.api_bull ?? 0} / {$ship?.api_bull_max} {ammoPercentage < 100 && ` (-${Math.max( 1, - Math.floor(($ship.api_bull_max - ship.api_bull) * (ship.api_lv > 99 ? 0.85 : 1)), + Math.floor( + (($ship?.api_bull_max || 0) - (ship.api_bull ?? 0)) * + (ship.api_lv && ship.api_lv > 99 ? 0.85 : 1), + ), )})`} ) + const shipBasicContent = ( <> Lv. {ship.api_lv || '??'} - {$shipTypes[$ship.api_stype] && $shipTypes[$ship.api_stype].api_name - ? t(`resources:${$shipTypes[$ship.api_stype].api_name}`) + {/* eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- api_stype is a number key used for $shipTypes lookup */} + {$shipTypes[$ship?.api_stype as number]?.api_name + ? // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- same api_stype assertion for translation key + t(`resources:${$shipTypes[$ship?.api_stype as number].api_name}`) : '??'} ) + const shipIndicatorsContent = ( <> {t(`main:${getSpeedLabel(ship.api_soku)}`)} @@ -165,6 +229,7 @@ export class ShipRow extends Component { )} ) + return ( -
{$ship.api_name ? t(`resources:${$ship.api_name}`) : '??'}
+
{$ship?.api_name ? t(`resources:${$ship.api_name}`) : '??'}
Lv. {ship.api_lv || '??'} Next. {(ship.api_exp || [])[1]}
@@ -190,7 +255,8 @@ export class ShipRow extends Component { {enableAvatar && ( <> - {$ship.api_name + {$ship?.api_name ? t(`resources:${$ship.api_name}`, { keySeparator: 'chiba' }) : '??'} @@ -297,3 +363,11 @@ export class ShipRow extends Component { ) } } + +const mapStateToProps = (state: unknown, ownProps: { shipId: number }): Partial => ({ + ...shipRowDataSelectorFactory(ownProps.shipId)(state), +}) + +export const ShipRow = connect(mapStateToProps)( + withTranslation(['main', 'resources'])(ShipRowComponent), +) diff --git a/views/components/ship/slotitems-data.es b/views/components/ship/slotitems-data.es deleted file mode 100644 index 978b98e71..000000000 --- a/views/components/ship/slotitems-data.es +++ /dev/null @@ -1,52 +0,0 @@ -import i18next from 'views/env-parts/i18next' - -const types = { - api_taik: 'HP', - api_souk: 'Armor', - api_houg: 'Firepower', - api_raig: 'Torpedo', - api_soku: 'Speed', - api_baku: 'Bombing', - api_tyku: 'AA', - api_tais: 'ASW', - api_houm: 'Accuracy', - //"api_raim": "Torpedo Accuracy", - api_houk: 'Evasion', - //"api_raik": "Torpedo Evasion", - //"api_bakk": "Bombing Evasion", - api_saku: 'LOS', - //"api_sakb": "Anti-LOS", - api_luck: 'Luck', - api_leng: 'Range', -} - -const landbaseFighterTypes = { - api_houm: 'Anti-Bomber', - api_houk: 'Interception', -} - -const range = ['Short', 'Medium', 'Long', 'Very Long'] - -export function getItemData(slotitem) { - const data = [] - for (const type in types) { - if (slotitem[type] && slotitem[type] != 0) { - if (type == 'api_leng') { - data.push( - `${i18next.t('data:' + types[type])} ${i18next.t('data:' + range[slotitem[type] - 1])}`, - ) - } else if ( - [48].includes(slotitem.api_type[2]) && - ['api_houk', 'api_houm'].includes(type) && - slotitem[type] > 0 - ) { - data.push(`${i18next.t('data:' + landbaseFighterTypes[type])} +${slotitem[type]}`) - } else if (slotitem[type] > 0) { - data.push(`${i18next.t('data:' + types[type])} +${slotitem[type]}`) - } else { - data.push(`${i18next.t('data:' + types[type])} ${slotitem[type]}`) - } - } - } - return data -} diff --git a/views/components/ship/slotitems-data.ts b/views/components/ship/slotitems-data.ts new file mode 100644 index 000000000..6511d0eb0 --- /dev/null +++ b/views/components/ship/slotitems-data.ts @@ -0,0 +1,62 @@ +import i18next from 'views/env-parts/i18next' + +interface TypesMap { + [key: string]: string +} + +const types: TypesMap = { + api_taik: 'HP', + api_souk: 'Armor', + api_houg: 'Firepower', + api_raig: 'Torpedo', + api_soku: 'Speed', + api_baku: 'Bombing', + api_tyku: 'AA', + api_tais: 'ASW', + api_houm: 'Accuracy', + // "api_raim": "Torpedo Accuracy", + api_houk: 'Evasion', + // "api_raik": "Torpedo Evasion", + // "api_bakk": "Bombing Evasion", + api_saku: 'LOS', + // "api_sakb": "Anti-LOS", + api_luck: 'Luck', + api_leng: 'Range', +} + +const landbaseFighterTypes: TypesMap = { + api_houm: 'Anti-Bomber', + api_houk: 'Interception', +} + +const range = ['Short', 'Medium', 'Long', 'Very Long'] + +interface SlotItem { + api_type: [number, number, number, number] + [key: string]: unknown +} + +export function getItemData(slotitem: SlotItem): string[] { + const data: string[] = [] + for (const type in types) { + const value = slotitem[type] as number | undefined // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion -- dynamic property access on SlotItem requires numeric assertion + if (value !== undefined && value !== 0) { + if (type === 'api_leng') { + data.push( + `${i18next.t('data:' + types[type])} ${i18next.t('data:' + (range[value - 1] ?? ''))}`, + ) + } else if ( + [48].includes(slotitem.api_type[2]) && + ['api_houk', 'api_houm'].includes(type) && + value > 0 + ) { + data.push(`${i18next.t('data:' + landbaseFighterTypes[type])} +${value}`) + } else if (value > 0) { + data.push(`${i18next.t('data:' + types[type])} +${value}`) + } else { + data.push(`${i18next.t('data:' + types[type])} ${value}`) + } + } + } + return data +} diff --git a/views/components/ship/slotitems.es b/views/components/ship/slotitems.es deleted file mode 100644 index 0c1f59e07..000000000 --- a/views/components/ship/slotitems.es +++ /dev/null @@ -1,196 +0,0 @@ -import { Tooltip, Intent, Position } from '@blueprintjs/core' -import classNames from 'classnames' -import { memoize } from 'lodash' -import { join } from 'path-extra' -import React from 'react' -import FontAwesome from 'react-fontawesome' -import { withNamespaces } from 'react-i18next' -import { connect } from 'react-redux' -import { compose } from 'redux' -import { createSelector } from 'reselect' -import { SlotitemIcon } from 'views/components/etc/icon' -import { - SlotItems, - SlotItemContainer, - OnSlotMini, - ALevel, -} from 'views/components/ship-parts/styled-components' -import { equipIsAircraft } from 'views/utils/game-utils' -import { - shipDataSelectorFactory, - shipEquipDataSelectorFactory, - landbaseSelectorFactory, - landbaseEquipDataSelectorFactory, -} from 'views/utils/selectors' - -import { getItemData } from './slotitems-data' - -const slotitemsDataSelectorFactory = memoize((shipId) => - createSelector( - [shipDataSelectorFactory(shipId), shipEquipDataSelectorFactory(shipId)], - ([ship, $ship] = [{}, {}], equipsData) => ({ - api_maxeq: $ship.api_maxeq, - equipsData, - exslotUnlocked: ship.api_slot_ex !== 0, - }), - ), -) - -const landbaseSlotitemsDataSelectorFactory = memoize((landbaseId) => - createSelector( - [landbaseSelectorFactory(landbaseId), landbaseEquipDataSelectorFactory(landbaseId)], - (landbase = {}, equipsData) => ({ - api_maxeq: (landbase.api_plane_info || []).map((l) => l.api_max_count), - api_cond: (landbase.api_plane_info || []).map((l) => l.api_cond), - api_state: (landbase.api_plane_info || []).map((l) => l.api_state), - equipsData, - }), - ), -) - -export const Slotitems = compose( - withNamespaces(['resources']), - connect((state, { shipId }) => slotitemsDataSelectorFactory(shipId)(state)), -)(({ api_maxeq, equipsData, exslotUnlocked, t }) => ( - - {equipsData && - equipsData.map((equipData, equipIdx) => { - const isExslot = equipIdx === equipsData.length - 1 - if (isExslot && !equipData && !exslotUnlocked) { - return
- } - const [equip, $equip, onslot] = equipData || [] - const itemOverlay = equipData && ( -
-
-
- {$equip.api_name - ? t(`resources:${$equip.api_name}`, { keySeparator: '%%%%' }) - : '??'} - {equip.api_level == null || equip.api_level == 0 ? undefined : ( - - {' '} - - {equip.api_level} - - )} - {equip.api_alv && equip.api_alv >= 1 && equip.api_alv <= 7 && ( - - )} -
- {$equip && getItemData($equip).map((data, propId) =>
{data}
)} -
-
- ) - - const equipIconId = equipData ? $equip.api_type[3] : 0 - const showOnslot = !equipData || isExslot || equipIsAircraft($equip) - const maxOnslot = isExslot ? 0 : api_maxeq[equipIdx] - const onslotText = isExslot ? '+' : equipData ? `${onslot}` : `${maxOnslot}` - const onslotWarning = equipData && onslot < maxOnslot - - return ( - - - - - - ) - })} - -)) - -export const LandbaseSlotitems = compose( - withNamespaces(['resources']), - connect((state, { landbaseId }) => landbaseSlotitemsDataSelectorFactory(landbaseId)(state)), -)(({ api_maxeq, api_cond, api_state, equipsData, isMini, t, className }) => ( - - {equipsData && - equipsData.map((equipData, equipIdx) => { - const [equip, $equip, onslot] = equipData || [] - const equipIconId = equipData ? $equip.api_type[3] : 0 - const showOnslot = !equipData || equipIsAircraft($equip) - const maxOnslot = api_maxeq[equipIdx] - const onslotWarning = equipData && onslot < maxOnslot - const onslotText = equipData ? onslot : maxOnslot - const iconStyle = { - opacity: api_state[equipIdx] === 2 ? 0.5 : null, - filter: - api_cond[equipIdx] > 1 - ? `drop-shadow(0px 0px 4px ${api_cond[equipIdx] === 2 ? '#FB8C00' : '#E53935'})` - : null, - } - const itemOverlay = equipData && ( -
-
-
- {$equip.api_name - ? t(`resources:${$equip.api_name}`, { keySeparator: 'chiba' }) - : '??'} - {equip.api_level > 0 && ( - - {' '} - - {equip.api_level} - - )} - {equip.api_alv && equip.api_alv >= 1 && equip.api_alv <= 7 && ( - - )} - {isMini && ( - - {onslotText} - - )} - {$equip.api_distance} -
- {$equip && getItemData($equip).map((data, propId) =>
{data}
)} -
-
- ) - - return ( - - - - - - ) - })} -
-)) diff --git a/views/components/ship/slotitems.tsx b/views/components/ship/slotitems.tsx new file mode 100644 index 000000000..e395b67a2 --- /dev/null +++ b/views/components/ship/slotitems.tsx @@ -0,0 +1,318 @@ +import { Tooltip, Intent, Position } from '@blueprintjs/core' +import classNames from 'classnames' +import { memoize } from 'lodash' +import { join } from 'path-extra' +import React from 'react' +import FontAwesome from 'react-fontawesome' +import { useTranslation } from 'react-i18next' +import { connect } from 'react-redux' +import { createSelector } from 'reselect' +import { SlotitemIcon } from 'views/components/etc/icon' +import { + SlotItems, + SlotItemContainer, + OnSlotMini, + ALevel, +} from 'views/components/ship-parts/styled-components' +import { equipIsAircraft } from 'views/utils/game-utils' +import { + shipDataSelectorFactory, + shipEquipDataSelectorFactory, + landbaseSelectorFactory, + landbaseEquipDataSelectorFactory, +} from 'views/utils/selectors' + +import { getItemData } from './slotitems-data' + +interface Equip { + api_slotitem_id?: number + api_name?: string + api_type: [number, number, number, number] + api_level?: number + api_alv?: number + api_distance?: number + [key: string]: unknown +} + +interface Ship { + api_slot_ex: number + api_maxeq?: number[] + [key: string]: unknown +} + +interface Landbase { + api_plane_info: Array<{ + api_max_count: number + api_cond: number + api_state: number + }> + [key: string]: unknown +} + +interface SlotitemsData { + api_maxeq: number[] + equipsData: ([Equip, Equip, number] | undefined)[] + exslotUnlocked: boolean +} + +interface LandbaseSlotitemsData { + api_maxeq: number[] + api_cond: number[] + api_state: number[] + equipsData: ([Equip, Equip, number] | undefined)[] +} + +interface SlotitemsProps { + shipId: number +} + +type SlotitemsStateProps = SlotitemsData + +interface SlotitemsComponentProps extends SlotitemsProps, SlotitemsStateProps {} + +interface LandbaseSlotitemsProps { + landbaseId: number + isMini?: boolean + className?: string +} + +type LandbaseSlotitemsStateProps = LandbaseSlotitemsData + +interface LandbaseSlotitemsComponentProps + extends LandbaseSlotitemsProps, LandbaseSlotitemsStateProps {} + +const slotitemsDataSelectorFactory = memoize((shipId: number) => + createSelector( + [shipDataSelectorFactory(shipId), shipEquipDataSelectorFactory(shipId)], + ( + shipData: [Ship, Ship] | undefined, + equipsData: ([Equip, Equip, number] | undefined)[] | undefined, + ): SlotitemsData => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- provide default empty Ship objects when shipData is undefined + const [ship, $ship] = shipData ?? [{} as Ship, {} as Ship] + return { + api_maxeq: $ship?.api_maxeq ?? [], + equipsData: equipsData ?? [], + exslotUnlocked: (ship?.api_slot_ex ?? 0) !== 0, + } + }, + ), +) + +const landbaseSlotitemsDataSelectorFactory = memoize((landbaseId: number) => + createSelector( + [landbaseSelectorFactory(landbaseId), landbaseEquipDataSelectorFactory(landbaseId)], + ( + landbase: Landbase | undefined, + equipsData: ([Equip, Equip, number] | undefined)[] | undefined, + ): LandbaseSlotitemsData => ({ + api_maxeq: (landbase?.api_plane_info || []).map((l) => l.api_max_count), + api_cond: (landbase?.api_plane_info || []).map((l) => l.api_cond), + api_state: (landbase?.api_plane_info || []).map((l) => l.api_state), + equipsData: equipsData ?? [], + }), + ), +) + +const SlotitemsComponent: React.FC = ({ + api_maxeq, + equipsData, + exslotUnlocked, +}) => { + const { t } = useTranslation(['resources']) + + return ( + + {equipsData && + equipsData.map((equipData, equipIdx) => { + const isExslot = equipIdx === equipsData.length - 1 + if (isExslot && !equipData && !exslotUnlocked) { + return
+ } + const [equip, $equip, onslot] = equipData || [] + const itemOverlay = equipData && ( +
+
+
+ {$equip?.api_name + ? t(`resources:${$equip.api_name}`, { keySeparator: '%%%%' }) + : '??'} + {equip?.api_level == null || equip?.api_level === 0 ? undefined : ( + + {' '} + + {equip.api_level} + + )} + {equip?.api_alv && equip.api_alv >= 1 && equip.api_alv <= 7 && ( + + )} +
+ {/* eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- $equip from selector conforms to Equip shape when truthy */} + {$equip && + getItemData($equip as Equip).map((data, propId) => ( +
{data}
+ ))} +
+
+ ) + + const equipIconId = equipData ? $equip?.api_type?.[3] : 0 + const showOnslot = !equipData || isExslot || equipIsAircraft($equip) + const maxOnslot = isExslot ? 0 : (api_maxeq?.[equipIdx] ?? 0) + const onslotText = isExslot ? '+' : equipData ? `${onslot}` : `${maxOnslot}` + const onslotWarning = !!equipData && typeof onslot === 'number' && onslot < maxOnslot + + return ( + + + + + + ) + })} + + ) +} + +const LandbaseSlotitemsComponent: React.FC = ({ + api_maxeq, + api_cond, + api_state, + equipsData, + isMini, + className, +}) => { + const { t } = useTranslation(['resources']) + + return ( + + {equipsData && + equipsData.map((equipData, equipIdx) => { + const [equip, $equip, onslot] = equipData || [] + const equipIconId = equipData ? $equip?.api_type?.[3] : 0 + const showOnslot = !equipData || equipIsAircraft($equip) + const maxOnslot = api_maxeq?.[equipIdx] ?? 0 + const onslotWarning = !!equipData && typeof onslot === 'number' && onslot < maxOnslot + const onslotText = equipData ? onslot : maxOnslot + const iconStyle: React.CSSProperties = { + opacity: api_state?.[equipIdx] === 2 ? 0.5 : undefined, + filter: + api_cond?.[equipIdx] > 1 + ? `drop-shadow(0px 0px 4px ${api_cond?.[equipIdx] === 2 ? '#FB8C00' : '#E53935'})` + : undefined, + } + const itemOverlay = equipData && ( +
+
+
+ {$equip?.api_name + ? t(`resources:${$equip.api_name}`, { keySeparator: 'chiba' }) + : '??'} + {equip && + (() => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- equip from equipData tuple is typed loosely, assert once for property access + const typedEquip = equip as Equip + return ( + <> + {typedEquip.api_level && typedEquip.api_level > 0 && ( + + {' '} + + {typedEquip.api_level} + + )} + {typedEquip?.api_alv && + typedEquip.api_alv >= 1 && + typedEquip.api_alv <= 7 && ( + + )} + + ) + })()} + {isMini && ( + + {onslotText} + + )} + {' '} + {/* eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- equip from equipData tuple needs assertion for api_distance access */} + {(equip as Equip)?.api_distance} +
+ {/* eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- $equip from selector conforms to Equip shape when truthy */} + {$equip && + getItemData($equip as Equip).map((data, propId) => ( +
{data}
+ ))} +
+
+ ) + + return ( + + + + + + ) + })} +
+ ) +} + +const mapStateToPropsSlotitems = ( + state: unknown, + ownProps: SlotitemsProps, +): SlotitemsStateProps => ({ + ...slotitemsDataSelectorFactory(ownProps.shipId)(state), +}) + +const mapStateToPropsLandbase = ( + state: unknown, + ownProps: LandbaseSlotitemsProps, +): LandbaseSlotitemsStateProps => ({ + ...landbaseSlotitemsDataSelectorFactory(ownProps.landbaseId)(state), +}) + +export const Slotitems = connect(mapStateToPropsSlotitems)(SlotitemsComponent) +export const LandbaseSlotitems = connect(mapStateToPropsLandbase)(LandbaseSlotitemsComponent)