Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified bun.lockb
Binary file not shown.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "stackly-ui",
"repository": "https://github.com/SwaprHQ/stackly-ui",
"version": "1.3.5",
"version": "1.4.0",
"license": "MIT",
"private": true,
"packageManager": "bun@1.0.6",
Expand Down
2 changes: 2 additions & 0 deletions packages/app/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,7 @@ NEXT_PUBLIC_FATHOM_SITE_ID=
RPC_GNOSIS=
RPC_MAINNET=
RPC_ARBITRUM=
RPC_BASE=


NEXT_STACKLY_SUBGRAPH_API_KEY=
Binary file modified packages/app/app/opengraph-image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/app/app/twitter-image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
55 changes: 55 additions & 0 deletions packages/app/components/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"use client";

import { twMerge } from "tailwind-merge";
import Image from "next/image";
import { Address, http } from "viem";
import { mainnet } from "viem/chains";
import { createConfig, fallback, useEnsAvatar } from "wagmi";
import { RPC_LIST } from "@/constants";

interface AvatarProps {
address: Address;
className?: string;
size?: number;
ensName?: string;
}

const mainnetConfigForENS = createConfig({
chains: [mainnet],
transports: {
[mainnet.id]: fallback([http(RPC_LIST[mainnet.id]), http()]),
},
});

export const Avatar = ({
address,
ensName,
className,
size = 24,
}: AvatarProps) => {
const { data: ensAvatar } = useEnsAvatar({
name: ensName ?? undefined,
chainId: mainnet.id,
config: mainnetConfigForENS,
});

const GRADIENT_BASED_ON_ADDRESS_URL = "https://avatars.jakerunzer.com";
const ENS_METADATA_AVATAR_URL = "https://metadata.ens.domains/mainnet/avatar";

const avatarGradientUrl = `${GRADIENT_BASED_ON_ADDRESS_URL}/${address}`;
const avatarEnsUrl = `${ENS_METADATA_AVATAR_URL}/${ensName}`;
const avatarUrl = ensName && ensAvatar ? avatarEnsUrl : avatarGradientUrl;

return (
<Image
src={avatarUrl}
alt={`${ensName} avatar`}
width={size}
height={size}
className={twMerge(
"bg-cover rounded-full size-6 bg-outline-low-em",
className
)}
/>
);
};
40 changes: 18 additions & 22 deletions packages/app/components/ConnectButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

import { ChainId, WETH, WXDAI } from "@stackly/sdk";
import { ConnectKitButton } from "connectkit";
import Image from "next/image";
import { useBalance, useEnsAvatar } from "wagmi";
import { useBalance } from "wagmi";
import { formatUnits } from "viem";

import { BodyText, Button, SizeProps } from "@/ui";
import { useAutoConnect } from "@/hooks";
import { useNetworkContext } from "@/contexts";
import { Avatar } from "@/components/Avatar";

const CustomConnectButton = ({
address,
Expand All @@ -21,15 +22,12 @@ const CustomConnectButton = ({
size: SizeProps;
}) => {
const { chainId } = useNetworkContext();
const { data: avatar } = useEnsAvatar({
name: ensName,
chainId: ChainId.ETHEREUM,
});

const TOKEN_BY_CHAIN: { [chainId: number]: string } = {
[ChainId.ETHEREUM]: WETH[ChainId.ETHEREUM].address,
[ChainId.GNOSIS]: WXDAI.address,
[ChainId.ARBITRUM]: WETH[ChainId.ARBITRUM].address,
[ChainId.BASE]: WETH[ChainId.BASE].address,
};

const { data: balance } = useBalance({
Expand All @@ -44,19 +42,19 @@ const CustomConnectButton = ({
)}`;

const formattedBalance = (balanceData: NonNullable<typeof balance>) =>
balanceData.formatted === "0"
balanceData.value === BigInt(0)
? `0 ${balanceData.symbol}`
: `${balanceData.formatted.substring(
: `${formatUnits(balanceData.value, balanceData.decimals).substring(
0,
balanceData.formatted.length - balanceData.decimals + 3
5
)} ${balanceData.symbol}`;

return (
<div className="flex bg-gray-alpha-50 rounded-xl p-0.5 justify-center items-center md:space-x-3">
{balance && (
<BodyText
size={2}
className="hidden ml-3 text-em-med md:block min-w-max"
className="hidden ml-3 min-w-max text-em-med md:block"
>
{formattedBalance(balance)}
</BodyText>
Expand All @@ -67,20 +65,18 @@ const CustomConnectButton = ({
iconRight="caret-down"
onClick={onClick}
width="full"
className="flex border-none shadow-sm rounded-xl hover:bg-surface-25 focus:bg-white focus:ring-0 active:ring-0"
className="flex rounded-xl border-none shadow-sm hover:bg-surface-25 focus:bg-white focus:ring-0 active:ring-0"
>
{avatar && (
<Image
width={20}
height={20}
src={avatar}
alt={address}
className="rounded-full"
/>
)}
<Avatar address={address} ensName={ensName} />
<BodyText size={2} className="text-black">
<span className="md:hidden">{truncatedAddress(2)}</span>
<span className="hidden md:block">{truncatedAddress(4)}</span>
{ensName ? (
<>{ensName}</>
) : (
<>
<span className="md:hidden">{truncatedAddress(2)}</span>
<span className="hidden md:block">{truncatedAddress(4)}</span>
</>
)}
</BodyText>
</Button>
</div>
Expand Down
38 changes: 26 additions & 12 deletions packages/app/components/SelectNetwork.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,31 @@
import { Fragment } from "react";
import { ChainIcon } from "connectkit";
import { Listbox, Transition } from "@headlessui/react";
import { usePathname } from "next/navigation";

import { Button, Icon } from "@/ui";
import { useNetworkContext, useStackboxFormContext } from "@/contexts";
import { PATHNAMES } from "@/constants";
import { useNetworkContext } from "@/contexts";
import Image from "next/image";
import { ChainId } from "@stackly/sdk";

const CustomChainIcon = ({ id, size }: { id: number; size: number }) => {
if (id === ChainId.BASE) {
return (
<Image
src="/assets/images/base-logo.svg"
alt="Base network logo"
width={size}
height={size}
className="rounded-full"
/>
);
}

// Fall back to ConnectKit's ChainIcon for other networks
return <ChainIcon size={size} id={id} />;
};

export const SelectNetwork = () => {
const { chains, changeNetwork, chainId, selectedChain } = useNetworkContext();
const { resetFormValues } = useStackboxFormContext();
const pathname = usePathname();

return (
<Listbox
value={chainId.toString()}
Expand All @@ -27,10 +41,10 @@ export const SelectNetwork = () => {
iconRight="caret-down"
size="sm"
variant="tertiary"
className="flex h-10 border-none shadow-sm rounded-xl focus:bg-white focus:ring-0 active:ring-0"
className="flex h-10 rounded-xl border-none shadow-sm focus:bg-white focus:ring-0 active:ring-0"
>
<div className="flex items-center space-x-2">
<ChainIcon size={20} id={chainId} />
<CustomChainIcon size={20} id={chainId} />
<span className="hidden md:inline-block">
{selectedChain?.name}
</span>
Expand All @@ -42,22 +56,22 @@ export const SelectNetwork = () => {
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute z-10 w-auto py-1 mt-1 overflow-auto text-base bg-white shadow-md max-h-60 rounded-2xl focus:outline-none sm:text-sm">
<Listbox.Options className="overflow-auto absolute z-10 py-1 mt-1 w-auto max-h-60 text-base bg-white rounded-2xl shadow-md focus:outline-none sm:text-sm">
{chains?.map(({ id, name }) => (
<Listbox.Option
key={id}
className="relative py-2 pl-4 pr-10 cursor-pointer select-none hover:bg-surface-75"
className="relative py-2 pr-10 pl-4 cursor-pointer select-none hover:bg-surface-75"
value={id.toString()}
>
{({ selected }) => {
return (
<>
<div className="flex items-center space-x-2">
<ChainIcon size={20} id={id} />
<CustomChainIcon size={20} id={id} />
<p className="text-nowrap">{name}</p>
</div>
{selected ? (
<div className="absolute inset-y-0 right-0 flex items-center pr-3 text-amber-600">
<div className="flex absolute inset-y-0 right-0 items-center pr-3 text-amber-600">
<Icon
name="check"
className="w-4 h-4 text-primary-600"
Expand Down
6 changes: 3 additions & 3 deletions packages/app/components/navbar/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function Navbar() {
const { chainId } = useNetworkContext();

return (
<header className="top-0 flex flex-col w-full px-4 border-b border-solid h-nav-height bg-surface-25 border-b-surface-75">
<header className="flex top-0 flex-col px-4 w-full border-b border-solid h-nav-height bg-surface-25 border-b-surface-75">
<nav className="flex items-center w-full h-full">
<div>
<Link
Expand All @@ -25,13 +25,13 @@ export function Navbar() {
query: `chainId=${chainId}`,
}}
title="Stackly Home"
className="flex items-center outline-none w-14 md:w-40"
className="flex items-center w-14 outline-none md:w-40"
>
<Logo />
</Link>
</div>
<Divider />
<div className="items-center justify-end hidden w-full gap-4 md:flex">
<div className="hidden gap-4 justify-end items-center w-full md:flex">
<ButtonLink
variant="quaternary"
size="sm"
Expand Down
50 changes: 49 additions & 1 deletion packages/app/components/strategies/constants.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { FREQUENCY_OPTIONS } from "@/models/stack";
import { gnosisTokens, mainnetTokens, arbitrumTokens } from "@/models/token";
import {
gnosisTokens,
mainnetTokens,
arbitrumTokens,
baseTokens,
} from "@/models/token";
import { Strategy } from "@/contexts";
import { ChainId } from "@stackly/sdk";

Expand Down Expand Up @@ -149,4 +154,47 @@ export const STRATEGY_CATEGORIES: { [chainId: number]: ChainStrategies } = {
],
},
},
[ChainId.BASE]: {
popular: {
label: "Popular Strategies",
strategies: [
{
id: 1,
buyToken: baseTokens.WETH,
daysAmount: 30,
frequency: FREQUENCY_OPTIONS.day,
sellAmountPerTimeframe: 50,
sellToken: baseTokens.USDC,
totalSellAmount: "1500",
},
{
id: 2,
buyToken: baseTokens.WETH,
daysAmount: 7,
frequency: FREQUENCY_OPTIONS.day,
sellAmountPerTimeframe: 20,
sellToken: baseTokens.USDC,
totalSellAmount: "140",
},
{
id: 3,
buyToken: baseTokens.CBBTC,
daysAmount: 10,
frequency: FREQUENCY_OPTIONS.hour,
sellAmountPerTimeframe: 5,
sellToken: baseTokens.USDC,
totalSellAmount: "1200",
},
{
id: 4,
buyToken: baseTokens.WETH,
daysAmount: 56,
frequency: FREQUENCY_OPTIONS.week,
sellAmountPerTimeframe: 100,
sellToken: baseTokens.USDC,
totalSellAmount: "800",
},
],
},
},
};
2 changes: 2 additions & 0 deletions packages/app/components/token-picker/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
arbitrumTokens,
gnosisTokens,
mainnetTokens,
baseTokens,
TokenFromTokenlist,
} from "@/models/token";
import { ChainId } from "@stackly/sdk";
Expand All @@ -28,4 +29,5 @@ export const TOKEN_PICKER_COMMON_TOKENS: {
gnosisTokens.WETH,
gnosisTokens.WXDAI,
],
[ChainId.BASE]: [baseTokens.USDC, baseTokens.WETH, baseTokens.CBBTC],
};
1 change: 1 addition & 0 deletions packages/app/constants/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const RPC_LIST: { [chainId: number]: string } = {
[ChainId.GNOSIS]: process.env.RPC_GNOSIS ?? "https://rpc.gnosis.gateway.fm/",
[ChainId.ARBITRUM]:
process.env.RPC_ARBITRUM ?? "https://arbitrum-one-rpc.publicnode.com/",
[ChainId.BASE]: process.env.RPC_BASE ?? "https://base-rpc.publicnode.com",
};

// App URLs
Expand Down
16 changes: 12 additions & 4 deletions packages/app/contexts/NetworkContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ interface NetworkContextProviderProps {
children: ReactNode;
}

const INVALID_CHAIN_ID = 0;

export const NetworkContextProvider = ({
children,
}: NetworkContextProviderProps) => {
Expand Down Expand Up @@ -90,7 +92,8 @@ export const NetworkContextProvider = ({
useEffect(() => {
const parsedSearchParamsChainId = searchParamsChainId
? parseInt(searchParamsChainId)
: 0;
: INVALID_CHAIN_ID;

const newChainIsDiferentFromCurrent =
parsedSearchParamsChainId !== chain?.id;

Expand All @@ -116,9 +119,14 @@ export const NetworkContextProvider = ({
}
}
}
// set default chain
setSelectedChain(arbitrum);
setSelectedChainId(arbitrum.id);

const chainIsNotSet = !chain?.id;

if (!isValidSearchParamsChainId && chainIsNotSet) {
// set default chain
setSelectedChain(arbitrum);
setSelectedChainId(arbitrum.id);
}

// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isConnected, chains, searchParamsChainId, switchChain]);
Expand Down
Loading
Loading