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: add landing page carousel #3407

Open
wants to merge 2 commits into
base: feat/landing-page
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
32 changes: 22 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"postinstall": "if [ -z \"$SKIP_HOOKS\" ]; then ./scripts/lambda-functions-dependencies.sh && scripts/generate-amplify-local-config.sh; fi",
"watch-amplify": "node scripts/watchAmplifyFiles",
"forward-time": "node scripts/forward-time",
"playwright:install": "npx playwright install --with-deps",
"playwright:install": "npx playwright install --with-deps",
"playwright:run": "playwright test",
"playwright:watch": "playwright test --ui"
},
Expand Down Expand Up @@ -191,6 +191,7 @@
"date-fns": "^2.30.0",
"decimal.js": "^10.2.1",
"dompurify": "^3.0.8",
"embla-carousel-autoplay": "^8.0.0-rc11",
"embla-carousel-react": "^8.0.0-rc11",
"eth-ens-namehash-ms": "^2.2.0",
"ethers": "^5.6.8",
Expand Down
47 changes: 36 additions & 11 deletions src/components/common/Extensions/ImageCarousel/ImageCarousel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import clsx from 'clsx';
import React, { type FC } from 'react';
import React, { useEffect, type FC } from 'react';

import { images } from './consts.ts';
import { useEmblaCarouselSettings } from './hooks.ts';
Expand All @@ -11,10 +11,18 @@ const displayName = 'common.Extensions.ImageCarousel';
const ImageCarousel: FC<ImageCarouselProps> = ({
slideUrls = images,
options = { loop: true, align: 'start' },
isAutoplay = false,
isImageFullWidth,
Copy link
Contributor

Choose a reason for hiding this comment

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

what if we rework slideUrls to be an array of image urls and classNames? would be a bit cleaner, since now we control all images from one variable

isChangeSlideDotButton = true,
setSelectedIndex,
className,
}) => {
const { scrollSnaps, emblaRef, scrollTo, selectedIndex } =
useEmblaCarouselSettings(options);
useEmblaCarouselSettings(options, isAutoplay);

useEffect(() => {
setSelectedIndex?.(selectedIndex);
}, [selectedIndex]);

return (
<div className={clsx(className, 'relative pb-[1.75rem]')}>
Expand All @@ -23,13 +31,20 @@ const ImageCarousel: FC<ImageCarouselProps> = ({
<div className="flex">
{slideUrls.map((url) => (
<div
className="min-w-full sm:mr-4 sm:w-[31.875rem] sm:min-w-[31.875rem]"
className={clsx('min-w-full', {
'sm:mr-4': !isImageFullWidth,
'sm:w-[31.875rem]': !isImageFullWidth,
'sm:min-w-[31.875rem]': !isImageFullWidth,
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we group all these classes into a single line
'sm:mr-4 sm:w-[31.875rem] sm:min-w-[31.875rem]': !isImageFullWidth?

})}
key={url}
>
<img
alt="file"
src={url}
className="aspect-[380/248] w-full object-cover sm:aspect-[510/248]"
className={clsx('w-full object-cover', {
'aspect-[380/248]': !isImageFullWidth,
'sm:aspect-[510/248]': !isImageFullWidth,
Copy link
Contributor

Choose a reason for hiding this comment

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

Also here 'aspect-[380/248] sm:aspect-[510/248]': !isImageFullWidth

})}
/>
</div>
))}
Expand All @@ -42,13 +57,23 @@ const ImageCarousel: FC<ImageCarouselProps> = ({
// eslint-disable-next-line react/no-array-index-key
key={index}
onClick={() => scrollTo(index)}
className={clsx(
'mx-1 h-2 w-2 cursor-pointer rounded-full bg-gray-200 transition-all duration-normal hover:bg-blue-400',
{
'bg-gray-500': index === selectedIndex,
},
)}
/>
className={clsx('group', {
'py-[10px]': !isChangeSlideDotButton,
Copy link
Contributor

Choose a reason for hiding this comment

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

could we just render a whole different thing if isChangeSlideDotButton is false?

'my-[-10px]': !isChangeSlideDotButton,
Copy link
Contributor

Choose a reason for hiding this comment

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

Also here 'py-[10px] my-[-10px]': !isChangeSlideDotButton

})}
>
<div
className={clsx(
'mx-1 h-2 w-2 cursor-pointer rounded-full bg-gray-200 transition-all duration-normal group-hover:bg-blue-400',
{
'bg-gray-500': index === selectedIndex,
'w-[5.875rem]': !isChangeSlideDotButton,
'h-[.1875rem]': !isChangeSlideDotButton,
'mx-[.3125rem]': !isChangeSlideDotButton,
Copy link
Contributor

Choose a reason for hiding this comment

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

Also here 'w-[5.875rem] h-[.1875rem] mx-[.3125rem]': !isChangeSlideDotButton

},
)}
/>
</DotButton>
))}
</div>
</div>
Expand Down
15 changes: 13 additions & 2 deletions src/components/common/Extensions/ImageCarousel/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
import Autoplay from 'embla-carousel-autoplay';
import useEmblaCarousel, { type EmblaCarouselType } from 'embla-carousel-react';
import { useCallback, useEffect, useState } from 'react';

export const useEmblaCarouselSettings = (options) => {
const [emblaRef, emblaApi] = useEmblaCarousel(options);
export const useEmblaCarouselSettings = (options, autoplay) => {
const [emblaRef, emblaApi] = useEmblaCarousel(
options,
autoplay
? [
Autoplay({
playOnInit: true,
delay: 3000,
}),
]
: [],
);
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we maybe refactor the plugins part as

const plugins: EmblaPluginType[] = [];
  if (autoplay) {
    plugins.push(
      Autoplay({
        playOnInit: true,
        delay: 3000,
      }),
    )
  }
  const [emblaRef, emblaApi] = useEmblaCarousel(
    options,
    plugins,
  );

thus making it clearer in the future if we plan on adding more plugins

const [selectedIndex, setSelectedIndex] = useState(0);
const [scrollSnaps, setScrollSnaps] = useState<number[]>([]);

Expand Down
4 changes: 4 additions & 0 deletions src/components/common/Extensions/ImageCarousel/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ export interface ImageCarouselProps {
slideUrls?: string[];
options?: EmblaOptionsType;
className?: string;
isImageFullWidth?: boolean;
isAutoplay?: boolean;
isChangeSlideDotButton?: boolean;
setSelectedIndex?: (index: number) => void;
}

export type DotButtonProps = React.DetailedHTMLProps<
Expand Down
109 changes: 109 additions & 0 deletions src/components/frame/LandingPage/LandingPageCarousel.tsx
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice job with this component @adam-strzelec 👍 however I would propose refactoring it as following, in order to store the messages in a single variable, then easily adjust the number of slides and their parsing.
Also it's always better to try to define a variable for storing a check rather than repeating it (currentSlide === index) as if in the future there is a need to change the condition you might end up loosing track of the places this needs to be changed

import clsx from 'clsx';
import React, { useState } from 'react';
import { defineMessages, FormattedMessage } from 'react-intl';

import ImageCarousel from '~common/Extensions/ImageCarousel/ImageCarousel.tsx';
import Slide1 from '~images/assets/landing/slider1.png';
import Slide2 from '~images/assets/landing/slider2.png';
import Slide3 from '~images/assets/landing/slider3.png';
import SlideMobile from '~images/assets/landing/sliderMobile.png';

const displayName = 'frame.LandingPageCarousel';

const MSG = defineMessages({
  titleSlide0: {
    id: `${displayName}.titleSlide0`,
    defaultMessage: 'A powerful, all-in-one payments suite',
  },
  titleSlide1: {
    id: `${displayName}.titleSlide1`,
    defaultMessage: 'Make bulk payments your way with ease',
  },
  titleSlide2: {
    id: `${displayName}.titleSlide2`,
    defaultMessage: 'Easily track & manage shared finances',
  },
  descriptionSlide0: {
    id: `${displayName}.descriptionSlide0`,
    defaultMessage:
      'From simple transactions to complex financial operations like Streaming, Milestone based, and Split payments, you can do it with Colony.',
  },
  descriptionSlide1: {
    id: `${displayName}.descriptionSlide1`,
    defaultMessage:
      'Make bulk payments to different recipients using various tokens, amounts and scheduling. Saving time and reducing potential errors.',
  },
  descriptionSlide2: {
    id: `${displayName}.descriptionSlide2`,
    defaultMessage:
      'Transparency and clarity around shared finances is made simply with dashboard highlights, transaction history, and shared decision making. ',
  },
});

const slides = [
  {
    title: MSG.titleSlide0,
    description: MSG.descriptionSlide0,
  },
  {
    title: MSG.titleSlide1,
    description: MSG.descriptionSlide1,
  },
  {
    title: MSG.titleSlide2,
    description: MSG.descriptionSlide2,
  },
];

const LandingPageCarousel = () => {
  const [currentSlide, setCurrentSlide] = useState(0);

  return (
    <div>
      <div className="w-full max-w-[31.25rem] md:hidden">
        <img className="h-auto w-full" src={SlideMobile} alt="slider mobile" />
      </div>
      <div className="hidden w-full max-w-[31.25rem] overflow-hidden md:block">
        <div className="relative mb-9 h-[7.25rem]">
          {slides.map((slide, index) => {
            const isCurrentSlide = currentSlide === index;
            return (
              <div className="absolute left-0 top-0">
                <h1
                  className={clsx(
                    'transition-opacity duration-normal heading-2',
                    {
                      'opacity-100 delay-150': isCurrentSlide,
                      'opacity-0': !isCurrentSlide,
                    },
                  )}
                >
                  <FormattedMessage {...slide.title} />
                </h1>
                <p
                  className={clsx(
                    'mt-[.875rem] text-md font-normal transition-opacity duration-normal',
                    {
                      'opacity-100 delay-150': isCurrentSlide,
                      'opacity-0': !isCurrentSlide,
                    },
                  )}
                >
                  <FormattedMessage {...slide.description} />
                </p>
              </div>
            );
          })}
        </div>
        <ImageCarousel
          slideUrls={[Slide1, Slide2, Slide3]}
          options={{ align: 'center', loop: true }}
          isImageFullWidth
          isAutoplay
          isChangeSlideDotButton={false}
          setSelectedIndex={(currentSlideIndex) =>
            setCurrentSlide(currentSlideIndex)
          }
          className="mx-[-30px] pb-[115px]"
        />
      </div>
    </div>
  );
};

LandingPageCarousel.displayName = displayName;

export default LandingPageCarousel;

Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import clsx from 'clsx';
import React, { useState } from 'react';
import { defineMessages, FormattedMessage } from 'react-intl';

import ImageCarousel from '~common/Extensions/ImageCarousel/ImageCarousel.tsx';
import Slide1 from '~images/assets/landing/slider1.png';
import Slide2 from '~images/assets/landing/slider2.png';
import Slide3 from '~images/assets/landing/slider3.png';
import SlideMobile from '~images/assets/landing/sliderMobile.png';

const displayName = 'frame.LandingPageCarousel';

const titleMSG = defineMessages({
titleSlide0: {
id: `${displayName}.titleSlide0`,
defaultMessage: 'A powerful, all-in-one payments suite',
},
titleSlide1: {
id: `${displayName}.titleSlide1`,
defaultMessage: 'Make bulk payments your way with ease',
},
titleSlide2: {
id: `${displayName}.titleSlide2`,
defaultMessage: 'Easily track & manage shared finances',
},
});

const descriptionMSG = defineMessages({
descriptionSlide0: {
id: `${displayName}.dascriptionSlide0`,
Copy link
Contributor

Choose a reason for hiding this comment

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

Please update id to ${displayName}.descriptionSlide0

defaultMessage:
'From simple transactions to complex financial operations like Streaming, Milestone based, and Split payments, you can do it with Colony.',
},
descriptionSlide1: {
id: `${displayName}.dascriptionSlide0`,
Copy link
Contributor

Choose a reason for hiding this comment

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

Please update id to ${displayName}.descriptionSlide1

defaultMessage:
'Make bulk payments to different recipients using various tokens, amounts and scheduling. Saving time and reducing potential errors.',
},
descriptionSlide2: {
id: `${displayName}.dascriptionSlide0`,
Copy link
Contributor

Choose a reason for hiding this comment

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

Please update id to ${displayName}.descriptionSlide2

defaultMessage:
'Transparency and clarity around shared finances is made simply with dashboard highlights, transaction history, and shared decision making. ',
},
});

const LandingPageCarousel = () => {
const [currentSlide, setCurrentSlide] = useState(0);

return (
<div>
<div className="w-full max-w-[31.25rem] md:hidden">
<img className="h-auto w-full" src={SlideMobile} alt="slider mobile" />
</div>
<div className="hidden w-full max-w-[31.25rem] overflow-hidden md:block">
<div>
<div className="relative h-[4.75rem]">
{Object.keys(titleMSG).map((_, index) => (
<h1
className={clsx(
'absolute left-0 top-0 transition-opacity duration-normal heading-2',
{
'opacity-100': currentSlide === index,
'opacity-0': currentSlide !== index,
'delay-150': currentSlide === index,
},
)}
>
<FormattedMessage {...titleMSG[`titleSlide${index}`]} />
</h1>
))}
</div>
<div className="relative mb-9 mt-[.875rem] h-[2.5rem]">
{Object.keys(descriptionMSG).map((_, index) => (
<p
className={clsx(
'absolute left-0 top-0 text-md font-normal transition-opacity duration-normal',
{
'opacity-100': currentSlide === index,
'opacity-0': currentSlide !== index,
'delay-150': currentSlide === index,
},
)}
>
<FormattedMessage
{...descriptionMSG[`descriptionSlide${index}`]}
/>
</p>
))}
</div>
</div>
<ImageCarousel
slideUrls={[Slide1, Slide2, Slide3]}
options={{ align: 'center', loop: true }}
isImageFullWidth
isAutoplay
isChangeSlideDotButton={false}
setSelectedIndex={(currentSlideIndex) =>
setCurrentSlide(currentSlideIndex)
}
className="mx-[-30px] pb-[115px]"
/>
</div>
</div>
);
};

LandingPageCarousel.displayName = displayName;

export default LandingPageCarousel;
Binary file added src/images/assets/landing/slider1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/images/assets/landing/slider2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/images/assets/landing/slider3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/images/assets/landing/sliderMobile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions src/stories/common/LandingPageCarousel.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { type Meta, type StoryObj } from '@storybook/react';
import React from 'react';

import LandingPageCarousel from '~frame/LandingPage/LandingPageCarousel.tsx';

const meta: Meta<typeof LandingPageCarousel> = {
title: 'Common/Landing Page Carousel',
component: LandingPageCarousel,
decorators: [
(Story) => (
<div className="flex w-full justify-center">
<Story />
</div>
),
],
};

export default meta;
type Story = StoryObj<typeof LandingPageCarousel>;

export const Base: Story = {};
Loading