Skip to content

Commit

Permalink
Block Countdown page (#2091)
Browse files Browse the repository at this point in the history
* page placeholder

* block info snippet

* add actions for calendar buttons

* add timer

* add redirect when timer runs out

* add redirect from not found block page

* block countdown index page

* link to countdown index page

* add link to search suggest

* add URL to an event

* add link to block countdown on mobile

* tests

* add countdown text to search result page

* update margins

* show block countdown when search results are not empty
  • Loading branch information
tom2drum authored Jul 18, 2024
1 parent 83cffb9 commit bfb0855
Show file tree
Hide file tree
Showing 64 changed files with 785 additions and 87 deletions.
3 changes: 3 additions & 0 deletions icons/apps_slim.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 0 additions & 3 deletions icons/apps_xs.svg

This file was deleted.

4 changes: 4 additions & 0 deletions icons/block_countdown.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions icons/hourglass.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 5 additions & 1 deletion lib/api/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import type {
ArbitrumL2BatchBlocks,
} from 'types/api/arbitrumL2';
import type { TxBlobs, Blob } from 'types/api/blobs';
import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters, BlockWithdrawalsResponse } from 'types/api/block';
import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters, BlockWithdrawalsResponse, BlockCountdownResponse } from 'types/api/block';
import type { ChartMarketResponse, ChartSecondaryCoinPriceResponse, ChartTransactionResponse } from 'types/api/charts';
import type { BackendVersionConfig } from 'types/api/configs';
import type {
Expand Down Expand Up @@ -852,6 +852,9 @@ export const RESOURCES = {
graphql: {
path: '/api/v1/graphql',
},
block_countdown: {
path: '/api',
},
};

export type ResourceName = keyof typeof RESOURCES;
Expand Down Expand Up @@ -937,6 +940,7 @@ Q extends 'stats_lines' ? stats.LineCharts :
Q extends 'stats_line' ? stats.LineChart :
Q extends 'blocks' ? BlocksResponse :
Q extends 'block' ? Block :
Q extends 'block_countdown' ? BlockCountdownResponse :
Q extends 'block_txs' ? BlockTransactionsResponse :
Q extends 'block_withdrawals' ? BlockWithdrawalsResponse :
Q extends 'txs_stats' ? TransactionsStats :
Expand Down
2 changes: 2 additions & 0 deletions lib/metadata/getPageOgType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
'/tx/[hash]': 'Regular page',
'/blocks': 'Root page',
'/block/[height_or_hash]': 'Regular page',
'/block/countdown': 'Regular page',
'/block/countdown/[height]': 'Regular page',
'/accounts': 'Root page',
'/address/[hash]': 'Regular page',
'/verified-contracts': 'Root page',
Expand Down
2 changes: 2 additions & 0 deletions lib/metadata/templates/description.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/tx/[hash]': 'View transaction %hash% on %network_title%',
'/blocks': DEFAULT_TEMPLATE,
'/block/[height_or_hash]': 'View the transactions, token transfers, and uncles for block %height_or_hash%',
'/block/countdown': DEFAULT_TEMPLATE,
'/block/countdown/[height]': DEFAULT_TEMPLATE,
'/accounts': DEFAULT_TEMPLATE,
'/address/[hash]': 'View the account balance, transactions, and other data for %hash% on the %network_title%',
'/verified-contracts': DEFAULT_TEMPLATE,
Expand Down
2 changes: 2 additions & 0 deletions lib/metadata/templates/title.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/tx/[hash]': '%network_name% transaction %hash%',
'/blocks': '%network_name% blocks',
'/block/[height_or_hash]': '%network_name% block %height_or_hash%',
'/block/countdown': '%network_name% block countdown index',
'/block/countdown/[height]': '%network_name% block %height% countdown',
'/accounts': '%network_name% top accounts',
'/address/[hash]': '%network_name% address details for %hash%',
'/verified-contracts': 'Verified %network_name% contracts lookup - %network_name% explorer',
Expand Down
2 changes: 2 additions & 0 deletions lib/mixpanel/getPageType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/tx/[hash]': 'Transaction details',
'/blocks': 'Blocks',
'/block/[height_or_hash]': 'Block details',
'/block/countdown': 'Block countdown search',
'/block/countdown/[height]': 'Block countdown',
'/accounts': 'Top accounts',
'/address/[hash]': 'Address details',
'/verified-contracts': 'Verified contracts',
Expand Down
2 changes: 2 additions & 0 deletions lib/regexp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ export const IPFS_PREFIX = /^ipfs:\/\//i;
export const HEX_REGEXP = /^(?:0x)?[\da-fA-F]+$/;

export const FILE_EXTENSION = /\.([\da-z]+)$/i;

export const BLOCK_HEIGHT = /^\d+$/;
2 changes: 2 additions & 0 deletions nextjs/nextjs-routes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ declare module "nextjs-routes" {
| StaticRoute<"/batches">
| DynamicRoute<"/blobs/[hash]", { "hash": string }>
| DynamicRoute<"/block/[height_or_hash]", { "height_or_hash": string }>
| DynamicRoute<"/block/countdown/[height]", { "height": string }>
| StaticRoute<"/block/countdown">
| StaticRoute<"/blocks">
| StaticRoute<"/contract-verification">
| StaticRoute<"/csv-export">
Expand Down
20 changes: 20 additions & 0 deletions pages/block/countdown/[height].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import React from 'react';

import type { Props } from 'nextjs/getServerSideProps';
import PageNextJs from 'nextjs/PageNextJs';

const BlockCountdown = dynamic(() => import('ui/pages/BlockCountdown'), { ssr: false });

const Page: NextPage<Props> = (props: Props) => {
return (
<PageNextJs pathname="/block/countdown/[height]" query={ props.query }>
<BlockCountdown/>
</PageNextJs>
);
};

export default Page;

export { base as getServerSideProps } from 'nextjs/getServerSideProps';
20 changes: 20 additions & 0 deletions pages/block/countdown/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import React from 'react';

import type { Props } from 'nextjs/getServerSideProps';
import PageNextJs from 'nextjs/PageNextJs';

const BlockCountdownIndex = dynamic(() => import('ui/pages/BlockCountdownIndex'), { ssr: false });

const Page: NextPage<Props> = (props: Props) => {
return (
<PageNextJs pathname="/block/countdown" query={ props.query }>
<BlockCountdownIndex/>
</PageNextJs>
);
};

export default Page;

export { base as getServerSideProps } from 'nextjs/getServerSideProps';
4 changes: 3 additions & 1 deletion public/icons/name.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
| "ABI"
| "API"
| "apps_list"
| "apps_xs"
| "apps_slim"
| "apps"
| "arrows/down-right"
| "arrows/east-mini"
Expand All @@ -20,6 +20,7 @@
| "blobs/image"
| "blobs/raw"
| "blobs/text"
| "block_countdown"
| "block_slim"
| "block"
| "brands/blockscout"
Expand Down Expand Up @@ -70,6 +71,7 @@
| "globe-b"
| "globe"
| "graphQL"
| "hourglass"
| "info"
| "integration/full"
| "integration/partial"
Expand Down
12 changes: 12 additions & 0 deletions public/static/apple_calendar.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions public/static/google_calendar.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions types/api/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,12 @@ export type BlockWithdrawalsItem = {
receiver: AddressParam;
validator_index: number;
}

export interface BlockCountdownResponse {
result: {
CountdownBlock: string;
CurrentBlock: string;
EstimateTimeInSec: string;
RemainingBlock: string;
} | null;
}
14 changes: 14 additions & 0 deletions types/client/search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type * as api from 'types/api/search';

export interface SearchResultFutureBlock {
type: 'block';
block_type: 'block';
block_number: number | string;
block_hash: string;
timestamp: undefined;
url?: string; // not used by the frontend, we build the url ourselves
}

export type SearchResultBlock = api.SearchResultBlock | SearchResultFutureBlock;

export type SearchResultItem = api.SearchResultItem | SearchResultBlock;
55 changes: 55 additions & 0 deletions ui/blockCountdown/BlockCountdownTimer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { HStack, StackDivider, useColorModeValue } from '@chakra-ui/react';
import React from 'react';

import { SECOND } from 'lib/consts';

import BlockCountdownTimerItem from './BlockCountdownTimerItem';
import splitSecondsInPeriods from './splitSecondsInPeriods';

interface Props {
value: number;
onFinish: () => void;
}

const BlockCountdownTimer = ({ value: initialValue, onFinish }: Props) => {

const [ value, setValue ] = React.useState(initialValue);

const bgColor = useColorModeValue('gray.50', 'whiteAlpha.100');

React.useEffect(() => {
const intervalId = window.setInterval(() => {
setValue((prev) => {
if (prev > 1) {
return prev - 1;
}

onFinish();
return 0;
});
}, SECOND);

return () => {
window.clearInterval(intervalId);
};
}, [ initialValue, onFinish ]);

const periods = splitSecondsInPeriods(value);

return (
<HStack
bgColor={ bgColor }
mt={{ base: 6, lg: 8 }}
p={{ base: 3, lg: 4 }}
borderRadius="base"
divider={ <StackDivider borderColor="divider"/> }
>
<BlockCountdownTimerItem label="Days" value={ periods.days }/>
<BlockCountdownTimerItem label="Hours" value={ periods.hours }/>
<BlockCountdownTimerItem label="Minutes" value={ periods.minutes }/>
<BlockCountdownTimerItem label="Seconds" value={ periods.seconds }/>
</HStack>
);
};

export default React.memo(BlockCountdownTimer);
32 changes: 32 additions & 0 deletions ui/blockCountdown/BlockCountdownTimerItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Box } from '@chakra-ui/react';
import React from 'react';

import TruncatedValue from 'ui/shared/TruncatedValue';

interface Props {
label: string;
value: string;
}

const BlockCountdownTimerItem = ({ label, value }: Props) => {
return (
<Box
minW={{ base: '70px', lg: '100px' }}
textAlign="center"
overflow="hidden"
flex="1 1 auto"
>
<TruncatedValue
value={ value }
fontFamily="heading"
fontSize={{ base: '40px', lg: '48px' }}
lineHeight="48px"
fontWeight={ 600 }
w="100%"
/>
<Box fontSize="sm" lineHeight="20px" mt={ 1 } color="text_secondary">{ label }</Box>
</Box>
);
};

export default React.memo(BlockCountdownTimerItem);
30 changes: 30 additions & 0 deletions ui/blockCountdown/createGoogleCalendarLink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { route } from 'nextjs-routes';

import config from 'configs/app';
import dayjs from 'lib/date/dayjs';
interface Params {
timeFromNow: number;
blockHeight: string;
}

const DATE_FORMAT = 'YYYYMMDDTHHmm';

export default function createGoogleCalendarLink({ timeFromNow, blockHeight }: Params): string {

const date = dayjs().add(timeFromNow, 's');
const name = `Block #${ blockHeight } reminder | ${ config.chain.name }`;
const description = `#${ blockHeight } block creation time on ${ config.chain.name } blockchain.`;
const startTime = date.format(DATE_FORMAT);
const endTime = date.add(15, 'minutes').format(DATE_FORMAT);
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const blockUrl = config.app.baseUrl + route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: blockHeight } });

const url = new URL('calendar/render', 'https://www.google.com/');
url.searchParams.append('action', 'TEMPLATE');
url.searchParams.append('text', name);
url.searchParams.append('details', description + '\n' + blockUrl);
url.searchParams.append('dates', `${ startTime }/${ endTime }`);
url.searchParams.append('ctz', timeZone);

return url.toString();
}
35 changes: 35 additions & 0 deletions ui/blockCountdown/createIcsFileBlob.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { route } from 'nextjs-routes';

import config from 'configs/app';
import type dayjs from 'lib/date/dayjs';

interface Params {
date: dayjs.Dayjs;
blockHeight: string;
}

const DATE_FORMAT = 'YYYYMMDDTHHmmss';

export default function createIcsFileBlob({ date, blockHeight }: Params): Blob {
const name = `Block #${ blockHeight } reminder | ${ config.chain.name }`;
const description = `#${ blockHeight } block creation time on ${ config.chain.name } blockchain.`;
const startTime = date.format(DATE_FORMAT);
const endTime = date.add(15, 'minutes').format(DATE_FORMAT);
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const blockUrl = config.app.baseUrl + route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: blockHeight } });

const icsContent = `BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
SUMMARY:${ name }
DESCRIPTION:${ description }
DTSTART;TZID=${ timeZone }:${ startTime }
DTEND;TZID=${ timeZone }:${ endTime }
URL:${ blockUrl }
END:VEVENT
END:VCALENDAR`;

const blob = new Blob([ icsContent ], { type: 'text/calendar' });

return blob;
}
Loading

0 comments on commit bfb0855

Please sign in to comment.