68 |
--------------------------------------------------------------------------------
/apps/web/src/styles/global.ts:
--------------------------------------------------------------------------------
1 | import { globalCss, animations } from '.'
2 |
3 | globalCss({
4 | '*': {
5 | margin: 0,
6 | padding: 0,
7 | border: 'none',
8 | boxSizing: 'border-box',
9 | },
10 |
11 | ':root': {
12 | fontSynthesis: 'none',
13 | textRendering: 'optimizeLegibility',
14 | '-webkit-font-smoothing': 'antialiased',
15 | '-moz-osx-font-smoothing': 'grayscale',
16 | '-webkit-text-size-adjust': '100%',
17 | colorScheme: 'dark',
18 | scrollBehavior: 'smooth',
19 | scrollbarGutter: 'stable',
20 | },
21 |
22 | 'body, input, button, textarea, select': {
23 | color: '$text-base',
24 | background: 'transparent',
25 | fontFamily: '$default',
26 | },
27 |
28 | 'h1, h2, h3, h4, h5, h6': {
29 | color: '$text-base',
30 | lineHeight: 1.1,
31 | },
32 |
33 | a: {
34 | color: '$text-base',
35 | textDecoration: 'none',
36 | transition: 'all 0.2s ease',
37 |
38 | 'svg path': {
39 | transition: 'all 0.2s ease',
40 | },
41 |
42 | '&:hover': {
43 | color: '$accent-primary',
44 |
45 | 'svg path': {
46 | fill: '$accent-primary',
47 | },
48 | },
49 |
50 | '&:active': {
51 | transform: 'scale(0.95)',
52 | },
53 | },
54 |
55 | li: { listStyle: 'none' },
56 |
57 | button: {
58 | cursor: 'pointer',
59 | background: 'transparent',
60 | transition: 'all 0.2s ease',
61 |
62 | 'svg path': {
63 | transition: 'all 0.2s ease',
64 | },
65 |
66 | '&:hover': {
67 | color: '$accent-primary',
68 |
69 | 'svg path': {
70 | fill: '$accent-primary',
71 | },
72 | },
73 |
74 | '&:active': {
75 | transform: 'scale(0.95)',
76 | },
77 | },
78 |
79 | body: {
80 | minWidth: '100vw',
81 | minHeight: '100vh',
82 | backgroundColor: '#000000',
83 | overflowX: 'hidden',
84 | },
85 |
86 | '::-webkit-scrollbar': {
87 | width: '0.6rem',
88 | height: '0.6rem',
89 | marginRight: '10px',
90 | },
91 |
92 | '::-webkit-scrollbar-corner': {
93 | background: 'none',
94 | border: 'none',
95 | },
96 |
97 | '::-webkit-scrollbar-thumb': {
98 | backgroundColor: '$shape-tertiary',
99 | borderRadius: '3px',
100 | cursor: 'move',
101 | },
102 |
103 | '::-webkit-scrollbar-track': {
104 | backgroundColor: 'transparent',
105 | border: 'none',
106 | },
107 |
108 | '.animate': {
109 | opacity: 0,
110 | animation: `${animations.fadeIn} 1s ease forwards`,
111 | },
112 | })()
113 |
--------------------------------------------------------------------------------
/.github/workflows/gh-page.yml:
--------------------------------------------------------------------------------
1 | name: Release GH Page
2 |
3 | on:
4 | workflow_dispatch:
5 | label:
6 | types:
7 | - created
8 |
9 | permissions:
10 | contents: read
11 | pages: write
12 | id-token: write
13 |
14 | concurrency:
15 | group: "pages"
16 | cancel-in-progress: true
17 |
18 | jobs:
19 | build:
20 | runs-on: ubuntu-latest
21 | steps:
22 | - name: Checkout
23 | uses: actions/checkout@v3
24 |
25 | - name: Detect package manager
26 | id: detect-package-manager
27 | run: |
28 | if [ -f "${{ github.workspace }}/yarn.lock" ]; then
29 | echo "::set-output name=manager::yarn"
30 | echo "::set-output name=command::install"
31 | echo "::set-output name=runner::yarn"
32 | exit 0
33 | elif [ -f "${{ github.workspace }}/package.json" ]; then
34 | echo "::set-output name=manager::npm"
35 | echo "::set-output name=command::ci"
36 | echo "::set-output name=runner::npx --no-install"
37 | exit 0
38 | else
39 | echo "Unable to determine packager manager"
40 | exit 1
41 | fi
42 |
43 | - name: Setup Node
44 | uses: actions/setup-node@v3
45 | with:
46 | node-version: "18"
47 | cache: ${{ steps.detect-package-manager.outputs.manager }}
48 |
49 | - name: Setup Pages
50 | uses: actions/configure-pages@v2
51 | with:
52 | token: ${{ secrets.GITHUB_TOKEN }}
53 | static_site_generator: next
54 | generator_config_file: ./apps/web/next.config.mjs
55 |
56 | - name: Restore cache
57 | uses: actions/cache@v3
58 | with:
59 | path: |
60 | ./apps/web/.next/cache
61 | key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}
62 | restore-keys: |
63 | ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}-
64 |
65 | - name: Install dependencies
66 | run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }}
67 |
68 | - name: Build with Next.js
69 | working-directory: ./apps/web
70 | run: ${{ steps.detect-package-manager.outputs.runner }} next build
71 |
72 | - name: Static HTML export with Next.js
73 | working-directory: ./apps/web
74 | run: ${{ steps.detect-package-manager.outputs.runner }} next export
75 |
76 | - name: Upload artifact
77 | uses: actions/upload-pages-artifact@v1
78 | with:
79 | path: ./apps/web/out
80 |
81 | deploy:
82 | environment:
83 | name: github-pages
84 | url: ${{ steps.deployment.outputs.page_url }}
85 | runs-on: ubuntu-latest
86 | needs: build
87 | steps:
88 | - name: Deploy to GitHub Pages
89 | id: deployment
90 | uses: actions/deploy-pages@v1
91 |
--------------------------------------------------------------------------------
/apps/web/src/templates/Docs/index.tsx:
--------------------------------------------------------------------------------
1 | import { useMDXComponents } from '@mdx-js/react'
2 | import { useEffect, useState } from 'react'
3 | import { MDXRemote } from 'next-mdx-remote'
4 | import { useRouter } from 'next/router'
5 | import Link from 'next/link'
6 |
7 | import { library, sizes } from 'shared/constants'
8 | import { useMatchMedia } from 'hooks'
9 |
10 | import {
11 | Title,
12 | Header,
13 | Section,
14 | Sidebar,
15 | Content,
16 | Article,
17 | MenuButton,
18 | } from './styles'
19 |
20 | import {
21 | Menu,
22 | Layout,
23 | GitHubIcon,
24 | DocLinkTree,
25 | FixedHeader,
26 | ExternalLink,
27 | LayoutSpacing,
28 | OuterClickArea,
29 | LibraryVersion,
30 | PaginationNavigator,
31 | } from 'components'
32 |
33 | import type { DocProps } from 'shared/types'
34 |
35 | export function DocsTemplate({ doc, links, pagination }: DocProps) {
36 | const route = useRouter()
37 | const MDXComponents = useMDXComponents()
38 | const [menuOpen, setMenuOpen] = useState(false)
39 |
40 | const matchesMobileWidth = useMatchMedia(
41 | `(max-width: ${sizes.breakpoints.mobile}px)`
42 | )
43 |
44 | function handleMenuVisibility() {
45 | setMenuOpen(!menuOpen)
46 | }
47 |
48 | useEffect(() => {
49 | setMenuOpen(false)
50 | }, [matchesMobileWidth, route.asPath])
51 |
52 | return (
53 |
54 |
55 |
56 |
57 |
72 |
73 |
74 |
75 |
76 |
77 |
Playground
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 | {menuOpen && }
94 |
95 |
96 | {doc?.source && (
97 |
98 |
99 |
100 | )}
101 |
102 |
103 |
104 |
105 |
106 | )
107 | }
108 |
--------------------------------------------------------------------------------
/packages/use-exit-intent/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | useExitIntent: 🐠 A React Hook to handle exit intent strategies
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | > The Exit Intent strategy is a great way to increase your conversion rate. That strategy is commonly used to show a modal/popup when the user is about to leave your website.
29 |
30 | # 🐠 Features
31 | - 🚀 Multiple handlers can be registred
32 | - 🔥 Highly configurable
33 | - 🧠 Different strategies for Desktop and Mobile
34 | - ⛔️ Unsubscription support with cookies
35 | - 🎉 Built with TypeScript
36 |
37 | # 🐠 Installation
38 | In your terminal, run:
39 | ```bash
40 | yarn add use-exit-intent
41 |
42 | # OR
43 |
44 | npm i use-exit-intent
45 | ```
46 |
47 | # 🐠 Usage
48 |
49 | In your React component:
50 |
51 | ```tsx
52 | import { useExitIntent } from 'use-exit-intent'
53 |
54 | export function App() {
55 | const { registerHandler } = useExitIntent()
56 |
57 | registerHandler({
58 | id: 'openModal',
59 | handler: () => console.log('Hello from handler!')
60 | })
61 |
62 | // ...
63 | }
64 | ```
65 |
66 | # 🐠 Knowledge
67 | - [Docs](https://use-exit-intent.daltonmenezes.com/docs/getting-started/overview)
68 | - [Playground](https://use-exit-intent.daltonmenezes.com/#playground)
69 |
70 |
71 | # 🐠 Contributing
72 | > **Note**: contributions are always welcome, but always **ask first**, — please — before work on a PR.
73 |
74 | That said, there's a bunch of ways you can contribute to this project, like by:
75 |
76 | - :beetle: Reporting a bug
77 | - :page_facing_up: Improving this documentation
78 | - :rotating_light: Sharing this project and recommending it to your friends
79 | - :dollar: Supporting this project on GitHub Sponsors or Patreon
80 | - :star2: Giving a star on this repository
81 |
82 | # License
83 |
84 | [MIT © Dalton Menezes](https://github.com/daltonmenezes/use-exit-intent/blob/main/LICENSE)
85 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | useExitIntent: 🐠 A React Hook to handle exit intent strategies
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | > The Exit Intent strategy is a great way to increase your conversion rate. That strategy is commonly used to show a modal/popup when the user is about to leave your website.
29 |
30 | # 🐠 Features
31 | - 🚀 Multiple handlers can be registred
32 | - 🔥 Highly configurable
33 | - 🧠 Different strategies for Desktop and Mobile
34 | - ⛔️ Unsubscription support with cookies
35 | - 🎉 Built with TypeScript
36 |
37 | # 🐠 Installation
38 | In your terminal, run:
39 |
40 | - npm
41 | ```bash
42 | npm i use-exit-intent
43 | ```
44 | - pnpm
45 | ```bash
46 | pnpm i use-exit-intent
47 | ```
48 | - yarn
49 | ```bash
50 | yarn add use-exit-intent
51 | ```
52 |
53 | # 🐠 Usage
54 |
55 | In your React component:
56 |
57 | ```tsx
58 | import { useExitIntent } from 'use-exit-intent'
59 |
60 | export function App() {
61 | const { registerHandler } = useExitIntent()
62 |
63 | registerHandler({
64 | id: 'openModal',
65 | handler: () => console.log('Hello from handler!')
66 | })
67 |
68 | // ...
69 | }
70 | ```
71 |
72 | # 🐠 Knowledge
73 | - [Docs](https://use-exit-intent.daltonmenezes.com/docs/getting-started/overview)
74 | - [Playground](https://use-exit-intent.daltonmenezes.com/#playground)
75 |
76 |
77 | # 🐠 Contributing
78 | > **Note**: contributions are always welcome, but always **ask first**, — please — before work on a PR.
79 |
80 | That said, there's a bunch of ways you can contribute to this project, like by:
81 |
82 | - :beetle: Reporting a bug
83 | - :page_facing_up: Improving this documentation
84 | - :rotating_light: Sharing this project and recommending it to your friends
85 | - :dollar: Supporting this project on GitHub Sponsors or Patreon
86 | - :star2: Giving a star on this repository
87 |
88 | # License
89 |
90 | [MIT © Dalton Menezes](https://github.com/daltonmenezes/use-exit-intent/blob/main/LICENSE)
91 |
--------------------------------------------------------------------------------
/apps/web/src/components/InstallationBox/index.tsx:
--------------------------------------------------------------------------------
1 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
2 | import { Fragment, useState } from 'react'
3 |
4 | import { queueTimeouts } from 'shared/utils'
5 | import { commands } from 'shared/constants'
6 |
7 | import { Separator, CheckmarkIcon, ClipboardIcon, Spinner } from 'components'
8 |
9 | import { Button, PackageButton, PackageManagerList, ShellBox } from './styles'
10 | import { usePackageManagerSelection } from 'hooks'
11 | import { codeTheme } from 'styles'
12 |
13 | const clipboardStateIcons = {
14 | copying: () => ,
15 | copied: () => ,
16 | default: () => ,
17 | } as const
18 |
19 | type ClipboardStateKeys = keyof typeof clipboardStateIcons
20 | type PackageManagerOptions = keyof typeof commands
21 |
22 | const defaultActivePackageManager = 'yarn'
23 |
24 | export function InstallationBox() {
25 | const [clipboardState, setClipboardState] =
26 | useState('default')
27 |
28 | const { activePackageManager, updateActivePackageManager } =
29 | usePackageManagerSelection(
30 | defaultActivePackageManager
31 | )
32 |
33 | const installCommand = commands[activePackageManager]
34 |
35 | const availablePackagesManagers = Object.keys(
36 | commands
37 | ) as PackageManagerOptions[]
38 |
39 | function copyToClipboard() {
40 | navigator.clipboard
41 | .writeText(installCommand)
42 | .then(() => setClipboardState('copying'))
43 | .then(() =>
44 | queueTimeouts(
45 | {
46 | delay: 1000,
47 | callback: () => setClipboardState('copied'),
48 | },
49 | {
50 | delay: 1000,
51 | callback: () => setClipboardState('default'),
52 | }
53 | )
54 | )
55 | }
56 |
57 | return (
58 | <>
59 |
60 | {availablePackagesManagers.map(
61 | (packageManagerName, index, packageManagers) => {
62 | const isLastItem = index === packageManagers.length - 1
63 |
64 | return (
65 |
66 |
67 |
70 | updateActivePackageManager(packageManagerName)
71 | }
72 | >
73 | {packageManagerName}
74 |
75 |
76 |
77 | {!isLastItem && }
78 |
79 | )
80 | }
81 | )}
82 |
83 |
84 |
85 |
86 | {installCommand}
87 |
88 |
89 |
96 |
97 | >
98 | )
99 | }
100 |
--------------------------------------------------------------------------------
/apps/web/src/hooks/useElementIntersection.ts:
--------------------------------------------------------------------------------
1 | import { useRef, useEffect, RefObject, useCallback } from 'react'
2 |
3 | import { createDebounce } from 'shared/utils'
4 |
5 | const defaultOptions = {
6 | offset: 0,
7 | timeout: 500,
8 | }
9 |
10 | export type IntersectionChecker = {
11 | element: HTMLElement
12 | offset: number
13 |
14 | position: {
15 | x: number
16 | y: number
17 | }
18 | }
19 |
20 | export type UseIntersectionOptions = typeof defaultOptions
21 | export type Callback = (props: CallbackProps) => void
22 |
23 | type CallbackProps = {
24 | position: IntersectionChecker['position']
25 | ref: RefObject
26 | }
27 |
28 | type InternalCallback = {
29 | id: string
30 | callback: (props: CallbackProps) => void
31 | }
32 |
33 | function checkIntersection({ element, offset, position }: IntersectionChecker) {
34 | const { top, height } = element.getBoundingClientRect()
35 | const elementY = Math.abs(top + position.y - offset)
36 | const bottom = elementY + height - offset
37 |
38 | if (elementY < offset && position.y <= elementY && position.y <= bottom) {
39 | return true
40 | }
41 |
42 | return position.y > elementY && position.y < bottom
43 | }
44 |
45 | export function useElementIntersection(
46 | options: UseIntersectionOptions = defaultOptions
47 | ) {
48 | const ref = useRef(null)
49 | const lastIntersection = useRef(false)
50 | const position = useRef({ x: 0, y: 0 }).current
51 | const callbacks = useRef([]).current
52 |
53 | useEffect(() => {
54 | const { execute, abort } = createDebounce(
55 | handleScroll,
56 | options.timeout || defaultOptions.timeout
57 | )
58 |
59 | function handleScroll() {
60 | const documentElement = document.documentElement
61 | position
62 |
63 | const left =
64 | (window.pageXOffset || documentElement.scrollLeft) -
65 | (documentElement.clientLeft || 0)
66 |
67 | const top =
68 | (window.pageYOffset || documentElement.scrollTop) -
69 | (documentElement.clientTop || 0)
70 |
71 | const homeCallbacks = callbacks.find((callback) => callback.id === 'home')
72 |
73 | if (homeCallbacks && top === 0) {
74 | homeCallbacks.callback({ position, ref })
75 | return
76 | }
77 |
78 | const isIntersecting = checkIntersection({
79 | element: ref.current,
80 | offset: options.offset,
81 |
82 | position: {
83 | x: left,
84 | y: top,
85 | },
86 | })
87 |
88 | const isIntersectionChanged = lastIntersection.current !== isIntersecting
89 |
90 | if (!isIntersectionChanged) return
91 |
92 | if (isIntersectionChanged) {
93 | lastIntersection.current = isIntersecting
94 | }
95 |
96 | if (!isIntersecting) return
97 |
98 | callbacks.forEach(({ callback }) => callback({ ref, position }))
99 | }
100 |
101 | window.addEventListener(`scroll`, execute)
102 |
103 | return () => {
104 | abort()
105 | window.removeEventListener(`scroll`, execute)
106 | }
107 | })
108 |
109 | const onIntersect = useCallback((id: string, callback: Callback) => {
110 | const alreadyPushed = callbacks.find(
111 | (prevCallback) => prevCallback.id === id
112 | )
113 |
114 | if (alreadyPushed) {
115 | callbacks[callbacks.indexOf(alreadyPushed)] = {
116 | id,
117 | callback,
118 | }
119 |
120 | return
121 | }
122 |
123 | callbacks.push({ id, callback })
124 | }, [])
125 |
126 | return {
127 | ref,
128 | onIntersect,
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/apps/web/src/templates/Home/Overview/styles.ts:
--------------------------------------------------------------------------------
1 | import { styled, animations } from 'styles'
2 | import { BoxStyles } from 'components'
3 |
4 | export const CodeContainer = styled('div', {
5 | width: '100%',
6 | overflowX: 'auto',
7 | overflowY: 'hidden',
8 |
9 | pre: {
10 | ...BoxStyles,
11 |
12 | color: '$accent-secondary',
13 | fontSize: '0.8rem',
14 | fontFamily: 'inherit',
15 | lineHeight: '1.5',
16 | textAlign: 'left',
17 | width: '100%',
18 | maxHeight: 546,
19 | padding: '0 1.4rem 1.4rem 1.4rem',
20 | animation: `${animations.reveal} 0.5s ease-in-out`,
21 | },
22 |
23 | 'pre code span': {
24 | transition: 'all 0.25s ease-in-out',
25 | },
26 |
27 | variants: {
28 | exampleState: {
29 | '0': {},
30 |
31 | '1': {
32 | 'pre code': {
33 | 'span:nth-of-type(-n + 40), span:nth-of-type(n + 115)': {
34 | filter: 'opacity(0.2) grayscale(100%)',
35 | },
36 | },
37 | },
38 |
39 | '2': {
40 | 'pre code': {
41 | 'span:nth-of-type(-n + 13), span:nth-of-type(n + 109)': {
42 | filter: 'opacity(0.2) grayscale(100%)',
43 | },
44 | },
45 | },
46 |
47 | '3': {
48 | 'pre code': {
49 | 'span:nth-of-type(-n + 99), span:nth-of-type(n + 109)': {
50 | filter: 'opacity(0.2) grayscale(100%)',
51 | },
52 | },
53 | },
54 |
55 | '4': {
56 | 'pre code': {
57 | 'span:nth-of-type(-n + 112), span:nth-of-type(n + 130)': {
58 | filter: 'opacity(0.2) grayscale(100%)',
59 | },
60 | },
61 | },
62 | },
63 | },
64 |
65 | defaultVariants: {
66 | exampleState: 0,
67 | },
68 | })
69 |
70 | export const Section = styled('section', {
71 | display: 'flex',
72 | flexDirection: 'column',
73 | justifyContent: 'center',
74 | gap: '1rem',
75 | height: '100%',
76 | width: '100%',
77 |
78 | '@bp4': {
79 | flexDirection: 'row',
80 | },
81 | })
82 |
83 | export const SectionContent = styled('div', {
84 | display: 'flex',
85 | flexDirection: 'column',
86 | justifyContent: 'center',
87 | alignItems: 'center',
88 | gap: '1rem',
89 | width: '100%',
90 | minHeight: '100%',
91 | })
92 |
93 | export const Header = styled('header', {
94 | display: 'flex',
95 | flexDirection: 'column',
96 | alignItems: 'center',
97 | alignSelf: 'center',
98 | width: '100%',
99 |
100 | '@bp4': {
101 | alignSelf: 'flex-start',
102 | alignItems: 'flex-start',
103 | width: 'fit-content',
104 | },
105 | })
106 |
107 | export const Title = styled('h2', {
108 | fontSize: '3rem',
109 | lineHeight: '4.25rem',
110 | color: '$accent-secondary',
111 | })
112 |
113 | export const Description = styled('p', {
114 | fontSize: '0.9rem',
115 | lineHeight: '1.5rem',
116 | color: '$text-support',
117 | width: '100%',
118 | paddingBottom: '3rem',
119 |
120 | strong: {
121 | fontSize: '1rem',
122 | color: '$accent-secondary',
123 | },
124 |
125 | 'strong:nth-of-type(even)': {
126 | color: '$accent-primary',
127 | },
128 |
129 | '@bp4': {
130 | paddingBottom: '1rem',
131 | width: '28rem',
132 | },
133 | })
134 |
135 | export const FeatureCard = styled('li', {
136 | ...BoxStyles,
137 |
138 | alignItems: 'center',
139 | gap: '1rem',
140 | width: '100%',
141 | padding: '1.215rem',
142 | borderRadius: 0,
143 |
144 | '&:first-of-type': {
145 | borderTopRightRadius: 15,
146 | borderTopLeftRadius: 15,
147 | },
148 |
149 | '&:last-of-type': {
150 | borderBottomRightRadius: 15,
151 | borderBottomLeftRadius: 15,
152 | },
153 |
154 | '&:hover': {
155 | cursor: 'default',
156 | },
157 |
158 | '@bp4': {
159 | maxWidth: '30rem',
160 | },
161 | })
162 |
--------------------------------------------------------------------------------
/apps/web/src/templates/Home/Playground/styles.ts:
--------------------------------------------------------------------------------
1 | import { BoxStyles, Separator as BaseSeparator } from 'components'
2 | import { styled } from 'styles'
3 |
4 | export const Section = styled('section', {
5 | display: 'flex',
6 | flexDirection: 'column',
7 | justifyContent: 'center',
8 | gap: '1rem',
9 | height: '100%',
10 | width: '100%',
11 |
12 | pre: {
13 | ...BoxStyles,
14 |
15 | fontSize: '0.8rem',
16 | fontFamily: 'inherit',
17 | lineHeight: '1.5',
18 | textAlign: 'left',
19 | width: '100%',
20 | color: '$accent-secondary',
21 | padding: '1rem',
22 | },
23 |
24 | '@bp4': {
25 | flexDirection: 'row',
26 | },
27 | })
28 |
29 | export const SectionContent = styled('div', {
30 | display: 'flex',
31 | flexDirection: 'column',
32 | justifyContent: 'center',
33 | alignItems: 'center',
34 | gap: '1rem',
35 | width: '100%',
36 | minHeight: '100%',
37 | })
38 |
39 | export const Header = styled('header', {
40 | display: 'flex',
41 | flexDirection: 'column',
42 | alignSelf: 'center',
43 | width: '100%',
44 | })
45 |
46 | export const Title = styled('h2', {
47 | fontSize: '3rem',
48 | lineHeight: '4.25rem',
49 | color: '$accent-secondary',
50 | textAlign: 'center',
51 |
52 | '@bp4': {
53 | textAlign: 'left',
54 | },
55 | })
56 |
57 | export const Description = styled('p', {
58 | fontSize: '0.9rem',
59 | lineHeight: '1.5rem',
60 | color: '$text-support',
61 |
62 | strong: {
63 | color: '$accent-secondary',
64 | },
65 |
66 | '@bp4': {
67 | maxWidth: '37rem',
68 | },
69 | })
70 |
71 | export const Fieldset = styled('fieldset', {
72 | ...BoxStyles,
73 | flexDirection: 'column',
74 | flex: '1',
75 | alignItems: 'baseline',
76 | width: '100%',
77 | gap: '1rem',
78 | padding: '1rem',
79 | position: 'relative',
80 | })
81 |
82 | export const Legend = styled('legend', {
83 | color: '$accent-secondary',
84 | right: '1rem',
85 | position: 'absolute',
86 | })
87 |
88 | export const Label = styled('label', {
89 | display: 'flex',
90 | gap: '0.5rem',
91 | alignItems: 'center',
92 | lineHeight: '1.5rem',
93 |
94 | 'input[type="text"], input[type="number"]': {
95 | ...BoxStyles,
96 |
97 | width: '4rem',
98 | height: '2.2rem',
99 | padding: '0.5rem',
100 | borderRadius: 4,
101 | color: '$accent-secondary',
102 | fontSize: '1rem',
103 | lineHeight: '1.5rem',
104 | },
105 | })
106 |
107 | export const Footer = styled('footer', {
108 | display: 'flex',
109 | flexDirection: 'column',
110 | alignItems: 'center',
111 | justifyContent: 'space-between',
112 | width: '100%',
113 | gap: '1rem',
114 |
115 | '@bp4': {
116 | gap: 0,
117 | flexDirection: 'row',
118 | },
119 | })
120 |
121 | export const StateContainer = styled('div', {
122 | ...BoxStyles,
123 |
124 | flexFlow: 'column wrap',
125 | alignItems: 'flex-start',
126 | justifyContent: 'center',
127 | gap: '1rem',
128 | padding: '1rem',
129 | borderRadius: 4,
130 | width: '100%',
131 |
132 | span: {
133 | strong: {
134 | color: '$accent-secondary',
135 | fontWeight: 400,
136 | },
137 | },
138 |
139 | '@bp4': {
140 | flexFlow: 'row wrap',
141 | alignItems: 'center',
142 | justifyContent: 'space-between',
143 | padding: '0.5rem 1rem',
144 | maxWidth: '37.858125rem',
145 | gap: '0.5rem',
146 | },
147 | })
148 |
149 | export const ResetControls = styled('div', {
150 | display: 'flex',
151 | gap: '0.5rem',
152 |
153 | width: '100%',
154 |
155 | button: {
156 | flex: 1,
157 | },
158 |
159 | '@bp4': {
160 | width: '100%',
161 | minWidth: '30.13875rem',
162 | marginLeft: '1rem',
163 | justifyContent: 'flex-end',
164 |
165 | gap: '0.5rem',
166 |
167 | button: {
168 | flex: 'unset',
169 | },
170 | },
171 | })
172 |
173 | export const Separator = styled(BaseSeparator, {
174 | display: 'none',
175 |
176 | '@bp4': {
177 | display: 'flex',
178 | },
179 | })
180 |
--------------------------------------------------------------------------------
/apps/web/src/templates/Home/Overview/index.tsx:
--------------------------------------------------------------------------------
1 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
2 | import { useState } from 'react'
3 |
4 | import { intersection, codes } from 'shared/constants'
5 | import { useElementIntersection } from 'hooks'
6 |
7 | import {
8 | Title,
9 | Header,
10 | Section,
11 | Description,
12 | FeatureCard,
13 | CodeContainer,
14 | } from './styles'
15 |
16 | import { codeTheme } from 'styles'
17 |
18 | type CodeStateVariants = '0' | '1' | '2' | '3' | '4'
19 | type CodeExampleKeys = keyof typeof codes.overview
20 | type LIEvent = React.SyntheticEvent
21 |
22 | export function OverviewSection() {
23 | const { ref, onIntersect } = useElementIntersection(intersection.options)
24 |
25 | const [codeExampleVariant, setCodeExampleVariant] =
26 | useState('0')
27 |
28 | const [codeExample, setCodeExample] = useState('default')
29 |
30 | onIntersect('overview', ({ ref }) => {
31 | history.replaceState(null, ``, `#${ref.current.id}`)
32 | })
33 |
34 | function handleDefaultCodeExample() {
35 | setCodeExample('default')
36 | setCodeExampleVariant('0')
37 | }
38 |
39 | function handleCodeExample(event: LIEvent) {
40 | setCodeExampleVariant(
41 | (event.currentTarget.dataset.id as CodeStateVariants) || '0'
42 | )
43 | }
44 |
45 | function handleConfigurableCodeExample(event: LIEvent) {
46 | setCodeExample('config')
47 | handleCodeExample(event)
48 | }
49 |
50 | return (
51 |
52 |
53 | Simple to use
54 |
55 |
56 | The Exit Intent strategy is a great way to increase your conversion
57 | rate. That strategy is commonly used to show a modal/popup when the
58 | user is about to leave your website.
59 |
60 |
61 |
62 | useExitIntent hook is simple to use and you can
63 | customize it to your needs. Just import it and call it in your
64 | component as in the example on the right.{' '}
65 | Here are the main features:
66 |
67 |
68 |
76 | 🚀 Multiple handlers can be registred
77 |
78 |
79 |
87 | 🔥 Highly configurable
88 |
89 |
90 |
98 | 🧠 Different strategies for Desktop and Mobile
99 |
100 |
101 |
109 | ⛔️ Unsubscription support with cookies
110 |
111 |
112 | 🎉 Built with TypeScript
113 |
114 |
115 |
116 |
117 | {codes.overview[codeExample]}
118 |
119 |
120 |
121 | )
122 | }
123 |
--------------------------------------------------------------------------------
/apps/web/src/components/Modals/ExitIntent/styles.ts:
--------------------------------------------------------------------------------
1 | import * as Dialog from '@radix-ui/react-dialog'
2 |
3 | import { getPublicPath } from 'shared/utils'
4 |
5 | import { styled, animations } from 'styles'
6 |
7 | export const Overlay = styled(Dialog.Overlay, {
8 | position: 'fixed',
9 | minWidth: '100vw',
10 | minHeight: '100vh',
11 | inset: 0,
12 | background: 'rgba(0, 0, 0, 0.75)',
13 | zIndex: 100,
14 | })
15 |
16 | export const Container = styled(Dialog.Content, {
17 | display: 'flex',
18 | flexDirection: 'column',
19 | alignItems: 'center',
20 | textAlign: 'center',
21 | width: '100%',
22 | height: '100%',
23 | top: '50%',
24 | left: '50%',
25 | position: 'fixed',
26 | backdropFilter: 'blur(10px)',
27 | transform: 'translate(-50%, -50%)',
28 | overflowX: 'hidden',
29 | zIndex: 9999,
30 |
31 | '@bp4': {
32 | flexDirection: 'row',
33 | justifyContent: 'space-between',
34 | width: '90%',
35 | height: '35.1875rem',
36 | maxWidth: '59.125rem',
37 | borderRadius: 10,
38 | gap: 45.5,
39 | border: '1px solid $colors$shape-tertiary',
40 | boxShadow: '8px 2px 60px -22px $colors$shape-quinary',
41 | overflow: 'hidden',
42 | },
43 | })
44 |
45 | export const Content = styled('div', {
46 | display: 'flex',
47 | flex: 1,
48 | flexDirection: 'column',
49 | width: '100%',
50 | height: '100%',
51 |
52 | '@bp4': {
53 | display: 'flex',
54 | flexDirection: 'row',
55 | width: 'auto',
56 | },
57 | })
58 |
59 | export const ContentGroup = styled('div', {
60 | display: 'flex',
61 | flexDirection: 'column',
62 | alignItems: 'center',
63 | width: '100%',
64 |
65 | textAlign: 'center',
66 |
67 | paddingHorizontal: '1.5rem',
68 | paddingTop: 'calc(4rem + 1.911rem)',
69 | paddingBottom: '3.10375rem',
70 |
71 | gap: 16,
72 |
73 | p: {
74 | fontSize: '0.875rem',
75 | fontWeight: 400,
76 | textAlign: 'center',
77 | lineHeight: '1.4rem',
78 |
79 | color: '$text-support',
80 | },
81 |
82 | a: {
83 | marginTop: 16,
84 | },
85 |
86 | button: {
87 | fontSize: '1rem',
88 | },
89 |
90 | 'button, a': {
91 | width: '100%',
92 | },
93 |
94 | '@bp4': {
95 | flex: 1,
96 | width: '100%',
97 | alignItems: 'flex-start',
98 | textAlign: 'left',
99 | marginLeft: '1.46875rem',
100 |
101 | p: {
102 | textAlign: 'left',
103 | },
104 | },
105 | })
106 |
107 | export const Title = styled(Dialog.Title, {
108 | display: 'inline-block',
109 | maxWidth: '20.1875rem',
110 |
111 | fontSize: '1.5rem',
112 | fontWeight: 700,
113 | lineHeight: '1.875rem',
114 |
115 | color: '$text-title',
116 |
117 | span: {
118 | textTransform: 'capitalize',
119 | },
120 |
121 | img: {
122 | width: '1.5625rem',
123 | marginLeft: '0.5rem',
124 | },
125 |
126 | '@bp4': {
127 | maxWidth: '33.3125rem',
128 |
129 | fontSize: '2.5rem',
130 | fontWeight: 700,
131 | textAlign: 'left',
132 | lineHeight: '3.125rem',
133 |
134 | img: {
135 | width: '2.5rem',
136 | marginLeft: '0.5rem',
137 | },
138 | },
139 | })
140 |
141 | export const CloseButton = styled(Dialog.Close, {
142 | fontSize: '1.2rem',
143 | lineHeight: 0,
144 | color: '$text-title',
145 | background: 'transparent',
146 | transform: 'scaleX(1.4)',
147 | top: '1.5rem',
148 | right: '1.5rem',
149 | border: 0,
150 | cursor: 'pointer',
151 | position: 'absolute',
152 | transition: 'color 0.2s ease-in-out',
153 |
154 | '&:hover, &:focus-within': {
155 | color: '$accent-primary',
156 | },
157 | })
158 |
159 | export const Tag = styled('h2', {
160 | fontSize: '0.875rem',
161 | fontWeight: 700,
162 | lineHeight: '1.4rem',
163 | textTransform: 'uppercase',
164 |
165 | color: '$accent-secondary',
166 |
167 | padding: '4px 8px',
168 |
169 | borderRadius: 3,
170 |
171 | variants: {
172 | type: {
173 | outlined: {
174 | border: '1px solid $accent-secondary',
175 | },
176 | },
177 | },
178 |
179 | defaultVariants: {
180 | type: 'outlined',
181 | },
182 | })
183 |
184 | export const Image = styled('div', {
185 | display: 'flex',
186 | width: '100%',
187 | height: '3.125rem',
188 | zIndex: -1,
189 |
190 | backgroundImage: `url("${getPublicPath('/abstract-background.jpg')}")`,
191 | backgroundPosition: 'center center',
192 | animation: `${animations.backgroundCover} 40s linear infinite alternate`,
193 |
194 | '@bp4': {
195 | maxWidth: '20rem',
196 | minHeight: '100%',
197 | },
198 | })
199 |
--------------------------------------------------------------------------------
/apps/web/src/styles/lib/react-syntax-highlighter/theme.ts:
--------------------------------------------------------------------------------
1 | export const codeTheme = {
2 | 'code[class*="language-"]': {
3 | textAlign: 'left',
4 | graySpace: 'pre',
5 | wordSpacing: 'normal',
6 | wordBreak: 'normal',
7 | wordWrap: 'normal',
8 | color: '#eee',
9 | fontFamily: 'Roboto Mono, monospace',
10 | fontSize: '1em',
11 | lineHeight: '1.5em',
12 | MozTabSize: '4',
13 | OTabSize: '4',
14 | tabSize: '4',
15 | WebkitHyphens: 'none',
16 | MozHyphens: 'none',
17 | msHyphens: 'none',
18 | hyphens: 'none',
19 | },
20 |
21 | 'pre[class*="language-"]': {
22 | textAlign: 'left',
23 | graySpace: 'pre',
24 | wordSpacing: 'normal',
25 | wordBreak: 'normal',
26 | wordWrap: 'normal',
27 | color: '#eee',
28 | fontFamily: 'Roboto Mono, monospace',
29 | fontSize: '1em',
30 | lineHeight: '1.5em',
31 | MozTabSize: '4',
32 | OTabSize: '4',
33 | tabSize: '4',
34 | WebkitHyphens: 'none',
35 | MozHyphens: 'none',
36 | msHyphens: 'none',
37 | hyphens: 'none',
38 | overflow: 'auto',
39 | position: 'relative',
40 | // margin: '0.5em 0',
41 | // padding: '1.25em 1em',
42 | },
43 |
44 | 'code[class*="language-"]::-moz-selection': {
45 | background: '#363636',
46 | },
47 |
48 | 'pre[class*="language-"]::-moz-selection': {
49 | background: '#363636',
50 | },
51 |
52 | 'code[class*="language-"] ::-moz-selection': {
53 | background: '#363636',
54 | },
55 |
56 | 'pre[class*="language-"] ::-moz-selection': {
57 | background: '#363636',
58 | },
59 |
60 | 'code[class*="language-"]::selection': {
61 | background: '#363636',
62 | },
63 |
64 | 'pre[class*="language-"]::selection': {
65 | background: '#363636',
66 | },
67 |
68 | 'code[class*="language-"] ::selection': {
69 | background: '#363636',
70 | },
71 |
72 | 'pre[class*="language-"] ::selection': {
73 | background: '#363636',
74 | },
75 |
76 | ':not(pre) > code[class*="language-"]': {
77 | graySpace: 'normal',
78 | borderRadius: '0.2em',
79 | padding: '0.1em',
80 | },
81 |
82 | '.language-css > code': {
83 | color: '#FD6584',
84 | },
85 |
86 | '.language-sass > code': {
87 | color: '#FD6584',
88 | },
89 |
90 | '.language-scss > code': {
91 | color: '#FD6584',
92 | },
93 |
94 | '[class*="language-"] .namespace': {
95 | Opacity: '0.7',
96 | },
97 |
98 | atrule: {
99 | color: '#FD6584',
100 | },
101 |
102 | 'attr-name': {
103 | color: '#64ffa4',
104 | },
105 |
106 | 'attr-value': {
107 | color: '#FD6584',
108 | },
109 |
110 | attribute: {
111 | color: '#FD6584',
112 | },
113 |
114 | boolean: {
115 | color: '#FD6584',
116 | },
117 |
118 | builtin: {
119 | color: '#ffcb6b',
120 | },
121 |
122 | cdata: {
123 | color: '#646cff',
124 | },
125 |
126 | char: {
127 | color: '#646cff',
128 | },
129 |
130 | class: {
131 | color: '#ffcb6b',
132 | },
133 |
134 | 'class-name': {
135 | color: '#646cff',
136 | },
137 |
138 | comment: {
139 | color: '#616161',
140 | },
141 |
142 | constant: {
143 | color: '#FD6584',
144 | },
145 |
146 | deleted: {
147 | color: '#FD6584',
148 | },
149 |
150 | doctype: {
151 | color: '#616161',
152 | },
153 |
154 | entity: {
155 | color: '#FD6584',
156 | },
157 |
158 | function: {
159 | color: '#646cff',
160 | },
161 |
162 | hexcode: {
163 | color: '#f2ff00',
164 | },
165 |
166 | id: {
167 | color: '#FD6584',
168 | fontWeight: 'bold',
169 | },
170 |
171 | important: {
172 | color: '#FD6584',
173 | fontWeight: 'bold',
174 | },
175 |
176 | inserted: {
177 | color: '#646cff',
178 | },
179 |
180 | keyword: {
181 | color: '#FD6584',
182 | },
183 |
184 | number: {
185 | color: '#FD6584',
186 | },
187 |
188 | operator: {
189 | color: 'gray',
190 | },
191 |
192 | prolog: {
193 | color: '#616161',
194 | },
195 |
196 | property: {
197 | color: '#646cff',
198 | },
199 |
200 | 'pseudo-class': {
201 | color: '#FD6584',
202 | },
203 |
204 | 'pseudo-element': {
205 | color: '#FD6584',
206 | },
207 |
208 | punctuation: {
209 | color: 'gray',
210 | },
211 |
212 | regex: {
213 | color: '#f2ff00',
214 | },
215 |
216 | selector: {
217 | color: '#FD6584',
218 | },
219 |
220 | string: {
221 | color: '#FD6584',
222 | },
223 |
224 | symbol: {
225 | color: '#FD6584',
226 | },
227 |
228 | tag: {
229 | color: '#FD6584',
230 | },
231 |
232 | unit: {
233 | color: '#FD6584',
234 | },
235 |
236 | url: {
237 | color: '#FD6584',
238 | },
239 |
240 | variable: {
241 | color: '#FD6584',
242 | },
243 | } as { [key: string]: React.CSSProperties }
244 |
--------------------------------------------------------------------------------
/apps/web/src/templates/Docs/styles.ts:
--------------------------------------------------------------------------------
1 | import { styled } from 'styles'
2 |
3 | import { Title as _Title } from 'components/Layout/Title'
4 | import { getPublicPath } from 'shared/utils'
5 |
6 | export const Section = styled('section', {
7 | display: 'flex',
8 | flexDirection: 'row',
9 | flex: 1,
10 | height: '100%',
11 | width: '100%',
12 | paddingBottom: '1.5rem',
13 |
14 | pre: {
15 | fontFamily: 'inherit',
16 | lineHeight: '1.5',
17 | textAlign: 'left',
18 | width: '100%',
19 | color: '$accent-secondary',
20 | },
21 | })
22 |
23 | export const MenuButton = styled('button', {
24 | display: 'flex',
25 |
26 | '@bp4': {
27 | display: 'none',
28 | },
29 |
30 | variants: {
31 | active: {
32 | true: {
33 | 'svg g': {
34 | stroke: '$accent-secondary',
35 | },
36 | },
37 | },
38 | },
39 | })
40 |
41 | export const Sidebar = styled('aside', {
42 | display: 'flex',
43 | flexDirection: 'column',
44 | flex: 1,
45 | width: '100%',
46 | maxWidth: '220px',
47 | height: '-webkit-fill-available',
48 | overflowY: 'auto',
49 | padding: '1.5rem',
50 | paddingTop: '3.3rem',
51 | fontSize: '1rem',
52 | lineHeight: '1.5rem',
53 | whiteSpace: 'pre-wrap',
54 | wordBreak: 'break-word',
55 | wordWrap: 'break-word',
56 | overflowWrap: 'break-word',
57 | hyphens: 'auto',
58 | top: '4.2rem',
59 | left: 0,
60 | zIndex: 999,
61 | position: 'fixed',
62 |
63 | background: '$shape-primary',
64 | border: '1px solid $border-primary',
65 | borderRadius: 15,
66 | borderTopLeftRadius: 0,
67 | borderBottomRightRadius: 0,
68 | boxShadow: '0 2px 2px 1px $colors$shadow-primary',
69 |
70 | ul: {
71 | display: 'flex',
72 | flexDirection: 'column',
73 | gap: '0.5rem',
74 | },
75 |
76 | li: {
77 | display: 'flex',
78 | flexDirection: 'column',
79 |
80 | div: {
81 | display: 'flex',
82 | flexDirection: 'column',
83 | gap: '0.5rem',
84 |
85 | span: {
86 | textTransform: 'capitalize',
87 | fontSize: '1.2rem',
88 | fontWeight: 700,
89 | color: '$accent-secondary',
90 | },
91 |
92 | a: {
93 | transition: 'all 0.2s ease-in-out',
94 | },
95 |
96 | '&:has(span) a': {
97 | marginLeft: '1rem',
98 | width: 'fit-content',
99 | },
100 |
101 | '&:has(span + div) a': {
102 | marginLeft: 'unset',
103 | },
104 |
105 | '&:has(span + div) div': {
106 | marginBottom: '0.5rem',
107 | },
108 |
109 | 'ul li': {
110 | marginLeft: '1rem',
111 |
112 | span: {
113 | fontSize: '1rem',
114 | },
115 | },
116 | },
117 | },
118 |
119 | '@bp4': {
120 | display: 'flex',
121 | top: 0,
122 | left: 0,
123 | position: 'relative',
124 | height: 'auto',
125 | zIndex: 0,
126 |
127 | borderTopLeftRadius: 15,
128 | borderTopRightRadius: 0,
129 | borderBottomRightRadius: 0,
130 | },
131 |
132 | transition: 'all 0.2s ease-in-out',
133 |
134 | variants: {
135 | visibility: {
136 | true: {
137 | transform: 'translateX(0)',
138 | },
139 |
140 | false: {
141 | transform: 'translateX(-100%)',
142 |
143 | '@bp4': {
144 | transform: 'translateX(0)',
145 | },
146 | },
147 | },
148 | },
149 | })
150 |
151 | export const Content = styled('div', {
152 | display: 'flex',
153 | flex: 1,
154 | flexDirection: 'column',
155 | justifyContent: 'space-between',
156 | padding: '1rem 1.5rem',
157 | gap: '1rem',
158 | backgroundColor: '$shape-primary',
159 | borderRadius: 15,
160 | border: '1px solid $border-primary',
161 | boxShadow: '0 2px 2px 1px $colors$shadow-primary',
162 | width: '100%',
163 | minHeight: '100%',
164 |
165 | '@bp4': {
166 | padding: '2rem 3rem',
167 | borderTopLeftRadius: 0,
168 | borderBottomLeftRadius: 0,
169 | borderLeft: '1px solid black',
170 | },
171 | })
172 |
173 | export const Header = styled('header', {
174 | display: 'flex',
175 | flex: 1,
176 | flexDirection: 'column',
177 | alignSelf: 'center',
178 | alignItems: 'center',
179 | minHeight: '100%',
180 | justifyContent: 'space-between',
181 | gap: '1rem',
182 |
183 | div: {
184 | display: 'flex',
185 | flexDirection: 'row',
186 | gap: '1rem',
187 | alignItems: 'center',
188 | },
189 |
190 | 'div:first-of-type': {
191 | width: '100%',
192 | justifyContent: 'flex-start',
193 | },
194 |
195 | 'div:last-of-type': {
196 | width: '100%',
197 | justifyContent: 'flex-end',
198 | },
199 |
200 | a: {
201 | transition: 'filter 0.2s ease-in-out',
202 |
203 | '&:hover': {
204 | filter: 'brightness(0.7)',
205 | },
206 | },
207 |
208 | '@bp2': {
209 | flexDirection: 'row',
210 | gap: 0,
211 |
212 | div: {
213 | flexDirection: 'row',
214 | },
215 |
216 | 'div:first-of-type': {
217 | width: 'fit-content',
218 | },
219 | },
220 | })
221 |
222 | export const Title = styled(_Title, {
223 | fontSize: '1.3rem',
224 | letterSpacing: '0.1rem',
225 | })
226 |
227 | export const Article = styled('article', {
228 | display: 'flex',
229 | flexDirection: 'column',
230 |
231 | h1: {
232 | maxWidth: 'fit-content',
233 | fontSize: '1.7rem',
234 | letterSpacing: 0,
235 | transition: 'all 0.2s ease-in-out',
236 | },
237 |
238 | 'h1:first-of-type:hover': {
239 | filter: 'brightness(0.7)',
240 | },
241 |
242 | 'h1, h2, h3, h4, h5, h6': {
243 | marginVertical: '1rem',
244 | position: 'relative',
245 |
246 | '&:hover a::before': {
247 | content: '',
248 | display: 'block',
249 | width: 15,
250 | height: 15,
251 | zIndex: 1,
252 | top: '20%',
253 | right: 'calc(100% + 0.2rem)',
254 | background: `url(${getPublicPath('/link.svg')}) no-repeat center`,
255 | backgroundSize: 'contain',
256 | position: 'absolute',
257 | },
258 | },
259 |
260 | p: {
261 | fontSize: '1rem',
262 | color: '$text-support',
263 | textAlign: 'left',
264 | },
265 |
266 | ul: {},
267 |
268 | li: {
269 | marginLeft: '1rem',
270 | listStyleType: 'circle',
271 | },
272 |
273 | strong: {
274 | color: '$accent-secondary',
275 |
276 | '&:nth-of-type(even)': {
277 | color: '$accent-primary',
278 | },
279 | },
280 | })
281 |
--------------------------------------------------------------------------------
/packages/use-exit-intent/src/index.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState, useCallback, useRef } from 'react'
2 | import Cookies from 'js-cookie'
3 |
4 | export * from './types'
5 |
6 | import {
7 | contexts,
8 | isMobile,
9 | isDesktop,
10 | createDebounce,
11 | defaultSettings,
12 | createIdleEvents,
13 | removeIdleEvents,
14 | secondsToMiliseconds,
15 | processHandlersByDeviceContext,
16 | } from './utils'
17 |
18 | import {
19 | ExitIntentHandler,
20 | ExitIntentSettings,
21 | InternalExitIntentSettings,
22 | } from './types'
23 |
24 | export function useExitIntent(props: ExitIntentSettings | void = {}) {
25 | const initialSettings: InternalExitIntentSettings = {
26 | ...defaultSettings,
27 |
28 | cookie: {
29 | ...defaultSettings.cookie,
30 | ...props?.cookie,
31 | },
32 |
33 | desktop: {
34 | ...defaultSettings.desktop,
35 | ...props?.desktop,
36 | },
37 |
38 | mobile: {
39 | ...defaultSettings.mobile,
40 | ...props?.mobile,
41 | },
42 | }
43 |
44 | const [settings, setSettings] =
45 | useState(initialSettings)
46 |
47 | const [isTriggered, setIsTriggered] = useState(false)
48 | const [isUnsubscribed, setIsUnsubscribed] = useState(false)
49 |
50 | const handlers = useRef([]).current
51 | const shouldNotTrigger = useRef(false)
52 |
53 | const { mobile, desktop, cookie } = settings
54 | const willBeTriggered = !(isUnsubscribed || isTriggered)
55 |
56 | shouldNotTrigger.current = isUnsubscribed || isTriggered
57 |
58 | const handleExitIntent = useCallback(() => {
59 | if (shouldNotTrigger.current) return
60 |
61 | setIsTriggered(true)
62 |
63 | handlers
64 | .filter((handler) => {
65 | const isDefault =
66 | handler.context?.filter(
67 | (context) =>
68 | context !== contexts.onDesktop && context !== contexts.onMobile
69 | ).length === 0
70 |
71 | return isDefault || handler.context?.includes(contexts.onTrigger)
72 | })
73 | .forEach(processHandlersByDeviceContext)
74 | }, [])
75 |
76 | const unsubscribe = useCallback(() => {
77 | Cookies.set(cookie.key, 'true', {
78 | expires: cookie.daysToExpire,
79 | sameSite: 'Strict',
80 | })
81 |
82 | handlers
83 | .filter((handler) => handler.context?.includes(contexts.onUnsubscribe))
84 | .forEach(processHandlersByDeviceContext)
85 |
86 | setIsUnsubscribed(true)
87 | }, [cookie?.key])
88 |
89 | const resetState = useCallback(() => {
90 | Cookies.remove(cookie?.key, { sameSite: 'Strict' })
91 | window.onbeforeunload = null
92 |
93 | setIsTriggered(false)
94 | setIsUnsubscribed(false)
95 | }, [cookie?.key])
96 |
97 | const resetSettings = useCallback(() => {
98 | resetState()
99 | setSettings(initialSettings)
100 | }, [])
101 |
102 | const registerHandler = useCallback((handler: ExitIntentHandler) => {
103 | const handlerAlreadyPushed = handlers.find(
104 | (registeredHandler) => registeredHandler.id === handler.id
105 | )
106 |
107 | const _handler: ExitIntentHandler = {
108 | ...handler,
109 | context: handler?.context || [],
110 | }
111 |
112 | if (handlerAlreadyPushed) {
113 | handlers[handlers.indexOf(handlerAlreadyPushed)] = _handler
114 |
115 | return
116 | }
117 |
118 | handlers.push(_handler)
119 | }, [])
120 |
121 | const updateSettings = useCallback(
122 | (settings: ExitIntentSettings = defaultSettings) => {
123 | const newSettings = settings as InternalExitIntentSettings
124 |
125 | resetState()
126 |
127 | setSettings((prevSettings) => ({
128 | ...(prevSettings || {}),
129 | ...(newSettings || {}),
130 |
131 | cookie: {
132 | ...(prevSettings?.cookie || {}),
133 | ...(newSettings?.cookie || {}),
134 | },
135 |
136 | desktop: {
137 | ...(prevSettings?.desktop || {}),
138 | ...(newSettings?.desktop || {}),
139 | },
140 |
141 | mobile: {
142 | ...(prevSettings?.mobile || {}),
143 | ...(newSettings?.mobile || {}),
144 | },
145 | }))
146 | },
147 | [settings]
148 | )
149 |
150 | useEffect(() => {
151 | setIsUnsubscribed(Cookies.get(cookie.key) === 'true')
152 | }, [])
153 |
154 | useEffect(() => {
155 | if (isMobile()) {
156 | const { execute, abort } = createDebounce(
157 | handleExitIntent,
158 | secondsToMiliseconds(mobile?.delayInSecondsToTrigger!)
159 | )
160 |
161 | if (shouldNotTrigger.current) {
162 | removeIdleEvents(execute)
163 | return
164 | }
165 |
166 | if (isMobile() && mobile?.triggerOnIdle) {
167 | createIdleEvents(execute)
168 | }
169 |
170 | return () => {
171 | abort()
172 | removeIdleEvents(execute)
173 | }
174 | }
175 |
176 | if (isDesktop()) {
177 | const { execute, abort } = createDebounce(
178 | handleExitIntent,
179 | secondsToMiliseconds(desktop?.delayInSecondsToTrigger!)
180 | )
181 |
182 | let mouseLeaveTimer: ReturnType
183 |
184 | const handleMouseLeave = () => {
185 | if (shouldNotTrigger.current) return
186 | handleExitIntent()
187 | }
188 |
189 | if (desktop?.triggerOnIdle) {
190 | createIdleEvents(execute)
191 | }
192 |
193 | if (desktop?.triggerOnMouseLeave) {
194 | mouseLeaveTimer = setTimeout(() => {
195 | document.body.addEventListener('mouseleave', handleMouseLeave)
196 | }, secondsToMiliseconds(desktop.mouseLeaveDelayInSeconds!))
197 | }
198 |
199 | if (desktop?.useBeforeUnload) {
200 | window.onbeforeunload = () => {
201 | if (shouldNotTrigger.current) return
202 |
203 | handleExitIntent()
204 |
205 | return ''
206 | }
207 | }
208 |
209 | return () => {
210 | abort()
211 | clearTimeout(mouseLeaveTimer)
212 | document.body.removeEventListener('mouseleave', handleMouseLeave),
213 | removeIdleEvents(execute),
214 | (window.onbeforeunload = null)
215 | }
216 | }
217 | })
218 |
219 | return {
220 | settings,
221 | resetState,
222 | isTriggered,
223 | unsubscribe,
224 | resetSettings,
225 | updateSettings,
226 | isUnsubscribed,
227 | registerHandler,
228 | willBeTriggered,
229 | }
230 | }
231 |
--------------------------------------------------------------------------------
/apps/web/src/templates/Home/Playground/index.tsx:
--------------------------------------------------------------------------------
1 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
2 | import * as RadixDialog from '@radix-ui/react-dialog'
3 | import { useExitIntent } from 'use-exit-intent'
4 |
5 | import { useDisclosure, useElementIntersection, useMatchMedia } from 'hooks'
6 | import { BaseLayout, Button, ExitIntentModal } from 'components'
7 | import { intersection, sizes } from 'shared/constants'
8 |
9 | import {
10 | Label,
11 | Title,
12 | Legend,
13 | Header,
14 | Footer,
15 | Section,
16 | Fieldset,
17 | Separator,
18 | Description,
19 | ResetControls,
20 | SectionContent,
21 | StateContainer,
22 | } from './styles'
23 |
24 | import { codeTheme } from 'styles'
25 |
26 | export function PlaygroundSection() {
27 | const { isOpen, open, toggle } = useDisclosure(false)
28 | const { ref, onIntersect } = useElementIntersection(intersection.options)
29 |
30 | const matchesMobileWidth = useMatchMedia(
31 | `(max-width: ${sizes.breakpoints.mobile}px)`
32 | )
33 |
34 | const {
35 | settings,
36 | resetState,
37 | unsubscribe,
38 | isTriggered,
39 | resetSettings,
40 | isUnsubscribed,
41 | updateSettings,
42 | registerHandler,
43 | willBeTriggered,
44 | } = useExitIntent({
45 | cookie: {
46 | key: 'use-exit-intent',
47 | },
48 |
49 | desktop: {
50 | delayInSecondsToTrigger: 1,
51 | triggerOnIdle: false,
52 | },
53 |
54 | mobile: {
55 | delayInSecondsToTrigger: 1,
56 | triggerOnIdle: true,
57 | },
58 | })
59 |
60 | registerHandler({
61 | id: 'openModal',
62 | handler: () => {
63 | console.log('event: trigger')
64 | open()
65 | },
66 | })
67 |
68 | registerHandler({
69 | id: 'anotherHandler',
70 | handler: () => console.log('Another handler'),
71 | context: ['onDesktop'],
72 | })
73 |
74 | registerHandler({
75 | id: 'onUnsubscription',
76 | handler: () => console.log('Unsubscription handler'),
77 | context: ['onUnsubscribe', 'onMobile'],
78 | })
79 |
80 | onIntersect('playground', ({ ref }) => {
81 | history.replaceState(null, ``, `#${ref.current.id}`)
82 | })
83 |
84 | function handleJustCloseModal(state = false) {
85 | toggle(state)
86 | }
87 |
88 | function handleCloseModalUnsubscription() {
89 | toggle(false)
90 | unsubscribe()
91 | }
92 |
93 | return (
94 |
95 |
96 | Playground
97 |
98 |
99 | These are the settings available in the useExitIntent{' '}
100 | hook, you can play around with them in the{' '}
101 | {matchesMobileWidth ? 'panes below' : 'right panes'} and these changes
102 | will be reflected in the exit intent modal on this page
103 |
104 |
105 |
106 |
107 |
108 | {JSON.stringify(settings, null, 2)}
109 |
110 |
111 |
112 |
190 |
191 |
224 |
225 |
226 |
227 |
256 |
257 |
258 | handleJustCloseModal(!open)}
261 | />
262 |
263 |
264 | )
265 | }
266 |
--------------------------------------------------------------------------------
/apps/web/public/banner.svg:
--------------------------------------------------------------------------------
1 |
27 |
--------------------------------------------------------------------------------