diff --git a/src/components/Decoder/index.tsx b/src/components/Decoder/index.tsx index cf9de5429..32ffa3d5c 100644 --- a/src/components/Decoder/index.tsx +++ b/src/components/Decoder/index.tsx @@ -7,7 +7,7 @@ import { hexToUtf8 } from '../../utils/string' import { useSetToast } from '../Toast' import { ReactComponent as CopyIcon } from '../../assets/copy_icon.svg' import styles from './styles.module.scss' -import { parseSporeCellData } from '../../utils/spore' +import { parseSporeCellData, parseSporeClusterData } from '../../utils/spore' import { parseBtcTimeLockArgs } from '../../utils/rgbpp' enum DecodeMethod { @@ -18,6 +18,7 @@ enum DecodeMethod { TokenInfo = 'token-info', XudtData = 'xudt-data', BTCTimeLock = 'btc-time-lock', + SporeCluster = 'spore-cluster', Spore = 'spore', JSON = 'json', } @@ -217,6 +218,10 @@ const Decoder = () => { const script = addressToScript(v) return { display: JSON.stringify(script, null, 2), copy: script } } + case DecodeMethod.SporeCluster: { + const data = parseSporeClusterData(v) + return { display: jsonToList(data), copy: data } + } case DecodeMethod.Spore: { const data = parseSporeCellData(v) switch (data.contentType) { diff --git a/src/components/Footer/index.module.scss b/src/components/Footer/index.module.scss index f2e331af3..bb9d53207 100644 --- a/src/components/Footer/index.module.scss +++ b/src/components/Footer/index.module.scss @@ -1,8 +1,141 @@ -.tokenFormBtn { - font-size: 18px; +@import '../../styles/variables.module'; + +.container { + background: #000; color: #acacac; - height: 23px; - margin: 8px 0; + padding: 0 120px; + white-space: nowrap; + + .navigations { + display: flex; + width: 100%; + padding: 44px 0 32px; + gap: 280px; + + a, + button { + font-size: 18px; + + @media screen and (width < 1240px) { + font-size: 16px; + } + } + + .title { + font-size: 1.75rem; + font-weight: 900; + color: #fff; + } + + .section:last-child { + flex-grow: 0; + flex-shrink: 0; + } + + .section:nth-last-child(2) { + flex: 1; + } + + @media screen and (width <1800px) { + gap: 160px; + } + + @media screen and (width <1400px) { + gap: 120px; + } + + @media screen and (width <1240px) { + gap: 80px; + } + + @media screen and (width <$largeBreakPoint) { + gap: 20px; + flex-flow: column wrap; + max-height: 500px; + } + + @media screen and (width < 600px) { + max-height: 800px; + } + + @media screen and (width < 450px) { + max-height: unset; + } + } + + .linkList { + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: flex-start; + width: 100%; + margin-top: 8px; + line-height: 2; + + &[data-is-grid='true'] { + width: 240px; + flex-flow: row wrap; + gap: 40px 0; + margin-left: auto; + + a { + flex-basis: 70px; + } + + @media screen and (width < 450px) { + margin-left: 0; + gap: 10px; + } + } + } + + .iconLink { + display: flex; + flex-direction: column; + font-size: 12px; + align-items: center; + gap: 4px; + } + + .annotation { + height: 52px; + font-size: 12px; + color: #acacac; + font-weight: 100; + border-top: 1px solid #333; + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + + a { + font-weight: inherit; + display: flex; + align-items: center; + gap: 4px; + } + + @media screen and (width < 500px) { + flex-direction: column; + height: auto; + padding: 16px 0; + } + } + + @media screen and (width < $xxlBreakPoint) { + padding: 0 100px; + } + + @media screen and (width < $extraLargeBreakPoint) { + padding: 0 45px; + } + + @media screen and (width < $mobileBreakPoint) { + padding: 0 18px; + } +} + +.tokenFormBtn { user-select: none; cursor: pointer; background: transparent; diff --git a/src/components/Footer/index.tsx b/src/components/Footer/index.tsx index b924bab09..ffb3682c1 100644 --- a/src/components/Footer/index.tsx +++ b/src/components/Footer/index.tsx @@ -1,4 +1,4 @@ -import { ReactNode, memo, useMemo, useState } from 'react' +import { ReactNode, memo, useState } from 'react' import { useTranslation } from 'react-i18next' import { SubmitTokenInfo } from '../SubmitTokenInfo' import { ReactComponent as XIcon } from './footer_X.svg' @@ -10,165 +10,180 @@ import { ReactComponent as ForumIcon } from './footer_forum.svg' import { ReactComponent as Discord } from './footer_discord.svg' import { ReactComponent as Open } from './open.svg' import { getCurrentYear } from '../../utils/date' -import { FooterMenuPanel, FooterItemPanel, FooterImageItemPanel, FooterPanel } from './styled' import styles from './index.module.scss' interface FooterLinkItem { - label?: string - url?: string + label: string + url: string icon?: ReactNode } -interface FooterLink { - name: string - items: FooterLinkItem[] -} - -const FooterItem = ({ item }: { item: FooterLinkItem }) => { - const { label, url } = item - return ( - - {item.label} - - ) -} - -const FooterImageItem = ({ item }: { item: FooterLinkItem }) => { - const { label, url, icon: IconComponent } = item - - return ( - - {IconComponent} - {label} - - ) -} +const Footers: { name: string; items: FooterLinkItem[] }[] = [ + { + name: 'nervos_foundation', + items: [ + { + label: 'about_us', + url: 'https://www.nervos.org/', + }, + { + label: 'media_kit', + url: 'https://www.nervos.org/media-kit', + }, + ], + }, + { + name: 'developer', + items: [ + { + label: 'docs', + url: 'https://docs.nervos.org', + }, + { + label: 'gitHub', + url: 'https://github.com/nervosnetwork', + }, + { + label: 'whitepaper', + url: 'https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0002-ckb/0002-ckb.md', + }, + { + label: 'faucet', + url: 'https://faucet.nervos.org/', + }, + { + label: 'api-doc', + url: 'https://ckb-explorer.readme.io/reference/transaction', + }, + { + label: 'community_nodes', + url: 'https://docs.nervos.org/docs/getting-started/blockchain-networks#public-networks', + }, + ], + }, + { + name: 'community', + items: [ + { + label: 'knowledge_base', + url: 'https://www.nervos.org/knowledge-base', + }, + { + label: 'network', + url: '/charts/node-geo-distribution', + }, + ], + }, + { + name: 'platform', + items: [ + { + label: 'discord', + icon: , + url: 'https://discord.com/invite/FKh8Zzvwqa', + }, + { + label: 'X', + icon: , + url: 'https://x.com/nervosnetwork', + }, + { + label: 'blog', + icon: , + url: 'https://medium.com/nervosnetwork', + }, + { + label: 'telegram', + icon: , + url: 'https://t.me/nervosnetwork', + }, + { + label: 'reddit', + icon: , + url: 'https://www.reddit.com/r/NervosNetwork/', + }, + { + label: 'youtube', + icon: , + url: 'https://www.youtube.com/channel/UCONuJGdMzUY0Y6jrPBOzH7A', + }, + { + label: 'forum', + icon: , + url: 'https://talk.nervos.org/', + }, + ], + }, +] export default memo(() => { const [t] = useTranslation() const [isTokenFormDisplayed, setIsTokenFormDisplayed] = useState(false) - const Footers = useMemo( - () => [ - { - name: t('footer.nervos_foundation'), - items: [ - { - label: t('footer.about_us'), - url: 'https://www.nervos.org/', - }, - ], - }, - { - name: t('footer.developer'), - items: [ - { - label: t('footer.docs'), - url: 'https://docs.nervos.org', - }, - { - label: t('footer.gitHub'), - url: 'https://github.com/nervosnetwork', - }, - { - label: t('footer.whitepaper'), - url: 'https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0002-ckb/0002-ckb.md', - }, - { - label: t('footer.faucet'), - url: 'https://faucet.nervos.org/', - }, - { - label: t('footer.api-doc'), - url: 'https://ckb-explorer.readme.io/reference/transaction', - }, - ], - }, - { - name: t('footer.community'), - items: [ - { - label: t('footer.discord'), - icon: , - url: 'https://discord.com/invite/FKh8Zzvwqa', - }, - { - label: t('footer.X'), - icon: , - url: 'https://x.com/nervosnetwork', - }, - { - label: t('footer.blog'), - icon: , - url: 'https://medium.com/nervosnetwork', - }, - { - label: t('footer.telegram'), - icon: , - url: 'https://t.me/nervosnetwork', - }, - { - label: t('footer.reddit'), - icon: , - url: 'https://www.reddit.com/r/NervosNetwork/', - }, - { - label: t('footer.youtube'), - icon: , - url: 'https://www.youtube.com/channel/UCONuJGdMzUY0Y6jrPBOzH7A', - }, - { - label: t('footer.forum'), - icon: , - url: 'https://talk.nervos.org/', - }, - ], - }, - ], - [t], - ) const onSubmitToken = () => { setIsTokenFormDisplayed(true) } + const lists = Footers + return ( - -
- -
-
{Footers[0].name}
- {Footers[0].items.map(item => ( - - ))} -
-
-
{Footers[1].name}
- {Footers[1].items - .filter(item => item.label !== undefined) - .map(item => ( - - ))} - -
-
- {Footers[2].items.map(item => ( - - ))} -
-
+
+
+ {lists.map(list => { + return ( +
+ {list.name !== 'platform' ?
{t(`footer.${list.name}`)}
: null} +
+ {list.items.map(item => { + if (item.icon) { + return ( + + {item.icon} + {t(`footer.${item.label}`)} + + ) + } + return ( + + {t(`footer.${item.label}`)} + + ) + })} +
+ {list.name === 'community' ? ( + + ) : null} +
+ ) + })}
-
-
- - Powered by Magickbase - + +
+ + Powered by Magickbase + +
{`Copyright © ${getCurrentYear()} Nervos Foundation. `} All Rights Reserved.
+ {isTokenFormDisplayed ? setIsTokenFormDisplayed(false)} /> : null} - +
) }) diff --git a/src/components/Footer/styled.tsx b/src/components/Footer/styled.tsx deleted file mode 100644 index 52c6a2c0a..000000000 --- a/src/components/Footer/styled.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import styled from 'styled-components' -import variables from '../../styles/variables.module.scss' - -export const FooterPanel = styled.div` - background-color: #000; - display: flex; - flex-direction: column; - align-items: center; - - @media (max-width: ${variables.mobileBreakPoint}) { - align-items: flex-start; - } - - .footerCopyright { - border-top: 1px solid #333; - padding-top: 16px; - display: flex; - flex-direction: row; - margin-bottom: 16px; - font-size: 12px; - color: #acacac; - - .power { - display: flex; - align-items: center; - color: #999; - margin-right: auto; - - &:hover { - color: ${props => props.theme.primary}; - } - } - - @media (max-width: ${variables.mobileBreakPoint}) { - margin: 0 0 20px 20px; - flex-direction: column; - } - } - - a:hover { - color: ${props => props.theme.primary}; - } -` - -export const FooterMenuPanel = styled.div` - overflow: hidden; - margin-top: 44px; - margin-bottom: 32px; - display: flex; - flex-direction: row; - justify-content: center; - gap: 280px; - - @media (max-width: ${variables.xxlBreakPoint}) { - gap: 200px; - } - - @media (max-width: ${variables.largeBreakPoint}) { - gap: 100px; - } - - @media (max-width: ${variables.mobileBreakPoint}) { - flex-direction: column; - align-items: flex-start; - gap: 50px; - margin: 32px 20px; - } - - .footerFoundation { - display: flex; - flex-direction: column; - } - - .footerDeveloper { - display: flex; - flex-direction: column; - } - - .footerCommunity { - display: grid; - grid-template-columns: repeat(3, max-content); - gap: 20px 32px; - - @media (max-width: ${variables.mobileBreakPoint}) { - grid-template-columns: repeat(4, max-content); - gap: 32px; - } - } - - .footerTitle { - font-size: 28px; - font-weight: bold; - color: #fff; - - @media (max-width: ${variables.mobileBreakPoint}) { - font-size: 26px; - } - } -` - -export const FooterItemPanel = styled.a` - font-size: 18px; - color: #acacac; - height: 23px; - margin: 8px 0; -` - -export const FooterImageItemPanel = styled.a` - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - gap: 8px; - color: #acacac; - - > svg { - width: 35px; - height: 35px; - } - - > span { - font-size: 12px; - } - - &:hover { - color: ${props => props.theme.primary}; - - & > svg { - /* stylelint-disable-next-line selector-class-pattern */ - .app-icon { - fill: ${props => props.theme.primary}; - } - } - } -` diff --git a/src/components/GraphChannelList/index.tsx b/src/components/GraphChannelList/index.tsx index a32a02b11..be5fd1b26 100644 --- a/src/components/GraphChannelList/index.tsx +++ b/src/components/GraphChannelList/index.tsx @@ -35,6 +35,7 @@ const GraphChannelList: FC<{ list: Fiber.Graph.Channel[]; node?: string }> = ({ : null const outpoint = `${outPoint.txHash}#${outPoint.index}` + // console.log(channel) return (
@@ -84,9 +85,9 @@ const GraphChannelList: FC<{ list: Fiber.Graph.Channel[]; node?: string }> = ({
Position
On - - - {localeNumberString(channel.fundingTxBlockNumber)} + + + {localeNumberString(channel.openTransactionInfo.blockNumber)}
@@ -117,7 +118,7 @@ const GraphChannelList: FC<{ list: Fiber.Graph.Channel[]; node?: string }> = ({
Fee Rate
-
{`${localeNumberString(channel.node1ToNode2FeeRate)} shannon/kB`}
+
{`${localeNumberString(channel.feeRateOfNode1)} shannon/kB`}
@@ -141,7 +142,7 @@ const GraphChannelList: FC<{ list: Fiber.Graph.Channel[]; node?: string }> = ({
Fee Rate
-
{`${localeNumberString(channel.node2ToNode1FeeRate)} shannon/kB`}
+
{`${localeNumberString(channel.feeRateOfNode2)} shannon/kB`}
diff --git a/src/components/Search/AggregateSearchResults.tsx b/src/components/Search/AggregateSearchResults.tsx index 6f0d451ff..c78903b98 100644 --- a/src/components/Search/AggregateSearchResults.tsx +++ b/src/components/Search/AggregateSearchResults.tsx @@ -267,7 +267,7 @@ const SearchResultItem: FC<{ keyword?: string; item: AggregateSearchResult }> =
- {t('search.fiber_graph_node')} # {localeNumberString(item.attributes.alias)} + {t('search.fiber_graph_node')} # {localeNumberString(item.attributes.nodeName)}
diff --git a/src/locales/en.json b/src/locales/en.json index ea5b1c8bf..d51373b5d 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -402,7 +402,11 @@ "youtube": "YouTube", "forum": "Forum", "tools": "Tools", - "api-doc": "API Documentation" + "api-doc": "API Documentation", + "knowledge_base": "Knowledge Base", + "network": "Network", + "community_nodes": "Community Nodes", + "media_kit": "Media Kit" }, "search": { "loading": "Loading...", @@ -1060,6 +1064,7 @@ "token-info": "Token Info", "xudt-data": "XUDT Data", "btc-time-lock": "BTC Time Lock", + "spore-cluster": "Spore Cluster", "spore": "Spore", "json": "JSON", "select-x-from-y": "Selected {{x}} chars from {{y}}", diff --git a/src/locales/zh.json b/src/locales/zh.json index 98763e2d1..09c7ab2ca 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -391,7 +391,11 @@ "youtube": "YouTube", "forum": "Forum", "tools": "工具箱", - "api-doc": "API 文档" + "api-doc": "API 文档", + "knowledge_base": "知识库", + "network": "网络", + "community_nodes": "社区节点", + "media_kit": "媒体包" }, "search": { "loading": "加载中...", @@ -1035,6 +1039,7 @@ "token-info": "Token Info", "xudt-data": "XUDT Data", "btc-time-lock": "BTC Time Lock", + "spore-cluster": "Spore Cluster", "spore": "Spore", "json": "JSON", "select-x-from-y": "从 {{y}} 字符起选择了 {{x}} 个字符", diff --git a/src/pages/Fiber/GraphNode/index.tsx b/src/pages/Fiber/GraphNode/index.tsx index cc3ff4390..85d625406 100644 --- a/src/pages/Fiber/GraphNode/index.tsx +++ b/src/pages/Fiber/GraphNode/index.tsx @@ -15,7 +15,6 @@ import GraphChannelList from '../../../components/GraphChannelList' import { getFundingThreshold } from '../utils' import { shannonToCkb } from '../../../utils/util' import { parseNumericAbbr } from '../../../utils/chart' -import { ChainHash } from '../../../constants/fiberChainHash' import { Link } from '../../../components/Link' import { localeNumberString } from '../../../utils/number' @@ -97,7 +96,6 @@ const GraphNode = () => { isOpen: true, isUdt, hash: c.openTransactionInfo.txHash, - index: c.fundingTxIndex, block: { number: c.openTransactionInfo.blockNumber, timestamp: c.openTransactionInfo.blockTimestamp, @@ -145,7 +143,7 @@ const GraphNode = () => { if (!node) { return
Fiber Peer Not Found
} - const channels = node.fiberGraphChannels.filter(c => !c.closedTransactionInfo) + const channels = node.fiberGraphChannels.filter(c => !c.closedTransactionInfo.txHash) const thresholds = getFundingThreshold(node) @@ -161,19 +159,17 @@ const GraphNode = () => { navigator?.clipboard.writeText(copyText).then(() => setToast({ message: t('common.copied') })) } - const chain = ChainHash.get(node.chainHash) ?? '-' - return (
- {node.alias ? ( + {node.nodeName ? (
-
{t('fiber.graph.alias')}
+
{t('fiber.graph.node')}
- {node.alias} -
@@ -214,12 +210,6 @@ const GraphNode = () => {
{t('fiber.graph.node.first_seen')}
{dayjs(+node.timestamp).format(TIME_TEMPLATE)}
-
-
{t('fiber.graph.node.chain')}
-
- {chain} -
-
{t('fiber.graph.node.total_capacity')}
{totalCkb}
@@ -314,18 +304,20 @@ const GraphNode = () => { ({acc1.amount})
-
- And - - - -
{acc2.address.slice(0, -8)}
-
{acc2.address.slice(-8)}
- -
-
- ({acc2.amount}) -
+ {acc2 ? ( +
+ And + + + +
{acc2.address.slice(0, -8)}
+
{acc2.address.slice(-8)}
+ +
+
+ ({acc2.amount}) +
+ ) : null}
) })} diff --git a/src/pages/Fiber/GraphNodeList/index.module.scss b/src/pages/Fiber/GraphNodeList/index.module.scss index 4356a5086..9a95bd0bd 100644 --- a/src/pages/Fiber/GraphNodeList/index.module.scss +++ b/src/pages/Fiber/GraphNodeList/index.module.scss @@ -106,25 +106,27 @@ margin: 24px 20px; } - @media screen and (width < 1330px) { + @media screen and (width < 1400px) { font-size: 14px; table { - th, - td { - &:nth-child(6) { - display: none; + tr:not([data-role='pagination']) { + th, + td { + &:nth-child(5) { + display: none; + } } } } } - @media screen and (width < 810px) { + @media screen and (width < 1240px) { table { tr:not([data-role='pagination']) { th, td { - &:last-child { + &:nth-child(1) { display: none; } } @@ -132,34 +134,57 @@ } } - @media screen and (width < 900px) { + @media screen and (width < 1024px) { table { - th, - td { - &:nth-child(5) { - display: none; + tr:not([data-role='pagination']) { + th, + td { + &:nth-child(2) { + display: none; + } } } } } - @media screen and (width < 700px) { + @media screen and (width < 840px) { table { - th, - td { - &:nth-child(3) { - display: none; + tr:not([data-role='pagination']) { + th, + td { + &:last-child { + display: none; + } } } } } - @media screen and (width < 520px) { + @media screen and (width < 545px) { table { - th, - td { - &:first-child { - display: none; + tr:not([data-role='pagination']) { + th, + td { + &:nth-child(6) { + display: none; + } + + &:nth-child(1) { + display: table-cell; + } + } + } + } + } + + @media screen and (width < 450px) { + table { + tr:not([data-role='pagination']) { + th, + td { + &:nth-child(3) { + display: none; + } } } } diff --git a/src/pages/Fiber/GraphNodeList/index.tsx b/src/pages/Fiber/GraphNodeList/index.tsx index 0dcc283d4..92bf16295 100644 --- a/src/pages/Fiber/GraphNodeList/index.tsx +++ b/src/pages/Fiber/GraphNodeList/index.tsx @@ -2,7 +2,7 @@ import { useQuery } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import { Link } from 'react-router-dom' import { Tooltip } from 'antd' -import { CopyIcon, InfoCircledIcon } from '@radix-ui/react-icons' +import { CopyIcon, InfoCircledIcon, LinkBreak2Icon } from '@radix-ui/react-icons' import dayjs from 'dayjs' import Content from '../../../components/Content' import { useSetToast } from '../../../components/Toast' @@ -16,14 +16,13 @@ import styles from './index.module.scss' import { shannonToCkb } from '../../../utils/util' import { parseNumericAbbr } from '../../../utils/chart' import { localeNumberString } from '../../../utils/number' -import { ChainHash } from '../../../constants/fiberChainHash' const TIME_TEMPLATE = 'YYYY/MM/DD hh:mm:ss' const fields = [ { - key: 'alias', - label: 'alias', + key: 'nodeName', + label: 'name', transformer: (v: unknown, i: Fiber.Graph.Node) => { if (typeof v !== 'string') return v return ( @@ -108,29 +107,21 @@ const fields = [ ) }, }, - { - key: 'chainHash', - label: 'chain', - transformer: (v: unknown) => { - if (typeof v !== 'string') return v - const chain = ChainHash.get(v) ?? '-' - return ( - - {chain} - - - ) - }, - }, { key: 'addresses', label: 'addresses', transformer: (v: unknown) => { if (!Array.isArray(v)) return v const addr = v[0] - if (!addr || typeof addr !== 'string') return v + if (!addr || typeof addr !== 'string') { + return ( + + + + + + ) + } return ( diff --git a/src/services/ExplorerService/fetcher.ts b/src/services/ExplorerService/fetcher.ts index 34f98ee04..668bf1fc4 100644 --- a/src/services/ExplorerService/fetcher.ts +++ b/src/services/ExplorerService/fetcher.ts @@ -116,7 +116,7 @@ export type AggregateSearchResult = > | Response.Wrapper< { - alias: string + nodeName: string nodeId: string peerId: string }, @@ -1588,7 +1588,7 @@ export namespace Fiber { } export interface Node { - alias: string + nodeName: string nodeId: string addresses: string[] timestamp: string @@ -1601,18 +1601,18 @@ export namespace Fiber { } export interface Channel { + capacity: string + chainHash: string channelOutpoint: string + closedTransactionInfo: ClosedTransactionInfo + createdTimestamp: string + feeRateOfNode1: string + feeRateOfNode2: string + lastUpdatedTimestampOfNode1: string + lastUpdatedTimestampOfNode2: string node1: string node2: string - chainHash: string - fundingTxBlockNumber: string - fundingTxIndex: string // number - lastUpdatedTimestamp: string - node1ToNode2FeeRate: string - node2ToNode1FeeRate: string - capacity: string openTransactionInfo: OpenTransactionInfo - closedTransactionInfo: ClosedTransactionInfo } export interface NodeDetail extends Node { diff --git a/src/utils/spore.ts b/src/utils/spore.ts index b789e4501..8e5cc0d4a 100644 --- a/src/utils/spore.ts +++ b/src/utils/spore.ts @@ -11,7 +11,21 @@ export function parseSporeClusterData(hexData: string) { const name = hexToUtf8(`0x${data.slice(nameOffset + 8, descriptionOffset)}`) const description = hexToUtf8(`0x${data.slice(descriptionOffset + 8)}`) - + try { + const parsed = JSON.parse(description) + if (typeof parsed === 'object') { + const v: Record = { name } + Object.keys(parsed).forEach(key => { + if (key === 'name') { + throw new Error('name key is reserved') + } + v[key] = JSON.stringify(parsed[key], null, 2) + }) + return v + } + } catch { + // ignore + } return { name, description } }