react-email 4.0.0-alpha.6 → 4.0.0-alpha.8
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 +13 -0
- package/dist/cli/index.js +18 -13
- package/dist/cli/index.mjs +26 -21
- package/dist/preview/.next/BUILD_ID +1 -1
- package/dist/preview/.next/app-build-manifest.json +14 -13
- 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 +3 -3
- package/dist/preview/.next/required-server-files.json +3 -3
- 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 +133 -25
- 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/42.js +1 -0
- package/dist/preview/.next/server/chunks/600.js +3 -3
- package/dist/preview/.next/server/chunks/{171.js → 816.js} +6 -6
- 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/pages-manifest.json +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/287-7864b805e6bdc854.js +1 -0
- package/dist/preview/.next/static/chunks/412-31817e53b50a3e73.js +1 -0
- package/dist/preview/.next/static/chunks/683-b769e5d91bdf9a82.js +1 -0
- package/dist/preview/.next/static/chunks/880-9c0b721328117b8b.js +1 -0
- package/dist/preview/.next/static/chunks/app/layout-7dee682873546401.js +1 -0
- package/dist/preview/.next/static/chunks/app/page-9ea0bd45cd6294b0.js +1 -0
- package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-a610d641c64448cc.js +1 -0
- package/dist/preview/.next/static/chunks/{main-app-c2e686acf8d370d7.js → main-app-256b213b179a95cc.js} +1 -1
- package/dist/preview/.next/static/css/e68ebc9bb8f7b3f4.css +3 -0
- package/dist/preview/.next/trace +26 -26
- package/dist/preview/.next/types/app/layout.ts +1 -1
- package/dist/preview/.next/types/app/page.ts +84 -0
- package/dist/preview/.next/types/app/preview/[...slug]/page.ts +1 -1
- package/package.json +1 -1
- package/src/actions/email-validation/check-compatibility.ts +16 -5
- package/src/actions/email-validation/check-images.spec.tsx +13 -11
- package/src/actions/email-validation/check-images.ts +6 -0
- package/src/actions/email-validation/check-links.spec.tsx +23 -11
- package/src/actions/email-validation/check-links.ts +6 -0
- package/src/actions/email-validation/get-code-location-from-ast-element.ts +18 -0
- package/src/actions/render-email-by-path.tsx +2 -2
- package/src/app/env.ts +3 -0
- package/src/app/preview/[...slug]/page.tsx +24 -11
- package/src/app/preview/[...slug]/preview.tsx +15 -12
- package/src/components/code-container.tsx +90 -71
- package/src/components/code.tsx +106 -42
- package/src/components/icons/icon-info.tsx +18 -0
- package/src/components/icons/icon-reload.tsx +13 -14
- package/src/components/logo.tsx +3 -2
- package/src/components/resizable-wrapper.tsx +1 -4
- package/src/components/sidebar/file-tree-directory-children.tsx +13 -2
- package/src/components/sidebar/file-tree-directory.tsx +26 -18
- package/src/components/sidebar/file-tree.tsx +2 -2
- package/src/components/sidebar/sidebar.tsx +16 -19
- package/src/components/toolbar/code-preview-line-link.tsx +40 -0
- package/src/components/toolbar/compatibility.tsx +113 -0
- package/src/components/toolbar/linter.tsx +69 -111
- package/src/components/toolbar/results.tsx +5 -2
- package/src/components/toolbar/spam-assassin.tsx +31 -20
- package/src/components/toolbar/toolbar-button.tsx +4 -2
- package/src/components/toolbar/use-cached-state.ts +2 -2
- package/src/components/toolbar.tsx +152 -30
- package/src/components/tooltip-content.tsx +1 -1
- package/src/components/topbar/view-size-controls.tsx +1 -2
- package/src/components/topbar.tsx +1 -20
- package/src/contexts/fragment-identifier.tsx +46 -0
- package/src/hooks/use-fragment-identifier.ts +14 -0
- package/src/utils/__snapshots__/get-email-component.spec.ts.snap +1 -1
- package/src/utils/caniemail/ast/__snapshots__/get-object-variables.spec.ts.snap +74 -0
- package/src/utils/caniemail/ast/__snapshots__/get-used-style-properties.spec.ts.snap +24 -0
- package/src/utils/caniemail/ast/get-object-variables.spec.ts +19 -0
- package/src/utils/caniemail/ast/get-used-style-properties.spec.ts +23 -0
- package/src/utils/caniemail/get-css-property-with-value.ts +2 -2
- package/src/utils/caniemail/tailwind/get-tailwind-config.ts +0 -2
- package/src/utils/get-email-component.ts +1 -1
- package/src/utils/get-line-and-column-from-offset.spec.ts +11 -0
- package/src/utils/get-line-and-column-from-offset.ts +11 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/linting.ts +5 -30
- package/src/utils/load-stream.ts +15 -0
- package/src/utils/sanitize.ts +6 -0
- package/dist/preview/.next/server/chunks/833.js +0 -1
- package/dist/preview/.next/static/chunks/416-56f79fc7e689f06f.js +0 -1
- package/dist/preview/.next/static/chunks/683-8bbfd191e5105f01.js +0 -1
- package/dist/preview/.next/static/chunks/87-38e35f08507de015.js +0 -1
- package/dist/preview/.next/static/chunks/app/layout-a6640e62690d8fd6.js +0 -1
- package/dist/preview/.next/static/chunks/app/page-ba68f50b287e7478.js +0 -1
- package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-4a5b026ab543e27f.js +0 -1
- package/dist/preview/.next/static/css/d7df9cfc3e182163.css +0 -3
- package/src/actions/email-validation/get-line-and-column-from-index.spec.ts +0 -22
- package/src/actions/email-validation/get-line-and-column-from-index.ts +0 -43
- package/src/components/icons/icon-scanner.tsx +0 -19
- package/src/components/icons/icon-scissors.tsx +0 -19
- /package/dist/preview/.next/static/{gFk9UfWL8joM4iD7-wlKF → SoPVDfPAp9R983pBBriVn}/_buildManifest.js +0 -0
- /package/dist/preview/.next/static/{gFk9UfWL8joM4iD7-wlKF → SoPVDfPAp9R983pBBriVn}/_ssgManifest.js +0 -0
|
@@ -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
|
+
};
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import prettyBytes from 'pretty-bytes';
|
|
2
|
-
import { useRef, useState } from 'react';
|
|
3
|
-
import { nicenames } from '../../actions/email-validation/caniemail-data';
|
|
4
|
-
import type { CompatibilityCheckingResult } from '../../actions/email-validation/check-compatibility';
|
|
2
|
+
import { Children, useRef, useState } from 'react';
|
|
5
3
|
import type { ImageCheckingResult } from '../../actions/email-validation/check-images';
|
|
6
4
|
import type { LinkCheckingResult } from '../../actions/email-validation/check-links';
|
|
7
|
-
import { cn } from '../../utils';
|
|
5
|
+
import { cn, sanitize } from '../../utils';
|
|
8
6
|
import { getLintingSources, loadLintingRowsFrom } from '../../utils/linting';
|
|
9
7
|
import { IconWarning } from '../icons/icon-warning';
|
|
8
|
+
import { CodePreviewLineLink } from './code-preview-line-link';
|
|
10
9
|
import { Results } from './results';
|
|
11
10
|
|
|
12
11
|
export type LintingRow =
|
|
@@ -17,10 +16,6 @@ export type LintingRow =
|
|
|
17
16
|
| {
|
|
18
17
|
source: 'link';
|
|
19
18
|
result: LinkCheckingResult;
|
|
20
|
-
}
|
|
21
|
-
| {
|
|
22
|
-
source: 'compatibility';
|
|
23
|
-
result: CompatibilityCheckingResult;
|
|
24
19
|
};
|
|
25
20
|
|
|
26
21
|
interface LinterProps {
|
|
@@ -29,14 +24,10 @@ interface LinterProps {
|
|
|
29
24
|
|
|
30
25
|
export const useLinter = ({
|
|
31
26
|
markup,
|
|
32
|
-
reactMarkup,
|
|
33
|
-
emailPath,
|
|
34
27
|
|
|
35
28
|
initialRows,
|
|
36
29
|
}: {
|
|
37
|
-
reactMarkup: string;
|
|
38
30
|
markup: string;
|
|
39
|
-
emailPath: string;
|
|
40
31
|
|
|
41
32
|
initialRows?: LintingRow[];
|
|
42
33
|
}) => {
|
|
@@ -44,8 +35,6 @@ export const useLinter = ({
|
|
|
44
35
|
|
|
45
36
|
const sources = getLintingSources(
|
|
46
37
|
markup,
|
|
47
|
-
reactMarkup,
|
|
48
|
-
emailPath,
|
|
49
38
|
'location' in global
|
|
50
39
|
? `${global.location.protocol}//${global.location.host}`
|
|
51
40
|
: '',
|
|
@@ -105,80 +94,44 @@ export const Linter = ({ rows }: LinterProps) => {
|
|
|
105
94
|
)!;
|
|
106
95
|
return (
|
|
107
96
|
<Result status={row.result.status} key={i}>
|
|
108
|
-
<Result.Name>{failingCheck.type}</Result.Name>
|
|
97
|
+
<Result.Name>{sanitize(failingCheck.type)}</Result.Name>
|
|
109
98
|
<Result.Description>
|
|
110
99
|
{failingCheck.type === 'security'
|
|
111
|
-
? 'Insecure URL, use HTTPS
|
|
100
|
+
? 'Insecure URL, use HTTPS instead of HTTP'
|
|
112
101
|
: null}
|
|
113
102
|
{failingCheck.type === 'fetch_attempt' &&
|
|
114
103
|
failingCheck.metadata.fetchStatusCode &&
|
|
115
104
|
failingCheck.metadata.fetchStatusCode >= 300 &&
|
|
116
|
-
failingCheck.metadata.fetchStatusCode < 400
|
|
117
|
-
|
|
118
|
-
|
|
105
|
+
failingCheck.metadata.fetchStatusCode < 400 ? (
|
|
106
|
+
<>
|
|
107
|
+
<strong>{failingCheck.metadata.fetchStatusCode}</strong>:
|
|
108
|
+
There was a redirect, the content may have been moved
|
|
109
|
+
</>
|
|
110
|
+
) : null}
|
|
119
111
|
{failingCheck.type === 'fetch_attempt' &&
|
|
120
112
|
failingCheck.metadata.fetchStatusCode &&
|
|
121
|
-
failingCheck.metadata.fetchStatusCode >= 400
|
|
122
|
-
|
|
123
|
-
|
|
113
|
+
failingCheck.metadata.fetchStatusCode >= 400 ? (
|
|
114
|
+
<>
|
|
115
|
+
<strong>{failingCheck.metadata.fetchStatusCode}</strong>:
|
|
116
|
+
The link is broken
|
|
117
|
+
</>
|
|
118
|
+
) : null}
|
|
124
119
|
{failingCheck.type === 'syntax'
|
|
125
120
|
? 'The link is broken due to invalid syntax'
|
|
126
121
|
: null}
|
|
127
122
|
|
|
128
|
-
<span className="
|
|
123
|
+
<span className="ml-2 text-ellipsis overflow-hidden text-nowrap max-w-[30ch]">
|
|
129
124
|
{row.result.link}
|
|
130
125
|
</span>
|
|
131
126
|
</Result.Description>
|
|
132
127
|
<Result.Metadata>
|
|
133
|
-
{
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (row.source === 'compatibility') {
|
|
142
|
-
const statsReportedNotWorking = Object.entries(
|
|
143
|
-
row.result.statsPerEmailClient,
|
|
144
|
-
).filter(([, stats]) => stats.status === 'error');
|
|
145
|
-
const statsReportedPartiallyWorking = Object.entries(
|
|
146
|
-
row.result.statsPerEmailClient,
|
|
147
|
-
).filter(([, stats]) => stats.status === 'warning');
|
|
148
|
-
|
|
149
|
-
const unsupportedClientsString = statsReportedNotWorking
|
|
150
|
-
.map(([emailClient]) => nicenames.family[emailClient])
|
|
151
|
-
.join(', ');
|
|
152
|
-
const partiallySupportedClientsString = statsReportedPartiallyWorking
|
|
153
|
-
.map(([emailClient]) => nicenames.family[emailClient])
|
|
154
|
-
.join(', ');
|
|
155
|
-
|
|
156
|
-
return (
|
|
157
|
-
<Result status={row.result.status} key={i}>
|
|
158
|
-
<Result.Name>{row.result.entry.title}</Result.Name>
|
|
159
|
-
<Result.Description>
|
|
160
|
-
{statsReportedNotWorking.length > 0
|
|
161
|
-
? `Not supported in ${unsupportedClientsString}`
|
|
162
|
-
: null}
|
|
163
|
-
{statsReportedPartiallyWorking.length > 0 &&
|
|
164
|
-
statsReportedNotWorking.length > 0
|
|
165
|
-
? '. '
|
|
166
|
-
: null}
|
|
167
|
-
{statsReportedPartiallyWorking.length > 0
|
|
168
|
-
? `Partially supported in ${partiallySupportedClientsString}`
|
|
169
|
-
: null}
|
|
170
|
-
</Result.Description>
|
|
171
|
-
<Result.Metadata>
|
|
172
|
-
{row.result.location.start.line.toString().padStart(2, '0')}:
|
|
173
|
-
{row.result.location.start.column.toString().padStart(2, '0')}
|
|
174
|
-
<a
|
|
175
|
-
href={row.result.entry.url}
|
|
176
|
-
className="underline ml-2"
|
|
177
|
-
rel="noreferrer"
|
|
178
|
-
target="_blank"
|
|
179
|
-
>
|
|
180
|
-
See more info
|
|
181
|
-
</a>
|
|
128
|
+
{[
|
|
129
|
+
<CodePreviewLineLink
|
|
130
|
+
line={row.result.codeLocation.line}
|
|
131
|
+
column={row.result.codeLocation.column}
|
|
132
|
+
type="html"
|
|
133
|
+
/>,
|
|
134
|
+
]}
|
|
182
135
|
</Result.Metadata>
|
|
183
136
|
</Result>
|
|
184
137
|
);
|
|
@@ -188,24 +141,43 @@ export const Linter = ({ rows }: LinterProps) => {
|
|
|
188
141
|
const failingCheck = row.result.checks.find(
|
|
189
142
|
(check) => check.passed === false,
|
|
190
143
|
)!;
|
|
144
|
+
const metadata: React.ReactNode[] = [];
|
|
145
|
+
for (const check of row.result.checks) {
|
|
146
|
+
if (check.type === 'image_size' && check.metadata.byteCount) {
|
|
147
|
+
metadata.push(prettyBytes(check.metadata.byteCount));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
metadata.push(
|
|
151
|
+
<CodePreviewLineLink
|
|
152
|
+
line={row.result.codeLocation.line}
|
|
153
|
+
column={row.result.codeLocation.column}
|
|
154
|
+
type="html"
|
|
155
|
+
/>,
|
|
156
|
+
);
|
|
191
157
|
return (
|
|
192
158
|
<Result status={row.result.status} key={i}>
|
|
193
|
-
<Result.Name>{failingCheck.type}</Result.Name>
|
|
159
|
+
<Result.Name>{sanitize(failingCheck.type)}</Result.Name>
|
|
194
160
|
<Result.Description>
|
|
195
161
|
{failingCheck.type === 'security'
|
|
196
|
-
? 'Insecure URL, use HTTPS
|
|
162
|
+
? 'Insecure URL, use HTTPS instead of HTTP'
|
|
197
163
|
: null}
|
|
198
164
|
{failingCheck.type === 'fetch_attempt' &&
|
|
199
165
|
failingCheck.metadata.fetchStatusCode &&
|
|
200
166
|
failingCheck.metadata.fetchStatusCode >= 300 &&
|
|
201
|
-
failingCheck.metadata.fetchStatusCode < 400
|
|
202
|
-
|
|
203
|
-
|
|
167
|
+
failingCheck.metadata.fetchStatusCode < 400 ? (
|
|
168
|
+
<>
|
|
169
|
+
<strong>{failingCheck.metadata.fetchStatusCode}</strong>:
|
|
170
|
+
There was a redirect, the image may have been moved
|
|
171
|
+
</>
|
|
172
|
+
) : null}
|
|
204
173
|
{failingCheck.type === 'fetch_attempt' &&
|
|
205
174
|
failingCheck.metadata.fetchStatusCode &&
|
|
206
|
-
failingCheck.metadata.fetchStatusCode >= 400
|
|
207
|
-
|
|
208
|
-
|
|
175
|
+
failingCheck.metadata.fetchStatusCode >= 400 ? (
|
|
176
|
+
<>
|
|
177
|
+
<strong>{failingCheck.metadata.fetchStatusCode}</strong>:
|
|
178
|
+
The image is broken
|
|
179
|
+
</>
|
|
180
|
+
) : null}
|
|
209
181
|
{failingCheck.type === 'syntax'
|
|
210
182
|
? 'The image is broken due to an invalid source'
|
|
211
183
|
: null}
|
|
@@ -219,32 +191,11 @@ export const Linter = ({ rows }: LinterProps) => {
|
|
|
219
191
|
? 'This image is too large, keep it under 1mb'
|
|
220
192
|
: null}
|
|
221
193
|
|
|
222
|
-
<span className="
|
|
194
|
+
<span className="ml-2 text-ellipsis overflow-hidden text-nowrap max-w-[30ch]">
|
|
223
195
|
{row.result.source}
|
|
224
196
|
</span>
|
|
225
197
|
</Result.Description>
|
|
226
|
-
<Result.Metadata>
|
|
227
|
-
{row.result.checks
|
|
228
|
-
.map((check) => {
|
|
229
|
-
if (
|
|
230
|
-
check.type === 'fetch_attempt' &&
|
|
231
|
-
check.metadata.fetchStatusCode
|
|
232
|
-
) {
|
|
233
|
-
return check.metadata.fetchStatusCode;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
if (
|
|
237
|
-
check.type === 'image_size' &&
|
|
238
|
-
check.metadata.byteCount
|
|
239
|
-
) {
|
|
240
|
-
return prettyBytes(check.metadata.byteCount);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return undefined;
|
|
244
|
-
})
|
|
245
|
-
.filter(Boolean)
|
|
246
|
-
.join('—')}
|
|
247
|
-
</Result.Metadata>
|
|
198
|
+
<Result.Metadata>{metadata}</Result.Metadata>
|
|
248
199
|
</Result>
|
|
249
200
|
);
|
|
250
201
|
}
|
|
@@ -277,9 +228,9 @@ Result.Name = ({
|
|
|
277
228
|
}: React.ComponentProps<typeof Results.Column>) => {
|
|
278
229
|
return (
|
|
279
230
|
<Results.Column {...props}>
|
|
280
|
-
<span className="flex uppercase gap-
|
|
231
|
+
<span className="flex uppercase gap-2 items-center group-data-[status=error]/result:text-red-400 group-data-[status=warning]/result:text-orange-300">
|
|
281
232
|
<IconWarning />
|
|
282
|
-
{children}
|
|
233
|
+
{typeof children === 'string' ? sanitize(children) : children}
|
|
283
234
|
</span>
|
|
284
235
|
</Results.Column>
|
|
285
236
|
);
|
|
@@ -293,18 +244,25 @@ Result.Description = ({
|
|
|
293
244
|
return <Results.Column {...props}>{children}</Results.Column>;
|
|
294
245
|
};
|
|
295
246
|
|
|
296
|
-
|
|
297
|
-
children
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}:
|
|
247
|
+
interface MetadatProps extends React.ComponentProps<typeof Results.Column> {
|
|
248
|
+
children: React.ReactNode[];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
Result.Metadata = ({ children, className, ...props }: MetadatProps) => {
|
|
301
252
|
return (
|
|
302
253
|
<Results.Column
|
|
303
254
|
align="right"
|
|
304
255
|
{...props}
|
|
305
256
|
className={cn('font-mono text-slate-11', className)}
|
|
306
257
|
>
|
|
307
|
-
{children
|
|
258
|
+
{Children.map(children, (child, index) => {
|
|
259
|
+
return (
|
|
260
|
+
<>
|
|
261
|
+
{index > 0 ? ' · ' : null}
|
|
262
|
+
{child}
|
|
263
|
+
</>
|
|
264
|
+
);
|
|
265
|
+
})}
|
|
308
266
|
</Results.Column>
|
|
309
267
|
);
|
|
310
268
|
};
|
|
@@ -8,7 +8,7 @@ export const Results = ({
|
|
|
8
8
|
return (
|
|
9
9
|
<table
|
|
10
10
|
className={cn(
|
|
11
|
-
'group relative w-full border-collapse text-left text-slate-10 text-
|
|
11
|
+
'group relative w-full border-collapse text-left text-slate-10 text-sm',
|
|
12
12
|
className,
|
|
13
13
|
)}
|
|
14
14
|
>
|
|
@@ -41,7 +41,10 @@ Results.Column = ({
|
|
|
41
41
|
...props
|
|
42
42
|
}: React.ComponentProps<'td'>) => {
|
|
43
43
|
return (
|
|
44
|
-
<td
|
|
44
|
+
<td
|
|
45
|
+
className={cn('py-1.5 align-bottom font-regular', className)}
|
|
46
|
+
{...props}
|
|
47
|
+
>
|
|
45
48
|
{children}
|
|
46
49
|
</td>
|
|
47
50
|
);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useRef, useState } from 'react';
|
|
2
2
|
import { toast } from 'sonner';
|
|
3
|
-
import { cn } from '../../utils';
|
|
3
|
+
import { cn, sanitize } from '../../utils';
|
|
4
4
|
import { IconWarning } from '../icons/icon-warning';
|
|
5
5
|
import { Results } from './results';
|
|
6
6
|
|
|
@@ -83,44 +83,55 @@ export const SpamAssassin = ({ result }: SpamAssassinProps) => {
|
|
|
83
83
|
<>
|
|
84
84
|
{result ? (
|
|
85
85
|
<Results>
|
|
86
|
-
<Results.Row className="sticky border-b
|
|
86
|
+
<Results.Row className="sticky border-b top-0 bg-black">
|
|
87
87
|
<Results.Column className="uppercase">
|
|
88
|
-
<span className="flex gap-
|
|
88
|
+
<span className="flex gap-2 items-center">
|
|
89
89
|
<IconWarning
|
|
90
90
|
className={cn(
|
|
91
|
-
result.points
|
|
92
|
-
result.points >
|
|
91
|
+
result.points === 0 ? 'text-green-400' : null,
|
|
92
|
+
result.points > 0 && result.points <= 1.5 ? null : null,
|
|
93
|
+
result.points > 1.5 ? 'text-yellow-100' : null,
|
|
94
|
+
result.points > 3 ? 'text-orange-400' : null,
|
|
93
95
|
result.points >= 5 ? 'text-red-400' : null,
|
|
94
96
|
)}
|
|
95
97
|
/>
|
|
96
98
|
Score
|
|
97
99
|
</span>
|
|
98
100
|
</Results.Column>
|
|
99
|
-
<Results.Column>
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
'
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
101
|
+
<Results.Column>
|
|
102
|
+
{result.points === 0
|
|
103
|
+
? 'Congratulations! Your email is clean of abuse indicators.'
|
|
104
|
+
: 'Higher scores are better'}
|
|
105
|
+
</Results.Column>
|
|
106
|
+
<Results.Column className="text-right tracking-tighter font-bold">
|
|
107
|
+
<span
|
|
108
|
+
className={cn(
|
|
109
|
+
'text-3xl',
|
|
110
|
+
result.points === 0 ? 'text-green-400' : null,
|
|
111
|
+
result.points > 0 && result.points <= 1.5 ? null : null,
|
|
112
|
+
result.points > 1.5 ? 'text-yellow-200' : null,
|
|
113
|
+
result.points > 3 ? 'text-orange-400' : null,
|
|
114
|
+
result.points >= 5 ? 'text-red-400' : null,
|
|
115
|
+
)}
|
|
116
|
+
>
|
|
117
|
+
{(10 - result.points).toFixed(1)}
|
|
118
|
+
</span>{' '}
|
|
119
|
+
<span className="text-lg">/ 10</span>
|
|
109
120
|
</Results.Column>
|
|
110
121
|
</Results.Row>
|
|
111
122
|
{toSorted(result.checks, (a, b) => b.points - a.points).map(
|
|
112
123
|
(check) => (
|
|
113
124
|
<Results.Row key={check.name}>
|
|
114
125
|
<Results.Column className="uppercase">
|
|
115
|
-
<span className="flex gap-
|
|
126
|
+
<span className="flex gap-2 items-center">
|
|
116
127
|
<IconWarning
|
|
117
128
|
className={cn(
|
|
118
129
|
check.points > 1 ? 'text-yellow-200' : null,
|
|
119
|
-
check.points > 2 ? 'text-orange-
|
|
130
|
+
check.points > 2 ? 'text-orange-400' : null,
|
|
120
131
|
check.points > 3 ? 'text-red-400' : null,
|
|
121
132
|
)}
|
|
122
133
|
/>
|
|
123
|
-
{check.name}
|
|
134
|
+
{sanitize(check.name)}
|
|
124
135
|
</span>
|
|
125
136
|
</Results.Column>
|
|
126
137
|
<Results.Column>{check.description}</Results.Column>
|
|
@@ -128,11 +139,11 @@ export const SpamAssassin = ({ result }: SpamAssassinProps) => {
|
|
|
128
139
|
className={cn(
|
|
129
140
|
'text-right font-mono tracking-tighter',
|
|
130
141
|
check.points > 1 ? 'text-yellow-200' : null,
|
|
131
|
-
check.points > 2 ? 'text-orange-
|
|
142
|
+
check.points > 2 ? 'text-orange-400' : null,
|
|
132
143
|
check.points > 3 ? 'text-red-400' : null,
|
|
133
144
|
)}
|
|
134
145
|
>
|
|
135
|
-
{check.points.toFixed(1)}
|
|
146
|
+
-{check.points.toFixed(1)}
|
|
136
147
|
</Results.Column>
|
|
137
148
|
</Results.Row>
|
|
138
149
|
),
|
|
@@ -6,6 +6,7 @@ interface ToolbarButtonProps extends React.ComponentProps<'button'> {
|
|
|
6
6
|
children: React.ReactNode;
|
|
7
7
|
active?: boolean;
|
|
8
8
|
tooltip?: React.ReactNode;
|
|
9
|
+
delayDuration?: number;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export const ToolbarButton = ({
|
|
@@ -13,17 +14,18 @@ export const ToolbarButton = ({
|
|
|
13
14
|
className,
|
|
14
15
|
active,
|
|
15
16
|
tooltip,
|
|
17
|
+
delayDuration = 500,
|
|
16
18
|
...props
|
|
17
19
|
}: ToolbarButtonProps) => {
|
|
18
20
|
return (
|
|
19
21
|
<Tooltip.Provider>
|
|
20
|
-
<Tooltip>
|
|
22
|
+
<Tooltip delayDuration={delayDuration}>
|
|
21
23
|
<Tooltip.Trigger asChild>
|
|
22
24
|
<button
|
|
23
25
|
type="button"
|
|
24
26
|
{...props}
|
|
25
27
|
className={cn(
|
|
26
|
-
'h-full w-fit font-
|
|
28
|
+
'h-full w-fit font-regular flex text-sm text-slate-10 items-center align-middle justify-center px-1 gap-2 relative',
|
|
27
29
|
'hover:text-slate-12 transition-colors',
|
|
28
30
|
active && 'data-[state=active]:text-cyan-11',
|
|
29
31
|
className,
|
|
@@ -4,7 +4,7 @@ export const useCachedState = <T>(key: string) => {
|
|
|
4
4
|
let value: T | undefined = undefined;
|
|
5
5
|
if ('localStorage' in global) {
|
|
6
6
|
const storedValue = global.localStorage.getItem(key);
|
|
7
|
-
if (storedValue !== null) {
|
|
7
|
+
if (storedValue !== null && storedValue !== 'undefined') {
|
|
8
8
|
try {
|
|
9
9
|
value = JSON.parse(storedValue) as T;
|
|
10
10
|
} catch (exception) {
|
|
@@ -12,7 +12,7 @@ export const useCachedState = <T>(key: string) => {
|
|
|
12
12
|
'Failed to load stored value for',
|
|
13
13
|
key,
|
|
14
14
|
'with value',
|
|
15
|
-
|
|
15
|
+
storedValue,
|
|
16
16
|
);
|
|
17
17
|
}
|
|
18
18
|
}
|