Skip to content

Commit

Permalink
Statistics, Search and Logo Components (Landing Page) (#2116)
Browse files Browse the repository at this point in the history
* wip: statistics stack component and main page layout

* feat: styled statistics component and kadena logo

* chore: changeset file

* fix: lint fixes

* fix: small corrections and fixes

* Search Combobox Component (#2166)

* wip: introduced dropdown

* wip: search combobox

* feast: changes in statistics stack and index

* wip: search combobox

* feat: search component

* fix: linting fixes

* chore: changeset file

* feat: implement polling on statistics data

* fix: small design fixes

* Delete .changeset/forty-frogs-fly.md

* Update brave-frogs-argue.md

* fix: typo fix

* feat: remove border from select

* wip: added statistics grid for mobile view and added react-responsive

* wip: apply getMediaQuery

* feat: tweak responsive values for search input and statistics grid

* fix: linting

* feat: adjust responsiveness

* feat: replace library causing hydration issue to another alternative compatible with ssr

* fix: lint formatting

* fix: remove react-responsive

* feat: apply media styling with updated variables

* fix: remove lib from devDependencies
  • Loading branch information
nil-amrutlal-dept committed Jun 3, 2024
1 parent 42b0a1b commit 9231982
Show file tree
Hide file tree
Showing 13 changed files with 543 additions and 7 deletions.
5 changes: 5 additions & 0 deletions .changeset/brave-frogs-argue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kadena/explorer": patch
---

Implemented statistics, search and logo components and necessary logic
34 changes: 34 additions & 0 deletions packages/apps/explorer/src/components/search/search.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { atoms, responsiveStyle } from '@kadena/react-ui/styles';
import { style } from '@vanilla-extract/css';

export const searchBoxClass = style({
...responsiveStyle({
md: {
width: 525,
},
sm: {
width: 475,
},
xs: {
width: 325,
},
}),
});

export const searchInputClass = style([
atoms({
backgroundColor: 'base.default',
fontSize: 'md',
fontFamily: 'primaryFont',
outline: 'none',
}),
{
height: 55,
border: 'none',
width: '75%',
},
]);

export const searchBadgeBoxClass = style({
width: '20%',
});
229 changes: 229 additions & 0 deletions packages/apps/explorer/src/components/search/search.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import { truncateValues } from '@/services/format';
import { MonoSearch } from '@kadena/react-icons/system';
import { Badge, Box } from '@kadena/react-ui';
import { atoms } from '@kadena/react-ui/styles';
import React, { useState } from 'react';
import {
searchBadgeBoxClass,
searchBoxClass,
searchInputClass,
} from './search.css';

export type SearchItemTitle =
| 'Account'
| 'Request Key'
| 'Block Height'
| 'Block Hash'
| 'Events';

export interface ISearchItem {
title: SearchItemTitle;
disabled?: boolean;
}
interface ISearchComponentProps {
placeholder: string;
searchItems: ISearchItem[];
}

const SearchCombobox: React.FC<ISearchComponentProps> = ({
placeholder,
searchItems,
}) => {
const [isEditing, setIsEditing] = useState(false);
const [searchOption, setSearchOption] = useState<number | null>(null);
const [searchValue, setSearchValue] = useState<string>('');
const [optionClicked, setOptionClicked] = useState(false);
const [escapePressed, setEscapePressed] = useState(false);

const setOptionsDisabledExcept = (exceptIndex: number): void => {
searchItems.forEach((item, index) => {
if (index !== exceptIndex) {
item.disabled = true;
}
});
};

const inferOption = (value: string): SearchItemTitle | undefined => {
if (
value.toLocaleLowerCase().startsWith('k:') ||
value.toLocaleLowerCase().startsWith('w:')
) {
return 'Account';
} else if (value.includes('.')) {
return 'Events';
} else if (value.length === 43) {
return 'Request Key';
} else if (/^\d+$/.test(value)) {
return 'Block Height';
}

return undefined;
};

const enableAllOptions = (): void => {
searchItems.forEach((item) => {
item.disabled = false;
});
};

const handleSearch = (value: string, option: number | null): void => {};

const handleSearchValueChange = (
e: React.ChangeEvent<HTMLInputElement>,
): void => {
setSearchValue(e.target.value);

if (escapePressed || optionClicked) return;

const inferedOption = inferOption(e.target.value);
if (inferedOption === 'Account') {
setSearchOption(0);
setOptionsDisabledExcept(0);
}
if (inferedOption === 'Request Key') {
setSearchOption(1);
setOptionsDisabledExcept(1);
}

if (inferedOption === 'Block Height') {
setSearchOption(2);
setOptionsDisabledExcept(2);
}

if (!inferedOption || inferedOption === undefined) {
setSearchOption(null);
enableAllOptions();
}
};

const handleSearchValueKeyDown = (
e: React.KeyboardEvent<HTMLDivElement>,
): void => {
if (e.key === 'ArrowDown') {
e.preventDefault();
setSearchOption((prev) =>
prev === null ? 0 : Math.min(prev + 1, searchItems.length - 1),
);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
setSearchOption((prev) => (prev === null ? 0 : Math.max(prev - 1, 0)));
} else if (e.key === 'Enter') {
e.preventDefault();
setIsEditing(false);
setEscapePressed(false);
setOptionClicked(false);
handleSearch(searchValue, searchOption);
} else if (e.key === 'Escape') {
setOptionClicked(false);
setSearchOption(null);
setEscapePressed(true);
enableAllOptions();
}
};

return (
<>
<Box
paddingInline={'xxl'}
display={'flex'}
flexDirection={'column'}
onKeyDown={(e) => handleSearchValueKeyDown(e)}
onBlur={() => {
if (!optionClicked) {
setIsEditing(false);
}
}}
>
<Box
display={'inline-flex'}
flexDirection={'row'}
alignItems={'center'}
borderStyle="solid"
borderWidth="hairline"
backgroundColor="base.default"
gap={'sm'}
paddingInlineStart={'sm'}
paddingInlineEnd={'sm'}
className={searchBoxClass}
>
<MonoSearch />

<input
type="text"
placeholder={placeholder}
value={searchValue}
onChange={(e) => handleSearchValueChange(e)}
onFocus={() => setIsEditing(true)}
className={searchInputClass}
/>

{searchOption !== null && (
<Box
display={'flex'}
justifyContent={'flex-end'}
className={searchBadgeBoxClass}
>
<Badge size="lg">{searchItems[searchOption].title}</Badge>
</Box>
)}
</Box>

{isEditing && (
<div
className={atoms({
display: 'grid',
borderStyle: 'solid',
borderWidth: 'hairline',
backgroundColor: 'base.@active',
fontSize: 'sm',
fontFamily: 'primaryFont',
})}
>
{searchItems.map((item, index) => (
<Box
key={index}
onMouseDown={() => setOptionClicked(true)}
onClick={() => {
if (!item.disabled) {
setSearchOption(index);
setIsEditing(false);
}
}}
style={{
gridTemplateColumns: '1fr 3fr',
borderLeft: index === searchOption ? 'solid' : 'none',
}}
className={atoms({
display: 'grid',
alignItems: 'flex-start',
paddingInlineStart: 'md',
cursor: item.disabled ? 'not-allowed' : 'pointer',
backgroundColor:
index === searchOption ? 'base.@active' : 'base.default',
width: '100%',
})}
>
<div
className={atoms({
alignItems: 'flex-start',
})}
>
{item.title}
</div>
<div
className={atoms({
alignItems: 'flex-end',
})}
>
{truncateValues(searchValue)}
</div>
</Box>
))}
</div>
)}
</Box>
</>
);
};

export default SearchCombobox;
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Grid, Stack, Text } from '@kadena/react-ui';
import { atoms } from '@kadena/react-ui/styles';
import React from 'react';

interface ISearchComponentProps {
data: { label: string; value: string }[];
}

const StatisticsGrid: React.FC<ISearchComponentProps> = ({ data }) => {
return (
<Grid columns={2} borderStyle="solid" borderWidth="hairline">
{data.map((item) => (
<Stack
flexDirection={'column'}
alignItems={'center'}
padding={'sm'}
borderStyle="solid"
borderWidth="hairline"
key={`statistic-stack-${item.label}`}
>
<Text variant="code">{item.value}</Text>
<Text
variant="code"
bold
size="smallest"
className={atoms({
flexWrap: 'nowrap',
})}
>
{item.label}
</Text>
</Stack>
))}
</Grid>
);
};

export default StatisticsGrid;
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { SpireKeyKdacolorLogoWhite } from '@kadena/react-icons/product';
import { MonoHub } from '@kadena/react-icons/system';
import { Button, Select, SelectItem, Stack, Text } from '@kadena/react-ui';
import { atoms } from '@kadena/react-ui/styles';
import React, { useState } from 'react';
import { Media } from '../layout/media';
import { borderStyleClass, statisticsSpireKeyClass } from './statistics.css';

interface IStatisticsStackProps {
data: { label: string; value: string }[];
}

const StatisticsStack: React.FC<IStatisticsStackProps> = ({ data }) => {
const [selectedNetwork, setSelectedNetwork] = useState('Mainnet');

return (
<Stack flexDirection={'row'}>
<Stack flexDirection={'row'}>
{data.map((item) => (
<Stack
flexDirection={'column'}
alignItems={'center'}
padding={'sm'}
borderStyle="solid"
borderWidth="hairline"
key={`statistic-stack-${item.label}`}
>
<Text variant="code">{item.value}</Text>
<Text variant="code" bold size="smallest">
{item.label}
</Text>
</Stack>
))}

<Stack flexDirection={'row'}>
<div className={borderStyleClass}>
<Media greaterThanOrEqual="md">
<Button variant="transparent" endVisual={<MonoHub />}>
Graph
</Button>
</Media>
<Media lessThan="md">
<Button variant="transparent" endVisual={<MonoHub />} />
</Media>
</div>

<div className={borderStyleClass}>
<Select
defaultSelectedKey={selectedNetwork}
fontType="code"
size="lg"
className={atoms({
height: '100%',
})}
onSelectionChange={(value) =>
setSelectedNetwork(value.toString())
}
>
<SelectItem key={'Mainnet'} textValue="Mainnet">
Mainnet
</SelectItem>
<SelectItem key={'Testnet'} textValue="Testnet">
Testnet
</SelectItem>
</Select>
</div>
<div className={statisticsSpireKeyClass}>
<Button
variant="transparent"
startVisual={<SpireKeyKdacolorLogoWhite />}
/>
</div>
</Stack>
</Stack>
</Stack>
);
};

export default StatisticsStack;
Loading

0 comments on commit 9231982

Please sign in to comment.