diff --git a/src/assets/block-rewards-cn.png b/src/assets/block-rewards-cn.png new file mode 100644 index 000000000..87d0ded7f Binary files /dev/null and b/src/assets/block-rewards-cn.png differ diff --git a/src/assets/block-rewards.png b/src/assets/block-rewards.png new file mode 100644 index 000000000..aee406a38 Binary files /dev/null and b/src/assets/block-rewards.png differ diff --git a/src/assets/calendar.svg b/src/assets/calendar.svg new file mode 100644 index 000000000..1332af123 --- /dev/null +++ b/src/assets/calendar.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/assets/ckb_base_issuance_trend.png b/src/assets/ckb_base_issuance_trend.png new file mode 100644 index 000000000..45362c374 Binary files /dev/null and b/src/assets/ckb_base_issuance_trend.png differ diff --git a/src/assets/fonts/digital-7.ttf b/src/assets/fonts/digital-7.ttf new file mode 100644 index 000000000..5dbe6f908 Binary files /dev/null and b/src/assets/fonts/digital-7.ttf differ diff --git a/src/assets/fonts/fonts.css b/src/assets/fonts/fonts.css index 64582cb8f..c64fff8be 100644 --- a/src/assets/fonts/fonts.css +++ b/src/assets/fonts/fonts.css @@ -4,3 +4,8 @@ font-weight: 300; font-style: normal; } + +@font-face { + font-family:'digital-clock-font'; + src: url('./digital-7.ttf'); +} diff --git a/src/assets/halving_banner.png b/src/assets/halving_banner.png new file mode 100644 index 000000000..dabba5f9e Binary files /dev/null and b/src/assets/halving_banner.png differ diff --git a/src/assets/halving_banner_success.png b/src/assets/halving_banner_success.png new file mode 100644 index 000000000..91ccb0edc Binary files /dev/null and b/src/assets/halving_banner_success.png differ diff --git a/src/assets/halving_bg.png b/src/assets/halving_bg.png new file mode 100644 index 000000000..4736a30a8 Binary files /dev/null and b/src/assets/halving_bg.png differ diff --git a/src/assets/halving_success_bg.png b/src/assets/halving_success_bg.png new file mode 100644 index 000000000..258afb464 Binary files /dev/null and b/src/assets/halving_success_bg.png differ diff --git a/src/assets/twitter.svg b/src/assets/twitter.svg new file mode 100644 index 000000000..5bf712052 --- /dev/null +++ b/src/assets/twitter.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/warning_circle.svg b/src/assets/warning_circle.svg new file mode 100644 index 000000000..23281be15 --- /dev/null +++ b/src/assets/warning_circle.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/components/BannerFallback/HalvingBanner.tsx b/src/components/BannerFallback/HalvingBanner.tsx new file mode 100644 index 000000000..b0eec3d7e --- /dev/null +++ b/src/components/BannerFallback/HalvingBanner.tsx @@ -0,0 +1,86 @@ +import classnames from 'classnames' +import styles from './index.module.scss' +import halvingBanner from '../../assets/halving_banner.png' +import halvingBannerSuccess from '../../assets/halving_banner_success.png' +import SimpleButton from '../SimpleButton' +import { useCountdown, useHalving } from '../../utils/hook' +import i18n from '../../utils/i18n' +import { capitalizeFirstLetter } from '../../utils/string' +import { fetchCachedData } from '../../utils/cache' + +function numberToOrdinal(number: number) { + switch (number) { + case 1: + return 'first' + case 2: + return 'second' + default: + break + } + + switch (number % 10) { + case 1: + return `${number}st` + case 2: + return `${number}nd` + case 3: + return `${number}rd` + default: + return `${number}th` + } +} + +export const HalvingBanner = () => { + const { estimatedDate, nextHalvingCount } = useHalving() + const [days, hours, minutes, seconds] = useCountdown(estimatedDate) + const lastedHavingKey = `lasted-having-${nextHalvingCount - 1}` + const unreadLastedHaving = nextHalvingCount > 1 && fetchCachedData(lastedHavingKey) === null + + const shortCountdown = () => { + if (days > 0) { + return `${days}${i18n.t('symbol.char_space')}${capitalizeFirstLetter(i18n.t('unit.days'))}` + } + if (hours > 0) { + return `${hours}${i18n.t('symbol.char_space')}${capitalizeFirstLetter(i18n.t('unit.hours'))}` + } + if (minutes > 0) { + return `${minutes}${i18n.t('symbol.char_space')}${capitalizeFirstLetter(i18n.t('unit.minutes'))}` + } + if (seconds > 0) { + return `${seconds}${i18n.t('symbol.char_space')}${capitalizeFirstLetter(i18n.t('unit.seconds'))}` + } + return `${capitalizeFirstLetter(i18n.t('halving.halving'))}!` + } + + return ( +
+
+
+ {unreadLastedHaving ? ( +
+ {i18n.t('halving.banner_congratulation', { + times: i18n.t(`ordinal.${numberToOrdinal(nextHalvingCount - 1)}`), + })} +
+ ) : ( +
+ Nervos CKB Layer 1 {capitalizeFirstLetter(i18n.t('halving.halving'))} +
+ )} + + + {unreadLastedHaving + ? i18n.t('halving.learn_more') + : `${i18n.t('halving.halving_countdown')} ${shortCountdown()}`} + + +
+
+
+ ) +} diff --git a/src/components/BannerFallback/index.module.scss b/src/components/BannerFallback/index.module.scss index e98dc92b9..810088035 100644 --- a/src/components/BannerFallback/index.module.scss +++ b/src/components/BannerFallback/index.module.scss @@ -1,6 +1,6 @@ $backgroudColor: #232323; -.Root { +.root { width: 100%; background-color: $backgroudColor; height: 200px; @@ -13,3 +13,64 @@ $backgroudColor: #232323; background-image: url('../../assets/ckb_explorer_banner_phone.svg'); } } + +.halvingBannerSuccess { + @media (max-width: 750px) { + background-size: 200% !important; + } + background-position: center !important; +} + +.halvingBannerWrapper { + width: 100%; + background-color: $backgroudColor; + height: 200px; + position: relative; + background-repeat: no-repeat; + background-position: bottom; + background-size: cover; +} + +.halvingBannerShadow { + width: 100%; + background-color: rgba(0, 0, 0, 0.3); + height: 100%; +} + +.halvingBanner { + height: 100%; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + gap: 24px; +} + +.halvingBannerText { + align-items: baseline; + display: flex; + font-size: 24px; + font-weight: bold; + + background: linear-gradient(45deg, rgb(183, 129, 219), rgba(133, 69, 178, 1), rgb(183, 129, 219)); + background-clip: text; + -webkit-text-fill-color: transparent; + + @media (min-width: 750px) { + font-size: 40px; + } +} + +.halvingBannerCount { + font-size: 36px; + margin-left: 24px; +} + +.learnMoreButton { + border: 0; + border-radius: 8px; + color: white; + padding: 8px 16px; + user-select: none; + background: linear-gradient(180deg, rgba(172, 54, 244, 1), rgba(138, 25, 207, 1)); +} diff --git a/src/components/BannerFallback/index.tsx b/src/components/BannerFallback/index.tsx index f4272ebf7..49017fcaf 100644 --- a/src/components/BannerFallback/index.tsx +++ b/src/components/BannerFallback/index.tsx @@ -1,3 +1,3 @@ import styles from './index.module.scss' -export default () =>
+export default () =>
diff --git a/src/locales/en.json b/src/locales/en.json index 763bbb8db..13ade151d 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -19,6 +19,82 @@ "minaraTime": "Est May 10th 2022", "miranaAlive": "MIRANA IS LIVE" }, + "unit": { + "days": "days", + "hours": "hours", + "minutes": "minutes", + "seconds": "seconds" + }, + "symbol": { + "char_space": " " + }, + "ordinal": { + "first": "first", + "second": "second", + "1st": "1st", + "2nd": "2nd", + "3rd": "3rd", + "4th": "4th", + "5th": "5th", + "6th": "6th", + "7th": "7th", + "8th": "8th", + "9th": "9th", + "10th": "10th" + }, + "halving": { + "congratulations": "Congratulations", + "the": "the", + "actived": "is atived on block", + "halving": "halving", + "next": "next", + "and": "and", + "halving_countdown": "Halving Countdown", + "learn_more": "Learn More", + "banner_congratulation": "The {{times}} CKB halving is atived.", + "countdown_tooltip_section1": "How is the countdown estimated?", + "countdown_tooltip_section2": "[Target Epoch - Current Epoch]*Single Epoch Average Time - Current Epoch Used Time", + "countdown_tooltip_section3": "*Single Epoch Average Time takes the average time of nearly 1000 epochs.", + "halving_desc_prefix": "will have a halving event every 4 years according to the mechanism, at which time the", + "base_issuance_rewards": "base issuance rewards", + "halving_desc_suffix": "will be halved.", + "current_block": "Current Block", + "current_epoch": "Current Epoch", + "target_epoch": "Target Epoch", + "estimated_time": "Estimated Time", + "share_tooltip": "Share CKB Halving Event to Twitter", + "halving_event": "What is a Halving Event?", + "halving_event_section_1": "In the Nervos ecosystem, mining is used to secure the network and distribute tokens in the form of block rewards. A total of 33.6 billion CKB tokens will be created through primary issuance over a period of approximately 84 years to incentivize the miners that secure the network.", + "halving_event_section_2": "Every epoch, a period of approximately four hours, a fixed amount of 1,917,808 CKB is introduced. Every 8,760 epochs, a period of approximately four years, this amount is cut in half. This event is called a halving and it is the point where the mining rewards from primary issuance are permanently reduced by 50%. This halving process will continue every four years until the year 2103, after which point all block rewards from primary issuance will cease completely.", + "significance": "What is the Significance of a Halving?", + "significance_section_1": "Each time a halving occurs, it causes a sharp decrease in the rewards generated per block. The supply of new CKB entering circulation is lowered, dramatically reducing the rate of inflation. This is important because it creates a shift in the underlying market equilibrium and forces a reevaluation of what is considered fair market value.", + "significance_section_2": "Halving events occur on a predetermined issuance schedule that cannot be changed, postponed, or delayed. Investors and community members often look forward to a halving event as something to celebrate since it marks an important milestone in the history of the project.", + "how_does_work": "How does the CKByte-Halving work?", + "how_does_work_section_1": "In order to make the halving plan work as expected, a concept of time called epoch was introduced.", + "how_does_work_section_2": "An epoch is a period of time for a set of blocks.", + "how_does_work_section_3": "In Nervos, the PoW difficulty changes on a new epoch. All the blocks in the same epoch share the same difficulty target. The difficulty adjustment algorithm aims to stabilize the orphan block rate at 2.5% and the epoch duration at 4 hours.", + "how_does_work_section_4": "Epochs per halving is", + "how_does_work_section_5": "and the Nth halving of CKBytes firstly occurs on epoch", + "how_does_work_section_6": "So, The CKByte halving event occurs on the specified epoch, e.g. 8760, 17520.", + "when": "When will CKByte be halved?", + "when_section_1": "The following table details the schedule for several upcoming CKB halvings and their corresponding base issuance rewards:", + "when_section_2": "Note that CKB block rewards include ", + "when_section_3": "Base (issuance) reward, Secondary (issuance) reward, Commit reward", + "when_section_4": "Proposal reward", + "when_section_5": "However, when we are discussing CKB halving, it only relates to ", + "when_section_6": ". Therefore, the block rewards listed in the table only include the portion of rewards from base issuance to help with understanding.", + "table_event": "Event", + "table_date": "Date", + "table_epoch_number": "Epoch number", + "table_epoch_reward": "Epoch reward", + "table_block_reward": "Block reward(Calculated based on 1800 blocks per epoch)", + "table_daily_reward": "Daily reward", + "table_total": "Total new CKB between events", + "table_launches": "Nervos launches", + "genesis_epoch": "genesis epoch", + "expected": "Expected", + "twitter_text": "📢 @NervosNetwork CKB Expected {{times}} halving on {{date}}🚀,\n\n📅There are {{countdown}} left in the countdown to the halving.\n\n🔗 Click 👉 https://explorer.nervos.org know more" + }, "glossary": { "block_height": "Also known as Block Number. The block height, which indicates the length of the blockchain, increases after the addition of the new block.", "cell": "CKB adopts cell model, which is more generic than UTXO model. A transaction destroys some outputs created in previous transactions and creates some new outputs. Every transaction's output is a cell.", diff --git a/src/locales/zh.json b/src/locales/zh.json index 02e6d8622..89abf7db5 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -19,6 +19,82 @@ "minaraTime": "美国东部时间: 2022 年 05 月 10 日", "miranaAlive": "MIRANA 已经激活" }, + "unit": { + "days": "天", + "hours": "小时", + "minutes": "分钟", + "seconds": "秒" + }, + "symbol": { + "char_space": "" + }, + "ordinal": { + "first": "首次", + "second": "第 2 次", + "1st": "首次", + "2nd": "第 2 次", + "3rd": "第 3 次", + "4th": "第 4 次", + "5th": "第 5 次", + "6th": "第 6 次", + "7th": "第 7 次", + "8th": "第 8 次", + "9th": "第 9 次", + "10th": "第 10 次" + }, + "halving": { + "congratulations": "恭喜", + "the": "", + "actived": "生效于高度", + "next": "下一次", + "halving": "减半", + "and": "和", + "halving_countdown": "减半倒计时", + "learn_more": "了解更多", + "banner_congratulation": "CKB {{times}}减半生效.", + "countdown_tooltip_section1": "倒计时是如何估算的?", + "countdown_tooltip_section2": "[目标Epoch- 当前Epoch]*单个Epoch平均用时 - 当前Epoch已用时间", + "countdown_tooltip_section3": "*单个Epoch平均用时取近1000个Epoch的平均用时.", + "halving_desc_prefix": "根据机制将每4年发生一次减半事件,届时", + "base_issuance_rewards": "基础发行奖励", + "halving_desc_suffix": "将减半。", + "current_block": "当前区块", + "current_epoch": "当前 Epoch", + "target_epoch": "目标 Epoch", + "estimated_time": "预计时间", + "share_tooltip": "分享CKB减半事件到Twitter", + "halving_event": "什么是减半事件?", + "halving_event_section_1": "在 Nervos 生态系统中,挖矿用于确保网络安全,并以区块奖励的形式分配代币。在大约 84 年的时间里,将通过首次发行创造总计 336 亿个 CKB 代币,以奖励确保网络安全的矿工。", + "halving_event_section_2": "每个Epoch(约 4 个小时)推出 1,917,808 CKB 的固定金额。每 8,760 个Epoch,即大约四年的时间,这一数额会减半。这一事件被称为减半,也就是初级发行的挖矿奖励永久减少 50%。这种减半过程将每四年持续一次,直到 2103 年。", + "significance": "减半的意义是什么?", + "significance_section_1": "每次减半都会导致每个区块产生的奖励急剧下降。进入流通的新 CKB 供应量减少,通货膨胀率大幅下降。这一点非常重要,因为它改变了潜在的市场平衡,迫使人们重新评估什么是公平的市场价值。", + "significance_section_2": "减半活动按照预定的发行时间表进行,不能更改、推迟或延迟。投资者和社区成员通常期待着减半事件的发生,因为这标志着项目历史上的一个重要里程碑,值得庆祝。", + "how_does_work": "CKB减半是如何运行的?", + "how_does_work_section_1": "为了使减半计划按预期运行,我们引入了一个名为 \"Epoch\"的时间概念。", + "how_does_work_section_2": "一个Epoch是一组区块的一段时间。", + "how_does_work_section_3": "在 Nervos 中,PoW 难度在新的Epoch中发生变化。同一Epoch中的所有区块共享相同的难度目标。难度调整算法旨在将孤块率稳定在 2.5%,将Epoch稳定在 4 小时。", + "how_does_work_section_4": "每次减半的Epoch为", + "how_does_work_section_5": "CKBytes 的第 N 次减半首先发生在Epoch上", + "how_does_work_section_6": "因此,CKByte 减半事件发生在指定的Epoch,例如 8760、17520。", + "when": "CKB何时减半?", + "when_section_1": "下表详细列出了即将进行的 10 次 CKB 半价及其相应的基本发行奖励的时间表:", + "when_section_2": "注意,CKB 块奖励包括", + "when_section_3": "基础(发行)奖励、二级(发行)奖励、提交奖励", + "when_section_4": "提案奖励", + "when_section_5": "但是,当我们讨论 CKB 减半时,它只与", + "when_section_6": "有关。因此,表中列出的区块奖励只包括基础发行奖励的部分,以帮助理解。", + "table_event": "事件", + "table_date": "日期", + "table_epoch_number": "Epoch序号", + "table_epoch_reward": "每个Epoch挖矿产出", + "table_block_reward": "区块奖励(按每epoch有1800个区块计算)", + "table_daily_reward": "每日挖矿产出", + "table_total": "期间累计挖矿产出", + "table_launches": "Nervos 主网上线", + "genesis_epoch": "创世纪元", + "expected": "预计", + "twitter_text": "📢 @NervosNetwork CKB 预计于 {{date}} 迎来{{times}}减半🚀,\n\n📅距离减半倒计时还有 {{countdown}}。\n\n🔗 访问👉 https://explorer.nervos.org 了解更多" + }, "glossary": { "block_height": "区块高度,又称为区块编号,表示区块链的长度,在添加新区块后会增加。", "cell": "CKB 采用了更通用的 Cell 模型,而不是 UTXO 模型。交易会销毁先前交易中创建的某些输出,并创建一些新的输出。每个交易的输出都是一个 Cell。", diff --git a/src/pages/Halving/HalvingCountdown.tsx b/src/pages/Halving/HalvingCountdown.tsx new file mode 100644 index 000000000..febc508f1 --- /dev/null +++ b/src/pages/Halving/HalvingCountdown.tsx @@ -0,0 +1,40 @@ +import i18n from '../../utils/i18n' +import { useCountdown, useHalving } from '../../utils/hook' +import styles from './index.module.scss' + +export const HalvingCountdown = () => { + const { estimatedDate } = useHalving() + const [days, hours, minutes, seconds] = useCountdown(estimatedDate) + + return ( +
+
+
+ {days} +
+
{i18n.t('common.days')}
+
+
+
+
+ {hours.toString().padStart(2, '0')} +
+
{i18n.t('common.hours')}
+
+
+
+
+ {minutes.toString().padStart(2, '0')} +
+
{i18n.t('common.minutes')}
+
+
+
+
+ {seconds.toString().padStart(2, '0')} +
+
{i18n.t('common.seconds')}
+
+
+ ) +} diff --git a/src/pages/Halving/HalvingInfo.tsx b/src/pages/Halving/HalvingInfo.tsx new file mode 100644 index 000000000..1078b08f1 --- /dev/null +++ b/src/pages/Halving/HalvingInfo.tsx @@ -0,0 +1,92 @@ +import { Tooltip } from 'antd' +import BigNumber from 'bignumber.js' +import moment from 'moment' +import { ReactComponent as WarningCircle } from '../../assets/warning_circle.svg' +import i18n from '../../utils/i18n' +import { useAppState } from '../../contexts/providers' +import { useHalving, useIsMobile } from '../../utils/hook' +import styles from './index.module.scss' + +export const HalvingInfo = () => { + const { statistics } = useAppState() + const isMobile = useIsMobile() + const { currentEpoch, targetEpoch, estimatedDate } = useHalving() + const utcOffset = moment().utcOffset() / 60 + + if (isMobile) { + return ( + <> +
+
+ {new BigNumber(statistics.tipBlockNumber).toFormat()} +
{i18n.t('halving.current_block')}
+
+
+
+ + {new BigNumber(currentEpoch).toFormat()} + + {statistics.epochInfo.index} / {statistics.epochInfo.epochLength} + + +
{i18n.t('halving.current_epoch')}
+
+
+ +
+
+ {new BigNumber(targetEpoch).toFormat()} +
{i18n.t('halving.target_epoch')}
+
+ +
+ +
+
+ 0 ? `+ ${utcOffset}` : utcOffset}`}> + {moment(estimatedDate).format('YYYY.MM.DD hh:mm:ss')} + +
+
{i18n.t('halving.estimated_time')}
+
+
+ + ) + } + + return ( +
+
+ {new BigNumber(statistics.tipBlockNumber).toFormat()} +
{i18n.t('halving.current_block')}
+
+
+
+ + {new BigNumber(currentEpoch).toFormat()} + + {statistics.epochInfo.index} / {statistics.epochInfo.epochLength} + + +
{i18n.t('halving.current_epoch')}
+
+
+ +
+ {new BigNumber(targetEpoch).toFormat()} +
{i18n.t('halving.target_epoch')}
+
+
+ +
+ + {moment(estimatedDate).format('YYYY.MM.DD hh:mm:ss')} + 0 ? `+ ${utcOffset}` : utcOffset}`}> + + + +
{i18n.t('halving.estimated_time')}
+
+
+ ) +} diff --git a/src/pages/Halving/HalvingTable.tsx b/src/pages/Halving/HalvingTable.tsx new file mode 100644 index 000000000..fcbd6207d --- /dev/null +++ b/src/pages/Halving/HalvingTable.tsx @@ -0,0 +1,172 @@ +import i18n from '../../utils/i18n' +import { capitalizeFirstLetter } from '../../utils/string' +import styles from './index.module.scss' + +export const HalvingTable = () => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{i18n.t('halving.table_event')}{i18n.t('halving.table_date')}{i18n.t('halving.table_epoch_number')}{i18n.t('halving.table_epoch_reward')}{i18n.t('halving.table_block_reward')}{i18n.t('halving.table_daily_reward')}{i18n.t('halving.table_total')}
{i18n.t('halving.table_launches')}16 Nov 20190 ({i18n.t('halving.genesis_epoch')})1,917,808 CKB1,065 CKB11,506,849 CKB16,800,000,000 CKB
+ + {capitalizeFirstLetter(i18n.t('ordinal.first'))} + {i18n.t('symbol.char_space')} + {i18n.t('halving.halving')} + + + {i18n.t('halving.expected')} 19 Nov 2023 + + 8,760 + + 958,904 CKB + + 533 CKB + + 5,753,424 CKB + + 8,400,000,000 CKB +
+ {capitalizeFirstLetter(i18n.t('ordinal.second'))} + {i18n.t('symbol.char_space')} + {i18n.t('halving.halving')} + {i18n.t('halving.expected')} November 202717,520479,452 CKB266 CKB2,876,712 CKB4,200,000,000 CKB
+ {capitalizeFirstLetter(i18n.t('ordinal.3rd'))} + {i18n.t('symbol.char_space')} + {i18n.t('halving.halving')} + {i18n.t('halving.expected')} November 203126,280239,726 CKB133 CKB1,438,356 CKB2,100,000,000 CKB
+ {capitalizeFirstLetter(i18n.t('ordinal.4th'))} + {i18n.t('symbol.char_space')} + {i18n.t('halving.halving')} + {i18n.t('halving.expected')} November 203535,040119,863 CKB67 CKB719,178 CKB1,050,000,000 CKB
+ {capitalizeFirstLetter(i18n.t('ordinal.5th'))} + {i18n.t('symbol.char_space')} + {i18n.t('halving.halving')} + {i18n.t('halving.expected')} November 203943,80059,932 CKB33.5 CKB359,589 CKB525,000,000 CKB
+ {capitalizeFirstLetter(i18n.t('ordinal.6th'))} + {i18n.t('symbol.char_space')} + {i18n.t('halving.halving')} + {i18n.t('halving.expected')} November 204352,56029,966 CKB16.75 CKB179,794.5 CKB262,500,000 CKB
+ {capitalizeFirstLetter(i18n.t('ordinal.7th'))} + {i18n.t('symbol.char_space')} + {i18n.t('halving.halving')} + {i18n.t('halving.expected')} November 204761,32014,983 CKB8.375 CKB89,897.25 CKB131,250,000 CKB
+ {capitalizeFirstLetter(i18n.t('ordinal.8th'))} + {i18n.t('symbol.char_space')} + {i18n.t('halving.halving')} + {i18n.t('halving.expected')} November 205170,0807,491 CKB4.1875 CKB44,948.625 CKB65,625,000 CKB
+ {capitalizeFirstLetter(i18n.t('ordinal.9th'))} + {i18n.t('symbol.char_space')} + {i18n.t('halving.halving')} + {i18n.t('halving.expected')} November 205578,8403,746 CKB2.09375 CKB22,474.3125 CKB32,812,500 CKB
+ {capitalizeFirstLetter(i18n.t('ordinal.10th'))} + {i18n.t('symbol.char_space')} + {i18n.t('halving.halving')} + {i18n.t('halving.expected')} November 205987,6001,873 CKB1.046875 CKB11,237.15625 CKB16,406,250 CKB
+) diff --git a/src/pages/Halving/index.module.scss b/src/pages/Halving/index.module.scss new file mode 100644 index 000000000..a8f6a8428 --- /dev/null +++ b/src/pages/Halving/index.module.scss @@ -0,0 +1,350 @@ +.twitterShareWrapper { + position: fixed; + bottom: 10px; + right: 18px; + + @media (min-width: 750px) { + right: 60px; + } +} + +.halvingCountdown { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + justify-content: space-between; + flex-direction: row; + + .digtialClockNumber { + border: 1px solid rgba(57, 21, 86, 1); + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + width: 64px; + height: 72px; + background: linear-gradient(rgba(49, 24, 65, 1), rgba(18, 18, 18, 1), rgba(46, 30, 54, 1)); + position: relative; + margin-bottom: 4px; + &::after { + content: ' '; + width: 100%; + height: 1px; + background-color: rgba(69, 46, 87, 1); + position: absolute; + z-index: 0; + } + span { + z-index: 1; + font-family: 'digital-clock-font'; + font-size: 32px; + font-weight: 400; + padding-top: 5px; + } + } + + .digtialClockSeparate { + width: 8px; + height: 100%; + &::after { + content: ':'; + font-size: 32px; + height: 64px; + line-height: 64px; + display: flex; + justify-content: center; + } + } + + .digtialClockText { + font-size: 12px; + font-weight: 500; + color: rgba(154, 44, 236, 1); + text-align: center; + } + + @media (min-width: 750px) { + .digtialClockNumber { + width: 136px; + height: 136px; + margin-bottom: 8px; + span { + font-size: 80px; + padding-top: 10px; + } + } + + .digtialClockSeparate { + width: 16px; + &::after { + font-size: 64px; + height: 136px; + line-height: 120px; + } + } + + .digtialClockText { + font-size: 14px; + } + } +} + +.halvingProgress { + line-height: 0; + :global { + .ant-progress-inner { + background-color: transparent; + border: 1px solid rgba(154, 44, 236, 1); + } + } +} + +.halvingProgressMarks { + color: rgba(154, 44, 236, 1); + display: flex; + justify-content: space-evenly; + span { + position: relative; + &::after { + content: ' '; + width: 2px; + height: 8px; + position: absolute; + top: -4px; + left: calc(50% - 1px); + display: flex; + align-items: center; + background-color: rgba(154, 44, 236, 1); + } + } +} + +.twitterIconBg { + display: flex; + background: rgb(90, 169, 232); + box-shadow: rgba(77, 77, 77, 0.2) 0px 2px 6px 0px; + border-radius: 6px; + padding: 8px; +} + +.halvingTable { + display: block; + width: 100%; + width: max-content; + max-width: 100%; + overflow: auto; + border-spacing: 0; + border-collapse: collapse; + thead { + display: table-header-group; + vertical-align: middle; + border-color: inherit; + background-color: rgba(47, 42, 49, 1); + } + tr { + &:nth-child(1n) { + background-color: rgba(38, 33, 40, 1); + } + &:nth-child(2n) { + background-color: rgba(47, 42, 49, 1); + } + } + th { + padding: 16px 24px; + border: 1px solid rgb(34, 27, 40); + } + td { + padding: 16px 24px; + border: 1px solid rgb(34, 27, 40); + } +} + +.halvingBanner { + position: relative; + padding-top: 32px; + padding-bottom: 128px; + color: white; + background-color: rgba(32, 32, 32, 1); + background-position: bottom; + background-size: 100% auto; + background-repeat: no-repeat; + .halvingBannerWrapper { + display: flex; + justify-content: center; + } + .halvingBannerContent { + max-width: 100%; + + @media (min-width: 960px) { + max-width: 960px; + } + + flex: 1; + } + .halvingBannerShadow { + width: 100%; + height: 72px; + position: absolute; + bottom: 0; + background: linear-gradient(180deg, rgba(0, 0, 0, 0), rgb(17, 17, 17)); + } +} + +.halvingPanel { + background: rgba(21, 19, 23, 1); + background-size: cover; + border: 2px solid rgba(51, 41, 69, 0.5); + border-radius: 12px; + overflow: hidden; + padding: 24px 16px; + gap: 24px; + display: flex; + flex-direction: column; + + .halvingSuccessText { + font-size: 24px; + font-weight: 500; + } + + .halvingSuccessBtn { + font-size: 16px; + font-weight: 500; + cursor: pointer; + padding: 8px 16px; + border-radius: 4px; + border: 0; + background: linear-gradient(180deg, rgba(172, 54, 244, 1), rgba(138, 25, 207, 1)); + } + + .halvingPanelTitle { + display: flex; + align-items: center; + } + + @media (min-width: 750px) { + padding: 32px 64px; + gap: 32px; + border-radius: 24px; + + .halvingPanelTitle { + font-weight: 700; + font-size: 20px; + } + + .halvingSuccessText { + font-size: 32px; + } + } +} + +.halvingDocuments { + padding-top: 32px; + padding-bottom: 64px; + display: flex; + flex-direction: column; + gap: 24px; + + @media (min-width: 750px) { + gap: 32px; + } +} + +.halvingTitle { + text-align: center; + font-weight: 500; + font-size: 20px; + margin-bottom: 16px; + + @media (min-width: 750px) { + font-size: 32px; + margin-bottom: 32px; + } +} + +.halvingSubtitle { + text-align: center; + font-weight: 400; + margin-bottom: 24px; + + @media (min-width: 750px) { + font-size: 24px; + margin-bottom: 48px; + } +} + +.panel { + color: rgba(240, 240, 240, 1); + background: rgba(34, 27, 40, 1); + display: flex; + flex-direction: column; + border-radius: 12px; + padding: 24px 16px; + gap: 16px; + font-size: 16px; + + @media (min-width: 750px) { + border-radius: 24px; + padding: 48px; + gap: 48px; + } +} + +.panelTitle { + font-weight: bolder; + font-size: 20px; + color: white; +} + +.blockquote { + background-color: rgba(47, 42, 49, 1); + padding: 16px; + border-radius: 16px; +} + +.epochInfo { + display: flex; + align-items: center; + justify-content: space-between; + + .epochInfoItem { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + flex: 1 1 0%; + } + + .epochInfoValue { + font-weight: 500; + display: flex; + align-items: center; + } + + .separate { + height: 40px; + background: rgba(234, 234, 234, 0.2); + width: 1px; + } + + @media (min-width: 750px) { + .epochInfoItem { + gap: 16px; + flex: inherit; + } + .epochInfoValue { + font-size: 20px; + } + } +} + +.textSecondary { + color: rgba(240, 240, 240, 0.5); +} + +.textPrimary { + color: rgba(154, 44, 236, 1); +} + +.textCenter { + text-align: center; +} diff --git a/src/pages/Halving/index.tsx b/src/pages/Halving/index.tsx new file mode 100644 index 000000000..8f6719abd --- /dev/null +++ b/src/pages/Halving/index.tsx @@ -0,0 +1,297 @@ +import { Progress, Tooltip, Popover, Table } from 'antd' +import BigNumber from 'bignumber.js' +import classnames from 'classnames' +import Content from '../../components/Content' +import baseIssuance from '../../assets/ckb_base_issuance_trend.png' +import blockRewards from '../../assets/block-rewards.png' +import blockRewardsCN from '../../assets/block-rewards-cn.png' +import halvingBg from '../../assets/halving_bg.png' +import halvingSuccessBg from '../../assets/halving_success_bg.png' +import { ReactComponent as TwitterIcon } from '../../assets/twitter.svg' +import { ReactComponent as CalendarIcon } from '../../assets/calendar.svg' +import { ReactComponent as WarningCircle } from '../../assets/warning_circle.svg' +import i18n, { currentLanguage } from '../../utils/i18n' +import { HalvingTable } from './HalvingTable' +import { HalvingInfo } from './HalvingInfo' +import { useAppState } from '../../contexts/providers' +import { HalvingCountdown } from './HalvingCountdown' +import { useCountdown, useHalving, useIsMobile } from '../../utils/hook' +import { fetchCachedData, storeCachedData } from '../../utils/cache' +import { capitalizeFirstLetter } from '../../utils/string' +import { getPrimaryColor } from '../../constants/common' +import styles from './index.module.scss' + +function numberToOrdinal(number: number) { + switch (number) { + case 1: + return 'first' + case 2: + return 'second' + default: + break + } + + switch (number % 10) { + case 1: + return `${number}st` + case 2: + return `${number}nd` + case 3: + return `${number}rd` + default: + return `${number}th` + } +} + +export const HalvingCountdownPage = () => { + const isMobile = useIsMobile() + const { statistics } = useAppState() + const { + currentEpoch, + estimatedDate, + singleEpochAverageTime, + currentEpochUsedTime, + EPOCHS_PER_HALVING, + nextHalvingCount, + } = useHalving() + + const lastedHavingKey = `lasted-having-${nextHalvingCount - 1}` + const unreadLastedHaving = nextHalvingCount > 1 && fetchCachedData(lastedHavingKey) === null + const percent = + (((currentEpoch % EPOCHS_PER_HALVING) * singleEpochAverageTime - currentEpochUsedTime) / + (EPOCHS_PER_HALVING * singleEpochAverageTime)) * + 100 + const [days, hours, minutes, seconds] = useCountdown(estimatedDate) + + const shortCountdown = () => { + if (days > 0) { + return `${days}${i18n.t('symbol.char_space')}${i18n.t('unit.days')}` + } + if (hours > 0) { + return `${hours}${i18n.t('symbol.char_space')}${i18n.t('unit.hours')}` + } + if (minutes > 0) { + return `${minutes}${i18n.t('symbol.char_space')}${i18n.t('unit.minutes')}` + } + return `${seconds}${i18n.t('symbol.char_space')}${i18n.t('unit.seconds')}` + } + + const shareText = i18n.t('halving.twitter_text', { + times: i18n.t(`ordinal.${numberToOrdinal(nextHalvingCount)}`), + date: estimatedDate.toUTCString(), + countdown: shortCountdown(), + }) + const shareUrl = `https://twitter.com/share?text=${encodeURIComponent(shareText)}&hashtags=CKB%2CPoW%2CHalving` + const getTargetBlockByHavingCount = (count: number) => { + return ( + EPOCHS_PER_HALVING * + (statistics.epochInfo.epochLength ? parseInt(statistics.epochInfo.epochLength, 10) : 1800) * + count + ) + } + + const renderHalvingPanel = () => { + if (unreadLastedHaving) { + return ( +
+
+ {i18n.t('halving.congratulations')}! +
+ {capitalizeFirstLetter(i18n.t('halving.the'))} + {i18n.t('symbol.char_space')} + {capitalizeFirstLetter(i18n.t(`ordinal.${numberToOrdinal(nextHalvingCount - 1)}`))} + {i18n.t('symbol.char_space')} + {capitalizeFirstLetter(i18n.t('halving.halving'))} + {capitalizeFirstLetter(i18n.t('halving.actived'))}{' '} + + {new BigNumber(getTargetBlockByHavingCount(nextHalvingCount - 1)).toFormat()}. + +
+
+
+ +
+
+ ) + } + + return ( +
+
+ {capitalizeFirstLetter(i18n.t(`ordinal.${numberToOrdinal(nextHalvingCount)}`))} + {i18n.t('symbol.char_space')} + {capitalizeFirstLetter(i18n.t('halving.halving'))} + + {nextHalvingCount > 1 && ( + ({ + key: index, + event: `${capitalizeFirstLetter(i18n.t(`ordinal.${numberToOrdinal(index + 1)}`))} + ${i18n.t('symbol.char_space')} + ${capitalizeFirstLetter(i18n.t('halving.halving'))}`, + epoch: new BigNumber(EPOCHS_PER_HALVING * (index + 1)).toFormat(), + height: getTargetBlockByHavingCount(index + 1), + }))} + columns={[ + { title: 'Event', dataIndex: 'event', key: 'event' }, + { title: 'Epoch', dataIndex: 'epoch', key: 'epoch' }, + { + title: 'Height', + dataIndex: 'height', + key: 'height', + render: block => ( + + {new BigNumber(block).toFormat()} + + ), + }, + ]} + /> + } + title={null} + trigger="hover" + > + + + )} + + +

{i18n.t('halving.countdown_tooltip_section1')}

+

+ {i18n.t('halving.countdown_tooltip_section2')} +

+

{i18n.t('halving.countdown_tooltip_section3')}

+ + } + > + +
+
+ + + +
+ +
+ 25% + 50% + 75% +
+
+ +
+ ) + } + + return ( + +
+
+
+
Nervos CKB Layer 1 {i18n.t('halving.halving_countdown')}
+
+ Nervos CKB Layer 1 {i18n.t('halving.halving_desc_prefix')}{' '} + {i18n.t('halving.base_issuance_rewards')} {i18n.t('halving.halving_desc_suffix')} +
+ {renderHalvingPanel()} +
+
+
+
+ +
+
+
{i18n.t('halving.halving_event')}
+
{i18n.t('halving.halving_event_section_1')}
+
{i18n.t('halving.halving_event_section_2')}
+
+ +
+
{i18n.t('halving.significance')}
+
{i18n.t('halving.significance_section_1')}
+
{i18n.t('halving.significance_section_2')}
+
+ +
+
{i18n.t('halving.how_does_work')}
+
{i18n.t('halving.how_does_work_section_1')}
+
+
{i18n.t('halving.how_does_work_section_2')}
+
{i18n.t('halving.how_does_work_section_3')}
+
+
+
+ {i18n.t('halving.how_does_work_section_4')} 4 * 365 * (24 / 4) = 8760,{' '} + {i18n.t('halving.how_does_work_section_5')}: the_Nth_halving_epoch = 8760 * N . +
+
{i18n.t('halving.how_does_work_section_6')}
+
+
+ +
+
{i18n.t('halving.when')}
+
+ {i18n.t('halving.when_section_1')} + +
+ ckb base issuance trend +
+ ⚠️ {i18n.t('halving.when_section_2')} + {i18n.t('halving.when_section_3')}, {i18n.t('halving.and')}{' '} + {i18n.t('halving.when_section_4')}: +
+ block rewards +
+ {i18n.t('halving.when_section_5')} + {i18n.t('halving.base_issuance_rewards')} + {i18n.t('halving.when_section_6')} +
+
+
+ + + +
+ +
+
+
+ + ) +} + +export default HalvingCountdownPage diff --git a/src/pages/Home/Banner/index.tsx b/src/pages/Home/Banner/index.tsx index 0a8be3f72..589beba4e 100644 --- a/src/pages/Home/Banner/index.tsx +++ b/src/pages/Home/Banner/index.tsx @@ -6,6 +6,7 @@ import styles from './index.module.scss' import { useIsMobile, usePrevious } from '../../../utils/hook' import { isMainnet as isMainnetFunc } from '../../../utils/chain' import BannerFallback from '../../../components/BannerFallback' +import { HalvingBanner } from '../../../components/BannerFallback/HalvingBanner' const GPUTier = { MIN_TIER: 2, @@ -81,5 +82,5 @@ const _Banner: FC<{ latestBlock?: State.Block }> = ({ latestBlock }) => { */ const isMainnet = isMainnetFunc() export const Banner = isMainnet - ? BannerFallback + ? HalvingBanner : memo(_Banner, (a, b) => a.latestBlock?.number === b.latestBlock?.number) diff --git a/src/routes/index.tsx b/src/routes/index.tsx index e81b25a9b..7b820a6c0 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -28,6 +28,7 @@ const ErrorPage = lazy(() => import('../pages/Error')) const SearchFail = lazy(() => import('../pages/SearchFail')) const StatisticsChart = lazy(() => import('../pages/StatisticsChart')) const Tokens = lazy(() => import('../pages/Tokens')) +const Halving = lazy(() => import('../pages/Halving')) const DifficultyHashRateChart = lazy(() => import('../pages/StatisticsChart/mining/DifficultyHashRate')) const DifficultyUncleRateEpochChart = lazy(() => import('../pages/StatisticsChart/mining/DifficultyUncleRateEpoch')) const DifficultyChart = lazy(() => import('../pages/StatisticsChart/mining/Difficulty')) @@ -65,6 +66,12 @@ const Containers: CustomRouter.Route[] = [ exact: true, comp: Home, }, + { + name: 'Halving', + path: '/halving', + exact: true, + comp: Halving, + }, { name: 'BlockList', path: '/block/list', diff --git a/src/utils/hook.ts b/src/utils/hook.ts index c0f124ec6..dd5053526 100644 --- a/src/utils/hook.ts +++ b/src/utils/hook.ts @@ -12,6 +12,7 @@ import { useQuery } from 'react-query' import { useResizeDetector } from 'react-resize-detector' import { interval, share } from 'rxjs' import { AppCachedKeys } from '../constants/cache' +import { useAppState } from '../contexts/providers' import { deprecatedAddrToNewAddr } from './util' import { startEndEllipsis } from './string' import { ListPageParams, PageParams } from '../constants/common' @@ -578,6 +579,53 @@ export function useParsedDate(timestamp: number): string { return parseDate(timestamp, now) } +export const useCountdown = (targetDate: Date) => { + const countdownDate = new Date(targetDate).getTime() + + const [countdown, setCountdown] = useState(countdownDate - new Date().getTime()) + + useEffect(() => { + const interval = setInterval(() => { + setCountdown(countdownDate - new Date().getTime()) + }, 1000) + + return () => clearInterval(interval) + }, [countdownDate]) + + const days = Math.floor(countdown / (1000 * 60 * 60 * 24)) + const hours = Math.floor((countdown % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)) + const minutes = Math.floor((countdown % (1000 * 60 * 60)) / (1000 * 60)) + const seconds = Math.floor((countdown % (1000 * 60)) / 1000) + + return [days, hours, minutes, seconds] +} + +export const useHalving = () => { + const { statistics } = useAppState() + + const EPOCHS_PER_HALVING = 8760 + const currentEpoch = Number(statistics.epochInfo.epochNumber) + const nextHalvingCount = Math.ceil(currentEpoch / EPOCHS_PER_HALVING) + const targetEpoch = EPOCHS_PER_HALVING * nextHalvingCount + const singleEpochAverageTime = Number(statistics.estimatedEpochTime) + const currentEpochUsedTime = + (Number(statistics.epochInfo.index) / Number(statistics.epochInfo.epochLength)) * singleEpochAverageTime + + const estimatedTime = (targetEpoch - currentEpoch) * singleEpochAverageTime - currentEpochUsedTime + + const estimatedDate = useMemo(() => new Date(new Date().getTime() + estimatedTime), [estimatedTime]) + + return { + EPOCHS_PER_HALVING, + currentEpoch, + targetEpoch, + nextHalvingCount, + singleEpochAverageTime, + currentEpochUsedTime, + estimatedDate, + } +} + export default { useInterval, useTimeout, diff --git a/src/utils/string.ts b/src/utils/string.ts index 84acb6618..60631adfc 100644 --- a/src/utils/string.ts +++ b/src/utils/string.ts @@ -145,3 +145,7 @@ export const handleBigNumberFloor = (value: BigNumber | string | number, decimal } export const sliceNftName = (name?: string) => (name && name.length > 32 ? `${name.slice(0, 32)}...` : name) + +export function capitalizeFirstLetter(str: string) { + return str.charAt(0).toUpperCase() + str.slice(1) +}