Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ jobs:
SQLX_OFFLINE: '1'
RUSTC_WRAPPER: sccache
SCCACHE_GHA_ENABLED: 'true'
DEFGUARD_CLIENT_WELCOME_SKIP: '1'
steps:
- name: Install system dependencies
run: |
Expand Down
38 changes: 38 additions & 0 deletions new-ui/src/pages/welcome/WelcomePage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Fragment } from 'react/jsx-runtime';
import { FullPage } from '../../shared/layouts/FullPage/FullPage';
import './style.scss';
import { IconKind } from '../../shared/components/Icon';
import { IconButton } from '../../shared/components/IconButton/IconButton';
import { IconButtonVariant } from '../../shared/components/IconButton/types';
import { Snackbar } from '../../shared/providers/snackbar/snackbar';
import { ThemeSpacing } from '../../shared/types';
import { WelcomeCarousel } from './components/WelcomeCarousel/WelcomeCarousel';
import { welcomeSlides } from './config';

export const WelcomePage = () => {
return (
<Fragment>
<div
className="outside-header"
data-tauri-drag-region
style={{
width: '100%',
height: ThemeSpacing.Md,
}}
></div>
<FullPage hideScrollContainer id="welcome-page">
<header>
<h1>{`What's new`}</h1>
<IconButton
icon={IconKind.Close}
variant={IconButtonVariant.Big}
onClick={() => {
Snackbar.default(`Close Welcome view`);
}}
/>
</header>
<WelcomeCarousel slides={welcomeSlides} />
</FullPage>
</Fragment>
);
};
Binary file added new-ui/src/pages/welcome/assets/test_frame.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions new-ui/src/pages/welcome/components/SlideImage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
type Props = {
src: string;
};

export const SlideImage = ({ src }: Props) => {
return (
<img
src={src}
loading="eager"
width={'100%'}
height={'auto'}
style={{ overflow: 'hidden' }}
/>
);
};
18 changes: 18 additions & 0 deletions new-ui/src/pages/welcome/components/SlideVideo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
type Props = {
src: string;
};

export const SlideVideo = ({ src }: Props) => {
return (
<video
src={src}
width={'100%'}
height={'auto'}
autoPlay
loop
muted
playsInline
style={{ overflow: 'hidden' }}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import './style.scss';
import { openUrl } from '@tauri-apps/plugin-opener';
import { AnimatePresence, motion } from 'motion/react';
import { Fragment, useState } from 'react';
import { Button } from '../../../../shared/components/Button/Button';
import { ButtonVariant } from '../../../../shared/components/Button/types';
import { Divider } from '../../../../shared/components/Divider/Divider';
import { SizedBox } from '../../../../shared/components/SizedBox/SizedBox';
import { motionTransitionStandard } from '../../../../shared/consts';
import { ThemeSpacing } from '../../../../shared/types';
import { isPresent } from '../../../../shared/utils/isPresent';
import { SlideImage } from '../SlideImage';
import { SlideVideo } from '../SlideVideo';
import type { CarouselSlide } from '../types';
import { CarouselControls } from './components/CarouselControls/CarouselControls';
import { CarouselIndicators } from './components/CarouselIndicators/CarouselIndicators';
import type { CarouselDirection, WelcomeCarouselProps } from './types';

const slideVariants = {
enter: (direction: CarouselDirection) => ({
x: direction > 0 ? '100%' : '-100%',
opacity: 0,
}),
center: {
x: 0,
opacity: 1,
},
exit: (direction: CarouselDirection) => ({
x: direction > 0 ? '-100%' : '100%',
opacity: 0,
}),
};

const slideTransition = {
...motionTransitionStandard,
duration: 0.32,
};

const renderSlide = (slide: CarouselSlide) => {
switch (slide.slideType) {
case 'image':
return <SlideImage src={slide.slideSrc} />;
case 'video':
return <SlideVideo src={slide.slideSrc} />;
}
};

export const WelcomeCarousel = ({ slides }: WelcomeCarouselProps) => {
const [[activeIndex, direction], setSlideState] = useState<[number, CarouselDirection]>(
[0, 1],
);

const isFirst = activeIndex === 0;
const isLast = activeIndex === slides.length - 1;
const activeSlide = slides[activeIndex];

const goTo = (index: number) => {
if (index === activeIndex) return;
setSlideState([index, index > activeIndex ? 1 : -1]);
};
const goPrev = () => {
if (!isFirst) goTo(activeIndex - 1);
};
const goNext = () => {
if (!isLast) goTo(activeIndex + 1);
};

return (
<div className="welcome-carousel">
<div className="carousel-viewport">
<AnimatePresence mode="popLayout" custom={direction} initial={false}>
<motion.div
key={activeIndex}
className="carousel-slide"
custom={direction}
variants={slideVariants}
initial="enter"
animate="center"
exit="exit"
transition={slideTransition}
>
{renderSlide(activeSlide)}
<div className="content">
<p className="title">{activeSlide.title}</p>
<SizedBox height={ThemeSpacing.Xs} />
<p className="description">{activeSlide.description}</p>
{isPresent(activeSlide.blogLink) && isPresent(activeSlide.blogLinkText) && (
<Fragment>
<SizedBox height={ThemeSpacing.Xl} />
<Button
text={activeSlide.blogLinkText}
variant={ButtonVariant.Primary}
onClick={() => openUrl(activeSlide.blogLink as string)}
/>
</Fragment>
)}
</div>
</motion.div>
</AnimatePresence>
</div>
{slides.length > 1 && (
<Fragment>
<Divider spacing={ThemeSpacing.Xl} />
<div className="carousel-footer">
<CarouselControls
onPrev={goPrev}
onNext={goNext}
disablePrev={isFirst}
disableNext={isLast}
>
<CarouselIndicators
count={slides.length}
activeIndex={activeIndex}
onSelect={goTo}
/>
</CarouselControls>
</div>
</Fragment>
)}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import './style.scss';
import clsx from 'clsx';
import type { PropsWithChildren } from 'react';
import {
Icon,
IconKind,
type IconKindValue,
} from '../../../../../../shared/components/Icon';
import { Direction, type DirectionValue } from '../../../../../../shared/types';

type Props = {
onPrev: () => void;
onNext: () => void;
disablePrev: boolean;
disableNext: boolean;
} & PropsWithChildren;

export const CarouselControls = ({
onPrev,
onNext,
disablePrev,
disableNext,
children,
}: Props) => {
return (
<div className="carousel-controls">
<ControlButton
icon={IconKind.ArrowSmall}
iconRotation={Direction.LEFT}
onClick={onPrev}
disabled={disablePrev}
/>
{children}
<ControlButton
icon={IconKind.ArrowSmall}
iconRotation={Direction.RIGHT}
onClick={onNext}
disabled={disableNext}
/>
</div>
);
};

const ControlButton = ({
disabled,
onClick,
icon,
iconRotation,
}: {
disabled: boolean;
onClick: () => void;
icon: IconKindValue;
iconRotation: DirectionValue;
}) => {
return (
<button
className={clsx({
disabled,
})}
onClick={onClick}
disabled={disabled}
>
<Icon icon={icon} size={20} rotationDirection={iconRotation} />
</button>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
.carousel-controls {
display: flex;
flex-flow: row;
column-gap: var(--spacing-md);
align-items: center;
justify-content: center;
width: 100%;

> button {
--color: var(--fg-white-100);

border: solid 1px var(--color);
border-radius: 100px;
height: 28px;
width: 28px;
margin: 0;
padding: 0;
background-color: transparent;
cursor: pointer;
position: relative;
display: flex;
flex-flow: row;
align-items: center;
justify-content: center;
overflow: hidden;
user-select: none;

@include animate(border);

&:disabled,
&.disabled {
--color: var(--fg-white-20);

cursor: not-allowed;
}

.icon {
--icon-color: var(--color);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import './style.scss';
import clsx from 'clsx';

type Props = {
count: number;
activeIndex: number;
onSelect: (index: number) => void;
};

export const CarouselIndicators = ({ count, activeIndex, onSelect }: Props) => {
return (
<div className="carousel-indicators">
{Array.from({ length: count }, (_, index) => (
<button
key={index}
type="button"
className={clsx('indicator', { active: index === activeIndex })}
aria-current={index === activeIndex}
aria-label={`Go to slide ${index + 1}`}
onClick={() => onSelect(index)}
>
<span className="dot"></span>
</button>
))}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
.carousel-indicators {
display: flex;
flex-flow: row;
align-items: center;

.indicator {
--scale: 1;
--color: var(--fg-white-30);

border: none;
padding: 5px;
cursor: pointer;
box-sizing: border-box;
margin: 0;
background: transparent;
user-select: none;

&.active {
--color: var(--fg-white-100);
--scale: 1.25;
}

&:hover:not(.active) {
--color: var(--fg-white-60);
--scale: 1;
}

> .dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--color);
transform: scale(var(--scale));
content: ' ';
display: block;

@include animate(background-color, transform);
}
}
}
Loading
Loading