Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: new yearn landing (#571) #608

Merged
merged 9 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions apps/common/CarouselControls.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={'hidden w-full justify-center md:flex'}>
<div className={'flex gap-x-3'}>
{Array(numberOfControls)
.fill('')
.map((_, index) => (
<button
key={index}
className={'p-[2px]'}
onClick={() => {
onDotsClick(index + 1);
}}>
<div
className={cl(
'size-2 rounded-full',
currentPage === index + 1 ? 'bg-white' : 'bg-gray-500'
)}
/>
</button>
))}
</div>
</div>
);
}
39 changes: 39 additions & 0 deletions apps/common/CarouselSlideArrows.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={cl('flex w-full justify-between', className)}>
<div />
<div className={'hidden gap-3 md:flex'}>
<button
onClick={onScrollBack}
className={
'flex !h-8 items-center rounded-[4px] px-4 text-white outline !outline-1 outline-gray-600/50 hover:bg-gray-600/40'
}>
<IconChevron className={'size-3 rotate-90'} />
</button>
<button
onClick={onScrollForward}
className={
'flex !h-8 items-center rounded-[4px] px-4 text-white outline !outline-1 outline-gray-600/50 hover:bg-gray-600/40'
}>
<IconChevron className={'size-3 -rotate-90'} />
</button>
</div>
</div>
);
}
72 changes: 72 additions & 0 deletions apps/common/components/AppCard.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<Link
href={props.app.appURI ?? ''}
target={'_blank'}
className={
'group relative hidden h-[240px] min-w-[208px] max-w-[208px] overflow-hidden rounded-lg border border-gray-700/50 bg-gray-900 p-6 hover:bg-gray-600/40 md:block'
}>
<div className={'mb-4'}>
<div
className={
'absolute right-2 top-2 hidden size-10 items-center justify-center rounded-lg bg-gray-900 group-hover:flex'
}>
<IconShare className={'size-[10px]'} />
</div>
{props.app.logoURI ? (
<Image
src={props.app.logoURI}
alt={props.app.name}
unoptimized
width={240}
height={240}
className={'size-[80px] rounded-full border border-[#292929]/80 object-contain'}
/>
) : (
<div className={'size-[80px] rounded-full bg-fallback'} />
)}
</div>
<div className={'mb-1 text-lg font-bold text-white'}>{props.app.name}</div>

<p className={'max-h-[60px] whitespace-normal text-sm text-gray-400'}>{props.app.description}</p>
</Link>
<Link
href={props.app.appURI}
className={'flex items-center md:hidden'}>
<div>
{props.app.logoURI ? (
<div className={'size-16 rounded-[32px]'}>
<Image
src={props.app.logoURI}
alt={props.app.name}
width={300}
height={300}
unoptimized
className={'size-full rounded-2xl bg-center object-cover md:rounded-[32px]'}
/>
</div>
) : (
<div className={'size-16 rounded-2xl bg-fallback md:rounded-[32px]'} />
)}
</div>

<div className={'ml-4'}>
<div className={'mb-1 text-base font-bold text-gray-300'}>{props.app.name}</div>
<p className={'line-clamp-2 h-12 text-xs text-gray-400 md:text-base'}>{props.app.description}</p>
</div>
</Link>
</>
);
}
81 changes: 81 additions & 0 deletions apps/common/components/AppsCarousel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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<HTMLDivElement>
): ReactElement => {
return (
<div className={props.isUsingFeatured ? 'h-[262px]' : 'h-[360px] md:h-[262px]'}>
<section className={'absolute left-0 -mx-1 w-full'}>
<div
className={
'pointer-events-none absolute left-0 top-0 z-30 h-[272px] w-1/6 bg-gradient-to-r from-gray-900/0 to-transparent md:h-full'
}
/>
<div
className={
'pointer-events-none absolute right-0 top-0 z-30 h-[272px] w-1/5 bg-gradient-to-l from-gray-900/0 to-transparent md:h-full'
}
/>
<div
ref={ref}
onScroll={props.onScroll}
className={cl(
'hidden md:flex overflow-x-auto pb-1 pl-[38px] scrollbar-none max-sm:pr-6',
props.isUsingFeatured ? 'gap-x-8' : 'flex-col md:flex-row gap-x-4 overflow-y-hidden'
)}>
{props.apps?.map((app, i) => {
return (
<React.Fragment key={app.appURI + i}>
{props.isUsingFeatured ? (
<FeaturedApp
key={app.name + i}
app={app}
/>
) : (
<AppCard
app={app}
key={app.name + i}
/>
)}
</React.Fragment>
);
})}
</div>
<div
onScroll={props.onScroll}
className={cl(
'flex md:hidden overflow-x-auto pb-1 pl-[38px] scrollbar-none max-sm:pr-6',
props.isUsingFeatured ? 'gap-x-8' : 'flex-col md:flex-row gap-y-4 overflow-y-hidden'
)}>
{props.apps?.slice(0, 4).map((app, i) => {
return (
<React.Fragment key={app.appURI + i}>
{props.isUsingFeatured ? (
<FeaturedApp
key={app.name + i}
app={app}
/>
) : (
<AppCard
app={app}
key={app.name + i}
/>
)}
</React.Fragment>
);
})}
</div>
</section>
</div>
);
}
);
131 changes: 131 additions & 0 deletions apps/common/components/CategorySection.tsx
Original file line number Diff line number Diff line change
@@ -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<TApp[]>([]);
const [currentPage, set_currentPage] = useState(1);
const carouselRef = useRef<HTMLDivElement | null>(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 (
<div className={'flex flex-col overflow-hidden'}>
<div className={'mb-6 flex h-10 w-full items-center justify-between pr-1'}>
<div className={'flex gap-x-4'}>
<div className={'whitespace-nowrap text-lg font-bold text-white'}>{title}</div>
<button
onClick={onExpandClick}
className={
'flex items-center rounded-[4px] px-4 py-2 outline !outline-1 outline-gray-600/50 hover:bg-gray-600/40'
}>
<span className={'mr-2 whitespace-nowrap text-xs text-white'}>{'View all'}</span>
<IconShare className={'size-3 text-white'} />
</button>
</div>
{apps?.length > 4 && (
<CarouselSlideArrows
onScrollBack={onScrollBack}
onScrollForward={onScrollForward}
/>
)}
</div>
<AppsCarousel
apps={shuffledApps}
ref={carouselRef}
onScroll={onScroll}
/>
<CarouselControls
carouselLength={apps.length}
onDotsClick={onDotsClick}
currentPage={currentPage}
/>
</div>
);
};
Loading
Loading