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

Add top users carousel #202

Closed
wants to merge 1 commit into from
Closed
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
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@
"dayjs": "^1.9.6",
"dotenv-webpack": "^1.0.2",
"easymde": "^2.15.0",
"embla-carousel": "^8.0.0-rc21",
"embla-carousel-autoplay": "^8.0.0-rc21",
"embla-carousel-react": "^8.0.0-rc21",
"graphql": "^15.5.0",
"graphql-request": "^5.2.0",
"graphql-tag": "^2.11.0",
Expand Down
66 changes: 48 additions & 18 deletions src/components/creators/TopUsersCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import Avatar from '../profiles/address-views/Avatar'
import ViewProfileLink from '../profiles/ViewProfileLink'
import { useIsMobileWidthOrDevice } from '../responsive'
import ViewSpaceLink from '../spaces/ViewSpaceLink'
import Carousel from '../utils/carousel/Carousel'
import { MutedSpan } from '../utils/MutedText'
import Segment from '../utils/Segment'

Expand Down Expand Up @@ -80,22 +81,52 @@ export default function TopUsersCard({ ...props }: TopUsersCardProps) {
>
<div className='d-flex flex-column FontSmall' style={{ minWidth: 0 }}>
<MutedSpan className='FontWeightMedium mb-1'>Stakers</MutedSpan>
<div className='d-flex flex-column GapTiny'>
{data.stakers.map((staker, i) => (
<UserInfo rank={i + 1} key={i} user={staker} />
))}
</div>
<Carousel
options={{ loop: true }}
slides={[
<div className='d-flex flex-column GapTiny' key={0}>
{data.stakers.slice(0, 3).map((staker, i) => (
<UserInfo rank={i + 1} key={i} user={staker} />
))}
</div>,
<div className='d-flex flex-column GapTiny' key={1}>
{data.stakers.slice(3, 6).map((staker, i) => (
<UserInfo rank={i + 1 + 3} key={i} user={staker} />
))}
</div>,
<div className='d-flex flex-column GapTiny' key={2}>
{data.stakers.slice(6, 9).map((staker, i) => (
<UserInfo rank={i + 1 + 6} key={i} user={staker} />
))}
</div>,
]}
/>
</div>
<div
className={clsx('d-flex flex-column FontSmall', !isMobile && 'mt-3 pt-2')}
style={{ borderTop: !isMobile ? '1px solid #E2E8F0' : 'none', minWidth: 0 }}
>
<MutedSpan className='FontWeightMedium mb-1'>Creators</MutedSpan>
<div className='d-flex flex-column GapTiny'>
{data?.creators.map((creator, i) => (
<UserInfo rank={i + 1} key={i} user={creator} />
))}
</div>
<Carousel
options={{ loop: true }}
slides={[
<div className='d-flex flex-column GapTiny' key={0}>
{data.creators.slice(0, 3).map((staker, i) => (
<UserInfo rank={i + 1} key={i} user={staker} />
))}
</div>,
<div className='d-flex flex-column GapTiny' key={1}>
{data.creators.slice(3, 6).map((staker, i) => (
<UserInfo rank={i + 1 + 3} key={i} user={staker} />
))}
</div>,
<div className='d-flex flex-column GapTiny' key={2}>
{data.creators.slice(6, 9).map((staker, i) => (
<UserInfo rank={i + 1 + 6} key={i} user={staker} />
))}
</div>,
]}
/>
</div>
</div>
{!isMobile && <div className='d-flex justify-content-center mt-2'>{seeMoreButton}</div>}
Expand Down Expand Up @@ -146,13 +177,11 @@ function UserInfo({ rank, user }: { rank: number; user: { address: string; rewar
<div className='d-flex align-items-center'>
<div className='position-relative'>
{profile ? <ViewSpaceLink space={profile.struct} title={avatar} /> : avatar}
{[1, 2, 3].includes(rank) && (
<Medal
className='position-absolute FontTiny'
style={{ bottom: -2, right: 6 }}
rank={rank as 1 | 2 | 3}
/>
)}
<Medal
className='position-absolute FontTiny'
style={{ bottom: -2, right: 6 }}
rank={rank as 1 | 2 | 3}
/>
</div>
<div className='d-flex flex-column' style={{ minWidth: 0 }}>
{profile ? (
Expand All @@ -178,7 +207,7 @@ function UserInfo({ rank, user }: { rank: number; user: { address: string; rewar
)
}

function Medal({ rank, ...props }: ComponentProps<'div'> & { rank: 1 | 2 | 3 }) {
function Medal({ rank, ...props }: ComponentProps<'div'> & { rank: number }) {
const rankStyles: Record<number, CSSProperties> = {
1: {
backgroundColor: '#FCDF40',
Expand All @@ -195,6 +224,7 @@ function Medal({ rank, ...props }: ComponentProps<'div'> & { rank: 1 | 2 | 3 })
}

const style: CSSProperties = {
background: 'white',
...rankStyles[rank],
border: '1px solid white',
width: '1rem',
Expand Down
50 changes: 50 additions & 0 deletions src/components/utils/carousel/Carousel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { EmblaOptionsType } from 'embla-carousel'
import Autoplay from 'embla-carousel-autoplay'
import useEmblaCarousel from 'embla-carousel-react'
import React, { ReactNode } from 'react'

type PropType = {
slides: ReactNode[]
options?: EmblaOptionsType
}

const Carousel: React.FC<PropType> = props => {
const { slides, options } = props
const [emblaRef] = useEmblaCarousel(options, [
Autoplay({ delay: 5000, stopOnInteraction: false }),
])

// const onButtonClick = useCallback((emblaApi: EmblaCarouselType) => {
// const { autoplay } = emblaApi.plugins()
// if (!autoplay) return
// if (autoplay.options.stopOnInteraction !== false) autoplay.stop()
// }, [])

// const { selectedIndex, scrollSnaps, onDotButtonClick } = useDotButton(emblaApi, onButtonClick)

return (
<div className='embla'>
<div className='embla__viewport' ref={emblaRef}>
<div className='embla__container'>
{slides.map((slide, index) => (
<div className='embla__slide' key={index}>
<div className='embla__slide__content'>{slide}</div>
</div>
))}
</div>
</div>

{/* <div className='embla__dots'>
{scrollSnaps.map((_, index) => (
<DotButton
key={index}
onClick={() => onDotButtonClick(index)}
className={'embla__dot'.concat(index === selectedIndex ? ' embla__dot--selected' : '')}
/>
))}
</div> */}
</div>
)
}

export default Carousel
63 changes: 63 additions & 0 deletions src/components/utils/carousel/CarouselDotButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { EmblaCarouselType } from 'embla-carousel'
import React, { PropsWithChildren, useCallback, useEffect, useState } from 'react'

type UseDotButtonType = {
selectedIndex: number
scrollSnaps: number[]
onDotButtonClick: (index: number) => void
}

export const useDotButton = (
emblaApi: EmblaCarouselType | undefined,
onButtonClick?: (emblaApi: EmblaCarouselType) => void,
): UseDotButtonType => {
const [selectedIndex, setSelectedIndex] = useState(0)
const [scrollSnaps, setScrollSnaps] = useState<number[]>([])

const onDotButtonClick = useCallback(
(index: number) => {
if (!emblaApi) return
emblaApi.scrollTo(index)
if (onButtonClick) onButtonClick(emblaApi)
},
[emblaApi, onButtonClick],
)

const onInit = useCallback((emblaApi: EmblaCarouselType) => {
setScrollSnaps(emblaApi.scrollSnapList())
}, [])

const onSelect = useCallback((emblaApi: EmblaCarouselType) => {
setSelectedIndex(emblaApi.selectedScrollSnap())
}, [])

useEffect(() => {
if (!emblaApi) return

onInit(emblaApi)
onSelect(emblaApi)
emblaApi.on('reInit', onInit)
emblaApi.on('reInit', onSelect)
emblaApi.on('select', onSelect)
}, [emblaApi, onInit, onSelect])

return {
selectedIndex,
scrollSnaps,
onDotButtonClick,
}
}

type PropType = PropsWithChildren<
React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>
>

export const DotButton: React.FC<PropType> = props => {
const { children, ...restProps } = props

return (
<button type='button' {...restProps}>
{children}
</button>
)
}
4 changes: 2 additions & 2 deletions src/components/utils/datahub/leaderboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const GET_TOP_USERS = gql`
staker: activeStakingAddressesRankedByRewardsForPeriod(
args: {
filter: { period: WEEK, role: STAKER, timestamp: $from }
limit: 3
limit: 9
offset: 0
order: DESC
}
Expand All @@ -23,7 +23,7 @@ const GET_TOP_USERS = gql`
creator: activeStakingAddressesRankedByRewardsForPeriod(
args: {
filter: { period: WEEK, role: CREATOR, timestamp: $from }
limit: 3
limit: 9
offset: 0
order: DESC
}
Expand Down
78 changes: 78 additions & 0 deletions src/styles/subsocial.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2325,3 +2325,81 @@ hr {
}
}
}

.embla {
--slide-spacing: 1rem;
--slide-size: 100%;
position: relative;
}
.embla__viewport {
overflow: hidden;
}
.embla__container {
backface-visibility: hidden;
display: flex;
touch-action: pan-y;
margin-left: calc(var(--slide-spacing) * -1);
}
.embla__slide {
flex: 0 0 var(--slide-size);
min-width: 0;
padding-left: var(--slide-spacing);
position: relative;
}
.embla__slide__content {
display: block;
width: 100%;
object-fit: cover;
}
.embla__slide__number > span {
color: var(--brand-primary);
background-image: linear-gradient(45deg, var(--brand-primary), var(--brand-secondary));
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-size: 1.6rem;
display: block;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.embla__dot {
-webkit-appearance: none;
background-color: transparent;
touch-action: manipulation;
display: inline-flex;
text-decoration: none;
cursor: pointer;
border: 0;
padding: 0;
margin: 0;
}
.embla__dots {
z-index: 1;
position: absolute;
right: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
}
.embla__dot {
width: 1rem;
height: 1rem;
display: flex;
align-items: center;
margin-right: $space_tiny;
margin-left: $space_tiny;
}
.embla__dot:after {
background: $color_chip_bg;
border-radius: 0.2rem;
width: 100%;
height: 0.3rem;
content: '';
}
.embla__dot--selected:after {
background: #edf4ff;
}
23 changes: 23 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5928,6 +5928,29 @@ elliptic@^6.5.3:
minimalistic-assert "^1.0.1"
minimalistic-crypto-utils "^1.0.1"

embla-carousel-autoplay@^8.0.0-rc21:
version "8.0.0-rc21"
resolved "https://registry.yarnpkg.com/embla-carousel-autoplay/-/embla-carousel-autoplay-8.0.0-rc21.tgz#7be3c680673430c0607568676588427e9b856f8d"
integrity sha512-o8T3+cpkZyJZfpN1PqxRDOdep/wlLvBQTIr0KUQbtk/y44esiAdZEGCjR/GJDJbl2huMIB/GzxkC7JQYEHZPxQ==

embla-carousel-react@^8.0.0-rc21:
version "8.0.0-rc21"
resolved "https://registry.yarnpkg.com/embla-carousel-react/-/embla-carousel-react-8.0.0-rc21.tgz#10eb8b1b0c829a8d292493693ab9be75678e0e26"
integrity sha512-DOa9hgF/T1fwb8D3rZ8FFMceY3aDXtbluZwzZYMLnN2Dqn0IBLN0l97o3obkMxI9Zzog0u1WMM6HE7AGF9SjEg==
dependencies:
embla-carousel "8.0.0-rc21"
embla-carousel-reactive-utils "8.0.0-rc21"

[email protected]:
version "8.0.0-rc21"
resolved "https://registry.yarnpkg.com/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.0.0-rc21.tgz#bd28b83a6696d0a1b490525638393a4655b2f767"
integrity sha512-TnV49hoTdwfcKr2vgQHQ3zcCqJSkvLJ5rR/pGzmEx5GeO07CV/e755lkSD7No0C6cz+JFB8dcHV7uS+5Gnc7Lg==

[email protected], embla-carousel@^8.0.0-rc21:
version "8.0.0-rc21"
resolved "https://registry.yarnpkg.com/embla-carousel/-/embla-carousel-8.0.0-rc21.tgz#c12f05528a0b6d0dc17e9f78f3e1da622dcc6d68"
integrity sha512-rK//vyPIhmD/5QUDtjk9A5RxPoDZ5LOATYMVSFECAzwcAe7yJmqXQbdYzEZf4ASOR+ivod5msqXsKgZXypA35Q==

emittery@^0.7.1:
version "0.7.2"
resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.7.2.tgz#25595908e13af0f5674ab419396e2fb394cdfa82"
Expand Down
Loading