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