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

feat: Add search profile support #3

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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
74 changes: 74 additions & 0 deletions app/api/account/[chainId]/[address]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { getDaimoAccountHistory } from '@/app/utils/accountHistory/getDaimoAccountHistory';
import { resolveAccountForAddress } from '@/app/utils/profiles';
import { AddressProfile } from '@/app/utils/types';
import { createViemClient } from '@/app/utils/viem/client';
import { Address, Hex } from 'viem';

// TransferLog type from Daimo API.
// TODO: add type when Daimo API is updated.
type TransferLog = {
type: string;
status: string;
timestamp: number;
from: string;
to: string;
amount: number;
blockNumber: number;
blockHash: string;
txHash: string;
logIndex: number;
nonceMetadata: string;
opHash: string;
requestStatus: string | null;
feeAmount: number;
};

export type TransferHistoryEntry = {
transferLog: TransferLog;
otherAccountProfile: AddressProfile;
};

/**
* Handle GET requests to /api/account/[chainId]/[address]
*
* @param {Object} params - The request parameters.
* @param {string} params.chainId - The chain ID of the desired account.
* @param {string} params.address - The address of the desired account.
* @returns {Object} The account profile in the form: { account: AccountProfile }.
*/
export async function GET(
req: Request,
{ params }: { params: { chainId: string; address: string } },
) {
const publicClient = createViemClient(params.chainId);

const accountHistory = await getDaimoAccountHistory(params.address as Address);
const accountProfile: AddressProfile = await resolveAccountForAddress(
params.address as Hex,
publicClient,
);

// Filter out non-transfer logs.
const accountTransfers: TransferLog[] = accountHistory
? accountHistory.transferLogs.filter((log: TransferLog) => log.type === 'transfer')
: [];

// Get other account profile for each transfer.
const transfers: TransferHistoryEntry[] = await Promise.all(
accountTransfers.map(async (transferLog) => {
const otherAccountAddress =
params.address == transferLog.from ? transferLog.to : transferLog.from;
const otherAccountProfile: AddressProfile = await resolveAccountForAddress(
otherAccountAddress as Address,
publicClient,
);

return { transferLog: transferLog, otherAccountProfile: otherAccountProfile };
}),
);

return Response.json({
accountProfile: accountProfile,
accountTransferHistory: transfers,
});
}
14 changes: 10 additions & 4 deletions app/components/AddressBubble.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { TextInitial } from './typography';
import { AccountIcon } from '@/public/profileIcons/profileIcons';
import { AccountAddress, AccountName } from './typography';
import { getProfileLink } from '../utils/profiles/getProfileLink';
import CopyText from './minorComponents/CopyText';

/** Header-value React component field for Address Bubble */
function AddressField(props: { name: string; address: string; accountType: AccountTypeStr }) {
Expand All @@ -18,18 +19,23 @@ function AddressField(props: { name: string; address: string; accountType: Accou
<AccountIcon accountType={props.accountType} />
</div>
</a>

<AccountAddress>{props.address}</AccountAddress>
<div className='flex flex-row gap-x-1 items-center'>
<AccountAddress>{props.address}</AccountAddress>
<CopyText text={props.address} hoverText='Copy address' />
</div>
</div>
);
}

/** Represents an address bubble component given an address profile*/
export default function AddressBubble(props: Readonly<{ addressProfile: AddressProfile }>) {
/** Represents an address bubble component given an address profile for transfers*/
export default function AddressBubble(
props: Readonly<{ addressProfile: AddressProfile; link?: boolean }>,
) {
const address = truncateAddress(props.addressProfile.accountAddress);
const pfp = props.addressProfile.account?.avatar ?? null;
const name = props.addressProfile.account?.name;
const nameInitial = name ? name[0].toUpperCase() : '0x';

return (
<div className='flex flex-row w-min-fit gap-x-4'>
<div className='flex'>
Expand Down
52 changes: 52 additions & 0 deletions app/components/AddressBubbleRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { AccountIcon } from '@/public/profileIcons/profileIcons';
import { truncateAddress } from '../utils/formatting';
import { AccountTypeStr, AddressProfile } from '../utils/types';
import { RowAccountProfileInitial } from './typography';
import Image from 'next/image';

function AddressField(props: { name: string; address: string; accountType: AccountTypeStr }) {
// Get profile link for an account name.
return (
<div className='flex flex-col'>
<div className='flex flex-row gap-x-2 items-center'>
{props.accountType == AccountTypeStr.UNKNOWN ? props.address : props.name}
<AccountIcon accountType={props.accountType} small={true} />
</div>
</div>
);
}

/** Represents an address bubble component given an address profile for transfer history */
export default function AddressBubbleRow(
props: Readonly<{ addressProfile: AddressProfile; link?: boolean }>,
) {
const address = truncateAddress(props.addressProfile.accountAddress);
const pfp = props.addressProfile.account?.avatar ?? null;
const name = props.addressProfile.account?.name;
const nameInitial = name ? name[0].toUpperCase() : '0x';

return (
<div className='flex flex-row w-min-fit gap-x-2'>
<div className='flex'>
{pfp ? (
<div className='rounded-[50%] w-6 h-6'>
<Image src={pfp} className='pfpImage' width='20' height='20' alt='pfp' />
</div>
) : (
<div className='bg-gradient-to-b w-6 h-6 rounded-[50%] from-[#F3F3F3] to-[#D6D6D6] p-[1px]'>
<div className='flex w-full h-full rounded-[50%] items-center justify-center bg-white'>
<RowAccountProfileInitial>{nameInitial}</RowAccountProfileInitial>
</div>
</div>
)}
</div>
<div className='flex flex-row items-center justify-start'>
<AddressField
name={name || ''}
address={address}
accountType={props.addressProfile.account?.type}
/>
</div>
</div>
);
}
2 changes: 1 addition & 1 deletion app/components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Daimo } from '@/public/profileIcons/Daimo';
/** Footer component for Eth Receipts */
export default function Footer() {
return (
<div className='sm:absolute relative bottom-0 w-full py-10 sm:px-[25%] px-2'>
<div className='relative bottom-0 w-full py-10 sm:px-[25%] px-2'>
<div className='flex flex-row flex-wrap w-full items-center justify-between w-full items-center min-w-fit'>
<div className='flex flex-row items-center gap-x-1'>
<OldDaimo />
Expand Down
5 changes: 2 additions & 3 deletions app/components/TransferCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@ import TransferArrow from './minorComponents/TransferArrow';
import { NeueMontreal } from '@/public/fonts';
import { formatValue } from '../utils/formatting';
import stablecoinsAddresses from '../utils/tokens/stablecoins';
import CopyReceipt from './minorComponents/CopyReceipt';
import { checkTokenWhitelist } from '../utils/tokens/tokenWhitelist';
import { Warning } from '@/public/icons';
import TokenWarning from './minorComponents/TokenWarning';
import CopyText from './minorComponents/CopyText';

/**
* Represents an ERC20 Transfer card.
Expand Down Expand Up @@ -49,7 +48,7 @@ export default function TransferCard(
<div className='flex flex-col bg-white rounded-[23px]'>
<div className='flex flex-col w-full items-center sm:px-10 px-8 sm:py-8 pt-8 pb-8'>
<div className='w-full flex justify-end sm:px-4 px-0 sm:mb-[-16px] mb-[-12px]'>
<CopyReceipt link={link} />
<CopyText text={link} hoverText='Copy link' />
</div>
<div className='flex flex-col items-center justify-center w-full min-w-fit'>
<div className={NeueMontreal.className}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
import { Link } from '@/public/icons';
import { useState } from 'react';

/* Component for copying receipt link to clipboard. */
export default function CopyReceipt({ link }: { link: string }) {
/* Component for copying text to clipboard. */
export default function CopyText({ text, hoverText }: { text: string; hoverText: string }) {
const [copied, setCopied] = useState(false);

// Copies link to clipboard.
const copyLink = () => {
setCopied(true);
navigator.clipboard.writeText(link);
navigator.clipboard.writeText(text);

// Reset after 1 second.
setTimeout(() => {
Expand All @@ -23,9 +23,8 @@ export default function CopyReceipt({ link }: { link: string }) {
<button onClick={copyLink} className='px-2 py-2'>
<Link />
</button>
<div className='absolute flex flex-row gap-x-[3px] bottom-8 scale-0 rounded bg-black p-2 text-xs text-white group-hover:scale-100'>
<div className='flex'>{copied ? 'Copied!' : 'Copy'}</div>
{!copied && <div className='flex'>link</div>}
<div className='absolute flex flex-row gap-x-[3px] bottom-8 scale-0 rounded bg-black p-2 text-xs text-white group-hover:scale-100 whitespace-nowrap'>
<div className='flex'>{copied ? 'Copied!' : hoverText}</div>
<div className='arrow-down' />
</div>
</div>
Expand Down
16 changes: 16 additions & 0 deletions app/components/minorComponents/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Search } from '@/public/icons';

export default function SearchBar() {
return (
<div className='flex flex-row items-center gap-x-2 px-10 py-4'>
<input
type='text'
placeholder='Search by address'
className='flex-1 border-[1px] border-[#F3F3F3] rounded-[36px] px-8 w-[700px] h-[64px] drop-shadow-3xl text-[20px] leading-[24px] font-light tracking-[-0.025em] focus:outline-none focus:border-[#AAAAAA]'
/>
<div className='-ml-14'>
<Search />
</div>
</div>
);
}
47 changes: 46 additions & 1 deletion app/components/typography.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export function Header({ children }: { children: React.ReactNode }) {
// Text for footer.
export function TextFooter({ children }: { children: React.ReactNode }) {
return (
<p className='sm:text-[14px] text-[10px] leading-[16.8px] tracking-[-0.025em] font-light color-[#AAAAAA] opacity-70'>
<p className='sm:text-[14px] text-[10px] leading-[16.8px] tracking-[-0.025em] font-light text-[#AAAAAA] opacity-70'>
{children}
</p>
);
Expand All @@ -98,3 +98,48 @@ export function LinkFooter({ href, children }: { href: string; children: React.R
</a>
);
}

/** Typography for profile search */

// Header for account activity on profile.
export function HeaderProfileActivity({ children }: { children: React.ReactNode }) {
return (
<p className='text-[16px] leading-[20px] tracking-[-0.025em] font-light text-[#000000]'>
{children}
</p>
);
}

// Row header for profile table.
export function RowHeaderProfileActivity({ children }: { children: React.ReactNode }) {
return (
<p className='text-[12px] leading-[14.5px] tracking-[-0.025em] font-normal text-[#323232]'>
{children}
</p>
);
}

// Transfers made to/from a specific account.
export function RowValueProfileActivity({ children }: { children: React.ReactNode }) {
return (
<p className='text-[12px] leading-[14.5px] tracking-[-0.025em] font-normal text-[#535353]'>
{children}
</p>
);
}

export function RowTimeProfileActivity({ children }: { children: React.ReactNode }) {
return (
<p className='text-[12px] leading-[14.5px] tracking-[-0.025em] font-normal text-[#AAAAAA]'>
{children}
</p>
);
}

export function RowAccountProfileInitial({ children }: { children: React.ReactNode }) {
return (
<p className='text-[10px] leading-[28px] tracking-[0.5px] font-medium bg-gradient-to-b from-[#535353] to-[#111111] bg-clip-text text-transparent'>
{children}
</p>
);
}
2 changes: 2 additions & 0 deletions app/env.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { DaimoChain, getChainConfig } from './utils/viem/chainConfig';

export const chainConfig = getChainConfig((process.env.DAIMO_CHAIN || 'baseSepolia') as DaimoChain);

export const apiUrl = process.env.ETH_RECEIPTS_DOMAIN || 'http://localhost:3000';
4 changes: 1 addition & 3 deletions app/l/[chainId]/[blockNumber]/[logIndex]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import Footer from '@/app/components/Footer';
import LogNotFound from '@/app/components/LogNotFound';
import TransferCard from '@/app/components/TransferCard';
import { Header } from '@/app/components/typography';

const apiUrl = process.env.ETH_RECEIPTS_DOMAIN || 'http://localhost:3000';
import { apiUrl } from '@/app/env';

/**
* Fetch log data from API.
Expand Down Expand Up @@ -58,7 +57,6 @@ export default async function Page({
) : (
<LogNotFound />
)}
<Footer />
</div>
);
}
Loading