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

Activities bar - added carousel #287

Merged
merged 20 commits into from
Apr 9, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
2 changes: 2 additions & 0 deletions dapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"@sentry/react": "^7.98.0",
"@sentry/types": "^7.102.0",
"@tanstack/react-table": "^8.11.3",
"@types/react-slick": "^0.23.13",
"axios": "^1.6.7",
"ethers": "^6.10.0",
"formik": "^2.4.5",
Expand All @@ -34,6 +35,7 @@
"react-number-format": "^5.3.1",
"react-redux": "^9.1.0",
"react-router-dom": "^6.22.0",
"react-slick": "^0.30.2",
"recharts": "^2.12.0"
},
"devDependencies": {
Expand Down
9 changes: 9 additions & 0 deletions dapp/src/components/GlobalStyles/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ export default function GlobalStyles() {
font-weight: 900;
font-style: normal;
}
// React-slick package: Chakra-ui with react-slick package doesn't generate flex style for auto-generated slick-track wrapper.
r-czajkowski marked this conversation as resolved.
Show resolved Hide resolved
.slick-track {
display: flex;
}
// React-slick package: Hiding arrows instead of disabling them in case when carousel is not fully completed by slides.
r-czajkowski marked this conversation as resolved.
Show resolved Hide resolved
[data-id="slick-arrow-prev"]:disabled:has(~ [data-id="slick-arrow-next"]:disabled),
[data-id="slick-arrow-prev"]:disabled ~ [data-id="slick-arrow-next"]:disabled{
display: none;
}
Comment on lines +53 to +56
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should not do it in global styles but in the code related to this carousel. But we can leave it that way now and move it in the future if needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably won't change it also for other carousel components if appears. I added a comment to this style when was created:

// React-slick package: Hiding arrows instead of disabling them in case 
// when carousel is not fully completed by slides.

`}
/>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React, { useCallback, useState } from "react"
import { Link as ReactRouterLink } from "react-router-dom"
import { Flex, Link as ChakraLink, FlexboxProps } from "@chakra-ui/react"
import ActivityCard from "./ActivityCard"
import { Flex } from "@chakra-ui/react"
import { ActivityCard } from "./ActivityCard"
import { mockedActivities } from "./mock-activities"

function ActivityBar(props: FlexboxProps) {
export function ActivityBar() {
// TODO: Lines 8-18 should be replaced by redux store when subgraphs are implemented
const [activities, setActivities] = useState(mockedActivities)

const onRemove = useCallback(
Expand All @@ -16,20 +16,16 @@ function ActivityBar(props: FlexboxProps) {
},
[activities],
)

return (
<Flex gap={3} {...props}>
<Flex gap={3} flexDirection="column">
r-czajkowski marked this conversation as resolved.
Show resolved Hide resolved
{activities.map((activity) => (
<ChakraLink
as={ReactRouterLink}
to="/activity-details"
state={{ activity }}
<ActivityCard
key={activity.txHash}
>
<ActivityCard activity={activity} onRemove={onRemove} />
</ChakraLink>
activity={activity}
onRemove={onRemove}
/>
))}
</Flex>
)
}

export default ActivityBar
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React, { useCallback } from "react"
import {
CardBody,
CardFooter,
CardHeader,
CardProps,
HStack,
Icon,
Tooltip,
CloseButton,
} from "@chakra-ui/react"
import { useLocation } from "react-router-dom"
import { ActivityInfo, LocationState } from "#/types"
import { capitalize } from "#/utils"
import { ChevronRightIcon } from "#/assets/icons"
import { CurrencyBalance } from "#/components/shared/CurrencyBalance"
import StatusInfo from "#/components/shared/StatusInfo"
import { TextSm } from "#/components/shared/Typography"
import { ActivityCardWrapper } from "./ActivityCardWrapper"
import { ActivityCardLinkWrapper } from "./ActivityCardLinkWrapper"

type ActivityCardType = CardProps & {
activity: ActivityInfo
onRemove: (txHash: string) => void
}

export function ActivityCard({
activity,
onRemove,
...props
}: ActivityCardType) {
const state = useLocation().state as LocationState | null
const isActive = state ? activity.txHash === state.activity.txHash : false
const isCompleted = activity.status === "completed"

const onClose = useCallback(
(event: React.MouseEvent) => {
event.preventDefault()
if (activity.txHash) {
onRemove(activity.txHash)
}
},
[onRemove, activity.txHash],
)

return (
<ActivityCardLinkWrapper activity={activity} {...props}>
<ActivityCardWrapper isCompleted={isCompleted} isActive={isActive}>
<CardHeader p={0} w="100%">
<HStack justifyContent="space-between">
<CurrencyBalance
currency={activity.currency}
amount={activity.amount}
size="xl"
balanceFontWeight="black"
symbolFontWeight="medium"
/>
{isCompleted ? (
<Tooltip label="Remove" placement="top" paddingX={3} paddingY={2}>
<CloseButton
size="sm"
onClick={onClose}
_hover={{ backgroundColor: undefined }}
/>
</Tooltip>
) : (
<Icon
as={ChevronRightIcon}
boxSize={5}
color={isActive ? "gold.700" : "grey.400"}
_hover={isActive ? { color: "gold.700" } : undefined}
/>
)}
</HStack>
</CardHeader>
<CardBody p={0}>
<TextSm fontWeight="semibold" marginBottom={4}>
kkosiorowska marked this conversation as resolved.
Show resolved Hide resolved
{capitalize(activity.action)}
</TextSm>
kkosiorowska marked this conversation as resolved.
Show resolved Hide resolved
</CardBody>
<CardFooter p={0}>
<StatusInfo
status={activity.status}
withIcon
withDefaultColor
fontWeight="medium"
/>
</CardFooter>
</ActivityCardWrapper>
</ActivityCardLinkWrapper>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from "react"

import { Link as ReactRouterLink } from "react-router-dom"
import { Link as ChakraLink } from "@chakra-ui/react"
import { ActivityInfo } from "#/types"

type ActivityCardLinkWrapperProps = {
activity: ActivityInfo
children: React.ReactNode
}

export function ActivityCardLinkWrapper({
activity,
kkosiorowska marked this conversation as resolved.
Show resolved Hide resolved
children,
...props
}: ActivityCardLinkWrapperProps) {
return (
<ChakraLink
as={ReactRouterLink}
to="/activity-details"
kkosiorowska marked this conversation as resolved.
Show resolved Hide resolved
state={{ activity }}
key={activity.txHash}
{...props}
>
{children}
</ChakraLink>
)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we should use special compnent for that case from chakra - LinkOverlay, because:

According to the specification](https://www.w3.org/TR/html5/text-level-semantics.html#the-a-element), an <a> element’s content model specifically states that an cannot contain any interactive descendants (button, anchors, input, etc.).

Copy link
Contributor Author

@ioay ioay Mar 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not entirely convinced about this solution, since we end up using it anyway prop as={ReactRouterLink} which generates <a> element (no matter if we use LinkOverlay/LinkBox). I'm wondering how we can get around this to meet the semantic assumptions 🤔

UPDATE:
Added click handler instead of using ChakraLink.: ba6d8ee

Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from "react"
import { CardProps, Card } from "@chakra-ui/react"

type ActivityCardContainerProps = CardProps & {
type ActivityCardWrapperProps = CardProps & {
isCompleted: boolean
isActive: boolean
children: React.ReactNode
Expand Down Expand Up @@ -34,12 +34,12 @@ const activeStyles = {
},
}

function ActivityCardContainer({
export function ActivityCardWrapper({
isActive,
isCompleted,
children,
...props
}: ActivityCardContainerProps) {
}: ActivityCardWrapperProps) {
return (
<Card
{...props}
Expand All @@ -60,5 +60,3 @@ function ActivityCardContainer({
</Card>
)
}

export default ActivityCardContainer
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./ActivityCard"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move to shared components:

.
└── src/
    └── components/
        └── shared/
            └── Carousel/
                └── index.tsx // included all utils

I would keep all the utils and component inside index.ts file. And then the ActivityCarousel should define responsive breakpoints because we calculate them based on the activity card.
`src/components/shared/Carousel/index.tsx

type PaginationArrowType = CustomArrowProps & IconButtonProps

function PaginationArrow({ icon, onClick, ...props }: PaginationArrowType) {
  return (
    <IconButton
      icon={icon}
      variant="pagination"
      size="sm"
      borderRadius={32}
      onClick={onClick}
      isDisabled={onClick === null}
      {...props}
    />
  )
}

function PrevArrowCarousel({ onClick }: CustomArrowProps) {
  return (
    <PaginationArrow
      position="absolute"
      mr={2}
      right={-56}
      top={-10}
      onClick={onClick}
      icon={<ArrowLeft />}
      aria-label="prev"
      data-id="slick-arrow-prev"
    />
  )
}
function NextArrowCarousel({ onClick }: CustomArrowProps) {
  return (
    <PaginationArrow
      position="absolute"
      right={-64}
      top={-10}
      onClick={onClick}
      icon={<ArrowRight />}
      aria-label="next"
      data-id="slick-arrow-next"
    />
  )
}

const carouselSettings = {
  dots: false,
  infinite: false,
  draggable: false,
  variableWidth: true,
  speed: 500,
  slidesToShow: 12,
  slidesToScroll: 1,
  nextArrow: <NextArrowCarousel />,
  prevArrow: <PrevArrowCarousel />,
}

export function Carousel({ children, ...props }: BoxProps) {
  return (
    <Box pos="relative" {...props}>
      <HStack
        as={Slider}
        overflow="hidden"
        ref={props.sliderRef}
        pl={2}
        ml={-2}
        overflowX="hidden"
        pb={6}
        _after={{
          content: '""',
          pos: "absolute",
          right: 0,
          w: 20,
          height: 40,
          bgGradient: "linear(to-r, transparent, gold.300)",
        }}
        {...carouselSettings}
      >
        {children}
      </HStack>
    </Box>
  )
}

src/components/shared/ActivityCarousel/index.tsx

const carouselBreakPoints = responsive: [
    {
      breakpoint: 820,
      settings: {
        slidesToShow: 1,
      },
    },
    {
      breakpoint: 1080,
      settings: {
        slidesToShow: 2,
      },
    },
    {
      breakpoint: 1360,
      settings: {
        slidesToShow: 3,
      },
    },
    {
      breakpoint: 1620,
      settings: {
        slidesToShow: 4,
      },
    },
    {
      breakpoint: 1900,
      settings: {
        slidesToShow: 5,
      },
    },
    {
      breakpoint: 2160,
      settings: {
        slidesToShow: 6,
      },
    },
    {
      breakpoint: 2440,
      settings: {
        slidesToShow: 7,
      },
    },
    {
      breakpoint: 2700,
      settings: {
        slidesToShow: 8,
      },
    },
    {
      breakpoint: 2980,
      settings: {
        slidesToShow: 9,
      },
    },
    {
      breakpoint: 3240,
      settings: {
        slidesToShow: 10,
      },
    },
    {
      breakpoint: 3520,
      settings: {
        slidesToShow: 11,
      },
    },
  ],

export function ActivityCarousel({ ...props }: BoxProps) {
  const sliderRef = useRef<HTMLDivElement & Slider>(null)
  
  ...
  
  return (
    <Carousel responsive={carouselBreakPoints} sliderRef={sliderRef}>
      {activities.map((activity) => (
        <ActivityCard
          key={activity.txHash}
          activity={activity}
          onRemove={onRemove}
          mr={3}
        />
      ))}
    </Carousel>
  )
}

Copy link
Contributor Author

@ioay ioay Mar 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this carousel should be a shared component, as you noticed: #287 (comment)
it's something specific for OverviewPage only (unique styles, arrow positions, breakpoints, slidesToShow, bgGradient on the right side). What do you think about moving it for now to the OverviewPage directory? If a new carousel appears in the design, we will consider modifying this component depending on what can be shared.

Update:
Slider with some general props was moved to shared components. Other props and arrows were left in an activityCarousel that is more closely related to it.

kkosiorowska marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React, { useCallback, useRef, useState } from "react"
import Slider from "react-slick"
import { Box, HStack, BoxProps } from "@chakra-ui/react"
import { ActivityCard } from "../ActivityCard"
import { activityCarouselSettings } from "./utils"
import { mockedActivities } from "../mock-activities"

export function ActivityCarousel({ ...props }: BoxProps) {
const sliderRef = useRef<HTMLDivElement & Slider>(null)

// TODO: Lines 12-30 should be replaced by redux store when subgraphs are implemented
const [activities, setActivities] = useState(mockedActivities)

const onRemove = useCallback(
(activityHash: string) => {
const removedIndex = activities.findIndex(
(activity) => activity.txHash === activityHash,
)
const filteredActivities = activities.filter(
(activity) => activity.txHash !== activityHash,
)
const isLastCard = removedIndex === activities.length - 1
if (isLastCard) {
sliderRef.current?.slickPrev()
}
sliderRef.current?.forceUpdate()
setActivities(filteredActivities)
},
[activities],
)

return (
<Box pos="relative" {...props}>
<HStack
as={Slider}
overflow="hidden"
ref={sliderRef}
pl={2}
ml={-2}
overflowX="hidden"
pb={6}
_after={{
content: '""',
pos: "absolute",
right: 0,
w: 20,
height: 40,
bgGradient: "linear(to-r, transparent, gold.300)",
}}
{...activityCarouselSettings}
>
{activities.map((activity) => (
<ActivityCard
key={activity.txHash}
activity={activity}
onRemove={onRemove}
mr={3}
/>
))}
</HStack>
</Box>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./ActivityCarousel"
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React from "react"
import { CustomArrowProps } from "react-slick"
import { IconButton, IconButtonProps } from "@chakra-ui/react"
import { ArrowLeft, ArrowRight } from "#/assets/icons"

type PaginationArrowType = CustomArrowProps & IconButtonProps

function PaginationArrow({ icon, onClick, ...props }: PaginationArrowType) {
return (
<IconButton
icon={icon}
variant="pagination"
size="sm"
borderRadius={32}
onClick={onClick}
isDisabled={onClick === null}
{...props}
/>
)
}

export function PrevArrowCarousel({ onClick }: CustomArrowProps) {
return (
<PaginationArrow
position="absolute"
mr={2}
right={-56}
top={-10}
onClick={onClick}
icon={<ArrowLeft />}
aria-label="prev"
data-id="slick-arrow-prev"
/>
)
}
export function NextArrowCarousel({ onClick }: CustomArrowProps) {
return (
<PaginationArrow
position="absolute"
right={-64}
top={-10}
onClick={onClick}
icon={<ArrowRight />}
aria-label="next"
data-id="slick-arrow-next"
/>
)
}
Loading
Loading