diff --git a/apps/common/CarouselControls.tsx b/apps/common/CarouselControls.tsx new file mode 100644 index 000000000..8f8473535 --- /dev/null +++ b/apps/common/CarouselControls.tsx @@ -0,0 +1,44 @@ +import {type ReactElement} from 'react'; +import {cl} from '@builtbymom/web3/utils'; + +type TCarouselControlsProps = { + carouselLength?: number; + onDotsClick: (destination: number) => void; + currentPage: number; +}; + +export function CarouselControls({ + carouselLength = 0, + onDotsClick, + currentPage +}: TCarouselControlsProps): ReactElement | null { + const numberOfControls = Math.ceil(carouselLength / 4); + + if (carouselLength && carouselLength < 5) { + return null; + } + + return ( +
+
+ {Array(numberOfControls) + .fill('') + .map((_, index) => ( + + ))} +
+
+ ); +} diff --git a/apps/common/CarouselSlideArrows.tsx b/apps/common/CarouselSlideArrows.tsx new file mode 100644 index 000000000..1930f53b3 --- /dev/null +++ b/apps/common/CarouselSlideArrows.tsx @@ -0,0 +1,39 @@ +import {cl} from '@builtbymom/web3/utils'; + +import {IconChevron} from './icons/IconChevron'; + +import type {ReactElement} from 'react'; + +type TCarouselSlideArrowsProps = { + onScrollBack?: VoidFunction; + onScrollForward?: VoidFunction; + className?: string; +}; + +export function CarouselSlideArrows({ + onScrollBack, + onScrollForward, + className +}: TCarouselSlideArrowsProps): ReactElement { + return ( +
+
+
+ + +
+
+ ); +} diff --git a/apps/common/components/AppCard.tsx b/apps/common/components/AppCard.tsx new file mode 100644 index 000000000..6f0de47b8 --- /dev/null +++ b/apps/common/components/AppCard.tsx @@ -0,0 +1,72 @@ +import Image from 'next/image'; +import Link from 'next/link'; +import {IconShare} from '@common/icons/IconShare'; + +import type {ReactElement} from 'react'; +import type {TApp} from '@common/types/category'; + +type TAppCardProps = { + app: TApp; +}; + +export function AppCard(props: TAppCardProps): ReactElement { + return ( + <> + +
+ + {props.app.logoURI ? ( + {props.app.name} + ) : ( +
+ )} +
+
{props.app.name}
+ +

{props.app.description}

+ + +
+ {props.app.logoURI ? ( +
+ {props.app.name} +
+ ) : ( +
+ )} +
+ +
+
{props.app.name}
+

{props.app.description}

+
+ + + ); +} diff --git a/apps/common/components/AppsCarousel.tsx b/apps/common/components/AppsCarousel.tsx new file mode 100644 index 000000000..3c2be3f4e --- /dev/null +++ b/apps/common/components/AppsCarousel.tsx @@ -0,0 +1,57 @@ +import {type ForwardedRef, forwardRef, type ReactElement} from 'react'; +import React from 'react'; +import {cl} from '@builtbymom/web3/utils'; + +import {AppCard} from './AppCard'; +import {FeaturedApp} from './FeaturedApp'; + +import type {TApp} from '@common/types/category'; + +export const AppsCarousel = forwardRef( + ( + props: {onScroll?: VoidFunction; isUsingFeatured?: boolean; apps: TApp[]}, + ref: ForwardedRef + ): ReactElement => { + return ( +
+
+
+
+
+ {props.apps?.map((app, i) => { + return ( + + {props.isUsingFeatured ? ( + + ) : ( + + )} + + ); + })} +
+
+
+ ); + } +); diff --git a/apps/common/components/CategorySection.tsx b/apps/common/components/CategorySection.tsx new file mode 100644 index 000000000..8ca1a9548 --- /dev/null +++ b/apps/common/components/CategorySection.tsx @@ -0,0 +1,131 @@ +import {type ReactElement, useRef, useState} from 'react'; +import {useMountEffect} from '@react-hookz/web'; +import {CarouselControls} from '@common/CarouselControls'; +import {CarouselSlideArrows} from '@common/CarouselSlideArrows'; +import {IconShare} from '@common/icons/IconShare'; + +import {AppsCarousel} from './AppsCarousel'; + +import type {TApp} from '@common/types/category'; + +type TAppSectionProps = { + title: string; + onExpandClick: () => void; + apps: TApp[]; +}; + +export const CategorySection = ({title, onExpandClick, apps}: TAppSectionProps): ReactElement => { + const [shuffledApps, set_shuffledApps] = useState([]); + const [currentPage, set_currentPage] = useState(1); + const carouselRef = useRef(null); + const [isProgrammaticScroll, set_isProgrammaticScroll] = useState(false); + + /********************************************************************************************** + ** Handles scrolling back to the previous page in the carousel. + ** It updates the scroll position, current page, and sets a flag to indicate programmatic + ** scrolling. The flag is reset after a delay to allow for smooth scrolling. + *********************************************************************************************/ + const onScrollBack = (): void => { + if (!carouselRef.current || currentPage === 1) return; + set_isProgrammaticScroll(true); + carouselRef.current.scrollLeft -= 880; + set_currentPage(prev => prev - 1); + + setTimeout(() => { + set_isProgrammaticScroll(false); + }, 3000); + }; + + /********************************************************************************************** + ** Handles scrolling forward to the next page in the carousel. + ** It updates the scroll position, current page, and sets a flag to indicate programmatic + ** scrolling. The flag is reset after a delay to allow for smooth scrolling. + *********************************************************************************************/ + const onScrollForward = (): void => { + if (!carouselRef.current || currentPage === Math.ceil(apps.length / 4)) return; + set_isProgrammaticScroll(true); + carouselRef.current.scrollLeft += 880; + set_currentPage(prev => prev + 1); + + setTimeout(() => { + set_isProgrammaticScroll(false); + }, 3000); + }; + + /********************************************************************************************** + ** Handles clicking on the carousel dots to navigate to a specific page. + ** It updates the scroll position, current page, and sets a flag to indicate programmatic + ** scrolling. The flag is reset after a delay to allow for smooth scrolling. + *********************************************************************************************/ + const onDotsClick = (destination: number): void => { + if (!carouselRef.current || destination === currentPage) return; + set_isProgrammaticScroll(true); + if (destination > currentPage) { + carouselRef.current.scrollLeft += 1000 * (destination - currentPage); + setTimeout(() => { + set_isProgrammaticScroll(false); + }, 3000); + } else { + carouselRef.current.scrollLeft -= 1000 * (currentPage - destination); + setTimeout(() => { + set_isProgrammaticScroll(false); + }, 3000); + } + set_currentPage(destination); + }; + + /********************************************************************************************** + ** Handles the scroll event of the carousel. + ** It calculates the current page based on the scroll position and updates the state. + ** This function is not triggered during programmatic scrolling to avoid conflicts. + *********************************************************************************************/ + const onScroll = (): void => { + if (!carouselRef.current || isProgrammaticScroll) return; + const {scrollLeft} = carouselRef.current; + const page = Math.ceil(scrollLeft / 1000) + 1; + set_currentPage(page); + }; + + /********************************************************************************************** + ** On component mount we shuffle the array of Partners to avoid any bias. + **********************************************************************************************/ + useMountEffect(() => { + if (apps?.length < 1) { + return; + } + set_shuffledApps(apps?.toSorted(() => 0.5 - Math.random())); + }); + return ( +
+
+
+
{title}
+ +
+ {apps?.length > 4 && ( + + )} +
+ + +
+ ); +}; diff --git a/apps/common/components/Cutaway.tsx b/apps/common/components/Cutaway.tsx new file mode 100644 index 000000000..649764641 --- /dev/null +++ b/apps/common/components/Cutaway.tsx @@ -0,0 +1,31 @@ +import Link from 'next/link'; +import {IconShare} from '@common/icons/IconShare'; + +import type {ReactElement} from 'react'; + +type TCutawayProps = { + title: string; + link: string; + icon: ReactElement; +}; + +export function Cutaway(props: TCutawayProps): ReactElement { + return ( + +
+
{props.icon}
+
+

{props.title}

+
+
+
+ +
+ + ); +} diff --git a/apps/common/components/FeaturedApp.tsx b/apps/common/components/FeaturedApp.tsx new file mode 100644 index 000000000..76f54c564 --- /dev/null +++ b/apps/common/components/FeaturedApp.tsx @@ -0,0 +1,38 @@ +import Image from 'next/image'; +import Link from 'next/link'; +import {cl} from '@builtbymom/web3/utils'; + +import type {ReactElement} from 'react'; +import type {TApp} from '@common/types/category'; + +export function FeaturedApp(props: {app: TApp}): ReactElement { + return ( + +
+ {props.app.name} +
+ +
+ {props.app.description} +
+ + ); +} diff --git a/apps/common/components/FilterBar.tsx b/apps/common/components/FilterBar.tsx new file mode 100644 index 000000000..64c1035e9 --- /dev/null +++ b/apps/common/components/FilterBar.tsx @@ -0,0 +1,30 @@ +import {cl} from '@builtbymom/web3/utils'; +import {CATEGORY_PAGE_FILTERS} from '@common/utils/constants'; + +import type {ReactElement} from 'react'; + +function FilterItem({isActive, title}: {isActive: boolean; title: string}): ReactElement { + return ( +
+ {title} +
+ ); +} + +export function FilterBar({selectedFilter}: {selectedFilter: {title: string; value: string}}): ReactElement { + return ( +
+ {CATEGORY_PAGE_FILTERS.map(filter => ( + + ))} +
+ ); +} diff --git a/apps/common/components/MobileNavbar.tsx b/apps/common/components/MobileNavbar.tsx new file mode 100644 index 000000000..ca54468c4 --- /dev/null +++ b/apps/common/components/MobileNavbar.tsx @@ -0,0 +1,69 @@ +import Link from 'next/link'; +import {usePathname} from 'next/navigation'; +import {cl} from '@builtbymom/web3/utils'; +import {LogoDiscordRound} from '@common/icons/LogoDiscordRound'; +import {LogoParagraphRound} from '@common/icons/LogoParagraphRound'; +import {LogoTwitterRound} from '@common/icons/LogoTwitterRound'; +import {iconsDict, LANDING_SIDEBAR_LINKS, MENU_TABS} from '@common/utils/constants'; + +import type {ReactElement} from 'react'; + +export function MobileNavbar({onClose}: {onClose: VoidFunction}): ReactElement { + const pathName = usePathname(); + + const currentTab = pathName?.startsWith('/home/') ? pathName?.split('/')[2] : '/'; + return ( +
+
+ {MENU_TABS.map(tab => ( + +
+ {iconsDict[tab.route as keyof typeof iconsDict]} +
+

{tab.title}

+ + ))} +
+ +
+
+ {LANDING_SIDEBAR_LINKS.slice(0, 5).map(link => ( + + {link.title} + + ))} +
+ +
+ + + + + + + + + +
+
+
+ ); +} diff --git a/apps/common/components/MobileTopNav.tsx b/apps/common/components/MobileTopNav.tsx new file mode 100644 index 000000000..3de809b2e --- /dev/null +++ b/apps/common/components/MobileTopNav.tsx @@ -0,0 +1,78 @@ +import {type ReactElement, useCallback} from 'react'; +import {useRouter} from 'next/router'; +import {useSearch} from '@common/contexts/useSearch'; +import {IconBurger} from '@common/icons/IconBurger'; +import {IconCross} from '@common/icons/IconCross'; +import {IconSearch} from '@common/icons/IconSearch'; +import {LogoYearn} from '@common/icons/LogoYearn'; + +import {SearchBar} from './SearchBar'; + +export function MobileTopNav({ + isSearchOpen, + isNavbarOpen, + set_isSearchOpen, + set_isNavbarOpen +}: { + isSearchOpen: boolean; + isNavbarOpen: boolean; + set_isSearchOpen: React.Dispatch>; + set_isNavbarOpen: React.Dispatch>; +}): ReactElement { + const {configuration, dispatch} = useSearch(); + const router = useRouter(); + + const onSearchClick = useCallback(() => { + if (!configuration.searchValue) { + return; + } + router.push(`/home/search?query=${configuration.searchValue}`); + }, [configuration.searchValue, router]); + + return ( +
+
+
+ + +
+ +
+ + {isSearchOpen && ( +
+ dispatch({searchValue: value})} + searchPlaceholder={'Search App'} + onSearchClick={onSearchClick} + shouldSearchByClick + /> +
+ )} +
+ ); +} diff --git a/apps/common/components/Pagination.tsx b/apps/common/components/Pagination.tsx index 1f0b0a1da..922b0b9b7 100644 --- a/apps/common/components/Pagination.tsx +++ b/apps/common/components/Pagination.tsx @@ -25,7 +25,7 @@ export function Pagination(props: TProps): ReactElement { role={'button'} href={'#'} className={ - 'border-gray-300 text-gray-700 hover:bg-gray-50 relative inline-flex items-center rounded-md border px-4 py-2 text-sm font-medium' + 'hover:bg-gray-50 relative inline-flex items-center rounded-md border border-gray-300 px-4 py-2 text-sm font-medium text-gray-500' }> {'Previous'} @@ -33,7 +33,7 @@ export function Pagination(props: TProps): ReactElement { role={'button'} href={'#'} className={ - 'border-gray-300 text-gray-700 hover:bg-gray-50 relative ml-3 inline-flex items-center rounded-md border px-4 py-2 text-sm font-medium' + 'hover:bg-gray-50 relative ml-3 inline-flex items-center rounded-md border border-gray-300 px-4 py-2 text-sm font-medium text-gray-500' }> {'Next'} diff --git a/apps/common/components/PromoPoster.tsx b/apps/common/components/PromoPoster.tsx new file mode 100644 index 000000000..e4a474ffc --- /dev/null +++ b/apps/common/components/PromoPoster.tsx @@ -0,0 +1,31 @@ +import Link from 'next/link'; +import {IconShare} from '@common/icons/IconShare'; + +import type {ReactElement} from 'react'; + +export function PromoPoster(): ReactElement { + return ( + +
+ {'earn with'} +
{'yearn'} +
+ +
+ +
+ +
+

+ { + 'Yearn is a decentralized suite of products helping individuals, DAOs, and other protocols earn yield on their digital assets.' + } +

+
+ + ); +} diff --git a/apps/common/components/SearchBar.tsx b/apps/common/components/SearchBar.tsx index c1dad29b8..4bc3e61f9 100644 --- a/apps/common/components/SearchBar.tsx +++ b/apps/common/components/SearchBar.tsx @@ -1,4 +1,6 @@ import {cl} from '@builtbymom/web3/utils'; +import {IconEnter} from '@common/icons/IconEnter'; +import {IconSearch} from '@common/icons/IconSearch'; import type {ChangeEvent, ReactElement} from 'react'; @@ -9,6 +11,8 @@ type TSearchBar = { className?: string; iconClassName?: string; inputClassName?: string; + shouldSearchByClick?: boolean; + onSearchClick?: () => void; }; export function SearchBar(props: TSearchBar): ReactElement { @@ -25,7 +29,7 @@ export function SearchBar(props: TSearchBar): ReactElement { suppressHydrationWarning className={cl( props.inputClassName, - 'h-10 w-full overflow-x-scroll border-none bg-transparent px-0 py-2 text-base outline-none scrollbar-none placeholder:text-neutral-400' + 'h-10 w-full overflow-x-scroll border-none bg-transparent pl-2 px-0 py-2 text-base outline-none scrollbar-none placeholder:text-neutral-400' )} type={'text'} placeholder={props.searchPlaceholder} @@ -33,23 +37,24 @@ export function SearchBar(props: TSearchBar): ReactElement { onChange={(e: ChangeEvent): void => { props.onSearch(e.target.value); }} + onKeyDown={e => { + if (!props.shouldSearchByClick) return; + if (e.key === 'Enter') { + return props.onSearchClick?.(); + } + }} /> -
- - - +
props.onSearchClick?.()} + className={cl(props.iconClassName, 'absolute right-0 text-neutral-400')}> + {props.shouldSearchByClick && props.searchValue ? ( +
+ +
+ ) : ( + + )}
diff --git a/apps/common/components/Sidebar.tsx b/apps/common/components/Sidebar.tsx new file mode 100644 index 000000000..6efc6ce01 --- /dev/null +++ b/apps/common/components/Sidebar.tsx @@ -0,0 +1,91 @@ +import {type ReactElement, useCallback} from 'react'; +import Link from 'next/link'; +import {usePathname} from 'next/navigation'; +import {useRouter} from 'next/router'; +import {cl} from '@builtbymom/web3/utils'; +import {useSearch} from '@common/contexts/useSearch'; +import {LogoYearn} from '@common/icons/LogoYearn'; +import {iconsDict, LANDING_SIDEBAR_LINKS} from '@common/utils/constants'; + +import {PromoPoster} from './PromoPoster'; +import {SearchBar} from './SearchBar'; + +type TSidebarProps = { + tabs: {route: string; title: string; isAcitve?: boolean}[]; +}; + +export function Sidebar(props: TSidebarProps): ReactElement { + const pathName = usePathname(); + const router = useRouter(); + const {configuration, dispatch} = useSearch(); + + const currentTab = pathName?.startsWith('/home/') ? pathName?.split('/')[2] : '/'; + + const onSearchClick = useCallback(() => { + if (!configuration.searchValue) { + router.push('/'); + return; + } + router.push(`/home/search/${encodeURIComponent(configuration.searchValue)}`); + }, [configuration.searchValue, router]); + + return ( +
+
+
+
+ + + +
+
+ +
+ dispatch({searchValue: value})} + shouldSearchByClick + onSearchClick={onSearchClick} + /> +
+
+ {props.tabs.map(tab => ( + +
+ {iconsDict[tab.route as '/' | 'community-apps' | 'vaults' | 'yearn-x' | 'integrations']} +
+

{tab.title}

+ + ))} +
+
+ +
+ {LANDING_SIDEBAR_LINKS.map(link => ( + + {link.title} + + ))} +
+
+ ); +} diff --git a/apps/common/components/SortingBar.tsx b/apps/common/components/SortingBar.tsx new file mode 100644 index 000000000..0d8d1668a --- /dev/null +++ b/apps/common/components/SortingBar.tsx @@ -0,0 +1,35 @@ +import {type ReactElement, useState} from 'react'; +import {IconChevron} from '@common/icons/IconChevron'; + +function SortItem({isActive, title}: {isActive: boolean; title: string}): ReactElement { + return
{title}
; +} + +export function SortingBar(): ReactElement { + const [isOpen, set_isOpen] = useState(false); + return ( + <> + + {isOpen && ( +
+ {Array(4) + .fill('List Item') + .map((item, i) => ( + + ))} +
+ )} + + ); +} diff --git a/apps/common/contexts/useSearch.tsx b/apps/common/contexts/useSearch.tsx new file mode 100644 index 000000000..384de5776 --- /dev/null +++ b/apps/common/contexts/useSearch.tsx @@ -0,0 +1,50 @@ +import {createContext, useContext, useState} from 'react'; +import {useDeepCompareMemo} from '@react-hookz/web'; +import {optionalRenderProps} from '@common/types/optionalRenderProps'; + +import type {Dispatch, ReactElement, SetStateAction} from 'react'; +import type {TOptionalRenderProps} from '@common/types/optionalRenderProps'; + +type TSearchContext = { + configuration: TSearchConfiguration; + dispatch: Dispatch>; +}; + +type TSearchConfiguration = { + searchValue: string; +}; + +const defaultProps = { + configuration: { + searchValue: '' + }, + dispatch: (): void => undefined +}; + +const SearchContext = createContext(defaultProps); +export const SearchContextApp = ({ + children +}: { + children: TOptionalRenderProps; +}): ReactElement => { + const [configuration, set_configuration] = useState(defaultProps.configuration); + + const contextValue = useDeepCompareMemo( + (): TSearchContext => ({configuration, dispatch: set_configuration}), + [configuration] + ); + + return ( + + {optionalRenderProps(children, contextValue)} + + ); +}; + +export const useSearch = (): TSearchContext => { + const ctx = useContext(SearchContext); + if (!ctx) { + throw new Error('SearchContext not found'); + } + return ctx; +}; diff --git a/apps/common/hooks/useInitialQueryParam.ts b/apps/common/hooks/useInitialQueryParam.ts new file mode 100644 index 000000000..cc7fc54f1 --- /dev/null +++ b/apps/common/hooks/useInitialQueryParam.ts @@ -0,0 +1,41 @@ +import {useEffect, useState} from 'react'; +import {useRouter} from 'next/router'; + +/************************************************************************************************ + ** useInitialQueryParam Hook + ** + ** This custom hook is designed to retrieve and manage the initial query parameter from the URL. + ** It handles both client-side and server-side rendering scenarios, ensuring that the query + ** parameter is correctly retrieved regardless of the rendering context. + ** + ** The hook performs the following tasks: + ** 1. On the client-side, it initially checks the URL for the query parameter. + ** 2. Once the router is ready, it updates the value based on the router's query object. + ** 3. It returns the current value of the query parameter, which can be used in the component. + ** + ** @param {string} key - The name of the query parameter to retrieve + ** @returns {string | null} - The value of the query parameter, or null if not found + ************************************************************************************************/ +export function useInitialQueryParam(key: string): string | null { + const router = useRouter(); + const [value, set_value] = useState(null); + + useEffect(() => { + if (typeof window !== 'undefined') { + const urlParams = new URLSearchParams(window.location.search); + const initialValue = urlParams.get(key); + set_value(initialValue); + } + }, [key]); + + useEffect(() => { + if (router.isReady && !value) { + const queryValue = router.query[key] as string; + if (queryValue) { + set_value(queryValue); + } + } + }, [router.isReady, router.query, key, value]); + + return value; +} diff --git a/apps/common/icons/IconAbout.tsx b/apps/common/icons/IconAbout.tsx new file mode 100644 index 000000000..db220aa22 --- /dev/null +++ b/apps/common/icons/IconAbout.tsx @@ -0,0 +1,23 @@ +import type {ReactElement} from 'react'; + +export function IconAbout(props: React.SVGProps): ReactElement { + return ( + + + + ); +} diff --git a/apps/common/icons/IconBurger.tsx b/apps/common/icons/IconBurger.tsx new file mode 100644 index 000000000..2bb5521aa --- /dev/null +++ b/apps/common/icons/IconBurger.tsx @@ -0,0 +1,35 @@ +import type {ReactElement} from 'react'; + +export function IconBurger(props: React.SVGProps): ReactElement { + return ( + + + + + + ); +} diff --git a/apps/common/icons/IconChevron.tsx b/apps/common/icons/IconChevron.tsx index 72044e721..21785373c 100755 --- a/apps/common/icons/IconChevron.tsx +++ b/apps/common/icons/IconChevron.tsx @@ -4,18 +4,18 @@ export function IconChevron(props: React.SVGProps): ReactElement return ( ); diff --git a/apps/common/icons/IconCommunity.tsx b/apps/common/icons/IconCommunity.tsx new file mode 100644 index 000000000..088bc4371 --- /dev/null +++ b/apps/common/icons/IconCommunity.tsx @@ -0,0 +1,30 @@ +import type {ReactElement} from 'react'; + +export function IconCommunity(props: React.SVGProps): ReactElement { + return ( + + + + + ); +} diff --git a/apps/common/icons/IconCross.tsx b/apps/common/icons/IconCross.tsx new file mode 100644 index 000000000..e50e81c0a --- /dev/null +++ b/apps/common/icons/IconCross.tsx @@ -0,0 +1,42 @@ +import type {ReactElement} from 'react'; + +export function IconCross(props: React.SVGProps): ReactElement { + return ( + + + + + + + ); +} diff --git a/apps/common/icons/IconEnter.tsx b/apps/common/icons/IconEnter.tsx new file mode 100644 index 000000000..033b92ecf --- /dev/null +++ b/apps/common/icons/IconEnter.tsx @@ -0,0 +1,39 @@ +import type {ReactElement} from 'react'; + +export function IconEnter(props: React.SVGProps): ReactElement { + return ( + + + + + + + + + + + + ); +} diff --git a/apps/common/icons/IconFrontends.tsx b/apps/common/icons/IconFrontends.tsx new file mode 100644 index 000000000..fd355f05e --- /dev/null +++ b/apps/common/icons/IconFrontends.tsx @@ -0,0 +1,30 @@ +import type {ReactElement} from 'react'; + +export function IconFrontends(props: React.SVGProps): ReactElement { + return ( + + + + + ); +} diff --git a/apps/common/icons/IconIntegrations.tsx b/apps/common/icons/IconIntegrations.tsx new file mode 100644 index 000000000..a2daae829 --- /dev/null +++ b/apps/common/icons/IconIntegrations.tsx @@ -0,0 +1,20 @@ +import type {ReactElement} from 'react'; + +export function IconIntegrations(props: React.SVGProps): ReactElement { + return ( + + + + ); +} diff --git a/apps/common/icons/IconLock.tsx b/apps/common/icons/IconLock.tsx new file mode 100644 index 000000000..557ca3f88 --- /dev/null +++ b/apps/common/icons/IconLock.tsx @@ -0,0 +1,32 @@ +import type {ReactElement} from 'react'; + +export function IconLock(props: React.SVGProps): ReactElement { + return ( + + + + + ); +} diff --git a/apps/common/icons/IconPools.tsx b/apps/common/icons/IconPools.tsx new file mode 100644 index 000000000..9e5aee6e3 --- /dev/null +++ b/apps/common/icons/IconPools.tsx @@ -0,0 +1,22 @@ +import type {ReactElement} from 'react'; + +export function IconPools(props: React.SVGProps): ReactElement { + return ( + + + + ); +} diff --git a/apps/common/icons/IconSearch.tsx b/apps/common/icons/IconSearch.tsx new file mode 100644 index 000000000..6002b24a8 --- /dev/null +++ b/apps/common/icons/IconSearch.tsx @@ -0,0 +1,22 @@ +import type {ReactElement} from 'react'; + +export function IconSearch(props: React.SVGProps): ReactElement { + return ( + + + + ); +} diff --git a/apps/common/icons/IconShare.tsx b/apps/common/icons/IconShare.tsx new file mode 100644 index 000000000..4548763c2 --- /dev/null +++ b/apps/common/icons/IconShare.tsx @@ -0,0 +1,28 @@ +import type {ReactElement} from 'react'; + +export function IconShare(props: React.SVGProps): ReactElement { + return ( + + + + + ); +} diff --git a/apps/common/icons/IconVaults.tsx b/apps/common/icons/IconVaults.tsx new file mode 100644 index 000000000..6789997aa --- /dev/null +++ b/apps/common/icons/IconVaults.tsx @@ -0,0 +1,64 @@ +import type {ReactElement} from 'react'; + +export function IconVaults(props: React.SVGProps): ReactElement { + return ( + + + + + + + + + ); +} diff --git a/apps/common/icons/IconYearn.tsx b/apps/common/icons/IconYearn.tsx new file mode 100644 index 000000000..d8b7e4b3b --- /dev/null +++ b/apps/common/icons/IconYearn.tsx @@ -0,0 +1,26 @@ +import type {ReactElement} from 'react'; + +export function IconYearn(props: React.SVGProps): ReactElement { + return ( + + + + + ); +} diff --git a/apps/common/icons/IconYearnXApps.tsx b/apps/common/icons/IconYearnXApps.tsx new file mode 100644 index 000000000..519dc9510 --- /dev/null +++ b/apps/common/icons/IconYearnXApps.tsx @@ -0,0 +1,50 @@ +import type {ReactElement} from 'react'; + +export function IconYearnXApps(props: React.SVGProps): ReactElement { + return ( + + + + + + + + + ); +} diff --git a/apps/common/icons/LogoDiscord.tsx b/apps/common/icons/LogoDiscord.tsx new file mode 100644 index 000000000..bfce36d00 --- /dev/null +++ b/apps/common/icons/LogoDiscord.tsx @@ -0,0 +1,20 @@ +import type {ReactElement} from 'react'; + +export function LogoDiscord(props: React.SVGProps): ReactElement { + return ( + + + + ); +} diff --git a/apps/common/icons/LogoDiscordRound.tsx b/apps/common/icons/LogoDiscordRound.tsx new file mode 100644 index 000000000..7475cd61d --- /dev/null +++ b/apps/common/icons/LogoDiscordRound.tsx @@ -0,0 +1,22 @@ +import type {ReactElement} from 'react'; + +export function LogoDiscordRound(props: React.SVGProps): ReactElement { + return ( + + + + ); +} diff --git a/apps/common/icons/LogoParagraphRound.tsx b/apps/common/icons/LogoParagraphRound.tsx new file mode 100644 index 000000000..284186e58 --- /dev/null +++ b/apps/common/icons/LogoParagraphRound.tsx @@ -0,0 +1,22 @@ +import type {ReactElement} from 'react'; + +export function LogoParagraphRound(props: React.SVGProps): ReactElement { + return ( + + + + ); +} diff --git a/apps/common/icons/LogoTwitter.tsx b/apps/common/icons/LogoTwitter.tsx new file mode 100644 index 000000000..4cef65247 --- /dev/null +++ b/apps/common/icons/LogoTwitter.tsx @@ -0,0 +1,20 @@ +import type {ReactElement} from 'react'; + +export function LogoTwitter(props: React.SVGProps): ReactElement { + return ( + + + + ); +} diff --git a/apps/common/icons/LogoTwitterRound.tsx b/apps/common/icons/LogoTwitterRound.tsx new file mode 100644 index 000000000..77e641818 --- /dev/null +++ b/apps/common/icons/LogoTwitterRound.tsx @@ -0,0 +1,22 @@ +import type {ReactElement} from 'react'; + +export function LogoTwitterRound(props: React.SVGProps): ReactElement { + return ( + + + + ); +} diff --git a/apps/common/types/category.ts b/apps/common/types/category.ts index 3cda3d496..806623839 100644 --- a/apps/common/types/category.ts +++ b/apps/common/types/category.ts @@ -18,3 +18,10 @@ export type TVaultListHeroCategory = (typeof VAULT_CATEGORIES)[number]; export function isValidCategory(input: string): input is T { return VAULT_CATEGORIES.includes(input as TVaultListHeroCategory); } + +export type TApp = { + name: string; + description?: string; + logoURI: string; + appURI: string; +}; diff --git a/apps/common/types/optionalRenderProps.ts b/apps/common/types/optionalRenderProps.ts new file mode 100644 index 000000000..57f502c45 --- /dev/null +++ b/apps/common/types/optionalRenderProps.ts @@ -0,0 +1,6 @@ +import type {ReactNode} from 'react'; + +export type TOptionalRenderProps = TChildren | ((renderProps: TProps) => TChildren); + +export const optionalRenderProps = (children: TOptionalRenderProps, renderProps: TProps): ReactNode => + typeof children === 'function' ? children(renderProps) : children; diff --git a/apps/common/utils/constants.ts b/apps/common/utils/constants.ts deleted file mode 100644 index be2898de1..000000000 --- a/apps/common/utils/constants.ts +++ /dev/null @@ -1,13 +0,0 @@ -import {arbitrum, base, fantom, mainnet, optimism, polygon} from 'viem/chains'; -import {toAddress} from '@builtbymom/web3/utils'; - -import type {TAddress, TNDict} from '@builtbymom/web3/types'; - -export const DEFAULT_SLIPPAGE = 0.5; -export const DEFAULT_MAX_LOSS = 1n; -export const YGAUGES_ZAP_ADDRESS = toAddress('0x1104215963474A0FA0Ac09f4E212EF7282F2A0bC'); //Address of the zap to deposit & stake in the veYFI gauge -export const V3_STAKING_ZAP_ADDRESS: TNDict = { - [mainnet.id]: toAddress('0x5435cA9b6D9468A6e0404a4819D39ebbF036DB1E'), - [arbitrum.id]: toAddress('0x1E789A49902370E5858Fae67518aF49d8deA299c') -}; //Address of the zap to deposit & stake for the v3 staking -export const SUPPORTED_NETWORKS = [mainnet, optimism, polygon, fantom, base, arbitrum]; diff --git a/apps/common/utils/constants.tsx b/apps/common/utils/constants.tsx new file mode 100644 index 000000000..75aba9e45 --- /dev/null +++ b/apps/common/utils/constants.tsx @@ -0,0 +1,289 @@ +import {arbitrum, base, fantom, mainnet, optimism, polygon} from 'viem/chains'; +import {toAddress} from '@builtbymom/web3/utils'; +import {IconAbout} from '@common/icons/IconAbout'; +import {IconFrontends} from '@common/icons/IconFrontends'; +import {IconIntegrations} from '@common/icons/IconIntegrations'; +import {IconVaults} from '@common/icons/IconVaults'; +import {IconYearn} from '@common/icons/IconYearn'; +import {IconYearnXApps} from '@common/icons/IconYearnXApps'; + +import type {TAddress, TNDict} from '@builtbymom/web3/types'; +import type {TApp} from '@common/types/category'; + +export const DEFAULT_SLIPPAGE = 0.5; +export const DEFAULT_MAX_LOSS = 1n; +export const YGAUGES_ZAP_ADDRESS = toAddress('0x1104215963474A0FA0Ac09f4E212EF7282F2A0bC'); //Address of the zap to deposit & stake in the veYFI gauge +export const V3_STAKING_ZAP_ADDRESS: TNDict = { + [mainnet.id]: toAddress('0x5435cA9b6D9468A6e0404a4819D39ebbF036DB1E'), + [arbitrum.id]: toAddress('0x1E789A49902370E5858Fae67518aF49d8deA299c') +}; //Address of the zap to deposit & stake for the v3 staking +export const SUPPORTED_NETWORKS = [mainnet, optimism, polygon, fantom, base, arbitrum]; + +export const VAULTS_APPS: TApp[] = [ + { + name: 'Gimme', + description: 'DeFi yields, designed for everyone.', + logoURI: 'https://gimme.mom/favicons/favicon-96x96.png', + appURI: 'https://gimme.mom/' + }, + { + name: 'Vaults', + description: 'The full Yearn experience with all Vaults, for sophisticated users.', + logoURI: '/v3.png', + appURI: 'https://yearn.fi/v3' + }, + { + name: 'Vaults V2', + description: "Discover Vaults from Yearn's v2 era.", + logoURI: '/v2.png', + appURI: 'https://yearn.fi/vaults' + }, + { + name: 'Juiced', + description: 'Discover yields juiced with extra token rewards.', + logoURI: '/juiced-featured.jpg', + appURI: 'https://juiced.app/' + } +]; + +export const COMMUNITY_APPS: TApp[] = [ + { + name: 'yETH', + description: 'A basket of LSTs in a single token.', + logoURI: 'https://yeth.yearn.fi/favicons/favicon-96x96.png', + appURI: 'https://yeth.yearn.fi/' + }, + { + name: 'veYFI', + description: 'Stake YFI to earn yield, boost gauges, and take part in governance.', + logoURI: 'https://assets.smold.app/api/token/1/0x41252E8691e964f7DE35156B68493bAb6797a275/logo-128.png', + appURI: 'https://veyfi.yearn.fi' + }, + { + name: 'yCRV', + description: 'Put your yCRV to work.', + logoURI: 'https://ycrv.yearn.fi/ycrv-logo.svg', + appURI: 'https://ycrv.yearn.fi' + }, + { + name: 'yPrisma', + description: 'Put your yPRISMA to work.', + logoURI: 'https://assets.smold.app/api/token/1/0xe3668873d944e4a949da05fc8bde419eff543882/logo-128.png', + appURI: 'https://yprisma.yearn.fi' + } +]; + +export const YEARN_X_APPS: TApp[] = [ + { + name: 'PoolTogether', + description: 'Get the best risk adjusted PoolTogether yields, with Yearn.', + logoURI: 'https://pooltogether.yearn.space/favicons/favicon-512x512.png', + appURI: 'https://pooltogether.yearn.space' + }, + { + name: 'Pendle', + description: 'The best Pendle yields, with auto-rolling functionality.', + logoURI: 'https://pendle.yearn.space/favicons/favicon-512x512.png', + appURI: 'https://pendle.yearn.space' + }, + { + name: 'AJNA', + description: 'Get the best risk adjusted Ajna yields, with Yearn.', + logoURI: 'https://ajna.yearn.space/favicons/favicon-512x512.png', + appURI: 'https://ajna.yearn.space' + }, + { + name: 'Velodrome', + description: 'Get the best risk adjusted Velodrome yields, with Yearn.', + logoURI: 'https://velodrome.yearn.space/favicons/favicon-512x512.png', + appURI: 'https://velodrome.yearn.space/' + }, + { + name: 'Aerodrome', + description: 'Get the best risk adjusted Aerodrome yields, with Yearn.', + logoURI: 'https://aerodrome.yearn.space/favicons/favicon-512x512.png', + appURI: 'https://aerodrome.yearn.space/' + }, + { + name: 'Curve', + description: 'Get the best risk adjusted Curve yields, with Yearn.', + logoURI: 'https://curve.yearn.space/favicons/favicon-512x512.png', + appURI: 'https://curve.yearn.space/' + } +]; + +export const POOLS_APPS: TApp[] = []; + +export const INTEGRATIONS_APPS: TApp[] = [ + { + name: 'Cove', + description: 'Earn the best yields on-chain without the hassle of managing a portfolio.', + logoURI: + 'https://assets-global.website-files.com/651af12fcd3055636b6ac9ad/66242dbf1d6e7ff1b18336c4_Twitter%20pp%20-%20Logo%202.png', + appURI: 'https://cove.finance/' + }, + { + name: '1UP', + description: '1UP is a public good liquid locker for YFI.', + logoURI: 'https://1up.tokyo/logo.svg', + appURI: 'https://1up.tokyo/' + }, + { + name: 'StakeDAO', + description: 'A non-custodial liquid staking platform focused on governance tokens.', + logoURI: 'https://www.stakedao.org/logo.png', + appURI: 'https://www.stakedao.org' + }, + { + name: 'Sturdy', + description: 'Isolated lending with shared liquidity.', + logoURI: 'https://avatars.githubusercontent.com/u/90377574?s=200&v=4', + appURI: 'https://v2.sturdy.finance' + }, + { + name: 'PWN', + description: 'PWN is a hub for peer-to-peer (P2P) loans backed by digital assets.', + logoURI: + 'https://3238501125-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FtZYbaMzoeA7Kw4Stxzvw%2Ficon%2F89KZ4VyGSZ33cSf5QBmo%2Fpwn.svg?alt=media', + appURI: 'https://app.pwn.xyz/' + }, + { + name: 'Superform', + description: 'Earn Yield & Distribute Vaults', + logoURI: 'https://www.superform.xyz/icon.png', + appURI: 'https://www.superform.xyz' + } +]; + +export const FEATURED_APPS = [ + { + name: 'Juiced', + description: 'Discover yields juiced with extra token rewards.', + logoURI: '/juiced-featured.jpg', + appURI: 'https://juiced.app/' + }, + { + name: 'Gimme', + description: 'DeFi yields, designed for everyone.', + logoURI: '/gimme-featured.jpg', + appURI: 'https://gimme.mom/' + }, + { + name: 'Vaults', + description: 'The full Yearn experience with all Vaults, for sophisticated users.', + logoURI: '/v3-featured.jpg', + appURI: '/v3' + }, + { + name: 'Juiced', + description: 'Discover yields juiced with extra token rewards.', + logoURI: '/juiced-featured.jpg', + appURI: 'https://juiced.app/' + }, + { + name: 'Gimme', + description: 'DeFi yields, designed for everyone.', + logoURI: '/gimme-featured.jpg', + appURI: 'https://gimme.mom/' + }, + { + name: 'Vaults', + description: 'The full Yearn experience with all Vaults, for sophisticated users.', + logoURI: '/v3-featured.jpg', + appURI: '/v3' + }, + { + name: 'Juiced', + description: 'Discover yields juiced with extra token rewards.', + logoURI: '/juiced-featured.jpg', + appURI: 'https://juiced.app/' + }, + { + name: 'Gimme', + description: 'DeFi yields, designed for everyone.', + logoURI: '/gimme-featured.jpg', + appURI: 'https://gimme.mom/' + }, + { + name: 'Vaults', + description: 'The full Yearn experience with all Vaults, for sophisticated users.', + logoURI: '/v3-featured.jpg', + appURI: '/v3' + } +]; + +export const ALL_APPS = [...FEATURED_APPS, ...VAULTS_APPS, ...COMMUNITY_APPS, ...YEARN_X_APPS, ...INTEGRATIONS_APPS]; + +export const CATEGORIES_DICT = { + 'featured-apps': { + categoryName: 'Featured apps', + categoryDescription: + 'Yearn is a decentralized suite of products helping individuals, DAOs, and other protocols earn yield on their digital assets.', + catrgorySlug: 'featured-apps', + apps: FEATURED_APPS + }, + vaults: { + categoryName: 'Vaults', + categoryDescription: + 'Yearn is a decentralized suite of products helping individuals, DAOs, and other protocols earn yield on their digital assets.', + catrgorySlug: 'vaults', + apps: VAULTS_APPS + }, + 'community-apps': { + categoryName: 'Community Apps', + categoryDescription: + 'Yearn is a decentralized suite of products helping individuals, DAOs, and other protocols earn yield on their digital assets.', + catrgorySlug: 'community-apps', + apps: COMMUNITY_APPS + }, + 'yearn-x': { + categoryName: 'Yearn X Projects', + categoryDescription: + 'Yearn is a decentralized suite of products helping individuals, DAOs, and other protocols earn yield on their digital assets.', + catrgorySlug: 'yearn-x', + apps: YEARN_X_APPS + }, + integrations: { + categoryName: 'Integrations', + categoryDescription: + 'Yearn is a decentralized suite of products helping individuals, DAOs, and other protocols earn yield on their digital assets.', + catrgorySlug: 'integrations', + apps: INTEGRATIONS_APPS + } +}; + +export const LANDING_SIDEBAR_LINKS = [ + {title: 'Governance', href: 'https://gov.yearn.fi/'}, + {title: 'API', href: 'https://github.com/yearn/ydaemon'}, + {title: 'Docs', href: 'https://docs.yearn.fi/'}, + {title: 'Blog', href: 'https://blog.yearn.fi/'}, + {title: 'Support', href: 'https://discord.com/invite/yearn'}, + {title: 'Discord', href: 'https://discord.com/invite/yearn'}, + {title: 'Paragraph', href: ''}, + {title: 'Twitter', href: 'https://twitter.com/yearnfi'} +]; + +export const MENU_TABS = [ + {title: 'Home', route: '/'}, + {title: 'Vaults', route: 'vaults'}, + {title: 'Community Apps', route: 'community-apps'}, + {title: 'Yearn X Projects', route: 'yearn-x'}, + {title: 'Integrations', route: 'integrations'} + // {title: 'About', route: 'about'} +]; + +export const CATEGORY_PAGE_FILTERS = [ + {title: 'All', value: 'all'}, + {title: 'Filter', value: 'filter'}, + {title: 'Tab', value: 'tab'}, + {title: 'Large Filter', value: 'large-filter'} +]; + +export const iconsDict = { + '/': , + about: , + vaults: , + 'community-apps': , + 'yearn-x': , + integrations: +}; diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 000000000..1df87f06a Binary files /dev/null and b/bun.lockb differ diff --git a/next-env.d.ts b/next-env.d.ts index a4a7b3f5c..4f11a03dc 100755 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/next.config.js b/next.config.js index 7757019f5..2a10ffdcb 100755 --- a/next.config.js +++ b/next.config.js @@ -28,6 +28,10 @@ module.exports = withPlausibleProxy({ { protocol: 'https', hostname: '**.yearn.fi' + }, + { + protocol: 'https', + hostname: '**.gimme.mom' } ] }, @@ -106,6 +110,11 @@ module.exports = withPlausibleProxy({ source: '/static/tokenlist.json', destination: 'https://raw.githubusercontent.com/SmolDapp/tokenLists/main/lists/yearn.json', permanent: true + }, + { + source: '/home/search', + destination: '/', + permanent: true } ]; }, diff --git a/pages/_app.tsx b/pages/_app.tsx index fa08b8784..657633e7b 100755 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,7 +1,5 @@ -import React, {memo} from 'react'; +import React, {memo, useState} from 'react'; import {Toaster} from 'react-hot-toast'; -import {usePathname} from 'next/navigation'; -import {useRouter} from 'next/router'; import PlausibleProvider from 'next-plausible'; import {AnimatePresence, domAnimation, LazyMotion, motion} from 'framer-motion'; import {WithMom} from '@builtbymom/web3/contexts/WithMom'; @@ -12,13 +10,18 @@ import {IconAlertError} from '@yearn-finance/web-lib/icons/IconAlertError'; import {IconCheckmark} from '@yearn-finance/web-lib/icons/IconCheckmark'; import AppHeader from '@common/components/Header'; import {Meta} from '@common/components/Meta'; +import {MobileNavbar} from '@common/components/MobileNavbar'; +import {MobileTopNav} from '@common/components/MobileTopNav'; +import {Sidebar} from '@common/components/Sidebar'; import {WithFonts} from '@common/components/WithFonts'; +import {SearchContextApp} from '@common/contexts/useSearch'; import {YearnContextApp} from '@common/contexts/useYearn'; import {useCurrentApp} from '@common/hooks/useCurrentApp'; import {variants} from '@common/utils/animations'; -import {SUPPORTED_NETWORKS} from '@common/utils/constants'; +import {MENU_TABS, SUPPORTED_NETWORKS} from '@common/utils/constants'; import type {AppProps} from 'next/app'; +import type {NextRouter} from 'next/router'; import type {ReactElement} from 'react'; import type {Chain} from 'viem'; @@ -38,11 +41,73 @@ import '../style.css'; ** The returned JSX structure is a div with the 'AppHeader' component, the current page component ** wrapped with layout, and the feedback popover if it should not be hidden. **************************************************************************************************/ -const WithLayout = memo(function WithLayout(props: {supportedNetworks: Chain[]} & AppProps): ReactElement { - const router = useRouter(); +const WithLayout = memo(function WithLayout( + props: {router: NextRouter; supportedNetworks: Chain[]} & AppProps +): ReactElement { const {Component, pageProps} = props; - const pathName = usePathname(); - const {name} = useCurrentApp(router); + const {name} = useCurrentApp(props.router); + const [isSearchOpen, set_isSearchOpen] = useState(false); + const [isNavbarOpen, set_isNavbarOpen] = useState(false); + const isOnLanding = props.router.asPath?.startsWith('/home/') || props.router.asPath === '/'; + + if (isOnLanding) { + return ( + +
+
+ + + + {isNavbarOpen && ( + + { + set_isNavbarOpen(false); + set_isSearchOpen(false); + }} + /> + + )} + + + + + + + + +
+
+
+ ); + } return ( <> @@ -56,7 +121,7 @@ const WithLayout = memo(function WithLayout(props: {supportedNetworks: Chain[]} -
+
diff --git a/pages/_document.tsx b/pages/_document.tsx index 539e89708..1048878c9 100755 --- a/pages/_document.tsx +++ b/pages/_document.tsx @@ -47,16 +47,21 @@ window.onload = observeUrlChange; `; class MyDocument extends Document { - static async getInitialProps(ctx: DocumentContext): Promise { + static async getInitialProps(ctx: DocumentContext): Promise { const initialProps = await Document.getInitialProps(ctx); - return {...initialProps}; + + // Determine the route from context + const route = ctx.pathname; + return {route, ...initialProps}; } render(): ReactElement { + const {route} = this.props as any; + const isLanding = route === '/' || route.startsWith('/home/'); return ( + className={`duration-150', bg-neutral-0 transition-colors ${isLanding && 'scrollbar-none'}`}>