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.
Files changed (74) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/cli/index.js +8 -10
  3. package/dist/cli/index.mjs +8 -13
  4. package/dist/preview/.next/BUILD_ID +1 -1
  5. package/dist/preview/.next/app-build-manifest.json +18 -18
  6. package/dist/preview/.next/build-manifest.json +6 -6
  7. package/dist/preview/.next/cache/.rscinfo +1 -1
  8. package/dist/preview/.next/cache/webpack/client-production/0.pack +0 -0
  9. package/dist/preview/.next/cache/webpack/client-production/index.pack +0 -0
  10. package/dist/preview/.next/cache/webpack/edge-server-production/index.pack +0 -0
  11. package/dist/preview/.next/cache/webpack/server-production/0.pack +0 -0
  12. package/dist/preview/.next/cache/webpack/server-production/index.pack +0 -0
  13. package/dist/preview/.next/next-minimal-server.js.nft.json +1 -1
  14. package/dist/preview/.next/next-server.js.nft.json +1 -1
  15. package/dist/preview/.next/prerender-manifest.json +1 -1
  16. package/dist/preview/.next/required-server-files.json +1 -1
  17. package/dist/preview/.next/server/app/_not-found/page.js +1 -1
  18. package/dist/preview/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  19. package/dist/preview/.next/server/app/favicon.ico/route.js +1 -1
  20. package/dist/preview/.next/server/app/page.js +1 -1
  21. package/dist/preview/.next/server/app/page.js.nft.json +1 -1
  22. package/dist/preview/.next/server/app/page_client-reference-manifest.js +1 -1
  23. package/dist/preview/.next/server/app/preview/[...slug]/page.js +6 -6
  24. package/dist/preview/.next/server/app/preview/[...slug]/page.js.nft.json +1 -1
  25. package/dist/preview/.next/server/app/preview/[...slug]/page_client-reference-manifest.js +1 -1
  26. package/dist/preview/.next/server/chunks/196.js +1 -1
  27. package/dist/preview/.next/server/chunks/282.js +15 -0
  28. package/dist/preview/.next/server/chunks/667.js +1 -0
  29. package/dist/preview/.next/server/middleware-build-manifest.js +1 -1
  30. package/dist/preview/.next/server/next-font-manifest.js +1 -1
  31. package/dist/preview/.next/server/next-font-manifest.json +1 -1
  32. package/dist/preview/.next/server/pages/500.html +1 -1
  33. package/dist/preview/.next/server/server-reference-manifest.js +1 -1
  34. package/dist/preview/.next/server/server-reference-manifest.json +1 -1
  35. package/dist/preview/.next/static/chunks/207-7ab46c2d84f60fed.js +1 -0
  36. package/dist/preview/.next/static/chunks/490-9a10c001ec2dffb2.js +1 -0
  37. package/dist/preview/.next/static/chunks/afa401a5-9ebf2515b1397993.js +6 -0
  38. package/dist/preview/.next/static/chunks/app/layout-f1bad3fcfbc7eb6b.js +1 -0
  39. package/dist/preview/.next/static/chunks/app/page-800163ba6c6d943d.js +1 -0
  40. package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-5b5c4557fc89db64.js +1 -0
  41. package/dist/preview/.next/static/chunks/main-app-d1b0aa870bcfb13e.js +1 -0
  42. package/dist/preview/.next/static/chunks/webpack-9255716c9496e606.js +1 -0
  43. package/dist/preview/.next/static/css/d6c4def4cc3fb858.css +3 -0
  44. package/dist/preview/.next/trace +22 -21
  45. package/dist/preview/.next/types/app/layout.ts +1 -1
  46. package/dist/preview/.next/types/app/preview/[...slug]/page.ts +1 -1
  47. package/module-punycode.d.ts +3 -0
  48. package/package.json +7 -9
  49. package/src/actions/email-validation/check-images.spec.tsx +90 -0
  50. package/src/actions/email-validation/check-images.ts +142 -0
  51. package/src/actions/email-validation/check-links.spec.tsx +92 -0
  52. package/src/actions/email-validation/check-links.ts +18 -15
  53. package/src/components/button.tsx +47 -36
  54. package/src/components/icons/icon-image.tsx +19 -0
  55. package/src/components/sidebar/checking-results.tsx +150 -0
  56. package/src/components/sidebar/file-tree-directory-children.tsx +3 -6
  57. package/src/components/sidebar/image-checker.tsx +161 -0
  58. package/src/components/sidebar/link-checker.tsx +83 -223
  59. package/src/components/sidebar/sidebar.tsx +74 -26
  60. package/src/hooks/use-icon-animation.ts +4 -7
  61. package/src/utils/static-node-modules-for-vm.ts +2 -1
  62. package/dist/preview/.next/server/chunks/273.js +0 -1
  63. package/dist/preview/.next/server/chunks/594.js +0 -10
  64. package/dist/preview/.next/static/chunks/18b16e15-6ad9b58e10ff8891.js +0 -1
  65. package/dist/preview/.next/static/chunks/490-48951f2e19ae3aef.js +0 -1
  66. package/dist/preview/.next/static/chunks/600-2e2ca4c8bbd97b61.js +0 -1
  67. package/dist/preview/.next/static/chunks/app/layout-490964e2c3604d33.js +0 -1
  68. package/dist/preview/.next/static/chunks/app/page-d2432acd08db8fc0.js +0 -1
  69. package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-f4e211e00c026401.js +0 -1
  70. package/dist/preview/.next/static/chunks/main-app-cd104297c6bcc87e.js +0 -1
  71. package/dist/preview/.next/static/chunks/webpack-7bf1ffb05f5540be.js +0 -1
  72. package/dist/preview/.next/static/css/5e0736cafbb392a9.css +0 -3
  73. /package/dist/preview/.next/static/{fZaiKz58wDr55pxLu9uHa → Mn2FuRztLqr32yO8CKHi9}/_buildManifest.js +0 -0
  74. /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 data-[root=true]:mt-2 overflow-y-hidden pl-1"
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="data-[root=true]:py-2 flex flex-col truncate">
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 = isBaseEmailsDirectory
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
+ };