react-email 4.0.0-alpha.0 → 4.0.0-alpha.2
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 +38 -0
- package/dist/cli/index.js +10 -10
- package/dist/cli/index.mjs +10 -13
- package/dist/preview/.next/BUILD_ID +1 -1
- package/dist/preview/.next/app-build-manifest.json +19 -19
- package/dist/preview/.next/app-path-routes-manifest.json +1 -1
- 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/app-paths-manifest.json +1 -1
- package/dist/preview/.next/server/chunks/196.js +2 -2
- package/dist/preview/.next/server/chunks/590.js +1 -0
- package/dist/preview/.next/server/chunks/631.js +2 -2
- package/dist/preview/.next/server/chunks/734.js +15 -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/285-dbf6306a0d45c33d.js +1 -0
- package/dist/preview/.next/static/chunks/490-d26ba2019ccd4d2f.js +1 -0
- package/dist/preview/.next/static/chunks/603-36207c8905355e23.js +1 -0
- package/dist/preview/.next/static/chunks/afa401a5-9ebf2515b1397993.js +6 -0
- package/dist/preview/.next/static/chunks/app/layout-b13c19549e2d3e57.js +1 -0
- package/dist/preview/.next/static/chunks/app/page-8f366f3c14282f33.js +1 -0
- package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-9906dc842681db05.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/b60917edfd15a496.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 +9 -9
- package/src/actions/email-validation/check-images.spec.tsx +89 -0
- package/src/actions/email-validation/check-images.ts +141 -0
- package/src/actions/email-validation/check-links.spec.tsx +91 -0
- package/src/actions/email-validation/check-links.ts +18 -15
- package/src/app/preview/[...slug]/preview.tsx +105 -19
- package/src/components/button.tsx +47 -36
- package/src/components/code-snippet.tsx +0 -2
- package/src/components/icons/icon-image.tsx +19 -0
- package/src/components/logo.tsx +0 -2
- package/src/components/resizable-wrapper.tsx +176 -0
- package/src/components/shell.tsx +17 -3
- 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 +75 -27
- package/src/components/topbar/active-view-toggle-group.tsx +86 -0
- package/src/components/topbar/view-size-controls.tsx +247 -0
- package/src/components/topbar.tsx +50 -125
- package/src/hooks/use-clamped-state.ts +24 -0
- package/src/hooks/use-icon-animation.ts +4 -7
- package/src/utils/static-node-modules-for-vm.ts +2 -1
- package/tailwind.config.ts +12 -17
- package/tsconfig.json +6 -2
- package/tsconfig.test.json +8 -0
- package/vitest.config.ts +13 -0
- 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/860-38d96c8819ba6f19.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 → ll_lhpCErxdDFU8uF5Ujy}/_buildManifest.js +0 -0
- /package/dist/preview/.next/static/{fZaiKz58wDr55pxLu9uHa → ll_lhpCErxdDFU8uF5Ujy}/_ssgManifest.js +0 -0
|
@@ -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
|
+
};
|
|
@@ -1,230 +1,107 @@
|
|
|
1
|
-
import * as Collapsible from '@radix-ui/react-collapsible';
|
|
2
|
-
import { clsx } from 'clsx';
|
|
3
|
-
import { AnimatePresence, motion } from 'framer-motion';
|
|
4
|
-
import Lottie from 'lottie-react';
|
|
5
1
|
import * as React from 'react';
|
|
6
2
|
import {
|
|
7
3
|
type LinkCheckingResult,
|
|
8
4
|
checkLinks,
|
|
9
5
|
} from '../../actions/email-validation/check-links';
|
|
10
|
-
import animatedLoadIcon from '../../animated-icons-data/load.json';
|
|
11
|
-
import { cn } from '../../utils';
|
|
12
6
|
import { Button } from '../button';
|
|
7
|
+
import { Result, ResultList, type ResultStatus } from './checking-results';
|
|
13
8
|
|
|
14
|
-
|
|
15
|
-
hidden: { opacity: 0, y: 10 },
|
|
16
|
-
visible: {
|
|
17
|
-
opacity: 1,
|
|
18
|
-
y: 0,
|
|
19
|
-
transition: { duration: 0.6, ease: 'easeOut', staggerChildren: 0.1 },
|
|
20
|
-
},
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const childAnimation = {
|
|
24
|
-
hidden: { opacity: 0, y: 5 },
|
|
25
|
-
visible: {
|
|
26
|
-
opacity: 1,
|
|
27
|
-
y: 0,
|
|
28
|
-
transition: { duration: 0.4, ease: 'easeOut' },
|
|
29
|
-
},
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const statusStyles = {
|
|
33
|
-
error: 'text-red-600 hover:bg-red-600/10',
|
|
34
|
-
warning: 'text-yellow-300 hover:bg-yellow-400/10',
|
|
35
|
-
success: 'text-green-600 hover:bg-green-600/10',
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
interface LinkCheckerProps {
|
|
39
|
-
emailSlug: string;
|
|
40
|
-
emailMarkup: string;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
type ResultStatus = 'error' | 'warning' | 'success';
|
|
44
|
-
|
|
45
|
-
interface CollapsibleTriggerProps {
|
|
46
|
-
count: number;
|
|
9
|
+
interface LinkCheckerResultsProps {
|
|
47
10
|
label: string;
|
|
48
|
-
variant: ResultStatus;
|
|
49
|
-
disabled?: boolean;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
interface ResultSectionProps {
|
|
53
11
|
status: ResultStatus;
|
|
54
|
-
label: string;
|
|
55
12
|
results: LinkCheckingResult[];
|
|
56
|
-
|
|
13
|
+
|
|
14
|
+
justLoadedIn: boolean;
|
|
57
15
|
}
|
|
58
16
|
|
|
59
|
-
const
|
|
60
|
-
count,
|
|
17
|
+
const LinkCheckerResults = ({
|
|
61
18
|
label,
|
|
62
|
-
variant,
|
|
63
|
-
disabled,
|
|
64
|
-
}: CollapsibleTriggerProps) => (
|
|
65
|
-
<Collapsible.Trigger
|
|
66
|
-
className={clsx(
|
|
67
|
-
'group flex w-full items-center gap-1 rounded p-2 transition-colors duration-200 ease-[cubic-bezier(.36,.66,.6,1)]',
|
|
68
|
-
statusStyles[variant],
|
|
69
|
-
disabled && 'cursor-not-allowed opacity-70',
|
|
70
|
-
)}
|
|
71
|
-
disabled={disabled}
|
|
72
|
-
>
|
|
73
|
-
<span
|
|
74
|
-
className={clsx(
|
|
75
|
-
'-mt-[.125rem] transition-transform duration-200 ease-[cubic-bezier(.36,.66,.6,1)]',
|
|
76
|
-
'rotate-0 group-data-[state=open]:rotate-90',
|
|
77
|
-
)}
|
|
78
|
-
>
|
|
79
|
-
<svg
|
|
80
|
-
fill="none"
|
|
81
|
-
height="15"
|
|
82
|
-
viewBox="0 0 15 15"
|
|
83
|
-
width="15"
|
|
84
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
85
|
-
>
|
|
86
|
-
<path
|
|
87
|
-
clipRule="evenodd"
|
|
88
|
-
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"
|
|
89
|
-
fill="currentColor"
|
|
90
|
-
fillRule="evenodd"
|
|
91
|
-
/>
|
|
92
|
-
</svg>
|
|
93
|
-
</span>
|
|
94
|
-
<div className="flex flex-1 items-center gap-1 font-bold text-[.625rem] uppercase tracking-wide">
|
|
95
|
-
<span>{label}</span>
|
|
96
|
-
<span>({count})</span>
|
|
97
|
-
</div>
|
|
98
|
-
</Collapsible.Trigger>
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
const ResultSection = ({
|
|
102
19
|
status,
|
|
103
|
-
label,
|
|
104
20
|
results,
|
|
105
|
-
open,
|
|
106
|
-
}: ResultSectionProps) => {
|
|
107
|
-
const isEmpty = results.length === 0;
|
|
108
|
-
|
|
109
|
-
return (
|
|
110
|
-
<Collapsible.Root className="group" defaultOpen={open && !isEmpty}>
|
|
111
|
-
<CollapsibleTrigger
|
|
112
|
-
count={results.length}
|
|
113
|
-
label={label}
|
|
114
|
-
variant={status}
|
|
115
|
-
disabled={isEmpty}
|
|
116
|
-
/>
|
|
117
|
-
{!isEmpty && (
|
|
118
|
-
<Collapsible.Content>
|
|
119
|
-
<ol className="mt-2 mb-1 flex list-none flex-col gap-4">
|
|
120
|
-
{results.map((result, index) => (
|
|
121
|
-
<LinkResultView key={index} {...result} />
|
|
122
|
-
))}
|
|
123
|
-
</ol>
|
|
124
|
-
</Collapsible.Content>
|
|
125
|
-
)}
|
|
126
|
-
</Collapsible.Root>
|
|
127
|
-
);
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
const LoadingButton = ({
|
|
131
|
-
loading,
|
|
132
|
-
onClick,
|
|
133
|
-
loadAnimation,
|
|
134
|
-
animatedLoadIcon,
|
|
135
|
-
children,
|
|
136
|
-
}: any) => {
|
|
137
|
-
React.useEffect(() => {
|
|
138
|
-
if (loading) {
|
|
139
|
-
loadAnimation.current?.play();
|
|
140
|
-
}
|
|
141
|
-
}, [loading, loadAnimation]);
|
|
142
21
|
|
|
22
|
+
justLoadedIn,
|
|
23
|
+
}: LinkCheckerResultsProps) => {
|
|
143
24
|
return (
|
|
144
|
-
<
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
25
|
+
<ResultList
|
|
26
|
+
label={
|
|
27
|
+
<>
|
|
28
|
+
<span>{label}</span>
|
|
29
|
+
<span>({results.length})</span>
|
|
30
|
+
</>
|
|
31
|
+
}
|
|
32
|
+
defaultOpen={justLoadedIn}
|
|
33
|
+
status={status}
|
|
34
|
+
disabled={results.length === 0}
|
|
148
35
|
>
|
|
149
|
-
|
|
150
|
-
<
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
36
|
+
{results.map(({ link, status, checks }) => (
|
|
37
|
+
<Result key={link} status={status}>
|
|
38
|
+
<a
|
|
39
|
+
href={link}
|
|
40
|
+
target="_blank"
|
|
41
|
+
rel="noopener noreferrer"
|
|
42
|
+
className="w-full"
|
|
43
|
+
>
|
|
44
|
+
<Result.Title>
|
|
45
|
+
<span className="block overflow-hidden truncate text-ellipsis whitespace-nowrap">
|
|
46
|
+
{link}
|
|
47
|
+
</span>
|
|
48
|
+
</Result.Title>
|
|
49
|
+
<Result.StatusDescription>
|
|
50
|
+
{checks
|
|
51
|
+
.map((check) => {
|
|
52
|
+
if (check.type === 'syntax' && !check.passed)
|
|
53
|
+
return 'Invalid URL';
|
|
54
|
+
if (check.type === 'fetch_attempt')
|
|
55
|
+
return `${check.metadata.fetchStatusCode}`;
|
|
56
|
+
if (check.type === 'security')
|
|
57
|
+
return check.passed ? 'Secure' : 'Insecure';
|
|
58
|
+
return null;
|
|
59
|
+
})
|
|
60
|
+
.filter(Boolean)
|
|
61
|
+
.join(' - ')}
|
|
62
|
+
</Result.StatusDescription>
|
|
63
|
+
</a>
|
|
64
|
+
</Result>
|
|
65
|
+
))}
|
|
66
|
+
</ResultList>
|
|
167
67
|
);
|
|
168
68
|
};
|
|
169
69
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
className="group/item relative w-full rounded-md p-2 pl-4 transition-colors duration-300 ease-out hover:bg-slate-5"
|
|
175
|
-
data-status={props.status}
|
|
176
|
-
initial="hidden"
|
|
177
|
-
layout
|
|
178
|
-
variants={containerAnimation}
|
|
179
|
-
>
|
|
180
|
-
<a
|
|
181
|
-
href={props.link}
|
|
182
|
-
target="_blank"
|
|
183
|
-
rel="noopener noreferrer"
|
|
184
|
-
className="w-full"
|
|
185
|
-
>
|
|
186
|
-
<motion.div
|
|
187
|
-
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"
|
|
188
|
-
variants={childAnimation}
|
|
189
|
-
>
|
|
190
|
-
<span className="block overflow-hidden truncate text-ellipsis whitespace-nowrap">
|
|
191
|
-
{props.link}
|
|
192
|
-
</span>
|
|
193
|
-
</motion.div>
|
|
194
|
-
<motion.div
|
|
195
|
-
className="mt-1 font-semibold text-[.625rem] uppercase"
|
|
196
|
-
variants={childAnimation}
|
|
197
|
-
>
|
|
198
|
-
{props.checks
|
|
199
|
-
.map((check) => {
|
|
200
|
-
if (check.type === 'syntax' && !check.passed)
|
|
201
|
-
return 'Invalid URL';
|
|
202
|
-
if (check.type === 'fetch_attempt')
|
|
203
|
-
return `${check.metadata.fetchStatusCode}`;
|
|
204
|
-
if (check.type === 'security')
|
|
205
|
-
return check.passed ? 'Secure' : 'Insecure';
|
|
206
|
-
return null;
|
|
207
|
-
})
|
|
208
|
-
.filter(Boolean)
|
|
209
|
-
.join(' - ')}
|
|
210
|
-
</motion.div>
|
|
211
|
-
</a>
|
|
212
|
-
</motion.li>
|
|
213
|
-
</AnimatePresence>
|
|
214
|
-
);
|
|
70
|
+
interface LinkCheckerProps {
|
|
71
|
+
emailSlug: string;
|
|
72
|
+
emailMarkup: string;
|
|
73
|
+
}
|
|
215
74
|
|
|
216
75
|
export const LinkChecker = ({ emailSlug, emailMarkup }: LinkCheckerProps) => {
|
|
217
76
|
const cacheKey = `link-checking-results-${emailSlug.replaceAll('/', '-')}`;
|
|
218
|
-
const cachedResults =
|
|
219
|
-
'localStorage' in window ? window.localStorage.getItem(cacheKey) : null;
|
|
220
77
|
|
|
221
78
|
const [results, setResults] = React.useState<
|
|
222
79
|
LinkCheckingResult[] | undefined
|
|
223
|
-
>(
|
|
224
|
-
|
|
225
|
-
|
|
80
|
+
>();
|
|
81
|
+
|
|
82
|
+
React.useEffect(() => {
|
|
83
|
+
const cachedValue =
|
|
84
|
+
'localStorage' in global ? global.localStorage.getItem(cacheKey) : null;
|
|
85
|
+
if (cachedValue) {
|
|
86
|
+
setResults(JSON.parse(cachedValue));
|
|
87
|
+
}
|
|
88
|
+
}, [cacheKey]);
|
|
89
|
+
|
|
90
|
+
const [justLoadedIn, setJustLoadedIn] = React.useState(false);
|
|
226
91
|
const [loading, setLoading] = React.useState(false);
|
|
227
92
|
|
|
93
|
+
const handleRun = () => {
|
|
94
|
+
setLoading(true);
|
|
95
|
+
checkLinks(emailMarkup)
|
|
96
|
+
.then((newResults) => {
|
|
97
|
+
setResults(newResults);
|
|
98
|
+
setJustLoadedIn(true);
|
|
99
|
+
localStorage.setItem(cacheKey, JSON.stringify(newResults));
|
|
100
|
+
})
|
|
101
|
+
.catch(console.error)
|
|
102
|
+
.finally(() => setLoading(false));
|
|
103
|
+
};
|
|
104
|
+
|
|
228
105
|
const errorResults = React.useMemo(
|
|
229
106
|
() => results?.filter((r) => r.status === 'error') || [],
|
|
230
107
|
[results],
|
|
@@ -238,39 +115,27 @@ export const LinkChecker = ({ emailSlug, emailMarkup }: LinkCheckerProps) => {
|
|
|
238
115
|
[results],
|
|
239
116
|
);
|
|
240
117
|
|
|
241
|
-
const handleRun = () => {
|
|
242
|
-
setLoading(true);
|
|
243
|
-
checkLinks(emailMarkup)
|
|
244
|
-
.then((newResults) => {
|
|
245
|
-
setResults(newResults);
|
|
246
|
-
setSectionsOpen(true);
|
|
247
|
-
localStorage.setItem(cacheKey, JSON.stringify(newResults));
|
|
248
|
-
})
|
|
249
|
-
.catch(console.error)
|
|
250
|
-
.finally(() => setLoading(false));
|
|
251
|
-
};
|
|
252
|
-
|
|
253
118
|
return (
|
|
254
119
|
<div className="mt-4 flex w-full flex-col gap-2 text-pretty">
|
|
255
120
|
{results ? (
|
|
256
121
|
<>
|
|
257
|
-
<
|
|
122
|
+
<LinkCheckerResults
|
|
258
123
|
label="Errors"
|
|
259
124
|
results={errorResults}
|
|
125
|
+
justLoadedIn={justLoadedIn}
|
|
260
126
|
status="error"
|
|
261
|
-
open={sectionsOpen}
|
|
262
127
|
/>
|
|
263
|
-
<
|
|
128
|
+
<LinkCheckerResults
|
|
264
129
|
label="Warnings"
|
|
265
130
|
results={warningResults}
|
|
131
|
+
justLoadedIn={justLoadedIn}
|
|
266
132
|
status="warning"
|
|
267
|
-
open={sectionsOpen}
|
|
268
133
|
/>
|
|
269
|
-
<
|
|
134
|
+
<LinkCheckerResults
|
|
270
135
|
label="Success"
|
|
271
136
|
results={successResults}
|
|
137
|
+
justLoadedIn={justLoadedIn}
|
|
272
138
|
status="success"
|
|
273
|
-
open={sectionsOpen}
|
|
274
139
|
/>
|
|
275
140
|
</>
|
|
276
141
|
) : (
|
|
@@ -278,14 +143,9 @@ export const LinkChecker = ({ emailSlug, emailMarkup }: LinkCheckerProps) => {
|
|
|
278
143
|
Check if all links are valid and redirect to the correct pages.
|
|
279
144
|
</span>
|
|
280
145
|
)}
|
|
281
|
-
<
|
|
282
|
-
loading={loading}
|
|
283
|
-
onClick={handleRun}
|
|
284
|
-
loadAnimation={loadAnimation}
|
|
285
|
-
animatedLoadIcon={animatedLoadIcon}
|
|
286
|
-
>
|
|
146
|
+
<Button loading={loading} onClick={handleRun}>
|
|
287
147
|
{results ? 'Re-run' : 'Run'}
|
|
288
|
-
</
|
|
148
|
+
</Button>
|
|
289
149
|
</div>
|
|
290
150
|
);
|
|
291
151
|
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { DotLottieReact } from '@lottiefiles/dotlottie-react';
|
|
3
4
|
import * as Tabs from '@radix-ui/react-tabs';
|
|
4
5
|
import { clsx } from 'clsx';
|
|
5
6
|
import { motion } from 'framer-motion';
|
|
6
|
-
import Lottie from 'lottie-react';
|
|
7
7
|
import Link from 'next/link';
|
|
8
8
|
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
|
9
9
|
import type * as React from 'react';
|
|
@@ -15,8 +15,10 @@ import { useIconAnimation } from '../../hooks/use-icon-animation';
|
|
|
15
15
|
import { cn } from '../../utils';
|
|
16
16
|
import { Button } from '../button';
|
|
17
17
|
import { Heading } from '../heading';
|
|
18
|
+
import { IconImage } from '../icons/icon-image';
|
|
18
19
|
import { Tooltip } from '../tooltip';
|
|
19
20
|
import { FileTree } from './file-tree';
|
|
21
|
+
import { ImageChecker } from './image-checker';
|
|
20
22
|
import { LinkChecker } from './link-checker';
|
|
21
23
|
|
|
22
24
|
type SidebarPanelValue = 'file-tree' | 'link-checker' | 'image-checker';
|
|
@@ -145,7 +147,7 @@ const Panel = ({ title, active, children }: PanelProps) => (
|
|
|
145
147
|
{title}
|
|
146
148
|
</Heading>
|
|
147
149
|
</div>
|
|
148
|
-
<div className="-mt-[.5px] relative h-[calc(
|
|
150
|
+
<div className="-mt-[.5px] relative h-[calc(100dvh-4.375rem)] w-full border-slate-4 border-t px-4 pb-3">
|
|
149
151
|
{children}
|
|
150
152
|
</div>
|
|
151
153
|
</>
|
|
@@ -218,12 +220,14 @@ export const Sidebar = ({
|
|
|
218
220
|
tabValue="file-tree"
|
|
219
221
|
tooltipText="File Explorer"
|
|
220
222
|
>
|
|
221
|
-
<
|
|
222
|
-
|
|
223
|
-
|
|
223
|
+
<DotLottieReact
|
|
224
|
+
data={animatedMailIcon}
|
|
225
|
+
autoplay={false}
|
|
224
226
|
className="h-5 w-5"
|
|
225
227
|
loop={false}
|
|
226
|
-
|
|
228
|
+
dotLottieRefCallback={(instance) => {
|
|
229
|
+
mailAnimation.ref.current = instance;
|
|
230
|
+
}}
|
|
227
231
|
/>
|
|
228
232
|
</TabTrigger>
|
|
229
233
|
<TabTrigger
|
|
@@ -234,14 +238,24 @@ export const Sidebar = ({
|
|
|
234
238
|
tabValue="link-checker"
|
|
235
239
|
tooltipText="Link Checker"
|
|
236
240
|
>
|
|
237
|
-
<
|
|
238
|
-
|
|
239
|
-
|
|
241
|
+
<DotLottieReact
|
|
242
|
+
data={animatedLinkIcon}
|
|
243
|
+
autoplay={false}
|
|
240
244
|
className="h-6 w-6"
|
|
241
245
|
loop={false}
|
|
242
|
-
|
|
246
|
+
dotLottieRefCallback={(instance) => {
|
|
247
|
+
linkAnimation.ref.current = instance;
|
|
248
|
+
}}
|
|
243
249
|
/>
|
|
244
250
|
</TabTrigger>
|
|
251
|
+
<TabTrigger
|
|
252
|
+
activeTabValue={activePanelValue}
|
|
253
|
+
className="relative"
|
|
254
|
+
tabValue="image-checker"
|
|
255
|
+
tooltipText="Image Checker"
|
|
256
|
+
>
|
|
257
|
+
<IconImage className="h-6 w-6" />
|
|
258
|
+
</TabTrigger>
|
|
245
259
|
<div className="mt-auto flex flex-col">
|
|
246
260
|
<NavigationButton
|
|
247
261
|
className="flex items-center justify-center"
|
|
@@ -251,12 +265,14 @@ export const Sidebar = ({
|
|
|
251
265
|
side="right"
|
|
252
266
|
tooltip="Documentation"
|
|
253
267
|
>
|
|
254
|
-
<
|
|
255
|
-
|
|
256
|
-
|
|
268
|
+
<DotLottieReact
|
|
269
|
+
data={animatedHelpIcon}
|
|
270
|
+
autoplay={false}
|
|
257
271
|
className="h-5 w-5"
|
|
258
272
|
loop={false}
|
|
259
|
-
|
|
273
|
+
dotLottieRefCallback={(instance) => {
|
|
274
|
+
helpAnimation.ref.current = instance;
|
|
275
|
+
}}
|
|
260
276
|
/>
|
|
261
277
|
</NavigationButton>
|
|
262
278
|
<NavigationButton
|
|
@@ -284,19 +300,28 @@ export const Sidebar = ({
|
|
|
284
300
|
emailSlug={currentEmailOpenSlug}
|
|
285
301
|
/>
|
|
286
302
|
) : (
|
|
287
|
-
<
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
303
|
+
<EmptyState
|
|
304
|
+
title="Link Checker"
|
|
305
|
+
onSelectTemplate={() => setActivePanelValue('file-tree')}
|
|
306
|
+
/>
|
|
307
|
+
)}
|
|
308
|
+
</Panel>
|
|
309
|
+
)}
|
|
310
|
+
{activePanelValue === 'image-checker' && (
|
|
311
|
+
<Panel
|
|
312
|
+
title="Image Checker"
|
|
313
|
+
active={activePanelValue === 'image-checker'}
|
|
314
|
+
>
|
|
315
|
+
{currentEmailOpenSlug && emailMarkup ? (
|
|
316
|
+
<ImageChecker
|
|
317
|
+
emailMarkup={emailMarkup}
|
|
318
|
+
emailSlug={currentEmailOpenSlug}
|
|
319
|
+
/>
|
|
320
|
+
) : (
|
|
321
|
+
<EmptyState
|
|
322
|
+
title="Image Checker"
|
|
323
|
+
onSelectTemplate={() => setActivePanelValue('file-tree')}
|
|
324
|
+
/>
|
|
300
325
|
)}
|
|
301
326
|
</Panel>
|
|
302
327
|
)}
|
|
@@ -317,3 +342,26 @@ export const Sidebar = ({
|
|
|
317
342
|
</Tabs.Root>
|
|
318
343
|
);
|
|
319
344
|
};
|
|
345
|
+
|
|
346
|
+
interface EmptyStateProps {
|
|
347
|
+
onSelectTemplate: () => void;
|
|
348
|
+
title: string;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const EmptyState = ({ onSelectTemplate, title }: EmptyStateProps) => {
|
|
352
|
+
return (
|
|
353
|
+
<div className="mt-4 flex w-full flex-col gap-2 text-pretty text-xs leading-relaxed">
|
|
354
|
+
<div className="flex flex-col gap-1 rounded-lg border border-[#0BB9CD]/50 bg-[#0BB9CD]/20 text-white">
|
|
355
|
+
<span className="mx-2.5 mt-2">
|
|
356
|
+
To use the {title}, you need to select a template.
|
|
357
|
+
</span>
|
|
358
|
+
<Button
|
|
359
|
+
className="mx-2 my-2.5 transition-all disabled:border-transparent disabled:bg-slate-11"
|
|
360
|
+
onClick={() => onSelectTemplate()}
|
|
361
|
+
>
|
|
362
|
+
Select a template
|
|
363
|
+
</Button>
|
|
364
|
+
</div>
|
|
365
|
+
</div>
|
|
366
|
+
);
|
|
367
|
+
};
|