react-email 4.0.0-alpha.0 → 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 +33 -0
- package/dist/cli/index.js +8 -10
- package/dist/cli/index.mjs +8 -13
- package/dist/preview/.next/BUILD_ID +1 -1
- package/dist/preview/.next/app-build-manifest.json +18 -18
- package/dist/preview/.next/build-manifest.json +6 -6
- 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 +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/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/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-d1b0aa870bcfb13e.js +1 -0
- package/dist/preview/.next/static/chunks/webpack-9255716c9496e606.js +1 -0
- 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/module-punycode.d.ts +3 -0
- package/package.json +7 -9
- 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 +18 -15
- package/src/components/button.tsx +47 -36
- package/src/components/icons/icon-image.tsx +19 -0
- package/src/components/sidebar/checking-results.tsx +150 -0
- package/src/components/sidebar/file-tree-directory-children.tsx +3 -6
- package/src/components/sidebar/image-checker.tsx +161 -0
- package/src/components/sidebar/link-checker.tsx +83 -223
- package/src/components/sidebar/sidebar.tsx +74 -26
- package/src/hooks/use-icon-animation.ts +4 -7
- package/src/utils/static-node-modules-for-vm.ts +2 -1
- package/dist/preview/.next/server/chunks/273.js +0 -1
- package/dist/preview/.next/server/chunks/594.js +0 -10
- package/dist/preview/.next/static/chunks/18b16e15-6ad9b58e10ff8891.js +0 -1
- package/dist/preview/.next/static/chunks/490-48951f2e19ae3aef.js +0 -1
- package/dist/preview/.next/static/chunks/600-2e2ca4c8bbd97b61.js +0 -1
- package/dist/preview/.next/static/chunks/app/layout-490964e2c3604d33.js +0 -1
- package/dist/preview/.next/static/chunks/app/page-d2432acd08db8fc0.js +0 -1
- package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-f4e211e00c026401.js +0 -1
- package/dist/preview/.next/static/chunks/main-app-cd104297c6bcc87e.js +0 -1
- package/dist/preview/.next/static/chunks/webpack-7bf1ffb05f5540be.js +0 -1
- package/dist/preview/.next/static/css/5e0736cafbb392a9.css +0 -3
- /package/dist/preview/.next/static/{fZaiKz58wDr55pxLu9uHa → Mn2FuRztLqr32yO8CKHi9}/_buildManifest.js +0 -0
- /package/dist/preview/.next/static/{fZaiKz58wDr55pxLu9uHa → Mn2FuRztLqr32yO8CKHi9}/_ssgManifest.js +0 -0
|
@@ -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
|
+
};
|
|
@@ -3,7 +3,6 @@ import { AnimatePresence, LayoutGroup, motion } from 'framer-motion';
|
|
|
3
3
|
import Link from 'next/link';
|
|
4
4
|
import { useSearchParams } from 'next/navigation';
|
|
5
5
|
import { cn } from '../../utils';
|
|
6
|
-
import { emailsDirectoryAbsolutePath } from '../../utils/emails-directory-absolute-path';
|
|
7
6
|
import type { EmailsDirectory } from '../../utils/get-emails-directory-metadata';
|
|
8
7
|
import { IconFile } from '../icons/icon-file';
|
|
9
8
|
import { FileTreeDirectory } from './file-tree-directory';
|
|
@@ -15,15 +14,13 @@ export const FileTreeDirectoryChildren = (props: {
|
|
|
15
14
|
isRoot?: boolean;
|
|
16
15
|
}) => {
|
|
17
16
|
const searchParams = useSearchParams();
|
|
18
|
-
const isBaseEmailsDirectory =
|
|
19
|
-
props.emailsDirectoryMetadata.absolutePath === emailsDirectoryAbsolutePath;
|
|
20
17
|
|
|
21
18
|
return (
|
|
22
19
|
<AnimatePresence initial={false}>
|
|
23
20
|
{props.open ? (
|
|
24
21
|
<Collapsible.Content
|
|
25
22
|
asChild
|
|
26
|
-
className="relative
|
|
23
|
+
className="relative overflow-y-hidden pl-1"
|
|
27
24
|
forceMount
|
|
28
25
|
>
|
|
29
26
|
<motion.div
|
|
@@ -34,7 +31,7 @@ export const FileTreeDirectoryChildren = (props: {
|
|
|
34
31
|
{props.isRoot ? null : (
|
|
35
32
|
<div className="line absolute left-2.5 h-full w-px bg-slate-6" />
|
|
36
33
|
)}
|
|
37
|
-
<div className="
|
|
34
|
+
<div className="flex flex-col truncate">
|
|
38
35
|
<LayoutGroup id="sidebar">
|
|
39
36
|
{props.emailsDirectoryMetadata.subDirectories.map(
|
|
40
37
|
(subDirectory) => (
|
|
@@ -48,7 +45,7 @@ export const FileTreeDirectoryChildren = (props: {
|
|
|
48
45
|
)}
|
|
49
46
|
{props.emailsDirectoryMetadata.emailFilenames.map(
|
|
50
47
|
(emailFilename, index) => {
|
|
51
|
-
const emailSlug =
|
|
48
|
+
const emailSlug = props.isRoot
|
|
52
49
|
? emailFilename
|
|
53
50
|
: `${props.emailsDirectoryMetadata.relativePath}/${emailFilename}`;
|
|
54
51
|
|
|
@@ -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
|
+
};
|