Intro.tsx raw

   1  import { EmblaCarouselType } from "embla-carousel";
   2  import {
   3    ArrowRightIcon,
   4    CloudLightningIcon,
   5    LucideIcon,
   6    ShieldCheckIcon,
   7    WalletIcon,
   8  } from "lucide-react";
   9  import React, { ReactElement } from "react";
  10  import { useNavigate } from "react-router";
  11  import Cloud from "src/assets/images/cloud.png";
  12  import Cloud2 from "src/assets/images/cloud2.png";
  13  import { Button } from "src/components/ui/button";
  14  import {
  15    Carousel,
  16    CarouselApi,
  17    CarouselContent,
  18    CarouselItem,
  19  } from "src/components/ui/carousel";
  20  import {
  21    CarouselDotButton,
  22    CarouselDots,
  23  } from "src/components/ui/custom/carousel-dots";
  24  import { useDotButton } from "src/components/ui/custom/useDotButton";
  25  import { useTheme } from "src/components/ui/theme-provider";
  26  import { useInfo } from "src/hooks/useInfo";
  27  import { cn } from "src/lib/utils";
  28  
  29  export function Intro() {
  30    const { data: info } = useInfo();
  31    const navigate = useNavigate();
  32    const [api, setApi] = React.useState<CarouselApi>();
  33    const [progress, setProgress] = React.useState<number>(0);
  34    const { selectedIndex, scrollSnaps, onDotButtonClick } = useDotButton(api);
  35    const { setDarkMode } = useTheme();
  36    const windowWidth = useWindowWidth();
  37  
  38    React.useEffect(() => {
  39      // Force dark mode on intro screen
  40      setDarkMode("dark");
  41      return () => {
  42        // Revert to default after exiting intro
  43        setDarkMode("system");
  44      };
  45    }, [setDarkMode]);
  46  
  47    React.useEffect(() => {
  48      if (!info?.setupCompleted) {
  49        return;
  50      }
  51      navigate("/");
  52    }, [info, navigate]);
  53  
  54    React.useEffect(() => {
  55      api?.on("scroll", (x) => {
  56        setProgress(x.scrollProgress());
  57      });
  58    }, [api]);
  59  
  60    const cloudDesktopSizePositionModifier = Math.floor(
  61      Math.max(1920 - windowWidth, 0) * 0.1
  62    );
  63  
  64    return (
  65      <>
  66        <title>Welcome ยท Alby Hub</title>
  67        <Carousel className={cn("w-full bg-background")} setApi={setApi}>
  68          <div
  69            className="w-full h-full absolute top-0 left-0 bg-no-repeat"
  70            style={{
  71              backgroundImage: `url(${Cloud})`,
  72              backgroundPositionX: `calc(${-Math.max(progress, 0) * 120}px - ${windowWidth * 0.06}px - ${cloudDesktopSizePositionModifier * 4}px)`,
  73            }}
  74          />
  75          <div
  76            className="w-full h-full absolute top-0 left-0 bg-no-repeat"
  77            style={{
  78              backgroundImage: `url(${Cloud2})`,
  79              backgroundPositionX: `calc(${-Math.max(progress, 0) * 120}px + ${windowWidth * 0.5}px + ${Math.floor(cloudDesktopSizePositionModifier * 0.1)}px)`,
  80              backgroundPositionY: "100%",
  81            }}
  82          />
  83          <CarouselContent className="select-none bg-transparent">
  84            <CarouselItem>
  85              <div className="flex flex-col justify-center items-center h-screen p-5">
  86                <div className="flex flex-col gap-4 text-center max-w-lg">
  87                  <div className="text-4xl font-extrabold text-foreground">
  88                    Welcome to Alby Hub
  89                  </div>
  90                  <div className="text-2xl text-muted-foreground font-semibold">
  91                    A powerful, all-in-one bitcoin lightning wallet with the
  92                    superpower of connecting to applications.
  93                  </div>
  94                  <div className="mt-20">
  95                    <Button onClick={() => api?.scrollNext()} size="lg">
  96                      Next
  97                    </Button>
  98                  </div>
  99                </div>
 100              </div>
 101            </CarouselItem>
 102            <CarouselItem>
 103              <Slide
 104                api={api}
 105                icon={CloudLightningIcon}
 106                title="Anywhere & Anytime"
 107                description="Your wallet is always online and ready to use on any device."
 108              />
 109            </CarouselItem>
 110            <CarouselItem>
 111              <Slide
 112                api={api}
 113                icon={ShieldCheckIcon}
 114                title="Your Keys Are Safe"
 115                description="Your wallet is encrypted by a password of your choice. No one can access your funds but you."
 116              />
 117            </CarouselItem>
 118            <CarouselItem>
 119              <Slide
 120                api={api}
 121                icon={WalletIcon}
 122                title="Take Your Wallet With You"
 123                description="Connect your wallet to dozens of apps and participate in the bitcoin digital economy."
 124              />
 125            </CarouselItem>
 126          </CarouselContent>
 127          <div className="absolute bottom-5 left-1/2 -translate-x-1/2">
 128            <CarouselDots>
 129              {scrollSnaps.map((_, index) => (
 130                <CarouselDotButton
 131                  key={index}
 132                  data-selected={index === selectedIndex}
 133                  onClick={() => onDotButtonClick(index)}
 134                  aria-label={`Go to slide ${index + 1}`}
 135                />
 136              ))}
 137            </CarouselDots>
 138          </div>
 139        </Carousel>
 140      </>
 141    );
 142  }
 143  
 144  function Slide({
 145    api,
 146    title,
 147    description,
 148    icon: Icon,
 149  }: {
 150    api: EmblaCarouselType | undefined;
 151    title: string;
 152    description: string;
 153    icon: LucideIcon;
 154    button?: ReactElement;
 155  }) {
 156    const navigate = useNavigate();
 157  
 158    const slideNext = function () {
 159      if (api?.canScrollNext()) {
 160        api.scrollNext();
 161      } else {
 162        navigate("/welcome");
 163      }
 164    };
 165  
 166    return (
 167      <div className="flex flex-col justify-center items-center h-screen gap-8 p-5">
 168        <Icon className="w-16 h-16 text-primary-background" />
 169        <div className="flex flex-col gap-4 text-center items-center max-w-lg">
 170          <div className="text-3xl font-semibold text-primary-background">
 171            {title}
 172          </div>
 173          <div className="text-lg text-muted-foreground font-semibold">
 174            {description}
 175          </div>
 176        </div>
 177        <Button size="icon" onClick={slideNext} className="">
 178          <ArrowRightIcon className="size-4" />
 179        </Button>
 180      </div>
 181    );
 182  }
 183  
 184  function useWindowWidth() {
 185    const [width, setWidth] = React.useState(window.innerWidth);
 186    React.useLayoutEffect(() => {
 187      function updateSize() {
 188        setWidth(window.innerWidth);
 189      }
 190      window.addEventListener("resize", updateSize);
 191      updateSize();
 192      return () => window.removeEventListener("resize", updateSize);
 193    }, []);
 194    return width;
 195  }
 196