From b68d3caa2587a786c46f44b7dafcf07fbfcedafb Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Wed, 16 Apr 2025 13:24:40 +0530 Subject: [PATCH 01/10] feat: new home page ui w/mock data + new reusable history card component --- src/app/(mobile-ui)/home/page.tsx | 374 +++++++++--------------- src/components/0_Bruddle/Button.tsx | 2 +- src/components/Home/HomeHistory.tsx | 6 +- src/components/Home/TransactionCard.tsx | 112 +++++++ tailwind.config.js | 1 + 5 files changed, 261 insertions(+), 234 deletions(-) create mode 100644 src/components/Home/TransactionCard.tsx diff --git a/src/app/(mobile-ui)/home/page.tsx b/src/app/(mobile-ui)/home/page.tsx index 606f4cd70..0f012cdf0 100644 --- a/src/app/(mobile-ui)/home/page.tsx +++ b/src/app/(mobile-ui)/home/page.tsx @@ -1,72 +1,28 @@ 'use client' -import { PEANUT_LOGO_BLACK } from '@/assets' -import DirectionalActionButtons from '@/components/Global/DirectionalActionButtons' -import LogoutButton from '@/components/Global/LogoutButton' +import { Button, ButtonSize, ButtonVariant } from '@/components/0_Bruddle' +import Icon from '@/components/Global/Icon' import PeanutLoading from '@/components/Global/PeanutLoading' import RewardsModal from '@/components/Global/RewardsModal' -import { WalletCard } from '@/components/Home/WalletCard' import HomeHistory from '@/components/Home/HomeHistory' -import PeanutWalletActions from '@/components/PeanutWalletActions' -import ProfileSection from '@/components/Profile/Components/ProfileSection' +import TransactionCard from '@/components/Home/TransactionCard' import { useAuth } from '@/context/authContext' import { useWallet } from '@/hooks/wallet/useWallet' -import { useWalletConnection } from '@/hooks/wallet/useWalletConnection' -import { WalletProviderType } from '@/interfaces' -import { useAppDispatch, useWalletStore } from '@/redux/hooks' -import { walletActions } from '@/redux/slices/wallet-slice' -import { getUserPreferences, updateUserPreferences } from '@/utils' -import { usePrimaryNameBatch } from '@justaname.id/react' -import classNames from 'classnames' -import { motion, useAnimation } from 'framer-motion' -import Image from 'next/image' -import { useRouter } from 'next/navigation' -import { useEffect, useMemo, useRef, useState } from 'react' - -const cardWidth = 300 -const cardMargin = 16 +import { formatExtendedNumber, getUserPreferences, printableUsdc, updateUserPreferences } from '@/utils' +import Link from 'next/link' +import { useMemo, useState } from 'react' +import { twMerge } from 'tailwind-merge' export default function Home() { - const dispatch = useAppDispatch() - const controls = useAnimation() - const router = useRouter() - const carouselRef = useRef(null) - const { connectWallet } = useWalletConnection() + const { peanutWalletDetails } = useWallet() const [isBalanceHidden, setIsBalanceHidden] = useState(() => { const prefs = getUserPreferences() return prefs?.balanceHidden ?? false }) - // state to track if content is ready to show - const [contentReady, setContentReady] = useState(false) - const { username, isFetchingUser } = useAuth() - const { selectedWallet, wallets, isWalletConnected, isFetchingWallets } = useWallet() - const { focusedWallet: focusedWalletId } = useWalletStore() - - const [focusedIndex, setFocusedIndex] = useState(0) - - // Fetch ENS names for all wallet addresses - const walletAddresses = useMemo(() => wallets.map((wallet) => wallet.address), [wallets]) - const { allPrimaryNames } = usePrimaryNameBatch({ - addresses: walletAddresses, - enabled: !!walletAddresses.length, - }) - - // update focusedIndex and focused wallet when selectedWallet changes - useEffect(() => { - const index = wallets.findIndex((wallet) => wallet.id === selectedWallet?.id) - if (index !== -1) { - setFocusedIndex(index) - dispatch(walletActions.setFocusedWallet(wallets[index])) - } - }, [selectedWallet, wallets]) - - const hasWallets = useMemo(() => wallets.length > 0, [wallets]) - const totalCards = useMemo(() => (hasWallets ? wallets.length + 1 : 1), [hasWallets, wallets]) - const handleToggleBalanceVisibility = (e: React.MouseEvent) => { e.stopPropagation() setIsBalanceHidden((prev: boolean) => { @@ -76,197 +32,155 @@ export default function Home() { }) } - useEffect(() => { - if (!hasWallets || isFetchingWallets) return - controls.start({ - x: -(focusedIndex * (cardWidth + cardMargin)), - transition: { type: 'spring', stiffness: 300, damping: 30 }, - }) - }, [focusedIndex, controls, hasWallets, isFetchingWallets]) - - const handleCardClick = (index: number) => { - if (index < wallets.length) { - const wallet = wallets[index] - - if (focusedIndex !== index) { - setFocusedIndex(index) - dispatch(walletActions.setFocusedWallet(wallet)) - controls.start({ - x: -(index * (cardWidth + cardMargin)), - transition: { type: 'spring', stiffness: 300, damping: 30 }, - }) + const isLoading = isFetchingUser && !username - // check wallet type and ID - const isValidPeanutWallet = - wallet.id.startsWith('peanut-wallet') && wallet.walletProviderType === WalletProviderType.PEANUT - - const isValidRewardsWallet = - wallet.id === 'pinta-wallet' && wallet.walletProviderType === WalletProviderType.REWARDS - - const isValidExternalWallet = - wallet.walletProviderType === WalletProviderType.BYOW && isWalletConnected(wallet) - - if (isValidPeanutWallet || isValidRewardsWallet || isValidExternalWallet) { - dispatch(walletActions.setSelectedWalletId(wallet.id)) - } - return - } - - if (focusedIndex === index) { - router.push('/wallet') - } - } + if (isLoading) { + return } - const handleDragEnd = (_e: any, { offset, velocity }: any) => { - const swipe = Math.abs(offset.x) * velocity.x - let targetIndex = focusedIndex - - if (swipe < -10000) { - targetIndex = Math.min(focusedIndex + 1, totalCards - 1) - } else if (swipe > 10000) { - targetIndex = Math.max(focusedIndex - 1, 0) - } - - setFocusedIndex(targetIndex) + return ( +
+
+ + + + + + + + + + + +
- if (targetIndex < wallets.length) { - const targetWallet = wallets[targetIndex] - dispatch(walletActions.setFocusedWallet(targetWallet)) + {/* Transaction cards - temperorary */} +
+ + + + + + + +
- // check wallet type and ID - const isValidPeanutWallet = - targetWallet.id.startsWith('peanut-wallet') && - targetWallet.walletProviderType === WalletProviderType.PEANUT - const isValidRewardsWallet = - targetWallet.id === 'pinta-wallet' && targetWallet.walletProviderType === WalletProviderType.REWARDS - const isValidExternalWallet = - targetWallet.walletProviderType === WalletProviderType.BYOW && isWalletConnected(targetWallet) + + +
+ ) +} - if (isValidPeanutWallet || isValidRewardsWallet || isValidExternalWallet) { - dispatch(walletActions.setSelectedWalletId(targetWallet.id)) - } +function WalletBalance({ + balance, + isBalanceHidden, + onToggleBalanceVisibility, +}: { + balance: bigint + isBalanceHidden: boolean + onToggleBalanceVisibility: (e: React.MouseEvent) => void +}) { + const balanceDisplay = useMemo(() => { + if (isBalanceHidden) { + return ( + + * * * * + + ) } - controls.start({ - x: -(targetIndex * (cardWidth + cardMargin)), - transition: { type: 'spring', stiffness: 300, damping: 30 }, - }) - } - - const isAddWalletFocused = useMemo(() => { - if ((wallets?.length ?? 0) === 0) return true - return wallets.length <= focusedIndex - }, [focusedIndex, wallets?.length]) - - const isLoading = (isFetchingWallets && !wallets.length) || (isFetchingUser && !username) + return formatExtendedNumber(printableUsdc(balance)) + }, [isBalanceHidden, balance]) - // use effect to delay showing content - useEffect(() => { - if (!isLoading) { - // small delay to ensure everything is loaded - const timer = setTimeout(() => { - setContentReady(true) - }, 100) + return ( +
+

+ {' '} + $ + {balanceDisplay} +

+ + +
+ ) +} - return () => clearTimeout(timer) - } else { - setContentReady(false) +function ActionButton({ + label, + action, + href, + variant = 'primary-soft', + size = 'small', +}: { + label: string + action: 'add' | 'withdraw' | 'send' | 'request' + href: string + variant?: ButtonVariant + size?: ButtonSize +}) { + // get icon based on action type + const renderIcon = () => { + switch (action) { + case 'send': + return + case 'withdraw': + return + case 'add': + return + case 'request': + return + default: + return null } - }, [isLoading]) - - // show loading if we're loading or content isn't ready yet - if (isLoading || !contentReady) { - return } return ( -
-
-
-
-
-
- Peanut Logo - -
-
- -
- -
-
0, - })} - style={{ - marginRight: -cardMargin, - marginLeft: -cardMargin, - }} - > - {hasWallets ? ( - - {!!wallets.length && - wallets.map((wallet, index) => ( - handleCardClick(index)} - index={index} - isBalanceHidden={isBalanceHidden} - onToggleBalanceVisibility={handleToggleBalanceVisibility} - isFocused={focusedIndex === index} - /> - ))} - - - - ) : ( -
- -
- )} -
-
- -
- {isAddWalletFocused ? null : focusedWalletId && - wallets.find((w) => w.id === focusedWalletId)?.walletProviderType === - WalletProviderType.REWARDS ? null : wallets.find((w) => w.id === focusedWalletId) - ?.walletProviderType === WalletProviderType.PEANUT ? ( - - ) : focusedWalletId && - wallets.find((w) => w.id === focusedWalletId) && - isWalletConnected(wallets.find((w) => w.id === focusedWalletId)!) ? ( -
- -
- ) : null} -
-
-
- - -
+ + + ) } + +function ActionButtonGroup({ children }: { children: React.ReactNode }) { + return
{children}
+} diff --git a/src/components/0_Bruddle/Button.tsx b/src/components/0_Bruddle/Button.tsx index ca7db4dd0..82ddfbc60 100644 --- a/src/components/0_Bruddle/Button.tsx +++ b/src/components/0_Bruddle/Button.tsx @@ -12,7 +12,7 @@ export type ButtonVariant = | 'yellow' | 'transparent' | 'primary-soft' -type ButtonSize = 'small' | 'medium' | 'large' | 'xl' | 'xl-fixed' +export type ButtonSize = 'small' | 'medium' | 'large' | 'xl' | 'xl-fixed' type ButtonShape = 'default' | 'square' type ShadowSize = '4' | '6' | '8' type ShadowType = 'primary' | 'secondary' diff --git a/src/components/Home/HomeHistory.tsx b/src/components/Home/HomeHistory.tsx index 845c4bca0..e73fb9daf 100644 --- a/src/components/Home/HomeHistory.tsx +++ b/src/components/Home/HomeHistory.tsx @@ -10,11 +10,11 @@ import { IDashboardItem } from '@/interfaces' import { formatAmount, formatDate, + formatPaymentStatus, getChainLogo, getHistoryTransactionStatus, getTokenLogo, isStableCoin, - formatPaymentStatus, } from '@/utils' import * as Sentry from '@sentry/nextjs' import { useInfiniteQuery } from '@tanstack/react-query' @@ -126,7 +126,7 @@ const HomeHistory = () => { if (dashboardData.length === 0) { return ( -
+

Recent Transactions

No transactions yet

@@ -136,7 +136,7 @@ const HomeHistory = () => { } return ( -
+

Recent Transactions

{!!data?.pages.length && diff --git a/src/components/Home/TransactionCard.tsx b/src/components/Home/TransactionCard.tsx new file mode 100644 index 000000000..4d12cec86 --- /dev/null +++ b/src/components/Home/TransactionCard.tsx @@ -0,0 +1,112 @@ +import Icon from '@/components/Global/Icon' +import { formatExtendedNumber, printableUsdc } from '@/utils' +import React from 'react' +import { twMerge } from 'tailwind-merge' +import { NavIcons } from '../0_Bruddle/icons' + +export type TransactionType = 'send' | 'withdraw' | 'add' | 'request' +export type TransactionStatus = 'completed' | 'pending' + +interface TransactionCardProps { + type: TransactionType + name: string + amount: bigint + status: TransactionStatus + initials?: string +} + +const TransactionCard: React.FC = ({ type, name, amount, status, initials = '' }) => { + // determine if amount should be displayed as positive or negative + const isNegative = type === 'send' || type === 'withdraw' + const displayAmount = isNegative + ? `-$${formatExtendedNumber(printableUsdc(amount))}` + : `+$${formatExtendedNumber(printableUsdc(amount))}` + + // for request and send type, show the raw amount without sign + const finalAmount = + type === 'request' || type === 'send' ? `$${formatExtendedNumber(printableUsdc(amount))}` : displayAmount + + return ( +
+
+
+ {/* Icon or Initials based on transaction type */} +
+ {renderIcon(type, initials)} +
+ +
+
{name}
+
+ + {type} +
+
+
+ +
+ {finalAmount} + + {status === 'completed' ? 'Completed' : 'Pending'} + +
+
+
+ ) +} + +// helper functions +function getIconBackgroundColor(type: TransactionType): string { + switch (type) { + case 'send': + case 'request': + return 'bg-success-1 text-black' + case 'withdraw': + return 'bg-black text-white' + case 'add': + return 'bg-black text-white' + default: + return 'bg-gray-200' + } +} + +function renderIcon(type: TransactionType, initials: string): React.ReactNode { + switch (type) { + case 'send': + case 'request': + return initials.substring(0, 2).toUpperCase() + case 'withdraw': + return + case 'add': + return + default: + return null + } +} + +function getActionIcon(type: TransactionType): string { + switch (type) { + case 'send': + return 'arrow-up-right' + case 'withdraw': + return 'arrow-up' + case 'add': + return 'arrow-up' + case 'request': + return 'arrow-down-left' + default: + return 'circle' + } +} + +export default TransactionCard diff --git a/tailwind.config.js b/tailwind.config.js index db3c91a67..6637c7750 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -86,6 +86,7 @@ module.exports = { }, success: { 1: '#16B413', + 2: '#C7F9C6', }, white: '#FFFFFF', red: '#FF0000', From 589411b4b8cc4b161c0ebb7fc0717b0da0ee2481 Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Wed, 16 Apr 2025 14:26:37 +0530 Subject: [PATCH 02/10] feat: import icons from figma --- src/components/Global/Icons/Icon.tsx | 66 +++++++++++++++++++ .../Global/Icons/arrow-down-left.tsx | 10 +++ src/components/Global/Icons/arrow-down.tsx | 10 +++ .../Global/Icons/arrow-up-right.tsx | 10 +++ src/components/Global/Icons/arrow-up.tsx | 10 +++ src/components/Global/Icons/bank.tsx | 10 +++ src/components/Global/Icons/check.tsx | 10 +++ src/components/Global/Icons/exchange.tsx | 10 +++ src/components/Global/Icons/fees.tsx | 14 ++++ src/components/Global/Icons/home.tsx | 10 +++ src/components/Global/Icons/open.tsx | 10 +++ .../Global/Icons/peanut-support.tsx | 25 +++++++ src/components/Global/Icons/search.tsx | 10 +++ src/components/Global/Icons/txn-off.tsx | 34 ++++++++++ src/components/Global/Icons/wallet.tsx | 12 ++++ src/components/Home/TransactionCard.tsx | 30 ++++----- 16 files changed, 262 insertions(+), 19 deletions(-) create mode 100644 src/components/Global/Icons/Icon.tsx create mode 100644 src/components/Global/Icons/arrow-down-left.tsx create mode 100644 src/components/Global/Icons/arrow-down.tsx create mode 100644 src/components/Global/Icons/arrow-up-right.tsx create mode 100644 src/components/Global/Icons/arrow-up.tsx create mode 100644 src/components/Global/Icons/bank.tsx create mode 100644 src/components/Global/Icons/check.tsx create mode 100644 src/components/Global/Icons/exchange.tsx create mode 100644 src/components/Global/Icons/fees.tsx create mode 100644 src/components/Global/Icons/home.tsx create mode 100644 src/components/Global/Icons/open.tsx create mode 100644 src/components/Global/Icons/peanut-support.tsx create mode 100644 src/components/Global/Icons/search.tsx create mode 100644 src/components/Global/Icons/txn-off.tsx create mode 100644 src/components/Global/Icons/wallet.tsx diff --git a/src/components/Global/Icons/Icon.tsx b/src/components/Global/Icons/Icon.tsx new file mode 100644 index 000000000..b30d3c307 --- /dev/null +++ b/src/components/Global/Icons/Icon.tsx @@ -0,0 +1,66 @@ +import { ComponentType, FC, SVGProps } from 'react' +import { ArrowDownIcon } from './arrow-down' +import { ArrowDownLeftIcon } from './arrow-down-left' +import { ArrowUpIcon } from './arrow-up' +import { ArrowUpRightIcon } from './arrow-up-right' +import { BankIcon } from './bank' +import { CheckIcon } from './check' +import { ExchangeIcon } from './exchange' +import { FeesIcon } from './fees' +import { HomeIcon } from './home' +import { OpenIcon } from './open' +import { PeanutSupportIcon } from './peanut-support' +import { SearchIcon } from './search' +import { TxnOffIcon } from './txn-off' +import { WalletIcon } from './wallet' + +// allowed icon names +export type IconName = + | 'arrow-down' + | 'arrow-down-left' + | 'arrow-up' + | 'arrow-up-right' + | 'check' + | 'exchange' + | 'fees' + | 'home' + | 'open' + | 'peanut-support' + | 'search' + | 'txn-off' + | 'bank' + | 'wallet' + +export interface IconProps extends SVGProps { + name: IconName + size?: number | string +} + +// icon names mapping to their components +const iconComponents: Record>> = { + 'arrow-down': ArrowDownIcon, + 'arrow-down-left': ArrowDownLeftIcon, + 'arrow-up': ArrowUpIcon, + 'arrow-up-right': ArrowUpRightIcon, + check: CheckIcon, + exchange: ExchangeIcon, + fees: FeesIcon, + home: HomeIcon, + open: OpenIcon, + 'peanut-support': PeanutSupportIcon, + search: SearchIcon, + 'txn-off': TxnOffIcon, + bank: BankIcon, + wallet: WalletIcon, +} + +export const Icon: FC = ({ name, size = 24, width, height, ...props }) => { + const IconComponent = iconComponents[name] + + if (!IconComponent) { + console.warn(`Icon "${name}" not found`) + return null + } + + return +} diff --git a/src/components/Global/Icons/arrow-down-left.tsx b/src/components/Global/Icons/arrow-down-left.tsx new file mode 100644 index 000000000..e0995e3af --- /dev/null +++ b/src/components/Global/Icons/arrow-down-left.tsx @@ -0,0 +1,10 @@ +import { FC, SVGProps } from 'react' + +export const ArrowDownLeftIcon: FC> = (props) => ( + + + +) diff --git a/src/components/Global/Icons/arrow-down.tsx b/src/components/Global/Icons/arrow-down.tsx new file mode 100644 index 000000000..2c804788c --- /dev/null +++ b/src/components/Global/Icons/arrow-down.tsx @@ -0,0 +1,10 @@ +import { FC, SVGProps } from 'react' + +export const ArrowDownIcon: FC> = (props) => ( + + + +) diff --git a/src/components/Global/Icons/arrow-up-right.tsx b/src/components/Global/Icons/arrow-up-right.tsx new file mode 100644 index 000000000..91878d52d --- /dev/null +++ b/src/components/Global/Icons/arrow-up-right.tsx @@ -0,0 +1,10 @@ +import { FC, SVGProps } from 'react' + +export const ArrowUpRightIcon: FC> = (props) => ( + + + +) diff --git a/src/components/Global/Icons/arrow-up.tsx b/src/components/Global/Icons/arrow-up.tsx new file mode 100644 index 000000000..f394f6138 --- /dev/null +++ b/src/components/Global/Icons/arrow-up.tsx @@ -0,0 +1,10 @@ +import { FC, SVGProps } from 'react' + +export const ArrowUpIcon: FC> = (props) => ( + + + +) diff --git a/src/components/Global/Icons/bank.tsx b/src/components/Global/Icons/bank.tsx new file mode 100644 index 000000000..28021c15d --- /dev/null +++ b/src/components/Global/Icons/bank.tsx @@ -0,0 +1,10 @@ +import { FC, SVGProps } from 'react' + +export const BankIcon: FC> = (props) => ( + + + +) diff --git a/src/components/Global/Icons/check.tsx b/src/components/Global/Icons/check.tsx new file mode 100644 index 000000000..d50c6e9fe --- /dev/null +++ b/src/components/Global/Icons/check.tsx @@ -0,0 +1,10 @@ +import { FC, SVGProps } from 'react' + +export const CheckIcon: FC> = (props) => ( + + + +) diff --git a/src/components/Global/Icons/exchange.tsx b/src/components/Global/Icons/exchange.tsx new file mode 100644 index 000000000..f1146b66c --- /dev/null +++ b/src/components/Global/Icons/exchange.tsx @@ -0,0 +1,10 @@ +import { FC, SVGProps } from 'react' + +export const ExchangeIcon: FC> = (props) => ( + + + +) diff --git a/src/components/Global/Icons/fees.tsx b/src/components/Global/Icons/fees.tsx new file mode 100644 index 000000000..0b2fee362 --- /dev/null +++ b/src/components/Global/Icons/fees.tsx @@ -0,0 +1,14 @@ +import { FC, SVGProps } from 'react' + +export const FeesIcon: FC> = (props) => ( + + + + +) diff --git a/src/components/Global/Icons/home.tsx b/src/components/Global/Icons/home.tsx new file mode 100644 index 000000000..d8a9d640c --- /dev/null +++ b/src/components/Global/Icons/home.tsx @@ -0,0 +1,10 @@ +import { FC, SVGProps } from 'react' + +export const HomeIcon: FC> = (props) => ( + + + +) diff --git a/src/components/Global/Icons/open.tsx b/src/components/Global/Icons/open.tsx new file mode 100644 index 000000000..1ae4f677b --- /dev/null +++ b/src/components/Global/Icons/open.tsx @@ -0,0 +1,10 @@ +import { FC, SVGProps } from 'react' + +export const OpenIcon: FC> = (props) => ( + + + +) diff --git a/src/components/Global/Icons/peanut-support.tsx b/src/components/Global/Icons/peanut-support.tsx new file mode 100644 index 000000000..4dbf82008 --- /dev/null +++ b/src/components/Global/Icons/peanut-support.tsx @@ -0,0 +1,25 @@ +import { FC, SVGProps } from 'react' + +export const PeanutSupportIcon: FC> = (props) => ( + + + + + + + +) diff --git a/src/components/Global/Icons/search.tsx b/src/components/Global/Icons/search.tsx new file mode 100644 index 000000000..c5cd646fc --- /dev/null +++ b/src/components/Global/Icons/search.tsx @@ -0,0 +1,10 @@ +import { FC, SVGProps } from 'react' + +export const SearchIcon: FC> = (props) => ( + + + +) diff --git a/src/components/Global/Icons/txn-off.tsx b/src/components/Global/Icons/txn-off.tsx new file mode 100644 index 000000000..05328d81f --- /dev/null +++ b/src/components/Global/Icons/txn-off.tsx @@ -0,0 +1,34 @@ +import { FC, SVGProps } from 'react' + +export const TxnOffIcon: FC> = (props) => ( + + + + + + + + + +) diff --git a/src/components/Global/Icons/wallet.tsx b/src/components/Global/Icons/wallet.tsx new file mode 100644 index 000000000..983db1724 --- /dev/null +++ b/src/components/Global/Icons/wallet.tsx @@ -0,0 +1,12 @@ +import { FC, SVGProps } from 'react' + +export const WalletIcon: FC> = (props) => ( + <> + + + + +) diff --git a/src/components/Home/TransactionCard.tsx b/src/components/Home/TransactionCard.tsx index 4d12cec86..3bf359dab 100644 --- a/src/components/Home/TransactionCard.tsx +++ b/src/components/Home/TransactionCard.tsx @@ -1,8 +1,6 @@ -import Icon from '@/components/Global/Icon' +import { Icon } from '@/components/Global/Icons/Icon' import { formatExtendedNumber, printableUsdc } from '@/utils' import React from 'react' -import { twMerge } from 'tailwind-merge' -import { NavIcons } from '../0_Bruddle/icons' export type TransactionType = 'send' | 'withdraw' | 'add' | 'request' export type TransactionStatus = 'completed' | 'pending' @@ -39,12 +37,8 @@ const TransactionCard: React.FC = ({ type, name, amount, s
{name}
-
- +
+ {getActionIcon(type)} {type}
@@ -86,26 +80,24 @@ function renderIcon(type: TransactionType, initials: string): React.ReactNode { case 'request': return initials.substring(0, 2).toUpperCase() case 'withdraw': - return + return case 'add': - return + return default: return null } } -function getActionIcon(type: TransactionType): string { +function getActionIcon(type: TransactionType): React.ReactNode { switch (type) { case 'send': - return 'arrow-up-right' + return + case 'request': + return case 'withdraw': - return 'arrow-up' + return case 'add': - return 'arrow-up' - case 'request': - return 'arrow-down-left' - default: - return 'circle' + return } } From 2220ecdd72f7d97b2f26ea32027bd0692d3e26f9 Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Wed, 16 Apr 2025 14:50:39 +0530 Subject: [PATCH 03/10] feat: update more iconsss --- src/app/(mobile-ui)/home/page.tsx | 34 +++++++++++-------- src/components/Global/Icons/Icon.tsx | 10 ++++-- src/components/Global/Icons/eye-slash.tsx | 10 ++++++ src/components/Global/Icons/eye.tsx | 10 ++++++ .../Global/WalletNavigation/index.tsx | 13 +++---- 5 files changed, 55 insertions(+), 22 deletions(-) create mode 100644 src/components/Global/Icons/eye-slash.tsx create mode 100644 src/components/Global/Icons/eye.tsx diff --git a/src/app/(mobile-ui)/home/page.tsx b/src/app/(mobile-ui)/home/page.tsx index 0f012cdf0..3b26b156f 100644 --- a/src/app/(mobile-ui)/home/page.tsx +++ b/src/app/(mobile-ui)/home/page.tsx @@ -1,7 +1,7 @@ 'use client' import { Button, ButtonSize, ButtonVariant } from '@/components/0_Bruddle' -import Icon from '@/components/Global/Icon' +import { Icon } from '@/components/Global/Icons/Icon' import PeanutLoading from '@/components/Global/PeanutLoading' import RewardsModal from '@/components/Global/RewardsModal' import HomeHistory from '@/components/Home/HomeHistory' @@ -148,19 +148,25 @@ function ActionButton({ size?: ButtonSize }) { // get icon based on action type - const renderIcon = () => { - switch (action) { - case 'send': - return - case 'withdraw': - return - case 'add': - return - case 'request': - return - default: - return null - } + const renderIcon = (): React.ReactNode => { + return ( +
+ {(() => { + switch (action) { + case 'send': + return + case 'withdraw': + return + case 'add': + return + case 'request': + return + default: + return null + } + })()} +
+ ) } return ( diff --git a/src/components/Global/Icons/Icon.tsx b/src/components/Global/Icons/Icon.tsx index b30d3c307..6821fb900 100644 --- a/src/components/Global/Icons/Icon.tsx +++ b/src/components/Global/Icons/Icon.tsx @@ -6,6 +6,8 @@ import { ArrowUpRightIcon } from './arrow-up-right' import { BankIcon } from './bank' import { CheckIcon } from './check' import { ExchangeIcon } from './exchange' +import { EyeIcon } from './eye' +import { EyeSlashIcon } from './eye-slash' import { FeesIcon } from './fees' import { HomeIcon } from './home' import { OpenIcon } from './open' @@ -20,7 +22,10 @@ export type IconName = | 'arrow-down-left' | 'arrow-up' | 'arrow-up-right' + | 'bank' | 'check' + | 'eye' + | 'eye-slash' | 'exchange' | 'fees' | 'home' @@ -28,7 +33,6 @@ export type IconName = | 'peanut-support' | 'search' | 'txn-off' - | 'bank' | 'wallet' export interface IconProps extends SVGProps { @@ -42,7 +46,10 @@ const iconComponents: Record>> = 'arrow-down-left': ArrowDownLeftIcon, 'arrow-up': ArrowUpIcon, 'arrow-up-right': ArrowUpRightIcon, + bank: BankIcon, check: CheckIcon, + eye: EyeIcon, + 'eye-slash': EyeSlashIcon, exchange: ExchangeIcon, fees: FeesIcon, home: HomeIcon, @@ -50,7 +57,6 @@ const iconComponents: Record>> = 'peanut-support': PeanutSupportIcon, search: SearchIcon, 'txn-off': TxnOffIcon, - bank: BankIcon, wallet: WalletIcon, } diff --git a/src/components/Global/Icons/eye-slash.tsx b/src/components/Global/Icons/eye-slash.tsx new file mode 100644 index 000000000..7ea3336ff --- /dev/null +++ b/src/components/Global/Icons/eye-slash.tsx @@ -0,0 +1,10 @@ +import { FC, SVGProps } from 'react' + +export const EyeSlashIcon: FC> = (props) => ( + + + +) diff --git a/src/components/Global/Icons/eye.tsx b/src/components/Global/Icons/eye.tsx new file mode 100644 index 000000000..5ffbc8154 --- /dev/null +++ b/src/components/Global/Icons/eye.tsx @@ -0,0 +1,10 @@ +import { FC, SVGProps } from 'react' + +export const EyeIcon: FC> = (props) => ( + + + +) diff --git a/src/components/Global/WalletNavigation/index.tsx b/src/components/Global/WalletNavigation/index.tsx index a789f3850..1c6dffc2d 100644 --- a/src/components/Global/WalletNavigation/index.tsx +++ b/src/components/Global/WalletNavigation/index.tsx @@ -1,11 +1,12 @@ import { PEANUT_LOGO } from '@/assets' -import { NavIcons, NavIconsName } from '@/components/0_Bruddle' +import { NavIconsName } from '@/components/0_Bruddle' +import DirectSendQr from '@/components/Global/DirectSendQR' +import Icon from '@/components/Global/Icon' +import { Icon as NavIcon } from '@/components/Global/Icons/Icon' import classNames from 'classnames' import Image from 'next/image' import Link from 'next/link' import { usePathname } from 'next/navigation' -import Icon from '@/components/Global/Icon' -import DirectSendQr from '@/components/Global/DirectSendQR' type NavPathProps = { name: string @@ -76,8 +77,8 @@ const MobileNav: React.FC = ({ pathName }) => ( { 'text-primary-1': pathName === '/home' } )} > - - Home + + Home {/* QR Button - Main Action */} @@ -92,7 +93,7 @@ const MobileNav: React.FC = ({ pathName }) => ( { 'text-primary-1': pathName === '/support' } )} > - + Support
From 35f2590b85d46b1fdd3a2ffed1af875901a79dde Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Wed, 16 Apr 2025 17:46:09 +0530 Subject: [PATCH 04/10] feat: rewards details on homepage --- src/app/(mobile-ui)/home/page.tsx | 114 ++++++++++++++++++------ src/components/Global/Card/index.tsx | 57 ++++++++++++ src/components/Home/TransactionCard.tsx | 17 +++- 3 files changed, 160 insertions(+), 28 deletions(-) create mode 100644 src/components/Global/Card/index.tsx diff --git a/src/app/(mobile-ui)/home/page.tsx b/src/app/(mobile-ui)/home/page.tsx index 3b26b156f..10c2aa954 100644 --- a/src/app/(mobile-ui)/home/page.tsx +++ b/src/app/(mobile-ui)/home/page.tsx @@ -1,6 +1,8 @@ 'use client' +import { PeanutArmHoldingBeer } from '@/assets' import { Button, ButtonSize, ButtonVariant } from '@/components/0_Bruddle' +import Card from '@/components/Global/Card' import { Icon } from '@/components/Global/Icons/Icon' import PeanutLoading from '@/components/Global/PeanutLoading' import RewardsModal from '@/components/Global/RewardsModal' @@ -9,12 +11,14 @@ import TransactionCard from '@/components/Home/TransactionCard' import { useAuth } from '@/context/authContext' import { useWallet } from '@/hooks/wallet/useWallet' import { formatExtendedNumber, getUserPreferences, printableUsdc, updateUserPreferences } from '@/utils' +import Image from 'next/image' import Link from 'next/link' -import { useMemo, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { twMerge } from 'tailwind-merge' export default function Home() { - const { peanutWalletDetails } = useWallet() + const { peanutWalletDetails, getRewardWalletBalance } = useWallet() + const [rewardsBalance, setRewardsBalance] = useState(undefined) const [isBalanceHidden, setIsBalanceHidden] = useState(() => { const prefs = getUserPreferences() @@ -34,6 +38,19 @@ export default function Home() { const isLoading = isFetchingUser && !username + useEffect(() => { + const fetchRewardsBalance = async () => { + try { + const balance = await getRewardWalletBalance() + setRewardsBalance(balance) + } catch (error) { + console.error('Failed to fetch rewards balance:', error) + } + } + + fetchRewardsBalance() + }, [getRewardWalletBalance]) + if (isLoading) { return } @@ -64,32 +81,47 @@ export default function Home() {
- {/* Transaction cards - temperorary */} -
- + {/* Rewards Card - only shows if balance is non-zero */} + - + {/* Transaction cards - temporary */} +
+

Transactions

+
+ - + - + + + +
@@ -190,3 +222,35 @@ function ActionButton({ function ActionButtonGroup({ children }: { children: React.ReactNode }) { return
{children}
} + +function RewardsCard({ balance }: { balance: string | undefined }) { + if (!balance || balance === '0') return null + + return ( +
+

Rewards

+ +
+
+
+ Peanut arm holding beer +
+ + Beers +
+ {balance} +
+
+
+ ) +} diff --git a/src/components/Global/Card/index.tsx b/src/components/Global/Card/index.tsx new file mode 100644 index 000000000..4b5ba9840 --- /dev/null +++ b/src/components/Global/Card/index.tsx @@ -0,0 +1,57 @@ +import React from 'react' +import { twMerge } from 'tailwind-merge' + +export type CardPosition = 'single' | 'first' | 'middle' | 'last' + +interface CardProps { + children: React.ReactNode + position?: CardPosition + className?: string + onClick?: () => void + border?: boolean +} + +const Card: React.FC = ({ children, position = 'single', className = '', onClick, border = true }) => { + const getBorderRadius = () => { + switch (position) { + case 'single': + return 'rounded-sm' + case 'first': + return 'rounded-t-sm' + case 'last': + return 'rounded-b-sm' + case 'middle': + return '' + default: + return 'rounded-sm' + } + } + + const getBorder = () => { + if (!border) return '' + + switch (position) { + case 'single': + return 'border border-black' + case 'first': + return 'border border-black' + case 'middle': + return 'border border-black border-t-0' + case 'last': + return 'border border-black border-t-0' + default: + return 'border border-black' + } + } + + return ( +
+ {children} +
+ ) +} + +export default Card diff --git a/src/components/Home/TransactionCard.tsx b/src/components/Home/TransactionCard.tsx index 3bf359dab..7d1d4ec83 100644 --- a/src/components/Home/TransactionCard.tsx +++ b/src/components/Home/TransactionCard.tsx @@ -1,3 +1,4 @@ +import Card, { CardPosition } from '@/components/Global/Card' import { Icon } from '@/components/Global/Icons/Icon' import { formatExtendedNumber, printableUsdc } from '@/utils' import React from 'react' @@ -11,9 +12,19 @@ interface TransactionCardProps { amount: bigint status: TransactionStatus initials?: string + position?: CardPosition + onClick?: () => void } -const TransactionCard: React.FC = ({ type, name, amount, status, initials = '' }) => { +const TransactionCard: React.FC = ({ + type, + name, + amount, + status, + initials = '', + position = 'middle', + onClick, +}) => { // determine if amount should be displayed as positive or negative const isNegative = type === 'send' || type === 'withdraw' const displayAmount = isNegative @@ -25,7 +36,7 @@ const TransactionCard: React.FC = ({ type, name, amount, s type === 'request' || type === 'send' ? `$${formatExtendedNumber(printableUsdc(amount))}` : displayAmount return ( -
+
{/* Icon or Initials based on transaction type */} @@ -55,7 +66,7 @@ const TransactionCard: React.FC = ({ type, name, amount, s
-
+ ) } From 78cd147b9fddd87e3e0bbacfa23fe0d463da36e5 Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Thu, 17 Apr 2025 21:09:25 +0530 Subject: [PATCH 05/10] feat: profile page ui --- src/app/(mobile-ui)/profile/page.tsx | 5 +- .../Global/Badges/AchievementsBadge.tsx | 89 ++++++ src/components/Global/Badges/StatusBadge.tsx | 73 +++++ src/components/Global/Icons/Icon.tsx | 24 +- src/components/Global/Icons/achievements.tsx | 10 + src/components/Global/Icons/chevron-up.tsx | 11 + src/components/Global/Icons/currency.tsx | 10 + src/components/Global/Icons/logout.tsx | 10 + src/components/Global/Icons/open.tsx | 10 - src/components/Global/Icons/share.tsx | 10 + src/components/Global/Icons/smile.tsx | 10 + src/components/Global/Icons/user.tsx | 14 + src/components/Global/NavHeader/index.tsx | 17 +- src/components/Home/TransactionCard.tsx | 12 +- .../Profile/Components/OptionsComponent.tsx | 44 --- .../Profile/Components/ProfileHeader.tsx | 30 --- .../Profile/Components/ProfileSection.tsx | 66 ----- .../Components/ProfileWalletBalance.tsx | 64 ----- .../Profile/Components/SkeletonPage.tsx | 209 -------------- .../Profile/Components/TableComponent.tsx | 254 ------------------ src/components/Profile/Components/Tabs.tsx | 36 --- src/components/Profile/Components/index.ts | 4 - src/components/Profile/ProfileHeader.tsx | 70 +++++ src/components/Profile/ProfileMenuItem.tsx | 58 ++++ src/components/Profile/index.tsx | 238 +++++----------- tailwind.config.js | 12 +- 26 files changed, 479 insertions(+), 911 deletions(-) create mode 100644 src/components/Global/Badges/AchievementsBadge.tsx create mode 100644 src/components/Global/Badges/StatusBadge.tsx create mode 100644 src/components/Global/Icons/achievements.tsx create mode 100644 src/components/Global/Icons/chevron-up.tsx create mode 100644 src/components/Global/Icons/currency.tsx create mode 100644 src/components/Global/Icons/logout.tsx delete mode 100644 src/components/Global/Icons/open.tsx create mode 100644 src/components/Global/Icons/share.tsx create mode 100644 src/components/Global/Icons/smile.tsx create mode 100644 src/components/Global/Icons/user.tsx delete mode 100644 src/components/Profile/Components/OptionsComponent.tsx delete mode 100644 src/components/Profile/Components/ProfileHeader.tsx delete mode 100644 src/components/Profile/Components/ProfileSection.tsx delete mode 100644 src/components/Profile/Components/ProfileWalletBalance.tsx delete mode 100644 src/components/Profile/Components/SkeletonPage.tsx delete mode 100644 src/components/Profile/Components/TableComponent.tsx delete mode 100644 src/components/Profile/Components/Tabs.tsx delete mode 100644 src/components/Profile/Components/index.ts create mode 100644 src/components/Profile/ProfileHeader.tsx create mode 100644 src/components/Profile/ProfileMenuItem.tsx diff --git a/src/app/(mobile-ui)/profile/page.tsx b/src/app/(mobile-ui)/profile/page.tsx index c76ee06a1..e999f17bd 100644 --- a/src/app/(mobile-ui)/profile/page.tsx +++ b/src/app/(mobile-ui)/profile/page.tsx @@ -1,10 +1,9 @@ import { Profile } from '@/components' - import { Metadata } from 'next' export const metadata: Metadata = { - title: 'Peanut Protocol', - description: 'Send to Anyone', + title: 'Profile | Peanut Protocol', + description: 'Manage your Peanut profile', metadataBase: new URL('https://peanut.me'), icons: { diff --git a/src/components/Global/Badges/AchievementsBadge.tsx b/src/components/Global/Badges/AchievementsBadge.tsx new file mode 100644 index 000000000..d5c2598de --- /dev/null +++ b/src/components/Global/Badges/AchievementsBadge.tsx @@ -0,0 +1,89 @@ +import { twMerge } from 'tailwind-merge' +import { Icon, IconName } from '../Icons/Icon' + +interface AchievementsBadgeProps { + icon?: IconName + size?: 'small' | 'medium' | 'large' + color?: string + className?: string + position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' +} + +const AchievementsBadge = ({ + icon = 'check', + size = 'medium', + color = 'bg-success-3', + className, + position = 'top-right', +}: AchievementsBadgeProps) => { + const getContainerSize = () => { + switch (size) { + case 'small': + return 'size-4' + case 'medium': + return 'size-5' + case 'large': + return 'size-6' + default: + return 'size-5' + } + } + + const getIconSize = () => { + switch (size) { + case 'small': + return 8 + case 'medium': + return 10 + case 'large': + return 14 + default: + return 10 + } + } + + const getPadding = () => { + switch (size) { + case 'small': + return 'p-0.5' + case 'medium': + return 'p-1' + case 'large': + return 'p-1.5' + default: + return 'p-1' + } + } + + const getPosition = () => { + switch (position) { + case 'top-right': + return 'right-0 top-0' + case 'top-left': + return 'left-0 top-0' + case 'bottom-right': + return 'right-0 bottom-0' + case 'bottom-left': + return 'left-0 bottom-0' + default: + return 'right-0 top-0' + } + } + + return ( +
+ +
+ ) +} + +export default AchievementsBadge diff --git a/src/components/Global/Badges/StatusBadge.tsx b/src/components/Global/Badges/StatusBadge.tsx new file mode 100644 index 000000000..62b8c3690 --- /dev/null +++ b/src/components/Global/Badges/StatusBadge.tsx @@ -0,0 +1,73 @@ +import React from 'react' +import { twMerge } from 'tailwind-merge' + +export type StatusType = 'completed' | 'pending' | 'failed' | 'cancelled' | 'soon' + +interface StatusBadgeProps { + status: StatusType + className?: string + size?: 'small' | 'medium' | 'large' +} + +const StatusBadge: React.FC = ({ status, className, size = 'small' }) => { + const getStatusStyles = () => { + switch (status) { + case 'completed': + return 'bg-success-2 text-success-1' + case 'pending': + return 'bg-secondary-4 text-secondary-1' + case 'failed': + case 'cancelled': + return 'bg-error-1 text-error' + case 'soon': + return 'bg-primary-3 text-primary-4' + default: + return 'bg-gray-200 text-gray-700' + } + } + + const getStatusText = () => { + switch (status) { + case 'completed': + return 'Completed' + case 'pending': + return 'Pending' + case 'failed': + return 'Failed' + case 'cancelled': + return 'Cancelled' + case 'soon': + return 'Soon!' + default: + return status + } + } + + const getSizeClasses = () => { + switch (size) { + case 'small': + return 'px-2 py-0.5 text-[10px]' + case 'medium': + return 'px-3 py-1 text-xs' + case 'large': + return 'px-4 py-1.5 text-sm' + default: + return 'px-2 py-0.5 text-[10px]' + } + } + + return ( + + {getStatusText()} + + ) +} + +export default StatusBadge diff --git a/src/components/Global/Icons/Icon.tsx b/src/components/Global/Icons/Icon.tsx index 6821fb900..3b7cb254a 100644 --- a/src/components/Global/Icons/Icon.tsx +++ b/src/components/Global/Icons/Icon.tsx @@ -1,19 +1,25 @@ import { ComponentType, FC, SVGProps } from 'react' +import { AchievementsIcon } from './achievements' import { ArrowDownIcon } from './arrow-down' import { ArrowDownLeftIcon } from './arrow-down-left' import { ArrowUpIcon } from './arrow-up' import { ArrowUpRightIcon } from './arrow-up-right' import { BankIcon } from './bank' import { CheckIcon } from './check' +import { ChevronUpIcon } from './chevron-up' +import { CurrencyIcon } from './currency' import { ExchangeIcon } from './exchange' import { EyeIcon } from './eye' import { EyeSlashIcon } from './eye-slash' import { FeesIcon } from './fees' import { HomeIcon } from './home' -import { OpenIcon } from './open' +import { LogoutIcon } from './logout' import { PeanutSupportIcon } from './peanut-support' import { SearchIcon } from './search' +import { ShareIcon } from './share' +import { SmileIcon } from './smile' import { TxnOffIcon } from './txn-off' +import { UserIcon } from './user' import { WalletIcon } from './wallet' // allowed icon names @@ -24,16 +30,22 @@ export type IconName = | 'arrow-up-right' | 'bank' | 'check' + | 'chevron-up' | 'eye' | 'eye-slash' | 'exchange' | 'fees' | 'home' - | 'open' | 'peanut-support' | 'search' | 'txn-off' | 'wallet' + | 'currency' + | 'achievements' + | 'logout' + | 'smile' + | 'user' + | 'share' export interface IconProps extends SVGProps { name: IconName @@ -48,16 +60,22 @@ const iconComponents: Record>> = 'arrow-up-right': ArrowUpRightIcon, bank: BankIcon, check: CheckIcon, + 'chevron-up': ChevronUpIcon, eye: EyeIcon, 'eye-slash': EyeSlashIcon, exchange: ExchangeIcon, fees: FeesIcon, home: HomeIcon, - open: OpenIcon, 'peanut-support': PeanutSupportIcon, search: SearchIcon, 'txn-off': TxnOffIcon, wallet: WalletIcon, + currency: CurrencyIcon, + achievements: AchievementsIcon, + logout: LogoutIcon, + smile: SmileIcon, + user: UserIcon, + share: ShareIcon, } export const Icon: FC = ({ name, size = 24, width, height, ...props }) => { diff --git a/src/components/Global/Icons/achievements.tsx b/src/components/Global/Icons/achievements.tsx new file mode 100644 index 000000000..5975adf8a --- /dev/null +++ b/src/components/Global/Icons/achievements.tsx @@ -0,0 +1,10 @@ +import { FC, SVGProps } from 'react' + +export const AchievementsIcon: FC> = (props) => ( + + + +) diff --git a/src/components/Global/Icons/chevron-up.tsx b/src/components/Global/Icons/chevron-up.tsx new file mode 100644 index 000000000..a19a12161 --- /dev/null +++ b/src/components/Global/Icons/chevron-up.tsx @@ -0,0 +1,11 @@ +import { FC, SVGProps } from 'react' + +interface ChevronUpIconProps extends SVGProps { + size?: number | string +} + +export const ChevronUpIcon: FC = (props) => ( + + + +) diff --git a/src/components/Global/Icons/currency.tsx b/src/components/Global/Icons/currency.tsx new file mode 100644 index 000000000..933c9da2d --- /dev/null +++ b/src/components/Global/Icons/currency.tsx @@ -0,0 +1,10 @@ +import { FC, SVGProps } from 'react' + +export const CurrencyIcon: FC> = (props) => ( + + + +) diff --git a/src/components/Global/Icons/logout.tsx b/src/components/Global/Icons/logout.tsx new file mode 100644 index 000000000..bb2ea8a47 --- /dev/null +++ b/src/components/Global/Icons/logout.tsx @@ -0,0 +1,10 @@ +import { FC, SVGProps } from 'react' + +export const LogoutIcon: FC> = (props) => ( + + + +) diff --git a/src/components/Global/Icons/open.tsx b/src/components/Global/Icons/open.tsx deleted file mode 100644 index 1ae4f677b..000000000 --- a/src/components/Global/Icons/open.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { FC, SVGProps } from 'react' - -export const OpenIcon: FC> = (props) => ( - - - -) diff --git a/src/components/Global/Icons/share.tsx b/src/components/Global/Icons/share.tsx new file mode 100644 index 000000000..87c9904ca --- /dev/null +++ b/src/components/Global/Icons/share.tsx @@ -0,0 +1,10 @@ +import { FC, SVGProps } from 'react' + +export const ShareIcon: FC> = (props) => ( + + + +) diff --git a/src/components/Global/Icons/smile.tsx b/src/components/Global/Icons/smile.tsx new file mode 100644 index 000000000..989aa58be --- /dev/null +++ b/src/components/Global/Icons/smile.tsx @@ -0,0 +1,10 @@ +import { FC, SVGProps } from 'react' + +export const SmileIcon: FC> = (props) => ( + + + +) diff --git a/src/components/Global/Icons/user.tsx b/src/components/Global/Icons/user.tsx new file mode 100644 index 000000000..49f623cae --- /dev/null +++ b/src/components/Global/Icons/user.tsx @@ -0,0 +1,14 @@ +import { FC, SVGProps } from 'react' + +export const UserIcon: FC> = (props) => ( + + + + +) diff --git a/src/components/Global/NavHeader/index.tsx b/src/components/Global/NavHeader/index.tsx index 0868a2aa5..8f5313c6f 100644 --- a/src/components/Global/NavHeader/index.tsx +++ b/src/components/Global/NavHeader/index.tsx @@ -1,23 +1,26 @@ import { Button } from '@/components/0_Bruddle' import Link from 'next/link' -import Icon from '../Icon' +import { Icon } from '../Icons/Icon' interface NavHeaderProps { - title: string + title?: string href?: string + hideLabel?: boolean } -const NavHeader = ({ title, href }: NavHeaderProps) => { +const NavHeader = ({ title, href, hideLabel = false }: NavHeaderProps) => { return (
-
- {title} -
+ {!hideLabel && ( +
+ {title} +
+ )}
) } diff --git a/src/components/Home/TransactionCard.tsx b/src/components/Home/TransactionCard.tsx index 7d1d4ec83..2ef4385eb 100644 --- a/src/components/Home/TransactionCard.tsx +++ b/src/components/Home/TransactionCard.tsx @@ -1,16 +1,16 @@ +import StatusBadge, { StatusType } from '@/components/Global/Badges/StatusBadge' import Card, { CardPosition } from '@/components/Global/Card' import { Icon } from '@/components/Global/Icons/Icon' import { formatExtendedNumber, printableUsdc } from '@/utils' import React from 'react' export type TransactionType = 'send' | 'withdraw' | 'add' | 'request' -export type TransactionStatus = 'completed' | 'pending' interface TransactionCardProps { type: TransactionType name: string amount: bigint - status: TransactionStatus + status: StatusType initials?: string position?: CardPosition onClick?: () => void @@ -57,13 +57,7 @@ const TransactionCard: React.FC = ({
{finalAmount} - - {status === 'completed' ? 'Completed' : 'Pending'} - +
diff --git a/src/components/Profile/Components/OptionsComponent.tsx b/src/components/Profile/Components/OptionsComponent.tsx deleted file mode 100644 index c2d422b29..000000000 --- a/src/components/Profile/Components/OptionsComponent.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import Icon from '@/components/Global/Icon' -import { Menu, Transition } from '@headlessui/react' - -/** - * OptionsComponent renders a menu with a list of action items. - * Each action item is a button that triggers a specified function when clicked. - */ -export const OptionsComponent = ({ - actionItems, -}: { - actionItems: { - name: string - action: () => void - }[] -}) => { - return ( - - - - - - - {actionItems.map((actionItem, index) => ( - -
{actionItem.name}
-
- ))} -
-
-
- ) -} diff --git a/src/components/Profile/Components/ProfileHeader.tsx b/src/components/Profile/Components/ProfileHeader.tsx deleted file mode 100644 index 6f8904e9f..000000000 --- a/src/components/Profile/Components/ProfileHeader.tsx +++ /dev/null @@ -1,30 +0,0 @@ -'use client' -import { Button, NavIcons } from '@/components/0_Bruddle' -import Icon from '@/components/Global/Icon' -import Link from 'next/link' -import { useRouter } from 'next/navigation' - -const ProfileHeader = () => { - const router = useRouter() - return ( -
- - - -
Profile
- - - -
- ) -} -export default ProfileHeader diff --git a/src/components/Profile/Components/ProfileSection.tsx b/src/components/Profile/Components/ProfileSection.tsx deleted file mode 100644 index d61f942e6..000000000 --- a/src/components/Profile/Components/ProfileSection.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { PEANUTMAN_PFP } from '@/assets' -import { Badge } from '@/components/0_Bruddle/Badge' -import CopyToClipboard from '@/components/Global/CopyToClipboard' -import { useAuth } from '@/context/authContext' -import Image from 'next/image' - -interface ProfileSectionProps {} - -const ProfileSection = ({}: ProfileSectionProps) => { - const { user } = useAuth() - - const points = { - Points: user?.points, - Invites: user?.totalReferralPoints, - // todo: implement boost logic - Boost: 0, - } - - return ( -
-
-
- {/*
*/} -
- profile image -
-
-
- peanut.me/ -
-
-
{user?.user.username}
- {user?.user.kycStatus === 'approved' && ( - - KYC Done - - )} -
-
-
-
- -
-
- {/* todo: revisit later, commenting out for now */} - {/*
- { -
- {Object.entries(points).map(([key, value]) => ( -
-
{value}
-
{key}
-
- ))} -
- } -
*/} -
- ) -} - -export default ProfileSection diff --git a/src/components/Profile/Components/ProfileWalletBalance.tsx b/src/components/Profile/Components/ProfileWalletBalance.tsx deleted file mode 100644 index 3c11d3dc1..000000000 --- a/src/components/Profile/Components/ProfileWalletBalance.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { ARBITRUM_ICON } from '@/assets' -import { Card } from '@/components/0_Bruddle' -import { useWallet } from '@/hooks/wallet/useWallet' -import { printableUsdc } from '@/utils' -import Image from 'next/image' -import { useMemo } from 'react' -import { parseUnits } from 'viem' - -const ProfileWalletBalance = () => { - const { selectedWallet } = useWallet() - - const getMaxBalanceToken = useMemo(() => { - if (!selectedWallet || !selectedWallet.balances || selectedWallet.balances.length === 0) return null - - return selectedWallet.balances.reduce((max, current) => { - return current.value > max.value ? current : max - }, selectedWallet.balances[0]) - }, [selectedWallet]) - - return ( -
-
My Peanut wallet
- - -
- {/* todo: figure out a way to get chain image dynamically here */} -
- token logo - token logo -
-
{getMaxBalanceToken?.symbol || 'USDC'}
-
-
-
- ${printableUsdc(parseUnits(getMaxBalanceToken?.value || '0', 6))} -
- {getMaxBalanceToken?.symbol && getMaxBalanceToken?.name && ( -
- {getMaxBalanceToken?.symbol} on {getMaxBalanceToken?.name} -
- )} -
-
-
-
- ) -} - -export default ProfileWalletBalance diff --git a/src/components/Profile/Components/SkeletonPage.tsx b/src/components/Profile/Components/SkeletonPage.tsx deleted file mode 100644 index c49d679c2..000000000 --- a/src/components/Profile/Components/SkeletonPage.tsx +++ /dev/null @@ -1,209 +0,0 @@ -import * as assets from '@/assets' -import Loading from '@/components/Global/Loading' -import { GlobalLoginComponent } from '@/components/Global/LoginComponent' -import { UpdateUserComponent } from '@/components/Global/UpdateUserComponent' -import { useWallet } from '@/hooks/wallet/useWallet' -import { useState } from 'react' - -type ProfileSkeletonProps = { - onClick: () => void - showOverlay?: boolean - errorState: { - showError: boolean - errorMessage: string - } - isLoading: boolean -} - -/** - * ProfileSkeleton is a component that displays a loading skeleton for the profile section of the app. - * It shows animated placeholders while loading or when there is no data. - * Additionally, it handles user login or registration, with an overlay that prompts users to log in or connect a wallet. - * It also handles error states, providing feedback when login or registration encounters an issue. - */ -export const ProfileSkeleton = ({ onClick, showOverlay = true, errorState, isLoading }: ProfileSkeletonProps) => { - const { address, signInModal } = useWallet() - const [userState, setUserState] = useState<'login' | 'register'>('login') - - return ( -
-
-
-
-
-
- -
-
-
-
-
-
- - - -
- - - - - - - - - - - - - - - {[...Array(3)].map((_, idx) => ( - - - - - - - - - - - ))} - -
- - - - - - - - - - - - - - - {' '} - -
- - - - - - - - - - - - - - - -
-
- {[...Array(3)].map((_, idx) => ( -
- - -
-
- - -
-
- -
-
- ))} -
-
-
- - {showOverlay && ( -
-
- {userState === 'login' ? ( - <> - { - // if (status === 'success') { - // handleEmail(watchOfframp()) - // } else { - // setErrorState({ - // showError: true, - // errorMessage: message, - // }) - // } - }} - /> - - Click{' '} - { - setUserState('register') - }} - > - here - {' '} - to register - - - ) : ( - <> - - - Click{' '} - { - setUserState('login') - }} - > - here - {' '} - to login - - - )} - -

Or

-
- {' '} -
-
- )} -
- ) -} diff --git a/src/components/Profile/Components/TableComponent.tsx b/src/components/Profile/Components/TableComponent.tsx deleted file mode 100644 index ceb1297f0..000000000 --- a/src/components/Profile/Components/TableComponent.tsx +++ /dev/null @@ -1,254 +0,0 @@ -'use client' -import AddressLink from '@/components/Global/AddressLink' -import Loading from '@/components/Global/Loading' -import Sorting from '@/components/Global/Sorting' -import * as consts from '@/constants' -import * as interfaces from '@/interfaces' -import * as utils from '@/utils' -import { useRouter } from 'next/navigation' -import { useCallback } from 'react' -import { OptionsComponent } from './OptionsComponent' - -/** - * TableComponent renders a responsive table for displaying profile-related data based on the selected tab (e.g., history, contacts, or accounts). - * It handles sorting, pagination, and action buttons for each row, such as viewing transaction details or sending tokens. - * The component also integrates specific actions using the OptionsComponent, which provides a dropdown for additional functionality like showing transactions in an explorer, copying links, or downloading attachments. - */ -export const TableComponent = ({ - data, - selectedTab, - currentPage, - itemsPerPage, - handleDeleteLink, -}: { - data: interfaces.IProfileTableData[] - selectedTab: 'contacts' | 'history' | 'accounts' | undefined - currentPage: number - itemsPerPage?: number - handleDeleteLink: (link: string) => void -}) => { - const router = useRouter() - const handleSendToAddress = useCallback( - (address: string) => { - router.push(`/send?recipientAddress=${encodeURIComponent(address)}`) - }, - [router] - ) - - return ( - - - {selectedTab === 'history' ? ( - - - - - - - - - - - ) : selectedTab === 'contacts' ? ( - - - - - - - - ) : ( - selectedTab === 'accounts' && ( - - - - {/* */} - - ) - )} - - - {data - .slice((currentPage - 1) * (itemsPerPage as number), currentPage * (itemsPerPage as number)) - .map((data) => - selectedTab === 'history' ? ( - data.dashboardItem && ( - - - - - - - - - - - - ) - ) : selectedTab === 'contacts' ? ( - - - - - - - - ) : ( - selectedTab === 'accounts' && ( - - - - - ) - ) - )} - -
- - - - - - - - - - - - - -
- - - - - -
- - - -
{data.dashboardItem.type} - {utils.formatTokenAmount(Number(data.dashboardItem.amount), 4)}{' '} - {data.dashboardItem.tokenSymbol} - {data.dashboardItem.chain}{utils.formatDate(new Date(data.dashboardItem.date))} - - - - {data.dashboardItem.message ? data.dashboardItem.message : ''} - - - {!data.dashboardItem.status ? ( -
- -
- ) : data.dashboardItem.status === 'claimed' ? ( -
- claimed -
- ) : data.dashboardItem.status === 'transfer' ? ( -
- sent -
- ) : data.dashboardItem.status === 'paid' ? ( -
- paid -
- ) : data.dashboardItem.status ? ( -
- {data.dashboardItem.status.toLowerCase().replaceAll('_', ' ')} -
- ) : ( -
- pending -
- )} -
- { - const chainId = - consts.supportedPeanutChains.find( - (chain) => chain.name === data.dashboardItem?.chain - )?.chainId ?? '' - - const explorerUrl = utils.getExplorerUrl(chainId) - window.open( - `${explorerUrl}/tx/${data?.dashboardItem?.txHash ?? ''}`, - '_blank' - ) - }, - }, - (data.dashboardItem?.type === 'Link Received' || - data.dashboardItem.type === 'Link Sent' || - data.dashboardItem.type === 'Request Link') && - data.dashboardItem?.link && { - name: 'Copy link', - action: () => { - utils.copyTextToClipboardWithFallback( - data.dashboardItem?.link ?? '' - ) - }, - }, - data.dashboardItem?.attachmentUrl && { - name: 'Download attachment', - action: () => { - window.open( - data.dashboardItem?.attachmentUrl ?? '', - '_blank' - ) - }, - }, - data.dashboardItem?.type !== 'Link Received' && - data.dashboardItem?.type !== 'Request Link' && - data.dashboardItem.status === 'pending' && { - name: 'Refund', - action: () => { - window.open(data.dashboardItem?.link ?? '', '_blank') - }, - }, - data.dashboardItem?.type === 'Offramp Claim' && - data.dashboardItem.status !== 'claimed' && { - name: 'Check status', - action: () => { - const url = new URL(data.dashboardItem?.link ?? '') - url.pathname = '/cashout/status' - - window.open(url.toString(), '_blank') - }, - }, - data.dashboardItem.type === 'Request Link' && - data.dashboardItem?.link && - data.dashboardItem.status === 'pending' && { - name: 'Delete', - action: () => { - handleDeleteLink(data.dashboardItem?.link as string) - }, - }, - ].filter(Boolean) as { name: string; action: () => void }[] - } - /> -
-
- -
-
{data.primaryText}{data.tertiaryText}{data.quaternaryText} - { - const recipientAddress = data.address as string - handleSendToAddress(recipientAddress) - }, - }, - ]} - /> -
{data.primaryText}{data.tertiaryText}
- ) -} diff --git a/src/components/Profile/Components/Tabs.tsx b/src/components/Profile/Components/Tabs.tsx deleted file mode 100644 index f90c2702e..000000000 --- a/src/components/Profile/Components/Tabs.tsx +++ /dev/null @@ -1,36 +0,0 @@ -type TabType = { - title: string - value: string - onClick?: () => void -} - -type TabsProps = { - className?: string - classButton?: string - items: TabType[] - value: number | string | undefined - setValue: any -} - -export const Tabs = ({ className, classButton, items, value, setValue }: TabsProps) => { - const handleClick = (value: string, onClick: any) => { - setValue(value) - onClick?.() - } - - return ( -
- {items.map((item, index) => ( - - ))} -
- ) -} diff --git a/src/components/Profile/Components/index.ts b/src/components/Profile/Components/index.ts deleted file mode 100644 index 1097d8ecb..000000000 --- a/src/components/Profile/Components/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './OptionsComponent' -export * from './SkeletonPage' -export * from './TableComponent' -export * from './Tabs' diff --git a/src/components/Profile/ProfileHeader.tsx b/src/components/Profile/ProfileHeader.tsx new file mode 100644 index 000000000..d23d4959c --- /dev/null +++ b/src/components/Profile/ProfileHeader.tsx @@ -0,0 +1,70 @@ +import { Icon } from '@/components/Global/Icons/Icon' +import React, { useState } from 'react' +import { twMerge } from 'tailwind-merge' +import { Button } from '../0_Bruddle' +import AchievementsBadge from '../Global/Badges/AchievementsBadge' +import QRBottomDrawer from '../Global/QRBottomDrawer' + +interface ProfileHeaderProps { + name: string + username: string + initials: string + isVerified?: boolean + className?: string +} + +const ProfileHeader: React.FC = ({ name, username, initials, isVerified = false, className }) => { + const [isQRScannerOpen, setIsQRScannerOpen] = useState(false) + + const profileUrl = `peanut.me/${username}` + + return ( + <> +
+ {/* Avatar with initials */} +
+
+ {initials} +
+ + {isVerified && } +
+ + {/* Name */} +

{name}

+ + {/* Username with share drawer */} + +
+ {isQRScannerOpen && ( + <> + + + )} + + ) +} + +export default ProfileHeader diff --git a/src/components/Profile/ProfileMenuItem.tsx b/src/components/Profile/ProfileMenuItem.tsx new file mode 100644 index 000000000..531c2b1ae --- /dev/null +++ b/src/components/Profile/ProfileMenuItem.tsx @@ -0,0 +1,58 @@ +import StatusBadge from '@/components/Global/Badges/StatusBadge' +import Card, { CardPosition } from '@/components/Global/Card' +import { Icon, IconName } from '@/components/Global/Icons/Icon' +import Link from 'next/link' +import React from 'react' + +interface ProfileMenuItemProps { + icon: IconName + label: string + href?: string + onClick?: () => void + position?: CardPosition + comingSoon?: boolean +} + +const ProfileMenuItem: React.FC = ({ + icon, + label, + href, + onClick, + position = 'middle', + comingSoon = false, +}) => { + const content = ( +
+
+ + {label} +
+ +
+ {comingSoon ? ( + + ) : ( + + )} +
+
+ ) + + if (comingSoon || !href) { + return ( + + {content} + + ) + } + + return ( + + + {content} + + + ) +} + +export default ProfileMenuItem diff --git a/src/components/Profile/index.tsx b/src/components/Profile/index.tsx index f7cb732c6..9bee933cf 100644 --- a/src/components/Profile/index.tsx +++ b/src/components/Profile/index.tsx @@ -1,87 +1,18 @@ 'use client' -import * as context from '@/context' +import { Button } from '@/components/0_Bruddle' +import { Icon } from '@/components/Global/Icons/Icon' +import { loadingStateContext } from '@/context' import { useAuth } from '@/context/authContext' -import { useWallet } from '@/hooks/wallet/useWallet' -import { fetchWithSentry, areEvmAddressesEqual, createSiweMessage } from '@/utils' -import { useContext, useState } from 'react' -import { useSignMessage } from 'wagmi' -import { Button } from '../0_Bruddle' -import AddressLink from '../Global/AddressLink' -import Icon from '../Global/Icon' -import Modal from '../Global/Modal' -import ProfileHeader from './Components/ProfileHeader' -import ProfileSection from './Components/ProfileSection' -import ProfileWalletBalance from './Components/ProfileWalletBalance' -import * as Sentry from '@sentry/nextjs' +import { captureException } from '@sentry/nextjs' +import { useContext } from 'react' +import NavHeader from '../Global/NavHeader' +import ProfileHeader from './ProfileHeader' +import ProfileMenuItem from './ProfileMenuItem' export const Profile = () => { - const { address } = useWallet() - const { setLoadingState, loadingState, isLoading } = useContext(context.loadingStateContext) - const { signMessageAsync } = useSignMessage() - const { user, fetchUser, isFetchingUser, logoutUser } = useAuth() - - const [_isLoading, _setIsLoading] = useState(false) - const [modalVisible, setModalVisible] = useState(false) - const [modalType, setModalType] = useState<'Boost' | 'Invites' | undefined>(undefined) - const [errorState, setErrorState] = useState<{ - showError: boolean - errorMessage: string - }>({ showError: false, errorMessage: '' }) - - const handleSiwe = async () => { - try { - _setIsLoading(true) - setErrorState({ - showError: false, - errorMessage: '', - }) - if (!address) return - - const userIdResponse = await fetchWithSentry('/api/peanut/user/get-user-id', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - accountIdentifier: address, - }), - }) - - const response = await userIdResponse.json() - - const siwemsg = createSiweMessage({ - address: address ?? '', - statement: `Sign in to peanut.to. This is your unique user identifier! ${response.userId}`, - }) - - const signature = await signMessageAsync({ - message: siwemsg, - }) - - await fetchWithSentry('/api/peanut/user/get-jwt-token', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - signature: signature, - message: siwemsg, - }), - }) - - fetchUser() - } catch (error) { - console.error('Authentication error:', error) - setErrorState({ - showError: true, - errorMessage: 'Error while authenticating. Please try again later.', - }) - Sentry.captureException(error) - } finally { - _setIsLoading(false) - } - } + const { setLoadingState, isLoading } = useContext(loadingStateContext) + const { logoutUser, user } = useAuth() const handleLogout = async () => { try { @@ -89,102 +20,73 @@ export const Profile = () => { await logoutUser() } catch (error) { console.error('Error logging out', error) - setErrorState({ - showError: true, - errorMessage: 'Error logging out', - }) - Sentry.captureException(error) + captureException(error) } finally { setLoadingState('Idle') } } - if (!user) { - // TODO: Sign In User Here - return ( -
- -
- ) - } else - return ( -
-
-
-
- - - -
+ const getInitials = (name: string) => { + return name + .split(' ') + .map((part) => part[0]) + .join('') + .toUpperCase() + .substring(0, 2) + } + + const fullName = user?.user.full_name || user?.user?.username || 'Anonymous User' + const username = user?.user.username || 'anonymous' + const initials = getInitials(fullName) + + return ( +
+ +
+ +
+ {/* Menu Item - Invite Entry */} + + {/* Menu Items - First Group */} +
+ + +
- { - setModalVisible(false) - }} - title={modalType} - classNameWrapperDiv="px-5 pb-7 pt-8" + {/* Menu Items - Second Group */} +
+ + + +
+ {/* Logout Button */} +
- ) +
+ ) } diff --git a/tailwind.config.js b/tailwind.config.js index 6637c7750..46f83ab14 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -16,6 +16,7 @@ module.exports = { 1: '#FF90E8', 2: '#CC73BA', 3: '#EFE4FF', + 4: '#BA8BFF', }, secondary: { 1: '#FFC900', @@ -29,6 +30,7 @@ module.exports = { 1: '#5F646D', 2: '#E7E8E9', 3: '#FAF4F0', + 4: '#EFEFF0', }, outline: { 1: '#98E9AB', @@ -48,6 +50,7 @@ module.exports = { 2: '#f5ff7c', 3: '#fbfdd8', 4: '#FAE8A4', + 5: '#FFD25C', }, pink: { 1: '#FF90E8', @@ -81,12 +84,10 @@ module.exports = { cyan: { 8: '#A0E6E0', }, - gold: { - 3: '#FFD25C', - }, success: { 1: '#16B413', 2: '#C7F9C6', + 3: '#29CC6A', }, white: '#FFFFFF', red: '#FF0000', @@ -98,7 +99,10 @@ module.exports = { 'secondary-base': 'var(--secondary-color)', background: '#FAF4F0', accent: 'var(--accent-color)', - error: '#B3261E', + error: { + DEFAULT: '#B3261E', + 1: '#FFD8D8', + }, }, zIndex: { 1: '1', From 7d08f0411032ce3143b2a55b54ae0d11a3473c4c Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Fri, 18 Apr 2025 01:38:12 +0530 Subject: [PATCH 06/10] fix: use drawer component for share profile --- src/components/Global/BottomDrawer/index.tsx | 11 ++++++-- src/components/Profile/ProfileHeader.tsx | 29 ++++++++++++++------ 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/src/components/Global/BottomDrawer/index.tsx b/src/components/Global/BottomDrawer/index.tsx index 4144bb494..612e356fe 100644 --- a/src/components/Global/BottomDrawer/index.tsx +++ b/src/components/Global/BottomDrawer/index.tsx @@ -1,6 +1,6 @@ -import React, { useState, useRef, useEffect, ReactNode } from 'react' -import { createPortal } from 'react-dom' import { Winking } from '@/assets' +import React, { ReactNode, useEffect, useRef, useState } from 'react' +import { createPortal } from 'react-dom' type DrawerPosition = 'collapsed' | 'half' | 'expanded' @@ -10,6 +10,7 @@ interface BottomDrawerProps { onClose?: () => void initialPosition?: DrawerPosition handleTitle?: string + handleSubtitle?: string collapsedHeight?: number halfHeight?: number expandedHeight?: number @@ -23,6 +24,7 @@ const BottomDrawer: React.FC = ({ onClose, initialPosition = 'half', handleTitle = '', + handleSubtitle = '', collapsedHeight = 15, halfHeight = 50, expandedHeight = 90, @@ -240,7 +242,10 @@ const BottomDrawer: React.FC = ({ style={{ cursor: isDragging ? 'grabbing' : 'grab' }} >
- {handleTitle &&

{handleTitle}

} +
+ {handleTitle &&

{handleTitle}

} + {handleSubtitle &&

{handleSubtitle}

} +
{/* Content area */} diff --git a/src/components/Profile/ProfileHeader.tsx b/src/components/Profile/ProfileHeader.tsx index d23d4959c..2658e65ae 100644 --- a/src/components/Profile/ProfileHeader.tsx +++ b/src/components/Profile/ProfileHeader.tsx @@ -2,8 +2,11 @@ import { Icon } from '@/components/Global/Icons/Icon' import React, { useState } from 'react' import { twMerge } from 'tailwind-merge' import { Button } from '../0_Bruddle' +import Divider from '../0_Bruddle/Divider' import AchievementsBadge from '../Global/Badges/AchievementsBadge' -import QRBottomDrawer from '../Global/QRBottomDrawer' +import BottomDrawer from '../Global/BottomDrawer' +import QRCodeWrapper from '../Global/QRCodeWrapper' +import ShareButton from '../Global/ShareButton' interface ProfileHeaderProps { name: string @@ -54,13 +57,23 @@ const ProfileHeader: React.FC = ({ name, username, initials,
{isQRScannerOpen && ( <> - + setIsQRScannerOpen(false)} + > +
+ + + + Your Peanut profile is public + +
+
)} From 6b379acb6ecce75ffeb7f00b38f83c95b4129b9c Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Tue, 22 Apr 2025 17:05:34 +0530 Subject: [PATCH 07/10] fix: desktop responsiveness --- src/app/(mobile-ui)/home/page.tsx | 127 +++++++++++---------- src/app/(mobile-ui)/profile/page.tsx | 7 +- src/components/0_Bruddle/PageContainer.tsx | 2 +- 3 files changed, 72 insertions(+), 64 deletions(-) diff --git a/src/app/(mobile-ui)/home/page.tsx b/src/app/(mobile-ui)/home/page.tsx index 10c2aa954..8f3094ec0 100644 --- a/src/app/(mobile-ui)/home/page.tsx +++ b/src/app/(mobile-ui)/home/page.tsx @@ -2,6 +2,7 @@ import { PeanutArmHoldingBeer } from '@/assets' import { Button, ButtonSize, ButtonVariant } from '@/components/0_Bruddle' +import PageContainer from '@/components/0_Bruddle/PageContainer' import Card from '@/components/Global/Card' import { Icon } from '@/components/Global/Icons/Icon' import PeanutLoading from '@/components/Global/PeanutLoading' @@ -56,77 +57,79 @@ export default function Home() { } return ( -
-
- - - - + +
+
+ + + + - - - - - - -
- {/* Rewards Card - only shows if balance is non-zero */} - + + + + +
- {/* Transaction cards - temporary */} -
-

Transactions

-
- + {/* Rewards Card - only shows if balance is non-zero */} + - + {/* Transaction cards - temporary */} +
+

Transactions

+
+ - + - + + + +
-
- - -
+ + +
+ ) } diff --git a/src/app/(mobile-ui)/profile/page.tsx b/src/app/(mobile-ui)/profile/page.tsx index e999f17bd..e366814a9 100644 --- a/src/app/(mobile-ui)/profile/page.tsx +++ b/src/app/(mobile-ui)/profile/page.tsx @@ -1,4 +1,5 @@ import { Profile } from '@/components' +import PageContainer from '@/components/0_Bruddle/PageContainer' import { Metadata } from 'next' export const metadata: Metadata = { @@ -19,5 +20,9 @@ export const metadata: Metadata = { } export default function ProfilePage() { - return + return ( + + + + ) } diff --git a/src/components/0_Bruddle/PageContainer.tsx b/src/components/0_Bruddle/PageContainer.tsx index 256f2e31f..140162adb 100644 --- a/src/components/0_Bruddle/PageContainer.tsx +++ b/src/components/0_Bruddle/PageContainer.tsx @@ -1,7 +1,7 @@ import { HTMLAttributes } from 'react' const PageContainer = (props: HTMLAttributes) => { - return
{props.children}
+ return
{props.children}
} export default PageContainer From 2006e20e16f1327b0cbcf5976dc6e81644d952ce Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Tue, 22 Apr 2025 20:10:43 +0530 Subject: [PATCH 08/10] fix: rename drawer state --- src/components/Profile/ProfileHeader.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/Profile/ProfileHeader.tsx b/src/components/Profile/ProfileHeader.tsx index 2658e65ae..1750b65cd 100644 --- a/src/components/Profile/ProfileHeader.tsx +++ b/src/components/Profile/ProfileHeader.tsx @@ -17,7 +17,7 @@ interface ProfileHeaderProps { } const ProfileHeader: React.FC = ({ name, username, initials, isVerified = false, className }) => { - const [isQRScannerOpen, setIsQRScannerOpen] = useState(false) + const [isDrawerOpen, setIsDrawerOpen] = useState(false) const profileUrl = `peanut.me/${username}` @@ -48,14 +48,14 @@ const ProfileHeader: React.FC = ({ name, username, initials, className="flex w-fit items-center justify-center gap-2 rounded-full px-4 py-2" onClick={() => { navigator.clipboard.writeText(profileUrl) - setIsQRScannerOpen(true) + setIsDrawerOpen(true) }} > peanut.me/{username}
- {isQRScannerOpen && ( + {isDrawerOpen && ( <> = ({ name, username, initials, handleSubtitle="Share it to receive payments!" collapsedHeight={80} expandedHeight={90} - isOpen={isQRScannerOpen} - onClose={() => setIsQRScannerOpen(false)} + isOpen={isDrawerOpen} + onClose={() => setIsDrawerOpen(false)} >
From 1883cb866f82d730ba3816776730eb298c4d9f99 Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Wed, 23 Apr 2025 13:37:41 +0530 Subject: [PATCH 09/10] feat: add username header in homepage --- src/app/(mobile-ui)/home/page.tsx | 84 +++++++++---------- .../Global/Badges/AchievementsBadge.tsx | 10 ++- .../Global/CopyToClipboard/index.tsx | 9 +- src/components/Global/DirectSendQR/utils.ts | 4 +- src/components/Global/Icons/Icon.tsx | 3 + src/components/Global/Icons/copy.tsx | 10 +++ src/components/Home/HomeHistory.tsx | 2 +- src/components/Profile/AvatarWithBadge.tsx | 41 +++++++++ src/components/Profile/ProfileHeader.tsx | 15 +--- 9 files changed, 115 insertions(+), 63 deletions(-) create mode 100644 src/components/Global/Icons/copy.tsx create mode 100644 src/components/Profile/AvatarWithBadge.tsx diff --git a/src/app/(mobile-ui)/home/page.tsx b/src/app/(mobile-ui)/home/page.tsx index 8f3094ec0..0ffabc3fa 100644 --- a/src/app/(mobile-ui)/home/page.tsx +++ b/src/app/(mobile-ui)/home/page.tsx @@ -4,11 +4,13 @@ import { PeanutArmHoldingBeer } from '@/assets' import { Button, ButtonSize, ButtonVariant } from '@/components/0_Bruddle' import PageContainer from '@/components/0_Bruddle/PageContainer' import Card from '@/components/Global/Card' +import CopyToClipboard from '@/components/Global/CopyToClipboard' +import { BASE_URL } from '@/components/Global/DirectSendQR/utils' import { Icon } from '@/components/Global/Icons/Icon' import PeanutLoading from '@/components/Global/PeanutLoading' import RewardsModal from '@/components/Global/RewardsModal' import HomeHistory from '@/components/Home/HomeHistory' -import TransactionCard from '@/components/Home/TransactionCard' +import AvatarWithBadge from '@/components/Profile/AvatarWithBadge' import { useAuth } from '@/context/authContext' import { useWallet } from '@/hooks/wallet/useWallet' import { formatExtendedNumber, getUserPreferences, printableUsdc, updateUserPreferences } from '@/utils' @@ -26,7 +28,12 @@ export default function Home() { return prefs?.balanceHidden ?? false }) - const { username, isFetchingUser } = useAuth() + const { username, isFetchingUser, user } = useAuth() + + const userFullName = useMemo(() => { + if (!user) return + return user.user.full_name + }, [user]) const handleToggleBalanceVisibility = (e: React.MouseEvent) => { e.stopPropagation() @@ -58,7 +65,8 @@ export default function Home() { return ( -
+
+
@@ -86,46 +94,6 @@ export default function Home() { {/* Rewards Card - only shows if balance is non-zero */} - {/* Transaction cards - temporary */} -
-

Transactions

-
- - - - - - - -
-
-
@@ -257,3 +225,33 @@ function RewardsCard({ balance }: { balance: string | undefined }) {
) } + +function UserHeader({ username, fullName }: { username: string; fullName?: string }) { + const initals = useMemo(() => { + if (fullName) { + return fullName + .split(' ') + .map((part) => part[0]) + .join('') + .toUpperCase() + .substring(0, 2) + } + + return username + .split(' ') + .map((part) => part[0]) + .join('') + .toUpperCase() + .substring(0, 2) + }, [username]) + + return ( +
+ + +
{username}
+ + +
+ ) +} diff --git a/src/components/Global/Badges/AchievementsBadge.tsx b/src/components/Global/Badges/AchievementsBadge.tsx index d5c2598de..ddbc5d603 100644 --- a/src/components/Global/Badges/AchievementsBadge.tsx +++ b/src/components/Global/Badges/AchievementsBadge.tsx @@ -1,9 +1,11 @@ import { twMerge } from 'tailwind-merge' import { Icon, IconName } from '../Icons/Icon' +export type AchievementsBadgeSize = 'extra-small' | 'small' | 'medium' | 'large' + interface AchievementsBadgeProps { icon?: IconName - size?: 'small' | 'medium' | 'large' + size?: AchievementsBadgeSize color?: string className?: string position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' @@ -18,6 +20,8 @@ const AchievementsBadge = ({ }: AchievementsBadgeProps) => { const getContainerSize = () => { switch (size) { + case 'extra-small': + return 'size-2.5' case 'small': return 'size-4' case 'medium': @@ -31,6 +35,8 @@ const AchievementsBadge = ({ const getIconSize = () => { switch (size) { + case 'extra-small': + return 6 case 'small': return 8 case 'medium': @@ -44,6 +50,8 @@ const AchievementsBadge = ({ const getPadding = () => { switch (size) { + case 'extra-small': + return 'p-0' case 'small': return 'p-0.5' case 'medium': diff --git a/src/components/Global/CopyToClipboard/index.tsx b/src/components/Global/CopyToClipboard/index.tsx index f3352d5e3..3e25058dc 100644 --- a/src/components/Global/CopyToClipboard/index.tsx +++ b/src/components/Global/CopyToClipboard/index.tsx @@ -1,14 +1,15 @@ import React, { useState } from 'react' import { twMerge } from 'tailwind-merge' -import Icon from '../Icon' +import { Icon } from '../Icons/Icon' interface Props { textToCopy: string fill?: string className?: string + iconSize?: '2' | '4' | '6' | '8' } -const CopyToClipboard = ({ textToCopy, fill, className }: Props) => { +const CopyToClipboard = ({ textToCopy, fill, className, iconSize = '6' }: Props) => { const [copied, setCopied] = useState(false) const handleCopy = (e: React.MouseEvent) => { @@ -21,8 +22,8 @@ const CopyToClipboard = ({ textToCopy, fill, className }: Props) => { return ( diff --git a/src/components/Global/DirectSendQR/utils.ts b/src/components/Global/DirectSendQR/utils.ts index aab236388..8e64db9e9 100644 --- a/src/components/Global/DirectSendQR/utils.ts +++ b/src/components/Global/DirectSendQR/utils.ts @@ -1,5 +1,5 @@ +import { getTokenSymbol, validateEnsName } from '@/utils' import { isAddress } from 'viem' -import { validateEnsName, getTokenSymbol } from '@/utils' // Constants const PINTA_MERCHANTS: Record = { @@ -67,7 +67,7 @@ const REGEXES_BY_TYPE: { [key in QrType]?: RegExp } = { /^(https?:\/\/)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/, } -const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL! +export const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL! export function recognizeQr(data: string): QrType | null { if (data.startsWith(BASE_URL)) { diff --git a/src/components/Global/Icons/Icon.tsx b/src/components/Global/Icons/Icon.tsx index 3b7cb254a..9e5f2a8c7 100644 --- a/src/components/Global/Icons/Icon.tsx +++ b/src/components/Global/Icons/Icon.tsx @@ -7,6 +7,7 @@ import { ArrowUpRightIcon } from './arrow-up-right' import { BankIcon } from './bank' import { CheckIcon } from './check' import { ChevronUpIcon } from './chevron-up' +import { CopyIcon } from './copy' import { CurrencyIcon } from './currency' import { ExchangeIcon } from './exchange' import { EyeIcon } from './eye' @@ -46,6 +47,7 @@ export type IconName = | 'smile' | 'user' | 'share' + | 'copy' export interface IconProps extends SVGProps { name: IconName @@ -76,6 +78,7 @@ const iconComponents: Record>> = smile: SmileIcon, user: UserIcon, share: ShareIcon, + copy: CopyIcon, } export const Icon: FC = ({ name, size = 24, width, height, ...props }) => { diff --git a/src/components/Global/Icons/copy.tsx b/src/components/Global/Icons/copy.tsx new file mode 100644 index 000000000..24e0d3ee3 --- /dev/null +++ b/src/components/Global/Icons/copy.tsx @@ -0,0 +1,10 @@ +import { FC, SVGProps } from 'react' + +export const CopyIcon: FC> = (props) => ( + + + +) diff --git a/src/components/Home/HomeHistory.tsx b/src/components/Home/HomeHistory.tsx index 5bf97437f..74b1465c1 100644 --- a/src/components/Home/HomeHistory.tsx +++ b/src/components/Home/HomeHistory.tsx @@ -137,7 +137,7 @@ const HomeHistory = () => { } return ( -
+

Transactions

diff --git a/src/components/Profile/AvatarWithBadge.tsx b/src/components/Profile/AvatarWithBadge.tsx new file mode 100644 index 000000000..b0a497565 --- /dev/null +++ b/src/components/Profile/AvatarWithBadge.tsx @@ -0,0 +1,41 @@ +import React from 'react' +import { twMerge } from 'tailwind-merge' +import AchievementsBadge, { AchievementsBadgeSize } from '../Global/Badges/AchievementsBadge' + +interface AvatarWithBadgeProps { + initials: string + isVerified?: boolean + className?: string + size?: 'extra-small' | 'small' | 'medium' | 'large' + achievementsBadgeSize?: AchievementsBadgeSize +} +const AvatarWithBadge: React.FC = ({ + initials, + isVerified = false, + className, + size = 'medium', + achievementsBadgeSize = 'small', +}) => { + const sizeClasses = { + 'extra-small': 'h-8 w-8 text-sm', + small: 'h-8 w-8 text-lg', + medium: 'h-16 w-16 text-2xl', + large: 'h-24 w-24 text-3xl', + } + + return ( +
+
+ {initials} +
+ {isVerified && } +
+ ) +} + +export default AvatarWithBadge diff --git a/src/components/Profile/ProfileHeader.tsx b/src/components/Profile/ProfileHeader.tsx index 1750b65cd..0c596186e 100644 --- a/src/components/Profile/ProfileHeader.tsx +++ b/src/components/Profile/ProfileHeader.tsx @@ -3,10 +3,11 @@ import React, { useState } from 'react' import { twMerge } from 'tailwind-merge' import { Button } from '../0_Bruddle' import Divider from '../0_Bruddle/Divider' -import AchievementsBadge from '../Global/Badges/AchievementsBadge' + import BottomDrawer from '../Global/BottomDrawer' import QRCodeWrapper from '../Global/QRCodeWrapper' import ShareButton from '../Global/ShareButton' +import AvatarWithBadge from './AvatarWithBadge' interface ProfileHeaderProps { name: string @@ -25,17 +26,7 @@ const ProfileHeader: React.FC = ({ name, username, initials, <>
{/* Avatar with initials */} -
-
- {initials} -
- - {isVerified && } -
+ {/* Name */}

{name}

From 2de7ad3ca2c641fe8dbb2ed0c33807c1df41d0bf Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Wed, 23 Apr 2025 13:49:19 +0530 Subject: [PATCH 10/10] fix: home page cta's --- src/app/(mobile-ui)/home/page.tsx | 56 ++++++++++++++++--------------- src/components/AddFunds/index.tsx | 16 ++++++--- 2 files changed, 40 insertions(+), 32 deletions(-) diff --git a/src/app/(mobile-ui)/home/page.tsx b/src/app/(mobile-ui)/home/page.tsx index 0ffabc3fa..c452883b2 100644 --- a/src/app/(mobile-ui)/home/page.tsx +++ b/src/app/(mobile-ui)/home/page.tsx @@ -3,6 +3,7 @@ import { PeanutArmHoldingBeer } from '@/assets' import { Button, ButtonSize, ButtonVariant } from '@/components/0_Bruddle' import PageContainer from '@/components/0_Bruddle/PageContainer' +import AddFunds from '@/components/AddFunds' import Card from '@/components/Global/Card' import CopyToClipboard from '@/components/Global/CopyToClipboard' import { BASE_URL } from '@/components/Global/DirectSendQR/utils' @@ -69,8 +70,8 @@ export default function Home() {
- - + } /> + - - + + + + ) +} + +function ActionButton({ label, action, variant = 'primary-soft', size = 'small' }: Omit) { // get icon based on action type const renderIcon = (): React.ReactNode => { return ( @@ -171,22 +176,19 @@ function ActionButton({
) } - return ( - - - + ) } diff --git a/src/components/AddFunds/index.tsx b/src/components/AddFunds/index.tsx index a03f5aebe..965ca5721 100644 --- a/src/components/AddFunds/index.tsx +++ b/src/components/AddFunds/index.tsx @@ -16,7 +16,7 @@ type FundingMethod = 'exchange' | 'request_link' | null type Wallet = { name: string; logo: string } // main component -const AddFunds = () => { +const AddFunds = ({ cta }: { cta?: ReactNode }) => { const [fundingMethod, setFundingMethod] = useState(null) const [showModal, setShowModal] = useState(false) const timerRef = useRef() @@ -47,10 +47,16 @@ const AddFunds = () => { return (
setShowModal(true)} className="flex flex-col items-center gap-2.5"> - -
Add
+ {cta ? ( + cta + ) : ( + <> + +
Add
+ + )}