react-email 3.0.7 → 4.0.0-alpha.1
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 +17 -0
- package/dist/cli/index.js +10 -4
- package/dist/cli/index.mjs +9 -3
- 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 +3 -3
- 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/required-server-files.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/favicon.ico/route.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 +5 -5
- 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/196.js +1 -1
- package/dist/preview/.next/server/chunks/282.js +15 -0
- package/dist/preview/.next/server/chunks/667.js +1 -0
- package/dist/preview/.next/server/middleware-build-manifest.js +1 -1
- package/dist/preview/.next/server/next-font-manifest.js +1 -1
- package/dist/preview/.next/server/next-font-manifest.json +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/207-7ab46c2d84f60fed.js +1 -0
- package/dist/preview/.next/static/chunks/490-9a10c001ec2dffb2.js +1 -0
- package/dist/preview/.next/static/chunks/afa401a5-9ebf2515b1397993.js +6 -0
- package/dist/preview/.next/static/chunks/app/layout-f1bad3fcfbc7eb6b.js +1 -0
- package/dist/preview/.next/static/chunks/app/page-800163ba6c6d943d.js +1 -0
- package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-5b5c4557fc89db64.js +1 -0
- package/dist/preview/.next/static/chunks/{main-app-771a0fc4ad5aa154.js → main-app-d1b0aa870bcfb13e.js} +1 -1
- package/dist/preview/.next/static/css/d6c4def4cc3fb858.css +3 -0
- package/dist/preview/.next/trace +22 -21
- package/dist/preview/.next/types/app/layout.ts +1 -1
- package/dist/preview/.next/types/app/preview/[...slug]/page.ts +1 -1
- package/package.json +8 -2
- package/src/actions/email-validation/check-images.spec.tsx +90 -0
- package/src/actions/email-validation/check-images.ts +142 -0
- package/src/actions/email-validation/check-links.spec.tsx +92 -0
- package/src/actions/email-validation/check-links.ts +91 -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/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 +6 -2
- package/src/app/page.tsx +8 -9
- package/src/app/preview/[...slug]/page.tsx +1 -0
- package/src/app/preview/[...slug]/preview.tsx +3 -3
- package/src/app/preview/[...slug]/rendering-error.tsx +6 -6
- package/src/components/button.tsx +53 -42
- package/src/components/code-container.tsx +6 -6
- package/src/components/code-snippet.tsx +11 -0
- package/src/components/code.tsx +4 -4
- 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-image.tsx +19 -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/checking-results.tsx +150 -0
- package/src/components/sidebar/{sidebar-directory-children.tsx → file-tree-directory-children.tsx} +19 -15
- package/src/components/sidebar/{sidebar-directory.tsx → file-tree-directory.tsx} +9 -10
- package/src/components/sidebar/file-tree.tsx +31 -0
- package/src/components/sidebar/image-checker.tsx +161 -0
- package/src/components/sidebar/link-checker.tsx +151 -0
- package/src/components/sidebar/sidebar.tsx +344 -22
- package/src/components/tooltip-content.tsx +2 -2
- package/src/components/topbar.tsx +13 -16
- package/src/hooks/use-icon-animation.ts +41 -0
- package/tsconfig.json +1 -0
- package/dist/preview/.next/server/chunks/693.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-f7f86c8589140c56.js +0 -1
- package/dist/preview/.next/static/chunks/app/layout-6d33e2ffcffd58d4.js +0 -1
- package/dist/preview/.next/static/chunks/app/page-43a07e4b8c5c0840.js +0 -1
- package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-71202390d5f9a34b.js +0 -1
- package/dist/preview/.next/static/css/a34876a6c565fff8.css +0 -3
- /package/dist/preview/.next/static/{RZga3-2qKYa2RLg-hxunV → Mn2FuRztLqr32yO8CKHi9}/_buildManifest.js +0 -0
- /package/dist/preview/.next/static/{RZga3-2qKYa2RLg-hxunV → Mn2FuRztLqr32yO8CKHi9}/_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
|
};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import * as Collapsible from '@radix-ui/react-collapsible';
|
|
2
|
+
import { AnimatePresence, motion } from 'framer-motion';
|
|
3
|
+
import type { ComponentProps } from 'react';
|
|
4
|
+
import { cn } from '../../utils';
|
|
5
|
+
|
|
6
|
+
export type ResultStatus = 'error' | 'warning' | 'success';
|
|
7
|
+
|
|
8
|
+
const statusStyles = {
|
|
9
|
+
error: 'text-red-600 hover:bg-red-600/10',
|
|
10
|
+
warning: 'text-yellow-300 hover:bg-yellow-400/10',
|
|
11
|
+
success: 'text-green-600 hover:bg-green-600/10',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
interface ResultListProps {
|
|
15
|
+
status: ResultStatus;
|
|
16
|
+
label: React.ReactNode;
|
|
17
|
+
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
defaultOpen?: boolean;
|
|
20
|
+
|
|
21
|
+
children: React.ReactNode;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const ResultList = ({
|
|
25
|
+
status,
|
|
26
|
+
label,
|
|
27
|
+
|
|
28
|
+
disabled,
|
|
29
|
+
defaultOpen,
|
|
30
|
+
|
|
31
|
+
children,
|
|
32
|
+
}: ResultListProps) => {
|
|
33
|
+
return (
|
|
34
|
+
<Collapsible.Root className="group" defaultOpen={defaultOpen && !disabled}>
|
|
35
|
+
<Collapsible.Trigger
|
|
36
|
+
className={cn(
|
|
37
|
+
'group flex w-full items-center gap-1 rounded p-2 transition-colors duration-200 ease-[cubic-bezier(.36,.66,.6,1)]',
|
|
38
|
+
statusStyles[status],
|
|
39
|
+
disabled && 'cursor-not-allowed opacity-70',
|
|
40
|
+
)}
|
|
41
|
+
disabled={disabled}
|
|
42
|
+
>
|
|
43
|
+
<span
|
|
44
|
+
className={cn(
|
|
45
|
+
'-mt-[.125rem] transition-transform duration-200 ease-[cubic-bezier(.36,.66,.6,1)]',
|
|
46
|
+
'rotate-0 group-data-[state=open]:rotate-90',
|
|
47
|
+
)}
|
|
48
|
+
>
|
|
49
|
+
<svg
|
|
50
|
+
fill="none"
|
|
51
|
+
height="15"
|
|
52
|
+
viewBox="0 0 15 15"
|
|
53
|
+
width="15"
|
|
54
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
55
|
+
>
|
|
56
|
+
<path
|
|
57
|
+
clipRule="evenodd"
|
|
58
|
+
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"
|
|
59
|
+
fill="currentColor"
|
|
60
|
+
fillRule="evenodd"
|
|
61
|
+
/>
|
|
62
|
+
</svg>
|
|
63
|
+
</span>
|
|
64
|
+
<div className="flex flex-1 items-center gap-1 font-bold text-[.625rem] uppercase tracking-wide">
|
|
65
|
+
{label}
|
|
66
|
+
</div>
|
|
67
|
+
</Collapsible.Trigger>
|
|
68
|
+
{children ? (
|
|
69
|
+
<Collapsible.Content>
|
|
70
|
+
<ol className="mt-2 mb-1 flex list-none flex-col gap-4">
|
|
71
|
+
{children}
|
|
72
|
+
</ol>
|
|
73
|
+
</Collapsible.Content>
|
|
74
|
+
) : null}
|
|
75
|
+
</Collapsible.Root>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
type ResultProps = {
|
|
80
|
+
status: ResultStatus;
|
|
81
|
+
} & ComponentProps<typeof motion.li>;
|
|
82
|
+
|
|
83
|
+
const resultAnimation = {
|
|
84
|
+
hidden: { opacity: 0, y: 10 },
|
|
85
|
+
visible: {
|
|
86
|
+
opacity: 1,
|
|
87
|
+
y: 0,
|
|
88
|
+
transition: { duration: 0.6, ease: 'easeOut', staggerChildren: 0.1 },
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const Result = ({ children, status, ...rest }: ResultProps) => {
|
|
93
|
+
return (
|
|
94
|
+
<AnimatePresence mode="wait">
|
|
95
|
+
<motion.li
|
|
96
|
+
data-status={status}
|
|
97
|
+
initial="hidden"
|
|
98
|
+
layout
|
|
99
|
+
variants={resultAnimation}
|
|
100
|
+
animate="visible"
|
|
101
|
+
{...rest}
|
|
102
|
+
className={cn(
|
|
103
|
+
'group/item relative w-full rounded-md p-2 pl-4 transition-colors duration-300 ease-out hover:bg-slate-5',
|
|
104
|
+
rest.className,
|
|
105
|
+
)}
|
|
106
|
+
>
|
|
107
|
+
{children}
|
|
108
|
+
</motion.li>
|
|
109
|
+
</AnimatePresence>
|
|
110
|
+
);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const titleStatusAnimation = {
|
|
114
|
+
hidden: { opacity: 0, y: 5 },
|
|
115
|
+
visible: {
|
|
116
|
+
opacity: 1,
|
|
117
|
+
y: 0,
|
|
118
|
+
transition: { duration: 0.4, ease: 'easeOut' },
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
interface ResultStatusDescriptionProps {
|
|
123
|
+
children: React.ReactNode;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
Result.StatusDescription = ({ children }: ResultStatusDescriptionProps) => {
|
|
127
|
+
return (
|
|
128
|
+
<motion.div
|
|
129
|
+
className="mt-1 font-semibold text-[.625rem] uppercase"
|
|
130
|
+
variants={titleStatusAnimation}
|
|
131
|
+
>
|
|
132
|
+
{children}
|
|
133
|
+
</motion.div>
|
|
134
|
+
);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
interface ResultTitleProps {
|
|
138
|
+
children: React.ReactNode;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
Result.Title = ({ children }: ResultTitleProps) => {
|
|
142
|
+
return (
|
|
143
|
+
<motion.div
|
|
144
|
+
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 "
|
|
145
|
+
variants={titleStatusAnimation}
|
|
146
|
+
>
|
|
147
|
+
{children}
|
|
148
|
+
</motion.div>
|
|
149
|
+
);
|
|
150
|
+
};
|
package/src/components/sidebar/{sidebar-directory-children.tsx → file-tree-directory-children.tsx}
RENAMED
|
@@ -5,9 +5,9 @@ import { useSearchParams } from 'next/navigation';
|
|
|
5
5
|
import { cn } from '../../utils';
|
|
6
6
|
import type { EmailsDirectory } from '../../utils/get-emails-directory-metadata';
|
|
7
7
|
import { IconFile } from '../icons/icon-file';
|
|
8
|
-
import {
|
|
8
|
+
import { FileTreeDirectory } from './file-tree-directory';
|
|
9
9
|
|
|
10
|
-
export const
|
|
10
|
+
export const FileTreeDirectoryChildren = (props: {
|
|
11
11
|
emailsDirectoryMetadata: EmailsDirectory;
|
|
12
12
|
currentEmailOpenSlug?: string;
|
|
13
13
|
open: boolean;
|
|
@@ -29,22 +29,20 @@ export const SidebarDirectoryChildren = (props: {
|
|
|
29
29
|
initial={{ opacity: 0, height: 0 }}
|
|
30
30
|
>
|
|
31
31
|
{props.isRoot ? null : (
|
|
32
|
-
<div className="line absolute left-2.5 w-px
|
|
32
|
+
<div className="line absolute left-2.5 h-full w-px bg-slate-6" />
|
|
33
33
|
)}
|
|
34
|
-
|
|
35
34
|
<div className="flex flex-col truncate">
|
|
36
35
|
<LayoutGroup id="sidebar">
|
|
37
36
|
{props.emailsDirectoryMetadata.subDirectories.map(
|
|
38
37
|
(subDirectory) => (
|
|
39
|
-
<
|
|
40
|
-
className="
|
|
38
|
+
<FileTreeDirectory
|
|
39
|
+
className="p-0 data-[state=open]:mb-2"
|
|
41
40
|
currentEmailOpenSlug={props.currentEmailOpenSlug}
|
|
42
41
|
emailsDirectoryMetadata={subDirectory}
|
|
43
42
|
key={subDirectory.absolutePath}
|
|
44
43
|
/>
|
|
45
44
|
),
|
|
46
45
|
)}
|
|
47
|
-
|
|
48
46
|
{props.emailsDirectoryMetadata.emailFilenames.map(
|
|
49
47
|
(emailFilename, index) => {
|
|
50
48
|
const emailSlug = props.isRoot
|
|
@@ -78,7 +76,7 @@ export const SidebarDirectoryChildren = (props: {
|
|
|
78
76
|
<motion.span
|
|
79
77
|
animate={{ x: 0, opacity: 1 }}
|
|
80
78
|
className={cn(
|
|
81
|
-
'
|
|
79
|
+
'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)]',
|
|
82
80
|
{
|
|
83
81
|
'text-cyan-11': isCurrentPage,
|
|
84
82
|
'hover:text-slate-12':
|
|
@@ -94,19 +92,25 @@ export const SidebarDirectoryChildren = (props: {
|
|
|
94
92
|
{isCurrentPage ? (
|
|
95
93
|
<motion.span
|
|
96
94
|
animate={{ opacity: 1 }}
|
|
97
|
-
className="absolute
|
|
95
|
+
className="absolute inset-0 rounded-md bg-cyan-5 opacity-0 transition-all duration-200 ease-[cubic-bezier(.6,.12,.34,.96)]"
|
|
98
96
|
exit={{ opacity: 0 }}
|
|
99
97
|
initial={{ opacity: 0 }}
|
|
100
98
|
>
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
99
|
+
<motion.div
|
|
100
|
+
className="absolute top-1 left-[.625rem] h-6 w-px rounded-sm bg-cyan-11"
|
|
101
|
+
layoutId="active-file"
|
|
102
|
+
transition={{
|
|
103
|
+
type: 'spring',
|
|
104
|
+
bounce: 0.2,
|
|
105
|
+
duration: 0.6,
|
|
106
|
+
}}
|
|
107
|
+
/>
|
|
104
108
|
</motion.span>
|
|
105
109
|
) : null}
|
|
106
110
|
<IconFile
|
|
107
|
-
className="absolute left-4
|
|
108
|
-
height="
|
|
109
|
-
width="
|
|
111
|
+
className="absolute left-4 h-5 w-5"
|
|
112
|
+
height="20"
|
|
113
|
+
width="20"
|
|
110
114
|
/>
|
|
111
115
|
<span className="truncate pl-8">{emailFilename}</span>
|
|
112
116
|
</motion.span>
|
|
@@ -7,7 +7,7 @@ import { Heading } from '../heading';
|
|
|
7
7
|
import { IconArrowDown } from '../icons/icon-arrow-down';
|
|
8
8
|
import { IconFolder } from '../icons/icon-folder';
|
|
9
9
|
import { IconFolderOpen } from '../icons/icon-folder-open';
|
|
10
|
-
import {
|
|
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,161 @@
|
|
|
1
|
+
import prettyBytes from 'pretty-bytes';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import {
|
|
4
|
+
type ImageCheckingResult,
|
|
5
|
+
checkImages,
|
|
6
|
+
} from '../../actions/email-validation/check-images';
|
|
7
|
+
import { Button } from '../button';
|
|
8
|
+
import { Result, ResultList, type ResultStatus } from './checking-results';
|
|
9
|
+
|
|
10
|
+
interface ImageCheckerResultsProps {
|
|
11
|
+
label: string;
|
|
12
|
+
status: ResultStatus;
|
|
13
|
+
results: ImageCheckingResult[];
|
|
14
|
+
|
|
15
|
+
justLoadedIn: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const ImageCheckerResults = ({
|
|
19
|
+
label,
|
|
20
|
+
status,
|
|
21
|
+
results,
|
|
22
|
+
|
|
23
|
+
justLoadedIn,
|
|
24
|
+
}: ImageCheckerResultsProps) => {
|
|
25
|
+
return (
|
|
26
|
+
<ResultList
|
|
27
|
+
label={
|
|
28
|
+
<>
|
|
29
|
+
<span>{label}</span>
|
|
30
|
+
<span>({results.length})</span>
|
|
31
|
+
</>
|
|
32
|
+
}
|
|
33
|
+
defaultOpen={justLoadedIn}
|
|
34
|
+
status={status}
|
|
35
|
+
disabled={results.length === 0}
|
|
36
|
+
>
|
|
37
|
+
{results.map(({ source, status, checks }) => (
|
|
38
|
+
<Result className="flex gap-2" key={source} status={status}>
|
|
39
|
+
<img
|
|
40
|
+
width="24px"
|
|
41
|
+
className="my-auto rounded-sm"
|
|
42
|
+
src={source}
|
|
43
|
+
// biome-ignore lint/a11y/noRedundantAlt: The word image does fit in with the context and thus is not redundant
|
|
44
|
+
alt="image checked"
|
|
45
|
+
/>
|
|
46
|
+
<div className="flex w-[calc(100%-.5rem-24px)] flex-col">
|
|
47
|
+
<Result.Title>
|
|
48
|
+
<span className="block overflow-hidden truncate text-ellipsis whitespace-nowrap">
|
|
49
|
+
{source}
|
|
50
|
+
</span>
|
|
51
|
+
</Result.Title>
|
|
52
|
+
<Result.StatusDescription>
|
|
53
|
+
{checks
|
|
54
|
+
.map((check) => {
|
|
55
|
+
if (check.type === 'syntax' && !check.passed)
|
|
56
|
+
return 'Invalid URL';
|
|
57
|
+
if (check.type === 'accessibility' && !check.passed)
|
|
58
|
+
return 'Missing alt';
|
|
59
|
+
if (check.type === 'security')
|
|
60
|
+
return check.passed ? 'Secure' : 'Insecure';
|
|
61
|
+
if (
|
|
62
|
+
check.type === 'fetch_attempt' &&
|
|
63
|
+
check.metadata.fetchStatusCode
|
|
64
|
+
)
|
|
65
|
+
return `${check.metadata.fetchStatusCode}`;
|
|
66
|
+
if (check.type === 'image_size' && check.metadata.byteCount)
|
|
67
|
+
return `${prettyBytes(check.metadata.byteCount)}`;
|
|
68
|
+
return null;
|
|
69
|
+
})
|
|
70
|
+
.filter(Boolean)
|
|
71
|
+
.join(' - ')}
|
|
72
|
+
</Result.StatusDescription>
|
|
73
|
+
</div>
|
|
74
|
+
</Result>
|
|
75
|
+
))}
|
|
76
|
+
</ResultList>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
interface ImageCheckerProps {
|
|
81
|
+
emailSlug: string;
|
|
82
|
+
emailMarkup: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const ImageChecker = ({ emailSlug, emailMarkup }: ImageCheckerProps) => {
|
|
86
|
+
const cacheKey = `image-checking-results-${emailSlug.replaceAll('/', '-')}`;
|
|
87
|
+
|
|
88
|
+
const [results, setResults] = React.useState<
|
|
89
|
+
ImageCheckingResult[] | undefined
|
|
90
|
+
>();
|
|
91
|
+
|
|
92
|
+
React.useEffect(() => {
|
|
93
|
+
const cachedValue =
|
|
94
|
+
'localStorage' in global ? global.localStorage.getItem(cacheKey) : null;
|
|
95
|
+
if (cachedValue) {
|
|
96
|
+
setResults(JSON.parse(cachedValue));
|
|
97
|
+
}
|
|
98
|
+
}, [cacheKey]);
|
|
99
|
+
|
|
100
|
+
const [justLoadedIn, setJustLoadedIn] = React.useState(false);
|
|
101
|
+
const [loading, setLoading] = React.useState(false);
|
|
102
|
+
|
|
103
|
+
const handleRun = () => {
|
|
104
|
+
setLoading(true);
|
|
105
|
+
checkImages(emailMarkup, `${location.protocol}//${location.host}`)
|
|
106
|
+
.then((newResults) => {
|
|
107
|
+
setResults(newResults);
|
|
108
|
+
setJustLoadedIn(true);
|
|
109
|
+
localStorage.setItem(cacheKey, JSON.stringify(newResults));
|
|
110
|
+
})
|
|
111
|
+
.catch(console.error)
|
|
112
|
+
.finally(() => setLoading(false));
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const errorResults = React.useMemo(
|
|
116
|
+
() => results?.filter((r) => r.status === 'error') || [],
|
|
117
|
+
[results],
|
|
118
|
+
);
|
|
119
|
+
const warningResults = React.useMemo(
|
|
120
|
+
() => results?.filter((r) => r.status === 'warning') || [],
|
|
121
|
+
[results],
|
|
122
|
+
);
|
|
123
|
+
const successResults = React.useMemo(
|
|
124
|
+
() => results?.filter((r) => r.status === 'success') || [],
|
|
125
|
+
[results],
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<div className="mt-4 flex w-full flex-col gap-2 text-pretty">
|
|
130
|
+
{results ? (
|
|
131
|
+
<>
|
|
132
|
+
<ImageCheckerResults
|
|
133
|
+
label="Errors"
|
|
134
|
+
results={errorResults}
|
|
135
|
+
justLoadedIn={justLoadedIn}
|
|
136
|
+
status="error"
|
|
137
|
+
/>
|
|
138
|
+
<ImageCheckerResults
|
|
139
|
+
label="Warnings"
|
|
140
|
+
results={warningResults}
|
|
141
|
+
justLoadedIn={justLoadedIn}
|
|
142
|
+
status="warning"
|
|
143
|
+
/>
|
|
144
|
+
<ImageCheckerResults
|
|
145
|
+
label="Success"
|
|
146
|
+
results={successResults}
|
|
147
|
+
justLoadedIn={justLoadedIn}
|
|
148
|
+
status="success"
|
|
149
|
+
/>
|
|
150
|
+
</>
|
|
151
|
+
) : (
|
|
152
|
+
<span className="text-xs leading-relaxed">
|
|
153
|
+
Check if all links are valid and redirect to the correct pages.
|
|
154
|
+
</span>
|
|
155
|
+
)}
|
|
156
|
+
<Button loading={loading} onClick={handleRun}>
|
|
157
|
+
{results ? 'Re-run' : 'Run'}
|
|
158
|
+
</Button>
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
};
|