Skip to content

Commit 76b7065

Browse files
refactor(StakingProductsCardGrid): improve type clarity and constraints
1 parent 379c440 commit 76b7065

File tree

1 file changed

+103
-74
lines changed

1 file changed

+103
-74
lines changed

src/components/Staking/StakingProductsCardGrid.tsx

Lines changed: 103 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { ComponentType, SVGProps, useEffect, useState } from "react"
1+
import React, { useEffect, useState } from "react"
22
import { shuffle } from "lodash"
33
import {
44
Badge,
@@ -8,7 +8,7 @@ import {
88
Flex,
99
Heading,
1010
HStack,
11-
Icon,
11+
Icon as ChakraIcon,
1212
List,
1313
ListIcon,
1414
ListItem,
@@ -44,28 +44,29 @@ enum FlagType {
4444
UNKNOWN = "unknown",
4545
}
4646

47-
const getIconFromName = (
48-
imageName: string
49-
): ComponentType<SVGProps<SVGElement>> => {
47+
const getIconFromName = (imageName: string): typeof ChakraIcon | undefined => {
5048
const { [imageName + "GlyphIcon"]: Icon } = require("../icons/staking")
5149
return Icon
5250
}
5351

54-
const Status: React.FC<{ status: FlagType }> = ({ status }) => {
52+
const Status = ({ status }: { status: FlagType }) => {
5553
if (!status) return null
5654

57-
const styles = { fontSize: "2xl", m: 0 }
58-
switch (status) {
59-
case "green-check":
60-
return <ListIcon as={GreenCheckProductGlyphIcon} {...styles} />
61-
case "caution":
62-
return <ListIcon as={CautionProductGlyphIcon} {...styles} />
63-
case "warning":
64-
case "false":
65-
return <ListIcon as={WarningProductGlyphIcon} {...styles} />
66-
default:
67-
return <ListIcon as={UnknownProductGlyphIcon} {...styles} />
55+
const getStatusIcon = () => {
56+
switch (status) {
57+
case "green-check":
58+
return GreenCheckProductGlyphIcon
59+
case "caution":
60+
return CautionProductGlyphIcon
61+
case "warning":
62+
case "false":
63+
return WarningProductGlyphIcon
64+
default:
65+
return UnknownProductGlyphIcon
66+
}
6867
}
68+
69+
return <ListIcon as={getStatusIcon()} fontSize="2xl" m={0} />
6970
}
7071

7172
const StakingBadge: React.FC<{
@@ -93,20 +94,20 @@ type Product = {
9394
url: string
9495
platforms: Array<string>
9596
ui: Array<string>
96-
minEth: number
97+
minEth?: number
9798
openSource: FlagType
9899
audited: FlagType
99100
bugBounty: FlagType
100101
battleTested: FlagType
101-
trustless: FlagType
102-
selfCustody: FlagType
103-
liquidityToken: FlagType
104-
permissionless: FlagType
105-
permissionlessNodes: FlagType
106-
multiClient: FlagType
107-
consensusDiversity: FlagType
108-
executionDiversity: FlagType
109-
economical: FlagType
102+
trustless?: FlagType
103+
selfCustody?: FlagType
104+
liquidityToken?: FlagType
105+
permissionless?: FlagType
106+
permissionlessNodes?: FlagType
107+
multiClient?: FlagType
108+
consensusDiversity?: FlagType
109+
executionDiversity?: FlagType
110+
economical?: FlagType
110111
matomo: MatomoEventOptions
111112
}
112113
interface ICardProps {
@@ -139,7 +140,8 @@ const StakingProductCard: React.FC<ICardProps> = ({
139140
},
140141
}) => {
141142
const Svg = getIconFromName(imageName)
142-
const data = [
143+
type DataType = { label: JSX.Element; status?: FlagType }
144+
const data: DataType[] = [
143145
{
144146
label: <Translation id="page-staking-considerations-solo-1-title" />,
145147
status: openSource,
@@ -192,7 +194,11 @@ const StakingProductCard: React.FC<ICardProps> = ({
192194
label: <Translation id="page-staking-considerations-solo-9-title" />,
193195
status: economical,
194196
},
195-
].filter(({ status }) => !!status)
197+
]
198+
199+
const filteredData = data.filter(
200+
(item): item is Required<DataType> => !!item.status
201+
)
196202

197203
return (
198204
<Flex
@@ -212,7 +218,7 @@ const StakingProductCard: React.FC<ICardProps> = ({
212218
borderRadius="base"
213219
maxH={24}
214220
>
215-
{!!Svg && <Icon as={Svg} fontSize="2rem" color="white" />}
221+
{!!Svg && <Svg fontSize="2rem" color="white" />}
216222
<Heading as="h4" fontSize="2xl" color="white">
217223
{name}
218224
</Heading>
@@ -250,8 +256,8 @@ const StakingProductCard: React.FC<ICardProps> = ({
250256
</Flex>
251257
<Box {...PADDED_DIV_STYLE} py={0}>
252258
<List m={0} gap={3}>
253-
{data &&
254-
data.map(({ label, status }, idx) => (
259+
{filteredData &&
260+
filteredData.map(({ label, status }, idx) => (
255261
<ListItem
256262
as={Flex}
257263
key={idx}
@@ -280,8 +286,20 @@ const StakingProductCard: React.FC<ICardProps> = ({
280286
)
281287
}
282288

289+
type StakingProductsType = typeof stakingProducts
290+
291+
type NodeToolsType = StakingProductsType["nodeTools"]
292+
type KeyGenType = StakingProductsType["keyGen"]
293+
type PoolsType = StakingProductsType["pools"]
294+
type SaasType = StakingProductsType["saas"]
295+
296+
type StakingProductsCategoryKeys = keyof StakingProductsType
297+
298+
type StakingCategoryType =
299+
StakingProductsType[StakingProductsCategoryKeys][number]
300+
283301
export interface IProps {
284-
category: string
302+
category: StakingProductsCategoryKeys
285303
}
286304

287305
const StakingProductCardGrid: React.FC<IProps> = ({ category }) => {
@@ -324,8 +342,12 @@ const StakingProductCardGrid: React.FC<IProps> = ({ category }) => {
324342
return product.multiClient === FlagType.VALID ? 1 : 0
325343
}
326344

327-
const scoreClientDiversity = (flag: FlagType): 2 | 1 | 0 => {
328-
return flag === FlagType.VALID ? 2 : flag === FlagType.WARNING ? 1 : 0
345+
const scoreClientDiversity = (flag: FlagType | undefined): 2 | 1 | 0 => {
346+
if (flag === FlagType.VALID) return 2
347+
348+
if (flag === FlagType.WARNING) return 1
349+
350+
return 0
329351
}
330352

331353
const scoreEconomical = (product: Product): 1 | 0 => {
@@ -348,75 +370,83 @@ const StakingProductCardGrid: React.FC<IProps> = ({ category }) => {
348370
return score
349371
}
350372

351-
const getBattleTestedFlag = (_launchDate: string): FlagType => {
352-
let battleTested = FlagType.WARNING
373+
const getBattleTestedFlag = (
374+
_launchDate: string
375+
): FlagType.CAUTION | FlagType.WARNING | FlagType.VALID => {
353376
const launchDate = new Date(_launchDate)
354377
const now = new Date()
355378
const halfYearAgo = new Date()
356379
const oneYearAgo = new Date()
357380
halfYearAgo.setDate(now.getDate() - 183)
358381
oneYearAgo.setDate(now.getDate() - 365)
382+
359383
if (halfYearAgo > launchDate) {
360-
battleTested = FlagType.CAUTION
384+
return FlagType.CAUTION
361385
}
386+
362387
if (oneYearAgo > launchDate) {
363-
battleTested = FlagType.VALID
388+
return FlagType.VALID
364389
}
365-
return battleTested
390+
391+
return FlagType.WARNING
366392
}
367393

368394
const getDiversityOfClients = (
369395
_pctMajorityClient: number | null
370-
): FlagType => {
396+
): FlagType.VALID | FlagType.UNKNOWN | FlagType.WARNING => {
371397
if (_pctMajorityClient === null) return FlagType.UNKNOWN
372398
if (_pctMajorityClient > 50) return FlagType.WARNING
373399
return FlagType.VALID
374400
}
375401

376-
const getFlagFromBoolean = (bool: boolean): FlagType =>
377-
!!bool ? FlagType.VALID : FlagType.FALSE
402+
const getFlagFromBoolean = (bool: boolean) =>
403+
bool ? FlagType.VALID : FlagType.FALSE
378404

379405
const getBrandProperties = ({
380-
name,
381-
imageName,
382406
hue,
383-
url,
384-
socials,
385-
matomo,
386-
}) => ({
387-
name,
388-
imageName,
407+
...rest
408+
}: Pick<
409+
StakingCategoryType,
410+
"name" | "imageName" | "hue" | "url" | "matomo"
411+
>): Pick<Product, "name" | "imageName" | "color" | "url" | "matomo"> => ({
389412
color: `hsla(${hue}, ${SAT}, ${LUM}, 1)`,
390-
url,
391-
socials,
392-
matomo,
413+
...rest,
393414
})
394415

395-
const getTagProperties = ({ platforms, ui }) => ({
396-
platforms,
397-
ui,
398-
})
416+
const getTagProperties = (
417+
props: Pick<StakingCategoryType, "platforms" | "ui">
418+
): Pick<Product, "platforms" | "ui"> => props
399419

400-
const getSharedSecurityProperties = ({
401-
isFoss,
402-
audits,
403-
hasBugBounty,
404-
launchDate,
405-
}) => ({
406-
openSource: getFlagFromBoolean(isFoss),
407-
audited: getFlagFromBoolean(audits?.length),
408-
bugBounty: getFlagFromBoolean(hasBugBounty),
409-
battleTested: getBattleTestedFlag(launchDate),
420+
const getSharedSecurityProperties = (
421+
props: Pick<
422+
StakingCategoryType,
423+
"isFoss" | "audits" | "hasBugBounty" | "launchDate"
424+
>
425+
): Pick<
426+
Product,
427+
"openSource" | "audited" | "bugBounty" | "battleTested"
428+
> => ({
429+
openSource: getFlagFromBoolean(props.isFoss),
430+
audited: getFlagFromBoolean(!!props.audits?.length),
431+
bugBounty: getFlagFromBoolean(props.hasBugBounty),
432+
battleTested: getBattleTestedFlag(props.launchDate),
410433
})
411434

412435
useEffect(() => {
413436
const categoryProducts = stakingProducts[category]
414437
const products: Array<Product> = []
415438

439+
function mapCatProducts<T extends unknown>(
440+
products: T[],
441+
cb: (listing: T) => Product
442+
) {
443+
return products.map((item) => cb(item))
444+
}
445+
416446
// Pooled staking services
417447
if (category === "pools") {
418448
products.push(
419-
...categoryProducts.map((listing) => ({
449+
...mapCatProducts(categoryProducts as PoolsType, (listing) => ({
420450
...getBrandProperties(listing),
421451
...getTagProperties(listing),
422452
...getSharedSecurityProperties(listing),
@@ -430,7 +460,7 @@ const StakingProductCardGrid: React.FC<IProps> = ({ category }) => {
430460
consensusDiversity: getDiversityOfClients(
431461
listing.pctMajorityConsensusClient
432462
),
433-
liquidityToken: getFlagFromBoolean(listing.tokens?.length),
463+
liquidityToken: getFlagFromBoolean(!!listing.tokens?.length),
434464
minEth: listing.minEth,
435465
}))
436466
)
@@ -439,7 +469,7 @@ const StakingProductCardGrid: React.FC<IProps> = ({ category }) => {
439469
// Solo staking products
440470
if (category === "nodeTools") {
441471
products.push(
442-
...categoryProducts.map((listing) => ({
472+
...mapCatProducts(categoryProducts as NodeToolsType, (listing) => ({
443473
...getBrandProperties(listing),
444474
...getTagProperties(listing),
445475
...getSharedSecurityProperties(listing),
@@ -455,7 +485,7 @@ const StakingProductCardGrid: React.FC<IProps> = ({ category }) => {
455485
// Staking as a service
456486
if (category === "saas") {
457487
products.push(
458-
...categoryProducts.map((listing) => ({
488+
...mapCatProducts(categoryProducts as SaasType, (listing) => ({
459489
...getBrandProperties(listing),
460490
...getTagProperties(listing),
461491
...getSharedSecurityProperties(listing),
@@ -474,7 +504,7 @@ const StakingProductCardGrid: React.FC<IProps> = ({ category }) => {
474504
// Key generators
475505
if (category === "keyGen") {
476506
products.push(
477-
...categoryProducts.map((listing) => ({
507+
...mapCatProducts(categoryProducts as KeyGenType, (listing) => ({
478508
...getBrandProperties(listing),
479509
...getTagProperties(listing),
480510
...getSharedSecurityProperties(listing),
@@ -494,7 +524,6 @@ const StakingProductCardGrid: React.FC<IProps> = ({ category }) => {
494524
.sort((a, b) => b.rankingScore - a.rankingScore)
495525
)
496526
}
497-
// eslint-disable-next-line react-hooks/exhaustive-deps
498527
}, [])
499528

500529
if (!rankedProducts) return null

0 commit comments

Comments
 (0)