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.description}
+
+
+
+ {props.app.logoURI ? (
+
+
+
+ ) : (
+
+ )}
+
+
+
+
{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 (
+
+
+
+
+
+
+ );
+}
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.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'}`}>
diff --git a/pages/home/[category].tsx b/pages/home/[category].tsx
new file mode 100644
index 000000000..d405aa7b9
--- /dev/null
+++ b/pages/home/[category].tsx
@@ -0,0 +1,58 @@
+import {type ReactElement, useMemo, useState} from 'react';
+import {useMountEffect} from '@react-hookz/web';
+import {AppCard} from '@common/components/AppCard';
+import {FilterBar} from '@common/components/FilterBar';
+import {SortingBar} from '@common/components/SortingBar';
+import {CATEGORIES_DICT} from '@common/utils/constants';
+
+import type {NextRouter} from 'next/router';
+import type {TApp} from '@common/types/category';
+
+export default function Index(props: {router: NextRouter}): ReactElement {
+ const [shuffledApps, set_shuffledApps] = useState();
+ const currentCatrgory = useMemo(() => {
+ const currentTab = props.router.asPath?.startsWith('/home/') ? props.router.asPath?.split('/')[2] : '/';
+ return CATEGORIES_DICT[currentTab as keyof typeof CATEGORIES_DICT];
+ }, [props.router.asPath]);
+
+ /**********************************************************************************************
+ ** On component mount we shuffle the array of Apps to avoid any bias.
+ **********************************************************************************************/
+ useMountEffect(() => {
+ if (currentCatrgory?.apps.length < 1) {
+ return;
+ }
+ set_shuffledApps(currentCatrgory?.apps.toSorted(() => 0.5 - Math.random()));
+ });
+
+ return (
+
+
+
+
+ {currentCatrgory?.categoryName}
+
+
+
+ {currentCatrgory?.categoryDescription}
+
+
+
+
+
+
+
+
+
+
+ {shuffledApps?.map(app =>
)}
+
+
+
+ );
+}
diff --git a/pages/home/index.tsx b/pages/home/index.tsx
new file mode 100644
index 000000000..077a41a3b
--- /dev/null
+++ b/pages/home/index.tsx
@@ -0,0 +1,94 @@
+import {type ReactElement, useRef} from 'react';
+import {useRouter} from 'next/router';
+import {useMountEffect} from '@react-hookz/web';
+import {CarouselSlideArrows} from '@common/CarouselSlideArrows';
+import {AppsCarousel} from '@common/components/AppsCarousel';
+import {CategorySection} from '@common/components/CategorySection';
+import {Cutaway} from '@common/components/Cutaway';
+import {PromoPoster} from '@common/components/PromoPoster';
+import {useSearch} from '@common/contexts/useSearch';
+import {LogoDiscord} from '@common/icons/LogoDiscord';
+import {LogoTwitter} from '@common/icons/LogoTwitter';
+import {COMMUNITY_APPS, FEATURED_APPS, INTEGRATIONS_APPS, VAULTS_APPS, YEARN_X_APPS} from '@common/utils/constants';
+
+export default function Home(): ReactElement {
+ const router = useRouter();
+ const {dispatch} = useSearch();
+
+ const carouselRef = useRef(null);
+
+ const onScrollBack = (): void => {
+ if (!carouselRef.current) return;
+ carouselRef.current.scrollLeft -= 400;
+ };
+
+ const onScrollForward = (): void => {
+ if (!carouselRef.current) return;
+ carouselRef.current.scrollLeft += 400;
+ };
+
+ useMountEffect(() => {
+ dispatch({searchValue: ''});
+ });
+
+ return (
+
+
+
+
+
+
+
+
+ router.push('/home/vaults')}
+ apps={VAULTS_APPS}
+ />
+ router.push('/home/community-apps')}
+ apps={COMMUNITY_APPS}
+ />
+ router.push('/home/yearn-x')}
+ apps={YEARN_X_APPS}
+ />
+ router.push('/home/integrations')}
+ apps={INTEGRATIONS_APPS}
+ />
+
+
+
+ }
+ link={'https://yearn.finance/twitter'}
+ />
+ }
+ link={'https://discord.com/invite/yearn'}
+ />
+
+
+
+ );
+}
diff --git a/pages/home/search/[query].tsx b/pages/home/search/[query].tsx
new file mode 100644
index 000000000..00c04ebfe
--- /dev/null
+++ b/pages/home/search/[query].tsx
@@ -0,0 +1,93 @@
+import {type ReactElement, useMemo} from 'react';
+import Image from 'next/image';
+import Link from 'next/link';
+import {cl} from '@builtbymom/web3/utils';
+import {AppCard} from '@common/components/AppCard';
+import {useInitialQueryParam} from '@common/hooks/useInitialQueryParam';
+import {ALL_APPS} from '@common/utils/constants';
+
+import type {GetServerSidePropsContext} from 'next';
+
+export default function SeachResults(): ReactElement {
+ const searchValue = useInitialQueryParam('query');
+ const searchFilteredApps = useMemo(() => {
+ if (!searchValue) {
+ return [];
+ }
+ return ALL_APPS.filter(app => app.name.toLowerCase().includes(searchValue.toLowerCase()));
+ }, [searchValue]);
+
+ return (
+
+
+
+
{`Results for "${searchValue}"`}
+ {searchFilteredApps.length < 1 ? (
+
+
+ {`Hmm, we couldn't find what you're looking for, did you spell it right? Try again or go`}{' '}
+
+ {'home'}
+
+
+
+
+
+
+
+ ) : (
+
+ {searchFilteredApps.map((app, index) => (
+
+ ))}
+
+ )}
+
+
+
+ );
+}
+
+/************************************************************************************************
+ ** getServerSideProps is used here for the following reasons:
+ ** 1. To extract the search query from the URL parameters on the server-side
+ ** 2. To ensure the search query is available as a prop when the page is initially rendered
+ ** 3. To enable server-side rendering (SSR) for this dynamic route, improving SEO and performance
+ ** 4. To handle cases where the query might be undefined, providing a fallback empty string
+ ** This approach allows for immediate access to the search query without client-side processing
+ ************************************************************************************************/
+export async function getServerSideProps(context: GetServerSidePropsContext): Promise<{props: {query: string}}> {
+ const {query} = context.params as {query: string};
+ return {
+ props: {
+ query: query || ''
+ }
+ };
+}
diff --git a/pages/index.tsx b/pages/index.tsx
index 8f3372d6b..f17ec3d44 100755
--- a/pages/index.tsx
+++ b/pages/index.tsx
@@ -1,265 +1,9 @@
-import {useEffect, useRef} from 'react';
-import Link from 'next/link';
-import {YCRV_TOKEN_ADDRESS} from '@yearn-finance/web-lib/utils/constants';
-import {ImageWithFallback} from '@common/components/ImageWithFallback';
-import {LogoYearn} from '@common/icons/LogoYearn';
+import Home from './home';
import type {ReactElement} from 'react';
-const apps = [
- {
- href: '/v3',
- title: 'V3',
- description: 'Deposit tokens and receive great yields.',
- icon: (
-
- )
- },
- {
- href: 'https://gimme.mom/',
- title: 'GIMME',
- description: 'Yields made simple.',
- icon: (
-
- )
- },
- {
- href: 'https://juiced.yearn.fi',
- title: 'Juiced Vaults',
- description: 'Freshly squeezed and bursting with yield.',
- icon: (
-
- )
- },
- {
- href: '/vaults',
- title: 'Vaults V2',
- description: 'Deposit tokens and receive yield.',
- icon: (
-
- )
- },
- {
- href: 'https://veyfi.yearn.fi',
- title: 'veYFI',
- description: 'Lock YFI\nto take part in governance.',
- icon: (
-
- )
- },
- {
- href: 'https://ycrv.yearn.fi',
- title: 'yCRV',
- description: 'Get the best CRV yields in DeFi.',
- icon: (
-
- )
- },
- {
- href: 'https://yeth.yearn.fi',
- title: 'yETH',
- description: 'Simple, straight forward, risk adjusted liquid staking yield.',
- icon: (
-
- )
- },
- {
- href: 'https://yprisma.yearn.fi',
- title: 'yPrisma',
- description: 'Every rainbow needs a pot of gold.',
- icon: (
-
- )
- }
-];
-function AppBox({app}: {app: (typeof apps)[0]}): ReactElement {
- return (
-
-
-
{app.icon}
-
-
{app.title}
-
{app.description}
-
-
-
- );
-}
-function TextAnimation(): ReactElement {
- const hasBeenTriggerd = useRef(false);
-
- function onStartAnimation(): void {
- hasBeenTriggerd.current = true;
- const words = document.getElementsByClassName('word') as HTMLCollectionOf;
- const wordArray: HTMLSpanElement[][] = [];
- let currentWord = 0;
-
- words[currentWord].style.opacity = '1';
- for (const word of Array.from(words)) {
- splitLetters(word);
- }
-
- function changeWord(): void {
- const cw = wordArray[currentWord];
- const nw = currentWord == words.length - 1 ? wordArray[0] : wordArray[currentWord + 1];
- if (!cw || !nw) {
- return;
- }
- for (let i = 0; i < cw.length; i++) {
- animateLetterOut(cw, i);
- }
-
- for (let i = 0; i < nw.length; i++) {
- nw[i].className = 'letter behind';
- if (nw?.[0]?.parentElement?.style) {
- nw[0].parentElement.style.opacity = '1';
- }
- animateLetterIn(nw, i);
- }
- currentWord = currentWord == wordArray.length - 1 ? 0 : currentWord + 1;
- }
-
- function animateLetterOut(cw: HTMLSpanElement[], i: number): void {
- setTimeout((): void => {
- cw[i].className = 'letter out';
- }, i * 80);
- }
-
- function animateLetterIn(nw: HTMLSpanElement[], i: number): void {
- setTimeout(
- (): void => {
- nw[i].className = 'letter in';
- },
- 340 + i * 80
- );
- }
-
- function splitLetters(word: HTMLSpanElement): void {
- const content = word.innerHTML;
- word.innerHTML = '';
- const letters = [];
- for (let i = 0; i < content.length; i++) {
- const letter = document.createElement('span');
- letter.className = 'letter';
- letter.innerHTML = content.charAt(i);
- word.appendChild(letter);
- letters.push(letter);
- }
-
- wordArray.push(letters);
- }
-
- setTimeout((): void => {
- changeWord();
- setInterval(changeWord, 3000);
- }, 3000);
- }
-
- useEffect((): void => {
- if (!hasBeenTriggerd.current) {
- onStartAnimation();
- }
- }, []);
-
- return (
- <>
-
-
- {'STAKE'}
- {'INVEST'}
- {'BUILD'}
- {'CHILL'}
- {'LOCK'}
- {'EARN'}
- {'APE'}
-
-
- >
- );
-}
-
function Index(): ReactElement {
- return (
-
-
-
-
-
-
{'WITH YEARN'}
-
-
- {'Yearn is a decentralized suite of products helping individuals, DAOs, and other protocols\n'}
- {'earn yield on their digital assets.'}
-
-
-
-
- {apps.map(
- (app): ReactElement => (
-
- )
- )}
-
-
- );
+ return ;
}
export default Index;
diff --git a/public/empty-lg.png b/public/empty-lg.png
new file mode 100644
index 000000000..dbb63b33c
Binary files /dev/null and b/public/empty-lg.png differ
diff --git a/public/empty-md.png b/public/empty-md.png
new file mode 100644
index 000000000..627ae3da2
Binary files /dev/null and b/public/empty-md.png differ
diff --git a/public/empty-sm.png b/public/empty-sm.png
new file mode 100644
index 000000000..5463a8413
Binary files /dev/null and b/public/empty-sm.png differ
diff --git a/public/gimme-featured.jpg b/public/gimme-featured.jpg
new file mode 100644
index 000000000..75ecaeb1f
Binary files /dev/null and b/public/gimme-featured.jpg differ
diff --git a/public/juiced-featured.jpg b/public/juiced-featured.jpg
new file mode 100644
index 000000000..0839b3c12
Binary files /dev/null and b/public/juiced-featured.jpg differ
diff --git a/public/v2.png b/public/v2.png
new file mode 100644
index 000000000..1fda24fa7
Binary files /dev/null and b/public/v2.png differ
diff --git a/public/v3-featured.jpg b/public/v3-featured.jpg
new file mode 100644
index 000000000..b941c1d5c
Binary files /dev/null and b/public/v3-featured.jpg differ
diff --git a/tailwind.config.js b/tailwind.config.js
index 42b89a1f4..0c0d97c99 100755
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -20,7 +20,20 @@ module.exports = {
white: 'rgb(255, 255, 255)',
transparent: 'transparent',
inherit: 'inherit',
- primary: '#0657F9'
+ primary: '#0657F9',
+ gray: {
+ 300: '#E1E1E1',
+ 400: '#9D9D9D',
+ 500: '#424242',
+ 600: '#292929',
+ 700: '#282828',
+ 800: '#181818',
+ 900: '#0C0C0C'
+ },
+ blue: {
+ 500: '#0657F9'
+ },
+ fallback: '#808080'
},
fontFamily: {
aeonik: ['var(--font-aeonik)', 'Aeonik', ...defaultTheme.fontFamily.sans],