Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Account Details Page #28

Merged
merged 5 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions src/app/address/[address]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
const AddressPage = ({ params: { address } }: { params: { address: string } }) => (
<h1>{address}</h1>
);

export default AddressPage;

import AddressComponent from "@/components/pages/address";
import { Address } from "viem";

const AddressPage = ({
params: { address },
}: {
params: { address: Address };
}) => <AddressComponent address={address} />;

export default AddressPage;
35 changes: 35 additions & 0 deletions src/components/common/copy-button/copy-button.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div>
{copied ? (
<Check size={16} className="ml-4 text-green-500" />
) : (
<CommonTooltip tooltipMessage="Copy Address" asChild>
<Copy
size={16}
className="ml-4 cursor-pointer"
onClick={copyAddress}
/>
</CommonTooltip>
)}
</div>
);
}
1 change: 1 addition & 0 deletions src/components/common/copy-button/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./copy-button";
57 changes: 57 additions & 0 deletions src/components/common/tooltip/common-tooltip.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<TooltipProvider>
<Tooltip delayDuration={delayDuration}>
<TooltipTrigger
className={className}
asChild={asChild}
onClick={(event) => {
if (!hideOnClick) event.preventDefault();
}}
ref={triggerRef}
>
{children}
</TooltipTrigger>
<TooltipContent
className={tooltipClassName}
onPointerDownOutside={(event) => {
if (event.target === triggerRef.current && !hideOnClick)
event.preventDefault();
}}
>
{tooltipMessage}
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
1 change: 1 addition & 0 deletions src/components/common/tooltip/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./common-tooltip";
14 changes: 12 additions & 2 deletions src/components/lib/description-list-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,28 @@ const DescriptionListItem = ({
title,
border = false,
children,
secondary = false,
}: {
title: string;
border?: boolean;
secondary?: boolean;
children: ReactNode;
}) => (
<div
className={cn(
{ "border-t border-gray-100 dark:border-white/10": border },
"px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0",
"px-4 py-3 sm:px-0",
secondary ? "flex flex-col sm:gap-3" : "sm:grid sm:grid-cols-3 sm:gap-4",
)}
>
<dt className="text-sm font-medium leading-6">{title}:</dt>
<dt
className={cn("text-sm font-medium leading-6", {
"text-muted-foreground": secondary,
})}
>
{title}
{secondary ? "" : ":"}
</dt>
<dd className="mt-1 flex items-center text-sm font-medium leading-6 sm:col-span-2 sm:mt-0">
{children}
</dd>
Expand Down
12 changes: 6 additions & 6 deletions src/components/lib/navbar/category-values-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className="custom-scroll flex max-h-64 flex-col items-center justify-start overflow-y-auto px-4">
Expand Down
24 changes: 14 additions & 10 deletions src/components/lib/navbar/search-result-dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -20,9 +20,13 @@ export function SearchResultDropDown({
}: Props) {
const dropdownRef = useRef<HTMLDivElement>(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);
}
};
Expand All @@ -47,15 +51,15 @@ export function SearchResultDropDown({
>
{searchResult.length !== 0 ? (
<div>
<CategoryListDropdown
<CategoryListDropdown
searchResult={searchResult}
selectedCategory={selectedCategory}
handleCategorySelect={handleCategorySelect}
/>
<CategoryValuesList
/>
<CategoryValuesList
searchResult={searchResult}
selectedCategory={selectedCategory}
/>
/>
</div>
) : (
<div className="w-full items-center justify-start p-4 text-base opacity-[0.6]">
Expand Down
18 changes: 12 additions & 6 deletions src/components/lib/navbar/search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,34 @@ 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 (
<form className="ml-auto flex-1 relative sm:flex-initial">
<form className="relative ml-auto flex-1 sm:flex-initial">
<div className="relative">
<div className="relative flex items-center">
<SearchIcon className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
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]"
/>
</div>
<SearchResultDropDown
showResult={showResult}
showResult={showResult}
searchResult={searchResult}
selectedCategory={selectedCategory}
handleCategorySelect={handleCategorySelect}
handleShowResult={handleShowResult}
/>
/>
</div>
</form>
);
Expand Down
44 changes: 44 additions & 0 deletions src/components/pages/address/address-details.tsx
Original file line number Diff line number Diff line change
@@ -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;
}) => (
<Card>
<CardHeader className="p-4">
<CardTitle>Overview</CardTitle>
</CardHeader>

<CardContent>
<dl>
<DescriptionListItem secondary title="ETH BALANCE">
<EthereumIcon className="mr-1 size-4" />
{formatEther(address.balance)} ETH
</DescriptionListItem>
<DescriptionListItem secondary title="ETH VALUE">
{(
Number(formatEther(address.balance)) * ethPriceToday
).toLocaleString("en-US", {
style: "currency",
currency: "USD",
})}{" "}
(@{" "}
{ethPriceToday.toLocaleString("en-US", {
style: "currency",
currency: "USD",
})}
/ETH)
</DescriptionListItem>
</dl>
</CardContent>
</Card>
);

export default AddressDetailsSection;
31 changes: 31 additions & 0 deletions src/components/pages/address/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<main className="flex flex-1 flex-col gap-4 p-4 md:gap-4 md:p-4">
<h2 className="mt-10 flex scroll-m-20 items-end border-b pb-2 text-3xl font-semibold tracking-tight transition-colors first:mt-0">
{addressType}
<span className="ml-2 flex items-center text-base">
{address}
<CopyButton toCopy={address} />
</span>
</h2>
<AddressDetails
address={{ addressType, balance }}
ethPriceToday={ethPrice.eth.today}
/>
</main>
);
};

export default AddressComponent;
2 changes: 1 addition & 1 deletion src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export * from './useSearch';
export * from "./useSearch";
Loading