Skip to content

Commit

Permalink
refactor(frontend): refactor user tour and cookies consent
Browse files Browse the repository at this point in the history
  • Loading branch information
MiracleHorizon committed Apr 4, 2024
1 parent 877ebf7 commit cce3642
Show file tree
Hide file tree
Showing 13 changed files with 129 additions and 113 deletions.
12 changes: 4 additions & 8 deletions apps/frontend/src/__setup__/__mocks__/zustand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@ const createUncurried = <T>(stateCreator: zustand.StateCreator<T>) => {
return store
}

export const create = (<T>(stateCreator: zustand.StateCreator<T>) => {
return typeof stateCreator === 'function' ? createUncurried(stateCreator) : createUncurried
}) as typeof zustand.create
export const create = (<T>(stateCreator: zustand.StateCreator<T>) => typeof stateCreator === 'function' ? createUncurried(stateCreator) : createUncurried) as typeof zustand.create

const createStoreUncurried = <T>(stateCreator: zustand.StateCreator<T>) => {
const store = actualCreateStore(stateCreator)
Expand All @@ -35,11 +33,9 @@ const createStoreUncurried = <T>(stateCreator: zustand.StateCreator<T>) => {
return store
}

export const createStore = (<T>(stateCreator: zustand.StateCreator<T>) => {
return typeof stateCreator === 'function'
? createStoreUncurried(stateCreator)
: createStoreUncurried
}) as typeof zustand.createStore
export const createStore = (<T>(stateCreator: zustand.StateCreator<T>) => typeof stateCreator === 'function'
? createStoreUncurried(stateCreator)
: createStoreUncurried) as typeof zustand.createStore

afterEach(() => {
act(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,44 +5,18 @@ import { Button, Card, Flex, Text } from '@radix-ui/themes'

import { CookieIcon } from '@scissors/react-icons/CookieIcon'

import { isTourCompleted as isTourCompletedCheck, TOUR_LS_KEY } from '@lib/tour'
import { TOUR_LS_KEY } from '@lib/tour'
import { useSelectedPath } from '@hooks/useSelectedPath'
import { PATH_ROOT } from '@site/paths'
import { acceptCookies, isAccepted, isVisibleCheck } from './utils'
import styles from './CookieConsentBanner.module.css'

const KEY = 'scissors-cookie-consent'

function isVisibleCheck(): boolean {
/*
* Check if already accepted.
*/
const isAccepted = !!localStorage.getItem(KEY)
if (isAccepted) {
return false
}

/*
* Banner can be shown only once and only if user tour is completed.
*/
const isTourCompleted = isTourCompletedCheck()
if (!isTourCompleted) {
return false
}

return isTourCompleted && !isAccepted
}

export default function CookieConsentBanner() {
const isHomePage = useSelectedPath(PATH_ROOT)
const [isVisible, setVisible] = useState(isVisibleCheck())

const handleCookiesAccept = () => {
localStorage.setItem(
KEY,
JSON.stringify({
accepted: true
})
)
acceptCookies()
setVisible(false)
}

Expand All @@ -53,8 +27,12 @@ export default function CookieConsentBanner() {
function handleCompleteTour(ev: StorageEvent) {
if (ev.key !== TOUR_LS_KEY) return

const isCompleted = ev.newValue !== null
/*
* If cookies already accepted - do nothing.
*/
if (isAccepted()) return

const isCompleted = ev.newValue !== null
if (isCompleted) {
setVisible(true)
}
Expand Down
32 changes: 32 additions & 0 deletions apps/frontend/src/components/CookieConsentBanner/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { isTourCompleted as isTourCompletedCheck } from '@lib/tour'

export const COOKIE_CONSENT_LS_KEY = 'scissors-cookie-consent'

export const acceptCookies = () =>
localStorage.setItem(
COOKIE_CONSENT_LS_KEY,
JSON.stringify({
accepted: true
})
)

export const isAccepted = () => !!localStorage.getItem(COOKIE_CONSENT_LS_KEY)

export function isVisibleCheck(): boolean {
/*
* Check if already accepted.
*/
if (isAccepted()) {
return false
}

/*
* Banner can be shown only once and only if user tour is completed.
*/
const isTourCompleted = isTourCompletedCheck()
if (!isTourCompleted) {
return false
}

return isTourCompleted && !isAccepted()
}
16 changes: 15 additions & 1 deletion apps/frontend/src/components/ImageUploader/ImageUploader.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,31 @@
'use client'

import { useEffect } from 'react'
import { Flex } from '@radix-ui/themes'
import 'driver.js/dist/driver.css'

import { ImageDropzone } from '@components/ImageDropzone'
import { ImageUploadPopover } from '@components/ImageUploadPopover'
import { ButtonUpload } from '@ui/ButtonUpload'
import { useOutputStore } from '@stores/output'
import { allowedImageFormats } from '@site/config'
import { TOUR_STEP } from '@lib/tour'
import { createTour, isTourCompleted, TOUR_STEP } from '@lib/tour'
import '@lib/tour/tour.css'

export function ImageUploader() {
const setFile = useOutputStore(state => state.setFile)

useEffect(() => {
if (isTourCompleted()) return

/*
* Start user tour.
*/
createTour().then(driver => {
driver.drive()
})
}, [])

return (
<Flex
gap='2'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,14 @@ import { ReaderIcon } from '@scissors/react-icons/ReaderIcon'
import { ImageIcon } from '@scissors/react-icons/ImageIcon'

import { PATH_DOCS, PATH_GALLERY } from '@site/paths'
import { TOUR_STEP } from '@lib/tour'
import type { NavigationItemModel } from './types'

export const navigationItems: NavigationItemModel[] = [
{
label: 'Documentation',
href: PATH_DOCS,
tooltipContent: 'Watch the documentation',
icon: <ReaderIcon width='18px' height='18px' label='go to documentation' />,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
'data-tourstep': TOUR_STEP.DOCUMENTATION_LINK
icon: <ReaderIcon width='18px' height='18px' label='go to documentation' />
},
{
label: 'Gallery',
Expand Down
5 changes: 2 additions & 3 deletions apps/frontend/src/lib/tour/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ export const TOUR_LS_KEY = 'scissors-tour-completed'
export const TOUR_STEP = {
FILE_UPLOAD: 1,
TOOLBAR_TAB_LIST: 2,
TOOLBAR_ACTIONS: 3,
TOOLBAR_MENU: 3,
SETTINGS_PANEL: 4,
REQUEST_BUTTON: 5,
DOWNLOAD_BUTTON: 6,
DOCUMENTATION_LINK: 7
DOWNLOAD_BUTTON: 6
} as const
71 changes: 28 additions & 43 deletions apps/frontend/src/lib/tour/driver.ts
Original file line number Diff line number Diff line change
@@ -1,89 +1,63 @@
import { clsx } from 'clsx/lite'

import { completeTour } from './actions'
import { TOUR_STEP } from './constants'
import type { PopoverDOM } from './types'

const createTourElementSelector = (step: number) => `[data-tourstep='${step}']`

export async function createTour() {
const { driver } = await import('driver.js')
const { geistSans } = await import('@app/fonts')
const { isPrefersReduceMotion } = await import('@helpers/isPrefersReduceMotion')

const steps = [
const steps: ReturnType<(typeof driver.arguments)['steps']> = [
{
element: `[data-tourstep='${TOUR_STEP.FILE_UPLOAD}']`,
element: createTourElementSelector(TOUR_STEP.FILE_UPLOAD),
popover: {
title: 'File upload zone',
description:
'Here you can upload an image from your computer. You can also use Drag-n-Drop to upload it.'
}
description: 'Here you can upload an image from your computer or from the web.'
},
showButtons: ['next']
},
{
element: `[data-tourstep='${TOUR_STEP.TOOLBAR_TAB_LIST}']`,
element: createTourElementSelector(TOUR_STEP.TOOLBAR_TAB_LIST),
popover: {
title: 'Processing tabs',
title: 'Tabs',
description: 'You can choose between options tabs to process the image.'
}
},
{
element: `[data-tourstep='${TOUR_STEP.TOOLBAR_ACTIONS}']`,
element: createTourElementSelector(TOUR_STEP.TOOLBAR_MENU),
popover: {
title: 'Toolbar actions',
title: 'Toolbar Menu',
description:
'You can export and import settings, configure their randomization, and reset and delete them.'
'You can export and import settings, configure their randomization, and reset and remove them.'
}
},
{
element: `[data-tourstep='${TOUR_STEP.SETTINGS_PANEL}']`,
element: createTourElementSelector(TOUR_STEP.SETTINGS_PANEL),
popover: {
title: 'Settings panel',
title: 'Settings Panel',
description:
'Here you can select a large number of different options for processing and formatting your uploaded images.'
'Here you can select a large number of different options for processing and formatting your images.'
}
},
{
element: `[data-tourstep='${TOUR_STEP.REQUEST_BUTTON}']`,
element: createTourElementSelector(TOUR_STEP.REQUEST_BUTTON),
popover: {
title: 'Request button',
description:
'Once you have completed the image processing configuration settings, you can click on this button to apply them.'
}
},
{
element: `[data-tourstep='${TOUR_STEP.DOWNLOAD_BUTTON}']`,
element: createTourElementSelector(TOUR_STEP.DOWNLOAD_BUTTON),
popover: {
title: 'Download button',
description:
'After processing the image, you can download it to your device by clicking this button'
}
},
{
element: `[data-tourstep='${TOUR_STEP.DOCUMENTATION_LINK}']`,
popover: {
title: 'Documentation page',
description:
'Here you can read more about a particular option, what it does and how its various parameters work.'
}
}
]

const tourDriver = driver({
allowClose: false,
showProgress: true,
animate: !isPrefersReduceMotion(),
showButtons: ['previous', 'next'],
onDestroyed: completeTour,
steps: steps.map(({ element, popover }) => ({
element,
popover: {
...popover,
popoverClass: clsx('tour-popover', geistSans.variable)
}
})),
prevBtnText: '&#8592; Prev',
onPopoverRender: popover => onPopoverRender(popover)
})

function createSkipButton() {
const skipButton = document.createElement('span')

Expand All @@ -103,5 +77,16 @@ export async function createTour() {
})
}

const tourDriver = driver({
allowClose: false,
showProgress: true,
animate: !isPrefersReduceMotion(),
showButtons: ['previous', 'next'],
steps,
prevBtnText: '&#8592; Prev',
onDestroyed: completeTour,
onPopoverRender
})

return tourDriver
}
29 changes: 25 additions & 4 deletions apps/frontend/src/lib/tour/tour.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,28 @@
}

html.dark {
--bg-color: var(--gray-3);

.driver-popover {
background-color: var(--gray-4);
background-color: var(--bg-color);
color: var(--accent-contrast);
}

.driver-popover-arrow-side-top {
border-top-color: var(--bg-color);
}

.driver-popover-arrow-side-bottom {
border-bottom-color: var(--bg-color);
}

.driver-popover-arrow-side-left {
border-left-color: var(--bg-color);
}

.driver-popover-arrow-side-right {
border-right-color: var(--bg-color);
}
}

.driver-popover *,
Expand All @@ -19,11 +37,14 @@ html.dark {
display: flex !important;
justify-content: space-between;
font-weight: 500 !important;
font-size: 17px;
}

.driver-popover-description {
margin-top: 6px !important;
font-weight: 400 !important;
font-size: 15px;
font-size: 14px;
color: #9a99a1;
}

.driver-popover-title,
Expand Down Expand Up @@ -62,7 +83,7 @@ html.dark {
color: #fff;
border: none;
border-radius: 4px;
padding: 5px 8px;
padding: 6px 12px 5px 12px;
margin: 0;
}

Expand All @@ -76,7 +97,7 @@ html.dark {
pointer-events: none !important;
}

/* Enable overflow for FileUploadZone component */
/* Enable overflow for ImageDropzone component */
:not(body):has(> [title='File is not uploaded'].driver-active-element) {
overflow: initial !important;
}
Loading

0 comments on commit cce3642

Please sign in to comment.