react-email 3.0.6 → 4.0.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/cli/index.js +768 -763
- package/dist/cli/index.mjs +480 -476
- package/dist/preview/.next/BUILD_ID +1 -1
- package/dist/preview/.next/app-build-manifest.json +14 -12
- package/dist/preview/.next/build-manifest.json +5 -5
- package/dist/preview/.next/cache/.rscinfo +1 -1
- package/dist/preview/.next/cache/webpack/client-production/0.pack +0 -0
- package/dist/preview/.next/cache/webpack/client-production/index.pack +0 -0
- package/dist/preview/.next/cache/webpack/edge-server-production/index.pack +0 -0
- package/dist/preview/.next/cache/webpack/server-production/0.pack +0 -0
- package/dist/preview/.next/cache/webpack/server-production/index.pack +0 -0
- package/dist/preview/.next/next-minimal-server.js.nft.json +1 -1
- package/dist/preview/.next/next-server.js.nft.json +1 -1
- package/dist/preview/.next/prerender-manifest.json +1 -1
- package/dist/preview/.next/server/app/_not-found/page.js +1 -1
- package/dist/preview/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/preview/.next/server/app/page.js +1 -1
- package/dist/preview/.next/server/app/page.js.nft.json +1 -1
- package/dist/preview/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/preview/.next/server/app/preview/[...slug]/page.js +6 -6
- package/dist/preview/.next/server/app/preview/[...slug]/page.js.nft.json +1 -1
- package/dist/preview/.next/server/app/preview/[...slug]/page_client-reference-manifest.js +1 -1
- package/dist/preview/.next/server/chunks/273.js +1 -0
- package/dist/preview/.next/server/chunks/594.js +10 -0
- package/dist/preview/.next/server/middleware-build-manifest.js +1 -1
- package/dist/preview/.next/server/pages/500.html +1 -1
- package/dist/preview/.next/server/server-reference-manifest.js +1 -1
- package/dist/preview/.next/server/server-reference-manifest.json +1 -1
- package/dist/preview/.next/server/webpack-runtime.js +1 -1
- package/dist/preview/.next/static/chunks/18b16e15-6ad9b58e10ff8891.js +1 -0
- package/dist/preview/.next/static/chunks/490-48951f2e19ae3aef.js +1 -0
- package/dist/preview/.next/static/chunks/600-2e2ca4c8bbd97b61.js +1 -0
- package/dist/preview/.next/static/chunks/app/{layout-a2901ed1c2c53661.js → layout-490964e2c3604d33.js} +1 -1
- package/dist/preview/.next/static/chunks/app/page-d2432acd08db8fc0.js +1 -0
- package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-f4e211e00c026401.js +1 -0
- package/dist/preview/.next/static/chunks/webpack-7bf1ffb05f5540be.js +1 -0
- package/dist/preview/.next/static/css/5e0736cafbb392a9.css +3 -0
- package/dist/preview/.next/trace +21 -21
- package/package.json +12 -7
- package/postcss.config.js +1 -1
- package/src/actions/email-validation/check-links.ts +88 -0
- package/src/actions/email-validation/get-line-and-column-from-index.spec.ts +22 -0
- package/src/actions/email-validation/get-line-and-column-from-index.ts +43 -0
- package/src/actions/email-validation/quick-fetch.ts +12 -0
- package/src/actions/get-email-path-from-slug.ts +7 -4
- package/src/actions/render-email-by-path.tsx +3 -3
- package/src/animated-icons-data/help.json +1082 -0
- package/src/animated-icons-data/link.json +1309 -0
- package/src/animated-icons-data/load.json +443 -0
- package/src/animated-icons-data/mail.json +1320 -0
- package/src/app/globals.css +0 -24
- package/src/app/layout.tsx +7 -3
- package/src/app/page.tsx +9 -10
- package/src/app/preview/[...slug]/page.tsx +3 -2
- package/src/app/preview/[...slug]/preview.tsx +5 -5
- package/src/app/preview/[...slug]/rendering-error.tsx +6 -6
- package/src/components/button.tsx +8 -8
- package/src/components/code-container.tsx +7 -7
- package/src/components/code-snippet.tsx +11 -0
- package/src/components/code.tsx +4 -4
- package/src/components/heading.tsx +1 -1
- package/src/components/icons/icon-button.tsx +1 -1
- package/src/components/icons/icon-circle-check.tsx +21 -0
- package/src/components/icons/icon-circle-close.tsx +17 -0
- package/src/components/icons/icon-circle-warning.tsx +17 -0
- package/src/components/icons/icon-email.tsx +18 -0
- package/src/components/icons/icon-link.tsx +14 -0
- package/src/components/icons/icon-stamp.tsx +14 -0
- package/src/components/send.tsx +9 -9
- package/src/components/shell.tsx +32 -34
- package/src/components/sidebar/{sidebar-directory-children.tsx → file-tree-directory-children.tsx} +22 -18
- package/src/components/sidebar/{sidebar-directory.tsx → file-tree-directory.tsx} +11 -12
- package/src/components/sidebar/file-tree.tsx +31 -0
- package/src/components/sidebar/link-checker.tsx +291 -0
- package/src/components/sidebar/sidebar.tsx +296 -22
- package/src/components/text.tsx +1 -1
- package/src/components/tooltip-content.tsx +3 -3
- package/src/components/tooltip.tsx +1 -1
- package/src/components/topbar.tsx +14 -17
- package/src/hooks/use-email-rendering-result.ts +2 -2
- package/src/hooks/use-icon-animation.ts +44 -0
- package/src/utils/cn.ts +1 -1
- package/src/utils/esbuild/renderring-utilities-exporter.ts +1 -1
- package/src/utils/get-email-component.ts +6 -6
- package/src/utils/get-emails-directory-metadata.spec.ts +0 -1
- package/src/utils/improve-error-with-sourcemap.ts +1 -1
- package/src/utils/static-node-modules-for-vm.ts +6 -6
- package/tsconfig.json +2 -6
- package/.eslintrc.js +0 -52
- package/.prettierignore +0 -3
- package/.prettierrc.js +0 -8
- package/dist/preview/.next/cache/eslint/.cache_1c3sgg +0 -1
- package/dist/preview/.next/server/chunks/391.js +0 -1
- package/dist/preview/.next/server/chunks/720.js +0 -10
- package/dist/preview/.next/static/chunks/12-b9450aa0845e7574.js +0 -1
- package/dist/preview/.next/static/chunks/154-4202f86af36ccff4.js +0 -1
- package/dist/preview/.next/static/chunks/app/page-54a86772095e22e0.js +0 -1
- package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-2bfad134b65ddd79.js +0 -1
- package/dist/preview/.next/static/chunks/webpack-9255716c9496e606.js +0 -1
- package/dist/preview/.next/static/css/eb0a93282704d7ab.css +0 -3
- /package/dist/preview/.next/static/{Trk1e7GzgKOLunAXBDCy- → fZaiKz58wDr55pxLu9uHa}/_buildManifest.js +0 -0
- /package/dist/preview/.next/static/{Trk1e7GzgKOLunAXBDCy- → fZaiKz58wDr55pxLu9uHa}/_ssgManifest.js +0 -0
package/src/components/shell.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
|
+
|
|
2
3
|
import * as React from 'react';
|
|
3
4
|
import { cn } from '../utils';
|
|
4
5
|
import { Logo } from './logo';
|
|
@@ -27,14 +28,13 @@ export const Shell = ({
|
|
|
27
28
|
const [triggerTransition, setTriggerTransition] = React.useState(false);
|
|
28
29
|
|
|
29
30
|
return (
|
|
30
|
-
|
|
31
|
-
<div className="flex
|
|
32
|
-
<div className="h-[
|
|
31
|
+
<>
|
|
32
|
+
<div className="flex h-[4.375rem] items-center justify-between border-slate-6 border-b px-6 lg:hidden">
|
|
33
|
+
<div className="flex h-[4.375rem] items-center">
|
|
33
34
|
<Logo />
|
|
34
35
|
</div>
|
|
35
|
-
|
|
36
36
|
<button
|
|
37
|
-
className="h-6 w-6
|
|
37
|
+
className="flex h-6 w-6 items-center justify-center rounded text-white"
|
|
38
38
|
onClick={() => {
|
|
39
39
|
setSidebarToggled((v) => !v);
|
|
40
40
|
}}
|
|
@@ -48,6 +48,7 @@ export const Shell = ({
|
|
|
48
48
|
width="16"
|
|
49
49
|
xmlns="http://www.w3.org/2000/svg"
|
|
50
50
|
>
|
|
51
|
+
<title>Menu</title>
|
|
51
52
|
<path
|
|
52
53
|
clipRule="evenodd"
|
|
53
54
|
d="M1.5 3C1.22386 3 1 3.22386 1 3.5C1 3.77614 1.22386 4 1.5 4H13.5C13.7761 4 14 3.77614 14 3.5C14 3.22386 13.7761 3 13.5 3H1.5ZM1 7.5C1 7.22386 1.22386 7 1.5 7H13.5C13.7761 7 14 7.22386 14 7.5C14 7.77614 13.7761 8 13.5 8H1.5C1.22386 8 1 7.77614 1 7.5ZM1 11.5C1 11.2239 1.22386 11 1.5 11H13.5C13.7761 11 14 11.2239 14 11.5C14 11.7761 13.7761 12 13.5 12H1.5C1.22386 12 1 11.7761 1 11.5Z"
|
|
@@ -57,36 +58,34 @@ export const Shell = ({
|
|
|
57
58
|
</svg>
|
|
58
59
|
</button>
|
|
59
60
|
</div>
|
|
60
|
-
|
|
61
|
-
<div className="flex bg-slate-2">
|
|
61
|
+
<React.Suspense>
|
|
62
62
|
<Sidebar
|
|
63
|
-
className={cn(
|
|
64
|
-
'
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
'-translate-x-full lg:translate-x-0': !sidebarToggled,
|
|
68
|
-
},
|
|
69
|
-
)}
|
|
63
|
+
className={cn({
|
|
64
|
+
'lg:-translate-x-full translate-x-0': sidebarToggled,
|
|
65
|
+
'-translate-x-full lg:translate-x-0': !sidebarToggled,
|
|
66
|
+
})}
|
|
70
67
|
currentEmailOpenSlug={currentEmailOpenSlug}
|
|
68
|
+
markup={markup}
|
|
71
69
|
style={{
|
|
72
70
|
transition: triggerTransition ? 'transform 0.2s ease-in-out' : '',
|
|
73
71
|
}}
|
|
74
72
|
/>
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
73
|
+
</React.Suspense>
|
|
74
|
+
<main
|
|
75
|
+
className={cn(
|
|
76
|
+
'relative h-full max-h-full min-h-screen w-[100vw] overflow-hidden will-change-width sm:mt-[4.375rem] md:absolute md:right-0 lg:mt-0',
|
|
77
|
+
{
|
|
78
|
+
'lg:w-[calc(100vw)] lg:translate-x-0': sidebarToggled,
|
|
79
|
+
'lg:w-[calc(100vw-20rem)] lg:translate-x-0': !sidebarToggled,
|
|
80
|
+
},
|
|
81
|
+
)}
|
|
82
|
+
style={{
|
|
83
|
+
transition: triggerTransition
|
|
84
|
+
? 'width 0.2s ease-in-out, transform 0.2s ease-in-out'
|
|
85
|
+
: '',
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
<div className="relative h-full w-full">
|
|
90
89
|
{currentEmailOpenSlug && pathSeparator ? (
|
|
91
90
|
<Topbar
|
|
92
91
|
activeView={activeView}
|
|
@@ -107,12 +106,11 @@ export const Shell = ({
|
|
|
107
106
|
setActiveView={setActiveView}
|
|
108
107
|
/>
|
|
109
108
|
) : null}
|
|
110
|
-
|
|
111
|
-
<div className="h-[calc(100vh_-_70px)] overflow-auto mx-auto">
|
|
109
|
+
<div className="relative mx-auto h-[calc(100vh-3.3125rem)] grow md:h-full">
|
|
112
110
|
{children}
|
|
113
111
|
</div>
|
|
114
|
-
</
|
|
115
|
-
</
|
|
116
|
-
|
|
112
|
+
</div>
|
|
113
|
+
</main>
|
|
114
|
+
</>
|
|
117
115
|
);
|
|
118
116
|
};
|
package/src/components/sidebar/{sidebar-directory-children.tsx → file-tree-directory-children.tsx}
RENAMED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { AnimatePresence, LayoutGroup, motion } from 'framer-motion';
|
|
2
1
|
import * as Collapsible from '@radix-ui/react-collapsible';
|
|
2
|
+
import { AnimatePresence, LayoutGroup, motion } from 'framer-motion';
|
|
3
3
|
import Link from 'next/link';
|
|
4
4
|
import { useSearchParams } from 'next/navigation';
|
|
5
|
-
import type { EmailsDirectory } from '../../utils/get-emails-directory-metadata';
|
|
6
|
-
import { emailsDirectoryAbsolutePath } from '../../utils/emails-directory-absolute-path';
|
|
7
5
|
import { cn } from '../../utils';
|
|
6
|
+
import { emailsDirectoryAbsolutePath } from '../../utils/emails-directory-absolute-path';
|
|
7
|
+
import type { EmailsDirectory } from '../../utils/get-emails-directory-metadata';
|
|
8
8
|
import { IconFile } from '../icons/icon-file';
|
|
9
|
-
import {
|
|
9
|
+
import { FileTreeDirectory } from './file-tree-directory';
|
|
10
10
|
|
|
11
|
-
export const
|
|
11
|
+
export const FileTreeDirectoryChildren = (props: {
|
|
12
12
|
emailsDirectoryMetadata: EmailsDirectory;
|
|
13
13
|
currentEmailOpenSlug?: string;
|
|
14
14
|
open: boolean;
|
|
@@ -32,22 +32,20 @@ export const SidebarDirectoryChildren = (props: {
|
|
|
32
32
|
initial={{ opacity: 0, height: 0 }}
|
|
33
33
|
>
|
|
34
34
|
{props.isRoot ? null : (
|
|
35
|
-
<div className="line absolute left-2.5 w-px
|
|
35
|
+
<div className="line absolute left-2.5 h-full w-px bg-slate-6" />
|
|
36
36
|
)}
|
|
37
|
-
|
|
38
37
|
<div className="data-[root=true]:py-2 flex flex-col truncate">
|
|
39
38
|
<LayoutGroup id="sidebar">
|
|
40
39
|
{props.emailsDirectoryMetadata.subDirectories.map(
|
|
41
40
|
(subDirectory) => (
|
|
42
|
-
<
|
|
43
|
-
className="
|
|
41
|
+
<FileTreeDirectory
|
|
42
|
+
className="p-0 data-[state=open]:mb-2"
|
|
44
43
|
currentEmailOpenSlug={props.currentEmailOpenSlug}
|
|
45
44
|
emailsDirectoryMetadata={subDirectory}
|
|
46
45
|
key={subDirectory.absolutePath}
|
|
47
46
|
/>
|
|
48
47
|
),
|
|
49
48
|
)}
|
|
50
|
-
|
|
51
49
|
{props.emailsDirectoryMetadata.emailFilenames.map(
|
|
52
50
|
(emailFilename, index) => {
|
|
53
51
|
const emailSlug = isBaseEmailsDirectory
|
|
@@ -81,7 +79,7 @@ export const SidebarDirectoryChildren = (props: {
|
|
|
81
79
|
<motion.span
|
|
82
80
|
animate={{ x: 0, opacity: 1 }}
|
|
83
81
|
className={cn(
|
|
84
|
-
'
|
|
82
|
+
'relative flex h-8 max-w-full items-center rounded-md pl-3 align-middle text-slate-11 text-sm transition-colors duration-100 ease-[cubic-bezier(.6,.12,.34,.96)]',
|
|
85
83
|
{
|
|
86
84
|
'text-cyan-11': isCurrentPage,
|
|
87
85
|
'hover:text-slate-12':
|
|
@@ -97,19 +95,25 @@ export const SidebarDirectoryChildren = (props: {
|
|
|
97
95
|
{isCurrentPage ? (
|
|
98
96
|
<motion.span
|
|
99
97
|
animate={{ opacity: 1 }}
|
|
100
|
-
className="absolute
|
|
98
|
+
className="absolute inset-0 rounded-md bg-cyan-5 opacity-0 transition-all duration-200 ease-[cubic-bezier(.6,.12,.34,.96)]"
|
|
101
99
|
exit={{ opacity: 0 }}
|
|
102
100
|
initial={{ opacity: 0 }}
|
|
103
101
|
>
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
102
|
+
<motion.div
|
|
103
|
+
className="absolute top-1 left-[.625rem] h-6 w-px rounded-sm bg-cyan-11"
|
|
104
|
+
layoutId="active-file"
|
|
105
|
+
transition={{
|
|
106
|
+
type: 'spring',
|
|
107
|
+
bounce: 0.2,
|
|
108
|
+
duration: 0.6,
|
|
109
|
+
}}
|
|
110
|
+
/>
|
|
107
111
|
</motion.span>
|
|
108
112
|
) : null}
|
|
109
113
|
<IconFile
|
|
110
|
-
className="absolute left-4
|
|
111
|
-
height="
|
|
112
|
-
width="
|
|
114
|
+
className="absolute left-4 h-5 w-5"
|
|
115
|
+
height="20"
|
|
116
|
+
width="20"
|
|
113
117
|
/>
|
|
114
118
|
<span className="truncate pl-8">{emailFilename}</span>
|
|
115
119
|
</motion.span>
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
import * as Collapsible from '@radix-ui/react-collapsible';
|
|
3
3
|
import * as React from 'react';
|
|
4
4
|
import { cn } from '../../utils';
|
|
5
|
-
import {
|
|
5
|
+
import type { EmailsDirectory } from '../../utils/get-emails-directory-metadata';
|
|
6
6
|
import { Heading } from '../heading';
|
|
7
|
+
import { IconArrowDown } from '../icons/icon-arrow-down';
|
|
7
8
|
import { IconFolder } from '../icons/icon-folder';
|
|
8
9
|
import { IconFolderOpen } from '../icons/icon-folder-open';
|
|
9
|
-
import {
|
|
10
|
-
import { SidebarDirectoryChildren } from './sidebar-directory-children';
|
|
10
|
+
import { FileTreeDirectoryChildren } from './file-tree-directory-children';
|
|
11
11
|
|
|
12
12
|
interface SidebarDirectoryProps {
|
|
13
13
|
emailsDirectoryMetadata: EmailsDirectory;
|
|
@@ -17,7 +17,7 @@ interface SidebarDirectoryProps {
|
|
|
17
17
|
|
|
18
18
|
const persistedOpenDirectories = new Set<string>();
|
|
19
19
|
|
|
20
|
-
export const
|
|
20
|
+
export const FileTreeDirectory = ({
|
|
21
21
|
emailsDirectoryMetadata: directoryMetadata,
|
|
22
22
|
className,
|
|
23
23
|
currentEmailOpenSlug,
|
|
@@ -51,21 +51,21 @@ export const SidebarDirectory = ({
|
|
|
51
51
|
>
|
|
52
52
|
<Collapsible.Trigger
|
|
53
53
|
className={cn(
|
|
54
|
-
'
|
|
54
|
+
'mt-1 mb-1.5 flex w-full items-center justify-between gap-2 font-medium text-[14px]',
|
|
55
55
|
{
|
|
56
56
|
'cursor-pointer': !isEmpty,
|
|
57
57
|
},
|
|
58
58
|
)}
|
|
59
59
|
>
|
|
60
|
-
<div className="flex items-center text-slate-11 transition ease-in-out
|
|
60
|
+
<div className="flex items-center gap-2 text-slate-11 transition duration-200 ease-in-out hover:text-slate-12">
|
|
61
61
|
{open ? (
|
|
62
|
-
<IconFolderOpen height="
|
|
62
|
+
<IconFolderOpen height="20" width="20" />
|
|
63
63
|
) : (
|
|
64
|
-
<IconFolder height="
|
|
64
|
+
<IconFolder height="20" width="20" />
|
|
65
65
|
)}
|
|
66
66
|
<Heading
|
|
67
67
|
as="h3"
|
|
68
|
-
className="transition ease-in-out
|
|
68
|
+
className="transition duration-200 ease-in-out hover:text-slate-12"
|
|
69
69
|
color="gray"
|
|
70
70
|
size="2"
|
|
71
71
|
weight="medium"
|
|
@@ -75,14 +75,13 @@ export const SidebarDirectory = ({
|
|
|
75
75
|
</div>
|
|
76
76
|
{!isEmpty ? (
|
|
77
77
|
<IconArrowDown
|
|
78
|
-
className="data-[open=true]:rotate-180
|
|
78
|
+
className="justify-self-end opacity-60 transition-transform data-[open=true]:rotate-180"
|
|
79
79
|
data-open={open}
|
|
80
80
|
/>
|
|
81
81
|
) : null}
|
|
82
82
|
</Collapsible.Trigger>
|
|
83
|
-
|
|
84
83
|
{!isEmpty ? (
|
|
85
|
-
<
|
|
84
|
+
<FileTreeDirectoryChildren
|
|
86
85
|
currentEmailOpenSlug={currentEmailOpenSlug}
|
|
87
86
|
emailsDirectoryMetadata={directoryMetadata}
|
|
88
87
|
open={open}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as Collapsible from '@radix-ui/react-collapsible';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import type { EmailsDirectory } from '../../utils/get-emails-directory-metadata';
|
|
4
|
+
import { FileTreeDirectoryChildren } from './file-tree-directory-children';
|
|
5
|
+
|
|
6
|
+
interface FileTreeProps {
|
|
7
|
+
currentEmailOpenSlug: string | undefined;
|
|
8
|
+
emailsDirectoryMetadata: EmailsDirectory;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const FileTree = ({
|
|
12
|
+
currentEmailOpenSlug,
|
|
13
|
+
emailsDirectoryMetadata,
|
|
14
|
+
}: FileTreeProps) => {
|
|
15
|
+
return (
|
|
16
|
+
<div className="flex h-full w-full flex-col overflow-hidden lg:w-full lg:min-w-[14.5rem] lg:max-w-[14.5rem]">
|
|
17
|
+
<nav className="flex w-full flex-grow flex-col overflow-y-auto p-4 pr-0 pl-0">
|
|
18
|
+
<Collapsible.Root open>
|
|
19
|
+
<React.Suspense>
|
|
20
|
+
<FileTreeDirectoryChildren
|
|
21
|
+
currentEmailOpenSlug={currentEmailOpenSlug}
|
|
22
|
+
emailsDirectoryMetadata={emailsDirectoryMetadata}
|
|
23
|
+
isRoot
|
|
24
|
+
open
|
|
25
|
+
/>
|
|
26
|
+
</React.Suspense>
|
|
27
|
+
</Collapsible.Root>
|
|
28
|
+
</nav>
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import * as Collapsible from '@radix-ui/react-collapsible';
|
|
2
|
+
import { clsx } from 'clsx';
|
|
3
|
+
import { AnimatePresence, motion } from 'framer-motion';
|
|
4
|
+
import Lottie from 'lottie-react';
|
|
5
|
+
import * as React from 'react';
|
|
6
|
+
import {
|
|
7
|
+
type LinkCheckingResult,
|
|
8
|
+
checkLinks,
|
|
9
|
+
} from '../../actions/email-validation/check-links';
|
|
10
|
+
import animatedLoadIcon from '../../animated-icons-data/load.json';
|
|
11
|
+
import { cn } from '../../utils';
|
|
12
|
+
import { Button } from '../button';
|
|
13
|
+
|
|
14
|
+
const containerAnimation = {
|
|
15
|
+
hidden: { opacity: 0, y: 10 },
|
|
16
|
+
visible: {
|
|
17
|
+
opacity: 1,
|
|
18
|
+
y: 0,
|
|
19
|
+
transition: { duration: 0.6, ease: 'easeOut', staggerChildren: 0.1 },
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const childAnimation = {
|
|
24
|
+
hidden: { opacity: 0, y: 5 },
|
|
25
|
+
visible: {
|
|
26
|
+
opacity: 1,
|
|
27
|
+
y: 0,
|
|
28
|
+
transition: { duration: 0.4, ease: 'easeOut' },
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const statusStyles = {
|
|
33
|
+
error: 'text-red-600 hover:bg-red-600/10',
|
|
34
|
+
warning: 'text-yellow-300 hover:bg-yellow-400/10',
|
|
35
|
+
success: 'text-green-600 hover:bg-green-600/10',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
interface LinkCheckerProps {
|
|
39
|
+
emailSlug: string;
|
|
40
|
+
emailMarkup: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type ResultStatus = 'error' | 'warning' | 'success';
|
|
44
|
+
|
|
45
|
+
interface CollapsibleTriggerProps {
|
|
46
|
+
count: number;
|
|
47
|
+
label: string;
|
|
48
|
+
variant: ResultStatus;
|
|
49
|
+
disabled?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface ResultSectionProps {
|
|
53
|
+
status: ResultStatus;
|
|
54
|
+
label: string;
|
|
55
|
+
results: LinkCheckingResult[];
|
|
56
|
+
open: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const CollapsibleTrigger = ({
|
|
60
|
+
count,
|
|
61
|
+
label,
|
|
62
|
+
variant,
|
|
63
|
+
disabled,
|
|
64
|
+
}: CollapsibleTriggerProps) => (
|
|
65
|
+
<Collapsible.Trigger
|
|
66
|
+
className={clsx(
|
|
67
|
+
'group flex w-full items-center gap-1 rounded p-2 transition-colors duration-200 ease-[cubic-bezier(.36,.66,.6,1)]',
|
|
68
|
+
statusStyles[variant],
|
|
69
|
+
disabled && 'cursor-not-allowed opacity-70',
|
|
70
|
+
)}
|
|
71
|
+
disabled={disabled}
|
|
72
|
+
>
|
|
73
|
+
<span
|
|
74
|
+
className={clsx(
|
|
75
|
+
'-mt-[.125rem] transition-transform duration-200 ease-[cubic-bezier(.36,.66,.6,1)]',
|
|
76
|
+
'rotate-0 group-data-[state=open]:rotate-90',
|
|
77
|
+
)}
|
|
78
|
+
>
|
|
79
|
+
<svg
|
|
80
|
+
fill="none"
|
|
81
|
+
height="15"
|
|
82
|
+
viewBox="0 0 15 15"
|
|
83
|
+
width="15"
|
|
84
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
85
|
+
>
|
|
86
|
+
<path
|
|
87
|
+
clipRule="evenodd"
|
|
88
|
+
d="M6.1584 3.13508C6.35985 2.94621 6.67627 2.95642 6.86514 3.15788L10.6151 7.15788C10.7954 7.3502 10.7954 7.64949 10.6151 7.84182L6.86514 11.8418C6.67627 12.0433 6.35985 12.0535 6.1584 11.8646C5.95694 11.6757 5.94673 11.3593 6.1356 11.1579L9.565 7.49985L6.1356 3.84182C5.94673 3.64036 5.95694 3.32394 6.1584 3.13508Z"
|
|
89
|
+
fill="currentColor"
|
|
90
|
+
fillRule="evenodd"
|
|
91
|
+
/>
|
|
92
|
+
</svg>
|
|
93
|
+
</span>
|
|
94
|
+
<div className="flex flex-1 items-center gap-1 font-bold text-[.625rem] uppercase tracking-wide">
|
|
95
|
+
<span>{label}</span>
|
|
96
|
+
<span>({count})</span>
|
|
97
|
+
</div>
|
|
98
|
+
</Collapsible.Trigger>
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const ResultSection = ({
|
|
102
|
+
status,
|
|
103
|
+
label,
|
|
104
|
+
results,
|
|
105
|
+
open,
|
|
106
|
+
}: ResultSectionProps) => {
|
|
107
|
+
const isEmpty = results.length === 0;
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<Collapsible.Root className="group" defaultOpen={open && !isEmpty}>
|
|
111
|
+
<CollapsibleTrigger
|
|
112
|
+
count={results.length}
|
|
113
|
+
label={label}
|
|
114
|
+
variant={status}
|
|
115
|
+
disabled={isEmpty}
|
|
116
|
+
/>
|
|
117
|
+
{!isEmpty && (
|
|
118
|
+
<Collapsible.Content>
|
|
119
|
+
<ol className="mt-2 mb-1 flex list-none flex-col gap-4">
|
|
120
|
+
{results.map((result, index) => (
|
|
121
|
+
<LinkResultView key={index} {...result} />
|
|
122
|
+
))}
|
|
123
|
+
</ol>
|
|
124
|
+
</Collapsible.Content>
|
|
125
|
+
)}
|
|
126
|
+
</Collapsible.Root>
|
|
127
|
+
);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const LoadingButton = ({
|
|
131
|
+
loading,
|
|
132
|
+
onClick,
|
|
133
|
+
loadAnimation,
|
|
134
|
+
animatedLoadIcon,
|
|
135
|
+
children,
|
|
136
|
+
}: any) => {
|
|
137
|
+
React.useEffect(() => {
|
|
138
|
+
if (loading) {
|
|
139
|
+
loadAnimation.current?.play();
|
|
140
|
+
}
|
|
141
|
+
}, [loading, loadAnimation]);
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<Button
|
|
145
|
+
className="mt-2 mb-4 min-w-[5rem] transition-all disabled:border-transparent disabled:bg-slate-11"
|
|
146
|
+
disabled={loading}
|
|
147
|
+
onClick={onClick}
|
|
148
|
+
>
|
|
149
|
+
<div className="flex items-center justify-center gap-2">
|
|
150
|
+
<span
|
|
151
|
+
className={cn(
|
|
152
|
+
'-ml-7 opacity-0 transition-opacity duration-200',
|
|
153
|
+
loading && 'opacity-100',
|
|
154
|
+
)}
|
|
155
|
+
>
|
|
156
|
+
<Lottie
|
|
157
|
+
animationData={animatedLoadIcon}
|
|
158
|
+
autoPlay={false}
|
|
159
|
+
className="h-5 w-5"
|
|
160
|
+
loop={true}
|
|
161
|
+
lottieRef={loadAnimation}
|
|
162
|
+
/>
|
|
163
|
+
</span>
|
|
164
|
+
<span>{children}</span>
|
|
165
|
+
</div>
|
|
166
|
+
</Button>
|
|
167
|
+
);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const LinkResultView = (props: LinkCheckingResult) => (
|
|
171
|
+
<AnimatePresence mode="wait">
|
|
172
|
+
<motion.li
|
|
173
|
+
animate="visible"
|
|
174
|
+
className="group/item relative w-full rounded-md p-2 pl-4 transition-colors duration-300 ease-out hover:bg-slate-5"
|
|
175
|
+
data-status={props.status}
|
|
176
|
+
initial="hidden"
|
|
177
|
+
layout
|
|
178
|
+
variants={containerAnimation}
|
|
179
|
+
>
|
|
180
|
+
<a
|
|
181
|
+
href={props.link}
|
|
182
|
+
target="_blank"
|
|
183
|
+
rel="noopener noreferrer"
|
|
184
|
+
className="w-full"
|
|
185
|
+
>
|
|
186
|
+
<motion.div
|
|
187
|
+
className="flex w-full items-center gap-2 text-xs group-data-[status=error]/item:text-red-400 group-data-[status=success]/item:text-green-400 group-data-[status=warning]/item:text-yellow-300"
|
|
188
|
+
variants={childAnimation}
|
|
189
|
+
>
|
|
190
|
+
<span className="block overflow-hidden truncate text-ellipsis whitespace-nowrap">
|
|
191
|
+
{props.link}
|
|
192
|
+
</span>
|
|
193
|
+
</motion.div>
|
|
194
|
+
<motion.div
|
|
195
|
+
className="mt-1 font-semibold text-[.625rem] uppercase"
|
|
196
|
+
variants={childAnimation}
|
|
197
|
+
>
|
|
198
|
+
{props.checks
|
|
199
|
+
.map((check) => {
|
|
200
|
+
if (check.type === 'syntax' && !check.passed)
|
|
201
|
+
return 'Invalid URL';
|
|
202
|
+
if (check.type === 'fetch_attempt')
|
|
203
|
+
return `${check.metadata.fetchStatusCode}`;
|
|
204
|
+
if (check.type === 'security')
|
|
205
|
+
return check.passed ? 'Secure' : 'Insecure';
|
|
206
|
+
return null;
|
|
207
|
+
})
|
|
208
|
+
.filter(Boolean)
|
|
209
|
+
.join(' - ')}
|
|
210
|
+
</motion.div>
|
|
211
|
+
</a>
|
|
212
|
+
</motion.li>
|
|
213
|
+
</AnimatePresence>
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
export const LinkChecker = ({ emailSlug, emailMarkup }: LinkCheckerProps) => {
|
|
217
|
+
const cacheKey = `link-checking-results-${emailSlug.replaceAll('/', '-')}`;
|
|
218
|
+
const cachedResults =
|
|
219
|
+
'localStorage' in window ? window.localStorage.getItem(cacheKey) : null;
|
|
220
|
+
|
|
221
|
+
const [results, setResults] = React.useState<
|
|
222
|
+
LinkCheckingResult[] | undefined
|
|
223
|
+
>(cachedResults ? JSON.parse(cachedResults) : undefined);
|
|
224
|
+
const [sectionsOpen, setSectionsOpen] = React.useState(false);
|
|
225
|
+
const loadAnimation = React.useRef(null);
|
|
226
|
+
const [loading, setLoading] = React.useState(false);
|
|
227
|
+
|
|
228
|
+
const errorResults = React.useMemo(
|
|
229
|
+
() => results?.filter((r) => r.status === 'error') || [],
|
|
230
|
+
[results],
|
|
231
|
+
);
|
|
232
|
+
const warningResults = React.useMemo(
|
|
233
|
+
() => results?.filter((r) => r.status === 'warning') || [],
|
|
234
|
+
[results],
|
|
235
|
+
);
|
|
236
|
+
const successResults = React.useMemo(
|
|
237
|
+
() => results?.filter((r) => r.status === 'success') || [],
|
|
238
|
+
[results],
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
const handleRun = () => {
|
|
242
|
+
setLoading(true);
|
|
243
|
+
checkLinks(emailMarkup)
|
|
244
|
+
.then((newResults) => {
|
|
245
|
+
setResults(newResults);
|
|
246
|
+
setSectionsOpen(true);
|
|
247
|
+
localStorage.setItem(cacheKey, JSON.stringify(newResults));
|
|
248
|
+
})
|
|
249
|
+
.catch(console.error)
|
|
250
|
+
.finally(() => setLoading(false));
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
return (
|
|
254
|
+
<div className="mt-4 flex w-full flex-col gap-2 text-pretty">
|
|
255
|
+
{results ? (
|
|
256
|
+
<>
|
|
257
|
+
<ResultSection
|
|
258
|
+
label="Errors"
|
|
259
|
+
results={errorResults}
|
|
260
|
+
status="error"
|
|
261
|
+
open={sectionsOpen}
|
|
262
|
+
/>
|
|
263
|
+
<ResultSection
|
|
264
|
+
label="Warnings"
|
|
265
|
+
results={warningResults}
|
|
266
|
+
status="warning"
|
|
267
|
+
open={sectionsOpen}
|
|
268
|
+
/>
|
|
269
|
+
<ResultSection
|
|
270
|
+
label="Success"
|
|
271
|
+
results={successResults}
|
|
272
|
+
status="success"
|
|
273
|
+
open={sectionsOpen}
|
|
274
|
+
/>
|
|
275
|
+
</>
|
|
276
|
+
) : (
|
|
277
|
+
<span className="text-xs leading-relaxed">
|
|
278
|
+
Check if all links are valid and redirect to the correct pages.
|
|
279
|
+
</span>
|
|
280
|
+
)}
|
|
281
|
+
<LoadingButton
|
|
282
|
+
loading={loading}
|
|
283
|
+
onClick={handleRun}
|
|
284
|
+
loadAnimation={loadAnimation}
|
|
285
|
+
animatedLoadIcon={animatedLoadIcon}
|
|
286
|
+
>
|
|
287
|
+
{results ? 'Re-run' : 'Run'}
|
|
288
|
+
</LoadingButton>
|
|
289
|
+
</div>
|
|
290
|
+
);
|
|
291
|
+
};
|