Skip to content

Commit

Permalink
feat: new yearn landing (#571)
Browse files Browse the repository at this point in the history
* feat: wip sidebar

* fix: build fix

* feat: search & category sections & style changes

* feat: carousel & search icon & hovers

* feat: cutaways + scrollbar dismiss + search layout + mobile layout

* feat: arrows for scroll + small fixes

* feat: imp of useSearch + small changes

* fix: small fix

* feat: filterBar + sorting

* fix: small fix

* fix: style changes

* fix: mobile style fixes

* feat: first round of apps

* fix: type

* fix: menu

* entrance anim

* feat: styles updates

* fix: small change

* fix: mobile add card imp

* feat: add description to apps

* feat: add icons

* feat: wip sidebar

* fix: build fix

* feat: search & category sections & style changes

* feat: carousel & search icon & hovers

* feat: cutaways + scrollbar dismiss + search layout + mobile layout

* feat: arrows for scroll + small fixes

* feat: imp of useSearch + small changes

* fix: small fix

* feat: filterBar + sorting

* fix: small fix

* fix: style changes

* fix: mobile style fixes

* feat: first round of apps

* fix: type

* fix: menu

* entrance anim

* feat: styles updates

* fix: small change

* fix: mobile add card imp

* feat: add description to apps

* feat: add icons

* fix: featured apps

* fix: upd images

* feat: changed sizes + hovers

* feat: small changes

* fix: assets

* fix: some spacing

* fix: design change

* chore: remove featured app label

* feat: randomize integration apps

* feat: moved About to the end of menu

* feat: shuffle all apps

* feat: carousel with controls

* chore: ts fix

* chore: route fix

* feat: change some descriptin and catrgory name

* chore: type fix

* fix: animation with search

* feat: upd copy

* feat: redirect

* fix: animation bug

* feat: mobile fixes

* fix: img

---------

Co-authored-by: Majorfi <[email protected]>
Co-authored-by: Major <[email protected]>
Co-authored-by: Daniil Polienko <[email protected]>
  • Loading branch information
4 people authored Nov 20, 2024
1 parent b9bdde5 commit 68be6d4
Show file tree
Hide file tree
Showing 56 changed files with 2,106 additions and 306 deletions.
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={
'bg-grey-900 group relative hidden h-[240px] min-w-[208px] max-w-[208px] overflow-hidden rounded-lg border border-gray-700/50 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>
</>
);
}
82 changes: 82 additions & 0 deletions apps/common/components/AppsCarousel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
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
ref={ref}
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

0 comments on commit 68be6d4

Please sign in to comment.