react-email 4.0.0-alpha.6 → 4.0.0-alpha.7

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 (103) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/cli/index.js +7 -3
  3. package/dist/cli/index.mjs +7 -3
  4. package/dist/preview/.next/BUILD_ID +1 -1
  5. package/dist/preview/.next/app-build-manifest.json +14 -13
  6. package/dist/preview/.next/app-path-routes-manifest.json +1 -1
  7. package/dist/preview/.next/build-manifest.json +3 -3
  8. package/dist/preview/.next/cache/.rscinfo +1 -1
  9. package/dist/preview/.next/cache/webpack/client-production/0.pack +0 -0
  10. package/dist/preview/.next/cache/webpack/client-production/index.pack +0 -0
  11. package/dist/preview/.next/cache/webpack/edge-server-production/index.pack +0 -0
  12. package/dist/preview/.next/cache/webpack/server-production/0.pack +0 -0
  13. package/dist/preview/.next/cache/webpack/server-production/index.pack +0 -0
  14. package/dist/preview/.next/next-minimal-server.js.nft.json +1 -1
  15. package/dist/preview/.next/next-server.js.nft.json +1 -1
  16. package/dist/preview/.next/prerender-manifest.json +3 -3
  17. package/dist/preview/.next/required-server-files.json +3 -3
  18. package/dist/preview/.next/server/app/_not-found/page.js +1 -1
  19. package/dist/preview/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  20. package/dist/preview/.next/server/app/favicon.ico/route.js +1 -1
  21. package/dist/preview/.next/server/app/page.js +1 -1
  22. package/dist/preview/.next/server/app/page.js.nft.json +1 -1
  23. package/dist/preview/.next/server/app/page_client-reference-manifest.js +1 -1
  24. package/dist/preview/.next/server/app/preview/[...slug]/page.js +29 -25
  25. package/dist/preview/.next/server/app/preview/[...slug]/page.js.nft.json +1 -1
  26. package/dist/preview/.next/server/app/preview/[...slug]/page_client-reference-manifest.js +1 -1
  27. package/dist/preview/.next/server/app-paths-manifest.json +1 -1
  28. package/dist/preview/.next/server/chunks/600.js +3 -3
  29. package/dist/preview/.next/server/chunks/{171.js → 816.js} +6 -6
  30. package/dist/preview/.next/server/chunks/943.js +1 -0
  31. package/dist/preview/.next/server/middleware-build-manifest.js +1 -1
  32. package/dist/preview/.next/server/next-font-manifest.js +1 -1
  33. package/dist/preview/.next/server/next-font-manifest.json +1 -1
  34. package/dist/preview/.next/server/pages/500.html +1 -1
  35. package/dist/preview/.next/server/pages-manifest.json +1 -1
  36. package/dist/preview/.next/server/server-reference-manifest.js +1 -1
  37. package/dist/preview/.next/server/server-reference-manifest.json +1 -1
  38. package/dist/preview/.next/static/chunks/287-7864b805e6bdc854.js +1 -0
  39. package/dist/preview/.next/static/chunks/412-31817e53b50a3e73.js +1 -0
  40. package/dist/preview/.next/static/chunks/683-1fb40795502f6e63.js +1 -0
  41. package/dist/preview/.next/static/chunks/880-9c0b721328117b8b.js +1 -0
  42. package/dist/preview/.next/static/chunks/app/layout-ffdee5cc1be30e7b.js +1 -0
  43. package/dist/preview/.next/static/chunks/app/page-9ea0bd45cd6294b0.js +1 -0
  44. package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-9e22979a25c836c0.js +1 -0
  45. package/dist/preview/.next/static/chunks/{main-app-c2e686acf8d370d7.js → main-app-256b213b179a95cc.js} +1 -1
  46. package/dist/preview/.next/static/css/eaae8ce545b295f9.css +3 -0
  47. package/dist/preview/.next/trace +26 -26
  48. package/dist/preview/.next/types/app/layout.ts +1 -1
  49. package/dist/preview/.next/types/app/page.ts +84 -0
  50. package/dist/preview/.next/types/app/preview/[...slug]/page.ts +1 -1
  51. package/package.json +1 -1
  52. package/src/actions/email-validation/check-compatibility.ts +0 -1
  53. package/src/actions/email-validation/check-images.spec.tsx +13 -11
  54. package/src/actions/email-validation/check-images.ts +6 -0
  55. package/src/actions/email-validation/check-links.spec.tsx +23 -11
  56. package/src/actions/email-validation/check-links.ts +6 -0
  57. package/src/actions/email-validation/get-code-location-from-ast-element.ts +18 -0
  58. package/src/actions/render-email-by-path.tsx +2 -2
  59. package/src/app/env.ts +3 -0
  60. package/src/app/preview/[...slug]/page.tsx +24 -11
  61. package/src/app/preview/[...slug]/preview.tsx +15 -12
  62. package/src/components/code-container.tsx +90 -71
  63. package/src/components/code.tsx +106 -42
  64. package/src/components/icons/icon-info.tsx +18 -0
  65. package/src/components/icons/icon-reload.tsx +13 -14
  66. package/src/components/logo.tsx +3 -2
  67. package/src/components/resizable-wrapper.tsx +1 -4
  68. package/src/components/sidebar/file-tree-directory-children.tsx +1 -0
  69. package/src/components/sidebar/sidebar.tsx +2 -3
  70. package/src/components/toolbar/code-preview-line-link.tsx +40 -0
  71. package/src/components/toolbar/compatibility.tsx +113 -0
  72. package/src/components/toolbar/linter.tsx +69 -111
  73. package/src/components/toolbar/results.tsx +5 -2
  74. package/src/components/toolbar/spam-assassin.tsx +20 -12
  75. package/src/components/toolbar/toolbar-button.tsx +4 -2
  76. package/src/components/toolbar.tsx +108 -30
  77. package/src/components/tooltip-content.tsx +1 -1
  78. package/src/components/topbar/view-size-controls.tsx +1 -2
  79. package/src/components/topbar.tsx +1 -20
  80. package/src/contexts/fragment-identifier.tsx +46 -0
  81. package/src/hooks/use-fragment-identifier.ts +14 -0
  82. package/src/utils/caniemail/tailwind/get-tailwind-config.ts +0 -2
  83. package/src/utils/get-email-component.ts +1 -1
  84. package/src/utils/get-line-and-column-from-offset.spec.ts +11 -0
  85. package/src/utils/get-line-and-column-from-offset.ts +11 -0
  86. package/src/utils/index.ts +1 -0
  87. package/src/utils/linting.ts +5 -30
  88. package/src/utils/load-stream.ts +15 -0
  89. package/src/utils/sanitize.ts +6 -0
  90. package/dist/preview/.next/server/chunks/833.js +0 -1
  91. package/dist/preview/.next/static/chunks/416-56f79fc7e689f06f.js +0 -1
  92. package/dist/preview/.next/static/chunks/683-8bbfd191e5105f01.js +0 -1
  93. package/dist/preview/.next/static/chunks/87-38e35f08507de015.js +0 -1
  94. package/dist/preview/.next/static/chunks/app/layout-a6640e62690d8fd6.js +0 -1
  95. package/dist/preview/.next/static/chunks/app/page-ba68f50b287e7478.js +0 -1
  96. package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-4a5b026ab543e27f.js +0 -1
  97. package/dist/preview/.next/static/css/d7df9cfc3e182163.css +0 -3
  98. package/src/actions/email-validation/get-line-and-column-from-index.spec.ts +0 -22
  99. package/src/actions/email-validation/get-line-and-column-from-index.ts +0 -43
  100. package/src/components/icons/icon-scanner.tsx +0 -19
  101. package/src/components/icons/icon-scissors.tsx +0 -19
  102. /package/dist/preview/.next/static/{gFk9UfWL8joM4iD7-wlKF → Pms2orsQgT5xpttCfZfH5}/_buildManifest.js +0 -0
  103. /package/dist/preview/.next/static/{gFk9UfWL8joM4iD7-wlKF → Pms2orsQgT5xpttCfZfH5}/_ssgManifest.js +0 -0
@@ -1,6 +1,10 @@
1
+ 'use client';
2
+ import Link from 'next/link';
3
+ import { useSearchParams } from 'next/navigation';
1
4
  import type { Language } from 'prism-react-renderer';
2
5
  import { Highlight } from 'prism-react-renderer';
3
- import * as React from 'react';
6
+ import { Fragment, useEffect } from 'react';
7
+ import { useFragmentIdentifier } from '../hooks/use-fragment-identifier';
4
8
  import { cn } from '../utils';
5
9
 
6
10
  interface CodeProps {
@@ -43,10 +47,43 @@ const theme = {
43
47
  ],
44
48
  };
45
49
 
50
+ const lineHashRegex = /#L(?<start>\d+)(?:,(?<end>\d+))?/;
51
+
46
52
  export const Code: React.FC<Readonly<CodeProps>> = ({
47
53
  children,
48
54
  language = 'html',
49
55
  }) => {
56
+ const locationHash = useFragmentIdentifier();
57
+ const highlight = (() => {
58
+ if (locationHash) {
59
+ const match = locationHash.match(lineHashRegex);
60
+ if (match?.groups?.start) {
61
+ const start = Number.parseInt(match.groups.start);
62
+ const end = match.groups.end
63
+ ? Number.parseInt(match.groups.end)
64
+ : start;
65
+ return [start, end] as const;
66
+ }
67
+ }
68
+ })();
69
+
70
+ const isHighlighting = (line: number) => {
71
+ if (!highlight) return false;
72
+
73
+ return highlight[0] <= line && highlight[1] >= line;
74
+ };
75
+
76
+ useEffect(() => {
77
+ if (highlight) {
78
+ document.getElementById(`L${highlight[0]}`)?.scrollIntoView({
79
+ block: 'start',
80
+ behavior: 'smooth',
81
+ });
82
+ }
83
+ }, [highlight]);
84
+
85
+ const searchParams = useSearchParams();
86
+
50
87
  const value = children.trim();
51
88
 
52
89
  return (
@@ -60,50 +97,77 @@ export const Code: React.FC<Readonly<CodeProps>> = ({
60
97
  'linear-gradient(90deg, rgba(56, 189, 248, 0) 0%, rgba(56, 189, 248, 0) 0%, rgba(232, 232, 232, 0.2) 33.02%, rgba(143, 143, 143, 0.6719) 64.41%, rgba(236, 72, 153, 0) 98.93%)',
61
98
  }}
62
99
  />
63
- <pre className="h-[650px] overflow-auto p-4">
64
- {tokens.map((line, i) => {
65
- const lineProps = getLineProps({
66
- line,
67
- key: i,
68
- });
69
- return (
70
- <div
71
- {...lineProps}
72
- className={cn('whitespace-pre', {
73
- "before:mr-2 before:text-slate-11 before:content-['$']":
74
- language === 'bash' && tokens.length === 1,
75
- })}
100
+ <div className="flex h-[650px] p-4 max-h-[calc(100vh-10rem)] after:w-full after:static after:block after:h-4 after:content-[''] overflow-auto">
101
+ <div className="text-[#49494f] text-[13px] font-light font-[MonoLisa,_Menlo,_monospace]">
102
+ {tokens.map((_, i) => (
103
+ <Link
104
+ id={`L${i + 1}`}
76
105
  key={i}
106
+ href={{
107
+ hash: `#L${i + 1}`,
108
+ search: searchParams.toString(),
109
+ }}
110
+ scroll={false}
111
+ className={cn(
112
+ 'align-middle block scroll-mt-[325px] rounded-l-sm select-none pr-3 cursor-pointer hover:text-slate-12',
113
+ isHighlighting(i + 1) &&
114
+ 'text-cyan-11 hover:text-cyan-11 bg-cyan-5',
115
+ )}
116
+ type="button"
77
117
  >
78
- {line.map((token, key) => {
79
- const tokenProps = getTokenProps({
80
- token,
81
- });
82
- const isException =
83
- token.content === 'from' &&
84
- line[key + 1]?.content === ':';
85
- const newTypes = isException
86
- ? [...token.types, 'key-white']
87
- : token.types;
88
- token.types = newTypes;
118
+ {i + 1}
119
+ </Link>
120
+ ))}
121
+ </div>
122
+ <pre>
123
+ {tokens.map((line, i) => {
124
+ const lineProps = getLineProps({
125
+ line,
126
+ key: i,
127
+ });
128
+ return (
129
+ <div
130
+ {...lineProps}
131
+ className={cn(
132
+ 'whitespace-pre flex transition-colors rounded-r-sm',
133
+ isHighlighting(i + 1) && 'bg-cyan-5',
134
+ {
135
+ "before:mr-2 before:text-slate-11 before:content-['$']":
136
+ language === 'bash' && tokens.length === 1,
137
+ },
138
+ )}
139
+ key={i}
140
+ >
141
+ {line.map((token, key) => {
142
+ const tokenProps = getTokenProps({
143
+ token,
144
+ });
145
+ const isException =
146
+ token.content === 'from' &&
147
+ line[key + 1]?.content === ':';
148
+ const newTypes = isException
149
+ ? [...token.types, 'key-white']
150
+ : token.types;
151
+ token.types = newTypes;
89
152
 
90
- return (
91
- <React.Fragment key={key}>
92
- <span {...tokenProps} />
93
- </React.Fragment>
94
- );
95
- })}
96
- </div>
97
- );
98
- })}
99
- </pre>
100
- <div
101
- className="absolute bottom-0 left-0 h-px w-[200px]"
102
- style={{
103
- background:
104
- 'linear-gradient(90deg, rgba(56, 189, 248, 0) 0%, rgba(56, 189, 248, 0) 0%, rgba(232, 232, 232, 0.2) 33.02%, rgba(143, 143, 143, 0.6719) 64.41%, rgba(236, 72, 153, 0) 98.93%)',
105
- }}
106
- />
153
+ return (
154
+ <Fragment key={key}>
155
+ <span {...tokenProps} />
156
+ </Fragment>
157
+ );
158
+ })}
159
+ </div>
160
+ );
161
+ })}
162
+ </pre>
163
+ <div
164
+ className="absolute bottom-0 left-0 h-px w-[200px]"
165
+ style={{
166
+ background:
167
+ 'linear-gradient(90deg, rgba(56, 189, 248, 0) 0%, rgba(56, 189, 248, 0) 0%, rgba(232, 232, 232, 0.2) 33.02%, rgba(143, 143, 143, 0.6719) 64.41%, rgba(236, 72, 153, 0) 98.93%)',
168
+ }}
169
+ />
170
+ </div>
107
171
  </>
108
172
  )}
109
173
  </Highlight>
@@ -0,0 +1,18 @@
1
+ import * as React from 'react';
2
+ import type { IconElement, IconProps } from './icon-base';
3
+ import { IconBase } from './icon-base';
4
+
5
+ export const IconInfo = React.forwardRef<IconElement, Readonly<IconProps>>(
6
+ ({ ...props }, forwardedRef) => (
7
+ <IconBase ref={forwardedRef} {...props}>
8
+ <path
9
+ d="M12 4C7.58173 4 4 7.58172 4 12C4 16.4182 7.58173 20 12 20C16.4183 20 20 16.4182 20 12C20 7.58172 16.4183 4 12 4ZM5.14754 12C5.14754 8.21549 8.21551 5.14754 12 5.14754C15.7845 5.14754 18.8525 8.21549 18.8525 12C18.8525 15.7844 15.7845 18.8525 12 18.8525C8.21551 18.8525 5.14754 15.7844 5.14754 12ZM12.906 8.37648C12.906 8.87682 12.5004 9.28243 12 9.28243C11.4997 9.28243 11.0941 8.87682 11.0941 8.37648C11.0941 7.87613 11.4997 7.47053 12 7.47053C12.5004 7.47053 12.906 7.87613 12.906 8.37648ZM10.1883 10.1884H10.7922H12.0002C12.3337 10.1884 12.6041 10.4588 12.6041 10.7924V15.0201H13.2081H13.8121V16.2281H13.2081H12.0002H10.7922H10.1883V15.0201H10.7922H11.3962V11.3963H10.7922H10.1883V10.1884Z"
10
+ fill="currentColor"
11
+ fillRule="evenodd"
12
+ clipRule="evenodd"
13
+ />
14
+ </IconBase>
15
+ ),
16
+ );
17
+
18
+ IconInfo.displayName = 'IconInfo';
@@ -1,19 +1,18 @@
1
- export const IconReload = (props: React.ComponentProps<'svg'>) => {
2
- return (
3
- <svg
4
- width="12"
5
- height="12"
6
- viewBox="0 0 12 12"
7
- fill="none"
8
- xmlns="http://www.w3.org/2000/svg"
9
- {...props}
10
- >
1
+ import * as React from 'react';
2
+ import type { IconElement, IconProps } from './icon-base';
3
+ import { IconBase } from './icon-base';
4
+
5
+ export const IconReload = React.forwardRef<IconElement, Readonly<IconProps>>(
6
+ ({ ...props }, forwardedRef) => (
7
+ <IconBase ref={forwardedRef} {...props}>
11
8
  <path
12
9
  fillRule="evenodd"
13
10
  clipRule="evenodd"
14
- d="M10.52 6C10.52 3.73168 8.75221 1.48 6.00006 1.48C3.77741 1.48 2.67886 3.1251 2.21074 3.99999H3.60005C3.82096 3.99999 4.00005 4.17908 4.00005 4.39999C4.00005 4.6209 3.82096 4.79999 3.60005 4.79999H1.20005C0.979137 4.79999 0.800049 4.6209 0.800049 4.39999V1.99999C0.800049 1.77908 0.979137 1.59999 1.20005 1.59999C1.42096 1.59999 1.60005 1.77908 1.60005 1.99999V3.45056C2.16367 2.45702 3.4673 0.679993 6.00006 0.679993C9.25029 0.679993 11.32 3.34831 11.32 6C11.32 8.65169 9.25029 11.32 6.00006 11.32C4.44499 11.32 3.15027 10.7047 2.22843 9.76673C1.73486 9.26449 1.34939 8.67121 1.08658 8.03257C1.0025 7.8283 1.09995 7.59453 1.30424 7.51046C1.50853 7.42638 1.7423 7.52384 1.82637 7.72812C2.05104 8.27401 2.38001 8.77961 2.79901 9.20593C3.57646 9.99705 4.66802 10.52 6.00006 10.52C8.75221 10.52 10.52 8.26833 10.52 6Z"
11
+ d="M17.9354 12C17.9354 9.01537 15.5828 6.05264 11.9202 6.05264C8.96229 6.05264 7.50033 8.21724 6.87735 9.36841H8.72625C9.02024 9.36841 9.25858 9.60406 9.25858 9.89473C9.25858 10.1854 9.02024 10.421 8.72625 10.421H5.53232C5.23833 10.421 5 10.1854 5 9.89473V6.73684C5 6.44617 5.23833 6.21052 5.53232 6.21052C5.82631 6.21052 6.06465 6.44617 6.06465 6.73684V8.64548C6.81471 7.33819 8.54959 5 11.9202 5C16.2456 5 19 8.51094 19 12C19 15.4891 16.2456 19 11.9202 19C9.8507 19 8.12769 18.1904 6.9009 16.9562C6.24405 16.2954 5.73107 15.5148 5.38132 14.6744C5.26942 14.4057 5.39911 14.0981 5.67098 13.9875C5.94285 13.8768 6.25395 14.0051 6.36583 14.2738C6.66482 14.9921 7.10262 15.6574 7.66023 16.2183C8.69486 17.2593 10.1475 17.9474 11.9202 17.9474C15.5828 17.9474 17.9354 14.9846 17.9354 12Z"
15
12
  fill="currentColor"
16
13
  />
17
- </svg>
18
- );
19
- };
14
+ </IconBase>
15
+ ),
16
+ );
17
+
18
+ IconReload.displayName = 'IconReload';
@@ -1,10 +1,11 @@
1
1
  export const Logo = () => (
2
2
  <svg
3
3
  fill="none"
4
- height="32"
4
+ height="30"
5
5
  viewBox="0 0 119 32"
6
- width="119"
6
+ width="113"
7
7
  xmlns="http://www.w3.org/2000/svg"
8
+ style={{ opacity: 0.9 }}
8
9
  >
9
10
  <g clipPath="url(#clip0_27_291)">
10
11
  <path
@@ -100,10 +100,7 @@ export const ResizableWarpper = ({
100
100
  return (
101
101
  <div
102
102
  {...rest}
103
- className={cn(
104
- 'relative mx-auto my-auto box-content px-4 py-2',
105
- rest.className,
106
- )}
103
+ className={cn('relative mx-auto my-auto box-content', rest.className)}
107
104
  >
108
105
  <div
109
106
  aria-label="resize-west"
@@ -71,6 +71,7 @@ export const FileTreeDirectoryChildren = (props: {
71
71
  pathname: `/preview/${emailSlug}`,
72
72
  search: searchParams.toString(),
73
73
  }}
74
+ prefetch
74
75
  key={emailSlug}
75
76
  >
76
77
  <motion.span
@@ -2,7 +2,6 @@
2
2
  import { clsx } from 'clsx';
3
3
  import { useEmails } from '../../contexts/emails';
4
4
  import { cn } from '../../utils';
5
- import { Heading } from '../heading';
6
5
  import { Logo } from '../logo';
7
6
  import { FileTree } from './file-tree';
8
7
 
@@ -29,9 +28,9 @@ export const Sidebar = ({ className, currentEmailOpenSlug }: SidebarProps) => {
29
28
  'hidden min-h-[3.3125rem] flex-shrink items-center p-3 px-4 lg:flex',
30
29
  )}
31
30
  >
32
- <Heading as="h2" className="truncate" size="2" weight="medium">
31
+ <h2>
33
32
  <Logo />
34
- </Heading>
33
+ </h2>
35
34
  </div>
36
35
  <div className="relative h-full w-full border-slate-4 border-t px-4 pb-3">
37
36
  <FileTree
@@ -0,0 +1,40 @@
1
+ import Link from 'next/link';
2
+ import { useSearchParams } from 'next/navigation';
3
+
4
+ interface CodePreviewLineLinkProps {
5
+ line: number;
6
+ column: number;
7
+
8
+ type: 'react' | 'html';
9
+ }
10
+
11
+ export const CodePreviewLineLink = ({
12
+ line,
13
+ column,
14
+ type,
15
+ }: CodePreviewLineLinkProps) => {
16
+ const searchParams = useSearchParams();
17
+
18
+ const newSearchParams = new URLSearchParams(searchParams);
19
+ newSearchParams.set('view', 'source');
20
+ if (type === 'html') {
21
+ newSearchParams.set('lang', 'markup');
22
+ } else if (type === 'react') {
23
+ newSearchParams.set('lang', 'jsx');
24
+ }
25
+
26
+ const fragmentIdentifier = `#L${line}`;
27
+
28
+ return (
29
+ <Link
30
+ href={{
31
+ search: newSearchParams.toString(),
32
+ hash: fragmentIdentifier,
33
+ }}
34
+ scroll={false}
35
+ className="appearance-none underline mx-2"
36
+ >
37
+ L{line.toString().padStart(2, '0')}
38
+ </Link>
39
+ );
40
+ };
@@ -0,0 +1,113 @@
1
+ import { useRef, useState } from 'react';
2
+ import { toast } from 'sonner';
3
+ import { nicenames } from '../../actions/email-validation/caniemail-data';
4
+ import {
5
+ type CompatibilityCheckingResult,
6
+ checkCompatibility,
7
+ } from '../../actions/email-validation/check-compatibility';
8
+ import { sanitize } from '../../utils';
9
+ import { loadStream } from '../../utils/load-stream';
10
+ import { IconWarning } from '../icons/icon-warning';
11
+ import { CodePreviewLineLink } from './code-preview-line-link';
12
+ import { Results } from './results';
13
+
14
+ export const useCompatibility = ({
15
+ reactMarkup,
16
+ emailPath,
17
+
18
+ initialResults,
19
+ }: {
20
+ reactMarkup: string;
21
+ emailPath: string;
22
+
23
+ initialResults?: CompatibilityCheckingResult[];
24
+ }) => {
25
+ const [results, setResults] = useState(initialResults);
26
+
27
+ const [loading, setLoading] = useState(false);
28
+ const isLoadingRef = useRef(false);
29
+
30
+ const load = async () => {
31
+ if (isLoadingRef.current) return;
32
+ isLoadingRef.current = true;
33
+ setLoading(true);
34
+
35
+ setResults([]);
36
+ let rawResults: CompatibilityCheckingResult[] = [];
37
+
38
+ try {
39
+ const stream = await checkCompatibility(reactMarkup, emailPath);
40
+ for await (const result of loadStream(stream)) {
41
+ if (result.status !== 'error') continue;
42
+ setResults((current) => {
43
+ if (!current) {
44
+ return [result];
45
+ }
46
+ rawResults = [...current, result];
47
+ return rawResults;
48
+ });
49
+ }
50
+ } catch (exception) {
51
+ console.error(exception);
52
+ toast.error(JSON.stringify(exception));
53
+ } finally {
54
+ setLoading(false);
55
+ isLoadingRef.current = false;
56
+ }
57
+
58
+ return rawResults;
59
+ };
60
+
61
+ return [results, { loading, load }] as const;
62
+ };
63
+
64
+ interface CompatibilityProps {
65
+ results: CompatibilityCheckingResult[] | undefined;
66
+ }
67
+
68
+ export const Compatibility = ({ results }: CompatibilityProps) => {
69
+ return (
70
+ <Results>
71
+ {results?.map((result, i) => {
72
+ const statsReportedNotWorking = Object.entries(
73
+ result.statsPerEmailClient,
74
+ ).filter(([, stats]) => stats.status === 'error');
75
+ const unsupportedClientsString = statsReportedNotWorking
76
+ .map(([emailClient]) => nicenames.family[emailClient])
77
+ .join(', ');
78
+
79
+ return (
80
+ <Results.Row key={i}>
81
+ <Results.Column>
82
+ <span className="flex text-red-400 uppercase gap-2 items-center">
83
+ <IconWarning />
84
+ {sanitize(result.entry.title)}
85
+ </span>
86
+ </Results.Column>
87
+ <Results.Column>
88
+ {statsReportedNotWorking.length > 0
89
+ ? `Not supported in ${unsupportedClientsString}`
90
+ : null}
91
+
92
+ <a
93
+ href={result.entry.url}
94
+ className="underline ml-2 decoration-slate-9 decoration-1 hover:decoration-slate-11 transition-colors hover:text-slate-12"
95
+ rel="noreferrer"
96
+ target="_blank"
97
+ >
98
+ More ↗
99
+ </a>
100
+ </Results.Column>
101
+ <Results.Column className="font-mono text-slate-11 text-right">
102
+ <CodePreviewLineLink
103
+ line={result.location.start.line}
104
+ column={result.location.start.column}
105
+ type="react"
106
+ />
107
+ </Results.Column>
108
+ </Results.Row>
109
+ );
110
+ })}
111
+ </Results>
112
+ );
113
+ };