'init'
This commit is contained in:
168
app/views/Introduction/Introduction.module.css
Normal file
168
app/views/Introduction/Introduction.module.css
Normal file
@@ -0,0 +1,168 @@
|
||||
.wrapper {
|
||||
box-sizing: border-box;
|
||||
background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-8));
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
overflow: auto;
|
||||
padding: 60px 20px 30px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-family:
|
||||
Greycliff CF,
|
||||
var(--mantine-font-family);
|
||||
font-size: 62px;
|
||||
font-weight: 900;
|
||||
line-height: 1.1;
|
||||
margin-top: 70px;
|
||||
padding: 0;
|
||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
|
||||
|
||||
@media (max-width: $mantine-breakpoint-sm) {
|
||||
font-size: 42px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-top: var(--mantine-spacing-xl);
|
||||
font-size: 24px;
|
||||
|
||||
@media (max-width: $mantine-breakpoint-sm) {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.controls {
|
||||
margin-top: calc(var(--mantine-spacing-xl) * 2);
|
||||
|
||||
@media (max-width: $mantine-breakpoint-sm) {
|
||||
margin-top: var(--mantine-spacing-xl);
|
||||
}
|
||||
}
|
||||
|
||||
.control {
|
||||
height: 54px;
|
||||
padding-left: 38px;
|
||||
padding-right: 38px;
|
||||
|
||||
@media (max-width: $mantine-breakpoint-sm) {
|
||||
height: 54px;
|
||||
padding-left: 18px;
|
||||
padding-right: 18px;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.carousel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 340px;
|
||||
}
|
||||
|
||||
.slideContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
justify-content: flex-start;
|
||||
animation: fadeInSlide 0.5s ease-in-out;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@keyframes fadeInSlide {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.iconWrapper {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 32px;
|
||||
transition: all 0.5s ease;
|
||||
animation: scaleIn 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes scaleIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
|
||||
animation: fadeIn 0.5s ease-in-out 0.1s both;
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 15px;
|
||||
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
|
||||
line-height: 1.5;
|
||||
max-width: 300px;
|
||||
animation: fadeIn 0.5s ease-in-out 0.2s both;
|
||||
min-height: 68px;
|
||||
}
|
||||
|
||||
.dots {
|
||||
margin: 20px 0;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4));
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.dot:hover {
|
||||
background-color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3));
|
||||
}
|
||||
|
||||
.dotActive {
|
||||
width: 24px;
|
||||
border-radius: 4px;
|
||||
background-color: light-dark(var(--mantine-color-blue-6), var(--mantine-color-blue-4));
|
||||
}
|
||||
160
app/views/Introduction/Introduction.tsx
Normal file
160
app/views/Introduction/Introduction.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Button, Group, Text, Flex } from '@mantine/core';
|
||||
import { IconShieldLock, IconBolt, IconDeviceMobile } from '@tabler/icons-react';
|
||||
import classes from './Introduction.module.css';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import useWindow, { ElectronTheme } from '@/app/hooks/useWindow';
|
||||
import { AnimatedButton } from '@/app/components/AnimatedButton/AnimatedButton';
|
||||
|
||||
const slides = [
|
||||
{
|
||||
title: 'Security',
|
||||
description: 'Your data is protected by modern encryption and stays only on your device',
|
||||
icon: IconShieldLock,
|
||||
color: '#4CAF50',
|
||||
},
|
||||
{
|
||||
title: 'Speed',
|
||||
description: 'Instant synchronization and fast performance without delays',
|
||||
icon: IconBolt,
|
||||
color: '#FF9800',
|
||||
},
|
||||
{
|
||||
title: 'Simple Interface',
|
||||
description: 'Intuitive design that is clear at first glance',
|
||||
icon: IconDeviceMobile,
|
||||
color: '#2196F3',
|
||||
},
|
||||
];
|
||||
|
||||
export function Introduction() {
|
||||
const navigate = useNavigate();
|
||||
const {setSize, setResizeble, setTheme} = useWindow();
|
||||
const [activeSlide, setActiveSlide] = useState(0);
|
||||
const [touchStart, setTouchStart] = useState<number | null>(null);
|
||||
const [touchEnd, setTouchEnd] = useState<number | null>(null);
|
||||
|
||||
// Минимальное расстояние свайпа (в px)
|
||||
const minSwipeDistance = 50;
|
||||
|
||||
useEffect(() => {
|
||||
setSize(385, 555);
|
||||
setResizeble(false);
|
||||
setTheme(ElectronTheme.SYSTEM);
|
||||
}, []);
|
||||
|
||||
const handleGetStarted = () => {
|
||||
navigate('/create-seed');
|
||||
};
|
||||
|
||||
const goToSlide = (index: number) => {
|
||||
setActiveSlide(index);
|
||||
};
|
||||
|
||||
const nextSlide = () => {
|
||||
setActiveSlide((prev) => (prev + 1) % slides.length);
|
||||
};
|
||||
|
||||
const prevSlide = () => {
|
||||
setActiveSlide((prev) => (prev - 1 + slides.length) % slides.length);
|
||||
};
|
||||
|
||||
const onTouchStart = (e: React.TouchEvent) => {
|
||||
setTouchEnd(null);
|
||||
setTouchStart(e.targetTouches[0].clientX);
|
||||
};
|
||||
|
||||
const onTouchMove = (e: React.TouchEvent) => {
|
||||
setTouchEnd(e.targetTouches[0].clientX);
|
||||
};
|
||||
|
||||
const onTouchEnd = () => {
|
||||
if (!touchStart || !touchEnd) return;
|
||||
|
||||
const distance = touchStart - touchEnd;
|
||||
const isLeftSwipe = distance > minSwipeDistance;
|
||||
const isRightSwipe = distance < -minSwipeDistance;
|
||||
|
||||
if (isLeftSwipe) {
|
||||
nextSlide();
|
||||
} else if (isRightSwipe) {
|
||||
prevSlide();
|
||||
}
|
||||
};
|
||||
|
||||
const handleCarouselClick = () => {
|
||||
nextSlide();
|
||||
};
|
||||
|
||||
const CurrentIcon = slides[activeSlide].icon;
|
||||
|
||||
return (
|
||||
<div className={classes.wrapper}>
|
||||
<Flex h={'100%'} w={'100%'} direction={'column'} justify={'space-between'} align={'center'}>
|
||||
<div>
|
||||
<div
|
||||
className={classes.carousel}
|
||||
onTouchStart={onTouchStart}
|
||||
onTouchMove={onTouchMove}
|
||||
onTouchEnd={onTouchEnd}
|
||||
onClick={handleCarouselClick}
|
||||
style={{ cursor: 'pointer', userSelect: 'none' }}
|
||||
>
|
||||
<div className={classes.slideContent} key={activeSlide}>
|
||||
<div
|
||||
className={classes.iconWrapper}
|
||||
style={{ backgroundColor: `${slides[activeSlide].color}20` }}
|
||||
>
|
||||
<CurrentIcon
|
||||
size={80}
|
||||
stroke={1.5}
|
||||
color={slides[activeSlide].color}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Text size={'sm'} className={classes.title} fw={500}>
|
||||
{slides[activeSlide].title}
|
||||
</Text>
|
||||
|
||||
<Text mt={'xs'} size={'xs'} c={'dimmed'} className={classes.description}>
|
||||
{slides[activeSlide].description}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Group justify="center" className={classes.dots}>
|
||||
{slides.map((_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
className={`${classes.dot} ${index === activeSlide ? classes.dotActive : ''}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
goToSlide(index);
|
||||
}}
|
||||
aria-label={`Go to slide ${index + 1}`}
|
||||
/>
|
||||
))}
|
||||
</Group>
|
||||
</div>
|
||||
</div>
|
||||
<Flex gap={'xs'} direction={'column'} w={'100%'}>
|
||||
<AnimatedButton
|
||||
fullWidth
|
||||
size="md"
|
||||
animated={['#2DA5FF', '#87DBFF']}
|
||||
onClick={handleGetStarted}
|
||||
>
|
||||
Get Started
|
||||
</AnimatedButton>
|
||||
<Button
|
||||
fullWidth
|
||||
variant={'transparent'}
|
||||
size="md"
|
||||
onClick={() => navigate('/exists-seed')}
|
||||
>
|
||||
Already have an account
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user