From 2b8e2f167ad046a425e1869bca87e58fd37334c6 Mon Sep 17 00:00:00 2001 From: bhavyagosai Date: Wed, 26 Jun 2024 03:00:36 +0530 Subject: [PATCH 1/5] add: ui components - tooltip - copy button --- .../common/copy-button/copy-button.tsx | 35 ++++++++++++ src/components/common/copy-button/index.ts | 1 + .../common/tooltip/common-tooltip.tsx | 57 +++++++++++++++++++ src/components/common/tooltip/index.ts | 1 + 4 files changed, 94 insertions(+) create mode 100644 src/components/common/copy-button/copy-button.tsx create mode 100644 src/components/common/copy-button/index.ts create mode 100644 src/components/common/tooltip/common-tooltip.tsx create mode 100644 src/components/common/tooltip/index.ts diff --git a/src/components/common/copy-button/copy-button.tsx b/src/components/common/copy-button/copy-button.tsx new file mode 100644 index 0000000..3eb55ea --- /dev/null +++ b/src/components/common/copy-button/copy-button.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { CommonTooltip } from "@/components/common/tooltip"; +import { Copy, Check } from "lucide-react"; +import React, { useState } from "react"; + +export function CopyButton({ toCopy }: { toCopy: string }) { + const [copied, setCopied] = useState(false); // State to manage copy status + + const copyAddress = () => { + navigator.clipboard + .writeText(toCopy) + .then(() => { + setCopied(true); // Set copied state to true + setTimeout(() => setCopied(false), 1000); // Revert back after 1 second + }) + .catch((err) => console.error("Failed to copy address: ", err)); + }; + + return ( +
+ {copied ? ( + + ) : ( + + + + )} +
+ ); +} diff --git a/src/components/common/copy-button/index.ts b/src/components/common/copy-button/index.ts new file mode 100644 index 0000000..1a03d84 --- /dev/null +++ b/src/components/common/copy-button/index.ts @@ -0,0 +1 @@ +export * from "./copy-button"; diff --git a/src/components/common/tooltip/common-tooltip.tsx b/src/components/common/tooltip/common-tooltip.tsx new file mode 100644 index 0000000..242c5ee --- /dev/null +++ b/src/components/common/tooltip/common-tooltip.tsx @@ -0,0 +1,57 @@ +"use client"; + +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { ReactNode, useRef } from "react"; + +type Props = { + className?: string; + tooltipClassName?: string; + children: ReactNode; + tooltipMessage: ReactNode; + asChild?: boolean; + hideOnClick?: boolean; + delayDuration?: number; +}; + +export function CommonTooltip(props: Props) { + const { + className, + children, + tooltipMessage, + asChild, + hideOnClick = true, + tooltipClassName, + delayDuration = 0, + } = props; + const triggerRef = useRef(null); + return ( + + + { + if (!hideOnClick) event.preventDefault(); + }} + ref={triggerRef} + > + {children} + + { + if (event.target === triggerRef.current && !hideOnClick) + event.preventDefault(); + }} + > + {tooltipMessage} + + + + ); +} diff --git a/src/components/common/tooltip/index.ts b/src/components/common/tooltip/index.ts new file mode 100644 index 0000000..0dd00e0 --- /dev/null +++ b/src/components/common/tooltip/index.ts @@ -0,0 +1 @@ +export * from "./common-tooltip"; From 6849875c40b5a18ddb57cd2d87554d76214e3daf Mon Sep 17 00:00:00 2001 From: bhavyagosai Date: Wed, 26 Jun 2024 03:01:43 +0530 Subject: [PATCH 2/5] add: types - address details type --- src/lib/types.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/lib/types.ts b/src/lib/types.ts index e70a528..f52a442 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -12,6 +12,11 @@ export type Transaction = { timestamp: bigint; }; +export type AddressDetails = { + addressType: "Contract" | "Address"; + balance: bigint; +}; + export type BlockWithTransactions = Block & { transactions: Transaction[] }; export type L1L2Transaction = { From 8238fb38b4af907abaa726371ce50ea91b00a99e Mon Sep 17 00:00:00 2001 From: bhavyagosai Date: Wed, 26 Jun 2024 03:02:12 +0530 Subject: [PATCH 3/5] update: description list item component add support for secondary list type --- src/components/lib/description-list-item.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/components/lib/description-list-item.tsx b/src/components/lib/description-list-item.tsx index 9035794..cca7bb0 100644 --- a/src/components/lib/description-list-item.tsx +++ b/src/components/lib/description-list-item.tsx @@ -5,18 +5,28 @@ const DescriptionListItem = ({ title, border = false, children, + secondary = false, }: { title: string; border?: boolean; + secondary?: boolean; children: ReactNode; }) => (
-
{title}:
+
+ {title} + {secondary ? "" : ":"} +
{children}
From cd6aa5b45c4b5c79a789d734d89c222b2262223a Mon Sep 17 00:00:00 2001 From: bhavyagosai Date: Wed, 26 Jun 2024 03:08:57 +0530 Subject: [PATCH 4/5] add: address details page --- src/app/address/[address]/page.tsx | 16 ++++--- .../pages/address/address-details.tsx | 44 +++++++++++++++++++ src/components/pages/address/index.tsx | 31 +++++++++++++ 3 files changed, 85 insertions(+), 6 deletions(-) create mode 100644 src/components/pages/address/address-details.tsx create mode 100644 src/components/pages/address/index.tsx diff --git a/src/app/address/[address]/page.tsx b/src/app/address/[address]/page.tsx index 3730e37..cede6a4 100644 --- a/src/app/address/[address]/page.tsx +++ b/src/app/address/[address]/page.tsx @@ -1,6 +1,10 @@ -const AddressPage = ({ params: { address } }: { params: { address: string } }) => ( -

{address}

- ); - - export default AddressPage; - \ No newline at end of file +import AddressComponent from "@/components/pages/address"; +import { Address } from "viem"; + +const AddressPage = ({ + params: { address }, +}: { + params: { address: Address }; +}) => ; + +export default AddressPage; diff --git a/src/components/pages/address/address-details.tsx b/src/components/pages/address/address-details.tsx new file mode 100644 index 0000000..97b1519 --- /dev/null +++ b/src/components/pages/address/address-details.tsx @@ -0,0 +1,44 @@ +import DescriptionListItem from "@/components/lib/description-list-item"; +import EthereumIcon from "@/components/lib/ethereum-icon"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { AddressDetails } from "@/lib/types"; +import { formatEther } from "viem"; + +const AddressDetailsSection = ({ + address, + ethPriceToday, +}: { + address: AddressDetails; + ethPriceToday: number; +}) => ( + + + Overview + + + +
+ + + {formatEther(address.balance)} ETH + + + {( + Number(formatEther(address.balance)) * ethPriceToday + ).toLocaleString("en-US", { + style: "currency", + currency: "USD", + })}{" "} + (@{" "} + {ethPriceToday.toLocaleString("en-US", { + style: "currency", + currency: "USD", + })} + /ETH) + +
+
+
+); + +export default AddressDetailsSection; diff --git a/src/components/pages/address/index.tsx b/src/components/pages/address/index.tsx new file mode 100644 index 0000000..17bf0a8 --- /dev/null +++ b/src/components/pages/address/index.tsx @@ -0,0 +1,31 @@ +import { Address } from "viem"; +import { l2PublicClient } from "@/lib/chains"; +import AddressDetails from "./address-details"; +import { fetchTokensPrices } from "@/lib/utils"; +import { CopyButton } from "@/components/common/copy-button"; + +const AddressComponent = async ({ address }: { address: Address }) => { + const ethPrice = await fetchTokensPrices(); + const byteCode = await l2PublicClient.getCode({ address }); + const balance = await l2PublicClient.getBalance({ address }); + + const addressType = byteCode ? "Contract" : "Address"; + + return ( +
+

+ {addressType} + + {address} + + +

+ +
+ ); +}; + +export default AddressComponent; From 72cd850b6f1df6ad5c4aa28dccc639f6657fd82a Mon Sep 17 00:00:00 2001 From: bhavyagosai Date: Wed, 26 Jun 2024 03:16:48 +0530 Subject: [PATCH 5/5] format: prettier --- .../lib/navbar/category-values-list.tsx | 12 +-- .../lib/navbar/search-result-dropdown.tsx | 24 +++--- src/components/lib/navbar/search.tsx | 18 +++-- src/hooks/index.ts | 2 +- src/hooks/useSearch.ts | 74 ++++++++++++------- src/interfaces/index.ts | 2 +- src/interfaces/inputs.interface.ts | 6 +- src/utils/index.ts | 2 +- src/utils/truncateText.ts | 17 ++--- 9 files changed, 94 insertions(+), 63 deletions(-) diff --git a/src/components/lib/navbar/category-values-list.tsx b/src/components/lib/navbar/category-values-list.tsx index 9930ede..a037827 100644 --- a/src/components/lib/navbar/category-values-list.tsx +++ b/src/components/lib/navbar/category-values-list.tsx @@ -8,12 +8,12 @@ interface Props { } export function CategoryValuesList({ selectedCategory, searchResult }: Props) { - const categoryToRedirect: string = - selectedCategory === "Transactions" - ? "tx" - : selectedCategory === "Blocks" - ? "block" - : "address"; + const categoryToRedirect: string = + selectedCategory === "Transactions" + ? "tx" + : selectedCategory === "Blocks" + ? "block" + : "address"; return (
diff --git a/src/components/lib/navbar/search-result-dropdown.tsx b/src/components/lib/navbar/search-result-dropdown.tsx index f1563d5..84287e8 100644 --- a/src/components/lib/navbar/search-result-dropdown.tsx +++ b/src/components/lib/navbar/search-result-dropdown.tsx @@ -4,11 +4,11 @@ import { CategoryListDropdown } from "./category-list-dropdown"; import { CategoryValuesList } from "./category-values-list"; interface Props { - showResult : boolean; - selectedCategory : string; - searchResult : SearchInputResult[]; + showResult: boolean; + selectedCategory: string; + searchResult: SearchInputResult[]; handleCategorySelect: (category: string) => void; - handleShowResult : (value: boolean) => void + handleShowResult: (value: boolean) => void; } export function SearchResultDropDown({ @@ -20,9 +20,13 @@ export function SearchResultDropDown({ }: Props) { const dropdownRef = useRef(null); - useEffect(() => { // Ensures that the menu is hidden if the user clicks on any part of the screen + useEffect(() => { + // Ensures that the menu is hidden if the user clicks on any part of the screen const handleClickOutside = (event: MouseEvent) => { - if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) + ) { handleShowResult(false); } }; @@ -47,15 +51,15 @@ export function SearchResultDropDown({ > {searchResult.length !== 0 ? (
- - + + />
) : (
diff --git a/src/components/lib/navbar/search.tsx b/src/components/lib/navbar/search.tsx index 807fb63..aad239d 100644 --- a/src/components/lib/navbar/search.tsx +++ b/src/components/lib/navbar/search.tsx @@ -5,11 +5,17 @@ import { useSearch } from "@/hooks"; import { SearchResultDropDown } from "./search-result-dropdown"; const Search = () => { - const { - searchResult, showResult, selectedCategory, handleCategorySelect, onQueryChanged, handleShowResult } = useSearch() + const { + searchResult, + showResult, + selectedCategory, + handleCategorySelect, + onQueryChanged, + handleShowResult, + } = useSearch(); return ( -
+
@@ -17,16 +23,16 @@ const Search = () => { type="search" onChange={onQueryChanged} placeholder="Search by Address / Txn Hash / Block / Token" - className="pl-8 w-full sm:w-[400px] md:w-[360px] lg:w-[480px]" + className="w-full pl-8 sm:w-[400px] md:w-[360px] lg:w-[480px]" />
+ />
); diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 30a83ee..ca78e3e 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1 +1 @@ -export * from './useSearch'; +export * from "./useSearch"; diff --git a/src/hooks/useSearch.ts b/src/hooks/useSearch.ts index c8e18b9..7056846 100644 --- a/src/hooks/useSearch.ts +++ b/src/hooks/useSearch.ts @@ -10,22 +10,32 @@ export function useSearch() { const [searchResult, setSearchResult] = useState([]); useEffect(() => { - const selectedDefaultCategory = searchResult.length !== 0 ? searchResult[0].category : "" - setSelectedCategory(selectedDefaultCategory) - }, [searchResult]) - + const selectedDefaultCategory = + searchResult.length !== 0 ? searchResult[0].category : ""; + setSelectedCategory(selectedDefaultCategory); + }, [searchResult]); + const onQueryChanged = (event: ChangeEvent) => { if (debounceRef.current) clearTimeout(debounceRef.current); - + debounceRef.current = setTimeout(async () => { const searchInputValue = event.target.value.trim(); setSearchResult([]); if (searchInputValue) { try { let updatedSearchResult = [...searchResult]; - updatedSearchResult = await handleBlockSearch(updatedSearchResult, searchInputValue); - updatedSearchResult = await handleTransactionSearch(updatedSearchResult, searchInputValue); - updatedSearchResult = await handleAddressSearch(updatedSearchResult, searchInputValue); + updatedSearchResult = await handleBlockSearch( + updatedSearchResult, + searchInputValue, + ); + updatedSearchResult = await handleTransactionSearch( + updatedSearchResult, + searchInputValue, + ); + updatedSearchResult = await handleAddressSearch( + updatedSearchResult, + searchInputValue, + ); setSearchResult(updatedSearchResult); } catch (err) { console.error("Error fetching data:", err); @@ -37,44 +47,55 @@ export function useSearch() { } }, 500); }; - - const handleBlockSearch = async (results: SearchInputResult[], searchValue: string) => { + + const handleBlockSearch = async ( + results: SearchInputResult[], + searchValue: string, + ) => { if (!isNumeric(searchValue)) return results; - + const blockNumber = BigInt(parseInt(searchValue, 10)); const blockData = await l2PublicClient.getBlock({ blockNumber }); if (blockData) { setSelectedCategory("Blocks"); return updateSearchResult(results, "Blocks", searchValue); } - + return results; }; - - const handleTransactionSearch = async (results: SearchInputResult[], searchValue: string) => { + + const handleTransactionSearch = async ( + results: SearchInputResult[], + searchValue: string, + ) => { if (!isValidHash(searchValue)) return results; - - const transactionData = await l2PublicClient.getTransaction({ hash: searchValue as `0x${string}` }); + + const transactionData = await l2PublicClient.getTransaction({ + hash: searchValue as `0x${string}`, + }); if (transactionData) { setSelectedCategory("Transactions"); return updateSearchResult(results, "Transactions", searchValue); } - + return results; }; - - const handleAddressSearch = async (results: SearchInputResult[], searchValue: string) => { + + const handleAddressSearch = async ( + results: SearchInputResult[], + searchValue: string, + ) => { if (!isAddress(searchValue)) return results; - + // const code = await l2PublicClient.getCode({ address: searchValue }); 👈 obtain additional address data if needed setSelectedCategory("Addresses"); return updateSearchResult(results, "Addresses", searchValue); }; - + const updateSearchResult = ( results: SearchInputResult[], category: string, - value: string + value: string, ): SearchInputResult[] => { const index = results.findIndex((item) => item.category === category); if (index !== -1) { @@ -86,7 +107,8 @@ export function useSearch() { }; const isNumeric = (str: string): boolean => /^\d+$/.test(str); - const isValidHash = (str: string): boolean => /^0x([A-Fa-f0-9]{64})$/.test(str); + const isValidHash = (str: string): boolean => + /^0x([A-Fa-f0-9]{64})$/.test(str); const handleCategorySelect = (category: string) => { setSelectedCategory(category); @@ -105,6 +127,6 @@ export function useSearch() { // Functions handleCategorySelect, onQueryChanged, - handleShowResult - } -} \ No newline at end of file + handleShowResult, + }; +} diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index 4cebc1c..fc1a43c 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -1 +1 @@ -export * from './inputs.interface'; +export * from "./inputs.interface"; diff --git a/src/interfaces/inputs.interface.ts b/src/interfaces/inputs.interface.ts index 692e90e..72e648c 100644 --- a/src/interfaces/inputs.interface.ts +++ b/src/interfaces/inputs.interface.ts @@ -1,4 +1,4 @@ export interface SearchInputResult { - category: string, - values: string[] -} \ No newline at end of file + category: string; + values: string[]; +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 5434d56..fa19bb5 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1 +1 @@ -export * from './truncateText'; +export * from "./truncateText"; diff --git a/src/utils/truncateText.ts b/src/utils/truncateText.ts index 180ce02..3b0bf10 100644 --- a/src/utils/truncateText.ts +++ b/src/utils/truncateText.ts @@ -1,10 +1,9 @@ - export function truncateText(text: string, length: number) { - if (text.length <= 2 * length) { - return text; - } - - const start = text.slice(0, length); - const end = text.slice(-length); - return `${start}...${end}`; - } \ No newline at end of file + if (text.length <= 2 * length) { + return text; + } + + const start = text.slice(0, length); + const end = text.slice(-length); + return `${start}...${end}`; +}